Skip to content
This repository has been archived by the owner on Oct 27, 2024. It is now read-only.

IPv6 Support #24

Open
fujiapple852 opened this issue Jan 25, 2024 · 7 comments
Open

IPv6 Support #24

fujiapple852 opened this issue Jan 25, 2024 · 7 comments
Labels
enhancement New feature or request partial has been partially supported

Comments

@fujiapple852
Copy link

Thoughts on what is needed to support IPv6?

It looks like rust-tun is mostly hard-wired to IPv4 (except for PacketProtocol) so seems non-trivial.

IPv6 is supported on tun devices on (at least) Linux, macOS and Windows (inc. wintun.dll and the wintun crate) so I don't think there is any fundamental reason preventing this.

This old issue (meh#4) suggests there is a key problem to solve but there are few details:

the problem is you need to go through a netlink socket to set up IPv6 addresses

I may be able to help with this as i'm motivated to have IPv6 support in rust-tun.

@ssrlive
Copy link
Owner

ssrlive commented Jan 25, 2024

Welcome your PR. Thanks.

@xmh0511 xmh0511 added the enhancement New feature or request label Jan 31, 2024
@xmh0511 xmh0511 added the partial has been partially supported label Feb 7, 2024
@DebianArch64
Copy link

DebianArch64 commented Jul 4, 2024

Hey, just wanted to let you guys know that I'm currently working on PR for IPV6 support.

I got it working on Mac, need to work on the other platforms you guys support and make sure the coding style matches. Don't worry about it saying 'broadcast' in the screenshot, IPv6 doesn't support it so I just skip over it.

image

@DebianArch64
Copy link

Sorry for the long response, I ended up going with making a Apple TunnelProvider and using the 'createUDPSession' to connect to my rust socket, but here is what I had:

Since my ipv6 implementation is pretty badly formatted, I'll let the owner make changes to it:
[1] Add 'ifaddr_in6' structures mostly from libc.

//            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
//                    Version 2, December 2004
//
// Copyleft (ↄ) meh. <[email protected]> | http://meh.schizofreni.co
//
// Everyone is permitted to copy and distribute verbatim or modified
// copies of this license document, and changing it is allowed as long
// as the name is changed.
//
//            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
//   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
//
//  0. You just DO WHAT THE FUCK YOU WANT TO.

//! Bindings to internal macOS stuff.

use libc::{c_char, c_uint, ifreq, sockaddr, sockaddr_in6, IFNAMSIZ};
use nix::{ioctl_readwrite, ioctl_write_ptr};

pub const UTUN_CONTROL_NAME: &str = "com.apple.net.utun_control";
pub const IN6_IFF_NODAD: i32 = 0x0020;
pub const IN6_IFF_SECURED: i32 = 0x0400;

#[allow(non_camel_case_types)]
#[repr(C)]
#[derive(Copy, Clone)]
pub struct ctl_info {
    pub ctl_id: c_uint,
    pub ctl_name: [c_char; 96],
}

#[allow(non_camel_case_types)]
#[repr(C)]
#[derive(Copy, Clone)]
pub struct ifaliasreq {
    pub ifra_name: [c_char; IFNAMSIZ],
    pub ifra_addr: sockaddr,
    pub ifra_broadaddr: sockaddr,
    pub ifra_mask: sockaddr,
}

#[allow(non_camel_case_types)]
#[repr(C)]
pub struct in6_ifaliasreq {
    pub ifra_name: [c_char; IFNAMSIZ],
    pub ifra_addr: sockaddr_in6,
    pub ifra_dstaddr: sockaddr_in6,
    pub ifra_prefixmask: sockaddr_in6,
    pub ifra_flags: libc::c_int,
    pub in6_addrlifetime: libc::in6_addrlifetime,
}

ioctl_readwrite!(ctliocginfo, b'N', 3, ctl_info);

ioctl_write_ptr!(siocsifflags, b'i', 16, ifreq);
ioctl_readwrite!(siocgifflags, b'i', 17, ifreq);

ioctl_write_ptr!(siocsifaddr, b'i', 12, ifreq);
ioctl_readwrite!(siocgifaddr, b'i', 33, ifreq);

ioctl_write_ptr!(siocsifdstaddr, b'i', 14, ifreq);
ioctl_readwrite!(siocgifdstaddr, b'i', 34, ifreq);

ioctl_write_ptr!(siocsifbrdaddr, b'i', 19, ifreq);
ioctl_readwrite!(siocgifbrdaddr, b'i', 35, ifreq);

ioctl_write_ptr!(siocsifnetmask, b'i', 22, ifreq);
ioctl_readwrite!(siocgifnetmask, b'i', 37, ifreq);

ioctl_write_ptr!(siocsifmtu, b'i', 52, ifreq);
ioctl_readwrite!(siocgifmtu, b'i', 51, ifreq);

ioctl_write_ptr!(siocaifaddr, b'i', 26, ifaliasreq);
ioctl_write_ptr!(siocdifaddr, b'i', 25, ifreq);

// basing off: https://opensource.apple.com/source/xnu/xnu-1504.15.3/bsd/netinet6/in6_var.h
ioctl_write_ptr!(siocaifaddr_in6, b'i', 26, in6_ifaliasreq);

ioctl_write_ptr!(siocsifaddr_in6, b'i', 12, libc::in6_ifreq);
ioctl_readwrite!(siocgifaddr_in6, b'i', 33, libc::in6_ifreq);

[2]
In 'set_alias' function is where we set IP address and do pretty much everything for IPV6.
'set_destination' and 'broadaddr' is useless for IPV6 and in Device::new() there needs to be a way to set family dynamically

let ctl = Some(posix::Fd::new(
                libc::socket(
                    /*AF_INET*/ AF_INET6,
                    SOCK_DGRAM, 0,
                ),
                true,
            )?);
fn set_alias(&mut self, addr: IpAddr, broadaddr: IpAddr, mask: IpAddr) -> Result<()> {
        let tun_name = self.tun_name.as_ref().ok_or(Error::InvalidConfig)?;
        let ctl = self.ctl.as_ref().ok_or(Error::InvalidConfig)?;
        match addr {
            IpAddr::V4(addr) => unsafe {
                let mut req: ifaliasreq = mem::zeroed();
                ptr::copy_nonoverlapping(
                    tun_name.as_ptr() as *const c_char,
                    req.ifra_name.as_mut_ptr(),
                    tun_name.len(),
                );

                req.ifra_addr = sockaddr_union::from((addr, 0)).addr;
                req.ifra_broadaddr = sockaddr_union::from((broadaddr, 0)).addr;
                req.ifra_mask = sockaddr_union::from((mask, 0)).addr;

                if let Err(err) = siocaifaddr(ctl.as_raw_fd(), &req) {
                    return Err(io::Error::from(err).into());
                }
                let route = Route {
                    addr: std::net::IpAddr::V4(addr),
                    netmask: mask,
                    dest: broadaddr,
                };
                if let Err(e) = self.set_route(route) {
                    log::warn!("{e:?}");
                }
                Ok(())
            },
            IpAddr::V6(_addr) => unsafe {
                let mut req: in6_ifaliasreq = mem::zeroed();
                ptr::copy_nonoverlapping(
                    tun_name.as_ptr() as *const c_char,
                    req.ifra_name.as_mut_ptr(),
                    tun_name.len(),
                );

                req.ifra_addr = sockaddr_union::from((addr, 0)).addr6;
                req.ifra_prefixmask = sockaddr_union::from((mask, 0)).addr6;
                req.in6_addrlifetime.ia6t_vltime = 0xffffffff_u32;
                req.in6_addrlifetime.ia6t_pltime = 0xffffffff_u32;

                // https://opensource.apple.com/source/xnu/xnu-6153.61.1/bsd/netinet6/in6_var.h.auto.html
                req.ifra_flags = IN6_IFF_NODAD;
                if let Err(err) = siocaifaddr_in6(ctl.as_raw_fd(), &req) {
                    return Err(io::Error::from(err).into());
                }

                let route = Route {
                    addr,
                    netmask: mask,
                    dest: broadaddr,
                };
                if let Err(e) = self.set_route(route) {
                    log::warn!("{e:?}");
                }
                Ok(())
            },
        }
    }

@AaronKutch
Copy link

AaronKutch commented Aug 7, 2024

I absolutely need IPv6 for my usecase, how close is this to being implemented for Linux and Windows? I would have the motivation to help, but I'm not very knowledgeable in low level OS subtleties and would need to do a lot of research for this one thing.

@AaronKutch
Copy link

With tun2 I am successfully reading whole IPv4 packets sent to the Device. It also receives some IPv6 solicitation packets on startup interestingly. I want to be able to use an IPv6 address in Configuration::address in case we are talking about different kinds of IP support. I am trying @DebianArch64's solution, but am getting a "No such device" error from a siocsifaddr. The linux platform doesn't have the set_alias function, and the only apparent IP-dependent thing is a AF_INET, and changing to AF_INET6 doesn't seem to change anything. For context, I am building a userspace Wireguard implementation. The reference I am using uses https://github.com/cableguard/altuntun/blob/main/altuntun/src/device/tun_linux.rs for their TUN. It looks almost identical to what rust-tun does. In the tests they also use manual ip commands https://github.com/cableguard/altuntun/blob/b39891b389baaa626ebb2b1ea9f680e7e50347ac/altuntun/src/device/integration_tests/mod.rs#L348 but I'm not getting any changes from using them. What am I missing?

@AaronKutch
Copy link

I figured out that if I commented out the device.configure line and used the ip commands afterwards, it would work. I am looking at the rtnetlink crate to see if it is feasible to use it in place of ioc calls

@AaronKutch
Copy link

AaronKutch commented Aug 8, 2024

So it is possible to use the unmodified tun2 by first creating the Device with it, then finding the interface index through the network-interface crate and looking up by the name the tunnel was given, then using net-route to add routes for the interface. From the device struct, I am successfully receiving whole IPv4 and IPv6 packets that were sent with a destination within the route subnets. This solves the problem of intercepting outgoing packets with global unicast IPv6 addresses.

However, I am now stuck on the problem of being able to create packets with arbitrary global unicast source addresses, and having sockets elsewhere receive them. I know that the writes are working, because I can send packets I get from the device straight back to it, and in a loop it will receive the same packet again except with a decremented hop counter. I can even modify the other fields and change the destination address as long as it is within a routing subnet for the interface. But I cannot set the destination address to something like ::1 and have it received by a socket elsewhere. Almost everything I can search online for only deals with the problem of reading from a network interface, and nothing about writing to a network interface. I suspect it has to do with the fact that you shouldn't be able to forge arbitrary source address without special permissions. What am I missing?

I just figured out that copying the random link local address to the destination address and setting the port causes the packet to be received by a socket listening on :: with the same port. However, the program will just see that random address when I want it to instead be a specific unicast address.
Edit: Once I had copied the source to the destination address, I could simply change the source address to whatever I wanted. The checksum also has to be updated correctly. The link local address could actually be read from one of the net-route structs.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request partial has been partially supported
Projects
None yet
Development

No branches or pull requests

5 participants