From 623f0aba55a9088ed6bbb87b61694cd14dc10813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 22 Jun 2026 15:49:11 +0800 Subject: [PATCH 01/24] Fix lint errors --- .github/workflows/lint.yml | 41 +- .golangci.yml | 27 +- Makefile | 9 +- internal/checksum_test/sum_bench_test.go | 4 +- internal/fdbased_darwin/endpoint.go | 5 +- internal/fdbased_darwin/endpoint_mutex.go | 4 +- internal/fdbased_darwin/errno.go | 2 + internal/fdbased_darwin/packet_dispatchers.go | 6 +- internal/fdbased_darwin/processor_mutex.go | 4 +- internal/fdbased_darwin/processors.go | 2 + internal/gtcpip/header/ndp_options.go | 2 +- internal/gtcpip/header/tcp.go | 17 +- internal/gtcpip/tcpip.go | 51 ++- internal/rawfile_darwin/rawfile.go | 17 +- internal/stopfd_darwin/stopfd.go | 2 + internal/winfw/winfw.go | 58 +-- internal/winipcfg/interface_change_handler.go | 2 + internal/winipcfg/luid.go | 2 + internal/winipcfg/mksyscall.go | 2 + internal/winipcfg/netsh.go | 19 +- internal/winipcfg/route_change_handler.go | 2 + internal/winipcfg/types.go | 432 +++++++++--------- internal/winipcfg/types_32.go | 2 +- internal/winipcfg/types_64.go | 2 +- internal/winipcfg/types_test.go | 2 + internal/winipcfg/types_test_32.go | 2 +- internal/winipcfg/types_test_64.go | 2 +- .../unicast_address_change_handler.go | 2 + internal/winipcfg/winipcfg.go | 2 + internal/winipcfg/winipcfg_test.go | 11 +- internal/wintun/memmod/memmod_windows.go | 92 ++-- internal/wintun/memmod/memmod_windows_64.go | 2 +- internal/wintun/session_windows.go | 15 +- internal/wintun/wintun_windows.go | 14 +- nfqueue_linux.go | 7 +- stack.go | 2 +- stack_system.go | 6 +- tun_darwin.go | 7 +- tun_linux.go | 1 - tun_offload.go | 10 +- tun_offload_linux.go | 14 +- 41 files changed, 498 insertions(+), 407 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7d5cea88..9de14e50 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,21 +16,56 @@ on: jobs: build: - name: Build + name: Lint ${{ matrix.goos }}/${{ matrix.goarch }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - goos: windows + goarch: amd64 + - goos: windows + goarch: '386' + - goos: windows + goarch: arm64 + - goos: linux + goarch: amd64 + - goos: linux + goarch: arm64 + - goos: linux + goarch: arm + - goos: linux + goarch: '386' + - goos: darwin + goarch: amd64 + - goos: darwin + goarch: arm64 + - goos: android + goarch: arm64 + - goos: freebsd + goarch: amd64 steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ^1.25 + - name: Cache go module + uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod + key: go-${{ hashFiles('**/go.sum') }} - name: golangci-lint uses: golangci/golangci-lint-action@v8 + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} with: version: latest args: --timeout=30m install-mode: binary - verify: false \ No newline at end of file + verify: false diff --git a/.golangci.yml b/.golangci.yml index 7a8a771d..0a8a526d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,35 +1,24 @@ version: "2" run: - go: "1.25" + go: "1.24" linters: default: none enable: - - govet - ineffassign - - paralleltest - staticcheck + - modernize settings: staticcheck: checks: - all - - -S1000 - - -S1008 - - -S1017 - - -ST1003 - - -QF1001 - - -QF1003 - - -QF1008 + - -QF1008 # could remove embedded field "" from selector + - -ST1003 # should not use ALL_CAPS in Go names; use CamelCase instead + - -QF1001 # could apply De Morgan's law exclusions: generated: lax presets: - comments - common-false-positives - - legacy - - std-error-handling - paths: - - third_party$ - - builtin$ - - examples$ formatters: enable: - gci @@ -42,8 +31,4 @@ formatters: - default custom-order: true exclusions: - generated: lax - paths: - - third_party$ - - builtin$ - - examples$ + generated: lax \ No newline at end of file diff --git a/Makefile b/Makefile index c7524749..ed36c792 100644 --- a/Makefile +++ b/Makefile @@ -18,11 +18,10 @@ fmt_install: go install -v github.com/daixiang0/gci@latest lint: - GOOS=linux golangci-lint run . - GOOS=android golangci-lint run . - GOOS=windows golangci-lint run . - GOOS=darwin golangci-lint run . - GOOS=freebsd golangci-lint run . + GOOS=linux golangci-lint --max-same-issues=0 --max-issues-per-linter=0 run ./... + GOOS=android golangci-lint --max-same-issues=0 --max-issues-per-linter=0 run ./... + GOOS=windows golangci-lint --max-same-issues=0 --max-issues-per-linter=0 run ./... + GOOS=darwin golangci-lint --max-same-issues=0 --max-issues-per-linter=0 run ./... lint_install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest diff --git a/internal/checksum_test/sum_bench_test.go b/internal/checksum_test/sum_bench_test.go index 35ee021c..a5372174 100644 --- a/internal/checksum_test/sum_bench_test.go +++ b/internal/checksum_test/sum_bench_test.go @@ -10,7 +10,7 @@ import ( func BenchmarkTsChecksum(b *testing.B) { packet := make([][]byte, 1000) - for i := 0; i < 1000; i++ { + for i := range 1000 { packet[i] = make([]byte, 1500) rand.Read(packet[i]) } @@ -22,7 +22,7 @@ func BenchmarkTsChecksum(b *testing.B) { func BenchmarkGChecksum(b *testing.B) { packet := make([][]byte, 1000) - for i := 0; i < 1000; i++ { + for i := range 1000 { packet[i] = make([]byte, 1500) rand.Read(packet[i]) } diff --git a/internal/fdbased_darwin/endpoint.go b/internal/fdbased_darwin/endpoint.go index f26bfe30..05371e77 100644 --- a/internal/fdbased_darwin/endpoint.go +++ b/internal/fdbased_darwin/endpoint.go @@ -35,6 +35,9 @@ // only use the first FD to write outbound packets. Once 5 tuple hashes for // all outbound packets are available we will make use of all underlying FD's to // write outbound packets. + +//go:build darwin + package fdbased import ( @@ -46,7 +49,7 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip" "github.com/sagernet/gvisor/pkg/tcpip/header" "github.com/sagernet/gvisor/pkg/tcpip/stack" - "github.com/sagernet/sing-tun/internal/rawfile_darwin" + rawfile "github.com/sagernet/sing-tun/internal/rawfile_darwin" "github.com/sagernet/sing/common" "golang.org/x/sys/unix" diff --git a/internal/fdbased_darwin/endpoint_mutex.go b/internal/fdbased_darwin/endpoint_mutex.go index d05b2640..4f2d4b83 100644 --- a/internal/fdbased_darwin/endpoint_mutex.go +++ b/internal/fdbased_darwin/endpoint_mutex.go @@ -1,3 +1,5 @@ +//go:build darwin + package fdbased import ( @@ -92,5 +94,5 @@ func endpointinitLockNames() {} func init() { endpointinitLockNames() - endpointprefixIndex = locking.NewMutexClass(reflect.TypeOf(endpointRWMutex{}), endpointlockNames) + endpointprefixIndex = locking.NewMutexClass(reflect.TypeFor[endpointRWMutex](), endpointlockNames) } diff --git a/internal/fdbased_darwin/errno.go b/internal/fdbased_darwin/errno.go index 074f4e2e..8c6f9663 100644 --- a/internal/fdbased_darwin/errno.go +++ b/internal/fdbased_darwin/errno.go @@ -1,3 +1,5 @@ +//go:build darwin + package fdbased import ( diff --git a/internal/fdbased_darwin/packet_dispatchers.go b/internal/fdbased_darwin/packet_dispatchers.go index a006d411..c102f6cc 100644 --- a/internal/fdbased_darwin/packet_dispatchers.go +++ b/internal/fdbased_darwin/packet_dispatchers.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build darwin + package fdbased import ( @@ -19,7 +21,7 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip" "github.com/sagernet/gvisor/pkg/tcpip/stack" "github.com/sagernet/gvisor/pkg/tcpip/stack/gro" - "github.com/sagernet/sing-tun/internal/rawfile_darwin" + rawfile "github.com/sagernet/sing-tun/internal/rawfile_darwin" "github.com/sagernet/sing-tun/internal/stopfd_darwin" "golang.org/x/sys/unix" @@ -177,7 +179,7 @@ func (d *recvMMsgDispatcher) dispatch() (bool, tcpip.Error) { d.gro.Dispatcher = dsp defer d.pkts.Reset() - for k := 0; k < nMsgs; k++ { + for k := range nMsgs { n := int(d.msgHdrs[k].DataLen) payload := d.bufs[k].pullBuffer(n) pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ diff --git a/internal/fdbased_darwin/processor_mutex.go b/internal/fdbased_darwin/processor_mutex.go index cd297d2a..87e1801c 100644 --- a/internal/fdbased_darwin/processor_mutex.go +++ b/internal/fdbased_darwin/processor_mutex.go @@ -1,3 +1,5 @@ +//go:build darwin + package fdbased import ( @@ -60,5 +62,5 @@ func processorinitLockNames() {} func init() { processorinitLockNames() - processorprefixIndex = locking.NewMutexClass(reflect.TypeOf(processorMutex{}), processorlockNames) + processorprefixIndex = locking.NewMutexClass(reflect.TypeFor[processorMutex](), processorlockNames) } diff --git a/internal/fdbased_darwin/processors.go b/internal/fdbased_darwin/processors.go index 9df6cfa4..1ceca834 100644 --- a/internal/fdbased_darwin/processors.go +++ b/internal/fdbased_darwin/processors.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build darwin + package fdbased import ( diff --git a/internal/gtcpip/header/ndp_options.go b/internal/gtcpip/header/ndp_options.go index ba293398..21ed757b 100644 --- a/internal/gtcpip/header/ndp_options.go +++ b/internal/gtcpip/header/ndp_options.go @@ -878,7 +878,7 @@ func (o NDPDNSSearchList) iterDomainNames(fn func(string)) error { } // Copy the label and add a trailing period. - for i := 0; i < labelLen; i++ { + for i := range labelLen { b, err := searchList.ReadByte() if err != nil { if err != io.EOF { diff --git a/internal/gtcpip/header/tcp.go b/internal/gtcpip/header/tcp.go index 1b58df86..04c22b59 100644 --- a/internal/gtcpip/header/tcp.go +++ b/internal/gtcpip/header/tcp.go @@ -476,20 +476,14 @@ func ParseSynOptions(opts []byte, isAck bool) TCPSynOptions { if mss == 0 { return synOpts } - synOpts.MSS = mss - if mss < TCPMinimumSendMSS { - synOpts.MSS = TCPMinimumSendMSS - } + synOpts.MSS = max(mss, TCPMinimumSendMSS) i += 4 case TCPOptionWS: if i+3 > limit || opts[i+1] != 3 { return synOpts } - ws := int(opts[i+2]) - if ws > MaxWndScale { - ws = MaxWndScale - } + ws := min(int(opts[i+2]), MaxWndScale) synOpts.WS = ws i += 3 @@ -561,7 +555,7 @@ func ParseTCPOptions(b []byte) TCPOptions { } numBlocks := (sackOptionLen - 2) / 8 opts.SACKBlocks = []SACKBlock{} - for j := 0; j < numBlocks; j++ { + for j := range numBlocks { start := binary.BigEndian.Uint32(b[i+2+j*8:]) end := binary.BigEndian.Uint32(b[i+2+j*8+4:]) opts.SACKBlocks = append(opts.SACKBlocks, SACKBlock{ @@ -646,10 +640,7 @@ func EncodeSACKBlocks(sackBlocks []SACKBlock, b []byte) int { if len(sackBlocks) == 0 { return 0 } - l := len(sackBlocks) - if l > TCPMaxSACKBlocks { - l = TCPMaxSACKBlocks - } + l := min(len(sackBlocks), TCPMaxSACKBlocks) if ll := (len(b) - 2) / 8; ll < l { l = ll } diff --git a/internal/gtcpip/tcpip.go b/internal/gtcpip/tcpip.go index 60d2892e..a3dbca84 100644 --- a/internal/gtcpip/tcpip.go +++ b/internal/gtcpip/tcpip.go @@ -245,6 +245,53 @@ func (a Address) Len() int { return a.length } +// String implements the fmt.Stringer interface. +func (a Address) String() string { + switch l := a.Len(); l { + case 4: + return fmt.Sprintf("%d.%d.%d.%d", int(a.addr[0]), int(a.addr[1]), int(a.addr[2]), int(a.addr[3])) + case 16: + // Find the longest subsequence of hexadecimal zeros. + start, end := -1, -1 + for i := 0; i < a.Len(); i += 2 { + j := i + for j < a.Len() && a.addr[j] == 0 && a.addr[j+1] == 0 { + j += 2 + } + if j > i+2 && j-i > end-start { + start, end = i, j + } + } + + var b strings.Builder + for i := 0; i < a.Len(); i += 2 { + if i == start { + b.WriteString("::") + i = end + if end >= a.Len() { + break + } + } else if i > 0 { + b.WriteByte(':') + } + v := uint16(a.addr[i+0])<<8 | uint16(a.addr[i+1]) + if v == 0 { + b.WriteByte('0') + } else { + const digits = "0123456789abcdef" + for i := uint(3); i < 4; i-- { + if v := v >> (i * 4); v != 0 { + b.WriteByte(digits[v&0xf]) + } + } + } + } + return b.String() + default: + return fmt.Sprintf("%x", a.addr[:l]) + } +} + // WithPrefix returns the address with a prefix that represents a point subnet. func (a Address) WithPrefix() AddressWithPrefix { return AddressWithPrefix{ @@ -541,7 +588,7 @@ func (a AddressWithPrefix) Subnet() Subnet { address: a.Address, mask: AddressMask{length: addrLen}, } - for i := 0; i < addrLen; i++ { + for i := range addrLen { sub.mask.mask[i] = 0xff } return sub @@ -550,7 +597,7 @@ func (a AddressWithPrefix) Subnet() Subnet { sa := Address{length: addrLen} sm := AddressMask{length: addrLen} n := uint(a.PrefixLen) - for i := 0; i < addrLen; i++ { + for i := range addrLen { if n >= 8 { sa.addr[i] = a.Address.addr[i] sm.mask[i] = 0xff diff --git a/internal/rawfile_darwin/rawfile.go b/internal/rawfile_darwin/rawfile.go index b73bd82f..e3a061b4 100644 --- a/internal/rawfile_darwin/rawfile.go +++ b/internal/rawfile_darwin/rawfile.go @@ -1,7 +1,8 @@ +//go:build darwin + package rawfile import ( - "reflect" "unsafe" "golang.org/x/sys/unix" @@ -25,12 +26,8 @@ func IovecFromBytes(bs []byte) unix.Iovec { return iov } -func bytesFromIovec(iov unix.Iovec) (bs []byte) { - sh := (*reflect.SliceHeader)(unsafe.Pointer(&bs)) - sh.Data = uintptr(unsafe.Pointer(iov.Base)) - sh.Len = int(iov.Len) - sh.Cap = int(iov.Len) - return +func bytesFromIovec(iov unix.Iovec) []byte { + return unsafe.Slice(iov.Base, iov.Len) } // AppendIovecFromBytes returns append(iovs, IovecFromBytes(bs)). If len(bs) == @@ -56,6 +53,7 @@ type MsgHdrX struct { } func NonBlockingSendMMsg(fd int, msgHdrs []MsgHdrX) (int, unix.Errno) { + //nolint:staticcheck n, _, e := unix.RawSyscall6(unix.SYS_SENDMSG_X, uintptr(fd), uintptr(unsafe.Pointer(&msgHdrs[0])), uintptr(len(msgHdrs)), unix.MSG_DONTWAIT, 0, 0) return int(n), e } @@ -66,12 +64,14 @@ const SizeofMsgHdrX = unsafe.Sizeof(MsgHdrX{}) // It fails if partial data is written. func NonBlockingWriteIovec(fd int, iovec []unix.Iovec) unix.Errno { iovecLen := uintptr(len(iovec)) + //nolint:staticcheck _, _, e := unix.RawSyscall(unix.SYS_WRITEV, uintptr(fd), uintptr(unsafe.Pointer(&iovec[0])), iovecLen) return e } func BlockingReadvUntilStopped(efd int, fd int, iovecs []unix.Iovec) (int, unix.Errno) { for { + //nolint:staticcheck n, _, e := unix.RawSyscall(unix.SYS_READV, uintptr(fd), uintptr(unsafe.Pointer(&iovecs[0])), uintptr(len(iovecs))) if e == 0 { return int(n), 0 @@ -91,6 +91,7 @@ func BlockingReadvUntilStopped(efd int, fd int, iovecs []unix.Iovec) (int, unix. func BlockingRecvMMsgUntilStopped(efd int, fd int, msgHdrs []MsgHdrX) (int, unix.Errno) { for { + //nolint:staticcheck n, _, e := unix.RawSyscall6(unix.SYS_RECVMSG_X, uintptr(fd), uintptr(unsafe.Pointer(&msgHdrs[0])), uintptr(len(msgHdrs)), unix.MSG_DONTWAIT, 0, 0) if e == 0 { return int(n), e @@ -162,7 +163,7 @@ func BlockingPollUntilStopped(efd int, fd int, events int16) (bool, unix.Errno) var efdHasData bool var errno unix.Errno - for i := 0; i < n; i++ { + for i := range n { ev := &revents[i] if int(ev.Ident) == efd && ev.Filter == unix.EVFILT_READ { diff --git a/internal/stopfd_darwin/stopfd.go b/internal/stopfd_darwin/stopfd.go index fdc39739..0a8f9802 100644 --- a/internal/stopfd_darwin/stopfd.go +++ b/internal/stopfd_darwin/stopfd.go @@ -1,3 +1,5 @@ +//go:build darwin + package stopfd import ( diff --git a/internal/winfw/winfw.go b/internal/winfw/winfw.go index f8f17bb4..8d5b80fa 100644 --- a/internal/winfw/winfw.go +++ b/internal/winfw/winfw.go @@ -98,105 +98,105 @@ func firewallRuleAdd(name, description, group, appPath, serviceName, ports, remo if profile == NET_FW_PROFILE2_CURRENT { currentProfiles, err := oleutil.GetProperty(fwPolicy, "CurrentProfileTypes") if err != nil { - return false, fmt.Errorf("Failed to get CurrentProfiles: %s", err) + return false, fmt.Errorf("failed to get CurrentProfiles: %s", err) } profile = currentProfiles.Value().(int32) } unknownRules, err := oleutil.GetProperty(fwPolicy, "Rules") if err != nil { - return false, fmt.Errorf("Failed to get Rules: %s", err) + return false, fmt.Errorf("failed to get Rules: %s", err) } rules := unknownRules.ToIDispatch() if ok, err := FirewallRuleExistsByName(rules, name); err != nil { - return false, fmt.Errorf("Error while checking rules for duplicate: %s", err) + return false, fmt.Errorf("error while checking rules for duplicate: %s", err) } else if ok { return false, nil } unknown2, err := oleutil.CreateObject("HNetCfg.FWRule") if err != nil { - return false, fmt.Errorf("Error creating Rule object: %s", err) + return false, fmt.Errorf("error creating Rule object: %s", err) } defer unknown2.Release() fwRule, err := unknown2.QueryInterface(ole.IID_IDispatch) if err != nil { - return false, fmt.Errorf("Error creating Rule object (2): %s", err) + return false, fmt.Errorf("error creating Rule object (2): %s", err) } defer fwRule.Release() if _, err := oleutil.PutProperty(fwRule, "Name", name); err != nil { - return false, fmt.Errorf("Error setting property (Name) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Name) of Rule: %s", err) } if _, err := oleutil.PutProperty(fwRule, "Description", description); err != nil { - return false, fmt.Errorf("Error setting property (Description) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Description) of Rule: %s", err) } if appPath != "" { if _, err := oleutil.PutProperty(fwRule, "Applicationname", appPath); err != nil { - return false, fmt.Errorf("Error setting property (Applicationname) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Applicationname) of Rule: %s", err) } } if serviceName != "" { if _, err := oleutil.PutProperty(fwRule, "ServiceName", serviceName); err != nil { - return false, fmt.Errorf("Error setting property (ServiceName) of Rule: %s", err) + return false, fmt.Errorf("error setting property (ServiceName) of Rule: %s", err) } } if protocol != 0 { if _, err := oleutil.PutProperty(fwRule, "Protocol", protocol); err != nil { - return false, fmt.Errorf("Error setting property (Protocol) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Protocol) of Rule: %s", err) } } if icmpTypes != "" { if _, err := oleutil.PutProperty(fwRule, "IcmpTypesAndCodes", icmpTypes); err != nil { - return false, fmt.Errorf("Error setting property (IcmpTypesAndCodes) of Rule: %s", err) + return false, fmt.Errorf("error setting property (IcmpTypesAndCodes) of Rule: %s", err) } } if ports != "" { if _, err := oleutil.PutProperty(fwRule, "LocalPorts", ports); err != nil { - return false, fmt.Errorf("Error setting property (LocalPorts) of Rule: %s", err) + return false, fmt.Errorf("error setting property (LocalPorts) of Rule: %s", err) } } if remotePorts != "" { if _, err := oleutil.PutProperty(fwRule, "RemotePorts", remotePorts); err != nil { - return false, fmt.Errorf("Error setting property (RemotePorts) of Rule: %s", err) + return false, fmt.Errorf("error setting property (RemotePorts) of Rule: %s", err) } } if localAddresses != "" { if _, err := oleutil.PutProperty(fwRule, "LocalAddresses", localAddresses); err != nil { - return false, fmt.Errorf("Error setting property (LocalAddresses) of Rule: %s", err) + return false, fmt.Errorf("error setting property (LocalAddresses) of Rule: %s", err) } } if remoteAddresses != "" { if _, err := oleutil.PutProperty(fwRule, "RemoteAddresses", remoteAddresses); err != nil { - return false, fmt.Errorf("Error setting property (RemoteAddresses) of Rule: %s", err) + return false, fmt.Errorf("error setting property (RemoteAddresses) of Rule: %s", err) } } if direction != 0 { if _, err := oleutil.PutProperty(fwRule, "Direction", direction); err != nil { - return false, fmt.Errorf("Error setting property (Direction) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Direction) of Rule: %s", err) } } if _, err := oleutil.PutProperty(fwRule, "Enabled", enabled); err != nil { - return false, fmt.Errorf("Error setting property (Enabled) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Enabled) of Rule: %s", err) } if _, err := oleutil.PutProperty(fwRule, "Grouping", group); err != nil { - return false, fmt.Errorf("Error setting property (Grouping) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Grouping) of Rule: %s", err) } if _, err := oleutil.PutProperty(fwRule, "Profiles", profile); err != nil { - return false, fmt.Errorf("Error setting property (Profiles) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Profiles) of Rule: %s", err) } if _, err := oleutil.PutProperty(fwRule, "Action", action); err != nil { - return false, fmt.Errorf("Error setting property (Action) of Rule: %s", err) + return false, fmt.Errorf("error setting property (Action) of Rule: %s", err) } if edgeTraversal { if _, err := oleutil.PutProperty(fwRule, "EdgeTraversal", edgeTraversal); err != nil { - return false, fmt.Errorf("Error setting property (EdgeTraversal) of Rule: %s", err) + return false, fmt.Errorf("error setting property (EdgeTraversal) of Rule: %s", err) } } if _, err := oleutil.CallMethod(rules, "Add", fwRule); err != nil { - return false, fmt.Errorf("Error adding Rule: %s", err) + return false, fmt.Errorf("error adding Rule: %s", err) } return true, nil @@ -205,13 +205,13 @@ func firewallRuleAdd(name, description, group, appPath, serviceName, ports, remo func FirewallRuleExistsByName(rules *ole.IDispatch, name string) (bool, error) { enumProperty, err := rules.GetProperty("_NewEnum") if err != nil { - return false, fmt.Errorf("Failed to get enumeration property on Rules: %s", err) + return false, fmt.Errorf("failed to get enumeration property on Rules: %s", err) } defer enumProperty.Clear() enum, err := enumProperty.ToIUnknown().IEnumVARIANT(ole.IID_IEnumVariant) if err != nil { - return false, fmt.Errorf("Failed to cast enum to correct type: %s", err) + return false, fmt.Errorf("failed to cast enum to correct type: %s", err) } if enum == nil { return false, fmt.Errorf("can't get IEnumVARIANT, enum is nil") @@ -219,7 +219,7 @@ func FirewallRuleExistsByName(rules *ole.IDispatch, name string) (bool, error) { for itemRaw, length, err := enum.Next(1); length > 0; itemRaw, length, err = enum.Next(1) { if err != nil { - return false, fmt.Errorf("Failed to seek next Rule item: %s", err) + return false, fmt.Errorf("failed to seek next Rule item: %s", err) } t, err := func() (bool, error) { @@ -227,7 +227,7 @@ func FirewallRuleExistsByName(rules *ole.IDispatch, name string) (bool, error) { defer item.Release() if item, err := oleutil.GetProperty(item, "Name"); err != nil { - return false, fmt.Errorf("Failed to get Property (Name) of Rule") + return false, fmt.Errorf("failed to get Property (Name) of Rule") } else if item.ToString() == name { return true, nil } @@ -251,18 +251,18 @@ func FirewallRuleExistsByName(rules *ole.IDispatch, name string) (bool, error) { func firewallAPIInit() (*ole.IUnknown, *ole.IDispatch, error) { err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED) if err != nil { - return nil, nil, fmt.Errorf("Failed to initialize COM: %s", err) + return nil, nil, fmt.Errorf("failed to initialize COM: %s", err) } unknown, err := oleutil.CreateObject("HNetCfg.FwPolicy2") if err != nil { - return nil, nil, fmt.Errorf("Failed to create FwPolicy Object: %s", err) + return nil, nil, fmt.Errorf("failed to create FwPolicy Object: %s", err) } fwPolicy, err := unknown.QueryInterface(ole.IID_IDispatch) if err != nil { unknown.Release() - return nil, nil, fmt.Errorf("Failed to create FwPolicy Object (2): %s", err) + return nil, nil, fmt.Errorf("failed to create FwPolicy Object (2): %s", err) } return unknown, fwPolicy, nil diff --git a/internal/winipcfg/interface_change_handler.go b/internal/winipcfg/interface_change_handler.go index af29801a..b1669404 100644 --- a/internal/winipcfg/interface_change_handler.go +++ b/internal/winipcfg/interface_change_handler.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. diff --git a/internal/winipcfg/luid.go b/internal/winipcfg/luid.go index 1f97314c..b7159cbc 100644 --- a/internal/winipcfg/luid.go +++ b/internal/winipcfg/luid.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. diff --git a/internal/winipcfg/mksyscall.go b/internal/winipcfg/mksyscall.go index d62d38df..f07abb34 100644 --- a/internal/winipcfg/mksyscall.go +++ b/internal/winipcfg/mksyscall.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. diff --git a/internal/winipcfg/netsh.go b/internal/winipcfg/netsh.go index 2c298cb7..51759103 100644 --- a/internal/winipcfg/netsh.go +++ b/internal/winipcfg/netsh.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. @@ -60,9 +62,10 @@ const ( func (luid LUID) fallbackSetDNSForFamily(family AddressFamily, dnses []netip.Addr) error { var templateFlush string - if family == windows.AF_INET { + switch family { + case windows.AF_INET: templateFlush = netshCmdTemplateFlush4 - } else if family == windows.AF_INET6 { + case windows.AF_INET6: templateFlush = netshCmdTemplateFlush6 } @@ -72,7 +75,7 @@ func (luid LUID) fallbackSetDNSForFamily(family AddressFamily, dnses []netip.Add return err } cmds = append(cmds, fmt.Sprintf(templateFlush, ipif.InterfaceIndex)) - for i := 0; i < len(dnses); i++ { + for i := range dnses { if dnses[i].Is4() && family == windows.AF_INET { cmds = append(cmds, fmt.Sprintf(netshCmdTemplateAdd4, ipif.InterfaceIndex, dnses[i].String())) } else if dnses[i].Is6() && family == windows.AF_INET6 { @@ -85,23 +88,23 @@ func (luid LUID) fallbackSetDNSForFamily(family AddressFamily, dnses []netip.Add func (luid LUID) fallbackSetDNSDomain(domain string) error { guid, err := luid.GUID() if err != nil { - return fmt.Errorf("Error converting luid to guid: %w", err) + return fmt.Errorf("error converting luid to guid: %w", err) } key, err := registry.OpenKey(registry.LOCAL_MACHINE, fmt.Sprintf("SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Adapters\\%v", guid), registry.QUERY_VALUE) if err != nil { - return fmt.Errorf("Error opening adapter-specific TCP/IP network registry key: %w", err) + return fmt.Errorf("error opening adapter-specific TCP/IP network registry key: %w", err) } paths, _, err := key.GetStringsValue("IpConfig") key.Close() if err != nil { - return fmt.Errorf("Error reading IpConfig registry key: %w", err) + return fmt.Errorf("error reading IpConfig registry key: %w", err) } if len(paths) == 0 { - return errors.New("No TCP/IP interfaces found on adapter") + return errors.New("no TCP/IP interfaces found on adapter") } key, err = registry.OpenKey(registry.LOCAL_MACHINE, fmt.Sprintf("SYSTEM\\CurrentControlSet\\Services\\%s", paths[0]), registry.SET_VALUE) if err != nil { - return fmt.Errorf("Unable to open TCP/IP network registry key: %w", err) + return fmt.Errorf("unable to open TCP/IP network registry key: %w", err) } err = key.SetStringValue("Domain", domain) key.Close() diff --git a/internal/winipcfg/route_change_handler.go b/internal/winipcfg/route_change_handler.go index 4b78331e..63e7aa13 100644 --- a/internal/winipcfg/route_change_handler.go +++ b/internal/winipcfg/route_change_handler.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. diff --git a/internal/winipcfg/types.go b/internal/winipcfg/types.go index 8e8f4a59..01b5cc05 100644 --- a/internal/winipcfg/types.go +++ b/internal/winipcfg/types.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. @@ -62,206 +64,206 @@ type IfType uint32 const ( IfTypeOther IfType = 1 // None of the below - IfTypeRegular1822 = 2 - IfTypeHdh1822 = 3 - IfTypeDdnX25 = 4 - IfTypeRfc877X25 = 5 - IfTypeEthernetCSMACD = 6 - IfTypeISO88023CSMACD = 7 - IfTypeISO88024Tokenbus = 8 - IfTypeISO88025Tokenring = 9 - IfTypeISO88026Man = 10 - IfTypeStarlan = 11 - IfTypeProteon10Mbit = 12 - IfTypeProteon80Mbit = 13 - IfTypeHyperchannel = 14 - IfTypeFddi = 15 - IfTypeLapB = 16 - IfTypeSdlc = 17 - IfTypeDs1 = 18 // DS1-MIB - IfTypeE1 = 19 // Obsolete; see DS1-MIB - IfTypeBasicISDN = 20 - IfTypePrimaryISDN = 21 - IfTypePropPoint2PointSerial = 22 // proprietary serial - IfTypePPP = 23 - IfTypeSoftwareLoopback = 24 - IfTypeEon = 25 // CLNP over IP - IfTypeEthernet3Mbit = 26 - IfTypeNsip = 27 // XNS over IP - IfTypeSlip = 28 // Generic Slip - IfTypeUltra = 29 // ULTRA Technologies - IfTypeDs3 = 30 // DS3-MIB - IfTypeSip = 31 // SMDS, coffee - IfTypeFramerelay = 32 // DTE only - IfTypeRs232 = 33 - IfTypePara = 34 // Parallel port - IfTypeArcnet = 35 - IfTypeArcnetPlus = 36 - IfTypeAtm = 37 // ATM cells - IfTypeMioX25 = 38 - IfTypeSonet = 39 // SONET or SDH - IfTypeX25Ple = 40 - IfTypeIso88022LLC = 41 - IfTypeLocaltalk = 42 - IfTypeSmdsDxi = 43 - IfTypeFramerelayService = 44 // FRNETSERV-MIB - IfTypeV35 = 45 - IfTypeHssi = 46 - IfTypeHippi = 47 - IfTypeModem = 48 // Generic Modem - IfTypeAal5 = 49 // AAL5 over ATM - IfTypeSonetPath = 50 - IfTypeSonetVt = 51 - IfTypeSmdsIcip = 52 // SMDS InterCarrier Interface - IfTypePropVirtual = 53 // Proprietary virtual/internal - IfTypePropMultiplexor = 54 // Proprietary multiplexing - IfTypeIEEE80212 = 55 // 100BaseVG - IfTypeFibrechannel = 56 - IfTypeHippiinterface = 57 - IfTypeFramerelayInterconnect = 58 // Obsolete, use 32 or 44 - IfTypeAflane8023 = 59 // ATM Emulated LAN for 802.3 - IfTypeAflane8025 = 60 // ATM Emulated LAN for 802.5 - IfTypeCctemul = 61 // ATM Emulated circuit - IfTypeFastether = 62 // Fast Ethernet (100BaseT) - IfTypeISDN = 63 // ISDN and X.25 - IfTypeV11 = 64 // CCITT V.11/X.21 - IfTypeV36 = 65 // CCITT V.36 - IfTypeG703_64k = 66 // CCITT G703 at 64Kbps - IfTypeG703_2mb = 67 // Obsolete; see DS1-MIB - IfTypeQllc = 68 // SNA QLLC - IfTypeFastetherFX = 69 // Fast Ethernet (100BaseFX) - IfTypeChannel = 70 - IfTypeIEEE80211 = 71 // Radio spread spectrum - IfTypeIBM370parchan = 72 // IBM System 360/370 OEMI Channel - IfTypeEscon = 73 // IBM Enterprise Systems Connection - IfTypeDlsw = 74 // Data Link Switching - IfTypeISDNS = 75 // ISDN S/T interface - IfTypeISDNU = 76 // ISDN U interface - IfTypeLapD = 77 // Link Access Protocol D - IfTypeIpswitch = 78 // IP Switching Objects - IfTypeRsrb = 79 // Remote Source Route Bridging - IfTypeAtmLogical = 80 // ATM Logical Port - IfTypeDs0 = 81 // Digital Signal Level 0 - IfTypeDs0Bundle = 82 // Group of ds0s on the same ds1 - IfTypeBsc = 83 // Bisynchronous Protocol - IfTypeAsync = 84 // Asynchronous Protocol - IfTypeCnr = 85 // Combat Net Radio - IfTypeIso88025rDtr = 86 // ISO 802.5r DTR - IfTypeEplrs = 87 // Ext Pos Loc Report Sys - IfTypeArap = 88 // Appletalk Remote Access Protocol - IfTypePropCnls = 89 // Proprietary Connectionless Proto - IfTypeHostpad = 90 // CCITT-ITU X.29 PAD Protocol - IfTypeTermpad = 91 // CCITT-ITU X.3 PAD Facility - IfTypeFramerelayMpi = 92 // Multiproto Interconnect over FR - IfTypeX213 = 93 // CCITT-ITU X213 - IfTypeAdsl = 94 // Asymmetric Digital Subscrbr Loop - IfTypeRadsl = 95 // Rate-Adapt Digital Subscrbr Loop - IfTypeSdsl = 96 // Symmetric Digital Subscriber Loop - IfTypeVdsl = 97 // Very H-Speed Digital Subscrb Loop - IfTypeIso88025Crfprint = 98 // ISO 802.5 CRFP - IfTypeMyrinet = 99 // Myricom Myrinet - IfTypeVoiceEm = 100 // Voice recEive and transMit - IfTypeVoiceFxo = 101 // Voice Foreign Exchange Office - IfTypeVoiceFxs = 102 // Voice Foreign Exchange Station - IfTypeVoiceEncap = 103 // Voice encapsulation - IfTypeVoiceOverip = 104 // Voice over IP encapsulation - IfTypeAtmDxi = 105 // ATM DXI - IfTypeAtmFuni = 106 // ATM FUNI - IfTypeAtmIma = 107 // ATM IMA - IfTypePPPmultilinkbundle = 108 // PPP Multilink Bundle - IfTypeIpoverCdlc = 109 // IBM ipOverCdlc - IfTypeIpoverClaw = 110 // IBM Common Link Access to Workstn - IfTypeStacktostack = 111 // IBM stackToStack - IfTypeVirtualipaddress = 112 // IBM VIPA - IfTypeMpc = 113 // IBM multi-proto channel support - IfTypeIpoverAtm = 114 // IBM ipOverAtm - IfTypeIso88025Fiber = 115 // ISO 802.5j Fiber Token Ring - IfTypeTdlc = 116 // IBM twinaxial data link control - IfTypeGigabitethernet = 117 - IfTypeHdlc = 118 - IfTypeLapF = 119 - IfTypeV37 = 120 - IfTypeX25Mlp = 121 // Multi-Link Protocol - IfTypeX25Huntgroup = 122 // X.25 Hunt Group - IfTypeTransphdlc = 123 - IfTypeInterleave = 124 // Interleave channel - IfTypeFast = 125 // Fast channel - IfTypeIP = 126 // IP (for APPN HPR in IP networks) - IfTypeDocscableMaclayer = 127 // CATV Mac Layer - IfTypeDocscableDownstream = 128 // CATV Downstream interface - IfTypeDocscableUpstream = 129 // CATV Upstream interface - IfTypeA12mppswitch = 130 // Avalon Parallel Processor - IfTypeTunnel = 131 // Encapsulation interface - IfTypeCoffee = 132 // Coffee pot - IfTypeCes = 133 // Circuit Emulation Service - IfTypeAtmSubinterface = 134 // ATM Sub Interface - IfTypeL2Vlan = 135 // Layer 2 Virtual LAN using 802.1Q - IfTypeL3Ipvlan = 136 // Layer 3 Virtual LAN using IP - IfTypeL3Ipxvlan = 137 // Layer 3 Virtual LAN using IPX - IfTypeDigitalpowerline = 138 // IP over Power Lines - IfTypeMediamailoverip = 139 // Multimedia Mail over IP - IfTypeDtm = 140 // Dynamic syncronous Transfer Mode - IfTypeDcn = 141 // Data Communications Network - IfTypeIpforward = 142 // IP Forwarding Interface - IfTypeMsdsl = 143 // Multi-rate Symmetric DSL - IfTypeIEEE1394 = 144 // IEEE1394 High Perf Serial Bus - IfTypeIfGsn = 145 - IfTypeDvbrccMaclayer = 146 - IfTypeDvbrccDownstream = 147 - IfTypeDvbrccUpstream = 148 - IfTypeAtmVirtual = 149 - IfTypeMplsTunnel = 150 - IfTypeSrp = 151 - IfTypeVoiceoveratm = 152 - IfTypeVoiceoverframerelay = 153 - IfTypeIdsl = 154 - IfTypeCompositelink = 155 - IfTypeSs7Siglink = 156 - IfTypePropWirelessP2P = 157 - IfTypeFrForward = 158 - IfTypeRfc1483 = 159 - IfTypeUsb = 160 - IfTypeIEEE8023adLag = 161 - IfTypeBgpPolicyAccounting = 162 - IfTypeFrf16MfrBundle = 163 - IfTypeH323Gatekeeper = 164 - IfTypeH323Proxy = 165 - IfTypeMpls = 166 - IfTypeMfSiglink = 167 - IfTypeHdsl2 = 168 - IfTypeShdsl = 169 - IfTypeDs1Fdl = 170 - IfTypePos = 171 - IfTypeDvbAsiIn = 172 - IfTypeDvbAsiOut = 173 - IfTypePlc = 174 - IfTypeNfas = 175 - IfTypeTr008 = 176 - IfTypeGr303Rdt = 177 - IfTypeGr303Idt = 178 - IfTypeIsup = 179 - IfTypePropDocsWirelessMaclayer = 180 - IfTypePropDocsWirelessDownstream = 181 - IfTypePropDocsWirelessUpstream = 182 - IfTypeHiperlan2 = 183 - IfTypePropBwaP2MP = 184 - IfTypeSonetOverheadChannel = 185 - IfTypeDigitalWrapperOverheadChannel = 186 - IfTypeAal2 = 187 - IfTypeRadioMac = 188 - IfTypeAtmRadio = 189 - IfTypeImt = 190 - IfTypeMvl = 191 - IfTypeReachDsl = 192 - IfTypeFrDlciEndpt = 193 - IfTypeAtmVciEndpt = 194 - IfTypeOpticalChannel = 195 - IfTypeOpticalTransport = 196 - IfTypeIEEE80216Wman = 237 - IfTypeWwanpp = 243 // WWAN devices based on GSM technology - IfTypeWwanpp2 = 244 // WWAN devices based on CDMA technology - IfTypeIEEE802154 = 259 // IEEE 802.15.4 WPAN interface - IfTypeXboxWireless = 281 + IfTypeRegular1822 IfType = 2 + IfTypeHdh1822 IfType = 3 + IfTypeDdnX25 IfType = 4 + IfTypeRfc877X25 IfType = 5 + IfTypeEthernetCSMACD IfType = 6 + IfTypeISO88023CSMACD IfType = 7 + IfTypeISO88024Tokenbus IfType = 8 + IfTypeISO88025Tokenring IfType = 9 + IfTypeISO88026Man IfType = 10 + IfTypeStarlan IfType = 11 + IfTypeProteon10Mbit IfType = 12 + IfTypeProteon80Mbit IfType = 13 + IfTypeHyperchannel IfType = 14 + IfTypeFddi IfType = 15 + IfTypeLapB IfType = 16 + IfTypeSdlc IfType = 17 + IfTypeDs1 IfType = 18 // DS1-MIB + IfTypeE1 IfType = 19 // Obsolete; see DS1-MIB + IfTypeBasicISDN IfType = 20 + IfTypePrimaryISDN IfType = 21 + IfTypePropPoint2PointSerial IfType = 22 // proprietary serial + IfTypePPP IfType = 23 + IfTypeSoftwareLoopback IfType = 24 + IfTypeEon IfType = 25 // CLNP over IP + IfTypeEthernet3Mbit IfType = 26 + IfTypeNsip IfType = 27 // XNS over IP + IfTypeSlip IfType = 28 // Generic Slip + IfTypeUltra IfType = 29 // ULTRA Technologies + IfTypeDs3 IfType = 30 // DS3-MIB + IfTypeSip IfType = 31 // SMDS, coffee + IfTypeFramerelay IfType = 32 // DTE only + IfTypeRs232 IfType = 33 + IfTypePara IfType = 34 // Parallel port + IfTypeArcnet IfType = 35 + IfTypeArcnetPlus IfType = 36 + IfTypeAtm IfType = 37 // ATM cells + IfTypeMioX25 IfType = 38 + IfTypeSonet IfType = 39 // SONET or SDH + IfTypeX25Ple IfType = 40 + IfTypeIso88022LLC IfType = 41 + IfTypeLocaltalk IfType = 42 + IfTypeSmdsDxi IfType = 43 + IfTypeFramerelayService IfType = 44 // FRNETSERV-MIB + IfTypeV35 IfType = 45 + IfTypeHssi IfType = 46 + IfTypeHippi IfType = 47 + IfTypeModem IfType = 48 // Generic Modem + IfTypeAal5 IfType = 49 // AAL5 over ATM + IfTypeSonetPath IfType = 50 + IfTypeSonetVt IfType = 51 + IfTypeSmdsIcip IfType = 52 // SMDS InterCarrier Interface + IfTypePropVirtual IfType = 53 // Proprietary virtual/internal + IfTypePropMultiplexor IfType = 54 // Proprietary multiplexing + IfTypeIEEE80212 IfType = 55 // 100BaseVG + IfTypeFibrechannel IfType = 56 + IfTypeHippiinterface IfType = 57 + IfTypeFramerelayInterconnect IfType = 58 // Obsolete, use 32 or 44 + IfTypeAflane8023 IfType = 59 // ATM Emulated LAN for 802.3 + IfTypeAflane8025 IfType = 60 // ATM Emulated LAN for 802.5 + IfTypeCctemul IfType = 61 // ATM Emulated circuit + IfTypeFastether IfType = 62 // Fast Ethernet (100BaseT) + IfTypeISDN IfType = 63 // ISDN and X.25 + IfTypeV11 IfType = 64 // CCITT V.11/X.21 + IfTypeV36 IfType = 65 // CCITT V.36 + IfTypeG703_64k IfType = 66 // CCITT G703 at 64Kbps + IfTypeG703_2mb IfType = 67 // Obsolete; see DS1-MIB + IfTypeQllc IfType = 68 // SNA QLLC + IfTypeFastetherFX IfType = 69 // Fast Ethernet (100BaseFX) + IfTypeChannel IfType = 70 + IfTypeIEEE80211 IfType = 71 // Radio spread spectrum + IfTypeIBM370parchan IfType = 72 // IBM System 360/370 OEMI Channel + IfTypeEscon IfType = 73 // IBM Enterprise Systems Connection + IfTypeDlsw IfType = 74 // Data Link Switching + IfTypeISDNS IfType = 75 // ISDN S/T interface + IfTypeISDNU IfType = 76 // ISDN U interface + IfTypeLapD IfType = 77 // Link Access Protocol D + IfTypeIpswitch IfType = 78 // IP Switching Objects + IfTypeRsrb IfType = 79 // Remote Source Route Bridging + IfTypeAtmLogical IfType = 80 // ATM Logical Port + IfTypeDs0 IfType = 81 // Digital Signal Level 0 + IfTypeDs0Bundle IfType = 82 // Group of ds0s on the same ds1 + IfTypeBsc IfType = 83 // Bisynchronous Protocol + IfTypeAsync IfType = 84 // Asynchronous Protocol + IfTypeCnr IfType = 85 // Combat Net Radio + IfTypeIso88025rDtr IfType = 86 // ISO 802.5r DTR + IfTypeEplrs IfType = 87 // Ext Pos Loc Report Sys + IfTypeArap IfType = 88 // Appletalk Remote Access Protocol + IfTypePropCnls IfType = 89 // Proprietary Connectionless Proto + IfTypeHostpad IfType = 90 // CCITT-ITU X.29 PAD Protocol + IfTypeTermpad IfType = 91 // CCITT-ITU X.3 PAD Facility + IfTypeFramerelayMpi IfType = 92 // Multiproto Interconnect over FR + IfTypeX213 IfType = 93 // CCITT-ITU X213 + IfTypeAdsl IfType = 94 // Asymmetric Digital Subscrbr Loop + IfTypeRadsl IfType = 95 // Rate-Adapt Digital Subscrbr Loop + IfTypeSdsl IfType = 96 // Symmetric Digital Subscriber Loop + IfTypeVdsl IfType = 97 // Very H-Speed Digital Subscrb Loop + IfTypeIso88025Crfprint IfType = 98 // ISO 802.5 CRFP + IfTypeMyrinet IfType = 99 // Myricom Myrinet + IfTypeVoiceEm IfType = 100 // Voice recEive and transMit + IfTypeVoiceFxo IfType = 101 // Voice Foreign Exchange Office + IfTypeVoiceFxs IfType = 102 // Voice Foreign Exchange Station + IfTypeVoiceEncap IfType = 103 // Voice encapsulation + IfTypeVoiceOverip IfType = 104 // Voice over IP encapsulation + IfTypeAtmDxi IfType = 105 // ATM DXI + IfTypeAtmFuni IfType = 106 // ATM FUNI + IfTypeAtmIma IfType = 107 // ATM IMA + IfTypePPPmultilinkbundle IfType = 108 // PPP Multilink Bundle + IfTypeIpoverCdlc IfType = 109 // IBM ipOverCdlc + IfTypeIpoverClaw IfType = 110 // IBM Common Link Access to Workstn + IfTypeStacktostack IfType = 111 // IBM stackToStack + IfTypeVirtualipaddress IfType = 112 // IBM VIPA + IfTypeMpc IfType = 113 // IBM multi-proto channel support + IfTypeIpoverAtm IfType = 114 // IBM ipOverAtm + IfTypeIso88025Fiber IfType = 115 // ISO 802.5j Fiber Token Ring + IfTypeTdlc IfType = 116 // IBM twinaxial data link control + IfTypeGigabitethernet IfType = 117 + IfTypeHdlc IfType = 118 + IfTypeLapF IfType = 119 + IfTypeV37 IfType = 120 + IfTypeX25Mlp IfType = 121 // Multi-Link Protocol + IfTypeX25Huntgroup IfType = 122 // X.25 Hunt Group + IfTypeTransphdlc IfType = 123 + IfTypeInterleave IfType = 124 // Interleave channel + IfTypeFast IfType = 125 // Fast channel + IfTypeIP IfType = 126 // IP (for APPN HPR in IP networks) + IfTypeDocscableMaclayer IfType = 127 // CATV Mac Layer + IfTypeDocscableDownstream IfType = 128 // CATV Downstream interface + IfTypeDocscableUpstream IfType = 129 // CATV Upstream interface + IfTypeA12mppswitch IfType = 130 // Avalon Parallel Processor + IfTypeTunnel IfType = 131 // Encapsulation interface + IfTypeCoffee IfType = 132 // Coffee pot + IfTypeCes IfType = 133 // Circuit Emulation Service + IfTypeAtmSubinterface IfType = 134 // ATM Sub Interface + IfTypeL2Vlan IfType = 135 // Layer 2 Virtual LAN using 802.1Q + IfTypeL3Ipvlan IfType = 136 // Layer 3 Virtual LAN using IP + IfTypeL3Ipxvlan IfType = 137 // Layer 3 Virtual LAN using IPX + IfTypeDigitalpowerline IfType = 138 // IP over Power Lines + IfTypeMediamailoverip IfType = 139 // Multimedia Mail over IP + IfTypeDtm IfType = 140 // Dynamic syncronous Transfer Mode + IfTypeDcn IfType = 141 // Data Communications Network + IfTypeIpforward IfType = 142 // IP Forwarding Interface + IfTypeMsdsl IfType = 143 // Multi-rate Symmetric DSL + IfTypeIEEE1394 IfType = 144 // IEEE1394 High Perf Serial Bus + IfTypeIfGsn IfType = 145 + IfTypeDvbrccMaclayer IfType = 146 + IfTypeDvbrccDownstream IfType = 147 + IfTypeDvbrccUpstream IfType = 148 + IfTypeAtmVirtual IfType = 149 + IfTypeMplsTunnel IfType = 150 + IfTypeSrp IfType = 151 + IfTypeVoiceoveratm IfType = 152 + IfTypeVoiceoverframerelay IfType = 153 + IfTypeIdsl IfType = 154 + IfTypeCompositelink IfType = 155 + IfTypeSs7Siglink IfType = 156 + IfTypePropWirelessP2P IfType = 157 + IfTypeFrForward IfType = 158 + IfTypeRfc1483 IfType = 159 + IfTypeUsb IfType = 160 + IfTypeIEEE8023adLag IfType = 161 + IfTypeBgpPolicyAccounting IfType = 162 + IfTypeFrf16MfrBundle IfType = 163 + IfTypeH323Gatekeeper IfType = 164 + IfTypeH323Proxy IfType = 165 + IfTypeMpls IfType = 166 + IfTypeMfSiglink IfType = 167 + IfTypeHdsl2 IfType = 168 + IfTypeShdsl IfType = 169 + IfTypeDs1Fdl IfType = 170 + IfTypePos IfType = 171 + IfTypeDvbAsiIn IfType = 172 + IfTypeDvbAsiOut IfType = 173 + IfTypePlc IfType = 174 + IfTypeNfas IfType = 175 + IfTypeTr008 IfType = 176 + IfTypeGr303Rdt IfType = 177 + IfTypeGr303Idt IfType = 178 + IfTypeIsup IfType = 179 + IfTypePropDocsWirelessMaclayer IfType = 180 + IfTypePropDocsWirelessDownstream IfType = 181 + IfTypePropDocsWirelessUpstream IfType = 182 + IfTypeHiperlan2 IfType = 183 + IfTypePropBwaP2MP IfType = 184 + IfTypeSonetOverheadChannel IfType = 185 + IfTypeDigitalWrapperOverheadChannel IfType = 186 + IfTypeAal2 IfType = 187 + IfTypeRadioMac IfType = 188 + IfTypeAtmRadio IfType = 189 + IfTypeImt IfType = 190 + IfTypeMvl IfType = 191 + IfTypeReachDsl IfType = 192 + IfTypeFrDlciEndpt IfType = 193 + IfTypeAtmVciEndpt IfType = 194 + IfTypeOpticalChannel IfType = 195 + IfTypeOpticalTransport IfType = 196 + IfTypeIEEE80216Wman IfType = 237 + IfTypeWwanpp IfType = 243 // WWAN devices based on GSM technology + IfTypeWwanpp2 IfType = 244 // WWAN devices based on CDMA technology + IfTypeIEEE802154 IfType = 259 // IEEE 802.15.4 WPAN interface + IfTypeXboxWireless IfType = 281 ) // MibIfEntryLevel enumeration specifies level of interface information to retrieve in GetIfTable2Ex function call. @@ -270,7 +272,7 @@ type MibIfEntryLevel uint32 const ( MibIfEntryNormal MibIfEntryLevel = 0 - MibIfEntryNormalWithoutStatistics = 2 + MibIfEntryNormalWithoutStatistics MibIfEntryLevel = 2 ) // NdisMedium enumeration type identifies the medium types that NDIS drivers support. @@ -522,12 +524,12 @@ type TunnelType uint32 const ( TunnelTypeNone TunnelType = 0 - TunnelTypeOther = 1 - TunnelTypeDirect = 2 - TunnelType6to4 = 11 - TunnelTypeIsatap = 13 - TunnelTypeTeredo = 14 - TunnelTypeIPHTTPS = 15 + TunnelTypeOther TunnelType = 1 + TunnelTypeDirect TunnelType = 2 + TunnelType6to4 TunnelType = 11 + TunnelTypeIsatap TunnelType = 13 + TunnelTypeTeredo TunnelType = 14 + TunnelTypeIPHTTPS TunnelType = 15 ) // InterfaceAndOperStatusFlags enumeration type defines interface and operation flags @@ -574,13 +576,13 @@ type ScopeLevel uint32 const ( ScopeLevelInterface ScopeLevel = 1 - ScopeLevelLink = 2 - ScopeLevelSubnet = 3 - ScopeLevelAdmin = 4 - ScopeLevelSite = 5 - ScopeLevelOrganization = 8 - ScopeLevelGlobal = 14 - ScopeLevelCount = 16 + ScopeLevelLink ScopeLevel = 2 + ScopeLevelSubnet ScopeLevel = 3 + ScopeLevelAdmin ScopeLevel = 4 + ScopeLevelSite ScopeLevel = 5 + ScopeLevelOrganization ScopeLevel = 8 + ScopeLevelGlobal ScopeLevel = 14 + ScopeLevelCount ScopeLevel = 16 ) // RouteData structure describes a route to add @@ -757,7 +759,7 @@ func (addr *RawSockaddrInet) SetAddrPort(addrPort netip.AddrPort) error { addr4.Family = windows.AF_INET addr4.Addr = addrPort.Addr().As4() addr4.Port = htons(addrPort.Port()) - for i := 0; i < 8; i++ { + for i := range 8 { addr4.Zero[i] = 0 } return nil diff --git a/internal/winipcfg/types_32.go b/internal/winipcfg/types_32.go index 1a8d4443..bac06baa 100644 --- a/internal/winipcfg/types_32.go +++ b/internal/winipcfg/types_32.go @@ -1,4 +1,4 @@ -//go:build 386 || arm +//go:build windows && (386 || arm) /* SPDX-License-Identifier: MIT * diff --git a/internal/winipcfg/types_64.go b/internal/winipcfg/types_64.go index 3a1fe07f..13d3ab91 100644 --- a/internal/winipcfg/types_64.go +++ b/internal/winipcfg/types_64.go @@ -1,4 +1,4 @@ -//go:build amd64 || arm64 +//go:build windows && (amd64 || arm64) /* SPDX-License-Identifier: MIT * diff --git a/internal/winipcfg/types_test.go b/internal/winipcfg/types_test.go index b72d73f5..f51c8c96 100644 --- a/internal/winipcfg/types_test.go +++ b/internal/winipcfg/types_test.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. diff --git a/internal/winipcfg/types_test_32.go b/internal/winipcfg/types_test_32.go index 9e62bfef..b6e10923 100644 --- a/internal/winipcfg/types_test_32.go +++ b/internal/winipcfg/types_test_32.go @@ -1,4 +1,4 @@ -//go:build 386 || arm +//go:build windows && (386 || arm) /* SPDX-License-Identifier: MIT * diff --git a/internal/winipcfg/types_test_64.go b/internal/winipcfg/types_test_64.go index 8a181575..f94a88dd 100644 --- a/internal/winipcfg/types_test_64.go +++ b/internal/winipcfg/types_test_64.go @@ -1,4 +1,4 @@ -//go:build amd64 || arm64 +//go:build windows && (amd64 || arm64) /* SPDX-License-Identifier: MIT * diff --git a/internal/winipcfg/unicast_address_change_handler.go b/internal/winipcfg/unicast_address_change_handler.go index cf4fcb3a..0d80f1d1 100644 --- a/internal/winipcfg/unicast_address_change_handler.go +++ b/internal/winipcfg/unicast_address_change_handler.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. diff --git a/internal/winipcfg/winipcfg.go b/internal/winipcfg/winipcfg.go index e24157b9..7a460d77 100644 --- a/internal/winipcfg/winipcfg.go +++ b/internal/winipcfg/winipcfg.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. diff --git a/internal/winipcfg/winipcfg_test.go b/internal/winipcfg/winipcfg_test.go index b49daf33..1cba10b1 100644 --- a/internal/winipcfg/winipcfg_test.go +++ b/internal/winipcfg/winipcfg_test.go @@ -1,3 +1,5 @@ +//go:build windows + /* SPDX-License-Identifier: MIT * * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. @@ -104,6 +106,9 @@ func TestAdaptersAddresses(t *testing.T) { } ifcs, err = GetAdaptersAddresses(windows.AF_UNSPEC, GAAFlagDefault) + if err != nil { + t.Errorf("GetAdaptersAddresses() returned error: %v", err) + } for _, i := range ifcs { ifc, err := i.LUID.Interface() @@ -370,7 +375,7 @@ func TestAddDeleteIPAddress(t *testing.T) { return } - addr, err := ifc.LUID.IPAddress(nonexistantIPv4ToAdd.Addr()) + _, err = ifc.LUID.IPAddress(nonexistantIPv4ToAdd.Addr()) if err == nil { t.Errorf("Unicast address %s already exists. Please set nonexistantIPv4ToAdd appropriately.", nonexistantIPv4ToAdd.Addr().String()) return @@ -414,7 +419,7 @@ func TestAddDeleteIPAddress(t *testing.T) { if count != 1 { t.Errorf("After adding there are %d new interface(s).", count) } - addr, err = ifc.LUID.IPAddress(nonexistantIPv4ToAdd.Addr()) + addr, err := ifc.LUID.IPAddress(nonexistantIPv4ToAdd.Addr()) if err != nil { t.Errorf("LUID.IPAddress() returned an error: %w", err) } else if addr == nil { @@ -431,7 +436,7 @@ func TestAddDeleteIPAddress(t *testing.T) { time.Sleep(500 * time.Millisecond) - addr, err = ifc.LUID.IPAddress(nonexistantIPv4ToAdd.Addr()) + _, err = ifc.LUID.IPAddress(nonexistantIPv4ToAdd.Addr()) if err == nil { t.Errorf("Unicast address %s still exists, although it's deleted successfully.", nonexistantIPv4ToAdd.Addr().String()) } else if err != windows.ERROR_NOT_FOUND { diff --git a/internal/wintun/memmod/memmod_windows.go b/internal/wintun/memmod/memmod_windows.go index 985d48a1..0b345ba6 100644 --- a/internal/wintun/memmod/memmod_windows.go +++ b/internal/wintun/memmod/memmod_windows.go @@ -56,16 +56,16 @@ func (module *Module) copySections(address, size uintptr, oldHeaders *IMAGE_NT_H if sectionSize == 0 { continue } - dest, err := windows.VirtualAlloc(module.codeBase+uintptr(sections[i].VirtualAddress), + _, err := windows.VirtualAlloc(module.codeBase+uintptr(sections[i].VirtualAddress), uintptr(sectionSize), windows.MEM_COMMIT, windows.PAGE_READWRITE) if err != nil { - return fmt.Errorf("Error allocating section: %w", err) + return fmt.Errorf("error allocating section: %w", err) } // Always use position from file to support alignments smaller than page size (allocation above will align to page size). - dest = module.codeBase + uintptr(sections[i].VirtualAddress) + dest := module.codeBase + uintptr(sections[i].VirtualAddress) // NOTE: On 64bit systems we truncate to 32bit here but expand again later when "PhysicalAddress" is used. sections[i].SetPhysicalAddress((uint32)(dest & 0xffffffff)) dst := unsafe.Slice((*byte)(a2p(dest)), sectionSize) @@ -76,7 +76,7 @@ func (module *Module) copySections(address, size uintptr, oldHeaders *IMAGE_NT_H } if size < uintptr(sections[i].PointerToRawData)+uintptr(sections[i].SizeOfRawData) { - return errors.New("Incomplete section") + return errors.New("incomplete section") } // Commit memory block and copy data from dll. @@ -85,7 +85,7 @@ func (module *Module) copySections(address, size uintptr, oldHeaders *IMAGE_NT_H windows.MEM_COMMIT, windows.PAGE_READWRITE) if err != nil { - return fmt.Errorf("Error allocating memory block: %w", err) + return fmt.Errorf("error allocating memory block: %w", err) } // Always use position from file to support alignments smaller than page size (allocation above will align to page size). @@ -158,7 +158,7 @@ func (module *Module) finalizeSection(sectionData *sectionFinalizeData) error { var oldProtect uint32 err := windows.VirtualProtect(sectionData.address, sectionData.size, protect, &oldProtect) if err != nil { - return fmt.Errorf("Error protecting memory page: %w", err) + return fmt.Errorf("error protecting memory page: %w", err) } return nil @@ -204,7 +204,7 @@ func (module *Module) finalizeSections() error { err := module.finalizeSection(§ionData) if err != nil { - return fmt.Errorf("Error finalizing section: %w", err) + return fmt.Errorf("error finalizing section: %w", err) } sectionData.address = sectionAddress sectionData.alignedAddress = alignedAddress @@ -214,7 +214,7 @@ func (module *Module) finalizeSections() error { sectionData.last = true err := module.finalizeSection(§ionData) if err != nil { - return fmt.Errorf("Error finalizing section: %w", err) + return fmt.Errorf("error finalizing section: %w", err) } return nil } @@ -250,10 +250,10 @@ func (module *Module) performBaseRelocation(delta uintptr) (relocated bool, err relocationHdr := (*IMAGE_BASE_RELOCATION)(a2p(relocBase)) for uintptr(unsafe.Pointer(relocationHdr))+unsafe.Sizeof(*relocationHdr) <= relocEnd && relocationHdr.VirtualAddress > 0 { if uintptr(relocationHdr.SizeOfBlock) < unsafe.Sizeof(*relocationHdr) { - return false, errors.New("Invalid relocation block size") + return false, errors.New("invalid relocation block size") } if uintptr(unsafe.Pointer(relocationHdr))+uintptr(relocationHdr.SizeOfBlock) > relocEnd { - return false, errors.New("Relocation block exceeds directory bounds") + return false, errors.New("relocation block exceeds directory bounds") } dest := module.codeBase + uintptr(relocationHdr.VirtualAddress) @@ -272,11 +272,9 @@ func (module *Module) performBaseRelocation(delta uintptr) (relocated bool, err case IMAGE_REL_BASED_LOW: *(*uint16)(a2p(dest + relOffset)) += uint16(delta & 0xffff) - break case IMAGE_REL_BASED_HIGH: *(*uint16)(a2p(dest + relOffset)) += uint16(uint32(delta) >> 16) - break case IMAGE_REL_BASED_HIGHLOW: *(*uint32)(a2p(dest + relOffset)) += uint32(delta) @@ -289,7 +287,7 @@ func (module *Module) performBaseRelocation(delta uintptr) (relocated bool, err imm16 := ((inst << 1) & 0x0800) + ((inst << 12) & 0xf000) + ((inst >> 20) & 0x0700) + ((inst >> 16) & 0x00ff) if (inst & 0x8000fbf0) != 0x0000f240 { - return false, fmt.Errorf("Wrong Thumb2 instruction %08x, expected MOVW", inst) + return false, fmt.Errorf("wrong Thumb2 instruction %08x, expected MOVW", inst) } imm16 += uint32(delta) & 0xffff hiDelta := (uint32(delta&0xffff0000) >> 16) + ((imm16 & 0xffff0000) >> 16) @@ -302,11 +300,11 @@ func (module *Module) performBaseRelocation(delta uintptr) (relocated bool, err imm16 = ((inst << 1) & 0x0800) + ((inst << 12) & 0xf000) + ((inst >> 20) & 0x0700) + ((inst >> 16) & 0x00ff) if (inst & 0x8000fbf0) != 0x0000f2c0 { - return false, fmt.Errorf("Wrong Thumb2 instruction %08x, expected MOVT", inst) + return false, fmt.Errorf("wrong Thumb2 instruction %08x, expected MOVT", inst) } imm16 += hiDelta if imm16 > 0xffff { - return false, fmt.Errorf("Resulting immediate value won't fit: %08x", imm16) + return false, fmt.Errorf("resulting immediate value won't fit: %08x", imm16) } *(*uint32)(a2p(dest + relOffset + 4)) = (inst & 0x8f00fbf0) + ((imm16 >> 1) & 0x0400) + @@ -316,7 +314,7 @@ func (module *Module) performBaseRelocation(delta uintptr) (relocated bool, err } default: - return false, fmt.Errorf("Unsupported relocation: %v", relType) + return false, fmt.Errorf("unsupported relocation: %v", relType) } } @@ -337,7 +335,7 @@ func (module *Module) buildImportTable() error { for importDesc.Name != 0 { handle, err := windows.LoadLibraryEx(windows.BytePtrToString((*byte)(a2p(module.codeBase+uintptr(importDesc.Name)))), 0, windows.LOAD_LIBRARY_SEARCH_SYSTEM32) if err != nil { - return fmt.Errorf("Error loading module: %w", err) + return fmt.Errorf("error loading module: %w", err) } var thunkRef, funcRef *uintptr if importDesc.OriginalFirstThunk() != 0 { @@ -357,7 +355,7 @@ func (module *Module) buildImportTable() error { } if err != nil { windows.FreeLibrary(handle) - return fmt.Errorf("Error getting function address: %w", err) + return fmt.Errorf("error getting function address: %w", err) } thunkRef = (*uintptr)(a2p(uintptr(unsafe.Pointer(thunkRef)) + unsafe.Sizeof(*thunkRef))) funcRef = (*uintptr)(a2p(uintptr(unsafe.Pointer(funcRef)) + unsafe.Sizeof(*funcRef))) @@ -371,14 +369,14 @@ func (module *Module) buildImportTable() error { func (module *Module) buildNameExports() error { directory := module.headerDirectory(IMAGE_DIRECTORY_ENTRY_EXPORT) if directory.Size == 0 { - return errors.New("No export table found") + return errors.New("no export table found") } exports := (*IMAGE_EXPORT_DIRECTORY)(a2p(module.codeBase + uintptr(directory.VirtualAddress))) if exports.NumberOfNames == 0 || exports.NumberOfFunctions == 0 { - return errors.New("No functions exported") + return errors.New("no functions exported") } if exports.NumberOfNames == 0 { - return errors.New("No functions exported by name") + return errors.New("no functions exported by name") } nameRefs := unsafe.Slice((*uint32)(a2p(module.codeBase+uintptr(exports.AddressOfNames))), exports.NumberOfNames) ordinals := unsafe.Slice((*uint16)(a2p(module.codeBase+uintptr(exports.AddressOfNameOrdinals))), exports.NumberOfNames) @@ -466,39 +464,39 @@ func hookRtlPcToFileHeader() error { func LoadLibrary(data []byte) (module *Module, err error) { size := uintptr(len(data)) if size < unsafe.Sizeof(IMAGE_DOS_HEADER{}) { - return nil, errors.New("Incomplete IMAGE_DOS_HEADER") + return nil, errors.New("incomplete IMAGE_DOS_HEADER") } addr := uintptr(unsafe.Pointer(&data[0])) dosHeader := (*IMAGE_DOS_HEADER)(a2p(addr)) if dosHeader.E_magic != IMAGE_DOS_SIGNATURE { - return nil, fmt.Errorf("Not an MS-DOS binary (provided: %x, expected: %x)", dosHeader.E_magic, IMAGE_DOS_SIGNATURE) + return nil, fmt.Errorf("not an MS-DOS binary (provided: %x, expected: %x)", dosHeader.E_magic, IMAGE_DOS_SIGNATURE) } if dosHeader.E_lfanew < 0 || (size < uintptr(dosHeader.E_lfanew)+unsafe.Sizeof(IMAGE_NT_HEADERS{})) { - return nil, errors.New("Incomplete IMAGE_NT_HEADERS") + return nil, errors.New("incomplete IMAGE_NT_HEADERS") } oldHeader := (*IMAGE_NT_HEADERS)(a2p(addr + uintptr(dosHeader.E_lfanew))) if oldHeader.Signature != IMAGE_NT_SIGNATURE { - return nil, fmt.Errorf("Not an NT binary (provided: %x, expected: %x)", oldHeader.Signature, IMAGE_NT_SIGNATURE) + return nil, fmt.Errorf("not an NT binary (provided: %x, expected: %x)", oldHeader.Signature, IMAGE_NT_SIGNATURE) } if oldHeader.FileHeader.Machine != imageFileProcess { - return nil, fmt.Errorf("Foreign platform (provided: %x, expected: %x)", oldHeader.FileHeader.Machine, imageFileProcess) + return nil, fmt.Errorf("foreign platform (provided: %x, expected: %x)", oldHeader.FileHeader.Machine, imageFileProcess) } if oldHeader.OptionalHeader.SectionAlignment == 0 || (oldHeader.OptionalHeader.SectionAlignment&(oldHeader.OptionalHeader.SectionAlignment-1)) != 0 { - return nil, errors.New("Unaligned section") + return nil, errors.New("unaligned section") } if oldHeader.FileHeader.NumberOfSections == 0 { - return nil, errors.New("No sections") + return nil, errors.New("no sections") } if uintptr(oldHeader.FileHeader.SizeOfOptionalHeader) < unsafe.Sizeof(oldHeader.OptionalHeader) { - return nil, errors.New("Incomplete optional header") + return nil, errors.New("incomplete optional header") } if oldHeader.OptionalHeader.NumberOfRvaAndSizes < IMAGE_NUMBEROF_DIRECTORY_ENTRIES { - return nil, errors.New("Incomplete data directory") + return nil, errors.New("incomplete data directory") } sectionHeadersEnd := uintptr(dosHeader.E_lfanew) + unsafe.Offsetof(oldHeader.OptionalHeader) + uintptr(oldHeader.FileHeader.SizeOfOptionalHeader) + uintptr(oldHeader.FileHeader.NumberOfSections)*unsafe.Sizeof(IMAGE_SECTION_HEADER{}) if size < sectionHeadersEnd { - return nil, errors.New("Incomplete section headers") + return nil, errors.New("incomplete section headers") } lastSectionEnd := uintptr(0) sections := oldHeader.Sections() @@ -517,7 +515,7 @@ func LoadLibrary(data []byte) (module *Module, err error) { } alignedImageSize := alignUp(uintptr(oldHeader.OptionalHeader.SizeOfImage), uintptr(oldHeader.OptionalHeader.SectionAlignment)) if alignedImageSize != alignUp(lastSectionEnd, uintptr(oldHeader.OptionalHeader.SectionAlignment)) { - return nil, errors.New("Section is not page-aligned") + return nil, errors.New("section is not page-aligned") } module = &Module{isDLL: (oldHeader.FileHeader.Characteristics & IMAGE_FILE_DLL) != 0} @@ -541,18 +539,18 @@ func LoadLibrary(data []byte) (module *Module, err error) { windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) if err != nil { - err = fmt.Errorf("Error allocating code: %w", err) + err = fmt.Errorf("error allocating code: %w", err) return } } err = module.check4GBBoundaries(alignedImageSize) if err != nil { - err = fmt.Errorf("Error reallocating code: %w", err) + err = fmt.Errorf("error reallocating code: %w", err) return } if size < uintptr(oldHeader.OptionalHeader.SizeOfHeaders) { - err = errors.New("Incomplete headers") + err = errors.New("incomplete headers") return } // Commit memory for headers. @@ -561,7 +559,7 @@ func LoadLibrary(data []byte) (module *Module, err error) { windows.MEM_COMMIT, windows.PAGE_READWRITE) if err != nil { - err = fmt.Errorf("Error allocating headers: %w", err) + err = fmt.Errorf("error allocating headers: %w", err) return } // Copy PE header to code. @@ -574,7 +572,7 @@ func LoadLibrary(data []byte) (module *Module, err error) { // Copy sections from DLL file block to new memory location. err = module.copySections(addr, size, oldHeader) if err != nil { - err = fmt.Errorf("Error copying sections: %w", err) + err = fmt.Errorf("error copying sections: %w", err) return } @@ -583,7 +581,7 @@ func LoadLibrary(data []byte) (module *Module, err error) { if locationDelta != 0 { module.isRelocated, err = module.performBaseRelocation(locationDelta) if err != nil { - err = fmt.Errorf("Error relocating module: %w", err) + err = fmt.Errorf("error relocating module: %w", err) return } if !module.isRelocated { @@ -597,14 +595,14 @@ func LoadLibrary(data []byte) (module *Module, err error) { // Load required dlls and adjust function table of imports. err = module.buildImportTable() if err != nil { - err = fmt.Errorf("Error building import table: %w", err) + err = fmt.Errorf("error building import table: %w", err) return } // Mark memory pages depending on section headers and release sections that are marked as "discardable". err = module.finalizeSections() if err != nil { - err = fmt.Errorf("Error finalizing sections: %w", err) + err = fmt.Errorf("error finalizing sections: %w", err) return } @@ -673,35 +671,35 @@ func (module *Module) Free() { func (module *Module) ProcAddressByName(name string) (uintptr, error) { directory := module.headerDirectory(IMAGE_DIRECTORY_ENTRY_EXPORT) if directory.Size == 0 { - return 0, errors.New("No export table found") + return 0, errors.New("no export table found") } exports := (*IMAGE_EXPORT_DIRECTORY)(a2p(module.codeBase + uintptr(directory.VirtualAddress))) if module.nameExports == nil { - return 0, errors.New("No functions exported by name") + return 0, errors.New("no functions exported by name") } if idx, ok := module.nameExports[name]; ok { if uint32(idx) >= exports.NumberOfFunctions { - return 0, errors.New("Ordinal number too high") + return 0, errors.New("ordinal number too high") } // AddressOfFunctions contains the RVAs to the "real" functions. return module.codeBase + uintptr(*(*uint32)(a2p(module.codeBase + uintptr(exports.AddressOfFunctions) + uintptr(idx)*4))), nil } - return 0, errors.New("Function not found by name") + return 0, errors.New("function not found by name") } // ProcAddressByOrdinal returns function address by exported ordinal. func (module *Module) ProcAddressByOrdinal(ordinal uint16) (uintptr, error) { directory := module.headerDirectory(IMAGE_DIRECTORY_ENTRY_EXPORT) if directory.Size == 0 { - return 0, errors.New("No export table found") + return 0, errors.New("no export table found") } exports := (*IMAGE_EXPORT_DIRECTORY)(a2p(module.codeBase + uintptr(directory.VirtualAddress))) if uint32(ordinal) < exports.Base { - return 0, errors.New("Ordinal number too low") + return 0, errors.New("ordinal number too low") } idx := ordinal - uint16(exports.Base) if uint32(idx) >= exports.NumberOfFunctions { - return 0, errors.New("Ordinal number too high") + return 0, errors.New("ordinal number too high") } // AddressOfFunctions contains the RVAs to the "real" functions. return module.codeBase + uintptr(*(*uint32)(a2p(module.codeBase + uintptr(exports.AddressOfFunctions) + uintptr(idx)*4))), nil diff --git a/internal/wintun/memmod/memmod_windows_64.go b/internal/wintun/memmod/memmod_windows_64.go index a53851c6..b3efca92 100644 --- a/internal/wintun/memmod/memmod_windows_64.go +++ b/internal/wintun/memmod/memmod_windows_64.go @@ -29,7 +29,7 @@ func (module *Module) check4GBBoundaries(alignedImageSize uintptr) (err error) { windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) if err != nil { - return fmt.Errorf("Error allocating memory block: %w", err) + return fmt.Errorf("error allocating memory block: %w", err) } } return diff --git a/internal/wintun/session_windows.go b/internal/wintun/session_windows.go index f023baf7..306f3b12 100644 --- a/internal/wintun/session_windows.go +++ b/internal/wintun/session_windows.go @@ -40,7 +40,7 @@ var ( ) func (wintun *Adapter) StartSession(capacity uint32) (session Session, err error) { - r0, _, e1 := syscall.Syscall(procWintunStartSession.Addr(), 2, uintptr(wintun.handle), uintptr(capacity), 0) + r0, _, e1 := syscall.SyscallN(procWintunStartSession.Addr(), uintptr(wintun.handle), uintptr(capacity)) if r0 == 0 { err = e1 } else { @@ -50,19 +50,18 @@ func (wintun *Adapter) StartSession(capacity uint32) (session Session, err error } func (session Session) End() { - syscall.Syscall(procWintunEndSession.Addr(), 1, session.handle, 0, 0) - session.handle = 0 + syscall.SyscallN(procWintunEndSession.Addr(), session.handle) } func (session Session) ReadWaitEvent() (handle windows.Handle) { - r0, _, _ := syscall.Syscall(procWintunGetReadWaitEvent.Addr(), 1, session.handle, 0, 0) + r0, _, _ := syscall.SyscallN(procWintunGetReadWaitEvent.Addr(), session.handle) handle = windows.Handle(r0) return } func (session Session) ReceivePacket() (packet []byte, err error) { var packetSize uint32 - r0, _, e1 := syscall.Syscall(procWintunReceivePacket.Addr(), 2, session.handle, uintptr(unsafe.Pointer(&packetSize)), 0) + r0, _, e1 := syscall.SyscallN(procWintunReceivePacket.Addr(), session.handle, uintptr(unsafe.Pointer(&packetSize))) if r0 == 0 { err = e1 return @@ -72,11 +71,11 @@ func (session Session) ReceivePacket() (packet []byte, err error) { } func (session Session) ReleaseReceivePacket(packet []byte) { - syscall.Syscall(procWintunReleaseReceivePacket.Addr(), 2, session.handle, uintptr(unsafe.Pointer(&packet[0])), 0) + syscall.SyscallN(procWintunReleaseReceivePacket.Addr(), session.handle, uintptr(unsafe.Pointer(&packet[0]))) } func (session Session) AllocateSendPacket(packetSize int) (packet []byte, err error) { - r0, _, e1 := syscall.Syscall(procWintunAllocateSendPacket.Addr(), 2, session.handle, uintptr(packetSize), 0) + r0, _, e1 := syscall.SyscallN(procWintunAllocateSendPacket.Addr(), session.handle, uintptr(packetSize)) if r0 == 0 { err = e1 return @@ -86,5 +85,5 @@ func (session Session) AllocateSendPacket(packetSize int) (packet []byte, err er } func (session Session) SendPacket(packet []byte) { - syscall.Syscall(procWintunSendPacket.Addr(), 2, session.handle, uintptr(unsafe.Pointer(&packet[0])), 0) + syscall.SyscallN(procWintunSendPacket.Addr(), session.handle, uintptr(unsafe.Pointer(&packet[0]))) } diff --git a/internal/wintun/wintun_windows.go b/internal/wintun/wintun_windows.go index 288d364b..8087a1e3 100644 --- a/internal/wintun/wintun_windows.go +++ b/internal/wintun/wintun_windows.go @@ -30,7 +30,7 @@ var ( ) func closeAdapter(wintun *Adapter) { - syscall.SyscallN(procWintunCloseAdapter.Addr(), 1, wintun.handle, 0, 0) + syscall.SyscallN(procWintunCloseAdapter.Addr(), wintun.handle) } // CreateAdapter creates a Wintun adapter. name is the cosmetic name of the adapter. @@ -53,7 +53,7 @@ func CreateAdapter(name string, tunnelType string, requestedGUID *windows.GUID) if err != nil { return } - r0, _, e1 := syscall.Syscall(procWintunCreateAdapter.Addr(), 3, uintptr(unsafe.Pointer(name16)), uintptr(unsafe.Pointer(tunnelType16)), uintptr(unsafe.Pointer(requestedGUID))) + r0, _, e1 := syscall.SyscallN(procWintunCreateAdapter.Addr(), uintptr(unsafe.Pointer(name16)), uintptr(unsafe.Pointer(tunnelType16)), uintptr(unsafe.Pointer(requestedGUID))) if r0 == 0 { err = e1 return @@ -70,7 +70,7 @@ func OpenAdapter(name string) (wintun *Adapter, err error) { if err != nil { return } - r0, _, e1 := syscall.Syscall(procWintunOpenAdapter.Addr(), 1, uintptr(unsafe.Pointer(name16)), 0, 0) + r0, _, e1 := syscall.SyscallN(procWintunOpenAdapter.Addr(), uintptr(unsafe.Pointer(name16))) if r0 == 0 { err = e1 return @@ -83,7 +83,7 @@ func OpenAdapter(name string) (wintun *Adapter, err error) { // Close closes a Wintun adapter. func (wintun *Adapter) Close() (err error) { runtime.SetFinalizer(wintun, nil) - r1, _, e1 := syscall.Syscall(procWintunCloseAdapter.Addr(), 1, wintun.handle, 0, 0) + r1, _, e1 := syscall.SyscallN(procWintunCloseAdapter.Addr(), wintun.handle) if r1 == 0 { err = e1 } @@ -92,7 +92,7 @@ func (wintun *Adapter) Close() (err error) { // Uninstall removes the driver from the system if no drivers are currently in use. func Uninstall() (err error) { - r1, _, e1 := syscall.Syscall(procWintunDeleteDriver.Addr(), 0, 0, 0, 0) + r1, _, e1 := syscall.SyscallN(procWintunDeleteDriver.Addr()) if r1 == 0 { err = e1 } @@ -101,7 +101,7 @@ func Uninstall() (err error) { // RunningVersion returns the version of the running Wintun driver. func RunningVersion() (version uint32, err error) { - r0, _, e1 := syscall.Syscall(procWintunGetRunningDriverVersion.Addr(), 0, 0, 0, 0) + r0, _, e1 := syscall.SyscallN(procWintunGetRunningDriverVersion.Addr()) version = uint32(r0) if version == 0 { err = e1 @@ -111,6 +111,6 @@ func RunningVersion() (version uint32, err error) { // LUID returns the LUID of the adapter. func (wintun *Adapter) LUID() (luid uint64) { - syscall.Syscall(procWintunGetAdapterLUID.Addr(), 2, uintptr(wintun.handle), uintptr(unsafe.Pointer(&luid)), 0) + syscall.SyscallN(procWintunGetAdapterLUID.Addr(), uintptr(wintun.handle), uintptr(unsafe.Pointer(&luid))) return } diff --git a/nfqueue_linux.go b/nfqueue_linux.go index baaefb54..ad16dc46 100644 --- a/nfqueue_linux.go +++ b/nfqueue_linux.go @@ -173,7 +173,8 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int { var tcpOffset int version := payload[0] >> 4 - if version == 4 { + switch version { + case 4: ipv4 := header.IPv4(payload) if !ipv4.IsValid(len(payload)) || ipv4.Protocol() != uint8(unix.IPPROTO_TCP) { h.setVerdict(packetID, nfqueue.NfAccept, 0) @@ -182,7 +183,7 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int { srcAddr = M.SocksaddrFrom(ipv4.SourceAddr(), 0) dstAddr = M.SocksaddrFrom(ipv4.DestinationAddr(), 0) tcpOffset = int(ipv4.HeaderLength()) - } else if version == 6 { + case 6: transportProto, transportOffset, ok := parseIPv6TransportHeader(payload) if !ok || transportProto != unix.IPPROTO_TCP { h.setVerdict(packetID, nfqueue.NfAccept, 0) @@ -192,7 +193,7 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int { srcAddr = M.SocksaddrFrom(ipv6.SourceAddr(), 0) dstAddr = M.SocksaddrFrom(ipv6.DestinationAddr(), 0) tcpOffset = transportOffset - } else { + default: h.setVerdict(packetID, nfqueue.NfAccept, 0) return 0 } diff --git a/stack.go b/stack.go index a6f6043a..d128feb1 100644 --- a/stack.go +++ b/stack.go @@ -68,7 +68,7 @@ func NewStack( func HasNextAddress(prefix netip.Prefix, count int) bool { checkAddr := prefix.Addr() - for i := 0; i < count; i++ { + for range count { checkAddr = checkAddr.Next() } return prefix.Contains(checkAddr) diff --git a/stack_system.go b/stack_system.go index 030eee17..031e87f5 100644 --- a/stack_system.go +++ b/stack_system.go @@ -131,7 +131,7 @@ func (s *System) start() error { var tcpListener net.Listener var err error if s.inet4NextAddress.IsValid() { - for i := 0; i < 3; i++ { + for range 3 { tcpListener, err = listener.Listen(s.ctx, "tcp4", net.JoinHostPort(s.inet4Address.String(), "0")) if !retryableListenError(err) { break @@ -146,7 +146,7 @@ func (s *System) start() error { go s.acceptLoop(tcpListener) } if s.inet6NextAddress.IsValid() { - for i := 0; i < 3; i++ { + for range 3 { tcpListener, err = listener.Listen(s.ctx, "tcp6", net.JoinHostPort(s.inet6Address.String(), "0")) if !retryableListenError(err) { break @@ -245,7 +245,7 @@ func (s *System) batchLoopLinux(linuxTUN LinuxTUN, batchSize int) { if n == 0 { continue } - for i := 0; i < n; i++ { + for i := range n { packetSize := packetSizes[i] if packetSize < header.IPv4MinimumSize { continue diff --git a/tun_darwin.go b/tun_darwin.go index 8aa6923f..fe1420e3 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -10,7 +10,6 @@ import ( "unsafe" "github.com/sagernet/sing-tun/internal/gtcpip/header" - "github.com/sagernet/sing-tun/internal/rawfile_darwin" "github.com/sagernet/sing-tun/internal/stopfd_darwin" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" @@ -132,7 +131,7 @@ func New(options Options) (Tun, error) { stopFd: common.Must1(stopfd.New()), sendMsgX: options.EXP_SendMsgX, } - for i := 0; i < batchSize; i++ { + for i := range batchSize { nativeTun.iovecs[i] = newIovecBuffer(int(options.MTU)) nativeTun.iovecsOutput[i] = newIovecBuffer(int(options.MTU)) } @@ -352,7 +351,7 @@ func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) { } n, errno := rawfile.BlockingRecvMMsgUntilStopped(t.stopFd.ReadFD, t.tunFd, t.msgHdrs) if errno != 0 { - for k := 0; k < n; k++ { + for k := range n { t.iovecs[k].buffer.Release() t.iovecs[k].buffer = nil } @@ -366,7 +365,7 @@ func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) { return nil, nil } buffers := t.buffers - for k := 0; k < n; k++ { + for k := range n { buffer := t.iovecs[k].buffer t.iovecs[k].buffer = nil buffer.Truncate(int(t.msgHdrs[k].DataLen) - PacketOffset) diff --git a/tun_linux.go b/tun_linux.go index 20fdce23..9cd6300c 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -42,7 +42,6 @@ type NativeTun struct { vnetHdrWriteBuf []byte gsoToWrite []int tcpGROTable *tcpGROTable - udpGroAccess sync.Mutex udpGROTable *udpGROTable gro groDisablementFlags txChecksumOffload bool diff --git a/tun_offload.go b/tun_offload.go index a0eee82f..c76aeacf 100644 --- a/tun_offload.go +++ b/tun_offload.go @@ -9,11 +9,6 @@ import ( "github.com/sagernet/sing-tun/internal/gtcpip/header" ) -const ( - gsoMaxSize = 65536 - idealBatchSize = 128 -) - // GSOType represents the type of segmentation offload. type GSOType int @@ -167,10 +162,7 @@ func GSOSplit(in []byte, options GSOOptions, outBufs [][]byte, sizes []int, outO if i == len(outBufs) { return i - 1, ErrTooManySegments } - nextSegmentEnd := nextSegmentDataAt + int(options.GSOSize) - if nextSegmentEnd > len(in) { - nextSegmentEnd = len(in) - } + nextSegmentEnd := min(nextSegmentDataAt+int(options.GSOSize), len(in)) segmentDataLen := nextSegmentEnd - nextSegmentDataAt totalLen := int(options.HdrLen) + segmentDataLen sizes[i] = totalLen diff --git a/tun_offload_linux.go b/tun_offload_linux.go index 77337607..07123419 100644 --- a/tun_offload_linux.go +++ b/tun_offload_linux.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "io" + "slices" "unsafe" "github.com/sagernet/sing-tun/internal/gtcpip" @@ -20,6 +21,11 @@ import ( "golang.org/x/sys/unix" ) +const ( + gsoMaxSize = 65536 + idealBatchSize = 128 +) + // virtioNetHdr is defined in the kernel in include/uapi/linux/virtio_net.h. The // kernel symbol is virtio_net_hdr. type virtioNetHdr struct { @@ -606,7 +612,7 @@ func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) if !existing { return groResultTableInsert } - for i := len(items) - 1; i >= 0; i-- { + for i, item := range slices.Backward(items) { // In the best case of packets arriving in order iterating in reverse is // more efficient if there are multiple items for a given flow. This // also enables a natural table.deleteAt() in the @@ -615,7 +621,6 @@ func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) // unordered packets, where pkt may land anywhere in items from a // sequence number perspective, however once an item is inserted into // the table it is never compared across other items later. - item := items[i] can := tcpPacketsCanCoalesce(pkt, uint8(iphLen), uint8(tcphLen), seq, pshSet, gsoSize, item, bufs, offset) if can != coalesceUnavailable { result := coalesceTCPPackets(can, pkt, pktI, gsoSize, seq, pshSet, &item, bufs, offset, isV6) @@ -793,7 +798,8 @@ func packetIsGROCandidate(b []byte, gro groDisablementFlags) groCandidateType { if len(b) < 28 { return notGROCandidate } - if b[0]>>4 == 4 { + switch b[0] >> 4 { + case 4: if b[0]&0x0F != 5 { // IPv4 packets w/IP options do not coalesce return notGROCandidate @@ -804,7 +810,7 @@ func packetIsGROCandidate(b []byte, gro groDisablementFlags) groCandidateType { if b[9] == unix.IPPROTO_UDP && gro.canUDPGRO() { return udp4GROCandidate } - } else if b[0]>>4 == 6 { + case 6: if b[6] == unix.IPPROTO_TCP && len(b) >= 60 && gro.canTCPGRO() { return tcp6GROCandidate } From ebb52fb649a2a1e834594e259568c8d1731c3cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 22 Jun 2026 16:16:12 +0800 Subject: [PATCH 02/24] ping: Fix missing TTL and ICMP error handling --- ping/destination.go | 73 ++++++++++++++++++++++++++++++-------- ping/ping.go | 80 ++++++++++++++++++++++++++++++------------ ping/socket_unix.go | 35 +++++++++++------- ping/socket_windows.go | 23 +++++++----- tun_darwin.go | 1 + 5 files changed, 153 insertions(+), 59 deletions(-) diff --git a/ping/destination.go b/ping/destination.go index 8648ecc8..e16763b0 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -105,23 +105,29 @@ func (d *Destination) loopRead() { } icmpHdr := header.ICMPv4(ipHdr.Payload()) if d.needFilter() { - if icmpHdr.Type() != header.ICMPv4EchoReply { - continue - } - var requestExists bool - request := pingRequest{Source: ipHdr.DestinationAddr(), Destination: ipHdr.SourceAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()} - d.requestAccess.Lock() - _, loaded := d.requests[request] - if loaded { - requestExists = true - delete(d.requests, request) - } - d.requestAccess.Unlock() - if !requestExists { + switch icmpHdr.Type() { + case header.ICMPv4EchoReply: + request := pingRequest{Source: ipHdr.DestinationAddr(), Destination: ipHdr.SourceAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()} + d.requestAccess.Lock() + _, loaded := d.requests[request] + if loaded { + delete(d.requests, request) + } + d.requestAccess.Unlock() + if !loaded { + continue + } + d.logger.TraceContext(d.ctx, "read ICMPv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) + case header.ICMPv4TimeExceeded, header.ICMPv4DstUnreachable: + if !d.rewriteICMPv4Error(ipHdr, icmpHdr) { + continue + } + default: continue } + } else { + d.logger.TraceContext(d.ctx, "read ICMPv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } - d.logger.TraceContext(d.ctx, "read ICMPv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } else { ipHdr := header.IPv6(buffer.Bytes()) if !ipHdr.IsValid(buffer.Len()) { @@ -191,6 +197,45 @@ func (d *Destination) WritePacket(packet *buf.Buffer) error { return d.conn.WriteIP(packet) } +func (d *Destination) rewriteICMPv4Error(ipHdr header.IPv4, icmpHdr header.ICMPv4) bool { + inner := icmpHdr.Payload() + if len(inner) < header.IPv4MinimumSize { + return false + } + innerIPHdr := header.IPv4(inner) + headerLen := int(innerIPHdr.HeaderLength()) + if headerLen < header.IPv4MinimumSize || len(inner) < headerLen+header.ICMPv4MinimumSize { + return false + } + if innerIPHdr.TransportProtocol() != header.ICMPv4ProtocolNumber { + return false + } + innerICMP := header.ICMPv4(inner[headerLen:]) + if innerICMP.Type() != header.ICMPv4Echo { + return false + } + originalIdent := ^innerICMP.Ident() + request := pingRequest{ + Source: ipHdr.DestinationAddr(), + Destination: innerIPHdr.DestinationAddr(), + Identifier: originalIdent, + Sequence: innerICMP.Sequence(), + } + d.requestAccess.Lock() + _, loaded := d.requests[request] + d.requestAccess.Unlock() + if !loaded { + return false + } + innerICMP.SetIdent(originalIdent) + innerICMP.SetChecksum(header.ICMPv4Checksum(innerICMP, 0)) + innerIPHdr.SetSourceAddr(ipHdr.DestinationAddr()) + innerIPHdr.SetChecksum(^innerIPHdr.CalculateChecksum()) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) + d.logger.TraceContext(d.ctx, "read ICMPv4 error type ", int(icmpHdr.Type()), " from ", ipHdr.SourceAddr(), " seq ", innerICMP.Sequence()) + return true +} + func (d *Destination) needFilter() bool { return !d.conn.isLinuxUnprivileged() } diff --git a/ping/ping.go b/ping/ping.go index 248987c2..d8554249 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -158,9 +158,19 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { }) } } else { - _, err := buffer.ReadOnceFrom(c.conn) - if err != nil { - return err + if runtime.GOOS == "linux" || runtime.GOOS == "android" || runtime.GOOS == "windows" { + // An unconnected SOCK_RAW IPv4 socket delivers the full packet including the IP + // header via ReadMsgIP, whereas ReadFrom strips it. + n, _, _, err := c.readMsg(buffer.FreeBytes(), nil) + if err != nil { + return err + } + buffer.Truncate(n) + } else { + _, err := buffer.ReadOnceFrom(c.conn) + if err != nil { + return err + } } if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) @@ -177,10 +187,12 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { ipHdr.SetDestinationAddr(c.source.Load()) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) icmpHdr := header.ICMPv4(ipHdr.Payload()) - if !c.isLinuxUnprivileged() { - icmpHdr.SetIdent(^icmpHdr.Ident()) + if icmpHdr.Type() == header.ICMPv4EchoReply { + if !c.isLinuxUnprivileged() { + icmpHdr.SetIdent(^icmpHdr.Ident()) + } + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) } - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) } else { ipHdr := header.IPv6(buffer.Bytes()) if !ipHdr.IsValid(buffer.Len()) { @@ -202,27 +214,41 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { } func (c *Conn) ReadICMP(buffer *buf.Buffer) error { + if !c.isLinuxUnprivileged() && !c.destination.Is6() { + if runtime.GOOS == "linux" || runtime.GOOS == "android" || runtime.GOOS == "windows" { + // An unconnected SOCK_RAW IPv4 socket delivers the full packet including the IP + // header via ReadMsgIP, whereas ReadFrom strips it. + n, _, _, err := c.readMsg(buffer.FreeBytes(), nil) + if err != nil { + return err + } + buffer.Truncate(n) + } else { + _, err := buffer.ReadOnceFrom(c.conn) + if err != nil { + return err + } + } + ipHdr := header.IPv4(buffer.Bytes()) + buffer.Advance(int(ipHdr.HeaderLength())) + + icmpHdr := header.ICMPv4(buffer.Bytes()) + icmpHdr.SetIdent(^icmpHdr.Ident()) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) + return nil + } _, err := buffer.ReadOnceFrom(c.conn) if err != nil { return err } - if !c.isLinuxUnprivileged() { - if !c.destination.Is6() { - ipHdr := header.IPv4(buffer.Bytes()) - buffer.Advance(int(ipHdr.HeaderLength())) - - icmpHdr := header.ICMPv4(buffer.Bytes()) - icmpHdr.SetIdent(^icmpHdr.Ident()) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) - } else { - icmpHdr := header.ICMPv6(buffer.Bytes()) - icmpHdr.SetIdent(^icmpHdr.Ident()) - icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr, - Src: c.destination.AsSlice(), - Dst: c.source.Load().AsSlice(), - })) - } + if c.destination.Is6() && !c.isLinuxUnprivileged() { + icmpHdr := header.ICMPv6(buffer.Bytes()) + icmpHdr.SetIdent(^icmpHdr.Ident()) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: c.destination.AsSlice(), + Dst: c.source.Load().AsSlice(), + })) } return nil } @@ -232,6 +258,10 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) if !c.isLinuxUnprivileged() { + err := ipv4.NewConn(c.conn).SetTTL(int(ipHdr.TTL())) + if err != nil { + return err + } icmpHdr := header.ICMPv4(ipHdr.Payload()) icmpHdr.SetIdent(^icmpHdr.Ident()) icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) @@ -241,6 +271,10 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { } else { ipHdr := header.IPv6(buffer.Bytes()) if !c.isLinuxUnprivileged() { + err := ipv6.NewConn(c.conn).SetHopLimit(int(ipHdr.HopLimit())) + if err != nil { + return err + } icmpHdr := header.ICMPv6(ipHdr.Payload()) icmpHdr.SetIdent(^icmpHdr.Ident()) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ diff --git a/ping/socket_unix.go b/ping/socket_unix.go index 1eec10a5..91282390 100644 --- a/ping/socket_unix.go +++ b/ping/socket_unix.go @@ -87,24 +87,33 @@ func connect(privileged bool, controlFunc control.Func, destination netip.Addr) return nil, err } - if runtime.GOOS == "darwin" && !privileged { - // When running in NetworkExtension on macOS, write to connected socket results in EPIPE. + useUnconnected := (runtime.GOOS == "darwin" && !privileged) || + ((runtime.GOOS == "linux" || runtime.GOOS == "android") && privileged) + if useUnconnected { + // A connected ICMP socket only receives messages whose source is the connected peer, + // so the Time Exceeded replies that transit routers send for traceroute never reach it. + // Additionally, on macOS NetworkExtension, writing to a connected socket returns EPIPE. var packetConn net.PacketConn packetConn, err = net.FilePacketConn(file) if err != nil { return nil, err } - return bufio.NewBindPacketConn(packetConn, M.SocksaddrFrom(destination, 0).UDPAddr()), nil - } else { - err = unix.Connect(fd, M.AddrPortToSockaddr(netip.AddrPortFrom(destination, 0))) - if err != nil { - return nil, err - } - var conn net.Conn - conn, err = net.FileConn(file) - if err != nil { - return nil, err + var writeTarget net.Addr + if privileged { + writeTarget = M.SocksaddrFrom(destination, 0).IPAddr() + } else { + writeTarget = M.SocksaddrFrom(destination, 0).UDPAddr() } - return conn, nil + return bufio.NewBindPacketConn(packetConn, writeTarget), nil + } + err = unix.Connect(fd, M.AddrPortToSockaddr(netip.AddrPortFrom(destination, 0))) + if err != nil { + return nil, err + } + var conn net.Conn + conn, err = net.FileConn(file) + if err != nil { + return nil, err } + return conn, nil } diff --git a/ping/socket_windows.go b/ping/socket_windows.go index daafd18a..332a2513 100644 --- a/ping/socket_windows.go +++ b/ping/socket_windows.go @@ -1,30 +1,29 @@ package ping import ( + "context" "net" "net/netip" "syscall" + "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/control" + M "github.com/sagernet/sing/common/metadata" "golang.org/x/sys/windows" ) func connect(privileged bool, controlFunc control.Func, destination netip.Addr) (net.Conn, error) { - var dialer net.Dialer - dialer.Control = controlFunc + var listenConfig net.ListenConfig + listenConfig.Control = controlFunc if destination.Is6() { - dialer.Control = control.Append(dialer.Control, func(network, address string, conn syscall.RawConn) error { + listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error { return control.Raw(conn, func(fd uintptr) error { err := windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_HOPLIMIT, 1) if err != nil { return err } - err = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_RECVTCLASS, 1) - if err != nil { - return err - } - return nil + return windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_RECVTCLASS, 1) }) }) } @@ -34,5 +33,11 @@ func connect(privileged bool, controlFunc control.Func, destination netip.Addr) } else { network = "ip6:ipv6-icmp" } - return dialer.Dial(network, destination.String()) + // A connected raw socket only receives messages from the connected peer, so transit routers' + // Time Exceeded replies needed by traceroute never arrive. + packetConn, err := listenConfig.ListenPacket(context.Background(), network, "") + if err != nil { + return nil, err + } + return bufio.NewBindPacketConn(packetConn, M.SocksaddrFrom(destination, 0).IPAddr()), nil } diff --git a/tun_darwin.go b/tun_darwin.go index fe1420e3..31312955 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -10,6 +10,7 @@ import ( "unsafe" "github.com/sagernet/sing-tun/internal/gtcpip/header" + rawfile "github.com/sagernet/sing-tun/internal/rawfile_darwin" "github.com/sagernet/sing-tun/internal/stopfd_darwin" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" From 53208c850ec23462132b779af8ad6fa784787a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 29 Jun 2026 10:04:52 +0800 Subject: [PATCH 03/24] Fix ping --- ping/ping.go | 7 ++- ping/ping_test.go | 114 +++++++++++++++++++++++++++++++++++++++++++ stack_gvisor.go | 2 +- stack_gvisor_icmp.go | 25 +++++++--- 4 files changed, 137 insertions(+), 11 deletions(-) diff --git a/ping/ping.go b/ping/ping.go index d8554249..81ac6969 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -24,6 +24,7 @@ type Conn struct { ctx context.Context privileged bool conn net.Conn + controlConn net.Conn destination netip.Addr source common.TypedValue[netip.Addr] closed atomic.Bool @@ -53,6 +54,7 @@ func (c *Conn) connect(controlFunc control.Func, idleTimeout time.Duration) (err return err } if ipConn, isIPConn := common.Cast[*net.IPConn](c.conn); isIPConn { + c.controlConn = ipConn c.readMsg = func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) { var ipAddr *net.IPAddr n, oobn, _, ipAddr, err = ipConn.ReadMsgIP(b, oob) @@ -62,6 +64,7 @@ func (c *Conn) connect(controlFunc control.Func, idleTimeout time.Duration) (err return } } else if udpConn, isUDPConn := common.Cast[*net.UDPConn](c.conn); isUDPConn { + c.controlConn = udpConn c.readMsg = func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) { var addrPort netip.AddrPort n, oobn, _, addrPort, err = udpConn.ReadMsgUDPAddrPort(b, oob) @@ -258,7 +261,7 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) if !c.isLinuxUnprivileged() { - err := ipv4.NewConn(c.conn).SetTTL(int(ipHdr.TTL())) + err := ipv4.NewConn(c.controlConn).SetTTL(int(ipHdr.TTL())) if err != nil { return err } @@ -271,7 +274,7 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { } else { ipHdr := header.IPv6(buffer.Bytes()) if !c.isLinuxUnprivileged() { - err := ipv6.NewConn(c.conn).SetHopLimit(int(ipHdr.HopLimit())) + err := ipv6.NewConn(c.controlConn).SetHopLimit(int(ipHdr.HopLimit())) if err != nil { return err } diff --git a/ping/ping_test.go b/ping/ping_test.go index 5a04be17..9fef5bf5 100644 --- a/ping/ping_test.go +++ b/ping/ping_test.go @@ -30,6 +30,9 @@ func TestPing(t *testing.T) { t.Run("read-ip", func(t *testing.T) { testPingIPv4ReadIP(t, false, addr4) }) + t.Run("write-ip", func(t *testing.T) { + testPingIPv4WriteIP(t, false, addr4) + }) }) t.Run("privileged", func(t *testing.T) { if runtime.GOOS != "windows" && os.Getuid() != 0 { @@ -41,6 +44,9 @@ func TestPing(t *testing.T) { t.Run("read-ip", func(t *testing.T) { testPingIPv4ReadIP(t, true, addr4) }) + t.Run("write-ip", func(t *testing.T) { + testPingIPv4WriteIP(t, true, addr4) + }) }) }) // const addr6 = "2606:4700:4700::1001" @@ -56,6 +62,9 @@ func TestPing(t *testing.T) { t.Run("read-ip", func(t *testing.T) { testPingIPv6ReadIP(t, false, addr6) }) + t.Run("write-ip", func(t *testing.T) { + testPingIPv6WriteIP(t, false, addr6) + }) }) t.Run("privileged", func(t *testing.T) { if runtime.GOOS != "windows" && os.Getuid() != 0 { @@ -67,6 +76,9 @@ func TestPing(t *testing.T) { t.Run("read-ip", func(t *testing.T) { testPingIPv6ReadIP(t, true, addr6) }) + t.Run("write-ip", func(t *testing.T) { + testPingIPv6WriteIP(t, true, addr6) + }) }) }) } @@ -196,3 +208,105 @@ func testPingIPv6ReadICMP(t *testing.T, privileged bool, addr string) { require.Equal(t, header.ICMPv6EchoReply, icmpHdr.Type()) require.Equal(t, request.Ident(), icmpHdr.Ident()) } + +// testPingIPv4WriteIP exercises the WriteIP send path, which is what the real +// TUN flow uses (Destination.WritePacket -> Conn.WriteIP). Unlike the ReadIP/ +// ReadICMP tests, which send via WriteICMP, this path runs the SetTTL call that +// regressed in ebb52fb: on privileged Linux / unprivileged macOS the socket is +// wrapped in a BindPacketConn that does not expose SyscallConn, so +// ipv4.NewConn(c.conn).SetTTL returned "invalid connection" and the echo +// request was never sent. +func testPingIPv4WriteIP(t *testing.T, privileged bool, addr string) { + conn, err := ping.Connect(context.Background(), privileged, nil, netip.MustParseAddr(addr), 0) + if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { + t.SkipNow() + } + require.NoError(t, err) + defer conn.Close() + + ident := uint16(rand.Uint32()) + const totalLen = header.IPv4MinimumSize + header.ICMPv4MinimumSize + packet := buf.NewSize(totalLen) + ipHdr := header.IPv4(packet.Extend(totalLen)) + ipHdr.Encode(&header.IPv4Fields{ + TotalLength: totalLen, + TTL: 64, + Protocol: uint8(header.ICMPv4ProtocolNumber), + SrcAddr: netip.MustParseAddr("127.0.0.1"), + DstAddr: netip.MustParseAddr(addr), + }) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + icmpHdr := header.ICMPv4(ipHdr.Payload()) + icmpHdr.SetType(header.ICMPv4Echo) + icmpHdr.SetIdent(ident) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) + + conn.SetLocalAddr(netip.MustParseAddr("127.0.0.1")) + err = conn.WriteIP(packet) + require.NoError(t, err, "WriteIP must send the echo request") + + require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second))) + response := buf.NewPacket() + defer response.Release() + err = conn.ReadIP(response) + require.NoError(t, err) + if runtime.GOOS == "linux" && privileged { + response.Reset() + err = conn.ReadIP(response) + require.NoError(t, err) + } + respIP := header.IPv4(response.Bytes()) + require.NotZero(t, respIP.TTL()) + respICMP := header.ICMPv4(respIP.Payload()) + require.Equal(t, header.ICMPv4EchoReply, respICMP.Type()) + require.Equal(t, ident, respICMP.Ident()) +} + +func testPingIPv6WriteIP(t *testing.T, privileged bool, addr string) { + conn, err := ping.Connect(context.Background(), privileged, nil, netip.MustParseAddr(addr), 0) + if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { + t.SkipNow() + } + require.NoError(t, err) + defer conn.Close() + + ident := uint16(rand.Uint32()) + const payloadLen = header.ICMPv6MinimumSize + packet := buf.NewSize(header.IPv6MinimumSize + payloadLen) + ipHdr := header.IPv6(packet.Extend(header.IPv6MinimumSize + payloadLen)) + ipHdr.Encode(&header.IPv6Fields{ + PayloadLength: payloadLen, + TransportProtocol: header.ICMPv6ProtocolNumber, + HopLimit: 64, + SrcAddr: netip.MustParseAddr("::1"), + DstAddr: netip.MustParseAddr(addr), + }) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + icmpHdr.SetType(header.ICMPv6EchoRequest) + icmpHdr.SetIdent(ident) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: ipHdr.SourceAddressSlice(), + Dst: ipHdr.DestinationAddressSlice(), + })) + + conn.SetLocalAddr(netip.MustParseAddr("::1")) + err = conn.WriteIP(packet) + require.NoError(t, err, "WriteIP must send the echo request") + + require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second))) + response := buf.NewPacket() + defer response.Release() + err = conn.ReadIP(response) + require.NoError(t, err) + if runtime.GOOS == "darwin" || runtime.GOOS == "linux" && privileged { + response.Reset() + err = conn.ReadIP(response) + require.NoError(t, err) + } + respIP := header.IPv6(response.Bytes()) + require.NotZero(t, respIP.HopLimit()) + respICMP := header.ICMPv6(respIP.Payload()) + require.Equal(t, header.ICMPv6EchoReply, respICMP.Type()) + require.Equal(t, ident, respICMP.Ident()) +} diff --git a/stack_gvisor.go b/stack_gvisor.go index 63df41a6..92e84ba4 100644 --- a/stack_gvisor.go +++ b/stack_gvisor.go @@ -95,7 +95,7 @@ func (t *GVisor) Start() error { } ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, NewTCPForwarderWithLoopback(t.ctx, ipStack, t.handler, t.inet4LoopbackAddress, t.inet6LoopbackAddress, t.tun).HandlePacket) ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, NewUDPForwarder(t.ctx, ipStack, t.handler, t.udpTimeout).HandlePacket) - icmpForwarder := NewICMPForwarder(t.ctx, ipStack, t.handler, t.icmpTimeout) + icmpForwarder := NewICMPForwarder(t.ctx, ipStack, t.logger, t.handler, t.icmpTimeout) icmpForwarder.SetLocalAddresses(t.inet4Address, t.inet6Address) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index da5549b6..1f11bbdd 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -18,6 +18,8 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip/network/ipv6" "github.com/sagernet/gvisor/pkg/tcpip/stack" "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) @@ -25,6 +27,7 @@ import ( type ICMPForwarder struct { ctx context.Context stack *stack.Stack + logger logger.Logger inet4Address netip.Addr inet6Address netip.Addr handler Handler @@ -34,12 +37,14 @@ type ICMPForwarder struct { func NewICMPForwarder( ctx context.Context, stack *stack.Stack, + logger logger.Logger, handler Handler, timeout time.Duration, ) *ICMPForwarder { return &ICMPForwarder{ ctx: ctx, stack: stack, + logger: logger, handler: handler, mapping: NewDirectRouteMapping(timeout), } @@ -81,8 +86,10 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa return true } if action != nil { - // TODO: handle error - _ = icmpWritePacketBuffer(action, pkt) + err = icmpWritePacketBuffer(action, pkt) + if err != nil { + f.logger.Error(E.Cause(err, "write ICMPv4 echo request")) + } return true } } @@ -95,7 +102,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) outgoingEP, gErr := f.stack.GetNetworkEndpoint(DefaultNIC, header.IPv4ProtocolNumber) if gErr != nil { - // TODO: log error + f.logger.Error(E.Cause(gonet.TranslateNetstackError(gErr), "get IPv4 network endpoint")) return true } route, gErr := f.stack.FindRoute( @@ -106,7 +113,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa false, ) if gErr != nil { - // TODO: log error + f.logger.Error(E.Cause(gonet.TranslateNetstackError(gErr), "find IPv4 route")) return true } defer route.Release() @@ -142,9 +149,11 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa return true } if action != nil { - // TODO: handle error pkt.IncRef() - _ = icmpWritePacketBuffer(action, pkt) + err = icmpWritePacketBuffer(action, pkt) + if err != nil { + f.logger.Error(E.Cause(err, "write ICMPv6 echo request")) + } return true } } @@ -161,7 +170,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa })) outgoingEP, gErr := f.stack.GetNetworkEndpoint(DefaultNIC, header.IPv4ProtocolNumber) if gErr != nil { - // TODO: log error + f.logger.Error(E.Cause(gonet.TranslateNetstackError(gErr), "get IPv6 network endpoint")) return true } route, gErr := f.stack.FindRoute( @@ -172,7 +181,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa false, ) if gErr != nil { - // TODO: log error + f.logger.Error(E.Cause(gonet.TranslateNetstackError(gErr), "find IPv6 route")) return true } defer route.Release() From 8f387ab39e68c7af1ebcfa2ed3fdd39712c1fd03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 9 Jul 2026 22:07:14 +0800 Subject: [PATCH 04/24] ping: Fix stale flows kept alive by unrelated ICMP traffic Unconnected raw ICMP sockets receive every ICMP packet arriving at the host, so any ICMP traffic refreshed the read deadline of every flow and stale flows (with their raw sockets and goroutines) were only reclaimed by LRU eviction while processing all host ICMP traffic in the meantime. Expire flows based on their own activity only, and on Linux attach a classic BPF ident filter to each raw socket so other flows' packets are dropped in the kernel instead of waking every flow. --- ping/destination.go | 16 +++++- ping/destination_gvisor.go | 32 ++++++++---- ping/destination_test.go | 85 ++++++++++++++++++++++++++++++++ ping/filter_linux.go | 99 ++++++++++++++++++++++++++++++++++++++ ping/filter_other.go | 8 +++ ping/ping.go | 3 ++ ping/ping_test.go | 7 +-- 7 files changed, 233 insertions(+), 17 deletions(-) create mode 100644 ping/filter_linux.go create mode 100644 ping/filter_other.go diff --git a/ping/destination.go b/ping/destination.go index e16763b0..d43730c1 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -11,6 +11,7 @@ import ( "github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" @@ -29,6 +30,7 @@ type Destination struct { destination netip.Addr routeContext tun.DirectRouteContext timeout time.Duration + lastActive common.TypedValue[time.Time] requestAccess sync.Mutex requests map[pingRequest]time.Time } @@ -73,6 +75,7 @@ func ConnectDestination( timeout: timeout, requests: make(map[pingRequest]time.Time), } + d.lastActive.Store(time.Now()) go d.loopRead() return d, nil } @@ -80,14 +83,21 @@ func ConnectDestination( func (d *Destination) loopRead() { defer d.Close() for { - buffer := buf.NewSize(maxICMPPacketSize) - err := d.conn.SetReadDeadline(time.Now().Add(d.timeout)) + deadline := d.lastActive.Load().Add(d.timeout) + if !time.Now().Before(deadline) { + return + } + err := d.conn.SetReadDeadline(deadline) if err != nil { d.logger.ErrorContext(d.ctx, E.Cause(err, "set read deadline for ICMP conn")) } + buffer := buf.NewSize(maxICMPPacketSize) err = d.conn.ReadIP(buffer) if err != nil { buffer.Release() + if E.IsTimeout(err) { + continue + } if !E.IsClosed(err) { d.logger.ErrorContext(d.ctx, E.Cause(err, "receive ICMP echo reply")) } @@ -158,6 +168,7 @@ func (d *Destination) loopRead() { } d.logger.TraceContext(d.ctx, "read ICMPv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } + d.lastActive.Store(time.Now()) err = d.routeContext.WritePacket(buffer.Bytes()) if err != nil { d.logger.ErrorContext(d.ctx, E.Cause(err, "write ICMP echo reply")) @@ -167,6 +178,7 @@ func (d *Destination) loopRead() { } func (d *Destination) WritePacket(packet *buf.Buffer) error { + d.lastActive.Store(time.Now()) if !d.destination.Is6() { ipHdr := header.IPv4(packet.Bytes()) if !ipHdr.IsValid(packet.Len()) { diff --git a/ping/destination_gvisor.go b/ping/destination_gvisor.go index 25fd36e5..0c508ee6 100644 --- a/ping/destination_gvisor.go +++ b/ping/destination_gvisor.go @@ -23,12 +23,13 @@ import ( var _ tun.DirectRouteDestination = (*GVisorDestination)(nil) type GVisorDestination struct { - ctx context.Context - logger logger.ContextLogger - endpoint tcpip.Endpoint - conn *gonet.TCPConn - rewriter *SourceRewriter - timeout time.Duration + ctx context.Context + logger logger.ContextLogger + endpoint tcpip.Endpoint + conn *gonet.TCPConn + rewriter *SourceRewriter + timeout time.Duration + lastActive common.TypedValue[time.Time] } func ConnectGVisor( @@ -86,6 +87,7 @@ func ConnectGVisor( rewriter: rewriter, timeout: timeout, } + destination.lastActive.Store(time.Now()) go destination.loopRead() return destination, nil } @@ -93,29 +95,41 @@ func ConnectGVisor( func (d *GVisorDestination) loopRead() { defer d.endpoint.Close() for { - buffer := buf.NewSize(maxICMPPacketSize) - err := d.conn.SetReadDeadline(time.Now().Add(d.timeout)) + deadline := d.lastActive.Load().Add(d.timeout) + if !time.Now().Before(deadline) { + return + } + err := d.conn.SetReadDeadline(deadline) if err != nil { d.logger.ErrorContext(d.ctx, E.Cause(err, "set read deadline for ICMP conn")) } + buffer := buf.NewSize(maxICMPPacketSize) n, err := d.conn.Read(buffer.FreeBytes()) if err != nil { buffer.Release() + if E.IsTimeout(err) { + continue + } if !E.IsClosed(err) { d.logger.ErrorContext(d.ctx, E.Cause(err, "receive ICMP echo reply")) } return } buffer.Truncate(n) - _, err = d.rewriter.WriteBack(buffer.Bytes()) + var matched bool + matched, err = d.rewriter.WriteBack(buffer.Bytes()) if err != nil { d.logger.ErrorContext(d.ctx, E.Cause(err, "write ICMP echo reply")) } + if matched { + d.lastActive.Store(time.Now()) + } buffer.Release() } } func (d *GVisorDestination) WritePacket(packet *buf.Buffer) error { + d.lastActive.Store(time.Now()) d.rewriter.RewritePacket(packet.Bytes()) return common.Error(d.conn.Write(packet.Bytes())) } diff --git a/ping/destination_test.go b/ping/destination_test.go index d0a1af88..db711395 100644 --- a/ping/destination_test.go +++ b/ping/destination_test.go @@ -2,11 +2,16 @@ package ping_test import ( "context" + "errors" "net/netip" + "os" + "slices" "testing" "time" + "github.com/sagernet/sing-tun/internal/gtcpip/header" "github.com/sagernet/sing-tun/ping" + "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/logger" "github.com/stretchr/testify/require" @@ -22,3 +27,83 @@ func TestIsClosed(t *testing.T) { destination.Close() require.True(t, destination.IsClosed()) } + +type channelWriter struct { + packets chan []byte +} + +func (w *channelWriter) WritePacket(packet []byte) error { + select { + case w.packets <- slices.Clone(packet): + default: + } + return nil +} + +type discardWriter struct{} + +func (w discardWriter) WritePacket(packet []byte) error { + return nil +} + +func buildEchoRequest(source, destination netip.Addr, identifier, sequence uint16) *buf.Buffer { + const totalLen = header.IPv4MinimumSize + header.ICMPv4MinimumSize + packet := buf.NewSize(totalLen) + ipHdr := header.IPv4(packet.Extend(totalLen)) + ipHdr.Encode(&header.IPv4Fields{ + TotalLength: totalLen, + TTL: 64, + Protocol: uint8(header.ICMPv4ProtocolNumber), + SrcAddr: source, + DstAddr: destination, + }) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + icmpHdr := header.ICMPv4(ipHdr.Payload()) + icmpHdr.SetType(header.ICMPv4Echo) + icmpHdr.SetIdent(identifier) + icmpHdr.SetSequence(sequence) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) + return packet +} + +// darwin unprivileged ICMP sockets and Linux raw sockets receive every +// loopback ICMP packet regardless of the flow it belongs to, so this test +// cannot run parallel to TestPing. +func TestDestinationIdleExpiry(t *testing.T) { + loopback := netip.MustParseAddr("127.0.0.1") + writer := &channelWriter{packets: make(chan []byte, 16)} + destination, err := ping.ConnectDestination(context.Background(), logger.NOP(), nil, loopback, writer, time.Second) + if errors.Is(err, os.ErrPermission) { + t.SkipNow() + } + require.NoError(t, err) + defer destination.Close() + + err = destination.WritePacket(buildEchoRequest(loopback, loopback, 0x1111, 1)) + require.NoError(t, err) + select { + case packet := <-writer.packets: + replyIPHdr := header.IPv4(packet) + replyICMPHdr := header.ICMPv4(replyIPHdr.Payload()) + require.Equal(t, header.ICMPv4EchoReply, replyICMPHdr.Type()) + require.Equal(t, uint16(0x1111), replyICMPHdr.Ident()) + case <-time.After(3 * time.Second): + t.Fatal("no echo reply received") + } + + noise, err := ping.ConnectDestination(context.Background(), logger.NOP(), nil, loopback, discardWriter{}, 30*time.Second) + require.NoError(t, err) + defer noise.Close() + + deadline := time.Now().Add(5 * time.Second) + var sequence uint16 + for time.Now().Before(deadline) { + sequence++ + _ = noise.WritePacket(buildEchoRequest(loopback, loopback, 0x2222, sequence)) + if destination.IsClosed() { + return + } + time.Sleep(150 * time.Millisecond) + } + t.Fatal("flow not closed after idle timeout despite unrelated ICMP traffic") +} diff --git a/ping/filter_linux.go b/ping/filter_linux.go new file mode 100644 index 00000000..13f32a38 --- /dev/null +++ b/ping/filter_linux.go @@ -0,0 +1,99 @@ +package ping + +import ( + "sync" + "syscall" + + "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/control" + + "golang.org/x/net/bpf" + "golang.org/x/sys/unix" +) + +type identFilterState struct { + access sync.Mutex + attached bool + disabled bool + identifier uint16 +} + +// The kernel clones every matching-protocol packet into every unconnected raw +// ICMP socket, so without a socket filter each flow receives and discards all +// other flows' traffic. +func (c *Conn) updateIdentFilter(wireIdentifier uint16) { + if !c.privileged { + return + } + syscallConn, isSyscallConn := common.Cast[syscall.Conn](c.conn) + if !isSyscallConn { + return + } + state := &c.identFilter + state.access.Lock() + defer state.access.Unlock() + if state.disabled { + return + } + if state.attached { + if state.identifier == wireIdentifier { + return + } + state.attached = false + state.disabled = true + _ = control.Conn(syscallConn, func(fd uintptr) error { + return unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_DETACH_FILTER, 0) + }) + return + } + rawInstructions, err := identFilterProgram(c.destination.Is6(), wireIdentifier) + if err != nil { + state.disabled = true + return + } + filter := make([]unix.SockFilter, len(rawInstructions)) + for i, instruction := range rawInstructions { + filter[i] = unix.SockFilter{Code: instruction.Op, Jt: instruction.Jt, Jf: instruction.Jf, K: instruction.K} + } + program := unix.SockFprog{Len: uint16(len(filter)), Filter: &filter[0]} + err = control.Conn(syscallConn, func(fd uintptr) error { + return unix.SetsockoptSockFprog(int(fd), unix.SOL_SOCKET, unix.SO_ATTACH_FILTER, &program) + }) + if err != nil { + state.disabled = true + return + } + state.attached = true + state.identifier = wireIdentifier +} + +func identFilterProgram(is6 bool, wireIdentifier uint16) ([]bpf.RawInstruction, error) { + if !is6 { + // Raw ICMPv4 sockets deliver the full packet including the IP header. + return bpf.Assemble([]bpf.Instruction{ + bpf.LoadMemShift{Off: 0}, + bpf.LoadIndirect{Off: 0, Size: 1}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(header.ICMPv4EchoReply), SkipTrue: 3}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(header.ICMPv4DstUnreachable), SkipTrue: 4}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(header.ICMPv4TimeExceeded), SkipTrue: 3}, + bpf.RetConstant{Val: 0}, + bpf.LoadIndirect{Off: 4, Size: 2}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(wireIdentifier), SkipFalse: 1}, + bpf.RetConstant{Val: 0xffffffff}, + bpf.RetConstant{Val: 0}, + }) + } + // Raw ICMPv6 sockets deliver the ICMPv6 message without the IP header. + return bpf.Assemble([]bpf.Instruction{ + bpf.LoadAbsolute{Off: 0, Size: 1}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(header.ICMPv6EchoReply), SkipTrue: 3}, + bpf.JumpIf{Cond: bpf.JumpGreaterThan, Val: uint32(header.ICMPv6ParamProblem), SkipTrue: 1}, + bpf.RetConstant{Val: 0xffffffff}, + bpf.RetConstant{Val: 0}, + bpf.LoadAbsolute{Off: 4, Size: 2}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(wireIdentifier), SkipFalse: 1}, + bpf.RetConstant{Val: 0xffffffff}, + bpf.RetConstant{Val: 0}, + }) +} diff --git a/ping/filter_other.go b/ping/filter_other.go new file mode 100644 index 00000000..34c2137d --- /dev/null +++ b/ping/filter_other.go @@ -0,0 +1,8 @@ +//go:build !linux + +package ping + +type identFilterState struct{} + +func (c *Conn) updateIdentFilter(wireIdentifier uint16) { +} diff --git a/ping/ping.go b/ping/ping.go index 81ac6969..23bf9073 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -28,6 +28,7 @@ type Conn struct { destination netip.Addr source common.TypedValue[netip.Addr] closed atomic.Bool + identFilter identFilterState readMsg func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) } @@ -268,6 +269,7 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { icmpHdr := header.ICMPv4(ipHdr.Payload()) icmpHdr.SetIdent(^icmpHdr.Ident()) icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) + c.updateIdentFilter(icmpHdr.Ident()) } c.source.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) return common.Error(c.conn.Write(ipHdr.Payload())) @@ -285,6 +287,7 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { Src: ipHdr.SourceAddressSlice(), Dst: ipHdr.DestinationAddressSlice(), })) + c.updateIdentFilter(icmpHdr.Ident()) } c.source.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) return common.Error(c.conn.Write(ipHdr.Payload())) diff --git a/ping/ping_test.go b/ping/ping_test.go index 9fef5bf5..be5f6a75 100644 --- a/ping/ping_test.go +++ b/ping/ping_test.go @@ -250,11 +250,6 @@ func testPingIPv4WriteIP(t *testing.T, privileged bool, addr string) { defer response.Release() err = conn.ReadIP(response) require.NoError(t, err) - if runtime.GOOS == "linux" && privileged { - response.Reset() - err = conn.ReadIP(response) - require.NoError(t, err) - } respIP := header.IPv4(response.Bytes()) require.NotZero(t, respIP.TTL()) respICMP := header.ICMPv4(respIP.Payload()) @@ -299,7 +294,7 @@ func testPingIPv6WriteIP(t *testing.T, privileged bool, addr string) { defer response.Release() err = conn.ReadIP(response) require.NoError(t, err) - if runtime.GOOS == "darwin" || runtime.GOOS == "linux" && privileged { + if runtime.GOOS == "darwin" { response.Reset() err = conn.ReadIP(response) require.NoError(t, err) From f2fb366418c227e951577d7c75e5e8f722827153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 3 Mar 2026 20:36:23 +0800 Subject: [PATCH 05/24] Add MAC address include/exclude filtering for nftables auto-redirect Support filtering traffic by source MAC address in the prerouting chain, using ether addr payload matching with set lookups for multiple addresses. --- redirect_nftables_rules.go | 144 +++++++++++++++++++++++++++++++++++++ tun.go | 2 + 2 files changed, 146 insertions(+) diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index dddd9c66..6e8cff50 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -3,6 +3,7 @@ package tun import ( + "net" "net/netip" _ "unsafe" @@ -376,6 +377,149 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft }) } } + if len(r.tunOptions.IncludeMACAddress) > 0 { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeyIIFTYPE, Register: 1}, + &expr.Cmp{ + Op: expr.CmpOpNeq, + Register: 1, + Data: binaryutil.NativeEndian.PutUint16(unix.ARPHRD_ETHER), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + if len(r.tunOptions.IncludeMACAddress) > 1 { + includeMACSet := &nftables.Set{ + Table: table, + Anonymous: true, + Constant: true, + KeyType: nftables.TypeEtherAddr, + } + err := nft.AddSet(includeMACSet, common.Map(r.tunOptions.IncludeMACAddress, func(it net.HardwareAddr) nftables.SetElement { + return nftables.SetElement{ + Key: []byte(it), + } + })) + if err != nil { + return err + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseLLHeader, + Offset: 6, + Len: 6, + }, + &expr.Lookup{ + SourceRegister: 1, + SetID: includeMACSet.ID, + SetName: includeMACSet.Name, + Invert: true, + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } else { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseLLHeader, + Offset: 6, + Len: 6, + }, + &expr.Cmp{ + Op: expr.CmpOpNeq, + Register: 1, + Data: []byte(r.tunOptions.IncludeMACAddress[0]), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } + } + if len(r.tunOptions.ExcludeMACAddress) > 0 { + if len(r.tunOptions.ExcludeMACAddress) > 1 { + excludeMACSet := &nftables.Set{ + Table: table, + Anonymous: true, + Constant: true, + KeyType: nftables.TypeEtherAddr, + } + err := nft.AddSet(excludeMACSet, common.Map(r.tunOptions.ExcludeMACAddress, func(it net.HardwareAddr) nftables.SetElement { + return nftables.SetElement{ + Key: []byte(it), + } + })) + if err != nil { + return err + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseLLHeader, + Offset: 6, + Len: 6, + }, + &expr.Lookup{ + SourceRegister: 1, + SetID: excludeMACSet.ID, + SetName: excludeMACSet.Name, + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } else { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseLLHeader, + Offset: 6, + Len: 6, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte(r.tunOptions.ExcludeMACAddress[0]), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } + } } else { if len(r.tunOptions.IncludeUID) > 0 { if len(r.tunOptions.IncludeUID) > 1 || r.tunOptions.IncludeUID[0].Start != r.tunOptions.IncludeUID[0].End { diff --git a/tun.go b/tun.go index 35cd0956..5f417bef 100644 --- a/tun.go +++ b/tun.go @@ -102,6 +102,8 @@ type Options struct { IncludeAndroidUser []int IncludePackage []string ExcludePackage []string + IncludeMACAddress []net.HardwareAddr + ExcludeMACAddress []net.HardwareAddr InterfaceFinder control.InterfaceFinder InterfaceMonitor DefaultInterfaceMonitor FileDescriptor int From d47cef7cec0ae949e65b75ce3da98a607a72c0a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 3 Apr 2026 02:07:40 +0800 Subject: [PATCH 06/24] Make gtcpip public for external use --- {internal/gtcpip => gtcpip}/README.md | 0 .../gtcpip => gtcpip}/checksum/checksum.go | 0 .../checksum/checksum_default.go | 0 .../gtcpip => gtcpip}/checksum/checksum_ts.go | 0 .../checksum/checksum_unsafe.go | 0 {internal/gtcpip => gtcpip}/errors.go | 0 {internal/gtcpip => gtcpip}/header/checksum.go | 4 ++-- {internal/gtcpip => gtcpip}/header/eth.go | 2 +- {internal/gtcpip => gtcpip}/header/icmpv4.go | 4 ++-- {internal/gtcpip => gtcpip}/header/icmpv6.go | 4 ++-- .../gtcpip => gtcpip}/header/interfaces.go | 2 +- {internal/gtcpip => gtcpip}/header/ipv4.go | 4 ++-- {internal/gtcpip => gtcpip}/header/ipv6.go | 2 +- .../header/ipv6_extension_headers.go | 2 +- .../gtcpip => gtcpip}/header/ipv6_fragment.go | 2 +- .../header/ndp_neighbor_advert.go | 2 +- .../header/ndp_neighbor_solicit.go | 2 +- .../gtcpip => gtcpip}/header/ndp_options.go | 2 +- .../header/ndp_router_advert.go | 0 .../header/ndp_router_solicit.go | 0 .../header/ndpoptionidentifier_string.go | 0 {internal/gtcpip => gtcpip}/header/netip.go | 0 {internal/gtcpip => gtcpip}/header/tcp.go | 6 +++--- {internal/gtcpip => gtcpip}/header/udp.go | 4 ++-- {internal/gtcpip => gtcpip}/seqnum/seqnum.go | 0 {internal/gtcpip => gtcpip}/tcpip.go | 0 internal/checksum_test/sum_bench_test.go | 2 +- nfqueue_linux.go | 2 +- ping/destination.go | 18 +++--------------- ping/destination_rewriter.go | 2 +- ping/ping.go | 2 +- ping/ping_test.go | 2 +- ping/socket_linux_unprivileged.go | 2 +- ping/source_rewriter.go | 2 +- stack_gvisor_tcp.go | 2 +- stack_mixed.go | 2 +- stack_system.go | 4 ++-- stack_system_packet.go | 2 +- tun_darwin.go | 2 +- tun_linux.go | 4 ++-- tun_offload.go | 6 +++--- tun_offload_linux.go | 6 +++--- 42 files changed, 45 insertions(+), 57 deletions(-) rename {internal/gtcpip => gtcpip}/README.md (100%) rename {internal/gtcpip => gtcpip}/checksum/checksum.go (100%) rename {internal/gtcpip => gtcpip}/checksum/checksum_default.go (100%) rename {internal/gtcpip => gtcpip}/checksum/checksum_ts.go (100%) rename {internal/gtcpip => gtcpip}/checksum/checksum_unsafe.go (100%) rename {internal/gtcpip => gtcpip}/errors.go (100%) rename {internal/gtcpip => gtcpip}/header/checksum.go (97%) rename {internal/gtcpip => gtcpip}/header/eth.go (99%) rename {internal/gtcpip => gtcpip}/header/icmpv4.go (98%) rename {internal/gtcpip => gtcpip}/header/icmpv6.go (98%) rename {internal/gtcpip => gtcpip}/header/interfaces.go (98%) rename {internal/gtcpip => gtcpip}/header/ipv4.go (99%) rename {internal/gtcpip => gtcpip}/header/ipv6.go (99%) rename {internal/gtcpip => gtcpip}/header/ipv6_extension_headers.go (99%) rename {internal/gtcpip => gtcpip}/header/ipv6_fragment.go (99%) rename {internal/gtcpip => gtcpip}/header/ndp_neighbor_advert.go (98%) rename {internal/gtcpip => gtcpip}/header/ndp_neighbor_solicit.go (97%) rename {internal/gtcpip => gtcpip}/header/ndp_options.go (99%) rename {internal/gtcpip => gtcpip}/header/ndp_router_advert.go (100%) rename {internal/gtcpip => gtcpip}/header/ndp_router_solicit.go (100%) rename {internal/gtcpip => gtcpip}/header/ndpoptionidentifier_string.go (100%) rename {internal/gtcpip => gtcpip}/header/netip.go (100%) rename {internal/gtcpip => gtcpip}/header/tcp.go (99%) rename {internal/gtcpip => gtcpip}/header/udp.go (98%) rename {internal/gtcpip => gtcpip}/seqnum/seqnum.go (100%) rename {internal/gtcpip => gtcpip}/tcpip.go (100%) diff --git a/internal/gtcpip/README.md b/gtcpip/README.md similarity index 100% rename from internal/gtcpip/README.md rename to gtcpip/README.md diff --git a/internal/gtcpip/checksum/checksum.go b/gtcpip/checksum/checksum.go similarity index 100% rename from internal/gtcpip/checksum/checksum.go rename to gtcpip/checksum/checksum.go diff --git a/internal/gtcpip/checksum/checksum_default.go b/gtcpip/checksum/checksum_default.go similarity index 100% rename from internal/gtcpip/checksum/checksum_default.go rename to gtcpip/checksum/checksum_default.go diff --git a/internal/gtcpip/checksum/checksum_ts.go b/gtcpip/checksum/checksum_ts.go similarity index 100% rename from internal/gtcpip/checksum/checksum_ts.go rename to gtcpip/checksum/checksum_ts.go diff --git a/internal/gtcpip/checksum/checksum_unsafe.go b/gtcpip/checksum/checksum_unsafe.go similarity index 100% rename from internal/gtcpip/checksum/checksum_unsafe.go rename to gtcpip/checksum/checksum_unsafe.go diff --git a/internal/gtcpip/errors.go b/gtcpip/errors.go similarity index 100% rename from internal/gtcpip/errors.go rename to gtcpip/errors.go diff --git a/internal/gtcpip/header/checksum.go b/gtcpip/header/checksum.go similarity index 97% rename from internal/gtcpip/header/checksum.go rename to gtcpip/header/checksum.go index 2c21e6d3..303502cc 100644 --- a/internal/gtcpip/header/checksum.go +++ b/gtcpip/header/checksum.go @@ -20,8 +20,8 @@ import ( "encoding/binary" "fmt" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" ) // PseudoHeaderChecksum calculates the pseudo-header checksum for the given diff --git a/internal/gtcpip/header/eth.go b/gtcpip/header/eth.go similarity index 99% rename from internal/gtcpip/header/eth.go rename to gtcpip/header/eth.go index 9d876ee6..613a72c6 100644 --- a/internal/gtcpip/header/eth.go +++ b/gtcpip/header/eth.go @@ -17,7 +17,7 @@ package header import ( "encoding/binary" - "github.com/sagernet/sing-tun/internal/gtcpip" + "github.com/sagernet/sing-tun/gtcpip" ) const ( diff --git a/internal/gtcpip/header/icmpv4.go b/gtcpip/header/icmpv4.go similarity index 98% rename from internal/gtcpip/header/icmpv4.go rename to gtcpip/header/icmpv4.go index 580101c0..3b481041 100644 --- a/internal/gtcpip/header/icmpv4.go +++ b/gtcpip/header/icmpv4.go @@ -17,8 +17,8 @@ package header import ( "encoding/binary" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" ) // ICMPv4 represents an ICMPv4 header stored in a byte array. diff --git a/internal/gtcpip/header/icmpv6.go b/gtcpip/header/icmpv6.go similarity index 98% rename from internal/gtcpip/header/icmpv6.go rename to gtcpip/header/icmpv6.go index 520b4036..7eae97ab 100644 --- a/internal/gtcpip/header/icmpv6.go +++ b/gtcpip/header/icmpv6.go @@ -17,8 +17,8 @@ package header import ( "encoding/binary" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" ) // ICMPv6 represents an ICMPv6 header stored in a byte array. diff --git a/internal/gtcpip/header/interfaces.go b/gtcpip/header/interfaces.go similarity index 98% rename from internal/gtcpip/header/interfaces.go rename to gtcpip/header/interfaces.go index fc13100c..c0bb410c 100644 --- a/internal/gtcpip/header/interfaces.go +++ b/gtcpip/header/interfaces.go @@ -17,7 +17,7 @@ package header import ( "net/netip" - tcpip "github.com/sagernet/sing-tun/internal/gtcpip" + tcpip "github.com/sagernet/sing-tun/gtcpip" ) const ( diff --git a/internal/gtcpip/header/ipv4.go b/gtcpip/header/ipv4.go similarity index 99% rename from internal/gtcpip/header/ipv4.go rename to gtcpip/header/ipv4.go index ad06f38c..d5ffbf1d 100644 --- a/internal/gtcpip/header/ipv4.go +++ b/gtcpip/header/ipv4.go @@ -20,8 +20,8 @@ import ( "net/netip" "time" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" "github.com/sagernet/sing/common" ) diff --git a/internal/gtcpip/header/ipv6.go b/gtcpip/header/ipv6.go similarity index 99% rename from internal/gtcpip/header/ipv6.go rename to gtcpip/header/ipv6.go index 1a5a7a05..4de30737 100644 --- a/internal/gtcpip/header/ipv6.go +++ b/gtcpip/header/ipv6.go @@ -20,7 +20,7 @@ import ( "fmt" "net/netip" - "github.com/sagernet/sing-tun/internal/gtcpip" + "github.com/sagernet/sing-tun/gtcpip" ) const ( diff --git a/internal/gtcpip/header/ipv6_extension_headers.go b/gtcpip/header/ipv6_extension_headers.go similarity index 99% rename from internal/gtcpip/header/ipv6_extension_headers.go rename to gtcpip/header/ipv6_extension_headers.go index 20064d8b..6c48b1bf 100644 --- a/internal/gtcpip/header/ipv6_extension_headers.go +++ b/gtcpip/header/ipv6_extension_headers.go @@ -20,7 +20,7 @@ import ( "fmt" "math" - "github.com/sagernet/sing-tun/internal/gtcpip" + "github.com/sagernet/sing-tun/gtcpip" "github.com/sagernet/sing/common" ) diff --git a/internal/gtcpip/header/ipv6_fragment.go b/gtcpip/header/ipv6_fragment.go similarity index 99% rename from internal/gtcpip/header/ipv6_fragment.go rename to gtcpip/header/ipv6_fragment.go index 49aaca71..38f0b202 100644 --- a/internal/gtcpip/header/ipv6_fragment.go +++ b/gtcpip/header/ipv6_fragment.go @@ -17,7 +17,7 @@ package header import ( "encoding/binary" - "github.com/sagernet/sing-tun/internal/gtcpip" + "github.com/sagernet/sing-tun/gtcpip" ) const ( diff --git a/internal/gtcpip/header/ndp_neighbor_advert.go b/gtcpip/header/ndp_neighbor_advert.go similarity index 98% rename from internal/gtcpip/header/ndp_neighbor_advert.go rename to gtcpip/header/ndp_neighbor_advert.go index 7a934cce..8f36765a 100644 --- a/internal/gtcpip/header/ndp_neighbor_advert.go +++ b/gtcpip/header/ndp_neighbor_advert.go @@ -14,7 +14,7 @@ package header -import "github.com/sagernet/sing-tun/internal/gtcpip" +import "github.com/sagernet/sing-tun/gtcpip" // NDPNeighborAdvert is an NDP Neighbor Advertisement message. It will // only contain the body of an ICMPv6 packet. diff --git a/internal/gtcpip/header/ndp_neighbor_solicit.go b/gtcpip/header/ndp_neighbor_solicit.go similarity index 97% rename from internal/gtcpip/header/ndp_neighbor_solicit.go rename to gtcpip/header/ndp_neighbor_solicit.go index 61d61a8a..b4af20ce 100644 --- a/internal/gtcpip/header/ndp_neighbor_solicit.go +++ b/gtcpip/header/ndp_neighbor_solicit.go @@ -14,7 +14,7 @@ package header -import "github.com/sagernet/sing-tun/internal/gtcpip" +import "github.com/sagernet/sing-tun/gtcpip" // NDPNeighborSolicit is an NDP Neighbor Solicitation message. It will only // contain the body of an ICMPv6 packet. diff --git a/internal/gtcpip/header/ndp_options.go b/gtcpip/header/ndp_options.go similarity index 99% rename from internal/gtcpip/header/ndp_options.go rename to gtcpip/header/ndp_options.go index 21ed757b..c545120d 100644 --- a/internal/gtcpip/header/ndp_options.go +++ b/gtcpip/header/ndp_options.go @@ -23,7 +23,7 @@ import ( "math" "time" - "github.com/sagernet/sing-tun/internal/gtcpip" + "github.com/sagernet/sing-tun/gtcpip" "github.com/sagernet/sing/common" ) diff --git a/internal/gtcpip/header/ndp_router_advert.go b/gtcpip/header/ndp_router_advert.go similarity index 100% rename from internal/gtcpip/header/ndp_router_advert.go rename to gtcpip/header/ndp_router_advert.go diff --git a/internal/gtcpip/header/ndp_router_solicit.go b/gtcpip/header/ndp_router_solicit.go similarity index 100% rename from internal/gtcpip/header/ndp_router_solicit.go rename to gtcpip/header/ndp_router_solicit.go diff --git a/internal/gtcpip/header/ndpoptionidentifier_string.go b/gtcpip/header/ndpoptionidentifier_string.go similarity index 100% rename from internal/gtcpip/header/ndpoptionidentifier_string.go rename to gtcpip/header/ndpoptionidentifier_string.go diff --git a/internal/gtcpip/header/netip.go b/gtcpip/header/netip.go similarity index 100% rename from internal/gtcpip/header/netip.go rename to gtcpip/header/netip.go diff --git a/internal/gtcpip/header/tcp.go b/gtcpip/header/tcp.go similarity index 99% rename from internal/gtcpip/header/tcp.go rename to gtcpip/header/tcp.go index 04c22b59..7ab428f8 100644 --- a/internal/gtcpip/header/tcp.go +++ b/gtcpip/header/tcp.go @@ -17,9 +17,9 @@ package header import ( "encoding/binary" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" - "github.com/sagernet/sing-tun/internal/gtcpip/seqnum" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/seqnum" "github.com/google/btree" ) diff --git a/internal/gtcpip/header/udp.go b/gtcpip/header/udp.go similarity index 98% rename from internal/gtcpip/header/udp.go rename to gtcpip/header/udp.go index a995a172..ce7708e1 100644 --- a/internal/gtcpip/header/udp.go +++ b/gtcpip/header/udp.go @@ -18,8 +18,8 @@ import ( "encoding/binary" "math" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" ) const ( diff --git a/internal/gtcpip/seqnum/seqnum.go b/gtcpip/seqnum/seqnum.go similarity index 100% rename from internal/gtcpip/seqnum/seqnum.go rename to gtcpip/seqnum/seqnum.go diff --git a/internal/gtcpip/tcpip.go b/gtcpip/tcpip.go similarity index 100% rename from internal/gtcpip/tcpip.go rename to gtcpip/tcpip.go diff --git a/internal/checksum_test/sum_bench_test.go b/internal/checksum_test/sum_bench_test.go index a5372174..fb4aaa4c 100644 --- a/internal/checksum_test/sum_bench_test.go +++ b/internal/checksum_test/sum_bench_test.go @@ -4,7 +4,7 @@ import ( "crypto/rand" "testing" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/checksum" "github.com/sagernet/sing-tun/internal/tschecksum" ) diff --git a/nfqueue_linux.go b/nfqueue_linux.go index ad16dc46..32778957 100644 --- a/nfqueue_linux.go +++ b/nfqueue_linux.go @@ -7,7 +7,7 @@ import ( "errors" "sync/atomic" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" diff --git a/ping/destination.go b/ping/destination.go index d43730c1..ece45b9a 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -10,8 +10,7 @@ import ( "time" "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun/internal/gtcpip/header" - "github.com/sagernet/sing/common" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" @@ -30,7 +29,6 @@ type Destination struct { destination netip.Addr routeContext tun.DirectRouteContext timeout time.Duration - lastActive common.TypedValue[time.Time] requestAccess sync.Mutex requests map[pingRequest]time.Time } @@ -75,7 +73,6 @@ func ConnectDestination( timeout: timeout, requests: make(map[pingRequest]time.Time), } - d.lastActive.Store(time.Now()) go d.loopRead() return d, nil } @@ -83,21 +80,14 @@ func ConnectDestination( func (d *Destination) loopRead() { defer d.Close() for { - deadline := d.lastActive.Load().Add(d.timeout) - if !time.Now().Before(deadline) { - return - } - err := d.conn.SetReadDeadline(deadline) + buffer := buf.NewSize(maxICMPPacketSize) + err := d.conn.SetReadDeadline(time.Now().Add(d.timeout)) if err != nil { d.logger.ErrorContext(d.ctx, E.Cause(err, "set read deadline for ICMP conn")) } - buffer := buf.NewSize(maxICMPPacketSize) err = d.conn.ReadIP(buffer) if err != nil { buffer.Release() - if E.IsTimeout(err) { - continue - } if !E.IsClosed(err) { d.logger.ErrorContext(d.ctx, E.Cause(err, "receive ICMP echo reply")) } @@ -168,7 +158,6 @@ func (d *Destination) loopRead() { } d.logger.TraceContext(d.ctx, "read ICMPv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } - d.lastActive.Store(time.Now()) err = d.routeContext.WritePacket(buffer.Bytes()) if err != nil { d.logger.ErrorContext(d.ctx, E.Cause(err, "write ICMP echo reply")) @@ -178,7 +167,6 @@ func (d *Destination) loopRead() { } func (d *Destination) WritePacket(packet *buf.Buffer) error { - d.lastActive.Store(time.Now()) if !d.destination.Is6() { ipHdr := header.IPv4(packet.Bytes()) if !ipHdr.IsValid(packet.Len()) { diff --git a/ping/destination_rewriter.go b/ping/destination_rewriter.go index a61e1556..26bb3551 100644 --- a/ping/destination_rewriter.go +++ b/ping/destination_rewriter.go @@ -4,7 +4,7 @@ import ( "net/netip" "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common/buf" ) diff --git a/ping/ping.go b/ping/ping.go index 23bf9073..dbb4f929 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -9,7 +9,7 @@ import ( "sync/atomic" "time" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" diff --git a/ping/ping_test.go b/ping/ping_test.go index be5f6a75..74f3df8c 100644 --- a/ping/ping_test.go +++ b/ping/ping_test.go @@ -9,7 +9,7 @@ import ( "time" "github.com/sagernet/gvisor/pkg/rand" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing-tun/ping" "github.com/sagernet/sing/common/buf" diff --git a/ping/socket_linux_unprivileged.go b/ping/socket_linux_unprivileged.go index 3742cc83..f709684a 100644 --- a/ping/socket_linux_unprivileged.go +++ b/ping/socket_linux_unprivileged.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" diff --git a/ping/source_rewriter.go b/ping/source_rewriter.go index 480c6a78..545560de 100644 --- a/ping/source_rewriter.go +++ b/ping/source_rewriter.go @@ -6,7 +6,7 @@ import ( "sync" "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common/logger" ) diff --git a/stack_gvisor_tcp.go b/stack_gvisor_tcp.go index 0c63ee11..ba8af6df 100644 --- a/stack_gvisor_tcp.go +++ b/stack_gvisor_tcp.go @@ -11,7 +11,7 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip/header" "github.com/sagernet/gvisor/pkg/tcpip/stack" "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/checksum" "github.com/sagernet/sing/common" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" diff --git a/stack_mixed.go b/stack_mixed.go index 8836d6ba..33284053 100644 --- a/stack_mixed.go +++ b/stack_mixed.go @@ -12,7 +12,7 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip/link/channel" "github.com/sagernet/gvisor/pkg/tcpip/stack" "github.com/sagernet/gvisor/pkg/tcpip/transport/udp" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" ) diff --git a/stack_system.go b/stack_system.go index 031e87f5..d2799724 100644 --- a/stack_system.go +++ b/stack_system.go @@ -8,8 +8,8 @@ import ( "syscall" "time" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" diff --git a/stack_system_packet.go b/stack_system_packet.go index 34fe51e4..a8f8076e 100644 --- a/stack_system_packet.go +++ b/stack_system_packet.go @@ -4,7 +4,7 @@ import ( "net/netip" "syscall" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common" ) diff --git a/tun_darwin.go b/tun_darwin.go index 31312955..8f7a186c 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -9,7 +9,7 @@ import ( "syscall" "unsafe" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" rawfile "github.com/sagernet/sing-tun/internal/rawfile_darwin" "github.com/sagernet/sing-tun/internal/stopfd_darwin" "github.com/sagernet/sing/common" diff --git a/tun_linux.go b/tun_linux.go index 9cd6300c..d125f82f 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -14,8 +14,8 @@ import ( "unsafe" "github.com/sagernet/netlink" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" diff --git a/tun_offload.go b/tun_offload.go index c76aeacf..3bf6268f 100644 --- a/tun_offload.go +++ b/tun_offload.go @@ -4,9 +4,9 @@ import ( "encoding/binary" "fmt" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/header" ) // GSOType represents the type of segmentation offload. diff --git a/tun_offload_linux.go b/tun_offload_linux.go index 07123419..57834ef1 100644 --- a/tun_offload_linux.go +++ b/tun_offload_linux.go @@ -14,9 +14,9 @@ import ( "slices" "unsafe" - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/header" "golang.org/x/sys/unix" ) From 6bb5b52ef7c0e923404efe477d9637df340e2669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 24 Apr 2026 09:31:40 +0800 Subject: [PATCH 07/24] Add compatibility with docker bridge --- redirect_linux.go | 2 + redirect_nftables.go | 11 +- redirect_nftables_docker.go | 262 ++++++++++++++++++++++++++++++++++++ 3 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 redirect_nftables_docker.go diff --git a/redirect_linux.go b/redirect_linux.go index e9c892c8..d575d192 100644 --- a/redirect_linux.go +++ b/redirect_linux.go @@ -44,6 +44,8 @@ type autoRedirect struct { nfqueueEnabled bool redirectRouteTableIndex int redirectInterfaces []control.Interface + dockerFirewallMonitor *nftables.Monitor + dockerFirewallDone chan struct{} } func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { diff --git a/redirect_nftables.go b/redirect_nftables.go index 266bbe91..f17e1f36 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -283,11 +283,15 @@ func (r *autoRedirect) setupNFTables() error { if err != nil { return E.Cause(err, "configure openwrt firewall4") } - err = nft.Flush() if err != nil { return E.Cause(err, "flush nftables") } + r.startDockerFirewallMonitor() + err = r.configureDockerFirewall(false) + if err != nil && r.logger != nil { + r.logger.Warn("configure docker firewall: ", err) + } r.networkListener = r.networkMonitor.RegisterCallback(func() { err = r.nftablesUpdateLocalAddressSet() @@ -361,6 +365,7 @@ func (r *autoRedirect) cleanupNFTables() { if r.networkListener != nil { r.networkMonitor.UnregisterCallback(r.networkListener) } + r.stopDockerFirewallMonitor() nft, err := nftables.New() if err != nil { return @@ -372,6 +377,10 @@ func (r *autoRedirect) cleanupNFTables() { _ = r.configureOpenWRTFirewall4(nft, true) _ = nft.Flush() _ = nft.CloseLasting() + err = r.configureDockerFirewall(true) + if err != nil && r.logger != nil { + r.logger.Warn("cleanup docker firewall: ", err) + } } func (r *autoRedirect) nftablesCreatePreMatchChains(nft *nftables.Conn, table *nftables.Table) error { diff --git a/redirect_nftables_docker.go b/redirect_nftables_docker.go new file mode 100644 index 00000000..23b2a52c --- /dev/null +++ b/redirect_nftables_docker.go @@ -0,0 +1,262 @@ +//go:build linux + +package tun + +import ( + "bytes" + "slices" + "strings" + + "github.com/sagernet/nftables" + "github.com/sagernet/nftables/expr" + "github.com/sagernet/nftables/userdata" + E "github.com/sagernet/sing/common/exceptions" +) + +const ( + nftablesDockerFilterTable = "filter" + nftablesDockerUserChain = "DOCKER-USER" +) + +func (r *autoRedirect) startDockerFirewallMonitor() { + if r.dockerFirewallMonitor != nil { + return + } + doneCh := make(chan struct{}) + r.dockerFirewallDone = doneCh + monitor := nftables.NewMonitor( + nftables.WithMonitorAction(nftables.MonitorActionAny), + nftables.WithMonitorObject(nftables.MonitorObjectRuleset), + nftables.WithMonitorEventBuffer(16), + ) + nft, err := nftables.New() + if err != nil { + if r.logger != nil { + r.logger.Warn("create nftables monitor connection: ", err) + } + close(doneCh) + r.dockerFirewallDone = nil + return + } + events, err := nft.AddGenerationalMonitor(monitor) + _ = nft.CloseLasting() + if err != nil { + if r.logger != nil { + r.logger.Warn("start nftables monitor: ", err) + } + close(doneCh) + r.dockerFirewallDone = nil + return + } + r.dockerFirewallMonitor = monitor + go r.loopDockerFirewallMonitor(events, doneCh) +} + +func (r *autoRedirect) stopDockerFirewallMonitor() { + if r.dockerFirewallMonitor == nil { + return + } + _ = r.dockerFirewallMonitor.Close() + <-r.dockerFirewallDone + r.dockerFirewallMonitor = nil + r.dockerFirewallDone = nil +} + +func (r *autoRedirect) loopDockerFirewallMonitor(events <-chan *nftables.MonitorEvents, doneCh chan<- struct{}) { + defer close(doneCh) + for monitorEvents := range events { + if monitorEvents != nil && monitorEvents.GeneratedBy != nil && monitorEvents.GeneratedBy.Error != nil { + if r.logger != nil { + r.logger.Warn("nftables monitor closed: ", monitorEvents.GeneratedBy.Error) + } + return + } + if !nftablesDockerFirewallEventsRelevant(monitorEvents) { + continue + } + err := r.configureDockerFirewall(false) + if err != nil && r.logger != nil { + r.logger.Warn("update docker firewall: ", err) + } + } +} + +func (r *autoRedirect) configureDockerFirewall(cleanup bool) error { + nft, err := nftables.New() + if err != nil { + return E.Cause(err, "create nftables connection") + } + defer nft.CloseLasting() + + err = r.configureDockerFirewallWithConn(nft, cleanup) + if err != nil { + return err + } + return nft.Flush() +} + +func (r *autoRedirect) configureDockerFirewallWithConn(nft *nftables.Conn, cleanup bool) error { + var err error + if r.enableIPv4 { + err = E.Errors(err, r.configureDockerFirewallForFamily(nft, nftables.TableFamilyIPv4, cleanup)) + } + if r.enableIPv6 { + err = E.Errors(err, r.configureDockerFirewallForFamily(nft, nftables.TableFamilyIPv6, cleanup)) + } + return err +} + +func (r *autoRedirect) configureDockerFirewallForFamily(nft *nftables.Conn, family nftables.TableFamily, cleanup bool) error { + table, chain, loaded, err := nftablesLoadDockerUserChain(nft, family) + if err != nil || !loaded { + return err + } + err = r.configureDockerFirewallRules(nft, table, chain, cleanup) + return err +} + +func (r *autoRedirect) configureDockerFirewallRules(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, cleanup bool) error { + rules, err := nft.GetRules(table, chain) + if err != nil { + return E.Cause(err, "list docker user rules") + } + if cleanup { + return r.cleanupDockerFirewallRules(nft, rules) + } + return r.reconcileDockerFirewallRules(nft, table, chain, rules) +} + +func nftablesLoadDockerUserChain(nft *nftables.Conn, family nftables.TableFamily) (*nftables.Table, *nftables.Chain, bool, error) { + table, err := nft.ListTableOfFamily(nftablesDockerFilterTable, family) + if err != nil { + return nil, nil, false, nil + } + chain, err := nft.ListChain(table, nftablesDockerUserChain) + if err != nil { + return nil, nil, false, nil + } + return table, chain, true, nil +} + +func nftablesDockerFirewallEventsRelevant(events *nftables.MonitorEvents) bool { + if events == nil { + return false + } + return slices.ContainsFunc(events.Changes, nftablesDockerFirewallEventRelevant) +} + +func nftablesDockerFirewallEventRelevant(event *nftables.MonitorEvent) bool { + if event == nil || event.Error != nil { + return false + } + switch data := event.Data.(type) { + case *nftables.Table: + return nftablesIsDockerFirewallTable(data) + case *nftables.Chain: + return data.Name == nftablesDockerUserChain && nftablesIsDockerFirewallTable(data.Table) + case *nftables.Rule: + return data.Chain != nil && data.Chain.Name == nftablesDockerUserChain && nftablesIsDockerFirewallTable(data.Table) + default: + return false + } +} + +func nftablesIsDockerFirewallTable(table *nftables.Table) bool { + return table != nil && + table.Name == nftablesDockerFilterTable && + (table.Family == nftables.TableFamilyIPv4 || table.Family == nftables.TableFamilyIPv6) +} + +func (r *autoRedirect) cleanupDockerFirewallRules(nft *nftables.Conn, rules []*nftables.Rule) error { + var deleteErr error + for _, rule := range rules { + if r.nftablesIsDockerCompatibilityRule(rule) { + deleteErr = E.Errors(deleteErr, nft.DelRule(rule)) + } + } + return deleteErr +} + +func (r *autoRedirect) reconcileDockerFirewallRules(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, rules []*nftables.Rule) error { + outputComment := r.nftablesDockerCompatibilityComment("output to tun") + inputComment := r.nftablesDockerCompatibilityComment("input from tun") + var hasOutputRule bool + var hasInputRule bool + var deleteErr error + for _, rule := range rules { + if nftablesDockerCompatibilityRuleMatches(rule, r.tunOptions.Name, expr.MetaKeyOIFNAME, outputComment) && !hasOutputRule { + hasOutputRule = true + } else if nftablesDockerCompatibilityRuleMatches(rule, r.tunOptions.Name, expr.MetaKeyIIFNAME, inputComment) && !hasInputRule { + hasInputRule = true + } else if r.nftablesIsDockerCompatibilityRule(rule) { + deleteErr = E.Errors(deleteErr, nft.DelRule(rule)) + } + } + if deleteErr != nil { + return deleteErr + } + if !hasOutputRule { + nft.InsertRule(nftablesDockerCompatibilityRule(table, chain, r.tunOptions.Name, expr.MetaKeyOIFNAME, outputComment)) + } + if !hasInputRule { + nft.InsertRule(nftablesDockerCompatibilityRule(table, chain, r.tunOptions.Name, expr.MetaKeyIIFNAME, inputComment)) + } + return nil +} + +func nftablesDockerCompatibilityRule(table *nftables.Table, chain *nftables.Chain, ifName string, ifNameKey expr.MetaKey, comment string) *nftables.Rule { + return &nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{ + Key: ifNameKey, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: nftablesIfname(ifName), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictAccept, + }, + }, + UserData: userdata.AppendString(nil, userdata.TypeComment, comment), + } +} + +func nftablesDockerCompatibilityRuleMatches(rule *nftables.Rule, ifName string, ifNameKey expr.MetaKey, comment string) bool { + ruleComment, loaded := userdata.GetString(rule.UserData, userdata.TypeComment) + if !loaded || ruleComment != comment || len(rule.Exprs) != 4 { + return false + } + meta, loaded := rule.Exprs[0].(*expr.Meta) + if !loaded || meta.Key != ifNameKey || meta.Register != 1 { + return false + } + cmp, loaded := rule.Exprs[1].(*expr.Cmp) + if !loaded || cmp.Op != expr.CmpOpEq || cmp.Register != 1 || !bytes.Equal(cmp.Data, nftablesIfname(ifName)) { + return false + } + _, loaded = rule.Exprs[2].(*expr.Counter) + if !loaded { + return false + } + verdict, loaded := rule.Exprs[3].(*expr.Verdict) + return loaded && verdict.Kind == expr.VerdictAccept +} + +func (r *autoRedirect) nftablesIsDockerCompatibilityRule(rule *nftables.Rule) bool { + comment, loaded := userdata.GetString(rule.UserData, userdata.TypeComment) + return loaded && strings.HasPrefix(comment, r.nftablesDockerCompatibilityCommentPrefix()) +} + +func (r *autoRedirect) nftablesDockerCompatibilityComment(direction string) string { + return r.nftablesDockerCompatibilityCommentPrefix() + direction +} + +func (r *autoRedirect) nftablesDockerCompatibilityCommentPrefix() string { + return "!" + r.tableName + ": Docker compatibility " +} From 712c4204821640893954e5b8bf8daa9c5bb4d3af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Apr 2026 07:11:03 +0800 Subject: [PATCH 08/24] Add read waiter support for gVisor conn --- stack_gvisor_lazy.go | 73 ++++----- stack_gvisor_tcp_conn.go | 325 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 355 insertions(+), 43 deletions(-) create mode 100644 stack_gvisor_tcp_conn.go diff --git a/stack_gvisor_lazy.go b/stack_gvisor_lazy.go index f5e2e6e6..dcbcafb9 100644 --- a/stack_gvisor_lazy.go +++ b/stack_gvisor_lazy.go @@ -19,7 +19,7 @@ import ( ) type gLazyConn struct { - tcpConn *gonet.TCPConn + tcpConn *gTCPConn parentCtx context.Context stack *stack.Stack request *tcp.ForwarderRequest @@ -31,9 +31,6 @@ type gLazyConn struct { } func (c *gLazyConn) HandshakeContext(ctx context.Context) error { - if c.handshakeDone { - return c.handshakeErr - } c.handshakeAccess.Lock() defer c.handshakeAccess.Unlock() if c.handshakeDone { @@ -66,15 +63,12 @@ func (c *gLazyConn) HandshakeContext(ctx context.Context) error { endpoint.SocketOptions().SetKeepAlive(true) endpoint.SetSockOpt(common.Ptr(tcpip.KeepaliveIdleOption(15 * time.Second))) endpoint.SetSockOpt(common.Ptr(tcpip.KeepaliveIntervalOption(15 * time.Second))) - tcpConn := gonet.NewTCPConn(&wq, endpoint) + tcpConn := newGTCPConn(&wq, endpoint, c.localAddr, c.remoteAddr) c.tcpConn = tcpConn return nil } func (c *gLazyConn) HandshakeFailure(err error) error { - if c.handshakeDone { - return os.ErrInvalid - } c.handshakeAccess.Lock() defer c.handshakeAccess.Unlock() if c.handshakeDone { @@ -90,6 +84,18 @@ func (c *gLazyConn) HandshakeSuccess() error { return c.HandshakeContext(context.Background()) } +func (c *gLazyConn) NeedHandshakeForRead() bool { + c.handshakeAccess.Lock() + defer c.handshakeAccess.Unlock() + return !c.handshakeDone +} + +func (c *gLazyConn) NeedHandshakeForWrite() bool { + c.handshakeAccess.Lock() + defer c.handshakeAccess.Unlock() + return !c.handshakeDone +} + func (c *gLazyConn) Read(b []byte) (n int, err error) { err = c.HandshakeContext(context.Background()) if err != nil { @@ -139,57 +145,38 @@ func (c *gLazyConn) SetWriteDeadline(t time.Time) error { } func (c *gLazyConn) Close() error { - if !c.handshakeDone { - c.handshakeAccess.Lock() - if !c.handshakeDone { - c.request.Complete(true) - c.handshakeErr = net.ErrClosed - c.handshakeDone = true - return nil - } else if c.handshakeErr != nil { - return nil - } - c.handshakeAccess.Unlock() - } else if c.handshakeErr != nil { + if c.closeBeforeHandshake() { return nil } return c.tcpConn.Close() } func (c *gLazyConn) CloseRead() error { - if !c.handshakeDone { - c.handshakeAccess.Lock() - if !c.handshakeDone { - c.request.Complete(true) - c.handshakeErr = net.ErrClosed - c.handshakeDone = true - return nil - } else if c.handshakeErr != nil { - return nil - } - c.handshakeAccess.Unlock() - } else if c.handshakeErr != nil { + if c.closeBeforeHandshake() { return nil } return c.tcpConn.CloseRead() } func (c *gLazyConn) CloseWrite() error { + if c.closeBeforeHandshake() { + return nil + } + return c.tcpConn.CloseWrite() +} + +func (c *gLazyConn) closeBeforeHandshake() bool { + c.handshakeAccess.Lock() + defer c.handshakeAccess.Unlock() if !c.handshakeDone { - c.handshakeAccess.Lock() - if !c.handshakeDone { + if c.request != nil { c.request.Complete(true) - c.handshakeErr = net.ErrClosed - c.handshakeDone = true - return nil - } else if c.handshakeErr != nil { - return nil } - c.handshakeAccess.Unlock() - } else if c.handshakeErr != nil { - return nil + c.handshakeErr = net.ErrClosed + c.handshakeDone = true + return true } - return c.tcpConn.CloseRead() + return c.handshakeErr != nil } func (c *gLazyConn) ReaderReplaceable() bool { diff --git a/stack_gvisor_tcp_conn.go b/stack_gvisor_tcp_conn.go new file mode 100644 index 00000000..ad48d42f --- /dev/null +++ b/stack_gvisor_tcp_conn.go @@ -0,0 +1,325 @@ +//go:build with_gvisor + +package tun + +import ( + "bytes" + "errors" + "io" + "net" + "os" + "time" + + "github.com/sagernet/gvisor/pkg/sync" + "github.com/sagernet/gvisor/pkg/tcpip" + "github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet" + "github.com/sagernet/gvisor/pkg/waiter" + "github.com/sagernet/sing/common/buf" + N "github.com/sagernet/sing/common/network" +) + +var ( + _ net.Conn = (*gTCPConn)(nil) + _ N.ReadWaiter = (*gTCPConn)(nil) +) + +type gTCPConn struct { + gTCPDeadline + + wq *waiter.Queue + ep tcpip.Endpoint + + localAddr net.Addr + remoteAddr net.Addr + + readMu sync.Mutex + readWaitOption N.ReadWaitOptions +} + +func newGTCPConn(wq *waiter.Queue, ep tcpip.Endpoint, localAddr net.Addr, remoteAddr net.Addr) *gTCPConn { + conn := &gTCPConn{ + wq: wq, + ep: ep, + localAddr: localAddr, + remoteAddr: remoteAddr, + } + conn.gTCPDeadline.init() + return conn +} + +func (c *gTCPConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readWaitOption = options + return false +} + +func (c *gTCPConn) WaitReadBuffer() (*buf.Buffer, error) { + c.readMu.Lock() + defer c.readMu.Unlock() + + deadline := c.readCancel() + for { + if err := c.waitReadable(deadline); err != nil { + return nil, err + } + buffer := c.readWaitOption.NewBuffer() + writer := tcpip.SliceWriter(buffer.FreeBytes()) + result, err := c.ep.Read(&writer, tcpip.ReadOptions{}) + if _, wouldBlock := err.(*tcpip.ErrWouldBlock); wouldBlock { + buffer.Release() + continue + } + if err != nil { + buffer.Release() + return nil, c.translateReadError(err) + } + if result.Count == 0 { + buffer.Release() + continue + } + buffer.Truncate(result.Count) + c.readWaitOption.PostReturn(buffer) + c.ep.ModerateRecvBuf(result.Count) + return buffer, nil + } +} + +func (c *gTCPConn) Read(b []byte) (int, error) { + c.readMu.Lock() + defer c.readMu.Unlock() + + writer := tcpip.SliceWriter(b) + n, err := c.readTo(&writer, c.readCancel()) + if n != 0 { + c.ep.ModerateRecvBuf(n) + } + return n, err +} + +func (c *gTCPConn) readTo(writer io.Writer, deadline <-chan struct{}) (int, error) { + select { + case <-deadline: + return 0, c.newOpError("read", os.ErrDeadlineExceeded) + default: + } + + result, err := c.ep.Read(writer, tcpip.ReadOptions{}) + if _, wouldBlock := err.(*tcpip.ErrWouldBlock); wouldBlock { + waitEntry, notifyCh := waiter.NewChannelEntry(waiter.ReadableEvents) + c.wq.EventRegister(&waitEntry) + defer c.wq.EventUnregister(&waitEntry) + for { + result, err = c.ep.Read(writer, tcpip.ReadOptions{}) + if _, wouldBlock = err.(*tcpip.ErrWouldBlock); !wouldBlock { + break + } + select { + case <-deadline: + return 0, c.newOpError("read", os.ErrDeadlineExceeded) + case <-notifyCh: + } + } + } + + if err != nil { + return 0, c.translateReadError(err) + } + return result.Count, nil +} + +func (c *gTCPConn) waitReadable(deadline <-chan struct{}) error { + select { + case <-deadline: + return c.newOpError("read", os.ErrDeadlineExceeded) + default: + } + if c.ep.Readiness(waiter.ReadableEvents)&waiter.ReadableEvents != 0 { + return nil + } + + waitEntry, notifyCh := waiter.NewChannelEntry(waiter.ReadableEvents) + c.wq.EventRegister(&waitEntry) + defer c.wq.EventUnregister(&waitEntry) + for c.ep.Readiness(waiter.ReadableEvents)&waiter.ReadableEvents == 0 { + select { + case <-deadline: + return c.newOpError("read", os.ErrDeadlineExceeded) + case <-notifyCh: + } + } + return nil +} + +func (c *gTCPConn) translateReadError(err tcpip.Error) error { + if _, closed := err.(*tcpip.ErrClosedForReceive); closed { + return io.EOF + } + return c.newOpError("read", gonet.TranslateNetstackError(err)) +} + +func (c *gTCPConn) Write(b []byte) (int, error) { + deadline := c.writeCancel() + + select { + case <-deadline: + return 0, c.newOpError("write", os.ErrDeadlineExceeded) + default: + } + + var ( + reader bytes.Reader + nBytes int + entry waiter.Entry + ch <-chan struct{} + ) + for nBytes != len(b) { + reader.Reset(b[nBytes:]) + n, err := c.ep.Write(&reader, tcpip.WriteOptions{}) + nBytes += int(n) + switch err.(type) { + case nil: + case *tcpip.ErrWouldBlock: + if ch == nil { + entry, ch = waiter.NewChannelEntry(waiter.WritableEvents) + c.wq.EventRegister(&entry) + defer c.wq.EventUnregister(&entry) + } else { + select { + case <-deadline: + return nBytes, c.newOpError("write", os.ErrDeadlineExceeded) + case <-ch: + continue + } + } + default: + return nBytes, c.newOpError("write", gonet.TranslateNetstackError(err)) + } + } + return nBytes, nil +} + +func (c *gTCPConn) Close() error { + c.ep.Close() + return nil +} + +func (c *gTCPConn) CloseRead() error { + if err := c.ep.Shutdown(tcpip.ShutdownRead); err != nil { + return c.newOpError("close", errors.New(err.String())) + } + return nil +} + +func (c *gTCPConn) CloseWrite() error { + if err := c.ep.Shutdown(tcpip.ShutdownWrite); err != nil { + return c.newOpError("close", errors.New(err.String())) + } + return nil +} + +func (c *gTCPConn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *gTCPConn) RemoteAddr() net.Addr { + return c.remoteAddr +} + +func (c *gTCPConn) SetDeadline(t time.Time) error { + return c.gTCPDeadline.SetDeadline(t) +} + +func (c *gTCPConn) SetReadDeadline(t time.Time) error { + return c.gTCPDeadline.SetReadDeadline(t) +} + +func (c *gTCPConn) SetWriteDeadline(t time.Time) error { + return c.gTCPDeadline.SetWriteDeadline(t) +} + +func (c *gTCPConn) newOpError(op string, err error) *net.OpError { + return &net.OpError{ + Op: op, + Net: "tcp", + Source: c.localAddr, + Addr: c.remoteAddr, + Err: err, + } +} + +type gTCPDeadline struct { + mu sync.Mutex + + readTimer *time.Timer + readCancelCh chan struct{} + writeTimer *time.Timer + writeCancelCh chan struct{} +} + +func (d *gTCPDeadline) init() { + d.readCancelCh = make(chan struct{}) + d.writeCancelCh = make(chan struct{}) +} + +func (d *gTCPDeadline) readCancel() <-chan struct{} { + d.mu.Lock() + cancelCh := d.readCancelCh + d.mu.Unlock() + return cancelCh +} + +func (d *gTCPDeadline) writeCancel() <-chan struct{} { + d.mu.Lock() + cancelCh := d.writeCancelCh + d.mu.Unlock() + return cancelCh +} + +func (d *gTCPDeadline) SetDeadline(t time.Time) error { + d.mu.Lock() + d.setDeadline(&d.readCancelCh, &d.readTimer, t) + d.setDeadline(&d.writeCancelCh, &d.writeTimer, t) + d.mu.Unlock() + return nil +} + +func (d *gTCPDeadline) SetReadDeadline(t time.Time) error { + d.mu.Lock() + d.setDeadline(&d.readCancelCh, &d.readTimer, t) + d.mu.Unlock() + return nil +} + +func (d *gTCPDeadline) SetWriteDeadline(t time.Time) error { + d.mu.Lock() + d.setDeadline(&d.writeCancelCh, &d.writeTimer, t) + d.mu.Unlock() + return nil +} + +func (d *gTCPDeadline) setDeadline(cancelCh *chan struct{}, timer **time.Timer, t time.Time) { + if *timer != nil && !(*timer).Stop() { + *cancelCh = make(chan struct{}) + } + + select { + case <-*cancelCh: + *cancelCh = make(chan struct{}) + default: + } + + if t.IsZero() { + *timer = nil + return + } + + timeout := time.Until(t) + if timeout <= 0 { + close(*cancelCh) + return + } + + ch := *cancelCh + *timer = time.AfterFunc(timeout, func() { + close(ch) + }) +} From 27a88e979f572d38039a291d819fd3e733b34891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 2 May 2026 15:42:00 +0800 Subject: [PATCH 09/24] Add DNS mode --- redirect_nftables_rules.go | 26 +++++++--------- tun.go | 62 +++++++++++++++++++++++++++++++++++++- tun_linux.go | 38 ++++++++++------------- tun_windows.go | 35 +++++++++------------ 4 files changed, 103 insertions(+), 58 deletions(-) diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index 6e8cff50..1ef5c19b 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -675,7 +675,7 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft nftablesCreateExcludeDestinationIPSet(nft, table, chain, inet6RouteExcludeAddress.ID, inet6RouteExcludeAddress.Name, nftables.TableFamilyIPv6, false) } - if !r.tunOptions.EXP_DisableDNSHijack && ((chain.Hooknum == nftables.ChainHookPrerouting && chain.Type == nftables.ChainTypeNAT) || + if r.tunOptions.DNSModeOrDefault() == DNSModeHijack && ((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, 5, "inet4_local_address_set") @@ -997,23 +997,19 @@ func (r *autoRedirect) nftablesCreateDNSHijackRulesForFamily( if err != nil { return E.Cause(err, "add dns protocol set") } - dnsServer := common.Find(r.tunOptions.DNSServers, func(it netip.Addr) bool { - return it.Is4() == (family == nftables.TableFamilyIPv4) - }) - if !dnsServer.IsValid() { - if family == nftables.TableFamilyIPv4 { - if HasNextAddress(r.tunOptions.Inet4Address[0], 1) { - dnsServer = r.tunOptions.Inet4Address[0].Addr().Next() - } - } else { - if HasNextAddress(r.tunOptions.Inet6Address[0], 1) { - dnsServer = r.tunOptions.Inet6Address[0].Addr().Next() - } - } + var dnsServers []netip.Addr + if family == nftables.TableFamilyIPv4 { + dnsServers, err = r.tunOptions.Inet4DNSAddress() + } else { + dnsServers, err = r.tunOptions.Inet6DNSAddress() + } + if err != nil { + return err } - if !dnsServer.IsValid() { + if len(dnsServers) == 0 { return nil } + dnsServer := dnsServers[0] exprs := []expr.Any{ &expr.Meta{ Key: expr.MetaKeyNFPROTO, diff --git a/tun.go b/tun.go index 5f417bef..abfe67fa 100644 --- a/tun.go +++ b/tun.go @@ -9,8 +9,10 @@ import ( "strings" "time" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" @@ -68,6 +70,12 @@ const ( DefaultIPRoute2AutoRedirectFallbackRuleIndex = 32768 ) +const ( + DNSModeDisabled = "disabled" + DNSModeNative = "native" + DNSModeHijack = "hijack" +) + type Options struct { Name string Inet4Address []netip.Prefix @@ -78,7 +86,8 @@ type Options struct { InterfaceScope bool Inet4Gateway netip.Addr Inet6Gateway netip.Addr - DNSServers []netip.Addr + DNSMode string + DNSAddress []netip.Addr IPRoute2TableIndex int IPRoute2RuleIndex int IPRoute2AutoRedirectFallbackRuleIndex int @@ -124,6 +133,57 @@ type Options struct { EXP_SendMsgX bool } +func (o *Options) DNSModeOrDefault() string { + if o.DNSMode == "" { + return DNSModeHijack + } + return o.DNSMode +} + +func (o *Options) DNSServerAddress() ([]netip.Addr, error) { + inet4DNS, err := o.Inet4DNSAddress() + if err != nil { + return nil, err + } + inet6DNS, err := o.Inet6DNSAddress() + if err != nil { + return nil, err + } + return append(inet4DNS, inet6DNS...), nil +} + +func (o *Options) Inet4DNSAddress() ([]netip.Addr, error) { + if len(o.Inet4Address) == 0 { + return nil, nil + } + if len(o.DNSAddress) > 0 { + return common.Filter(o.DNSAddress, netip.Addr.Is4), nil + } + if HasNextAddress(o.Inet4Address[0], 1) { + return []netip.Addr{o.Inet4Address[0].Addr().Next()}, nil + } + if !(len(o.Inet6Address) > 0 && HasNextAddress(o.Inet6Address[0], 1)) { + return nil, E.New("no IPv4 server configured and no usable next address in ", o.Inet6Address[0], " for DNS") + } + return nil, nil +} + +func (o *Options) Inet6DNSAddress() ([]netip.Addr, error) { + if len(o.Inet6Address) == 0 { + return nil, nil + } + if len(o.DNSAddress) > 0 { + return common.Filter(o.DNSAddress, netip.Addr.Is6), nil + } + if HasNextAddress(o.Inet6Address[0], 1) { + return []netip.Addr{o.Inet6Address[0].Addr().Next()}, nil + } + if !(len(o.Inet4Address) > 0 && HasNextAddress(o.Inet4Address[0], 1)) { + return nil, E.New("no IPv6 server configured and no usable next address in ", o.Inet6Address[0], " for DNS") + } + return nil, nil +} + func (o *Options) Inet4GatewayAddr() netip.Addr { if o.Inet4Gateway.IsValid() { return o.Inet4Gateway diff --git a/tun_linux.go b/tun_linux.go index d125f82f..bc553fa9 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -316,7 +316,12 @@ func (t *NativeTun) Start() error { return E.Cause(err, "set rules") } - t.setSearchDomainForSystemdResolved() + if t.options.DNSMode != DNSModeDisabled { + err = t.setSearchDomainForSystemdResolved() + if err != nil { + return E.Cause(err, "set search domain") + } + } if t.options.AutoRoute && runtime.GOOS == "android" { t.interfaceCallback = t.options.InterfaceMonitor.RegisterCallback(t.routeUpdate) @@ -331,7 +336,9 @@ func (t *NativeTun) Close() error { if t.options.EXP_ExternalConfiguration { return common.Close(common.PtrOrNil(t.tunFile)) } - t.unsetSearchDomainForSystemdResolved() + if t.options.DNSMode != DNSModeDisabled { + t.unsetSearchDomainForSystemdResolved() + } t.unsetAddresses() return E.Errors(t.unsetRoute(), t.unsetRules(), common.Close(common.PtrOrNil(t.tunFile))) } @@ -1072,37 +1079,24 @@ func (t *NativeTun) routeUpdate(_ *control.Interface, flags int) { } } -func (t *NativeTun) setSearchDomainForSystemdResolved() { - if t.options.EXP_DisableDNSHijack { - return - } +func (t *NativeTun) setSearchDomainForSystemdResolved() error { ctlPath, err := exec.LookPath("resolvectl") if err != nil { - return - } - dnsServer := t.options.DNSServers - if len(dnsServer) == 0 { - if len(t.options.Inet4Address) > 0 && HasNextAddress(t.options.Inet4Address[0], 1) { - dnsServer = append(dnsServer, t.options.Inet4Address[0].Addr().Next()) - } - if len(t.options.Inet6Address) > 0 && HasNextAddress(t.options.Inet6Address[0], 1) { - dnsServer = append(dnsServer, t.options.Inet6Address[0].Addr().Next()) - } + return nil } - if len(dnsServer) == 0 { - return + dnsAddress, err := t.options.DNSServerAddress() + if err != nil { + return err } 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() + _ = shell.Exec(ctlPath, append([]string{"dns", t.options.Name}, common.Map(dnsAddress, netip.Addr.String)...)...).Run() }() + return nil } func (t *NativeTun) unsetSearchDomainForSystemdResolved() { - if t.options.EXP_DisableDNSHijack { - return - } ctlPath, err := exec.LookPath("resolvectl") if err != nil { return diff --git a/tun_windows.go b/tun_windows.go index d00d51db..6dfce2f2 100644 --- a/tun_windows.go +++ b/tun_windows.go @@ -16,7 +16,6 @@ import ( "github.com/sagernet/sing-tun/internal/winipcfg" "github.com/sagernet/sing-tun/internal/winsys" "github.com/sagernet/sing-tun/internal/wintun" - "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/windnsapi" @@ -81,16 +80,14 @@ func (t *NativeTun) configure() error { if err != nil { return E.Cause(err, "set ipv4 address") } - if t.options.AutoRoute && !t.options.EXP_DisableDNSHijack { - dnsServers := common.Filter(t.options.DNSServers, netip.Addr.Is4) - if len(dnsServers) == 0 && HasNextAddress(t.options.Inet4Address[0], 1) { - dnsServers = []netip.Addr{t.options.Inet4Address[0].Addr().Next()} + if t.options.AutoRoute && t.options.DNSModeOrDefault() != DNSModeDisabled { + dnsServers, err := t.options.Inet4DNSAddress() + if err != nil { + return err } - if len(dnsServers) > 0 { - err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET), dnsServers, nil) - if err != nil { - return E.Cause(err, "set ipv4 dns") - } + err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET), dnsServers, nil) + if err != nil { + return E.Cause(err, "set ipv4 dns") } } else { err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET), nil, nil) @@ -104,16 +101,14 @@ func (t *NativeTun) configure() error { if err != nil { return E.Cause(err, "set ipv6 address") } - if t.options.AutoRoute && !t.options.EXP_DisableDNSHijack { - dnsServers := common.Filter(t.options.DNSServers, netip.Addr.Is6) - if len(dnsServers) == 0 && HasNextAddress(t.options.Inet6Address[0], 1) { - dnsServers = []netip.Addr{t.options.Inet6Address[0].Addr().Next()} + if t.options.AutoRoute && t.options.DNSModeOrDefault() != DNSModeDisabled { + dnsServers, err := t.options.Inet6DNSAddress() + if err != nil { + return err } - if len(dnsServers) > 0 { - err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET6), dnsServers, nil) - if err != nil { - return E.Cause(err, "set ipv6 dns") - } + err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET6), dnsServers, nil) + if err != nil { + return E.Cause(err, "set ipv6 dns") } } else { err = luid.SetDNS(winipcfg.AddressFamily(windows.AF_INET6), nil, nil) @@ -334,7 +329,7 @@ func (t *NativeTun) Start() error { } } - if !t.options.EXP_DisableDNSHijack { + if t.options.DNSModeOrDefault() == DNSModeHijack { blockDNSCondition := make([]winsys.FWPM_FILTER_CONDITION0, 1) blockDNSCondition[0].FieldKey = winsys.FWPM_CONDITION_IP_REMOTE_PORT blockDNSCondition[0].MatchType = winsys.FWP_MATCH_EQUAL From f0cdbedba7cb78235d0549ff96b1b2b223a20b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 6 Jul 2026 11:49:18 +0800 Subject: [PATCH 10/24] Add flow dispatcher --- flow.go | 32 ++ flow_dispatch.go | 631 +++++++++++++++++++++++++++++++++++ flow_mtu.go | 159 +++++++++ flow_nat.go | 106 ++++++ flow_parse.go | 256 ++++++++++++++ flow_reject.go | 210 ++++++++++++ flow_rewrite.go | 350 +++++++++++++++++++ nfqueue_linux.go | 30 +- ping/destination.go | 27 +- ping/destination_gvisor.go | 143 -------- ping/destination_rewriter.go | 79 ----- ping/port.go | 194 +++++++++++ ping/source_rewriter.go | 150 --------- redirect_linux.go | 34 +- route_direct.go | 61 ---- stack.go | 6 - stack_gvisor.go | 57 +++- stack_gvisor_filter.go | 69 +++- stack_gvisor_icmp.go | 360 ++++++++++++-------- stack_gvisor_lazy.go | 3 +- stack_gvisor_tcp.go | 11 +- stack_gvisor_udp.go | 11 +- stack_mixed.go | 20 +- stack_system.go | 372 +++++---------------- stack_system_nat.go | 13 +- tun.go | 14 +- tun_linux.go | 6 +- 27 files changed, 2453 insertions(+), 951 deletions(-) create mode 100644 flow.go create mode 100644 flow_dispatch.go create mode 100644 flow_mtu.go create mode 100644 flow_nat.go create mode 100644 flow_parse.go create mode 100644 flow_reject.go create mode 100644 flow_rewrite.go delete mode 100644 ping/destination_gvisor.go delete mode 100644 ping/destination_rewriter.go create mode 100644 ping/port.go delete mode 100644 ping/source_rewriter.go delete mode 100644 route_direct.go diff --git a/flow.go b/flow.go new file mode 100644 index 00000000..a920e93e --- /dev/null +++ b/flow.go @@ -0,0 +1,32 @@ +package tun + +import "net/netip" + +type FlowVerdict struct { + Action FlowAction + Port Port + Destination netip.AddrPort +} + +type FlowAction uint8 + +const ( + ActionAccept FlowAction = iota + ActionFlow + ActionReject + ActionDrop + ActionBypass +) + +type Port interface { + PortAddresses() (v4 netip.Addr, v6 netip.Addr) + PortMTU() uint32 + AttachReturn(returnPath Return) error + DetachReturn(returnPath Return) error + WritePackets(packets [][]byte) error +} + +type Return interface { + ReturnHeadroom() int + ReturnPackets(packets [][]byte) [][]byte +} diff --git a/flow_dispatch.go b/flow_dispatch.go new file mode 100644 index 00000000..d9ec9a31 --- /dev/null +++ b/flow_dispatch.go @@ -0,0 +1,631 @@ +package tun + +import ( + "net/netip" + "sync/atomic" + "time" + + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/header" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" +) + +const ( + tcpEstablishedTimeout = 2*time.Hour + 4*time.Minute + tcpTransitoryTimeout = 4 * time.Minute + + defaultUDPTimeout = 5 * time.Minute + + defaultICMPTimeout = time.Minute + + flowTombstoneTimeout = 4 * time.Minute + + flowTableCapacity = 16384 + + flowSweepInterval = 30 * time.Second + flowSweepLimit = flowTableCapacity / int(flowTombstoneTimeout/flowSweepInterval) +) + +type ForwardWriteback interface { + ReturnHeadroom() int + WriteReturnPackets(packets [][]byte) error +} + +type flowEntry struct { + action FlowAction + deadline int64 + idle time.Duration + flow *forwardFlow +} + +type forwardFlow struct { + nat *portNAT + reverseKey flowKey + forwardRule rewriteRule + reverseRule rewriteRule + effectiveMTU uint32 + protocol uint8 + + clientAddress netip.Addr + clientSelector uint16 + clientDestinationAddress netip.Addr + clientDestinationPort uint16 + serverAddress netip.Addr + dnatAddress bool + dnatPort bool + + finForward bool + established atomic.Bool + finReverse atomic.Bool + closed atomic.Bool + lastReverse atomic.Int64 +} + +func (f *forwardFlow) observeReverse(packet *forwardPacket, now int64) { + f.lastReverse.Store(now) + if packet.protocol != uint8(header.TCPProtocolNumber) { + return + } + f.established.Store(true) + if packet.tcpFlags&header.TCPFlagRst != 0 { + f.closed.Store(true) + return + } + if packet.tcpFlags&header.TCPFlagFin != 0 { + f.finReverse.Store(true) + } +} + +type ForwardDispatcher struct { + epoch time.Time + handler Handler + writeback ForwardWriteback + logger logger.Logger + udpTimeout time.Duration + icmpTimeout time.Duration + + table map[flowKey]*flowEntry + lastSweep int64 + ports map[Port]*portNAT + natList atomic.Pointer[[]*portNAT] + + activeNATs []*portNAT + writebackBatch [][]byte + returnPath forwardReturn + + segmentBuffers [][]byte + segmentSizes []int +} + +func NewForwardDispatcher(handler Handler, writeback ForwardWriteback, logger logger.Logger, udpTimeout time.Duration, icmpTimeout time.Duration) *ForwardDispatcher { + dispatcher := &ForwardDispatcher{ + epoch: time.Now(), + handler: handler, + writeback: writeback, + logger: logger, + udpTimeout: udpTimeout, + icmpTimeout: icmpTimeout, + table: make(map[flowKey]*flowEntry), + ports: make(map[Port]*portNAT), + } + if dispatcher.udpTimeout <= 0 { + dispatcher.udpTimeout = defaultUDPTimeout + } + if dispatcher.icmpTimeout <= 0 { + dispatcher.icmpTimeout = defaultICMPTimeout + } + dispatcher.returnPath.dispatcher = dispatcher + return dispatcher +} + +func (d *ForwardDispatcher) now() int64 { + return int64(time.Since(d.epoch)) +} + +func (d *ForwardDispatcher) Close() { + if d == nil { + return + } + d.returnPath.closed.Store(true) + for port, nat := range d.ports { + if nat != nil { + port.DetachReturn(&d.returnPath) + } + } +} + +func (d *ForwardDispatcher) Dispatch(packet []byte) bool { + if d == nil { + return false + } + parsed, ok := parseForwardPacket(packet) + if !ok || parsed.fragment || !parsed.hasFlow { + return false + } + key := parsed.flowKey() + now := d.now() + entry, loaded := d.table[key] + if loaded && d.entryExpired(entry, now) { + d.removeEntry(key, entry) + loaded = false + } + if loaded { + return d.handleHit(key, entry, &parsed, packet, now) + } + if parsed.protocol == uint8(header.TCPProtocolNumber) && + (parsed.tcpFlags&header.TCPFlagSyn == 0 || parsed.tcpFlags&header.TCPFlagAck != 0) { + return false + } + return d.judgeAndInstall(key, &parsed, packet, now) +} + +func (d *ForwardDispatcher) handleHit(key flowKey, entry *flowEntry, packet *forwardPacket, raw []byte, now int64) bool { + switch entry.action { + case ActionFlow: + flow := entry.flow + if flow.closed.Load() { + d.tombstoneEntry(entry, now) + return true + } + if packet.protocol == uint8(header.TCPProtocolNumber) { + if packet.tcpFlags&header.TCPFlagRst != 0 { + d.forwardToPort(flow, packet, raw) + flow.closed.Store(true) + d.tombstoneEntry(entry, now) + return true + } + if packet.tcpFlags&header.TCPFlagFin != 0 { + flow.finForward = true + } + } + entry.idle = d.flowIdle(flow) + entry.deadline = now + int64(entry.idle) + d.forwardToPort(flow, packet, raw) + return true + case ActionAccept: + entry.deadline = now + int64(entry.idle) + if packet.protocol == uint8(header.TCPProtocolNumber) && packet.tcpFlags&header.TCPFlagRst != 0 { + d.removeEntry(key, entry) + } + return false + case ActionReject: + entry.deadline = now + int64(entry.idle) + d.stageReject(packet) + return true + default: + entry.deadline = now + int64(entry.idle) + return true + } +} + +func (d *ForwardDispatcher) judgeAndInstall(key flowKey, packet *forwardPacket, raw []byte, now int64) bool { + verdict := d.handler.JudgeFlow(packet.protocol, packet.source, packet.destination) + switch verdict.Action { + case ActionFlow: + if verdict.Port != nil { + flow, created := d.createFlow(packet, verdict) + if created { + entry := &flowEntry{action: ActionFlow, flow: flow, idle: d.flowIdle(flow)} + entry.deadline = now + int64(entry.idle) + d.insertEntry(key, entry, now) + d.forwardToPort(flow, packet, raw) + return true + } + } + d.installSimple(key, ActionAccept, packet.protocol, now) + return false + case ActionReject: + d.installSimple(key, ActionReject, packet.protocol, now) + d.stageReject(packet) + return true + case ActionDrop: + d.installSimple(key, ActionDrop, packet.protocol, now) + return true + default: + d.installSimple(key, ActionAccept, packet.protocol, now) + return false + } +} + +func (d *ForwardDispatcher) installSimple(key flowKey, action FlowAction, protocol uint8, now int64) { + entry := &flowEntry{action: action, idle: d.idleTimeout(protocol, false)} + entry.deadline = now + int64(entry.idle) + d.insertEntry(key, entry, now) +} + +func (d *ForwardDispatcher) idleTimeout(protocol uint8, established bool) time.Duration { + switch protocol { + case uint8(header.TCPProtocolNumber): + if established { + return tcpEstablishedTimeout + } + return tcpTransitoryTimeout + case uint8(header.UDPProtocolNumber): + return d.udpTimeout + default: + return d.icmpTimeout + } +} + +func (d *ForwardDispatcher) flowIdle(flow *forwardFlow) time.Duration { + established := flow.established.Load() && !flow.finForward && !flow.finReverse.Load() + return d.idleTimeout(flow.protocol, established) +} + +func (d *ForwardDispatcher) createFlow(packet *forwardPacket, verdict FlowVerdict) (*forwardFlow, bool) { + var portAddress netip.Addr + inet4Address, inet6Address := verdict.Port.PortAddresses() + if packet.ipVersion == 6 { + portAddress = inet6Address + } else { + portAddress = inet4Address + } + if !portAddress.IsValid() { + return nil, false + } + effectiveMTU := verdict.Port.PortMTU() + if packet.ipVersion == 6 && effectiveMTU != 0 && effectiveMTU < header.IPv6MinimumMTU { + return nil, false + } + isICMP := isICMPProtocol(packet.protocol) + clientDestinationAddress := packet.destination.Addr() + clientDestinationPort := packet.destination.Port() + serverAddress := clientDestinationAddress + serverPort := clientDestinationPort + if verdict.Destination.Addr().IsValid() { + serverAddress = verdict.Destination.Addr() + } + if verdict.Destination.Port() != 0 && !isICMP { + serverPort = verdict.Destination.Port() + } + nat := d.natFor(verdict.Port) + if nat == nil { + return nil, false + } + selector, reverseKey, allocated := nat.allocateSelector(packet.protocol, portAddress, serverAddress, serverPort, packet.source.Port()) + if !allocated { + return nil, false + } + flow := &forwardFlow{ + nat: nat, + reverseKey: reverseKey, + effectiveMTU: effectiveMTU, + protocol: packet.protocol, + clientAddress: packet.source.Addr(), + clientSelector: packet.source.Port(), + clientDestinationAddress: clientDestinationAddress, + clientDestinationPort: clientDestinationPort, + serverAddress: serverAddress, + dnatAddress: serverAddress != clientDestinationAddress, + dnatPort: serverPort != clientDestinationPort && !isICMP, + } + flow.forwardRule = rewriteRule{ + sourceAddress: tcpip.AddrFromSlice(portAddress.AsSlice()), + sourcePort: selector, + rewriteSourcePort: true, + } + if flow.dnatAddress { + flow.forwardRule.destinationAddress = tcpip.AddrFromSlice(serverAddress.AsSlice()) + } + if flow.dnatPort { + flow.forwardRule.destinationPort = serverPort + flow.forwardRule.rewriteDestinationPort = true + } + flow.reverseRule = rewriteRule{ + destinationAddress: tcpip.AddrFromSlice(flow.clientAddress.AsSlice()), + destinationPort: flow.clientSelector, + rewriteDestinationPort: true, + } + if flow.dnatAddress { + flow.reverseRule.sourceAddress = tcpip.AddrFromSlice(clientDestinationAddress.AsSlice()) + } + if flow.dnatPort { + flow.reverseRule.sourcePort = clientDestinationPort + flow.reverseRule.rewriteSourcePort = true + } + nat.insert(reverseKey, flow) + return flow, true +} + +func (d *ForwardDispatcher) natFor(port Port) *portNAT { + nat, loaded := d.ports[port] + if loaded { + return nat + } + err := port.AttachReturn(&d.returnPath) + if err != nil { + d.logger.Trace(E.Cause(err, "attach return path")) + d.ports[port] = nil + return nil + } + nat = newPortNAT(port) + d.ports[port] = nat + var natList []*portNAT + current := d.natList.Load() + if current != nil { + natList = append(natList, *current...) + } + natList = append(natList, nat) + d.natList.Store(&natList) + return nat +} + +func (d *ForwardDispatcher) forwardToPort(flow *forwardFlow, packet *forwardPacket, raw []byte) { + if flow.effectiveMTU != 0 && uint32(len(raw)) > flow.effectiveMTU { + if packet.protocol == uint8(header.TCPProtocolNumber) { + d.rewriteForward(flow, packet) + d.resegmentTCP(flow, packet, raw) + return + } + if packet.ipVersion == 4 { + ipHdr := packet.network.(header.IPv4) + if ipHdr.Flags()&header.IPv4FlagDontFragment == 0 { + d.rewriteForward(flow, packet) + fragments, ok := fragmentIPv4Packet(ipHdr, flow.effectiveMTU) + if ok { + for _, fragment := range fragments { + d.stagePort(flow.nat, fragment) + } + } + return + } + reply, ok := buildFragmentationNeeded(ipHdr, flow.effectiveMTU, d.writeback.ReturnHeadroom()) + if ok { + d.writebackBatch = append(d.writebackBatch, reply) + } + return + } + reply, ok := buildPacketTooBig(packet.network.(header.IPv6), flow.effectiveMTU, d.writeback.ReturnHeadroom()) + if ok { + d.writebackBatch = append(d.writebackBatch, reply) + } + return + } + d.rewriteForward(flow, packet) + d.stagePort(flow.nat, raw) +} + +func (d *ForwardDispatcher) rewriteForward(flow *forwardFlow, packet *forwardPacket) { + if packet.isTCPSyn() { + applyRewriteRaw(packet, &flow.forwardRule) + clampTCPMSS(packet, flow.effectiveMTU) + recomputeChecksums(packet) + } else { + applyRewrite(packet, &flow.forwardRule) + } +} + +func (d *ForwardDispatcher) stagePort(nat *portNAT, packet []byte) { + if len(nat.pending) == 0 { + d.activeNATs = append(d.activeNATs, nat) + } + nat.pending = append(nat.pending, packet) +} + +func (d *ForwardDispatcher) flushPort(nat *portNAT) { + if len(nat.pending) == 0 { + return + } + err := nat.port.WritePackets(nat.pending) + if err != nil { + d.logger.Trace(E.Cause(err, "forward packets")) + } + nat.pending = nat.pending[:0] +} + +func (d *ForwardDispatcher) stageReject(packet *forwardPacket) { + reply, ok := buildReject(packet, d.writeback.ReturnHeadroom()) + if ok { + d.writebackBatch = append(d.writebackBatch, reply) + } +} + +func (d *ForwardDispatcher) Flush() { + if d == nil { + return + } + for _, nat := range d.activeNATs { + d.flushPort(nat) + } + d.activeNATs = d.activeNATs[:0] + if len(d.writebackBatch) > 0 { + err := d.writeback.WriteReturnPackets(d.writebackBatch) + if err != nil { + d.logger.Trace(E.Cause(err, "write back packets")) + } + d.writebackBatch = d.writebackBatch[:0] + } + d.maybeSweep(d.now()) +} + +func (d *ForwardDispatcher) entryExpired(entry *flowEntry, now int64) bool { + if now <= entry.deadline { + return false + } + if entry.action == ActionFlow { + lastReverse := entry.flow.lastReverse.Load() + reverseDeadline := lastReverse + int64(entry.idle) + if lastReverse != 0 && now <= reverseDeadline { + entry.deadline = reverseDeadline + return false + } + } + return true +} + +func (d *ForwardDispatcher) tombstoneEntry(entry *flowEntry, now int64) { + entry.action = ActionDrop + entry.idle = flowTombstoneTimeout + entry.deadline = now + int64(entry.idle) +} + +func (d *ForwardDispatcher) removeEntry(key flowKey, entry *flowEntry) { + delete(d.table, key) + if entry.flow != nil { + entry.flow.closed.Store(true) + entry.flow.nat.delete(entry.flow.reverseKey) + } +} + +func (d *ForwardDispatcher) insertEntry(key flowKey, entry *flowEntry, now int64) { + if len(d.table) >= flowTableCapacity { + d.evictEntries(now) + } + d.table[key] = entry +} + +func (d *ForwardDispatcher) evictEntries(now int64) { + var ( + freed int + visited int + oldestKey flowKey + oldest *flowEntry + ) + for key, entry := range d.table { + if d.entryExpired(entry, now) { + d.removeEntry(key, entry) + freed++ + } else if oldest == nil || entry.deadline < oldest.deadline { + oldestKey = key + oldest = entry + } + visited++ + if visited >= flowSweepLimit { + break + } + } + if freed == 0 && oldest != nil { + d.removeEntry(oldestKey, oldest) + } +} + +func (d *ForwardDispatcher) maybeSweep(now int64) { + if now-d.lastSweep < int64(flowSweepInterval) { + return + } + d.lastSweep = now + visited := 0 + for key, entry := range d.table { + if entry.action == ActionFlow && entry.flow.closed.Load() { + d.tombstoneEntry(entry, now) + } else if d.entryExpired(entry, now) { + d.removeEntry(key, entry) + } + visited++ + if visited >= flowSweepLimit { + break + } + } +} + +func isICMPProtocol(protocol uint8) bool { + return protocol == uint8(header.ICMPv4ProtocolNumber) || protocol == uint8(header.ICMPv6ProtocolNumber) +} + +var _ Return = (*forwardReturn)(nil) + +type forwardReturn struct { + dispatcher *ForwardDispatcher + closed atomic.Bool +} + +func (r *forwardReturn) ReturnHeadroom() int { + return r.dispatcher.writeback.ReturnHeadroom() +} + +func (r *forwardReturn) ReturnPackets(packets [][]byte) [][]byte { + if r.closed.Load() { + return packets + } + natListPtr := r.dispatcher.natList.Load() + if natListPtr == nil { + return packets + } + natList := *natListPtr + headroom := r.dispatcher.writeback.ReturnHeadroom() + unconsumed := packets[:0] + var writeBatch [][]byte + now := r.dispatcher.now() + for _, raw := range packets { + if len(raw) < headroom+header.IPv4MinimumSize { + unconsumed = append(unconsumed, raw) + continue + } + parsed, ok := parseForwardPacket(raw[headroom:]) + if !ok || parsed.fragment { + unconsumed = append(unconsumed, raw) + continue + } + if !parsed.hasFlow { + if parsed.isICMPError() && returnICMPError(natList, &parsed) { + writeBatch = append(writeBatch, raw) + } else { + unconsumed = append(unconsumed, raw) + } + continue + } + var flow *forwardFlow + for _, nat := range natList { + flow = nat.lookup(parsed.flowKey()) + if flow != nil { + break + } + } + if flow == nil { + unconsumed = append(unconsumed, raw) + continue + } + if flow.closed.Load() { + continue + } + flow.observeReverse(&parsed, now) + if parsed.isTCPSyn() { + applyRewriteRaw(&parsed, &flow.reverseRule) + clampTCPMSS(&parsed, flow.effectiveMTU) + recomputeChecksums(&parsed) + } else { + applyRewrite(&parsed, &flow.reverseRule) + } + writeBatch = append(writeBatch, raw) + } + if len(writeBatch) > 0 { + err := r.dispatcher.writeback.WriteReturnPackets(writeBatch) + if err != nil { + r.dispatcher.logger.Trace(E.Cause(err, "write return packets")) + } + } + return unconsumed +} + +func returnICMPError(natList []*portNAT, parsed *forwardPacket) bool { + inner, ok := parsed.icmpErrorInner() + if !ok { + return false + } + embedded, parsedInner := parseEmbedded(inner) + if !parsedInner { + return false + } + key := embedded.flowKey().reversed() + var flow *forwardFlow + for _, nat := range natList { + flow = nat.lookup(key) + if flow != nil { + break + } + } + if flow == nil || flow.closed.Load() { + return false + } + rewriteEmbeddedSource(&embedded, tcpip.AddrFromSlice(flow.clientAddress.AsSlice()), flow.clientSelector, true) + if flow.dnatAddress || flow.dnatPort { + rewriteEmbeddedDestination(&embedded, tcpip.AddrFromSlice(flow.clientDestinationAddress.AsSlice()), flow.clientDestinationPort, flow.dnatPort) + } + parsed.network.SetDestinationAddr(flow.clientAddress) + if parsed.network.SourceAddr() == flow.serverAddress { + parsed.network.SetSourceAddr(flow.clientDestinationAddress) + } + recomputeChecksums(parsed) + return true +} diff --git a/flow_mtu.go b/flow_mtu.go new file mode 100644 index 00000000..26c675ba --- /dev/null +++ b/flow_mtu.go @@ -0,0 +1,159 @@ +package tun + +import ( + "github.com/sagernet/sing-tun/gtcpip/header" + E "github.com/sagernet/sing/common/exceptions" +) + +const segmentScratchCount = 128 + +// Linux delivers TSO aggregates to the TUN even with IFF_VNET_HDR off +// (observed on 6.x: the pre-segmentation skb is handed to the fd as-is). +func (d *ForwardDispatcher) resegmentTCP(flow *forwardFlow, packet *forwardPacket, raw []byte) { + if len(packet.transport) < header.TCPMinimumSize { + return + } + headerLength := len(raw) - len(packet.transport) + if packet.ipVersion == 6 && headerLength != header.IPv6MinimumSize { + reply, ok := buildPacketTooBig(packet.network.(header.IPv6), flow.effectiveMTU, d.writeback.ReturnHeadroom()) + if ok { + d.writebackBatch = append(d.writebackBatch, reply) + } + return + } + tcpHeaderLength := int(header.TCP(packet.transport).DataOffset()) + if tcpHeaderLength < header.TCPMinimumSize || tcpHeaderLength > len(packet.transport) { + return + } + totalHeaderLength := headerLength + tcpHeaderLength + segmentSize := int(flow.effectiveMTU) - totalHeaderLength + if segmentSize <= 0 { + return + } + gsoType := GSOTCPv4 + if packet.ipVersion == 6 { + gsoType = GSOTCPv6 + } + neededSegments := max((len(raw)-totalHeaderLength+segmentSize-1)/segmentSize, 1) + if d.segmentBuffers == nil || len(d.segmentBuffers) < neededSegments || len(d.segmentBuffers[0]) < int(flow.effectiveMTU) { + bufferSize := int(flow.effectiveMTU) + if d.segmentBuffers != nil && len(d.segmentBuffers[0]) > bufferSize { + bufferSize = len(d.segmentBuffers[0]) + } + segmentCount := max(neededSegments, segmentScratchCount, len(d.segmentBuffers)) + d.segmentBuffers = make([][]byte, segmentCount) + for i := range d.segmentBuffers { + d.segmentBuffers[i] = make([]byte, bufferSize) + } + d.segmentSizes = make([]int, segmentCount) + } + n, err := GSOSplit(raw, GSOOptions{ + GSOType: gsoType, + HdrLen: uint16(totalHeaderLength), + CsumStart: uint16(headerLength), + CsumOffset: header.TCPChecksumOffset, + GSOSize: uint16(segmentSize), + }, d.segmentBuffers, d.segmentSizes, 0) + if err != nil { + d.logger.Trace(E.Cause(err, "resegment packet")) + return + } + for i := range n { + d.stagePort(flow.nat, d.segmentBuffers[i][:d.segmentSizes[i]]) + } + d.flushPort(flow.nat) +} + +const synthesizedTTL = 64 + +func fragmentIPv4Packet(packet header.IPv4, effectiveMTU uint32) ([][]byte, bool) { + headerLength := int(packet.HeaderLength()) + if headerLength < header.IPv4MinimumSize || headerLength >= len(packet) { + return nil, false + } + payload := packet[headerLength:] + maxFragmentPayload := (int(effectiveMTU) - headerLength) &^ 7 + if maxFragmentPayload <= 0 { + return nil, false + } + baseOffset := packet.FragmentOffset() + originalMore := packet.Flags()&header.IPv4FlagMoreFragments != 0 + baseFlags := packet.Flags() &^ header.IPv4FlagMoreFragments + var fragments [][]byte + for start := 0; start < len(payload); start += maxFragmentPayload { + end := min(start+maxFragmentPayload, len(payload)) + fragment := header.IPv4(make([]byte, headerLength+end-start)) + copy(fragment, packet[:headerLength]) + copy(fragment[headerLength:], payload[start:end]) + flags := baseFlags + if originalMore || end < len(payload) { + flags |= header.IPv4FlagMoreFragments + } + fragment.SetFlagsFragmentOffset(flags, baseOffset+uint16(start)) + fragment.SetTotalLength(uint16(len(fragment))) + fragment.SetChecksum(0) + fragment.SetChecksum(^fragment.CalculateChecksum()) + fragments = append(fragments, fragment) + } + return fragments, true +} + +func buildFragmentationNeeded(packet header.IPv4, effectiveMTU uint32, headroom int) ([]byte, bool) { + advertised := max(effectiveMTU, header.IPv4MinimumMTU) + originalLength := min(int(packet.TotalLength()), len(packet)) + minPayloadLength := int(packet.HeaderLength()) + header.ICMPv4MinimumErrorPayloadSize + if originalLength < minPayloadLength { + return nil, false + } + maxPayloadLength := header.IPv4MinimumProcessableDatagramSize - header.IPv4MinimumSize - header.ICMPv4MinimumSize + payloadLength := min(originalLength, maxPayloadLength) + size := header.IPv4MinimumSize + header.ICMPv4MinimumSize + payloadLength + buffer := make([]byte, headroom+size) + response := header.IPv4(buffer[headroom:]) + response.Encode(&header.IPv4Fields{ + TotalLength: uint16(size), + TTL: synthesizedTTL, + Protocol: uint8(header.ICMPv4ProtocolNumber), + SrcAddr: packet.DestinationAddr(), + DstAddr: packet.SourceAddr(), + }) + response.SetChecksum(^response.CalculateChecksum()) + icmpHdr := header.ICMPv4(response.Payload()) + icmpHdr.SetType(header.ICMPv4DstUnreachable) + icmpHdr.SetCode(header.ICMPv4FragmentationNeeded) + icmpHdr.SetMTU(uint16(min(advertised, uint32(0xffff)))) + copy(icmpHdr.Payload(), packet[:payloadLength]) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) + return buffer, true +} + +func buildPacketTooBig(packet header.IPv6, effectiveMTU uint32, headroom int) ([]byte, bool) { + advertised := max(effectiveMTU, header.IPv6MinimumMTU) + originalLength := min(header.IPv6MinimumSize+int(packet.PayloadLength()), len(packet)) + if originalLength < header.IPv6MinimumSize { + return nil, false + } + maxPayloadLength := header.IPv6MinimumMTU - header.IPv6MinimumSize - header.ICMPv6PacketTooBigMinimumSize + payloadLength := min(originalLength, maxPayloadLength) + size := header.IPv6MinimumSize + header.ICMPv6PacketTooBigMinimumSize + payloadLength + buffer := make([]byte, headroom+size) + response := header.IPv6(buffer[headroom:]) + response.Encode(&header.IPv6Fields{ + PayloadLength: uint16(header.ICMPv6PacketTooBigMinimumSize + payloadLength), + TransportProtocol: header.ICMPv6ProtocolNumber, + HopLimit: synthesizedTTL, + SrcAddr: packet.DestinationAddr(), + DstAddr: packet.SourceAddr(), + }) + icmpHdr := header.ICMPv6(response.Payload()) + icmpHdr.SetType(header.ICMPv6PacketTooBig) + icmpHdr.SetCode(header.ICMPv6UnusedCode) + icmpHdr.SetMTU(advertised) + copy(icmpHdr.Payload(), packet[:payloadLength]) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: response.SourceAddressSlice(), + Dst: response.DestinationAddressSlice(), + })) + return buffer, true +} diff --git a/flow_nat.go b/flow_nat.go new file mode 100644 index 00000000..fe4ea1af --- /dev/null +++ b/flow_nat.go @@ -0,0 +1,106 @@ +package tun + +import ( + "net/netip" + "runtime" + "sync" + + "github.com/sagernet/sing-tun/gtcpip/header" + "github.com/sagernet/sing/contrab/maphash" +) + +const ( + natSelectorMin = 49152 + natSelectorMax = 65535 +) + +type portNAT struct { + port Port + hasher maphash.Hasher[flowKey] + shardMask uint32 + shards []natShard + + counter uint32 + pending [][]byte +} + +type natShard struct { + access sync.RWMutex + flows map[flowKey]*forwardFlow +} + +func newPortNAT(port Port) *portNAT { + shardCount := 1 + for shardCount < runtime.GOMAXPROCS(0) { + shardCount <<= 1 + } + nat := &portNAT{ + port: port, + hasher: maphash.NewHasher[flowKey](), + shardMask: uint32(shardCount - 1), + shards: make([]natShard, shardCount), + } + for i := range nat.shards { + nat.shards[i].flows = make(map[flowKey]*forwardFlow) + } + return nat +} + +func (n *portNAT) shard(key flowKey) *natShard { + return &n.shards[n.hasher.Hash32(key)&n.shardMask] +} + +func (n *portNAT) lookup(key flowKey) *forwardFlow { + shard := n.shard(key) + shard.access.RLock() + flow := shard.flows[key] + shard.access.RUnlock() + return flow +} + +func (n *portNAT) insert(key flowKey, flow *forwardFlow) { + shard := n.shard(key) + shard.access.Lock() + shard.flows[key] = flow + shard.access.Unlock() +} + +func (n *portNAT) delete(key flowKey) { + shard := n.shard(key) + shard.access.Lock() + delete(shard.flows, key) + shard.access.Unlock() +} + +func (n *portNAT) reverseKeyFor(protocol uint8, portAddress, serverAddress netip.Addr, serverPort, selector uint16) flowKey { + if protocol == uint8(header.ICMPv4ProtocolNumber) || protocol == uint8(header.ICMPv6ProtocolNumber) { + return flowKey{ + protocol: protocol, + source: netip.AddrPortFrom(serverAddress, selector), + destination: netip.AddrPortFrom(portAddress, selector), + } + } + return flowKey{ + protocol: protocol, + source: netip.AddrPortFrom(serverAddress, serverPort), + destination: netip.AddrPortFrom(portAddress, selector), + } +} + +func (n *portNAT) allocateSelector(protocol uint8, portAddress, serverAddress netip.Addr, serverPort, clientSelector uint16) (uint16, flowKey, bool) { + if clientSelector != 0 { + key := n.reverseKeyFor(protocol, portAddress, serverAddress, serverPort, clientSelector) + if n.lookup(key) == nil { + return clientSelector, key, true + } + } + for range natSelectorMax - natSelectorMin + 1 { + n.counter++ + candidate := uint16(natSelectorMin + n.counter%(natSelectorMax-natSelectorMin+1)) + key := n.reverseKeyFor(protocol, portAddress, serverAddress, serverPort, candidate) + if n.lookup(key) == nil { + return candidate, key, true + } + } + return 0, flowKey{}, false +} diff --git a/flow_parse.go b/flow_parse.go new file mode 100644 index 00000000..45887994 --- /dev/null +++ b/flow_parse.go @@ -0,0 +1,256 @@ +package tun + +import ( + "encoding/binary" + "net/netip" + + "github.com/sagernet/sing-tun/gtcpip/header" +) + +type flowKey struct { + protocol uint8 + source netip.AddrPort + destination netip.AddrPort +} + +func (k flowKey) reversed() flowKey { + return flowKey{protocol: k.protocol, source: k.destination, destination: k.source} +} + +type forwardPacket struct { + ipVersion uint8 + protocol uint8 + network header.Network + transport []byte + source netip.AddrPort + destination netip.AddrPort + tcpFlags header.TCPFlags + icmpType uint8 + fragment bool + hasFlow bool +} + +func (p *forwardPacket) flowKey() flowKey { + return flowKey{protocol: p.protocol, source: p.source, destination: p.destination} +} + +func (p *forwardPacket) isTCPSyn() bool { + return p.protocol == uint8(header.TCPProtocolNumber) && p.tcpFlags&header.TCPFlagSyn != 0 +} + +func parseForwardPacket(packet []byte) (forwardPacket, bool) { + switch header.IPVersion(packet) { + case header.IPv4Version: + ipHdr := header.IPv4(packet) + if !ipHdr.IsValid(len(packet)) { + return forwardPacket{}, false + } + parsed := forwardPacket{ + ipVersion: 4, + protocol: uint8(ipHdr.TransportProtocol()), + network: ipHdr, + source: netip.AddrPortFrom(ipHdr.SourceAddr(), 0), + destination: netip.AddrPortFrom(ipHdr.DestinationAddr(), 0), + } + if ipHdr.More() || ipHdr.FragmentOffset() != 0 { + parsed.fragment = true + return parsed, true + } + parsed.parseTransport(ipHdr.Payload()) + return parsed, true + case header.IPv6Version: + ipHdr := header.IPv6(packet) + if !ipHdr.IsValid(len(packet)) { + return forwardPacket{}, false + } + protocol, payload, fragment, transportPresent := skipIPv6ExtensionHeaders(uint8(ipHdr.TransportProtocol()), ipHdr.Payload()) + parsed := forwardPacket{ + ipVersion: 6, + protocol: protocol, + network: ipHdr, + source: netip.AddrPortFrom(ipHdr.SourceAddr(), 0), + destination: netip.AddrPortFrom(ipHdr.DestinationAddr(), 0), + fragment: fragment, + } + if fragment || !transportPresent { + return parsed, true + } + parsed.parseTransport(payload) + return parsed, true + default: + return forwardPacket{}, false + } +} + +func skipIPv6ExtensionHeaders(protocol uint8, payload []byte) (uint8, []byte, bool, bool) { + for { + switch header.IPv6ExtensionHeaderIdentifier(protocol) { + case header.IPv6HopByHopOptionsExtHdrIdentifier, header.IPv6RoutingExtHdrIdentifier, header.IPv6DestinationOptionsExtHdrIdentifier: + if len(payload) < 2 { + return protocol, payload, false, false + } + extensionLength := (int(payload[1]) + 1) * 8 + if len(payload) < extensionLength { + return protocol, payload, false, false + } + protocol = payload[0] + payload = payload[extensionLength:] + case header.IPv6FragmentExtHdrIdentifier: + return protocol, payload, true, false + default: + return protocol, payload, false, true + } + } +} + +func (p *forwardPacket) parseTransport(payload []byte) { + p.transport = payload + switch p.protocol { + case uint8(header.TCPProtocolNumber): + if len(payload) < header.TCPMinimumSize { + return + } + tcpHdr := header.TCP(payload) + p.source = netip.AddrPortFrom(p.source.Addr(), tcpHdr.SourcePort()) + p.destination = netip.AddrPortFrom(p.destination.Addr(), tcpHdr.DestinationPort()) + p.tcpFlags = tcpHdr.Flags() + p.hasFlow = true + case uint8(header.UDPProtocolNumber): + if len(payload) < header.UDPMinimumSize { + return + } + udpHdr := header.UDP(payload) + p.source = netip.AddrPortFrom(p.source.Addr(), udpHdr.SourcePort()) + p.destination = netip.AddrPortFrom(p.destination.Addr(), udpHdr.DestinationPort()) + p.hasFlow = true + case uint8(header.ICMPv4ProtocolNumber): + if len(payload) < header.ICMPv4MinimumSize { + return + } + icmpHdr := header.ICMPv4(payload) + p.icmpType = uint8(icmpHdr.Type()) + switch icmpHdr.Type() { + case header.ICMPv4Echo, header.ICMPv4EchoReply: + identifier := icmpHdr.Ident() + p.source = netip.AddrPortFrom(p.source.Addr(), identifier) + p.destination = netip.AddrPortFrom(p.destination.Addr(), identifier) + p.hasFlow = true + } + case uint8(header.ICMPv6ProtocolNumber): + if len(payload) < header.ICMPv6MinimumSize { + return + } + icmpHdr := header.ICMPv6(payload) + p.icmpType = uint8(icmpHdr.Type()) + switch icmpHdr.Type() { + case header.ICMPv6EchoRequest, header.ICMPv6EchoReply: + identifier := icmpHdr.Ident() + p.source = netip.AddrPortFrom(p.source.Addr(), identifier) + p.destination = netip.AddrPortFrom(p.destination.Addr(), identifier) + p.hasFlow = true + } + } +} + +func (p *forwardPacket) isICMPError() bool { + switch p.protocol { + case uint8(header.ICMPv4ProtocolNumber): + switch header.ICMPv4Type(p.icmpType) { + case header.ICMPv4DstUnreachable, header.ICMPv4SrcQuench, header.ICMPv4Redirect, header.ICMPv4TimeExceeded, header.ICMPv4ParamProblem: + return true + } + return false + case uint8(header.ICMPv6ProtocolNumber): + return header.ICMPv6Type(p.icmpType).IsErrorType() + default: + return false + } +} + +func (p *forwardPacket) icmpErrorInner() ([]byte, bool) { + var innerOffset int + switch p.protocol { + case uint8(header.ICMPv4ProtocolNumber): + innerOffset = header.ICMPv4MinimumSize + case uint8(header.ICMPv6ProtocolNumber): + innerOffset = header.ICMPv6ErrorHeaderSize + default: + return nil, false + } + if len(p.transport) <= innerOffset { + return nil, false + } + return p.transport[innerOffset:], true +} + +type embeddedPacket struct { + network header.Network + payload []byte + protocol uint8 + source netip.AddrPort + destination netip.AddrPort +} + +func (p *embeddedPacket) flowKey() flowKey { + return flowKey{protocol: p.protocol, source: p.source, destination: p.destination} +} + +func parseEmbedded(inner []byte) (embeddedPacket, bool) { + switch header.IPVersion(inner) { + case header.IPv4Version: + if len(inner) < header.IPv4MinimumSize { + return embeddedPacket{}, false + } + ipHdr := header.IPv4(inner) + headerLength := int(ipHdr.HeaderLength()) + if headerLength < header.IPv4MinimumSize || headerLength > len(inner) { + return embeddedPacket{}, false + } + return parseEmbeddedTransport(ipHdr, inner[headerLength:], uint8(ipHdr.TransportProtocol()), ipHdr.SourceAddr(), ipHdr.DestinationAddr()) + case header.IPv6Version: + if len(inner) < header.IPv6MinimumSize { + return embeddedPacket{}, false + } + ipHdr := header.IPv6(inner) + protocol, payload, _, transportPresent := skipIPv6ExtensionHeaders(uint8(ipHdr.TransportProtocol()), inner[header.IPv6MinimumSize:]) + if !transportPresent { + return embeddedPacket{}, false + } + return parseEmbeddedTransport(ipHdr, payload, protocol, ipHdr.SourceAddr(), ipHdr.DestinationAddr()) + default: + return embeddedPacket{}, false + } +} + +func parseEmbeddedTransport(network header.Network, payload []byte, protocol uint8, source, destination netip.Addr) (embeddedPacket, bool) { + embedded := embeddedPacket{ + network: network, + payload: payload, + protocol: protocol, + } + switch protocol { + case uint8(header.TCPProtocolNumber), uint8(header.UDPProtocolNumber): + if len(payload) < 4 { + return embeddedPacket{}, false + } + embedded.source = netip.AddrPortFrom(source, binary.BigEndian.Uint16(payload[0:])) + embedded.destination = netip.AddrPortFrom(destination, binary.BigEndian.Uint16(payload[2:])) + case uint8(header.ICMPv4ProtocolNumber): + if len(payload) < header.ICMPv4MinimumSize { + return embeddedPacket{}, false + } + identifier := header.ICMPv4(payload).Ident() + embedded.source = netip.AddrPortFrom(source, identifier) + embedded.destination = netip.AddrPortFrom(destination, identifier) + case uint8(header.ICMPv6ProtocolNumber): + if len(payload) < header.ICMPv6MinimumSize { + return embeddedPacket{}, false + } + identifier := header.ICMPv6(payload).Ident() + embedded.source = netip.AddrPortFrom(source, identifier) + embedded.destination = netip.AddrPortFrom(destination, identifier) + default: + return embeddedPacket{}, false + } + return embedded, true +} diff --git a/flow_reject.go b/flow_reject.go new file mode 100644 index 00000000..af82eb5c --- /dev/null +++ b/flow_reject.go @@ -0,0 +1,210 @@ +package tun + +import ( + "net/netip" + + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/header" +) + +func buildReject(packet *forwardPacket, headroom int) ([]byte, bool) { + switch packet.protocol { + case uint8(header.TCPProtocolNumber): + if len(packet.transport) < header.TCPMinimumSize { + return nil, false + } + tcpHdr := header.TCP(packet.transport) + switch ipHdr := packet.network.(type) { + case header.IPv4: + return buildResetIPv4(ipHdr, tcpHdr, headroom), true + case header.IPv6: + return buildResetIPv6(ipHdr, tcpHdr, headroom), true + default: + return nil, false + } + case uint8(header.UDPProtocolNumber): + switch ipHdr := packet.network.(type) { + case header.IPv4: + return buildRejectICMPv4(ipHdr, header.ICMPv4PortUnreachable, ipHdr.DestinationAddr(), headroom) + case header.IPv6: + return buildRejectICMPv6(ipHdr, header.ICMPv6PortUnreachable, ipHdr.DestinationAddr(), headroom) + default: + return nil, false + } + default: + switch ipHdr := packet.network.(type) { + case header.IPv4: + return buildRejectICMPv4(ipHdr, header.ICMPv4HostUnreachable, ipHdr.DestinationAddr(), headroom) + case header.IPv6: + return buildRejectICMPv6(ipHdr, header.ICMPv6AddressUnreachable, ipHdr.DestinationAddr(), headroom) + default: + return nil, false + } + } +} + +func buildResetIPv4(origIPHdr header.IPv4, origTCPHdr header.TCP, headroom int) []byte { + size := header.IPv4MinimumSize + header.TCPMinimumSize + buffer := make([]byte, headroom+size) + ipHdr := header.IPv4(buffer[headroom:]) + ipHdr.Encode(&header.IPv4Fields{ + TotalLength: uint16(size), + TTL: synthesizedTTL, + Protocol: uint8(header.TCPProtocolNumber), + SrcAddr: origIPHdr.DestinationAddr(), + DstAddr: origIPHdr.SourceAddr(), + }) + tcpHdr := header.TCP(ipHdr.Payload()) + encodeResetTCP(tcpHdr, origTCPHdr) + tcpHdr.SetChecksum(^tcpHdr.CalculateChecksum(header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), header.TCPMinimumSize))) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + return buffer +} + +func buildResetIPv6(origIPHdr header.IPv6, origTCPHdr header.TCP, headroom int) []byte { + size := header.IPv6MinimumSize + header.TCPMinimumSize + buffer := make([]byte, headroom+size) + ipHdr := header.IPv6(buffer[headroom:]) + ipHdr.Encode(&header.IPv6Fields{ + PayloadLength: uint16(header.TCPMinimumSize), + TransportProtocol: header.TCPProtocolNumber, + HopLimit: synthesizedTTL, + SrcAddr: origIPHdr.DestinationAddr(), + DstAddr: origIPHdr.SourceAddr(), + }) + tcpHdr := header.TCP(ipHdr.Payload()) + encodeResetTCP(tcpHdr, origTCPHdr) + tcpHdr.SetChecksum(^tcpHdr.CalculateChecksum(header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), header.TCPMinimumSize))) + return buffer +} + +func encodeResetTCP(tcpHdr header.TCP, origTCPHdr header.TCP) { + fields := header.TCPFields{ + SrcPort: origTCPHdr.DestinationPort(), + DstPort: origTCPHdr.SourcePort(), + DataOffset: header.TCPMinimumSize, + Flags: header.TCPFlagRst, + } + if origTCPHdr.Flags()&header.TCPFlagAck != 0 { + fields.SeqNum = origTCPHdr.AckNumber() + } else { + fields.Flags |= header.TCPFlagAck + ackNumber := origTCPHdr.SequenceNumber() + uint32(len(origTCPHdr.Payload())) + if origTCPHdr.Flags()&header.TCPFlagSyn != 0 { + ackNumber++ + } + if origTCPHdr.Flags()&header.TCPFlagFin != 0 { + ackNumber++ + } + fields.AckNum = ackNumber + } + tcpHdr.Encode(&fields) +} + +func buildRejectICMPv4(ipHdr header.IPv4, code header.ICMPv4Code, source netip.Addr, headroom int) ([]byte, bool) { + const maxIPData = header.IPv4MinimumProcessableDatagramSize - header.IPv4MinimumSize + available := maxIPData - header.ICMPv4MinimumSize + if len(ipHdr) < header.ICMPv4MinimumErrorPayloadSize { + return nil, false + } + payload := []byte(ipHdr) + if len(payload) > available { + payload = payload[:available] + } + size := header.IPv4MinimumSize + header.ICMPv4MinimumSize + len(payload) + buffer := make([]byte, headroom+size) + newIPHdr := header.IPv4(buffer[headroom:]) + newIPHdr.Encode(&header.IPv4Fields{ + TotalLength: uint16(size), + TTL: synthesizedTTL, + Protocol: uint8(header.ICMPv4ProtocolNumber), + SrcAddr: source, + DstAddr: ipHdr.SourceAddr(), + }) + newIPHdr.SetChecksum(^newIPHdr.CalculateChecksum()) + icmpHdr := header.ICMPv4(newIPHdr.Payload()) + icmpHdr.SetType(header.ICMPv4DstUnreachable) + icmpHdr.SetCode(code) + copy(icmpHdr.Payload(), payload) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(payload, 0))) + return buffer, true +} + +func buildRejectICMPv6(ipHdr header.IPv6, code header.ICMPv6Code, source netip.Addr, headroom int) ([]byte, bool) { + const maxIPv6Data = header.IPv6MinimumMTU - header.IPv6FixedHeaderSize + available := maxIPv6Data - header.ICMPv6ErrorHeaderSize + if available < header.IPv6MinimumSize { + return nil, false + } + payload := []byte(ipHdr) + if len(payload) > available { + payload = payload[:available] + } + size := header.IPv6MinimumSize + header.ICMPv6DstUnreachableMinimumSize + len(payload) + buffer := make([]byte, headroom+size) + newIPHdr := header.IPv6(buffer[headroom:]) + newIPHdr.Encode(&header.IPv6Fields{ + PayloadLength: uint16(header.ICMPv6DstUnreachableMinimumSize + len(payload)), + TransportProtocol: header.ICMPv6ProtocolNumber, + HopLimit: synthesizedTTL, + SrcAddr: source, + DstAddr: ipHdr.SourceAddr(), + }) + icmpHdr := header.ICMPv6(newIPHdr.Payload()) + icmpHdr.SetType(header.ICMPv6DstUnreachable) + icmpHdr.SetCode(code) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr[:header.ICMPv6DstUnreachableMinimumSize], + Src: newIPHdr.SourceAddressSlice(), + Dst: newIPHdr.DestinationAddressSlice(), + PayloadCsum: checksum.Checksum(payload, 0), + PayloadLen: len(payload), + })) + copy(icmpHdr.Payload(), payload) + return buffer, true +} + +func BuildUnreachable(packet []byte, source netip.Addr, headroom int) ([]byte, bool) { + switch header.IPVersion(packet) { + case header.IPv4Version: + ipHdr := header.IPv4(packet) + if !ipHdr.IsValid(len(packet)) || ipHdr.FragmentOffset() != 0 { + return nil, false + } + sourceAddr := ipHdr.SourceAddr() + if sourceAddr.IsUnspecified() || sourceAddr.IsMulticast() { + return nil, false + } + if ipHdr.TransportProtocol() == header.ICMPv4ProtocolNumber { + if len(ipHdr.Payload()) < header.ICMPv4MinimumSize || header.ICMPv4(ipHdr.Payload()).Type() != header.ICMPv4Echo { + return nil, false + } + } + replySource := ipHdr.DestinationAddr() + if source.Is4() { + replySource = source + } + return buildRejectICMPv4(ipHdr, header.ICMPv4HostUnreachable, replySource, headroom) + case header.IPv6Version: + ipHdr := header.IPv6(packet) + if !ipHdr.IsValid(len(packet)) { + return nil, false + } + sourceAddr := ipHdr.SourceAddr() + if sourceAddr.IsUnspecified() || sourceAddr.IsMulticast() { + return nil, false + } + if ipHdr.TransportProtocol() == header.ICMPv6ProtocolNumber { + if len(ipHdr.Payload()) < header.ICMPv6MinimumSize || header.ICMPv6(ipHdr.Payload()).Type() != header.ICMPv6EchoRequest { + return nil, false + } + } + replySource := ipHdr.DestinationAddr() + if source.Is6() { + replySource = source + } + return buildRejectICMPv6(ipHdr, header.ICMPv6NetworkUnreachable, replySource, headroom) + default: + return nil, false + } +} diff --git a/flow_rewrite.go b/flow_rewrite.go new file mode 100644 index 00000000..e4d79673 --- /dev/null +++ b/flow_rewrite.go @@ -0,0 +1,350 @@ +package tun + +import ( + "encoding/binary" + + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/header" +) + +type rewriteRule struct { + sourceAddress tcpip.Address + sourcePort uint16 + rewriteSourcePort bool + destinationAddress tcpip.Address + destinationPort uint16 + rewriteDestinationPort bool +} + +func applyRewrite(packet *forwardPacket, rule *rewriteRule) { + oldSource := packet.network.SourceAddress() + oldDestination := packet.network.DestinationAddress() + newSource := oldSource + newDestination := oldDestination + if rule.sourceAddress.Len() > 0 { + newSource = rule.sourceAddress + } + if rule.destinationAddress.Len() > 0 { + newDestination = rule.destinationAddress + } + if ipHdr, isIPv4 := packet.network.(header.IPv4); isIPv4 { + if newSource != oldSource { + ipHdr.SetSourceAddressWithChecksumUpdate(newSource) + } + if newDestination != oldDestination { + ipHdr.SetDestinationAddressWithChecksumUpdate(newDestination) + } + } else { + if newSource != oldSource { + packet.network.SetSourceAddress(newSource) + } + if newDestination != oldDestination { + packet.network.SetDestinationAddress(newDestination) + } + } + transport := packet.transport + switch packet.protocol { + case uint8(header.TCPProtocolNumber): + if len(transport) < header.TCPMinimumSize { + return + } + tcpHdr := header.TCP(transport) + if newSource != oldSource { + tcpHdr.UpdateChecksumPseudoHeaderAddress(oldSource, newSource, true) + } + if newDestination != oldDestination { + tcpHdr.UpdateChecksumPseudoHeaderAddress(oldDestination, newDestination, true) + } + if rule.rewriteSourcePort { + tcpHdr.SetSourcePortWithChecksumUpdate(rule.sourcePort) + } + if rule.rewriteDestinationPort { + tcpHdr.SetDestinationPortWithChecksumUpdate(rule.destinationPort) + } + case uint8(header.UDPProtocolNumber): + if len(transport) < header.UDPMinimumSize { + return + } + udpHdr := header.UDP(transport) + if packet.ipVersion == 4 && udpHdr.Checksum() == 0 { + if rule.rewriteSourcePort { + udpHdr.SetSourcePort(rule.sourcePort) + } + if rule.rewriteDestinationPort { + udpHdr.SetDestinationPort(rule.destinationPort) + } + return + } + if newSource != oldSource { + udpHdr.UpdateChecksumPseudoHeaderAddress(oldSource, newSource, true) + } + if newDestination != oldDestination { + udpHdr.UpdateChecksumPseudoHeaderAddress(oldDestination, newDestination, true) + } + if rule.rewriteSourcePort { + udpHdr.SetSourcePortWithChecksumUpdate(rule.sourcePort) + } + if rule.rewriteDestinationPort { + udpHdr.SetDestinationPortWithChecksumUpdate(rule.destinationPort) + } + case uint8(header.ICMPv4ProtocolNumber): + if len(transport) < header.ICMPv4MinimumSize { + return + } + icmpHdr := header.ICMPv4(transport) + if rule.rewriteSourcePort { + icmpHdr.SetIdentWithChecksumUpdate(rule.sourcePort) + } else if rule.rewriteDestinationPort { + icmpHdr.SetIdentWithChecksumUpdate(rule.destinationPort) + } + case uint8(header.ICMPv6ProtocolNumber): + if len(transport) < header.ICMPv6MinimumSize { + return + } + icmpHdr := header.ICMPv6(transport) + if newSource != oldSource { + icmpHdr.UpdateChecksumPseudoHeaderAddress(oldSource, newSource) + } + if newDestination != oldDestination { + icmpHdr.UpdateChecksumPseudoHeaderAddress(oldDestination, newDestination) + } + if rule.rewriteSourcePort { + icmpHdr.SetIdentWithChecksumUpdate(rule.sourcePort) + } else if rule.rewriteDestinationPort { + icmpHdr.SetIdentWithChecksumUpdate(rule.destinationPort) + } + } +} + +func applyRewriteRaw(packet *forwardPacket, rule *rewriteRule) { + if rule.sourceAddress.Len() > 0 { + packet.network.SetSourceAddress(rule.sourceAddress) + } + if rule.destinationAddress.Len() > 0 { + packet.network.SetDestinationAddress(rule.destinationAddress) + } + transport := packet.transport + switch packet.protocol { + case uint8(header.TCPProtocolNumber): + if len(transport) < header.TCPMinimumSize { + return + } + tcpHdr := header.TCP(transport) + if rule.rewriteSourcePort { + tcpHdr.SetSourcePort(rule.sourcePort) + } + if rule.rewriteDestinationPort { + tcpHdr.SetDestinationPort(rule.destinationPort) + } + case uint8(header.UDPProtocolNumber): + if len(transport) < header.UDPMinimumSize { + return + } + udpHdr := header.UDP(transport) + if rule.rewriteSourcePort { + udpHdr.SetSourcePort(rule.sourcePort) + } + if rule.rewriteDestinationPort { + udpHdr.SetDestinationPort(rule.destinationPort) + } + case uint8(header.ICMPv4ProtocolNumber): + if len(transport) < header.ICMPv4MinimumSize { + return + } + icmpHdr := header.ICMPv4(transport) + if rule.rewriteSourcePort { + icmpHdr.SetIdent(rule.sourcePort) + } else if rule.rewriteDestinationPort { + icmpHdr.SetIdent(rule.destinationPort) + } + case uint8(header.ICMPv6ProtocolNumber): + if len(transport) < header.ICMPv6MinimumSize { + return + } + icmpHdr := header.ICMPv6(transport) + if rule.rewriteSourcePort { + icmpHdr.SetIdent(rule.sourcePort) + } else if rule.rewriteDestinationPort { + icmpHdr.SetIdent(rule.destinationPort) + } + } +} + +func recomputeChecksums(packet *forwardPacket) { + if ipHdr, isIPv4 := packet.network.(header.IPv4); isIPv4 { + ipHdr.SetChecksum(0) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + } + transport := packet.transport + switch packet.protocol { + case uint8(header.TCPProtocolNumber): + if len(transport) < header.TCPMinimumSize { + return + } + tcpHdr := header.TCP(transport) + tcpHdr.SetChecksum(0) + payloadChecksum := checksum.Checksum(tcpHdr.Payload(), 0) + pseudoChecksum := header.PseudoHeaderChecksum(header.TCPProtocolNumber, packet.network.SourceAddressSlice(), packet.network.DestinationAddressSlice(), uint16(len(transport))) + tcpHdr.SetChecksum(^tcpHdr.CalculateChecksum(checksum.Combine(pseudoChecksum, payloadChecksum))) + case uint8(header.UDPProtocolNumber): + if len(transport) < header.UDPMinimumSize { + return + } + udpHdr := header.UDP(transport) + if packet.ipVersion == 4 && udpHdr.Checksum() == 0 { + return + } + udpHdr.SetChecksum(0) + payloadChecksum := checksum.Checksum(udpHdr.Payload(), 0) + pseudoChecksum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, packet.network.SourceAddressSlice(), packet.network.DestinationAddressSlice(), udpHdr.Length()) + udpChecksum := ^udpHdr.CalculateChecksum(checksum.Combine(pseudoChecksum, payloadChecksum)) + if udpChecksum == 0 { + udpChecksum = 0xffff + } + udpHdr.SetChecksum(udpChecksum) + case uint8(header.ICMPv4ProtocolNumber): + if len(transport) < header.ICMPv4MinimumSize { + return + } + icmpHdr := header.ICMPv4(transport) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) + case uint8(header.ICMPv6ProtocolNumber): + if len(transport) < header.ICMPv6MinimumSize { + return + } + icmpHdr := header.ICMPv6(transport) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: packet.network.SourceAddressSlice(), + Dst: packet.network.DestinationAddressSlice(), + })) + } +} + +func clampTCPMSS(packet *forwardPacket, effectiveMTU uint32) { + if effectiveMTU == 0 || packet.protocol != uint8(header.TCPProtocolNumber) { + return + } + transport := packet.transport + if len(transport) < header.TCPMinimumSize { + return + } + tcpHdr := header.TCP(transport) + tcpHeaderLength := int(tcpHdr.DataOffset()) + if tcpHeaderLength < header.TCPMinimumSize || tcpHeaderLength > len(transport) { + return + } + var networkHeaderLength int + switch packet.ipVersion { + case 4: + networkHeaderLength = len(packet.network.(header.IPv4)) - len(transport) + default: + networkHeaderLength = len(packet.network.(header.IPv6)) - len(transport) + } + if effectiveMTU <= uint32(networkHeaderLength+header.TCPMinimumSize) { + return + } + maxMSS := min(effectiveMTU-uint32(networkHeaderLength+header.TCPMinimumSize), header.TCPMaximumMSS) + options := tcpHdr.Options() + for i := 0; i < len(options); { + switch options[i] { + case header.TCPOptionEOL: + return + case header.TCPOptionNOP: + i++ + continue + case header.TCPOptionMSS: + if i+header.TCPOptionMSSLength > len(options) || options[i+1] != header.TCPOptionMSSLength { + return + } + currentMSS := binary.BigEndian.Uint16(options[i+2:]) + if uint32(currentMSS) <= maxMSS { + return + } + binary.BigEndian.PutUint16(options[i+2:], uint16(maxMSS)) + return + default: + if i+2 > len(options) { + return + } + optionLength := int(options[i+1]) + if optionLength < 2 || i+optionLength > len(options) { + return + } + i += optionLength + } + } +} + +func rewriteEmbeddedDestination(embedded *embeddedPacket, destination tcpip.Address, selector uint16, remapSelector bool) { + oldDestination := embedded.network.DestinationAddress() + if ipHdr, isIPv4 := embedded.network.(header.IPv4); isIPv4 { + ipHdr.SetDestinationAddressWithChecksumUpdate(destination) + } else { + embedded.network.SetDestinationAddress(destination) + } + rewriteEmbeddedSelector(embedded, oldDestination, destination, selector, remapSelector, true) +} + +func rewriteEmbeddedSource(embedded *embeddedPacket, source tcpip.Address, selector uint16, remapSelector bool) { + oldSource := embedded.network.SourceAddress() + if ipHdr, isIPv4 := embedded.network.(header.IPv4); isIPv4 { + ipHdr.SetSourceAddressWithChecksumUpdate(source) + } else { + embedded.network.SetSourceAddress(source) + } + rewriteEmbeddedSelector(embedded, oldSource, source, selector, remapSelector, false) +} + +func rewriteEmbeddedSelector(embedded *embeddedPacket, oldAddress, newAddress tcpip.Address, selector uint16, remapSelector bool, destinationSide bool) { + if !remapSelector { + return + } + payload := embedded.payload + _, isIPv4 := embedded.network.(header.IPv4) + switch embedded.protocol { + case uint8(header.TCPProtocolNumber): + if len(payload) >= 4 { + if destinationSide { + binary.BigEndian.PutUint16(payload[2:], selector) + } else { + binary.BigEndian.PutUint16(payload[0:], selector) + } + } + case uint8(header.UDPProtocolNumber): + if len(payload) >= header.UDPMinimumSize { + udpHdr := header.UDP(payload) + if isIPv4 && udpHdr.Checksum() == 0 { + if destinationSide { + udpHdr.SetDestinationPort(selector) + } else { + udpHdr.SetSourcePort(selector) + } + } else { + if oldAddress != newAddress { + udpHdr.UpdateChecksumPseudoHeaderAddress(oldAddress, newAddress, true) + } + if destinationSide { + udpHdr.SetDestinationPortWithChecksumUpdate(selector) + } else { + udpHdr.SetSourcePortWithChecksumUpdate(selector) + } + } + } + case uint8(header.ICMPv4ProtocolNumber): + if len(payload) >= header.ICMPv4MinimumSize { + header.ICMPv4(payload).SetIdentWithChecksumUpdate(selector) + } + case uint8(header.ICMPv6ProtocolNumber): + if len(payload) >= header.ICMPv6MinimumSize { + icmpHdr := header.ICMPv6(payload) + if oldAddress != newAddress { + icmpHdr.UpdateChecksumPseudoHeaderAddress(oldAddress, newAddress) + } + icmpHdr.SetIdentWithChecksumUpdate(selector) + } + } +} diff --git a/nfqueue_linux.go b/nfqueue_linux.go index 32778957..c6593e44 100644 --- a/nfqueue_linux.go +++ b/nfqueue_linux.go @@ -4,14 +4,12 @@ package tun import ( "context" - "errors" + "net/netip" "sync/atomic" "github.com/sagernet/sing-tun/gtcpip/header" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" "github.com/florianl/go-nfqueue/v2" "github.com/mdlayher/netlink" @@ -169,7 +167,7 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int { return 0 } - var srcAddr, dstAddr M.Socksaddr + var sourceAddr, destinationAddr netip.Addr var tcpOffset int version := payload[0] >> 4 @@ -180,8 +178,8 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int { h.setVerdict(packetID, nfqueue.NfAccept, 0) return 0 } - srcAddr = M.SocksaddrFrom(ipv4.SourceAddr(), 0) - dstAddr = M.SocksaddrFrom(ipv4.DestinationAddr(), 0) + sourceAddr = ipv4.SourceAddr() + destinationAddr = ipv4.DestinationAddr() tcpOffset = int(ipv4.HeaderLength()) case 6: transportProto, transportOffset, ok := parseIPv6TransportHeader(payload) @@ -190,8 +188,8 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int { return 0 } ipv6 := header.IPv6(payload) - srcAddr = M.SocksaddrFrom(ipv6.SourceAddr(), 0) - dstAddr = M.SocksaddrFrom(ipv6.DestinationAddr(), 0) + sourceAddr = ipv6.SourceAddr() + destinationAddr = ipv6.DestinationAddr() tcpOffset = transportOffset default: h.setVerdict(packetID, nfqueue.NfAccept, 0) @@ -204,8 +202,6 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int { } tcp := header.TCP(payload[tcpOffset:]) - srcAddr = M.SocksaddrFrom(srcAddr.Addr, tcp.SourcePort()) - dstAddr = M.SocksaddrFrom(dstAddr.Addr, tcp.DestinationPort()) flags := tcp.Flags() if !flags.Contains(header.TCPFlagSyn) || flags.Contains(header.TCPFlagAck) { @@ -213,18 +209,22 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int { return 0 } - _, pErr := h.handler.PrepareConnection(N.NetworkTCP, srcAddr, dstAddr, nil, 0) + verdict := h.handler.JudgeFlow( + uint8(unix.IPPROTO_TCP), + netip.AddrPortFrom(sourceAddr, tcp.SourcePort()), + netip.AddrPortFrom(destinationAddr, tcp.DestinationPort()), + ) // Use NfRepeat for bypass/reset so the packet re-enters the chain // from the beginning, allowing mark-checking rules to save the mark // to conntrack. NfAccept is a terminal verdict in nftables — it exits // the chain immediately, skipping any rules after the queue statement. - switch { - case errors.Is(pErr, ErrBypass): + switch verdict.Action { + case ActionBypass: h.setVerdict(packetID, nfqueue.NfRepeat, h.outputMark) - case errors.Is(pErr, ErrReset): + case ActionReject: h.setVerdict(packetID, nfqueue.NfRepeat, h.resetMark) - case errors.Is(pErr, ErrDrop): + case ActionDrop: h.setVerdict(packetID, nfqueue.NfDrop, 0) default: h.setVerdict(packetID, nfqueue.NfAccept, 0) diff --git a/ping/destination.go b/ping/destination.go index ece45b9a..1e5d5333 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -9,7 +9,6 @@ import ( "sync" "time" - "github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" @@ -20,14 +19,16 @@ import ( // Although its theoretical maximum may be 64k, I don’t yet know of any practical use case for that. For memory-usage reasons, I’m just using a 2k buffer. const maxICMPPacketSize = 2048 -var _ tun.DirectRouteDestination = (*Destination)(nil) +type PacketWriter interface { + WritePacket(packet []byte) error +} type Destination struct { conn *Conn ctx context.Context logger logger.ContextLogger destination netip.Addr - routeContext tun.DirectRouteContext + writer PacketWriter timeout time.Duration requestAccess sync.Mutex requests map[pingRequest]time.Time @@ -45,9 +46,9 @@ func ConnectDestination( logger logger.ContextLogger, controlFunc control.Func, destination netip.Addr, - routeContext tun.DirectRouteContext, + writer PacketWriter, timeout time.Duration, -) (tun.DirectRouteDestination, error) { +) (*Destination, error) { var ( conn *Conn err error @@ -65,13 +66,13 @@ func ConnectDestination( return nil, err } d := &Destination{ - conn: conn, - ctx: ctx, - logger: logger, - destination: destination, - routeContext: routeContext, - timeout: timeout, - requests: make(map[pingRequest]time.Time), + conn: conn, + ctx: ctx, + logger: logger, + destination: destination, + writer: writer, + timeout: timeout, + requests: make(map[pingRequest]time.Time), } go d.loopRead() return d, nil @@ -158,7 +159,7 @@ func (d *Destination) loopRead() { } d.logger.TraceContext(d.ctx, "read ICMPv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } - err = d.routeContext.WritePacket(buffer.Bytes()) + err = d.writer.WritePacket(buffer.Bytes()) if err != nil { d.logger.ErrorContext(d.ctx, E.Cause(err, "write ICMP echo reply")) } diff --git a/ping/destination_gvisor.go b/ping/destination_gvisor.go deleted file mode 100644 index 0c508ee6..00000000 --- a/ping/destination_gvisor.go +++ /dev/null @@ -1,143 +0,0 @@ -//go:build with_gvisor - -package ping - -import ( - "context" - "net/netip" - "time" - - "github.com/sagernet/gvisor/pkg/tcpip" - "github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet" - "github.com/sagernet/gvisor/pkg/tcpip/header" - "github.com/sagernet/gvisor/pkg/tcpip/stack" - "github.com/sagernet/gvisor/pkg/tcpip/transport" - "github.com/sagernet/gvisor/pkg/waiter" - "github.com/sagernet/sing-tun" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/logger" -) - -var _ tun.DirectRouteDestination = (*GVisorDestination)(nil) - -type GVisorDestination struct { - ctx context.Context - logger logger.ContextLogger - endpoint tcpip.Endpoint - conn *gonet.TCPConn - rewriter *SourceRewriter - timeout time.Duration - lastActive common.TypedValue[time.Time] -} - -func ConnectGVisor( - ctx context.Context, logger logger.ContextLogger, - sourceAddress, destinationAddress netip.Addr, - routeContext tun.DirectRouteContext, - stack *stack.Stack, - bindAddress4, bindAddress6 netip.Addr, - timeout time.Duration, -) (*GVisorDestination, error) { - var ( - bindAddress tcpip.Address - wq waiter.Queue - endpoint tcpip.Endpoint - gErr tcpip.Error - ) - if !destinationAddress.Is6() { - if !bindAddress4.IsValid() { - return nil, E.New("missing IPv4 interface address") - } - bindAddress = tun.AddressFromAddr(bindAddress4) - endpoint, gErr = stack.NewRawEndpoint(header.ICMPv4ProtocolNumber, header.IPv4ProtocolNumber, &wq, true) - } else { - if !bindAddress6.IsValid() { - return nil, E.New("missing IPv6 interface address") - } - bindAddress = tun.AddressFromAddr(bindAddress6) - endpoint, gErr = stack.NewRawEndpoint(header.ICMPv6ProtocolNumber, header.IPv6ProtocolNumber, &wq, true) - } - if gErr != nil { - return nil, gonet.TranslateNetstackError(gErr) - } - gErr = endpoint.Bind(tcpip.FullAddress{ - NIC: 1, - Addr: bindAddress, - }) - if gErr != nil { - return nil, gonet.TranslateNetstackError(gErr) - } - gErr = endpoint.Connect(tcpip.FullAddress{ - NIC: 1, - Addr: tun.AddressFromAddr(destinationAddress), - }) - if gErr != nil { - return nil, gonet.TranslateNetstackError(gErr) - } - endpoint.SocketOptions().SetHeaderIncluded(true) - rewriter := NewSourceRewriter(ctx, logger, bindAddress4, bindAddress6) - rewriter.CreateSession(tun.DirectRouteSession{Source: sourceAddress, Destination: destinationAddress}, routeContext) - destination := &GVisorDestination{ - ctx: ctx, - logger: logger, - endpoint: endpoint, - conn: gonet.NewTCPConn(&wq, endpoint), - rewriter: rewriter, - timeout: timeout, - } - destination.lastActive.Store(time.Now()) - go destination.loopRead() - return destination, nil -} - -func (d *GVisorDestination) loopRead() { - defer d.endpoint.Close() - for { - deadline := d.lastActive.Load().Add(d.timeout) - if !time.Now().Before(deadline) { - return - } - err := d.conn.SetReadDeadline(deadline) - if err != nil { - d.logger.ErrorContext(d.ctx, E.Cause(err, "set read deadline for ICMP conn")) - } - buffer := buf.NewSize(maxICMPPacketSize) - n, err := d.conn.Read(buffer.FreeBytes()) - if err != nil { - buffer.Release() - if E.IsTimeout(err) { - continue - } - if !E.IsClosed(err) { - d.logger.ErrorContext(d.ctx, E.Cause(err, "receive ICMP echo reply")) - } - return - } - buffer.Truncate(n) - var matched bool - matched, err = d.rewriter.WriteBack(buffer.Bytes()) - if err != nil { - d.logger.ErrorContext(d.ctx, E.Cause(err, "write ICMP echo reply")) - } - if matched { - d.lastActive.Store(time.Now()) - } - buffer.Release() - } -} - -func (d *GVisorDestination) WritePacket(packet *buf.Buffer) error { - d.lastActive.Store(time.Now()) - d.rewriter.RewritePacket(packet.Bytes()) - return common.Error(d.conn.Write(packet.Bytes())) -} - -func (d *GVisorDestination) Close() error { - return d.conn.Close() -} - -func (d *GVisorDestination) IsClosed() bool { - return transport.DatagramEndpointState(d.endpoint.State()) == transport.DatagramEndpointStateClosed -} diff --git a/ping/destination_rewriter.go b/ping/destination_rewriter.go deleted file mode 100644 index 26bb3551..00000000 --- a/ping/destination_rewriter.go +++ /dev/null @@ -1,79 +0,0 @@ -package ping - -import ( - "net/netip" - - "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun/gtcpip/header" - "github.com/sagernet/sing/common/buf" -) - -type DestinationWriter struct { - tun.DirectRouteDestination - destination netip.Addr -} - -func NewDestinationWriter(routeDestination tun.DirectRouteDestination, destination netip.Addr) *DestinationWriter { - return &DestinationWriter{routeDestination, destination} -} - -func (w *DestinationWriter) WritePacket(packet *buf.Buffer) error { - var ipHdr header.Network - switch header.IPVersion(packet.Bytes()) { - case header.IPv4Version: - ipHdr = header.IPv4(packet.Bytes()) - case header.IPv6Version: - ipHdr = header.IPv6(packet.Bytes()) - default: - return w.DirectRouteDestination.WritePacket(packet) - } - ipHdr.SetDestinationAddr(w.destination) - if ipHdr4, isIPv4 := ipHdr.(header.IPv4); isIPv4 { - ipHdr4.SetChecksum(^ipHdr4.CalculateChecksum()) - } - if ipHdr.TransportProtocol() == header.ICMPv6ProtocolNumber { - icmpHdr := header.ICMPv6(ipHdr.Payload()) - icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr, - Src: ipHdr.SourceAddressSlice(), - Dst: ipHdr.DestinationAddressSlice(), - })) - } - return w.DirectRouteDestination.WritePacket(packet) -} - -type ContextDestinationWriter struct { - tun.DirectRouteContext - destination netip.Addr -} - -func NewContextDestinationWriter(context tun.DirectRouteContext, destination netip.Addr) *ContextDestinationWriter { - return &ContextDestinationWriter{ - context, destination, - } -} - -func (w *ContextDestinationWriter) WritePacket(packet []byte) error { - var ipHdr header.Network - switch header.IPVersion(packet) { - case header.IPv4Version: - ipHdr = header.IPv4(packet) - case header.IPv6Version: - ipHdr = header.IPv6(packet) - default: - return w.DirectRouteContext.WritePacket(packet) - } - ipHdr.SetSourceAddr(w.destination) - if ipHdr4, isIPv4 := ipHdr.(header.IPv4); isIPv4 { - ipHdr4.SetChecksum(^ipHdr4.CalculateChecksum()) - } - if ipHdr.TransportProtocol() == header.ICMPv6ProtocolNumber { - icmpHdr := header.ICMPv6(ipHdr.Payload()) - icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr, - Src: ipHdr.SourceAddressSlice(), - Dst: ipHdr.DestinationAddressSlice(), - })) - } - return w.DirectRouteContext.WritePacket(packet) -} diff --git a/ping/port.go b/ping/port.go new file mode 100644 index 00000000..e0cc2a1d --- /dev/null +++ b/ping/port.go @@ -0,0 +1,194 @@ +package ping + +import ( + "context" + "net/netip" + "slices" + "sync" + "time" + + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/gtcpip/header" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" +) + +const defaultFlowTimeout = time.Minute + +type Port struct { + ctx context.Context + logger logger.ContextLogger + controlFunc func(destination netip.Addr) control.Func + timeout time.Duration + + returnAccess sync.Mutex + returnPaths []tun.Return + + flowAccess sync.Mutex + flows map[flowKey]*Destination + lastSweep time.Time +} + +type flowKey struct { + source netip.Addr + destination netip.Addr + identifier uint16 +} + +func NewPort(ctx context.Context, logger logger.ContextLogger, controlFunc func(destination netip.Addr) control.Func, timeout time.Duration) *Port { + if timeout <= 0 { + timeout = defaultFlowTimeout + } + return &Port{ + ctx: ctx, + logger: logger, + controlFunc: controlFunc, + timeout: timeout, + flows: make(map[flowKey]*Destination), + } +} + +func (p *Port) PortAddresses() (netip.Addr, netip.Addr) { + return netip.IPv4Unspecified(), netip.IPv6Unspecified() +} + +func (p *Port) PortMTU() uint32 { + return 0 +} + +func (p *Port) AttachReturn(returnPath tun.Return) error { + p.returnAccess.Lock() + defer p.returnAccess.Unlock() + if slices.Contains(p.returnPaths, returnPath) { + return nil + } + p.returnPaths = append(p.returnPaths[:len(p.returnPaths):len(p.returnPaths)], returnPath) + return nil +} + +func (p *Port) DetachReturn(returnPath tun.Return) error { + p.returnAccess.Lock() + defer p.returnAccess.Unlock() + returnPaths := make([]tun.Return, 0, len(p.returnPaths)) + for _, existing := range p.returnPaths { + if existing != returnPath { + returnPaths = append(returnPaths, existing) + } + } + p.returnPaths = returnPaths + return nil +} + +func (p *Port) WritePackets(packets [][]byte) error { + var errs []error + for _, packet := range packets { + err := p.writePacket(packet) + if err != nil { + errs = append(errs, err) + } + } + return E.Errors(errs...) +} + +func (p *Port) writePacket(packet []byte) error { + var ( + source netip.Addr + destination netip.Addr + identifier uint16 + ) + switch header.IPVersion(packet) { + case header.IPv4Version: + ipHdr := header.IPv4(packet) + if !ipHdr.IsValid(len(packet)) || ipHdr.TransportProtocol() != header.ICMPv4ProtocolNumber || ipHdr.PayloadLength() < header.ICMPv4MinimumSize { + return nil + } + icmpHdr := header.ICMPv4(ipHdr.Payload()) + if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != 0 { + return nil + } + source = ipHdr.SourceAddr() + destination = ipHdr.DestinationAddr() + identifier = icmpHdr.Ident() + case header.IPv6Version: + ipHdr := header.IPv6(packet) + if !ipHdr.IsValid(len(packet)) || ipHdr.TransportProtocol() != header.ICMPv6ProtocolNumber || ipHdr.PayloadLength() < header.ICMPv6MinimumSize { + return nil + } + icmpHdr := header.ICMPv6(ipHdr.Payload()) + if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != 0 { + return nil + } + source = ipHdr.SourceAddr() + destination = ipHdr.DestinationAddr() + identifier = icmpHdr.Ident() + default: + return nil + } + flow, err := p.flowFor(source, destination, identifier) + if err != nil { + return E.Cause(err, "connect ICMP flow to ", destination) + } + return flow.WritePacket(buf.As(packet)) +} + +func (p *Port) flowFor(source netip.Addr, destination netip.Addr, identifier uint16) (*Destination, error) { + key := flowKey{source: source, destination: destination, identifier: identifier} + p.flowAccess.Lock() + defer p.flowAccess.Unlock() + now := time.Now() + if now.Sub(p.lastSweep) >= p.timeout { + p.lastSweep = now + for oldKey, oldFlow := range p.flows { + if oldFlow.IsClosed() { + delete(p.flows, oldKey) + } + } + } + flow, loaded := p.flows[key] + if loaded && !flow.IsClosed() { + return flow, nil + } + var controlFunc control.Func + if p.controlFunc != nil { + controlFunc = p.controlFunc(destination) + } + flow, err := ConnectDestination(p.ctx, p.logger, controlFunc, destination, portWriter{p}, p.timeout) + if err != nil { + return nil, err + } + p.flows[key] = flow + return flow, nil +} + +type portWriter struct { + port *Port +} + +func (w portWriter) WritePacket(packet []byte) error { + w.port.returnAccess.Lock() + returnPaths := w.port.returnPaths + w.port.returnAccess.Unlock() + for _, returnPath := range returnPaths { + headroom := returnPath.ReturnHeadroom() + buffer := make([]byte, headroom+len(packet)) + copy(buffer[headroom:], packet) + unconsumed := returnPath.ReturnPackets([][]byte{buffer}) + if len(unconsumed) == 0 { + return nil + } + } + return nil +} + +func (p *Port) Close() error { + p.flowAccess.Lock() + defer p.flowAccess.Unlock() + var errs []error + for key, flow := range p.flows { + errs = append(errs, flow.Close()) + delete(p.flows, key) + } + return E.Errors(errs...) +} diff --git a/ping/source_rewriter.go b/ping/source_rewriter.go deleted file mode 100644 index 545560de..00000000 --- a/ping/source_rewriter.go +++ /dev/null @@ -1,150 +0,0 @@ -package ping - -import ( - "context" - "net/netip" - "sync" - - "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun/gtcpip/header" - "github.com/sagernet/sing/common/logger" -) - -type SourceRewriter struct { - ctx context.Context - logger logger.ContextLogger - access sync.RWMutex - sessions map[tun.DirectRouteSession]tun.DirectRouteContext - sourceAddress map[uint16]netip.Addr - inet4Address netip.Addr - inet6Address netip.Addr -} - -func NewSourceRewriter(ctx context.Context, logger logger.ContextLogger, inet4Address netip.Addr, inet6Address netip.Addr) *SourceRewriter { - return &SourceRewriter{ - ctx: ctx, - logger: logger, - sessions: make(map[tun.DirectRouteSession]tun.DirectRouteContext), - sourceAddress: make(map[uint16]netip.Addr), - inet4Address: inet4Address, - inet6Address: inet6Address, - } -} - -func (m *SourceRewriter) CreateSession(session tun.DirectRouteSession, context tun.DirectRouteContext) { - m.access.Lock() - m.sessions[session] = context - m.access.Unlock() -} - -func (m *SourceRewriter) DeleteSession(session tun.DirectRouteSession) { - m.access.Lock() - delete(m.sessions, session) - m.access.Unlock() -} - -func (m *SourceRewriter) RewritePacket(packet []byte) { - var ipHdr header.Network - var bindAddr netip.Addr - switch header.IPVersion(packet) { - case header.IPv4Version: - ipHdr = header.IPv4(packet) - bindAddr = m.inet4Address - case header.IPv6Version: - ipHdr = header.IPv6(packet) - bindAddr = m.inet6Address - default: - return - } - sourceAddr := ipHdr.SourceAddr() - ipHdr.SetSourceAddr(bindAddr) - if ipHdr4, isIPv4 := ipHdr.(header.IPv4); isIPv4 { - ipHdr4.SetChecksum(^ipHdr4.CalculateChecksum()) - } - switch ipHdr.TransportProtocol() { - case header.ICMPv4ProtocolNumber: - icmpHdr := header.ICMPv4(ipHdr.Payload()) - m.access.Lock() - m.sourceAddress[icmpHdr.Ident()] = sourceAddr - m.access.Unlock() - m.logger.TraceContext(m.ctx, "write ICMPv4 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) - case header.ICMPv6ProtocolNumber: - icmpHdr := header.ICMPv6(ipHdr.Payload()) - icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr, - Src: ipHdr.SourceAddressSlice(), - Dst: ipHdr.DestinationAddressSlice(), - })) - m.access.Lock() - m.sourceAddress[icmpHdr.Ident()] = sourceAddr - m.access.Unlock() - m.logger.TraceContext(m.ctx, "write ICMPv6 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) - } -} - -func (m *SourceRewriter) WriteBack(packet []byte) (bool, error) { - var ipHdr header.Network - var routeSession tun.DirectRouteSession - switch header.IPVersion(packet) { - case header.IPv4Version: - ipHdr = header.IPv4(packet) - routeSession.Destination = ipHdr.SourceAddr() - case header.IPv6Version: - ipHdr = header.IPv6(packet) - routeSession.Destination = ipHdr.SourceAddr() - default: - return false, nil - } - switch ipHdr.TransportProtocol() { - case header.ICMPv4ProtocolNumber: - icmpHdr := header.ICMPv4(ipHdr.Payload()) - m.access.Lock() - ident := icmpHdr.Ident() - source, loaded := m.sourceAddress[ident] - if !loaded { - m.access.Unlock() - return false, nil - } - delete(m.sourceAddress, icmpHdr.Ident()) - m.access.Unlock() - routeSession.Source = source - case header.ICMPv6ProtocolNumber: - icmpHdr := header.ICMPv6(ipHdr.Payload()) - m.access.Lock() - ident := icmpHdr.Ident() - source, loaded := m.sourceAddress[ident] - if !loaded { - m.access.Unlock() - return false, nil - } - delete(m.sourceAddress, icmpHdr.Ident()) - m.access.Unlock() - routeSession.Source = source - default: - return false, nil - } - m.access.RLock() - context, loaded := m.sessions[routeSession] - m.access.RUnlock() - if !loaded { - return false, nil - } - ipHdr.SetDestinationAddr(routeSession.Source) - if ipHdr4, isIPv4 := ipHdr.(header.IPv4); isIPv4 { - ipHdr4.SetChecksum(^ipHdr4.CalculateChecksum()) - } - switch ipHdr.TransportProtocol() { - case header.ICMPv4ProtocolNumber: - icmpHdr := header.ICMPv4(ipHdr.Payload()) - m.logger.TraceContext(m.ctx, "read ICMPv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) - case header.ICMPv6ProtocolNumber: - icmpHdr := header.ICMPv6(ipHdr.Payload()) - icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr, - Src: ipHdr.SourceAddressSlice(), - Dst: ipHdr.DestinationAddressSlice(), - })) - m.logger.TraceContext(m.ctx, "read ICMPv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) - } - return true, context.WritePacket(packet) -} diff --git a/redirect_linux.go b/redirect_linux.go index d575d192..c679e5f0 100644 --- a/redirect_linux.go +++ b/redirect_linux.go @@ -137,22 +137,24 @@ func (r *autoRedirect) Start() error { r.redirectServer = server } if r.useNFTables { - var handler *nfqueueHandler - handler, err = newNFQueueHandler(nfqueueOptions{ - Context: r.ctx, - Handler: r.handler, - Logger: r.logger, - Queue: r.effectiveNFQueue(), - OutputMark: r.effectiveOutputMark(), - ResetMark: r.effectiveResetMark(), - }) - if err != nil { - r.logger.Warn("nfqueue not available, pre-match disabled (missing nfnetlink_queue and nft_queue kernel module?): ", err) - } else if err = handler.Start(); err != nil { - r.logger.Warn("nfqueue start failed, pre-match disabled (missing nfnetlink_queue and nft_queue kernel module?): ", err) - } else { - r.nfqueueHandler = handler - r.nfqueueEnabled = true + if r.handler != nil { + var handler *nfqueueHandler + handler, err = newNFQueueHandler(nfqueueOptions{ + Context: r.ctx, + Handler: r.handler, + Logger: r.logger, + Queue: r.effectiveNFQueue(), + OutputMark: r.effectiveOutputMark(), + ResetMark: r.effectiveResetMark(), + }) + if err != nil { + r.logger.Warn("nfqueue not available, pre-match disabled (missing nfnetlink_queue and nft_queue kernel module?): ", err) + } else if err = handler.Start(); err != nil { + r.logger.Warn("nfqueue start failed, pre-match disabled (missing nfnetlink_queue and nft_queue kernel module?): ", err) + } else { + r.nfqueueHandler = handler + r.nfqueueEnabled = true + } } r.cleanupNFTables() err = r.setupNFTables() diff --git a/route_direct.go b/route_direct.go deleted file mode 100644 index 444eb5e0..00000000 --- a/route_direct.go +++ /dev/null @@ -1,61 +0,0 @@ -package tun - -import ( - "net/netip" - "time" - - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/contrab/freelru" - "github.com/sagernet/sing/contrab/maphash" -) - -type DirectRouteDestination interface { - WritePacket(packet *buf.Buffer) error - Close() error - IsClosed() bool -} - -type DirectRouteSession struct { - // IPVersion uint8 - // Network uint8 - Source netip.Addr - Destination netip.Addr -} - -type DirectRouteMapping struct { - mapping freelru.Cache[DirectRouteSession, DirectRouteDestination] - timeout time.Duration -} - -func NewDirectRouteMapping(timeout time.Duration) *DirectRouteMapping { - mapping := common.Must1(freelru.NewSharded[DirectRouteSession, DirectRouteDestination](1024, maphash.NewHasher[DirectRouteSession]().Hash32)) - mapping.SetHealthCheck(func(session DirectRouteSession, action DirectRouteDestination) bool { - if action != nil { - return !action.IsClosed() - } - return true - }) - mapping.SetOnEvict(func(session DirectRouteSession, action DirectRouteDestination) { - if action != nil { - action.Close() - } - }) - mapping.SetLifetime(timeout) - return &DirectRouteMapping{mapping, timeout} -} - -func (m *DirectRouteMapping) Lookup(session DirectRouteSession, constructor func(timeout time.Duration) (DirectRouteDestination, error)) (DirectRouteDestination, error) { - var ( - created DirectRouteDestination - err error - ) - action, _, ok := m.mapping.GetAndRefreshOrAdd(session, func() (DirectRouteDestination, bool) { - created, err = constructor(m.timeout) - return created, err == nil - }) - if !ok { - return nil, err - } - return action, nil -} diff --git a/stack.go b/stack.go index d128feb1..eaf24058 100644 --- a/stack.go +++ b/stack.go @@ -12,12 +12,6 @@ import ( "github.com/sagernet/sing/common/logger" ) -var ( - ErrDrop = E.New("drop by rule") - ErrReset = E.New("reset by rule") - ErrBypass = E.New("bypass by rule") -) - type Stack interface { Start() error Close() error diff --git a/stack_gvisor.go b/stack_gvisor.go index 92e84ba4..03b28732 100644 --- a/stack_gvisor.go +++ b/stack_gvisor.go @@ -6,8 +6,10 @@ import ( "context" "net/netip" "runtime" + "sync" "time" + "github.com/sagernet/gvisor/pkg/buffer" "github.com/sagernet/gvisor/pkg/tcpip" "github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet" "github.com/sagernet/gvisor/pkg/tcpip/header" @@ -40,6 +42,8 @@ type GVisor struct { logger logger.Logger stack *stack.Stack endpoint stack.LinkEndpoint + dispatcher *ForwardDispatcher + icmpForwarder *ICMPForwarder } type GVisorTun interface { @@ -88,23 +92,39 @@ func (t *GVisor) Start() error { if err != nil { return err } - linkEndpoint = &LinkEndpointFilter{linkEndpoint, t.broadcastAddr, t.tun} + if t.handler != nil { + t.dispatcher = NewForwardDispatcher(t.handler, &gvisorWriteback{tun: t.tun}, t.logger, t.udpTimeout, t.icmpTimeout) + } + linkEndpoint = &LinkEndpointFilter{ + LinkEndpoint: linkEndpoint, + BroadcastAddress: t.broadcastAddr, + Writer: t.tun, + Dispatcher: t.dispatcher, + Inet4Address: t.inet4Address, + Inet6Address: t.inet6Address, + Inet4LoopbackAddress: t.inet4LoopbackAddress, + Inet6LoopbackAddress: t.inet6LoopbackAddress, + } ipStack, err := newGVisorStack(linkEndpoint, nicOptions, false, true) if err != nil { return err } ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, NewTCPForwarderWithLoopback(t.ctx, ipStack, t.handler, t.inet4LoopbackAddress, t.inet6LoopbackAddress, t.tun).HandlePacket) ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, NewUDPForwarder(t.ctx, ipStack, t.handler, t.udpTimeout).HandlePacket) - icmpForwarder := NewICMPForwarder(t.ctx, ipStack, t.logger, t.handler, t.icmpTimeout) - icmpForwarder.SetLocalAddresses(t.inet4Address, t.inet6Address) + icmpForwarder := NewICMPForwarder(ipStack, t.handler, t.logger) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) + t.icmpForwarder = icmpForwarder t.stack = ipStack t.endpoint = linkEndpoint return nil } func (t *GVisor) Close() error { + t.dispatcher.Close() + if t.icmpForwarder != nil { + t.icmpForwarder.Close() + } if t.stack == nil { return nil } @@ -116,6 +136,37 @@ func (t *GVisor) Close() error { return nil } +type gvisorWriteback struct { + tun GVisorTun + access sync.Mutex +} + +func (w *gvisorWriteback) ReturnHeadroom() int { + return 0 +} + +func (w *gvisorWriteback) WriteReturnPackets(packets [][]byte) error { + w.access.Lock() + defer w.access.Unlock() + var writeErrors []error + for _, packet := range packets { + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(packet), + }) + if header.IPVersion(packet) == header.IPv6Version { + pkt.NetworkProtocolNumber = header.IPv6ProtocolNumber + } else { + pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber + } + _, err := w.tun.WritePacket(pkt) + pkt.DecRef() + if err != nil { + writeErrors = append(writeErrors, err) + } + } + return E.Errors(writeErrors...) +} + func AddressFromAddr(destination netip.Addr) tcpip.Address { if destination.Is6() { return tcpip.AddrFrom16(destination.As16()) diff --git a/stack_gvisor_filter.go b/stack_gvisor_filter.go index 18e46e8f..abdbcaa7 100644 --- a/stack_gvisor_filter.go +++ b/stack_gvisor_filter.go @@ -14,20 +14,39 @@ var _ stack.LinkEndpoint = (*LinkEndpointFilter)(nil) type LinkEndpointFilter struct { stack.LinkEndpoint - BroadcastAddress netip.Addr - Writer GVisorTun + BroadcastAddress netip.Addr + Writer GVisorTun + Dispatcher *ForwardDispatcher + Inet4Address netip.Addr + Inet6Address netip.Addr + Inet4LoopbackAddress []netip.Addr + Inet6LoopbackAddress []netip.Addr } func (w *LinkEndpointFilter) Attach(dispatcher stack.NetworkDispatcher) { - w.LinkEndpoint.Attach(&networkDispatcherFilter{dispatcher, w.BroadcastAddress, w.Writer}) + w.LinkEndpoint.Attach(&networkDispatcherFilter{ + NetworkDispatcher: dispatcher, + broadcastAddress: w.BroadcastAddress, + writer: w.Writer, + dispatcher: w.Dispatcher, + inet4Address: w.Inet4Address, + inet6Address: w.Inet6Address, + inet4LoopbackAddress: w.Inet4LoopbackAddress, + inet6LoopbackAddress: w.Inet6LoopbackAddress, + }) } var _ stack.NetworkDispatcher = (*networkDispatcherFilter)(nil) type networkDispatcherFilter struct { stack.NetworkDispatcher - broadcastAddress netip.Addr - writer GVisorTun + broadcastAddress netip.Addr + writer GVisorTun + dispatcher *ForwardDispatcher + inet4Address netip.Addr + inet6Address netip.Addr + inet4LoopbackAddress []netip.Addr + inet6LoopbackAddress []netip.Addr } func (w *networkDispatcherFilter) DeliverNetworkPacket(protocol tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) { @@ -50,5 +69,45 @@ func (w *networkDispatcherFilter) DeliverNetworkPacket(protocol tcpip.NetworkPro w.writer.WritePacket(pkt) return } + if w.dispatcher != nil && pkt.GSOOptions.Type == stack.GSONone && !pkt.GSOOptions.NeedsCsum { + if view, loaded := pkt.Data().PullUp(pkt.Data().Size()); loaded { + consumed := w.dispatch(protocol, destination, view) + w.dispatcher.Flush() + if consumed { + return + } + } + } w.NetworkDispatcher.DeliverNetworkPacket(protocol, pkt) } + +func (w *networkDispatcherFilter) dispatch(protocol tcpip.NetworkProtocolNumber, destination netip.Addr, view []byte) bool { + if protocol == header.IPv4ProtocolNumber { + switch header.IPv4(view).TransportProtocol() { + case header.TCPProtocolNumber: + for _, inet4LoopbackAddress := range w.inet4LoopbackAddress { + if destination == inet4LoopbackAddress { + return false + } + } + case header.ICMPv4ProtocolNumber: + if destination == w.inet4Address { + return false + } + } + } else { + switch header.IPv6(view).TransportProtocol() { + case header.TCPProtocolNumber: + for _, inet6LoopbackAddress := range w.inet6LoopbackAddress { + if destination == inet6LoopbackAddress { + return false + } + } + case header.ICMPv6ProtocolNumber: + if destination == w.inet6Address { + return false + } + } + } + return w.dispatcher.Dispatch(view) +} diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index 1f11bbdd..979f4ad9 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -3,10 +3,9 @@ package tun import ( - "context" - "errors" "net/netip" "sync" + "sync/atomic" "time" "github.com/sagernet/gvisor/pkg/buffer" @@ -17,42 +16,51 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip/network/ipv4" "github.com/sagernet/gvisor/pkg/tcpip/network/ipv6" "github.com/sagernet/gvisor/pkg/tcpip/stack" - "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" ) type ICMPForwarder struct { - ctx context.Context - stack *stack.Stack - logger logger.Logger - inet4Address netip.Addr - inet6Address netip.Addr - handler Handler - mapping *DirectRouteMapping + stack *stack.Stack + handler Handler + logger logger.Logger + + returnPath icmpForwarderReturn + + flowAccess sync.Mutex + flows map[icmpFlowKey]time.Time + lastSweep time.Time + attachedPorts map[Port]bool } -func NewICMPForwarder( - ctx context.Context, - stack *stack.Stack, - logger logger.Logger, - handler Handler, - timeout time.Duration, -) *ICMPForwarder { - return &ICMPForwarder{ - ctx: ctx, - stack: stack, - logger: logger, - handler: handler, - mapping: NewDirectRouteMapping(timeout), +type icmpFlowKey struct { + v6 bool + source netip.Addr + destination netip.Addr + identifier uint16 +} + +func NewICMPForwarder(stack *stack.Stack, handler Handler, logger logger.Logger) *ICMPForwarder { + forwarder := &ICMPForwarder{ + stack: stack, + handler: handler, + logger: logger, + flows: make(map[icmpFlowKey]time.Time), + attachedPorts: make(map[Port]bool), } + forwarder.returnPath.forwarder = forwarder + return forwarder } -func (f *ICMPForwarder) SetLocalAddresses(inet4Address, inet6Address netip.Addr) { - f.inet4Address = inet4Address - f.inet6Address = inet6Address +func (f *ICMPForwarder) Close() error { + f.returnPath.closed.Store(true) + f.flowAccess.Lock() + defer f.flowAccess.Unlock() + for port := range f.attachedPorts { + port.DetachReturn(&f.returnPath) + delete(f.attachedPorts, port) + } + return nil } func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { @@ -62,34 +70,17 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != 0 { return false } - sourceAddr := M.AddrFromIP(ipHdr.SourceAddressSlice()) - destinationAddr := M.AddrFromIP(ipHdr.DestinationAddressSlice()) - if destinationAddr != f.inet4Address { - action, err := f.mapping.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { - return f.handler.PrepareConnection( - N.NetworkICMP, - M.SocksaddrFrom(sourceAddr, 0), - M.SocksaddrFrom(destinationAddr, 0), - &ICMPBackWriter{ - stack: f.stack, - packet: pkt, - source: ipHdr.SourceAddress(), - sourceNetwork: header.IPv4ProtocolNumber, - }, - timeout, - ) - }) - if errors.Is(err, ErrReset) { - gWriteUnreachable(f.stack, pkt) - return true - } else if errors.Is(err, ErrDrop) { - return true - } - if action != nil { - err = icmpWritePacketBuffer(action, pkt) - if err != nil { - f.logger.Error(E.Cause(err, "write ICMPv4 echo request")) - } + identifier := icmpHdr.Ident() + verdict := f.handler.JudgeFlow( + uint8(header.ICMPv4ProtocolNumber), + netip.AddrPortFrom(AddrFromAddress(ipHdr.SourceAddress()), identifier), + netip.AddrPortFrom(AddrFromAddress(ipHdr.DestinationAddress()), identifier), + ) + switch verdict.Action { + case ActionReject, ActionDrop: + return true + case ActionFlow: + if f.forwardFlow(verdict.Port, false, AddrFromAddress(ipHdr.SourceAddress()), AddrFromAddress(ipHdr.DestinationAddress()), identifier, pkt) { return true } } @@ -125,35 +116,17 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != 0 { return false } - sourceAddr := M.AddrFromIP(ipHdr.SourceAddressSlice()) - destinationAddr := M.AddrFromIP(ipHdr.DestinationAddressSlice()) - if destinationAddr != f.inet6Address { - action, err := f.mapping.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { - return f.handler.PrepareConnection( - N.NetworkICMP, - M.SocksaddrFrom(sourceAddr, 0), - M.SocksaddrFrom(destinationAddr, 0), - &ICMPBackWriter{ - stack: f.stack, - packet: pkt, - source: ipHdr.SourceAddress(), - sourceNetwork: header.IPv6ProtocolNumber, - }, - timeout, - ) - }) - if errors.Is(err, ErrReset) { - gWriteUnreachable(f.stack, pkt) - return true - } else if errors.Is(err, ErrDrop) { - return true - } - if action != nil { - pkt.IncRef() - err = icmpWritePacketBuffer(action, pkt) - if err != nil { - f.logger.Error(E.Cause(err, "write ICMPv6 echo request")) - } + identifier := icmpHdr.Ident() + verdict := f.handler.JudgeFlow( + uint8(header.ICMPv6ProtocolNumber), + netip.AddrPortFrom(AddrFromAddress(ipHdr.SourceAddress()), identifier), + netip.AddrPortFrom(AddrFromAddress(ipHdr.DestinationAddress()), identifier), + ) + switch verdict.Action { + case ActionReject, ActionDrop: + return true + case ActionFlow: + if f.forwardFlow(verdict.Port, true, AddrFromAddress(ipHdr.SourceAddress()), AddrFromAddress(ipHdr.DestinationAddress()), identifier, pkt) { return true } } @@ -190,64 +163,179 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa } } -type ICMPBackWriter struct { - access sync.Mutex - stack *stack.Stack - packet *stack.PacketBuffer - source tcpip.Address - sourceNetwork tcpip.NetworkProtocolNumber +func (f *ICMPForwarder) forwardFlow(port Port, v6 bool, source netip.Addr, destination netip.Addr, identifier uint16, pkt *stack.PacketBuffer) bool { + if port == nil { + return false + } + inet4Address, inet6Address := port.PortAddresses() + portAddress := inet4Address + if v6 { + portAddress = inet6Address + } + if !portAddress.IsValid() || !portAddress.IsUnspecified() { + return false + } + f.flowAccess.Lock() + if !f.attachedPorts[port] { + err := port.AttachReturn(&f.returnPath) + if err != nil { + f.flowAccess.Unlock() + f.logger.Trace(E.Cause(err, "attach ICMP return path")) + return false + } + f.attachedPorts[port] = true + } + now := time.Now() + if now.Sub(f.lastSweep) >= defaultICMPTimeout { + f.lastSweep = now + for key, deadline := range f.flows { + if now.After(deadline) { + delete(f.flows, key) + } + } + } + f.flows[icmpFlowKey{v6: v6, source: source, destination: destination, identifier: identifier}] = now.Add(defaultICMPTimeout) + f.flowAccess.Unlock() + networkSlice := pkt.NetworkHeader().Slice() + transportSlice := pkt.TransportHeader().Slice() + dataSlice := pkt.Data().AsRange().ToSlice() + packetSlice := make([]byte, 0, len(networkSlice)+len(transportSlice)+len(dataSlice)) + packetSlice = append(packetSlice, networkSlice...) + packetSlice = append(packetSlice, transportSlice...) + packetSlice = append(packetSlice, dataSlice...) + err := port.WritePackets([][]byte{packetSlice}) + if err != nil { + f.logger.Trace(E.Cause(err, "forward ICMP packet")) + } + return true } -func (w *ICMPBackWriter) WritePacket(p []byte) error { - if w.sourceNetwork == header.IPv4ProtocolNumber { - route, err := w.stack.FindRoute( - DefaultNIC, - header.IPv4(p).SourceAddress(), - w.source, - w.sourceNetwork, - false, - ) - if err != nil { - return gonet.TranslateNetstackError(err) +func (f *ICMPForwarder) lookupFlow(key icmpFlowKey) bool { + f.flowAccess.Lock() + defer f.flowAccess.Unlock() + deadline, loaded := f.flows[key] + if !loaded { + return false + } + now := time.Now() + if now.After(deadline) { + delete(f.flows, key) + return false + } + f.flows[key] = now.Add(defaultICMPTimeout) + return true +} + +type icmpForwarderReturn struct { + forwarder *ICMPForwarder + closed atomic.Bool +} + +func (r *icmpForwarderReturn) ReturnHeadroom() int { + return 0 +} + +func (r *icmpForwarderReturn) ReturnPackets(packets [][]byte) [][]byte { + if r.closed.Load() { + return packets + } + unconsumed := packets[:0] + for _, packet := range packets { + if !r.forwarder.returnPacket(packet) { + unconsumed = append(unconsumed, packet) } - defer route.Release() - packet := stack.NewPacketBuffer(stack.PacketBufferOptions{ - Payload: buffer.MakeWithData(p), - }) - defer packet.DecRef() - parse.IPv4(packet) - err = route.WritePacketDirect(packet) - if err != nil { - return gonet.TranslateNetstackError(err) + } + return unconsumed +} + +func (f *ICMPForwarder) returnPacket(packet []byte) bool { + if len(packet) == 0 { + return false + } + switch header.IPVersion(packet) { + case header.IPv4Version: + ipHdr := header.IPv4(packet) + if !ipHdr.IsValid(len(packet)) || ipHdr.TransportProtocol() != header.ICMPv4ProtocolNumber || len(ipHdr.Payload()) < header.ICMPv4MinimumSize { + return false } - } else { - route, err := w.stack.FindRoute( - DefaultNIC, - header.IPv6(p).SourceAddress(), - w.source, - w.sourceNetwork, - false, - ) - if err != nil { - return gonet.TranslateNetstackError(err) + icmpHdr := header.ICMPv4(ipHdr.Payload()) + var key icmpFlowKey + switch icmpHdr.Type() { + case header.ICMPv4EchoReply: + key = icmpFlowKey{ + source: AddrFromAddress(ipHdr.DestinationAddress()), + destination: AddrFromAddress(ipHdr.SourceAddress()), + identifier: icmpHdr.Ident(), + } + case header.ICMPv4TimeExceeded, header.ICMPv4DstUnreachable: + inner := icmpHdr.Payload() + if len(inner) < header.IPv4MinimumSize { + return false + } + innerIPHdr := header.IPv4(inner) + innerHeaderLength := int(innerIPHdr.HeaderLength()) + if innerHeaderLength < header.IPv4MinimumSize || len(inner) < innerHeaderLength+header.ICMPv4MinimumSize { + return false + } + if innerIPHdr.TransportProtocol() != header.ICMPv4ProtocolNumber { + return false + } + innerICMPHdr := header.ICMPv4(inner[innerHeaderLength:]) + key = icmpFlowKey{ + source: AddrFromAddress(innerIPHdr.SourceAddress()), + destination: AddrFromAddress(innerIPHdr.DestinationAddress()), + identifier: innerICMPHdr.Ident(), + } + default: + return false } - defer route.Release() - packet := stack.NewPacketBuffer(stack.PacketBufferOptions{ - Payload: buffer.MakeWithData(p), - }) - parse.IPv6(packet) - defer packet.DecRef() - err = route.WritePacketDirect(packet) - if err != nil { - return gonet.TranslateNetstackError(err) + if !f.lookupFlow(key) { + return false + } + return f.writeBack(packet, header.IPv4ProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress()) + case header.IPv6Version: + ipHdr := header.IPv6(packet) + if !ipHdr.IsValid(len(packet)) || ipHdr.TransportProtocol() != header.ICMPv6ProtocolNumber || len(ipHdr.Payload()) < header.ICMPv6MinimumSize { + return false + } + icmpHdr := header.ICMPv6(ipHdr.Payload()) + if icmpHdr.Type() != header.ICMPv6EchoReply { + return false } + key := icmpFlowKey{ + v6: true, + source: AddrFromAddress(ipHdr.DestinationAddress()), + destination: AddrFromAddress(ipHdr.SourceAddress()), + identifier: icmpHdr.Ident(), + } + if !f.lookupFlow(key) { + return false + } + return f.writeBack(packet, header.IPv6ProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress()) + default: + return false } - return nil } -func icmpWritePacketBuffer(action DirectRouteDestination, packetBuffer *stack.PacketBuffer) error { - packetSlice := packetBuffer.NetworkHeader().Slice() - packetSlice = append(packetSlice, packetBuffer.TransportHeader().Slice()...) - packetSlice = append(packetSlice, packetBuffer.Data().AsRange().ToSlice()...) - return action.WritePacket(buf.As(packetSlice).ToOwned()) +func (f *ICMPForwarder) writeBack(packet []byte, protocol tcpip.NetworkProtocolNumber, localAddress tcpip.Address, remoteAddress tcpip.Address) bool { + route, gErr := f.stack.FindRoute(DefaultNIC, localAddress, remoteAddress, protocol, false) + if gErr != nil { + f.logger.Error(E.Cause(gonet.TranslateNetstackError(gErr), "find route for ICMP reply")) + return true + } + defer route.Release() + packetBuffer := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(packet), + }) + defer packetBuffer.DecRef() + if protocol == header.IPv4ProtocolNumber { + parse.IPv4(packetBuffer) + } else { + parse.IPv6(packetBuffer) + } + gErr = route.WritePacketDirect(packetBuffer) + if gErr != nil { + f.logger.Error(E.Cause(gonet.TranslateNetstackError(gErr), "write ICMP reply")) + } + return true } diff --git a/stack_gvisor_lazy.go b/stack_gvisor_lazy.go index dcbcafb9..258392d1 100644 --- a/stack_gvisor_lazy.go +++ b/stack_gvisor_lazy.go @@ -4,7 +4,6 @@ package tun import ( "context" - "errors" "net" "os" "sync" @@ -74,7 +73,7 @@ func (c *gLazyConn) HandshakeFailure(err error) error { if c.handshakeDone { return os.ErrInvalid } - c.request.Complete(!errors.Is(err, ErrDrop)) + c.request.Complete(true) c.handshakeDone = true c.handshakeErr = err return nil diff --git a/stack_gvisor_tcp.go b/stack_gvisor_tcp.go index ba8af6df..371c480b 100644 --- a/stack_gvisor_tcp.go +++ b/stack_gvisor_tcp.go @@ -4,7 +4,6 @@ package tun import ( "context" - "errors" "net/netip" "github.com/sagernet/gvisor/pkg/tcpip" @@ -14,7 +13,6 @@ import ( "github.com/sagernet/sing-tun/gtcpip/checksum" "github.com/sagernet/sing/common" M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" ) type TCPForwarder struct { @@ -79,9 +77,12 @@ func (f *TCPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac func (f *TCPForwarder) Forward(r *tcp.ForwarderRequest) { source := M.SocksaddrFrom(AddrFromAddress(r.ID().RemoteAddress), r.ID().RemotePort) destination := M.SocksaddrFrom(AddrFromAddress(r.ID().LocalAddress), r.ID().LocalPort) - _, pErr := f.handler.PrepareConnection(N.NetworkTCP, source, destination, nil, 0) - if pErr != nil { - r.Complete(!errors.Is(pErr, ErrDrop)) + switch f.handler.JudgeFlow(uint8(header.TCPProtocolNumber), source.AddrPort(), destination.AddrPort()).Action { + case ActionReject: + r.Complete(true) + return + case ActionDrop: + r.Complete(false) return } conn := &gLazyConn{ diff --git a/stack_gvisor_udp.go b/stack_gvisor_udp.go index f91a2b3e..3ad027ec 100644 --- a/stack_gvisor_udp.go +++ b/stack_gvisor_udp.go @@ -4,7 +4,6 @@ package tun import ( "context" - "errors" "math" "net/netip" "os" @@ -58,11 +57,11 @@ func (f *UDPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac func rangeIterate(r stack.Range, fn func(*buffer.View)) func (f *UDPForwarder) PreparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { - _, pErr := f.handler.PrepareConnection(N.NetworkUDP, source, destination, nil, 0) - if pErr != nil { - if !errors.Is(pErr, ErrDrop) { - gWriteUnreachable(f.stack, userData.(*stack.PacketBuffer)) - } + switch f.handler.JudgeFlow(uint8(header.UDPProtocolNumber), source.AddrPort(), destination.AddrPort()).Action { + case ActionReject: + gWriteUnreachable(f.stack, userData.(*stack.PacketBuffer)) + return false, nil, nil, nil + case ActionDrop: return false, nil, nil, nil } var sourceNetwork tcpip.NetworkProtocolNumber diff --git a/stack_mixed.go b/stack_mixed.go index 33284053..4f9242b9 100644 --- a/stack_mixed.go +++ b/stack_mixed.go @@ -73,8 +73,6 @@ func (m *Mixed) tunLoop() { return } if linuxTUN, isLinuxTUN := m.tun.(LinuxTUN); isLinuxTUN { - m.frontHeadroom = linuxTUN.FrontHeadroom() - m.txChecksumOffload = linuxTUN.TXChecksumOffload() batchSize := linuxTUN.BatchSize() if batchSize > 1 { m.batchLoopLinux(linuxTUN, batchSize) @@ -105,6 +103,7 @@ func (m *Mixed) tunLoop() { m.logger.Trace(E.Cause(err, "write packet")) } } + m.dispatcher.Flush() } } @@ -124,6 +123,7 @@ func (m *Mixed) wintunLoop(winTun WinTun) { m.logger.Trace(E.Cause(err, "write packet")) } } + m.dispatcher.Flush() release() } } @@ -164,11 +164,13 @@ func (m *Mixed) batchLoopLinux(linuxTUN LinuxTUN, batchSize int) { } writeBuffers = writeBuffers[:0] } + m.dispatcher.Flush() } } func (m *Mixed) batchLoopDarwin(darwinTUN DarwinTUN) { var writeBuffers []*buf.Buffer + var releaseBuffers []*buf.Buffer for { buffers, err := darwinTUN.BatchRead() if err != nil { @@ -181,6 +183,7 @@ func (m *Mixed) batchLoopDarwin(darwinTUN DarwinTUN) { continue } writeBuffers = writeBuffers[:0] + releaseBuffers = releaseBuffers[:0] for _, buffer := range buffers { packetSize := buffer.Len() if packetSize < header.IPv4MinimumSize { @@ -190,7 +193,7 @@ func (m *Mixed) batchLoopDarwin(darwinTUN DarwinTUN) { if m.processPacket(buffer.Bytes()) { writeBuffers = append(writeBuffers, buffer) } else { - buffer.Release() + releaseBuffers = append(releaseBuffers, buffer) } } if len(writeBuffers) > 0 { @@ -200,6 +203,8 @@ func (m *Mixed) batchLoopDarwin(darwinTUN DarwinTUN) { } buf.ReleaseMulti(writeBuffers) } + m.dispatcher.Flush() + buf.ReleaseMulti(releaseBuffers) } } @@ -229,6 +234,9 @@ func (m *Mixed) processIPv4(ipHdr header.IPv4) (writeBack bool, err error) { if destination == m.broadcastAddr || !destination.IsGlobalUnicast() { return } + if m.dispatchIPv4(ipHdr, destination) { + return false, nil + } switch ipHdr.TransportProtocol() { case header.TCPProtocolNumber: writeBack, err = m.processIPv4TCP(ipHdr, ipHdr.Payload()) @@ -249,9 +257,13 @@ func (m *Mixed) processIPv4(ipHdr header.IPv4) (writeBack bool, err error) { func (m *Mixed) processIPv6(ipHdr header.IPv6) (writeBack bool, err error) { writeBack = true - if !ipHdr.DestinationAddr().IsGlobalUnicast() { + destination := ipHdr.DestinationAddr() + if !destination.IsGlobalUnicast() { return } + if m.dispatchIPv6(ipHdr, destination) { + return false, nil + } switch ipHdr.TransportProtocol() { case header.TCPProtocolNumber: writeBack, err = m.processIPv6TCP(ipHdr, ipHdr.Payload()) diff --git a/stack_system.go b/stack_system.go index d2799724..be9cb33b 100644 --- a/stack_system.go +++ b/stack_system.go @@ -5,6 +5,7 @@ import ( "errors" "net" "net/netip" + "slices" "syscall" "time" @@ -46,7 +47,7 @@ type System struct { tcpPort6 uint16 tcpNat *TCPNat udpNat *udpnat.Service - directNat *DirectRouteMapping + dispatcher *ForwardDispatcher bindInterface bool interfaceFinder control.InterfaceFinder frontHeadroom int @@ -101,6 +102,7 @@ func NewSystem(options StackOptions) (Stack, error) { } func (s *System) Close() error { + s.dispatcher.Close() return common.Close( s.tcpListener, s.tcpListener6, @@ -162,7 +164,13 @@ func (s *System) start() error { } s.tcpNat = NewNat(s.ctx, s.udpTimeout) s.udpNat = udpnat.New(s.handler, s.preparePacketConnection, s.udpTimeout, false) - s.directNat = NewDirectRouteMapping(s.icmpTimeout) + if linuxTUN, isLinuxTUN := s.tun.(LinuxTUN); isLinuxTUN { + s.frontHeadroom = linuxTUN.FrontHeadroom() + s.txChecksumOffload = linuxTUN.TXChecksumOffload() + } + if s.handler != nil { + s.dispatcher = NewForwardDispatcher(s.handler, newSystemWriteback(s.tun, s.frontHeadroom), s.logger, s.udpTimeout, s.icmpTimeout) + } return nil } @@ -172,8 +180,6 @@ func (s *System) tunLoop() { return } if linuxTUN, isLinuxTUN := s.tun.(LinuxTUN); isLinuxTUN { - s.frontHeadroom = linuxTUN.FrontHeadroom() - s.txChecksumOffload = linuxTUN.TXChecksumOffload() batchSize := linuxTUN.BatchSize() if batchSize > 1 { s.batchLoopLinux(linuxTUN, batchSize) @@ -204,6 +210,7 @@ func (s *System) tunLoop() { s.logger.Trace(E.Cause(err, "write packet")) } } + s.dispatcher.Flush() } } @@ -223,6 +230,7 @@ func (s *System) wintunLoop(winTun WinTun) { s.logger.Trace(E.Cause(err, "write packet")) } } + s.dispatcher.Flush() release() } } @@ -263,11 +271,13 @@ func (s *System) batchLoopLinux(linuxTUN LinuxTUN, batchSize int) { } writeBuffers = writeBuffers[:0] } + s.dispatcher.Flush() } } func (s *System) batchLoopDarwin(darwinTUN DarwinTUN) { var writeBuffers []*buf.Buffer + var releaseBuffers []*buf.Buffer for { buffers, err := darwinTUN.BatchRead() if err != nil { @@ -280,6 +290,7 @@ func (s *System) batchLoopDarwin(darwinTUN DarwinTUN) { continue } writeBuffers = writeBuffers[:0] + releaseBuffers = releaseBuffers[:0] for _, buffer := range buffers { packetSize := buffer.Len() if packetSize < header.IPv4MinimumSize { @@ -289,7 +300,7 @@ func (s *System) batchLoopDarwin(darwinTUN DarwinTUN) { if s.processPacket(buffer.Bytes()) { writeBuffers = append(writeBuffers, buffer) } else { - buffer.Release() + releaseBuffers = append(releaseBuffers, buffer) } } if len(writeBuffers) > 0 { @@ -299,6 +310,8 @@ func (s *System) batchLoopDarwin(darwinTUN DarwinTUN) { } buf.ReleaseMulti(writeBuffers) } + s.dispatcher.Flush() + buf.ReleaseMulti(releaseBuffers) } } @@ -338,11 +351,53 @@ func (s *System) acceptLoop(listener net.Listener) { } } +func (s *System) dispatchIPv4(ipHdr header.IPv4, destination netip.Addr) bool { + switch ipHdr.TransportProtocol() { + case header.TCPProtocolNumber: + if slices.Contains(s.inet4LoopbackAddress, destination) { + return false + } + if ipHdr.SourceAddr() == s.inet4Address && + ipHdr.FragmentOffset() == 0 && + len(ipHdr.Payload()) >= header.TCPMinimumSize && + header.TCP(ipHdr.Payload()).SourcePort() == s.tcpPort { + return false + } + case header.ICMPv4ProtocolNumber: + if destination == s.inet4Address { + return false + } + } + return s.dispatcher.Dispatch(ipHdr) +} + +func (s *System) dispatchIPv6(ipHdr header.IPv6, destination netip.Addr) bool { + switch ipHdr.TransportProtocol() { + case header.TCPProtocolNumber: + if slices.Contains(s.inet6LoopbackAddress, destination) { + return false + } + if ipHdr.SourceAddr() == s.inet6Address && + len(ipHdr.Payload()) >= header.TCPMinimumSize && + header.TCP(ipHdr.Payload()).SourcePort() == s.tcpPort6 { + return false + } + case header.ICMPv6ProtocolNumber: + if destination == s.inet6Address { + return false + } + } + return s.dispatcher.Dispatch(ipHdr) +} + func (s *System) processIPv4(ipHdr header.IPv4) (writeBack bool, err error) { destination := ipHdr.DestinationAddr() if destination == s.broadcastAddr || !destination.IsGlobalUnicast() { return } + if s.dispatchIPv4(ipHdr, destination) { + return false, nil + } writeBack = true switch ipHdr.TransportProtocol() { case header.TCPProtocolNumber: @@ -360,9 +415,13 @@ func (s *System) processIPv4(ipHdr header.IPv4) (writeBack bool, err error) { } func (s *System) processIPv6(ipHdr header.IPv6) (writeBack bool, err error) { - if !ipHdr.DestinationAddr().IsGlobalUnicast() { + destination := ipHdr.DestinationAddr() + if !destination.IsGlobalUnicast() { return } + if s.dispatchIPv6(ipHdr, destination) { + return false, nil + } writeBack = true switch ipHdr.TransportProtocol() { case header.TCPProtocolNumber: @@ -404,14 +463,7 @@ func (s *System) processIPv4TCP(ipHdr header.IPv4, tcpHdr header.TCP) (bool, err } } if !loopback { - natPort, err := s.tcpNat.Lookup(source, destination, s.handler) - if err != nil { - if errors.Is(err, ErrDrop) { - return false, nil - } else { - return false, s.resetIPv4TCP(ipHdr, tcpHdr) - } - } + natPort := s.tcpNat.Lookup(source, destination) ipHdr.SetSourceAddr(s.inet4NextAddress) tcpHdr.SetSourcePort(natPort) ipHdr.SetDestinationAddr(s.inet4Address) @@ -429,51 +481,6 @@ func (s *System) processIPv4TCP(ipHdr header.IPv4, tcpHdr header.TCP) (bool, err return true, nil } -func (s *System) resetIPv4TCP(origIPHdr header.IPv4, origTCPHdr header.TCP) error { - frontHeadroom := s.frontHeadroom + PacketOffset - newPacket := buf.NewSize(frontHeadroom + header.IPv4MinimumSize + header.TCPMinimumSize) - defer newPacket.Release() - newPacket.Resize(frontHeadroom, header.IPv4MinimumSize+header.TCPMinimumSize) - ipHdr := header.IPv4(newPacket.Bytes()) - ipHdr.Encode(&header.IPv4Fields{ - TotalLength: uint16(newPacket.Len()), - Protocol: uint8(header.TCPProtocolNumber), - SrcAddr: origIPHdr.DestinationAddr(), - DstAddr: origIPHdr.SourceAddr(), - }) - tcpHdr := header.TCP(ipHdr.Payload()) - fields := header.TCPFields{ - SrcPort: origTCPHdr.DestinationPort(), - DstPort: origTCPHdr.SourcePort(), - DataOffset: header.TCPMinimumSize, - Flags: header.TCPFlagRst, - } - if origTCPHdr.Flags()&header.TCPFlagAck != 0 { - fields.SeqNum = origTCPHdr.AckNumber() - } else { - fields.Flags |= header.TCPFlagAck - ackNum := origTCPHdr.SequenceNumber() + uint32(len(origTCPHdr.Payload())) - if origTCPHdr.Flags()&header.TCPFlagSyn != 0 { - ackNum++ - } - if origTCPHdr.Flags()&header.TCPFlagFin != 0 { - ackNum++ - } - fields.AckNum = ackNum - } - tcpHdr.Encode(&fields) - if !s.txChecksumOffload { - tcpHdr.SetChecksum(^tcpHdr.CalculateChecksum(header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), header.TCPMinimumSize))) - } - ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) - if PacketOffset > 0 { - PacketFillHeader(newPacket.ExtendHeader(PacketOffset), header.IPv4Version) - } else { - newPacket.Advance(-s.frontHeadroom) - } - return common.Error(s.tun.Write(newPacket.Bytes())) -} - func (s *System) processIPv6TCP(ipHdr header.IPv6, tcpHdr header.TCP) (bool, error) { source := netip.AddrPortFrom(ipHdr.SourceAddr(), tcpHdr.SourcePort()) destination := netip.AddrPortFrom(ipHdr.DestinationAddr(), tcpHdr.DestinationPort()) @@ -499,14 +506,7 @@ func (s *System) processIPv6TCP(ipHdr header.IPv6, tcpHdr header.TCP) (bool, err } } if !loopback { - natPort, err := s.tcpNat.Lookup(source, destination, s.handler) - if err != nil { - if errors.Is(err, ErrDrop) { - return false, nil - } else { - return false, s.resetIPv6TCP(ipHdr, tcpHdr) - } - } + natPort := s.tcpNat.Lookup(source, destination) ipHdr.SetSourceAddr(s.inet6NextAddress) tcpHdr.SetSourcePort(natPort) ipHdr.SetDestinationAddr(s.inet6Address) @@ -523,50 +523,6 @@ func (s *System) processIPv6TCP(ipHdr header.IPv6, tcpHdr header.TCP) (bool, err return true, nil } -func (s *System) resetIPv6TCP(origIPHdr header.IPv6, origTCPHdr header.TCP) error { - frontHeadroom := s.frontHeadroom + PacketOffset - newPacket := buf.NewSize(frontHeadroom + header.IPv6MinimumSize + header.TCPMinimumSize) - defer newPacket.Release() - newPacket.Resize(frontHeadroom, header.IPv6MinimumSize+header.TCPMinimumSize) - ipHdr := header.IPv6(newPacket.Bytes()) - ipHdr.Encode(&header.IPv6Fields{ - PayloadLength: uint16(header.TCPMinimumSize), - TransportProtocol: header.TCPProtocolNumber, - SrcAddr: origIPHdr.DestinationAddr(), - DstAddr: origIPHdr.SourceAddr(), - }) - tcpHdr := header.TCP(ipHdr.Payload()) - fields := header.TCPFields{ - SrcPort: origTCPHdr.DestinationPort(), - DstPort: origTCPHdr.SourcePort(), - DataOffset: header.TCPMinimumSize, - Flags: header.TCPFlagRst, - } - if origTCPHdr.Flags()&header.TCPFlagAck != 0 { - fields.SeqNum = origTCPHdr.AckNumber() - } else { - fields.Flags |= header.TCPFlagAck - ackNum := origTCPHdr.SequenceNumber() + uint32(len(origTCPHdr.Payload())) - if origTCPHdr.Flags()&header.TCPFlagSyn != 0 { - ackNum++ - } - if origTCPHdr.Flags()&header.TCPFlagFin != 0 { - ackNum++ - } - fields.AckNum = ackNum - } - tcpHdr.Encode(&fields) - if !s.txChecksumOffload { - tcpHdr.SetChecksum(^tcpHdr.CalculateChecksum(header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), header.TCPMinimumSize))) - } - if PacketOffset > 0 { - PacketFillHeader(newPacket.ExtendHeader(PacketOffset), header.IPv6Version) - } else { - newPacket.Advance(-s.frontHeadroom) - } - return common.Error(s.tun.Write(newPacket.Bytes())) -} - func (s *System) processIPv4UDP(ipHdr header.IPv4, udpHdr header.UDP) error { if ipHdr.Flags()&header.IPv4FlagMoreFragments != 0 { return E.New("ipv4: fragment dropped") @@ -594,19 +550,6 @@ func (s *System) processIPv6UDP(ipHdr header.IPv6, udpHdr header.UDP) error { } func (s *System) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { - _, pErr := s.handler.PrepareConnection(N.NetworkUDP, source, destination, nil, 0) - if pErr != nil { - if !errors.Is(pErr, ErrDrop) { - if source.IsIPv4() { - ipHdr := userData.(header.IPv4) - s.rejectIPv4WithICMP(ipHdr, header.ICMPv4PortUnreachable) - } else { - ipHdr := userData.(header.IPv6) - s.rejectIPv6WithICMP(ipHdr, header.ICMPv6PortUnreachable) - } - } - return false, nil, nil, nil - } var writer N.PacketWriter if source.IsIPv4() { packet := userData.(header.IPv4) @@ -640,29 +583,6 @@ func (s *System) processIPv4ICMP(ipHdr header.IPv4, icmpHdr header.ICMPv4) (bool if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != 0 { return false, nil } - sourceAddr := ipHdr.SourceAddr() - destinationAddr := ipHdr.DestinationAddr() - if destinationAddr != s.inet4Address { - action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { - return s.handler.PrepareConnection( - N.NetworkICMP, - M.SocksaddrFrom(sourceAddr, 0), - M.SocksaddrFrom(destinationAddr, 0), - &systemICMPDirectPacketWriter4{s.tun, s.frontHeadroom + PacketOffset, sourceAddr}, - timeout, - ) - }) - if err != nil { - if errors.Is(err, ErrReset) { - return false, s.rejectIPv4WithICMP(ipHdr, header.ICMPv4HostUnreachable) - } else if errors.Is(err, ErrDrop) { - return false, nil - } - } - if action != nil { - return false, action.WritePacket(buf.As(ipHdr).ToOwned()) - } - } icmpHdr.SetType(header.ICMPv4EchoReply) sourceAddress := ipHdr.SourceAddr() ipHdr.SetSourceAddr(ipHdr.DestinationAddr()) @@ -672,70 +592,10 @@ func (s *System) processIPv4ICMP(ipHdr header.IPv4, icmpHdr header.ICMPv4) (bool return true, nil } -func (s *System) rejectIPv4WithICMP(ipHdr header.IPv4, code header.ICMPv4Code) error { - frontHeadroom := s.frontHeadroom + PacketOffset - mtu := s.mtu - const maxIPData = header.IPv4MinimumProcessableDatagramSize - header.IPv4MinimumSize - if mtu > maxIPData { - mtu = maxIPData - } - available := mtu - header.ICMPv4MinimumSize - if available < len(ipHdr)+header.ICMPv4MinimumErrorPayloadSize { - return nil - } - payload := ipHdr - if len(payload) > available { - payload = payload[:available] - } - newPacket := buf.NewSize(frontHeadroom + header.IPv4MinimumSize + header.ICMPv4MinimumSize + len(payload)) - defer newPacket.Release() - newPacket.Resize(frontHeadroom, header.IPv4MinimumSize+header.ICMPv4MinimumSize+len(payload)) - newIPHdr := header.IPv4(newPacket.Bytes()) - newIPHdr.Encode(&header.IPv4Fields{ - TotalLength: uint16(newPacket.Len()), - Protocol: uint8(header.ICMPv4ProtocolNumber), - SrcAddr: ipHdr.DestinationAddr(), - DstAddr: ipHdr.SourceAddr(), - }) - newIPHdr.SetChecksum(^newIPHdr.CalculateChecksum()) - icmpHdr := header.ICMPv4(newIPHdr.Payload()) - icmpHdr.SetType(header.ICMPv4DstUnreachable) - icmpHdr.SetCode(code) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(ipHdr.Payload(), 0))) - copy(icmpHdr.Payload(), payload) - if PacketOffset > 0 { - newPacket.ExtendHeader(PacketOffset)[3] = syscall.AF_INET - } else { - newPacket.Advance(-s.frontHeadroom) - } - return common.Error(s.tun.Write(newPacket.Bytes())) -} - func (s *System) processIPv6ICMP(ipHdr header.IPv6, icmpHdr header.ICMPv6) (bool, error) { if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != 0 { return false, nil } - sourceAddr := ipHdr.SourceAddr() - destinationAddr := ipHdr.DestinationAddr() - if destinationAddr != s.inet6Address { - action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { - return s.handler.PrepareConnection( - N.NetworkICMP, - M.SocksaddrFrom(sourceAddr, 0), - M.SocksaddrFrom(destinationAddr, 0), - &systemICMPDirectPacketWriter6{s.tun, s.frontHeadroom + PacketOffset, sourceAddr}, - timeout, - ) - }) - if errors.Is(err, ErrReset) { - return false, s.rejectIPv6WithICMP(ipHdr, header.ICMPv6AddressUnreachable) - } else if errors.Is(err, ErrDrop) { - return false, nil - } - if action != nil { - return false, action.WritePacket(buf.As(ipHdr).ToOwned()) - } - } icmpHdr.SetType(header.ICMPv6EchoReply) sourceAddress := ipHdr.SourceAddr() ipHdr.SetSourceAddr(ipHdr.DestinationAddr()) @@ -748,50 +608,6 @@ func (s *System) processIPv6ICMP(ipHdr header.IPv6, icmpHdr header.ICMPv6) (bool return true, nil } -func (s *System) rejectIPv6WithICMP(ipHdr header.IPv6, code header.ICMPv6Code) error { - frontHeadroom := s.frontHeadroom + PacketOffset - mtu := s.mtu - const maxIPv6Data = header.IPv6MinimumMTU - header.IPv6FixedHeaderSize - if mtu > maxIPv6Data { - mtu = maxIPv6Data - } - available := mtu - header.ICMPv6ErrorHeaderSize - if available < header.IPv6MinimumSize { - return nil - } - payload := ipHdr - if len(payload) > available { - payload = payload[:available] - } - newPacket := buf.NewSize(frontHeadroom + header.IPv6MinimumSize + header.ICMPv6DstUnreachableMinimumSize + len(payload)) - defer newPacket.Release() - newPacket.Resize(frontHeadroom, header.IPv6MinimumSize+header.ICMPv6DstUnreachableMinimumSize+len(payload)) - newIPHdr := header.IPv6(newPacket.Bytes()) - newIPHdr.Encode(&header.IPv6Fields{ - PayloadLength: uint16(header.ICMPv6DstUnreachableMinimumSize + len(payload)), - TransportProtocol: header.ICMPv6ProtocolNumber, - SrcAddr: ipHdr.DestinationAddr(), - DstAddr: ipHdr.SourceAddr(), - }) - icmpHdr := header.ICMPv6(newIPHdr.Payload()) - icmpHdr.SetType(header.ICMPv6DstUnreachable) - icmpHdr.SetCode(code) - icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr[:header.ICMPv6DstUnreachableMinimumSize], - Src: newIPHdr.SourceAddressSlice(), - Dst: newIPHdr.DestinationAddressSlice(), - PayloadCsum: checksum.Checksum(payload, 0), - PayloadLen: len(payload), - })) - copy(icmpHdr.Payload(), payload) - if PacketOffset > 0 { - PacketFillHeader(newPacket.ExtendHeader(PacketOffset), header.IPv6Version) - } else { - newPacket.Advance(-s.frontHeadroom) - } - return common.Error(s.tun.Write(newPacket.Bytes())) -} - type systemUDPPacketWriter4 struct { tun Tun frontHeadroom int @@ -868,45 +684,37 @@ func (w *systemUDPPacketWriter6) WritePacket(buffer *buf.Buffer, destination M.S return common.Error(w.tun.Write(newPacket.Bytes())) } -type systemICMPDirectPacketWriter4 struct { +type systemWriteback struct { tun Tun + linuxTUN LinuxTUN frontHeadroom int - source netip.Addr } -func (w *systemICMPDirectPacketWriter4) WritePacket(p []byte) error { - newPacket := buf.NewSize(w.frontHeadroom + len(p)) - defer newPacket.Release() - newPacket.Resize(w.frontHeadroom, 0) - newPacket.Write(p) - ipHdr := header.IPv4(newPacket.Bytes()) - ipHdr.SetDestinationAddr(w.source) - ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) - if PacketOffset > 0 { - PacketFillHeader(newPacket.ExtendHeader(PacketOffset), header.IPv4Version) - } else { - newPacket.Advance(-w.frontHeadroom) +func newSystemWriteback(tunInterface Tun, frontHeadroom int) *systemWriteback { + writeback := &systemWriteback{tun: tunInterface, frontHeadroom: frontHeadroom} + if linuxTUN, isLinuxTUN := tunInterface.(LinuxTUN); isLinuxTUN { + writeback.linuxTUN = linuxTUN } - return common.Error(w.tun.Write(newPacket.Bytes())) + return writeback } -type systemICMPDirectPacketWriter6 struct { - tun Tun - frontHeadroom int - source netip.Addr +func (w *systemWriteback) ReturnHeadroom() int { + return w.frontHeadroom + PacketOffset } -func (w *systemICMPDirectPacketWriter6) WritePacket(p []byte) error { - newPacket := buf.NewSize(w.frontHeadroom + len(p)) - defer newPacket.Release() - newPacket.Resize(w.frontHeadroom, 0) - newPacket.Write(p) - ipHdr := header.IPv6(newPacket.Bytes()) - ipHdr.SetDestinationAddr(w.source) - if PacketOffset > 0 { - PacketFillHeader(newPacket.ExtendHeader(PacketOffset), header.IPv6Version) - } else { - newPacket.Advance(-w.frontHeadroom) +func (w *systemWriteback) WriteReturnPackets(packets [][]byte) error { + if w.linuxTUN != nil { + return common.Error(w.linuxTUN.BatchWrite(packets, w.frontHeadroom)) } - return common.Error(w.tun.Write(newPacket.Bytes())) + var writeErrors []error + for _, packet := range packets { + if PacketOffset > 0 { + PacketFillHeader(packet, header.IPVersion(packet[PacketOffset:])) + } + _, err := w.tun.Write(packet) + if err != nil { + writeErrors = append(writeErrors, err) + } + } + return E.Errors(writeErrors...) } diff --git a/stack_system_nat.go b/stack_system_nat.go index cc460171..fd0e3823 100644 --- a/stack_system_nat.go +++ b/stack_system_nat.go @@ -5,9 +5,6 @@ import ( "net/netip" "sync" "time" - - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" ) type TCPNat struct { @@ -85,17 +82,13 @@ func (n *TCPNat) LookupBack(port uint16) *TCPSession { return session } -func (n *TCPNat) Lookup(source netip.AddrPort, destination netip.AddrPort, handler Handler) (uint16, error) { +func (n *TCPNat) Lookup(source netip.AddrPort, destination netip.AddrPort) uint16 { key := tcpNatKey{Source: source, Destination: destination} n.addrAccess.RLock() port, loaded := n.addrMap[key] n.addrAccess.RUnlock() if loaded { - return port, nil - } - _, pErr := handler.PrepareConnection(N.NetworkTCP, M.SocksaddrFromNetIP(source), M.SocksaddrFromNetIP(destination), nil, 0) - if pErr != nil { - return 0, pErr + return port } n.addrAccess.Lock() nextPort := n.portIndex @@ -114,5 +107,5 @@ func (n *TCPNat) Lookup(source netip.AddrPort, destination netip.AddrPort, handl LastActive: time.Now(), } n.portAccess.Unlock() - return nextPort, nil + return nextPort } diff --git a/tun.go b/tun.go index abfe67fa..eda669a2 100644 --- a/tun.go +++ b/tun.go @@ -7,7 +7,6 @@ import ( "runtime" "strconv" "strings" - "time" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" @@ -15,27 +14,16 @@ import ( E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/logger" - M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ranges" ) type Handler interface { - PrepareConnection( - network string, - source M.Socksaddr, - destination M.Socksaddr, - routeContext DirectRouteContext, - timeout time.Duration, - ) (DirectRouteDestination, error) + JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort) FlowVerdict N.TCPConnectionHandlerEx N.UDPConnectionHandlerEx } -type DirectRouteContext interface { - WritePacket(packet []byte) error -} - type Tun interface { io.ReadWriter Name() (string, error) diff --git a/tun_linux.go b/tun_linux.go index bc553fa9..c35a800f 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -450,8 +450,10 @@ func (t *NativeTun) BatchRead(buffers [][]byte, offset int, readN []int) (n int, func (t *NativeTun) BatchWrite(buffers [][]byte, offset int) (int, error) { t.writeAccess.Lock() defer func() { - t.tcpGROTable.reset() - t.udpGROTable.reset() + if t.vnetHdr { + t.tcpGROTable.reset() + t.udpGROTable.reset() + } t.writeAccess.Unlock() }() var ( From b017e6dd0d71e4e49f3947f3849f7b5d70d0a32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 6 Jul 2026 16:55:25 +0800 Subject: [PATCH 11/24] Fix gVisor process --- stack_gvisor_udp.go | 2 +- stack_mixed.go | 2 +- stack_system.go | 2 +- tun_darwin_gvisor.go | 13 +++++++------ tun_linux_gvisor.go | 22 ++++++++++++---------- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/stack_gvisor_udp.go b/stack_gvisor_udp.go index 3ad027ec..fb31ba9d 100644 --- a/stack_gvisor_udp.go +++ b/stack_gvisor_udp.go @@ -37,7 +37,7 @@ func NewUDPForwarder(ctx context.Context, stack *stack.Stack, handler Handler, t stack: stack, handler: handler, } - forwarder.udpNat = udpnat.New(handler, forwarder.PreparePacketConnection, timeout, true) + forwarder.udpNat = udpnat.New(handler, forwarder.PreparePacketConnection, timeout, false) return forwarder } diff --git a/stack_mixed.go b/stack_mixed.go index 4f9242b9..46803807 100644 --- a/stack_mixed.go +++ b/stack_mixed.go @@ -130,7 +130,7 @@ func (m *Mixed) wintunLoop(winTun WinTun) { func (m *Mixed) batchLoopLinux(linuxTUN LinuxTUN, batchSize int) { packetBuffers := make([][]byte, batchSize) - writeBuffers := make([][]byte, batchSize) + writeBuffers := make([][]byte, 0, batchSize) packetSizes := make([]int, batchSize) for i := range packetBuffers { packetBuffers[i] = make([]byte, m.mtu+PacketOffset+m.frontHeadroom) diff --git a/stack_system.go b/stack_system.go index be9cb33b..8ab9be67 100644 --- a/stack_system.go +++ b/stack_system.go @@ -237,7 +237,7 @@ func (s *System) wintunLoop(winTun WinTun) { func (s *System) batchLoopLinux(linuxTUN LinuxTUN, batchSize int) { packetBuffers := make([][]byte, batchSize) - writeBuffers := make([][]byte, batchSize) + writeBuffers := make([][]byte, 0, batchSize) packetSizes := make([]int, batchSize) for i := range packetBuffers { packetBuffers[i] = make([]byte, s.mtu+s.frontHeadroom) diff --git a/tun_darwin_gvisor.go b/tun_darwin_gvisor.go index ef940c3d..b506db40 100644 --- a/tun_darwin_gvisor.go +++ b/tun_darwin_gvisor.go @@ -43,12 +43,13 @@ func (t *NativeTun) WritePacket(pkt *stack.PacketBuffer) (int, error) { func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) { ep, err := fdbased.New(&fdbased.Options{ - FDs: []int{t.tunFd}, - MTU: t.options.MTU, - RXChecksumOffload: true, - PacketDispatchMode: fdbased.RecvMMsg, - MultiPendingPackets: t.options.EXP_MultiPendingPackets, - SendMsgX: t.options.EXP_SendMsgX, + FDs: []int{t.tunFd}, + MTU: t.options.MTU, + ProcessorsPerChannel: 1, + RXChecksumOffload: true, + PacketDispatchMode: fdbased.RecvMMsg, + MultiPendingPackets: t.options.EXP_MultiPendingPackets, + SendMsgX: t.options.EXP_SendMsgX, }) if err != nil { return nil, stack.NICOptions{}, err diff --git a/tun_linux_gvisor.go b/tun_linux_gvisor.go index 8adf4c5d..ac4a76e7 100644 --- a/tun_linux_gvisor.go +++ b/tun_linux_gvisor.go @@ -74,12 +74,13 @@ func (t *NativeTun) WritePacket(pkt *stack.PacketBuffer) (int, error) { func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) { if t.vnetHdr { ep, err := fdbased.New(&fdbased.Options{ - FDs: []int{t.tunFd}, - MTU: t.options.MTU, - GSOMaxSize: gsoMaxSize, - GRO: true, - RXChecksumOffload: true, - TXChecksumOffload: t.txChecksumOffload, + FDs: []int{t.tunFd}, + MTU: t.options.MTU, + ProcessorsPerChannel: 1, + GSOMaxSize: gsoMaxSize, + GRO: true, + RXChecksumOffload: true, + TXChecksumOffload: t.txChecksumOffload, }) if err != nil { return nil, stack.NICOptions{}, err @@ -87,10 +88,11 @@ func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) return ep, stack.NICOptions{}, nil } else { ep, err := fdbased.New(&fdbased.Options{ - FDs: []int{t.tunFd}, - MTU: t.options.MTU, - RXChecksumOffload: true, - TXChecksumOffload: t.txChecksumOffload, + FDs: []int{t.tunFd}, + MTU: t.options.MTU, + ProcessorsPerChannel: 1, + RXChecksumOffload: true, + TXChecksumOffload: t.txChecksumOffload, }) if err != nil { return nil, stack.NICOptions{}, err From 4412996bf54c975bbde7d611f4198af11fac3469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 6 Jul 2026 21:05:21 +0800 Subject: [PATCH 12/24] Drain pending packets in Linux batch read --- tun_linux.go | 108 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 98 insertions(+), 10 deletions(-) diff --git a/tun_linux.go b/tun_linux.go index c35a800f..c7557bf4 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -39,6 +39,9 @@ type NativeTun struct { writeAccess sync.Mutex vnetHdr bool writeBuffer []byte + readRawConn syscall.RawConn + pendingBuffer []byte + pendingLength int vnetHdrWriteBuf []byte gsoToWrite []int tcpGROTable *tcpGROTable @@ -192,6 +195,11 @@ func (t *NativeTun) enableGSO() error { } t.vnetHdr = true t.writeBuffer = make([]byte, virtioNetHdrLen+int(gsoMaxSize)) + t.pendingBuffer = make([]byte, virtioNetHdrLen+int(gsoMaxSize)) + t.readRawConn, err = t.tunFile.SyscallConn() + if err != nil { + return E.Cause(err, "enable offload: get raw conn") + } t.tcpGROTable = newTCPGROTable() t.udpGROTable = newUDPGROTable() err = setUDPOffload(t.tunFd) @@ -371,16 +379,24 @@ func (t *NativeTun) Read(p []byte) (n int, err error) { // each buffer. It mutates sizes to reflect the size of each element of bufs, // and returns the number of packets read. func handleVirtioRead(in []byte, bufs [][]byte, sizes []int, offset int) (int, error) { + payload, options, err := parseVirtioRead(in) + if err != nil { + return 0, err + } + return GSOSplit(payload, options, bufs, sizes, offset) +} + +func parseVirtioRead(in []byte) ([]byte, GSOOptions, error) { var hdr virtioNetHdr err := hdr.decode(in) if err != nil { - return 0, err + return nil, GSOOptions{}, err } in = in[virtioNetHdrLen:] options, err := hdr.toGSOOptions() if err != nil { - return 0, err + return nil, GSOOptions{}, err } // Don't trust HdrLen from the kernel as it can be equal to the length @@ -391,18 +407,26 @@ func handleVirtioRead(in []byte, bufs [][]byte, sizes []int, offset int) (int, e options.HdrLen = options.CsumStart + 8 } else if options.GSOType != GSONone { if len(in) <= int(options.CsumStart+12) { - return 0, errors.New("packet is too short") + return nil, GSOOptions{}, errors.New("packet is too short") } tcpHLen := uint16(in[options.CsumStart+12] >> 4 * 4) if tcpHLen < 20 || tcpHLen > 60 { // A TCP header must be between 20 and 60 bytes in length. - return 0, fmt.Errorf("tcp header len is invalid: %d", tcpHLen) + return nil, GSOOptions{}, fmt.Errorf("tcp header len is invalid: %d", tcpHLen) } options.HdrLen = options.CsumStart + tcpHLen } - return GSOSplit(in, options, bufs, sizes, offset) + return in, options, nil +} + +func gsoSegmentCount(payload []byte, options GSOOptions) int { + dataLength := len(payload) - int(options.HdrLen) + if options.GSOType == GSONone || options.GSOSize == 0 || dataLength < int(options.GSOSize) { + return 1 + } + return (dataLength + int(options.GSOSize) - 1) / int(options.GSOSize) } func (t *NativeTun) Write(p []byte) (n int, err error) { @@ -437,14 +461,78 @@ func (t *NativeTun) BatchSize() int { return idealBatchSize } -func (t *NativeTun) BatchRead(buffers [][]byte, offset int, readN []int) (n int, err error) { +func (t *NativeTun) BatchRead(buffers [][]byte, offset int, readN []int) (int, error) { t.readAccess.Lock() defer t.readAccess.Unlock() - n, err = t.tunFile.Read(t.writeBuffer) - if err != nil { - return + var used int + if t.pendingLength > 0 { + pendingLength := t.pendingLength + t.pendingLength = 0 + count, err := handleVirtioRead(t.pendingBuffer[:pendingLength], buffers, readN, offset) + if err != nil { + return count, err + } + used = count + } + for used < len(buffers) { + var ( + readLength int + err error + ) + if used == 0 { + readLength, err = t.tunFile.Read(t.writeBuffer) + if err != nil { + return 0, err + } + } else { + readLength, err = t.readNonblocking(t.writeBuffer) + if err != nil || readLength == 0 { + break + } + } + payload, options, parseErr := parseVirtioRead(t.writeBuffer[:readLength]) + if parseErr != nil { + if used > 0 { + break + } + return 0, parseErr + } + if used > 0 && gsoSegmentCount(payload, options) > len(buffers)-used { + t.writeBuffer, t.pendingBuffer = t.pendingBuffer, t.writeBuffer + t.pendingLength = readLength + break + } + count, splitErr := GSOSplit(payload, options, buffers[used:], readN[used:], offset) + if splitErr != nil { + if used > 0 { + break + } + return count, splitErr + } + used += count + } + return used, nil +} + +func (t *NativeTun) readNonblocking(buffer []byte) (int, error) { + var ( + readLength int + readErr error + ) + controlErr := t.readRawConn.Read(func(fd uintptr) bool { + readLength, readErr = syscall.Read(int(fd), buffer) + return true + }) + if controlErr != nil { + return 0, controlErr + } + if readErr != nil { + if readErr == syscall.EAGAIN { + return 0, nil + } + return 0, readErr } - return handleVirtioRead(t.writeBuffer[:n], buffers, readN, offset) + return readLength, nil } func (t *NativeTun) BatchWrite(buffers [][]byte, offset int) (int, error) { From 321ffeca7bb8918845d6787051c130b2f584623a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 6 Jul 2026 23:37:38 +0800 Subject: [PATCH 13/24] Add flow tracking --- flow.go | 43 +++++++++++++ flow_dispatch.go | 72 +++++++++++++++++---- nfqueue_linux.go | 1 + stack_gvisor_icmp.go | 148 +++++++++++++++++++++++++++++++++++-------- stack_gvisor_tcp.go | 2 +- stack_gvisor_udp.go | 11 +++- tun.go | 2 +- 7 files changed, 240 insertions(+), 39 deletions(-) diff --git a/flow.go b/flow.go index a920e93e..db2cd1d8 100644 --- a/flow.go +++ b/flow.go @@ -6,6 +6,7 @@ type FlowVerdict struct { Action FlowAction Port Port Destination netip.AddrPort + NewTracker func() FlowTracker } type FlowAction uint8 @@ -18,6 +19,48 @@ const ( ActionBypass ) +type FlowTracker interface { + AttachFlow(handle FlowHandle) + CountForward(n int) + CountReverse(n int) + FlowEstablished() + CloseFlow(reason FlowCloseReason) +} + +type FlowHandle interface { + CloseFlow() +} + +type FlowCloseReason uint8 + +const ( + FlowCloseReset FlowCloseReason = iota + FlowCloseFinished + FlowCloseTimeout + FlowCloseEvicted + FlowCloseShutdown + FlowCloseInterrupted +) + +func (r FlowCloseReason) String() string { + switch r { + case FlowCloseReset: + return "connection reset" + case FlowCloseFinished: + return "finished" + case FlowCloseTimeout: + return "idle timeout" + case FlowCloseEvicted: + return "evicted" + case FlowCloseShutdown: + return "stack closed" + case FlowCloseInterrupted: + return "interrupted" + default: + return "unknown" + } +} + type Port interface { PortAddresses() (v4 netip.Addr, v6 netip.Addr) PortMTU() uint32 diff --git a/flow_dispatch.go b/flow_dispatch.go index d9ec9a31..b70dc1f3 100644 --- a/flow_dispatch.go +++ b/flow_dispatch.go @@ -14,6 +14,7 @@ import ( const ( tcpEstablishedTimeout = 2*time.Hour + 4*time.Minute tcpTransitoryTimeout = 4 * time.Minute + tcpClosingTimeout = 10 * time.Second defaultUDPTimeout = 5 * time.Minute @@ -46,6 +47,7 @@ type forwardFlow struct { reverseRule rewriteRule effectiveMTU uint32 protocol uint8 + tracker FlowTracker clientAddress netip.Addr clientSelector uint16 @@ -62,14 +64,29 @@ type forwardFlow struct { lastReverse atomic.Int64 } +func (f *forwardFlow) close(reason FlowCloseReason) { + if !f.closed.CompareAndSwap(false, true) { + return + } + if f.tracker != nil { + f.tracker.CloseFlow(reason) + } +} + +func (f *forwardFlow) CloseFlow() { + f.close(FlowCloseInterrupted) +} + func (f *forwardFlow) observeReverse(packet *forwardPacket, now int64) { f.lastReverse.Store(now) if packet.protocol != uint8(header.TCPProtocolNumber) { return } - f.established.Store(true) + if f.established.CompareAndSwap(false, true) && f.tracker != nil { + f.tracker.FlowEstablished() + } if packet.tcpFlags&header.TCPFlagRst != 0 { - f.closed.Store(true) + f.close(FlowCloseReset) return } if packet.tcpFlags&header.TCPFlagFin != 0 { @@ -128,6 +145,11 @@ func (d *ForwardDispatcher) Close() { return } d.returnPath.closed.Store(true) + for _, entry := range d.table { + if entry.flow != nil { + entry.flow.close(FlowCloseShutdown) + } + } for port, nat := range d.ports { if nat != nil { port.DetachReturn(&d.returnPath) @@ -147,7 +169,7 @@ func (d *ForwardDispatcher) Dispatch(packet []byte) bool { now := d.now() entry, loaded := d.table[key] if loaded && d.entryExpired(entry, now) { - d.removeEntry(key, entry) + d.removeEntry(key, entry, FlowCloseTimeout) loaded = false } if loaded { @@ -171,7 +193,7 @@ func (d *ForwardDispatcher) handleHit(key flowKey, entry *flowEntry, packet *for if packet.protocol == uint8(header.TCPProtocolNumber) { if packet.tcpFlags&header.TCPFlagRst != 0 { d.forwardToPort(flow, packet, raw) - flow.closed.Store(true) + flow.close(FlowCloseReset) d.tombstoneEntry(entry, now) return true } @@ -186,7 +208,7 @@ func (d *ForwardDispatcher) handleHit(key flowKey, entry *flowEntry, packet *for case ActionAccept: entry.deadline = now + int64(entry.idle) if packet.protocol == uint8(header.TCPProtocolNumber) && packet.tcpFlags&header.TCPFlagRst != 0 { - d.removeEntry(key, entry) + d.removeEntry(key, entry, FlowCloseReset) } return false case ActionReject: @@ -200,7 +222,11 @@ func (d *ForwardDispatcher) handleHit(key flowKey, entry *flowEntry, packet *for } func (d *ForwardDispatcher) judgeAndInstall(key flowKey, packet *forwardPacket, raw []byte, now int64) bool { - verdict := d.handler.JudgeFlow(packet.protocol, packet.source, packet.destination) + var firstPacket []byte + if packet.protocol == uint8(header.UDPProtocolNumber) { + firstPacket = header.UDP(packet.transport).Payload() + } + verdict := d.handler.JudgeFlow(packet.protocol, packet.source, packet.destination, firstPacket) switch verdict.Action { case ActionFlow: if verdict.Port != nil { @@ -249,6 +275,9 @@ func (d *ForwardDispatcher) idleTimeout(protocol uint8, established bool) time.D } func (d *ForwardDispatcher) flowIdle(flow *forwardFlow) time.Duration { + if flow.protocol == uint8(header.TCPProtocolNumber) && flow.finForward && flow.finReverse.Load() { + return tcpClosingTimeout + } established := flow.established.Load() && !flow.finForward && !flow.finReverse.Load() return d.idleTimeout(flow.protocol, established) } @@ -324,6 +353,12 @@ func (d *ForwardDispatcher) createFlow(packet *forwardPacket, verdict FlowVerdic flow.reverseRule.sourcePort = clientDestinationPort flow.reverseRule.rewriteSourcePort = true } + if verdict.NewTracker != nil { + flow.tracker = verdict.NewTracker() + if flow.tracker != nil { + flow.tracker.AttachFlow(flow) + } + } nat.insert(reverseKey, flow) return flow, true } @@ -354,6 +389,9 @@ func (d *ForwardDispatcher) natFor(port Port) *portNAT { func (d *ForwardDispatcher) forwardToPort(flow *forwardFlow, packet *forwardPacket, raw []byte) { if flow.effectiveMTU != 0 && uint32(len(raw)) > flow.effectiveMTU { if packet.protocol == uint8(header.TCPProtocolNumber) { + if flow.tracker != nil { + flow.tracker.CountForward(len(raw)) + } d.rewriteForward(flow, packet) d.resegmentTCP(flow, packet, raw) return @@ -361,6 +399,9 @@ func (d *ForwardDispatcher) forwardToPort(flow *forwardFlow, packet *forwardPack if packet.ipVersion == 4 { ipHdr := packet.network.(header.IPv4) if ipHdr.Flags()&header.IPv4FlagDontFragment == 0 { + if flow.tracker != nil { + flow.tracker.CountForward(len(raw)) + } d.rewriteForward(flow, packet) fragments, ok := fragmentIPv4Packet(ipHdr, flow.effectiveMTU) if ok { @@ -382,6 +423,9 @@ func (d *ForwardDispatcher) forwardToPort(flow *forwardFlow, packet *forwardPack } return } + if flow.tracker != nil { + flow.tracker.CountForward(len(raw)) + } d.rewriteForward(flow, packet) d.stagePort(flow.nat, raw) } @@ -460,10 +504,13 @@ func (d *ForwardDispatcher) tombstoneEntry(entry *flowEntry, now int64) { entry.deadline = now + int64(entry.idle) } -func (d *ForwardDispatcher) removeEntry(key flowKey, entry *flowEntry) { +func (d *ForwardDispatcher) removeEntry(key flowKey, entry *flowEntry, reason FlowCloseReason) { delete(d.table, key) if entry.flow != nil { - entry.flow.closed.Store(true) + if reason == FlowCloseTimeout && entry.flow.finForward && entry.flow.finReverse.Load() { + reason = FlowCloseFinished + } + entry.flow.close(reason) entry.flow.nat.delete(entry.flow.reverseKey) } } @@ -484,7 +531,7 @@ func (d *ForwardDispatcher) evictEntries(now int64) { ) for key, entry := range d.table { if d.entryExpired(entry, now) { - d.removeEntry(key, entry) + d.removeEntry(key, entry, FlowCloseTimeout) freed++ } else if oldest == nil || entry.deadline < oldest.deadline { oldestKey = key @@ -496,7 +543,7 @@ func (d *ForwardDispatcher) evictEntries(now int64) { } } if freed == 0 && oldest != nil { - d.removeEntry(oldestKey, oldest) + d.removeEntry(oldestKey, oldest, FlowCloseEvicted) } } @@ -510,7 +557,7 @@ func (d *ForwardDispatcher) maybeSweep(now int64) { if entry.action == ActionFlow && entry.flow.closed.Load() { d.tombstoneEntry(entry, now) } else if d.entryExpired(entry, now) { - d.removeEntry(key, entry) + d.removeEntry(key, entry, FlowCloseTimeout) } visited++ if visited >= flowSweepLimit { @@ -579,6 +626,9 @@ func (r *forwardReturn) ReturnPackets(packets [][]byte) [][]byte { if flow.closed.Load() { continue } + if flow.tracker != nil { + flow.tracker.CountReverse(len(raw) - headroom) + } flow.observeReverse(&parsed, now) if parsed.isTCPSyn() { applyRewriteRaw(&parsed, &flow.reverseRule) diff --git a/nfqueue_linux.go b/nfqueue_linux.go index c6593e44..64e73907 100644 --- a/nfqueue_linux.go +++ b/nfqueue_linux.go @@ -213,6 +213,7 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int { uint8(unix.IPPROTO_TCP), netip.AddrPortFrom(sourceAddr, tcp.SourcePort()), netip.AddrPortFrom(destinationAddr, tcp.DestinationPort()), + nil, ) // Use NfRepeat for bypass/reset so the packet re-enters the chain diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index 979f4ad9..96466c16 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -28,7 +28,7 @@ type ICMPForwarder struct { returnPath icmpForwarderReturn flowAccess sync.Mutex - flows map[icmpFlowKey]time.Time + flows map[icmpFlowKey]*icmpFlow lastSweep time.Time attachedPorts map[Port]bool } @@ -40,12 +40,32 @@ type icmpFlowKey struct { identifier uint16 } +type icmpFlow struct { + port Port + tracker FlowTracker + deadline time.Time + closed atomic.Bool +} + +func (f *icmpFlow) close(reason FlowCloseReason) { + if !f.closed.CompareAndSwap(false, true) { + return + } + if f.tracker != nil { + f.tracker.CloseFlow(reason) + } +} + +func (f *icmpFlow) CloseFlow() { + f.close(FlowCloseInterrupted) +} + func NewICMPForwarder(stack *stack.Stack, handler Handler, logger logger.Logger) *ICMPForwarder { forwarder := &ICMPForwarder{ stack: stack, handler: handler, logger: logger, - flows: make(map[icmpFlowKey]time.Time), + flows: make(map[icmpFlowKey]*icmpFlow), attachedPorts: make(map[Port]bool), } forwarder.returnPath.forwarder = forwarder @@ -56,6 +76,10 @@ func (f *ICMPForwarder) Close() error { f.returnPath.closed.Store(true) f.flowAccess.Lock() defer f.flowAccess.Unlock() + for key, flow := range f.flows { + flow.close(FlowCloseShutdown) + delete(f.flows, key) + } for port := range f.attachedPorts { port.DetachReturn(&f.returnPath) delete(f.attachedPorts, port) @@ -71,16 +95,25 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa return false } identifier := icmpHdr.Ident() + key := icmpFlowKey{ + source: AddrFromAddress(ipHdr.SourceAddress()), + destination: AddrFromAddress(ipHdr.DestinationAddress()), + identifier: identifier, + } + if f.forwardCached(key, pkt) { + return true + } verdict := f.handler.JudgeFlow( uint8(header.ICMPv4ProtocolNumber), - netip.AddrPortFrom(AddrFromAddress(ipHdr.SourceAddress()), identifier), - netip.AddrPortFrom(AddrFromAddress(ipHdr.DestinationAddress()), identifier), + netip.AddrPortFrom(key.source, identifier), + netip.AddrPortFrom(key.destination, identifier), + nil, ) switch verdict.Action { case ActionReject, ActionDrop: return true case ActionFlow: - if f.forwardFlow(verdict.Port, false, AddrFromAddress(ipHdr.SourceAddress()), AddrFromAddress(ipHdr.DestinationAddress()), identifier, pkt) { + if f.installFlow(key, verdict, pkt) { return true } } @@ -117,16 +150,26 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa return false } identifier := icmpHdr.Ident() + key := icmpFlowKey{ + v6: true, + source: AddrFromAddress(ipHdr.SourceAddress()), + destination: AddrFromAddress(ipHdr.DestinationAddress()), + identifier: identifier, + } + if f.forwardCached(key, pkt) { + return true + } verdict := f.handler.JudgeFlow( uint8(header.ICMPv6ProtocolNumber), - netip.AddrPortFrom(AddrFromAddress(ipHdr.SourceAddress()), identifier), - netip.AddrPortFrom(AddrFromAddress(ipHdr.DestinationAddress()), identifier), + netip.AddrPortFrom(key.source, identifier), + netip.AddrPortFrom(key.destination, identifier), + nil, ) switch verdict.Action { case ActionReject, ActionDrop: return true case ActionFlow: - if f.forwardFlow(verdict.Port, true, AddrFromAddress(ipHdr.SourceAddress()), AddrFromAddress(ipHdr.DestinationAddress()), identifier, pkt) { + if f.installFlow(key, verdict, pkt) { return true } } @@ -163,13 +206,38 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa } } -func (f *ICMPForwarder) forwardFlow(port Port, v6 bool, source netip.Addr, destination netip.Addr, identifier uint16, pkt *stack.PacketBuffer) bool { +func (f *ICMPForwarder) forwardCached(key icmpFlowKey, pkt *stack.PacketBuffer) bool { + now := time.Now() + f.flowAccess.Lock() + flow, loaded := f.flows[key] + if loaded { + if flow.closed.Load() { + delete(f.flows, key) + loaded = false + } else if now.After(flow.deadline) { + delete(f.flows, key) + flow.close(FlowCloseTimeout) + loaded = false + } else { + flow.deadline = now.Add(defaultICMPTimeout) + } + } + f.flowAccess.Unlock() + if !loaded { + return false + } + f.writeToPort(flow, pkt) + return true +} + +func (f *ICMPForwarder) installFlow(key icmpFlowKey, verdict FlowVerdict, pkt *stack.PacketBuffer) bool { + port := verdict.Port if port == nil { return false } inet4Address, inet6Address := port.PortAddresses() portAddress := inet4Address - if v6 { + if key.v6 { portAddress = inet6Address } if !portAddress.IsValid() || !portAddress.IsUnspecified() { @@ -188,14 +256,29 @@ func (f *ICMPForwarder) forwardFlow(port Port, v6 bool, source netip.Addr, desti now := time.Now() if now.Sub(f.lastSweep) >= defaultICMPTimeout { f.lastSweep = now - for key, deadline := range f.flows { - if now.After(deadline) { - delete(f.flows, key) + for flowKey, cachedFlow := range f.flows { + if cachedFlow.closed.Load() { + delete(f.flows, flowKey) + } else if now.After(cachedFlow.deadline) { + delete(f.flows, flowKey) + cachedFlow.close(FlowCloseTimeout) } } } - f.flows[icmpFlowKey{v6: v6, source: source, destination: destination, identifier: identifier}] = now.Add(defaultICMPTimeout) + flow := &icmpFlow{port: port, deadline: now.Add(defaultICMPTimeout)} + if verdict.NewTracker != nil { + flow.tracker = verdict.NewTracker() + } + f.flows[key] = flow f.flowAccess.Unlock() + if flow.tracker != nil { + flow.tracker.AttachFlow(flow) + } + f.writeToPort(flow, pkt) + return true +} + +func (f *ICMPForwarder) writeToPort(flow *icmpFlow, pkt *stack.PacketBuffer) { networkSlice := pkt.NetworkHeader().Slice() transportSlice := pkt.TransportHeader().Slice() dataSlice := pkt.Data().AsRange().ToSlice() @@ -203,27 +286,34 @@ func (f *ICMPForwarder) forwardFlow(port Port, v6 bool, source netip.Addr, desti packetSlice = append(packetSlice, networkSlice...) packetSlice = append(packetSlice, transportSlice...) packetSlice = append(packetSlice, dataSlice...) - err := port.WritePackets([][]byte{packetSlice}) + if flow.tracker != nil { + flow.tracker.CountForward(len(packetSlice)) + } + err := flow.port.WritePackets([][]byte{packetSlice}) if err != nil { f.logger.Trace(E.Cause(err, "forward ICMP packet")) } - return true } -func (f *ICMPForwarder) lookupFlow(key icmpFlowKey) bool { +func (f *ICMPForwarder) lookupFlow(key icmpFlowKey) *icmpFlow { f.flowAccess.Lock() defer f.flowAccess.Unlock() - deadline, loaded := f.flows[key] + flow, loaded := f.flows[key] if !loaded { - return false + return nil + } + if flow.closed.Load() { + delete(f.flows, key) + return nil } now := time.Now() - if now.After(deadline) { + if now.After(flow.deadline) { delete(f.flows, key) - return false + flow.close(FlowCloseTimeout) + return nil } - f.flows[key] = now.Add(defaultICMPTimeout) - return true + flow.deadline = now.Add(defaultICMPTimeout) + return flow } type icmpForwarderReturn struct { @@ -289,9 +379,13 @@ func (f *ICMPForwarder) returnPacket(packet []byte) bool { default: return false } - if !f.lookupFlow(key) { + flow := f.lookupFlow(key) + if flow == nil { return false } + if flow.tracker != nil { + flow.tracker.CountReverse(len(packet)) + } return f.writeBack(packet, header.IPv4ProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress()) case header.IPv6Version: ipHdr := header.IPv6(packet) @@ -308,9 +402,13 @@ func (f *ICMPForwarder) returnPacket(packet []byte) bool { destination: AddrFromAddress(ipHdr.SourceAddress()), identifier: icmpHdr.Ident(), } - if !f.lookupFlow(key) { + flow := f.lookupFlow(key) + if flow == nil { return false } + if flow.tracker != nil { + flow.tracker.CountReverse(len(packet)) + } return f.writeBack(packet, header.IPv6ProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress()) default: return false diff --git a/stack_gvisor_tcp.go b/stack_gvisor_tcp.go index 371c480b..f432e287 100644 --- a/stack_gvisor_tcp.go +++ b/stack_gvisor_tcp.go @@ -77,7 +77,7 @@ func (f *TCPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac func (f *TCPForwarder) Forward(r *tcp.ForwarderRequest) { source := M.SocksaddrFrom(AddrFromAddress(r.ID().RemoteAddress), r.ID().RemotePort) destination := M.SocksaddrFrom(AddrFromAddress(r.ID().LocalAddress), r.ID().LocalPort) - switch f.handler.JudgeFlow(uint8(header.TCPProtocolNumber), source.AddrPort(), destination.AddrPort()).Action { + switch f.handler.JudgeFlow(uint8(header.TCPProtocolNumber), source.AddrPort(), destination.AddrPort(), nil).Action { case ActionReject: r.Complete(true) return diff --git a/stack_gvisor_udp.go b/stack_gvisor_udp.go index fb31ba9d..3e4afddb 100644 --- a/stack_gvisor_udp.go +++ b/stack_gvisor_udp.go @@ -57,7 +57,16 @@ func (f *UDPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac func rangeIterate(r stack.Range, fn func(*buffer.View)) func (f *UDPForwarder) PreparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { - switch f.handler.JudgeFlow(uint8(header.UDPProtocolNumber), source.AddrPort(), destination.AddrPort()).Action { + firstPacketBuffer := userData.(*stack.PacketBuffer) + var firstPacket []byte + rangeIterate(firstPacketBuffer.Data().AsRange(), func(view *buffer.View) { + if firstPacket == nil { + firstPacket = view.AsSlice() + } else { + firstPacket = append(firstPacket[:len(firstPacket):len(firstPacket)], view.AsSlice()...) + } + }) + switch f.handler.JudgeFlow(uint8(header.UDPProtocolNumber), source.AddrPort(), destination.AddrPort(), firstPacket).Action { case ActionReject: gWriteUnreachable(f.stack, userData.(*stack.PacketBuffer)) return false, nil, nil, nil diff --git a/tun.go b/tun.go index eda669a2..eae9153d 100644 --- a/tun.go +++ b/tun.go @@ -19,7 +19,7 @@ import ( ) type Handler interface { - JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort) FlowVerdict + JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) FlowVerdict N.TCPConnectionHandlerEx N.UDPConnectionHandlerEx } From f456a50f3913ff9f3812a44a4e4c0d75c9f97bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 7 Jul 2026 15:36:21 +0800 Subject: [PATCH 14/24] Minor fixes --- flow_dispatch.go | 192 ++++++++++++++++++++++----------- flow_mtu.go | 42 +++++--- monitor_android.go | 8 +- monitor_shared.go | 14 +-- stack_gvisor_icmp.go | 4 +- stack_gvisor_lazy.go | 24 +++-- stack_gvisor_tcpbuf_default.go | 4 +- stack_gvisor_tcpbuf_ios.go | 4 +- stack_gvisor_udp.go | 9 +- stack_system.go | 132 +++++++++++++++++------ stack_system_nat.go | 68 +++++++++--- tun_offload.go | 8 +- tun_offload_linux.go | 25 +++-- 13 files changed, 355 insertions(+), 179 deletions(-) diff --git a/flow_dispatch.go b/flow_dispatch.go index b70dc1f3..e35a5217 100644 --- a/flow_dispatch.go +++ b/flow_dispatch.go @@ -106,6 +106,7 @@ type ForwardDispatcher struct { lastSweep int64 ports map[Port]*portNAT natList atomic.Pointer[[]*portNAT] + revNAT atomic.Pointer[map[netip.Addr]*portNAT] activeNATs []*portNAT writebackBatch [][]byte @@ -113,6 +114,14 @@ type ForwardDispatcher struct { segmentBuffers [][]byte segmentSizes []int + segmentUsed int +} + +func addrToTCPIP(addr netip.Addr) tcpip.Address { + if addr.Is4() { + return tcpip.AddrFrom4(addr.As4()) + } + return tcpip.AddrFrom16(addr.As16()) } func NewForwardDispatcher(handler Handler, writeback ForwardWriteback, logger logger.Logger, udpTimeout time.Duration, icmpTimeout time.Duration) *ForwardDispatcher { @@ -206,10 +215,16 @@ func (d *ForwardDispatcher) handleHit(key flowKey, entry *flowEntry, packet *for d.forwardToPort(flow, packet, raw) return true case ActionAccept: - entry.deadline = now + int64(entry.idle) - if packet.protocol == uint8(header.TCPProtocolNumber) && packet.tcpFlags&header.TCPFlagRst != 0 { - d.removeEntry(key, entry, FlowCloseReset) + if packet.protocol == uint8(header.TCPProtocolNumber) { + if packet.tcpFlags&header.TCPFlagRst != 0 { + d.removeEntry(key, entry, FlowCloseReset) + return false + } + if packet.tcpFlags&header.TCPFlagSyn == 0 { + entry.idle = tcpEstablishedTimeout + } } + entry.deadline = now + int64(entry.idle) return false case ActionReject: entry.deadline = now + int64(entry.idle) @@ -330,24 +345,24 @@ func (d *ForwardDispatcher) createFlow(packet *forwardPacket, verdict FlowVerdic dnatPort: serverPort != clientDestinationPort && !isICMP, } flow.forwardRule = rewriteRule{ - sourceAddress: tcpip.AddrFromSlice(portAddress.AsSlice()), + sourceAddress: addrToTCPIP(portAddress), sourcePort: selector, rewriteSourcePort: true, } if flow.dnatAddress { - flow.forwardRule.destinationAddress = tcpip.AddrFromSlice(serverAddress.AsSlice()) + flow.forwardRule.destinationAddress = addrToTCPIP(serverAddress) } if flow.dnatPort { flow.forwardRule.destinationPort = serverPort flow.forwardRule.rewriteDestinationPort = true } flow.reverseRule = rewriteRule{ - destinationAddress: tcpip.AddrFromSlice(flow.clientAddress.AsSlice()), + destinationAddress: addrToTCPIP(flow.clientAddress), destinationPort: flow.clientSelector, rewriteDestinationPort: true, } if flow.dnatAddress { - flow.reverseRule.sourceAddress = tcpip.AddrFromSlice(clientDestinationAddress.AsSlice()) + flow.reverseRule.sourceAddress = addrToTCPIP(clientDestinationAddress) } if flow.dnatPort { flow.reverseRule.sourcePort = clientDestinationPort @@ -371,7 +386,6 @@ func (d *ForwardDispatcher) natFor(port Port) *portNAT { err := port.AttachReturn(&d.returnPath) if err != nil { d.logger.Trace(E.Cause(err, "attach return path")) - d.ports[port] = nil return nil } nat = newPortNAT(port) @@ -383,6 +397,20 @@ func (d *ForwardDispatcher) natFor(port Port) *portNAT { } natList = append(natList, nat) d.natList.Store(&natList) + revMap := make(map[netip.Addr]*portNAT) + if currentRev := d.revNAT.Load(); currentRev != nil { + for addr, existing := range *currentRev { + revMap[addr] = existing + } + } + v4Address, v6Address := port.PortAddresses() + if v4Address.IsValid() { + revMap[v4Address] = nat + } + if v6Address.IsValid() { + revMap[v6Address] = nat + } + d.revNAT.Store(&revMap) return nat } @@ -473,6 +501,12 @@ func (d *ForwardDispatcher) Flush() { d.flushPort(nat) } d.activeNATs = d.activeNATs[:0] + if retain := max(d.segmentUsed, segmentRetainCount); len(d.segmentBuffers) > retain { + clear(d.segmentBuffers[retain:]) + d.segmentBuffers = d.segmentBuffers[:retain] + d.segmentSizes = d.segmentSizes[:retain] + } + d.segmentUsed = 0 if len(d.writebackBatch) > 0 { err := d.writeback.WriteReturnPackets(d.writebackBatch) if err != nil { @@ -581,6 +615,14 @@ func (r *forwardReturn) ReturnHeadroom() int { return r.dispatcher.writeback.ReturnHeadroom() } +type returnDecision uint8 + +const ( + returnPass returnDecision = iota + returnWrite + returnDrop +) + func (r *forwardReturn) ReturnPackets(packets [][]byte) [][]byte { if r.closed.Load() { return packets @@ -590,65 +632,96 @@ func (r *forwardReturn) ReturnPackets(packets [][]byte) [][]byte { return packets } natList := *natListPtr + var revMap map[netip.Addr]*portNAT + if revPtr := r.dispatcher.revNAT.Load(); revPtr != nil { + revMap = *revPtr + } headroom := r.dispatcher.writeback.ReturnHeadroom() - unconsumed := packets[:0] - var writeBatch [][]byte now := r.dispatcher.now() - for _, raw := range packets { - if len(raw) < headroom+header.IPv4MinimumSize { - unconsumed = append(unconsumed, raw) - continue - } - parsed, ok := parseForwardPacket(raw[headroom:]) - if !ok || parsed.fragment { - unconsumed = append(unconsumed, raw) - continue - } - if !parsed.hasFlow { - if parsed.isICMPError() && returnICMPError(natList, &parsed) { - writeBatch = append(writeBatch, raw) - } else { - unconsumed = append(unconsumed, raw) - } - continue - } - var flow *forwardFlow - for _, nat := range natList { - flow = nat.lookup(parsed.flowKey()) - if flow != nil { - break + + if len(packets) == 1 { + switch r.classifyReturn(packets[0], natList, revMap, headroom, now) { + case returnWrite: + if err := r.dispatcher.writeback.WriteReturnPackets(packets[:1]); err != nil { + r.dispatcher.logger.Trace(E.Cause(err, "write return packets")) } + return packets[:0] + case returnDrop: + return packets[:0] + default: + return packets } - if flow == nil { + } + + unconsumed := packets[:0] + var writeBatch [][]byte + for _, raw := range packets { + switch r.classifyReturn(raw, natList, revMap, headroom, now) { + case returnWrite: + writeBatch = append(writeBatch, raw) + case returnDrop: + default: unconsumed = append(unconsumed, raw) - continue - } - if flow.closed.Load() { - continue } - if flow.tracker != nil { - flow.tracker.CountReverse(len(raw) - headroom) - } - flow.observeReverse(&parsed, now) - if parsed.isTCPSyn() { - applyRewriteRaw(&parsed, &flow.reverseRule) - clampTCPMSS(&parsed, flow.effectiveMTU) - recomputeChecksums(&parsed) - } else { - applyRewrite(&parsed, &flow.reverseRule) - } - writeBatch = append(writeBatch, raw) } if len(writeBatch) > 0 { - err := r.dispatcher.writeback.WriteReturnPackets(writeBatch) - if err != nil { + if err := r.dispatcher.writeback.WriteReturnPackets(writeBatch); err != nil { r.dispatcher.logger.Trace(E.Cause(err, "write return packets")) } } return unconsumed } -func returnICMPError(natList []*portNAT, parsed *forwardPacket) bool { +func (r *forwardReturn) classifyReturn(raw []byte, natList []*portNAT, revMap map[netip.Addr]*portNAT, headroom int, now int64) returnDecision { + if len(raw) < headroom+header.IPv4MinimumSize { + return returnPass + } + parsed, ok := parseForwardPacket(raw[headroom:]) + if !ok || parsed.fragment { + return returnPass + } + if !parsed.hasFlow { + if parsed.isICMPError() && returnICMPError(natList, revMap, &parsed) { + return returnWrite + } + return returnPass + } + flow := findReverseFlow(natList, revMap, parsed.flowKey()) + if flow == nil { + return returnPass + } + if flow.closed.Load() { + return returnDrop + } + if flow.tracker != nil { + flow.tracker.CountReverse(len(raw) - headroom) + } + flow.observeReverse(&parsed, now) + if parsed.isTCPSyn() { + applyRewriteRaw(&parsed, &flow.reverseRule) + clampTCPMSS(&parsed, flow.effectiveMTU) + recomputeChecksums(&parsed) + } else { + applyRewrite(&parsed, &flow.reverseRule) + } + return returnWrite +} + +func findReverseFlow(natList []*portNAT, revMap map[netip.Addr]*portNAT, key flowKey) *forwardFlow { + if nat, ok := revMap[key.destination.Addr()]; ok { + if flow := nat.lookup(key); flow != nil { + return flow + } + } + for _, nat := range natList { + if flow := nat.lookup(key); flow != nil { + return flow + } + } + return nil +} + +func returnICMPError(natList []*portNAT, revMap map[netip.Addr]*portNAT, parsed *forwardPacket) bool { inner, ok := parsed.icmpErrorInner() if !ok { return false @@ -657,20 +730,13 @@ func returnICMPError(natList []*portNAT, parsed *forwardPacket) bool { if !parsedInner { return false } - key := embedded.flowKey().reversed() - var flow *forwardFlow - for _, nat := range natList { - flow = nat.lookup(key) - if flow != nil { - break - } - } + flow := findReverseFlow(natList, revMap, embedded.flowKey().reversed()) if flow == nil || flow.closed.Load() { return false } - rewriteEmbeddedSource(&embedded, tcpip.AddrFromSlice(flow.clientAddress.AsSlice()), flow.clientSelector, true) + rewriteEmbeddedSource(&embedded, addrToTCPIP(flow.clientAddress), flow.clientSelector, true) if flow.dnatAddress || flow.dnatPort { - rewriteEmbeddedDestination(&embedded, tcpip.AddrFromSlice(flow.clientDestinationAddress.AsSlice()), flow.clientDestinationPort, flow.dnatPort) + rewriteEmbeddedDestination(&embedded, addrToTCPIP(flow.clientDestinationAddress), flow.clientDestinationPort, flow.dnatPort) } parsed.network.SetDestinationAddr(flow.clientAddress) if parsed.network.SourceAddr() == flow.serverAddress { diff --git a/flow_mtu.go b/flow_mtu.go index 26c675ba..6ce275b3 100644 --- a/flow_mtu.go +++ b/flow_mtu.go @@ -5,7 +5,9 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -const segmentScratchCount = 128 +// segmentRetainCount bounds how many segment buffers survive a Flush; the pool +// grows to the burst high-water mark within a batch and is trimmed afterwards. +const segmentRetainCount = 128 // Linux delivers TSO aggregates to the TUN even with IFF_VNET_HDR off // (observed on 6.x: the pre-segmentation skb is handed to the fd as-is). @@ -35,33 +37,39 @@ func (d *ForwardDispatcher) resegmentTCP(flow *forwardFlow, packet *forwardPacke gsoType = GSOTCPv6 } neededSegments := max((len(raw)-totalHeaderLength+segmentSize-1)/segmentSize, 1) - if d.segmentBuffers == nil || len(d.segmentBuffers) < neededSegments || len(d.segmentBuffers[0]) < int(flow.effectiveMTU) { - bufferSize := int(flow.effectiveMTU) - if d.segmentBuffers != nil && len(d.segmentBuffers[0]) > bufferSize { - bufferSize = len(d.segmentBuffers[0]) - } - segmentCount := max(neededSegments, segmentScratchCount, len(d.segmentBuffers)) - d.segmentBuffers = make([][]byte, segmentCount) - for i := range d.segmentBuffers { - d.segmentBuffers[i] = make([]byte, bufferSize) - } - d.segmentSizes = make([]int, segmentCount) - } + bufs, sizes := d.reserveSegments(neededSegments, int(flow.effectiveMTU)) n, err := GSOSplit(raw, GSOOptions{ GSOType: gsoType, HdrLen: uint16(totalHeaderLength), CsumStart: uint16(headerLength), CsumOffset: header.TCPChecksumOffset, GSOSize: uint16(segmentSize), - }, d.segmentBuffers, d.segmentSizes, 0) + }, bufs, sizes, 0) if err != nil { d.logger.Trace(E.Cause(err, "resegment packet")) return } for i := range n { - d.stagePort(flow.nat, d.segmentBuffers[i][:d.segmentSizes[i]]) + d.stagePort(flow.nat, bufs[i][:sizes[i]]) + } +} + +func (d *ForwardDispatcher) reserveSegments(count, size int) ([][]byte, []int) { + start := d.segmentUsed + end := start + count + for len(d.segmentBuffers) < end { + d.segmentBuffers = append(d.segmentBuffers, make([]byte, size)) + d.segmentSizes = append(d.segmentSizes, 0) + } + for i := start; i < end; i++ { + if cap(d.segmentBuffers[i]) < size { + d.segmentBuffers[i] = make([]byte, size) + } else { + d.segmentBuffers[i] = d.segmentBuffers[i][:size] + } } - d.flushPort(flow.nat) + d.segmentUsed = end + return d.segmentBuffers[start:end], d.segmentSizes[start:end] } const synthesizedTTL = 64 @@ -79,7 +87,7 @@ func fragmentIPv4Packet(packet header.IPv4, effectiveMTU uint32) ([][]byte, bool baseOffset := packet.FragmentOffset() originalMore := packet.Flags()&header.IPv4FlagMoreFragments != 0 baseFlags := packet.Flags() &^ header.IPv4FlagMoreFragments - var fragments [][]byte + fragments := make([][]byte, 0, (len(payload)+maxFragmentPayload-1)/maxFragmentPayload) for start := 0; start < len(payload); start += maxFragmentPayload { end := min(start+maxFragmentPayload, len(payload)) fragment := header.IPv4(make([]byte, headerLength+end-start)) diff --git a/monitor_android.go b/monitor_android.go index c83440d9..5d0e9c62 100644 --- a/monitor_android.go +++ b/monitor_android.go @@ -11,7 +11,7 @@ func (m *defaultInterfaceMonitor) checkUpdate() error { return E.Cause(err, "list rules") } - oldVPNEnabled := m.androidVPNEnabled + oldVPNEnabled := m.androidVPNEnabled.Load() var defaultTableIndex int var vpnEnabled bool for _, rule := range ruleList { @@ -30,7 +30,7 @@ func (m *defaultInterfaceMonitor) checkUpdate() error { break } } - m.androidVPNEnabled = vpnEnabled + m.androidVPNEnabled.Store(vpnEnabled) if defaultTableIndex == 0 { return ErrNoRoute @@ -56,11 +56,11 @@ func (m *defaultInterfaceMonitor) checkUpdate() error { return E.Cause(err, "find updated interface: ", link.Attrs().Name) } oldInterface := m.defaultInterface.Swap(newInterface) - if oldInterface != nil && oldInterface.Equals(*newInterface) && oldVPNEnabled == m.androidVPNEnabled { + if oldInterface != nil && oldInterface.Equals(*newInterface) && oldVPNEnabled == m.androidVPNEnabled.Load() { return nil } var flags int - if oldVPNEnabled != m.androidVPNEnabled { + if oldVPNEnabled != m.androidVPNEnabled.Load() { flags = FlagAndroidVPNUpdate } m.emit(newInterface, flags) diff --git a/monitor_shared.go b/monitor_shared.go index 8d239d29..ad48ce04 100644 --- a/monitor_shared.go +++ b/monitor_shared.go @@ -39,8 +39,8 @@ type defaultInterfaceMonitor struct { overrideAndroidVPN bool underNetworkExtension bool defaultInterface atomic.Pointer[control.Interface] - androidVPNEnabled bool - noRoute bool + androidVPNEnabled atomic.Bool + noRoute atomic.Bool networkMonitor NetworkUpdateMonitor logger logger.Logger checkUpdateTimer *time.Timer @@ -67,6 +67,8 @@ func (m *defaultInterfaceMonitor) Start() error { } func (m *defaultInterfaceMonitor) delayCheckUpdate() { + m.access.Lock() + defer m.access.Unlock() if m.checkUpdateTimer == nil { m.checkUpdateTimer = time.AfterFunc(time.Second, m.postCheckUpdate) } else { @@ -82,15 +84,15 @@ func (m *defaultInterfaceMonitor) postCheckUpdate() { } err = m.checkUpdate() if errors.Is(err, ErrNoRoute) { - if !m.noRoute { - m.noRoute = true + if !m.noRoute.Load() { + m.noRoute.Store(true) m.defaultInterface.Store(nil) m.emit(nil, 0) } } else if err != nil { m.logger.Error("check interface: ", err) } else { - m.noRoute = false + m.noRoute.Store(false) } } @@ -110,7 +112,7 @@ func (m *defaultInterfaceMonitor) OverrideAndroidVPN() bool { } func (m *defaultInterfaceMonitor) AndroidVPNEnabled() bool { - return m.androidVPNEnabled + return m.androidVPNEnabled.Load() } func (m *defaultInterfaceMonitor) RegisterCallback(callback DefaultInterfaceUpdateCallback) *list.Element[DefaultInterfaceUpdateCallback] { diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index 96466c16..9b742e56 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -133,7 +133,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa DefaultNIC, id.LocalAddress, id.RemoteAddress, - header.IPv6ProtocolNumber, + header.IPv4ProtocolNumber, false, ) if gErr != nil { @@ -184,7 +184,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa PayloadCsum: pkt.Data().Checksum(), PayloadLen: pkt.Data().Size(), })) - outgoingEP, gErr := f.stack.GetNetworkEndpoint(DefaultNIC, header.IPv4ProtocolNumber) + outgoingEP, gErr := f.stack.GetNetworkEndpoint(DefaultNIC, header.IPv6ProtocolNumber) if gErr != nil { f.logger.Error(E.Cause(gonet.TranslateNetstackError(gErr), "get IPv6 network endpoint")) return true diff --git a/stack_gvisor_lazy.go b/stack_gvisor_lazy.go index 258392d1..96d8897a 100644 --- a/stack_gvisor_lazy.go +++ b/stack_gvisor_lazy.go @@ -42,16 +42,22 @@ func (c *gLazyConn) HandshakeContext(ctx context.Context) error { wq waiter.Queue endpoint tcpip.Endpoint ) - handshakeCtx, cancel := context.WithCancel(ctx) - go func() { - select { - case <-c.parentCtx.Done(): - wq.Notify(wq.Events()) - case <-handshakeCtx.Done(): - } - }() + var cancel context.CancelFunc + if parentDone := c.parentCtx.Done(); parentDone != nil { + var handshakeCtx context.Context + handshakeCtx, cancel = context.WithCancel(ctx) + go func() { + select { + case <-parentDone: + wq.Notify(wq.Events()) + case <-handshakeCtx.Done(): + } + }() + } endpoint, err := c.request.CreateEndpoint(&wq) - cancel() + if cancel != nil { + cancel() + } if err != nil { gErr := gonet.TranslateNetstackError(err) c.handshakeErr = gErr diff --git a/stack_gvisor_tcpbuf_default.go b/stack_gvisor_tcpbuf_default.go index f636d1a4..fc3bc2b6 100644 --- a/stack_gvisor_tcpbuf_default.go +++ b/stack_gvisor_tcpbuf_default.go @@ -9,10 +9,10 @@ import "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" const ( tcpRXBufMinSize = tcp.MinBufferSize - tcpRXBufDefSize = tcp.DefaultSendBufferSize + tcpRXBufDefSize = tcp.DefaultReceiveBufferSize tcpRXBufMaxSize = 8 << 20 // 8MiB tcpTXBufMinSize = tcp.MinBufferSize - tcpTXBufDefSize = tcp.DefaultReceiveBufferSize + tcpTXBufDefSize = tcp.DefaultSendBufferSize tcpTXBufMaxSize = 6 << 20 // 6MiB ) diff --git a/stack_gvisor_tcpbuf_ios.go b/stack_gvisor_tcpbuf_ios.go index 495e59bf..6704c9d6 100644 --- a/stack_gvisor_tcpbuf_ios.go +++ b/stack_gvisor_tcpbuf_ios.go @@ -12,10 +12,10 @@ const ( // unchanged on iOS for now as to not increase pressure towards the // NetworkExtension memory limit. tcpRXBufMinSize = tcp.MinBufferSize - tcpRXBufDefSize = tcp.DefaultSendBufferSize + tcpRXBufDefSize = tcp.DefaultReceiveBufferSize tcpRXBufMaxSize = tcp.MaxBufferSize tcpTXBufMinSize = tcp.MinBufferSize - tcpTXBufDefSize = tcp.DefaultReceiveBufferSize + tcpTXBufDefSize = tcp.DefaultSendBufferSize tcpTXBufMaxSize = tcp.MaxBufferSize ) diff --git a/stack_gvisor_udp.go b/stack_gvisor_udp.go index 3e4afddb..2ae54cf9 100644 --- a/stack_gvisor_udp.go +++ b/stack_gvisor_udp.go @@ -44,12 +44,9 @@ func NewUDPForwarder(ctx context.Context, stack *stack.Stack, handler Handler, t func (f *UDPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { source := M.SocksaddrFrom(AddrFromAddress(id.RemoteAddress), id.RemotePort) destination := M.SocksaddrFrom(AddrFromAddress(id.LocalAddress), id.LocalPort) - bufferRange := pkt.Data().AsRange() - var bufferSlices [][]byte - rangeIterate(bufferRange, func(view *buffer.View) { - bufferSlices = append(bufferSlices, view.AsSlice()) - }) - f.udpNat.NewPacket(bufferSlices, source, destination, pkt) + data := pkt.Data() + payload, _ := data.PullUp(data.Size()) + f.udpNat.NewPacket([][]byte{payload}, source, destination, pkt) return true } diff --git a/stack_system.go b/stack_system.go index 8ab9be67..46f6f87d 100644 --- a/stack_system.go +++ b/stack_system.go @@ -9,6 +9,7 @@ import ( "syscall" "time" + "github.com/sagernet/sing-tun/gtcpip" "github.com/sagernet/sing-tun/gtcpip/checksum" "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common" @@ -448,36 +449,30 @@ func (s *System) processIPv4TCP(ipHdr header.IPv4, tcpHdr header.TCP) (bool, err if session == nil { return false, E.New("ipv4: tcp: session not found: ", destination.Port()) } - ipHdr.SetSourceAddr(session.Destination.Addr()) - tcpHdr.SetSourcePort(session.Destination.Port()) - ipHdr.SetDestinationAddr(session.Source.Addr()) - tcpHdr.SetDestinationPort(session.Source.Port()) + rewriteIPv4TCP(ipHdr, tcpHdr, s.txChecksumOffload, + session.Destination.Addr(), session.Destination.Port(), true, + session.Source.Addr(), session.Source.Port(), true) } else { var loopback bool for _, inet4LoopbackAddress := range s.inet4LoopbackAddress { if destination.Addr() == inet4LoopbackAddress { - ipHdr.SetDestinationAddr(ipHdr.SourceAddr()) - ipHdr.SetSourceAddr(inet4LoopbackAddress) + rewriteIPv4TCP(ipHdr, tcpHdr, s.txChecksumOffload, + inet4LoopbackAddress, 0, false, + source.Addr(), 0, false) loopback = true break } } if !loopback { natPort := s.tcpNat.Lookup(source, destination) - ipHdr.SetSourceAddr(s.inet4NextAddress) - tcpHdr.SetSourcePort(natPort) - ipHdr.SetDestinationAddr(s.inet4Address) - tcpHdr.SetDestinationPort(s.tcpPort) + if natPort == 0 { + return false, E.New("ipv4: tcp: NAT port space exhausted") + } + rewriteIPv4TCP(ipHdr, tcpHdr, s.txChecksumOffload, + s.inet4NextAddress, natPort, true, + s.inet4Address, s.tcpPort, true) } } - if !s.txChecksumOffload { - tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( - header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), ipHdr.PayloadLength()), - ))) - } else { - tcpHdr.SetChecksum(0) - } - ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) return true, nil } @@ -491,36 +486,107 @@ func (s *System) processIPv6TCP(ipHdr header.IPv6, tcpHdr header.TCP) (bool, err if session == nil { return false, E.New("ipv6: tcp: session not found: ", destination.Port()) } - ipHdr.SetSourceAddr(session.Destination.Addr()) - tcpHdr.SetSourcePort(session.Destination.Port()) - ipHdr.SetDestinationAddr(session.Source.Addr()) - tcpHdr.SetDestinationPort(session.Source.Port()) + rewriteIPv6TCP(ipHdr, tcpHdr, s.txChecksumOffload, + session.Destination.Addr(), session.Destination.Port(), true, + session.Source.Addr(), session.Source.Port(), true) } else { var loopback bool for _, inet6LoopbackAddress := range s.inet6LoopbackAddress { if destination.Addr() == inet6LoopbackAddress { - ipHdr.SetDestinationAddr(ipHdr.SourceAddr()) - ipHdr.SetSourceAddr(inet6LoopbackAddress) + rewriteIPv6TCP(ipHdr, tcpHdr, s.txChecksumOffload, + inet6LoopbackAddress, 0, false, + source.Addr(), 0, false) loopback = true break } } if !loopback { natPort := s.tcpNat.Lookup(source, destination) - ipHdr.SetSourceAddr(s.inet6NextAddress) - tcpHdr.SetSourcePort(natPort) - ipHdr.SetDestinationAddr(s.inet6Address) - tcpHdr.SetDestinationPort(s.tcpPort6) + if natPort == 0 { + return false, E.New("ipv6: tcp: NAT port space exhausted") + } + rewriteIPv6TCP(ipHdr, tcpHdr, s.txChecksumOffload, + s.inet6NextAddress, natPort, true, + s.inet6Address, s.tcpPort6, true) } } - if !s.txChecksumOffload { - tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( - header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), ipHdr.PayloadLength()), - ))) + return true, nil +} + +func rewriteIPv4TCP(ipHdr header.IPv4, tcpHdr header.TCP, txChecksumOffload bool, + newSource netip.Addr, newSourcePort uint16, rewriteSourcePort bool, + newDestination netip.Addr, newDestinationPort uint16, rewriteDestinationPort bool, +) { + oldSource := ipHdr.SourceAddress() + oldDestination := ipHdr.DestinationAddress() + newSourceAddr := tcpip.AddrFrom4(newSource.As4()) + newDestinationAddr := tcpip.AddrFrom4(newDestination.As4()) + if newSourceAddr != oldSource { + ipHdr.SetSourceAddressWithChecksumUpdate(newSourceAddr) + if !txChecksumOffload { + tcpHdr.UpdateChecksumPseudoHeaderAddress(oldSource, newSourceAddr, true) + } + } + if newDestinationAddr != oldDestination { + ipHdr.SetDestinationAddressWithChecksumUpdate(newDestinationAddr) + if !txChecksumOffload { + tcpHdr.UpdateChecksumPseudoHeaderAddress(oldDestination, newDestinationAddr, true) + } + } + if txChecksumOffload { + if rewriteSourcePort { + tcpHdr.SetSourcePort(newSourcePort) + } + if rewriteDestinationPort { + tcpHdr.SetDestinationPort(newDestinationPort) + } + tcpHdr.SetChecksum(0) } else { + if rewriteSourcePort { + tcpHdr.SetSourcePortWithChecksumUpdate(newSourcePort) + } + if rewriteDestinationPort { + tcpHdr.SetDestinationPortWithChecksumUpdate(newDestinationPort) + } + } +} + +func rewriteIPv6TCP(ipHdr header.IPv6, tcpHdr header.TCP, txChecksumOffload bool, + newSource netip.Addr, newSourcePort uint16, rewriteSourcePort bool, + newDestination netip.Addr, newDestinationPort uint16, rewriteDestinationPort bool, +) { + oldSource := ipHdr.SourceAddress() + oldDestination := ipHdr.DestinationAddress() + newSourceAddr := tcpip.AddrFrom16(newSource.As16()) + newDestinationAddr := tcpip.AddrFrom16(newDestination.As16()) + if newSourceAddr != oldSource { + ipHdr.SetSourceAddress(newSourceAddr) + if !txChecksumOffload { + tcpHdr.UpdateChecksumPseudoHeaderAddress(oldSource, newSourceAddr, true) + } + } + if newDestinationAddr != oldDestination { + ipHdr.SetDestinationAddress(newDestinationAddr) + if !txChecksumOffload { + tcpHdr.UpdateChecksumPseudoHeaderAddress(oldDestination, newDestinationAddr, true) + } + } + if txChecksumOffload { + if rewriteSourcePort { + tcpHdr.SetSourcePort(newSourcePort) + } + if rewriteDestinationPort { + tcpHdr.SetDestinationPort(newDestinationPort) + } tcpHdr.SetChecksum(0) + } else { + if rewriteSourcePort { + tcpHdr.SetSourcePortWithChecksumUpdate(newSourcePort) + } + if rewriteDestinationPort { + tcpHdr.SetDestinationPortWithChecksumUpdate(newDestinationPort) + } } - return true, nil } func (s *System) processIPv4UDP(ipHdr header.IPv4, udpHdr header.UDP) error { diff --git a/stack_system_nat.go b/stack_system_nat.go index fd0e3823..2fec29c0 100644 --- a/stack_system_nat.go +++ b/stack_system_nat.go @@ -54,18 +54,36 @@ func (n *TCPNat) loopCheckTimeout(ctx context.Context) { func (n *TCPNat) checkTimeout() { now := time.Now() - n.portAccess.Lock() - defer n.portAccess.Unlock() - n.addrAccess.Lock() - defer n.addrAccess.Unlock() + type expiredSession struct { + port uint16 + session *TCPSession + } + var expired []expiredSession + n.portAccess.RLock() for natPort, session := range n.portMap { session.Lock() - if now.Sub(session.LastActive) > n.timeout { - delete(n.addrMap, tcpNatKey{Source: session.Source, Destination: session.Destination}) - delete(n.portMap, natPort) - } + timedOut := now.Sub(session.LastActive) > n.timeout session.Unlock() + if timedOut { + expired = append(expired, expiredSession{port: natPort, session: session}) + } } + n.portAccess.RUnlock() + if len(expired) == 0 { + return + } + n.addrAccess.Lock() + n.portAccess.Lock() + for _, e := range expired { + e.session.Lock() + if now.Sub(e.session.LastActive) > n.timeout { + delete(n.addrMap, tcpNatKey{Source: e.session.Source, Destination: e.session.Destination}) + delete(n.portMap, e.port) + } + e.session.Unlock() + } + n.portAccess.Unlock() + n.addrAccess.Unlock() } func (n *TCPNat) LookupBack(port uint16) *TCPSession { @@ -91,21 +109,37 @@ func (n *TCPNat) Lookup(source netip.AddrPort, destination netip.AddrPort) uint1 return port } n.addrAccess.Lock() - nextPort := n.portIndex - if nextPort == 0 { - nextPort = 10000 - n.portIndex = 10001 - } else { - n.portIndex++ + defer n.addrAccess.Unlock() + if port, loaded = n.addrMap[key]; loaded { + return port } - n.addrMap[key] = nextPort - n.addrAccess.Unlock() n.portAccess.Lock() + defer n.portAccess.Unlock() + nextPort, ok := n.allocatePortLocked() + if !ok { + return 0 + } n.portMap[nextPort] = &TCPSession{ Source: source, Destination: destination, LastActive: time.Now(), } - n.portAccess.Unlock() + n.addrMap[key] = nextPort return nextPort } + +func (n *TCPNat) allocatePortLocked() (uint16, bool) { + for range 65535 - 10000 + 1 { + nextPort := n.portIndex + if nextPort == 0 { + nextPort = 10000 + n.portIndex = 10001 + } else { + n.portIndex++ + } + if _, occupied := n.portMap[nextPort]; !occupied { + return nextPort, true + } + } + return 0, false +} diff --git a/tun_offload.go b/tun_offload.go index 3bf6268f..f68fbfeb 100644 --- a/tun_offload.go +++ b/tun_offload.go @@ -156,6 +156,7 @@ func GSOSplit(in []byte, options GSOOptions, outBufs [][]byte, sizes []int, outO } else { protocol = ipProtoUDP } + pseudoSumBase := header.PseudoHeaderChecksum(tcpip.TransportProtocolNumber(protocol), in[srcAddrOffset:srcAddrOffset+addrLen], in[srcAddrOffset+addrLen:srcAddrOffset+addrLen*2], 0) nextSegmentDataAt := int(options.HdrLen) i := 0 for ; nextSegmentDataAt < len(in); i++ { @@ -168,7 +169,7 @@ func GSOSplit(in []byte, options GSOOptions, outBufs [][]byte, sizes []int, outO sizes[i] = totalLen out := outBufs[i][outOffset:] - copy(out, in[:iphLen]) + copy(out[:options.HdrLen], in[:options.HdrLen]) if ipVersion == 4 { // For IPv4 we are responsible for incrementing the ID field, // updating the total len field, and recalculating the header @@ -187,9 +188,6 @@ func GSOSplit(in []byte, options GSOOptions, outBufs [][]byte, sizes []int, outO binary.BigEndian.PutUint16(out[4:], uint16(totalLen-iphLen)) } - // copy transport header - copy(out[options.CsumStart:options.HdrLen], in[options.CsumStart:options.HdrLen]) - if protocol == ipProtoTCP { // set TCP seq and adjust TCP flags tcpSeq := firstTCPSeqNum + uint32(options.GSOSize*uint16(i)) @@ -211,7 +209,7 @@ func GSOSplit(in []byte, options GSOOptions, outBufs [][]byte, sizes []int, outO out[transportCsumAt], out[transportCsumAt+1] = 0, 0 // clear tcp/udp checksum transportHeaderLen := int(options.HdrLen - options.CsumStart) lenForPseudo := uint16(transportHeaderLen + segmentDataLen) - transportCSum := header.PseudoHeaderChecksum(tcpip.TransportProtocolNumber(protocol), in[srcAddrOffset:srcAddrOffset+addrLen], in[srcAddrOffset+addrLen:srcAddrOffset+addrLen*2], lenForPseudo) + transportCSum := checksum.Combine(pseudoSumBase, lenForPseudo) transportCSum = ^checksum.Checksum(out[options.CsumStart:totalLen], transportCSum) binary.BigEndian.PutUint16(out[options.CsumStart+options.CsumOffset:], transportCSum) diff --git a/tun_offload_linux.go b/tun_offload_linux.go index 57834ef1..4e4dc79c 100644 --- a/tun_offload_linux.go +++ b/tun_offload_linux.go @@ -129,14 +129,12 @@ func (t *tcpGROTable) lookupOrInsert(pkt []byte, srcAddrOffset, dstAddrOffset, t if ok { return items, ok } - // TODO: insert() performs another map lookup. This could be rearranged to avoid. - t.insert(pkt, srcAddrOffset, dstAddrOffset, tcphOffset, tcphLen, bufsIndex) + t.insert(key, pkt, tcphOffset, tcphLen, bufsIndex) return nil, false } // insert an item in the table for the provided packet and packet metadata. -func (t *tcpGROTable) insert(pkt []byte, srcAddrOffset, dstAddrOffset, tcphOffset, tcphLen, bufsIndex int) { - key := newTCPFlowKey(pkt, srcAddrOffset, dstAddrOffset, tcphOffset) +func (t *tcpGROTable) insert(key tcpFlowKey, pkt []byte, tcphOffset, tcphLen, bufsIndex int) { item := tcpGROItem{ key: key, bufsIndex: uint16(bufsIndex), @@ -236,14 +234,12 @@ func (u *udpGROTable) lookupOrInsert(pkt []byte, srcAddrOffset, dstAddrOffset, u if ok { return items, ok } - // TODO: insert() performs another map lookup. This could be rearranged to avoid. - u.insert(pkt, srcAddrOffset, dstAddrOffset, udphOffset, bufsIndex, false) + u.insert(key, pkt, udphOffset, bufsIndex, false) return nil, false } // insert an item in the table for the provided packet and packet metadata. -func (u *udpGROTable) insert(pkt []byte, srcAddrOffset, dstAddrOffset, udphOffset, bufsIndex int, cSumKnownInvalid bool) { - key := newUDPFlowKey(pkt, srcAddrOffset, dstAddrOffset, udphOffset) +func (u *udpGROTable) insert(key udpFlowKey, pkt []byte, udphOffset, bufsIndex int, cSumKnownInvalid bool) { item := udpGROItem{ key: key, bufsIndex: uint16(bufsIndex), @@ -456,7 +452,8 @@ func coalesceUDPPackets(pkt []byte, item *udpGROItem, bufs [][]byte, bufsOffset return coalescePktInvalidCSum } extendBy := len(pkt) - int(headersLen) - bufs[item.bufsIndex] = append(bufs[item.bufsIndex], make([]byte, extendBy)...) + b := bufs[item.bufsIndex] + bufs[item.bufsIndex] = b[:len(b)+extendBy] copy(bufs[item.bufsIndex][bufsOffset+len(pktHead):], pkt[headersLen:]) item.numMerged++ @@ -493,7 +490,8 @@ func coalesceTCPPackets(mode canCoalesce, pkt []byte, pktBuffsIndex int, gsoSize } item.sentSeq = seq extendBy := coalescedLen - len(pktHead) - bufs[pktBuffsIndex] = append(bufs[pktBuffsIndex], make([]byte, extendBy)...) + b := bufs[pktBuffsIndex] + bufs[pktBuffsIndex] = b[:len(b)+extendBy] copy(bufs[pktBuffsIndex][bufsOffset+len(pkt):], bufs[item.bufsIndex][bufsOffset+int(headersLen):]) // Flip the slice headers in bufs as part of prepend. The index of item // is already being tracked for writing. @@ -519,7 +517,8 @@ func coalesceTCPPackets(mode canCoalesce, pkt []byte, pktBuffsIndex int, gsoSize pktHead[item.iphLen+tcpFlagsOffset] |= tcpFlagPSH } extendBy := len(pkt) - int(headersLen) - bufs[item.bufsIndex] = append(bufs[item.bufsIndex], make([]byte, extendBy)...) + b := bufs[item.bufsIndex] + bufs[item.bufsIndex] = b[:len(b)+extendBy] copy(bufs[item.bufsIndex][bufsOffset+len(pktHead):], pkt[headersLen:]) } @@ -639,7 +638,7 @@ func tcpGRO(bufs [][]byte, offset int, pktI int, table *tcpGROTable, isV6 bool) } } // failed to coalesce with any other packets; store the item in the flow - table.insert(pkt, srcAddrOffset, srcAddrOffset+addrLen, iphLen, tcphLen, pktI) + table.insert(newTCPFlowKey(pkt, srcAddrOffset, srcAddrOffset+addrLen, iphLen), pkt, iphLen, tcphLen, pktI) return groResultTableInsert } @@ -900,7 +899,7 @@ func udpGRO(bufs [][]byte, offset int, pktI int, table *udpGROTable, isV6 bool) } } // failed to coalesce with any other packets; store the item in the flow - table.insert(pkt, srcAddrOffset, srcAddrOffset+addrLen, iphLen, pktI, pktCSumKnownInvalid) + table.insert(newUDPFlowKey(pkt, srcAddrOffset, srcAddrOffset+addrLen, iphLen), pkt, iphLen, pktI, pktCSumKnownInvalid) return groResultTableInsert } From 95180d1432ffd7cc56471fec4c39bc033cf45e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 7 Jul 2026 21:19:43 +0800 Subject: [PATCH 15/24] Fix CalculateInterfaceName on iOS --- tun.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tun.go b/tun.go index eae9153d..c6518f40 100644 --- a/tun.go +++ b/tun.go @@ -225,7 +225,7 @@ func (o *Options) Inet6GatewayAddr() netip.Addr { } func CalculateInterfaceName(name string) (tunName string) { - if runtime.GOOS == "darwin" { + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { tunName = "utun" } else if name != "" { tunName = name From 0e0b6ec8b8f130b242318fd53e3ccdc7130f60e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 7 Jul 2026 21:19:56 +0800 Subject: [PATCH 16/24] Add UDPTimeout option for flow --- flow.go | 6 +++++- flow_dispatch.go | 14 +++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/flow.go b/flow.go index db2cd1d8..4fbf8019 100644 --- a/flow.go +++ b/flow.go @@ -1,11 +1,15 @@ package tun -import "net/netip" +import ( + "net/netip" + "time" +) type FlowVerdict struct { Action FlowAction Port Port Destination netip.AddrPort + UDPTimeout time.Duration NewTracker func() FlowTracker } diff --git a/flow_dispatch.go b/flow_dispatch.go index e35a5217..152a5bbf 100644 --- a/flow_dispatch.go +++ b/flow_dispatch.go @@ -1,6 +1,7 @@ package tun import ( + "maps" "net/netip" "sync/atomic" "time" @@ -47,6 +48,7 @@ type forwardFlow struct { reverseRule rewriteRule effectiveMTU uint32 protocol uint8 + udpTimeout time.Duration tracker FlowTracker clientAddress netip.Addr @@ -293,6 +295,9 @@ func (d *ForwardDispatcher) flowIdle(flow *forwardFlow) time.Duration { if flow.protocol == uint8(header.TCPProtocolNumber) && flow.finForward && flow.finReverse.Load() { return tcpClosingTimeout } + if flow.udpTimeout > 0 { + return flow.udpTimeout + } established := flow.established.Load() && !flow.finForward && !flow.finReverse.Load() return d.idleTimeout(flow.protocol, established) } @@ -331,11 +336,16 @@ func (d *ForwardDispatcher) createFlow(packet *forwardPacket, verdict FlowVerdic if !allocated { return nil, false } + var udpTimeout time.Duration + if packet.protocol == uint8(header.UDPProtocolNumber) { + udpTimeout = verdict.UDPTimeout + } flow := &forwardFlow{ nat: nat, reverseKey: reverseKey, effectiveMTU: effectiveMTU, protocol: packet.protocol, + udpTimeout: udpTimeout, clientAddress: packet.source.Addr(), clientSelector: packet.source.Port(), clientDestinationAddress: clientDestinationAddress, @@ -399,9 +409,7 @@ func (d *ForwardDispatcher) natFor(port Port) *portNAT { d.natList.Store(&natList) revMap := make(map[netip.Addr]*portNAT) if currentRev := d.revNAT.Load(); currentRev != nil { - for addr, existing := range *currentRev { - revMap[addr] = existing - } + maps.Copy(revMap, *currentRev) } v4Address, v6Address := port.PortAddresses() if v4Address.IsValid() { From 127c9418486636812cf3f0c7541a0ac347878fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 8 Jul 2026 11:14:28 +0800 Subject: [PATCH 17/24] Fix android GSO --- tun_linux.go | 61 +++++++++++++++++++++++++++++----------------- tun_linux_flags.go | 10 +++++--- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/tun_linux.go b/tun_linux.go index c7557bf4..940b7d03 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -76,6 +76,14 @@ func New(options Options) (Tun, error) { tunFile: os.NewFile(uintptr(options.FileDescriptor), "tun"), options: options, } + if options.GSO { + err := nativeTun.enableGSO() + if err != nil { + if options.Logger != nil { + options.Logger.Warn(err) + } + } + } } return nativeTun, nil } @@ -189,28 +197,38 @@ func (t *NativeTun) enableGSO() error { if !vnetHdrEnabled { return E.Cause(err, "enable offload: IFF_VNET_HDR not enabled") } + t.vnetHdr = true + t.writeBuffer = make([]byte, virtioNetHdrLen+gsoMaxSize) + t.pendingBuffer = make([]byte, virtioNetHdrLen+gsoMaxSize) + t.tcpGROTable = newTCPGROTable() + t.udpGROTable = newUDPGROTable() err = setTCPOffload(t.tunFd) if err != nil { - return E.Cause(err, "enable TCP offload") + if !(errors.Is(err, unix.EPERM) && t.options.FileDescriptor != 0) { + t.options.Logger.Warn(E.Cause(err, "enable offload: set tcp offload")) + t.gro.disableTCPGRO() + t.gro.disableUDPGRO() + } + } else { + err = setUDPOffload(t.tunFd) + if err != nil { + t.options.Logger.Warn(E.Cause(err, "enable offload: set udp offload")) + t.gro.disableUDPGRO() + } } - t.vnetHdr = true - t.writeBuffer = make([]byte, virtioNetHdrLen+int(gsoMaxSize)) - t.pendingBuffer = make([]byte, virtioNetHdrLen+int(gsoMaxSize)) t.readRawConn, err = t.tunFile.SyscallConn() if err != nil { return E.Cause(err, "enable offload: get raw conn") } - t.tcpGROTable = newTCPGROTable() - t.udpGROTable = newUDPGROTable() - err = setUDPOffload(t.tunFd) - if err != nil { - t.gro.disableUDPGRO() - } return nil } func (t *NativeTun) probeTCPGRO() error { - ipPort := netip.AddrPortFrom(t.options.Inet4Address[0].Addr(), 0) + probeAddr := netip.AddrFrom4([4]byte{127, 0, 0, 1}) + if len(t.options.Inet4Address) > 0 { + probeAddr = t.options.Inet4Address[0].Addr() + } + ipPort := netip.AddrPortFrom(probeAddr, 0) fingerprint := []byte("sing-tun-probe-tun-gro") segmentSize := len(fingerprint) iphLen := 20 @@ -267,6 +285,16 @@ func (t *NativeTun) Name() (string, error) { } func (t *NativeTun) Start() error { + if t.vnetHdr && t.gro.canTCPGRO() { + err := t.probeTCPGRO() + if err != nil { + t.gro.disableTCPGRO() + t.gro.disableUDPGRO() + if t.options.Logger != nil { + t.options.Logger.Warn(E.Cause(err, "disabled TUN TCP & UDP GRO due to GRO probe error")) + } + } + } if t.options.FileDescriptor != 0 { return nil } @@ -283,17 +311,6 @@ func (t *NativeTun) Start() error { return E.Cause(err, "set tun up") } - if t.vnetHdr && len(t.options.Inet4Address) > 0 { - err = t.probeTCPGRO() - if err != nil { - t.gro.disableTCPGRO() - t.gro.disableUDPGRO() - if t.options.Logger != nil { - t.options.Logger.Warn(E.Cause(err, "disabled TUN TCP & UDP GRO due to GRO probe error")) - } - } - } - if t.options.EXP_ExternalConfiguration { return nil } diff --git a/tun_linux_flags.go b/tun_linux_flags.go index 53fff08a..347dba0f 100644 --- a/tun_linux_flags.go +++ b/tun_linux_flags.go @@ -7,8 +7,6 @@ import ( "syscall" "unsafe" - E "github.com/sagernet/sing/common/exceptions" - "golang.org/x/sys/unix" ) @@ -33,13 +31,17 @@ func checkVNETHDREnabled(fd int, name string) (bool, error) { func setTCPOffload(fd int) error { err := unix.IoctlSetInt(fd, unix.TUNSETOFFLOAD, tunTCPOffloads) if err != nil { - return E.Cause(os.NewSyscallError("TUNSETOFFLOAD", err), "enable offload") + return os.NewSyscallError("TUNSETOFFLOAD", err) } return nil } func setUDPOffload(fd int) error { - return unix.IoctlSetInt(fd, unix.TUNSETOFFLOAD, tunTCPOffloads|tunUDPOffloads) + err := unix.IoctlSetInt(fd, unix.TUNSETOFFLOAD, tunTCPOffloads|tunUDPOffloads) + if err != nil { + return os.NewSyscallError("TUNSETOFFLOAD", err) + } + return nil } type ifreqData struct { From 4fb6a79f85ac72bb3ff0d4746dce3035d17daa09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 8 Jul 2026 17:05:34 +0800 Subject: [PATCH 18/24] Add flow PortWithSelectorRange --- flow.go | 5 +++++ flow_nat.go | 29 ++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/flow.go b/flow.go index 4fbf8019..0954601b 100644 --- a/flow.go +++ b/flow.go @@ -73,6 +73,11 @@ type Port interface { WritePackets(packets [][]byte) error } +type PortWithSelectorRange interface { + Port + PortSelectorRange() (start uint16, count uint16) +} + type Return interface { ReturnHeadroom() int ReturnPackets(packets [][]byte) [][]byte diff --git a/flow_nat.go b/flow_nat.go index fe4ea1af..4d2bca3b 100644 --- a/flow_nat.go +++ b/flow_nat.go @@ -15,10 +15,12 @@ const ( ) type portNAT struct { - port Port - hasher maphash.Hasher[flowKey] - shardMask uint32 - shards []natShard + port Port + hasher maphash.Hasher[flowKey] + shardMask uint32 + shards []natShard + selectorStart uint16 + selectorCount uint16 counter uint32 pending [][]byte @@ -40,6 +42,9 @@ func newPortNAT(port Port) *portNAT { shardMask: uint32(shardCount - 1), shards: make([]natShard, shardCount), } + if rangedPort, isRanged := port.(PortWithSelectorRange); isRanged { + nat.selectorStart, nat.selectorCount = rangedPort.PortSelectorRange() + } for i := range nat.shards { nat.shards[i].flows = make(map[flowKey]*forwardFlow) } @@ -87,16 +92,26 @@ func (n *portNAT) reverseKeyFor(protocol uint8, portAddress, serverAddress netip } } +func (n *portNAT) selectorRange(protocol uint8) (uint16, uint32) { + if n.selectorCount == 0 || + protocol == uint8(header.ICMPv4ProtocolNumber) || protocol == uint8(header.ICMPv6ProtocolNumber) { + return natSelectorMin, natSelectorMax - natSelectorMin + 1 + } + return n.selectorStart, uint32(n.selectorCount) +} + func (n *portNAT) allocateSelector(protocol uint8, portAddress, serverAddress netip.Addr, serverPort, clientSelector uint16) (uint16, flowKey, bool) { - if clientSelector != 0 { + rangeStart, rangeCount := n.selectorRange(protocol) + if clientSelector != 0 && + clientSelector >= rangeStart && uint32(clientSelector-rangeStart) < rangeCount { key := n.reverseKeyFor(protocol, portAddress, serverAddress, serverPort, clientSelector) if n.lookup(key) == nil { return clientSelector, key, true } } - for range natSelectorMax - natSelectorMin + 1 { + for range rangeCount { n.counter++ - candidate := uint16(natSelectorMin + n.counter%(natSelectorMax-natSelectorMin+1)) + candidate := rangeStart + uint16(n.counter%rangeCount) key := n.reverseKeyFor(protocol, portAddress, serverAddress, serverPort, candidate) if n.lookup(key) == nil { return candidate, key, true From f14624c3c9d8ccb410908d66ef5d0be92ea1bb09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 8 Jul 2026 17:14:49 +0800 Subject: [PATCH 19/24] Reject flows on selector range exhaustion --- flow_dispatch.go | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/flow_dispatch.go b/flow_dispatch.go index 152a5bbf..8911bf1a 100644 --- a/flow_dispatch.go +++ b/flow_dispatch.go @@ -113,6 +113,7 @@ type ForwardDispatcher struct { activeNATs []*portNAT writebackBatch [][]byte returnPath forwardReturn + exhaustedLogAt int64 segmentBuffers [][]byte segmentSizes []int @@ -247,14 +248,23 @@ func (d *ForwardDispatcher) judgeAndInstall(key flowKey, packet *forwardPacket, switch verdict.Action { case ActionFlow: if verdict.Port != nil { - flow, created := d.createFlow(packet, verdict) - if created { + flow, result := d.createFlow(packet, verdict) + if result == createFlowOK { entry := &flowEntry{action: ActionFlow, flow: flow, idle: d.flowIdle(flow)} entry.deadline = now + int64(entry.idle) d.insertEntry(key, entry, now) d.forwardToPort(flow, packet, raw) return true } + if result == createFlowExhausted { + if now-d.exhaustedLogAt >= int64(exhaustedLogInterval) { + d.exhaustedLogAt = now + d.logger.Warn("port selector range exhausted, rejecting flow to ", packet.destination) + } + d.installSimple(key, ActionReject, packet.protocol, now) + d.stageReject(packet) + return true + } } d.installSimple(key, ActionAccept, packet.protocol, now) return false @@ -302,7 +312,17 @@ func (d *ForwardDispatcher) flowIdle(flow *forwardFlow) time.Duration { return d.idleTimeout(flow.protocol, established) } -func (d *ForwardDispatcher) createFlow(packet *forwardPacket, verdict FlowVerdict) (*forwardFlow, bool) { +type createFlowResult uint8 + +const ( + createFlowOK createFlowResult = iota + createFlowUnsupported + createFlowExhausted +) + +const exhaustedLogInterval = 5 * time.Second + +func (d *ForwardDispatcher) createFlow(packet *forwardPacket, verdict FlowVerdict) (*forwardFlow, createFlowResult) { var portAddress netip.Addr inet4Address, inet6Address := verdict.Port.PortAddresses() if packet.ipVersion == 6 { @@ -311,11 +331,11 @@ func (d *ForwardDispatcher) createFlow(packet *forwardPacket, verdict FlowVerdic portAddress = inet4Address } if !portAddress.IsValid() { - return nil, false + return nil, createFlowUnsupported } effectiveMTU := verdict.Port.PortMTU() if packet.ipVersion == 6 && effectiveMTU != 0 && effectiveMTU < header.IPv6MinimumMTU { - return nil, false + return nil, createFlowUnsupported } isICMP := isICMPProtocol(packet.protocol) clientDestinationAddress := packet.destination.Addr() @@ -330,11 +350,11 @@ func (d *ForwardDispatcher) createFlow(packet *forwardPacket, verdict FlowVerdic } nat := d.natFor(verdict.Port) if nat == nil { - return nil, false + return nil, createFlowUnsupported } selector, reverseKey, allocated := nat.allocateSelector(packet.protocol, portAddress, serverAddress, serverPort, packet.source.Port()) if !allocated { - return nil, false + return nil, createFlowExhausted } var udpTimeout time.Duration if packet.protocol == uint8(header.UDPProtocolNumber) { @@ -385,7 +405,7 @@ func (d *ForwardDispatcher) createFlow(packet *forwardPacket, verdict FlowVerdic } } nat.insert(reverseKey, flow) - return flow, true + return flow, createFlowOK } func (d *ForwardDispatcher) natFor(port Port) *portNAT { From 467c206b16af05f1ff659a323594b074d7ad9b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 8 Jul 2026 18:44:57 +0800 Subject: [PATCH 20/24] Fix flow close reasons --- flow.go | 13 +------------ flow_dispatch.go | 36 ++++++++++++++++++++++++++---------- stack_gvisor_icmp.go | 4 ++-- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/flow.go b/flow.go index 0954601b..1b1f34d6 100644 --- a/flow.go +++ b/flow.go @@ -41,27 +41,16 @@ const ( FlowCloseReset FlowCloseReason = iota FlowCloseFinished FlowCloseTimeout - FlowCloseEvicted - FlowCloseShutdown - FlowCloseInterrupted ) func (r FlowCloseReason) String() string { switch r { - case FlowCloseReset: - return "connection reset" case FlowCloseFinished: return "finished" case FlowCloseTimeout: return "idle timeout" - case FlowCloseEvicted: - return "evicted" - case FlowCloseShutdown: - return "stack closed" - case FlowCloseInterrupted: - return "interrupted" default: - return "unknown" + return "connection reset" } } diff --git a/flow_dispatch.go b/flow_dispatch.go index 8911bf1a..3f71751a 100644 --- a/flow_dispatch.go +++ b/flow_dispatch.go @@ -59,15 +59,16 @@ type forwardFlow struct { dnatAddress bool dnatPort bool - finForward bool + finForward atomic.Bool established atomic.Bool finReverse atomic.Bool + reported atomic.Bool closed atomic.Bool lastReverse atomic.Int64 } -func (f *forwardFlow) close(reason FlowCloseReason) { - if !f.closed.CompareAndSwap(false, true) { +func (f *forwardFlow) report(reason FlowCloseReason) { + if !f.reported.CompareAndSwap(false, true) { return } if f.tracker != nil { @@ -75,8 +76,15 @@ func (f *forwardFlow) close(reason FlowCloseReason) { } } +func (f *forwardFlow) close(reason FlowCloseReason) { + if !f.closed.CompareAndSwap(false, true) { + return + } + f.report(reason) +} + func (f *forwardFlow) CloseFlow() { - f.close(FlowCloseInterrupted) + f.close(FlowCloseReset) } func (f *forwardFlow) observeReverse(packet *forwardPacket, now int64) { @@ -93,6 +101,8 @@ func (f *forwardFlow) observeReverse(packet *forwardPacket, now int64) { } if packet.tcpFlags&header.TCPFlagFin != 0 { f.finReverse.Store(true) + } else if f.finReverse.Load() && f.finForward.Load() { + f.report(FlowCloseFinished) } } @@ -159,7 +169,7 @@ func (d *ForwardDispatcher) Close() { d.returnPath.closed.Store(true) for _, entry := range d.table { if entry.flow != nil { - entry.flow.close(FlowCloseShutdown) + entry.flow.close(FlowCloseReset) } } for port, nat := range d.ports { @@ -202,6 +212,7 @@ func (d *ForwardDispatcher) handleHit(key flowKey, entry *flowEntry, packet *for d.tombstoneEntry(entry, now) return true } + var flowFinished bool if packet.protocol == uint8(header.TCPProtocolNumber) { if packet.tcpFlags&header.TCPFlagRst != 0 { d.forwardToPort(flow, packet, raw) @@ -210,12 +221,17 @@ func (d *ForwardDispatcher) handleHit(key flowKey, entry *flowEntry, packet *for return true } if packet.tcpFlags&header.TCPFlagFin != 0 { - flow.finForward = true + flow.finForward.Store(true) + } else if flow.finForward.Load() && flow.finReverse.Load() { + flowFinished = true } } entry.idle = d.flowIdle(flow) entry.deadline = now + int64(entry.idle) d.forwardToPort(flow, packet, raw) + if flowFinished { + flow.report(FlowCloseFinished) + } return true case ActionAccept: if packet.protocol == uint8(header.TCPProtocolNumber) { @@ -302,13 +318,13 @@ func (d *ForwardDispatcher) idleTimeout(protocol uint8, established bool) time.D } func (d *ForwardDispatcher) flowIdle(flow *forwardFlow) time.Duration { - if flow.protocol == uint8(header.TCPProtocolNumber) && flow.finForward && flow.finReverse.Load() { + if flow.protocol == uint8(header.TCPProtocolNumber) && flow.finForward.Load() && flow.finReverse.Load() { return tcpClosingTimeout } if flow.udpTimeout > 0 { return flow.udpTimeout } - established := flow.established.Load() && !flow.finForward && !flow.finReverse.Load() + established := flow.established.Load() && !flow.finForward.Load() && !flow.finReverse.Load() return d.idleTimeout(flow.protocol, established) } @@ -569,7 +585,7 @@ func (d *ForwardDispatcher) tombstoneEntry(entry *flowEntry, now int64) { func (d *ForwardDispatcher) removeEntry(key flowKey, entry *flowEntry, reason FlowCloseReason) { delete(d.table, key) if entry.flow != nil { - if reason == FlowCloseTimeout && entry.flow.finForward && entry.flow.finReverse.Load() { + if reason == FlowCloseTimeout && entry.flow.finForward.Load() && entry.flow.finReverse.Load() { reason = FlowCloseFinished } entry.flow.close(reason) @@ -605,7 +621,7 @@ func (d *ForwardDispatcher) evictEntries(now int64) { } } if freed == 0 && oldest != nil { - d.removeEntry(oldestKey, oldest, FlowCloseEvicted) + d.removeEntry(oldestKey, oldest, FlowCloseReset) } } diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index 9b742e56..11e82afe 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -57,7 +57,7 @@ func (f *icmpFlow) close(reason FlowCloseReason) { } func (f *icmpFlow) CloseFlow() { - f.close(FlowCloseInterrupted) + f.close(FlowCloseReset) } func NewICMPForwarder(stack *stack.Stack, handler Handler, logger logger.Logger) *ICMPForwarder { @@ -77,7 +77,7 @@ func (f *ICMPForwarder) Close() error { f.flowAccess.Lock() defer f.flowAccess.Unlock() for key, flow := range f.flows { - flow.close(FlowCloseShutdown) + flow.close(FlowCloseReset) delete(f.flows, key) } for port := range f.attachedPorts { From 1b35243e344b9cad37642eeb0c830c14584bd724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 8 Jul 2026 19:38:44 +0800 Subject: [PATCH 21/24] Improve darwin forwarding --- internal/fdbased_darwin/packet_dispatchers.go | 11 ++- internal/rawfile_darwin/rawfile.go | 75 ++++++++----------- stack_system.go | 48 +++++++++--- tun_darwin.go | 36 ++++++++- 4 files changed, 115 insertions(+), 55 deletions(-) diff --git a/internal/fdbased_darwin/packet_dispatchers.go b/internal/fdbased_darwin/packet_dispatchers.go index c102f6cc..afe362f6 100644 --- a/internal/fdbased_darwin/packet_dispatchers.go +++ b/internal/fdbased_darwin/packet_dispatchers.go @@ -90,6 +90,8 @@ type recvMMsgDispatcher struct { // fd is the file descriptor used to send and receive packets. fd int + poller *rawfile.Poller + // e is the endpoint this dispatcher is attached to. e *endpoint @@ -123,9 +125,15 @@ func newRecvMMsgDispatcher(fd int, e *endpoint, opts *Options) (linkDispatcher, } else { batchSize = 1 } + poller, err := rawfile.NewPoller(stopFD.ReadFD, fd) + if err != nil { + stopFD.Close() + return nil, err + } d := &recvMMsgDispatcher{ StopFD: stopFD, fd: fd, + poller: poller, e: e, bufs: make([]*iovecBuffer, batchSize), msgHdrs: make([]rawfile.MsgHdrX, batchSize), @@ -144,6 +152,7 @@ func (d *recvMMsgDispatcher) release() { for _, iov := range d.bufs { iov.release() } + _ = d.poller.Close() d.mgr.close() } @@ -161,7 +170,7 @@ func (d *recvMMsgDispatcher) dispatch() (bool, tcpip.Error) { d.msgHdrs[k].Msg.SetIovlen(iovLen) } - nMsgs, errno := rawfile.BlockingRecvMMsgUntilStopped(d.ReadFD, d.fd, d.msgHdrs) + nMsgs, errno := rawfile.BlockingRecvMMsgUntilStopped(d.poller, d.fd, d.msgHdrs) if errno != 0 { return false, TranslateErrno(errno) } diff --git a/internal/rawfile_darwin/rawfile.go b/internal/rawfile_darwin/rawfile.go index e3a061b4..e586ef2e 100644 --- a/internal/rawfile_darwin/rawfile.go +++ b/internal/rawfile_darwin/rawfile.go @@ -69,7 +69,7 @@ func NonBlockingWriteIovec(fd int, iovec []unix.Iovec) unix.Errno { return e } -func BlockingReadvUntilStopped(efd int, fd int, iovecs []unix.Iovec) (int, unix.Errno) { +func BlockingReadvUntilStopped(poller *Poller, fd int, iovecs []unix.Iovec) (int, unix.Errno) { for { //nolint:staticcheck n, _, e := unix.RawSyscall(unix.SYS_READV, uintptr(fd), uintptr(unsafe.Pointer(&iovecs[0])), uintptr(len(iovecs))) @@ -79,7 +79,7 @@ func BlockingReadvUntilStopped(efd int, fd int, iovecs []unix.Iovec) (int, unix. if e != 0 && e != unix.EWOULDBLOCK { return 0, e } - stopped, e := BlockingPollUntilStopped(efd, fd, unix.POLLIN) + stopped, e := poller.Wait() if stopped { return -1, e } @@ -89,7 +89,7 @@ func BlockingReadvUntilStopped(efd int, fd int, iovecs []unix.Iovec) (int, unix. } } -func BlockingRecvMMsgUntilStopped(efd int, fd int, msgHdrs []MsgHdrX) (int, unix.Errno) { +func BlockingRecvMMsgUntilStopped(poller *Poller, fd int, msgHdrs []MsgHdrX) (int, unix.Errno) { for { //nolint:staticcheck n, _, e := unix.RawSyscall6(unix.SYS_RECVMSG_X, uintptr(fd), uintptr(unsafe.Pointer(&msgHdrs[0])), uintptr(len(msgHdrs)), unix.MSG_DONTWAIT, 0, 0) @@ -101,7 +101,7 @@ func BlockingRecvMMsgUntilStopped(efd int, fd int, msgHdrs []MsgHdrX) (int, unix return 0, e } - stopped, e := BlockingPollUntilStopped(efd, fd, unix.POLLIN) + stopped, e := poller.Wait() if stopped { return -1, e } @@ -111,71 +111,58 @@ func BlockingRecvMMsgUntilStopped(efd int, fd int, msgHdrs []MsgHdrX) (int, unix } } -func BlockingPollUntilStopped(efd int, fd int, events int16) (bool, unix.Errno) { - // Create kqueue +type Poller struct { + kq int + efd int + fd int +} + +func NewPoller(efd int, fd int) (*Poller, error) { kq, err := unix.Kqueue() if err != nil { - return false, unix.Errno(err.(unix.Errno)) + return nil, err } - defer unix.Close(kq) - - // Prepare kevents for registration - var kevents []unix.Kevent_t - - // Always monitor efd for read events - kevents = append(kevents, unix.Kevent_t{ - Ident: uint64(efd), - Filter: unix.EVFILT_READ, - Flags: unix.EV_ADD | unix.EV_ENABLE, - }) - - // Monitor fd based on requested events - // Convert poll events to kqueue filters - if events&unix.POLLIN != 0 { - kevents = append(kevents, unix.Kevent_t{ - Ident: uint64(fd), + kevents := []unix.Kevent_t{ + { + Ident: uint64(efd), Filter: unix.EVFILT_READ, Flags: unix.EV_ADD | unix.EV_ENABLE, - }) - } - if events&unix.POLLOUT != 0 { - kevents = append(kevents, unix.Kevent_t{ + }, + { Ident: uint64(fd), - Filter: unix.EVFILT_WRITE, + Filter: unix.EVFILT_READ, Flags: unix.EV_ADD | unix.EV_ENABLE, - }) + }, } - - // Register events _, err = unix.Kevent(kq, kevents, nil, nil) if err != nil { - return false, unix.Errno(err.(unix.Errno)) + unix.Close(kq) + return nil, err } + return &Poller{kq: kq, efd: efd, fd: fd}, nil +} - // Wait for events (blocking) - revents := make([]unix.Kevent_t, len(kevents)) - n, err := unix.Kevent(kq, nil, revents, nil) +func (p *Poller) Wait() (bool, unix.Errno) { + var revents [2]unix.Kevent_t + n, err := unix.Kevent(p.kq, nil, revents[:], nil) if err != nil { - return false, unix.Errno(err.(unix.Errno)) + return false, err.(unix.Errno) } - // Check results var efdHasData bool var errno unix.Errno for i := range n { ev := &revents[i] - if int(ev.Ident) == efd && ev.Filter == unix.EVFILT_READ { + if int(ev.Ident) == p.efd && ev.Filter == unix.EVFILT_READ { efdHasData = true } - if int(ev.Ident) == fd { - // Check for errors or EOF + if int(ev.Ident) == p.fd { if ev.Flags&unix.EV_EOF != 0 { errno = unix.ECONNRESET } else if ev.Flags&unix.EV_ERROR != 0 { - // Extract error from Data field if ev.Data != 0 { errno = unix.Errno(ev.Data) } else { @@ -187,3 +174,7 @@ func BlockingPollUntilStopped(efd int, fd int, events int16) (bool, unix.Errno) return efdHasData, errno } + +func (p *Poller) Close() error { + return unix.Close(p.kq) +} diff --git a/stack_system.go b/stack_system.go index 46f6f87d..3cb0cb04 100644 --- a/stack_system.go +++ b/stack_system.go @@ -750,18 +750,49 @@ func (w *systemUDPPacketWriter6) WritePacket(buffer *buf.Buffer, destination M.S return common.Error(w.tun.Write(newPacket.Bytes())) } -type systemWriteback struct { - tun Tun +func newSystemWriteback(tunInterface Tun, frontHeadroom int) ForwardWriteback { + if linuxTUN, isLinuxTUN := tunInterface.(LinuxTUN); isLinuxTUN { + return &systemWritebackLinux{linuxTUN: linuxTUN, frontHeadroom: frontHeadroom} + } + if darwinTUN, isDarwinTUN := tunInterface.(DarwinTUN); isDarwinTUN { + return &systemWritebackDarwin{darwinTUN: darwinTUN, frontHeadroom: frontHeadroom} + } + return &systemWriteback{tun: tunInterface, frontHeadroom: frontHeadroom} +} + +type systemWritebackLinux struct { linuxTUN LinuxTUN frontHeadroom int } -func newSystemWriteback(tunInterface Tun, frontHeadroom int) *systemWriteback { - writeback := &systemWriteback{tun: tunInterface, frontHeadroom: frontHeadroom} - if linuxTUN, isLinuxTUN := tunInterface.(LinuxTUN); isLinuxTUN { - writeback.linuxTUN = linuxTUN +func (w *systemWritebackLinux) ReturnHeadroom() int { + return w.frontHeadroom + PacketOffset +} + +func (w *systemWritebackLinux) WriteReturnPackets(packets [][]byte) error { + return common.Error(w.linuxTUN.BatchWrite(packets, w.frontHeadroom)) +} + +type systemWritebackDarwin struct { + darwinTUN DarwinTUN + frontHeadroom int +} + +func (w *systemWritebackDarwin) ReturnHeadroom() int { + return w.frontHeadroom + PacketOffset +} + +func (w *systemWritebackDarwin) WriteReturnPackets(packets [][]byte) error { + buffers := make([]*buf.Buffer, 0, len(packets)) + for _, packet := range packets { + buffers = append(buffers, buf.As(packet[PacketOffset:])) } - return writeback + return w.darwinTUN.BatchWrite(buffers) +} + +type systemWriteback struct { + tun Tun + frontHeadroom int } func (w *systemWriteback) ReturnHeadroom() int { @@ -769,9 +800,6 @@ func (w *systemWriteback) ReturnHeadroom() int { } func (w *systemWriteback) WriteReturnPackets(packets [][]byte) error { - if w.linuxTUN != nil { - return common.Error(w.linuxTUN.BatchWrite(packets, w.frontHeadroom)) - } var writeErrors []error for _, packet := range packets { if PacketOffset > 0 { diff --git a/tun_darwin.go b/tun_darwin.go index 8f7a186c..4b00ac59 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -6,6 +6,7 @@ import ( "net" "net/netip" "os" + "sync" "syscall" "unsafe" @@ -36,6 +37,8 @@ type NativeTun struct { msgHdrsOutput []rawfile.MsgHdrX buffers []*buf.Buffer stopFd stopfd.StopFD + readPoller *rawfile.Poller + writeAccess sync.Mutex options Options inet4Address [4]byte inet6Address [16]byte @@ -132,6 +135,7 @@ func New(options Options) (Tun, error) { stopFd: common.Must1(stopfd.New()), sendMsgX: options.EXP_SendMsgX, } + nativeTun.readPoller = common.Must1(rawfile.NewPoller(nativeTun.stopFd.ReadFD, tunFd)) for i := range batchSize { nativeTun.iovecs[i] = newIovecBuffer(int(options.MTU)) nativeTun.iovecsOutput[i] = newIovecBuffer(int(options.MTU)) @@ -155,15 +159,26 @@ func (t *NativeTun) Start() error { func (t *NativeTun) Close() error { if t.options.EXP_ExternalConfiguration { - return t.tunFile.Close() + t.stopFd.Stop() + err := t.tunFile.Close() + t.closePollers() + t.stopFd.Close() + return err } defer flushDNSCache() t.stopFd.Stop() err := E.Errors(t.unsetRoutes(), t.tunFile.Close()) + t.closePollers() t.stopFd.Close() return err } +func (t *NativeTun) closePollers() { + if t.readPoller != nil { + _ = t.readPoller.Close() + } +} + func (t *NativeTun) Read(p []byte) (n int, err error) { return t.tunFile.Read(p) } @@ -350,7 +365,7 @@ func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) { t.msgHdrs[i].Msg.Iov = &iovecs[0] t.msgHdrs[i].Msg.Iovlen = 2 } - n, errno := rawfile.BlockingRecvMMsgUntilStopped(t.stopFd.ReadFD, t.tunFd, t.msgHdrs) + n, errno := rawfile.BlockingRecvMMsgUntilStopped(t.readPoller, t.tunFd, t.msgHdrs) if errno != 0 { for k := range n { t.iovecs[k].buffer.Release() @@ -377,6 +392,23 @@ func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) { } func (t *NativeTun) BatchWrite(buffers []*buf.Buffer) error { + t.writeAccess.Lock() + defer t.writeAccess.Unlock() + for len(buffers) > 0 { + chunk := buffers + if len(chunk) > t.batchSize { + chunk = chunk[:t.batchSize] + } + buffers = buffers[len(chunk):] + err := t.batchWriteChunk(chunk) + if err != nil { + return err + } + } + return nil +} + +func (t *NativeTun) batchWriteChunk(buffers []*buf.Buffer) error { if !t.sendMsgX { for i, buffer := range buffers { t.iovecsOutput[i].nextIovecsOutput(buffer) From f6c89a831064e51792f73f5784b3a26b1c897753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 9 Jul 2026 21:28:10 +0800 Subject: [PATCH 22/24] ping: Fix stale flows kept alive by unrelated ICMP traffic Unconnected raw ICMP sockets receive every ICMP packet arriving at the host, so any ICMP traffic refreshed the read deadline of every flow and stale flows (with their raw sockets and goroutines) accumulated forever, making per-ping cost grow linearly with uptime. Expire flows based on their own activity only, and on Linux attach a classic BPF ident filter to each raw socket so other flows' packets are dropped in the kernel instead of waking every flow. --- ping/destination.go | 16 ++++++++++++++-- ping/destination_test.go | 2 +- ping/filter_linux.go | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ping/destination.go b/ping/destination.go index 1e5d5333..e9e65291 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -10,6 +10,7 @@ import ( "time" "github.com/sagernet/sing-tun/gtcpip/header" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" @@ -30,6 +31,7 @@ type Destination struct { destination netip.Addr writer PacketWriter timeout time.Duration + lastActive common.TypedValue[time.Time] requestAccess sync.Mutex requests map[pingRequest]time.Time } @@ -74,6 +76,7 @@ func ConnectDestination( timeout: timeout, requests: make(map[pingRequest]time.Time), } + d.lastActive.Store(time.Now()) go d.loopRead() return d, nil } @@ -81,14 +84,21 @@ func ConnectDestination( func (d *Destination) loopRead() { defer d.Close() for { - buffer := buf.NewSize(maxICMPPacketSize) - err := d.conn.SetReadDeadline(time.Now().Add(d.timeout)) + deadline := d.lastActive.Load().Add(d.timeout) + if !time.Now().Before(deadline) { + return + } + err := d.conn.SetReadDeadline(deadline) if err != nil { d.logger.ErrorContext(d.ctx, E.Cause(err, "set read deadline for ICMP conn")) } + buffer := buf.NewSize(maxICMPPacketSize) err = d.conn.ReadIP(buffer) if err != nil { buffer.Release() + if E.IsTimeout(err) { + continue + } if !E.IsClosed(err) { d.logger.ErrorContext(d.ctx, E.Cause(err, "receive ICMP echo reply")) } @@ -159,6 +169,7 @@ func (d *Destination) loopRead() { } d.logger.TraceContext(d.ctx, "read ICMPv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } + d.lastActive.Store(time.Now()) err = d.writer.WritePacket(buffer.Bytes()) if err != nil { d.logger.ErrorContext(d.ctx, E.Cause(err, "write ICMP echo reply")) @@ -168,6 +179,7 @@ func (d *Destination) loopRead() { } func (d *Destination) WritePacket(packet *buf.Buffer) error { + d.lastActive.Store(time.Now()) if !d.destination.Is6() { ipHdr := header.IPv4(packet.Bytes()) if !ipHdr.IsValid(packet.Len()) { diff --git a/ping/destination_test.go b/ping/destination_test.go index db711395..a09f1464 100644 --- a/ping/destination_test.go +++ b/ping/destination_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing-tun/ping" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/logger" diff --git a/ping/filter_linux.go b/ping/filter_linux.go index 13f32a38..2a1d66da 100644 --- a/ping/filter_linux.go +++ b/ping/filter_linux.go @@ -4,7 +4,7 @@ import ( "sync" "syscall" - "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" From 8a2ff73b807453faf10156883454c2e69880d485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 9 Jul 2026 22:18:58 +0800 Subject: [PATCH 23/24] Fix logger check --- tun_linux.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tun_linux.go b/tun_linux.go index 940b7d03..97dc6456 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -205,14 +205,18 @@ func (t *NativeTun) enableGSO() error { err = setTCPOffload(t.tunFd) if err != nil { if !(errors.Is(err, unix.EPERM) && t.options.FileDescriptor != 0) { - t.options.Logger.Warn(E.Cause(err, "enable offload: set tcp offload")) + if t.options.Logger != nil { + t.options.Logger.Warn(E.Cause(err, "enable offload: set tcp offload")) + } t.gro.disableTCPGRO() t.gro.disableUDPGRO() } } else { err = setUDPOffload(t.tunFd) if err != nil { - t.options.Logger.Warn(E.Cause(err, "enable offload: set udp offload")) + if t.options.Logger != nil { + t.options.Logger.Warn(E.Cause(err, "enable offload: set udp offload")) + } t.gro.disableUDPGRO() } } From 9432f470c6a50e5857361b4b62c7e7f8dfc90026 Mon Sep 17 00:00:00 2001 From: nekohasekai Date: Sat, 11 Jul 2026 17:21:24 +0100 Subject: [PATCH 24/24] Avoid Docker firewall policy bypass --- redirect_nftables.go | 6 ---- redirect_nftables_docker.go | 27 ++------------- redirect_nftables_docker_test.go | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 31 deletions(-) create mode 100644 redirect_nftables_docker_test.go diff --git a/redirect_nftables.go b/redirect_nftables.go index f17e1f36..a2940871 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -287,12 +287,6 @@ func (r *autoRedirect) setupNFTables() error { if err != nil { return E.Cause(err, "flush nftables") } - r.startDockerFirewallMonitor() - err = r.configureDockerFirewall(false) - if err != nil && r.logger != nil { - r.logger.Warn("configure docker firewall: ", err) - } - r.networkListener = r.networkMonitor.RegisterCallback(func() { err = r.nftablesUpdateLocalAddressSet() if err != nil { diff --git a/redirect_nftables_docker.go b/redirect_nftables_docker.go index 23b2a52c..0276e2e8 100644 --- a/redirect_nftables_docker.go +++ b/redirect_nftables_docker.go @@ -177,31 +177,8 @@ func (r *autoRedirect) cleanupDockerFirewallRules(nft *nftables.Conn, rules []*n return deleteErr } -func (r *autoRedirect) reconcileDockerFirewallRules(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, rules []*nftables.Rule) error { - outputComment := r.nftablesDockerCompatibilityComment("output to tun") - inputComment := r.nftablesDockerCompatibilityComment("input from tun") - var hasOutputRule bool - var hasInputRule bool - var deleteErr error - for _, rule := range rules { - if nftablesDockerCompatibilityRuleMatches(rule, r.tunOptions.Name, expr.MetaKeyOIFNAME, outputComment) && !hasOutputRule { - hasOutputRule = true - } else if nftablesDockerCompatibilityRuleMatches(rule, r.tunOptions.Name, expr.MetaKeyIIFNAME, inputComment) && !hasInputRule { - hasInputRule = true - } else if r.nftablesIsDockerCompatibilityRule(rule) { - deleteErr = E.Errors(deleteErr, nft.DelRule(rule)) - } - } - if deleteErr != nil { - return deleteErr - } - if !hasOutputRule { - nft.InsertRule(nftablesDockerCompatibilityRule(table, chain, r.tunOptions.Name, expr.MetaKeyOIFNAME, outputComment)) - } - if !hasInputRule { - nft.InsertRule(nftablesDockerCompatibilityRule(table, chain, r.tunOptions.Name, expr.MetaKeyIIFNAME, inputComment)) - } - return nil +func (r *autoRedirect) reconcileDockerFirewallRules(nft *nftables.Conn, _ *nftables.Table, _ *nftables.Chain, rules []*nftables.Rule) error { + return r.cleanupDockerFirewallRules(nft, rules) } func nftablesDockerCompatibilityRule(table *nftables.Table, chain *nftables.Chain, ifName string, ifNameKey expr.MetaKey, comment string) *nftables.Rule { diff --git a/redirect_nftables_docker_test.go b/redirect_nftables_docker_test.go new file mode 100644 index 00000000..c4d1cf9f --- /dev/null +++ b/redirect_nftables_docker_test.go @@ -0,0 +1,57 @@ +//go:build linux + +package tun + +import ( + "reflect" + "testing" + + "github.com/sagernet/nftables" + "github.com/sagernet/nftables/expr" +) + +func TestDockerFirewallReconcileDoesNotInsertAcceptRules(t *testing.T) { + r := &autoRedirect{ + tunOptions: &Options{Name: "tun0"}, + tableName: "sing-tun", + } + nft, err := nftables.New() + if err != nil { + t.Fatal(err) + } + table := &nftables.Table{Name: nftablesDockerFilterTable, Family: nftables.TableFamilyIPv4} + chain := &nftables.Chain{Name: nftablesDockerUserChain, Table: table} + + if err := r.reconcileDockerFirewallRules(nft, table, chain, nil); err != nil { + t.Fatal(err) + } + if messages := nftablesConnMessageCount(nft); messages != 0 { + t.Fatalf("reconcile queued %d netlink messages; want 0", messages) + } +} + +func TestDockerFirewallReconcileCleansExistingCompatibilityRules(t *testing.T) { + r := &autoRedirect{ + tunOptions: &Options{Name: "tun0"}, + tableName: "sing-tun", + } + nft, err := nftables.New() + if err != nil { + t.Fatal(err) + } + table := &nftables.Table{Name: nftablesDockerFilterTable, Family: nftables.TableFamilyIPv4} + chain := &nftables.Chain{Name: nftablesDockerUserChain, Table: table} + rule := nftablesDockerCompatibilityRule(table, chain, "tun0", expr.MetaKeyOIFNAME, r.nftablesDockerCompatibilityComment("output to tun")) + rule.Handle = 1 + + if err := r.reconcileDockerFirewallRules(nft, table, chain, []*nftables.Rule{rule}); err != nil { + t.Fatal(err) + } + if messages := nftablesConnMessageCount(nft); messages != 1 { + t.Fatalf("reconcile queued %d netlink messages; want 1 cleanup delete", messages) + } +} + +func nftablesConnMessageCount(nft *nftables.Conn) int { + return reflect.ValueOf(nft).Elem().FieldByName("messages").Len() +}