From 85f5f2dd587df233dee6254d244cd39ba7653c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 12 Jun 2024 02:37:51 +0800 Subject: [PATCH] auto-redirect: Add route address set support for nftables --- go.mod | 11 +- go.sum | 22 +- monitor_darwin.go | 6 +- redirect.go | 27 +- redirect_iptables.go | 22 +- redirect_linux.go | 35 +- redirect_nftables.go | 327 +++++++-------- redirect_nftables_expr.go | 179 --------- redirect_nftables_exprs.go | 181 +++++++++ redirect_nftables_rules.go | 620 +++++++++++++++++++++++++++++ redirect_nftables_rules_openwrt.go | 103 +++++ tun.go | 10 +- tun_darwin.go | 3 + tun_linux.go | 115 ++++-- 14 files changed, 1245 insertions(+), 416 deletions(-) delete mode 100644 redirect_nftables_expr.go create mode 100644 redirect_nftables_exprs.go create mode 100644 redirect_nftables_rules.go create mode 100644 redirect_nftables_rules_openwrt.go diff --git a/go.mod b/go.mod index 5cfda58..118e790 100644 --- a/go.mod +++ b/go.mod @@ -6,21 +6,22 @@ require ( github.com/fsnotify/fsnotify v1.7.0 github.com/go-ole/go-ole v1.3.0 github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f - github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba - github.com/sagernet/nftables v0.3.0-beta.2 - github.com/sagernet/sing v0.5.0-alpha.9 + github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a + github.com/sagernet/nftables v0.3.0-beta.4 + github.com/sagernet/sing v0.5.0-alpha.10 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 golang.org/x/net v0.26.0 golang.org/x/sys v0.21.0 ) require ( github.com/google/btree v1.1.2 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/josharian/native v1.1.0 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/vishvananda/netns v0.0.4 // indirect - golang.org/x/sync v0.1.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/time v0.5.0 // indirect ) diff --git a/go.sum b/go.sum index 55a9c72..29b6826 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/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= @@ -16,21 +16,23 @@ github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8Ku github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f h1:NkhuupzH5ch7b/Y/6ZHJWrnNLoiNnSJaow6DPb8VW2I= github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f/go.mod h1:KXmw+ouSJNOsuRpg4wgwwCQuunrGz4yoAqQjsLjc6N0= -github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba h1:EY5AS7CCtfmARNv2zXUOrsEMPFDGYxaw65JzA2p51Vk= -github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= -github.com/sagernet/nftables v0.3.0-beta.2 h1:yKqMl4Dpb6nKxAmlE6fXjJRlLO2c1f2wyNFBg4hBr8w= -github.com/sagernet/nftables v0.3.0-beta.2/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= -github.com/sagernet/sing v0.5.0-alpha.9 h1:Mmg+LCbaKXBeQD/ttzi0/MQa3NcUyfadIgkGzhQW7o0= -github.com/sagernet/sing v0.5.0-alpha.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= +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.5.0-alpha.10 h1:kuHl10gpjbKQAdQfyogQU3u0CVnpqC3wrAHe/+BFaXc= +github.com/sagernet/sing v0.5.0-alpha.10/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= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/monitor_darwin.go b/monitor_darwin.go index bf9db84..baeb1e1 100644 --- a/monitor_darwin.go +++ b/monitor_darwin.go @@ -154,9 +154,9 @@ func (m *defaultInterfaceMonitor) checkUpdate() error { if routeMessage.Flags&unix.RTF_GATEWAY == 0 { continue } - if routeMessage.Flags&unix.RTF_IFSCOPE != 0 { - // continue - } + // if routeMessage.Flags&unix.RTF_IFSCOPE != 0 { + //continue + //} defaultInterface = routeInterface break } diff --git a/redirect.go b/redirect.go index d61db97..189f09e 100644 --- a/redirect.go +++ b/redirect.go @@ -3,20 +3,33 @@ package tun import ( "context" + "github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/logger" + + "go4.org/netipx" +) + +const ( + DefaultAutoRedirectInputMark = 0x2023 + DefaultAutoRedirectOutputMark = 0x2024 ) type AutoRedirect interface { Start() error Close() error + UpdateRouteAddressSet() } type AutoRedirectOptions struct { - TunOptions *Options - Context context.Context - Handler Handler - Logger logger.Logger - TableName string - DisableNFTables bool - CustomRedirectPort func() int + TunOptions *Options + Context context.Context + Handler Handler + Logger logger.Logger + NetworkMonitor NetworkUpdateMonitor + InterfaceFinder control.InterfaceFinder + TableName string + DisableNFTables bool + CustomRedirectPort func() int + RouteAddressSet *[]*netipx.IPSet + RouteExcludeAddressSet *[]*netipx.IPSet } diff --git a/redirect_iptables.go b/redirect_iptables.go index c197dd9..81aa839 100644 --- a/redirect_iptables.go +++ b/redirect_iptables.go @@ -29,8 +29,9 @@ func (r *autoRedirect) setupIPTables() error { } func (r *autoRedirect) setupIPTablesForFamily(iptablesPath string) error { - tableNameOutput := r.tableName + "-output" + tableNameInput := r.tableName + "-input" tableNameForward := r.tableName + "-forward" + tableNameOutput := r.tableName + "-output" tableNamePreRouteing := r.tableName + "-prerouting" redirectPort := r.redirectPort() // OUTPUT @@ -51,6 +52,25 @@ func (r *autoRedirect) setupIPTablesForFamily(iptablesPath string) error { if r.androidSu { return nil } + // INPUT + err = r.runShell(iptablesPath, "-N", tableNameInput) + if err != nil { + return err + } + err = r.runShell(iptablesPath, "-A", tableNameInput, + "-i", r.tunOptions.Name, "-j", "ACCEPT") + if err != nil { + return err + } + err = r.runShell(iptablesPath, "-A", tableNameInput, + "-o", r.tunOptions.Name, "-j", "ACCEPT") + if err != nil { + return err + } + err = r.runShell(iptablesPath, "-I FORWARD -j", tableNameInput) + if err != nil { + return err + } // FORWARD err = r.runShell(iptablesPath, "-N", tableNameForward) if err != nil { diff --git a/redirect_linux.go b/redirect_linux.go index bcad482..cafdd61 100644 --- a/redirect_linux.go +++ b/redirect_linux.go @@ -6,12 +6,17 @@ import ( "os" "os/exec" "runtime" + "time" "github.com/sagernet/nftables" "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/x/list" + + "go4.org/netipx" ) type autoRedirect struct { @@ -20,6 +25,10 @@ type autoRedirect struct { 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 @@ -30,6 +39,8 @@ type autoRedirect struct { useNFTables bool androidSu bool suPath string + routeAddressSet *[]*netipx.IPSet + routeExcludeAddressSet *[]*netipx.IPSet } func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { @@ -38,9 +49,13 @@ func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { ctx: options.Context, handler: options.Handler, logger: options.Logger, + networkMonitor: options.NetworkMonitor, + interfaceFinder: options.InterfaceFinder, tableName: options.TableName, useNFTables: runtime.GOOS != "android" && !options.DisableNFTables, customRedirectPortFunc: options.CustomRedirectPort, + routeAddressSet: options.RouteAddressSet, + routeExcludeAddressSet: options.RouteExcludeAddressSet, } var err error if runtime.GOOS == "android" { @@ -116,11 +131,18 @@ func (r *autoRedirect) Start() error { } r.redirectServer = server } + startAt := time.Now() + var err error if r.useNFTables { - return r.setupNFTables() + err = r.setupNFTables() } else { - return r.setupIPTables() + err = r.setupIPTables() + } + if err != nil { + return err } + r.logger.Debug("auto-redirect configured in ", time.Since(startAt)) + return nil } func (r *autoRedirect) Close() error { @@ -134,6 +156,15 @@ func (r *autoRedirect) Close() error { ) } +func (r *autoRedirect) UpdateRouteAddressSet() { + if r.useNFTables { + err := r.nftablesUpdateRouteAddressSet() + if err != nil { + r.logger.Error("update route address set: ", err) + } + } +} + func (r *autoRedirect) initializeNFTables() error { nft, err := nftables.New() if err != nil { diff --git a/redirect_nftables.go b/redirect_nftables.go index 3b3e04c..d1f1437 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -9,7 +9,9 @@ import ( "github.com/sagernet/nftables/binaryutil" "github.com/sagernet/nftables/expr" "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/control" + "golang.org/x/exp/slices" "golang.org/x/sys/unix" ) @@ -25,28 +27,21 @@ func (r *autoRedirect) setupNFTables() error { Family: nftables.TableFamilyINet, }) - chainForward := nft.AddChain(&nftables.Chain{ - Name: "forward", - Table: table, - Hooknum: nftables.ChainHookForward, - Priority: nftables.ChainPriorityMangle, - }) - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainForward, - Exprs: nftablesRuleIfName(expr.MetaKeyIIFNAME, r.tunOptions.Name, &expr.Verdict{ - Kind: expr.VerdictAccept, - }), - }) - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainForward, - Exprs: nftablesRuleIfName(expr.MetaKeyOIFNAME, r.tunOptions.Name, &expr.Verdict{ - Kind: expr.VerdictAccept, - }), + err = r.nftablesCreateAddressSets(nft, table, false) + if err != nil { + return err + } + + r.localAddresses = 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() + }) }) + err = r.nftablesCreateLocalAddressSets(nft, table, r.localAddresses, nil) + if err != nil { + return err + } - redirectPort := r.redirectPort() chainOutput := nft.AddChain(&nftables.Chain{ Name: "output", Table: table, @@ -54,11 +49,37 @@ func (r *autoRedirect) setupNFTables() error { Priority: nftables.ChainPriorityMangle, Type: nftables.ChainTypeNAT, }) - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainOutput, - Exprs: nftablesRuleIfName(expr.MetaKeyOIFNAME, r.tunOptions.Name, nftablesRuleRedirectToPorts(redirectPort)...), - }) + if r.tunOptions.AutoRedirectMarkMode { + err = r.nftablesCreateExcludeRules(nft, table, chainOutput) + if err != nil { + return err + } + r.nftablesCreateUnreachable(nft, table, chainOutput) + r.nftablesCreateRedirect(nft, table, chainOutput) + + chainOutputUDP := nft.AddChain(&nftables.Chain{ + Name: "output_udp", + Table: table, + Hooknum: nftables.ChainHookOutput, + Priority: nftables.ChainPriorityMangle, + Type: nftables.ChainTypeRoute, + }) + err = r.nftablesCreateExcludeRules(nft, table, chainOutputUDP) + if err != nil { + return err + } + r.nftablesCreateUnreachable(nft, table, chainOutputUDP) + r.nftablesCreateMark(nft, table, chainOutputUDP) + } else { + r.nftablesCreateRedirect(nft, table, chainOutput, &expr.Meta{ + Key: expr.MetaKeyOIFNAME, + Register: 1, + }, &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: nftablesIfname(r.tunOptions.Name), + }) + } chainPreRouting := nft.AddChain(&nftables.Chain{ Name: "prerouting", @@ -67,178 +88,136 @@ func (r *autoRedirect) setupNFTables() error { Priority: nftables.ChainPriorityMangle, Type: nftables.ChainTypeNAT, }) - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainPreRouting, - Exprs: nftablesRuleIfName(expr.MetaKeyIIFNAME, r.tunOptions.Name, &expr.Verdict{ - Kind: expr.VerdictReturn, - }), - }) - var ( - routeAddress []netip.Prefix - routeExcludeAddress []netip.Prefix - ) - if r.enableIPv4 { - routeAddress = append(routeAddress, r.tunOptions.Inet4RouteAddress...) - routeExcludeAddress = append(routeExcludeAddress, r.tunOptions.Inet4RouteExcludeAddress...) - } - if r.enableIPv6 { - routeAddress = append(routeAddress, r.tunOptions.Inet6RouteAddress...) - routeExcludeAddress = append(routeExcludeAddress, r.tunOptions.Inet6RouteExcludeAddress...) - } - for _, address := range routeExcludeAddress { - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainPreRouting, - Exprs: nftablesRuleDestinationAddress(address, &expr.Verdict{ - Kind: expr.VerdictReturn, - }), - }) + err = r.nftablesCreateExcludeRules(nft, table, chainPreRouting) + if err != nil { + return err } - for _, name := range r.tunOptions.ExcludeInterface { - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainPreRouting, - Exprs: nftablesRuleIfName(expr.MetaKeyIIFNAME, name, &expr.Verdict{ - Kind: expr.VerdictReturn, - }), + r.nftablesCreateUnreachable(nft, table, chainPreRouting) + r.nftablesCreateRedirect(nft, table, chainPreRouting) + r.nftablesCreateMark(nft, table, chainPreRouting) + + if r.tunOptions.AutoRedirectMarkMode { + chainPreRoutingUDP := nft.AddChain(&nftables.Chain{ + Name: "prerouting_udp", + Table: table, + Hooknum: nftables.ChainHookPrerouting, + Priority: nftables.ChainPriorityRef(*nftables.ChainPriorityMangle + 1), + Type: nftables.ChainTypeFilter, }) - } - for _, uidRange := range r.tunOptions.ExcludeUID { + if r.enableIPv4 { + nftablesCreateExcludeDestinationIPSet(nft, table, chainPreRoutingUDP, 5, "inet4_local_address_set", nftables.TableFamilyIPv4, false) + } + if r.enableIPv6 { + nftablesCreateExcludeDestinationIPSet(nft, table, chainPreRoutingUDP, 6, "inet6_local_address_set", nftables.TableFamilyIPv6, false) + } nft.AddRule(&nftables.Rule{ Table: table, - Chain: chainPreRouting, - Exprs: nftablesRuleMetaUInt32Range(expr.MetaKeySKUID, uidRange, &expr.Verdict{ - Kind: expr.VerdictReturn, - }), + Chain: chainPreRoutingUDP, + Exprs: []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyL4PROTO, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{unix.IPPROTO_UDP}, + }, + &expr.Ct{ + Key: expr.CtKeyMARK, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectInputMark), + }, + &expr.Meta{ + Key: expr.MetaKeyMARK, + Register: 1, + SourceRegister: true, + }, + &expr.Counter{}, + }, }) } - var routeExprs []expr.Any - if len(routeAddress) > 0 { - for _, address := range routeAddress { - routeExprs = append(routeExprs, nftablesRuleDestinationAddress(address)...) - } + err = r.configureOpenWRTFirewall4(nft, false) + if err != nil { + return err } - if !r.tunOptions.EXP_DisableDNSHijack { - dnsServer4 := common.Find(r.tunOptions.DNSServers, func(it netip.Addr) bool { - return it.Is4() - }) - dnsServer6 := common.Find(r.tunOptions.DNSServers, func(it netip.Addr) bool { - return it.Is6() - }) - if r.enableIPv4 && !dnsServer4.IsValid() { - dnsServer4 = r.tunOptions.Inet4Address[0].Addr().Next() - } - if r.enableIPv6 && !dnsServer6.IsValid() { - dnsServer6 = r.tunOptions.Inet6Address[0].Addr().Next() - } - if len(r.tunOptions.IncludeInterface) > 0 || len(r.tunOptions.IncludeUID) > 0 { - for _, name := range r.tunOptions.IncludeInterface { - if r.enableIPv4 { - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainPreRouting, - Exprs: nftablesRuleIfName(expr.MetaKeyIIFNAME, name, append(routeExprs, nftablesRuleHijackDNS(nftables.TableFamilyIPv4, dnsServer4)...)...), - }) - } - if r.enableIPv6 { - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainPreRouting, - Exprs: nftablesRuleIfName(expr.MetaKeyIIFNAME, name, append(routeExprs, nftablesRuleHijackDNS(nftables.TableFamilyIPv6, dnsServer6)...)...), - }) - } - } - for _, uidRange := range r.tunOptions.IncludeUID { - if r.enableIPv4 { - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainPreRouting, - Exprs: nftablesRuleMetaUInt32Range(expr.MetaKeySKUID, uidRange, append(routeExprs, nftablesRuleHijackDNS(nftables.TableFamilyIPv4, dnsServer4)...)...), - }) - } - if r.enableIPv6 { - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainPreRouting, - Exprs: nftablesRuleMetaUInt32Range(expr.MetaKeySKUID, uidRange, append(routeExprs, nftablesRuleHijackDNS(nftables.TableFamilyIPv6, dnsServer6)...)...), - }) - } - } - } else { - if r.enableIPv4 { - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainPreRouting, - Exprs: append(routeExprs, nftablesRuleHijackDNS(nftables.TableFamilyIPv4, dnsServer4)...), - }) - } - if r.enableIPv6 { - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainPreRouting, - Exprs: append(routeExprs, nftablesRuleHijackDNS(nftables.TableFamilyIPv6, dnsServer6)...), - }) - } - } + err = nft.Flush() + if err != nil { + return err } - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainPreRouting, - Exprs: []expr.Any{ - &expr.Fib{ - Register: 1, - FlagDADDR: true, - ResultADDRTYPE: true, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: binaryutil.NativeEndian.PutUint32(unix.RTN_LOCAL), - }, - &expr.Verdict{ - Kind: expr.VerdictReturn, - }, - }, + r.networkListener = r.networkMonitor.RegisterCallback(func() { + err = r.nftablesUpdateLocalAddressSet() + if err != nil { + r.logger.Error("update local address set: ", err) + } }) + return nil +} - if len(r.tunOptions.IncludeInterface) > 0 || len(r.tunOptions.IncludeUID) > 0 { - for _, name := range r.tunOptions.IncludeInterface { - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainPreRouting, - Exprs: nftablesRuleIfName(expr.MetaKeyIIFNAME, name, append(routeExprs, nftablesRuleRedirectToPorts(redirectPort)...)...), - }) - } - for _, uidRange := range r.tunOptions.IncludeUID { - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainPreRouting, - Exprs: nftablesRuleMetaUInt32Range(expr.MetaKeySKUID, uidRange, append(routeExprs, nftablesRuleRedirectToPorts(redirectPort)...)...), - }) - } - } else { - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chainPreRouting, - Exprs: append(routeExprs, nftablesRuleRedirectToPorts(redirectPort)...), +// TODO; test is this works +func (r *autoRedirect) nftablesUpdateLocalAddressSet() error { + 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() }) + }) + if slices.Equal(newLocalAddresses, r.localAddresses) { + return nil + } + nft, err := nftables.New() + if err != nil { + return err + } + defer nft.CloseLasting() + table, err := nft.ListTableOfFamily(r.tableName, nftables.TableFamilyINet) + if err != nil { + return err + } + err = r.nftablesCreateLocalAddressSets(nft, table, newLocalAddresses, r.localAddresses) + if err != nil { + return err + } + r.localAddresses = newLocalAddresses + return nft.Flush() +} + +func (r *autoRedirect) nftablesUpdateRouteAddressSet() error { + nft, err := nftables.New() + if err != nil { + return err + } + defer nft.CloseLasting() + table, err := nft.ListTableOfFamily(r.tableName, nftables.TableFamilyINet) + if err != nil { + return err + } + err = r.nftablesCreateAddressSets(nft, table, true) + if err != nil { + return err } return nft.Flush() } func (r *autoRedirect) cleanupNFTables() { - conn, err := nftables.New() + if r.networkListener != nil { + r.networkMonitor.UnregisterCallback(r.networkListener) + } + nft, err := nftables.New() if err != nil { return } - conn.DelTable(&nftables.Table{ + nft.DelTable(&nftables.Table{ Name: r.tableName, Family: nftables.TableFamilyINet, }) - _ = conn.Flush() - _ = conn.CloseLasting() + common.Must(r.configureOpenWRTFirewall4(nft, true)) + _ = nft.Flush() + _ = nft.CloseLasting() } diff --git a/redirect_nftables_expr.go b/redirect_nftables_expr.go deleted file mode 100644 index 9692d8a..0000000 --- a/redirect_nftables_expr.go +++ /dev/null @@ -1,179 +0,0 @@ -//go:build linux - -package tun - -import ( - "net" - "net/netip" - - "github.com/sagernet/nftables" - "github.com/sagernet/nftables/binaryutil" - "github.com/sagernet/nftables/expr" - "github.com/sagernet/sing/common/ranges" - - "golang.org/x/sys/unix" -) - -func nftablesIfname(n string) []byte { - b := make([]byte, 16) - copy(b, n+"\x00") - return b -} - -func nftablesRuleIfName(key expr.MetaKey, value string, exprs ...expr.Any) []expr.Any { - newExprs := []expr.Any{ - &expr.Meta{Key: key, Register: 1}, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: nftablesIfname(value), - }, - } - newExprs = append(newExprs, exprs...) - return newExprs -} - -func nftablesRuleMetaUInt32Range(key expr.MetaKey, uidRange ranges.Range[uint32], exprs ...expr.Any) []expr.Any { - newExprs := []expr.Any{ - &expr.Meta{Key: key, Register: 1}, - &expr.Range{ - Op: expr.CmpOpEq, - Register: 1, - FromData: binaryutil.BigEndian.PutUint32(uidRange.Start), - ToData: binaryutil.BigEndian.PutUint32(uidRange.End), - }, - } - newExprs = append(newExprs, exprs...) - return newExprs -} - -func nftablesRuleDestinationAddress(address netip.Prefix, exprs ...expr.Any) []expr.Any { - newExprs := []expr.Any{ - &expr.Meta{ - Key: expr.MetaKeyNFPROTO, - Register: 1, - }, - } - if address.Addr().Is4() { - newExprs = append(newExprs, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: []byte{unix.NFPROTO_IPV4}, - }, - &expr.Payload{ - OperationType: expr.PayloadLoad, - DestRegister: 1, - SourceRegister: 0, - Base: expr.PayloadBaseNetworkHeader, - Offset: 16, - Len: 4, - }, &expr.Bitwise{ - SourceRegister: 1, - DestRegister: 1, - Len: 4, - Xor: make([]byte, 4), - Mask: net.CIDRMask(address.Bits(), 32), - }) - } else { - newExprs = append(newExprs, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: []byte{unix.NFPROTO_IPV6}, - }, - &expr.Payload{ - OperationType: expr.PayloadLoad, - DestRegister: 1, - SourceRegister: 0, - Base: expr.PayloadBaseNetworkHeader, - Offset: 24, - Len: 16, - }, &expr.Bitwise{ - SourceRegister: 1, - DestRegister: 1, - Len: 16, - Xor: make([]byte, 16), - Mask: net.CIDRMask(address.Bits(), 128), - }) - } - newExprs = append(newExprs, &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: address.Masked().Addr().AsSlice(), - }) - newExprs = append(newExprs, exprs...) - return newExprs -} - -func nftablesRuleHijackDNS(family nftables.TableFamily, dnsServerAddress netip.Addr) []expr.Any { - return []expr.Any{ - &expr.Meta{ - Key: expr.MetaKeyNFPROTO, - Register: 1, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: []byte{uint8(family)}, - }, - &expr.Meta{ - Key: expr.MetaKeyL4PROTO, - Register: 1, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: []byte{unix.IPPROTO_UDP}, - }, - &expr.Payload{ - OperationType: expr.PayloadLoad, - DestRegister: 1, - SourceRegister: 0, - Base: expr.PayloadBaseTransportHeader, - Offset: 2, - Len: 2, - }, &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: binaryutil.BigEndian.PutUint16(53), - }, &expr.Immediate{ - Register: 1, - Data: dnsServerAddress.AsSlice(), - }, &expr.NAT{ - Type: expr.NATTypeDestNAT, - Family: uint32(family), - RegAddrMin: 1, - }, - } -} - -const ( - NF_NAT_RANGE_MAP_IPS = 1 << iota - NF_NAT_RANGE_PROTO_SPECIFIED - NF_NAT_RANGE_PROTO_RANDOM - NF_NAT_RANGE_PERSISTENT - NF_NAT_RANGE_PROTO_RANDOM_FULLY - NF_NAT_RANGE_PROTO_OFFSET -) - -func nftablesRuleRedirectToPorts(redirectPort uint16) []expr.Any { - return []expr.Any{ - &expr.Meta{ - Key: expr.MetaKeyL4PROTO, - Register: 1, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: []byte{unix.IPPROTO_TCP}, - }, - &expr.Immediate{ - Register: 1, - Data: binaryutil.BigEndian.PutUint16(redirectPort), - }, &expr.Redir{ - RegisterProtoMin: 1, - Flags: NF_NAT_RANGE_PROTO_SPECIFIED, - }, - } -} diff --git a/redirect_nftables_exprs.go b/redirect_nftables_exprs.go new file mode 100644 index 0000000..09fe589 --- /dev/null +++ b/redirect_nftables_exprs.go @@ -0,0 +1,181 @@ +//go:build linux + +package tun + +import ( + "net/netip" + "unsafe" + + "github.com/sagernet/nftables" + "github.com/sagernet/nftables/expr" + + "go4.org/netipx" +) + +func nftablesIfname(n string) []byte { + b := make([]byte, 16) + copy(b, n+"\x00") + return b +} + +func nftablesCreateExcludeDestinationIPSet( + nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, + id uint32, name string, family nftables.TableFamily, invert bool, +) { + exprs := []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyNFPROTO, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{byte(family)}, + }, + } + if family == nftables.TableFamilyIPv4 { + exprs = append(exprs, + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseNetworkHeader, + Offset: 16, + Len: 4, + }, + ) + } else { + exprs = append(exprs, + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseNetworkHeader, + Offset: 24, + Len: 16, + }, + ) + } + exprs = append(exprs, + &expr.Lookup{ + SourceRegister: 1, + SetID: id, + SetName: name, + Invert: invert, + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }) + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: exprs, + }) +} + +func nftablesCreateIPSet( + nft *nftables.Conn, table *nftables.Table, + id uint32, name string, family nftables.TableFamily, + setList []*netipx.IPSet, prefixList []netip.Prefix, appendDefault bool, update bool, +) (*nftables.Set, error) { + if len(prefixList) > 0 { + var builder netipx.IPSetBuilder + if appendDefault && len(setList) == 0 { + if family == nftables.TableFamilyIPv4 { + prefixList = append(prefixList, netip.PrefixFrom(netip.IPv4Unspecified(), 0)) + } else { + prefixList = append(prefixList, netip.PrefixFrom(netip.IPv6Unspecified(), 0)) + } + } + for _, prefix := range prefixList { + builder.AddPrefix(prefix) + } + + ipSet, err := builder.IPSet() + if err != nil { + return nil, err + } + setList = append(setList, ipSet) + } + ipSets := make([]*myIPSet, 0, len(setList)) + var rangeLen int + for _, set := range setList { + mySet := (*myIPSet)(unsafe.Pointer(set)) + ipSets = append(ipSets, mySet) + rangeLen += len(mySet.rr) + } + setElements := make([]nftables.SetElement, 0, len(prefixList)+rangeLen) + for _, mySet := range ipSets { + for _, rr := range mySet.rr { + if (family == nftables.TableFamilyIPv4) != rr.from.Is4() { + continue + } + endAddr := rr.to.Next() + if !endAddr.IsValid() { + endAddr = rr.from + } + setElements = append(setElements, nftables.SetElement{ + Key: rr.from.AsSlice(), + }) + setElements = append(setElements, nftables.SetElement{ + Key: endAddr.AsSlice(), + IntervalEnd: true, + }) + } + } + var keyType nftables.SetDatatype + if family == nftables.TableFamilyIPv4 { + keyType = nftables.TypeIPAddr + } else { + keyType = nftables.TypeIP6Addr + } + mySet := &nftables.Set{ + Table: table, + ID: id, + Name: name, + Interval: true, + KeyType: keyType, + } + if id == 0 { + mySet.Anonymous = true + mySet.Constant = true + } + if id == 0 { + err := nft.AddSet(mySet, setElements) + if err != nil { + return nil, err + } + return mySet, nil + } else if update { + nft.FlushSet(mySet) + } else { + err := nft.AddSet(mySet, nil) + if err != nil { + return nil, err + } + } + for len(setElements) > 0 { + toAdd := setElements + if len(toAdd) > 1000 { + toAdd = toAdd[:1000] + } + setElements = setElements[len(toAdd):] + err := nft.SetAddElements(mySet, toAdd) + if err != nil { + return nil, err + } + err = nft.Flush() + if err != nil { + return nil, err + } + } + return mySet, nil +} + +type myIPSet struct { + rr []myIPRange +} + +type myIPRange struct { + from netip.Addr + to netip.Addr +} diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go new file mode 100644 index 0000000..8cb78e5 --- /dev/null +++ b/redirect_nftables_rules.go @@ -0,0 +1,620 @@ +//go:build linux + +package tun + +import ( + "net/netip" + _ "unsafe" + + "github.com/sagernet/nftables" + "github.com/sagernet/nftables/binaryutil" + "github.com/sagernet/nftables/expr" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/ranges" + + "golang.org/x/exp/slices" + "golang.org/x/sys/unix" +) + +//go:linkname allocSetID github.com/sagernet/nftables.allocSetID +var allocSetID uint32 + +func init() { + allocSetID = 6 +} + +func (r *autoRedirect) nftablesCreateAddressSets( + nft *nftables.Conn, table *nftables.Table, + update bool, +) error { + routeAddressSet := *r.routeAddressSet + routeExcludeAddressSet := *r.routeExcludeAddressSet + if len(routeAddressSet) == 0 && len(routeExcludeAddressSet) == 0 { + return nil + } + + if len(routeAddressSet) > 0 { + if r.enableIPv4 { + _, err := nftablesCreateIPSet(nft, table, 1, "inet4_route_address_set", nftables.TableFamilyIPv4, routeAddressSet, nil, true, update) + if err != nil { + return err + } + } + if r.enableIPv6 { + _, err := nftablesCreateIPSet(nft, table, 2, "inet6_route_address_set", nftables.TableFamilyIPv6, routeAddressSet, nil, true, update) + if err != nil { + return err + } + } + } + + if len(routeExcludeAddressSet) > 0 { + if r.enableIPv4 { + _, err := nftablesCreateIPSet(nft, table, 3, "inet4_route_exclude_address_set", nftables.TableFamilyIPv4, routeExcludeAddressSet, nil, false, update) + if err != nil { + return err + } + } + if r.enableIPv6 { + _, err := nftablesCreateIPSet(nft, table, 4, "inet6_route_exclude_address_set", nftables.TableFamilyIPv6, routeExcludeAddressSet, nil, false, update) + if err != nil { + return err + } + } + } + return nil +} + +func (r *autoRedirect) nftablesCreateLocalAddressSets( + nft *nftables.Conn, table *nftables.Table, + localAddresses []netip.Prefix, lastAddresses []netip.Prefix, +) error { + if r.enableIPv4 { + 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) { + update = true + } + } + if len(lastAddresses) == 0 || update { + _, err := nftablesCreateIPSet(nft, table, 5, "inet4_local_address_set", nftables.TableFamilyIPv4, nil, localAddresses4, false, update) + if err != nil { + return err + } + } + } + if r.enableIPv6 { + 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) { + 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 { + return err + } + } + } + return nil +} + +func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain) error { + if r.tunOptions.AutoRedirectMarkMode && chain.Hooknum == nftables.ChainHookOutput { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyMARK, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectOutputMark), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } + if chain.Hooknum == nftables.ChainHookPrerouting { + if len(r.tunOptions.IncludeInterface) > 0 { + if len(r.tunOptions.IncludeInterface) > 1 { + includeInterface := &nftables.Set{ + Table: table, + Anonymous: true, + Constant: true, + KeyType: nftables.TypeIFName, + } + err := nft.AddSet(includeInterface, common.Map(r.tunOptions.IncludeInterface, func(it string) nftables.SetElement { + return nftables.SetElement{ + Key: nftablesIfname(it), + } + })) + if err != nil { + return err + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1}, + &expr.Lookup{ + SourceRegister: 1, + SetID: includeInterface.ID, + SetName: includeInterface.Name, + Invert: true, + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } else { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1}, + &expr.Cmp{ + Op: expr.CmpOpNeq, + Register: 1, + Data: nftablesIfname(r.tunOptions.IncludeInterface[0]), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } + } + + if len(r.tunOptions.ExcludeInterface) > 0 { + if len(r.tunOptions.ExcludeInterface) > 1 { + excludeInterface := &nftables.Set{ + Table: table, + Anonymous: true, + Constant: true, + KeyType: nftables.TypeIFName, + } + err := nft.AddSet(excludeInterface, common.Map(r.tunOptions.ExcludeInterface, func(it string) nftables.SetElement { + return nftables.SetElement{ + Key: nftablesIfname(it), + } + })) + if err != nil { + return err + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1}, + &expr.Lookup{ + SourceRegister: 1, + SetID: excludeInterface.ID, + SetName: excludeInterface.Name, + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } else { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1}, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: nftablesIfname(r.tunOptions.ExcludeInterface[0]), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } + } + } else { + if len(r.tunOptions.IncludeUID) > 0 { + includeUID := &nftables.Set{ + Table: table, + Anonymous: true, + Constant: true, + Interval: true, + KeyType: nftables.TypeUID, + } + err := nft.AddSet(includeUID, common.FlatMap(r.tunOptions.IncludeUID, func(it ranges.Range[uint32]) []nftables.SetElement { + return []nftables.SetElement{ + { + Key: binaryutil.BigEndian.PutUint32(it.Start), + }, + { + Key: binaryutil.BigEndian.PutUint32(it.End + 1), + IntervalEnd: true, + }, + } + })) + if err != nil { + return err + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeySKUID, Register: 1}, + &expr.Lookup{ + SourceRegister: 1, + SetID: includeUID.ID, + SetName: includeUID.Name, + Invert: true, + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } + + if len(r.tunOptions.ExcludeUID) > 0 { + excludeUID := &nftables.Set{ + Table: table, + Anonymous: true, + Constant: true, + Interval: true, + KeyType: nftables.TypeUID, + } + err := nft.AddSet(excludeUID, common.FlatMap(r.tunOptions.ExcludeUID, func(it ranges.Range[uint32]) []nftables.SetElement { + return []nftables.SetElement{ + { + Key: binaryutil.BigEndian.PutUint32(it.Start), + }, + { + Key: binaryutil.BigEndian.PutUint32(it.End + 1), + IntervalEnd: true, + }, + } + })) + if err != nil { + return err + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeySKUID, Register: 1}, + &expr.Lookup{ + SourceRegister: 1, + SetID: excludeUID.ID, + SetName: excludeUID.Name, + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } + } + + if len(r.tunOptions.Inet4RouteAddress) > 0 { + inet4RouteAddress, err := nftablesCreateIPSet(nft, table, 0, "", nftables.TableFamilyIPv4, nil, r.tunOptions.Inet4RouteAddress, false, false) + if err != nil { + return err + } + nftablesCreateExcludeDestinationIPSet(nft, table, chain, inet4RouteAddress.ID, inet4RouteAddress.Name, nftables.TableFamilyIPv4, true) + } + + if len(r.tunOptions.Inet6RouteAddress) > 0 { + inet6RouteAddress, err := nftablesCreateIPSet(nft, table, 0, "", nftables.TableFamilyIPv6, nil, r.tunOptions.Inet6RouteAddress, false, false) + if err != nil { + return err + } + nftablesCreateExcludeDestinationIPSet(nft, table, chain, inet6RouteAddress.ID, inet6RouteAddress.Name, nftables.TableFamilyIPv6, true) + } + + if len(r.tunOptions.Inet4RouteExcludeAddress) > 0 { + inet4RouteExcludeAddress, err := nftablesCreateIPSet(nft, table, 0, "", nftables.TableFamilyIPv4, nil, r.tunOptions.Inet4RouteExcludeAddress, false, false) + if err != nil { + return err + } + nftablesCreateExcludeDestinationIPSet(nft, table, chain, inet4RouteExcludeAddress.ID, inet4RouteExcludeAddress.Name, nftables.TableFamilyIPv4, false) + } + + if len(r.tunOptions.Inet6RouteExcludeAddress) > 0 { + inet6RouteExcludeAddress, err := nftablesCreateIPSet(nft, table, 0, "", nftables.TableFamilyIPv6, nil, r.tunOptions.Inet6RouteExcludeAddress, false, false) + if err != nil { + return err + } + nftablesCreateExcludeDestinationIPSet(nft, table, chain, inet6RouteExcludeAddress.ID, inet6RouteExcludeAddress.Name, nftables.TableFamilyIPv6, false) + } + + if !r.tunOptions.EXP_DisableDNSHijack && ((chain.Hooknum == nftables.ChainHookPrerouting && chain.Type == nftables.ChainTypeNAT) || + (r.tunOptions.AutoRedirectMarkMode && chain.Hooknum == nftables.ChainHookOutput && chain.Type == nftables.ChainTypeNAT)) { + if r.enableIPv4 { + err := r.nftablesCreateDNSHijackRulesForFamily(nft, table, chain, nftables.TableFamilyIPv4) + if err != nil { + return err + } + } + if r.enableIPv6 { + err := r.nftablesCreateDNSHijackRulesForFamily(nft, table, chain, nftables.TableFamilyIPv6) + if err != nil { + return err + } + } + } + + if r.tunOptions.AutoRedirectMarkMode && + ((chain.Hooknum == nftables.ChainHookOutput && chain.Type == nftables.ChainTypeRoute) || + (chain.Hooknum == nftables.ChainHookPrerouting && chain.Type == nftables.ChainTypeFilter)) { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyL4PROTO, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpNeq, + Register: 1, + Data: []byte{unix.IPPROTO_UDP}, + }, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } + + if r.enableIPv4 { + nftablesCreateExcludeDestinationIPSet(nft, table, chain, 5, "inet4_local_address_set", nftables.TableFamilyIPv4, false) + } + if r.enableIPv6 { + nftablesCreateExcludeDestinationIPSet(nft, table, chain, 6, "inet6_local_address_set", nftables.TableFamilyIPv6, false) + } + + routeAddressSet := *r.routeAddressSet + routeExcludeAddressSet := *r.routeExcludeAddressSet + + if r.enableIPv4 && len(routeAddressSet) > 0 { + nftablesCreateExcludeDestinationIPSet(nft, table, chain, 1, "inet4_route_address_set", nftables.TableFamilyIPv4, true) + } + + if r.enableIPv6 && len(routeAddressSet) > 0 { + nftablesCreateExcludeDestinationIPSet(nft, table, chain, 2, "inet6_route_address_set", nftables.TableFamilyIPv6, true) + } + + if r.enableIPv4 && len(routeExcludeAddressSet) > 0 { + nftablesCreateExcludeDestinationIPSet(nft, table, chain, 3, "inet4_route_exclude_address_set", nftables.TableFamilyIPv4, false) + } + + if r.enableIPv6 && len(routeExcludeAddressSet) > 0 { + nftablesCreateExcludeDestinationIPSet(nft, table, chain, 4, "inet6_route_exclude_address_set", nftables.TableFamilyIPv6, false) + } + + return nil +} + +func (r *autoRedirect) nftablesCreateMark(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain) { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Immediate{ + Register: 1, + Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectInputMark), + }, + &expr.Meta{ + Key: expr.MetaKeyMARK, + Register: 1, + SourceRegister: true, + }, + &expr.Meta{ + Key: expr.MetaKeyMARK, + Register: 1, + }, // output meta mark set myMark ct mark set meta mark + &expr.Ct{ + Key: expr.CtKeyMARK, + Register: 1, + SourceRegister: true, + }, + &expr.Counter{}, + }, + }) +} + +func (r *autoRedirect) nftablesCreateRedirect( + nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, + exprs ...expr.Any, +) { + if r.enableIPv4 && !r.enableIPv6 { + exprs = append(exprs, + &expr.Meta{ + Key: expr.MetaKeyNFPROTO, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{uint8(nftables.TableFamilyIPv4)}, + }) + } else if !r.enableIPv4 && r.enableIPv6 { + exprs = append(exprs, + &expr.Meta{ + Key: expr.MetaKeyNFPROTO, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{uint8(nftables.TableFamilyIPv6)}, + }) + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: append(exprs, + &expr.Meta{ + Key: expr.MetaKeyL4PROTO, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{unix.IPPROTO_TCP}, + }, + &expr.Counter{}, + &expr.Immediate{ + Register: 1, + Data: binaryutil.BigEndian.PutUint16(r.redirectPort()), + }, + &expr.Redir{ + RegisterProtoMin: 1, + Flags: unix.NF_NAT_RANGE_PROTO_SPECIFIED, + }, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + ), + }) +} + +func (r *autoRedirect) nftablesCreateDNSHijackRulesForFamily( + nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, + family nftables.TableFamily, +) error { + ipProto := &nftables.Set{ + Table: table, + Anonymous: true, + Constant: true, + KeyType: nftables.TypeInetProto, + } + err := nft.AddSet(ipProto, []nftables.SetElement{ + {Key: []byte{unix.IPPROTO_TCP}}, + {Key: []byte{unix.IPPROTO_UDP}}, + }) + if err != nil { + return err + } + dnsServer := common.Find(r.tunOptions.DNSServers, func(it netip.Addr) bool { + return it.Is4() == (family == nftables.TableFamilyIPv4) + }) + if !dnsServer.IsValid() { + if family == nftables.TableFamilyIPv4 { + dnsServer = r.tunOptions.Inet4Address[0].Addr().Next() + } else { + dnsServer = r.tunOptions.Inet6Address[0].Addr().Next() + } + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyNFPROTO, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{uint8(family)}, + }, + &expr.Meta{ + Key: expr.MetaKeyL4PROTO, + Register: 1, + }, + &expr.Lookup{ + SourceRegister: 1, + SetID: ipProto.ID, + SetName: ipProto.Name, + }, + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseTransportHeader, + Offset: 2, + Len: 2, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: binaryutil.BigEndian.PutUint16(53), + }, + &expr.Counter{}, + &expr.Immediate{ + Register: 1, + Data: dnsServer.AsSlice(), + }, + &expr.NAT{ + Type: expr.NATTypeDestNAT, + Family: uint32(family), + RegAddrMin: 1, + }, + }, + }) + return nil +} + +func (r *autoRedirect) nftablesCreateUnreachable( + nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, +) { + if (r.enableIPv4 && r.enableIPv6) || !r.tunOptions.StrictRoute { + return + } + var nfProto nftables.TableFamily + if r.enableIPv4 { + nfProto = nftables.TableFamilyIPv6 + } else { + nfProto = nftables.TableFamilyIPv4 + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyNFPROTO, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{uint8(nfProto)}, + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictDrop, + }, + }, + }) +} diff --git a/redirect_nftables_rules_openwrt.go b/redirect_nftables_rules_openwrt.go new file mode 100644 index 0000000..4792316 --- /dev/null +++ b/redirect_nftables_rules_openwrt.go @@ -0,0 +1,103 @@ +//go:build linux + +package tun + +import ( + "github.com/sagernet/nftables" + "github.com/sagernet/nftables/expr" + + "golang.org/x/exp/slices" +) + +func (r *autoRedirect) configureOpenWRTFirewall4(nft *nftables.Conn, cleanup bool) error { + tableFW4, err := nft.ListTableOfFamily("fw4", nftables.TableFamilyINet) + if err != nil { + return nil + } + if !cleanup { + ruleIif := &nftables.Rule{ + Table: tableFW4, + 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.VerdictAccept, + }, + }, + } + ruleOif := &nftables.Rule{ + Table: tableFW4, + Exprs: []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyOIFNAME, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: nftablesIfname(r.tunOptions.Name), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictAccept, + }, + }, + } + chainForward := &nftables.Chain{ + Name: "forward", + } + ruleIif.Chain = chainForward + ruleOif.Chain = chainForward + nft.InsertRule(ruleOif) + nft.InsertRule(ruleIif) + chainInput := &nftables.Chain{ + Name: "input", + } + ruleIif.Chain = chainInput + ruleOif.Chain = chainInput + nft.InsertRule(ruleOif) + nft.InsertRule(ruleIif) + return nil + } + for _, chainName := range []string{"input", "forward"} { + var rules []*nftables.Rule + rules, err = nft.GetRules(tableFW4, &nftables.Chain{ + Name: chainName, + }) + if err != nil { + return err + } + for _, rule := range rules { + if len(rule.Exprs) != 4 { + continue + } + exprMeta, isMeta := rule.Exprs[0].(*expr.Meta) + if !isMeta { + continue + } + if exprMeta.Key != expr.MetaKeyIIFNAME && exprMeta.Key != expr.MetaKeyOIFNAME { + continue + } + exprCmp, isCmp := rule.Exprs[1].(*expr.Cmp) + if !isCmp { + continue + } + if !slices.Equal(exprCmp.Data, nftablesIfname(r.tunOptions.Name)) { + continue + } + err = nft.DelRule(rule) + if err != nil { + return err + } + } + } + return nil +} diff --git a/tun.go b/tun.go index 541242b..26cf318 100644 --- a/tun.go +++ b/tun.go @@ -41,6 +41,11 @@ type LinuxTUN interface { TXChecksumOffload() bool } +const ( + DefaultIPRoute2TableIndex = 2022 + DefaultIPRoute2RuleIndex = 9000 +) + type Options struct { Name string Inet4Address []netip.Prefix @@ -49,7 +54,11 @@ type Options struct { GSO bool AutoRoute bool DNSServers []netip.Addr + IPRoute2TableIndex int IPRoute2RuleIndex int + AutoRedirectMarkMode bool + AutoRedirectInputMark uint32 + AutoRedirectOutputMark uint32 StrictRoute bool Inet4RouteAddress []netip.Prefix Inet6RouteAddress []netip.Prefix @@ -63,7 +72,6 @@ type Options struct { IncludePackage []string ExcludePackage []string InterfaceMonitor DefaultInterfaceMonitor - TableIndex int FileDescriptor int Logger logger.Logger diff --git a/tun_darwin.go b/tun_darwin.go index 26782f0..d6d8d94 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -242,6 +242,9 @@ func configure(tunFd int, ifIndex int, name string, options Options) error { if options.AutoRoute { var routeRanges []netip.Prefix routeRanges, err = options.BuildAutoRouteRanges(false) + if err != nil { + return err + } for _, routeRange := range routeRanges { if routeRange.Addr().Is4() { err = addRoute(routeRange, options.Inet4Address[0].Addr()) diff --git a/tun_linux.go b/tun_linux.go index 35d303d..d83ed45 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -293,10 +293,10 @@ func (t *NativeTun) configure(tunLink netlink.Link) error { return err } - if t.options.TableIndex == 0 { + if t.options.IPRoute2TableIndex == 0 { for { - t.options.TableIndex = int(rand.Uint32()) - routeList, fErr := netlink.RouteListFiltered(netlink.FAMILY_ALL, &netlink.Route{Table: t.options.TableIndex}, netlink.RT_FILTER_TABLE) + 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 } @@ -354,7 +354,7 @@ func (t *NativeTun) routes(tunLink netlink.Link) ([]netlink.Route, error) { return netlink.Route{ Dst: prefixToIPNet(it), LinkIndex: tunLink.Attrs().Index, - Table: t.options.TableIndex, + Table: t.options.IPRoute2TableIndex, } }), nil } @@ -380,7 +380,7 @@ func (t *NativeTun) rules() []*netlink.Rule { if len(t.options.Inet6Address) > 0 { it := netlink.NewRule() it.Priority = t.nextIndex6() - it.Table = t.options.TableIndex + it.Table = t.options.IPRoute2TableIndex it.Family = unix.AF_INET6 it.OifName = t.options.Name return []*netlink.Rule{it} @@ -408,13 +408,62 @@ func (t *NativeTun) rules() []*netlink.Rule { excludeRanges := t.options.ExcludedRanges() ruleStart := t.options.IPRoute2RuleIndex - if ruleStart == 0 { - ruleStart = 9000 - } priority := ruleStart priority6 := priority - nopPriority := ruleStart + 10 + if t.options.AutoRedirectMarkMode { + if p4 { + it = netlink.NewRule() + it.Priority = priority + it.Mark = t.options.AutoRedirectOutputMark + it.MarkSet = true + it.Goto = priority + 2 + it.Family = unix.AF_INET + rules = append(rules, it) + priority++ + + it = netlink.NewRule() + it.Priority = priority + it.Mark = t.options.AutoRedirectInputMark + it.MarkSet = true + it.Table = t.options.IPRoute2TableIndex + it.Family = unix.AF_INET + rules = append(rules, it) + priority++ + + it = netlink.NewRule() + it.Priority = priority + it.Family = unix.AF_INET + rules = append(rules, it) + } + if p6 { + it = netlink.NewRule() + it.Priority = priority6 + it.Mark = t.options.AutoRedirectOutputMark + it.MarkSet = true + it.Goto = priority6 + 2 + it.Family = unix.AF_INET6 + rules = append(rules, it) + priority6++ + + it = netlink.NewRule() + it.Priority = priority6 + it.Mark = t.options.AutoRedirectInputMark + it.MarkSet = true + it.Table = t.options.IPRoute2TableIndex + it.Family = unix.AF_INET6 + rules = append(rules, it) + priority6++ + + it = netlink.NewRule() + it.Priority = priority6 + it.Family = unix.AF_INET6 + rules = append(rules, it) + } + return rules + } + + nopPriority := ruleStart + 10 for _, excludeRange := range excludeRanges { if p4 { it = netlink.NewRule() @@ -567,7 +616,7 @@ func (t *NativeTun) rules() []*netlink.Rule { it = netlink.NewRule() it.Priority = priority it.Dst = address.Masked() - it.Table = t.options.TableIndex + it.Table = t.options.IPRoute2TableIndex it.Family = unix.AF_INET rules = append(rules, it) } @@ -575,7 +624,7 @@ func (t *NativeTun) rules() []*netlink.Rule { it = netlink.NewRule() it.Priority = priority - it.Table = t.options.TableIndex + it.Table = t.options.IPRoute2TableIndex it.SuppressPrefixlen = 0 it.Family = unix.AF_INET rules = append(rules, it) @@ -584,13 +633,13 @@ func (t *NativeTun) rules() []*netlink.Rule { if p6 { it = netlink.NewRule() it.Priority = priority6 - it.Table = t.options.TableIndex + it.Table = t.options.IPRoute2TableIndex it.SuppressPrefixlen = 0 it.Family = unix.AF_INET6 rules = append(rules, it) priority6++ } - if p4 && !t.options.StrictRoute { + if p4 { it = netlink.NewRule() it.Priority = priority it.Invert = true @@ -599,16 +648,16 @@ func (t *NativeTun) rules() []*netlink.Rule { it.SuppressPrefixlen = 0 it.Family = unix.AF_INET rules = append(rules, it) - + } + if p4 && !t.options.StrictRoute { it = netlink.NewRule() it.Priority = priority it.IPProto = syscall.IPPROTO_ICMP it.Goto = nopPriority it.Family = unix.AF_INET rules = append(rules, it) - priority++ } - if p6 && !t.options.StrictRoute { + if p6 { it = netlink.NewRule() it.Priority = priority6 it.Invert = true @@ -617,7 +666,9 @@ func (t *NativeTun) rules() []*netlink.Rule { it.SuppressPrefixlen = 0 it.Family = unix.AF_INET6 rules = append(rules, it) + } + if p6 && !t.options.StrictRoute { it = netlink.NewRule() it.Priority = priority6 it.IPProto = syscall.IPPROTO_ICMPV6 @@ -640,7 +691,7 @@ func (t *NativeTun) rules() []*netlink.Rule { it.Priority = priority it.Invert = true it.IifName = "lo" - it.Table = t.options.TableIndex + it.Table = t.options.IPRoute2TableIndex it.Family = unix.AF_INET rules = append(rules, it) @@ -648,7 +699,7 @@ func (t *NativeTun) rules() []*netlink.Rule { it.Priority = priority it.IifName = "lo" it.Src = netip.PrefixFrom(netip.IPv4Unspecified(), 32) - it.Table = t.options.TableIndex + it.Table = t.options.IPRoute2TableIndex it.Family = unix.AF_INET rules = append(rules, it) @@ -657,24 +708,13 @@ func (t *NativeTun) rules() []*netlink.Rule { it.Priority = priority it.IifName = "lo" it.Src = address.Masked() - it.Table = t.options.TableIndex + it.Table = t.options.IPRoute2TableIndex it.Family = unix.AF_INET rules = append(rules, it) } priority++ } if p6 { - for _, address := range t.options.Inet6Address { - it = netlink.NewRule() - it.Priority = priority6 - it.IifName = "lo" - it.Src = address.Masked() - it.Table = t.options.TableIndex - it.Family = unix.AF_INET6 - rules = append(rules, it) - } - priority6++ - it = netlink.NewRule() it.Priority = priority6 it.IifName = t.options.Name @@ -697,12 +737,22 @@ func (t *NativeTun) rules() []*netlink.Rule { it.Goto = nopPriority it.Family = unix.AF_INET6 rules = append(rules, it) + priority6++ + for _, address := range t.options.Inet6Address { + it = netlink.NewRule() + it.Priority = priority6 + it.IifName = "lo" + it.Src = address.Masked() + it.Table = t.options.IPRoute2TableIndex + it.Family = unix.AF_INET6 + rules = append(rules, it) + } priority6++ it = netlink.NewRule() it.Priority = priority6 - it.Table = t.options.TableIndex + it.Table = t.options.IPRoute2TableIndex it.Family = unix.AF_INET6 rules = append(rules, it) priority6++ @@ -789,9 +839,6 @@ func (t *NativeTun) unsetRules() error { } for _, rule := range ruleList { ruleStart := t.options.IPRoute2RuleIndex - if ruleStart == 0 { - ruleStart = 9000 - } ruleEnd := ruleStart + 10 if rule.Priority >= ruleStart && rule.Priority <= ruleEnd { ruleToDel := netlink.NewRule()