Compare commits

..

19 Commits

Author SHA1 Message Date
世界
6628e7d1b1 Fix darwin batch loop not exit on EBADF 2026-03-05 21:19:36 +08:00
wwqgtxx
775acdc3b7 Fix udp/icmp not work on gso with mixed stack 2026-03-05 21:19:34 +08:00
eronez
3fe0cd13db Fix DNS not revert while close tun 2026-03-05 21:19:26 +08:00
世界
31f5c13494 Fix nftablesCreateLocalAddressSets 2026-03-05 21:19:03 +08:00
wwqgtxx
52c15164b5 Fix logging maybe panic 2026-02-13 00:28:23 +08:00
wwqgtxx
647e962311 Fix udp/icmp not work on gso with system stack 2026-02-13 00:25:33 +08:00
世界
d97b0f06d6 Add back random iproute2 table index 2026-02-02 14:10:06 +08:00
世界
5400abd7f7 Revert "Disable rp filter atomically"
This reverts commit e71911f94d.
2026-02-02 14:08:27 +08:00
世界
e71911f94d Disable rp filter atomically 2026-02-01 10:16:17 +08:00
世界
3a6fedf64a Fix auto_redirect fallback rule 2026-01-29 11:50:35 +08:00
世界
42a84746a9 Fix auto_redirect on IPv6-only or IPv4-only servers 2026-01-28 16:54:33 +08:00
世界
7f3af59109 Skip tun interface traffic in prerouting UDP/ICMP chain 2026-01-28 14:34:55 +08:00
世界
8ccd51404e Fix IPv6 ULA addresses excluded from local address set
The filter condition `IsGlobalUnicast() && !IsPrivate()` incorrectly
excluded ULA addresses (fc00::/7) from inet6_local_address_set,
causing DNS hijack to fail for IPv6 ULA addresses.

Fixes: sagernet/sing-box#3698
2026-01-17 20:42:54 +08:00
世界
7bd004f141 Skip strict routing on Windows versions below 10 2026-01-17 18:38:29 +08:00
世界
d44e0c68d4 Fix darwin batch read not exit on stop 2026-01-15 02:03:38 +08:00
世界
bc23daa800 Fix TUN interface restart fails with existing addresses 2026-01-04 23:24:54 +08:00
世界
e6c219a61e Fix race condition in ReadPacket 2025-12-17 19:45:13 +08:00
世界
ddc824fb9c redirect: Fix compatibility with /product/bin/su 2025-10-21 21:26:31 +08:00
世界
e229d7041e Fix race codes 2025-09-12 18:02:37 +08:00
11 changed files with 208 additions and 69 deletions

View File

@@ -69,6 +69,7 @@ func (r *autoRedirect) Start() error {
r.androidSu = true
for _, suPath := range []string{
"su",
"/product/bin/su",
"/system/bin/su",
} {
r.suPath, err = exec.LookPath(suPath)

View File

@@ -4,6 +4,7 @@ package tun
import (
"net/netip"
"strings"
"github.com/sagernet/nftables"
"github.com/sagernet/nftables/binaryutil"
@@ -182,6 +183,25 @@ func (r *autoRedirect) setupNFTables() error {
},
},
})
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRoutingUDP,
Exprs: []expr.Any{
&expr.Meta{
Key: expr.MetaKeyIIFNAME,
Register: 1,
},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: nftablesIfname(r.tunOptions.Name),
},
&expr.Counter{},
&expr.Verdict{
Kind: expr.VerdictReturn,
},
},
})
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRoutingUDP,
@@ -269,6 +289,10 @@ func (r *autoRedirect) setupNFTables() error {
// TODO; test is this works
func (r *autoRedirect) nftablesUpdateLocalAddressSet() error {
err := r.interfaceFinder.Update()
if err != nil {
return err
}
newLocalAddresses := common.FlatMap(r.interfaceFinder.Interfaces(), func(it control.Interface) []netip.Prefix {
return common.Filter(it.Addresses, func(prefix netip.Prefix) bool {
return it.Name == "lo" || prefix.Addr().IsGlobalUnicast()
@@ -277,6 +301,11 @@ func (r *autoRedirect) nftablesUpdateLocalAddressSet() error {
if slices.Equal(newLocalAddresses, r.localAddresses) {
return nil
}
if r.logger != nil {
r.logger.Debug("updating local address set to [", strings.Join(common.Map(newLocalAddresses, func(it netip.Prefix) string {
return it.String()
}), ", ")+"]")
}
nft, err := nftables.New()
if err != nil {
return err

View File

@@ -74,12 +74,11 @@ func (r *autoRedirect) nftablesCreateLocalAddressSets(
localAddresses4 := common.Filter(localAddresses, func(it netip.Prefix) bool {
return it.Addr().Is4()
})
updateAddresses4 := common.Filter(localAddresses, func(it netip.Prefix) bool {
return it.Addr().Is4()
})
var update bool
if len(lastAddresses) != 0 {
if !slices.Equal(localAddresses4, updateAddresses4) {
if !slices.Equal(localAddresses4, common.Filter(lastAddresses, func(it netip.Prefix) bool {
return it.Addr().Is4()
})) {
update = true
}
}
@@ -94,19 +93,14 @@ func (r *autoRedirect) nftablesCreateLocalAddressSets(
localAddresses6 := common.Filter(localAddresses, func(it netip.Prefix) bool {
return it.Addr().Is6()
})
updateAddresses6 := common.Filter(localAddresses, func(it netip.Prefix) bool {
return it.Addr().Is6()
})
var update bool
if len(lastAddresses) != 0 {
if !slices.Equal(localAddresses6, updateAddresses6) {
if !slices.Equal(localAddresses6, common.Filter(lastAddresses, func(it netip.Prefix) bool {
return it.Addr().Is6()
})) {
update = true
}
}
localAddresses6 = common.Filter(localAddresses6, func(it netip.Prefix) bool {
address := it.Addr()
return address.IsLoopback() || address.IsGlobalUnicast() && !address.IsPrivate()
})
if len(lastAddresses) == 0 || update {
_, err := nftablesCreateIPSet(nft, table, 6, "inet6_local_address_set", nftables.TableFamilyIPv6, nil, localAddresses6, false, update)
if err != nil {

View File

@@ -3,6 +3,9 @@
package tun
import (
"errors"
"syscall"
"github.com/sagernet/gvisor/pkg/buffer"
"github.com/sagernet/gvisor/pkg/tcpip"
gHdr "github.com/sagernet/gvisor/pkg/tcpip/header"
@@ -169,7 +172,7 @@ func (m *Mixed) batchLoopDarwin(darwinTUN DarwinTUN) {
for {
buffers, err := darwinTUN.BatchRead()
if err != nil {
if E.IsClosed(err) {
if E.IsClosed(err) || errors.Is(err, syscall.EBADF) {
return
}
m.logger.Error(E.Cause(err, "batch read packet"))

View File

@@ -267,7 +267,7 @@ func (s *System) batchLoopDarwin(darwinTUN DarwinTUN) {
for {
buffers, err := darwinTUN.BatchRead()
if err != nil {
if E.IsClosed(err) {
if E.IsClosed(err) || errors.Is(err, syscall.EBADF) {
return
}
s.logger.Error(E.Cause(err, "batch read packet"))

View File

@@ -11,6 +11,7 @@ import (
)
type TCPNat struct {
timeout time.Duration
portIndex uint16
portAccess sync.RWMutex
addrAccess sync.RWMutex
@@ -19,6 +20,7 @@ type TCPNat struct {
}
type TCPSession struct {
sync.Mutex
Source netip.AddrPort
Destination netip.AddrPort
LastActive time.Time
@@ -26,38 +28,41 @@ type TCPSession struct {
func NewNat(ctx context.Context, timeout time.Duration) *TCPNat {
natMap := &TCPNat{
timeout: timeout,
portIndex: 10000,
addrMap: make(map[netip.AddrPort]uint16),
portMap: make(map[uint16]*TCPSession),
}
go natMap.loopCheckTimeout(ctx, timeout)
go natMap.loopCheckTimeout(ctx)
return natMap
}
func (n *TCPNat) loopCheckTimeout(ctx context.Context, timeout time.Duration) {
ticker := time.NewTicker(timeout)
func (n *TCPNat) loopCheckTimeout(ctx context.Context) {
ticker := time.NewTicker(n.timeout)
defer ticker.Stop()
for {
select {
case <-ticker.C:
n.checkTimeout(timeout)
n.checkTimeout()
case <-ctx.Done():
return
}
}
}
func (n *TCPNat) checkTimeout(timeout time.Duration) {
func (n *TCPNat) checkTimeout() {
now := time.Now()
n.portAccess.Lock()
defer n.portAccess.Unlock()
n.addrAccess.Lock()
defer n.addrAccess.Unlock()
for natPort, session := range n.portMap {
if now.Sub(session.LastActive) > timeout {
session.Lock()
if now.Sub(session.LastActive) > n.timeout {
delete(n.addrMap, session.Source)
delete(n.portMap, natPort)
}
session.Unlock()
}
}
@@ -66,7 +71,11 @@ func (n *TCPNat) LookupBack(port uint16) *TCPSession {
session := n.portMap[port]
n.portAccess.RUnlock()
if session != nil {
session.LastActive = time.Now()
session.Lock()
if time.Since(session.LastActive) > time.Second {
session.LastActive = time.Now()
}
session.Unlock()
}
return session
}

72
tun.go
View File

@@ -52,44 +52,46 @@ type DarwinTUN interface {
}
const (
DefaultIPRoute2TableIndex = 2022
DefaultIPRoute2RuleIndex = 9000
DefaultIPRoute2TableIndex = 2022
DefaultIPRoute2RuleIndex = 9000
DefaultIPRoute2AutoRedirectFallbackRuleIndex = 32768
)
type Options struct {
Name string
Inet4Address []netip.Prefix
Inet6Address []netip.Prefix
MTU uint32
GSO bool
AutoRoute bool
InterfaceScope bool
Inet4Gateway netip.Addr
Inet6Gateway netip.Addr
DNSServers []netip.Addr
IPRoute2TableIndex int
IPRoute2RuleIndex int
AutoRedirectMarkMode bool
AutoRedirectInputMark uint32
AutoRedirectOutputMark uint32
Inet4LoopbackAddress []netip.Addr
Inet6LoopbackAddress []netip.Addr
StrictRoute bool
Inet4RouteAddress []netip.Prefix
Inet6RouteAddress []netip.Prefix
Inet4RouteExcludeAddress []netip.Prefix
Inet6RouteExcludeAddress []netip.Prefix
IncludeInterface []string
ExcludeInterface []string
IncludeUID []ranges.Range[uint32]
ExcludeUID []ranges.Range[uint32]
IncludeAndroidUser []int
IncludePackage []string
ExcludePackage []string
InterfaceFinder control.InterfaceFinder
InterfaceMonitor DefaultInterfaceMonitor
FileDescriptor int
Logger logger.Logger
Name string
Inet4Address []netip.Prefix
Inet6Address []netip.Prefix
MTU uint32
GSO bool
AutoRoute bool
InterfaceScope bool
Inet4Gateway netip.Addr
Inet6Gateway netip.Addr
DNSServers []netip.Addr
IPRoute2TableIndex int
IPRoute2RuleIndex int
IPRoute2AutoRedirectFallbackRuleIndex int
AutoRedirectMarkMode bool
AutoRedirectInputMark uint32
AutoRedirectOutputMark uint32
Inet4LoopbackAddress []netip.Addr
Inet6LoopbackAddress []netip.Addr
StrictRoute bool
Inet4RouteAddress []netip.Prefix
Inet6RouteAddress []netip.Prefix
Inet4RouteExcludeAddress []netip.Prefix
Inet6RouteExcludeAddress []netip.Prefix
IncludeInterface []string
ExcludeInterface []string
IncludeUID []ranges.Range[uint32]
ExcludeUID []ranges.Range[uint32]
IncludeAndroidUser []int
IncludePackage []string
ExcludePackage []string
InterfaceFinder control.InterfaceFinder
InterfaceMonitor DefaultInterfaceMonitor
FileDescriptor int
Logger logger.Logger
// No work for TCP, do not use.
_TXChecksumOffload bool

View File

@@ -152,7 +152,10 @@ func (t *NativeTun) Start() error {
func (t *NativeTun) Close() error {
defer flushDNSCache()
return E.Errors(t.unsetRoutes(), t.tunFile.Close())
t.stopFd.Stop()
err := E.Errors(t.unsetRoutes(), t.tunFile.Close())
t.stopFd.Close()
return err
}
func (t *NativeTun) Read(p []byte) (n int, err error) {
@@ -347,6 +350,9 @@ func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) {
t.buffers = t.buffers[:0]
return nil, errno
}
if n < 0 {
return nil, os.ErrClosed
}
if n < 1 {
return nil, nil
}

View File

@@ -17,7 +17,6 @@ import (
"github.com/sagernet/sing-tun/internal/gtcpip/checksum"
"github.com/sagernet/sing-tun/internal/gtcpip/header"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
@@ -40,6 +39,7 @@ type NativeTun struct {
writeAccess sync.Mutex
vnetHdr bool
writeBuffer []byte
vnetHdrWriteBuf []byte
gsoToWrite []int
tcpGROTable *tcpGROTable
udpGroAccess sync.Mutex
@@ -130,7 +130,7 @@ func (t *NativeTun) configure(tunLink netlink.Link) error {
for _, address := range t.options.Inet4Address {
addr4, _ := netlink.ParseAddr(address.String())
err = netlink.AddrAdd(tunLink, addr4)
if err != nil {
if err != nil && !errors.Is(err, unix.EEXIST) {
return err
}
}
@@ -139,7 +139,7 @@ func (t *NativeTun) configure(tunLink netlink.Link) error {
for _, address := range t.options.Inet6Address {
addr6, _ := netlink.ParseAddr(address.String())
err = netlink.AddrAdd(tunLink, addr6)
if err != nil {
if err != nil && !errors.Is(err, unix.EEXIST) {
return err
}
}
@@ -148,7 +148,9 @@ func (t *NativeTun) configure(tunLink netlink.Link) error {
if t.options.GSO {
err = t.enableGSO()
if err != nil {
t.options.Logger.Warn(err)
if t.options.Logger != nil {
t.options.Logger.Warn(err)
}
}
}
@@ -273,7 +275,9 @@ func (t *NativeTun) Start() error {
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.Logger != nil {
t.options.Logger.Warn(E.Cause(err, "disabled TUN TCP & UDP GRO due to GRO probe error"))
}
}
}
@@ -315,6 +319,8 @@ func (t *NativeTun) Close() error {
if t.interfaceCallback != nil {
t.options.InterfaceMonitor.UnregisterCallback(t.interfaceCallback)
}
t.unsetSearchDomainForSystemdResolved()
t.unsetAddresses()
return E.Errors(t.unsetRoute(), t.unsetRules(), common.Close(common.PtrOrNil(t.tunFile)))
}
@@ -382,10 +388,7 @@ func handleVirtioRead(in []byte, bufs [][]byte, sizes []int, offset int) (int, e
func (t *NativeTun) Write(p []byte) (n int, err error) {
if t.vnetHdr {
buffer := buf.Get(virtioNetHdrLen + len(p))
copy(buffer[virtioNetHdrLen:], p)
_, err = t.BatchWrite([][]byte{buffer}, virtioNetHdrLen)
buf.Put(buffer)
_, err = t.BatchWrite([][]byte{p}, virtioNetHdrLen)
if err != nil {
return
}
@@ -616,6 +619,22 @@ func (t *NativeTun) rules() []*netlink.Rule {
it.Family = unix.AF_INET6
rules = append(rules, it)
}
// Fallback rules after system default rules (32766: main, 32767: default)
// Only reached when main and default tables have no route
if p4 {
it = netlink.NewRule()
it.Priority = t.options.IPRoute2AutoRedirectFallbackRuleIndex
it.Table = t.options.IPRoute2TableIndex
it.Family = unix.AF_INET
rules = append(rules, it)
}
if p6 {
it = netlink.NewRule()
it.Priority = t.options.IPRoute2AutoRedirectFallbackRuleIndex
it.Table = t.options.IPRoute2TableIndex
it.Family = unix.AF_INET6
rules = append(rules, it)
}
return rules
}
@@ -989,7 +1008,7 @@ func (t *NativeTun) unsetRules() error {
for _, rule := range ruleList {
ruleStart := t.options.IPRoute2RuleIndex
ruleEnd := ruleStart + 10
if rule.Priority >= ruleStart && rule.Priority <= ruleEnd {
if rule.Priority >= ruleStart && rule.Priority <= ruleEnd || (t.options.AutoRedirectMarkMode && rule.Priority == t.options.IPRoute2AutoRedirectFallbackRuleIndex) {
ruleToDel := netlink.NewRule()
ruleToDel.Family = rule.Family
ruleToDel.Priority = rule.Priority
@@ -1003,6 +1022,24 @@ func (t *NativeTun) unsetRules() error {
return nil
}
func (t *NativeTun) unsetAddresses() {
if t.options.FileDescriptor > 0 {
return
}
tunLink, err := netlink.LinkByName(t.options.Name)
if err != nil {
return
}
for _, address := range t.options.Inet4Address {
addr, _ := netlink.ParseAddr(address.String())
_ = netlink.AddrDel(tunLink, addr)
}
for _, address := range t.options.Inet6Address {
addr, _ := netlink.ParseAddr(address.String())
_ = netlink.AddrDel(tunLink, addr)
}
}
func (t *NativeTun) resetRules() error {
t.unsetRules()
return t.setRules()
@@ -1046,3 +1083,14 @@ func (t *NativeTun) setSearchDomainForSystemdResolved() {
_ = shell.Exec(ctlPath, append([]string{"dns", t.options.Name}, common.Map(dnsServer, netip.Addr.String)...)...).Run()
}()
}
func (t *NativeTun) unsetSearchDomainForSystemdResolved() {
if t.options.EXP_DisableDNSHijack {
return
}
ctlPath, err := exec.LookPath("resolvectl")
if err != nil {
return
}
_ = shell.Exec(ctlPath, "revert", t.options.Name).Run()
}

View File

@@ -3,6 +3,8 @@
package tun
import (
"fmt"
"github.com/sagernet/gvisor/pkg/rawfile"
"github.com/sagernet/gvisor/pkg/tcpip/link/fdbased"
"github.com/sagernet/gvisor/pkg/tcpip/stack"
@@ -18,6 +20,37 @@ var _ GVisorTun = (*NativeTun)(nil)
func (t *NativeTun) WritePacket(pkt *stack.PacketBuffer) (int, error) {
iovecs := t.iovecsOutputDefault
if t.vnetHdr {
if t.vnetHdrWriteBuf == nil {
t.vnetHdrWriteBuf = make([]byte, virtioNetHdrLen)
}
vnetHdr := virtioNetHdr{}
if pkt.GSOOptions.Type != stack.GSONone {
vnetHdr.hdrLen = uint16(pkt.HeaderSize())
if pkt.GSOOptions.NeedsCsum {
vnetHdr.flags = unix.VIRTIO_NET_HDR_F_NEEDS_CSUM
vnetHdr.csumStart = pkt.GSOOptions.L3HdrLen
vnetHdr.csumOffset = pkt.GSOOptions.CsumOffset
}
if uint16(pkt.Data().Size()) > pkt.GSOOptions.MSS {
switch pkt.GSOOptions.Type {
case stack.GSOTCPv4:
vnetHdr.gsoType = unix.VIRTIO_NET_HDR_GSO_TCPV4
case stack.GSOTCPv6:
vnetHdr.gsoType = unix.VIRTIO_NET_HDR_GSO_TCPV6
default:
panic(fmt.Sprintf("Unknown gso type: %v", pkt.GSOOptions.Type))
}
vnetHdr.gsoSize = pkt.GSOOptions.MSS
}
}
if err := vnetHdr.encode(t.vnetHdrWriteBuf); err != nil {
return 0, err
}
iovec := unix.Iovec{Base: &t.vnetHdrWriteBuf[0]}
iovec.SetLen(virtioNetHdrLen)
iovecs = append(iovecs, iovec)
}
var dataLen int
for _, packetSlice := range pkt.AsSlices() {
dataLen += len(packetSlice)

View File

@@ -181,6 +181,13 @@ func (t *NativeTun) Start() error {
return err
}
if t.options.StrictRoute {
major, _, _ := windows.RtlGetNtVersionNumbers()
if major < 10 {
if t.options.Logger != nil {
t.options.Logger.Warn("strict routing is not supported on Windows versions below 10")
}
return nil
}
var engine uintptr
session := &winsys.FWPM_SESSION0{Flags: winsys.FWPM_SESSION_FLAG_DYNAMIC}
err := winsys.FwpmEngineOpen0(nil, winsys.RPC_C_AUTHN_DEFAULT, nil, session, unsafe.Pointer(&engine))
@@ -395,15 +402,16 @@ retry:
func (t *NativeTun) ReadPacket() ([]byte, func(), error) {
t.running.Add(1)
defer t.running.Done()
retry:
if t.close.Load() == 1 {
t.running.Done()
return nil, nil, os.ErrClosed
}
start := nanotime()
shouldSpin := t.rate.current.Load() >= spinloopRateThreshold && uint64(start-t.rate.nextStartTime.Load()) <= rateMeasurementGranularity*2
for {
if t.close.Load() == 1 {
t.running.Done()
return nil, nil, os.ErrClosed
}
packet, err := t.session.ReceivePacket()
@@ -411,7 +419,10 @@ retry:
case nil:
packetSize := len(packet)
t.rate.update(uint64(packetSize))
return packet, func() { t.session.ReleaseReceivePacket(packet) }, nil
return packet, func() {
t.session.ReleaseReceivePacket(packet)
t.running.Done()
}, nil
case windows.ERROR_NO_MORE_ITEMS:
if !shouldSpin || uint64(nanotime()-start) >= spinloopDuration {
windows.WaitForSingleObject(t.readWait, windows.INFINITE)
@@ -420,10 +431,13 @@ retry:
procyield(1)
continue
case windows.ERROR_HANDLE_EOF:
t.running.Done()
return nil, nil, os.ErrClosed
case windows.ERROR_INVALID_DATA:
t.running.Done()
return nil, nil, errors.New("send ring corrupt")
}
t.running.Done()
return nil, nil, fmt.Errorf("read failed: %w", err)
}
}