//go:build darwin package tun import ( "errors" "fmt" "net" "net/netip" "os" "strconv" "syscall" "unsafe" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/platform" "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/buffer" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/stack" ) const ( utunControlName = "com.apple.net.utun_control" sysprotoControl = 2 gateway = "169.254.10.1/30" utunHeaderSize = 4 ) const ( SIOCAIFADDR6 = 2155899162 // netinet6/in6_var.h IN6_IFF_NODAD = 0x0020 // netinet6/in6_var.h IN6_IFF_SECURED = 0x0400 // netinet6/in6_var.h ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h ) //go:linkname procyield runtime.procyield func procyield(cycles uint32) type DarwinTun struct { tunFile *os.File options TunOptions ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system) } var _ Tun = (*DarwinTun)(nil) var _ GVisorTun = (*DarwinTun)(nil) var _ GVisorDevice = (*DarwinTun)(nil) func NewTun(options TunOptions) (Tun, error) { // Check if fd is provided via environment (iOS mode) fdStr := platform.NewEnvFlag(platform.TunFdKey).GetValue(func() string { return "" }) if fdStr != "" { // iOS: use provided fd from NetworkExtension fd, err := strconv.Atoi(fdStr) if err != nil { return nil, err } if err = unix.SetNonblock(fd, true); err != nil { return nil, err } return &DarwinTun{ tunFile: os.NewFile(uintptr(fd), "utun"), options: options, ownsFd: false, }, nil } // macOS: create our own utun interface tunFile, err := open(options.Name) if err != nil { return nil, err } err = setup(options.Name, options.MTU) if err != nil { _ = tunFile.Close() return nil, err } return &DarwinTun{ tunFile: tunFile, options: options, ownsFd: true, }, nil } func (t *DarwinTun) Start() error { return nil } func (t *DarwinTun) Close() error { if t.ownsFd { return t.tunFile.Close() } // iOS: don't close the fd, it's owned by NetworkExtension return nil } // WritePacket implements GVisorDevice method to write one packet to the tun device func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error { // request memory to write from reusable buffer pool b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize) defer b.Release() // prepare Darwin specific packet header _, _ = b.Write([]byte{0x0, 0x0, 0x0, 0x0}) // copy the bytes of slices that compose the packet into the allocated buffer for _, packetElement := range packet.AsSlices() { _, _ = b.Write(packetElement) } // fill Darwin specific header from the first raw packet byte, that we can access now var family byte switch b.Byte(4) >> 4 { case 4: family = unix.AF_INET case 6: family = unix.AF_INET6 default: return &tcpip.ErrAborted{} } b.SetByte(3, family) if _, err := t.tunFile.Write(b.Bytes()); err != nil { if errors.Is(err, unix.EAGAIN) { return &tcpip.ErrWouldBlock{} } return &tcpip.ErrAborted{} } return nil } // ReadPacket implements GVisorDevice method to read one packet from the tun device // It is expected that the method will not block, rather return ErrQueueEmpty when there is nothing on the line, // which will make the stack call Wait which should implement desired push-back func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) { // request memory to write from reusable buffer pool b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize) // read the bytes to the interface file n, err := b.ReadFrom(t.tunFile) if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) { b.Release() return 0, nil, ErrQueueEmpty } if err != nil { b.Release() return 0, nil, err } // discard empty or sub-empty packets if n <= utunHeaderSize { b.Release() return 0, nil, ErrQueueEmpty } // network protocol version from first byte of the raw packet, the one that follows Darwin specific header version := b.Byte(utunHeaderSize) >> 4 packetBuffer := buffer.MakeWithData(b.BytesFrom(utunHeaderSize)) return version, stack.NewPacketBuffer(stack.PacketBufferOptions{ Payload: packetBuffer, IsForwardedPacket: true, OnRelease: func() { b.Release() }, }), nil } // Wait some cpu cycles func (t *DarwinTun) Wait() { procyield(1) } func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) { return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil } // open the interface, by creating new utunN if in the system and returning its file descriptor func open(name string) (*os.File, error) { ifIndex := -1 _, err := fmt.Sscanf(name, "utun%d", &ifIndex) if err != nil || ifIndex < 0 { return nil, errors.New("interface name must be utunN, where N is a number, e.g. utun9, utun11 and so on") } fd, err := unix.Socket(unix.AF_SYSTEM, unix.SOCK_DGRAM, sysprotoControl) if err != nil { return nil, err } ctlInfo := &unix.CtlInfo{} copy(ctlInfo.Name[:], utunControlName) if err := unix.IoctlCtlInfo(fd, ctlInfo); err != nil { _ = unix.Close(fd) return nil, err } sockaddr := &unix.SockaddrCtl{ ID: ctlInfo.Id, Unit: uint32(ifIndex) + 1, } if err := unix.Connect(fd, sockaddr); err != nil { _ = unix.Close(fd) return nil, err } if err := unix.SetNonblock(fd, true); err != nil { _ = unix.Close(fd) return nil, err } return os.NewFile(uintptr(fd), name), nil } // setup the interface by name func setup(name string, MTU uint32) error { if err := setMTU(name, MTU); err != nil { return err } /* * Darwin routing require tunnel type interface to have local and remote address, to be routable. * To simplify inevitable task, assign the interface static ip address, which in current implementation * is just some random ip from link-local pool, allowing to not bother about existing routing intersection. */ syntheticIP, _ := netip.ParsePrefix(gateway) if err := setIPAddress(name, syntheticIP); err != nil { return err } return nil } // setMTU sets MTU on the interface by given name func setMTU(name string, mtu uint32) error { socket, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) if err != nil { return err } defer unix.Close(socket) ifr := unix.IfreqMTU{MTU: int32(mtu)} copy(ifr.Name[:], name) return unix.IoctlSetIfreqMTU(socket, &ifr) } type ifAliasReq4 struct { Name [unix.IFNAMSIZ]byte Addr unix.RawSockaddrInet4 Dstaddr unix.RawSockaddrInet4 Mask unix.RawSockaddrInet4 } type ifAliasReq6 struct { Name [unix.IFNAMSIZ]byte Addr unix.RawSockaddrInet6 Dstaddr unix.RawSockaddrInet6 Mask unix.RawSockaddrInet6 Flags uint32 Lifetime addrLifetime6 } type addrLifetime6 struct { Expire float64 Preferred float64 Vltime uint32 Pltime uint32 } // setIPAddress sets ipv4 and ipv6 addresses to the interface, required for the routing to work func setIPAddress(name string, gateway netip.Prefix) error { socket4, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0) if err != nil { return err } defer unix.Close(socket4) // assume local ip address is next one from the remote address local4 := gateway.Addr().As4() local4[3]++ // fill the configuration for ipv4 ifReq4 := ifAliasReq4{ Addr: unix.RawSockaddrInet4{ Len: unix.SizeofSockaddrInet4, Family: unix.AF_INET, Addr: local4, }, Dstaddr: unix.RawSockaddrInet4{ Len: unix.SizeofSockaddrInet4, Family: unix.AF_INET, Addr: gateway.Addr().As4(), }, Mask: unix.RawSockaddrInet4{ Len: unix.SizeofSockaddrInet4, Family: unix.AF_INET, Addr: netip.MustParseAddr(net.IP(net.CIDRMask(gateway.Bits(), 32)).String()).As4(), }, } copy(ifReq4.Name[:], name) if err = ioctlPtr(socket4, unix.SIOCAIFADDR, unsafe.Pointer(&ifReq4)); err != nil { return os.NewSyscallError("SIOCAIFADDR", err) } socket6, err := unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, 0) if err != nil { return err } defer unix.Close(socket6) // link-local ipv6 address with suffix from ipv6 local6 := netip.AddrFrom16([16]byte{0: 0xfe, 1: 0x80, 12: local4[0], 13: local4[1], 14: local4[2], 15: local4[3]}) // fill the configuration for ipv6 // only link-local address without the destination is enough for it ifReq6 := ifAliasReq6{ Addr: unix.RawSockaddrInet6{ Len: unix.SizeofSockaddrInet6, Family: unix.AF_INET6, Addr: local6.As16(), }, Mask: unix.RawSockaddrInet6{ Len: unix.SizeofSockaddrInet6, Family: unix.AF_INET6, Addr: netip.MustParseAddr(net.IP(net.CIDRMask(64, 128)).String()).As16(), }, Flags: IN6_IFF_NODAD, Lifetime: addrLifetime6{ Vltime: ND6_INFINITE_LIFETIME, Pltime: ND6_INFINITE_LIFETIME, }, } // assign link-local ipv6 address to the interface. // this will additionally trigger OS level autoconfiguration, which will result two different link-local // addresses - the requested one, and autoconfigured one. // this really has no known side effects, just look excessive. and actually considered pretty normal way to // enable the ipv6 on the interface by macOS concepts. copy(ifReq6.Name[:], name) if err = ioctlPtr(socket6, SIOCAIFADDR6, unsafe.Pointer(&ifReq6)); err != nil { return os.NewSyscallError("SIOCAIFADDR6", err) } return nil } func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error { _, _, errno := unix.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg)) if errno != 0 { return errno } return nil }