auto-redirect: Add route address set support for nftables

This commit is contained in:
世界 2024-06-12 02:37:51 +08:00
parent 85fe25a592
commit 85f5f2dd58
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
14 changed files with 1255 additions and 426 deletions

11
go.mod
View file

@ -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
)

22
go.sum
View file

@ -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=

View file

@ -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
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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,
}),
err = r.nftablesCreateExcludeRules(nft, table, chainPreRouting)
if err != nil {
return err
}
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,
})
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: 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{},
},
})
}
err = r.configureOpenWRTFirewall4(nft, false)
if err != nil {
return err
}
err = nft.Flush()
if err != nil {
return err
}
r.networkListener = r.networkMonitor.RegisterCallback(func() {
err = r.nftablesUpdateLocalAddressSet()
if err != nil {
r.logger.Error("update local address set: ", err)
}
})
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,
}),
})
}
for _, name := range r.tunOptions.ExcludeInterface {
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRouting,
Exprs: nftablesRuleIfName(expr.MetaKeyIIFNAME, name, &expr.Verdict{
Kind: expr.VerdictReturn,
}),
})
}
for _, uidRange := range r.tunOptions.ExcludeUID {
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRouting,
Exprs: nftablesRuleMetaUInt32Range(expr.MetaKeySKUID, uidRange, &expr.Verdict{
Kind: expr.VerdictReturn,
}),
})
}
return nil
}
var routeExprs []expr.Any
if len(routeAddress) > 0 {
for _, address := range routeAddress {
routeExprs = append(routeExprs, nftablesRuleDestinationAddress(address)...)
}
}
if !r.tunOptions.EXP_DisableDNSHijack {
dnsServer4 := common.Find(r.tunOptions.DNSServers, func(it netip.Addr) bool {
return it.Is4()
// 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()
})
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)...),
})
}
}
}
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,
},
},
})
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()
}
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)...),
})
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()
}

View file

@ -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,
},
}
}

181
redirect_nftables_exprs.go Normal file
View file

@ -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
}

620
redirect_nftables_rules.go Normal file
View file

@ -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,
},
},
})
}

View file

@ -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
}

10
tun.go
View file

@ -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

View file

@ -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())

View file

@ -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()