Compare commits

...

42 Commits

Author SHA1 Message Date
wwqgtxx
b925011238 Using netipx.IPSet safely 2024-06-19 10:03:15 +08:00
世界
ef83d1643c auto-redirect: Fix android rules 2024-06-17 22:49:09 +08:00
世界
9cf07a2b47 auto-redirect: Cleanup before start 2024-06-17 22:38:34 +08:00
世界
086271ef29 Fix generate empty sets 2024-06-17 22:28:32 +08:00
世界
1048b277ea Skip generate nftables output chain if lo in excluded interface list 2024-06-17 13:29:56 +08:00
世界
85f5f2dd58 auto-redirect: Add route address set support for nftables 2024-06-16 15:37:27 +08:00
世界
85fe25a592 Improve nftables rules 2024-06-11 21:12:08 +08:00
世界
e5f9651d3d Fix linter 2024-06-09 13:41:05 +08:00
世界
21b78edd8b Fix missing address unwrap in redirect server 2024-06-09 10:03:38 +08:00
世界
c4df3641c7 Update workflow 2024-06-09 10:03:37 +08:00
世界
cca25493d3 Drop support for go1.18 and go1.19 2024-06-07 15:51:55 +08:00
世界
65383d3c39 Make DNS hijack optional 2024-06-07 15:49:16 +08:00
世界
67a5b408ef Add auto-redirect 2024-06-07 15:48:43 +08:00
世界
ad763519ff Improve iproute2 rules 2024-06-07 15:47:16 +08:00
世界
9939b78c88 Revert "Update dependencies"
This reverts commit 5d9bd04495.

golang.org/x/net@v0.26.0 requires golang.org/x/crypto@24.0 requires go1.19
2024-06-07 15:02:53 +08:00
世界
4efde6372e Do not submit EventNoRoute repeatedly 2024-06-06 22:29:41 +08:00
世界
1c6d2891ab Fix "Fix darwin routes" 2024-06-06 22:29:38 +08:00
世界
5d9bd04495 Update dependencies 2024-06-06 22:29:34 +08:00
世界
5bf54dc69a Fix netlink 2024-05-23 14:54:56 +08:00
世界
840f3758f9 Prioritize *_route_address in linux auto-route 2024-05-20 22:19:46 +08:00
XYenon
d923e5d10a Fix darwin routes 2024-05-20 19:07:24 +08:00
世界
779d1c7db2 Fix linux auto-route sequence 2024-05-15 22:22:08 +08:00
世界
3f128a4a6a Fix Remove bad suppress_prefixlength iproute2 rule 2024-05-10 17:50:56 +08:00
世界
fb6e917a2c Add StackOptions.IncludeAllNetworks 2024-05-07 20:20:44 +08:00
世界
e272ff0ad3 Remove bad suppress_prefixlength iproute2 rule
This change gives tun priority over DHCP 121 rules
2024-05-07 19:52:59 +08:00
世界
5584917e52 Update gVisor to 20240422.0 2024-05-07 19:52:59 +08:00
世界
e0ddbbb84f Update gVisor to 20240212.0-65-g71212d503 2024-05-07 19:52:59 +08:00
世界
a9895a7d88 Update gVisor to 20240206.0 2024-05-07 19:52:59 +08:00
世界
9380493c39 Fix bad usage for exec 2024-05-07 19:52:44 +08:00
世界
63f6630a0a Fix darwin monitor 2024-05-03 15:33:14 +08:00
世界
d174625727 Update dependencies 2024-04-06 22:39:43 +08:00
世界
520d1bc9bb Remove dependency on comshim 2024-04-06 22:38:48 +08:00
wwqgtxx
fc63ec9388 avoid netlink dos networkUpdateMonitor 2024-04-06 22:23:48 +08:00
世界
cddf60537d Fix timer usage for monitor check update 2024-04-02 22:53:57 +08:00
世界
8bfb64cf04 Fix GSO batch size 2024-03-22 14:52:39 +08:00
世界
689e60891c Fix darwin monitor 2024-03-14 13:37:55 +08:00
世界
6ef2a6cdaa Fix deadlock on network update 2024-02-26 22:52:11 +08:00
世界
8d285f70fb Fix darwin monitor 2024-02-26 13:22:23 +08:00
世界
e8633c66d2 Update .gitignore 2024-02-26 13:22:15 +08:00
世界
951af3ca7a Update dependencies 2024-02-26 13:21:30 +08:00
世界
9b7c2a0a3c Fix unaligned panic on windows 2024-02-10 21:17:30 +08:00
世界
38c945fec5 Remove duplicated rules 2024-02-02 14:29:06 +08:00
32 changed files with 2284 additions and 322 deletions

View File

@@ -11,34 +11,86 @@ on:
- '!.github/workflows/debug.yml'
pull_request:
branches:
- main
- dev
jobs:
build:
name: Debug build
name: Linux Debug build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get latest go version
id: version
run: |
echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
- name: Setup Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: ${{ steps.version.outputs.go_version }}
- name: Add cache to Go proxy
go-version: ^1.22
- name: Build
run: |
version=`git rev-parse HEAD`
mkdir build
pushd build
go mod init build
go get -v github.com/sagernet/sing-tun@$version
popd
make test
build_go120:
name: Linux Debug build (Go 1.20)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: ~1.20
continue-on-error: true
- name: Build
run: |
go build -v .
make test
build_go121:
name: Linux Debug build (Go 1.21)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: ~1.21
continue-on-error: true
- name: Build
run: |
make test
build__windows:
name: Windows Debug build
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: ^1.22
continue-on-error: true
- name: Build
run: |
make test
build_darwin:
name: macOS Debug build
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: ^1.22
continue-on-error: true
- name: Build
run: |
make test

39
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Lint
on:
push:
branches:
- main
- dev
paths-ignore:
- '**.md'
- '.github/**'
- '!.github/workflows/lint.yml'
pull_request:
branches:
- main
- dev
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: ^1.22
- name: Cache go module
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
key: go-${{ hashFiles('**/go.sum') }}
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/.idea/
/vendor/
.DS_Store

View File

@@ -6,6 +6,10 @@ linters:
# - gci
- staticcheck
issues:
exclude-dirs:
- internal
linters-settings:
# gci:
# sections:

22
go.mod
View File

@@ -1,21 +1,27 @@
module github.com/sagernet/sing-tun
go 1.18
go 1.20
require (
github.com/fsnotify/fsnotify v1.7.0
github.com/go-ole/go-ole v1.3.0
github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97
github.com/sagernet/sing v0.3.0
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9
github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f
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/net v0.19.0
golang.org/x/sys v0.16.0
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/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // 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.7.0 // indirect
golang.org/x/time v0.5.0 // indirect
)

43
go.sum
View File

@@ -5,26 +5,37 @@ 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.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=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e h1:DOkjByVeAR56dkszjnMZke4wr7yM/1xHaJF3G9olkEE=
github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e/go.mod h1:fLxq/gtp0qzkaEwywlRRiGmjOK5ES/xUzyIKIFP2Asw=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/sing v0.3.0 h1:PIDVFZHnQAAYRL1UYqNM+0k5s8f/tb1lUW6UDcQiOc8=
github.com/sagernet/sing v0.3.0/go.mod h1:9pfuAH6mZfgnz/YjP6xu5sxx882rfyjpcrTdUpd6w3g=
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg=
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
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-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/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -12,7 +12,6 @@ import (
"github.com/go-ole/go-ole"
"github.com/go-ole/go-ole/oleutil"
"github.com/scjalliance/comshim"
)
// Firewall related API constants.
@@ -250,7 +249,10 @@ func FirewallRuleExistsByName(rules *ole.IDispatch, name string) (bool, error) {
// then:
// dispatch firewallAPIRelease(u, fwp)
func firewallAPIInit() (*ole.IUnknown, *ole.IDispatch, error) {
comshim.Add(1)
err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED)
if err != nil {
return nil, nil, fmt.Errorf("Failed to initialize COM: %s", err)
}
unknown, err := oleutil.CreateObject("HNetCfg.FwPolicy2")
if err != nil {
@@ -270,5 +272,5 @@ func firewallAPIInit() (*ole.IUnknown, *ole.IDispatch, error) {
func firewallAPIRelease(u *ole.IUnknown, fwp *ole.IDispatch) {
fwp.Release()
u.Release()
comshim.Done()
ole.CoUninitialize()
}

View File

@@ -42,7 +42,7 @@ func (m *networkUpdateMonitor) loopUpdate() {
select {
case <-m.done:
return
case <-time.After(time.Second):
default:
}
err := m.loopUpdate0()
if err != nil {
@@ -57,17 +57,31 @@ func (m *networkUpdateMonitor) loopUpdate0() error {
if err != nil {
return err
}
err = unix.SetNonblock(routeSocket, true)
if err != nil {
unix.Close(routeSocket)
return err
}
routeSocketFile := os.NewFile(uintptr(routeSocket), "route")
defer routeSocketFile.Close()
m.routeSocketFile = routeSocketFile
m.loopUpdate1(routeSocketFile)
return nil
}
func (m *networkUpdateMonitor) loopUpdate1(routeSocketFile *os.File) {
defer routeSocketFile.Close()
buffer := buf.NewPacket()
defer buffer.Release()
done := make(chan struct{})
go func() {
select {
case <-m.done:
routeSocketFile.Close()
case <-done:
}
}()
n, err := routeSocketFile.Read(buffer.FreeBytes())
close(done)
if err != nil {
return
}
@@ -92,57 +106,59 @@ func (m *networkUpdateMonitor) Close() error {
}
func (m *defaultInterfaceMonitor) checkUpdate() error {
ribMessage, err := route.FetchRIB(unix.AF_UNSPEC, route.RIBTypeRoute, 0)
if err != nil {
return err
}
routeMessages, err := route.ParseRIB(route.RIBTypeRoute, ribMessage)
if err != nil {
return err
}
var defaultInterface *net.Interface
for _, rawRouteMessage := range routeMessages {
routeMessage := rawRouteMessage.(*route.RouteMessage)
if len(routeMessage.Addrs) <= unix.RTAX_NETMASK {
continue
}
destination, isIPv4Destination := routeMessage.Addrs[unix.RTAX_DST].(*route.Inet4Addr)
if !isIPv4Destination {
continue
}
if destination.IP != netip.IPv4Unspecified().As4() {
continue
}
mask, isIPv4Mask := routeMessage.Addrs[unix.RTAX_NETMASK].(*route.Inet4Addr)
if !isIPv4Mask {
continue
}
ones, _ := net.IPMask(mask.IP[:]).Size()
if ones != 0 {
continue
}
routeInterface, err := net.InterfaceByIndex(routeMessage.Index)
var (
defaultInterface *net.Interface
err error
)
if m.options.UnderNetworkExtension {
defaultInterface, err = getDefaultInterfaceBySocket()
if err != nil {
return err
}
if routeMessage.Flags&unix.RTF_UP == 0 {
continue
} else {
ribMessage, err := route.FetchRIB(unix.AF_UNSPEC, route.RIBTypeRoute, 0)
if err != nil {
return err
}
if routeMessage.Flags&unix.RTF_GATEWAY == 0 {
continue
routeMessages, err := route.ParseRIB(route.RIBTypeRoute, ribMessage)
if err != nil {
return err
}
if routeMessage.Flags&unix.RTF_IFSCOPE != 0 {
// continue
}
defaultInterface = routeInterface
break
}
if defaultInterface == nil {
if m.options.UnderNetworkExtension {
defaultInterface, err = getDefaultInterfaceBySocket()
for _, rawRouteMessage := range routeMessages {
routeMessage := rawRouteMessage.(*route.RouteMessage)
if len(routeMessage.Addrs) <= unix.RTAX_NETMASK {
continue
}
destination, isIPv4Destination := routeMessage.Addrs[unix.RTAX_DST].(*route.Inet4Addr)
if !isIPv4Destination {
continue
}
if destination.IP != netip.IPv4Unspecified().As4() {
continue
}
mask, isIPv4Mask := routeMessage.Addrs[unix.RTAX_NETMASK].(*route.Inet4Addr)
if !isIPv4Mask {
continue
}
ones, _ := net.IPMask(mask.IP[:]).Size()
if ones != 0 {
continue
}
routeInterface, err := net.InterfaceByIndex(routeMessage.Index)
if err != nil {
return err
}
if routeMessage.Flags&unix.RTF_UP == 0 {
continue
}
if routeMessage.Flags&unix.RTF_GATEWAY == 0 {
continue
}
// if routeMessage.Flags&unix.RTF_IFSCOPE != 0 {
//continue
//}
defaultInterface = routeInterface
break
}
}
if defaultInterface == nil {
@@ -170,6 +186,8 @@ func getDefaultInterfaceBySocket() (*net.Interface, error) {
Port: 80,
})
result := make(chan netip.Addr, 1)
done := make(chan struct{})
defer close(done)
go func() {
for {
sockname, sockErr := unix.Getsockname(socketFd)
@@ -182,8 +200,13 @@ func getDefaultInterfaceBySocket() (*net.Interface, error) {
}
addr := netip.AddrFrom4(sockaddr.Addr)
if addr.IsUnspecified() {
time.Sleep(time.Millisecond)
continue
select {
case <-done:
break
default:
time.Sleep(10 * time.Millisecond)
continue
}
}
result <- addr
break
@@ -193,7 +216,7 @@ func getDefaultInterfaceBySocket() (*net.Interface, error) {
select {
case selectedAddr = <-result:
case <-time.After(time.Second):
return nil, os.ErrDeadlineExceeded
return nil, nil
}
interfaces, err := net.Interfaces()
if err != nil {

View File

@@ -4,6 +4,7 @@ import (
"os"
"runtime"
"sync"
"time"
"github.com/sagernet/netlink"
E "github.com/sagernet/sing/common/exceptions"
@@ -67,6 +68,9 @@ func (m *networkUpdateMonitor) Start() error {
}
func (m *networkUpdateMonitor) loopUpdate() {
const minDuration = time.Second
timer := time.NewTimer(minDuration)
defer timer.Stop()
for {
select {
case <-m.close:
@@ -75,6 +79,12 @@ func (m *networkUpdateMonitor) loopUpdate() {
case <-m.linkUpdate:
}
m.emit()
select {
case <-m.close:
return
case <-timer.C:
timer.Reset(minDuration)
}
}
}

View File

@@ -42,7 +42,9 @@ type defaultInterfaceMonitor struct {
defaultInterfaceName string
defaultInterfaceIndex int
androidVPNEnabled bool
noRoute bool
networkMonitor NetworkUpdateMonitor
checkUpdateTimer *time.Timer
element *list.Element[NetworkUpdateCallback]
access sync.Mutex
callbacks list.List[DefaultInterfaceUpdateCallback]
@@ -71,16 +73,30 @@ func (m *defaultInterfaceMonitor) Start() error {
}
func (m *defaultInterfaceMonitor) delayCheckUpdate() {
time.Sleep(time.Second)
if m.checkUpdateTimer == nil {
m.checkUpdateTimer = time.AfterFunc(time.Second, m.postCheckUpdate)
} else {
m.checkUpdateTimer.Reset(time.Second)
}
}
func (m *defaultInterfaceMonitor) postCheckUpdate() {
err := m.updateInterfaces()
if err != nil {
m.logger.Error("update interfaces: ", err)
}
err = m.checkUpdate()
if errors.Is(err, ErrNoRoute) {
m.defaultInterfaceName = ""
m.defaultInterfaceIndex = -1
m.emit(EventNoRoute)
if !m.noRoute {
m.noRoute = true
m.defaultInterfaceName = ""
m.defaultInterfaceIndex = -1
m.emit(EventNoRoute)
}
} else if err != nil {
m.logger.Error("check interface: ", err)
} else {
m.noRoute = false
}
}
@@ -127,9 +143,6 @@ func (m *defaultInterfaceMonitor) DefaultInterfaceName(destination netip.Addr) s
}
}
}
if m.defaultInterfaceIndex == -1 {
m.checkUpdate()
}
return m.defaultInterfaceName
}
@@ -141,9 +154,6 @@ func (m *defaultInterfaceMonitor) DefaultInterfaceIndex(destination netip.Addr)
}
}
}
if m.defaultInterfaceIndex == -1 {
m.checkUpdate()
}
return m.defaultInterfaceIndex
}
@@ -155,9 +165,6 @@ func (m *defaultInterfaceMonitor) DefaultInterface(destination netip.Addr) (stri
}
}
}
if m.defaultInterfaceIndex == -1 {
m.checkUpdate()
}
return m.defaultInterfaceName, m.defaultInterfaceIndex
}

35
redirect.go Normal file
View File

@@ -0,0 +1,35 @@
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
NetworkMonitor NetworkUpdateMonitor
InterfaceFinder control.InterfaceFinder
TableName string
DisableNFTables bool
CustomRedirectPort func() int
RouteAddressSet *[]*netipx.IPSet
RouteExcludeAddressSet *[]*netipx.IPSet
}

269
redirect_iptables.go Normal file
View File

@@ -0,0 +1,269 @@
//go:build linux
package tun
import (
"net/netip"
"os/exec"
"runtime"
"strings"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
func (r *autoRedirect) setupIPTables() error {
if r.enableIPv4 {
err := r.setupIPTablesForFamily(r.iptablesPath)
if err != nil {
return err
}
}
if r.enableIPv6 {
err := r.setupIPTablesForFamily(r.ip6tablesPath)
if err != nil {
return err
}
}
return nil
}
func (r *autoRedirect) setupIPTablesForFamily(iptablesPath string) error {
tableNameInput := r.tableName + "-input"
tableNameForward := r.tableName + "-forward"
tableNameOutput := r.tableName + "-output"
tableNamePreRouteing := r.tableName + "-prerouting"
redirectPort := r.redirectPort()
// OUTPUT
err := r.runShell(iptablesPath, "-t nat -N", tableNameOutput)
if err != nil {
return err
}
err = r.runShell(iptablesPath, "-t nat -A", tableNameOutput,
"-p tcp -o", r.tunOptions.Name,
"-j REDIRECT --to-ports", redirectPort)
if err != nil {
return err
}
err = r.runShell(iptablesPath, "-t nat -I OUTPUT -j", tableNameOutput)
if err != nil {
return err
}
if runtime.GOOS == "android" {
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 {
return err
}
err = r.runShell(iptablesPath, "-A", tableNameForward,
"-i", r.tunOptions.Name, "-j", "ACCEPT")
if err != nil {
return err
}
err = r.runShell(iptablesPath, "-A", tableNameForward,
"-o", r.tunOptions.Name, "-j", "ACCEPT")
if err != nil {
return err
}
err = r.runShell(iptablesPath, "-I FORWARD -j", tableNameForward)
if err != nil {
return err
}
// PREROUTING
err = r.runShell(iptablesPath, "-t nat -N", tableNamePreRouteing)
if err != nil {
return err
}
var (
routeAddress []netip.Prefix
routeExcludeAddress []netip.Prefix
)
if iptablesPath == r.iptablesPath {
routeAddress = r.tunOptions.Inet4RouteAddress
routeExcludeAddress = r.tunOptions.Inet4RouteExcludeAddress
} else {
routeAddress = r.tunOptions.Inet6RouteAddress
routeExcludeAddress = r.tunOptions.Inet6RouteExcludeAddress
}
if len(routeAddress) > 0 && (len(r.tunOptions.IncludeInterface) > 0 || len(r.tunOptions.IncludeUID) > 0) {
return E.New("`*_route_address` is conflict with `include_interface` or `include_uid`")
}
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
"-i", r.tunOptions.Name, "-j RETURN")
if err != nil {
return err
}
for _, address := range routeExcludeAddress {
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
"-d", address.String(), "-j RETURN")
if err != nil {
return err
}
}
for _, name := range r.tunOptions.ExcludeInterface {
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
"-i", name, "-j RETURN")
if err != nil {
return err
}
}
for _, uid := range r.tunOptions.ExcludeUID {
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
"-m owner --uid-owner", uid, "-j RETURN")
if err != nil {
return err
}
}
if !r.tunOptions.EXP_DisableDNSHijack {
dnsServer := common.Find(r.tunOptions.DNSServers, func(it netip.Addr) bool {
return it.Is4() == (iptablesPath == r.iptablesPath)
})
if !dnsServer.IsValid() {
if iptablesPath == r.iptablesPath {
dnsServer = r.tunOptions.Inet4Address[0].Addr().Next()
} else {
dnsServer = r.tunOptions.Inet6Address[0].Addr().Next()
}
}
if len(routeAddress) > 0 {
for _, address := range routeAddress {
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
"-d", address.String(), "-p udp --dport 53 -j DNAT --to", dnsServer)
if err != nil {
return err
}
}
} else if len(r.tunOptions.IncludeInterface) > 0 || len(r.tunOptions.IncludeUID) > 0 {
for _, name := range r.tunOptions.IncludeInterface {
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
"-i", name, "-p udp --dport 53 -j DNAT --to", dnsServer)
if err != nil {
return err
}
}
for _, uidRange := range r.tunOptions.IncludeUID {
for uid := uidRange.Start; uid <= uidRange.End; uid++ {
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
"-m owner --uid-owner", uid, "-p udp --dport 53 -j DNAT --to", dnsServer)
if err != nil {
return err
}
}
}
} else {
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
"-p udp --dport 53 -j DNAT --to", dnsServer)
if err != nil {
return err
}
}
}
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, "-m addrtype --dst-type LOCAL -j RETURN")
if err != nil {
return err
}
if len(routeAddress) > 0 {
for _, address := range routeAddress {
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
"-d", address.String(), "-p tcp -j REDIRECT --to-ports", redirectPort)
if err != nil {
return err
}
}
} else if len(r.tunOptions.IncludeInterface) > 0 || len(r.tunOptions.IncludeUID) > 0 {
for _, name := range r.tunOptions.IncludeInterface {
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
"-i", name, "-p tcp -j REDIRECT --to-ports", redirectPort)
if err != nil {
return err
}
}
for _, uidRange := range r.tunOptions.IncludeUID {
for uid := uidRange.Start; uid <= uidRange.End; uid++ {
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
"-m owner --uid-owner", uid, "-p tcp -j REDIRECT --to-ports", redirectPort)
if err != nil {
return err
}
}
}
} else {
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
"-p tcp -j REDIRECT --to-ports", redirectPort)
if err != nil {
return err
}
}
err = r.runShell(iptablesPath, "-t nat -I PREROUTING -j", tableNamePreRouteing)
if err != nil {
return err
}
return nil
}
func (r *autoRedirect) cleanupIPTables() {
if r.enableIPv4 {
r.cleanupIPTablesForFamily(r.iptablesPath)
}
if r.enableIPv6 {
r.cleanupIPTablesForFamily(r.ip6tablesPath)
}
}
func (r *autoRedirect) cleanupIPTablesForFamily(iptablesPath string) {
tableNameOutput := r.tableName + "-output"
tableNameForward := r.tableName + "-forward"
tableNamePreRouteing := r.tableName + "-prerouting"
_ = r.runShell(iptablesPath, "-t nat -D OUTPUT -j", tableNameOutput)
_ = r.runShell(iptablesPath, "-t nat -F", tableNameOutput)
_ = r.runShell(iptablesPath, "-t nat -X", tableNameOutput)
if runtime.GOOS == "android" {
return
}
_ = r.runShell(iptablesPath, "-D FORWARD -j", tableNameForward)
_ = r.runShell(iptablesPath, "-F", tableNameForward)
_ = r.runShell(iptablesPath, "-X", tableNameForward)
_ = r.runShell(iptablesPath, "-t nat -D PREROUTING -j", tableNamePreRouteing)
_ = r.runShell(iptablesPath, "-t nat -F", tableNamePreRouteing)
_ = r.runShell(iptablesPath, "-t nat -X", tableNamePreRouteing)
}
func (r *autoRedirect) runShell(commands ...any) error {
commandStr := strings.Join(F.MapToString(commands), " ")
var command *exec.Cmd
if r.androidSu {
command = exec.Command(r.suPath, "-c", commandStr)
} else {
commandArray := strings.Split(commandStr, " ")
command = exec.Command(commandArray[0], commandArray[1:]...)
}
combinedOutput, err := command.CombinedOutput()
if err != nil {
return E.Extend(err, F.ToString(commandStr, ": ", string(combinedOutput)))
}
return nil
}

189
redirect_linux.go Normal file
View File

@@ -0,0 +1,189 @@
package tun
import (
"context"
"net/netip"
"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 {
tunOptions *Options
ctx context.Context
handler Handler
logger logger.Logger
tableName string
networkMonitor NetworkUpdateMonitor
networkListener *list.Element[NetworkUpdateCallback]
interfaceFinder control.InterfaceFinder
localAddresses []netip.Prefix
customRedirectPortFunc func() int
customRedirectPort int
redirectServer *redirectServer
enableIPv4 bool
enableIPv6 bool
iptablesPath string
ip6tablesPath string
useNFTables bool
androidSu bool
suPath string
routeAddressSet *[]*netipx.IPSet
routeExcludeAddressSet *[]*netipx.IPSet
}
func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {
r := &autoRedirect{
tunOptions: options.TunOptions,
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" {
r.enableIPv4 = true
r.iptablesPath = "/system/bin/iptables"
userId := os.Getuid()
if userId != 0 {
r.androidSu = true
for _, suPath := range []string{
"su",
"/system/bin/su",
} {
r.suPath, err = exec.LookPath(suPath)
if err == nil {
break
}
}
if err != nil {
return nil, E.Extend(E.Cause(err, "root permission is required for auto redirect"), os.Getenv("PATH"))
}
}
} else {
if r.useNFTables {
err = r.initializeNFTables()
if err != nil && err != os.ErrInvalid {
r.logger.Debug("device has no nftables support: ", err)
}
}
if len(r.tunOptions.Inet4Address) > 0 {
r.enableIPv4 = true
if !r.useNFTables {
r.iptablesPath, err = exec.LookPath("iptables")
if err != nil {
return nil, E.Cause(err, "iptables is required")
}
}
}
if len(r.tunOptions.Inet6Address) > 0 {
r.enableIPv6 = true
if !r.useNFTables {
r.ip6tablesPath, err = exec.LookPath("ip6tables")
if err != nil {
if !r.enableIPv4 {
return nil, E.Cause(err, "ip6tables is required")
} else {
r.enableIPv6 = false
r.logger.Error("device has no ip6tables nat support: ", err)
}
}
}
}
}
return r, nil
}
func (r *autoRedirect) Start() error {
if r.customRedirectPortFunc != nil {
r.customRedirectPort = r.customRedirectPortFunc()
}
if r.customRedirectPort == 0 {
var listenAddr netip.Addr
if runtime.GOOS == "android" {
listenAddr = netip.AddrFrom4([4]byte{127, 0, 0, 1})
} else if r.enableIPv6 {
listenAddr = netip.IPv6Unspecified()
} else {
listenAddr = netip.IPv4Unspecified()
}
server := newRedirectServer(r.ctx, r.handler, r.logger, listenAddr)
err := server.Start()
if err != nil {
return E.Cause(err, "start redirect server")
}
r.redirectServer = server
}
startAt := time.Now()
var err error
if r.useNFTables {
r.cleanupNFTables()
err = r.setupNFTables()
} else {
r.cleanupIPTables()
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 {
if r.useNFTables {
r.cleanupNFTables()
} else {
r.cleanupIPTables()
}
return common.Close(
common.PtrOrNil(r.redirectServer),
)
}
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 {
return err
}
defer nft.CloseLasting()
_, err = nft.ListTablesOfFamily(nftables.TableFamilyIPv4)
if err != nil {
return err
}
r.useNFTables = true
return nil
}
func (r *autoRedirect) redirectPort() uint16 {
if r.customRedirectPort > 0 {
return uint16(r.customRedirectPort)
}
return M.AddrPortFromNet(r.redirectServer.listener.Addr()).Port()
}

226
redirect_nftables.go Normal file
View File

@@ -0,0 +1,226 @@
//go:build linux
package tun
import (
"net/netip"
"github.com/sagernet/nftables"
"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"
)
func (r *autoRedirect) setupNFTables() error {
nft, err := nftables.New()
if err != nil {
return err
}
defer nft.CloseLasting()
table := nft.AddTable(&nftables.Table{
Name: r.tableName,
Family: nftables.TableFamilyINet,
})
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
}
skipOutput := len(r.tunOptions.IncludeInterface) > 0 && !common.Contains(r.tunOptions.IncludeInterface, "lo") || common.Contains(r.tunOptions.ExcludeInterface, "lo")
if !skipOutput {
chainOutput := nft.AddChain(&nftables.Chain{
Name: "output",
Table: table,
Hooknum: nftables.ChainHookOutput,
Priority: nftables.ChainPriorityMangle,
Type: nftables.ChainTypeNAT,
})
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",
Table: table,
Hooknum: nftables.ChainHookPrerouting,
Priority: nftables.ChainPriorityMangle,
Type: nftables.ChainTypeNAT,
})
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)
}
})
return nil
}
// 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() {
if r.networkListener != nil {
r.networkMonitor.UnregisterCallback(r.networkListener)
}
nft, err := nftables.New()
if err != nil {
return
}
nft.DelTable(&nftables.Table{
Name: r.tableName,
Family: nftables.TableFamilyINet,
})
common.Must(r.configureOpenWRTFirewall4(nft, true))
_ = nft.Flush()
_ = nft.CloseLasting()
}

172
redirect_nftables_exprs.go Normal file
View File

@@ -0,0 +1,172 @@
//go:build linux
package tun
import (
"net/netip"
"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) {
var builder netipx.IPSetBuilder
for _, prefix := range prefixList {
builder.AddPrefix(prefix)
}
for _, set := range setList {
builder.AddSet(set)
}
ipSet, err := builder.IPSet()
if err != nil {
return nil, err
}
ipRanges := ipSet.Ranges()
setElements := make([]nftables.SetElement, 0, len(ipRanges))
for _, rr := range ipRanges {
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.To().AsSlice(),
})
setElements = append(setElements, nftables.SetElement{
Key: endAddr.AsSlice(),
IntervalEnd: true,
})
}
if len(prefixList) == 0 && appendDefault {
if family == nftables.TableFamilyIPv4 {
setElements = append(setElements, nftables.SetElement{
Key: netip.IPv4Unspecified().AsSlice(),
}, nftables.SetElement{
Key: netip.IPv4Unspecified().AsSlice(),
IntervalEnd: true,
})
} else {
setElements = append(setElements, nftables.SetElement{
Key: netip.IPv6Unspecified().AsSlice(),
}, nftables.SetElement{
Key: netip.IPv6Unspecified().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
}

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
}

88
redirect_server.go Normal file
View File

@@ -0,0 +1,88 @@
//go:build linux
package tun
import (
"context"
"errors"
"net"
"net/netip"
"time"
"github.com/sagernet/sing/common/atomic"
"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"
)
const ProtocolRedirect = "redirect"
type redirectServer struct {
ctx context.Context
handler Handler
logger logger.Logger
listenAddr netip.Addr
listener *net.TCPListener
inShutdown atomic.Bool
}
func newRedirectServer(ctx context.Context, handler Handler, logger logger.Logger, listenAddr netip.Addr) *redirectServer {
return &redirectServer{
ctx: ctx,
handler: handler,
logger: logger,
listenAddr: listenAddr,
}
}
func (s *redirectServer) Start() error {
var listenConfig net.ListenConfig
// listenConfig.KeepAlive = C.TCPKeepAliveInitial
listenConfig.KeepAlive = 10 * time.Minute
listener, err := listenConfig.Listen(s.ctx, M.NetworkFromNetAddr("tcp", s.listenAddr), M.SocksaddrFrom(s.listenAddr, 0).String())
if err != nil {
return err
}
s.listener = listener.(*net.TCPListener)
go s.loopIn()
return nil
}
func (s *redirectServer) Close() error {
s.inShutdown.Store(true)
return s.listener.Close()
}
func (s *redirectServer) loopIn() {
for {
conn, err := s.listener.AcceptTCP()
if err != nil {
var netError net.Error
//goland:noinspection GoDeprecation
//nolint:staticcheck
if errors.As(err, &netError) && netError.Temporary() {
s.logger.Error(err)
continue
}
if s.inShutdown.Load() && E.IsClosed(err) {
return
}
s.listener.Close()
s.logger.Error("serve error: ", err)
continue
}
var metadata M.Metadata
metadata.Protocol = ProtocolRedirect
metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap()
destination, err := control.GetOriginalDestination(conn)
if err != nil {
_ = conn.SetLinger(0)
_ = conn.Close()
s.logger.Error("process connection from ", metadata.Source, ": invalid connection: ", err)
continue
}
metadata.Destination = M.SocksaddrFromNetIP(destination).Unwrap()
go s.handler.NewConnection(s.ctx, conn, metadata)
}
}

11
redirect_stub.go Normal file
View File

@@ -0,0 +1,11 @@
//go:build !linux
package tun
import (
"os"
)
func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {
return nil, os.ErrInvalid
}

View File

@@ -25,6 +25,7 @@ type StackOptions struct {
Handler Handler
Logger logger.Logger
ForwarderBindInterface bool
IncludeAllNetworks bool
InterfaceFinder control.InterfaceFinder
}
@@ -34,7 +35,9 @@ func NewStack(
) (Stack, error) {
switch stack {
case "":
if WithGVisor && !options.TunOptions.GSO {
if options.IncludeAllNetworks {
return NewGVisor(options)
} else if WithGVisor && !options.TunOptions.GSO {
return NewMixed(options)
} else {
return NewSystem(options)
@@ -42,8 +45,14 @@ func NewStack(
case "gvisor":
return NewGVisor(options)
case "mixed":
if options.IncludeAllNetworks {
return nil, ErrIncludeAllNetworks
}
return NewMixed(options)
case "system":
if options.IncludeAllNetworks {
return nil, ErrIncludeAllNetworks
}
return NewSystem(options)
default:
return nil, E.New("unknown stack: ", stack)

View File

@@ -122,7 +122,7 @@ func (t *GVisor) Start() error {
if err != nil {
return
}
udpConn := gonet.NewUDPConn(ipStack, &wq, endpoint)
udpConn := gonet.NewUDPConn(&wq, endpoint)
lAddr := udpConn.RemoteAddr()
rAddr := udpConn.LocalAddr()
if lAddr == nil || rAddr == nil {

View File

@@ -32,7 +32,7 @@ type networkDispatcherFilter struct {
writer N.VectorisedWriter
}
func (w *networkDispatcherFilter) DeliverNetworkPacket(protocol tcpip.NetworkProtocolNumber, pkt stack.PacketBufferPtr) {
func (w *networkDispatcherFilter) DeliverNetworkPacket(protocol tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) {
var network header.Network
if protocol == header.IPv4ProtocolNumber {
if headerPackets, loaded := pkt.Data().PullUp(header.IPv4MinimumSize); loaded {

View File

@@ -42,7 +42,7 @@ func NewUDPForwarder(ctx context.Context, stack *stack.Stack, handler Handler, u
}
}
func (f *UDPForwarder) HandlePacket(id stack.TransportEndpointID, pkt stack.PacketBufferPtr) bool {
func (f *UDPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
var upstreamMetadata M.Metadata
upstreamMetadata.Source = M.SocksaddrFrom(AddrFromAddress(id.RemoteAddress), id.RemotePort)
upstreamMetadata.Destination = M.SocksaddrFrom(AddrFromAddress(id.LocalAddress), id.LocalPort)
@@ -138,7 +138,6 @@ func (w *UDPBackWriter) WritePacket(packetBuffer *buf.Buffer, destination M.Sock
TTL: route.DefaultTTL(),
TOS: 0,
}, packet)
if err != nil {
route.Stats().UDP.PacketSendErrors.Increment()
return wrapStackError(err)
@@ -174,7 +173,7 @@ func (c *gUDPConn) Close() error {
return c.UDPConn.Close()
}
func gWriteUnreachable(gStack *stack.Stack, packet stack.PacketBufferPtr, err error) (retErr error) {
func gWriteUnreachable(gStack *stack.Stack, packet *stack.PacketBuffer, err error) (retErr error) {
if errors.Is(err, syscall.ENETUNREACH) {
if packet.NetworkProtocolNumber == header.IPv4ProtocolNumber {
return gWriteUnreachable4(gStack, packet, stack.RejectIPv4WithICMPNetUnreachable)
@@ -197,7 +196,7 @@ func gWriteUnreachable(gStack *stack.Stack, packet stack.PacketBufferPtr, err er
return nil
}
func gWriteUnreachable4(gStack *stack.Stack, packet stack.PacketBufferPtr, icmpCode stack.RejectIPv4WithICMPType) error {
func gWriteUnreachable4(gStack *stack.Stack, packet *stack.PacketBuffer, icmpCode stack.RejectIPv4WithICMPType) error {
err := gStack.NetworkProtocolInstance(header.IPv4ProtocolNumber).(stack.RejectIPv4WithHandler).SendRejectionError(packet, icmpCode, true)
if err != nil {
return wrapStackError(err)
@@ -205,7 +204,7 @@ func gWriteUnreachable4(gStack *stack.Stack, packet stack.PacketBufferPtr, icmpC
return nil
}
func gWriteUnreachable6(gStack *stack.Stack, packet stack.PacketBufferPtr, icmpCode stack.RejectIPv6WithICMPType) error {
func gWriteUnreachable6(gStack *stack.Stack, packet *stack.PacketBuffer, icmpCode stack.RejectIPv6WithICMPType) error {
err := gStack.NetworkProtocolInstance(header.IPv6ProtocolNumber).(stack.RejectIPv6WithHandler).SendRejectionError(packet, icmpCode, true)
if err != nil {
return wrapStackError(err)

View File

@@ -56,7 +56,7 @@ func (m *Mixed) Start() error {
if err != nil {
return
}
udpConn := gonet.NewUDPConn(ipStack, &wq, endpoint)
udpConn := gonet.NewUDPConn(&wq, endpoint)
lAddr := udpConn.RemoteAddr()
rAddr := udpConn.LocalAddr()
if lAddr == nil || rAddr == nil {

View File

@@ -18,6 +18,8 @@ import (
"github.com/sagernet/sing/common/udpnat"
)
var ErrIncludeAllNetworks = E.New("`system` and `mixed` stack are not available when `includeAllNetworks` is enabled. See https://github.com/SagerNet/sing-tun/issues/25")
type System struct {
ctx context.Context
tun Tun

17
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
@@ -48,6 +53,12 @@ type Options struct {
MTU uint32
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
@@ -61,12 +72,14 @@ type Options struct {
IncludePackage []string
ExcludePackage []string
InterfaceMonitor DefaultInterfaceMonitor
TableIndex int
FileDescriptor int
Logger logger.Logger
// No work for TCP, do not use.
_TXChecksumOffload bool
// For library usages.
EXP_DisableDNSHijack bool
}
func CalculateInterfaceName(name string) (tunName string) {
@@ -85,7 +98,7 @@ func CalculateInterfaceName(name string) (tunName string) {
for _, netInterface := range interfaces {
if strings.HasPrefix(netInterface.Name, tunName) {
index, parseErr := strconv.ParseInt(netInterface.Name[len(tunName):], 10, 16)
if parseErr == nil {
if parseErr == nil && int(index) >= tunIndex {
tunIndex = int(index) + 1
}
}

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

@@ -102,10 +102,10 @@ func (e *DarwinEndpoint) ARPHardwareType() header.ARPHardwareType {
return header.ARPHardwareNone
}
func (e *DarwinEndpoint) AddHeader(buffer stack.PacketBufferPtr) {
func (e *DarwinEndpoint) AddHeader(buffer *stack.PacketBuffer) {
}
func (e *DarwinEndpoint) ParseHeader(ptr stack.PacketBufferPtr) bool {
func (e *DarwinEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool {
return true
}

View File

@@ -136,11 +136,13 @@ func (t *NativeTun) BatchSize() int {
if !t.gsoEnabled {
return 1
}
/* // Not works on some devices: https://github.com/SagerNet/sing-box/issues/1605
batchSize := int(gsoMaxSize/t.options.MTU) * 2
if batchSize > idealBatchSize {
batchSize = idealBatchSize
}
return batchSize
return batchSize*/
return idealBatchSize
}
func (t *NativeTun) BatchRead(buffers [][]byte, offset int, readN []int) (n int, err error) {
@@ -291,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
}
@@ -352,16 +354,11 @@ 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
}
const (
ruleStart = 9000
ruleEnd = ruleStart + 10
)
func (t *NativeTun) nextIndex6() int {
ruleList, err := netlink.RuleList(netlink.FAMILY_V6)
if err != nil {
@@ -383,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}
@@ -409,10 +406,64 @@ func (t *NativeTun) rules() []*netlink.Rule {
var it *netlink.Rule
excludeRanges := t.options.ExcludedRanges()
ruleStart := t.options.IPRoute2RuleIndex
priority := ruleStart
priority6 := priority
nopPriority := ruleEnd
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()
@@ -450,14 +501,6 @@ func (t *NativeTun) rules() []*netlink.Rule {
it.Family = unix.AF_INET
rules = append(rules, it)
priority++
it = netlink.NewRule()
it.Priority = priority
it.OifName = includeInterface
it.Goto = matchPriority
it.Family = unix.AF_INET
rules = append(rules, it)
priority++
}
if p6 {
it = netlink.NewRule()
@@ -467,14 +510,6 @@ func (t *NativeTun) rules() []*netlink.Rule {
it.Family = unix.AF_INET6
rules = append(rules, it)
priority6++
it = netlink.NewRule()
it.Priority = priority6
it.OifName = includeInterface
it.Goto = matchPriority
it.Family = unix.AF_INET6
rules = append(rules, it)
priority6++
}
}
if p4 {
@@ -515,14 +550,6 @@ func (t *NativeTun) rules() []*netlink.Rule {
it.Family = unix.AF_INET
rules = append(rules, it)
priority++
it = netlink.NewRule()
it.Priority = priority
it.OifName = excludeInterface
it.Goto = nopPriority
it.Family = unix.AF_INET
rules = append(rules, it)
priority++
}
if p6 {
it = netlink.NewRule()
@@ -532,21 +559,13 @@ func (t *NativeTun) rules() []*netlink.Rule {
it.Family = unix.AF_INET6
rules = append(rules, it)
priority6++
it = netlink.NewRule()
it.Priority = priority6
it.OifName = excludeInterface
it.Goto = nopPriority
it.Family = unix.AF_INET6
rules = append(rules, it)
priority6++
}
}
}
if runtime.GOOS == "android" && t.options.InterfaceMonitor.AndroidVPNEnabled() {
const protectedFromVPN = 0x20000
if p4 || t.options.StrictRoute {
if p4 {
it = netlink.NewRule()
if t.options.InterfaceMonitor.OverrideAndroidVPN() {
it.Mark = protectedFromVPN
@@ -558,7 +577,7 @@ func (t *NativeTun) rules() []*netlink.Rule {
rules = append(rules, it)
priority++
}
if p6 || t.options.StrictRoute {
if p6 {
it = netlink.NewRule()
if t.options.InterfaceMonitor.OverrideAndroidVPN() {
it.Mark = protectedFromVPN
@@ -597,30 +616,59 @@ 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)
}
priority++
}
/*if p6 {
it = netlink.NewRule()
it.Priority = priority
it.Dst = t.options.Inet6Address.Masked()
it.Table = tunTableIndex
it.Table = t.options.IPRoute2TableIndex
it.SuppressPrefixlen = 0
it.Family = unix.AF_INET
rules = append(rules, it)
priority++
}
if p6 {
it = netlink.NewRule()
it.Priority = priority6
it.Table = t.options.IPRoute2TableIndex
it.SuppressPrefixlen = 0
it.Family = unix.AF_INET6
rules = append(rules, it)
}*/
priority6++
}
if p4 {
it = netlink.NewRule()
it.Priority = priority
it.Invert = true
it.Dport = netlink.NewRulePortRange(53, 53)
it.Table = unix.RT_TABLE_MAIN
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 {
it = netlink.NewRule()
it.Priority = priority6
it.Invert = true
it.Dport = netlink.NewRulePortRange(53, 53)
it.Table = unix.RT_TABLE_MAIN
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
@@ -629,99 +677,82 @@ func (t *NativeTun) rules() []*netlink.Rule {
rules = append(rules, it)
priority6++
}
if p4 {
it = netlink.NewRule()
it.Priority = priority
it.Invert = true
it.Dport = netlink.NewRulePortRange(53, 53)
it.Table = unix.RT_TABLE_MAIN
it.SuppressPrefixlen = 0
it.Family = unix.AF_INET
rules = append(rules, it)
}
if p6 {
it = netlink.NewRule()
it.Priority = priority6
it.Invert = true
it.Dport = netlink.NewRulePortRange(53, 53)
it.Table = unix.RT_TABLE_MAIN
it.SuppressPrefixlen = 0
it.Family = unix.AF_INET6
rules = append(rules, it)
}
}
if p4 {
if t.options.StrictRoute {
it = netlink.NewRule()
it.Priority = priority
it.Table = t.options.TableIndex
it.Family = unix.AF_INET
rules = append(rules, it)
} else {
it = netlink.NewRule()
it.Priority = priority
it.Invert = true
it.IifName = "lo"
it.Table = t.options.TableIndex
it.Family = unix.AF_INET
rules = append(rules, it)
it = netlink.NewRule()
it.Priority = priority
it.IifName = t.options.Name
it.Goto = nopPriority
it.Family = unix.AF_INET
rules = append(rules, it)
priority++
it = netlink.NewRule()
it.Priority = priority
it.Invert = true
it.IifName = "lo"
it.Table = t.options.IPRoute2TableIndex
it.Family = unix.AF_INET
rules = append(rules, it)
it = netlink.NewRule()
it.Priority = priority
it.IifName = "lo"
it.Src = netip.PrefixFrom(netip.IPv4Unspecified(), 32)
it.Table = t.options.IPRoute2TableIndex
it.Family = unix.AF_INET
rules = append(rules, it)
for _, address := range t.options.Inet4Address {
it = netlink.NewRule()
it.Priority = priority
it.IifName = "lo"
it.Src = netip.PrefixFrom(netip.IPv4Unspecified(), 32)
it.Table = t.options.TableIndex
it.Src = address.Masked()
it.Table = t.options.IPRoute2TableIndex
it.Family = unix.AF_INET
rules = append(rules, it)
for _, address := range t.options.Inet4Address {
it = netlink.NewRule()
it.Priority = priority
it.IifName = "lo"
it.Src = address.Masked()
it.Table = t.options.TableIndex
it.Family = unix.AF_INET
rules = append(rules, it)
}
}
priority++
}
if p6 {
if !t.options.StrictRoute {
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 = "lo"
it.Src = netip.PrefixFrom(netip.IPv6Unspecified(), 1)
it.Goto = nopPriority
it.Family = unix.AF_INET6
rules = append(rules, it)
it = netlink.NewRule()
it.Priority = priority6
it.IifName = "lo"
it.Src = netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 128}), 1)
it.Goto = nopPriority
it.Family = unix.AF_INET6
rules = append(rules, it)
priority6++
}
it = netlink.NewRule()
it.Priority = priority6
it.IifName = t.options.Name
it.Goto = nopPriority
it.Family = unix.AF_INET6
rules = append(rules, it)
it = netlink.NewRule()
it.Priority = priority6
it.Table = t.options.TableIndex
it.IifName = "lo"
it.Src = netip.PrefixFrom(netip.IPv6Unspecified(), 1)
it.Goto = nopPriority
it.Family = unix.AF_INET6
rules = append(rules, it)
it = netlink.NewRule()
it.Priority = priority6
it.IifName = "lo"
it.Src = netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 128}), 1)
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.IPRoute2TableIndex
it.Family = unix.AF_INET6
rules = append(rules, it)
priority6++
@@ -807,6 +838,8 @@ func (t *NativeTun) unsetRules() error {
return err
}
for _, rule := range ruleList {
ruleStart := t.options.IPRoute2RuleIndex
ruleEnd := ruleStart + 10
if rule.Priority >= ruleStart && rule.Priority <= ruleEnd {
ruleToDel := netlink.NewRule()
ruleToDel.Family = rule.Family
@@ -839,20 +872,28 @@ func (t *NativeTun) routeUpdate(event int) {
}
func (t *NativeTun) setSearchDomainForSystemdResolved() {
if t.options.EXP_DisableDNSHijack {
return
}
ctlPath, err := exec.LookPath("resolvectl")
if err != nil {
return
}
var dnsServer []netip.Addr
if len(t.options.Inet4Address) > 0 {
dnsServer = append(dnsServer, t.options.Inet4Address[0].Addr().Next())
dnsServer := t.options.DNSServers
if len(dnsServer) == 0 {
if len(t.options.Inet4Address) > 0 {
dnsServer = append(dnsServer, t.options.Inet4Address[0].Addr().Next())
}
if len(t.options.Inet6Address) > 0 {
dnsServer = append(dnsServer, t.options.Inet6Address[0].Addr().Next())
}
}
if len(t.options.Inet6Address) > 0 {
dnsServer = append(dnsServer, t.options.Inet6Address[0].Addr().Next())
}
shell.Exec(ctlPath, "domain", t.options.Name, "~.").Start()
if t.options.AutoRoute {
shell.Exec(ctlPath, "default-route", t.options.Name, "true").Start()
shell.Exec(ctlPath, append([]string{"dns", t.options.Name}, common.Map(dnsServer, netip.Addr.String)...)...).Start()
if len(dnsServer) == 0 {
return
}
go func() {
_ = shell.Exec(ctlPath, "domain", t.options.Name, "~.").Run()
_ = shell.Exec(ctlPath, "default-route", t.options.Name, "true").Run()
_ = shell.Exec(ctlPath, append([]string{"dns", t.options.Name}, common.Map(dnsServer, netip.Addr.String)...)...).Run()
}()
}

View File

@@ -109,6 +109,13 @@ func (o *Options) BuildAutoRouteRanges(underNetworkExtension bool) ([]netip.Pref
var inet4Ranges []netip.Prefix
if len(o.Inet4RouteAddress) > 0 {
inet4Ranges = o.Inet4RouteAddress
if runtime.GOOS == "darwin" {
for _, address := range o.Inet4Address {
if address.Bits() < 32 {
inet4Ranges = append(inet4Ranges, address.Masked())
}
}
}
} else if autoRouteUseSubRanges && !underNetworkExtension {
inet4Ranges = []netip.Prefix{
netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 1}), 8),
@@ -144,6 +151,13 @@ func (o *Options) BuildAutoRouteRanges(underNetworkExtension bool) ([]netip.Pref
var inet6Ranges []netip.Prefix
if len(o.Inet6RouteAddress) > 0 {
inet6Ranges = o.Inet6RouteAddress
if runtime.GOOS == "darwin" {
for _, address := range o.Inet6Address {
if address.Bits() < 32 {
inet6Ranges = append(inet6Ranges, address.Masked())
}
}
}
} else if autoRouteUseSubRanges && !underNetworkExtension {
inet6Ranges = []netip.Prefix{
netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 1}), 8),

View File

@@ -9,7 +9,6 @@ import (
"net/netip"
"os"
"sync"
"sync/atomic"
"time"
"unsafe"
@@ -17,6 +16,7 @@ import (
"github.com/sagernet/sing-tun/internal/winsys"
"github.com/sagernet/sing-tun/internal/wintun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/windnsapi"
@@ -34,7 +34,7 @@ type NativeTun struct {
rate rateJuggler
running sync.WaitGroup
closeOnce sync.Once
close int32
close atomic.Int32
fwpmSession uintptr
}
@@ -72,9 +72,15 @@ func (t *NativeTun) configure() error {
if err != nil {
return E.Cause(err, "set ipv4 address")
}
err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET), []netip.Addr{t.options.Inet4Address[0].Addr().Next()}, nil)
if err != nil {
return E.Cause(err, "set ipv4 dns")
if !t.options.EXP_DisableDNSHijack {
dnsServers := common.Filter(t.options.DNSServers, netip.Addr.Is4)
if len(dnsServers) == 0 {
dnsServers = []netip.Addr{t.options.Inet4Address[0].Addr().Next()}
}
err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET), dnsServers, nil)
if err != nil {
return E.Cause(err, "set ipv4 dns")
}
}
}
if len(t.options.Inet6Address) > 0 {
@@ -82,9 +88,15 @@ func (t *NativeTun) configure() error {
if err != nil {
return E.Cause(err, "set ipv6 address")
}
err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET6), []netip.Addr{t.options.Inet6Address[0].Addr().Next()}, nil)
if err != nil {
return E.Cause(err, "set ipv6 dns")
if !t.options.EXP_DisableDNSHijack {
dnsServers := common.Filter(t.options.DNSServers, netip.Addr.Is6)
if len(dnsServers) == 0 {
dnsServers = []netip.Addr{t.options.Inet6Address[0].Addr().Next()}
}
err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET6), dnsServers, nil)
if err != nil {
return E.Cause(err, "set ipv6 dns")
}
}
}
if len(t.options.Inet4Address) > 0 || len(t.options.Inet6Address) > 0 {
@@ -102,6 +114,9 @@ func (t *NativeTun) configure() error {
err = luid.AddRoute(routeRange, netip.IPv6Unspecified(), 0)
}
}
if err != nil {
return err
}
err = windnsapi.FlushResolverCache()
if err != nil {
return err
@@ -284,42 +299,40 @@ func (t *NativeTun) configure() error {
}
}
blockDNSCondition := make([]winsys.FWPM_FILTER_CONDITION0, 2)
blockDNSCondition[0].FieldKey = winsys.FWPM_CONDITION_IP_PROTOCOL
blockDNSCondition[0].MatchType = winsys.FWP_MATCH_EQUAL
blockDNSCondition[0].ConditionValue.Type = winsys.FWP_UINT8
blockDNSCondition[0].ConditionValue.Value = uintptr(uint8(winsys.IPPROTO_UDP))
blockDNSCondition[1].FieldKey = winsys.FWPM_CONDITION_IP_REMOTE_PORT
blockDNSCondition[1].MatchType = winsys.FWP_MATCH_EQUAL
blockDNSCondition[1].ConditionValue.Type = winsys.FWP_UINT16
blockDNSCondition[1].ConditionValue.Value = uintptr(uint16(53))
if !t.options.EXP_DisableDNSHijack {
blockDNSCondition := make([]winsys.FWPM_FILTER_CONDITION0, 1)
blockDNSCondition[0].FieldKey = winsys.FWPM_CONDITION_IP_REMOTE_PORT
blockDNSCondition[0].MatchType = winsys.FWP_MATCH_EQUAL
blockDNSCondition[0].ConditionValue.Type = winsys.FWP_UINT16
blockDNSCondition[0].ConditionValue.Value = uintptr(uint16(53))
blockDNSFilter4 := winsys.FWPM_FILTER0{}
blockDNSFilter4.FilterCondition = &blockDNSCondition[0]
blockDNSFilter4.NumFilterConditions = 2
blockDNSFilter4.DisplayData = winsys.CreateDisplayData(TunnelType, "block ipv4 dns")
blockDNSFilter4.SubLayerKey = subLayerKey
blockDNSFilter4.LayerKey = winsys.FWPM_LAYER_ALE_AUTH_CONNECT_V4
blockDNSFilter4.Action.Type = winsys.FWP_ACTION_BLOCK
blockDNSFilter4.Weight.Type = winsys.FWP_UINT8
blockDNSFilter4.Weight.Value = uintptr(10)
err = winsys.FwpmFilterAdd0(engine, &blockDNSFilter4, 0, &filterId)
if err != nil {
return os.NewSyscallError("FwpmFilterAdd0", err)
}
blockDNSFilter4 := winsys.FWPM_FILTER0{}
blockDNSFilter4.FilterCondition = &blockDNSCondition[0]
blockDNSFilter4.NumFilterConditions = 1
blockDNSFilter4.DisplayData = winsys.CreateDisplayData(TunnelType, "block ipv4 dns")
blockDNSFilter4.SubLayerKey = subLayerKey
blockDNSFilter4.LayerKey = winsys.FWPM_LAYER_ALE_AUTH_CONNECT_V4
blockDNSFilter4.Action.Type = winsys.FWP_ACTION_BLOCK
blockDNSFilter4.Weight.Type = winsys.FWP_UINT8
blockDNSFilter4.Weight.Value = uintptr(10)
err = winsys.FwpmFilterAdd0(engine, &blockDNSFilter4, 0, &filterId)
if err != nil {
return os.NewSyscallError("FwpmFilterAdd0", err)
}
blockDNSFilter6 := winsys.FWPM_FILTER0{}
blockDNSFilter6.FilterCondition = &blockDNSCondition[0]
blockDNSFilter6.NumFilterConditions = 2
blockDNSFilter6.DisplayData = winsys.CreateDisplayData(TunnelType, "block ipv6 dns")
blockDNSFilter6.SubLayerKey = subLayerKey
blockDNSFilter6.LayerKey = winsys.FWPM_LAYER_ALE_AUTH_CONNECT_V6
blockDNSFilter6.Action.Type = winsys.FWP_ACTION_BLOCK
blockDNSFilter6.Weight.Type = winsys.FWP_UINT8
blockDNSFilter6.Weight.Value = uintptr(10)
err = winsys.FwpmFilterAdd0(engine, &blockDNSFilter6, 0, &filterId)
if err != nil {
return os.NewSyscallError("FwpmFilterAdd0", err)
blockDNSFilter6 := winsys.FWPM_FILTER0{}
blockDNSFilter6.FilterCondition = &blockDNSCondition[0]
blockDNSFilter6.NumFilterConditions = 1
blockDNSFilter6.DisplayData = winsys.CreateDisplayData(TunnelType, "block ipv6 dns")
blockDNSFilter6.SubLayerKey = subLayerKey
blockDNSFilter6.LayerKey = winsys.FWPM_LAYER_ALE_AUTH_CONNECT_V6
blockDNSFilter6.Action.Type = winsys.FWP_ACTION_BLOCK
blockDNSFilter6.Weight.Type = winsys.FWP_UINT8
blockDNSFilter6.Weight.Value = uintptr(10)
err = winsys.FwpmFilterAdd0(engine, &blockDNSFilter6, 0, &filterId)
if err != nil {
return os.NewSyscallError("FwpmFilterAdd0", err)
}
}
}
@@ -334,13 +347,13 @@ func (t *NativeTun) ReadPacket() ([]byte, func(), error) {
t.running.Add(1)
defer t.running.Done()
retry:
if atomic.LoadInt32(&t.close) == 1 {
if t.close.Load() == 1 {
return nil, nil, os.ErrClosed
}
start := nanotime()
shouldSpin := atomic.LoadUint64(&t.rate.current) >= spinloopRateThreshold && uint64(start-atomic.LoadInt64(&t.rate.nextStartTime)) <= rateMeasurementGranularity*2
shouldSpin := t.rate.current.Load() >= spinloopRateThreshold && uint64(start-t.rate.nextStartTime.Load()) <= rateMeasurementGranularity*2
for {
if atomic.LoadInt32(&t.close) == 1 {
if t.close.Load() == 1 {
return nil, nil, os.ErrClosed
}
packet, err := t.session.ReceivePacket()
@@ -369,13 +382,13 @@ func (t *NativeTun) ReadFunc(block func(b []byte)) error {
t.running.Add(1)
defer t.running.Done()
retry:
if atomic.LoadInt32(&t.close) == 1 {
if t.close.Load() == 1 {
return os.ErrClosed
}
start := nanotime()
shouldSpin := atomic.LoadUint64(&t.rate.current) >= spinloopRateThreshold && uint64(start-atomic.LoadInt64(&t.rate.nextStartTime)) <= rateMeasurementGranularity*2
shouldSpin := t.rate.current.Load() >= spinloopRateThreshold && uint64(start-t.rate.nextStartTime.Load()) <= rateMeasurementGranularity*2
for {
if atomic.LoadInt32(&t.close) == 1 {
if t.close.Load() == 1 {
return os.ErrClosed
}
packet, err := t.session.ReceivePacket()
@@ -405,7 +418,7 @@ retry:
func (t *NativeTun) Write(p []byte) (n int, err error) {
t.running.Add(1)
defer t.running.Done()
if atomic.LoadInt32(&t.close) == 1 {
if t.close.Load() == 1 {
return 0, os.ErrClosed
}
t.rate.update(uint64(len(p)))
@@ -427,7 +440,7 @@ func (t *NativeTun) Write(p []byte) (n int, err error) {
func (t *NativeTun) write(packetElementList [][]byte) (n int, err error) {
t.running.Add(1)
defer t.running.Done()
if atomic.LoadInt32(&t.close) == 1 {
if t.close.Load() == 1 {
return 0, os.ErrClosed
}
var packetSize int
@@ -461,7 +474,7 @@ func (t *NativeTun) WriteVectorised(buffers []*buf.Buffer) error {
func (t *NativeTun) Close() error {
var err error
t.closeOnce.Do(func() {
atomic.StoreInt32(&t.close, 1)
t.close.Store(1)
windows.SetEvent(t.readWait)
t.running.Wait()
t.session.End()
@@ -491,24 +504,24 @@ func procyield(cycles uint32)
func nanotime() int64
type rateJuggler struct {
current uint64
nextByteCount uint64
nextStartTime int64
changing int32
current atomic.Uint64
nextByteCount atomic.Uint64
nextStartTime atomic.Int64
changing atomic.Int32
}
func (rate *rateJuggler) update(packetLen uint64) {
now := nanotime()
total := atomic.AddUint64(&rate.nextByteCount, packetLen)
period := uint64(now - atomic.LoadInt64(&rate.nextStartTime))
total := rate.nextByteCount.Add(packetLen)
period := uint64(now - rate.nextStartTime.Load())
if period >= rateMeasurementGranularity {
if !atomic.CompareAndSwapInt32(&rate.changing, 0, 1) {
if !rate.changing.CompareAndSwap(0, 1) {
return
}
atomic.StoreInt64(&rate.nextStartTime, now)
atomic.StoreUint64(&rate.current, total*uint64(time.Second/time.Nanosecond)/period)
atomic.StoreUint64(&rate.nextByteCount, 0)
atomic.StoreInt32(&rate.changing, 0)
rate.nextStartTime.Store(now)
rate.current.Store(total * uint64(time.Second/time.Nanosecond) / period)
rate.nextByteCount.Store(0)
rate.changing.Store(0)
}
}

View File

@@ -99,10 +99,10 @@ func (e *WintunEndpoint) ARPHardwareType() header.ARPHardwareType {
return header.ARPHardwareNone
}
func (e *WintunEndpoint) AddHeader(buffer stack.PacketBufferPtr) {
func (e *WintunEndpoint) AddHeader(buffer *stack.PacketBuffer) {
}
func (e *WintunEndpoint) ParseHeader(ptr stack.PacketBufferPtr) bool {
func (e *WintunEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool {
return true
}