Compare commits

...

17 Commits

Author SHA1 Message Date
世界
0329538ecd Fix "Fix auto_redirect dropping SO_BINDTODEVICE traffic" 2026-03-14 20:58:43 +08:00
世界
caaf8469e0 Fix auto_redirect dropping SO_BINDTODEVICE traffic
REDIRECT in the OUTPUT chain rewrites the destination to 127.0.0.1,
then ip_route_me_harder() reroutes with the socket's bound interface
constraint (flowi4_oif). Since 127.0.0.1 is only reachable via lo,
the routing lookup fails and the packet is silently dropped.

Add a fallback routing table with `local 127.0.0.1` entries for each
non-loopback interface. When the local table lookup fails due to OIF
mismatch, the fallback table provides a matching RTN_LOCAL route.
The kernel then overrides dev_out to loopback (route.c:2857), so the
packet is delivered locally to the redirect server as intended.

This fixes NetworkManager connectivity checks and other tools that
use SO_BINDTODEVICE (e.g. curl --interface).
2026-03-11 21:15:11 +08:00
世界
c81ce6d358 Fix darwin batch loop not exit on EBADF 2026-03-05 20:35:38 +08:00
wwqgtxx
90ea7a0b69 Fix udp/icmp not work on gso with mixed stack 2026-03-03 23:48:15 +08:00
世界
6ee3db839d Update dependencies 2026-03-02 06:50:15 +08:00
eronez
1fb8456021 Fix DNS not revert while close tun 2026-03-02 06:46:28 +08:00
世界
5715a3919a Fix nftablesCreateLocalAddressSets 2026-02-23 17:52:46 +08:00
wwqgtxx
2a33d64abc Fix logging maybe panic 2026-02-13 00:28:14 +08:00
wwqgtxx
3144f43fc2 Fix udp/icmp not work on gso with system stack 2026-02-13 00:27:34 +08:00
世界
635920688d Add back random iproute2 table index 2026-02-02 14:15:26 +08:00
世界
2e21cd99ef Revert "Disable rp filter atomically"
This reverts commit 983b3caf40.
2026-02-02 14:15:10 +08:00
世界
983b3caf40 Disable rp filter atomically 2026-02-01 10:46:55 +08:00
世界
1d02d635b9 Fix auto_redirect fallback rule 2026-01-29 13:34:49 +08:00
世界
e88ed52dbc Fix auto_redirect on IPv6-only or IPv4-only servers 2026-01-28 18:27:44 +08:00
世界
381bf9d40d Skip tun interface traffic in prerouting UDP/ICMP chain 2026-01-28 18:27:44 +08:00
世界
2377e62d4a 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:31 +08:00
世界
a23b66f721 Skip strict routing on Windows versions below 10 2026-01-17 18:16:51 +08:00
12 changed files with 392 additions and 89 deletions

12
go.mod
View File

@@ -6,25 +6,25 @@ require (
github.com/florianl/go-nfqueue/v2 v2.0.2
github.com/go-ole/go-ole v1.3.0
github.com/google/btree v1.1.3
github.com/mdlayher/netlink v1.7.2
github.com/mdlayher/netlink v1.9.0
github.com/sagernet/fswatch v0.1.1
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
github.com/sagernet/nftables v0.3.0-beta.4
github.com/sagernet/sing v0.8.0-beta.2
github.com/sagernet/sing v0.8.0
github.com/stretchr/testify v1.11.1
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
golang.org/x/net v0.43.0
golang.org/x/sys v0.35.0
golang.org/x/net v0.50.0
golang.org/x/sys v0.41.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
golang.org/x/sync v0.7.0 // indirect

12
go.sum
View File

@@ -10,12 +10,18 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco=
github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
@@ -28,6 +34,8 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/sing v0.8.0-beta.2 h1:3khO2eE5LMylD/v47+pnVMtFzl6lBY2v/b/V+79qpsE=
github.com/sagernet/sing v0.8.0-beta.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.8.0 h1:OwLEwbcYfZHvu4olZVljxxC1XRicBqJ1HfiFr6F2WEE=
github.com/sagernet/sing v0.8.0/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
@@ -38,11 +46,15 @@ golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0J
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@@ -19,29 +19,31 @@ import (
)
type autoRedirect struct {
tunOptions *Options
ctx context.Context
handler Handler
logger logger.Logger
tableName string
networkMonitor NetworkUpdateMonitor
networkListener *list.Element[NetworkUpdateCallback]
interfaceFinder control.InterfaceFinder
localAddresses []netip.Prefix
customRedirectPortFunc func() int
customRedirectPort int
redirectServer *redirectServer
enableIPv4 bool
enableIPv6 bool
iptablesPath string
ip6tablesPath string
useNFTables bool
androidSu bool
suPath string
routeAddressSet *[]*netipx.IPSet
routeExcludeAddressSet *[]*netipx.IPSet
nfqueueHandler *nfqueueHandler
nfqueueEnabled bool
tunOptions *Options
ctx context.Context
handler Handler
logger logger.Logger
tableName string
networkMonitor NetworkUpdateMonitor
networkListener *list.Element[NetworkUpdateCallback]
interfaceFinder control.InterfaceFinder
localAddresses []netip.Prefix
customRedirectPortFunc func() int
customRedirectPort int
redirectServer *redirectServer
enableIPv4 bool
enableIPv6 bool
iptablesPath string
ip6tablesPath string
useNFTables bool
androidSu bool
suPath string
routeAddressSet *[]*netipx.IPSet
routeExcludeAddressSet *[]*netipx.IPSet
nfqueueHandler *nfqueueHandler
nfqueueEnabled bool
redirectRouteTableIndex int
redirectInterfaces []control.Interface
}
func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {
@@ -152,6 +154,12 @@ func (r *autoRedirect) Start() error {
}
r.cleanupNFTables()
err = r.setupNFTables()
if err == nil && r.tunOptions.AutoRedirectMarkMode {
err = r.setupRedirectRoutes()
if err != nil {
r.cleanupNFTables()
}
}
} else {
r.cleanupIPTables()
err = r.setupIPTables()
@@ -164,6 +172,7 @@ func (r *autoRedirect) Close() error {
r.nfqueueHandler.Close()
}
if r.useNFTables {
r.cleanupRedirectRoutes()
r.cleanupNFTables()
} else {
r.cleanupIPTables()

View File

@@ -4,6 +4,7 @@ package tun
import (
"net/netip"
"strings"
"github.com/sagernet/nftables"
"github.com/sagernet/nftables/binaryutil"
@@ -192,6 +193,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,
@@ -273,12 +293,22 @@ func (r *autoRedirect) setupNFTables() error {
if err != nil {
r.logger.Error("update local address set: ", err)
}
if r.tunOptions.AutoRedirectMarkMode {
err = r.updateRedirectRoutes()
if err != nil {
r.logger.Error("update redirect routes: ", err)
}
}
})
return nil
}
// TODO: test if 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()
@@ -287,6 +317,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 {

179
redirect_route_linux.go Normal file
View File

@@ -0,0 +1,179 @@
//go:build linux
package tun
import (
"math/rand"
"net"
"net/netip"
"github.com/sagernet/netlink"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
"golang.org/x/sys/unix"
)
const redirectRouteRulePriority = 1
func (r *autoRedirect) setupRedirectRoutes() error {
for {
r.redirectRouteTableIndex = int(rand.Uint32())
if r.redirectRouteTableIndex == r.tunOptions.IPRoute2TableIndex {
continue
}
routeList, fErr := netlink.RouteListFiltered(netlink.FAMILY_ALL,
&netlink.Route{Table: r.redirectRouteTableIndex},
netlink.RT_FILTER_TABLE)
if len(routeList) == 0 || fErr != nil {
break
}
}
err := r.interfaceFinder.Update()
if err != nil {
return err
}
tunName := r.tunOptions.Name
r.redirectInterfaces = common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool {
return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0
})
r.cleanupRedirectRoutes()
for _, iface := range r.redirectInterfaces {
err = r.addRedirectRoutes(iface)
if err != nil {
return err
}
}
if r.enableIPv4 {
rule := netlink.NewRule()
rule.Priority = redirectRouteRulePriority
rule.Table = r.redirectRouteTableIndex
rule.Family = unix.AF_INET
err = netlink.RuleAdd(rule)
if err != nil {
return err
}
}
if r.enableIPv6 {
rule := netlink.NewRule()
rule.Priority = redirectRouteRulePriority
rule.Table = r.redirectRouteTableIndex
rule.Family = unix.AF_INET6
err = netlink.RuleAdd(rule)
if err != nil {
return err
}
}
return nil
}
func (r *autoRedirect) addRedirectRoutes(iface control.Interface) error {
if r.enableIPv4 && common.Any(iface.Addresses, func(it netip.Prefix) bool {
return it.Addr().Is4()
}) {
err := netlink.RouteAppend(&netlink.Route{
LinkIndex: iface.Index,
Dst: &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(32, 32)},
Table: r.redirectRouteTableIndex,
Type: unix.RTN_LOCAL,
Scope: netlink.SCOPE_HOST,
})
if err != nil {
return err
}
}
if r.enableIPv6 && common.Any(iface.Addresses, func(it netip.Prefix) bool {
return it.Addr().Is6() && !it.Addr().Is4In6()
}) {
err := netlink.RouteAppend(&netlink.Route{
LinkIndex: iface.Index,
Dst: &net.IPNet{IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)},
Table: r.redirectRouteTableIndex,
Type: unix.RTN_LOCAL,
Scope: netlink.SCOPE_HOST,
})
if err != nil {
return err
}
}
return nil
}
func (r *autoRedirect) removeRedirectRoutes(linkIndex int) {
if r.enableIPv4 {
_ = netlink.RouteDel(&netlink.Route{
LinkIndex: linkIndex,
Dst: &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(32, 32)},
Table: r.redirectRouteTableIndex,
Type: unix.RTN_LOCAL,
})
}
if r.enableIPv6 {
_ = netlink.RouteDel(&netlink.Route{
LinkIndex: linkIndex,
Dst: &net.IPNet{IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)},
Table: r.redirectRouteTableIndex,
Type: unix.RTN_LOCAL,
})
}
}
func (r *autoRedirect) updateRedirectRoutes() error {
err := r.interfaceFinder.Update()
if err != nil {
return err
}
tunName := r.tunOptions.Name
newInterfaces := common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool {
return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0
})
oldMap := make(map[int]bool, len(r.redirectInterfaces))
for _, iface := range r.redirectInterfaces {
oldMap[iface.Index] = true
}
newMap := make(map[int]bool, len(newInterfaces))
for _, iface := range newInterfaces {
newMap[iface.Index] = true
}
for _, iface := range newInterfaces {
if !oldMap[iface.Index] {
err = r.addRedirectRoutes(iface)
if err != nil {
return err
}
}
}
for _, iface := range r.redirectInterfaces {
if !newMap[iface.Index] {
r.removeRedirectRoutes(iface.Index)
}
}
r.redirectInterfaces = newInterfaces
return nil
}
func (r *autoRedirect) cleanupRedirectRoutes() {
if r.redirectRouteTableIndex == 0 {
return
}
routes, _ := netlink.RouteListFiltered(netlink.FAMILY_ALL,
&netlink.Route{Table: r.redirectRouteTableIndex},
netlink.RT_FILTER_TABLE)
for _, route := range routes {
_ = netlink.RouteDel(&route)
}
if r.enableIPv4 {
rule := netlink.NewRule()
rule.Priority = redirectRouteRulePriority
rule.Table = r.redirectRouteTableIndex
rule.Family = unix.AF_INET
_ = netlink.RuleDel(rule)
}
if r.enableIPv6 {
rule := netlink.NewRule()
rule.Priority = redirectRouteRulePriority
rule.Table = r.redirectRouteTableIndex
rule.Family = unix.AF_INET6
_ = netlink.RuleDel(rule)
}
}

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

@@ -269,7 +269,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"))

78
tun.go
View File

@@ -63,47 +63,49 @@ 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
AutoRedirectResetMark uint32
AutoRedirectNFQueue uint16
ExcludeMPTCP bool
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
AutoRedirectResetMark uint32
AutoRedirectNFQueue uint16
ExcludeMPTCP bool
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

@@ -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
@@ -149,7 +149,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)
}
}
}
@@ -276,7 +278,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"))
}
}
}
@@ -325,6 +329,7 @@ func (t *NativeTun) Close() error {
if t.options.EXP_ExternalConfiguration {
return common.Close(common.PtrOrNil(t.tunFile))
}
t.unsetSearchDomainForSystemdResolved()
t.unsetAddresses()
return E.Errors(t.unsetRoute(), t.unsetRules(), common.Close(common.PtrOrNil(t.tunFile)))
}
@@ -393,10 +398,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
}
@@ -630,6 +632,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
}
@@ -1003,7 +1021,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
@@ -1078,3 +1096,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

@@ -184,6 +184,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))