diff --git a/go.mod b/go.mod index ba0bcfa..0bb96e7 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/sagernet/nftables v0.3.0-beta.4 - github.com/sagernet/sing v0.6.0-alpha.25 + github.com/sagernet/sing v0.6.0-beta.2 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 golang.org/x/net v0.26.0 diff --git a/go.sum b/go.sum index 8b45e30..e2ea32a 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= -github.com/sagernet/sing v0.6.0-alpha.25 h1:r/UxU+1O6436MjvEXEMRfBBtqMEImgA6uqxezXKZ/Rs= -github.com/sagernet/sing v0.6.0-alpha.25/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.6.0-beta.2 h1:Dcutp3kxrsZes9q3oTiHQhYYjQvDn5rwp1OI9fDLYwQ= +github.com/sagernet/sing v0.6.0-beta.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= diff --git a/tun.go b/tun.go index 4664cef..a2cae52 100644 --- a/tun.go +++ b/tun.go @@ -25,6 +25,7 @@ type Handler interface { type Tun interface { io.ReadWriter N.VectorisedWriter + Name() (string, error) Start() error Close() error } diff --git a/tun_darwin.go b/tun_darwin.go index b7f8aa7..52872fa 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -32,6 +32,14 @@ type NativeTun struct { routerSet bool } +func (t *NativeTun) Name() (string, error) { + return unix.GetsockoptString( + int(t.tunFile.Fd()), + 2, /* #define SYSPROTO_CONTROL 2 */ + 2, /* #define UTUN_OPT_IFNAME 2 */ + ) +} + func New(options Options) (Tun, error) { var tunFd int if options.FileDescriptor == 0 { diff --git a/tun_linux.go b/tun_linux.go index 9336ecd..ea0cef6 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -85,11 +85,245 @@ func New(options Options) (Tun, error) { return nativeTun, nil } -func (t *NativeTun) FrontHeadroom() int { - if t.vnetHdr { - return virtioNetHdrLen +var controlPath string + +func init() { + const defaultTunPath = "/dev/net/tun" + const androidTunPath = "/dev/tun" + if rw.IsFile(androidTunPath) { + controlPath = androidTunPath + } else { + controlPath = defaultTunPath } - return 0 +} + +func open(name string, vnetHdr bool) (int, error) { + fd, err := unix.Open(controlPath, unix.O_RDWR, 0) + if err != nil { + return -1, err + } + ifr, err := unix.NewIfreq(name) + if err != nil { + unix.Close(fd) + return 0, err + } + flags := unix.IFF_TUN | unix.IFF_NO_PI + if vnetHdr { + flags |= unix.IFF_VNET_HDR + } + ifr.SetUint16(uint16(flags)) + err = unix.IoctlIfreq(fd, unix.TUNSETIFF, ifr) + if err != nil { + unix.Close(fd) + return 0, err + } + err = unix.SetNonblock(fd, true) + if err != nil { + unix.Close(fd) + return 0, err + } + return fd, nil +} + +func (t *NativeTun) configure(tunLink netlink.Link) error { + err := netlink.LinkSetMTU(tunLink, int(t.options.MTU)) + if errors.Is(err, unix.EPERM) { + return nil + } else if err != nil { + return err + } + + if len(t.options.Inet4Address) > 0 { + for _, address := range t.options.Inet4Address { + addr4, _ := netlink.ParseAddr(address.String()) + err = netlink.AddrAdd(tunLink, addr4) + if err != nil { + return err + } + } + } + if len(t.options.Inet6Address) > 0 { + for _, address := range t.options.Inet6Address { + addr6, _ := netlink.ParseAddr(address.String()) + err = netlink.AddrAdd(tunLink, addr6) + if err != nil { + return err + } + } + } + + if t.options.GSO { + err = t.enableGSO() + if err != nil { + t.options.Logger.Warn(err) + } + } + + var rxChecksumOffload bool + rxChecksumOffload, err = checkChecksumOffload(t.options.Name, unix.ETHTOOL_GRXCSUM) + if err == nil && !rxChecksumOffload { + _ = setChecksumOffload(t.options.Name, unix.ETHTOOL_SRXCSUM) + } + + if t.options._TXChecksumOffload { + var txChecksumOffload bool + txChecksumOffload, err = checkChecksumOffload(t.options.Name, unix.ETHTOOL_GTXCSUM) + if err != nil { + return err + } + if !txChecksumOffload { + err = setChecksumOffload(t.options.Name, unix.ETHTOOL_STXCSUM) + if err != nil { + return err + } + } + t.txChecksumOffload = true + } + + return nil +} + +func (t *NativeTun) enableGSO() error { + vnetHdrEnabled, err := checkVNETHDREnabled(t.tunFd, t.options.Name) + if err != nil { + return E.Cause(err, "enable offload: check IFF_VNET_HDR enabled") + } + if !vnetHdrEnabled { + return E.Cause(err, "enable offload: IFF_VNET_HDR not enabled") + } + err = setTCPOffload(t.tunFd) + if err != nil { + return E.Cause(err, "enable TCP offload") + } + t.vnetHdr = true + t.writeBuffer = make([]byte, virtioNetHdrLen+int(gsoMaxSize)) + t.tcpGROTable = newTCPGROTable() + t.udpGROTable = newUDPGROTable() + err = setUDPOffload(t.tunFd) + if err != nil { + t.gro.disableUDPGRO() + return E.Cause(err, "enable UDP offload") + } + return nil +} + +func (t *NativeTun) probeTCPGRO() error { + ipPort := netip.AddrPortFrom(t.options.Inet4Address[0].Addr(), 0) + fingerprint := []byte("sing-tun-probe-tun-gro") + segmentSize := len(fingerprint) + iphLen := 20 + tcphLen := 20 + totalLen := iphLen + tcphLen + segmentSize + bufs := make([][]byte, 2) + for i := range bufs { + bufs[i] = make([]byte, virtioNetHdrLen+totalLen, virtioNetHdrLen+(totalLen*2)) + ipv4H := header.IPv4(bufs[i][virtioNetHdrLen:]) + ipv4H.Encode(&header.IPv4Fields{ + SrcAddr: ipPort.Addr(), + DstAddr: ipPort.Addr(), + Protocol: unix.IPPROTO_TCP, + // Use a zero value TTL as best effort means to reduce chance of + // probe packet leaking further than it needs to. + TTL: 0, + TotalLength: uint16(totalLen), + }) + tcpH := header.TCP(bufs[i][virtioNetHdrLen+iphLen:]) + tcpH.Encode(&header.TCPFields{ + SrcPort: ipPort.Port(), + DstPort: ipPort.Port(), + SeqNum: 1 + uint32(i*segmentSize), + AckNum: 1, + DataOffset: 20, + Flags: header.TCPFlagAck, + WindowSize: 3000, + }) + copy(bufs[i][virtioNetHdrLen+iphLen+tcphLen:], fingerprint) + ipv4H.SetChecksum(^ipv4H.CalculateChecksum()) + pseudoCsum := header.PseudoHeaderChecksum(unix.IPPROTO_TCP, ipv4H.SourceAddressSlice(), ipv4H.DestinationAddressSlice(), uint16(tcphLen+segmentSize)) + pseudoCsum = checksum.Checksum(bufs[i][virtioNetHdrLen+iphLen+tcphLen:], pseudoCsum) + tcpH.SetChecksum(^tcpH.CalculateChecksum(pseudoCsum)) + } + _, err := t.BatchWrite(bufs, virtioNetHdrLen) + return err +} + +func (t *NativeTun) Name() (string, error) { + var ifr [unix.IFNAMSIZ + 64]byte + _, _, errno := unix.Syscall( + unix.SYS_IOCTL, + uintptr(t.tunFd), + uintptr(unix.TUNGETIFF), + uintptr(unsafe.Pointer(&ifr[0])), + ) + if errno != 0 { + return "", os.NewSyscallError("ioctl TUNGETIFF", errno) + } + return unix.ByteSliceToString(ifr[:]), nil +} + +func (t *NativeTun) Start() error { + if t.options.FileDescriptor != 0 { + return nil + } + + tunLink, err := netlink.LinkByName(t.options.Name) + if err != nil { + return err + } + + err = netlink.LinkSetUp(tunLink) + if err != nil { + return err + } + + if t.vnetHdr && len(t.options.Inet4Address) > 0 { + err = t.probeTCPGRO() + if err != nil { + t.gro.disableTCPGRO() + t.gro.disableUDPGRO() + t.options.Logger.Warn(E.Cause(err, "disabled TUN TCP & UDP GRO due to GRO probe error")) + } + } + + if t.options.IPRoute2TableIndex == 0 { + for { + t.options.IPRoute2TableIndex = int(rand.Uint32()) + routeList, fErr := netlink.RouteListFiltered(netlink.FAMILY_ALL, &netlink.Route{Table: t.options.IPRoute2TableIndex}, netlink.RT_FILTER_TABLE) + if len(routeList) == 0 || fErr != nil { + break + } + } + } + + err = t.setRoute(tunLink) + if err != nil { + _ = t.unsetRoute0(tunLink) + return err + } + + err = t.unsetRules() + if err != nil { + return E.Cause(err, "cleanup rules") + } + err = t.setRules() + if err != nil { + _ = t.unsetRules() + return err + } + + t.setSearchDomainForSystemdResolved() + + if t.options.AutoRoute && runtime.GOOS == "android" { + t.interfaceCallback = t.options.InterfaceMonitor.RegisterCallback(t.routeUpdate) + } + return nil +} + +func (t *NativeTun) Close() error { + if t.interfaceCallback != nil { + t.options.InterfaceMonitor.UnregisterCallback(t.interfaceCallback) + } + return E.Errors(t.unsetRoute(), t.unsetRules(), common.Close(common.PtrOrNil(t.tunFile))) } func (t *NativeTun) Read(p []byte) (n int, err error) { @@ -183,6 +417,13 @@ func (t *NativeTun) WriteVectorised(buffers []*buf.Buffer) error { } } +func (t *NativeTun) FrontHeadroom() int { + if t.vnetHdr { + return virtioNetHdrLen + } + return 0 +} + func (t *NativeTun) BatchSize() int { if !t.vnetHdr { return 1 @@ -196,46 +437,6 @@ func (t *NativeTun) BatchSize() int { return idealBatchSize } -func (t *NativeTun) probeTCPGRO() error { - ipPort := netip.AddrPortFrom(t.options.Inet4Address[0].Addr(), 0) - fingerprint := []byte("sing-tun-probe-tun-gro") - segmentSize := len(fingerprint) - iphLen := 20 - tcphLen := 20 - totalLen := iphLen + tcphLen + segmentSize - bufs := make([][]byte, 2) - for i := range bufs { - bufs[i] = make([]byte, virtioNetHdrLen+totalLen, virtioNetHdrLen+(totalLen*2)) - ipv4H := header.IPv4(bufs[i][virtioNetHdrLen:]) - ipv4H.Encode(&header.IPv4Fields{ - SrcAddr: ipPort.Addr(), - DstAddr: ipPort.Addr(), - Protocol: unix.IPPROTO_TCP, - // Use a zero value TTL as best effort means to reduce chance of - // probe packet leaking further than it needs to. - TTL: 0, - TotalLength: uint16(totalLen), - }) - tcpH := header.TCP(bufs[i][virtioNetHdrLen+iphLen:]) - tcpH.Encode(&header.TCPFields{ - SrcPort: ipPort.Port(), - DstPort: ipPort.Port(), - SeqNum: 1 + uint32(i*segmentSize), - AckNum: 1, - DataOffset: 20, - Flags: header.TCPFlagAck, - WindowSize: 3000, - }) - copy(bufs[i][virtioNetHdrLen+iphLen+tcphLen:], fingerprint) - ipv4H.SetChecksum(^ipv4H.CalculateChecksum()) - pseudoCsum := header.PseudoHeaderChecksum(unix.IPPROTO_TCP, ipv4H.SourceAddressSlice(), ipv4H.DestinationAddressSlice(), uint16(tcphLen+segmentSize)) - pseudoCsum = checksum.Checksum(bufs[i][virtioNetHdrLen+iphLen+tcphLen:], pseudoCsum) - tcpH.SetChecksum(^tcpH.CalculateChecksum(pseudoCsum)) - } - _, err := t.BatchWrite(bufs, virtioNetHdrLen) - return err -} - func (t *NativeTun) BatchRead(buffers [][]byte, offset int, readN []int) (n int, err error) { t.readAccess.Lock() defer t.readAccess.Unlock() @@ -283,196 +484,6 @@ func (t *NativeTun) BatchWrite(buffers [][]byte, offset int) (int, error) { return total, errs } -var controlPath string - -func init() { - const defaultTunPath = "/dev/net/tun" - const androidTunPath = "/dev/tun" - if rw.IsFile(androidTunPath) { - controlPath = androidTunPath - } else { - controlPath = defaultTunPath - } -} - -func open(name string, vnetHdr bool) (int, error) { - fd, err := unix.Open(controlPath, unix.O_RDWR, 0) - if err != nil { - return -1, err - } - - var ifr struct { - name [16]byte - flags uint16 - _ [22]byte - } - - copy(ifr.name[:], name) - ifr.flags = unix.IFF_TUN | unix.IFF_NO_PI - if vnetHdr { - ifr.flags |= unix.IFF_VNET_HDR - } - _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), unix.TUNSETIFF, uintptr(unsafe.Pointer(&ifr))) - if errno != 0 { - unix.Close(fd) - return -1, errno - } - - if err = unix.SetNonblock(fd, true); err != nil { - unix.Close(fd) - return -1, err - } - - return fd, nil -} - -func (t *NativeTun) configure(tunLink netlink.Link) error { - err := netlink.LinkSetMTU(tunLink, int(t.options.MTU)) - if errors.Is(err, unix.EPERM) { - return nil - } else if err != nil { - return err - } - - if len(t.options.Inet4Address) > 0 { - for _, address := range t.options.Inet4Address { - addr4, _ := netlink.ParseAddr(address.String()) - err = netlink.AddrAdd(tunLink, addr4) - if err != nil { - return err - } - } - } - if len(t.options.Inet6Address) > 0 { - for _, address := range t.options.Inet6Address { - addr6, _ := netlink.ParseAddr(address.String()) - err = netlink.AddrAdd(tunLink, addr6) - if err != nil { - return err - } - } - } - - if t.options.GSO { - err = t.enableGSO() - if err != nil { - t.options.Logger.Warn(err) - } - } - - var rxChecksumOffload bool - rxChecksumOffload, err = checkChecksumOffload(t.options.Name, unix.ETHTOOL_GRXCSUM) - if err == nil && !rxChecksumOffload { - _ = setChecksumOffload(t.options.Name, unix.ETHTOOL_SRXCSUM) - } - - if t.options._TXChecksumOffload { - var txChecksumOffload bool - txChecksumOffload, err = checkChecksumOffload(t.options.Name, unix.ETHTOOL_GTXCSUM) - if err != nil { - return err - } - if !txChecksumOffload { - err = setChecksumOffload(t.options.Name, unix.ETHTOOL_STXCSUM) - if err != nil { - return err - } - } - t.txChecksumOffload = true - } - - return nil -} - -func (t *NativeTun) enableGSO() error { - vnetHdrEnabled, err := checkVNETHDREnabled(t.tunFd, t.options.Name) - if err != nil { - return E.Cause(err, "enable offload: check IFF_VNET_HDR enabled") - } - if !vnetHdrEnabled { - return E.Cause(err, "enable offload: IFF_VNET_HDR not enabled") - } - err = setTCPOffload(t.tunFd) - if err != nil { - return E.Cause(err, "enable TCP offload") - } - t.vnetHdr = true - t.writeBuffer = make([]byte, virtioNetHdrLen+int(gsoMaxSize)) - t.tcpGROTable = newTCPGROTable() - t.udpGROTable = newUDPGROTable() - err = setUDPOffload(t.tunFd) - if err != nil { - t.gro.disableUDPGRO() - return E.Cause(err, "enable UDP offload") - } - return nil -} - -func (t *NativeTun) Start() error { - if t.options.FileDescriptor != 0 { - return nil - } - - tunLink, err := netlink.LinkByName(t.options.Name) - if err != nil { - return err - } - - err = netlink.LinkSetUp(tunLink) - if err != nil { - return err - } - - if t.vnetHdr && len(t.options.Inet4Address) > 0 { - err = t.probeTCPGRO() - if err != nil { - t.gro.disableTCPGRO() - t.gro.disableUDPGRO() - t.options.Logger.Warn(E.Cause(err, "disabled TUN TCP & UDP GRO due to GRO probe error")) - } - } - - if t.options.IPRoute2TableIndex == 0 { - for { - t.options.IPRoute2TableIndex = int(rand.Uint32()) - routeList, fErr := netlink.RouteListFiltered(netlink.FAMILY_ALL, &netlink.Route{Table: t.options.IPRoute2TableIndex}, netlink.RT_FILTER_TABLE) - if len(routeList) == 0 || fErr != nil { - break - } - } - } - - err = t.setRoute(tunLink) - if err != nil { - _ = t.unsetRoute0(tunLink) - return err - } - - err = t.unsetRules() - if err != nil { - return E.Cause(err, "cleanup rules") - } - err = t.setRules() - if err != nil { - _ = t.unsetRules() - return err - } - - t.setSearchDomainForSystemdResolved() - - if t.options.AutoRoute && runtime.GOOS == "android" { - t.interfaceCallback = t.options.InterfaceMonitor.RegisterCallback(t.routeUpdate) - } - return nil -} - -func (t *NativeTun) Close() error { - if t.interfaceCallback != nil { - t.options.InterfaceMonitor.UnregisterCallback(t.interfaceCallback) - } - return E.Errors(t.unsetRoute(), t.unsetRules(), common.Close(common.PtrOrNil(t.tunFile))) -} - func (t *NativeTun) TXChecksumOffload() bool { return t.txChecksumOffload } diff --git a/tun_windows.go b/tun_windows.go index 392b78c..c63dbb7 100644 --- a/tun_windows.go +++ b/tun_windows.go @@ -148,6 +148,10 @@ func (t *NativeTun) configure() error { return nil } +func (t *NativeTun) Name() (string, error) { + return t.options.Name, nil +} + func (t *NativeTun) Start() error { if !t.options.AutoRoute { return nil