From d093b820646af310bba5a87686c07624879caff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 7 Jan 2025 19:01:54 +0800 Subject: [PATCH 001/121] auto-redirect: Fetch interfaces --- redirect_nftables.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/redirect_nftables.go b/redirect_nftables.go index be861145..02a9ca8c 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -32,6 +32,10 @@ func (r *autoRedirect) setupNFTables() error { return err } + err = r.interfaceFinder.Update() + if err != nil { + return err + } r.localAddresses = common.FlatMap(r.interfaceFinder.Interfaces(), func(it control.Interface) []netip.Prefix { return common.Filter(it.Addresses, func(prefix netip.Prefix) bool { return it.Name == "lo" || prefix.Addr().IsGlobalUnicast() From 8cc5351bb35ea37cef0de3f90970dc2f47c79672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 6 Feb 2025 08:43:50 +0800 Subject: [PATCH 002/121] auto-redirect: Move initialize to start --- redirect_linux.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/redirect_linux.go b/redirect_linux.go index 1645b851..113d6f1b 100644 --- a/redirect_linux.go +++ b/redirect_linux.go @@ -44,7 +44,7 @@ type autoRedirect struct { } func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { - r := &autoRedirect{ + return &autoRedirect{ tunOptions: options.TunOptions, ctx: options.Context, handler: options.Handler, @@ -56,7 +56,10 @@ func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { customRedirectPortFunc: options.CustomRedirectPort, routeAddressSet: options.RouteAddressSet, routeExcludeAddressSet: options.RouteExcludeAddressSet, - } + }, nil +} + +func (r *autoRedirect) Start() error { var err error if runtime.GOOS == "android" { r.enableIPv4 = true @@ -74,7 +77,7 @@ func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { } } if err != nil { - return nil, E.Extend(E.Cause(err, "root permission is required for auto redirect"), os.Getenv("PATH")) + return E.Extend(E.Cause(err, "root permission is required for auto redirect"), os.Getenv("PATH")) } } } else { @@ -90,7 +93,7 @@ func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { if !r.useNFTables { r.iptablesPath, err = exec.LookPath("iptables") if err != nil { - return nil, E.Cause(err, "iptables is required") + return E.Cause(err, "iptables is required") } } } @@ -100,7 +103,7 @@ func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { r.ip6tablesPath, err = exec.LookPath("ip6tables") if err != nil { if !r.enableIPv4 { - return nil, E.Cause(err, "ip6tables is required") + return E.Cause(err, "ip6tables is required") } else { r.enableIPv6 = false r.logger.Error("device has no ip6tables nat support: ", err) @@ -109,10 +112,6 @@ func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { } } } - return r, nil -} - -func (r *autoRedirect) Start() error { if r.customRedirectPortFunc != nil { r.customRedirectPort = r.customRedirectPortFunc() } @@ -132,7 +131,6 @@ func (r *autoRedirect) Start() error { } r.redirectServer = server } - var err error if r.useNFTables { r.cleanupNFTables() err = r.setupNFTables() From c8c29842618b186b8eb802345504cc19d3d06872 Mon Sep 17 00:00:00 2001 From: bemarkt Date: Sat, 7 Dec 2024 16:40:31 +0800 Subject: [PATCH 003/121] Fix wrong parameter in ICMPv4Checksum --- stack_system.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack_system.go b/stack_system.go index e7b68ddc..e48b8b61 100644 --- a/stack_system.go +++ b/stack_system.go @@ -586,7 +586,7 @@ func (s *System) processIPv4ICMP(ipHdr header.IPv4, icmpHdr header.ICMPv4) error sourceAddress := ipHdr.SourceAddr() ipHdr.SetSourceAddr(ipHdr.DestinationAddr()) ipHdr.SetDestinationAddr(sourceAddress) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, checksum.Checksum(icmpHdr.Payload(), 0))) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) ipHdr.SetChecksum(0) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) return nil From 618be14c7baa8fa851205e89bc28902aabf39ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 17 Feb 2025 21:56:07 +0800 Subject: [PATCH 004/121] Fix generate darwin rules --- tun_darwin.go | 58 ++++++++++++++++++++++---------------------- tun_rules.go | 66 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 71 insertions(+), 53 deletions(-) diff --git a/tun_darwin.go b/tun_darwin.go index e887ce84..a0dd54a9 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -268,45 +268,47 @@ func (t *NativeTun) UpdateRouteOptions(tunOptions Options) error { } func (t *NativeTun) setRoutes() error { - if t.options.AutoRoute && t.options.FileDescriptor == 0 { + if t.options.FileDescriptor == 0 { routeRanges, err := t.options.BuildAutoRouteRanges(false) if err != nil { return err } - gateway4, gateway6 := t.options.Inet4GatewayAddr(), t.options.Inet6GatewayAddr() - for _, destination := range routeRanges { - var gateway netip.Addr - if destination.Addr().Is4() { - gateway = gateway4 - } else { - gateway = gateway6 - } - var interfaceIndex int - if t.options.InterfaceScope { - iff, err := t.options.InterfaceFinder.ByName(t.options.Name) - if err != nil { - return err + if len(routeRanges) > 0 { + gateway4, gateway6 := t.options.Inet4GatewayAddr(), t.options.Inet6GatewayAddr() + for _, destination := range routeRanges { + var gateway netip.Addr + if destination.Addr().Is4() { + gateway = gateway4 + } else { + gateway = gateway6 } - interfaceIndex = iff.Index - } - err = execRoute(unix.RTM_ADD, t.options.InterfaceScope, interfaceIndex, destination, gateway) - if err != nil { - if errors.Is(err, unix.EEXIST) { - err = execRoute(unix.RTM_DELETE, false, 0, destination, gateway) + var interfaceIndex int + if t.options.InterfaceScope { + iff, err := t.options.InterfaceFinder.ByName(t.options.Name) if err != nil { - return E.Cause(err, "remove existing route: ", destination) + return err } - err = execRoute(unix.RTM_ADD, t.options.InterfaceScope, interfaceIndex, destination, gateway) - if err != nil { - return E.Cause(err, "re-add route: ", destination) + interfaceIndex = iff.Index + } + err = execRoute(unix.RTM_ADD, t.options.InterfaceScope, interfaceIndex, destination, gateway) + if err != nil { + if errors.Is(err, unix.EEXIST) { + err = execRoute(unix.RTM_DELETE, false, 0, destination, gateway) + if err != nil { + return E.Cause(err, "remove existing route: ", destination) + } + err = execRoute(unix.RTM_ADD, t.options.InterfaceScope, interfaceIndex, destination, gateway) + if err != nil { + return E.Cause(err, "re-add route: ", destination) + } + } else { + return E.Cause(err, "add route: ", destination) } - } else { - return E.Cause(err, "add route: ", destination) } } + flushDNSCache() + t.routeSet = true } - flushDNSCache() - t.routeSet = true } return nil } diff --git a/tun_rules.go b/tun_rules.go index 93b0430b..c1b983f1 100644 --- a/tun_rules.go +++ b/tun_rules.go @@ -108,7 +108,7 @@ const autoRouteUseSubRanges = runtime.GOOS == "darwin" func (o *Options) BuildAutoRouteRanges(underNetworkExtension bool) ([]netip.Prefix, error) { var routeRanges []netip.Prefix - if o.AutoRoute && len(o.Inet4Address) > 0 { + if len(o.Inet4Address) > 0 { var inet4Ranges []netip.Prefix if len(o.Inet4RouteAddress) > 0 { inet4Ranges = o.Inet4RouteAddress @@ -119,19 +119,27 @@ func (o *Options) BuildAutoRouteRanges(underNetworkExtension bool) ([]netip.Pref } } } - } else if autoRouteUseSubRanges && !underNetworkExtension { - inet4Ranges = []netip.Prefix{ - netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 1}), 8), - netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 2}), 7), - netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 4}), 6), - netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 8}), 5), - netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 16}), 4), - netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 32}), 3), - netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 64}), 2), - netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 128}), 1), + } else if o.AutoRoute { + if autoRouteUseSubRanges && !underNetworkExtension { + inet4Ranges = []netip.Prefix{ + netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 1}), 8), + netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 2}), 7), + netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 4}), 6), + netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 8}), 5), + netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 16}), 4), + netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 32}), 3), + netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 64}), 2), + netip.PrefixFrom(netip.AddrFrom4([4]byte{0: 128}), 1), + } + } else { + inet4Ranges = []netip.Prefix{netip.PrefixFrom(netip.IPv4Unspecified(), 0)} + } + } else if runtime.GOOS == "darwin" { + for _, address := range o.Inet4Address { + if address.Bits() < 32 { + inet4Ranges = append(inet4Ranges, address.Masked()) + } } - } else { - inet4Ranges = []netip.Prefix{netip.PrefixFrom(netip.IPv4Unspecified(), 0)} } if len(o.Inet4RouteExcludeAddress) == 0 { routeRanges = append(routeRanges, inet4Ranges...) @@ -161,19 +169,27 @@ func (o *Options) BuildAutoRouteRanges(underNetworkExtension bool) ([]netip.Pref } } } - } else if autoRouteUseSubRanges && !underNetworkExtension { - inet6Ranges = []netip.Prefix{ - netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 1}), 8), - netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 2}), 7), - netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 4}), 6), - netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 8}), 5), - netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 16}), 4), - netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 32}), 3), - netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 64}), 2), - netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 128}), 1), + } else if o.AutoRoute { + if autoRouteUseSubRanges && !underNetworkExtension { + inet6Ranges = []netip.Prefix{ + netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 1}), 8), + netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 2}), 7), + netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 4}), 6), + netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 8}), 5), + netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 16}), 4), + netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 32}), 3), + netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 64}), 2), + netip.PrefixFrom(netip.AddrFrom16([16]byte{0: 128}), 1), + } + } else { + inet6Ranges = []netip.Prefix{netip.PrefixFrom(netip.IPv6Unspecified(), 0)} + } + } else if runtime.GOOS == "darwin" { + for _, address := range o.Inet6Address { + if address.Bits() < 32 { + inet6Ranges = append(inet6Ranges, address.Masked()) + } } - } else { - inet6Ranges = []netip.Prefix{netip.PrefixFrom(netip.IPv6Unspecified(), 0)} } if len(o.Inet6RouteExcludeAddress) == 0 { routeRanges = append(routeRanges, inet6Ranges...) From 22b811f938fdf52b6fb4ef317568f21389a683e1 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Wed, 12 Mar 2025 12:36:26 +0800 Subject: [PATCH 005/121] Remove gvisor's buffer dependence of gtcpip --- .../gtcpip/header/ipv6_extension_headers.go | 448 ------------------ 1 file changed, 448 deletions(-) diff --git a/internal/gtcpip/header/ipv6_extension_headers.go b/internal/gtcpip/header/ipv6_extension_headers.go index 3ab135d7..20064d8b 100644 --- a/internal/gtcpip/header/ipv6_extension_headers.go +++ b/internal/gtcpip/header/ipv6_extension_headers.go @@ -18,10 +18,8 @@ import ( "encoding/binary" "errors" "fmt" - "io" "math" - "github.com/sagernet/gvisor/pkg/buffer" "github.com/sagernet/sing-tun/internal/gtcpip" "github.com/sagernet/sing/common" ) @@ -145,79 +143,6 @@ func ipv6OptionsAlignmentPadding(headerOffset int, align int, alignOffset int) i return ((padLen + align - 1) & ^(align - 1)) - padLen } -// IPv6PayloadHeader is implemented by the various headers that can be found -// in an IPv6 payload. -// -// These headers include IPv6 extension headers or upper layer data. -type IPv6PayloadHeader interface { - isIPv6PayloadHeader() - - // Release frees all resources held by the header. - Release() -} - -// IPv6RawPayloadHeader the remainder of an IPv6 payload after an iterator -// encounters a Next Header field it does not recognize as an IPv6 extension -// header. The caller is responsible for releasing the underlying buffer after -// it's no longer needed. -type IPv6RawPayloadHeader struct { - Identifier IPv6ExtensionHeaderIdentifier - Buf buffer.Buffer -} - -// isIPv6PayloadHeader implements IPv6PayloadHeader.isIPv6PayloadHeader. -func (IPv6RawPayloadHeader) isIPv6PayloadHeader() {} - -// Release implements IPv6PayloadHeader.Release. -func (i IPv6RawPayloadHeader) Release() { - i.Buf.Release() -} - -// ipv6OptionsExtHdr is an IPv6 extension header that holds options. -type ipv6OptionsExtHdr struct { - buf *buffer.View -} - -// Release implements IPv6PayloadHeader.Release. -func (i ipv6OptionsExtHdr) Release() { - if i.buf != nil { - i.buf.Release() - } -} - -// Iter returns an iterator over the IPv6 extension header options held in b. -func (i ipv6OptionsExtHdr) Iter() IPv6OptionsExtHdrOptionsIterator { - it := IPv6OptionsExtHdrOptionsIterator{} - it.reader = i.buf - return it -} - -// IPv6OptionsExtHdrOptionsIterator is an iterator over IPv6 extension header -// options. -// -// Note, between when an IPv6OptionsExtHdrOptionsIterator is obtained and last -// used, no changes to the underlying buffer may happen. Doing so may cause -// undefined and unexpected behaviour. It is fine to obtain an -// IPv6OptionsExtHdrOptionsIterator, iterate over the first few options then -// modify the backing payload so long as the IPv6OptionsExtHdrOptionsIterator -// obtained before modification is no longer used. -type IPv6OptionsExtHdrOptionsIterator struct { - reader *buffer.View - - // optionOffset is the number of bytes from the first byte of the - // options field to the beginning of the current option. - optionOffset uint32 - - // nextOptionOffset is the offset of the next option. - nextOptionOffset uint32 -} - -// OptionOffset returns the number of bytes parsed while processing the -// option field of the current Extension Header. -func (i *IPv6OptionsExtHdrOptionsIterator) OptionOffset() uint32 { - return i.optionOffset -} - // IPv6OptionUnknownAction is the action that must be taken if the processing // IPv6 node does not recognize the option, as outlined in RFC 8200 section 4.2. type IPv6OptionUnknownAction int @@ -294,143 +219,6 @@ func ipv6UnknownActionFromIdentifier(id IPv6ExtHdrOptionIdentifier) IPv6OptionUn // is malformed. var ErrMalformedIPv6ExtHdrOption = errors.New("malformed IPv6 extension header option") -// IPv6UnknownExtHdrOption holds the identifier and data for an IPv6 extension -// header option that is unknown by the parsing utilities. -type IPv6UnknownExtHdrOption struct { - Identifier IPv6ExtHdrOptionIdentifier - Data *buffer.View -} - -// UnknownAction implements IPv6OptionUnknownAction.UnknownAction. -func (o *IPv6UnknownExtHdrOption) UnknownAction() IPv6OptionUnknownAction { - return ipv6UnknownActionFromIdentifier(o.Identifier) -} - -// isIPv6ExtHdrOption implements IPv6ExtHdrOption.isIPv6ExtHdrOption. -func (*IPv6UnknownExtHdrOption) isIPv6ExtHdrOption() {} - -// Next returns the next option in the options data. -// -// If the next item is not a known extension header option, -// IPv6UnknownExtHdrOption will be returned with the option identifier and data. -// -// The return is of the format (option, done, error). done will be true when -// Next is unable to return anything because the iterator has reached the end of -// the options data, or an error occurred. -func (i *IPv6OptionsExtHdrOptionsIterator) Next() (IPv6ExtHdrOption, bool, error) { - for { - i.optionOffset = i.nextOptionOffset - temp, err := i.reader.ReadByte() - if err != nil { - // If we can't read the first byte of a new option, then we know the - // options buffer has been exhausted and we are done iterating. - return nil, true, nil - } - id := IPv6ExtHdrOptionIdentifier(temp) - - // If the option identifier indicates the option is a Pad1 option, then we - // know the option does not have Length and Data fields. End processing of - // the Pad1 option and continue processing the buffer as a new option. - if id == ipv6Pad1ExtHdrOptionIdentifier { - i.nextOptionOffset = i.optionOffset + 1 - continue - } - - length, err := i.reader.ReadByte() - if err != nil { - if err != io.EOF { - // ReadByte should only ever return nil or io.EOF. - panic(fmt.Sprintf("unexpected error when reading the option's Length field for option with id = %d: %s", id, err)) - } - - // We use io.ErrUnexpectedEOF as exhausting the buffer is unexpected once - // we start parsing an option; we expect the reader to contain enough - // bytes for the whole option. - return nil, true, fmt.Errorf("error when reading the option's Length field for option with id = %d: %w", id, io.ErrUnexpectedEOF) - } - - // Do we have enough bytes in the reader for the next option? - if n := i.reader.Size(); n < int(length) { - // Consume the remaining buffer. - i.reader.TrimFront(i.reader.Size()) - - // We return the same error as if we failed to read a non-padding option - // so consumers of this iterator don't need to differentiate between - // padding and non-padding options. - return nil, true, fmt.Errorf("read %d out of %d option data bytes for option with id = %d: %w", n, length, id, io.ErrUnexpectedEOF) - } - - i.nextOptionOffset = i.optionOffset + uint32(length) + 1 /* option ID */ + 1 /* length byte */ - - switch id { - case ipv6PadNExtHdrOptionIdentifier: - // Special-case the variable length padding option to avoid a copy. - i.reader.TrimFront(int(length)) - continue - case ipv6RouterAlertHopByHopOptionIdentifier: - var routerAlertValue [ipv6RouterAlertPayloadLength]byte - if n, err := io.ReadFull(i.reader, routerAlertValue[:]); err != nil { - switch err { - case io.EOF, io.ErrUnexpectedEOF: - return nil, true, fmt.Errorf("got invalid length (%d) for router alert option (want = %d): %w", length, ipv6RouterAlertPayloadLength, ErrMalformedIPv6ExtHdrOption) - default: - return nil, true, fmt.Errorf("read %d out of %d option data bytes for router alert option: %w", n, ipv6RouterAlertPayloadLength, err) - } - } else if n != int(length) { - return nil, true, fmt.Errorf("got invalid length (%d) for router alert option (want = %d): %w", length, ipv6RouterAlertPayloadLength, ErrMalformedIPv6ExtHdrOption) - } - return &IPv6RouterAlertOption{Value: IPv6RouterAlertValue(binary.BigEndian.Uint16(routerAlertValue[:]))}, false, nil - default: - bytes := buffer.NewView(int(length)) - if n, err := io.CopyN(bytes, i.reader, int64(length)); err != nil { - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - - return nil, true, fmt.Errorf("read %d out of %d option data bytes for option with id = %d: %w", n, length, id, err) - } - return &IPv6UnknownExtHdrOption{Identifier: id, Data: bytes}, false, nil - } - } -} - -// IPv6HopByHopOptionsExtHdr is a buffer holding the Hop By Hop Options -// extension header. -type IPv6HopByHopOptionsExtHdr struct { - ipv6OptionsExtHdr -} - -// isIPv6PayloadHeader implements IPv6PayloadHeader.isIPv6PayloadHeader. -func (IPv6HopByHopOptionsExtHdr) isIPv6PayloadHeader() {} - -// IPv6DestinationOptionsExtHdr is a buffer holding the Destination Options -// extension header. -type IPv6DestinationOptionsExtHdr struct { - ipv6OptionsExtHdr -} - -// isIPv6PayloadHeader implements IPv6PayloadHeader.isIPv6PayloadHeader. -func (IPv6DestinationOptionsExtHdr) isIPv6PayloadHeader() {} - -// IPv6RoutingExtHdr is a buffer holding the Routing extension header specific -// data as outlined in RFC 8200 section 4.4. -type IPv6RoutingExtHdr struct { - Buf *buffer.View -} - -// isIPv6PayloadHeader implements IPv6PayloadHeader.isIPv6PayloadHeader. -func (IPv6RoutingExtHdr) isIPv6PayloadHeader() {} - -// Release implements IPv6PayloadHeader.Release. -func (b IPv6RoutingExtHdr) Release() { - b.Buf.Release() -} - -// SegmentsLeft returns the Segments Left field. -func (b IPv6RoutingExtHdr) SegmentsLeft() uint8 { - return b.Buf.AsSlice()[ipv6RoutingExtHdrSegmentsLeftIdx] -} - // IPv6FragmentExtHdr is a buffer holding the Fragment extension header specific // data as outlined in RFC 8200 section 4.5. // @@ -473,242 +261,6 @@ func (b IPv6FragmentExtHdr) IsAtomic() bool { return !b.More() && b.FragmentOffset() == 0 } -// IPv6PayloadIterator is an iterator over the contents of an IPv6 payload. -// -// The IPv6 payload may contain IPv6 extension headers before any upper layer -// data. -// -// Note, between when an IPv6PayloadIterator is obtained and last used, no -// changes to the payload may happen. Doing so may cause undefined and -// unexpected behaviour. It is fine to obtain an IPv6PayloadIterator, iterate -// over the first few headers then modify the backing payload so long as the -// IPv6PayloadIterator obtained before modification is no longer used. -type IPv6PayloadIterator struct { - // The identifier of the next header to parse. - nextHdrIdentifier IPv6ExtensionHeaderIdentifier - - payload buffer.Buffer - - // Indicates to the iterator that it should return the remaining payload as a - // raw payload on the next call to Next. - forceRaw bool - - // headerOffset is the offset of the beginning of the current extension - // header starting from the beginning of the fixed header. - headerOffset uint32 - - // parseOffset is the byte offset into the current extension header of the - // field we are currently examining. It can be added to the header offset - // if the absolute offset within the packet is required. - parseOffset uint32 - - // nextOffset is the offset of the next header. - nextOffset uint32 -} - -// HeaderOffset returns the offset to the start of the extension -// header most recently processed. -func (i IPv6PayloadIterator) HeaderOffset() uint32 { - return i.headerOffset -} - -// ParseOffset returns the number of bytes successfully parsed. -func (i IPv6PayloadIterator) ParseOffset() uint32 { - return i.headerOffset + i.parseOffset -} - -// MakeIPv6PayloadIterator returns an iterator over the IPv6 payload containing -// extension headers, or a raw payload if the payload cannot be parsed. The -// iterator takes ownership of the payload. -func MakeIPv6PayloadIterator(nextHdrIdentifier IPv6ExtensionHeaderIdentifier, payload buffer.Buffer) IPv6PayloadIterator { - return IPv6PayloadIterator{ - nextHdrIdentifier: nextHdrIdentifier, - payload: payload, - nextOffset: IPv6FixedHeaderSize, - } -} - -// Release frees the resources owned by the iterator. -func (i *IPv6PayloadIterator) Release() { - i.payload.Release() -} - -// AsRawHeader returns the remaining payload of i as a raw header and -// optionally consumes the iterator. -// -// If consume is true, calls to Next after calling AsRawHeader on i will -// indicate that the iterator is done. The returned header takes ownership of -// its payload. -func (i *IPv6PayloadIterator) AsRawHeader(consume bool) IPv6RawPayloadHeader { - identifier := i.nextHdrIdentifier - - var buf buffer.Buffer - if consume { - // Since we consume the iterator, we return the payload as is. - buf = i.payload - - // Mark i as done, but keep track of where we were for error reporting. - *i = IPv6PayloadIterator{ - nextHdrIdentifier: IPv6NoNextHeaderIdentifier, - headerOffset: i.headerOffset, - nextOffset: i.nextOffset, - } - } else { - buf = i.payload.Clone() - } - - return IPv6RawPayloadHeader{Identifier: identifier, Buf: buf} -} - -// Next returns the next item in the payload. -// -// If the next item is not a known IPv6 extension header, IPv6RawPayloadHeader -// will be returned with the remaining bytes and next header identifier. -// -// The return is of the format (header, done, error). done will be true when -// Next is unable to return anything because the iterator has reached the end of -// the payload, or an error occurred. -func (i *IPv6PayloadIterator) Next() (IPv6PayloadHeader, bool, error) { - i.headerOffset = i.nextOffset - i.parseOffset = 0 - // We could be forced to return i as a raw header when the previous header was - // a fragment extension header as the data following the fragment extension - // header may not be complete. - if i.forceRaw { - return i.AsRawHeader(true /* consume */), false, nil - } - - // Is the header we are parsing a known extension header? - switch i.nextHdrIdentifier { - case IPv6HopByHopOptionsExtHdrIdentifier: - nextHdrIdentifier, view, err := i.nextHeaderData(false /* fragmentHdr */, nil) - if err != nil { - return nil, true, err - } - - i.nextHdrIdentifier = nextHdrIdentifier - return IPv6HopByHopOptionsExtHdr{ipv6OptionsExtHdr{view}}, false, nil - case IPv6RoutingExtHdrIdentifier: - nextHdrIdentifier, view, err := i.nextHeaderData(false /* fragmentHdr */, nil) - if err != nil { - return nil, true, err - } - - i.nextHdrIdentifier = nextHdrIdentifier - return IPv6RoutingExtHdr{view}, false, nil - case IPv6FragmentExtHdrIdentifier: - var data [6]byte - // We ignore the returned bytes because we know the fragment extension - // header specific data will fit in data. - nextHdrIdentifier, _, err := i.nextHeaderData(true /* fragmentHdr */, data[:]) - if err != nil { - return nil, true, err - } - - fragmentExtHdr := IPv6FragmentExtHdr(data) - - // If the packet is not the first fragment, do not attempt to parse anything - // after the fragment extension header as the payload following the fragment - // extension header should not contain any headers; the first fragment must - // hold all the headers up to and including any upper layer headers, as per - // RFC 8200 section 4.5. - if fragmentExtHdr.FragmentOffset() != 0 { - i.forceRaw = true - } - - i.nextHdrIdentifier = nextHdrIdentifier - return fragmentExtHdr, false, nil - case IPv6DestinationOptionsExtHdrIdentifier: - nextHdrIdentifier, view, err := i.nextHeaderData(false /* fragmentHdr */, nil) - if err != nil { - return nil, true, err - } - - i.nextHdrIdentifier = nextHdrIdentifier - return IPv6DestinationOptionsExtHdr{ipv6OptionsExtHdr{view}}, false, nil - case IPv6NoNextHeaderIdentifier: - // This indicates the end of the IPv6 payload. - return nil, true, nil - - default: - // The header we are parsing is not a known extension header. Return the - // raw payload. - return i.AsRawHeader(true /* consume */), false, nil - } -} - -// NextHeaderIdentifier returns the identifier of the header next returned by -// it.Next(). -func (i *IPv6PayloadIterator) NextHeaderIdentifier() IPv6ExtensionHeaderIdentifier { - return i.nextHdrIdentifier -} - -// nextHeaderData returns the extension header's Next Header field and raw data. -// -// fragmentHdr indicates that the extension header being parsed is the Fragment -// extension header so the Length field should be ignored as it is Reserved -// for the Fragment extension header. -// -// If bytes is not nil, extension header specific data will be read into bytes -// if it has enough capacity. If bytes is provided but does not have enough -// capacity for the data, nextHeaderData will panic. -func (i *IPv6PayloadIterator) nextHeaderData(fragmentHdr bool, bytes []byte) (IPv6ExtensionHeaderIdentifier, *buffer.View, error) { - // We ignore the number of bytes read because we know we will only ever read - // at max 1 bytes since rune has a length of 1. If we read 0 bytes, the Read - // would return io.EOF to indicate that io.Reader has reached the end of the - // payload. - rdr := i.payload.AsBufferReader() - nextHdrIdentifier, err := rdr.ReadByte() - if err != nil { - return 0, nil, fmt.Errorf("error when reading the Next Header field for extension header with id = %d: %w", i.nextHdrIdentifier, err) - } - i.parseOffset++ - - var length uint8 - length, err = rdr.ReadByte() - if err != nil { - if fragmentHdr { - return 0, nil, fmt.Errorf("error when reading the Length field for extension header with id = %d: %w", i.nextHdrIdentifier, err) - } - - return 0, nil, fmt.Errorf("error when reading the Reserved field for extension header with id = %d: %w", i.nextHdrIdentifier, err) - } - if fragmentHdr { - length = 0 - } - - // Make parseOffset point to the first byte of the Extension Header - // specific data. - i.parseOffset++ - - // length is in 8 byte chunks but doesn't include the first one. - // See RFC 8200 for each header type, sections 4.3-4.6 and the requirement - // in section 4.8 for new extension headers at the top of page 24. - // [ Hdr Ext Len ] ... Length of the Destination Options header in 8-octet - // units, not including the first 8 octets. - i.nextOffset += uint32((length + 1) * ipv6ExtHdrLenBytesPerUnit) - - bytesLen := int(length)*ipv6ExtHdrLenBytesPerUnit + ipv6ExtHdrLenBytesExcluded - if fragmentHdr { - if n := len(bytes); n < bytesLen { - panic(fmt.Sprintf("bytes only has space for %d bytes but need space for %d bytes (length = %d) for extension header with id = %d", n, bytesLen, length, i.nextHdrIdentifier)) - } - if n, err := io.ReadFull(&rdr, bytes); err != nil { - return 0, nil, fmt.Errorf("read %d out of %d extension header data bytes (length = %d) for header with id = %d: %w", n, bytesLen, length, i.nextHdrIdentifier, err) - } - return IPv6ExtensionHeaderIdentifier(nextHdrIdentifier), nil, nil - } - v := buffer.NewView(bytesLen) - if n, err := io.CopyN(v, &rdr, int64(bytesLen)); err != nil { - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - v.Release() - return 0, nil, fmt.Errorf("read %d out of %d extension header data bytes (length = %d) for header with id = %d: %w", n, bytesLen, length, i.nextHdrIdentifier, err) - } - return IPv6ExtensionHeaderIdentifier(nextHdrIdentifier), v, nil -} - // IPv6SerializableExtHdr provides serialization for IPv6 extension // headers. type IPv6SerializableExtHdr interface { From 7f3343169a2ce923cd3d13329e30b804b0fd91aa Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Wed, 12 Mar 2025 15:58:38 +0800 Subject: [PATCH 006/121] Better atomic using --- monitor_android.go | 3 +-- monitor_darwin.go | 3 +-- monitor_linux_default.go | 3 +-- monitor_windows.go | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/monitor_android.go b/monitor_android.go index 2734c855..1c7e711b 100644 --- a/monitor_android.go +++ b/monitor_android.go @@ -51,12 +51,11 @@ func (m *defaultInterfaceMonitor) checkUpdate() error { return err } - oldInterface := m.defaultInterface.Load() newInterface, err := m.interfaceFinder.ByIndex(link.Attrs().Index) if err != nil { return E.Cause(err, "find updated interface: ", link.Attrs().Name) } - m.defaultInterface.Store(newInterface) + oldInterface := m.defaultInterface.Swap(newInterface) if oldInterface != nil && oldInterface.Equals(*newInterface) && oldVPNEnabled == m.androidVPNEnabled { return nil } diff --git a/monitor_darwin.go b/monitor_darwin.go index 88ea90c0..f937c37e 100644 --- a/monitor_darwin.go +++ b/monitor_darwin.go @@ -165,12 +165,11 @@ func (m *defaultInterfaceMonitor) checkUpdate() error { if defaultInterface == nil { return ErrNoRoute } - oldInterface := m.defaultInterface.Load() newInterface, err := m.interfaceFinder.ByIndex(defaultInterface.Index) if err != nil { return E.Cause(err, "find updated interface: ", defaultInterface.Name) } - m.defaultInterface.Store(newInterface) + oldInterface := m.defaultInterface.Swap(newInterface) if oldInterface != nil && oldInterface.Equals(*newInterface) { return nil } diff --git a/monitor_linux_default.go b/monitor_linux_default.go index e9cce1db..72ba1be3 100644 --- a/monitor_linux_default.go +++ b/monitor_linux_default.go @@ -25,12 +25,11 @@ func (m *defaultInterfaceMonitor) checkUpdate() error { return err } - oldInterface := m.defaultInterface.Load() newInterface, err := m.interfaceFinder.ByIndex(link.Attrs().Index) if err != nil { return E.Cause(err, "find updated interface: ", link.Attrs().Name) } - m.defaultInterface.Store(newInterface) + oldInterface := m.defaultInterface.Swap(newInterface) if oldInterface != nil && oldInterface.Equals(*newInterface) { return nil } diff --git a/monitor_windows.go b/monitor_windows.go index 179f0741..18a795fd 100644 --- a/monitor_windows.go +++ b/monitor_windows.go @@ -102,12 +102,11 @@ func (m *defaultInterfaceMonitor) checkUpdate() error { return ErrNoRoute } - oldInterface := m.defaultInterface.Load() newInterface, err := m.interfaceFinder.ByIndex(index) if err != nil { return E.Cause(err, "find updated interface: ", alias) } - m.defaultInterface.Store(newInterface) + oldInterface := m.defaultInterface.Swap(newInterface) if oldInterface != nil && oldInterface.Equals(*newInterface) { return nil } From 57aba1a5c42bd02cab45bc3b351a3a3a70a57ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 12 Mar 2025 16:24:42 +0800 Subject: [PATCH 007/121] Fix checksum bench --- internal/checksum_test/sum_bench_test.go | 2 +- internal/gtcpip/checksum/checksum.go | 5 +++++ internal/gtcpip/checksum/checksum_default.go | 3 +-- internal/gtcpip/checksum/checksum_unsafe.go | 2 -- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/internal/checksum_test/sum_bench_test.go b/internal/checksum_test/sum_bench_test.go index bfc07527..35ee021c 100644 --- a/internal/checksum_test/sum_bench_test.go +++ b/internal/checksum_test/sum_bench_test.go @@ -28,6 +28,6 @@ func BenchmarkGChecksum(b *testing.B) { } b.ResetTimer() for i := 0; i < b.N; i++ { - checksum.Checksum(packet[i%1000], 0) + checksum.ChecksumDefault(packet[i%1000], 0) } } diff --git a/internal/gtcpip/checksum/checksum.go b/internal/gtcpip/checksum/checksum.go index dfb4dd77..db03e649 100644 --- a/internal/gtcpip/checksum/checksum.go +++ b/internal/gtcpip/checksum/checksum.go @@ -38,3 +38,8 @@ func Combine(a, b uint16) uint16 { v := uint32(a) + uint32(b) return uint16(v + v>>16) } + +func ChecksumDefault(buf []byte, initial uint16) uint16 { + s, _ := calculateChecksum(buf, false, initial) + return s +} diff --git a/internal/gtcpip/checksum/checksum_default.go b/internal/gtcpip/checksum/checksum_default.go index ea4585e5..99a2d753 100644 --- a/internal/gtcpip/checksum/checksum_default.go +++ b/internal/gtcpip/checksum/checksum_default.go @@ -8,6 +8,5 @@ package checksum // // The initial checksum must have been computed on an even number of bytes. func Checksum(buf []byte, initial uint16) uint16 { - s, _ := calculateChecksum(buf, false, initial) - return s + return ChecksumDefault(buf, initial) } diff --git a/internal/gtcpip/checksum/checksum_unsafe.go b/internal/gtcpip/checksum/checksum_unsafe.go index 83f35c83..66b7ab67 100644 --- a/internal/gtcpip/checksum/checksum_unsafe.go +++ b/internal/gtcpip/checksum/checksum_unsafe.go @@ -1,5 +1,3 @@ -//go:build !amd64 - // Copyright 2023 The gVisor Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); From 9105485a50c23e2fd587bf6480bc094d72c2d9a6 Mon Sep 17 00:00:00 2001 From: greathongtu <69706194+greathongtu@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:36:05 +0800 Subject: [PATCH 008/121] Fix typo of 'Android' in ErrNetlinkBanned error message --- monitor_linux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitor_linux.go b/monitor_linux.go index e92f469e..86dd28b3 100644 --- a/monitor_linux.go +++ b/monitor_linux.go @@ -27,7 +27,7 @@ type networkUpdateMonitor struct { var ErrNetlinkBanned = E.New( "netlink socket in Android is banned by Google, " + "use the root or system (ADB) user to run sing-box, " + - "or switch to the sing-box Adnroid graphical interface client", + "or switch to the sing-box Android graphical interface client", ) func NewNetworkUpdateMonitor(logger logger.Logger) (NetworkUpdateMonitor, error) { From 5cb6d272881640d040deb97f74cc8f486e838c19 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Wed, 12 Mar 2025 16:51:28 +0800 Subject: [PATCH 009/121] Add bench test to makefile --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 3efdba99..f7a8532c 100644 --- a/Makefile +++ b/Makefile @@ -29,4 +29,5 @@ lint_install: test: go build -v . + go test -bench=. ./internal/checksum_test #go test -v . From 35b5747b44ec8355684c208531cad34bbbcd5c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 19 Mar 2025 20:34:23 +0800 Subject: [PATCH 010/121] Ignore UDP offload error --- tun_linux.go | 1 - 1 file changed, 1 deletion(-) diff --git a/tun_linux.go b/tun_linux.go index 8c7bac63..72aac6a5 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -202,7 +202,6 @@ func (t *NativeTun) enableGSO() error { err = setUDPOffload(t.tunFd) if err != nil { t.gro.disableUDPGRO() - return E.Cause(err, "enable UDP offload") } return nil } From a8ce3838bc56f0e733f70582b56403856fa3b5cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 2 Apr 2025 15:44:43 +0800 Subject: [PATCH 011/121] redirect: Only hijack DNS requests from local addresses --- redirect_nftables.go | 4 +++- redirect_nftables_rules.go | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/redirect_nftables.go b/redirect_nftables.go index 02a9ca8c..2f3adeac 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -101,7 +101,9 @@ func (r *autoRedirect) setupNFTables() error { } r.nftablesCreateUnreachable(nft, table, chainPreRouting) r.nftablesCreateRedirect(nft, table, chainPreRouting) - r.nftablesCreateMark(nft, table, chainPreRouting) + if r.tunOptions.AutoRedirectMarkMode { + r.nftablesCreateMark(nft, table, chainPreRouting) + } if r.tunOptions.AutoRedirectMarkMode { chainPreRoutingUDP := nft.AddChain(&nftables.Chain{ diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index a5840dc2..cc694c63 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -402,13 +402,13 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft if !r.tunOptions.EXP_DisableDNSHijack && ((chain.Hooknum == nftables.ChainHookPrerouting && chain.Type == nftables.ChainTypeNAT) || (r.tunOptions.AutoRedirectMarkMode && chain.Hooknum == nftables.ChainHookOutput && chain.Type == nftables.ChainTypeNAT)) { if r.enableIPv4 { - err := r.nftablesCreateDNSHijackRulesForFamily(nft, table, chain, nftables.TableFamilyIPv4) + err := r.nftablesCreateDNSHijackRulesForFamily(nft, table, chain, nftables.TableFamilyIPv4, 5, "inet4_local_address_set") if err != nil { return err } } if r.enableIPv6 { - err := r.nftablesCreateDNSHijackRulesForFamily(nft, table, chain, nftables.TableFamilyIPv6) + err := r.nftablesCreateDNSHijackRulesForFamily(nft, table, chain, nftables.TableFamilyIPv6, 6, "inet6_local_address_set") if err != nil { return err } @@ -553,7 +553,7 @@ func (r *autoRedirect) nftablesCreateRedirect( func (r *autoRedirect) nftablesCreateDNSHijackRulesForFamily( nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, - family nftables.TableFamily, + family nftables.TableFamily, setID uint32, setName string, ) error { ipProto := &nftables.Set{ Table: table, @@ -611,6 +611,33 @@ func (r *autoRedirect) nftablesCreateDNSHijackRulesForFamily( Data: nftablesIfname("lo"), }, ) + } else { + if family == nftables.TableFamilyIPv4 { + exprs = append(exprs, + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseNetworkHeader, + Offset: 12, + Len: 4, + }, + ) + } else { + exprs = append(exprs, + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseNetworkHeader, + Offset: 8, + Len: 16, + }, + ) + } + exprs = append(exprs, &expr.Lookup{ + SourceRegister: 1, + SetID: setID, + SetName: setName, + }) } exprs = append(exprs, &expr.Meta{ From 219c612399bef8c0ec3bb2007c00b4e67325e850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 8 Apr 2025 15:56:14 +0800 Subject: [PATCH 012/121] redirect: Fix UDP rules --- redirect_nftables.go | 60 +++++++++++++++++++++++++++++++++----- redirect_nftables_rules.go | 21 +++++++++++++ 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/redirect_nftables.go b/redirect_nftables.go index 2f3adeac..e89a246e 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -113,12 +113,6 @@ func (r *autoRedirect) setupNFTables() error { Priority: nftables.ChainPriorityRef(*nftables.ChainPriorityNATDest + 2), Type: nftables.ChainTypeFilter, }) - if r.enableIPv4 { - nftablesCreateExcludeDestinationIPSet(nft, table, chainPreRoutingUDP, 5, "inet4_local_address_set", nftables.TableFamilyIPv4, false) - } - if r.enableIPv6 { - nftablesCreateExcludeDestinationIPSet(nft, table, chainPreRoutingUDP, 6, "inet6_local_address_set", nftables.TableFamilyIPv6, false) - } nft.AddRule(&nftables.Rule{ Table: table, Chain: chainPreRoutingUDP, @@ -128,10 +122,28 @@ func (r *autoRedirect) setupNFTables() error { Register: 1, }, &expr.Cmp{ - Op: expr.CmpOpEq, + Op: expr.CmpOpNeq, Register: 1, Data: []byte{unix.IPPROTO_UDP}, }, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chainPreRoutingUDP, + Exprs: []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyIIFNAME, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpNeq, + Register: 1, + Data: nftablesIfname(r.tunOptions.Name), + }, &expr.Ct{ Key: expr.CtKeyMARK, Register: 1, @@ -149,6 +161,40 @@ func (r *autoRedirect) setupNFTables() error { &expr.Counter{}, }, }) + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chainPreRoutingUDP, + Exprs: []expr.Any{ + &expr.Ct{ + Key: expr.CtKeyMARK, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpNeq, + Register: 1, + Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectInputMark), + }, + &expr.Immediate{ + Register: 1, + Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectOutputMark), + }, + &expr.Meta{ + Key: expr.MetaKeyMARK, + Register: 1, + SourceRegister: true, + }, + &expr.Meta{ + Key: expr.MetaKeyMARK, + Register: 1, + }, + &expr.Ct{ + Key: expr.CtKeyMARK, + Register: 1, + SourceRegister: true, + }, + &expr.Counter{}, + }, + }) } err = r.configureOpenWRTFirewall4(nft, false) diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index cc694c63..83619174 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -138,6 +138,27 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft }, }, }) + if chain.Type == nftables.ChainTypeRoute { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Ct{ + Key: expr.CtKeyMARK, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectOutputMark), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } } if chain.Hooknum == nftables.ChainHookPrerouting { if len(r.tunOptions.IncludeInterface) > 0 { From d89ab3f20716768026de28640b669d51767eae06 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Wed, 9 Apr 2025 13:35:20 +0800 Subject: [PATCH 013/121] Fix IncludeInterface/ExcludeInterface priority --- tun_linux.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tun_linux.go b/tun_linux.go index 72aac6a5..6a3549d3 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -668,7 +668,7 @@ func (t *NativeTun) rules() []*netlink.Rule { } } if len(t.options.IncludeInterface) > 0 { - matchPriority := priority + 2*len(t.options.IncludeInterface) + 1 + matchPriority := priority + 2 for _, includeInterface := range t.options.IncludeInterface { if p4 { it = netlink.NewRule() @@ -677,7 +677,6 @@ func (t *NativeTun) rules() []*netlink.Rule { it.Goto = matchPriority it.Family = unix.AF_INET rules = append(rules, it) - priority++ } if p6 { it = netlink.NewRule() @@ -686,9 +685,14 @@ func (t *NativeTun) rules() []*netlink.Rule { it.Goto = matchPriority it.Family = unix.AF_INET6 rules = append(rules, it) - priority6++ } } + if p4 { + priority++ + } + if p6 { + priority6++ + } if p4 { it = netlink.NewRule() it.Priority = priority @@ -726,7 +730,6 @@ func (t *NativeTun) rules() []*netlink.Rule { it.Goto = nopPriority it.Family = unix.AF_INET rules = append(rules, it) - priority++ } if p6 { it = netlink.NewRule() @@ -735,9 +738,15 @@ func (t *NativeTun) rules() []*netlink.Rule { it.Goto = nopPriority it.Family = unix.AF_INET6 rules = append(rules, it) - priority6++ } } + + if p4 { + priority++ + } + if p6 { + priority6++ + } } if runtime.GOOS == "android" && t.options.InterfaceMonitor.AndroidVPNEnabled() { From c410f7050cf6c00ef2739a35711d21b66790960a Mon Sep 17 00:00:00 2001 From: dyhkwong <50692134+dyhkwong@users.noreply.github.com> Date: Wed, 9 Apr 2025 13:05:26 +0800 Subject: [PATCH 014/121] Add firewall rule for Profile ALL on Windows system stack --- stack_system.go | 6 ++---- stack_system_windows.go | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/stack_system.go b/stack_system.go index e48b8b61..5a301d5b 100644 --- a/stack_system.go +++ b/stack_system.go @@ -107,10 +107,7 @@ func (s *System) Start() error { } func (s *System) start() error { - err := fixWindowsFirewall() - if err != nil { - return E.Cause(err, "fix windows firewall for system stack") - } + _ = fixWindowsFirewall() var listener net.ListenConfig if s.bindInterface { listener.Control = control.Append(listener.Control, func(network, address string, conn syscall.RawConn) error { @@ -122,6 +119,7 @@ func (s *System) start() error { }) } var tcpListener net.Listener + var err error if s.inet4Address.IsValid() { for i := 0; i < 3; i++ { tcpListener, err = listener.Listen(s.ctx, "tcp4", net.JoinHostPort(s.inet4ServerAddress.String(), "0")) diff --git a/stack_system_windows.go b/stack_system_windows.go index ffa2a093..f6c66d0d 100644 --- a/stack_system_windows.go +++ b/stack_system_windows.go @@ -22,6 +22,7 @@ func fixWindowsFirewall() error { Protocol: winfw.NET_FW_IP_PROTOCOL_TCP, Direction: winfw.NET_FW_RULE_DIR_IN, Action: winfw.NET_FW_ACTION_ALLOW, + Profiles: winfw.NET_FW_PROFILE2_ALL, } _, err = winfw.FirewallRuleAddAdvanced(rule) return err From 31e29f93cceb82ab3c8d710b4359e5715887dd5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 11 Apr 2025 10:39:23 +0800 Subject: [PATCH 015/121] redirect: Remove iptables rules except basic output redirect for Android --- redirect_iptables.go | 202 ------------------------------------------- redirect_linux.go | 5 +- 2 files changed, 2 insertions(+), 205 deletions(-) diff --git a/redirect_iptables.go b/redirect_iptables.go index 59c2d1d1..2c6e2e29 100644 --- a/redirect_iptables.go +++ b/redirect_iptables.go @@ -3,12 +3,9 @@ package tun import ( - "net/netip" "os/exec" - "runtime" "strings" - "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" ) @@ -30,10 +27,7 @@ func (r *autoRedirect) setupIPTables() error { } func (r *autoRedirect) setupIPTablesForFamily(iptablesPath string) error { - tableNameInput := r.tableName + "-input" - tableNameForward := r.tableName + "-forward" tableNameOutput := r.tableName + "-output" - tableNamePreRouteing := r.tableName + "-prerouting" redirectPort := r.redirectPort() // OUTPUT err := r.runShell(iptablesPath, "-t nat -N", tableNameOutput) @@ -50,184 +44,6 @@ func (r *autoRedirect) setupIPTablesForFamily(iptablesPath string) error { if err != nil { return err } - if runtime.GOOS == "android" { - return nil - } - // INPUT - err = r.runShell(iptablesPath, "-N", tableNameInput) - if err != nil { - return err - } - err = r.runShell(iptablesPath, "-A", tableNameInput, - "-i", r.tunOptions.Name, "-j", "ACCEPT") - if err != nil { - return err - } - err = r.runShell(iptablesPath, "-A", tableNameInput, - "-o", r.tunOptions.Name, "-j", "ACCEPT") - if err != nil { - return err - } - err = r.runShell(iptablesPath, "-I INPUT -j", tableNameInput) - if err != nil { - return err - } - // FORWARD - err = r.runShell(iptablesPath, "-N", tableNameForward) - if err != nil { - return err - } - err = r.runShell(iptablesPath, "-A", tableNameForward, - "-i", r.tunOptions.Name, "-j", "ACCEPT") - if err != nil { - return err - } - err = r.runShell(iptablesPath, "-A", tableNameForward, - "-o", r.tunOptions.Name, "-j", "ACCEPT") - if err != nil { - return err - } - err = r.runShell(iptablesPath, "-I FORWARD -j", tableNameForward) - if err != nil { - return err - } - // PREROUTING - err = r.runShell(iptablesPath, "-t nat -N", tableNamePreRouteing) - if err != nil { - return err - } - var ( - routeAddress []netip.Prefix - routeExcludeAddress []netip.Prefix - ) - if iptablesPath == r.iptablesPath { - routeAddress = r.tunOptions.Inet4RouteAddress - routeExcludeAddress = r.tunOptions.Inet4RouteExcludeAddress - } else { - routeAddress = r.tunOptions.Inet6RouteAddress - routeExcludeAddress = r.tunOptions.Inet6RouteExcludeAddress - } - if len(routeAddress) > 0 && (len(r.tunOptions.IncludeInterface) > 0 || len(r.tunOptions.IncludeUID) > 0) { - return E.New("`*_route_address` is conflict with `include_interface` or `include_uid`") - } - err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, - "-i", r.tunOptions.Name, "-j RETURN") - if err != nil { - return err - } - for _, address := range routeExcludeAddress { - err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, - "-d", address.String(), "-j RETURN") - if err != nil { - return err - } - } - for _, name := range r.tunOptions.ExcludeInterface { - err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, - "-i", name, "-j RETURN") - if err != nil { - return err - } - } - for _, uid := range r.tunOptions.ExcludeUID { - err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, - "-m owner --uid-owner", uid, "-j RETURN") - if err != nil { - return err - } - } - if !r.tunOptions.EXP_DisableDNSHijack { - dnsServer := common.Find(r.tunOptions.DNSServers, func(it netip.Addr) bool { - return it.Is4() == (iptablesPath == r.iptablesPath) - }) - if !dnsServer.IsValid() { - if iptablesPath == r.iptablesPath { - 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() - } - } - } - if dnsServer.IsValid() { - if len(routeAddress) > 0 { - for _, address := range routeAddress { - err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, - "-d", address.String(), "-p udp --dport 53 -j DNAT --to", dnsServer) - if err != nil { - return err - } - } - } else if len(r.tunOptions.IncludeInterface) > 0 || len(r.tunOptions.IncludeUID) > 0 { - for _, name := range r.tunOptions.IncludeInterface { - err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, - "-i", name, "-p udp --dport 53 -j DNAT --to", dnsServer) - if err != nil { - return err - } - } - for _, uidRange := range r.tunOptions.IncludeUID { - for uid := uidRange.Start; uid <= uidRange.End; uid++ { - err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, - "-m owner --uid-owner", uid, "-p udp --dport 53 -j DNAT --to", dnsServer) - if err != nil { - return err - } - } - } - } else { - err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, - "-p udp --dport 53 -j DNAT --to", dnsServer) - if err != nil { - return err - } - } - } - } - - err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, "-m addrtype --dst-type LOCAL -j RETURN") - if err != nil { - return err - } - - if len(routeAddress) > 0 { - for _, address := range routeAddress { - err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, - "-d", address.String(), "-p tcp -j REDIRECT --to-ports", redirectPort) - if err != nil { - return err - } - } - } else if len(r.tunOptions.IncludeInterface) > 0 || len(r.tunOptions.IncludeUID) > 0 { - for _, name := range r.tunOptions.IncludeInterface { - err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, - "-i", name, "-p tcp -j REDIRECT --to-ports", redirectPort) - if err != nil { - return err - } - } - for _, uidRange := range r.tunOptions.IncludeUID { - for uid := uidRange.Start; uid <= uidRange.End; uid++ { - err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, - "-m owner --uid-owner", uid, "-p tcp -j REDIRECT --to-ports", redirectPort) - if err != nil { - return err - } - } - } - } else { - err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, - "-p tcp -j REDIRECT --to-ports", redirectPort) - if err != nil { - return err - } - } - err = r.runShell(iptablesPath, "-t nat -I PREROUTING -j", tableNamePreRouteing) - if err != nil { - return err - } return nil } @@ -241,29 +57,11 @@ func (r *autoRedirect) cleanupIPTables() { } func (r *autoRedirect) cleanupIPTablesForFamily(iptablesPath string) { - tableNameInput := r.tableName + "-input" tableNameOutput := r.tableName + "-output" - tableNameForward := r.tableName + "-forward" - tableNamePreRouteing := r.tableName + "-prerouting" _ = r.runShell(iptablesPath, "-t nat -D OUTPUT -j", tableNameOutput) _ = r.runShell(iptablesPath, "-t nat -F", tableNameOutput) _ = r.runShell(iptablesPath, "-t nat -X", tableNameOutput) - if runtime.GOOS == "android" { - return - } - - _ = r.runShell(iptablesPath, "-D INPUT -j", tableNameInput) - _ = r.runShell(iptablesPath, "-F", tableNameInput) - _ = r.runShell(iptablesPath, "-X", tableNameInput) - - _ = r.runShell(iptablesPath, "-D FORWARD -j", tableNameForward) - _ = r.runShell(iptablesPath, "-F", tableNameForward) - _ = r.runShell(iptablesPath, "-X", tableNameForward) - - _ = r.runShell(iptablesPath, "-t nat -D PREROUTING -j", tableNamePreRouteing) - _ = r.runShell(iptablesPath, "-t nat -F", tableNamePreRouteing) - _ = r.runShell(iptablesPath, "-t nat -X", tableNamePreRouteing) } func (r *autoRedirect) runShell(commands ...any) error { diff --git a/redirect_linux.go b/redirect_linux.go index 113d6f1b..5441bc10 100644 --- a/redirect_linux.go +++ b/redirect_linux.go @@ -83,9 +83,8 @@ func (r *autoRedirect) Start() error { } else { if r.useNFTables { err = r.initializeNFTables() - if err != nil && err != os.ErrInvalid { - r.useNFTables = false - r.logger.Debug("missing nftables support: ", err) + if err != nil { + return E.Cause(err, "missing nftables support") } } if len(r.tunOptions.Inet4Address) > 0 { From 51ac6b34f1923491ead2fb3a6ded8cd081132108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 12 Apr 2025 12:07:56 +0800 Subject: [PATCH 016/121] redirect: Fix handling of local pings --- redirect_nftables.go | 2 +- redirect_nftables_rules.go | 23 +++++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/redirect_nftables.go b/redirect_nftables.go index e89a246e..3545617f 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -64,7 +64,7 @@ func (r *autoRedirect) setupNFTables() error { r.nftablesCreateRedirect(nft, table, chainOutput) chainOutputUDP := nft.AddChain(&nftables.Chain{ - Name: "output_udp", + Name: "output_udp_icmp", Table: table, Hooknum: nftables.ChainHookOutput, Priority: nftables.ChainPriorityMangle, diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index 83619174..b12f7318 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -439,6 +439,20 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft if r.tunOptions.AutoRedirectMarkMode && ((chain.Hooknum == nftables.ChainHookOutput && chain.Type == nftables.ChainTypeRoute) || (chain.Hooknum == nftables.ChainHookPrerouting && chain.Type == nftables.ChainTypeFilter)) { + ipProto := &nftables.Set{ + Table: table, + Anonymous: true, + Constant: true, + KeyType: nftables.TypeInetProto, + } + err := nft.AddSet(ipProto, []nftables.SetElement{ + {Key: []byte{unix.IPPROTO_UDP}}, + {Key: []byte{unix.IPPROTO_ICMP}}, + {Key: []byte{unix.IPPROTO_ICMPV6}}, + }) + if err != nil { + return err + } nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, @@ -447,10 +461,11 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft Key: expr.MetaKeyL4PROTO, Register: 1, }, - &expr.Cmp{ - Op: expr.CmpOpNeq, - Register: 1, - Data: []byte{unix.IPPROTO_UDP}, + &expr.Lookup{ + SourceRegister: 1, + SetID: ipProto.ID, + SetName: ipProto.Name, + Invert: true, }, &expr.Verdict{ Kind: expr.VerdictReturn, From f13cd94aa0d3c438dfa638e1d481d3f545a3cf28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 28 Apr 2025 11:04:53 +0800 Subject: [PATCH 017/121] redirect: Fix counter position --- redirect_nftables_rules.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index b12f7318..4ab046ff 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -697,6 +697,7 @@ func (r *autoRedirect) nftablesCreateDNSHijackRulesForFamily( Register: 1, Data: binaryutil.BigEndian.PutUint16(53), }, + &expr.Counter{}, &expr.Immediate{ Register: 1, Data: dnsServer.AsSlice(), @@ -706,7 +707,6 @@ func (r *autoRedirect) nftablesCreateDNSHijackRulesForFamily( Family: uint32(family), RegAddrMin: 1, }, - &expr.Counter{}, ) nft.AddRule(&nftables.Rule{ Table: table, From 494b0ef8584894dc07e720b539f80a32ea0ef754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 29 May 2025 15:04:21 +0800 Subject: [PATCH 018/121] redirect: Fix unreachable --- redirect_nftables_rules.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index 4ab046ff..9c0767d7 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -742,9 +742,7 @@ func (r *autoRedirect) nftablesCreateUnreachable( Data: []byte{uint8(nfProto)}, }, &expr.Counter{}, - &expr.Verdict{ - Kind: expr.VerdictDrop, - }, + &expr.Reject{}, }, }) } From 3df19f464eeca407573880b4cf36c505d5df7056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 13 Jun 2025 11:35:09 +0800 Subject: [PATCH 019/121] Fix gLazyConn race --- stack_gvisor_lazy.go | 123 ++++++++++++++++++++++--------------------- 1 file changed, 64 insertions(+), 59 deletions(-) diff --git a/stack_gvisor_lazy.go b/stack_gvisor_lazy.go index 59c993b2..387b9538 100644 --- a/stack_gvisor_lazy.go +++ b/stack_gvisor_lazy.go @@ -6,6 +6,7 @@ import ( "context" "net" "os" + "sync" "time" "github.com/sagernet/gvisor/pkg/tcpip" @@ -17,19 +18,25 @@ import ( ) type gLazyConn struct { - tcpConn *gonet.TCPConn - parentCtx context.Context - stack *stack.Stack - request *tcp.ForwarderRequest - localAddr net.Addr - remoteAddr net.Addr - handshakeDone bool - handshakeErr error + tcpConn *gonet.TCPConn + parentCtx context.Context + stack *stack.Stack + request *tcp.ForwarderRequest + localAddr net.Addr + remoteAddr net.Addr + handshakeAccess sync.Mutex + handshakeDone bool + handshakeErr error } func (c *gLazyConn) HandshakeContext(ctx context.Context) error { if c.handshakeDone { - return nil + return c.handshakeErr + } + c.handshakeAccess.Lock() + defer c.handshakeAccess.Unlock() + if c.handshakeDone { + return c.handshakeErr } defer func() { c.handshakeDone = true @@ -64,6 +71,11 @@ func (c *gLazyConn) HandshakeContext(ctx context.Context) error { } func (c *gLazyConn) HandshakeFailure(err error) error { + if c.handshakeDone { + return os.ErrInvalid + } + c.handshakeAccess.Lock() + defer c.handshakeAccess.Unlock() if c.handshakeDone { return os.ErrInvalid } @@ -78,25 +90,17 @@ func (c *gLazyConn) HandshakeSuccess() error { } func (c *gLazyConn) Read(b []byte) (n int, err error) { - if !c.handshakeDone { - err = c.HandshakeContext(context.Background()) - if err != nil { - return - } - } else if c.handshakeErr != nil { - return 0, c.handshakeErr + err = c.HandshakeContext(context.Background()) + if err != nil { + return } return c.tcpConn.Read(b) } func (c *gLazyConn) Write(b []byte) (n int, err error) { - if !c.handshakeDone { - err = c.HandshakeContext(context.Background()) - if err != nil { - return - } - } else if c.handshakeErr != nil { - return 0, c.handshakeErr + err = c.HandshakeContext(context.Background()) + if err != nil { + return } return c.tcpConn.Write(b) } @@ -110,79 +114,80 @@ func (c *gLazyConn) RemoteAddr() net.Addr { } func (c *gLazyConn) SetDeadline(t time.Time) error { - if !c.handshakeDone { - err := c.HandshakeContext(context.Background()) - if err != nil { - return err - } - } else if c.handshakeErr != nil { - return c.handshakeErr + err := c.HandshakeContext(context.Background()) + if err != nil { + return err } return c.tcpConn.SetDeadline(t) } func (c *gLazyConn) SetReadDeadline(t time.Time) error { - if !c.handshakeDone { - err := c.HandshakeContext(context.Background()) - if err != nil { - return err - } - } else if c.handshakeErr != nil { - return c.handshakeErr + err := c.HandshakeContext(context.Background()) + if err != nil { + return err } return c.tcpConn.SetReadDeadline(t) } func (c *gLazyConn) SetWriteDeadline(t time.Time) error { - if !c.handshakeDone { - err := c.HandshakeContext(context.Background()) - if err != nil { - return err - } - } else if c.handshakeErr != nil { - return c.handshakeErr + err := c.HandshakeContext(context.Background()) + if err != nil { + return err } return c.tcpConn.SetWriteDeadline(t) } func (c *gLazyConn) Close() error { if !c.handshakeDone { - c.request.Complete(true) - c.handshakeErr = net.ErrClosed - return nil - } else if c.handshakeErr != nil { - return nil + c.handshakeAccess.Lock() + if !c.handshakeDone { + c.request.Complete(true) + c.handshakeErr = net.ErrClosed + c.handshakeDone = true + return nil + } + c.handshakeAccess.Unlock() } return c.tcpConn.Close() } func (c *gLazyConn) CloseRead() error { if !c.handshakeDone { - c.request.Complete(true) - c.handshakeErr = net.ErrClosed - return nil - } else if c.handshakeErr != nil { - return nil + c.handshakeAccess.Lock() + if !c.handshakeDone { + c.request.Complete(true) + c.handshakeErr = net.ErrClosed + c.handshakeDone = true + return nil + } + c.handshakeAccess.Unlock() } return c.tcpConn.CloseRead() } func (c *gLazyConn) CloseWrite() error { if !c.handshakeDone { - c.request.Complete(true) - c.handshakeErr = net.ErrClosed - return nil - } else if c.handshakeErr != nil { - return nil + c.handshakeAccess.Lock() + if !c.handshakeDone { + c.request.Complete(true) + c.handshakeErr = net.ErrClosed + c.handshakeDone = true + return nil + } + c.handshakeAccess.Unlock() } return c.tcpConn.CloseRead() } func (c *gLazyConn) ReaderReplaceable() bool { + c.handshakeAccess.Lock() + defer c.handshakeAccess.Unlock() return c.handshakeDone && c.handshakeErr == nil } func (c *gLazyConn) WriterReplaceable() bool { + c.handshakeAccess.Lock() + defer c.handshakeAccess.Unlock() return c.handshakeDone && c.handshakeErr == nil } From bea26198e748aeead5cb1d93fc80511a907afa3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 15 Jun 2025 17:30:35 +0800 Subject: [PATCH 020/121] Fix "Fix gLazyConn race" --- stack_gvisor_lazy.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/stack_gvisor_lazy.go b/stack_gvisor_lazy.go index 387b9538..59fcb356 100644 --- a/stack_gvisor_lazy.go +++ b/stack_gvisor_lazy.go @@ -145,8 +145,12 @@ func (c *gLazyConn) Close() error { 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 } return c.tcpConn.Close() } @@ -159,8 +163,12 @@ func (c *gLazyConn) CloseRead() error { 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 } return c.tcpConn.CloseRead() } @@ -173,8 +181,12 @@ func (c *gLazyConn) CloseWrite() error { 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 } return c.tcpConn.CloseRead() } From 2121bc3f01df42b4ef5b6bc693ef6b5f878e5be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 20 Jun 2025 12:47:19 +0800 Subject: [PATCH 021/121] Fix error usages --- stack_gvisor_lazy.go | 3 ++- stack_gvisor_tcp.go | 3 ++- stack_gvisor_udp.go | 3 ++- stack_system.go | 7 ++++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/stack_gvisor_lazy.go b/stack_gvisor_lazy.go index 59fcb356..f5e2e6e6 100644 --- a/stack_gvisor_lazy.go +++ b/stack_gvisor_lazy.go @@ -4,6 +4,7 @@ package tun import ( "context" + "errors" "net" "os" "sync" @@ -79,7 +80,7 @@ func (c *gLazyConn) HandshakeFailure(err error) error { if c.handshakeDone { return os.ErrInvalid } - c.request.Complete(err != ErrDrop) + c.request.Complete(!errors.Is(err, ErrDrop)) c.handshakeDone = true c.handshakeErr = err return nil diff --git a/stack_gvisor_tcp.go b/stack_gvisor_tcp.go index 33cf40e5..0a129334 100644 --- a/stack_gvisor_tcp.go +++ b/stack_gvisor_tcp.go @@ -4,6 +4,7 @@ package tun import ( "context" + "errors" "github.com/sagernet/gvisor/pkg/tcpip/stack" "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" @@ -37,7 +38,7 @@ func (f *TCPForwarder) Forward(r *tcp.ForwarderRequest) { destination := M.SocksaddrFrom(AddrFromAddress(r.ID().LocalAddress), r.ID().LocalPort) pErr := f.handler.PrepareConnection(N.NetworkTCP, source, destination) if pErr != nil { - r.Complete(pErr != ErrDrop) + r.Complete(!errors.Is(pErr, ErrDrop)) return } conn := &gLazyConn{ diff --git a/stack_gvisor_udp.go b/stack_gvisor_udp.go index 3027798c..473eec46 100644 --- a/stack_gvisor_udp.go +++ b/stack_gvisor_udp.go @@ -4,6 +4,7 @@ package tun import ( "context" + "errors" "math" "net/netip" "os" @@ -59,7 +60,7 @@ 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) if pErr != nil { - if pErr != ErrDrop { + if !errors.Is(pErr, ErrDrop) { gWriteUnreachable(f.stack, userData.(*stack.PacketBuffer)) } return false, nil, nil, nil diff --git a/stack_system.go b/stack_system.go index 5a301d5b..eaf8314f 100644 --- a/stack_system.go +++ b/stack_system.go @@ -2,6 +2,7 @@ package tun import ( "context" + "errors" "net" "net/netip" "syscall" @@ -354,7 +355,7 @@ func (s *System) processIPv4TCP(ipHdr header.IPv4, tcpHdr header.TCP) (bool, err } else { natPort, err := s.tcpNat.Lookup(source, destination, s.handler) if err != nil { - if err == ErrDrop { + if errors.Is(err, ErrDrop) { return false, nil } else { return false, s.resetIPv4TCP(ipHdr, tcpHdr) @@ -441,7 +442,7 @@ func (s *System) processIPv6TCP(ipHdr header.IPv6, tcpHdr header.TCP) (bool, err } else { natPort, err := s.tcpNat.Lookup(source, destination, s.handler) if err != nil { - if err == ErrDrop { + if errors.Is(err, ErrDrop) { return false, nil } else { return false, s.resetIPv6TCP(ipHdr, tcpHdr) @@ -536,7 +537,7 @@ 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) if pErr != nil { - if pErr != ErrDrop { + if !errors.Is(pErr, ErrDrop) { if source.IsIPv4() { ipHdr := userData.(header.IPv4) s.rejectIPv4WithICMP(ipHdr, header.ICMPv4PortUnreachable) From f57754918d4d8010b08c5ff2221703085c3a915c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 12 Apr 2025 14:39:22 +0800 Subject: [PATCH 022/121] Add DefaultInterfaceMonitor.MyInterface --- monitor.go | 2 ++ monitor_shared.go | 15 ++++++++++++++- tun_darwin.go | 1 + tun_linux.go | 2 +- tun_windows.go | 1 + 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/monitor.go b/monitor.go index a3b4ef63..a1866c88 100644 --- a/monitor.go +++ b/monitor.go @@ -30,6 +30,8 @@ type DefaultInterfaceMonitor interface { AndroidVPNEnabled() bool RegisterCallback(callback DefaultInterfaceUpdateCallback) *list.Element[DefaultInterfaceUpdateCallback] UnregisterCallback(element *list.Element[DefaultInterfaceUpdateCallback]) + RegisterMyInterface(interfaceName string) + MyInterface() string } type DefaultInterfaceMonitorOptions struct { diff --git a/monitor_shared.go b/monitor_shared.go index a5ee4e30..12e3e21b 100644 --- a/monitor_shared.go +++ b/monitor_shared.go @@ -42,11 +42,12 @@ type defaultInterfaceMonitor struct { androidVPNEnabled bool noRoute bool networkMonitor NetworkUpdateMonitor + logger logger.Logger checkUpdateTimer *time.Timer element *list.Element[NetworkUpdateCallback] access sync.Mutex callbacks list.List[DefaultInterfaceUpdateCallback] - logger logger.Logger + myInterface string } func NewDefaultInterfaceMonitor(networkMonitor NetworkUpdateMonitor, logger logger.Logger, options DefaultInterfaceMonitorOptions) (DefaultInterfaceMonitor, error) { @@ -132,3 +133,15 @@ func (m *defaultInterfaceMonitor) emit(defaultInterface *control.Interface, flag callback(defaultInterface, flags) } } + +func (m *defaultInterfaceMonitor) RegisterMyInterface(interfaceName string) { + m.access.Lock() + defer m.access.Unlock() + m.myInterface = interfaceName +} + +func (m *defaultInterfaceMonitor) MyInterface() string { + m.access.Lock() + defer m.access.Unlock() + return m.myInterface +} diff --git a/tun_darwin.go b/tun_darwin.go index a0dd54a9..2462d75a 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -82,6 +82,7 @@ func New(options Options) (Tun, error) { } func (t *NativeTun) Start() error { + t.options.InterfaceMonitor.RegisterMyInterface(t.options.Name) return t.setRoutes() } diff --git a/tun_linux.go b/tun_linux.go index 6a3549d3..9c283773 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -264,7 +264,7 @@ func (t *NativeTun) Start() error { if t.options.FileDescriptor != 0 { return nil } - + t.options.InterfaceMonitor.RegisterMyInterface(t.options.Name) tunLink, err := netlink.LinkByName(t.options.Name) if err != nil { return err diff --git a/tun_windows.go b/tun_windows.go index 9cb1e96d..dde61990 100644 --- a/tun_windows.go +++ b/tun_windows.go @@ -163,6 +163,7 @@ func (t *NativeTun) Name() (string, error) { } func (t *NativeTun) Start() error { + t.options.InterfaceMonitor.RegisterMyInterface(t.options.Name) if !t.options.AutoRoute { return nil } From 5e343c4b66b2ede6f2e0eec03468dff1d306d49d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 9 Jun 2025 18:51:17 +0800 Subject: [PATCH 023/121] Add loopback address support --- redirect_nftables.go | 50 +++++- redirect_nftables_exprs.go | 86 ++++++++-- redirect_nftables_rules.go | 327 ++++++++++++++++++++++++++++--------- stack_gvisor.go | 34 ++-- stack_gvisor_tcp.go | 58 ++++++- stack_system.go | 138 +++++++++------- tun.go | 2 + 7 files changed, 518 insertions(+), 177 deletions(-) diff --git a/redirect_nftables.go b/redirect_nftables.go index 3545617f..b2d54f93 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -46,6 +46,11 @@ func (r *autoRedirect) setupNFTables() error { return err } + err = r.nftablesCreateLoopbackAddressSets(nft, table) + if err != nil { + return err + } + skipOutput := len(r.tunOptions.IncludeInterface) > 0 && !common.Contains(r.tunOptions.IncludeInterface, "lo") || common.Contains(r.tunOptions.ExcludeInterface, "lo") if !skipOutput { chainOutput := nft.AddChain(&nftables.Chain{ @@ -61,8 +66,23 @@ func (r *autoRedirect) setupNFTables() error { return err } r.nftablesCreateUnreachable(nft, table, chainOutput) - r.nftablesCreateRedirect(nft, table, chainOutput) - + err = r.nftablesCreateRedirect(nft, table, chainOutput) + if err != nil { + return err + } + if len(r.tunOptions.Inet4LoopbackAddress) > 0 || len(r.tunOptions.Inet6LoopbackAddress) > 0 { + chainOutputRoute := nft.AddChain(&nftables.Chain{ + Name: "output_route", + Table: table, + Hooknum: nftables.ChainHookOutput, + Priority: nftables.ChainPriorityMangle, + Type: nftables.ChainTypeRoute, + }) + err = r.nftablesCreateLoopbackReroute(nft, table, chainOutputRoute) + if err != nil { + return err + } + } chainOutputUDP := nft.AddChain(&nftables.Chain{ Name: "output_udp_icmp", Table: table, @@ -77,7 +97,7 @@ func (r *autoRedirect) setupNFTables() error { r.nftablesCreateUnreachable(nft, table, chainOutputUDP) r.nftablesCreateMark(nft, table, chainOutputUDP) } else { - r.nftablesCreateRedirect(nft, table, chainOutput, &expr.Meta{ + err = r.nftablesCreateRedirect(nft, table, chainOutput, &expr.Meta{ Key: expr.MetaKeyOIFNAME, Register: 1, }, &expr.Cmp{ @@ -85,6 +105,9 @@ func (r *autoRedirect) setupNFTables() error { Register: 1, Data: nftablesIfname(r.tunOptions.Name), }) + if err != nil { + return err + } } } @@ -100,12 +123,25 @@ func (r *autoRedirect) setupNFTables() error { return err } r.nftablesCreateUnreachable(nft, table, chainPreRouting) - r.nftablesCreateRedirect(nft, table, chainPreRouting) - if r.tunOptions.AutoRedirectMarkMode { - r.nftablesCreateMark(nft, table, chainPreRouting) + err = r.nftablesCreateRedirect(nft, table, chainPreRouting) + if err != nil { + return err } - if r.tunOptions.AutoRedirectMarkMode { + r.nftablesCreateMark(nft, table, chainPreRouting) + if len(r.tunOptions.Inet4LoopbackAddress) > 0 || len(r.tunOptions.Inet6LoopbackAddress) > 0 { + chainPreRoutingFilter := nft.AddChain(&nftables.Chain{ + Name: "prerouting_filter", + Table: table, + Hooknum: nftables.ChainHookPrerouting, + Priority: nftables.ChainPriorityRef(*nftables.ChainPriorityNATDest + 1), + Type: nftables.ChainTypeFilter, + }) + err = r.nftablesCreateLoopbackReroute(nft, table, chainPreRoutingFilter) + if err != nil { + return err + } + } chainPreRoutingUDP := nft.AddChain(&nftables.Chain{ Name: "prerouting_udp", Table: table, diff --git a/redirect_nftables_exprs.go b/redirect_nftables_exprs.go index a2f01958..d29c1137 100644 --- a/redirect_nftables_exprs.go +++ b/redirect_nftables_exprs.go @@ -7,6 +7,7 @@ import ( "github.com/sagernet/nftables" "github.com/sagernet/nftables/expr" + "github.com/sagernet/sing/common" "go4.org/netipx" ) @@ -21,6 +22,20 @@ func nftablesCreateExcludeDestinationIPSet( nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, id uint32, name string, family nftables.TableFamily, invert bool, ) { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: append( + nftablesCreateDestinationIPSetExprs(id, name, family, invert), + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + ), + }) +} + +func nftablesCreateDestinationIPSetExprs(id uint32, name string, family nftables.TableFamily, invert bool) []expr.Any { exprs := []expr.Any{ &expr.Meta{ Key: expr.MetaKeyNFPROTO, @@ -53,22 +68,63 @@ func nftablesCreateExcludeDestinationIPSet( }, ) } - exprs = append(exprs, - &expr.Lookup{ - SourceRegister: 1, - SetID: id, - SetName: name, - Invert: invert, - }, - &expr.Counter{}, - &expr.Verdict{ - Kind: expr.VerdictReturn, - }) - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chain, - Exprs: exprs, + exprs = append(exprs, &expr.Lookup{ + SourceRegister: 1, + SetID: id, + SetName: name, + Invert: invert, }) + return exprs +} + +func nftablesCreateIPConst( + nft *nftables.Conn, table *nftables.Table, id uint32, name string, family nftables.TableFamily, addressList []netip.Addr, +) (*nftables.Set, error) { + var keyType nftables.SetDatatype + if family == nftables.TableFamilyIPv4 { + keyType = nftables.TypeIPAddr + } else { + keyType = nftables.TypeIP6Addr + } + mySet := &nftables.Set{ + Table: table, + ID: id, + Name: name, + KeyType: keyType, + Constant: true, + } + if id == 0 { + mySet.Anonymous = true + } + setElements := common.Map(addressList, func(addr netip.Addr) nftables.SetElement { return nftables.SetElement{Key: addr.AsSlice()} }) + if id == 0 { + err := nft.AddSet(mySet, setElements) + if err != nil { + return nil, err + } + return mySet, nil + } else { + err := nft.AddSet(mySet, nil) + if err != nil { + return nil, err + } + } + for len(setElements) > 0 { + toAdd := setElements + if len(toAdd) > 1000 { + toAdd = toAdd[:1000] + } + setElements = setElements[len(toAdd):] + err := nft.SetAddElements(mySet, toAdd) + if err != nil { + return nil, err + } + err = nft.Flush() + if err != nil { + return nil, err + } + } + return mySet, nil } func nftablesCreateIPSet( diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index 9c0767d7..ba4ee872 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -117,8 +117,61 @@ func (r *autoRedirect) nftablesCreateLocalAddressSets( return nil } +func (r *autoRedirect) nftablesCreateLoopbackAddressSets( + nft *nftables.Conn, table *nftables.Table, +) error { + if r.enableIPv4 && len(r.tunOptions.Inet4LoopbackAddress) > 0 { + _, err := nftablesCreateIPConst(nft, table, 7, "inet4_local_redirect_address_set", nftables.TableFamilyIPv4, r.tunOptions.Inet4LoopbackAddress) + if err != nil { + return err + } + } + if r.enableIPv6 && len(r.tunOptions.Inet6LoopbackAddress) > 0 { + _, err := nftablesCreateIPConst(nft, table, 8, "inet6_local_redirect_address_set", nftables.TableFamilyIPv6, r.tunOptions.Inet6LoopbackAddress) + if err != nil { + return err + } + } + return nil +} + func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain) error { if r.tunOptions.AutoRedirectMarkMode && chain.Hooknum == nftables.ChainHookOutput { + if chain.Type == nftables.ChainTypeRoute { + ipProto := &nftables.Set{ + Table: table, + Anonymous: true, + Constant: true, + KeyType: nftables.TypeInetProto, + } + err := nft.AddSet(ipProto, []nftables.SetElement{ + {Key: []byte{unix.IPPROTO_UDP}}, + {Key: []byte{unix.IPPROTO_ICMP}}, + {Key: []byte{unix.IPPROTO_ICMPV6}}, + }) + if err != nil { + return err + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyL4PROTO, + Register: 1, + }, + &expr.Lookup{ + SourceRegister: 1, + SetID: ipProto.ID, + SetName: ipProto.Name, + Invert: true, + }, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, @@ -161,6 +214,25 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft } } if chain.Hooknum == nftables.ChainHookPrerouting { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyIIFNAME, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: nftablesIfname(r.tunOptions.Name), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) if len(r.tunOptions.IncludeInterface) > 0 { if len(r.tunOptions.IncludeInterface) > 1 { includeInterface := &nftables.Set{ @@ -436,44 +508,6 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft } } - if r.tunOptions.AutoRedirectMarkMode && - ((chain.Hooknum == nftables.ChainHookOutput && chain.Type == nftables.ChainTypeRoute) || - (chain.Hooknum == nftables.ChainHookPrerouting && chain.Type == nftables.ChainTypeFilter)) { - ipProto := &nftables.Set{ - Table: table, - Anonymous: true, - Constant: true, - KeyType: nftables.TypeInetProto, - } - err := nft.AddSet(ipProto, []nftables.SetElement{ - {Key: []byte{unix.IPPROTO_UDP}}, - {Key: []byte{unix.IPPROTO_ICMP}}, - {Key: []byte{unix.IPPROTO_ICMPV6}}, - }) - if err != nil { - return err - } - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chain, - Exprs: []expr.Any{ - &expr.Meta{ - Key: expr.MetaKeyL4PROTO, - Register: 1, - }, - &expr.Lookup{ - SourceRegister: 1, - SetID: ipProto.ID, - SetName: ipProto.Name, - Invert: true, - }, - &expr.Verdict{ - Kind: expr.VerdictReturn, - }, - }, - }) - } - if r.enableIPv4 { nftablesCreateExcludeDestinationIPSet(nft, table, chain, 5, "inet4_local_address_set", nftables.TableFamilyIPv4, false) } @@ -527,6 +561,9 @@ func (r *autoRedirect) nftablesCreateMark(nft *nftables.Conn, table *nftables.Ta SourceRegister: true, }, &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, }, }) } @@ -534,57 +571,193 @@ func (r *autoRedirect) nftablesCreateMark(nft *nftables.Conn, table *nftables.Ta func (r *autoRedirect) nftablesCreateRedirect( nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, exprs ...expr.Any, -) { - if r.enableIPv4 && !r.enableIPv6 { - exprs = append(exprs, - &expr.Meta{ - Key: expr.MetaKeyNFPROTO, - Register: 1, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: []byte{uint8(nftables.TableFamilyIPv4)}, +) error { + exprsRedirect := []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyL4PROTO, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{unix.IPPROTO_TCP}, + }, + &expr.Counter{}, + &expr.Immediate{ + Register: 1, + Data: binaryutil.BigEndian.PutUint16(r.redirectPort()), + }, + &expr.Redir{ + RegisterProtoMin: 1, + Flags: unix.NF_NAT_RANGE_PROTO_SPECIFIED, + }, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + } + if len(r.tunOptions.Inet4LoopbackAddress) == 0 && len(r.tunOptions.Inet6LoopbackAddress) == 0 { + if r.enableIPv4 && !r.enableIPv6 { + exprs = append(exprs, + &expr.Meta{ + Key: expr.MetaKeyNFPROTO, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{uint8(nftables.TableFamilyIPv4)}, + }) + } else if !r.enableIPv4 && r.enableIPv6 { + exprs = append(exprs, + &expr.Meta{ + Key: expr.MetaKeyNFPROTO, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{uint8(nftables.TableFamilyIPv6)}, + }) + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: append(exprs, exprsRedirect...), + }) + } else { + if r.enableIPv4 { + exprs4 := exprs + if len(r.tunOptions.Inet4LoopbackAddress) > 0 { + exprs4 = append(exprs4, nftablesCreateDestinationIPSetExprs(7, "inet4_local_redirect_address_set", nftables.TableFamilyIPv4, true)...) + } else { + exprs4 = append(exprs4, &expr.Meta{ + Key: expr.MetaKeyNFPROTO, + Register: 1, + }, &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{uint8(nftables.TableFamilyIPv4)}, + }) + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: append(exprs4, exprsRedirect...), }) - } else if !r.enableIPv4 && r.enableIPv6 { - exprs = append(exprs, - &expr.Meta{ - Key: expr.MetaKeyNFPROTO, - Register: 1, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: []byte{uint8(nftables.TableFamilyIPv6)}, + } + if r.enableIPv6 { + exprs6 := exprs + if len(r.tunOptions.Inet6LoopbackAddress) > 0 { + exprs6 = append(exprs6, nftablesCreateDestinationIPSetExprs(8, "inet6_local_redirect_address_set", nftables.TableFamilyIPv6, true)...) + } else { + exprs6 = append(exprs6, &expr.Meta{ + Key: expr.MetaKeyNFPROTO, + Register: 1, + }, &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{uint8(nftables.TableFamilyIPv6)}, + }) + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: append(exprs6, exprsRedirect...), }) + } } - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chain, - Exprs: append(exprs, - &expr.Meta{ - Key: expr.MetaKeyL4PROTO, + return nil +} + +func (r *autoRedirect) nftablesCreateLoopbackReroute( + nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, +) error { + exprs := []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyL4PROTO, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{unix.IPPROTO_TCP}, + }, + &expr.Meta{ + Key: expr.MetaKeyMARK, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpNeq, + Register: 1, + Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectInputMark), + }, + } + var exprs4 []expr.Any + if r.enableIPv4 && len(r.tunOptions.Inet4LoopbackAddress) > 0 { + exprs4 = append(exprs, nftablesCreateDestinationIPSetExprs(7, "inet4_local_redirect_address_set", nftables.TableFamilyIPv4, false)...) + } + var exprs6 []expr.Any + if r.enableIPv6 && len(r.tunOptions.Inet6LoopbackAddress) > 0 { + exprs6 = append(exprs, nftablesCreateDestinationIPSetExprs(8, "inet6_local_redirect_address_set", nftables.TableFamilyIPv6, false)...) + } + var exprsCreateMark []expr.Any + if chain.Hooknum == nftables.ChainHookPrerouting { + exprsCreateMark = []expr.Any{ + &expr.Immediate{ Register: 1, + Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectInputMark), }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: []byte{unix.IPPROTO_TCP}, + &expr.Meta{ + Key: expr.MetaKeyMARK, + Register: 1, + SourceRegister: true, }, &expr.Counter{}, + } + } else { + exprsCreateMark = []expr.Any{ &expr.Immediate{ Register: 1, - Data: binaryutil.BigEndian.PutUint16(r.redirectPort()), + Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectInputMark), }, - &expr.Redir{ - RegisterProtoMin: 1, - Flags: unix.NF_NAT_RANGE_PROTO_SPECIFIED, + &expr.Meta{ + Key: expr.MetaKeyMARK, + Register: 1, + SourceRegister: true, }, - &expr.Verdict{ - Kind: expr.VerdictReturn, + &expr.Meta{ + Key: expr.MetaKeyMARK, + Register: 1, }, - ), - }) + &expr.Ct{ + Key: expr.CtKeyMARK, + Register: 1, + SourceRegister: true, + }, + &expr.Counter{}, + } + } + if len(exprs4) > 0 { + exprs4 = append(exprs4, exprsCreateMark...) + } + if len(exprs6) > 0 { + exprs6 = append(exprs6, exprsCreateMark...) + } + if len(exprs4) > 0 { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: exprs4, + }) + } + if len(exprs6) > 0 { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: exprs6, + }) + } + return nil } func (r *autoRedirect) nftablesCreateDNSHijackRulesForFamily( diff --git a/stack_gvisor.go b/stack_gvisor.go index 65bb7bd0..213d50fb 100644 --- a/stack_gvisor.go +++ b/stack_gvisor.go @@ -26,14 +26,16 @@ const WithGVisor = true const DefaultNIC tcpip.NICID = 1 type GVisor struct { - ctx context.Context - tun GVisorTun - udpTimeout time.Duration - broadcastAddr netip.Addr - handler Handler - logger logger.Logger - stack *stack.Stack - endpoint stack.LinkEndpoint + ctx context.Context + tun GVisorTun + inet4LoopbackAddress []netip.Addr + inet6LoopbackAddress []netip.Addr + udpTimeout time.Duration + broadcastAddr netip.Addr + handler Handler + logger logger.Logger + stack *stack.Stack + endpoint stack.LinkEndpoint } type GVisorTun interface { @@ -50,12 +52,14 @@ func NewGVisor( } gStack := &GVisor{ - ctx: options.Context, - tun: gTun, - udpTimeout: options.UDPTimeout, - broadcastAddr: BroadcastAddr(options.TunOptions.Inet4Address), - handler: options.Handler, - logger: options.Logger, + ctx: options.Context, + tun: gTun, + inet4LoopbackAddress: options.TunOptions.Inet4LoopbackAddress, + inet6LoopbackAddress: options.TunOptions.Inet6LoopbackAddress, + udpTimeout: options.UDPTimeout, + broadcastAddr: BroadcastAddr(options.TunOptions.Inet4Address), + handler: options.Handler, + logger: options.Logger, } return gStack, nil } @@ -70,7 +74,7 @@ func (t *GVisor) Start() error { if err != nil { return err } - ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, NewTCPForwarder(t.ctx, ipStack, t.handler).HandlePacket) + 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) t.stack = ipStack t.endpoint = linkEndpoint diff --git a/stack_gvisor_tcp.go b/stack_gvisor_tcp.go index 0a129334..cd397781 100644 --- a/stack_gvisor_tcp.go +++ b/stack_gvisor_tcp.go @@ -5,31 +5,75 @@ package tun import ( "context" "errors" + "net/netip" + "github.com/sagernet/gvisor/pkg/tcpip" + "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/common" + "github.com/sagernet/sing/common/bufio" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) type TCPForwarder struct { - ctx context.Context - stack *stack.Stack - handler Handler - forwarder *tcp.Forwarder + ctx context.Context + stack *stack.Stack + handler Handler + inet4LoopbackAddress []tcpip.Address + inet6LoopbackAddress []tcpip.Address + tun GVisorTun + forwarder *tcp.Forwarder } func NewTCPForwarder(ctx context.Context, stack *stack.Stack, handler Handler) *TCPForwarder { + return NewTCPForwarderWithLoopback(ctx, stack, handler, nil, nil, nil) +} + +func NewTCPForwarderWithLoopback(ctx context.Context, stack *stack.Stack, handler Handler, inet4LoopbackAddress []netip.Addr, inet6LoopbackAddress []netip.Addr, tun GVisorTun) *TCPForwarder { forwarder := &TCPForwarder{ - ctx: ctx, - stack: stack, - handler: handler, + ctx: ctx, + stack: stack, + handler: handler, + inet4LoopbackAddress: common.Map(inet4LoopbackAddress, AddressFromAddr), + inet6LoopbackAddress: common.Map(inet6LoopbackAddress, AddressFromAddr), + tun: tun, } forwarder.forwarder = tcp.NewForwarder(stack, 0, 1024, forwarder.Forward) return forwarder } func (f *TCPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { + for _, inet4LoopbackAddress := range f.inet4LoopbackAddress { + if id.LocalAddress == inet4LoopbackAddress { + ipHdr := pkt.Network().(header.IPv4) + ipHdr.SetDestinationAddressWithChecksumUpdate(ipHdr.SourceAddress()) + ipHdr.SetSourceAddressWithChecksumUpdate(inet4LoopbackAddress) + tcpHdr := header.TCP(pkt.TransportHeader().Slice()) + tcpHdr.SetChecksum(0) + tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( + header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress(), ipHdr.PayloadLength()), + ))) + bufio.WriteVectorised(f.tun, pkt.AsSlices()) + return true + } + } + for _, inet6LoopbackAddress := range f.inet6LoopbackAddress { + if id.LocalAddress == inet6LoopbackAddress { + ipHdr := pkt.Network().(header.IPv6) + ipHdr.SetDestinationAddress(ipHdr.SourceAddress()) + ipHdr.SetSourceAddress(inet6LoopbackAddress) + tcpHdr := header.TCP(pkt.TransportHeader().Slice()) + tcpHdr.SetChecksum(0) + tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( + header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress(), ipHdr.PayloadLength()), + ))) + bufio.WriteVectorised(f.tun, pkt.AsSlices()) + return true + } + } return f.forwarder.HandlePacket(id, pkt) } diff --git a/stack_system.go b/stack_system.go index eaf8314f..23070fe1 100644 --- a/stack_system.go +++ b/stack_system.go @@ -23,30 +23,32 @@ import ( var ErrIncludeAllNetworks = E.New("`system` and `mixed` stack are not available when `includeAllNetworks` is enabled. See https://github.com/SagerNet/sing-tun/issues/25") type System struct { - ctx context.Context - tun Tun - tunName string - mtu int - handler Handler - logger logger.Logger - inet4Prefixes []netip.Prefix - inet6Prefixes []netip.Prefix - inet4ServerAddress netip.Addr - inet4Address netip.Addr - inet6ServerAddress netip.Addr - inet6Address netip.Addr - broadcastAddr netip.Addr - udpTimeout time.Duration - tcpListener net.Listener - tcpListener6 net.Listener - tcpPort uint16 - tcpPort6 uint16 - tcpNat *TCPNat - udpNat *udpnat.Service - bindInterface bool - interfaceFinder control.InterfaceFinder - frontHeadroom int - txChecksumOffload bool + ctx context.Context + tun Tun + tunName string + mtu int + handler Handler + logger logger.Logger + inet4Prefixes []netip.Prefix + inet6Prefixes []netip.Prefix + inet4ServerAddress netip.Addr + inet4Address netip.Addr + inet6ServerAddress netip.Addr + inet6Address netip.Addr + broadcastAddr netip.Addr + inet4LoopbackAddress []netip.Addr + inet6LoopbackAddress []netip.Addr + udpTimeout time.Duration + tcpListener net.Listener + tcpListener6 net.Listener + tcpPort uint16 + tcpPort6 uint16 + tcpNat *TCPNat + udpNat *udpnat.Service + bindInterface bool + interfaceFinder control.InterfaceFinder + frontHeadroom int + txChecksumOffload bool } type Session struct { @@ -58,18 +60,20 @@ type Session struct { func NewSystem(options StackOptions) (Stack, error) { stack := &System{ - ctx: options.Context, - tun: options.Tun, - tunName: options.TunOptions.Name, - mtu: int(options.TunOptions.MTU), - udpTimeout: options.UDPTimeout, - handler: options.Handler, - logger: options.Logger, - inet4Prefixes: options.TunOptions.Inet4Address, - inet6Prefixes: options.TunOptions.Inet6Address, - broadcastAddr: BroadcastAddr(options.TunOptions.Inet4Address), - bindInterface: options.ForwarderBindInterface, - interfaceFinder: options.InterfaceFinder, + ctx: options.Context, + tun: options.Tun, + tunName: options.TunOptions.Name, + mtu: int(options.TunOptions.MTU), + inet4LoopbackAddress: options.TunOptions.Inet4LoopbackAddress, + inet6LoopbackAddress: options.TunOptions.Inet6LoopbackAddress, + udpTimeout: options.UDPTimeout, + handler: options.Handler, + logger: options.Logger, + inet4Prefixes: options.TunOptions.Inet4Address, + inet6Prefixes: options.TunOptions.Inet6Address, + broadcastAddr: BroadcastAddr(options.TunOptions.Inet4Address), + bindInterface: options.ForwarderBindInterface, + interfaceFinder: options.InterfaceFinder, } if len(options.TunOptions.Inet4Address) > 0 { if !HasNextAddress(options.TunOptions.Inet4Address[0], 1) { @@ -353,18 +357,29 @@ func (s *System) processIPv4TCP(ipHdr header.IPv4, tcpHdr header.TCP) (bool, err ipHdr.SetDestinationAddr(session.Source.Addr()) tcpHdr.SetDestinationPort(session.Source.Port()) } else { - 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) + var loopback bool + for _, inet4LoopbackAddress := range s.inet4LoopbackAddress { + if destination.Addr() == inet4LoopbackAddress { + ipHdr.SetDestinationAddr(ipHdr.SourceAddr()) + ipHdr.SetSourceAddr(inet4LoopbackAddress) + loopback = true + break + } + } + 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) + } } + ipHdr.SetSourceAddr(s.inet4Address) + tcpHdr.SetSourcePort(natPort) + ipHdr.SetDestinationAddr(s.inet4ServerAddress) + tcpHdr.SetDestinationPort(s.tcpPort) } - ipHdr.SetSourceAddr(s.inet4Address) - tcpHdr.SetSourcePort(natPort) - ipHdr.SetDestinationAddr(s.inet4ServerAddress) - tcpHdr.SetDestinationPort(s.tcpPort) } if !s.txChecksumOffload { tcpHdr.SetChecksum(0) @@ -440,18 +455,29 @@ func (s *System) processIPv6TCP(ipHdr header.IPv6, tcpHdr header.TCP) (bool, err ipHdr.SetDestinationAddr(session.Source.Addr()) tcpHdr.SetDestinationPort(session.Source.Port()) } else { - 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) + var loopback bool + for _, inet6LoopbackAddress := range s.inet6LoopbackAddress { + if destination.Addr() == inet6LoopbackAddress { + ipHdr.SetDestinationAddr(ipHdr.SourceAddr()) + ipHdr.SetSourceAddr(inet6LoopbackAddress) + loopback = true + break + } + } + 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) + } } + ipHdr.SetSourceAddr(s.inet6Address) + tcpHdr.SetSourcePort(natPort) + ipHdr.SetDestinationAddr(s.inet6ServerAddress) + tcpHdr.SetDestinationPort(s.tcpPort6) } - ipHdr.SetSourceAddr(s.inet6Address) - tcpHdr.SetSourcePort(natPort) - ipHdr.SetDestinationAddr(s.inet6ServerAddress) - tcpHdr.SetDestinationPort(s.tcpPort6) } if !s.txChecksumOffload { tcpHdr.SetChecksum(0) diff --git a/tun.go b/tun.go index b0f573a2..882adc5a 100644 --- a/tun.go +++ b/tun.go @@ -66,6 +66,8 @@ type Options struct { AutoRedirectMarkMode bool AutoRedirectInputMark uint32 AutoRedirectOutputMark uint32 + Inet4LoopbackAddress []netip.Addr + Inet6LoopbackAddress []netip.Addr StrictRoute bool Inet4RouteAddress []netip.Prefix Inet6RouteAddress []netip.Prefix From 8763c24e493517c0d8a8a60df9e4febbb7871fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 30 Jun 2025 18:00:36 +0800 Subject: [PATCH 024/121] Improve nftables rules for openwrt --- redirect_nftables_rules_openwrt.go | 115 ++++++++--------------------- 1 file changed, 32 insertions(+), 83 deletions(-) diff --git a/redirect_nftables_rules_openwrt.go b/redirect_nftables_rules_openwrt.go index 47923167..fd6a9698 100644 --- a/redirect_nftables_rules_openwrt.go +++ b/redirect_nftables_rules_openwrt.go @@ -3,101 +3,50 @@ package tun import ( - "github.com/sagernet/nftables" - "github.com/sagernet/nftables/expr" + "os" + "os/exec" - "golang.org/x/exp/slices" + "github.com/sagernet/nftables" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/shell" ) func (r *autoRedirect) configureOpenWRTFirewall4(nft *nftables.Conn, cleanup bool) error { - tableFW4, err := nft.ListTableOfFamily("fw4", nftables.TableFamilyINet) + _, err := nft.ListTableOfFamily("fw4", nftables.TableFamilyINet) if err != nil { return nil } - if !cleanup { - ruleIif := &nftables.Rule{ - Table: tableFW4, - Exprs: []expr.Any{ - &expr.Meta{ - Key: expr.MetaKeyIIFNAME, - Register: 1, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: nftablesIfname(r.tunOptions.Name), - }, - &expr.Counter{}, - &expr.Verdict{ - Kind: expr.VerdictAccept, - }, - }, - } - ruleOif := &nftables.Rule{ - Table: tableFW4, - Exprs: []expr.Any{ - &expr.Meta{ - Key: expr.MetaKeyOIFNAME, - Register: 1, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: nftablesIfname(r.tunOptions.Name), - }, - &expr.Counter{}, - &expr.Verdict{ - Kind: expr.VerdictAccept, - }, - }, - } - chainForward := &nftables.Chain{ - Name: "forward", - } - ruleIif.Chain = chainForward - ruleOif.Chain = chainForward - nft.InsertRule(ruleOif) - nft.InsertRule(ruleIif) - chainInput := &nftables.Chain{ - Name: "input", - } - ruleIif.Chain = chainInput - ruleOif.Chain = chainInput - nft.InsertRule(ruleOif) - nft.InsertRule(ruleIif) + fw4Path, err := exec.LookPath("fw4") + if err != nil { return nil } - for _, chainName := range []string{"input", "forward"} { - var rules []*nftables.Rule - rules, err = nft.GetRules(tableFW4, &nftables.Chain{ - Name: chainName, - }) + rulePath := "/etc/nftables.d/0-" + r.tableName + "-auto-redirect.nft" + if !cleanup { + err = os.WriteFile(rulePath, []byte(`chain input { + type filter hook input priority filter; policy accept; + iifname "`+r.tunOptions.Name+`" counter accept comment "!`+r.tableName+`: Accept traffic from tun" + oifname "`+r.tunOptions.Name+`" counter accept comment "!`+r.tableName+`: Accept traffic from tun" +} +chain forward { + type filter hook forward priority filter; policy accept; + iifname "`+r.tunOptions.Name+`" counter accept comment "!`+r.tableName+`: Accept traffic from tun" + oifname "`+r.tunOptions.Name+`" counter accept comment "!`+r.tableName+`: Accept traffic from tun" +} +`), 0o644) if err != nil { - return err + return E.Cause(err, "write fw4 rules") } - for _, rule := range rules { - if len(rule.Exprs) != 4 { - continue - } - exprMeta, isMeta := rule.Exprs[0].(*expr.Meta) - if !isMeta { - continue - } - if exprMeta.Key != expr.MetaKeyIIFNAME && exprMeta.Key != expr.MetaKeyOIFNAME { - continue - } - exprCmp, isCmp := rule.Exprs[1].(*expr.Cmp) - if !isCmp { - continue - } - if !slices.Equal(exprCmp.Data, nftablesIfname(r.tunOptions.Name)) { - continue - } - err = nft.DelRule(rule) - if err != nil { - return err - } + } else if _, err = os.Stat(rulePath); os.IsNotExist(err) { + return nil + } else { + err = os.Remove(rulePath) + if err != nil { + return E.Cause(err, "clean fw4 rules") } } + _, err = shell.Exec(fw4Path, "reload").Read() + if err != nil { + return E.Cause(err, "reload fw4 rules") + } return nil } From a0881ada32519e91890f365873ded0a092e1b8cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 2 Jul 2025 19:16:14 +0800 Subject: [PATCH 025/121] Improve darwin tun performance --- internal/fdbased_darwin/endpoint.go | 648 ++++++++++++++++++ internal/fdbased_darwin/endpoint_mutex.go | 96 +++ internal/fdbased_darwin/errno.go | 54 ++ internal/fdbased_darwin/packet_dispatchers.go | 229 +++++++ internal/fdbased_darwin/processor_mutex.go | 64 ++ internal/fdbased_darwin/processors.go | 275 ++++++++ internal/rawfile_darwin/rawfile.go | 188 +++++ internal/stopfd_darwin/stopfd.go | 61 ++ stack_gvisor.go | 12 +- stack_mixed.go | 45 +- stack_system.go | 42 +- tun.go | 7 + tun_darwin.go | 168 ++++- tun_darwin_gvisor.go | 135 +--- tun_linux_gvisor.go | 25 +- tun_windows_gvisor.go | 4 +- 16 files changed, 1894 insertions(+), 159 deletions(-) create mode 100644 internal/fdbased_darwin/endpoint.go create mode 100644 internal/fdbased_darwin/endpoint_mutex.go create mode 100644 internal/fdbased_darwin/errno.go create mode 100644 internal/fdbased_darwin/packet_dispatchers.go create mode 100644 internal/fdbased_darwin/processor_mutex.go create mode 100644 internal/fdbased_darwin/processors.go create mode 100644 internal/rawfile_darwin/rawfile.go create mode 100644 internal/stopfd_darwin/stopfd.go diff --git a/internal/fdbased_darwin/endpoint.go b/internal/fdbased_darwin/endpoint.go new file mode 100644 index 00000000..b5427930 --- /dev/null +++ b/internal/fdbased_darwin/endpoint.go @@ -0,0 +1,648 @@ +// Copyright 2018 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package fdbased provides the implementation of data-link layer endpoints +// backed by boundary-preserving file descriptors (e.g., TUN devices, +// seqpacket/datagram sockets). +// +// FD based endpoints can be used in the networking stack by calling New() to +// create a new endpoint, and then passing it as an argument to +// Stack.CreateNIC(). +// +// FD based endpoints can use more than one file descriptor to read incoming +// packets. If there are more than one FDs specified and the underlying FD is an +// AF_PACKET then the endpoint will enable FANOUT mode on the socket so that the +// host kernel will consistently hash the packets to the sockets. This ensures +// that packets for the same TCP streams are not reordered. +// +// Similarly if more than one FD's are specified where the underlying FD is not +// AF_PACKET then it's the caller's responsibility to ensure that all inbound +// packets on the descriptors are consistently 5 tuple hashed to one of the +// descriptors to prevent TCP reordering. +// +// Since netstack today does not compute 5 tuple hashes for outgoing packets we +// 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. +package fdbased + +import ( + "fmt" + "runtime" + + "github.com/sagernet/gvisor/pkg/buffer" + "github.com/sagernet/gvisor/pkg/sync" + "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" + "github.com/sagernet/sing/common" + + "golang.org/x/sys/unix" +) + +// linkDispatcher reads packets from the link FD and dispatches them to the +// NetworkDispatcher. +type linkDispatcher interface { + Stop() + dispatch() (bool, tcpip.Error) + release() +} + +// PacketDispatchMode are the various supported methods of receiving and +// dispatching packets from the underlying FD. +type PacketDispatchMode int + +// BatchSize is the number of packets to write in each syscall. It is 47 +// because when GVisorGSO is in use then a single 65KB TCP segment can get +// split into 46 segments of 1420 bytes and a single 216 byte segment. +const BatchSize = 47 + +const ( + // Readv is the default dispatch mode and is the least performant of the + // dispatch options but the one that is supported by all underlying FD + // types. + Readv PacketDispatchMode = iota +) + +func (p PacketDispatchMode) String() string { + switch p { + case Readv: + return "Readv" + default: + return fmt.Sprintf("unknown packet dispatch mode '%d'", p) + } +} + +var ( + _ stack.LinkEndpoint = (*endpoint)(nil) + _ stack.GSOEndpoint = (*endpoint)(nil) +) + +// +stateify savable +type fdInfo struct { + fd int + isSocket bool +} + +// +stateify savable +type endpoint struct { + // fds is the set of file descriptors each identifying one inbound/outbound + // channel. The endpoint will dispatch from all inbound channels as well as + // hash outbound packets to specific channels based on the packet hash. + fds []fdInfo + + // hdrSize specifies the link-layer header size. If set to 0, no header + // is added/removed; otherwise an ethernet header is used. + hdrSize int + + // caps holds the endpoint capabilities. + caps stack.LinkEndpointCapabilities + + // closed is a function to be called when the FD's peer (if any) closes + // its end of the communication pipe. + closed func(tcpip.Error) `state:"nosave"` + + inboundDispatchers []linkDispatcher + + mu endpointRWMutex `state:"nosave"` + // +checklocks:mu + dispatcher stack.NetworkDispatcher + + // packetDispatchMode controls the packet dispatcher used by this + // endpoint. + packetDispatchMode PacketDispatchMode + + // wg keeps track of running goroutines. + wg sync.WaitGroup `state:"nosave"` + + // maxSyscallHeaderBytes has the same meaning as + // Options.MaxSyscallHeaderBytes. + maxSyscallHeaderBytes uintptr + + // writevMaxIovs is the maximum number of iovecs that may be passed to + // rawfile.NonBlockingWriteIovec, as possibly limited by + // maxSyscallHeaderBytes. (No analogous limit is defined for + // rawfile.NonBlockingSendMMsg, since in that case the maximum number of + // iovecs also depends on the number of mmsghdrs. Instead, if sendBatch + // encounters a packet whose iovec count is limited by + // maxSyscallHeaderBytes, it falls back to writing the packet using writev + // via WritePacket.) + writevMaxIovs int + + // addr is the address of the endpoint. + // + // +checklocks:mu + addr tcpip.LinkAddress + + // mtu (maximum transmission unit) is the maximum size of a packet. + // +checklocks:mu + mtu uint32 + + batchSize int +} + +// Options specify the details about the fd-based endpoint to be created. +// +// +stateify savable +type Options struct { + // FDs is a set of FDs used to read/write packets. + FDs []int + + // MTU is the mtu to use for this endpoint. + MTU uint32 + + // EthernetHeader if true, indicates that the endpoint should read/write + // ethernet frames instead of IP packets. + EthernetHeader bool + + // ClosedFunc is a function to be called when an endpoint's peer (if + // any) closes its end of the communication pipe. + ClosedFunc func(tcpip.Error) + + // Address is the link address for this endpoint. Only used if + // EthernetHeader is true. + Address tcpip.LinkAddress + + // SaveRestore if true, indicates that this NIC capability set should + // include CapabilitySaveRestore + SaveRestore bool + + // DisconnectOk if true, indicates that this NIC capability set should + // include CapabilityDisconnectOk. + DisconnectOk bool + + // PacketDispatchMode specifies the type of inbound dispatcher to be + // used for this endpoint. + PacketDispatchMode PacketDispatchMode + + // TXChecksumOffload if true, indicates that this endpoints capability + // set should include CapabilityTXChecksumOffload. + TXChecksumOffload bool + + // RXChecksumOffload if true, indicates that this endpoints capability + // set should include CapabilityRXChecksumOffload. + RXChecksumOffload bool + + // If MaxSyscallHeaderBytes is non-zero, it is the maximum number of bytes + // of struct iovec, msghdr, and mmsghdr that may be passed by each host + // system call. + MaxSyscallHeaderBytes int + + // InterfaceIndex is the interface index of the underlying device. + InterfaceIndex int + + // ProcessorsPerChannel is the number of goroutines used to handle packets + // from each FD. + ProcessorsPerChannel int +} + +// New creates a new fd-based endpoint. +// +// Makes fd non-blocking, but does not take ownership of fd, which must remain +// open for the lifetime of the returned endpoint (until after the endpoint has +// stopped being using and Wait returns). +func New(opts *Options) (stack.LinkEndpoint, error) { + caps := stack.LinkEndpointCapabilities(0) + if opts.RXChecksumOffload { + caps |= stack.CapabilityRXChecksumOffload + } + + if opts.TXChecksumOffload { + caps |= stack.CapabilityTXChecksumOffload + } + + hdrSize := 0 + if opts.EthernetHeader { + hdrSize = header.EthernetMinimumSize + caps |= stack.CapabilityResolutionRequired + } + + if opts.SaveRestore { + caps |= stack.CapabilitySaveRestore + } + + if opts.DisconnectOk { + caps |= stack.CapabilityDisconnectOk + } + + if len(opts.FDs) == 0 { + return nil, fmt.Errorf("opts.FD is empty, at least one FD must be specified") + } + + if opts.MaxSyscallHeaderBytes < 0 { + return nil, fmt.Errorf("opts.MaxSyscallHeaderBytes is negative") + } + + e := &endpoint{ + mtu: opts.MTU, + caps: caps, + closed: opts.ClosedFunc, + addr: opts.Address, + hdrSize: hdrSize, + packetDispatchMode: opts.PacketDispatchMode, + maxSyscallHeaderBytes: uintptr(opts.MaxSyscallHeaderBytes), + writevMaxIovs: rawfile.MaxIovs, + batchSize: int((512*1024)/(opts.MTU)) + 1, + } + if e.maxSyscallHeaderBytes != 0 { + if max := int(e.maxSyscallHeaderBytes / rawfile.SizeofIovec); max < e.writevMaxIovs { + e.writevMaxIovs = max + } + } + + // Create per channel dispatchers. + for _, fd := range opts.FDs { + if err := unix.SetNonblock(fd, true); err != nil { + return nil, fmt.Errorf("unix.SetNonblock(%v) failed: %v", fd, err) + } + + e.fds = append(e.fds, fdInfo{fd: fd, isSocket: true}) + if opts.ProcessorsPerChannel == 0 { + opts.ProcessorsPerChannel = common.Max(1, runtime.GOMAXPROCS(0)/len(opts.FDs)) + } + + inboundDispatcher, err := newRecvMMsgDispatcher(fd, e, opts) + if err != nil { + return nil, fmt.Errorf("createInboundDispatcher(...) = %v", err) + } + e.inboundDispatchers = append(e.inboundDispatchers, inboundDispatcher) + } + + return e, nil +} + +func isSocketFD(fd int) (bool, error) { + var stat unix.Stat_t + if err := unix.Fstat(fd, &stat); err != nil { + return false, fmt.Errorf("unix.Fstat(%v,...) failed: %v", fd, err) + } + return (stat.Mode & unix.S_IFSOCK) == unix.S_IFSOCK, nil +} + +// Attach launches the goroutine that reads packets from the file descriptor and +// dispatches them via the provided dispatcher. If one is already attached, +// then nothing happens. +// +// Attach implements stack.LinkEndpoint.Attach. +func (e *endpoint) Attach(dispatcher stack.NetworkDispatcher) { + e.mu.Lock() + + // nil means the NIC is being removed. + if dispatcher == nil && e.dispatcher != nil { + for _, dispatcher := range e.inboundDispatchers { + dispatcher.Stop() + } + e.dispatcher = nil + // NOTE(gvisor.dev/issue/11456): Unlock e.mu before e.Wait(). + e.mu.Unlock() + e.Wait() + return + } + defer e.mu.Unlock() + if dispatcher != nil && e.dispatcher == nil { + e.dispatcher = dispatcher + // Link endpoints are not savable. When transportation endpoints are + // saved, they stop sending outgoing packets and all incoming packets + // are rejected. + for i := range e.inboundDispatchers { + e.wg.Add(1) + go func(i int) { // S/R-SAFE: See above. + e.dispatchLoop(e.inboundDispatchers[i]) + e.wg.Done() + }(i) + } + } +} + +// IsAttached implements stack.LinkEndpoint.IsAttached. +func (e *endpoint) IsAttached() bool { + e.mu.RLock() + defer e.mu.RUnlock() + return e.dispatcher != nil +} + +// MTU implements stack.LinkEndpoint.MTU. +func (e *endpoint) MTU() uint32 { + e.mu.RLock() + defer e.mu.RUnlock() + return e.mtu +} + +// SetMTU implements stack.LinkEndpoint.SetMTU. +func (e *endpoint) SetMTU(mtu uint32) { + e.mu.Lock() + defer e.mu.Unlock() + e.mtu = mtu +} + +// Capabilities implements stack.LinkEndpoint.Capabilities. +func (e *endpoint) Capabilities() stack.LinkEndpointCapabilities { + return e.caps +} + +// MaxHeaderLength returns the maximum size of the link-layer header. +func (e *endpoint) MaxHeaderLength() uint16 { + return uint16(e.hdrSize) +} + +// LinkAddress returns the link address of this endpoint. +func (e *endpoint) LinkAddress() tcpip.LinkAddress { + e.mu.RLock() + defer e.mu.RUnlock() + return e.addr +} + +// SetLinkAddress implements stack.LinkEndpoint.SetLinkAddress. +func (e *endpoint) SetLinkAddress(addr tcpip.LinkAddress) { + e.mu.Lock() + defer e.mu.Unlock() + e.addr = addr +} + +// Wait implements stack.LinkEndpoint.Wait. It waits for the endpoint to stop +// reading from its FD. +func (e *endpoint) Wait() { + e.wg.Wait() +} + +// AddHeader implements stack.LinkEndpoint.AddHeader. +func (e *endpoint) AddHeader(pkt *stack.PacketBuffer) { + if e.hdrSize > 0 { + // Add ethernet header if needed. + eth := header.Ethernet(pkt.LinkHeader().Push(header.EthernetMinimumSize)) + eth.Encode(&header.EthernetFields{ + SrcAddr: pkt.EgressRoute.LocalLinkAddress, + DstAddr: pkt.EgressRoute.RemoteLinkAddress, + Type: pkt.NetworkProtocolNumber, + }) + } +} + +func (e *endpoint) parseHeader(pkt *stack.PacketBuffer) (header.Ethernet, bool) { + if e.hdrSize <= 0 { + return nil, true + } + hdrBytes, ok := pkt.LinkHeader().Consume(e.hdrSize) + if !ok { + return nil, false + } + hdr := header.Ethernet(hdrBytes) + pkt.NetworkProtocolNumber = hdr.Type() + return hdr, true +} + +// parseInboundHeader parses the link header of pkt and returns true if the +// header is well-formed and sent to this endpoint's MAC or the broadcast +// address. +func (e *endpoint) parseInboundHeader(pkt *stack.PacketBuffer, wantAddr tcpip.LinkAddress) bool { + hdr, ok := e.parseHeader(pkt) + if !ok || e.hdrSize <= 0 { + return ok + } + dstAddr := hdr.DestinationAddress() + // Per RFC 9542 2.1 on the least significant bit of the first octet of + // a MAC address: "If it is zero, the MAC address is unicast. If it is + // a one, the address is groupcast (multicast or broadcast)." Multicast + // and broadcast are the same thing to ethernet; they are both sent to + // everyone. + return dstAddr == wantAddr || byte(dstAddr[0])&0x01 == 1 +} + +// ParseHeader implements stack.LinkEndpoint.ParseHeader. +func (e *endpoint) ParseHeader(pkt *stack.PacketBuffer) bool { + _, ok := e.parseHeader(pkt) + return ok +} + +var ( + packetHeader4 = []byte{0x00, 0x00, 0x00, unix.AF_INET} + packetHeader6 = []byte{0x00, 0x00, 0x00, unix.AF_INET6} +) + +// writePacket writes outbound packets to the file descriptor. If it is not +// currently writable, the packet is dropped. +func (e *endpoint) writePacket(pkt *stack.PacketBuffer) tcpip.Error { + fdInfo := e.fds[pkt.Hash%uint32(len(e.fds))] + fd := fdInfo.fd + var vnetHdrBuf []byte + if pkt.NetworkProtocolNumber == header.IPv4ProtocolNumber { + vnetHdrBuf = packetHeader4 + } else { + vnetHdrBuf = packetHeader6 + } + views := pkt.AsSlices() + numIovecs := len(views) + if len(vnetHdrBuf) != 0 { + numIovecs++ + } + if numIovecs > e.writevMaxIovs { + numIovecs = e.writevMaxIovs + } + + // Allocate small iovec arrays on the stack. + var iovecsArr [8]unix.Iovec + iovecs := iovecsArr[:0] + if numIovecs > len(iovecsArr) { + iovecs = make([]unix.Iovec, 0, numIovecs) + } + iovecs = rawfile.AppendIovecFromBytes(iovecs, vnetHdrBuf, numIovecs) + for _, v := range views { + iovecs = rawfile.AppendIovecFromBytes(iovecs, v, numIovecs) + } + if errno := rawfile.NonBlockingWriteIovec(fd, iovecs); errno != 0 { + return TranslateErrno(errno) + } + return nil +} + +func (e *endpoint) sendBatch(batchFDInfo fdInfo, pkts []*stack.PacketBuffer) (int, tcpip.Error) { + // Degrade to writePacket if underlying fd is not a socket. + if !batchFDInfo.isSocket { + var written int + var err tcpip.Error + for written < len(pkts) { + if err = e.writePacket(pkts[written]); err != nil { + break + } + written++ + } + return written, err + } + + // Send a batch of packets through batchFD. + batchFD := batchFDInfo.fd + mmsgHdrsStorage := make([]rawfile.MsgHdrX, 0, len(pkts)) + packets := 0 + for packets < len(pkts) { + mmsgHdrs := mmsgHdrsStorage + batch := pkts[packets:] + syscallHeaderBytes := uintptr(0) + for _, pkt := range batch { + var vnetHdrBuf []byte + if pkt.NetworkProtocolNumber == header.IPv4ProtocolNumber { + vnetHdrBuf = packetHeader4 + } else { + vnetHdrBuf = packetHeader6 + } + views, offset := pkt.AsViewList() + var skipped int + var view *buffer.View + for view = views.Front(); view != nil && offset >= view.Size(); view = view.Next() { + offset -= view.Size() + skipped++ + } + + // We've made it to the usable views. + numIovecs := views.Len() - skipped + if len(vnetHdrBuf) != 0 { + numIovecs++ + } + if numIovecs > rawfile.MaxIovs { + numIovecs = rawfile.MaxIovs + } + if e.maxSyscallHeaderBytes != 0 { + syscallHeaderBytes += rawfile.SizeofMsgHdrX + uintptr(numIovecs)*rawfile.SizeofIovec + if syscallHeaderBytes > e.maxSyscallHeaderBytes { + // We can't fit this packet into this call to sendmmsg(). + // We could potentially do so if we reduced numIovecs + // further, but this might incur considerable extra + // copying. Leave it to the next batch instead. + break + } + } + + // We can't easily allocate iovec arrays on the stack here since + // they will escape this loop iteration via mmsgHdrs. + iovecs := make([]unix.Iovec, 0, numIovecs) + iovecs = rawfile.AppendIovecFromBytes(iovecs, vnetHdrBuf, numIovecs) + // At most one slice has a non-zero offset. + iovecs = rawfile.AppendIovecFromBytes(iovecs, view.AsSlice()[offset:], numIovecs) + for view = view.Next(); view != nil; view = view.Next() { + iovecs = rawfile.AppendIovecFromBytes(iovecs, view.AsSlice(), numIovecs) + } + + var mmsgHdr rawfile.MsgHdrX + mmsgHdr.Msg.Iov = &iovecs[0] + mmsgHdr.Msg.SetIovlen(len(iovecs)) + // mmsgHdr.DataLen = uint32(len(iovecs)) + mmsgHdrs = append(mmsgHdrs, mmsgHdr) + } + + if len(mmsgHdrs) == 0 { + // We can't fit batch[0] into a mmsghdr while staying under + // e.maxSyscallHeaderBytes. Use WritePacket, which will avoid the + // mmsghdr (by using writev) and re-buffer iovecs more aggressively + // if necessary (by using e.writevMaxIovs instead of + // rawfile.MaxIovs). + pkt := batch[0] + if err := e.writePacket(pkt); err != nil { + return packets, err + } + packets++ + } else { + for len(mmsgHdrs) > 0 { + sent, errno := rawfile.NonBlockingSendMMsg(batchFD, mmsgHdrs) + if errno != 0 { + return packets, TranslateErrno(errno) + } + packets += sent + mmsgHdrs = mmsgHdrs[sent:] + } + } + } + + return packets, nil +} + +// WritePackets writes outbound packets to the underlying file descriptors. If +// one is not currently writable, the packet is dropped. +// +// Being a batch API, each packet in pkts should have the following +// fields populated: +// - pkt.EgressRoute +// - pkt.GSOOptions +// - pkt.NetworkProtocolNumber +func (e *endpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error) { + // Preallocate to avoid repeated reallocation as we append to batch. + batch := make([]*stack.PacketBuffer, 0, e.batchSize) + batchFDInfo := fdInfo{fd: -1, isSocket: false} + sentPackets := 0 + for _, pkt := range pkts.AsSlice() { + if len(batch) == 0 { + batchFDInfo = e.fds[pkt.Hash%uint32(len(e.fds))] + } + pktFDInfo := e.fds[pkt.Hash%uint32(len(e.fds))] + if sendNow := pktFDInfo != batchFDInfo; !sendNow { + batch = append(batch, pkt) + continue + } + n, err := e.sendBatch(batchFDInfo, batch) + sentPackets += n + if err != nil { + return sentPackets, err + } + batch = batch[:0] + batch = append(batch, pkt) + batchFDInfo = pktFDInfo + } + + if len(batch) != 0 { + n, err := e.sendBatch(batchFDInfo, batch) + sentPackets += n + if err != nil { + return sentPackets, err + } + } + return sentPackets, nil +} + +// dispatchLoop reads packets from the file descriptor in a loop and dispatches +// them to the network stack. +func (e *endpoint) dispatchLoop(inboundDispatcher linkDispatcher) tcpip.Error { + for { + cont, err := inboundDispatcher.dispatch() + if err != nil || !cont { + if e.closed != nil { + e.closed(err) + } + inboundDispatcher.release() + return err + } + } +} + +// GSOMaxSize implements stack.GSOEndpoint. +func (e *endpoint) GSOMaxSize() uint32 { + return 0 +} + +// SupportedGSO implements stack.GSOEndpoint. +func (e *endpoint) SupportedGSO() stack.SupportedGSO { + return stack.GSONotSupported +} + +// ARPHardwareType implements stack.LinkEndpoint.ARPHardwareType. +func (e *endpoint) ARPHardwareType() header.ARPHardwareType { + if e.hdrSize > 0 { + return header.ARPHardwareEther + } + return header.ARPHardwareNone +} + +// Close implements stack.LinkEndpoint. +func (e *endpoint) Close() {} + +// SetOnCloseAction implements stack.LinkEndpoint. +func (*endpoint) SetOnCloseAction(func()) {} diff --git a/internal/fdbased_darwin/endpoint_mutex.go b/internal/fdbased_darwin/endpoint_mutex.go new file mode 100644 index 00000000..d05b2640 --- /dev/null +++ b/internal/fdbased_darwin/endpoint_mutex.go @@ -0,0 +1,96 @@ +package fdbased + +import ( + "reflect" + + "github.com/sagernet/gvisor/pkg/sync" + "github.com/sagernet/gvisor/pkg/sync/locking" +) + +// RWMutex is sync.RWMutex with the correctness validator. +type endpointRWMutex struct { + mu sync.RWMutex +} + +// lockNames is a list of user-friendly lock names. +// Populated in init. +var endpointlockNames []string + +// lockNameIndex is used as an index passed to NestedLock and NestedUnlock, +// referring to an index within lockNames. +// Values are specified using the "consts" field of go_template_instance. +type endpointlockNameIndex int + +// DO NOT REMOVE: The following function automatically replaced with lock index constants. +// LOCK_NAME_INDEX_CONSTANTS +const () + +// Lock locks m. +// +checklocksignore +func (m *endpointRWMutex) Lock() { + locking.AddGLock(endpointprefixIndex, -1) + m.mu.Lock() +} + +// NestedLock locks m knowing that another lock of the same type is held. +// +checklocksignore +func (m *endpointRWMutex) NestedLock(i endpointlockNameIndex) { + locking.AddGLock(endpointprefixIndex, int(i)) + m.mu.Lock() +} + +// Unlock unlocks m. +// +checklocksignore +func (m *endpointRWMutex) Unlock() { + m.mu.Unlock() + locking.DelGLock(endpointprefixIndex, -1) +} + +// NestedUnlock unlocks m knowing that another lock of the same type is held. +// +checklocksignore +func (m *endpointRWMutex) NestedUnlock(i endpointlockNameIndex) { + m.mu.Unlock() + locking.DelGLock(endpointprefixIndex, int(i)) +} + +// RLock locks m for reading. +// +checklocksignore +func (m *endpointRWMutex) RLock() { + locking.AddGLock(endpointprefixIndex, -1) + m.mu.RLock() +} + +// RUnlock undoes a single RLock call. +// +checklocksignore +func (m *endpointRWMutex) RUnlock() { + m.mu.RUnlock() + locking.DelGLock(endpointprefixIndex, -1) +} + +// RLockBypass locks m for reading without executing the validator. +// +checklocksignore +func (m *endpointRWMutex) RLockBypass() { + m.mu.RLock() +} + +// RUnlockBypass undoes a single RLockBypass call. +// +checklocksignore +func (m *endpointRWMutex) RUnlockBypass() { + m.mu.RUnlock() +} + +// DowngradeLock atomically unlocks rw for writing and locks it for reading. +// +checklocksignore +func (m *endpointRWMutex) DowngradeLock() { + m.mu.DowngradeLock() +} + +var endpointprefixIndex *locking.MutexClass + +// DO NOT REMOVE: The following function is automatically replaced. +func endpointinitLockNames() {} + +func init() { + endpointinitLockNames() + endpointprefixIndex = locking.NewMutexClass(reflect.TypeOf(endpointRWMutex{}), endpointlockNames) +} diff --git a/internal/fdbased_darwin/errno.go b/internal/fdbased_darwin/errno.go new file mode 100644 index 00000000..074f4e2e --- /dev/null +++ b/internal/fdbased_darwin/errno.go @@ -0,0 +1,54 @@ +package fdbased + +import ( + "github.com/sagernet/gvisor/pkg/tcpip" + + "golang.org/x/sys/unix" +) + +func TranslateErrno(e unix.Errno) tcpip.Error { + switch e { + case unix.EEXIST: + return &tcpip.ErrDuplicateAddress{} + case unix.ENETUNREACH: + return &tcpip.ErrHostUnreachable{} + case unix.EINVAL: + return &tcpip.ErrInvalidEndpointState{} + case unix.EALREADY: + return &tcpip.ErrAlreadyConnecting{} + case unix.EISCONN: + return &tcpip.ErrAlreadyConnected{} + case unix.EADDRINUSE: + return &tcpip.ErrPortInUse{} + case unix.EADDRNOTAVAIL: + return &tcpip.ErrBadLocalAddress{} + case unix.EPIPE: + return &tcpip.ErrClosedForSend{} + case unix.EWOULDBLOCK: + return &tcpip.ErrWouldBlock{} + case unix.ECONNREFUSED: + return &tcpip.ErrConnectionRefused{} + case unix.ETIMEDOUT: + return &tcpip.ErrTimeout{} + case unix.EINPROGRESS: + return &tcpip.ErrConnectStarted{} + case unix.EDESTADDRREQ: + return &tcpip.ErrDestinationRequired{} + case unix.ENOTSUP: + return &tcpip.ErrNotSupported{} + case unix.ENOTTY: + return &tcpip.ErrQueueSizeNotSupported{} + case unix.ENOTCONN: + return &tcpip.ErrNotConnected{} + case unix.ECONNRESET: + return &tcpip.ErrConnectionReset{} + case unix.ECONNABORTED: + return &tcpip.ErrConnectionAborted{} + case unix.EMSGSIZE: + return &tcpip.ErrMessageTooLong{} + case unix.ENOBUFS: + return &tcpip.ErrNoBufferSpace{} + default: + return &tcpip.ErrInvalidEndpointState{} + } +} diff --git a/internal/fdbased_darwin/packet_dispatchers.go b/internal/fdbased_darwin/packet_dispatchers.go new file mode 100644 index 00000000..967f2a88 --- /dev/null +++ b/internal/fdbased_darwin/packet_dispatchers.go @@ -0,0 +1,229 @@ +// Copyright 2018 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fdbased + +import ( + "github.com/sagernet/gvisor/pkg/buffer" + "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" + "github.com/sagernet/sing-tun/internal/stopfd_darwin" + + "golang.org/x/sys/unix" +) + +// BufConfig defines the shape of the buffer used to read packets from the NIC. +var BufConfig = []int{4, 128, 256, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768} + +// +stateify savable +type iovecBuffer struct { + // buffer is the actual buffer that holds the packet contents. Some contents + // are reused across calls to pullBuffer if number of requested bytes is + // smaller than the number of bytes allocated in the buffer. + views []*buffer.View + + // iovecs are initialized with base pointers/len of the corresponding + // entries in the views defined above, except when GSO is enabled + // (skipsVnetHdr) then the first iovec points to a buffer for the vnet header + // which is stripped before the views are passed up the stack for further + // processing. + iovecs []unix.Iovec `state:"nosave"` + + // sizes is an array of buffer sizes for the underlying views. sizes is + // immutable. + sizes []int + + // pulledIndex is the index of the last []byte buffer pulled from the + // underlying buffer storage during a call to pullBuffers. It is -1 + // if no buffer is pulled. + pulledIndex int +} + +func newIovecBuffer(sizes []int) *iovecBuffer { + b := &iovecBuffer{ + views: make([]*buffer.View, len(sizes)), + iovecs: make([]unix.Iovec, len(sizes)), + sizes: sizes, + } + return b +} + +func (b *iovecBuffer) nextIovecs() []unix.Iovec { + for i := range b.views { + if b.views[i] != nil { + break + } + v := buffer.NewViewSize(b.sizes[i]) + b.views[i] = v + b.iovecs[i] = unix.Iovec{Base: v.BasePtr()} + b.iovecs[i].SetLen(v.Size()) + } + return b.iovecs +} + +// pullBuffer extracts the enough underlying storage from b.buffer to hold n +// bytes. It removes this storage from b.buffer, returns a new buffer +// that holds the storage, and updates pulledIndex to indicate which part +// of b.buffer's storage must be reallocated during the next call to +// nextIovecs. +func (b *iovecBuffer) pullBuffer(n int) buffer.Buffer { + var views []*buffer.View + c := 0 + // Remove the used views from the buffer. + for i, v := range b.views { + c += v.Size() + if c >= n { + b.views[i].CapLength(v.Size() - (c - n)) + views = append(views, b.views[:i+1]...) + break + } + } + for i := range views { + b.views[i] = nil + } + pulled := buffer.Buffer{} + for _, v := range views { + pulled.Append(v) + } + pulled.Truncate(int64(n)) + return pulled +} + +func (b *iovecBuffer) release() { + for _, v := range b.views { + if v != nil { + v.Release() + v = nil + } + } +} + +// recvMMsgDispatcher uses the recvmmsg system call to read inbound packets and +// dispatches them. +// +// +stateify savable +type recvMMsgDispatcher struct { + stopfd.StopFD + // fd is the file descriptor used to send and receive packets. + fd int + + // e is the endpoint this dispatcher is attached to. + e *endpoint + + // bufs is an array of iovec buffers that contain packet contents. + bufs []*iovecBuffer + + // msgHdrs is an array of MMsgHdr objects where each MMsghdr is used to + // reference an array of iovecs in the iovecs field defined above. This + // array is passed as the parameter to recvmmsg call to retrieve + // potentially more than 1 packet per unix. + msgHdrs []rawfile.MsgHdrX `state:"nosave"` + + // pkts is reused to avoid allocations. + pkts stack.PacketBufferList + + // gro coalesces incoming packets to increase throughput. + gro gro.GRO + + // mgr is the processor goroutine manager. + mgr *processorManager +} + +func newRecvMMsgDispatcher(fd int, e *endpoint, opts *Options) (linkDispatcher, error) { + stopFD, err := stopfd.New() + if err != nil { + return nil, err + } + batchSize := int((512*1024)/(opts.MTU)) + 1 + d := &recvMMsgDispatcher{ + StopFD: stopFD, + fd: fd, + e: e, + bufs: make([]*iovecBuffer, batchSize), + msgHdrs: make([]rawfile.MsgHdrX, batchSize), + } + bufConfig := []int{4, int(opts.MTU)} + for i := range d.bufs { + d.bufs[i] = newIovecBuffer(bufConfig) + } + d.gro.Init(false) + d.mgr = newProcessorManager(opts, e) + d.mgr.start() + + return d, nil +} + +func (d *recvMMsgDispatcher) release() { + for _, iov := range d.bufs { + iov.release() + } + d.mgr.close() +} + +// recvMMsgDispatch reads more than one packet at a time from the file +// descriptor and dispatches it. +func (d *recvMMsgDispatcher) dispatch() (bool, tcpip.Error) { + // Fill message headers. + for k := range d.msgHdrs { + if d.msgHdrs[k].Msg.Iovlen > 0 { + break + } + iovecs := d.bufs[k].nextIovecs() + iovLen := len(iovecs) + d.msgHdrs[k].DataLen = 0 + d.msgHdrs[k].Msg.Iov = &iovecs[0] + d.msgHdrs[k].Msg.SetIovlen(iovLen) + } + + nMsgs, errno := rawfile.BlockingRecvMMsgUntilStopped(d.ReadFD, d.fd, d.msgHdrs) + if errno != 0 { + return false, TranslateErrno(errno) + } + if nMsgs == -1 { + return false, nil + } + + // Process each of received packets. + + d.e.mu.RLock() + addr := d.e.addr + dsp := d.e.dispatcher + d.e.mu.RUnlock() + + d.gro.Dispatcher = dsp + defer d.pkts.Reset() + + for k := 0; k < nMsgs; k++ { + n := int(d.msgHdrs[k].DataLen) + payload := d.bufs[k].pullBuffer(n) + payload.TrimFront(4) + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: payload, + }) + d.pkts.PushBack(pkt) + + // Mark that this iovec has been processed. + d.msgHdrs[k].Msg.Iovlen = 0 + + if d.e.parseInboundHeader(pkt, addr) { + pkt.RXChecksumValidated = d.e.caps&stack.CapabilityRXChecksumOffload != 0 + d.mgr.queuePacket(pkt, d.e.hdrSize > 0) + } + } + d.mgr.wakeReady() + + return true, nil +} diff --git a/internal/fdbased_darwin/processor_mutex.go b/internal/fdbased_darwin/processor_mutex.go new file mode 100644 index 00000000..cd297d2a --- /dev/null +++ b/internal/fdbased_darwin/processor_mutex.go @@ -0,0 +1,64 @@ +package fdbased + +import ( + "reflect" + + "github.com/sagernet/gvisor/pkg/sync" + "github.com/sagernet/gvisor/pkg/sync/locking" +) + +// Mutex is sync.Mutex with the correctness validator. +type processorMutex struct { + mu sync.Mutex +} + +var processorprefixIndex *locking.MutexClass + +// lockNames is a list of user-friendly lock names. +// Populated in init. +var processorlockNames []string + +// lockNameIndex is used as an index passed to NestedLock and NestedUnlock, +// referring to an index within lockNames. +// Values are specified using the "consts" field of go_template_instance. +type processorlockNameIndex int + +// DO NOT REMOVE: The following function automatically replaced with lock index constants. +// LOCK_NAME_INDEX_CONSTANTS +const () + +// Lock locks m. +// +checklocksignore +func (m *processorMutex) Lock() { + locking.AddGLock(processorprefixIndex, -1) + m.mu.Lock() +} + +// NestedLock locks m knowing that another lock of the same type is held. +// +checklocksignore +func (m *processorMutex) NestedLock(i processorlockNameIndex) { + locking.AddGLock(processorprefixIndex, int(i)) + m.mu.Lock() +} + +// Unlock unlocks m. +// +checklocksignore +func (m *processorMutex) Unlock() { + locking.DelGLock(processorprefixIndex, -1) + m.mu.Unlock() +} + +// NestedUnlock unlocks m knowing that another lock of the same type is held. +// +checklocksignore +func (m *processorMutex) NestedUnlock(i processorlockNameIndex) { + locking.DelGLock(processorprefixIndex, int(i)) + m.mu.Unlock() +} + +// DO NOT REMOVE: The following function is automatically replaced. +func processorinitLockNames() {} + +func init() { + processorinitLockNames() + processorprefixIndex = locking.NewMutexClass(reflect.TypeOf(processorMutex{}), processorlockNames) +} diff --git a/internal/fdbased_darwin/processors.go b/internal/fdbased_darwin/processors.go new file mode 100644 index 00000000..9df6cfa4 --- /dev/null +++ b/internal/fdbased_darwin/processors.go @@ -0,0 +1,275 @@ +// Copyright 2024 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fdbased + +import ( + "context" + "encoding/binary" + + "github.com/sagernet/gvisor/pkg/rand" + "github.com/sagernet/gvisor/pkg/sleep" + "github.com/sagernet/gvisor/pkg/sync" + "github.com/sagernet/gvisor/pkg/tcpip" + "github.com/sagernet/gvisor/pkg/tcpip/hash/jenkins" + "github.com/sagernet/gvisor/pkg/tcpip/header" + "github.com/sagernet/gvisor/pkg/tcpip/stack" + "github.com/sagernet/gvisor/pkg/tcpip/stack/gro" +) + +// +stateify savable +type processor struct { + mu processorMutex `state:"nosave"` + // +checklocks:mu + pkts stack.PacketBufferList + + e *endpoint + gro gro.GRO + sleeper sleep.Sleeper + packetWaker sleep.Waker + closeWaker sleep.Waker +} + +func (p *processor) start(wg *sync.WaitGroup) { + defer wg.Done() + defer p.sleeper.Done() + for { + switch w := p.sleeper.Fetch(true); { + case w == &p.packetWaker: + p.deliverPackets() + case w == &p.closeWaker: + p.mu.Lock() + p.pkts.Reset() + p.mu.Unlock() + return + } + } +} + +func (p *processor) deliverPackets() { + p.e.mu.RLock() + p.gro.Dispatcher = p.e.dispatcher + p.e.mu.RUnlock() + if p.gro.Dispatcher == nil { + p.mu.Lock() + p.pkts.Reset() + p.mu.Unlock() + return + } + + p.mu.Lock() + for p.pkts.Len() > 0 { + pkt := p.pkts.PopFront() + p.mu.Unlock() + p.gro.Enqueue(pkt) + pkt.DecRef() + p.mu.Lock() + } + p.mu.Unlock() + p.gro.Flush() +} + +// processorManager handles starting, closing, and queuing packets on processor +// goroutines. +// +// +stateify savable +type processorManager struct { + processors []processor + seed uint32 + wg sync.WaitGroup `state:"nosave"` + e *endpoint + ready []bool +} + +// newProcessorManager creates a new processor manager. +func newProcessorManager(opts *Options, e *endpoint) *processorManager { + m := &processorManager{} + m.seed = rand.Uint32() + m.ready = make([]bool, opts.ProcessorsPerChannel) + m.processors = make([]processor, opts.ProcessorsPerChannel) + m.e = e + m.wg.Add(opts.ProcessorsPerChannel) + + for i := range m.processors { + p := &m.processors[i] + p.sleeper.AddWaker(&p.packetWaker) + p.sleeper.AddWaker(&p.closeWaker) + p.gro.Init(false) + p.e = e + } + + return m +} + +// start starts the processor goroutines if the processor manager is configured +// with more than one processor. +func (m *processorManager) start() { + for i := range m.processors { + p := &m.processors[i] + // Only start processor in a separate goroutine if we have multiple of them. + if len(m.processors) > 1 { + go p.start(&m.wg) + } + } +} + +// afterLoad is invoked by stateify. +func (m *processorManager) afterLoad(context.Context) { + m.wg.Add(len(m.processors)) + m.start() +} + +func (m *processorManager) connectionHash(cid *connectionID) uint32 { + var payload [4]byte + binary.LittleEndian.PutUint16(payload[0:], cid.srcPort) + binary.LittleEndian.PutUint16(payload[2:], cid.dstPort) + + h := jenkins.Sum32(m.seed) + h.Write(payload[:]) + h.Write(cid.srcAddr) + h.Write(cid.dstAddr) + return h.Sum32() +} + +// queuePacket queues a packet to be delivered to the appropriate processor. +func (m *processorManager) queuePacket(pkt *stack.PacketBuffer, hasEthHeader bool) { + var pIdx uint32 + cid, nonConnectionPkt := tcpipConnectionID(pkt) + if !hasEthHeader { + if nonConnectionPkt { + // If there's no eth header this should be a standard tcpip packet. If + // it isn't the packet is invalid so drop it. + return + } + pkt.NetworkProtocolNumber = cid.proto + } + if len(m.processors) == 1 || nonConnectionPkt { + // If the packet is not associated with an active connection, use the + // first processor. + pIdx = 0 + } else { + pIdx = m.connectionHash(&cid) % uint32(len(m.processors)) + } + p := &m.processors[pIdx] + p.mu.Lock() + defer p.mu.Unlock() + p.pkts.PushBack(pkt.IncRef()) + m.ready[pIdx] = true +} + +type connectionID struct { + srcAddr, dstAddr []byte + srcPort, dstPort uint16 + proto tcpip.NetworkProtocolNumber +} + +// tcpipConnectionID returns a tcpip connection id tuple based on the data found +// in the packet. It returns true if the packet is not associated with an active +// connection (e.g ARP, NDP, etc). The method assumes link headers have already +// been processed if they were present. +func tcpipConnectionID(pkt *stack.PacketBuffer) (connectionID, bool) { + var cid connectionID + h, ok := pkt.Data().PullUp(1) + if !ok { + // Skip this packet. + return cid, true + } + + const tcpSrcDstPortLen = 4 + switch header.IPVersion(h) { + case header.IPv4Version: + hdrLen := header.IPv4(h).HeaderLength() + h, ok = pkt.Data().PullUp(int(hdrLen) + tcpSrcDstPortLen) + if !ok { + return cid, true + } + ipHdr := header.IPv4(h[:hdrLen]) + tcpHdr := header.TCP(h[hdrLen:][:tcpSrcDstPortLen]) + + cid.srcAddr = ipHdr.SourceAddressSlice() + cid.dstAddr = ipHdr.DestinationAddressSlice() + // All fragment packets need to be processed by the same goroutine, so + // only record the TCP ports if this is not a fragment packet. + if ipHdr.IsValid(pkt.Data().Size()) && !ipHdr.More() && ipHdr.FragmentOffset() == 0 { + cid.srcPort = tcpHdr.SourcePort() + cid.dstPort = tcpHdr.DestinationPort() + } + cid.proto = header.IPv4ProtocolNumber + case header.IPv6Version: + h, ok = pkt.Data().PullUp(header.IPv6FixedHeaderSize + tcpSrcDstPortLen) + if !ok { + return cid, true + } + ipHdr := header.IPv6(h) + + var tcpHdr header.TCP + if tcpip.TransportProtocolNumber(ipHdr.NextHeader()) == header.TCPProtocolNumber { + tcpHdr = header.TCP(h[header.IPv6FixedHeaderSize:][:tcpSrcDstPortLen]) + } else { + // Slow path for IPv6 extension headers :(. + dataBuf := pkt.Data().ToBuffer() + dataBuf.TrimFront(header.IPv6MinimumSize) + it := header.MakeIPv6PayloadIterator(header.IPv6ExtensionHeaderIdentifier(ipHdr.NextHeader()), dataBuf) + defer it.Release() + for { + hdr, done, err := it.Next() + if done || err != nil { + break + } + hdr.Release() + } + h, ok = pkt.Data().PullUp(int(it.HeaderOffset()) + tcpSrcDstPortLen) + if !ok { + return cid, true + } + tcpHdr = header.TCP(h[it.HeaderOffset():][:tcpSrcDstPortLen]) + } + cid.srcAddr = ipHdr.SourceAddressSlice() + cid.dstAddr = ipHdr.DestinationAddressSlice() + cid.srcPort = tcpHdr.SourcePort() + cid.dstPort = tcpHdr.DestinationPort() + cid.proto = header.IPv6ProtocolNumber + default: + return cid, true + } + return cid, false +} + +func (m *processorManager) close() { + if len(m.processors) < 2 { + return + } + for i := range m.processors { + p := &m.processors[i] + p.closeWaker.Assert() + } +} + +// wakeReady wakes up all processors that have a packet queued. If there is only +// one processor, the method delivers the packet inline without waking a +// goroutine. +func (m *processorManager) wakeReady() { + for i, ready := range m.ready { + if !ready { + continue + } + p := &m.processors[i] + if len(m.processors) > 1 { + p.packetWaker.Assert() + } else { + p.deliverPackets() + } + m.ready[i] = false + } +} diff --git a/internal/rawfile_darwin/rawfile.go b/internal/rawfile_darwin/rawfile.go new file mode 100644 index 00000000..b73bd82f --- /dev/null +++ b/internal/rawfile_darwin/rawfile.go @@ -0,0 +1,188 @@ +package rawfile + +import ( + "reflect" + "unsafe" + + "golang.org/x/sys/unix" +) + +// SizeofIovec is the size of a unix.Iovec in bytes. +const SizeofIovec = unsafe.Sizeof(unix.Iovec{}) + +// MaxIovs is UIO_MAXIOV, the maximum number of iovecs that may be passed to a +// host system call in a single array. +const MaxIovs = 1024 + +// IovecFromBytes returns a unix.Iovec representing bs. +// +// Preconditions: len(bs) > 0. +func IovecFromBytes(bs []byte) unix.Iovec { + iov := unix.Iovec{ + Base: &bs[0], + } + iov.SetLen(len(bs)) + 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 +} + +// AppendIovecFromBytes returns append(iovs, IovecFromBytes(bs)). If len(bs) == +// 0, AppendIovecFromBytes returns iovs without modification. If len(iovs) >= +// max, AppendIovecFromBytes replaces the final iovec in iovs with one that +// also includes the contents of bs. Note that this implies that +// AppendIovecFromBytes is only usable when the returned iovec slice is used as +// the source of a write. +func AppendIovecFromBytes(iovs []unix.Iovec, bs []byte, max int) []unix.Iovec { + if len(bs) == 0 { + return iovs + } + if len(iovs) < max { + return append(iovs, IovecFromBytes(bs)) + } + iovs[len(iovs)-1] = IovecFromBytes(append(bytesFromIovec(iovs[len(iovs)-1]), bs...)) + return iovs +} + +type MsgHdrX struct { + Msg unix.Msghdr + DataLen uint32 +} + +func NonBlockingSendMMsg(fd int, msgHdrs []MsgHdrX) (int, unix.Errno) { + 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 +} + +const SizeofMsgHdrX = unsafe.Sizeof(MsgHdrX{}) + +// NonBlockingWriteIovec writes iovec to a file descriptor in a single unix. +// It fails if partial data is written. +func NonBlockingWriteIovec(fd int, iovec []unix.Iovec) unix.Errno { + iovecLen := uintptr(len(iovec)) + _, _, 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 { + n, _, e := unix.RawSyscall(unix.SYS_READV, uintptr(fd), uintptr(unsafe.Pointer(&iovecs[0])), uintptr(len(iovecs))) + if e == 0 { + return int(n), 0 + } + if e != 0 && e != unix.EWOULDBLOCK { + return 0, e + } + stopped, e := BlockingPollUntilStopped(efd, fd, unix.POLLIN) + if stopped { + return -1, e + } + if e != 0 && e != unix.EINTR { + return 0, e + } + } +} + +func BlockingRecvMMsgUntilStopped(efd int, fd int, msgHdrs []MsgHdrX) (int, unix.Errno) { + for { + 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 + } + + if e != 0 && e != unix.EWOULDBLOCK { + return 0, e + } + + stopped, e := BlockingPollUntilStopped(efd, fd, unix.POLLIN) + if stopped { + return -1, e + } + if e != 0 && e != unix.EINTR { + return 0, e + } + } +} + +func BlockingPollUntilStopped(efd int, fd int, events int16) (bool, unix.Errno) { + // Create kqueue + kq, err := unix.Kqueue() + if err != nil { + return false, unix.Errno(err.(unix.Errno)) + } + 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), + 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, + 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)) + } + + // Wait for events (blocking) + revents := make([]unix.Kevent_t, len(kevents)) + n, err := unix.Kevent(kq, nil, revents, nil) + if err != nil { + return false, unix.Errno(err.(unix.Errno)) + } + + // Check results + var efdHasData bool + var errno unix.Errno + + for i := 0; i < n; i++ { + ev := &revents[i] + + if int(ev.Ident) == efd && ev.Filter == unix.EVFILT_READ { + efdHasData = true + } + + if int(ev.Ident) == fd { + // Check for errors or EOF + 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 { + errno = unix.ECONNRESET + } + } + } + } + + return efdHasData, errno +} diff --git a/internal/stopfd_darwin/stopfd.go b/internal/stopfd_darwin/stopfd.go new file mode 100644 index 00000000..fdc39739 --- /dev/null +++ b/internal/stopfd_darwin/stopfd.go @@ -0,0 +1,61 @@ +package stopfd + +import ( + "fmt" + + "golang.org/x/sys/unix" +) + +type StopFD struct { + ReadFD int + WriteFD int +} + +func New() (StopFD, error) { + fds := make([]int, 2) + err := unix.Pipe(fds) + if err != nil { + return StopFD{ReadFD: -1, WriteFD: -1}, fmt.Errorf("failed to create pipe: %w", err) + } + + if err := unix.SetNonblock(fds[0], true); err != nil { + unix.Close(fds[0]) + unix.Close(fds[1]) + return StopFD{ReadFD: -1, WriteFD: -1}, fmt.Errorf("failed to set read end non-blocking: %w", err) + } + + if err := unix.SetNonblock(fds[1], true); err != nil { + unix.Close(fds[0]) + unix.Close(fds[1]) + return StopFD{ReadFD: -1, WriteFD: -1}, fmt.Errorf("failed to set write end non-blocking: %w", err) + } + + return StopFD{ReadFD: fds[0], WriteFD: fds[1]}, nil +} + +func (sf *StopFD) Stop() { + signal := []byte{1} + if n, err := unix.Write(sf.WriteFD, signal); n != len(signal) || err != nil { + panic(fmt.Sprintf("write(WriteFD) = (%d, %s), want (%d, nil)", n, err, len(signal))) + } +} + +func (sf *StopFD) Close() error { + var err1, err2 error + if sf.ReadFD != -1 { + err1 = unix.Close(sf.ReadFD) + sf.ReadFD = -1 + } + if sf.WriteFD != -1 { + err2 = unix.Close(sf.WriteFD) + sf.WriteFD = -1 + } + if err1 != nil { + return err1 + } + return err2 +} + +func (sf *StopFD) EFD() int { + return sf.ReadFD +} diff --git a/stack_gvisor.go b/stack_gvisor.go index 213d50fb..5e03cc60 100644 --- a/stack_gvisor.go +++ b/stack_gvisor.go @@ -40,7 +40,7 @@ type GVisor struct { type GVisorTun interface { Tun - NewEndpoint() (stack.LinkEndpoint, error) + NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) } func NewGVisor( @@ -65,12 +65,12 @@ func NewGVisor( } func (t *GVisor) Start() error { - linkEndpoint, err := t.tun.NewEndpoint() + linkEndpoint, nicOptions, err := t.tun.NewEndpoint() if err != nil { return err } linkEndpoint = &LinkEndpointFilter{linkEndpoint, t.broadcastAddr, t.tun} - ipStack, err := NewGVisorStack(linkEndpoint) + ipStack, err := NewGVisorStackWithOptions(linkEndpoint, nicOptions) if err != nil { return err } @@ -110,6 +110,10 @@ func AddrFromAddress(address tcpip.Address) netip.Addr { } func NewGVisorStack(ep stack.LinkEndpoint) (*stack.Stack, error) { + return NewGVisorStackWithOptions(ep, stack.NICOptions{}) +} + +func NewGVisorStackWithOptions(ep stack.LinkEndpoint, opts stack.NICOptions) (*stack.Stack, error) { ipStack := stack.New(stack.Options{ NetworkProtocols: []stack.NetworkProtocolFactory{ ipv4.NewProtocol, @@ -122,7 +126,7 @@ func NewGVisorStack(ep stack.LinkEndpoint) (*stack.Stack, error) { icmp.NewProtocol6, }, }) - err := ipStack.CreateNIC(DefaultNIC, ep) + err := ipStack.CreateNICWithOptions(DefaultNIC, ep, opts) if err != nil { return nil, gonet.TranslateNetstackError(err) } diff --git a/stack_mixed.go b/stack_mixed.go index 9293fb89..36eef1e6 100644 --- a/stack_mixed.go +++ b/stack_mixed.go @@ -10,6 +10,7 @@ import ( "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/common/buf" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" ) @@ -72,10 +73,14 @@ func (m *Mixed) tunLoop() { m.txChecksumOffload = linuxTUN.TXChecksumOffload() batchSize := linuxTUN.BatchSize() if batchSize > 1 { - m.batchLoop(linuxTUN, batchSize) + m.batchLoopLinux(linuxTUN, batchSize) return } } + if darwinTUN, isDarwinTUN := m.tun.(DarwinTUN); isDarwinTUN { + m.batchLoopDarwin(darwinTUN) + return + } packetBuffer := make([]byte, m.mtu+PacketOffset) for { n, err := m.tun.Read(packetBuffer) @@ -119,12 +124,12 @@ func (m *Mixed) wintunLoop(winTun WinTun) { } } -func (m *Mixed) batchLoop(linuxTUN LinuxTUN, batchSize int) { +func (m *Mixed) batchLoopLinux(linuxTUN LinuxTUN, batchSize int) { packetBuffers := make([][]byte, batchSize) writeBuffers := make([][]byte, batchSize) packetSizes := make([]int, batchSize) for i := range packetBuffers { - packetBuffers[i] = make([]byte, m.mtu+m.frontHeadroom) + packetBuffers[i] = make([]byte, m.mtu+PacketOffset+m.frontHeadroom) } for { n, err := linuxTUN.BatchRead(packetBuffers, m.frontHeadroom, packetSizes) @@ -158,6 +163,40 @@ func (m *Mixed) batchLoop(linuxTUN LinuxTUN, batchSize int) { } } +func (m *Mixed) batchLoopDarwin(darwinTUN DarwinTUN) { + var writeBuffers []*buf.Buffer + for { + buffers, err := darwinTUN.BatchRead() + if err != nil { + if E.IsClosed(err) { + return + } + m.logger.Error(E.Cause(err, "batch read packet")) + } + if len(buffers) == 0 { + continue + } + writeBuffers = writeBuffers[:0] + for _, buffer := range buffers { + packetSize := buffer.Len() + if packetSize < header.IPv4MinimumSize { + continue + } + if m.processPacket(buffer.Bytes()) { + writeBuffers = append(writeBuffers, buffer) + } else { + buffer.Release() + } + } + if len(writeBuffers) > 0 { + err = darwinTUN.BatchWrite(writeBuffers) + if err != nil { + m.logger.Trace(E.Cause(err, "batch write packet")) + } + } + } +} + func (m *Mixed) processPacket(packet []byte) bool { var ( writeBack bool diff --git a/stack_system.go b/stack_system.go index 23070fe1..6b2fde47 100644 --- a/stack_system.go +++ b/stack_system.go @@ -170,10 +170,14 @@ func (s *System) tunLoop() { s.txChecksumOffload = linuxTUN.TXChecksumOffload() batchSize := linuxTUN.BatchSize() if batchSize > 1 { - s.batchLoop(linuxTUN, batchSize) + s.batchLoopLinux(linuxTUN, batchSize) return } } + if darwinTUN, isDarwinTUN := s.tun.(DarwinTUN); isDarwinTUN { + s.batchLoopDarwin(darwinTUN) + return + } packetBuffer := make([]byte, s.mtu+PacketOffset) for { n, err := s.tun.Read(packetBuffer) @@ -217,7 +221,7 @@ func (s *System) wintunLoop(winTun WinTun) { } } -func (s *System) batchLoop(linuxTUN LinuxTUN, batchSize int) { +func (s *System) batchLoopLinux(linuxTUN LinuxTUN, batchSize int) { packetBuffers := make([][]byte, batchSize) writeBuffers := make([][]byte, batchSize) packetSizes := make([]int, batchSize) @@ -256,6 +260,40 @@ func (s *System) batchLoop(linuxTUN LinuxTUN, batchSize int) { } } +func (s *System) batchLoopDarwin(darwinTUN DarwinTUN) { + var writeBuffers []*buf.Buffer + for { + buffers, err := darwinTUN.BatchRead() + if err != nil { + if E.IsClosed(err) { + return + } + s.logger.Error(E.Cause(err, "batch read packet")) + } + if len(buffers) == 0 { + continue + } + writeBuffers = writeBuffers[:0] + for _, buffer := range buffers { + packetSize := buffer.Len() + if packetSize < header.IPv4MinimumSize { + continue + } + if s.processPacket(buffer.Bytes()) { + writeBuffers = append(writeBuffers, buffer) + } else { + buffer.Release() + } + } + if len(writeBuffers) > 0 { + err = darwinTUN.BatchWrite(writeBuffers) + if err != nil { + s.logger.Trace(E.Cause(err, "batch write packet")) + } + } + } +} + func (s *System) processPacket(packet []byte) bool { var ( writeBack bool diff --git a/tun.go b/tun.go index 882adc5a..03ded6a2 100644 --- a/tun.go +++ b/tun.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/logger" @@ -45,6 +46,12 @@ type LinuxTUN interface { TXChecksumOffload() bool } +type DarwinTUN interface { + Tun + BatchRead() ([]*buf.Buffer, error) + BatchWrite(buffers []*buf.Buffer) error +} + const ( DefaultIPRoute2TableIndex = 2022 DefaultIPRoute2RuleIndex = 9000 diff --git a/tun_darwin.go b/tun_darwin.go index 2462d75a..ed042340 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -10,6 +10,8 @@ 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" "github.com/sagernet/sing/common/bufio" @@ -21,15 +23,64 @@ import ( "golang.org/x/sys/unix" ) +var _ DarwinTUN = (*NativeTun)(nil) + const PacketOffset = 4 type NativeTun struct { - tunFile *os.File - tunWriter N.VectorisedWriter - options Options - inet4Address [4]byte - inet6Address [16]byte - routeSet bool + tunFd int + tunFile *os.File + batchSize int + iovecs []iovecBuffer + iovecsOutput []iovecBuffer + msgHdrs []rawfile.MsgHdrX + msgHdrsOutput []rawfile.MsgHdrX + buffers []*buf.Buffer + stopFd stopfd.StopFD + tunWriter N.VectorisedWriter + options Options + inet4Address [4]byte + inet6Address [16]byte + routeSet bool +} + +type iovecBuffer struct { + mtu int + buffer *buf.Buffer + iovecs []unix.Iovec +} + +func newIovecBuffer(mtu int) iovecBuffer { + return iovecBuffer{ + mtu: mtu, + iovecs: make([]unix.Iovec, 2), + } +} + +func (b *iovecBuffer) nextIovecs() []unix.Iovec { + if b.iovecs[0].Len == 0 { + headBuffer := make([]byte, PacketOffset) + b.iovecs[0].Base = &headBuffer[0] + b.iovecs[0].SetLen(PacketOffset) + } + if b.buffer == nil { + b.buffer = buf.NewSize(b.mtu) + b.iovecs[1].Base = &b.buffer.FreeBytes()[0] + b.iovecs[1].SetLen(b.mtu) + } + return b.iovecs +} + +func (b *iovecBuffer) nextIovecsOutput(buffer *buf.Buffer) []unix.Iovec { + switch header.IPVersion(buffer.Bytes()) { + case header.IPv4Version: + b.iovecs[0] = packetHeaderVec4 + case header.IPv6Version: + b.iovecs[0] = packetHeaderVec6 + } + b.iovecs[1].Base = &buffer.Bytes()[0] + b.iovecs[1].SetLen(buffer.Len()) + return b.iovecs } func (t *NativeTun) Name() (string, error) { @@ -42,6 +93,7 @@ func (t *NativeTun) Name() (string, error) { func New(options Options) (Tun, error) { var tunFd int + batchSize := ((512 * 1024) / int(options.MTU)) + 1 if options.FileDescriptor == 0 { ifIndex := -1 _, err := fmt.Sscanf(options.Name, "utun%d", &ifIndex) @@ -54,18 +106,37 @@ func New(options Options) (Tun, error) { return nil, err } - err = configure(tunFd, ifIndex, options.Name, options) + err = create(tunFd, ifIndex, options.Name, options) + if err != nil { + unix.Close(tunFd) + return nil, err + } + err = configure(tunFd, batchSize) if err != nil { unix.Close(tunFd) return nil, err } } else { tunFd = options.FileDescriptor + err := configure(tunFd, batchSize) + if err != nil { + return nil, err + } } - nativeTun := &NativeTun{ - tunFile: os.NewFile(uintptr(tunFd), "utun"), - options: options, + tunFd: tunFd, + tunFile: os.NewFile(uintptr(tunFd), "utun"), + options: options, + batchSize: batchSize, + iovecs: make([]iovecBuffer, batchSize), + iovecsOutput: make([]iovecBuffer, batchSize), + msgHdrs: make([]rawfile.MsgHdrX, batchSize), + msgHdrsOutput: make([]rawfile.MsgHdrX, batchSize), + stopFd: common.Must1(stopfd.New()), + } + for i := 0; i < batchSize; i++ { + nativeTun.iovecs[i] = newIovecBuffer(int(options.MTU)) + nativeTun.iovecsOutput[i] = newIovecBuffer(int(options.MTU)) } if len(options.Inet4Address) > 0 { nativeTun.inet4Address = options.Inet4Address[0].Addr().As4() @@ -100,10 +171,17 @@ func (t *NativeTun) Write(p []byte) (n int, err error) { } var ( - packetHeader4 = [4]byte{0x00, 0x00, 0x00, unix.AF_INET} - packetHeader6 = [4]byte{0x00, 0x00, 0x00, unix.AF_INET6} + packetHeader4 = []byte{0x00, 0x00, 0x00, unix.AF_INET} + packetHeader6 = []byte{0x00, 0x00, 0x00, unix.AF_INET6} + packetHeaderVec4 = unix.Iovec{Base: &packetHeader4[0]} + packetHeaderVec6 = unix.Iovec{Base: &packetHeader6[0]} ) +func init() { + packetHeaderVec4.SetLen(4) + packetHeaderVec6.SetLen(4) +} + func (t *NativeTun) WriteVectorised(buffers []*buf.Buffer) error { var packetHeader []byte switch header.IPVersion(buffers[0].Bytes()) { @@ -147,7 +225,7 @@ type addrLifetime6 struct { Pltime uint32 } -func configure(tunFd int, ifIndex int, name string, options Options) error { +func create(tunFd int, ifIndex int, name string, options Options) error { ctlInfo := &unix.CtlInfo{} copy(ctlInfo.Name[:], utunControlName) err := unix.IoctlCtlInfo(tunFd, ctlInfo) @@ -163,11 +241,6 @@ func configure(tunFd int, ifIndex int, name string, options Options) error { return os.NewSyscallError("Connect", err) } - err = unix.SetNonblock(tunFd, true) - if err != nil { - return os.NewSyscallError("SetNonblock", err) - } - err = useSocket(unix.AF_INET, unix.SOCK_DGRAM, 0, func(socketFd int) error { var ifr unix.IfreqMTU copy(ifr.Name[:], name) @@ -259,6 +332,65 @@ func configure(tunFd int, ifIndex int, name string, options Options) error { return nil } +func configure(tunFd int, batchSize int) error { + err := unix.SetNonblock(tunFd, true) + if err != nil { + return os.NewSyscallError("SetNonblock", err) + } + const UTUN_OPT_MAX_PENDING_PACKETS = 16 + err = unix.SetsockoptInt(tunFd, 2, UTUN_OPT_MAX_PENDING_PACKETS, batchSize) + if err != nil { + return os.NewSyscallError("SetsockoptInt UTUN_OPT_MAX_PENDING_PACKETS", err) + } + return nil +} + +func (t *NativeTun) BatchSize() int { + return t.batchSize +} + +func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) { + for i := 0; i < t.batchSize; i++ { + iovecs := t.iovecs[i].nextIovecs() + t.msgHdrs[i].DataLen = 0 + t.msgHdrs[i].Msg.Iov = &iovecs[0] + t.msgHdrs[i].Msg.Iovlen = 2 + } + n, errno := rawfile.BlockingRecvMMsgUntilStopped(t.stopFd.ReadFD, t.tunFd, t.msgHdrs) + if errno != 0 { + return nil, errno + } + if n < 1 { + return nil, nil + } + buffers := t.buffers + for k := 0; k < n; k++ { + buffer := t.iovecs[k].buffer + t.iovecs[k].buffer = nil + buffer.Truncate(int(t.msgHdrs[k].DataLen) - PacketOffset) + buffers = append(buffers, buffer) + } + t.buffers = buffers[:0] + return buffers, nil +} + +func (t *NativeTun) BatchWrite(buffers []*buf.Buffer) error { + for i, buffer := range buffers { + iovecs := t.iovecsOutput[i].nextIovecsOutput(buffer) + t.msgHdrsOutput[i].Msg.Iov = &iovecs[0] + t.msgHdrsOutput[i].Msg.Iovlen = 2 + } + _, errno := rawfile.NonBlockingSendMMsg(t.tunFd, t.msgHdrsOutput[:len(buffers)]) + if errno != 0 { + return errno + } + return nil +} + +func (t *NativeTun) TXChecksumOffload() bool { + return false +} + func (t *NativeTun) UpdateRouteOptions(tunOptions Options) error { err := t.unsetRoutes() if err != nil { diff --git a/tun_darwin_gvisor.go b/tun_darwin_gvisor.go index df46bf13..16ecbe78 100644 --- a/tun_darwin_gvisor.go +++ b/tun_darwin_gvisor.go @@ -3,132 +3,23 @@ package tun import ( - "github.com/sagernet/gvisor/pkg/buffer" - "github.com/sagernet/gvisor/pkg/tcpip" - "github.com/sagernet/gvisor/pkg/tcpip/header" + "github.com/sagernet/gvisor/pkg/tcpip/link/qdisc/fifo" "github.com/sagernet/gvisor/pkg/tcpip/stack" - "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing-tun/internal/fdbased_darwin" ) var _ GVisorTun = (*NativeTun)(nil) -func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, error) { - return &DarwinEndpoint{tun: t}, nil -} - -var _ stack.LinkEndpoint = (*DarwinEndpoint)(nil) - -type DarwinEndpoint struct { - tun *NativeTun - dispatcher stack.NetworkDispatcher -} - -func (e *DarwinEndpoint) MTU() uint32 { - return e.tun.options.MTU -} - -func (e *DarwinEndpoint) SetMTU(mtu uint32) { -} - -func (e *DarwinEndpoint) MaxHeaderLength() uint16 { - return 0 -} - -func (e *DarwinEndpoint) LinkAddress() tcpip.LinkAddress { - return "" -} - -func (e *DarwinEndpoint) SetLinkAddress(addr tcpip.LinkAddress) { -} - -func (e *DarwinEndpoint) Capabilities() stack.LinkEndpointCapabilities { - return stack.CapabilityRXChecksumOffload -} - -func (e *DarwinEndpoint) Attach(dispatcher stack.NetworkDispatcher) { - if dispatcher == nil && e.dispatcher != nil { - e.dispatcher = nil - return - } - if dispatcher != nil && e.dispatcher == nil { - e.dispatcher = dispatcher - go e.dispatchLoop() - } -} - -func (e *DarwinEndpoint) dispatchLoop() { - packetBuffer := make([]byte, e.tun.options.MTU+PacketOffset) - for { - n, err := e.tun.tunFile.Read(packetBuffer) - if err != nil { - break - } - packet := packetBuffer[PacketOffset:n] - var networkProtocol tcpip.NetworkProtocolNumber - switch header.IPVersion(packet) { - case header.IPv4Version: - networkProtocol = header.IPv4ProtocolNumber - if header.IPv4(packet).DestinationAddress().As4() == e.tun.inet4Address { - e.tun.tunFile.Write(packetBuffer[:n]) - continue - } - case header.IPv6Version: - networkProtocol = header.IPv6ProtocolNumber - if header.IPv6(packet).DestinationAddress().As16() == e.tun.inet6Address { - e.tun.tunFile.Write(packetBuffer[:n]) - continue - } - default: - e.tun.tunFile.Write(packetBuffer[:n]) - continue - } - pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ - Payload: buffer.MakeWithData(packetBuffer[4:n]), - IsForwardedPacket: true, - }) - pkt.NetworkProtocolNumber = networkProtocol - dispatcher := e.dispatcher - if dispatcher == nil { - pkt.DecRef() - return - } - dispatcher.DeliverNetworkPacket(networkProtocol, pkt) - pkt.DecRef() +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, + }) + if err != nil { + return nil, stack.NICOptions{}, err } -} - -func (e *DarwinEndpoint) IsAttached() bool { - return e.dispatcher != nil -} - -func (e *DarwinEndpoint) Wait() { -} - -func (e *DarwinEndpoint) ARPHardwareType() header.ARPHardwareType { - return header.ARPHardwareNone -} - -func (e *DarwinEndpoint) AddHeader(buffer *stack.PacketBuffer) { -} - -func (e *DarwinEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool { - return true -} - -func (e *DarwinEndpoint) WritePackets(packetBufferList stack.PacketBufferList) (int, tcpip.Error) { - var n int - for _, packet := range packetBufferList.AsSlice() { - _, err := bufio.WriteVectorised(e.tun, packet.AsSlices()) - if err != nil { - return n, &tcpip.ErrAborted{} - } - n++ - } - return n, nil -} - -func (e *DarwinEndpoint) Close() { -} - -func (e *DarwinEndpoint) SetOnCloseAction(f func()) { + return ep, stack.NICOptions{ + QDisc: fifo.New(ep, 1, 1000), + }, nil } diff --git a/tun_linux_gvisor.go b/tun_linux_gvisor.go index f82d762a..680d6f51 100644 --- a/tun_linux_gvisor.go +++ b/tun_linux_gvisor.go @@ -9,9 +9,9 @@ import ( var _ GVisorTun = (*NativeTun)(nil) -func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, error) { +func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) { if t.vnetHdr { - return fdbased.New(&fdbased.Options{ + ep, err := fdbased.New(&fdbased.Options{ FDs: []int{t.tunFd}, MTU: t.options.MTU, GSOMaxSize: gsoMaxSize, @@ -19,11 +19,20 @@ func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, error) { RXChecksumOffload: true, TXChecksumOffload: t.txChecksumOffload, }) + if err != nil { + return nil, stack.NICOptions{}, err + } + 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, + }) + if err != nil { + return nil, stack.NICOptions{}, err + } + return ep, stack.NICOptions{}, nil } - return fdbased.New(&fdbased.Options{ - FDs: []int{t.tunFd}, - MTU: t.options.MTU, - RXChecksumOffload: true, - TXChecksumOffload: t.txChecksumOffload, - }) } diff --git a/tun_windows_gvisor.go b/tun_windows_gvisor.go index b87dbfe5..463a88bd 100644 --- a/tun_windows_gvisor.go +++ b/tun_windows_gvisor.go @@ -11,8 +11,8 @@ import ( var _ GVisorTun = (*NativeTun)(nil) -func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, error) { - return &WintunEndpoint{tun: t}, nil +func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) { + return &WintunEndpoint{tun: t}, stack.NICOptions{}, nil } var _ stack.LinkEndpoint = (*WintunEndpoint)(nil) From 4c81c8a62a832bdc11a73eb57bf8d1de41c9d68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 8 Jul 2025 15:44:35 +0800 Subject: [PATCH 026/121] Fix usages of readmsg_x --- internal/fdbased_darwin/endpoint.go | 25 ++++-- internal/fdbased_darwin/packet_dispatchers.go | 90 +++++++------------ tun_darwin.go | 25 ++++-- tun_darwin_gvisor.go | 15 ++-- 4 files changed, 73 insertions(+), 82 deletions(-) diff --git a/internal/fdbased_darwin/endpoint.go b/internal/fdbased_darwin/endpoint.go index b5427930..91a33aa8 100644 --- a/internal/fdbased_darwin/endpoint.go +++ b/internal/fdbased_darwin/endpoint.go @@ -74,12 +74,29 @@ const ( // dispatch options but the one that is supported by all underlying FD // types. Readv PacketDispatchMode = iota + // RecvMMsg enables use of recvmmsg() syscall instead of readv() to + // read inbound packets. This reduces # of syscalls needed to process + // packets. + // + // NOTE: recvmmsg() is only supported for sockets, so if the underlying + // FD is not a socket then the code will still fall back to the readv() + // path. + RecvMMsg + // PacketMMap enables use of PACKET_RX_RING to receive packets from the + // NIC. PacketMMap requires that the underlying FD be an AF_PACKET. The + // primary use-case for this is runsc which uses an AF_PACKET FD to + // receive packets from the veth device. + PacketMMap ) func (p PacketDispatchMode) String() string { switch p { case Readv: return "Readv" + case RecvMMsg: + return "RecvMMsg" + case PacketMMap: + return "PacketMMap" default: return fmt.Sprintf("unknown packet dispatch mode '%d'", p) } @@ -283,14 +300,6 @@ func New(opts *Options) (stack.LinkEndpoint, error) { return e, nil } -func isSocketFD(fd int) (bool, error) { - var stat unix.Stat_t - if err := unix.Fstat(fd, &stat); err != nil { - return false, fmt.Errorf("unix.Fstat(%v,...) failed: %v", fd, err) - } - return (stat.Mode & unix.S_IFSOCK) == unix.S_IFSOCK, nil -} - // Attach launches the goroutine that reads packets from the file descriptor and // dispatches them via the provided dispatcher. If one is already attached, // then nothing happens. diff --git a/internal/fdbased_darwin/packet_dispatchers.go b/internal/fdbased_darwin/packet_dispatchers.go index 967f2a88..f66c4389 100644 --- a/internal/fdbased_darwin/packet_dispatchers.go +++ b/internal/fdbased_darwin/packet_dispatchers.go @@ -25,51 +25,31 @@ import ( "golang.org/x/sys/unix" ) -// BufConfig defines the shape of the buffer used to read packets from the NIC. -var BufConfig = []int{4, 128, 256, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768} - -// +stateify savable type iovecBuffer struct { - // buffer is the actual buffer that holds the packet contents. Some contents - // are reused across calls to pullBuffer if number of requested bytes is - // smaller than the number of bytes allocated in the buffer. - views []*buffer.View - - // iovecs are initialized with base pointers/len of the corresponding - // entries in the views defined above, except when GSO is enabled - // (skipsVnetHdr) then the first iovec points to a buffer for the vnet header - // which is stripped before the views are passed up the stack for further - // processing. + mtu int + views []*buffer.View iovecs []unix.Iovec `state:"nosave"` - - // sizes is an array of buffer sizes for the underlying views. sizes is - // immutable. - sizes []int - - // pulledIndex is the index of the last []byte buffer pulled from the - // underlying buffer storage during a call to pullBuffers. It is -1 - // if no buffer is pulled. - pulledIndex int } -func newIovecBuffer(sizes []int) *iovecBuffer { +func newIovecBuffer(mtu uint32) *iovecBuffer { b := &iovecBuffer{ - views: make([]*buffer.View, len(sizes)), - iovecs: make([]unix.Iovec, len(sizes)), - sizes: sizes, + mtu: int(mtu), + views: make([]*buffer.View, 2), + iovecs: make([]unix.Iovec, 2), } return b } func (b *iovecBuffer) nextIovecs() []unix.Iovec { - for i := range b.views { - if b.views[i] != nil { - break - } - v := buffer.NewViewSize(b.sizes[i]) - b.views[i] = v - b.iovecs[i] = unix.Iovec{Base: v.BasePtr()} - b.iovecs[i].SetLen(v.Size()) + if b.views[0] == nil { + b.views[0] = buffer.NewViewSize(4) + b.iovecs[0] = unix.Iovec{Base: b.views[0].BasePtr()} + b.iovecs[0].SetLen(4) + } + if b.views[1] == nil { + b.views[1] = buffer.NewViewSize(b.mtu) + b.iovecs[1] = unix.Iovec{Base: b.views[1].BasePtr()} + b.iovecs[1].SetLen(b.mtu) } return b.iovecs } @@ -80,25 +60,13 @@ func (b *iovecBuffer) nextIovecs() []unix.Iovec { // of b.buffer's storage must be reallocated during the next call to // nextIovecs. func (b *iovecBuffer) pullBuffer(n int) buffer.Buffer { - var views []*buffer.View - c := 0 - // Remove the used views from the buffer. - for i, v := range b.views { - c += v.Size() - if c >= n { - b.views[i].CapLength(v.Size() - (c - n)) - views = append(views, b.views[:i+1]...) - break - } - } - for i := range views { - b.views[i] = nil - } pulled := buffer.Buffer{} - for _, v := range views { - pulled.Append(v) - } + pulled.Append(b.views[0]) + pulled.Append(b.views[1]) pulled.Truncate(int64(n)) + pulled.TrimFront(4) + b.views[0] = nil + b.views[1] = nil return pulled } @@ -147,7 +115,12 @@ func newRecvMMsgDispatcher(fd int, e *endpoint, opts *Options) (linkDispatcher, if err != nil { return nil, err } - batchSize := int((512*1024)/(opts.MTU)) + 1 + var batchSize int + if opts.MTU < 49152 { + batchSize = int((512*1024)/(opts.MTU)) + 1 + } else { + batchSize = 1 + } d := &recvMMsgDispatcher{ StopFD: stopFD, fd: fd, @@ -155,9 +128,8 @@ func newRecvMMsgDispatcher(fd int, e *endpoint, opts *Options) (linkDispatcher, bufs: make([]*iovecBuffer, batchSize), msgHdrs: make([]rawfile.MsgHdrX, batchSize), } - bufConfig := []int{4, int(opts.MTU)} for i := range d.bufs { - d.bufs[i] = newIovecBuffer(bufConfig) + d.bufs[i] = newIovecBuffer(opts.MTU) } d.gro.Init(false) d.mgr = newProcessorManager(opts, e) @@ -178,12 +150,11 @@ func (d *recvMMsgDispatcher) release() { func (d *recvMMsgDispatcher) dispatch() (bool, tcpip.Error) { // Fill message headers. for k := range d.msgHdrs { - if d.msgHdrs[k].Msg.Iovlen > 0 { - break - } iovecs := d.bufs[k].nextIovecs() iovLen := len(iovecs) - d.msgHdrs[k].DataLen = 0 + // Cannot clear only the length field. Older versions of the darwin kernel will check whether other data is empty. + // https://github.com/Darm64/XNU/blob/xnu-2782.40.9/bsd/kern/uipc_syscalls.c#L2026-L2048 + d.msgHdrs[k] = rawfile.MsgHdrX{} d.msgHdrs[k].Msg.Iov = &iovecs[0] d.msgHdrs[k].Msg.SetIovlen(iovLen) } @@ -209,7 +180,6 @@ func (d *recvMMsgDispatcher) dispatch() (bool, tcpip.Error) { for k := 0; k < nMsgs; k++ { n := int(d.msgHdrs[k].DataLen) payload := d.bufs[k].pullBuffer(n) - payload.TrimFront(4) pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ Payload: payload, }) diff --git a/tun_darwin.go b/tun_darwin.go index ed042340..80e5e947 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -111,14 +111,14 @@ func New(options Options) (Tun, error) { unix.Close(tunFd) return nil, err } - err = configure(tunFd, batchSize) + err = configure(tunFd, options.MTU, batchSize) if err != nil { unix.Close(tunFd) return nil, err } } else { tunFd = options.FileDescriptor - err := configure(tunFd, batchSize) + err := configure(tunFd, options.MTU, batchSize) if err != nil { return nil, err } @@ -332,15 +332,17 @@ func create(tunFd int, ifIndex int, name string, options Options) error { return nil } -func configure(tunFd int, batchSize int) error { +func configure(tunFd int, tunMTU uint32, batchSize int) error { err := unix.SetNonblock(tunFd, true) if err != nil { return os.NewSyscallError("SetNonblock", err) } - const UTUN_OPT_MAX_PENDING_PACKETS = 16 - err = unix.SetsockoptInt(tunFd, 2, UTUN_OPT_MAX_PENDING_PACKETS, batchSize) - if err != nil { - return os.NewSyscallError("SetsockoptInt UTUN_OPT_MAX_PENDING_PACKETS", err) + if tunMTU < 49152 { + const UTUN_OPT_MAX_PENDING_PACKETS = 16 + err = unix.SetsockoptInt(tunFd, 2, UTUN_OPT_MAX_PENDING_PACKETS, batchSize) + if err != nil { + return os.NewSyscallError("SetsockoptInt UTUN_OPT_MAX_PENDING_PACKETS", err) + } } return nil } @@ -352,12 +354,19 @@ func (t *NativeTun) BatchSize() int { func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) { for i := 0; i < t.batchSize; i++ { iovecs := t.iovecs[i].nextIovecs() - t.msgHdrs[i].DataLen = 0 + // Cannot clear only the length field. Older versions of the darwin kernel will check whether other data is empty. + // https://github.com/Darm64/XNU/blob/xnu-2782.40.9/bsd/kern/uipc_syscalls.c#L2026-L2048 + t.msgHdrs[i] = rawfile.MsgHdrX{} t.msgHdrs[i].Msg.Iov = &iovecs[0] t.msgHdrs[i].Msg.Iovlen = 2 } n, errno := rawfile.BlockingRecvMMsgUntilStopped(t.stopFd.ReadFD, t.tunFd, t.msgHdrs) if errno != 0 { + for k := 0; k < n; k++ { + t.iovecs[k].buffer.Release() + t.iovecs[k].buffer = nil + } + t.buffers = t.buffers[:0] return nil, errno } if n < 1 { diff --git a/tun_darwin_gvisor.go b/tun_darwin_gvisor.go index 16ecbe78..6a2286da 100644 --- a/tun_darwin_gvisor.go +++ b/tun_darwin_gvisor.go @@ -12,14 +12,17 @@ var _ GVisorTun = (*NativeTun)(nil) 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, + FDs: []int{t.tunFd}, + MTU: t.options.MTU, + RXChecksumOffload: true, + PacketDispatchMode: fdbased.RecvMMsg, }) if err != nil { return nil, stack.NICOptions{}, err } - return ep, stack.NICOptions{ - QDisc: fifo.New(ep, 1, 1000), - }, nil + var nicOptions stack.NICOptions + if t.options.MTU < 49152 { + nicOptions.QDisc = fifo.New(ep, 1, 1000) + } + return ep, nicOptions, nil } From 7812930a486941d639eb775e056cbcb8849cac96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 10 Jul 2025 21:41:46 +0800 Subject: [PATCH 027/121] Minor fixes --- stack_mixed.go | 2 +- stack_system.go | 2 +- tun_darwin.go | 1 + tun_darwin_gvisor.go | 8 +++----- tun_linux_gvisor.go | 4 ++++ 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/stack_mixed.go b/stack_mixed.go index 36eef1e6..ffab459f 100644 --- a/stack_mixed.go +++ b/stack_mixed.go @@ -77,7 +77,7 @@ func (m *Mixed) tunLoop() { return } } - if darwinTUN, isDarwinTUN := m.tun.(DarwinTUN); isDarwinTUN { + if darwinTUN, isDarwinTUN := m.tun.(DarwinTUN); isDarwinTUN && m.mtu < 49152 { m.batchLoopDarwin(darwinTUN) return } diff --git a/stack_system.go b/stack_system.go index 6b2fde47..1feba5a8 100644 --- a/stack_system.go +++ b/stack_system.go @@ -174,7 +174,7 @@ func (s *System) tunLoop() { return } } - if darwinTUN, isDarwinTUN := s.tun.(DarwinTUN); isDarwinTUN { + if darwinTUN, isDarwinTUN := s.tun.(DarwinTUN); isDarwinTUN && s.mtu < 49152 { s.batchLoopDarwin(darwinTUN) return } diff --git a/tun_darwin.go b/tun_darwin.go index 80e5e947..53950a68 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -386,6 +386,7 @@ func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) { func (t *NativeTun) BatchWrite(buffers []*buf.Buffer) error { for i, buffer := range buffers { iovecs := t.iovecsOutput[i].nextIovecsOutput(buffer) + t.msgHdrsOutput[i] = rawfile.MsgHdrX{} t.msgHdrsOutput[i].Msg.Iov = &iovecs[0] t.msgHdrsOutput[i].Msg.Iovlen = 2 } diff --git a/tun_darwin_gvisor.go b/tun_darwin_gvisor.go index 6a2286da..cae8678b 100644 --- a/tun_darwin_gvisor.go +++ b/tun_darwin_gvisor.go @@ -20,9 +20,7 @@ func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) if err != nil { return nil, stack.NICOptions{}, err } - var nicOptions stack.NICOptions - if t.options.MTU < 49152 { - nicOptions.QDisc = fifo.New(ep, 1, 1000) - } - return ep, nicOptions, nil + return ep, stack.NICOptions{ + QDisc: fifo.New(ep, 1, 1000), + }, nil } diff --git a/tun_linux_gvisor.go b/tun_linux_gvisor.go index 680d6f51..3e26e186 100644 --- a/tun_linux_gvisor.go +++ b/tun_linux_gvisor.go @@ -7,6 +7,10 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip/stack" ) +func init() { + fdbased.BufConfig = []int{65535} +} + var _ GVisorTun = (*NativeTun)(nil) func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) { From aa1fd4d994db8ab74b1c4e7247f5197b55339a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 17 Jul 2025 17:33:41 +0800 Subject: [PATCH 028/121] Improve darwin tun performance --- internal/fdbased_darwin/endpoint.go | 15 +++- internal/fdbased_darwin/packet_dispatchers.go | 2 +- stack_gvisor.go | 1 + stack_gvisor_filter.go | 8 +- stack_gvisor_tcp.go | 5 +- stack_mixed.go | 13 +-- stack_system.go | 11 ++- stack_system_unix.go | 26 ++++++ stack_system_windows.go | 5 ++ tun.go | 8 +- tun_darwin.go | 90 +++++++++---------- tun_darwin_gvisor.go | 35 +++++++- tun_linux.go | 53 ++++------- tun_linux_gvisor.go | 25 ++++++ tun_windows.go | 6 -- tun_windows_gvisor.go | 4 + 16 files changed, 192 insertions(+), 115 deletions(-) create mode 100644 stack_system_unix.go diff --git a/internal/fdbased_darwin/endpoint.go b/internal/fdbased_darwin/endpoint.go index 91a33aa8..f26bfe30 100644 --- a/internal/fdbased_darwin/endpoint.go +++ b/internal/fdbased_darwin/endpoint.go @@ -168,6 +168,7 @@ type endpoint struct { mtu uint32 batchSize int + sendMsgX bool } // Options specify the details about the fd-based endpoint to be created. @@ -223,6 +224,9 @@ type Options struct { // ProcessorsPerChannel is the number of goroutines used to handle packets // from each FD. ProcessorsPerChannel int + + MultiPendingPackets bool + SendMsgX bool } // New creates a new fd-based endpoint. @@ -261,6 +265,12 @@ func New(opts *Options) (stack.LinkEndpoint, error) { if opts.MaxSyscallHeaderBytes < 0 { return nil, fmt.Errorf("opts.MaxSyscallHeaderBytes is negative") } + var batchSize int + if opts.MultiPendingPackets { + batchSize = int((512*1024)/(opts.MTU)) + 1 + } else { + batchSize = 1 + } e := &endpoint{ mtu: opts.MTU, @@ -271,7 +281,8 @@ func New(opts *Options) (stack.LinkEndpoint, error) { packetDispatchMode: opts.PacketDispatchMode, maxSyscallHeaderBytes: uintptr(opts.MaxSyscallHeaderBytes), writevMaxIovs: rawfile.MaxIovs, - batchSize: int((512*1024)/(opts.MTU)) + 1, + batchSize: batchSize, + sendMsgX: opts.SendMsgX, } if e.maxSyscallHeaderBytes != 0 { if max := int(e.maxSyscallHeaderBytes / rawfile.SizeofIovec); max < e.writevMaxIovs { @@ -478,7 +489,7 @@ func (e *endpoint) writePacket(pkt *stack.PacketBuffer) tcpip.Error { func (e *endpoint) sendBatch(batchFDInfo fdInfo, pkts []*stack.PacketBuffer) (int, tcpip.Error) { // Degrade to writePacket if underlying fd is not a socket. - if !batchFDInfo.isSocket { + if !batchFDInfo.isSocket || !e.sendMsgX { var written int var err tcpip.Error for written < len(pkts) { diff --git a/internal/fdbased_darwin/packet_dispatchers.go b/internal/fdbased_darwin/packet_dispatchers.go index f66c4389..a006d411 100644 --- a/internal/fdbased_darwin/packet_dispatchers.go +++ b/internal/fdbased_darwin/packet_dispatchers.go @@ -116,7 +116,7 @@ func newRecvMMsgDispatcher(fd int, e *endpoint, opts *Options) (linkDispatcher, return nil, err } var batchSize int - if opts.MTU < 49152 { + if opts.MultiPendingPackets { batchSize = int((512*1024)/(opts.MTU)) + 1 } else { batchSize = 1 diff --git a/stack_gvisor.go b/stack_gvisor.go index 5e03cc60..0dc995b4 100644 --- a/stack_gvisor.go +++ b/stack_gvisor.go @@ -40,6 +40,7 @@ type GVisor struct { type GVisorTun interface { Tun + WritePacket(pkt *stack.PacketBuffer) (int, error) NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) } diff --git a/stack_gvisor_filter.go b/stack_gvisor_filter.go index fc6319e4..18e46e8f 100644 --- a/stack_gvisor_filter.go +++ b/stack_gvisor_filter.go @@ -8,8 +8,6 @@ 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/common/bufio" - N "github.com/sagernet/sing/common/network" ) var _ stack.LinkEndpoint = (*LinkEndpointFilter)(nil) @@ -17,7 +15,7 @@ var _ stack.LinkEndpoint = (*LinkEndpointFilter)(nil) type LinkEndpointFilter struct { stack.LinkEndpoint BroadcastAddress netip.Addr - Writer N.VectorisedWriter + Writer GVisorTun } func (w *LinkEndpointFilter) Attach(dispatcher stack.NetworkDispatcher) { @@ -29,7 +27,7 @@ var _ stack.NetworkDispatcher = (*networkDispatcherFilter)(nil) type networkDispatcherFilter struct { stack.NetworkDispatcher broadcastAddress netip.Addr - writer N.VectorisedWriter + writer GVisorTun } func (w *networkDispatcherFilter) DeliverNetworkPacket(protocol tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) { @@ -49,7 +47,7 @@ func (w *networkDispatcherFilter) DeliverNetworkPacket(protocol tcpip.NetworkPro } destination := AddrFromAddress(network.DestinationAddress()) if destination == w.broadcastAddress || !destination.IsGlobalUnicast() { - _, _ = bufio.WriteVectorised(w.writer, pkt.AsSlices()) + w.writer.WritePacket(pkt) return } w.NetworkDispatcher.DeliverNetworkPacket(protocol, pkt) diff --git a/stack_gvisor_tcp.go b/stack_gvisor_tcp.go index cd397781..aad97cf6 100644 --- a/stack_gvisor_tcp.go +++ b/stack_gvisor_tcp.go @@ -13,7 +13,6 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" "github.com/sagernet/sing-tun/internal/gtcpip/checksum" "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/bufio" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) @@ -56,7 +55,7 @@ func (f *TCPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress(), ipHdr.PayloadLength()), ))) - bufio.WriteVectorised(f.tun, pkt.AsSlices()) + f.tun.WritePacket(pkt) return true } } @@ -70,7 +69,7 @@ func (f *TCPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress(), ipHdr.PayloadLength()), ))) - bufio.WriteVectorised(f.tun, pkt.AsSlices()) + f.tun.WritePacket(pkt) return true } } diff --git a/stack_mixed.go b/stack_mixed.go index ffab459f..a48639d4 100644 --- a/stack_mixed.go +++ b/stack_mixed.go @@ -11,12 +11,12 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip/transport/udp" "github.com/sagernet/sing-tun/internal/gtcpip/header" "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" ) type Mixed struct { *System + tun GVisorTun stack *stack.Stack endpoint *channel.Endpoint } @@ -30,6 +30,7 @@ func NewMixed( } return &Mixed{ System: system.(*System), + tun: system.(*System).tun.(GVisorTun), }, nil } @@ -77,7 +78,7 @@ func (m *Mixed) tunLoop() { return } } - if darwinTUN, isDarwinTUN := m.tun.(DarwinTUN); isDarwinTUN && m.mtu < 49152 { + if darwinTUN, isDarwinTUN := m.tun.(DarwinTUN); isDarwinTUN && m.multiPendingPackets { m.batchLoopDarwin(darwinTUN) return } @@ -265,11 +266,11 @@ func (m *Mixed) processIPv6(ipHdr header.IPv6) (writeBack bool, err error) { func (m *Mixed) packetLoop() { for { - packet := m.endpoint.ReadContext(m.ctx) - if packet == nil { + pkt := m.endpoint.ReadContext(m.ctx) + if pkt == nil { break } - bufio.WriteVectorised(m.tun, packet.AsSlices()) - packet.DecRef() + m.tun.WritePacket(pkt) + pkt.DecRef() } } diff --git a/stack_system.go b/stack_system.go index 1feba5a8..d549ed36 100644 --- a/stack_system.go +++ b/stack_system.go @@ -49,6 +49,7 @@ type System struct { interfaceFinder control.InterfaceFinder frontHeadroom int txChecksumOffload bool + multiPendingPackets bool } type Session struct { @@ -74,6 +75,7 @@ func NewSystem(options StackOptions) (Stack, error) { broadcastAddr: BroadcastAddr(options.TunOptions.Inet4Address), bindInterface: options.ForwarderBindInterface, interfaceFinder: options.InterfaceFinder, + multiPendingPackets: options.TunOptions.EXP_MultiPendingPackets, } if len(options.TunOptions.Inet4Address) > 0 { if !HasNextAddress(options.TunOptions.Inet4Address[0], 1) { @@ -174,7 +176,7 @@ func (s *System) tunLoop() { return } } - if darwinTUN, isDarwinTUN := s.tun.(DarwinTUN); isDarwinTUN && s.mtu < 49152 { + if darwinTUN, isDarwinTUN := s.tun.(DarwinTUN); isDarwinTUN && s.multiPendingPackets { s.batchLoopDarwin(darwinTUN) return } @@ -320,6 +322,13 @@ func (s *System) acceptLoop(listener net.Listener) { if err != nil { return } + err = acceptConn(conn) + if err != nil { + s.logger.Error("set buffer for conn: ", err) + _ = conn.Close() + listener.Close() + return + } connPort := M.SocksaddrFromNet(conn.RemoteAddr()).Port session := s.tcpNat.LookupBack(connPort) if session == nil { diff --git a/stack_system_unix.go b/stack_system_unix.go new file mode 100644 index 00000000..0f10042f --- /dev/null +++ b/stack_system_unix.go @@ -0,0 +1,26 @@ +//go:build !windows + +package tun + +import ( + "net" + + "github.com/sagernet/sing/common/control" + + "golang.org/x/sys/unix" +) + +func acceptConn(conn net.Conn) error { + return control.Conn(conn.(*net.TCPConn), func(fd uintptr) error { + const bufferSize = 1024 * 1024 + oErr := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_RCVBUF, bufferSize) + if oErr != nil { + return oErr + } + oErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SNDBUF, bufferSize) + if oErr != nil { + return oErr + } + return nil + }) +} diff --git a/stack_system_windows.go b/stack_system_windows.go index f6c66d0d..ed76d572 100644 --- a/stack_system_windows.go +++ b/stack_system_windows.go @@ -2,6 +2,7 @@ package tun import ( "errors" + "net" "os" "path/filepath" @@ -31,3 +32,7 @@ func fixWindowsFirewall() error { func retryableListenError(err error) bool { return errors.Is(err, windows.WSAEADDRNOTAVAIL) } + +func acceptConn(conn net.Conn) error { + return nil +} diff --git a/tun.go b/tun.go index 03ded6a2..92eab64a 100644 --- a/tun.go +++ b/tun.go @@ -25,7 +25,6 @@ type Handler interface { type Tun interface { io.ReadWriter - N.VectorisedWriter Name() (string, error) Start() error Close() error @@ -97,6 +96,13 @@ type Options struct { // For library usages. EXP_DisableDNSHijack bool + + // For gvisor stack, it should be enabled when MTU is less than 32768; otherwise it should be less than or equal to 8192. + // The above condition is just an estimate and not exact, calculated on M4 pro. + EXP_MultiPendingPackets bool + + // Will cause the darwin network to die, do not use. + EXP_SendMsgX bool } func (o *Options) Inet4GatewayAddr() netip.Addr { diff --git a/tun_darwin.go b/tun_darwin.go index 53950a68..e8d019bd 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -14,9 +14,7 @@ import ( "github.com/sagernet/sing-tun/internal/stopfd_darwin" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" - N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/shell" "golang.org/x/net/route" @@ -28,20 +26,21 @@ var _ DarwinTUN = (*NativeTun)(nil) const PacketOffset = 4 type NativeTun struct { - tunFd int - tunFile *os.File - batchSize int - iovecs []iovecBuffer - iovecsOutput []iovecBuffer - msgHdrs []rawfile.MsgHdrX - msgHdrsOutput []rawfile.MsgHdrX - buffers []*buf.Buffer - stopFd stopfd.StopFD - tunWriter N.VectorisedWriter - options Options - inet4Address [4]byte - inet6Address [16]byte - routeSet bool + tunFd int + tunFile *os.File + batchSize int + iovecs []iovecBuffer + iovecsOutput []iovecBuffer + iovecsOutputDefault []unix.Iovec + msgHdrs []rawfile.MsgHdrX + msgHdrsOutput []rawfile.MsgHdrX + buffers []*buf.Buffer + stopFd stopfd.StopFD + options Options + inet4Address [4]byte + inet6Address [16]byte + routeSet bool + writeMsgX bool } type iovecBuffer struct { @@ -111,14 +110,14 @@ func New(options Options) (Tun, error) { unix.Close(tunFd) return nil, err } - err = configure(tunFd, options.MTU, batchSize) + err = configure(tunFd, options.EXP_MultiPendingPackets, batchSize) if err != nil { unix.Close(tunFd) return nil, err } } else { tunFd = options.FileDescriptor - err := configure(tunFd, options.MTU, batchSize) + err := configure(tunFd, options.EXP_MultiPendingPackets, batchSize) if err != nil { return nil, err } @@ -133,6 +132,7 @@ func New(options Options) (Tun, error) { msgHdrs: make([]rawfile.MsgHdrX, batchSize), msgHdrsOutput: make([]rawfile.MsgHdrX, batchSize), stopFd: common.Must1(stopfd.New()), + writeMsgX: options.EXP_SendMsgX, } for i := 0; i < batchSize; i++ { nativeTun.iovecs[i] = newIovecBuffer(int(options.MTU)) @@ -144,11 +144,6 @@ func New(options Options) (Tun, error) { if len(options.Inet6Address) > 0 { nativeTun.inet6Address = options.Inet6Address[0].Addr().As16() } - var ok bool - nativeTun.tunWriter, ok = bufio.CreateVectorisedWriter(nativeTun.tunFile) - if !ok { - panic("create vectorised writer") - } return nativeTun, nil } @@ -182,17 +177,6 @@ func init() { packetHeaderVec6.SetLen(4) } -func (t *NativeTun) WriteVectorised(buffers []*buf.Buffer) error { - var packetHeader []byte - switch header.IPVersion(buffers[0].Bytes()) { - case header.IPv4Version: - packetHeader = packetHeader4[:] - case header.IPv6Version: - packetHeader = packetHeader6[:] - } - return t.tunWriter.WriteVectorised(append([]*buf.Buffer{buf.As(packetHeader)}, buffers...)) -} - const utunControlName = "com.apple.net.utun_control" const ( @@ -332,12 +316,12 @@ func create(tunFd int, ifIndex int, name string, options Options) error { return nil } -func configure(tunFd int, tunMTU uint32, batchSize int) error { +func configure(tunFd int, multiPendingPackets bool, batchSize int) error { err := unix.SetNonblock(tunFd, true) if err != nil { return os.NewSyscallError("SetNonblock", err) } - if tunMTU < 49152 { + if multiPendingPackets { const UTUN_OPT_MAX_PENDING_PACKETS = 16 err = unix.SetsockoptInt(tunFd, 2, UTUN_OPT_MAX_PENDING_PACKETS, batchSize) if err != nil { @@ -347,10 +331,6 @@ func configure(tunFd int, tunMTU uint32, batchSize int) error { return nil } -func (t *NativeTun) BatchSize() int { - return t.batchSize -} - func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) { for i := 0; i < t.batchSize; i++ { iovecs := t.iovecs[i].nextIovecs() @@ -384,15 +364,27 @@ func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) { } func (t *NativeTun) BatchWrite(buffers []*buf.Buffer) error { - for i, buffer := range buffers { - iovecs := t.iovecsOutput[i].nextIovecsOutput(buffer) - t.msgHdrsOutput[i] = rawfile.MsgHdrX{} - t.msgHdrsOutput[i].Msg.Iov = &iovecs[0] - t.msgHdrsOutput[i].Msg.Iovlen = 2 - } - _, errno := rawfile.NonBlockingSendMMsg(t.tunFd, t.msgHdrsOutput[:len(buffers)]) - if errno != 0 { - return errno + if !t.writeMsgX { + for i, buffer := range buffers { + t.iovecsOutput[i].nextIovecsOutput(buffer) + } + for i := range buffers { + errno := rawfile.NonBlockingWriteIovec(t.tunFd, t.iovecsOutput[i].iovecs) + if errno != 0 { + return errno + } + } + } else { + for i, buffer := range buffers { + iovecs := t.iovecsOutput[i].nextIovecsOutput(buffer) + t.msgHdrsOutput[i] = rawfile.MsgHdrX{} + t.msgHdrsOutput[i].Msg.Iov = &iovecs[0] + t.msgHdrsOutput[i].Msg.Iovlen = 2 + } + _, errno := rawfile.NonBlockingSendMMsg(t.tunFd, t.msgHdrsOutput[:len(buffers)]) + if errno != 0 { + return errno + } } return nil } diff --git a/tun_darwin_gvisor.go b/tun_darwin_gvisor.go index cae8678b..7879bb50 100644 --- a/tun_darwin_gvisor.go +++ b/tun_darwin_gvisor.go @@ -6,16 +6,43 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip/link/qdisc/fifo" "github.com/sagernet/gvisor/pkg/tcpip/stack" "github.com/sagernet/sing-tun/internal/fdbased_darwin" + "github.com/sagernet/sing-tun/internal/rawfile_darwin" + + "golang.org/x/sys/unix" ) var _ GVisorTun = (*NativeTun)(nil) +func (t *NativeTun) WritePacket(pkt *stack.PacketBuffer) (int, error) { + iovecs := t.iovecsOutputDefault + var dataLen int + for _, packetSlice := range pkt.AsSlices() { + dataLen += len(packetSlice) + iovec := unix.Iovec{ + Base: &packetSlice[0], + } + iovec.SetLen(len(packetSlice)) + iovecs = append(iovecs, iovec) + } + if cap(iovecs) > cap(t.iovecsOutputDefault) { + t.iovecsOutputDefault = iovecs[:0] + } + errno := rawfile.NonBlockingWriteIovec(t.tunFd, iovecs) + if errno == 0 { + return dataLen, nil + } else { + return 0, errno + } +} + 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, + FDs: []int{t.tunFd}, + MTU: t.options.MTU, + 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.go b/tun_linux.go index 9c283773..6d7dfed9 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -18,10 +18,8 @@ import ( "github.com/sagernet/sing-tun/internal/gtcpip/header" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" - N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/shell" "github.com/sagernet/sing/common/x/list" @@ -32,22 +30,22 @@ import ( var _ LinuxTUN = (*NativeTun)(nil) type NativeTun struct { - tunFd int - tunFile *os.File - tunWriter N.VectorisedWriter - interfaceCallback *list.Element[DefaultInterfaceUpdateCallback] - options Options - ruleIndex6 []int - readAccess sync.Mutex - writeAccess sync.Mutex - vnetHdr bool - writeBuffer []byte - gsoToWrite []int - tcpGROTable *tcpGROTable - udpGroAccess sync.Mutex - udpGROTable *udpGROTable - gro groDisablementFlags - txChecksumOffload bool + tunFd int + tunFile *os.File + iovecsOutputDefault []unix.Iovec + interfaceCallback *list.Element[DefaultInterfaceUpdateCallback] + options Options + ruleIndex6 []int + readAccess sync.Mutex + writeAccess sync.Mutex + vnetHdr bool + writeBuffer []byte + gsoToWrite []int + tcpGROTable *tcpGROTable + udpGroAccess sync.Mutex + udpGROTable *udpGROTable + gro groDisablementFlags + txChecksumOffload bool } func New(options Options) (Tun, error) { @@ -77,11 +75,6 @@ func New(options Options) (Tun, error) { options: options, } } - var ok bool - nativeTun.tunWriter, ok = bufio.CreateVectorisedWriter(nativeTun.tunFile) - if !ok { - panic("create vectorised writer") - } return nativeTun, nil } @@ -402,20 +395,6 @@ func (t *NativeTun) Write(p []byte) (n int, err error) { return t.tunFile.Write(p) } -func (t *NativeTun) WriteVectorised(buffers []*buf.Buffer) error { - if t.vnetHdr { - n := buf.LenMulti(buffers) - buffer := buf.NewSize(virtioNetHdrLen + n) - buffer.Truncate(virtioNetHdrLen) - buf.CopyMulti(buffer.Extend(n), buffers) - _, err := t.tunFile.Write(buffer.Bytes()) - buffer.Release() - return err - } else { - return t.tunWriter.WriteVectorised(buffers) - } -} - func (t *NativeTun) FrontHeadroom() int { if t.vnetHdr { return virtioNetHdrLen diff --git a/tun_linux_gvisor.go b/tun_linux_gvisor.go index 3e26e186..cb0561b6 100644 --- a/tun_linux_gvisor.go +++ b/tun_linux_gvisor.go @@ -3,8 +3,11 @@ package tun import ( + "github.com/sagernet/gvisor/pkg/rawfile" "github.com/sagernet/gvisor/pkg/tcpip/link/fdbased" "github.com/sagernet/gvisor/pkg/tcpip/stack" + + "golang.org/x/sys/unix" ) func init() { @@ -13,6 +16,28 @@ func init() { var _ GVisorTun = (*NativeTun)(nil) +func (t *NativeTun) WritePacket(pkt *stack.PacketBuffer) (int, error) { + iovecs := t.iovecsOutputDefault + var dataLen int + for _, packetSlice := range pkt.AsSlices() { + dataLen += len(packetSlice) + iovec := unix.Iovec{ + Base: &packetSlice[0], + } + iovec.SetLen(len(packetSlice)) + iovecs = append(iovecs, iovec) + } + if cap(iovecs) > cap(t.iovecsOutputDefault) { + t.iovecsOutputDefault = iovecs[:0] + } + errno := rawfile.NonBlockingWriteIovec(t.tunFd, iovecs) + if errno == 0 { + return dataLen, nil + } else { + return 0, errno + } +} + func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) { if t.vnetHdr { ep, err := fdbased.New(&fdbased.Options{ diff --git a/tun_windows.go b/tun_windows.go index dde61990..66fb13d4 100644 --- a/tun_windows.go +++ b/tun_windows.go @@ -17,7 +17,6 @@ import ( "github.com/sagernet/sing-tun/internal/wintun" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/atomic" - "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/windnsapi" @@ -517,11 +516,6 @@ func (t *NativeTun) write(packetElementList [][]byte) (n int, err error) { return 0, fmt.Errorf("write failed: %w", err) } -func (t *NativeTun) WriteVectorised(buffers []*buf.Buffer) error { - defer buf.ReleaseMulti(buffers) - return common.Error(t.write(buf.ToSliceMulti(buffers))) -} - func (t *NativeTun) Close() error { var err error t.closeOnce.Do(func() { diff --git a/tun_windows_gvisor.go b/tun_windows_gvisor.go index 463a88bd..2ead278b 100644 --- a/tun_windows_gvisor.go +++ b/tun_windows_gvisor.go @@ -11,6 +11,10 @@ import ( var _ GVisorTun = (*NativeTun)(nil) +func (t *NativeTun) WritePacket(pkt *stack.PacketBuffer) (int, error) { + return t.write(pkt.AsSlices()) +} + func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) { return &WintunEndpoint{tun: t}, stack.NICOptions{}, nil } From 3af7305b853e5474f1dbfd3f40721c147d477351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 18 Jul 2025 11:00:19 +0800 Subject: [PATCH 029/121] Fix darwin WritePacket --- tun_darwin_gvisor.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tun_darwin_gvisor.go b/tun_darwin_gvisor.go index 7879bb50..ef940c3d 100644 --- a/tun_darwin_gvisor.go +++ b/tun_darwin_gvisor.go @@ -3,6 +3,7 @@ package tun import ( + "github.com/sagernet/gvisor/pkg/tcpip/header" "github.com/sagernet/gvisor/pkg/tcpip/link/qdisc/fifo" "github.com/sagernet/gvisor/pkg/tcpip/stack" "github.com/sagernet/sing-tun/internal/fdbased_darwin" @@ -15,6 +16,11 @@ var _ GVisorTun = (*NativeTun)(nil) func (t *NativeTun) WritePacket(pkt *stack.PacketBuffer) (int, error) { iovecs := t.iovecsOutputDefault + if pkt.NetworkProtocolNumber == header.IPv4ProtocolNumber { + iovecs = append(iovecs, packetHeaderVec4) + } else { + iovecs = append(iovecs, packetHeaderVec6) + } var dataLen int for _, packetSlice := range pkt.AsSlices() { dataLen += len(packetSlice) From 0310956cc0af67510ff2c03b34ba13c1229e7d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 20 Jul 2025 16:15:25 +0800 Subject: [PATCH 030/121] Fix darwin writev --- go.mod | 2 +- go.sum | 4 ++-- tun_darwin.go | 22 ++++++++++++---------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 0bb96e75..9e3eb8d8 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/sagernet/nftables v0.3.0-beta.4 - github.com/sagernet/sing v0.6.0-beta.2 + github.com/sagernet/sing v0.7.0-beta.1 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 golang.org/x/net v0.26.0 diff --git a/go.sum b/go.sum index e2ea32a3..fb428267 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= -github.com/sagernet/sing v0.6.0-beta.2 h1:Dcutp3kxrsZes9q3oTiHQhYYjQvDn5rwp1OI9fDLYwQ= -github.com/sagernet/sing v0.6.0-beta.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.7.0-beta.1 h1:2D44KzgeDZwD/R4Ts8jwSUHTRR238a1FpXDrl7l4tVw= +github.com/sagernet/sing v0.7.0-beta.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= diff --git a/tun_darwin.go b/tun_darwin.go index e8d019bd..45efa291 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -40,7 +40,7 @@ type NativeTun struct { inet4Address [4]byte inet6Address [16]byte routeSet bool - writeMsgX bool + sendMsgX bool } type iovecBuffer struct { @@ -64,8 +64,7 @@ func (b *iovecBuffer) nextIovecs() []unix.Iovec { } if b.buffer == nil { b.buffer = buf.NewSize(b.mtu) - b.iovecs[1].Base = &b.buffer.FreeBytes()[0] - b.iovecs[1].SetLen(b.mtu) + b.iovecs[1] = b.buffer.Iovec(b.buffer.Cap()) } return b.iovecs } @@ -77,8 +76,7 @@ func (b *iovecBuffer) nextIovecsOutput(buffer *buf.Buffer) []unix.Iovec { case header.IPv6Version: b.iovecs[0] = packetHeaderVec6 } - b.iovecs[1].Base = &buffer.Bytes()[0] - b.iovecs[1].SetLen(buffer.Len()) + b.iovecs[1] = buffer.Iovec(buffer.Len()) return b.iovecs } @@ -132,7 +130,7 @@ func New(options Options) (Tun, error) { msgHdrs: make([]rawfile.MsgHdrX, batchSize), msgHdrsOutput: make([]rawfile.MsgHdrX, batchSize), stopFd: common.Must1(stopfd.New()), - writeMsgX: options.EXP_SendMsgX, + sendMsgX: options.EXP_SendMsgX, } for i := 0; i < batchSize; i++ { nativeTun.iovecs[i] = newIovecBuffer(int(options.MTU)) @@ -364,7 +362,7 @@ func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) { } func (t *NativeTun) BatchWrite(buffers []*buf.Buffer) error { - if !t.writeMsgX { + if !t.sendMsgX { for i, buffer := range buffers { t.iovecsOutput[i].nextIovecsOutput(buffer) } @@ -381,9 +379,13 @@ func (t *NativeTun) BatchWrite(buffers []*buf.Buffer) error { t.msgHdrsOutput[i].Msg.Iov = &iovecs[0] t.msgHdrsOutput[i].Msg.Iovlen = 2 } - _, errno := rawfile.NonBlockingSendMMsg(t.tunFd, t.msgHdrsOutput[:len(buffers)]) - if errno != 0 { - return errno + var n int + for n != len(buffers) { + sent, errno := rawfile.NonBlockingSendMMsg(t.tunFd, t.msgHdrsOutput[n:len(buffers)]) + if errno != 0 { + return errno + } + n += sent } } return nil From ebbe32588cfb163536ca75d44acc3bb9618c4cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 21 Jul 2025 09:44:17 +0800 Subject: [PATCH 031/121] Fix system stack --- stack_system.go | 7 ------- stack_system_unix.go | 26 -------------------------- stack_system_windows.go | 5 ----- 3 files changed, 38 deletions(-) delete mode 100644 stack_system_unix.go diff --git a/stack_system.go b/stack_system.go index d549ed36..825c5f26 100644 --- a/stack_system.go +++ b/stack_system.go @@ -322,13 +322,6 @@ func (s *System) acceptLoop(listener net.Listener) { if err != nil { return } - err = acceptConn(conn) - if err != nil { - s.logger.Error("set buffer for conn: ", err) - _ = conn.Close() - listener.Close() - return - } connPort := M.SocksaddrFromNet(conn.RemoteAddr()).Port session := s.tcpNat.LookupBack(connPort) if session == nil { diff --git a/stack_system_unix.go b/stack_system_unix.go deleted file mode 100644 index 0f10042f..00000000 --- a/stack_system_unix.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build !windows - -package tun - -import ( - "net" - - "github.com/sagernet/sing/common/control" - - "golang.org/x/sys/unix" -) - -func acceptConn(conn net.Conn) error { - return control.Conn(conn.(*net.TCPConn), func(fd uintptr) error { - const bufferSize = 1024 * 1024 - oErr := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_RCVBUF, bufferSize) - if oErr != nil { - return oErr - } - oErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SNDBUF, bufferSize) - if oErr != nil { - return oErr - } - return nil - }) -} diff --git a/stack_system_windows.go b/stack_system_windows.go index ed76d572..f6c66d0d 100644 --- a/stack_system_windows.go +++ b/stack_system_windows.go @@ -2,7 +2,6 @@ package tun import ( "errors" - "net" "os" "path/filepath" @@ -32,7 +31,3 @@ func fixWindowsFirewall() error { func retryableListenError(err error) bool { return errors.Is(err, windows.WSAEADDRNOTAVAIL) } - -func acceptConn(conn net.Conn) error { - return nil -} From 07e21b9170f4ef45af3070fdb671099dc2578b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 1 Aug 2025 16:46:29 +0800 Subject: [PATCH 032/121] Fix redirect panic --- redirect_nftables.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redirect_nftables.go b/redirect_nftables.go index b2d54f93..ec4bf5d8 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -308,7 +308,7 @@ func (r *autoRedirect) cleanupNFTables() { Name: r.tableName, Family: nftables.TableFamilyINet, }) - common.Must(r.configureOpenWRTFirewall4(nft, true)) + _ = r.configureOpenWRTFirewall4(nft, true) _ = nft.Flush() _ = nft.CloseLasting() } From 4a56d470354f688121e890155e4e8310705060ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 3 Aug 2025 00:12:24 +0800 Subject: [PATCH 033/121] test: Add log for fw4 reload error --- redirect_nftables_rules_openwrt.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redirect_nftables_rules_openwrt.go b/redirect_nftables_rules_openwrt.go index fd6a9698..cbbf2987 100644 --- a/redirect_nftables_rules_openwrt.go +++ b/redirect_nftables_rules_openwrt.go @@ -44,9 +44,9 @@ chain forward { return E.Cause(err, "clean fw4 rules") } } - _, err = shell.Exec(fw4Path, "reload").Read() + output, err := shell.Exec(fw4Path, "reload").Read() if err != nil { - return E.Cause(err, "reload fw4 rules") + return E.Extend(E.Cause(err, "reload fw4 rules"), output) } return nil } From 933bd2b2d5fa394234b01af43dc6b7eac0c1d2f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 17 Feb 2025 21:56:18 +0800 Subject: [PATCH 034/121] Add ping proxy support --- go.mod | 4 +- go.sum | 8 +- internal/gtcpip/header/interfaces.go | 4 + route_mapping.go | 45 ++++++ route_nat.go | 109 ++++++++++++++ route_nat_gvisor.go | 49 +++++++ route_nat_non_gvisor.go | 12 ++ stack_gvisor.go | 3 + stack_gvisor_icmp.go | 207 +++++++++++++++++++++++++++ stack_gvisor_tcp.go | 2 +- stack_gvisor_udp.go | 2 +- stack_mixed.go | 4 +- stack_system.go | 102 +++++++++++-- stack_system_nat.go | 2 +- stack_system_packet.go | 2 + tun.go | 6 +- 16 files changed, 542 insertions(+), 19 deletions(-) create mode 100644 route_mapping.go create mode 100644 route_nat.go create mode 100644 route_nat_gvisor.go create mode 100644 route_nat_non_gvisor.go create mode 100644 stack_gvisor_icmp.go diff --git a/go.mod b/go.mod index 9e3eb8d8..84d6651f 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/sagernet/sing-tun -go 1.20 +go 1.23.1 require ( github.com/go-ole/go-ole v1.3.0 github.com/google/btree v1.1.3 github.com/sagernet/fswatch v0.1.1 - github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff + github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/sagernet/nftables v0.3.0-beta.4 github.com/sagernet/sing v0.7.0-beta.1 diff --git a/go.sum b/go.sum index fb428267..d561248b 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= @@ -14,10 +15,11 @@ github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= -github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff h1:mlohw3360Wg1BNGook/UHnISXhUx4Gd/3tVLs5T0nSs= -github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff/go.mod h1:ehZwnT2UpmOWAHFL48XdBhnd4Qu4hN2O3Ji0us3ZHMw= +github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb h1:pprQtDqNgqXkRsXn+0E8ikKOemzmum8bODjSfDene38= +github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= @@ -25,6 +27,7 @@ github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/l github.com/sagernet/sing v0.7.0-beta.1 h1:2D44KzgeDZwD/R4Ts8jwSUHTRR238a1FpXDrl7l4tVw= github.com/sagernet/sing v0.7.0-beta.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= @@ -41,3 +44,4 @@ golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/gtcpip/header/interfaces.go b/internal/gtcpip/header/interfaces.go index b3045322..c2f8cdf4 100644 --- a/internal/gtcpip/header/interfaces.go +++ b/internal/gtcpip/header/interfaces.go @@ -86,6 +86,8 @@ type Network interface { // SourceAddress returns the value of the "source address" field. SourceAddress() tcpip.Address + SourceAddr() netip.Addr + // DestinationAddress returns the value of the "destination address" // field. DestinationAddress() tcpip.Address @@ -98,6 +100,8 @@ type Network interface { // SetSourceAddress sets the value of the "source address" field. SetSourceAddress(tcpip.Address) + SetSourceAddr(netip.Addr) + // SetDestinationAddress sets the value of the "destination address" // field. SetDestinationAddress(tcpip.Address) diff --git a/route_mapping.go b/route_mapping.go new file mode 100644 index 00000000..bd51212b --- /dev/null +++ b/route_mapping.go @@ -0,0 +1,45 @@ +package tun + +import ( + "net/netip" + "time" + + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/contrab/freelru" + "github.com/sagernet/sing/contrab/maphash" +) + +type DirectRouteSession struct { + // IPVersion uint8 + // Network uint8 + Source netip.Addr + Destination netip.Addr +} + +type RouteMapping struct { + status freelru.Cache[DirectRouteSession, DirectRouteDestination] +} + +func NewRouteMapping(timeout time.Duration) *RouteMapping { + status := common.Must1(freelru.NewSharded[DirectRouteSession, DirectRouteDestination](1024, maphash.NewHasher[DirectRouteSession]().Hash32)) + status.SetOnEvict(func(session DirectRouteSession, action DirectRouteDestination) { + action.Close() + }) + status.SetLifetime(timeout) + return &RouteMapping{status} +} + +func (m *RouteMapping) Lookup(session DirectRouteSession, constructor func() (DirectRouteDestination, error)) (DirectRouteDestination, error) { + var ( + created DirectRouteDestination + err error + ) + action, _, ok := m.status.GetAndRefreshOrAdd(session, func() (DirectRouteDestination, bool) { + created, err = constructor() + return created, err != nil + }) + if !ok { + return created, err + } + return action, nil +} diff --git a/route_nat.go b/route_nat.go new file mode 100644 index 00000000..59977035 --- /dev/null +++ b/route_nat.go @@ -0,0 +1,109 @@ +package tun + +import ( + "net/netip" + "sync" + + "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/internal/gtcpip/header" +) + +type NatMapping struct { + access sync.RWMutex + sessions map[DirectRouteSession]DirectRouteContext + ipRewrite bool +} + +func NewNatMapping(ipRewrite bool) *NatMapping { + return &NatMapping{ + sessions: make(map[DirectRouteSession]DirectRouteContext), + ipRewrite: ipRewrite, + } +} + +func (m *NatMapping) CreateSession(session DirectRouteSession, context DirectRouteContext) { + if m.ipRewrite { + session.Source = netip.Addr{} + } + m.access.Lock() + m.sessions[session] = context + m.access.Unlock() +} + +func (m *NatMapping) DeleteSession(session DirectRouteSession) { + if m.ipRewrite { + session.Source = netip.Addr{} + } + m.access.Lock() + delete(m.sessions, session) + m.access.Unlock() +} + +func (m *NatMapping) WritePacket(packet []byte) (bool, error) { + var routeSession DirectRouteSession + switch header.IPVersion(packet) { + case header.IPv4Version: + ipHdr := header.IPv4(packet) + routeSession.Source = ipHdr.DestinationAddr() + routeSession.Destination = ipHdr.SourceAddr() + case header.IPv6Version: + ipHdr := header.IPv6(packet) + routeSession.Source = ipHdr.DestinationAddr() + routeSession.Destination = ipHdr.SourceAddr() + default: + return false, nil + } + m.access.RLock() + context, loaded := m.sessions[routeSession] + m.access.RUnlock() + if !loaded { + return false, nil + } + return true, context.WritePacket(packet) +} + +type NatWriter struct { + inet4Address netip.Addr + inet6Address netip.Addr +} + +func NewNatWriter(inet4Address netip.Addr, inet6Address netip.Addr) *NatWriter { + return &NatWriter{ + inet4Address: inet4Address, + inet6Address: inet6Address, + } +} + +func (w *NatWriter) RewritePacket(packet []byte) { + var ipHdr header.Network + var bindAddr netip.Addr + switch header.IPVersion(packet) { + case header.IPv4Version: + ipHdr = header.IPv4(packet) + bindAddr = w.inet4Address + case header.IPv6Version: + ipHdr = header.IPv6(packet) + bindAddr = w.inet6Address + default: + return + } + ipHdr.SetSourceAddr(bindAddr) + switch ipHdr.TransportProtocol() { + case header.ICMPv4ProtocolNumber: + icmpHdr := header.ICMPv4(packet) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + case header.ICMPv6ProtocolNumber: + icmpHdr := header.ICMPv6(packet) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: ipHdr.SourceAddress(), + Dst: ipHdr.DestinationAddress(), + })) + } + if ipHdr4, isIPv4 := ipHdr.(header.IPv4); isIPv4 { + ipHdr4.SetChecksum(0) + ipHdr4.SetChecksum(^ipHdr4.CalculateChecksum()) + } +} diff --git a/route_nat_gvisor.go b/route_nat_gvisor.go new file mode 100644 index 00000000..be8febb1 --- /dev/null +++ b/route_nat_gvisor.go @@ -0,0 +1,49 @@ +//go:build with_gvisor + +package tun + +import ( + "github.com/sagernet/gvisor/pkg/tcpip" + "github.com/sagernet/gvisor/pkg/tcpip/header" + stack "github.com/sagernet/gvisor/pkg/tcpip/stack" + "github.com/sagernet/sing/common/buf" +) + +type DirectRouteDestination interface { + WritePacket(packet *buf.Buffer) error + WritePacketBuffer(packetBuffer *stack.PacketBuffer) error + Close() error +} + +func (w *NatWriter) RewritePacketBuffer(packetBuffer *stack.PacketBuffer) { + var bindAddr tcpip.Address + if packetBuffer.NetworkProtocolNumber == header.IPv4ProtocolNumber { + bindAddr = AddressFromAddr(w.inet4Address) + } else { + bindAddr = AddressFromAddr(w.inet6Address) + } + /*var ipHdr header.Network + switch packetBuffer.NetworkProtocolNumber { + case header.IPv4ProtocolNumber: + ipHdr = header.IPv4(packetBuffer.NetworkHeader().Slice()) + case header.IPv6ProtocolNumber: + ipHdr = header.IPv6(packetBuffer.NetworkHeader().Slice()) + default: + return + }*/ + ipHdr := packetBuffer.Network() + oldAddr := ipHdr.SourceAddress() + if checksumHdr, needChecksum := ipHdr.(header.ChecksummableNetwork); needChecksum { + checksumHdr.SetSourceAddressWithChecksumUpdate(bindAddr) + } else { + ipHdr.SetSourceAddress(bindAddr) + } + switch packetBuffer.TransportProtocolNumber { + case header.TCPProtocolNumber: + tcpHdr := header.TCP(packetBuffer.TransportHeader().Slice()) + tcpHdr.UpdateChecksumPseudoHeaderAddress(oldAddr, bindAddr, true) + case header.UDPProtocolNumber: + udpHdr := header.UDP(packetBuffer.TransportHeader().Slice()) + udpHdr.UpdateChecksumPseudoHeaderAddress(oldAddr, bindAddr, true) + } +} diff --git a/route_nat_non_gvisor.go b/route_nat_non_gvisor.go new file mode 100644 index 00000000..049b0748 --- /dev/null +++ b/route_nat_non_gvisor.go @@ -0,0 +1,12 @@ +//go:build !with_gvisor + +package tun + +import ( + "github.com/sagernet/sing/common/buf" +) + +type DirectRouteDestination interface { + DirectRouteAction + WritePacket(packet *buf.Buffer) error +} diff --git a/stack_gvisor.go b/stack_gvisor.go index 0dc995b4..f67b53fa 100644 --- a/stack_gvisor.go +++ b/stack_gvisor.go @@ -77,6 +77,9 @@ 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.udpTimeout) + ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) + ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) t.stack = ipStack t.endpoint = linkEndpoint return nil diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go new file mode 100644 index 00000000..d93c5e33 --- /dev/null +++ b/stack_gvisor_icmp.go @@ -0,0 +1,207 @@ +//go:build with_gvisor + +package tun + +import ( + "context" + "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" + "github.com/sagernet/gvisor/pkg/tcpip/network/ipv4" + "github.com/sagernet/gvisor/pkg/tcpip/network/ipv6" + "github.com/sagernet/gvisor/pkg/tcpip/stack" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type ICMPForwarder struct { + ctx context.Context + stack *stack.Stack + handler Handler + directNat *RouteMapping +} + +func NewICMPForwarder(ctx context.Context, stack *stack.Stack, handler Handler, timeout time.Duration) *ICMPForwarder { + return &ICMPForwarder{ + ctx: ctx, + stack: stack, + handler: handler, + directNat: NewRouteMapping(timeout), + } +} + +func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { + if pkt.NetworkProtocolNumber == header.IPv4ProtocolNumber { + ipHdr := header.IPv4(pkt.NetworkHeader().Slice()) + icmpHdr := header.ICMPv4(pkt.TransportHeader().Slice()) + if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != 0 { + return false + } + sourceAddr := M.AddrFromIP(ipHdr.SourceAddressSlice()) + destinationAddr := M.AddrFromIP(ipHdr.DestinationAddressSlice()) + action, err := f.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { + return f.handler.PrepareConnection( + N.NetworkICMPv4, + M.SocksaddrFrom(sourceAddr, 0), + M.SocksaddrFrom(destinationAddr, 0), + &ICMPBackWriter{ + stack: f.stack, + packet: pkt, + source: ipHdr.SourceAddress(), + sourceNetwork: header.IPv4ProtocolNumber, + }, + ) + }) + if err != nil { + return true + } + if action != nil { + // TODO: handle error + pkt.IncRef() + _ = action.WritePacketBuffer(pkt) + return true + } + icmpHdr.SetType(header.ICMPv4EchoReply) + sourceAddress := ipHdr.SourceAddress() + ipHdr.SetSourceAddress(ipHdr.DestinationAddress()) + ipHdr.SetDestinationAddress(sourceAddress) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], pkt.Data().Checksum())) + ipHdr.SetChecksum(0) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + outgoingEP, gErr := f.stack.GetNetworkEndpoint(DefaultNIC, header.IPv4ProtocolNumber) + if gErr != nil { + // TODO: log error + return true + } + route, gErr := f.stack.FindRoute( + DefaultNIC, + id.LocalAddress, + id.RemoteAddress, + header.IPv6ProtocolNumber, + false, + ) + if gErr != nil { + // TODO: log error + return true + } + defer route.Release() + outgoingEP.(ipv4.ExportedEndpoint).WritePacketDirect(route, pkt) + return true + } else { + ipHdr := header.IPv6(pkt.NetworkHeader().Slice()) + icmpHdr := header.ICMPv6(pkt.TransportHeader().Slice()) + if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != 0 { + return false + } + sourceAddr := M.AddrFromIP(ipHdr.SourceAddressSlice()) + destinationAddr := M.AddrFromIP(ipHdr.DestinationAddressSlice()) + action, err := f.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { + return f.handler.PrepareConnection( + N.NetworkICMPv6, + M.SocksaddrFrom(sourceAddr, 0), + M.SocksaddrFrom(destinationAddr, 0), + &ICMPBackWriter{ + stack: f.stack, + packet: pkt, + source: ipHdr.SourceAddress(), + sourceNetwork: header.IPv6ProtocolNumber, + }, + ) + }) + if err != nil { + return true + } + if action != nil { + // TODO: handle error + pkt.IncRef() + _ = action.WritePacketBuffer(pkt) + return true + } + icmpHdr.SetType(header.ICMPv6EchoReply) + sourceAddress := ipHdr.SourceAddress() + ipHdr.SetSourceAddress(ipHdr.DestinationAddress()) + ipHdr.SetDestinationAddress(sourceAddress) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: ipHdr.SourceAddress(), + Dst: ipHdr.DestinationAddress(), + })) + outgoingEP, gErr := f.stack.GetNetworkEndpoint(DefaultNIC, header.IPv4ProtocolNumber) + if gErr != nil { + // TODO: log error + return true + } + route, gErr := f.stack.FindRoute( + DefaultNIC, + id.LocalAddress, + id.RemoteAddress, + header.IPv6ProtocolNumber, + false, + ) + if gErr != nil { + // TODO: log error + return true + } + defer route.Release() + outgoingEP.(ipv6.ExportedEndpoint).WritePacketDirect(route, pkt) + return true + } +} + +type ICMPBackWriter struct { + access sync.Mutex + stack *stack.Stack + packet *stack.PacketBuffer + source tcpip.Address + sourceNetwork tcpip.NetworkProtocolNumber +} + +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) + } + defer route.Release() + packet := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(p), + }) + defer packet.DecRef() + err = route.WriteHeaderIncludedPacket(packet) + if err != nil { + return gonet.TranslateNetstackError(err) + } + } else { + route, err := w.stack.FindRoute( + DefaultNIC, + header.IPv6(p).SourceAddress(), + w.source, + w.sourceNetwork, + false, + ) + if err != nil { + return gonet.TranslateNetstackError(err) + } + defer route.Release() + packet := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(p), + }) + defer packet.DecRef() + err = route.WriteHeaderIncludedPacket(packet) + if err != nil { + return gonet.TranslateNetstackError(err) + } + } + return nil +} diff --git a/stack_gvisor_tcp.go b/stack_gvisor_tcp.go index aad97cf6..84bc3ff1 100644 --- a/stack_gvisor_tcp.go +++ b/stack_gvisor_tcp.go @@ -79,7 +79,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) - pErr := f.handler.PrepareConnection(N.NetworkTCP, source, destination) + _, pErr := f.handler.PrepareConnection(N.NetworkTCP, source, destination, nil) if pErr != nil { r.Complete(!errors.Is(pErr, ErrDrop)) return diff --git a/stack_gvisor_udp.go b/stack_gvisor_udp.go index 473eec46..db06b644 100644 --- a/stack_gvisor_udp.go +++ b/stack_gvisor_udp.go @@ -58,7 +58,7 @@ 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) + _, pErr := f.handler.PrepareConnection(N.NetworkUDP, source, destination, nil) if pErr != nil { if !errors.Is(pErr, ErrDrop) { gWriteUnreachable(f.stack, userData.(*stack.PacketBuffer)) diff --git a/stack_mixed.go b/stack_mixed.go index a48639d4..083ae973 100644 --- a/stack_mixed.go +++ b/stack_mixed.go @@ -237,7 +237,7 @@ func (m *Mixed) processIPv4(ipHdr header.IPv4) (writeBack bool, err error) { pkt.DecRef() return case header.ICMPv4ProtocolNumber: - err = m.processIPv4ICMP(ipHdr, ipHdr.Payload()) + writeBack, err = m.processIPv4ICMP(ipHdr, ipHdr.Payload()) } return } @@ -259,7 +259,7 @@ func (m *Mixed) processIPv6(ipHdr header.IPv6) (writeBack bool, err error) { m.endpoint.InjectInbound(tcpip.NetworkProtocolNumber(header.IPv6ProtocolNumber), pkt) pkt.DecRef() case header.ICMPv6ProtocolNumber: - err = m.processIPv6ICMP(ipHdr, ipHdr.Payload()) + writeBack, err = m.processIPv6ICMP(ipHdr, ipHdr.Payload()) } return } diff --git a/stack_system.go b/stack_system.go index 825c5f26..9075a784 100644 --- a/stack_system.go +++ b/stack_system.go @@ -45,6 +45,7 @@ type System struct { tcpPort6 uint16 tcpNat *TCPNat udpNat *udpnat.Service + directNat *RouteMapping bindInterface bool interfaceFinder control.InterfaceFinder frontHeadroom int @@ -159,6 +160,7 @@ 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 = NewRouteMapping(s.udpTimeout) return nil } @@ -361,7 +363,10 @@ func (s *System) processIPv4(ipHdr header.IPv4) (writeBack bool, err error) { writeBack = false err = s.processIPv4UDP(ipHdr, ipHdr.Payload()) case header.ICMPv4ProtocolNumber: - err = s.processIPv4ICMP(ipHdr, ipHdr.Payload()) + writeBack, err = s.processIPv4ICMP(ipHdr, ipHdr.Payload()) + } + if err != nil { + writeBack = false } return } @@ -377,7 +382,10 @@ func (s *System) processIPv6(ipHdr header.IPv6) (writeBack bool, err error) { case header.UDPProtocolNumber: err = s.processIPv6UDP(ipHdr, ipHdr.Payload()) case header.ICMPv6ProtocolNumber: - err = s.processIPv6ICMP(ipHdr, ipHdr.Payload()) + writeBack, err = s.processIPv6ICMP(ipHdr, ipHdr.Payload()) + } + if err != nil { + writeBack = false } return } @@ -601,7 +609,7 @@ 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) + _, pErr := s.handler.PrepareConnection(N.NetworkUDP, source, destination, nil) if pErr != nil { if !errors.Is(pErr, ErrDrop) { if source.IsIPv4() { @@ -643,9 +651,25 @@ func (s *System) preparePacketConnection(source M.Socksaddr, destination M.Socks return true, s.ctx, writer, nil } -func (s *System) processIPv4ICMP(ipHdr header.IPv4, icmpHdr header.ICMPv4) error { +func (s *System) processIPv4ICMP(ipHdr header.IPv4, icmpHdr header.ICMPv4) (bool, error) { if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != 0 { - return nil + return false, nil + } + sourceAddr := ipHdr.SourceAddr() + destinationAddr := ipHdr.DestinationAddr() + action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { + return s.handler.PrepareConnection( + N.NetworkICMPv4, + M.SocksaddrFrom(sourceAddr, 0), + M.SocksaddrFrom(destinationAddr, 0), + &systemICMPDirectPacketWriter4{s.tun, s.frontHeadroom + PacketOffset, sourceAddr}, + ) + }) + if err != nil { + return false, nil + } + if action != nil { + return false, action.WritePacket(buf.As(ipHdr).ToOwned()) } icmpHdr.SetType(header.ICMPv4EchoReply) sourceAddress := ipHdr.SourceAddr() @@ -654,7 +678,7 @@ func (s *System) processIPv4ICMP(ipHdr header.IPv4, icmpHdr header.ICMPv4) error icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) ipHdr.SetChecksum(0) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) - return nil + return true, nil } func (s *System) rejectIPv4WithICMP(ipHdr header.IPv4, code header.ICMPv4Code) error { @@ -696,9 +720,25 @@ func (s *System) rejectIPv4WithICMP(ipHdr header.IPv4, code header.ICMPv4Code) e return common.Error(s.tun.Write(newPacket.Bytes())) } -func (s *System) processIPv6ICMP(ipHdr header.IPv6, icmpHdr header.ICMPv6) error { +func (s *System) processIPv6ICMP(ipHdr header.IPv6, icmpHdr header.ICMPv6) (bool, error) { if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != 0 { - return nil + return false, nil + } + sourceAddr := ipHdr.SourceAddr() + destinationAddr := ipHdr.DestinationAddr() + action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { + return s.handler.PrepareConnection( + N.NetworkICMPv6, + M.SocksaddrFrom(sourceAddr, 0), + M.SocksaddrFrom(destinationAddr, 0), + &systemICMPDirectPacketWriter6{s.tun, s.frontHeadroom + PacketOffset, sourceAddr}, + ) + }) + if err != nil { + return false, nil + } + if action != nil { + return false, action.WritePacket(buf.As(ipHdr).ToOwned()) } icmpHdr.SetType(header.ICMPv6EchoReply) sourceAddress := ipHdr.SourceAddr() @@ -709,7 +749,7 @@ func (s *System) processIPv6ICMP(ipHdr header.IPv6, icmpHdr header.ICMPv6) error Src: ipHdr.SourceAddress(), Dst: ipHdr.DestinationAddress(), })) - return nil + return true, nil } func (s *System) rejectIPv6WithICMP(ipHdr header.IPv6, code header.ICMPv6Code) error { @@ -834,3 +874,47 @@ func (w *systemUDPPacketWriter6) WritePacket(buffer *buf.Buffer, destination M.S } return common.Error(w.tun.Write(newPacket.Bytes())) } + +type systemICMPDirectPacketWriter4 struct { + tun Tun + 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(0) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + if PacketOffset > 0 { + PacketFillHeader(newPacket.ExtendHeader(PacketOffset), header.IPv4Version) + } else { + newPacket.Advance(-w.frontHeadroom) + } + return common.Error(w.tun.Write(newPacket.Bytes())) +} + +type systemICMPDirectPacketWriter6 struct { + tun Tun + frontHeadroom int + source netip.Addr +} + +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) + } + return common.Error(w.tun.Write(newPacket.Bytes())) +} diff --git a/stack_system_nat.go b/stack_system_nat.go index 1d0216ed..6b581bc0 100644 --- a/stack_system_nat.go +++ b/stack_system_nat.go @@ -78,7 +78,7 @@ func (n *TCPNat) Lookup(source netip.AddrPort, destination netip.AddrPort, handl if loaded { return port, nil } - pErr := handler.PrepareConnection(N.NetworkTCP, M.SocksaddrFromNetIP(source), M.SocksaddrFromNetIP(destination)) + _, pErr := handler.PrepareConnection(N.NetworkTCP, M.SocksaddrFromNetIP(source), M.SocksaddrFromNetIP(destination), nil) if pErr != nil { return 0, pErr } diff --git a/stack_system_packet.go b/stack_system_packet.go index b5060b00..34fe51e4 100644 --- a/stack_system_packet.go +++ b/stack_system_packet.go @@ -5,6 +5,7 @@ import ( "syscall" "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing/common" ) func PacketIPVersion(packet []byte) int { @@ -13,6 +14,7 @@ func PacketIPVersion(packet []byte) int { func PacketFillHeader(packet []byte, ipVersion int) { if PacketOffset > 0 { + common.ClearArray(packet[:3]) switch ipVersion { case header.IPv4Version: packet[3] = syscall.AF_INET diff --git a/tun.go b/tun.go index 92eab64a..09497f7f 100644 --- a/tun.go +++ b/tun.go @@ -18,11 +18,15 @@ import ( ) type Handler interface { - PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error + PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext DirectRouteContext) (DirectRouteDestination, error) N.TCPConnectionHandlerEx N.UDPConnectionHandlerEx } +type DirectRouteContext interface { + WritePacket(packet []byte) error +} + type Tun interface { io.ReadWriter Name() (string, error) From 036d61a0aace68e5336e2ba620dba66a75b9012b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 22 Aug 2025 14:21:21 +0800 Subject: [PATCH 035/121] Add ping proxy implementation --- go.mod | 2 +- go.sum | 4 +- icmp.go | 29 +++++++ icmp_privileged.go | 112 ++++++++++++++++++++++++++ icmp_privileged_gvisor.go | 22 ++++++ icmp_unprivileged.go | 154 ++++++++++++++++++++++++++++++++++++ icmp_unprivileged_gvisor.go | 22 ++++++ route_mapping.go | 4 +- stack_gvisor_icmp.go | 6 +- 9 files changed, 348 insertions(+), 7 deletions(-) create mode 100644 icmp.go create mode 100644 icmp_privileged.go create mode 100644 icmp_privileged_gvisor.go create mode 100644 icmp_unprivileged.go create mode 100644 icmp_unprivileged_gvisor.go diff --git a/go.mod b/go.mod index 84d6651f..3f7f0a21 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/go-ole/go-ole v1.3.0 github.com/google/btree v1.1.3 github.com/sagernet/fswatch v0.1.1 - github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb + github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/sagernet/nftables v0.3.0-beta.4 github.com/sagernet/sing v0.7.0-beta.1 diff --git a/go.sum b/go.sum index d561248b..c964e1c8 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= -github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb h1:pprQtDqNgqXkRsXn+0E8ikKOemzmum8bODjSfDene38= -github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4= +github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 h1:SUPFNB+vSP4RBPrSEgNII+HkfqC8hKMpYLodom4o4EU= +github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= diff --git a/icmp.go b/icmp.go new file mode 100644 index 00000000..a1db779e --- /dev/null +++ b/icmp.go @@ -0,0 +1,29 @@ +package tun + +import ( + "context" + "net" + "net/netip" + "os" + "runtime" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + + "golang.org/x/sys/unix" +) + +func NewICMPDestination(ctx context.Context, logger logger.Logger, dialer net.Dialer, network string, address netip.Addr, routeContext DirectRouteContext) (DirectRouteDestination, error) { + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + return NewUnprivilegedICMPDestination(ctx, logger, dialer, network, address, routeContext) + } else { + destination, err := NewPrivilegedICMPDestination(ctx, logger, dialer, network, address, routeContext) + if err != nil { + if E.IsMulti(err, os.ErrPermission, unix.EPERM) { + return NewUnprivilegedICMPDestination(ctx, logger, dialer, network, address, routeContext) + } + return nil, err + } + return destination, nil + } +} diff --git a/icmp_privileged.go b/icmp_privileged.go new file mode 100644 index 00000000..aef7d189 --- /dev/null +++ b/icmp_privileged.go @@ -0,0 +1,112 @@ +package tun + +import ( + "context" + "net" + "net/netip" + "os" + + "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing/common/atomic" + "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 PrivilegedICMPDestination struct { + ctx context.Context + cancel context.CancelCauseFunc + logger logger.Logger + routeContext DirectRouteContext + isIPv6 bool + localAddr atomic.TypedValue[netip.Addr] + rawConn net.Conn +} + +func NewPrivilegedICMPDestination(ctx context.Context, logger logger.Logger, dialer net.Dialer, network string, address netip.Addr, routeContext DirectRouteContext) (DirectRouteDestination, error) { + var dialNetwork string + switch network { + case N.NetworkICMPv4: + dialNetwork = "ip4:icmp" + case N.NetworkICMPv6: + dialNetwork = "ip6:icmp" + default: + return nil, E.New("unsupported network: ", network) + } + ctx, cancel := context.WithCancelCause(ctx) + rawConn, err := dialer.DialContext(ctx, dialNetwork, address.String()) + if err != nil { + cancel(err) + return nil, err + } + d := &PrivilegedICMPDestination{ + ctx: ctx, + cancel: cancel, + logger: logger, + routeContext: routeContext, + isIPv6: network == N.NetworkICMPv6, + rawConn: rawConn, + } + go d.loopRead() + return d, nil +} + +func (d *PrivilegedICMPDestination) loopRead() { + for { + buffer := buf.NewPacket() + _, err := buffer.ReadOnceFrom(d.rawConn) + if err != nil { + return + } + if !d.isIPv6 { + ipHdr := header.IPv4(buffer.Bytes()) + ipHdr.SetDestinationAddr(d.localAddr.Load()) + ipHdr.SetChecksum(0) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + icmpHdr := header.ICMPv4(ipHdr.Payload()) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + } else { + ipHdr := header.IPv6(buffer.Bytes()) + ipHdr.SetDestinationAddr(d.localAddr.Load()) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: ipHdr.SourceAddress(), + Dst: ipHdr.DestinationAddress(), + })) + } + err = d.routeContext.WritePacket(buffer.Bytes()) + if err != nil { + d.logger.Error(err) + } + } +} + +func (d *PrivilegedICMPDestination) WritePacket(packet *buf.Buffer) error { + if !d.isIPv6 { + ipHdr := header.IPv4(packet.Bytes()) + d.localAddr.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + _, err := d.rawConn.Write(icmpHdr) + if err != nil { + return err + } + } else { + ipHdr := header.IPv6(packet.Bytes()) + d.localAddr.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + _, err := d.rawConn.Write(icmpHdr) + if err != nil { + return err + } + } + return nil +} + +func (d *PrivilegedICMPDestination) Close() error { + d.cancel(os.ErrClosed) + return d.rawConn.Close() +} diff --git a/icmp_privileged_gvisor.go b/icmp_privileged_gvisor.go new file mode 100644 index 00000000..002207f3 --- /dev/null +++ b/icmp_privileged_gvisor.go @@ -0,0 +1,22 @@ +//go:build with_gvisor + +package tun + +import ( + "net/netip" + + "github.com/sagernet/gvisor/pkg/tcpip/stack" +) + +func (d *PrivilegedICMPDestination) WritePacketBuffer(packetBuffer *stack.PacketBuffer) error { + ipHdr := packetBuffer.Network() + if !d.isIPv6 { + d.localAddr.Store(netip.AddrFrom4(ipHdr.SourceAddress().As4())) + } else { + d.localAddr.Store(netip.AddrFrom16(ipHdr.SourceAddress().As16())) + } + packetSlice := packetBuffer.TransportHeader().Slice() + packetSlice = append(packetSlice, packetBuffer.Data().AsRange().ToSlice()...) + _, err := d.rawConn.Write(packetSlice) + return err +} diff --git a/icmp_unprivileged.go b/icmp_unprivileged.go new file mode 100644 index 00000000..d246771d --- /dev/null +++ b/icmp_unprivileged.go @@ -0,0 +1,154 @@ +package tun + +import ( + "context" + "net" + "net/netip" + "os" + "syscall" + "unsafe" + + "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing/common/atomic" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + 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" + "golang.org/x/sys/unix" +) + +type UnprivilegedICMPDestination struct { + ctx context.Context + cancel context.CancelCauseFunc + logger logger.Logger + routeContext DirectRouteContext + isIPv6 bool + localAddr atomic.TypedValue[netip.Addr] + rawConn net.Conn + ipHdr bool +} + +func NewUnprivilegedICMPDestination(ctx context.Context, logger logger.Logger, dialer net.Dialer, network string, address netip.Addr, routeContext DirectRouteContext) (DirectRouteDestination, error) { + var ( + isIPv6 bool + fd int + ipHdr bool + err error + ) + var dialNetwork string + switch network { + case N.NetworkICMPv4: + dialNetwork = "ip4:icmp" + case N.NetworkICMPv6: + dialNetwork = "ip6:icmp" + isIPv6 = true + default: + return nil, E.New("unsupported network: ", network) + } + if !isIPv6 { + fd, err = unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_ICMP) + } else { + fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_ICMPV6) + } + if err != nil { + return nil, err + } + name, nameLen := bufio.ToSockaddr(M.SocksaddrFrom(address, 0).AddrPort()) + err = unixConnect(fd, name, nameLen) + if err != nil { + return nil, err + } + rawConn, err := net.FileConn(os.NewFile(uintptr(fd), "datagram-oriented icmp")) + if err != nil { + syscall.Close(fd) + return nil, err + } + if dialer.Control != nil { + var syscallConn syscall.RawConn + syscallConn, err = rawConn.(syscall.Conn).SyscallConn() + if err != nil { + return nil, err + } + err = dialer.Control(dialNetwork, address.String(), syscallConn) + if err != nil { + return nil, err + } + } + d := &UnprivilegedICMPDestination{ + ctx: ctx, + logger: logger, + routeContext: routeContext, + isIPv6: network == N.NetworkICMPv6, + rawConn: rawConn, + ipHdr: ipHdr, + } + go d.loopRead() + return d, nil +} + +//go:linkname unixConnect golang.org/x/sys/unix.connect +func unixConnect(fd int, addr unsafe.Pointer, addrlen uint32) error + +func (d *UnprivilegedICMPDestination) loopRead() { + for { + buffer := buf.NewPacket() + _, err := buffer.ReadOnceFrom(d.rawConn) + if err != nil { + return + } + if d.ipHdr { + if !d.isIPv6 { + ipHdr := header.IPv4(buffer.Bytes()) + ipHdr.SetDestinationAddr(d.localAddr.Load()) + ipHdr.SetChecksum(0) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + icmpHdr := header.ICMPv4(ipHdr.Payload()) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + } else { + ipHdr := header.IPv6(buffer.Bytes()) + ipHdr.SetDestinationAddr(d.localAddr.Load()) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: ipHdr.SourceAddress(), + Dst: ipHdr.DestinationAddress(), + })) + } + err = d.routeContext.WritePacket(buffer.Bytes()) + if err != nil { + d.logger.Error(err) + } + } else { + panic("impl no hdr version for windows and linux") + } + } +} + +func (d *UnprivilegedICMPDestination) WritePacket(packet *buf.Buffer) error { + if !d.isIPv6 { + ipHdr := header.IPv4(packet.Bytes()) + d.localAddr.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + _, err := d.rawConn.Write(icmpHdr) + if err != nil { + return err + } + } else { + ipHdr := header.IPv6(packet.Bytes()) + d.localAddr.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + _, err := d.rawConn.Write(icmpHdr) + if err != nil { + return err + } + } + return nil +} + +func (d *UnprivilegedICMPDestination) Close() error { + d.cancel(os.ErrClosed) + return d.rawConn.Close() +} diff --git a/icmp_unprivileged_gvisor.go b/icmp_unprivileged_gvisor.go new file mode 100644 index 00000000..daba0016 --- /dev/null +++ b/icmp_unprivileged_gvisor.go @@ -0,0 +1,22 @@ +//go:build with_gvisor + +package tun + +import ( + "net/netip" + + "github.com/sagernet/gvisor/pkg/tcpip/stack" +) + +func (d *UnprivilegedICMPDestination) WritePacketBuffer(packetBuffer *stack.PacketBuffer) error { + ipHdr := packetBuffer.Network() + if !d.isIPv6 { + d.localAddr.Store(netip.AddrFrom4(ipHdr.SourceAddress().As4())) + } else { + d.localAddr.Store(netip.AddrFrom16(ipHdr.SourceAddress().As16())) + } + packetSlice := packetBuffer.TransportHeader().Slice() + packetSlice = append(packetSlice, packetBuffer.Data().AsRange().ToSlice()...) + _, err := d.rawConn.Write(packetSlice) + return err +} diff --git a/route_mapping.go b/route_mapping.go index bd51212b..50292b9c 100644 --- a/route_mapping.go +++ b/route_mapping.go @@ -36,10 +36,10 @@ func (m *RouteMapping) Lookup(session DirectRouteSession, constructor func() (Di ) action, _, ok := m.status.GetAndRefreshOrAdd(session, func() (DirectRouteDestination, bool) { created, err = constructor() - return created, err != nil + return created, err == nil }) if !ok { - return created, err + return nil, err } return action, nil } diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index d93c5e33..a45ba583 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -11,6 +11,7 @@ import ( "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/header/parse" "github.com/sagernet/gvisor/pkg/tcpip/network/ipv4" "github.com/sagernet/gvisor/pkg/tcpip/network/ipv6" "github.com/sagernet/gvisor/pkg/tcpip/stack" @@ -178,7 +179,8 @@ func (w *ICMPBackWriter) WritePacket(p []byte) error { Payload: buffer.MakeWithData(p), }) defer packet.DecRef() - err = route.WriteHeaderIncludedPacket(packet) + parse.IPv4(packet) + err = route.WritePacketDirect(packet) if err != nil { return gonet.TranslateNetstackError(err) } @@ -198,7 +200,7 @@ func (w *ICMPBackWriter) WritePacket(p []byte) error { Payload: buffer.MakeWithData(p), }) defer packet.DecRef() - err = route.WriteHeaderIncludedPacket(packet) + err = route.WritePacketDirect(packet) if err != nil { return gonet.TranslateNetstackError(err) } From 8dbb51cfb7e57ee672bd8ea5650aca44066ec039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 23 Aug 2025 10:38:54 +0800 Subject: [PATCH 036/121] Add ping client --- .github/workflows/test.yml | 42 +++--- Makefile | 4 +- go.mod | 10 +- go.sum | 14 +- icmp.go | 29 ---- icmp_privileged.go | 112 --------------- icmp_privileged_gvisor.go | 22 --- icmp_unprivileged.go | 154 -------------------- icmp_unprivileged_gvisor.go | 22 --- internal/gtcpip/header/icmpv6.go | 6 +- internal/gtcpip/header/interfaces.go | 4 + ping/cmsg_unix.go | 16 +++ ping/cmsg_windows.go | 46 ++++++ ping/destination.go | 75 ++++++++++ ping/ping.go | 207 +++++++++++++++++++++++++++ ping/ping_test.go | 193 +++++++++++++++++++++++++ ping/socket_unix.go | 86 +++++++++++ ping/socket_windows.go | 38 +++++ route_nat.go | 4 +- route_nat_non_gvisor.go | 2 +- stack_system.go | 8 +- 21 files changed, 710 insertions(+), 384 deletions(-) delete mode 100644 icmp.go delete mode 100644 icmp_privileged.go delete mode 100644 icmp_privileged_gvisor.go delete mode 100644 icmp_unprivileged.go delete mode 100644 icmp_unprivileged_gvisor.go create mode 100644 ping/cmsg_unix.go create mode 100644 ping/cmsg_windows.go create mode 100644 ping/destination.go create mode 100644 ping/ping.go create mode 100644 ping/ping_test.go create mode 100644 ping/socket_unix.go create mode 100644 ping/socket_windows.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3afe9684..be40dea8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,12 +26,14 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ^1.23 + go-version: ^1.25.0 - name: Build run: | make test - build_go120: - name: Linux (Go 1.20) + go test -c -o ping_test ./ping + sudo ./ping_test -test.v + build_go124: + name: Linux (Go 1.24) runs-on: ubuntu-latest steps: - name: Checkout @@ -41,13 +43,15 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.20 + go-version: ~1.24 continue-on-error: true - name: Build run: | make test - build_go121: - name: Linux (Go 1.21) + go test -c -o ping_test ./ping + sudo ./ping_test -test.v + build_go123: + name: Linux (Go 1.23) runs-on: ubuntu-latest steps: - name: Checkout @@ -57,27 +61,13 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.21 - continue-on-error: true - - name: Build - run: | - make test - build_go122: - name: Linux (Go 1.22) - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ~1.22 + go-version: ~1.23 continue-on-error: true - name: Build run: | make test + go test -c -o ping_test ./ping + sudo ./ping_test -test.v build_windows: name: Windows runs-on: windows-latest @@ -94,6 +84,7 @@ jobs: - name: Build run: | make test + go test -v ./ping build_darwin: name: macOS runs-on: macos-latest @@ -109,4 +100,7 @@ jobs: continue-on-error: true - name: Build run: | - make test \ No newline at end of file + make test + go test -v ./ping + go test -c -o ping_test ./ping + sudo ./ping_test -test.v \ No newline at end of file diff --git a/Makefile b/Makefile index f7a8532c..c7524749 100644 --- a/Makefile +++ b/Makefile @@ -29,5 +29,5 @@ lint_install: test: go build -v . - go test -bench=. ./internal/checksum_test - #go test -v . + #go test -bench=. ./internal/checksum_test + go test -v . diff --git a/go.mod b/go.mod index 3f7f0a21..7d497394 100644 --- a/go.mod +++ b/go.mod @@ -9,20 +9,24 @@ require ( github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/sagernet/nftables v0.3.0-beta.4 - github.com/sagernet/sing v0.7.0-beta.1 + github.com/sagernet/sing v0.7.6-0.20250823024003-88f1880f43af + github.com/stretchr/testify v1.9.0 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 - golang.org/x/net v0.26.0 - golang.org/x/sys v0.26.0 + golang.org/x/net v0.43.0 + golang.org/x/sys v0.35.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/josharian/native v1.1.0 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.4.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/vishvananda/netns v0.0.4 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/time v0.7.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c964e1c8..fceb588f 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= -github.com/sagernet/sing v0.7.0-beta.1 h1:2D44KzgeDZwD/R4Ts8jwSUHTRR238a1FpXDrl7l4tVw= -github.com/sagernet/sing v0.7.0-beta.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.7.6-0.20250823024003-88f1880f43af h1:/1H30c/+j7Q9BBPuJuX6eHyzKpbGWrr7S/4DcdtNIfw= +github.com/sagernet/sing v0.7.6-0.20250823024003-88f1880f43af/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= @@ -34,14 +34,16 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/icmp.go b/icmp.go deleted file mode 100644 index a1db779e..00000000 --- a/icmp.go +++ /dev/null @@ -1,29 +0,0 @@ -package tun - -import ( - "context" - "net" - "net/netip" - "os" - "runtime" - - E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/logger" - - "golang.org/x/sys/unix" -) - -func NewICMPDestination(ctx context.Context, logger logger.Logger, dialer net.Dialer, network string, address netip.Addr, routeContext DirectRouteContext) (DirectRouteDestination, error) { - if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { - return NewUnprivilegedICMPDestination(ctx, logger, dialer, network, address, routeContext) - } else { - destination, err := NewPrivilegedICMPDestination(ctx, logger, dialer, network, address, routeContext) - if err != nil { - if E.IsMulti(err, os.ErrPermission, unix.EPERM) { - return NewUnprivilegedICMPDestination(ctx, logger, dialer, network, address, routeContext) - } - return nil, err - } - return destination, nil - } -} diff --git a/icmp_privileged.go b/icmp_privileged.go deleted file mode 100644 index aef7d189..00000000 --- a/icmp_privileged.go +++ /dev/null @@ -1,112 +0,0 @@ -package tun - -import ( - "context" - "net" - "net/netip" - "os" - - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" - "github.com/sagernet/sing-tun/internal/gtcpip/header" - "github.com/sagernet/sing/common/atomic" - "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 PrivilegedICMPDestination struct { - ctx context.Context - cancel context.CancelCauseFunc - logger logger.Logger - routeContext DirectRouteContext - isIPv6 bool - localAddr atomic.TypedValue[netip.Addr] - rawConn net.Conn -} - -func NewPrivilegedICMPDestination(ctx context.Context, logger logger.Logger, dialer net.Dialer, network string, address netip.Addr, routeContext DirectRouteContext) (DirectRouteDestination, error) { - var dialNetwork string - switch network { - case N.NetworkICMPv4: - dialNetwork = "ip4:icmp" - case N.NetworkICMPv6: - dialNetwork = "ip6:icmp" - default: - return nil, E.New("unsupported network: ", network) - } - ctx, cancel := context.WithCancelCause(ctx) - rawConn, err := dialer.DialContext(ctx, dialNetwork, address.String()) - if err != nil { - cancel(err) - return nil, err - } - d := &PrivilegedICMPDestination{ - ctx: ctx, - cancel: cancel, - logger: logger, - routeContext: routeContext, - isIPv6: network == N.NetworkICMPv6, - rawConn: rawConn, - } - go d.loopRead() - return d, nil -} - -func (d *PrivilegedICMPDestination) loopRead() { - for { - buffer := buf.NewPacket() - _, err := buffer.ReadOnceFrom(d.rawConn) - if err != nil { - return - } - if !d.isIPv6 { - ipHdr := header.IPv4(buffer.Bytes()) - ipHdr.SetDestinationAddr(d.localAddr.Load()) - ipHdr.SetChecksum(0) - ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) - icmpHdr := header.ICMPv4(ipHdr.Payload()) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) - } else { - ipHdr := header.IPv6(buffer.Bytes()) - ipHdr.SetDestinationAddr(d.localAddr.Load()) - icmpHdr := header.ICMPv6(ipHdr.Payload()) - icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr, - Src: ipHdr.SourceAddress(), - Dst: ipHdr.DestinationAddress(), - })) - } - err = d.routeContext.WritePacket(buffer.Bytes()) - if err != nil { - d.logger.Error(err) - } - } -} - -func (d *PrivilegedICMPDestination) WritePacket(packet *buf.Buffer) error { - if !d.isIPv6 { - ipHdr := header.IPv4(packet.Bytes()) - d.localAddr.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) - icmpHdr := header.ICMPv6(ipHdr.Payload()) - _, err := d.rawConn.Write(icmpHdr) - if err != nil { - return err - } - } else { - ipHdr := header.IPv6(packet.Bytes()) - d.localAddr.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) - icmpHdr := header.ICMPv6(ipHdr.Payload()) - _, err := d.rawConn.Write(icmpHdr) - if err != nil { - return err - } - } - return nil -} - -func (d *PrivilegedICMPDestination) Close() error { - d.cancel(os.ErrClosed) - return d.rawConn.Close() -} diff --git a/icmp_privileged_gvisor.go b/icmp_privileged_gvisor.go deleted file mode 100644 index 002207f3..00000000 --- a/icmp_privileged_gvisor.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build with_gvisor - -package tun - -import ( - "net/netip" - - "github.com/sagernet/gvisor/pkg/tcpip/stack" -) - -func (d *PrivilegedICMPDestination) WritePacketBuffer(packetBuffer *stack.PacketBuffer) error { - ipHdr := packetBuffer.Network() - if !d.isIPv6 { - d.localAddr.Store(netip.AddrFrom4(ipHdr.SourceAddress().As4())) - } else { - d.localAddr.Store(netip.AddrFrom16(ipHdr.SourceAddress().As16())) - } - packetSlice := packetBuffer.TransportHeader().Slice() - packetSlice = append(packetSlice, packetBuffer.Data().AsRange().ToSlice()...) - _, err := d.rawConn.Write(packetSlice) - return err -} diff --git a/icmp_unprivileged.go b/icmp_unprivileged.go deleted file mode 100644 index d246771d..00000000 --- a/icmp_unprivileged.go +++ /dev/null @@ -1,154 +0,0 @@ -package tun - -import ( - "context" - "net" - "net/netip" - "os" - "syscall" - "unsafe" - - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" - "github.com/sagernet/sing-tun/internal/gtcpip/header" - "github.com/sagernet/sing/common/atomic" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - 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" - "golang.org/x/sys/unix" -) - -type UnprivilegedICMPDestination struct { - ctx context.Context - cancel context.CancelCauseFunc - logger logger.Logger - routeContext DirectRouteContext - isIPv6 bool - localAddr atomic.TypedValue[netip.Addr] - rawConn net.Conn - ipHdr bool -} - -func NewUnprivilegedICMPDestination(ctx context.Context, logger logger.Logger, dialer net.Dialer, network string, address netip.Addr, routeContext DirectRouteContext) (DirectRouteDestination, error) { - var ( - isIPv6 bool - fd int - ipHdr bool - err error - ) - var dialNetwork string - switch network { - case N.NetworkICMPv4: - dialNetwork = "ip4:icmp" - case N.NetworkICMPv6: - dialNetwork = "ip6:icmp" - isIPv6 = true - default: - return nil, E.New("unsupported network: ", network) - } - if !isIPv6 { - fd, err = unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_ICMP) - } else { - fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_ICMPV6) - } - if err != nil { - return nil, err - } - name, nameLen := bufio.ToSockaddr(M.SocksaddrFrom(address, 0).AddrPort()) - err = unixConnect(fd, name, nameLen) - if err != nil { - return nil, err - } - rawConn, err := net.FileConn(os.NewFile(uintptr(fd), "datagram-oriented icmp")) - if err != nil { - syscall.Close(fd) - return nil, err - } - if dialer.Control != nil { - var syscallConn syscall.RawConn - syscallConn, err = rawConn.(syscall.Conn).SyscallConn() - if err != nil { - return nil, err - } - err = dialer.Control(dialNetwork, address.String(), syscallConn) - if err != nil { - return nil, err - } - } - d := &UnprivilegedICMPDestination{ - ctx: ctx, - logger: logger, - routeContext: routeContext, - isIPv6: network == N.NetworkICMPv6, - rawConn: rawConn, - ipHdr: ipHdr, - } - go d.loopRead() - return d, nil -} - -//go:linkname unixConnect golang.org/x/sys/unix.connect -func unixConnect(fd int, addr unsafe.Pointer, addrlen uint32) error - -func (d *UnprivilegedICMPDestination) loopRead() { - for { - buffer := buf.NewPacket() - _, err := buffer.ReadOnceFrom(d.rawConn) - if err != nil { - return - } - if d.ipHdr { - if !d.isIPv6 { - ipHdr := header.IPv4(buffer.Bytes()) - ipHdr.SetDestinationAddr(d.localAddr.Load()) - ipHdr.SetChecksum(0) - ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) - icmpHdr := header.ICMPv4(ipHdr.Payload()) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) - } else { - ipHdr := header.IPv6(buffer.Bytes()) - ipHdr.SetDestinationAddr(d.localAddr.Load()) - icmpHdr := header.ICMPv6(ipHdr.Payload()) - icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr, - Src: ipHdr.SourceAddress(), - Dst: ipHdr.DestinationAddress(), - })) - } - err = d.routeContext.WritePacket(buffer.Bytes()) - if err != nil { - d.logger.Error(err) - } - } else { - panic("impl no hdr version for windows and linux") - } - } -} - -func (d *UnprivilegedICMPDestination) WritePacket(packet *buf.Buffer) error { - if !d.isIPv6 { - ipHdr := header.IPv4(packet.Bytes()) - d.localAddr.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) - icmpHdr := header.ICMPv6(ipHdr.Payload()) - _, err := d.rawConn.Write(icmpHdr) - if err != nil { - return err - } - } else { - ipHdr := header.IPv6(packet.Bytes()) - d.localAddr.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) - icmpHdr := header.ICMPv6(ipHdr.Payload()) - _, err := d.rawConn.Write(icmpHdr) - if err != nil { - return err - } - } - return nil -} - -func (d *UnprivilegedICMPDestination) Close() error { - d.cancel(os.ErrClosed) - return d.rawConn.Close() -} diff --git a/icmp_unprivileged_gvisor.go b/icmp_unprivileged_gvisor.go deleted file mode 100644 index daba0016..00000000 --- a/icmp_unprivileged_gvisor.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build with_gvisor - -package tun - -import ( - "net/netip" - - "github.com/sagernet/gvisor/pkg/tcpip/stack" -) - -func (d *UnprivilegedICMPDestination) WritePacketBuffer(packetBuffer *stack.PacketBuffer) error { - ipHdr := packetBuffer.Network() - if !d.isIPv6 { - d.localAddr.Store(netip.AddrFrom4(ipHdr.SourceAddress().As4())) - } else { - d.localAddr.Store(netip.AddrFrom16(ipHdr.SourceAddress().As16())) - } - packetSlice := packetBuffer.TransportHeader().Slice() - packetSlice = append(packetSlice, packetBuffer.Data().AsRange().ToSlice()...) - _, err := d.rawConn.Write(packetSlice) - return err -} diff --git a/internal/gtcpip/header/icmpv6.go b/internal/gtcpip/header/icmpv6.go index 970f7436..520b4036 100644 --- a/internal/gtcpip/header/icmpv6.go +++ b/internal/gtcpip/header/icmpv6.go @@ -276,8 +276,8 @@ func (b ICMPv6) Payload() []byte { // ICMPv6ChecksumParams contains parameters to calculate ICMPv6 checksum. type ICMPv6ChecksumParams struct { Header ICMPv6 - Src tcpip.Address - Dst tcpip.Address + Src []byte + Dst []byte PayloadCsum uint16 PayloadLen int } @@ -287,7 +287,7 @@ type ICMPv6ChecksumParams struct { func ICMPv6Checksum(params ICMPv6ChecksumParams) uint16 { h := params.Header - xsum := PseudoHeaderChecksum(ICMPv6ProtocolNumber, params.Src.AsSlice(), params.Dst.AsSlice(), uint16(len(h)+params.PayloadLen)) + xsum := PseudoHeaderChecksum(ICMPv6ProtocolNumber, params.Src, params.Dst, uint16(len(h)+params.PayloadLen)) xsum = checksum.Combine(xsum, params.PayloadCsum) // h[2:4] is the checksum itself, skip it to avoid checksumming the checksum. diff --git a/internal/gtcpip/header/interfaces.go b/internal/gtcpip/header/interfaces.go index c2f8cdf4..fc13100c 100644 --- a/internal/gtcpip/header/interfaces.go +++ b/internal/gtcpip/header/interfaces.go @@ -88,12 +88,16 @@ type Network interface { SourceAddr() netip.Addr + SourceAddressSlice() []byte + // DestinationAddress returns the value of the "destination address" // field. DestinationAddress() tcpip.Address DestinationAddr() netip.Addr + DestinationAddressSlice() []byte + // Checksum returns the value of the "checksum" field. Checksum() uint16 diff --git a/ping/cmsg_unix.go b/ping/cmsg_unix.go new file mode 100644 index 00000000..222cb851 --- /dev/null +++ b/ping/cmsg_unix.go @@ -0,0 +1,16 @@ +//go:build !windows + +package ping + +import ( + "golang.org/x/net/ipv6" +) + +func parseIPv6ControlMessage(cmsg []byte) (*ipv6.ControlMessage, error) { + var controlMessage ipv6.ControlMessage + err := controlMessage.Parse(cmsg) + if err != nil { + return nil, err + } + return &controlMessage, nil +} diff --git a/ping/cmsg_windows.go b/ping/cmsg_windows.go new file mode 100644 index 00000000..be5be9b9 --- /dev/null +++ b/ping/cmsg_windows.go @@ -0,0 +1,46 @@ +package ping + +import ( + "encoding/binary" + "fmt" + "unsafe" + + "golang.org/x/net/ipv6" + "golang.org/x/sys/windows" +) + +const ( + IPV6_HOPLIMIT = 21 + IPV6_TCLASS = 39 + IPV6_RECVTCLASS = 40 +) + +var ( + alignedSizeofCmsghdr = (sizeofCmsghdr + cmsgAlignTo - 1) & ^(cmsgAlignTo - 1) + sizeofCmsghdr = int(unsafe.Sizeof(windows.WSACMSGHDR{})) + cmsgAlignTo = int(unsafe.Sizeof(uintptr(0))) +) + +func cmsgAlign(n int) int { + return (n + cmsgAlignTo - 1) & ^(cmsgAlignTo - 1) +} + +func parseIPv6ControlMessage(cmsg []byte) (*ipv6.ControlMessage, error) { + var controlMessage ipv6.ControlMessage + for len(cmsg) >= sizeofCmsghdr { + cmsghdr := (*windows.WSACMSGHDR)(unsafe.Pointer(unsafe.SliceData(cmsg))) + msgLen := int(cmsghdr.Len) + msgSize := cmsgAlign(msgLen) + if msgLen < sizeofCmsghdr || msgSize > len(cmsg) { + return nil, fmt.Errorf("invalid control message length %d", cmsghdr.Len) + } + switch cmsghdr.Type { + case IPV6_TCLASS: + controlMessage.TrafficClass = int(binary.NativeEndian.Uint32(cmsg[alignedSizeofCmsghdr : alignedSizeofCmsghdr+4])) + case IPV6_HOPLIMIT: + controlMessage.HopLimit = int(binary.NativeEndian.Uint32(cmsg[alignedSizeofCmsghdr : alignedSizeofCmsghdr+4])) + } + cmsg = cmsg[msgSize:] + } + return &controlMessage, nil +} diff --git a/ping/destination.go b/ping/destination.go new file mode 100644 index 00000000..553c7f84 --- /dev/null +++ b/ping/destination.go @@ -0,0 +1,75 @@ +package ping + +import ( + "errors" + "net/netip" + "os" + "runtime" + + "github.com/sagernet/sing-tun" + "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" +) + +var _ tun.DirectRouteDestination = (*Destination)(nil) + +type Destination struct { + logger logger.Logger + routeContext tun.DirectRouteContext + conn *Conn +} + +func ConnectDestination(logger logger.Logger, controlFunc control.Func, address netip.Addr, routeContext tun.DirectRouteContext) (tun.DirectRouteDestination, error) { + var ( + conn *Conn + err error + ) + switch runtime.GOOS { + case "darwin", "ios", "windows": + conn, err = Connect(false, controlFunc, address) + default: + conn, err = Connect(true, controlFunc, address) + if errors.Is(err, os.ErrPermission) { + conn, err = Connect(false, controlFunc, address) + } + } + if err != nil { + return nil, err + } + d := &Destination{ + logger: logger, + routeContext: routeContext, + conn: conn, + } + go d.loopRead() + return d, nil +} + +func (d *Destination) loopRead() { + for { + buffer := buf.NewPacket() + err := d.conn.ReadIP(buffer) + if err != nil { + buffer.Release() + if !E.IsClosed(err) { + d.logger.Error(E.Cause(err, "receive ICMP echo reply")) + } + return + } + err = d.routeContext.WritePacket(buffer.Bytes()) + if err != nil { + d.logger.Error(E.Cause(err, "write ICMP echo reply")) + } + buffer.Release() + } +} + +func (d *Destination) WritePacket(packet *buf.Buffer) error { + return d.conn.WriteIP(packet) +} + +func (d *Destination) Close() error { + return d.conn.Close() +} diff --git a/ping/ping.go b/ping/ping.go new file mode 100644 index 00000000..caad5f46 --- /dev/null +++ b/ping/ping.go @@ -0,0 +1,207 @@ +package ping + +import ( + "net" + "net/netip" + "reflect" + "runtime" + "time" + + "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/atomic" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +type Conn struct { + privileged bool + conn net.Conn + destination netip.Addr + source atomic.TypedValue[netip.Addr] +} + +func Connect(privileged bool, controlFunc control.Func, destination netip.Addr) (*Conn, error) { + conn, err := connect(privileged, controlFunc, destination) + if err != nil { + return nil, err + } + return &Conn{ + privileged: privileged, + conn: conn, + destination: destination, + }, nil +} + +func (c *Conn) ReadIP(buffer *buf.Buffer) error { + if c.destination.Is6() || runtime.GOOS == "linux" && !c.privileged { + var readMsg func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) + switch conn := c.conn.(type) { + case *net.IPConn: + readMsg = func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) { + var ipAddr *net.IPAddr + n, oobn, _, ipAddr, err = conn.ReadMsgIP(b, oob) + if ipAddr != nil { + addr = M.AddrFromNet(ipAddr) + } + return + } + case *net.UDPConn: + readMsg = func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) { + var udpAddr *net.UDPAddr + n, oobn, _, udpAddr, err = conn.ReadMsgUDP(b, oob) + if udpAddr != nil { + addr = M.AddrFromNet(udpAddr) + } + return + } + default: + return E.New("unsupported conn type: ", reflect.TypeOf(c.conn)) + } + if !c.destination.Is6() { + oob := ipv4.NewControlMessage(ipv4.FlagTTL) + buffer.Advance(header.IPv4MinimumSize) + var ttl int + // tos int + n, oobn, addr, err := readMsg(buffer.FreeBytes(), oob) + if err != nil { + return err + } + if err != nil { + return err + } + buffer.Truncate(n) + if oobn > 0 { + var controlMessage ipv4.ControlMessage + err = controlMessage.Parse(oob[:oobn]) + if err != nil { + return err + } + ttl = controlMessage.TTL + } + ipHdr := header.IPv4(buffer.ExtendHeader(header.IPv4MinimumSize)) + ipHdr.Encode(&header.IPv4Fields{ + // TOS: uint8(tos), + SrcAddr: addr, + DstAddr: c.source.Load(), + Protocol: uint8(header.ICMPv4ProtocolNumber), + TTL: uint8(ttl), + TotalLength: uint16(buffer.Len()), + }) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + } else { + oob := make([]byte, 1024) + buffer.Advance(header.IPv6MinimumSize) + var ( + hopLimit int + trafficClass int + ) + n, oobn, addr, err := readMsg(buffer.FreeBytes(), oob) + if err != nil { + return err + } + buffer.Truncate(n) + if oobn > 0 { + var controlMessage *ipv6.ControlMessage + controlMessage, err = parseIPv6ControlMessage(oob[:oobn]) + if err != nil { + return err + } + hopLimit = controlMessage.HopLimit + trafficClass = controlMessage.TrafficClass + } + icmpHdr := header.ICMPv6(buffer.Bytes()) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr[:header.ICMPv6DstUnreachableMinimumSize], + Src: addr.AsSlice(), + Dst: c.source.Load().AsSlice(), + })) + ipHdr := header.IPv6(buffer.ExtendHeader(header.IPv6MinimumSize)) + ipHdr.Encode(&header.IPv6Fields{ + TrafficClass: uint8(trafficClass), + PayloadLength: uint16(buffer.Len() - header.IPv6MinimumSize), + TransportProtocol: header.ICMPv6ProtocolNumber, + HopLimit: uint8(hopLimit), + SrcAddr: addr, + DstAddr: c.source.Load(), + }) + } + } else { + _, err := buffer.ReadOnceFrom(c.conn) + if err != nil { + return err + } + if !c.destination.Is6() { + ipHdr := header.IPv4(buffer.Bytes()) + ipHdr.SetDestinationAddr(c.source.Load()) + ipHdr.SetChecksum(0) + ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + icmpHdr := header.ICMPv4(ipHdr.Payload()) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + } else { + ipHdr := header.IPv6(buffer.Bytes()) + ipHdr.SetDestinationAddr(c.source.Load()) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: ipHdr.SourceAddressSlice(), + Dst: ipHdr.DestinationAddressSlice(), + })) + } + } + return nil +} + +func (c *Conn) ReadICMP(buffer *buf.Buffer) error { + _, err := buffer.ReadOnceFrom(c.conn) + if err != nil { + return err + } + if c.destination.Is6() || runtime.GOOS == "linux" && !c.privileged { + return nil + } + if !c.destination.Is6() { + ipHdr := header.IPv4(buffer.Bytes()) + buffer.Advance(int(ipHdr.HeaderLength())) + } else { + ipHdr := header.IPv6(buffer.Bytes()) + buffer.Advance(buffer.Len() - int(ipHdr.PayloadLength())) + } + return nil +} + +func (c *Conn) WriteIP(buffer *buf.Buffer) error { + defer buffer.Release() + if !c.destination.Is6() { + ipHdr := header.IPv4(buffer.Bytes()) + c.source.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) + return common.Error(c.conn.Write(ipHdr.Payload())) + } else { + ipHdr := header.IPv6(buffer.Bytes()) + c.source.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) + return common.Error(c.conn.Write(ipHdr.Payload())) + } +} + +func (c *Conn) WriteICMP(buffer *buf.Buffer) error { + defer buffer.Release() + return common.Error(c.conn.Write(buffer.Bytes())) +} + +func (c *Conn) SetLocalAddr(addr netip.Addr) { + c.source.Store(addr) +} + +func (c *Conn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +func (c *Conn) Close() error { + return c.conn.Close() +} diff --git a/ping/ping_test.go b/ping/ping_test.go new file mode 100644 index 00000000..65a6da90 --- /dev/null +++ b/ping/ping_test.go @@ -0,0 +1,193 @@ +package ping_test + +import ( + "net/netip" + "os" + "runtime" + "testing" + "time" + + "github.com/sagernet/gvisor/pkg/rand" + "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing-tun/ping" + "github.com/sagernet/sing/common/buf" + + "github.com/stretchr/testify/require" +) + +func TestPing(t *testing.T) { + t.Parallel() + const addr4 = "127.0.0.1" + t.Run("ipv4", func(t *testing.T) { + t.Run("unprivileged", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.SkipNow() + } + t.Run("read-icmp", func(t *testing.T) { + testPingIPv4ReadICMP(t, false, addr4) + }) + t.Run("read-ip", func(t *testing.T) { + testPingIPv4ReadIP(t, false, addr4) + }) + }) + t.Run("privileged", func(t *testing.T) { + if runtime.GOOS != "windows" && os.Getuid() != 0 { + t.SkipNow() + } + t.Run("read-icmp", func(t *testing.T) { + testPingIPv4ReadICMP(t, true, addr4) + }) + t.Run("read-ip", func(t *testing.T) { + testPingIPv4ReadIP(t, true, addr4) + }) + }) + }) + // const addr6 = "2606:4700:4700::1001" + const addr6 = "::1" + t.Run("ipv6", func(t *testing.T) { + t.Run("unprivileged", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.SkipNow() + } + t.Run("read-icmp", func(t *testing.T) { + testPingIPv6ReadICMP(t, false, addr6) + }) + t.Run("read-ip", func(t *testing.T) { + testPingIPv6ReadIP(t, false, addr6) + }) + }) + t.Run("privileged", func(t *testing.T) { + if runtime.GOOS != "windows" && os.Getuid() != 0 { + t.SkipNow() + } + t.Run("read-icmp", func(t *testing.T) { + testPingIPv6ReadICMP(t, true, addr6) + }) + t.Run("read-ip", func(t *testing.T) { + testPingIPv6ReadIP(t, true, addr6) + }) + }) + }) +} + +func testPingIPv4ReadIP(t *testing.T, privileged bool, addr string) { + conn, err := ping.Connect(privileged, nil, netip.MustParseAddr(addr)) + if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { + t.SkipNow() + } + require.NoError(t, err) + + request := make(header.ICMPv4, header.ICMPv4MinimumSize) + request.SetType(header.ICMPv4Echo) + request.SetIdent(uint16(rand.Uint32())) + request.SetChecksum(header.ICMPv4Checksum(request, 0)) + + err = conn.WriteICMP(buf.As(request)) + require.NoError(t, err) + + conn.SetLocalAddr(netip.MustParseAddr("127.0.0.1")) + require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second))) + + response := buf.NewPacket() + err = conn.ReadIP(response) + require.NoError(t, err) + if runtime.GOOS == "linux" && privileged { + response.Reset() + err = conn.ReadIP(response) + require.NoError(t, err) + } + ipHdr := header.IPv4(response.Bytes()) + require.NotZero(t, ipHdr.TTL()) + icmpHdr := header.ICMPv4(ipHdr.Payload()) + require.Equal(t, header.ICMPv4EchoReply, icmpHdr.Type()) +} + +func testPingIPv4ReadICMP(t *testing.T, privileged bool, addr string) { + conn, err := ping.Connect(privileged, nil, netip.MustParseAddr(addr)) + if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { + t.SkipNow() + } + require.NoError(t, err) + + request := make(header.ICMPv4, header.ICMPv4MinimumSize) + request.SetType(header.ICMPv4Echo) + request.SetIdent(uint16(rand.Uint32())) + request.SetChecksum(header.ICMPv4Checksum(request, 0)) + + err = conn.WriteICMP(buf.As(request)) + require.NoError(t, err) + + require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second))) + + response := buf.NewPacket() + err = conn.ReadICMP(response) + require.NoError(t, err) + + if runtime.GOOS == "linux" && privileged { + response.Reset() + err = conn.ReadICMP(response) + require.NoError(t, err) + } + + icmpHdr := header.ICMPv4(response.Bytes()) + require.Equal(t, header.ICMPv4EchoReply, icmpHdr.Type()) +} + +func testPingIPv6ReadIP(t *testing.T, privileged bool, addr string) { + conn, err := ping.Connect(privileged, nil, netip.MustParseAddr(addr)) + if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { + t.SkipNow() + } + require.NoError(t, err) + + request := make(header.ICMPv6, header.ICMPv6MinimumSize) + request.SetType(header.ICMPv6EchoRequest) + request.SetIdent(uint16(rand.Uint32())) + + err = conn.WriteICMP(buf.As(request)) + require.NoError(t, err) + + conn.SetLocalAddr(netip.MustParseAddr("::1")) + require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second))) + + response := buf.NewPacket() + 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) + } + ipHdr := header.IPv6(response.Bytes()) + require.NotZero(t, ipHdr.HopLimit()) + icmpHdr := header.ICMPv6(ipHdr.Payload()) + require.Equal(t, header.ICMPv6EchoReply, icmpHdr.Type()) +} + +func testPingIPv6ReadICMP(t *testing.T, privileged bool, addr string) { + conn, err := ping.Connect(privileged, nil, netip.MustParseAddr(addr)) + if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { + t.SkipNow() + } + require.NoError(t, err) + + request := make(header.ICMPv6, header.ICMPv6MinimumSize) + request.SetType(header.ICMPv6EchoRequest) + request.SetIdent(uint16(rand.Uint32())) + + err = conn.WriteICMP(buf.As(request)) + require.NoError(t, err) + + require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second))) + + response := buf.NewPacket() + err = conn.ReadICMP(response) + require.NoError(t, err) + if runtime.GOOS == "darwin" || runtime.GOOS == "linux" && privileged { + response.Reset() + err = conn.ReadICMP(response) + require.NoError(t, err) + } + icmpHdr := header.ICMPv6(response.Bytes()) + require.Equal(t, header.ICMPv6EchoReply, icmpHdr.Type()) +} diff --git a/ping/socket_unix.go b/ping/socket_unix.go new file mode 100644 index 00000000..11fa7a9e --- /dev/null +++ b/ping/socket_unix.go @@ -0,0 +1,86 @@ +//go:build unix + +package ping + +import ( + "net" + "net/netip" + "os" + "runtime" + "syscall" + + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + "golang.org/x/sys/unix" +) + +func connect(privileged bool, controlFunc control.Func, destination netip.Addr) (net.Conn, error) { + var ( + network string + fd int + err error + ) + if destination.Is4() { + network = "ip4:icmp" + if !privileged { + fd, err = unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_ICMP) + } else { + fd, err = unix.Socket(unix.AF_INET, unix.SOCK_RAW, unix.IPPROTO_ICMP) + } + } else { + network = "ip6:icmp" + if !privileged { + fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_ICMPV6) + } else { + fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_RAW, unix.IPPROTO_ICMPV6) + } + } + if err != nil { + return nil, E.Cause(err, "socket()") + } + file := os.NewFile(uintptr(fd), "datagram-oriented icmp") + defer file.Close() + err = unix.Connect(fd, M.AddrPortToSockaddr(netip.AddrPortFrom(destination, 0))) + if err != nil { + return nil, E.Cause(err, "connect()") + } + + if destination.Is4() && runtime.GOOS == "linux" { + //err = unix.SetsockoptInt(fd, unix.IPPROTO_IP, unix.IP_RECVTOS, 1) + //if err != nil { + // return nil, err + //} + err = unix.SetsockoptInt(fd, unix.IPPROTO_IP, unix.IP_RECVTTL, 1) + if err != nil { + return nil, E.Cause(err, "setsockopt()") + } + } + if destination.Is6() { + err = unix.SetsockoptInt(fd, unix.IPPROTO_IPV6, unix.IPV6_RECVHOPLIMIT, 1) + if err != nil { + return nil, E.Cause(err, "setsockopt()") + } + err = unix.SetsockoptInt(fd, unix.IPPROTO_IPV6, unix.IPV6_RECVTCLASS, 1) + if err != nil { + return nil, E.Cause(err, "setsockopt()") + } + } + + conn, err := net.FileConn(file) + if err != nil { + return nil, err + } + if controlFunc != nil { + var syscallConn syscall.RawConn + syscallConn, err = conn.(syscall.Conn).SyscallConn() + if err != nil { + return nil, err + } + err = controlFunc(network, destination.String(), syscallConn) + if err != nil { + return nil, err + } + } + return conn, nil +} diff --git a/ping/socket_windows.go b/ping/socket_windows.go new file mode 100644 index 00000000..daafd18a --- /dev/null +++ b/ping/socket_windows.go @@ -0,0 +1,38 @@ +package ping + +import ( + "net" + "net/netip" + "syscall" + + "github.com/sagernet/sing/common/control" + + "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 + if destination.Is6() { + dialer.Control = control.Append(dialer.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 + }) + }) + } + var network string + if destination.Is4() { + network = "ip4:icmp" + } else { + network = "ip6:ipv6-icmp" + } + return dialer.Dial(network, destination.String()) +} diff --git a/route_nat.go b/route_nat.go index 59977035..a4f33cc1 100644 --- a/route_nat.go +++ b/route_nat.go @@ -98,8 +98,8 @@ func (w *NatWriter) RewritePacket(packet []byte) { icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmpHdr, - Src: ipHdr.SourceAddress(), - Dst: ipHdr.DestinationAddress(), + Src: ipHdr.SourceAddressSlice(), + Dst: ipHdr.DestinationAddressSlice(), })) } if ipHdr4, isIPv4 := ipHdr.(header.IPv4); isIPv4 { diff --git a/route_nat_non_gvisor.go b/route_nat_non_gvisor.go index 049b0748..a0c6ceae 100644 --- a/route_nat_non_gvisor.go +++ b/route_nat_non_gvisor.go @@ -7,6 +7,6 @@ import ( ) type DirectRouteDestination interface { - DirectRouteAction WritePacket(packet *buf.Buffer) error + Close() error } diff --git a/stack_system.go b/stack_system.go index 9075a784..64f366dc 100644 --- a/stack_system.go +++ b/stack_system.go @@ -746,8 +746,8 @@ func (s *System) processIPv6ICMP(ipHdr header.IPv6, icmpHdr header.ICMPv6) (bool ipHdr.SetDestinationAddr(sourceAddress) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmpHdr, - Src: ipHdr.SourceAddress(), - Dst: ipHdr.DestinationAddress(), + Src: ipHdr.SourceAddressSlice(), + Dst: ipHdr.DestinationAddressSlice(), })) return true, nil } @@ -782,8 +782,8 @@ func (s *System) rejectIPv6WithICMP(ipHdr header.IPv6, code header.ICMPv6Code) e icmpHdr.SetCode(code) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmpHdr[:header.ICMPv6DstUnreachableMinimumSize], - Src: newIPHdr.SourceAddress(), - Dst: newIPHdr.DestinationAddress(), + Src: newIPHdr.SourceAddressSlice(), + Dst: newIPHdr.DestinationAddressSlice(), PayloadCsum: checksum.Checksum(payload, 0), PayloadLen: len(payload), })) From f46791bc0d8c176a106aac24b096a284e5af2625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 23 Aug 2025 16:21:48 +0800 Subject: [PATCH 037/121] Fix gvisor icmp destination --- ping/destination.go | 11 +++++++---- ping/socket_unix.go | 1 + route_nat.go | 6 ++++++ route_nat_gvisor.go | 9 +-------- route_nat_non_gvisor.go | 12 ------------ stack_gvisor_icmp.go | 12 +++++++++--- 6 files changed, 24 insertions(+), 27 deletions(-) delete mode 100644 route_nat_non_gvisor.go diff --git a/ping/destination.go b/ping/destination.go index 553c7f84..0640b2ec 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -1,6 +1,7 @@ package ping import ( + "context" "errors" "net/netip" "os" @@ -16,12 +17,13 @@ import ( var _ tun.DirectRouteDestination = (*Destination)(nil) type Destination struct { - logger logger.Logger + ctx context.Context + logger logger.ContextLogger routeContext tun.DirectRouteContext conn *Conn } -func ConnectDestination(logger logger.Logger, controlFunc control.Func, address netip.Addr, routeContext tun.DirectRouteContext) (tun.DirectRouteDestination, error) { +func ConnectDestination(ctx context.Context, logger logger.ContextLogger, controlFunc control.Func, address netip.Addr, routeContext tun.DirectRouteContext) (tun.DirectRouteDestination, error) { var ( conn *Conn err error @@ -39,6 +41,7 @@ func ConnectDestination(logger logger.Logger, controlFunc control.Func, address return nil, err } d := &Destination{ + ctx: ctx, logger: logger, routeContext: routeContext, conn: conn, @@ -54,13 +57,13 @@ func (d *Destination) loopRead() { if err != nil { buffer.Release() if !E.IsClosed(err) { - d.logger.Error(E.Cause(err, "receive ICMP echo reply")) + d.logger.ErrorContext(d.ctx, E.Cause(err, "receive ICMP echo reply")) } return } err = d.routeContext.WritePacket(buffer.Bytes()) if err != nil { - d.logger.Error(E.Cause(err, "write ICMP echo reply")) + d.logger.Error(d.ctx, E.Cause(err, "write ICMP echo reply")) } buffer.Release() } diff --git a/ping/socket_unix.go b/ping/socket_unix.go index 11fa7a9e..caf0f310 100644 --- a/ping/socket_unix.go +++ b/ping/socket_unix.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" + "golang.org/x/sys/unix" ) diff --git a/route_nat.go b/route_nat.go index a4f33cc1..ccb8ae30 100644 --- a/route_nat.go +++ b/route_nat.go @@ -6,8 +6,14 @@ import ( "github.com/sagernet/sing-tun/internal/gtcpip/checksum" "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing/common/buf" ) +type DirectRouteDestination interface { + WritePacket(packet *buf.Buffer) error + Close() error +} + type NatMapping struct { access sync.RWMutex sessions map[DirectRouteSession]DirectRouteContext diff --git a/route_nat_gvisor.go b/route_nat_gvisor.go index be8febb1..487e86b2 100644 --- a/route_nat_gvisor.go +++ b/route_nat_gvisor.go @@ -5,16 +5,9 @@ package tun import ( "github.com/sagernet/gvisor/pkg/tcpip" "github.com/sagernet/gvisor/pkg/tcpip/header" - stack "github.com/sagernet/gvisor/pkg/tcpip/stack" - "github.com/sagernet/sing/common/buf" + "github.com/sagernet/gvisor/pkg/tcpip/stack" ) -type DirectRouteDestination interface { - WritePacket(packet *buf.Buffer) error - WritePacketBuffer(packetBuffer *stack.PacketBuffer) error - Close() error -} - func (w *NatWriter) RewritePacketBuffer(packetBuffer *stack.PacketBuffer) { var bindAddr tcpip.Address if packetBuffer.NetworkProtocolNumber == header.IPv4ProtocolNumber { diff --git a/route_nat_non_gvisor.go b/route_nat_non_gvisor.go deleted file mode 100644 index a0c6ceae..00000000 --- a/route_nat_non_gvisor.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !with_gvisor - -package tun - -import ( - "github.com/sagernet/sing/common/buf" -) - -type DirectRouteDestination interface { - WritePacket(packet *buf.Buffer) error - Close() error -} diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index a45ba583..6888eace 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -15,6 +15,7 @@ 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" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) @@ -62,8 +63,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa } if action != nil { // TODO: handle error - pkt.IncRef() - _ = action.WritePacketBuffer(pkt) + _ = icmpWritePacketBuffer(action, pkt) return true } icmpHdr.SetType(header.ICMPv4EchoReply) @@ -120,7 +120,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa if action != nil { // TODO: handle error pkt.IncRef() - _ = action.WritePacketBuffer(pkt) + _ = icmpWritePacketBuffer(action, pkt) return true } icmpHdr.SetType(header.ICMPv6EchoReply) @@ -207,3 +207,9 @@ func (w *ICMPBackWriter) WritePacket(p []byte) error { } return nil } + +func icmpWritePacketBuffer(action DirectRouteDestination, packetBuffer *stack.PacketBuffer) error { + packetSlice := packetBuffer.TransportHeader().Slice() + packetSlice = append(packetSlice, packetBuffer.Data().AsRange().ToSlice()...) + return action.WritePacket(buf.As(packetSlice).ToOwned()) +} From a256dca36b82da2186ede589403a941fb831249a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 23 Aug 2025 16:37:46 +0800 Subject: [PATCH 038/121] Fix ping response for tun address --- stack_gvisor.go | 17 ++++++- stack_gvisor_icmp.go | 116 ++++++++++++++++++++++++------------------- stack_system.go | 90 +++++++++++++++++---------------- 3 files changed, 129 insertions(+), 94 deletions(-) diff --git a/stack_gvisor.go b/stack_gvisor.go index f67b53fa..a3b6fad2 100644 --- a/stack_gvisor.go +++ b/stack_gvisor.go @@ -28,6 +28,8 @@ const DefaultNIC tcpip.NICID = 1 type GVisor struct { ctx context.Context tun GVisorTun + inet4Address netip.Addr + inet6Address netip.Addr inet4LoopbackAddress []netip.Addr inet6LoopbackAddress []netip.Addr udpTimeout time.Duration @@ -52,9 +54,22 @@ func NewGVisor( return nil, E.New("gVisor stack is unsupported on current platform") } + var ( + inet4Address netip.Addr + inet6Address netip.Addr + ) + if len(options.TunOptions.Inet4Address) > 0 { + inet4Address = options.TunOptions.Inet4Address[0].Addr() + } + if len(options.TunOptions.Inet6Address) > 0 { + inet6Address = options.TunOptions.Inet6Address[0].Addr() + } + gStack := &GVisor{ ctx: options.Context, tun: gTun, + inet4Address: inet4Address, + inet6Address: inet6Address, inet4LoopbackAddress: options.TunOptions.Inet4LoopbackAddress, inet6LoopbackAddress: options.TunOptions.Inet6LoopbackAddress, udpTimeout: options.UDPTimeout, @@ -77,7 +92,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.udpTimeout) + icmpForwarder := NewICMPForwarder(t.ctx, ipStack, t.inet4Address, t.inet6Address, t.handler, t.udpTimeout) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) t.stack = ipStack diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index 6888eace..c9a79154 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -4,6 +4,7 @@ package tun import ( "context" + "net/netip" "sync" "time" @@ -21,18 +22,29 @@ import ( ) type ICMPForwarder struct { - ctx context.Context - stack *stack.Stack - handler Handler - directNat *RouteMapping + ctx context.Context + stack *stack.Stack + inet4Address netip.Addr + inet6Address netip.Addr + handler Handler + directNat *RouteMapping } -func NewICMPForwarder(ctx context.Context, stack *stack.Stack, handler Handler, timeout time.Duration) *ICMPForwarder { +func NewICMPForwarder( + ctx context.Context, + stack *stack.Stack, + inet4Address netip.Addr, + inet6Address netip.Addr, + handler Handler, + timeout time.Duration, +) *ICMPForwarder { return &ICMPForwarder{ - ctx: ctx, - stack: stack, - handler: handler, - directNat: NewRouteMapping(timeout), + ctx: ctx, + stack: stack, + inet4Address: inet4Address, + inet6Address: inet6Address, + handler: handler, + directNat: NewRouteMapping(timeout), } } @@ -45,26 +57,28 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa } sourceAddr := M.AddrFromIP(ipHdr.SourceAddressSlice()) destinationAddr := M.AddrFromIP(ipHdr.DestinationAddressSlice()) - action, err := f.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { - return f.handler.PrepareConnection( - N.NetworkICMPv4, - M.SocksaddrFrom(sourceAddr, 0), - M.SocksaddrFrom(destinationAddr, 0), - &ICMPBackWriter{ - stack: f.stack, - packet: pkt, - source: ipHdr.SourceAddress(), - sourceNetwork: header.IPv4ProtocolNumber, - }, - ) - }) - if err != nil { - return true - } - if action != nil { - // TODO: handle error - _ = icmpWritePacketBuffer(action, pkt) - return true + if destinationAddr != f.inet4Address { + action, err := f.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { + return f.handler.PrepareConnection( + N.NetworkICMPv4, + M.SocksaddrFrom(sourceAddr, 0), + M.SocksaddrFrom(destinationAddr, 0), + &ICMPBackWriter{ + stack: f.stack, + packet: pkt, + source: ipHdr.SourceAddress(), + sourceNetwork: header.IPv4ProtocolNumber, + }, + ) + }) + if err != nil { + return true + } + if action != nil { + // TODO: handle error + _ = icmpWritePacketBuffer(action, pkt) + return true + } } icmpHdr.SetType(header.ICMPv4EchoReply) sourceAddress := ipHdr.SourceAddress() @@ -101,27 +115,29 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa } sourceAddr := M.AddrFromIP(ipHdr.SourceAddressSlice()) destinationAddr := M.AddrFromIP(ipHdr.DestinationAddressSlice()) - action, err := f.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { - return f.handler.PrepareConnection( - N.NetworkICMPv6, - M.SocksaddrFrom(sourceAddr, 0), - M.SocksaddrFrom(destinationAddr, 0), - &ICMPBackWriter{ - stack: f.stack, - packet: pkt, - source: ipHdr.SourceAddress(), - sourceNetwork: header.IPv6ProtocolNumber, - }, - ) - }) - if err != nil { - return true - } - if action != nil { - // TODO: handle error - pkt.IncRef() - _ = icmpWritePacketBuffer(action, pkt) - return true + if destinationAddr != f.inet6Address { + action, err := f.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { + return f.handler.PrepareConnection( + N.NetworkICMPv6, + M.SocksaddrFrom(sourceAddr, 0), + M.SocksaddrFrom(destinationAddr, 0), + &ICMPBackWriter{ + stack: f.stack, + packet: pkt, + source: ipHdr.SourceAddress(), + sourceNetwork: header.IPv6ProtocolNumber, + }, + ) + }) + if err != nil { + return true + } + if action != nil { + // TODO: handle error + pkt.IncRef() + _ = icmpWritePacketBuffer(action, pkt) + return true + } } icmpHdr.SetType(header.ICMPv6EchoReply) sourceAddress := ipHdr.SourceAddress() diff --git a/stack_system.go b/stack_system.go index 64f366dc..e23366bf 100644 --- a/stack_system.go +++ b/stack_system.go @@ -31,10 +31,10 @@ type System struct { logger logger.Logger inet4Prefixes []netip.Prefix inet6Prefixes []netip.Prefix - inet4ServerAddress netip.Addr inet4Address netip.Addr - inet6ServerAddress netip.Addr + inet4NextAddress netip.Addr inet6Address netip.Addr + inet6NextAddress netip.Addr broadcastAddr netip.Addr inet4LoopbackAddress []netip.Addr inet6LoopbackAddress []netip.Addr @@ -82,17 +82,17 @@ func NewSystem(options StackOptions) (Stack, error) { if !HasNextAddress(options.TunOptions.Inet4Address[0], 1) { return nil, E.New("need one more IPv4 address in first prefix for system stack") } - stack.inet4ServerAddress = options.TunOptions.Inet4Address[0].Addr() - stack.inet4Address = stack.inet4ServerAddress.Next() + stack.inet4Address = options.TunOptions.Inet4Address[0].Addr() + stack.inet4NextAddress = stack.inet4Address.Next() } if len(options.TunOptions.Inet6Address) > 0 { if !HasNextAddress(options.TunOptions.Inet6Address[0], 1) { return nil, E.New("need one more IPv6 address in first prefix for system stack") } - stack.inet6ServerAddress = options.TunOptions.Inet6Address[0].Addr() - stack.inet6Address = stack.inet6ServerAddress.Next() + stack.inet6Address = options.TunOptions.Inet6Address[0].Addr() + stack.inet6NextAddress = stack.inet6Address.Next() } - if !stack.inet4Address.IsValid() && !stack.inet6Address.IsValid() { + if !stack.inet4NextAddress.IsValid() && !stack.inet6NextAddress.IsValid() { return nil, E.New("missing interface address") } return stack, nil @@ -128,9 +128,9 @@ func (s *System) start() error { } var tcpListener net.Listener var err error - if s.inet4Address.IsValid() { + if s.inet4NextAddress.IsValid() { for i := 0; i < 3; i++ { - tcpListener, err = listener.Listen(s.ctx, "tcp4", net.JoinHostPort(s.inet4ServerAddress.String(), "0")) + tcpListener, err = listener.Listen(s.ctx, "tcp4", net.JoinHostPort(s.inet4Address.String(), "0")) if !retryableListenError(err) { break } @@ -143,9 +143,9 @@ func (s *System) start() error { s.tcpPort = M.SocksaddrFromNet(tcpListener.Addr()).Port go s.acceptLoop(tcpListener) } - if s.inet6Address.IsValid() { + if s.inet6NextAddress.IsValid() { for i := 0; i < 3; i++ { - tcpListener, err = listener.Listen(s.ctx, "tcp6", net.JoinHostPort(s.inet6ServerAddress.String(), "0")) + tcpListener, err = listener.Listen(s.ctx, "tcp6", net.JoinHostPort(s.inet6Address.String(), "0")) if !retryableListenError(err) { break } @@ -395,7 +395,7 @@ func (s *System) processIPv4TCP(ipHdr header.IPv4, tcpHdr header.TCP) (bool, err destination := netip.AddrPortFrom(ipHdr.DestinationAddr(), tcpHdr.DestinationPort()) if !destination.Addr().IsGlobalUnicast() { return false, nil - } else if source.Addr() == s.inet4ServerAddress && source.Port() == s.tcpPort { + } else if source.Addr() == s.inet4Address && source.Port() == s.tcpPort { session := s.tcpNat.LookupBack(destination.Port()) if session == nil { return false, E.New("ipv4: tcp: session not found: ", destination.Port()) @@ -423,9 +423,9 @@ func (s *System) processIPv4TCP(ipHdr header.IPv4, tcpHdr header.TCP) (bool, err return false, s.resetIPv4TCP(ipHdr, tcpHdr) } } - ipHdr.SetSourceAddr(s.inet4Address) + ipHdr.SetSourceAddr(s.inet4NextAddress) tcpHdr.SetSourcePort(natPort) - ipHdr.SetDestinationAddr(s.inet4ServerAddress) + ipHdr.SetDestinationAddr(s.inet4Address) tcpHdr.SetDestinationPort(s.tcpPort) } } @@ -493,7 +493,7 @@ func (s *System) processIPv6TCP(ipHdr header.IPv6, tcpHdr header.TCP) (bool, err destination := netip.AddrPortFrom(ipHdr.DestinationAddr(), tcpHdr.DestinationPort()) if !destination.Addr().IsGlobalUnicast() { return false, nil - } else if source.Addr() == s.inet6ServerAddress && source.Port() == s.tcpPort6 { + } else if source.Addr() == s.inet6Address && source.Port() == s.tcpPort6 { session := s.tcpNat.LookupBack(destination.Port()) if session == nil { return false, E.New("ipv6: tcp: session not found: ", destination.Port()) @@ -521,9 +521,9 @@ func (s *System) processIPv6TCP(ipHdr header.IPv6, tcpHdr header.TCP) (bool, err return false, s.resetIPv6TCP(ipHdr, tcpHdr) } } - ipHdr.SetSourceAddr(s.inet6Address) + ipHdr.SetSourceAddr(s.inet6NextAddress) tcpHdr.SetSourcePort(natPort) - ipHdr.SetDestinationAddr(s.inet6ServerAddress) + ipHdr.SetDestinationAddr(s.inet6Address) tcpHdr.SetDestinationPort(s.tcpPort6) } } @@ -657,19 +657,21 @@ func (s *System) processIPv4ICMP(ipHdr header.IPv4, icmpHdr header.ICMPv4) (bool } sourceAddr := ipHdr.SourceAddr() destinationAddr := ipHdr.DestinationAddr() - action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { - return s.handler.PrepareConnection( - N.NetworkICMPv4, - M.SocksaddrFrom(sourceAddr, 0), - M.SocksaddrFrom(destinationAddr, 0), - &systemICMPDirectPacketWriter4{s.tun, s.frontHeadroom + PacketOffset, sourceAddr}, - ) - }) - if err != nil { - return false, nil - } - if action != nil { - return false, action.WritePacket(buf.As(ipHdr).ToOwned()) + if destinationAddr != s.inet4Address { + action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { + return s.handler.PrepareConnection( + N.NetworkICMPv4, + M.SocksaddrFrom(sourceAddr, 0), + M.SocksaddrFrom(destinationAddr, 0), + &systemICMPDirectPacketWriter4{s.tun, s.frontHeadroom + PacketOffset, sourceAddr}, + ) + }) + if err != nil { + return false, nil + } + if action != nil { + return false, action.WritePacket(buf.As(ipHdr).ToOwned()) + } } icmpHdr.SetType(header.ICMPv4EchoReply) sourceAddress := ipHdr.SourceAddr() @@ -726,19 +728,21 @@ func (s *System) processIPv6ICMP(ipHdr header.IPv6, icmpHdr header.ICMPv6) (bool } sourceAddr := ipHdr.SourceAddr() destinationAddr := ipHdr.DestinationAddr() - action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { - return s.handler.PrepareConnection( - N.NetworkICMPv6, - M.SocksaddrFrom(sourceAddr, 0), - M.SocksaddrFrom(destinationAddr, 0), - &systemICMPDirectPacketWriter6{s.tun, s.frontHeadroom + PacketOffset, sourceAddr}, - ) - }) - if err != nil { - return false, nil - } - if action != nil { - return false, action.WritePacket(buf.As(ipHdr).ToOwned()) + if destinationAddr != s.inet6Address { + action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { + return s.handler.PrepareConnection( + N.NetworkICMPv6, + M.SocksaddrFrom(sourceAddr, 0), + M.SocksaddrFrom(destinationAddr, 0), + &systemICMPDirectPacketWriter6{s.tun, s.frontHeadroom + PacketOffset, sourceAddr}, + ) + }) + if err != nil { + return false, nil + } + if action != nil { + return false, action.WritePacket(buf.As(ipHdr).ToOwned()) + } } icmpHdr.SetType(header.ICMPv6EchoReply) sourceAddress := ipHdr.SourceAddr() From 12c9fb6a5daeb5d81a36b7241138db33f02810ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 23 Aug 2025 16:51:29 +0800 Subject: [PATCH 039/121] Fix gvisor icmp write --- stack_gvisor_icmp.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index c9a79154..e02aa45e 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -225,7 +225,8 @@ func (w *ICMPBackWriter) WritePacket(p []byte) error { } func icmpWritePacketBuffer(action DirectRouteDestination, packetBuffer *stack.PacketBuffer) error { - packetSlice := packetBuffer.TransportHeader().Slice() + packetSlice := packetBuffer.NetworkHeader().Slice() + packetSlice = append(packetSlice, packetBuffer.TransportHeader().Slice()...) packetSlice = append(packetSlice, packetBuffer.Data().AsRange().ToSlice()...) return action.WritePacket(buf.As(packetSlice).ToOwned()) } From 86d96064d57bdf8a847104a7cf8ba311afff9be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 24 Aug 2025 10:36:16 +0800 Subject: [PATCH 040/121] ping: Add gVisor destination --- ping/destination_gvisor.go | 113 +++++++++++++++++++++++++++++ ping/rewriter.go | 142 +++++++++++++++++++++++++++++++++++++ route_direct.go | 51 +++++++++++++ route_mapping.go | 45 ------------ route_nat.go | 115 ------------------------------ route_nat_gvisor.go | 42 ----------- stack_gvisor.go | 18 +++-- stack_gvisor_icmp.go | 23 +++--- stack_system.go | 4 +- 9 files changed, 332 insertions(+), 221 deletions(-) create mode 100644 ping/destination_gvisor.go create mode 100644 ping/rewriter.go create mode 100644 route_direct.go delete mode 100644 route_mapping.go delete mode 100644 route_nat.go delete mode 100644 route_nat_gvisor.go diff --git a/ping/destination_gvisor.go b/ping/destination_gvisor.go new file mode 100644 index 00000000..abe98c84 --- /dev/null +++ b/ping/destination_gvisor.go @@ -0,0 +1,113 @@ +//go:build with_gvisor + +package ping + +import ( + "context" + "net/netip" + + "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/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 + conn *gonet.TCPConn + rewriter *Rewriter +} + +func ConnectGVisor( + ctx context.Context, logger logger.ContextLogger, + sourceAddress, destinationAddress netip.Addr, + routeContext tun.DirectRouteContext, + stack *stack.Stack, + bindAddress4, bindAddress6 netip.Addr, +) (*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 := NewRewriter(bindAddress4, bindAddress6) + rewriter.CreateSession(tun.DirectRouteSession{Source: sourceAddress, Destination: destinationAddress}, routeContext) + destination := &GVisorDestination{ + ctx: ctx, + logger: logger, + conn: gonet.NewTCPConn(&wq, endpoint), + rewriter: rewriter, + } + go destination.loopRead() + return destination, nil +} + +func (d *GVisorDestination) loopRead() { + for { + buffer := buf.NewPacket() + n, err := d.conn.Read(buffer.FreeBytes()) + if err != nil { + buffer.Release() + 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()) + if err != nil { + d.logger.ErrorContext(d.ctx, E.Cause(err, "write ICMP echo reply")) + } + buffer.Release() + } +} + +func (d *GVisorDestination) WritePacket(packet *buf.Buffer) error { + d.rewriter.RewritePacket(packet.Bytes()) + return common.Error(d.conn.Write(packet.Bytes())) +} + +func (d *GVisorDestination) Close() error { + return d.conn.Close() +} diff --git a/ping/rewriter.go b/ping/rewriter.go new file mode 100644 index 00000000..a842df1b --- /dev/null +++ b/ping/rewriter.go @@ -0,0 +1,142 @@ +package ping + +import ( + "net/netip" + "sync" + + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/internal/gtcpip/header" +) + +type Rewriter struct { + access sync.RWMutex + sessions map[tun.DirectRouteSession]tun.DirectRouteContext + source4Address map[uint16]netip.Addr + source6Address map[uint16]netip.Addr + inet4Address netip.Addr + inet6Address netip.Addr +} + +func NewRewriter(inet4Address netip.Addr, inet6Address netip.Addr) *Rewriter { + return &Rewriter{ + sessions: make(map[tun.DirectRouteSession]tun.DirectRouteContext), + inet4Address: inet4Address, + inet6Address: inet6Address, + } +} + +func (m *Rewriter) CreateSession(session tun.DirectRouteSession, context tun.DirectRouteContext) { + m.access.Lock() + m.sessions[session] = context + m.access.Unlock() +} + +func (m *Rewriter) DeleteSession(session tun.DirectRouteSession) { + m.access.Lock() + delete(m.sessions, session) + m.access.Unlock() +} + +func (m *Rewriter) 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(0) + ipHdr4.SetChecksum(^ipHdr4.CalculateChecksum()) + } + switch ipHdr.TransportProtocol() { + case header.ICMPv4ProtocolNumber: + icmpHdr := header.ICMPv4(ipHdr.Payload()) + m.access.Lock() + m.source4Address[icmpHdr.Ident()] = sourceAddr + m.access.Lock() + case header.ICMPv6ProtocolNumber: + icmpHdr := header.ICMPv6(ipHdr.Payload()) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: ipHdr.SourceAddressSlice(), + Dst: ipHdr.DestinationAddressSlice(), + })) + m.access.Lock() + m.source6Address[icmpHdr.Ident()] = sourceAddr + m.access.Lock() + } +} + +func (m *Rewriter) 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.source4Address[ident] + if !loaded { + m.access.Unlock() + return false, nil + } + delete(m.source4Address, icmpHdr.Ident()) + m.access.Lock() + routeSession.Source = source + case header.ICMPv6ProtocolNumber: + icmpHdr := header.ICMPv6(ipHdr.Payload()) + m.access.Lock() + ident := icmpHdr.Ident() + source, loaded := m.source6Address[ident] + if !loaded { + m.access.Unlock() + return false, nil + } + delete(m.source6Address, icmpHdr.Ident()) + m.access.Lock() + 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(0) + ipHdr4.SetChecksum(^ipHdr4.CalculateChecksum()) + } + switch ipHdr.TransportProtocol() { + case header.ICMPv6ProtocolNumber: + icmpHdr := header.ICMPv6(ipHdr.Payload()) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: ipHdr.SourceAddressSlice(), + Dst: ipHdr.DestinationAddressSlice(), + })) + } + return true, context.WritePacket(packet) +} diff --git a/route_direct.go b/route_direct.go new file mode 100644 index 00000000..8358ae5f --- /dev/null +++ b/route_direct.go @@ -0,0 +1,51 @@ +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 +} + +type DirectRouteSession struct { + // IPVersion uint8 + // Network uint8 + Source netip.Addr + Destination netip.Addr +} + +type DirectRouteMapping struct { + mapping freelru.Cache[DirectRouteSession, DirectRouteDestination] +} + +func NewDirectRouteMapping(timeout time.Duration) *DirectRouteMapping { + mapping := common.Must1(freelru.NewSharded[DirectRouteSession, DirectRouteDestination](1024, maphash.NewHasher[DirectRouteSession]().Hash32)) + mapping.SetOnEvict(func(session DirectRouteSession, action DirectRouteDestination) { + action.Close() + }) + mapping.SetLifetime(timeout) + return &DirectRouteMapping{mapping} +} + +func (m *DirectRouteMapping) Lookup(session DirectRouteSession, constructor func() (DirectRouteDestination, error)) (DirectRouteDestination, error) { + var ( + created DirectRouteDestination + err error + ) + action, _, ok := m.mapping.GetAndRefreshOrAdd(session, func() (DirectRouteDestination, bool) { + created, err = constructor() + return created, err == nil + }) + if !ok { + return nil, err + } + return action, nil +} diff --git a/route_mapping.go b/route_mapping.go deleted file mode 100644 index 50292b9c..00000000 --- a/route_mapping.go +++ /dev/null @@ -1,45 +0,0 @@ -package tun - -import ( - "net/netip" - "time" - - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/contrab/freelru" - "github.com/sagernet/sing/contrab/maphash" -) - -type DirectRouteSession struct { - // IPVersion uint8 - // Network uint8 - Source netip.Addr - Destination netip.Addr -} - -type RouteMapping struct { - status freelru.Cache[DirectRouteSession, DirectRouteDestination] -} - -func NewRouteMapping(timeout time.Duration) *RouteMapping { - status := common.Must1(freelru.NewSharded[DirectRouteSession, DirectRouteDestination](1024, maphash.NewHasher[DirectRouteSession]().Hash32)) - status.SetOnEvict(func(session DirectRouteSession, action DirectRouteDestination) { - action.Close() - }) - status.SetLifetime(timeout) - return &RouteMapping{status} -} - -func (m *RouteMapping) Lookup(session DirectRouteSession, constructor func() (DirectRouteDestination, error)) (DirectRouteDestination, error) { - var ( - created DirectRouteDestination - err error - ) - action, _, ok := m.status.GetAndRefreshOrAdd(session, func() (DirectRouteDestination, bool) { - created, err = constructor() - return created, err == nil - }) - if !ok { - return nil, err - } - return action, nil -} diff --git a/route_nat.go b/route_nat.go deleted file mode 100644 index ccb8ae30..00000000 --- a/route_nat.go +++ /dev/null @@ -1,115 +0,0 @@ -package tun - -import ( - "net/netip" - "sync" - - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" - "github.com/sagernet/sing-tun/internal/gtcpip/header" - "github.com/sagernet/sing/common/buf" -) - -type DirectRouteDestination interface { - WritePacket(packet *buf.Buffer) error - Close() error -} - -type NatMapping struct { - access sync.RWMutex - sessions map[DirectRouteSession]DirectRouteContext - ipRewrite bool -} - -func NewNatMapping(ipRewrite bool) *NatMapping { - return &NatMapping{ - sessions: make(map[DirectRouteSession]DirectRouteContext), - ipRewrite: ipRewrite, - } -} - -func (m *NatMapping) CreateSession(session DirectRouteSession, context DirectRouteContext) { - if m.ipRewrite { - session.Source = netip.Addr{} - } - m.access.Lock() - m.sessions[session] = context - m.access.Unlock() -} - -func (m *NatMapping) DeleteSession(session DirectRouteSession) { - if m.ipRewrite { - session.Source = netip.Addr{} - } - m.access.Lock() - delete(m.sessions, session) - m.access.Unlock() -} - -func (m *NatMapping) WritePacket(packet []byte) (bool, error) { - var routeSession DirectRouteSession - switch header.IPVersion(packet) { - case header.IPv4Version: - ipHdr := header.IPv4(packet) - routeSession.Source = ipHdr.DestinationAddr() - routeSession.Destination = ipHdr.SourceAddr() - case header.IPv6Version: - ipHdr := header.IPv6(packet) - routeSession.Source = ipHdr.DestinationAddr() - routeSession.Destination = ipHdr.SourceAddr() - default: - return false, nil - } - m.access.RLock() - context, loaded := m.sessions[routeSession] - m.access.RUnlock() - if !loaded { - return false, nil - } - return true, context.WritePacket(packet) -} - -type NatWriter struct { - inet4Address netip.Addr - inet6Address netip.Addr -} - -func NewNatWriter(inet4Address netip.Addr, inet6Address netip.Addr) *NatWriter { - return &NatWriter{ - inet4Address: inet4Address, - inet6Address: inet6Address, - } -} - -func (w *NatWriter) RewritePacket(packet []byte) { - var ipHdr header.Network - var bindAddr netip.Addr - switch header.IPVersion(packet) { - case header.IPv4Version: - ipHdr = header.IPv4(packet) - bindAddr = w.inet4Address - case header.IPv6Version: - ipHdr = header.IPv6(packet) - bindAddr = w.inet6Address - default: - return - } - ipHdr.SetSourceAddr(bindAddr) - switch ipHdr.TransportProtocol() { - case header.ICMPv4ProtocolNumber: - icmpHdr := header.ICMPv4(packet) - icmpHdr.SetChecksum(0) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) - case header.ICMPv6ProtocolNumber: - icmpHdr := header.ICMPv6(packet) - icmpHdr.SetChecksum(0) - icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr, - Src: ipHdr.SourceAddressSlice(), - Dst: ipHdr.DestinationAddressSlice(), - })) - } - if ipHdr4, isIPv4 := ipHdr.(header.IPv4); isIPv4 { - ipHdr4.SetChecksum(0) - ipHdr4.SetChecksum(^ipHdr4.CalculateChecksum()) - } -} diff --git a/route_nat_gvisor.go b/route_nat_gvisor.go deleted file mode 100644 index 487e86b2..00000000 --- a/route_nat_gvisor.go +++ /dev/null @@ -1,42 +0,0 @@ -//go:build with_gvisor - -package tun - -import ( - "github.com/sagernet/gvisor/pkg/tcpip" - "github.com/sagernet/gvisor/pkg/tcpip/header" - "github.com/sagernet/gvisor/pkg/tcpip/stack" -) - -func (w *NatWriter) RewritePacketBuffer(packetBuffer *stack.PacketBuffer) { - var bindAddr tcpip.Address - if packetBuffer.NetworkProtocolNumber == header.IPv4ProtocolNumber { - bindAddr = AddressFromAddr(w.inet4Address) - } else { - bindAddr = AddressFromAddr(w.inet6Address) - } - /*var ipHdr header.Network - switch packetBuffer.NetworkProtocolNumber { - case header.IPv4ProtocolNumber: - ipHdr = header.IPv4(packetBuffer.NetworkHeader().Slice()) - case header.IPv6ProtocolNumber: - ipHdr = header.IPv6(packetBuffer.NetworkHeader().Slice()) - default: - return - }*/ - ipHdr := packetBuffer.Network() - oldAddr := ipHdr.SourceAddress() - if checksumHdr, needChecksum := ipHdr.(header.ChecksummableNetwork); needChecksum { - checksumHdr.SetSourceAddressWithChecksumUpdate(bindAddr) - } else { - ipHdr.SetSourceAddress(bindAddr) - } - switch packetBuffer.TransportProtocolNumber { - case header.TCPProtocolNumber: - tcpHdr := header.TCP(packetBuffer.TransportHeader().Slice()) - tcpHdr.UpdateChecksumPseudoHeaderAddress(oldAddr, bindAddr, true) - case header.UDPProtocolNumber: - udpHdr := header.UDP(packetBuffer.TransportHeader().Slice()) - udpHdr.UpdateChecksumPseudoHeaderAddress(oldAddr, bindAddr, true) - } -} diff --git a/stack_gvisor.go b/stack_gvisor.go index a3b6fad2..cc488f65 100644 --- a/stack_gvisor.go +++ b/stack_gvisor.go @@ -15,6 +15,7 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip/network/ipv6" "github.com/sagernet/gvisor/pkg/tcpip/stack" "github.com/sagernet/gvisor/pkg/tcpip/transport/icmp" + "github.com/sagernet/gvisor/pkg/tcpip/transport/raw" "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" "github.com/sagernet/gvisor/pkg/tcpip/transport/udp" E "github.com/sagernet/sing/common/exceptions" @@ -86,13 +87,14 @@ func (t *GVisor) Start() error { return err } linkEndpoint = &LinkEndpointFilter{linkEndpoint, t.broadcastAddr, t.tun} - ipStack, err := NewGVisorStackWithOptions(linkEndpoint, nicOptions) + ipStack, err := NewGVisorStackWithOptions(linkEndpoint, nicOptions, false) 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.inet4Address, t.inet6Address, t.handler, t.udpTimeout) + icmpForwarder := NewICMPForwarder(t.ctx, ipStack, t.handler, t.udpTimeout) + icmpForwarder.SetLocalAddresses(t.inet4Address, t.inet6Address) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) t.stack = ipStack @@ -129,11 +131,11 @@ func AddrFromAddress(address tcpip.Address) netip.Addr { } func NewGVisorStack(ep stack.LinkEndpoint) (*stack.Stack, error) { - return NewGVisorStackWithOptions(ep, stack.NICOptions{}) + return NewGVisorStackWithOptions(ep, stack.NICOptions{}, false) } -func NewGVisorStackWithOptions(ep stack.LinkEndpoint, opts stack.NICOptions) (*stack.Stack, error) { - ipStack := stack.New(stack.Options{ +func NewGVisorStackWithOptions(ep stack.LinkEndpoint, opts stack.NICOptions, allowRawEndpoint bool) (*stack.Stack, error) { + stackOptions := stack.Options{ NetworkProtocols: []stack.NetworkProtocolFactory{ ipv4.NewProtocol, ipv6.NewProtocol, @@ -144,7 +146,11 @@ func NewGVisorStackWithOptions(ep stack.LinkEndpoint, opts stack.NICOptions) (*s icmp.NewProtocol4, icmp.NewProtocol6, }, - }) + } + if allowRawEndpoint { + stackOptions.RawFactory = new(raw.EndpointFactory) + } + ipStack := stack.New(stackOptions) err := ipStack.CreateNICWithOptions(DefaultNIC, ep, opts) if err != nil { return nil, gonet.TranslateNetstackError(err) diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index e02aa45e..3a134a95 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -27,27 +27,28 @@ type ICMPForwarder struct { inet4Address netip.Addr inet6Address netip.Addr handler Handler - directNat *RouteMapping + mapping *DirectRouteMapping } func NewICMPForwarder( ctx context.Context, stack *stack.Stack, - inet4Address netip.Addr, - inet6Address netip.Addr, handler Handler, timeout time.Duration, ) *ICMPForwarder { return &ICMPForwarder{ - ctx: ctx, - stack: stack, - inet4Address: inet4Address, - inet6Address: inet6Address, - handler: handler, - directNat: NewRouteMapping(timeout), + ctx: ctx, + stack: stack, + handler: handler, + mapping: NewDirectRouteMapping(timeout), } } +func (f *ICMPForwarder) SetLocalAddresses(inet4Address, inet6Address netip.Addr) { + f.inet4Address = inet4Address + f.inet6Address = inet6Address +} + func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { if pkt.NetworkProtocolNumber == header.IPv4ProtocolNumber { ipHdr := header.IPv4(pkt.NetworkHeader().Slice()) @@ -58,7 +59,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa sourceAddr := M.AddrFromIP(ipHdr.SourceAddressSlice()) destinationAddr := M.AddrFromIP(ipHdr.DestinationAddressSlice()) if destinationAddr != f.inet4Address { - action, err := f.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { + action, err := f.mapping.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { return f.handler.PrepareConnection( N.NetworkICMPv4, M.SocksaddrFrom(sourceAddr, 0), @@ -116,7 +117,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa sourceAddr := M.AddrFromIP(ipHdr.SourceAddressSlice()) destinationAddr := M.AddrFromIP(ipHdr.DestinationAddressSlice()) if destinationAddr != f.inet6Address { - action, err := f.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { + action, err := f.mapping.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { return f.handler.PrepareConnection( N.NetworkICMPv6, M.SocksaddrFrom(sourceAddr, 0), diff --git a/stack_system.go b/stack_system.go index e23366bf..be8873ab 100644 --- a/stack_system.go +++ b/stack_system.go @@ -45,7 +45,7 @@ type System struct { tcpPort6 uint16 tcpNat *TCPNat udpNat *udpnat.Service - directNat *RouteMapping + directNat *DirectRouteMapping bindInterface bool interfaceFinder control.InterfaceFinder frontHeadroom int @@ -160,7 +160,7 @@ 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 = NewRouteMapping(s.udpTimeout) + s.directNat = NewDirectRouteMapping(s.udpTimeout) return nil } From dd18aa2b8633680102966f6b99b61327463561df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 24 Aug 2025 10:47:15 +0800 Subject: [PATCH 041/121] ping: Fix on android --- ping/ping.go | 4 ++-- ping/socket_unix.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ping/ping.go b/ping/ping.go index caad5f46..1f78c529 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -40,7 +40,7 @@ func Connect(privileged bool, controlFunc control.Func, destination netip.Addr) } func (c *Conn) ReadIP(buffer *buf.Buffer) error { - if c.destination.Is6() || runtime.GOOS == "linux" && !c.privileged { + if c.destination.Is6() || (runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged { var readMsg func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) switch conn := c.conn.(type) { case *net.IPConn: @@ -163,7 +163,7 @@ func (c *Conn) ReadICMP(buffer *buf.Buffer) error { if err != nil { return err } - if c.destination.Is6() || runtime.GOOS == "linux" && !c.privileged { + if c.destination.Is6() || (runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged { return nil } if !c.destination.Is6() { diff --git a/ping/socket_unix.go b/ping/socket_unix.go index caf0f310..a01ef929 100644 --- a/ping/socket_unix.go +++ b/ping/socket_unix.go @@ -47,7 +47,7 @@ func connect(privileged bool, controlFunc control.Func, destination netip.Addr) return nil, E.Cause(err, "connect()") } - if destination.Is4() && runtime.GOOS == "linux" { + if destination.Is4() && (runtime.GOOS == "linux" || runtime.GOOS == "android") { //err = unix.SetsockoptInt(fd, unix.IPPROTO_IP, unix.IP_RECVTOS, 1) //if err != nil { // return nil, err From 7f41766568647f2e2c9983382fb88a9facb80690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 24 Aug 2025 12:39:50 +0800 Subject: [PATCH 042/121] ping: Fix control --- ping/socket_unix.go | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/ping/socket_unix.go b/ping/socket_unix.go index a01ef929..851d271e 100644 --- a/ping/socket_unix.go +++ b/ping/socket_unix.go @@ -12,7 +12,6 @@ import ( "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" - "golang.org/x/sys/unix" ) @@ -42,11 +41,17 @@ func connect(privileged bool, controlFunc control.Func, destination netip.Addr) } file := os.NewFile(uintptr(fd), "datagram-oriented icmp") defer file.Close() - err = unix.Connect(fd, M.AddrPortToSockaddr(netip.AddrPortFrom(destination, 0))) - if err != nil { - return nil, E.Cause(err, "connect()") + if controlFunc != nil { + var syscallConn syscall.RawConn + syscallConn, err = file.SyscallConn() + if err != nil { + return nil, err + } + err = controlFunc(network, destination.String(), syscallConn) + if err != nil { + return nil, err + } } - if destination.Is4() && (runtime.GOOS == "linux" || runtime.GOOS == "android") { //err = unix.SetsockoptInt(fd, unix.IPPROTO_IP, unix.IP_RECVTOS, 1) //if err != nil { @@ -68,20 +73,14 @@ func connect(privileged bool, controlFunc control.Func, destination netip.Addr) } } + err = unix.Connect(fd, M.AddrPortToSockaddr(netip.AddrPortFrom(destination, 0))) + if err != nil { + return nil, E.Cause(err, "connect()") + } + conn, err := net.FileConn(file) if err != nil { return nil, err } - if controlFunc != nil { - var syscallConn syscall.RawConn - syscallConn, err = conn.(syscall.Conn).SyscallConn() - if err != nil { - return nil, err - } - err = controlFunc(network, destination.String(), syscallConn) - if err != nil { - return nil, err - } - } return conn, nil } From d53158b8d7faaa5029ae1fb25746f23f3881b068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 24 Aug 2025 12:39:56 +0800 Subject: [PATCH 043/121] ping: Add logs --- ping/destination.go | 6 +++--- ping/ping.go | 16 +++++++++++++++- ping/ping_test.go | 10 ++++++---- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/ping/destination.go b/ping/destination.go index 0640b2ec..04411585 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -30,11 +30,11 @@ func ConnectDestination(ctx context.Context, logger logger.ContextLogger, contro ) switch runtime.GOOS { case "darwin", "ios", "windows": - conn, err = Connect(false, controlFunc, address) + conn, err = Connect(ctx, logger, false, controlFunc, address) default: - conn, err = Connect(true, controlFunc, address) + conn, err = Connect(ctx, logger, true, controlFunc, address) if errors.Is(err, os.ErrPermission) { - conn, err = Connect(false, controlFunc, address) + conn, err = Connect(ctx, logger, false, controlFunc, address) } } if err != nil { diff --git a/ping/ping.go b/ping/ping.go index 1f78c529..8717cc82 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -1,6 +1,7 @@ package ping import ( + "context" "net" "net/netip" "reflect" @@ -14,6 +15,7 @@ import ( "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" M "github.com/sagernet/sing/common/metadata" "golang.org/x/net/ipv4" @@ -21,18 +23,22 @@ import ( ) type Conn struct { + ctx context.Context + logger logger.ContextLogger privileged bool conn net.Conn destination netip.Addr source atomic.TypedValue[netip.Addr] } -func Connect(privileged bool, controlFunc control.Func, destination netip.Addr) (*Conn, error) { +func Connect(ctx context.Context, logger logger.ContextLogger, privileged bool, controlFunc control.Func, destination netip.Addr) (*Conn, error) { conn, err := connect(privileged, controlFunc, destination) if err != nil { return nil, err } return &Conn{ + ctx: ctx, + logger: logger, privileged: privileged, conn: conn, destination: destination, @@ -95,6 +101,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { TotalLength: uint16(buffer.Len()), }) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) + c.logger.TraceContext(c.ctx, "read icmpv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) } else { oob := make([]byte, 1024) buffer.Advance(header.IPv6MinimumSize) @@ -131,6 +138,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { SrcAddr: addr, DstAddr: c.source.Load(), }) + c.logger.TraceContext(c.ctx, "read icmpv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) } } else { _, err := buffer.ReadOnceFrom(c.conn) @@ -144,6 +152,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) icmpHdr := header.ICMPv4(ipHdr.Payload()) icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + c.logger.TraceContext(c.ctx, "read icmpv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) } else { ipHdr := header.IPv6(buffer.Bytes()) ipHdr.SetDestinationAddr(c.source.Load()) @@ -153,6 +162,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { Src: ipHdr.SourceAddressSlice(), Dst: ipHdr.DestinationAddressSlice(), })) + c.logger.TraceContext(c.ctx, "read icmpv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) } } return nil @@ -169,9 +179,11 @@ func (c *Conn) ReadICMP(buffer *buf.Buffer) error { if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) buffer.Advance(int(ipHdr.HeaderLength())) + c.logger.TraceContext(c.ctx, "read icmpv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) } else { ipHdr := header.IPv6(buffer.Bytes()) buffer.Advance(buffer.Len() - int(ipHdr.PayloadLength())) + c.logger.TraceContext(c.ctx, "read icmpv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) } return nil } @@ -181,10 +193,12 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) c.source.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) + c.logger.TraceContext(c.ctx, "write icmpv4 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) return common.Error(c.conn.Write(ipHdr.Payload())) } else { ipHdr := header.IPv6(buffer.Bytes()) c.source.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) + c.logger.TraceContext(c.ctx, "write icmpv6 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) return common.Error(c.conn.Write(ipHdr.Payload())) } } diff --git a/ping/ping_test.go b/ping/ping_test.go index 65a6da90..d9980b08 100644 --- a/ping/ping_test.go +++ b/ping/ping_test.go @@ -1,6 +1,7 @@ package ping_test import ( + "context" "net/netip" "os" "runtime" @@ -11,6 +12,7 @@ import ( "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" ) @@ -71,7 +73,7 @@ func TestPing(t *testing.T) { } func testPingIPv4ReadIP(t *testing.T, privileged bool, addr string) { - conn, err := ping.Connect(privileged, nil, netip.MustParseAddr(addr)) + conn, err := ping.Connect(context.Background(), logger.NOP(), privileged, nil, netip.MustParseAddr(addr)) if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { t.SkipNow() } @@ -103,7 +105,7 @@ func testPingIPv4ReadIP(t *testing.T, privileged bool, addr string) { } func testPingIPv4ReadICMP(t *testing.T, privileged bool, addr string) { - conn, err := ping.Connect(privileged, nil, netip.MustParseAddr(addr)) + conn, err := ping.Connect(context.Background(), logger.NOP(), privileged, nil, netip.MustParseAddr(addr)) if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { t.SkipNow() } @@ -134,7 +136,7 @@ func testPingIPv4ReadICMP(t *testing.T, privileged bool, addr string) { } func testPingIPv6ReadIP(t *testing.T, privileged bool, addr string) { - conn, err := ping.Connect(privileged, nil, netip.MustParseAddr(addr)) + conn, err := ping.Connect(context.Background(), logger.NOP(), privileged, nil, netip.MustParseAddr(addr)) if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { t.SkipNow() } @@ -165,7 +167,7 @@ func testPingIPv6ReadIP(t *testing.T, privileged bool, addr string) { } func testPingIPv6ReadICMP(t *testing.T, privileged bool, addr string) { - conn, err := ping.Connect(privileged, nil, netip.MustParseAddr(addr)) + conn, err := ping.Connect(context.Background(), logger.NOP(), privileged, nil, netip.MustParseAddr(addr)) if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { t.SkipNow() } From bee7be8598cefcd90287d8ae28a1c6e4e60d5970 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sun, 24 Aug 2025 12:50:12 +0800 Subject: [PATCH 044/121] ping: fix Logs --- ping/destination.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ping/destination.go b/ping/destination.go index 04411585..3b87c743 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -63,7 +63,7 @@ func (d *Destination) loopRead() { } err = d.routeContext.WritePacket(buffer.Bytes()) if err != nil { - d.logger.Error(d.ctx, E.Cause(err, "write ICMP echo reply")) + d.logger.ErrorContext(d.ctx, E.Cause(err, "write ICMP echo reply")) } buffer.Release() } From 3faf8cf679840d382c8f5d4e5a3bd82088e04c63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 24 Aug 2025 14:07:11 +0800 Subject: [PATCH 045/121] ping: Add test for ident --- ping/ping_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ping/ping_test.go b/ping/ping_test.go index d9980b08..015faa3b 100644 --- a/ping/ping_test.go +++ b/ping/ping_test.go @@ -102,6 +102,7 @@ func testPingIPv4ReadIP(t *testing.T, privileged bool, addr string) { require.NotZero(t, ipHdr.TTL()) icmpHdr := header.ICMPv4(ipHdr.Payload()) require.Equal(t, header.ICMPv4EchoReply, icmpHdr.Type()) + require.Equal(t, request.Ident(), icmpHdr.Ident()) } func testPingIPv4ReadICMP(t *testing.T, privileged bool, addr string) { @@ -133,6 +134,7 @@ func testPingIPv4ReadICMP(t *testing.T, privileged bool, addr string) { icmpHdr := header.ICMPv4(response.Bytes()) require.Equal(t, header.ICMPv4EchoReply, icmpHdr.Type()) + require.Equal(t, request.Ident(), icmpHdr.Ident()) } func testPingIPv6ReadIP(t *testing.T, privileged bool, addr string) { @@ -164,6 +166,7 @@ func testPingIPv6ReadIP(t *testing.T, privileged bool, addr string) { require.NotZero(t, ipHdr.HopLimit()) icmpHdr := header.ICMPv6(ipHdr.Payload()) require.Equal(t, header.ICMPv6EchoReply, icmpHdr.Type()) + require.Equal(t, request.Ident(), icmpHdr.Ident()) } func testPingIPv6ReadICMP(t *testing.T, privileged bool, addr string) { @@ -192,4 +195,5 @@ func testPingIPv6ReadICMP(t *testing.T, privileged bool, addr string) { } icmpHdr := header.ICMPv6(response.Bytes()) require.Equal(t, header.ICMPv6EchoReply, icmpHdr.Type()) + require.Equal(t, request.Ident(), icmpHdr.Ident()) } From 8f6cc9f62e60b7684c228f294f5e95b54ee61f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 24 Aug 2025 15:06:09 +0800 Subject: [PATCH 046/121] ping: Fix unprivileged response on linux --- ping/ping.go | 25 +++-- ping/socket_linux_unprivileged.go | 173 ++++++++++++++++++++++++++++++ ping/socket_unix.go | 3 +- 3 files changed, 194 insertions(+), 7 deletions(-) create mode 100644 ping/socket_linux_unprivileged.go diff --git a/ping/ping.go b/ping/ping.go index 8717cc82..cbe06724 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -32,7 +32,7 @@ type Conn struct { } func Connect(ctx context.Context, logger logger.ContextLogger, privileged bool, controlFunc control.Func, destination netip.Addr) (*Conn, error) { - conn, err := connect(privileged, controlFunc, destination) + conn, err := connect0(ctx, privileged, controlFunc, destination) if err != nil { return nil, err } @@ -45,6 +45,14 @@ func Connect(ctx context.Context, logger logger.ContextLogger, privileged bool, }, nil } +func connect0(ctx context.Context, privileged bool, controlFunc control.Func, destination netip.Addr) (net.Conn, error) { + if (runtime.GOOS == "linux" || runtime.GOOS == "android") && !privileged { + return newUnprivilegedConn(ctx, controlFunc, destination) + } else { + return connect(privileged, controlFunc, destination) + } +} + func (c *Conn) ReadIP(buffer *buf.Buffer) error { if c.destination.Is6() || (runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged { var readMsg func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) @@ -53,20 +61,22 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { readMsg = func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) { var ipAddr *net.IPAddr n, oobn, _, ipAddr, err = conn.ReadMsgIP(b, oob) - if ipAddr != nil { + if err == nil { addr = M.AddrFromNet(ipAddr) } return } case *net.UDPConn: readMsg = func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) { - var udpAddr *net.UDPAddr - n, oobn, _, udpAddr, err = conn.ReadMsgUDP(b, oob) - if udpAddr != nil { - addr = M.AddrFromNet(udpAddr) + var addrPort netip.AddrPort + n, oobn, _, addrPort, err = conn.ReadMsgUDPAddrPort(b, oob) + if err == nil { + addr = addrPort.Addr() } return } + case *UnprivilegedConn: + readMsg = conn.ReadMsg default: return E.New("unsupported conn type: ", reflect.TypeOf(c.conn)) } @@ -124,6 +134,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { trafficClass = controlMessage.TrafficClass } icmpHdr := header.ICMPv6(buffer.Bytes()) + icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmpHdr[:header.ICMPv6DstUnreachableMinimumSize], Src: addr.AsSlice(), @@ -151,12 +162,14 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { ipHdr.SetChecksum(0) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) icmpHdr := header.ICMPv4(ipHdr.Payload()) + icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) c.logger.TraceContext(c.ctx, "read icmpv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) } else { ipHdr := header.IPv6(buffer.Bytes()) ipHdr.SetDestinationAddr(c.source.Load()) icmpHdr := header.ICMPv6(ipHdr.Payload()) + icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmpHdr, Src: ipHdr.SourceAddressSlice(), diff --git a/ping/socket_linux_unprivileged.go b/ping/socket_linux_unprivileged.go new file mode 100644 index 00000000..6059347a --- /dev/null +++ b/ping/socket_linux_unprivileged.go @@ -0,0 +1,173 @@ +package ping + +import ( + "context" + "net" + "net/netip" + "os" + "time" + + "github.com/sagernet/sing-tun/internal/gtcpip/checksum" + "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing/common/atomic" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/control" + M "github.com/sagernet/sing/common/metadata" +) + +type UnprivilegedConn struct { + ctx context.Context + cancel context.CancelFunc + controlFunc control.Func + destination netip.Addr + receiveChan chan *unprivilegedResponse + readDeadline atomic.TypedValue[time.Time] + writeDeadline atomic.TypedValue[time.Time] +} + +type unprivilegedResponse struct { + Buffer *buf.Buffer + Cmsg *buf.Buffer + Addr netip.Addr +} + +func newUnprivilegedConn(ctx context.Context, controlFunc control.Func, destination netip.Addr) (net.Conn, error) { + conn, err := connect(false, controlFunc, destination) + if err != nil { + return nil, err + } + conn.Close() + ctx, cancel := context.WithCancel(ctx) + return &UnprivilegedConn{ + ctx: ctx, + cancel: cancel, + controlFunc: controlFunc, + destination: destination, + receiveChan: make(chan *unprivilegedResponse), + }, nil +} + +func (c *UnprivilegedConn) Read(b []byte) (n int, err error) { + select { + case packet := <-c.receiveChan: + n = copy(b, packet.Buffer.Bytes()) + packet.Buffer.Release() + packet.Cmsg.Release() + return + case <-c.ctx.Done(): + return 0, os.ErrClosed + } +} + +func (c *UnprivilegedConn) ReadMsg(b []byte, oob []byte) (n, oobn int, addr netip.Addr, err error) { + select { + case packet := <-c.receiveChan: + n = copy(b, packet.Buffer.Bytes()) + oobn = copy(oob, packet.Cmsg.Bytes()) + addr = packet.Addr + packet.Buffer.Release() + packet.Cmsg.Release() + return + case <-c.ctx.Done(): + return 0, 0, netip.Addr{}, os.ErrClosed + } +} + +func (c *UnprivilegedConn) Write(b []byte) (n int, err error) { + conn, err := connect(false, c.controlFunc, c.destination) + if err != nil { + return + } + var identifier uint16 + if !c.destination.Is6() { + icmpHdr := header.ICMPv4(b) + identifier = icmpHdr.Ident() + } else { + icmpHdr := header.ICMPv6(b) + identifier = icmpHdr.Ident() + } + if readDeadline := c.readDeadline.Load(); !readDeadline.IsZero() { + conn.SetReadDeadline(readDeadline) + } + if writeDeadline := c.writeDeadline.Load(); !writeDeadline.IsZero() { + conn.SetWriteDeadline(writeDeadline) + } + n, err = conn.Write(b) + if err != nil { + conn.Close() + return + } + go c.fetchResponse(conn, identifier) + return +} + +func (c *UnprivilegedConn) fetchResponse(conn net.Conn, identifier uint16) { + done := make(chan struct{}) + defer close(done) + go func() { + select { + case <-c.ctx.Done(): + case <-done: + } + conn.Close() + }() + buffer := buf.NewPacket() + cmsgBuffer := buf.NewSize(1024) + n, oobN, _, addr, err := conn.(*net.UDPConn).ReadMsgUDPAddrPort(buffer.FreeBytes(), cmsgBuffer.FreeBytes()) + if err != nil { + buffer.Release() + cmsgBuffer.Release() + return + } + buffer.Truncate(n) + cmsgBuffer.Truncate(oobN) + if !c.destination.Is6() { + icmpHdr := header.ICMPv4(buffer.Bytes()) + icmpHdr.SetIdent(identifier) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + } else { + icmpHdr := header.ICMPv6(buffer.Bytes()) + icmpHdr.SetIdent(identifier) + // offload checksum here since we don't have source address here + } + select { + case c.receiveChan <- &unprivilegedResponse{ + Buffer: buffer, + Cmsg: cmsgBuffer, + Addr: addr.Addr(), + }: + case <-c.ctx.Done(): + buffer.Release() + cmsgBuffer.Release() + } +} + +func (c *UnprivilegedConn) Close() error { + c.cancel() + return nil +} + +func (c *UnprivilegedConn) LocalAddr() net.Addr { + return M.Socksaddr{} +} + +func (c *UnprivilegedConn) RemoteAddr() net.Addr { + return M.SocksaddrFrom(c.destination, 0).UDPAddr() +} + +func (c *UnprivilegedConn) SetDeadline(t time.Time) error { + c.readDeadline.Store(t) + c.writeDeadline.Store(t) + return nil +} + +func (c *UnprivilegedConn) SetReadDeadline(t time.Time) error { + c.readDeadline.Store(t) + return nil +} + +func (c *UnprivilegedConn) SetWriteDeadline(t time.Time) error { + c.writeDeadline.Store(t) + return nil +} diff --git a/ping/socket_unix.go b/ping/socket_unix.go index 851d271e..1618dfc9 100644 --- a/ping/socket_unix.go +++ b/ping/socket_unix.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" + "golang.org/x/sys/unix" ) @@ -77,7 +78,7 @@ func connect(privileged bool, controlFunc control.Func, destination netip.Addr) if err != nil { return nil, E.Cause(err, "connect()") } - + conn, err := net.FileConn(file) if err != nil { return nil, err From 737ebf01c43c3aaf59d71624c2032674e011a004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 24 Aug 2025 17:46:03 +0800 Subject: [PATCH 047/121] ping: Add timeout to destinations --- ping/destination.go | 27 +++++++++++++++++++++++---- ping/destination_gvisor.go | 16 ++++++++++++++++ ping/destination_test.go | 24 ++++++++++++++++++++++++ ping/ping.go | 6 ++++++ ping/socket_linux_unprivileged.go | 23 ++++++++--------------- route_direct.go | 4 ++++ 6 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 ping/destination_test.go diff --git a/ping/destination.go b/ping/destination.go index 3b87c743..dc20112d 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -6,6 +6,7 @@ import ( "net/netip" "os" "runtime" + "time" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/buf" @@ -17,13 +18,21 @@ import ( var _ tun.DirectRouteDestination = (*Destination)(nil) type Destination struct { + conn *Conn ctx context.Context logger logger.ContextLogger routeContext tun.DirectRouteContext - conn *Conn + timeout time.Duration } -func ConnectDestination(ctx context.Context, logger logger.ContextLogger, controlFunc control.Func, address netip.Addr, routeContext tun.DirectRouteContext) (tun.DirectRouteDestination, error) { +func ConnectDestination( + ctx context.Context, + logger logger.ContextLogger, + controlFunc control.Func, + address netip.Addr, + routeContext tun.DirectRouteContext, + timeout time.Duration, +) (tun.DirectRouteDestination, error) { var ( conn *Conn err error @@ -41,19 +50,25 @@ func ConnectDestination(ctx context.Context, logger logger.ContextLogger, contro return nil, err } d := &Destination{ + conn: conn, ctx: ctx, logger: logger, routeContext: routeContext, - conn: conn, + timeout: timeout, } go d.loopRead() return d, nil } func (d *Destination) loopRead() { + defer d.Close() for { buffer := buf.NewPacket() - err := d.conn.ReadIP(buffer) + 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")) + } + err = d.conn.ReadIP(buffer) if err != nil { buffer.Release() if !E.IsClosed(err) { @@ -76,3 +91,7 @@ func (d *Destination) WritePacket(packet *buf.Buffer) error { func (d *Destination) Close() error { return d.conn.Close() } + +func (d *Destination) IsClosed() bool { + return d.conn.IsClosed() +} diff --git a/ping/destination_gvisor.go b/ping/destination_gvisor.go index abe98c84..a026f4af 100644 --- a/ping/destination_gvisor.go +++ b/ping/destination_gvisor.go @@ -5,11 +5,13 @@ 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" @@ -23,8 +25,10 @@ var _ tun.DirectRouteDestination = (*GVisorDestination)(nil) type GVisorDestination struct { ctx context.Context logger logger.ContextLogger + endpoint tcpip.Endpoint conn *gonet.TCPConn rewriter *Rewriter + timeout time.Duration } func ConnectGVisor( @@ -33,6 +37,7 @@ func ConnectGVisor( routeContext tun.DirectRouteContext, stack *stack.Stack, bindAddress4, bindAddress6 netip.Addr, + timeout time.Duration, ) (*GVisorDestination, error) { var ( bindAddress tcpip.Address @@ -76,16 +81,23 @@ func ConnectGVisor( destination := &GVisorDestination{ ctx: ctx, logger: logger, + endpoint: endpoint, conn: gonet.NewTCPConn(&wq, endpoint), rewriter: rewriter, + timeout: timeout, } go destination.loopRead() return destination, nil } func (d *GVisorDestination) loopRead() { + defer d.endpoint.Close() for { buffer := buf.NewPacket() + 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")) + } n, err := d.conn.Read(buffer.FreeBytes()) if err != nil { buffer.Release() @@ -111,3 +123,7 @@ func (d *GVisorDestination) WritePacket(packet *buf.Buffer) error { 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_test.go b/ping/destination_test.go new file mode 100644 index 00000000..d0a1af88 --- /dev/null +++ b/ping/destination_test.go @@ -0,0 +1,24 @@ +package ping_test + +import ( + "context" + "net/netip" + "testing" + "time" + + "github.com/sagernet/sing-tun/ping" + "github.com/sagernet/sing/common/logger" + + "github.com/stretchr/testify/require" +) + +func TestIsClosed(t *testing.T) { + t.Parallel() + destination, err := ping.ConnectDestination(context.Background(), logger.NOP(), nil, netip.MustParseAddr("1.1.1.1"), nil, 30*time.Second) + require.NoError(t, err) + defer destination.Close() + time.Sleep(1 * time.Second) + require.False(t, destination.IsClosed()) + destination.Close() + require.True(t, destination.IsClosed()) +} diff --git a/ping/ping.go b/ping/ping.go index cbe06724..1ea0edfe 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -29,6 +29,7 @@ type Conn struct { conn net.Conn destination netip.Addr source atomic.TypedValue[netip.Addr] + closed atomic.Bool } func Connect(ctx context.Context, logger logger.ContextLogger, privileged bool, controlFunc control.Func, destination netip.Addr) (*Conn, error) { @@ -230,5 +231,10 @@ func (c *Conn) SetReadDeadline(t time.Time) error { } func (c *Conn) Close() error { + defer c.closed.Store(true) return c.conn.Close() } + +func (c *Conn) IsClosed() bool { + return c.closed.Load() +} diff --git a/ping/socket_linux_unprivileged.go b/ping/socket_linux_unprivileged.go index 6059347a..1da0c83f 100644 --- a/ping/socket_linux_unprivileged.go +++ b/ping/socket_linux_unprivileged.go @@ -16,13 +16,12 @@ import ( ) type UnprivilegedConn struct { - ctx context.Context - cancel context.CancelFunc - controlFunc control.Func - destination netip.Addr - receiveChan chan *unprivilegedResponse - readDeadline atomic.TypedValue[time.Time] - writeDeadline atomic.TypedValue[time.Time] + ctx context.Context + cancel context.CancelFunc + controlFunc control.Func + destination netip.Addr + receiveChan chan *unprivilegedResponse + readDeadline atomic.TypedValue[time.Time] } type unprivilegedResponse struct { @@ -89,9 +88,6 @@ func (c *UnprivilegedConn) Write(b []byte) (n int, err error) { if readDeadline := c.readDeadline.Load(); !readDeadline.IsZero() { conn.SetReadDeadline(readDeadline) } - if writeDeadline := c.writeDeadline.Load(); !writeDeadline.IsZero() { - conn.SetWriteDeadline(writeDeadline) - } n, err = conn.Write(b) if err != nil { conn.Close() @@ -157,9 +153,7 @@ func (c *UnprivilegedConn) RemoteAddr() net.Addr { } func (c *UnprivilegedConn) SetDeadline(t time.Time) error { - c.readDeadline.Store(t) - c.writeDeadline.Store(t) - return nil + return os.ErrInvalid } func (c *UnprivilegedConn) SetReadDeadline(t time.Time) error { @@ -168,6 +162,5 @@ func (c *UnprivilegedConn) SetReadDeadline(t time.Time) error { } func (c *UnprivilegedConn) SetWriteDeadline(t time.Time) error { - c.writeDeadline.Store(t) - return nil + return os.ErrInvalid } diff --git a/route_direct.go b/route_direct.go index 8358ae5f..2279aa82 100644 --- a/route_direct.go +++ b/route_direct.go @@ -13,6 +13,7 @@ import ( type DirectRouteDestination interface { WritePacket(packet *buf.Buffer) error Close() error + IsClosed() bool } type DirectRouteSession struct { @@ -28,6 +29,9 @@ type DirectRouteMapping struct { func NewDirectRouteMapping(timeout time.Duration) *DirectRouteMapping { mapping := common.Must1(freelru.NewSharded[DirectRouteSession, DirectRouteDestination](1024, maphash.NewHasher[DirectRouteSession]().Hash32)) + mapping.SetHealthCheck(func(session DirectRouteSession, destination DirectRouteDestination) bool { + return !destination.IsClosed() + }) mapping.SetOnEvict(func(session DirectRouteSession, action DirectRouteDestination) { action.Close() }) From ccfe5c0f0f2bfe9c05e13d0c28ca47a2bcd5ca8e Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sun, 24 Aug 2025 18:29:39 +0800 Subject: [PATCH 048/121] ping: Rewrite UnprivilegedConn --- ping/socket_linux_unprivileged.go | 135 ++++++++++++++++++------------ 1 file changed, 82 insertions(+), 53 deletions(-) diff --git a/ping/socket_linux_unprivileged.go b/ping/socket_linux_unprivileged.go index 1da0c83f..78d10260 100644 --- a/ping/socket_linux_unprivileged.go +++ b/ping/socket_linux_unprivileged.go @@ -5,14 +5,16 @@ import ( "net" "net/netip" "os" + "sync" "time" "github.com/sagernet/sing-tun/internal/gtcpip/checksum" "github.com/sagernet/sing-tun/internal/gtcpip/header" - "github.com/sagernet/sing/common/atomic" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/pipe" ) type UnprivilegedConn struct { @@ -21,7 +23,9 @@ type UnprivilegedConn struct { controlFunc control.Func destination netip.Addr receiveChan chan *unprivilegedResponse - readDeadline atomic.TypedValue[time.Time] + readDeadline pipe.Deadline + natMap map[uint16]net.Conn + natMapMutex sync.Mutex } type unprivilegedResponse struct { @@ -38,11 +42,13 @@ func newUnprivilegedConn(ctx context.Context, controlFunc control.Func, destinat conn.Close() ctx, cancel := context.WithCancel(ctx) return &UnprivilegedConn{ - ctx: ctx, - cancel: cancel, - controlFunc: controlFunc, - destination: destination, - receiveChan: make(chan *unprivilegedResponse), + ctx: ctx, + cancel: cancel, + controlFunc: controlFunc, + destination: destination, + receiveChan: make(chan *unprivilegedResponse), + readDeadline: pipe.MakeDeadline(), + natMap: make(map[uint16]net.Conn), }, nil } @@ -55,6 +61,8 @@ func (c *UnprivilegedConn) Read(b []byte) (n int, err error) { return case <-c.ctx.Done(): return 0, os.ErrClosed + case <-c.readDeadline.Wait(): + return 0, os.ErrDeadlineExceeded } } @@ -69,14 +77,12 @@ func (c *UnprivilegedConn) ReadMsg(b []byte, oob []byte) (n, oobn int, addr neti return case <-c.ctx.Done(): return 0, 0, netip.Addr{}, os.ErrClosed + case <-c.readDeadline.Wait(): + return 0, 0, netip.Addr{}, os.ErrDeadlineExceeded } } func (c *UnprivilegedConn) Write(b []byte) (n int, err error) { - conn, err := connect(false, c.controlFunc, c.destination) - if err != nil { - return - } var identifier uint16 if !c.destination.Is6() { icmpHdr := header.ICMPv4(b) @@ -85,62 +91,85 @@ func (c *UnprivilegedConn) Write(b []byte) (n int, err error) { icmpHdr := header.ICMPv6(b) identifier = icmpHdr.Ident() } - if readDeadline := c.readDeadline.Load(); !readDeadline.IsZero() { - conn.SetReadDeadline(readDeadline) + + c.natMapMutex.Lock() + if err = c.ctx.Err(); err != nil { + c.natMapMutex.Unlock() + return 0, err + } + conn, ok := c.natMap[identifier] + if !ok { + conn, err = connect(false, c.controlFunc, c.destination) + if err != nil { + c.natMapMutex.Unlock() + return 0, err + } + go c.fetchResponse(conn.(*net.UDPConn), identifier) } + c.natMapMutex.Unlock() + n, err = conn.Write(b) if err != nil { - conn.Close() + c.removeConn(conn.(*net.UDPConn), identifier) return } - go c.fetchResponse(conn, identifier) return } -func (c *UnprivilegedConn) fetchResponse(conn net.Conn, identifier uint16) { - done := make(chan struct{}) - defer close(done) - go func() { +func (c *UnprivilegedConn) fetchResponse(conn *net.UDPConn, identifier uint16) { + defer c.removeConn(conn, identifier) + for { + buffer := buf.NewPacket() + cmsgBuffer := buf.NewSize(1024) + n, oobN, _, addr, err := conn.ReadMsgUDPAddrPort(buffer.FreeBytes(), cmsgBuffer.FreeBytes()) + if err != nil { + buffer.Release() + cmsgBuffer.Release() + return + } + buffer.Truncate(n) + cmsgBuffer.Truncate(oobN) + if !c.destination.Is6() { + icmpHdr := header.ICMPv4(buffer.Bytes()) + icmpHdr.SetIdent(identifier) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + } else { + icmpHdr := header.ICMPv6(buffer.Bytes()) + icmpHdr.SetIdent(identifier) + // offload checksum here since we don't have source address here + } select { + case c.receiveChan <- &unprivilegedResponse{ + Buffer: buffer, + Cmsg: cmsgBuffer, + Addr: addr.Addr(), + }: case <-c.ctx.Done(): - case <-done: + buffer.Release() + cmsgBuffer.Release() + return } - conn.Close() - }() - buffer := buf.NewPacket() - cmsgBuffer := buf.NewSize(1024) - n, oobN, _, addr, err := conn.(*net.UDPConn).ReadMsgUDPAddrPort(buffer.FreeBytes(), cmsgBuffer.FreeBytes()) - if err != nil { - buffer.Release() - cmsgBuffer.Release() - return } - buffer.Truncate(n) - cmsgBuffer.Truncate(oobN) - if !c.destination.Is6() { - icmpHdr := header.ICMPv4(buffer.Bytes()) - icmpHdr.SetIdent(identifier) - icmpHdr.SetChecksum(0) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) - } else { - icmpHdr := header.ICMPv6(buffer.Bytes()) - icmpHdr.SetIdent(identifier) - // offload checksum here since we don't have source address here - } - select { - case c.receiveChan <- &unprivilegedResponse{ - Buffer: buffer, - Cmsg: cmsgBuffer, - Addr: addr.Addr(), - }: - case <-c.ctx.Done(): - buffer.Release() - cmsgBuffer.Release() +} + +func (c *UnprivilegedConn) removeConn(conn *net.UDPConn, identifier uint16) { + c.natMapMutex.Lock() + _ = conn.Close() + if c.natMap[identifier] == conn { + delete(c.natMap, identifier) } + c.natMapMutex.Unlock() } func (c *UnprivilegedConn) Close() error { + c.natMapMutex.Lock() c.cancel() + for _, conn := range c.natMap { + _ = conn.Close() + } + common.ClearMap(c.natMap) + c.natMapMutex.Unlock() return nil } @@ -153,14 +182,14 @@ func (c *UnprivilegedConn) RemoteAddr() net.Addr { } func (c *UnprivilegedConn) SetDeadline(t time.Time) error { - return os.ErrInvalid + return c.SetReadDeadline(t) } func (c *UnprivilegedConn) SetReadDeadline(t time.Time) error { - c.readDeadline.Store(t) + c.readDeadline.Set(t) return nil } func (c *UnprivilegedConn) SetWriteDeadline(t time.Time) error { - return os.ErrInvalid + return nil } From 9532c7f1f6f7cb27027702d02ce8c1f838d25b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 24 Aug 2025 18:43:22 +0800 Subject: [PATCH 049/121] ping: Update style for `socket_linux_unprivileged.go` --- ping/socket_linux_unprivileged.go | 65 ++++++++++++++----------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/ping/socket_linux_unprivileged.go b/ping/socket_linux_unprivileged.go index 78d10260..efaf11f6 100644 --- a/ping/socket_linux_unprivileged.go +++ b/ping/socket_linux_unprivileged.go @@ -18,14 +18,14 @@ import ( ) type UnprivilegedConn struct { - ctx context.Context - cancel context.CancelFunc - controlFunc control.Func - destination netip.Addr - receiveChan chan *unprivilegedResponse - readDeadline pipe.Deadline - natMap map[uint16]net.Conn - natMapMutex sync.Mutex + ctx context.Context + cancel context.CancelFunc + controlFunc control.Func + destination netip.Addr + receiveChan chan *unprivilegedResponse + readDeadline pipe.Deadline + mappingAccess sync.Mutex + mapping map[uint16]net.Conn } type unprivilegedResponse struct { @@ -48,7 +48,7 @@ func newUnprivilegedConn(ctx context.Context, controlFunc control.Func, destinat destination: destination, receiveChan: make(chan *unprivilegedResponse), readDeadline: pipe.MakeDeadline(), - natMap: make(map[uint16]net.Conn), + mapping: make(map[uint16]net.Conn), }, nil } @@ -59,10 +59,10 @@ func (c *UnprivilegedConn) Read(b []byte) (n int, err error) { packet.Buffer.Release() packet.Cmsg.Release() return - case <-c.ctx.Done(): - return 0, os.ErrClosed case <-c.readDeadline.Wait(): return 0, os.ErrDeadlineExceeded + case <-c.ctx.Done(): + return 0, os.ErrClosed } } @@ -75,10 +75,10 @@ func (c *UnprivilegedConn) ReadMsg(b []byte, oob []byte) (n, oobn int, addr neti packet.Buffer.Release() packet.Cmsg.Release() return - case <-c.ctx.Done(): - return 0, 0, netip.Addr{}, os.ErrClosed case <-c.readDeadline.Wait(): return 0, 0, netip.Addr{}, os.ErrDeadlineExceeded + case <-c.ctx.Done(): + return 0, 0, netip.Addr{}, os.ErrClosed } } @@ -92,26 +92,23 @@ func (c *UnprivilegedConn) Write(b []byte) (n int, err error) { identifier = icmpHdr.Ident() } - c.natMapMutex.Lock() - if err = c.ctx.Err(); err != nil { - c.natMapMutex.Unlock() - return 0, err + c.mappingAccess.Lock() + if c.ctx.Err() != nil { + return 0, c.ctx.Err() } - conn, ok := c.natMap[identifier] - if !ok { + conn, loaded := c.mapping[identifier] + if !loaded { conn, err = connect(false, c.controlFunc, c.destination) if err != nil { - c.natMapMutex.Unlock() - return 0, err + c.mappingAccess.Unlock() + return } go c.fetchResponse(conn.(*net.UDPConn), identifier) } - c.natMapMutex.Unlock() - + c.mappingAccess.Unlock() n, err = conn.Write(b) if err != nil { c.removeConn(conn.(*net.UDPConn), identifier) - return } return } @@ -154,22 +151,20 @@ func (c *UnprivilegedConn) fetchResponse(conn *net.UDPConn, identifier uint16) { } func (c *UnprivilegedConn) removeConn(conn *net.UDPConn, identifier uint16) { - c.natMapMutex.Lock() + c.mappingAccess.Lock() + defer c.mappingAccess.Unlock() _ = conn.Close() - if c.natMap[identifier] == conn { - delete(c.natMap, identifier) - } - c.natMapMutex.Unlock() + delete(c.mapping, identifier) } func (c *UnprivilegedConn) Close() error { - c.natMapMutex.Lock() + c.mappingAccess.Lock() + defer c.mappingAccess.Unlock() c.cancel() - for _, conn := range c.natMap { + for _, conn := range c.mapping { _ = conn.Close() } - common.ClearMap(c.natMap) - c.natMapMutex.Unlock() + common.ClearMap(c.mapping) return nil } @@ -182,7 +177,7 @@ func (c *UnprivilegedConn) RemoteAddr() net.Addr { } func (c *UnprivilegedConn) SetDeadline(t time.Time) error { - return c.SetReadDeadline(t) + return os.ErrInvalid } func (c *UnprivilegedConn) SetReadDeadline(t time.Time) error { @@ -191,5 +186,5 @@ func (c *UnprivilegedConn) SetReadDeadline(t time.Time) error { } func (c *UnprivilegedConn) SetWriteDeadline(t time.Time) error { - return nil + return os.ErrInvalid } From ff4941daa4bb1148d83c1957750df949b38d46e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 24 Aug 2025 18:59:55 +0800 Subject: [PATCH 050/121] Pass timeout to PrepareConnection --- route_direct.go | 7 ++++--- stack_gvisor_icmp.go | 6 ++++-- stack_gvisor_tcp.go | 2 +- stack_gvisor_udp.go | 2 +- stack_system.go | 8 +++++--- stack_system_nat.go | 2 +- tun.go | 9 ++++++++- 7 files changed, 24 insertions(+), 12 deletions(-) diff --git a/route_direct.go b/route_direct.go index 2279aa82..b043ab57 100644 --- a/route_direct.go +++ b/route_direct.go @@ -25,6 +25,7 @@ type DirectRouteSession struct { type DirectRouteMapping struct { mapping freelru.Cache[DirectRouteSession, DirectRouteDestination] + timeout time.Duration } func NewDirectRouteMapping(timeout time.Duration) *DirectRouteMapping { @@ -36,16 +37,16 @@ func NewDirectRouteMapping(timeout time.Duration) *DirectRouteMapping { action.Close() }) mapping.SetLifetime(timeout) - return &DirectRouteMapping{mapping} + return &DirectRouteMapping{mapping, timeout} } -func (m *DirectRouteMapping) Lookup(session DirectRouteSession, constructor func() (DirectRouteDestination, error)) (DirectRouteDestination, error) { +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() + created, err = constructor(m.timeout) return created, err == nil }) if !ok { diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index 3a134a95..c2e369b6 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -59,7 +59,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa 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() (DirectRouteDestination, error) { + action, err := f.mapping.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { return f.handler.PrepareConnection( N.NetworkICMPv4, M.SocksaddrFrom(sourceAddr, 0), @@ -70,6 +70,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa source: ipHdr.SourceAddress(), sourceNetwork: header.IPv4ProtocolNumber, }, + timeout, ) }) if err != nil { @@ -117,7 +118,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa 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() (DirectRouteDestination, error) { + action, err := f.mapping.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { return f.handler.PrepareConnection( N.NetworkICMPv6, M.SocksaddrFrom(sourceAddr, 0), @@ -128,6 +129,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa source: ipHdr.SourceAddress(), sourceNetwork: header.IPv6ProtocolNumber, }, + timeout, ) }) if err != nil { diff --git a/stack_gvisor_tcp.go b/stack_gvisor_tcp.go index 84bc3ff1..15927999 100644 --- a/stack_gvisor_tcp.go +++ b/stack_gvisor_tcp.go @@ -79,7 +79,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) - _, pErr := f.handler.PrepareConnection(N.NetworkTCP, source, destination, nil) + _, pErr := f.handler.PrepareConnection(N.NetworkTCP, source, destination, nil, 0) if pErr != nil { r.Complete(!errors.Is(pErr, ErrDrop)) return diff --git a/stack_gvisor_udp.go b/stack_gvisor_udp.go index db06b644..2e8ff3e7 100644 --- a/stack_gvisor_udp.go +++ b/stack_gvisor_udp.go @@ -58,7 +58,7 @@ 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) + _, pErr := f.handler.PrepareConnection(N.NetworkUDP, source, destination, nil, 0) if pErr != nil { if !errors.Is(pErr, ErrDrop) { gWriteUnreachable(f.stack, userData.(*stack.PacketBuffer)) diff --git a/stack_system.go b/stack_system.go index be8873ab..5797fa92 100644 --- a/stack_system.go +++ b/stack_system.go @@ -609,7 +609,7 @@ 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) + _, pErr := s.handler.PrepareConnection(N.NetworkUDP, source, destination, nil, 0) if pErr != nil { if !errors.Is(pErr, ErrDrop) { if source.IsIPv4() { @@ -658,12 +658,13 @@ func (s *System) processIPv4ICMP(ipHdr header.IPv4, icmpHdr header.ICMPv4) (bool sourceAddr := ipHdr.SourceAddr() destinationAddr := ipHdr.DestinationAddr() if destinationAddr != s.inet4Address { - action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { + action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { return s.handler.PrepareConnection( N.NetworkICMPv4, M.SocksaddrFrom(sourceAddr, 0), M.SocksaddrFrom(destinationAddr, 0), &systemICMPDirectPacketWriter4{s.tun, s.frontHeadroom + PacketOffset, sourceAddr}, + timeout, ) }) if err != nil { @@ -729,12 +730,13 @@ func (s *System) processIPv6ICMP(ipHdr header.IPv6, icmpHdr header.ICMPv6) (bool sourceAddr := ipHdr.SourceAddr() destinationAddr := ipHdr.DestinationAddr() if destinationAddr != s.inet6Address { - action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func() (DirectRouteDestination, error) { + action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { return s.handler.PrepareConnection( N.NetworkICMPv6, M.SocksaddrFrom(sourceAddr, 0), M.SocksaddrFrom(destinationAddr, 0), &systemICMPDirectPacketWriter6{s.tun, s.frontHeadroom + PacketOffset, sourceAddr}, + timeout, ) }) if err != nil { diff --git a/stack_system_nat.go b/stack_system_nat.go index 6b581bc0..6a6d6b97 100644 --- a/stack_system_nat.go +++ b/stack_system_nat.go @@ -78,7 +78,7 @@ func (n *TCPNat) Lookup(source netip.AddrPort, destination netip.AddrPort, handl if loaded { return port, nil } - _, pErr := handler.PrepareConnection(N.NetworkTCP, M.SocksaddrFromNetIP(source), M.SocksaddrFromNetIP(destination), nil) + _, pErr := handler.PrepareConnection(N.NetworkTCP, M.SocksaddrFromNetIP(source), M.SocksaddrFromNetIP(destination), nil, 0) if pErr != nil { return 0, pErr } diff --git a/tun.go b/tun.go index 09497f7f..d831742f 100644 --- a/tun.go +++ b/tun.go @@ -7,6 +7,7 @@ import ( "runtime" "strconv" "strings" + "time" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" @@ -18,7 +19,13 @@ import ( ) type Handler interface { - PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext DirectRouteContext) (DirectRouteDestination, error) + PrepareConnection( + network string, + source M.Socksaddr, + destination M.Socksaddr, + routeContext DirectRouteContext, + timeout time.Duration, + ) (DirectRouteDestination, error) N.TCPConnectionHandlerEx N.UDPConnectionHandlerEx } From dbd8e28fc8a61e115ce0e4c914cc75a19232bbdd Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sun, 24 Aug 2025 21:09:55 +0800 Subject: [PATCH 051/121] ping: fix healthCheck panic --- route_direct.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/route_direct.go b/route_direct.go index b043ab57..444eb5e0 100644 --- a/route_direct.go +++ b/route_direct.go @@ -30,11 +30,16 @@ type DirectRouteMapping struct { func NewDirectRouteMapping(timeout time.Duration) *DirectRouteMapping { mapping := common.Must1(freelru.NewSharded[DirectRouteSession, DirectRouteDestination](1024, maphash.NewHasher[DirectRouteSession]().Hash32)) - mapping.SetHealthCheck(func(session DirectRouteSession, destination DirectRouteDestination) bool { - return !destination.IsClosed() + mapping.SetHealthCheck(func(session DirectRouteSession, action DirectRouteDestination) bool { + if action != nil { + return !action.IsClosed() + } + return true }) mapping.SetOnEvict(func(session DirectRouteSession, action DirectRouteDestination) { - action.Close() + if action != nil { + action.Close() + } }) mapping.SetLifetime(timeout) return &DirectRouteMapping{mapping, timeout} From 0d3df84673ce28e9541bf2beaa0b11cec24b763e Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sun, 24 Aug 2025 23:05:48 +0800 Subject: [PATCH 052/121] ping: fix UnprivilegedConn --- ping/socket_linux_unprivileged.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ping/socket_linux_unprivileged.go b/ping/socket_linux_unprivileged.go index efaf11f6..4e17945b 100644 --- a/ping/socket_linux_unprivileged.go +++ b/ping/socket_linux_unprivileged.go @@ -104,6 +104,7 @@ func (c *UnprivilegedConn) Write(b []byte) (n int, err error) { return } go c.fetchResponse(conn.(*net.UDPConn), identifier) + c.mapping[identifier] = conn } c.mappingAccess.Unlock() n, err = conn.Write(b) From 58f331b49e07807bd3d53d29a9699164c7a29512 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Mon, 25 Aug 2025 00:00:02 +0800 Subject: [PATCH 053/121] ping: fix network --- ping/socket_unix.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ping/socket_unix.go b/ping/socket_unix.go index 1618dfc9..af072ae9 100644 --- a/ping/socket_unix.go +++ b/ping/socket_unix.go @@ -23,14 +23,14 @@ func connect(privileged bool, controlFunc control.Func, destination netip.Addr) err error ) if destination.Is4() { - network = "ip4:icmp" + network = "ip4" // like std's netFD.ctrlNetwork if !privileged { fd, err = unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_ICMP) } else { fd, err = unix.Socket(unix.AF_INET, unix.SOCK_RAW, unix.IPPROTO_ICMP) } } else { - network = "ip6:icmp" + network = "ip6" // like std's netFD.ctrlNetwork if !privileged { fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_ICMPV6) } else { From fe4e54bb0dc59802270ec47a35132506336e0a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 25 Aug 2025 00:16:56 +0800 Subject: [PATCH 054/121] ping: check invalid ip header --- ping/ping.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ping/ping.go b/ping/ping.go index 1ea0edfe..02aa6796 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -159,6 +159,9 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { } if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) + if !ipHdr.IsValid(buffer.Len()) { + return E.New("invalid IPv4 header received") + } ipHdr.SetDestinationAddr(c.source.Load()) ipHdr.SetChecksum(0) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) @@ -168,6 +171,9 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { c.logger.TraceContext(c.ctx, "read icmpv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) } else { ipHdr := header.IPv6(buffer.Bytes()) + if !ipHdr.IsValid(buffer.Len()) { + return E.New("invalid IPv6 header received") + } ipHdr.SetDestinationAddr(c.source.Load()) icmpHdr := header.ICMPv6(ipHdr.Payload()) icmpHdr.SetChecksum(0) From c089ffbd6c1cea98341142c5fff808a3f34f2c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 25 Aug 2025 01:19:52 +0800 Subject: [PATCH 055/121] ping: Fix read ipv4 header on darwin --- internal/gtcpip/header/ipv4.go | 21 +++++++++++++++++++++ ping/ping.go | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/internal/gtcpip/header/ipv4.go b/internal/gtcpip/header/ipv4.go index d76db68e..624166c4 100644 --- a/internal/gtcpip/header/ipv4.go +++ b/internal/gtcpip/header/ipv4.go @@ -315,6 +315,10 @@ func (b IPv4) Flags() uint8 { return uint8(binary.BigEndian.Uint16(b[flagsFO:]) >> 13) } +func (b IPv4) FlagsDarwinRaw() uint8 { + return uint8(binary.BigEndian.Uint16(b[flagsFO:]) >> 13) +} + // More returns whether the more fragments flag is set. func (b IPv4) More() bool { return b.Flags()&IPv4FlagMoreFragments != 0 @@ -330,11 +334,19 @@ func (b IPv4) FragmentOffset() uint16 { return binary.BigEndian.Uint16(b[flagsFO:]) << 3 } +func (b IPv4) FragmentOffsetDarwinRaw() uint16 { + return binary.NativeEndian.Uint16(b[flagsFO:]) << 3 +} + // TotalLength returns the "total length" field of the IPv4 header. func (b IPv4) TotalLength() uint16 { return binary.BigEndian.Uint16(b[IPv4TotalLenOffset:]) } +func (b IPv4) TotalLengthDarwinRaw() uint16 { + return binary.NativeEndian.Uint16(b[IPv4TotalLenOffset:]) + uint16(b.HeaderLength()) +} + // Checksum returns the checksum field of the IPv4 header. func (b IPv4) Checksum() uint16 { return binary.BigEndian.Uint16(b[xsum:]) @@ -428,6 +440,10 @@ func (b IPv4) SetTotalLength(totalLength uint16) { binary.BigEndian.PutUint16(b[IPv4TotalLenOffset:], totalLength) } +func (b IPv4) SetTotalLengthDarwinRaw(totalLength uint16) { + binary.NativeEndian.PutUint16(b[IPv4TotalLenOffset:], totalLength) +} + // SetChecksum sets the checksum field of the IPv4 header. func (b IPv4) SetChecksum(v uint16) { checksum.Put(b[xsum:], v) @@ -440,6 +456,11 @@ func (b IPv4) SetFlagsFragmentOffset(flags uint8, offset uint16) { binary.BigEndian.PutUint16(b[flagsFO:], v) } +func (b IPv4) SetFlagsFragmentOffsetDarwinRaw(flags uint8, offset uint16) { + v := (uint16(flags) << 13) | (offset >> 3) + binary.NativeEndian.PutUint16(b[flagsFO:], v) +} + // SetID sets the identification field. func (b IPv4) SetID(v uint16) { binary.BigEndian.PutUint16(b[id:], v) diff --git a/ping/ping.go b/ping/ping.go index 02aa6796..f3f661cb 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -159,6 +159,10 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { } if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + ipHdr.SetTotalLength(ipHdr.TotalLengthDarwinRaw()) + ipHdr.SetFlagsFragmentOffset(ipHdr.FlagsDarwinRaw(), ipHdr.FragmentOffsetDarwinRaw()) + } if !ipHdr.IsValid(buffer.Len()) { return E.New("invalid IPv4 header received") } From 06ddb3e0a7265a8a054b9b225a2d9fb994535cde Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Mon, 25 Aug 2025 10:18:22 +0800 Subject: [PATCH 056/121] ping: Add comments --- internal/gtcpip/header/ipv4.go | 8 ++++---- ping/cmsg_windows.go | 7 ++++--- ping/ping.go | 3 +++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/internal/gtcpip/header/ipv4.go b/internal/gtcpip/header/ipv4.go index 624166c4..82539363 100644 --- a/internal/gtcpip/header/ipv4.go +++ b/internal/gtcpip/header/ipv4.go @@ -335,7 +335,7 @@ func (b IPv4) FragmentOffset() uint16 { } func (b IPv4) FragmentOffsetDarwinRaw() uint16 { - return binary.NativeEndian.Uint16(b[flagsFO:]) << 3 + return common.NativeEndian.Uint16(b[flagsFO:]) << 3 } // TotalLength returns the "total length" field of the IPv4 header. @@ -344,7 +344,7 @@ func (b IPv4) TotalLength() uint16 { } func (b IPv4) TotalLengthDarwinRaw() uint16 { - return binary.NativeEndian.Uint16(b[IPv4TotalLenOffset:]) + uint16(b.HeaderLength()) + return common.NativeEndian.Uint16(b[IPv4TotalLenOffset:]) + uint16(b.HeaderLength()) } // Checksum returns the checksum field of the IPv4 header. @@ -441,7 +441,7 @@ func (b IPv4) SetTotalLength(totalLength uint16) { } func (b IPv4) SetTotalLengthDarwinRaw(totalLength uint16) { - binary.NativeEndian.PutUint16(b[IPv4TotalLenOffset:], totalLength) + common.NativeEndian.PutUint16(b[IPv4TotalLenOffset:], totalLength) } // SetChecksum sets the checksum field of the IPv4 header. @@ -458,7 +458,7 @@ func (b IPv4) SetFlagsFragmentOffset(flags uint8, offset uint16) { func (b IPv4) SetFlagsFragmentOffsetDarwinRaw(flags uint8, offset uint16) { v := (uint16(flags) << 13) | (offset >> 3) - binary.NativeEndian.PutUint16(b[flagsFO:], v) + common.NativeEndian.PutUint16(b[flagsFO:], v) } // SetID sets the identification field. diff --git a/ping/cmsg_windows.go b/ping/cmsg_windows.go index be5be9b9..07c322c8 100644 --- a/ping/cmsg_windows.go +++ b/ping/cmsg_windows.go @@ -1,10 +1,11 @@ package ping import ( - "encoding/binary" "fmt" "unsafe" + "github.com/sagernet/sing/common" + "golang.org/x/net/ipv6" "golang.org/x/sys/windows" ) @@ -36,9 +37,9 @@ func parseIPv6ControlMessage(cmsg []byte) (*ipv6.ControlMessage, error) { } switch cmsghdr.Type { case IPV6_TCLASS: - controlMessage.TrafficClass = int(binary.NativeEndian.Uint32(cmsg[alignedSizeofCmsghdr : alignedSizeofCmsghdr+4])) + controlMessage.TrafficClass = int(common.NativeEndian.Uint32(cmsg[alignedSizeofCmsghdr : alignedSizeofCmsghdr+4])) case IPV6_HOPLIMIT: - controlMessage.HopLimit = int(binary.NativeEndian.Uint32(cmsg[alignedSizeofCmsghdr : alignedSizeofCmsghdr+4])) + controlMessage.HopLimit = int(common.NativeEndian.Uint32(cmsg[alignedSizeofCmsghdr : alignedSizeofCmsghdr+4])) } cmsg = cmsg[msgSize:] } diff --git a/ping/ping.go b/ping/ping.go index f3f661cb..e8b6ef55 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -160,6 +160,9 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + // MacOS have different TotalLen and FragOff in ipv4 header from socket api: + // https://stackoverflow.com/questions/13829712/mac-changes-ip-total-length-field/15881825#15881825 + // but in the tun api still same data format as other system ipHdr.SetTotalLength(ipHdr.TotalLengthDarwinRaw()) ipHdr.SetFlagsFragmentOffset(ipHdr.FlagsDarwinRaw(), ipHdr.FragmentOffsetDarwinRaw()) } From ce050baa589a4ffc72e2f4df153ce445b94bbf73 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Mon, 25 Aug 2025 10:32:59 +0800 Subject: [PATCH 057/121] ping: Add bitwiseID --- ping/ping.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/ping/ping.go b/ping/ping.go index e8b6ef55..8f793c46 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -26,6 +26,7 @@ type Conn struct { ctx context.Context logger logger.ContextLogger privileged bool + bitwiseID bool conn net.Conn destination netip.Addr source atomic.TypedValue[netip.Addr] @@ -37,10 +38,15 @@ func Connect(ctx context.Context, logger logger.ContextLogger, privileged bool, if err != nil { return nil, err } + replaceID := true + if _, ok := conn.(*UnprivilegedConn); ok { + replaceID = false + } return &Conn{ ctx: ctx, logger: logger, privileged: privileged, + bitwiseID: replaceID, conn: conn, destination: destination, }, nil @@ -102,6 +108,12 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { } ttl = controlMessage.TTL } + if c.bitwiseID { + icmpHdr := header.ICMPv4(buffer.Bytes()) + icmpHdr.SetIdent(^icmpHdr.Ident()) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + } ipHdr := header.IPv4(buffer.ExtendHeader(header.IPv4MinimumSize)) ipHdr.Encode(&header.IPv4Fields{ // TOS: uint8(tos), @@ -135,6 +147,9 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { trafficClass = controlMessage.TrafficClass } icmpHdr := header.ICMPv6(buffer.Bytes()) + if c.bitwiseID { + icmpHdr.SetIdent(^icmpHdr.Ident()) + } icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmpHdr[:header.ICMPv6DstUnreachableMinimumSize], @@ -173,6 +188,9 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { ipHdr.SetChecksum(0) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) icmpHdr := header.ICMPv4(ipHdr.Payload()) + if c.bitwiseID { + icmpHdr.SetIdent(^icmpHdr.Ident()) + } icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) c.logger.TraceContext(c.ctx, "read icmpv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) @@ -183,6 +201,9 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { } ipHdr.SetDestinationAddr(c.source.Load()) icmpHdr := header.ICMPv6(ipHdr.Payload()) + if c.bitwiseID { + icmpHdr.SetIdent(^icmpHdr.Ident()) + } icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmpHdr, @@ -219,11 +240,27 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { defer buffer.Release() if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) + if c.bitwiseID { + icmpHdr := header.ICMPv4(ipHdr.Payload()) + icmpHdr.SetIdent(^icmpHdr.Ident()) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + } c.source.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) c.logger.TraceContext(c.ctx, "write icmpv4 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) return common.Error(c.conn.Write(ipHdr.Payload())) } else { ipHdr := header.IPv6(buffer.Bytes()) + if c.bitwiseID { + icmpHdr := header.ICMPv6(ipHdr.Payload()) + icmpHdr.SetIdent(^icmpHdr.Ident()) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: ipHdr.SourceAddressSlice(), + Dst: ipHdr.DestinationAddressSlice(), + })) + } c.source.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) c.logger.TraceContext(c.ctx, "write icmpv6 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) return common.Error(c.conn.Write(ipHdr.Payload())) From 548f51cc9d84ca2598950d6aeb3a65e988f60b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 25 Aug 2025 10:48:44 +0800 Subject: [PATCH 058/121] ping: Fix test --- ping/ping.go | 71 ++++++++++++++++++++++++++++++++--------------- ping/ping_test.go | 8 +++--- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/ping/ping.go b/ping/ping.go index 8f793c46..5794acb0 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -26,7 +26,6 @@ type Conn struct { ctx context.Context logger logger.ContextLogger privileged bool - bitwiseID bool conn net.Conn destination netip.Addr source atomic.TypedValue[netip.Addr] @@ -38,15 +37,10 @@ func Connect(ctx context.Context, logger logger.ContextLogger, privileged bool, if err != nil { return nil, err } - replaceID := true - if _, ok := conn.(*UnprivilegedConn); ok { - replaceID = false - } return &Conn{ ctx: ctx, logger: logger, privileged: privileged, - bitwiseID: replaceID, conn: conn, destination: destination, }, nil @@ -108,7 +102,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { } ttl = controlMessage.TTL } - if c.bitwiseID { + if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { icmpHdr := header.ICMPv4(buffer.Bytes()) icmpHdr.SetIdent(^icmpHdr.Ident()) icmpHdr.SetChecksum(0) @@ -147,7 +141,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { trafficClass = controlMessage.TrafficClass } icmpHdr := header.ICMPv6(buffer.Bytes()) - if c.bitwiseID { + if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { icmpHdr.SetIdent(^icmpHdr.Ident()) } icmpHdr.SetChecksum(0) @@ -188,7 +182,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { ipHdr.SetChecksum(0) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) icmpHdr := header.ICMPv4(ipHdr.Payload()) - if c.bitwiseID { + if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { icmpHdr.SetIdent(^icmpHdr.Ident()) } icmpHdr.SetChecksum(0) @@ -201,7 +195,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { } ipHdr.SetDestinationAddr(c.source.Load()) icmpHdr := header.ICMPv6(ipHdr.Payload()) - if c.bitwiseID { + if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { icmpHdr.SetIdent(^icmpHdr.Ident()) } icmpHdr.SetChecksum(0) @@ -221,17 +215,25 @@ func (c *Conn) ReadICMP(buffer *buf.Buffer) error { if err != nil { return err } - if c.destination.Is6() || (runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged { - return nil - } - if !c.destination.Is6() { - ipHdr := header.IPv4(buffer.Bytes()) - buffer.Advance(int(ipHdr.HeaderLength())) - c.logger.TraceContext(c.ctx, "read icmpv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) - } else { - ipHdr := header.IPv6(buffer.Bytes()) - buffer.Advance(buffer.Len() - int(ipHdr.PayloadLength())) - c.logger.TraceContext(c.ctx, "read icmpv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) + if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { + 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(0) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + } else { + icmpHdr := header.ICMPv6(buffer.Bytes()) + icmpHdr.SetIdent(^icmpHdr.Ident()) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: c.destination.AsSlice(), + Dst: c.source.Load().AsSlice(), + })) + } } return nil } @@ -240,7 +242,7 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { defer buffer.Release() if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) - if c.bitwiseID { + if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { icmpHdr := header.ICMPv4(ipHdr.Payload()) icmpHdr.SetIdent(^icmpHdr.Ident()) icmpHdr.SetChecksum(0) @@ -251,7 +253,7 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { return common.Error(c.conn.Write(ipHdr.Payload())) } else { ipHdr := header.IPv6(buffer.Bytes()) - if c.bitwiseID { + if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { icmpHdr := header.ICMPv6(ipHdr.Payload()) icmpHdr.SetIdent(^icmpHdr.Ident()) icmpHdr.SetChecksum(0) @@ -269,6 +271,29 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { func (c *Conn) WriteICMP(buffer *buf.Buffer) error { defer buffer.Release() + if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { + if !c.destination.Is6() { + icmpHdr := header.ICMPv4(buffer.Bytes()) + icmpHdr.SetIdent(^icmpHdr.Ident()) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + c.logger.TraceContext(c.ctx, "write icmpv4 echo request to ", c.destination) + } else { + icmpHdr := header.ICMPv6(buffer.Bytes()) + icmpHdr.SetIdent(^icmpHdr.Ident()) + icmpHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpHdr, + Src: c.source.Load().AsSlice(), + Dst: c.destination.AsSlice(), + })) + } + } + if !c.destination.Is6() { + c.logger.TraceContext(c.ctx, "write icmpv4 echo request to ", c.destination) + } else { + c.logger.TraceContext(c.ctx, "write icmpv6 echo request to ", c.destination) + } return common.Error(c.conn.Write(buffer.Bytes())) } diff --git a/ping/ping_test.go b/ping/ping_test.go index 015faa3b..3091a0a6 100644 --- a/ping/ping_test.go +++ b/ping/ping_test.go @@ -84,7 +84,7 @@ func testPingIPv4ReadIP(t *testing.T, privileged bool, addr string) { request.SetIdent(uint16(rand.Uint32())) request.SetChecksum(header.ICMPv4Checksum(request, 0)) - err = conn.WriteICMP(buf.As(request)) + err = conn.WriteICMP(buf.As(request).ToOwned()) require.NoError(t, err) conn.SetLocalAddr(netip.MustParseAddr("127.0.0.1")) @@ -117,7 +117,7 @@ func testPingIPv4ReadICMP(t *testing.T, privileged bool, addr string) { request.SetIdent(uint16(rand.Uint32())) request.SetChecksum(header.ICMPv4Checksum(request, 0)) - err = conn.WriteICMP(buf.As(request)) + err = conn.WriteICMP(buf.As(request).ToOwned()) require.NoError(t, err) require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second))) @@ -148,7 +148,7 @@ func testPingIPv6ReadIP(t *testing.T, privileged bool, addr string) { request.SetType(header.ICMPv6EchoRequest) request.SetIdent(uint16(rand.Uint32())) - err = conn.WriteICMP(buf.As(request)) + err = conn.WriteICMP(buf.As(request).ToOwned()) require.NoError(t, err) conn.SetLocalAddr(netip.MustParseAddr("::1")) @@ -180,7 +180,7 @@ func testPingIPv6ReadICMP(t *testing.T, privileged bool, addr string) { request.SetType(header.ICMPv6EchoRequest) request.SetIdent(uint16(rand.Uint32())) - err = conn.WriteICMP(buf.As(request)) + err = conn.WriteICMP(buf.As(request).ToOwned()) require.NoError(t, err) require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second))) From a0b34a4be9cd341b94a9cb4df7370e8e73d519ed Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Mon, 25 Aug 2025 11:07:23 +0800 Subject: [PATCH 059/121] ping: Code cleanup --- ping/ping.go | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/ping/ping.go b/ping/ping.go index 5794acb0..fbf2fbd5 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -33,29 +33,34 @@ type Conn struct { } func Connect(ctx context.Context, logger logger.ContextLogger, privileged bool, controlFunc control.Func, destination netip.Addr) (*Conn, error) { - conn, err := connect0(ctx, privileged, controlFunc, destination) - if err != nil { - return nil, err - } - return &Conn{ + c := &Conn{ ctx: ctx, logger: logger, privileged: privileged, - conn: conn, destination: destination, - }, nil + } + err := c.connect(controlFunc) + if err != nil { + return nil, err + } + return c, nil } -func connect0(ctx context.Context, privileged bool, controlFunc control.Func, destination netip.Addr) (net.Conn, error) { - if (runtime.GOOS == "linux" || runtime.GOOS == "android") && !privileged { - return newUnprivilegedConn(ctx, controlFunc, destination) +func (c *Conn) connect(controlFunc control.Func) (err error) { + if c.IsLinuxUnprivileged() { + c.conn, err = newUnprivilegedConn(c.ctx, controlFunc, c.destination) } else { - return connect(privileged, controlFunc, destination) + c.conn, err = connect(c.privileged, controlFunc, c.destination) } + return +} + +func (c *Conn) IsLinuxUnprivileged() bool { + return (runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged } func (c *Conn) ReadIP(buffer *buf.Buffer) error { - if c.destination.Is6() || (runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged { + if c.destination.Is6() || c.IsLinuxUnprivileged() { var readMsg func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) switch conn := c.conn.(type) { case *net.IPConn: @@ -102,7 +107,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { } ttl = controlMessage.TTL } - if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { + if !c.IsLinuxUnprivileged() { icmpHdr := header.ICMPv4(buffer.Bytes()) icmpHdr.SetIdent(^icmpHdr.Ident()) icmpHdr.SetChecksum(0) @@ -141,7 +146,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { trafficClass = controlMessage.TrafficClass } icmpHdr := header.ICMPv6(buffer.Bytes()) - if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { + if !c.IsLinuxUnprivileged() { icmpHdr.SetIdent(^icmpHdr.Ident()) } icmpHdr.SetChecksum(0) @@ -182,7 +187,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { ipHdr.SetChecksum(0) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) icmpHdr := header.ICMPv4(ipHdr.Payload()) - if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { + if !c.IsLinuxUnprivileged() { icmpHdr.SetIdent(^icmpHdr.Ident()) } icmpHdr.SetChecksum(0) @@ -195,7 +200,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { } ipHdr.SetDestinationAddr(c.source.Load()) icmpHdr := header.ICMPv6(ipHdr.Payload()) - if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { + if !c.IsLinuxUnprivileged() { icmpHdr.SetIdent(^icmpHdr.Ident()) } icmpHdr.SetChecksum(0) @@ -215,7 +220,7 @@ func (c *Conn) ReadICMP(buffer *buf.Buffer) error { if err != nil { return err } - if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { + if !c.IsLinuxUnprivileged() { if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) buffer.Advance(int(ipHdr.HeaderLength())) @@ -242,7 +247,7 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { defer buffer.Release() if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) - if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { + if !c.IsLinuxUnprivileged() { icmpHdr := header.ICMPv4(ipHdr.Payload()) icmpHdr.SetIdent(^icmpHdr.Ident()) icmpHdr.SetChecksum(0) @@ -253,7 +258,7 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { return common.Error(c.conn.Write(ipHdr.Payload())) } else { ipHdr := header.IPv6(buffer.Bytes()) - if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { + if !c.IsLinuxUnprivileged() { icmpHdr := header.ICMPv6(ipHdr.Payload()) icmpHdr.SetIdent(^icmpHdr.Ident()) icmpHdr.SetChecksum(0) @@ -271,7 +276,7 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { func (c *Conn) WriteICMP(buffer *buf.Buffer) error { defer buffer.Release() - if !((runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged) { + if !c.IsLinuxUnprivileged() { if !c.destination.Is6() { icmpHdr := header.ICMPv4(buffer.Bytes()) icmpHdr.SetIdent(^icmpHdr.Ident()) From 854e40dc407bf02b55a35082eec2ac0284d3a6b1 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Mon, 25 Aug 2025 11:51:22 +0800 Subject: [PATCH 060/121] ping: Fix icmp checksum --- ping/ping.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ping/ping.go b/ping/ping.go index fbf2fbd5..1adbd91c 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -151,7 +151,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { } icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr[:header.ICMPv6DstUnreachableMinimumSize], + Header: icmpHdr, Src: addr.AsSlice(), Dst: c.source.Load().AsSlice(), })) From d0ff7b6f6c43157a93c97347b1059414395d42c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 25 Aug 2025 19:59:21 +0800 Subject: [PATCH 061/121] Replace usages of common/atomic --- go.mod | 2 +- go.sum | 4 ++-- monitor_shared.go | 2 +- ping/ping.go | 4 ++-- redirect_server.go | 2 +- tun_windows.go | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 7d497394..672eb6ef 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/sagernet/nftables v0.3.0-beta.4 - github.com/sagernet/sing v0.7.6-0.20250823024003-88f1880f43af + github.com/sagernet/sing v0.7.6-0.20250825115037-1dbcbdf691a5 github.com/stretchr/testify v1.9.0 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 diff --git a/go.sum b/go.sum index fceb588f..8f45d8cd 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= -github.com/sagernet/sing v0.7.6-0.20250823024003-88f1880f43af h1:/1H30c/+j7Q9BBPuJuX6eHyzKpbGWrr7S/4DcdtNIfw= -github.com/sagernet/sing v0.7.6-0.20250823024003-88f1880f43af/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.7.6-0.20250825115037-1dbcbdf691a5 h1:ozQMu9iZGuSNdNaoBTy2E9ukYsE0uY4p9U0jA0CqrsM= +github.com/sagernet/sing v0.7.6-0.20250825115037-1dbcbdf691a5/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= diff --git a/monitor_shared.go b/monitor_shared.go index 12e3e21b..3595d856 100644 --- a/monitor_shared.go +++ b/monitor_shared.go @@ -5,9 +5,9 @@ package tun import ( "errors" "sync" + "sync/atomic" "time" - "github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/x/list" diff --git a/ping/ping.go b/ping/ping.go index 1adbd91c..4c2d98e6 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -6,12 +6,12 @@ import ( "net/netip" "reflect" "runtime" + "sync/atomic" "time" "github.com/sagernet/sing-tun/internal/gtcpip/checksum" "github.com/sagernet/sing-tun/internal/gtcpip/header" "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" @@ -28,7 +28,7 @@ type Conn struct { privileged bool conn net.Conn destination netip.Addr - source atomic.TypedValue[netip.Addr] + source common.TypedValue[netip.Addr] closed atomic.Bool } diff --git a/redirect_server.go b/redirect_server.go index 86abfd8c..7590b35d 100644 --- a/redirect_server.go +++ b/redirect_server.go @@ -7,9 +7,9 @@ import ( "errors" "net" "net/netip" + "sync/atomic" "time" - "github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" diff --git a/tun_windows.go b/tun_windows.go index 66fb13d4..e62789ca 100644 --- a/tun_windows.go +++ b/tun_windows.go @@ -9,6 +9,7 @@ import ( "net/netip" "os" "sync" + "sync/atomic" "time" "unsafe" @@ -16,7 +17,6 @@ import ( "github.com/sagernet/sing-tun/internal/winsys" "github.com/sagernet/sing-tun/internal/wintun" "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/atomic" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/windnsapi" From 144683d882e251ac43c7644f4ff2a0f995c5c22e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 25 Aug 2025 20:51:03 +0800 Subject: [PATCH 062/121] ping: Add filter to destination --- ping/destination.go | 112 ++++++++++++++++++++++++++++++++++++++++---- ping/ping.go | 17 +------ ping/ping_test.go | 10 ++-- 3 files changed, 108 insertions(+), 31 deletions(-) diff --git a/ping/destination.go b/ping/destination.go index dc20112d..cf197a00 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -6,9 +6,11 @@ import ( "net/netip" "os" "runtime" + "sync" "time" "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/internal/gtcpip/header" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" @@ -18,18 +20,28 @@ import ( var _ tun.DirectRouteDestination = (*Destination)(nil) type Destination struct { - conn *Conn - ctx context.Context - logger logger.ContextLogger - routeContext tun.DirectRouteContext - timeout time.Duration + conn *Conn + ctx context.Context + logger logger.ContextLogger + destination netip.Addr + routeContext tun.DirectRouteContext + timeout time.Duration + requestAccess sync.Mutex + requests map[pingRequest]bool +} + +type pingRequest struct { + Source netip.Addr + Destination netip.Addr + Identifier uint16 + Sequence uint16 } func ConnectDestination( ctx context.Context, logger logger.ContextLogger, controlFunc control.Func, - address netip.Addr, + destination netip.Addr, routeContext tun.DirectRouteContext, timeout time.Duration, ) (tun.DirectRouteDestination, error) { @@ -39,11 +51,11 @@ func ConnectDestination( ) switch runtime.GOOS { case "darwin", "ios", "windows": - conn, err = Connect(ctx, logger, false, controlFunc, address) + conn, err = Connect(ctx, false, controlFunc, destination) default: - conn, err = Connect(ctx, logger, true, controlFunc, address) + conn, err = Connect(ctx, true, controlFunc, destination) if errors.Is(err, os.ErrPermission) { - conn, err = Connect(ctx, logger, false, controlFunc, address) + conn, err = Connect(ctx, false, controlFunc, destination) } } if err != nil { @@ -53,8 +65,10 @@ func ConnectDestination( conn: conn, ctx: ctx, logger: logger, + destination: destination, routeContext: routeContext, timeout: timeout, + requests: make(map[pingRequest]bool), } go d.loopRead() return d, nil @@ -76,6 +90,59 @@ func (d *Destination) loopRead() { } return } + if !d.destination.Is6() { + ipHdr := header.IPv4(buffer.Bytes()) + if !ipHdr.IsValid(buffer.Len()) { + d.logger.ErrorContext(d.ctx, E.New("invalid IPv4 header received")) + continue + } + if ipHdr.PayloadLength() < header.ICMPv4MinimumSize { + d.logger.ErrorContext(d.ctx, E.New("invalid ICMPv4 header received")) + continue + } + icmpHdr := header.ICMPv4(ipHdr.Payload()) + 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() + if d.requests[request] { + requestExists = true + delete(d.requests, request) + } + d.requestAccess.Unlock() + if !requestExists { + continue + } + 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()) { + d.logger.ErrorContext(d.ctx, E.New("invalid IPv6 header received")) + continue + } + if ipHdr.PayloadLength() < header.ICMPv6MinimumSize { + d.logger.ErrorContext(d.ctx, E.New("invalid ICMPv6 header received")) + continue + } + icmpHdr := header.ICMPv6(ipHdr.Payload()) + if icmpHdr.Type() != header.ICMPv6EchoReply { + continue + } + var requestExists bool + request := pingRequest{Source: ipHdr.DestinationAddr(), Destination: ipHdr.SourceAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()} + d.requestAccess.Lock() + if d.requests[request] { + requestExists = true + delete(d.requests, request) + } + d.requestAccess.Unlock() + if !requestExists { + continue + } + 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()) if err != nil { d.logger.ErrorContext(d.ctx, E.Cause(err, "write ICMP echo reply")) @@ -85,6 +152,33 @@ func (d *Destination) loopRead() { } func (d *Destination) WritePacket(packet *buf.Buffer) error { + if !d.destination.Is6() { + ipHdr := header.IPv4(packet.Bytes()) + if !ipHdr.IsValid(packet.Len()) { + return E.New("invalid IPv4 header") + } + if ipHdr.PayloadLength() < header.ICMPv4MinimumSize { + return E.New("invalid ICMPv4 header") + } + icmpHdr := header.ICMPv4(ipHdr.Payload()) + d.requestAccess.Lock() + d.requests[pingRequest{Source: ipHdr.SourceAddr(), Destination: ipHdr.DestinationAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()}] = true + d.requestAccess.Unlock() + d.logger.TraceContext(d.ctx, "write ICMPv4 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) + } else { + ipHdr := header.IPv6(packet.Bytes()) + if !ipHdr.IsValid(packet.Len()) { + return E.New("invalid IPv6 header") + } + if ipHdr.PayloadLength() < header.ICMPv6MinimumSize { + return E.New("invalid ICMPv6 header") + } + icmpHdr := header.ICMPv6(ipHdr.Payload()) + d.requestAccess.Lock() + d.requests[pingRequest{Source: ipHdr.SourceAddr(), Destination: ipHdr.DestinationAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()}] = true + d.requestAccess.Unlock() + d.logger.TraceContext(d.ctx, "write ICMPv6 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) + } return d.conn.WriteIP(packet) } diff --git a/ping/ping.go b/ping/ping.go index 4c2d98e6..d6518521 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -15,7 +15,6 @@ import ( "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" M "github.com/sagernet/sing/common/metadata" "golang.org/x/net/ipv4" @@ -24,7 +23,6 @@ import ( type Conn struct { ctx context.Context - logger logger.ContextLogger privileged bool conn net.Conn destination netip.Addr @@ -32,10 +30,9 @@ type Conn struct { closed atomic.Bool } -func Connect(ctx context.Context, logger logger.ContextLogger, privileged bool, controlFunc control.Func, destination netip.Addr) (*Conn, error) { +func Connect(ctx context.Context, privileged bool, controlFunc control.Func, destination netip.Addr) (*Conn, error) { c := &Conn{ ctx: ctx, - logger: logger, privileged: privileged, destination: destination, } @@ -123,7 +120,6 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { TotalLength: uint16(buffer.Len()), }) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) - c.logger.TraceContext(c.ctx, "read icmpv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) } else { oob := make([]byte, 1024) buffer.Advance(header.IPv6MinimumSize) @@ -164,7 +160,6 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { SrcAddr: addr, DstAddr: c.source.Load(), }) - c.logger.TraceContext(c.ctx, "read icmpv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) } } else { _, err := buffer.ReadOnceFrom(c.conn) @@ -192,7 +187,6 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { } icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) - c.logger.TraceContext(c.ctx, "read icmpv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) } else { ipHdr := header.IPv6(buffer.Bytes()) if !ipHdr.IsValid(buffer.Len()) { @@ -209,7 +203,6 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { Src: ipHdr.SourceAddressSlice(), Dst: ipHdr.DestinationAddressSlice(), })) - c.logger.TraceContext(c.ctx, "read icmpv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) } } return nil @@ -254,7 +247,6 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) } c.source.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) - c.logger.TraceContext(c.ctx, "write icmpv4 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) return common.Error(c.conn.Write(ipHdr.Payload())) } else { ipHdr := header.IPv6(buffer.Bytes()) @@ -269,7 +261,6 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { })) } c.source.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) - c.logger.TraceContext(c.ctx, "write icmpv6 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr()) return common.Error(c.conn.Write(ipHdr.Payload())) } } @@ -282,7 +273,6 @@ func (c *Conn) WriteICMP(buffer *buf.Buffer) error { icmpHdr.SetIdent(^icmpHdr.Ident()) icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) - c.logger.TraceContext(c.ctx, "write icmpv4 echo request to ", c.destination) } else { icmpHdr := header.ICMPv6(buffer.Bytes()) icmpHdr.SetIdent(^icmpHdr.Ident()) @@ -294,11 +284,6 @@ func (c *Conn) WriteICMP(buffer *buf.Buffer) error { })) } } - if !c.destination.Is6() { - c.logger.TraceContext(c.ctx, "write icmpv4 echo request to ", c.destination) - } else { - c.logger.TraceContext(c.ctx, "write icmpv6 echo request to ", c.destination) - } return common.Error(c.conn.Write(buffer.Bytes())) } diff --git a/ping/ping_test.go b/ping/ping_test.go index 3091a0a6..73a56b26 100644 --- a/ping/ping_test.go +++ b/ping/ping_test.go @@ -12,8 +12,6 @@ import ( "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" ) @@ -73,7 +71,7 @@ func TestPing(t *testing.T) { } func testPingIPv4ReadIP(t *testing.T, privileged bool, addr string) { - conn, err := ping.Connect(context.Background(), logger.NOP(), privileged, nil, netip.MustParseAddr(addr)) + conn, err := ping.Connect(context.Background(), privileged, nil, netip.MustParseAddr(addr)) if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { t.SkipNow() } @@ -106,7 +104,7 @@ func testPingIPv4ReadIP(t *testing.T, privileged bool, addr string) { } func testPingIPv4ReadICMP(t *testing.T, privileged bool, addr string) { - conn, err := ping.Connect(context.Background(), logger.NOP(), privileged, nil, netip.MustParseAddr(addr)) + conn, err := ping.Connect(context.Background(), privileged, nil, netip.MustParseAddr(addr)) if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { t.SkipNow() } @@ -138,7 +136,7 @@ func testPingIPv4ReadICMP(t *testing.T, privileged bool, addr string) { } func testPingIPv6ReadIP(t *testing.T, privileged bool, addr string) { - conn, err := ping.Connect(context.Background(), logger.NOP(), privileged, nil, netip.MustParseAddr(addr)) + conn, err := ping.Connect(context.Background(), privileged, nil, netip.MustParseAddr(addr)) if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { t.SkipNow() } @@ -170,7 +168,7 @@ func testPingIPv6ReadIP(t *testing.T, privileged bool, addr string) { } func testPingIPv6ReadICMP(t *testing.T, privileged bool, addr string) { - conn, err := ping.Connect(context.Background(), logger.NOP(), privileged, nil, netip.MustParseAddr(addr)) + conn, err := ping.Connect(context.Background(), privileged, nil, netip.MustParseAddr(addr)) if runtime.GOOS == "linux" && err != nil && err.Error() == "socket(): permission denied" { t.SkipNow() } From ce559298839f170767f7f16088a9f4ccdc9652b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 25 Aug 2025 21:28:11 +0800 Subject: [PATCH 063/121] ping: Fix missing bind for unix socket --- ping/socket_unix.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ping/socket_unix.go b/ping/socket_unix.go index af072ae9..3d3c59be 100644 --- a/ping/socket_unix.go +++ b/ping/socket_unix.go @@ -12,7 +12,6 @@ import ( "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" - "golang.org/x/sys/unix" ) @@ -74,6 +73,17 @@ func connect(privileged bool, controlFunc control.Func, destination netip.Addr) } } + var bindAddress netip.Addr + if !destination.Is6() { + bindAddress = netip.AddrFrom4([4]byte{0, 0, 0, 0}) + } else { + bindAddress = netip.AddrFrom16([16]byte{}) + } + err = unix.Bind(fd, M.AddrPortToSockaddr(netip.AddrPortFrom(bindAddress, 0))) + if err != nil { + return nil, err + } + err = unix.Connect(fd, M.AddrPortToSockaddr(netip.AddrPortFrom(destination, 0))) if err != nil { return nil, E.Cause(err, "connect()") From 59e42c0d1faaa1ee53578b6a3667dc104c723cd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 25 Aug 2025 22:04:16 +0800 Subject: [PATCH 064/121] ping: Fix rewriter --- ping/destination_gvisor.go | 2 +- ping/rewriter.go | 52 +++++++++++++++++++++++--------------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/ping/destination_gvisor.go b/ping/destination_gvisor.go index a026f4af..2a2d0e5c 100644 --- a/ping/destination_gvisor.go +++ b/ping/destination_gvisor.go @@ -76,7 +76,7 @@ func ConnectGVisor( return nil, gonet.TranslateNetstackError(gErr) } endpoint.SocketOptions().SetHeaderIncluded(true) - rewriter := NewRewriter(bindAddress4, bindAddress6) + rewriter := NewRewriter(ctx, logger, bindAddress4, bindAddress6) rewriter.CreateSession(tun.DirectRouteSession{Source: sourceAddress, Destination: destinationAddress}, routeContext) destination := &GVisorDestination{ ctx: ctx, diff --git a/ping/rewriter.go b/ping/rewriter.go index a842df1b..52666b74 100644 --- a/ping/rewriter.go +++ b/ping/rewriter.go @@ -1,27 +1,33 @@ package ping import ( + "context" "net/netip" "sync" "github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun/internal/gtcpip/header" + "github.com/sagernet/sing/common/logger" ) type Rewriter struct { - access sync.RWMutex - sessions map[tun.DirectRouteSession]tun.DirectRouteContext - source4Address map[uint16]netip.Addr - source6Address map[uint16]netip.Addr - inet4Address netip.Addr - inet6Address netip.Addr + 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 NewRewriter(inet4Address netip.Addr, inet6Address netip.Addr) *Rewriter { +func NewRewriter(ctx context.Context, logger logger.ContextLogger, inet4Address netip.Addr, inet6Address netip.Addr) *Rewriter { return &Rewriter{ - sessions: make(map[tun.DirectRouteSession]tun.DirectRouteContext), - inet4Address: inet4Address, - inet6Address: inet6Address, + ctx: ctx, + logger: logger, + sessions: make(map[tun.DirectRouteSession]tun.DirectRouteContext), + sourceAddress: make(map[uint16]netip.Addr), + inet4Address: inet4Address, + inet6Address: inet6Address, } } @@ -60,8 +66,9 @@ func (m *Rewriter) RewritePacket(packet []byte) { case header.ICMPv4ProtocolNumber: icmpHdr := header.ICMPv4(ipHdr.Payload()) m.access.Lock() - m.source4Address[icmpHdr.Ident()] = sourceAddr - 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(0) @@ -71,8 +78,9 @@ func (m *Rewriter) RewritePacket(packet []byte) { Dst: ipHdr.DestinationAddressSlice(), })) m.access.Lock() - m.source6Address[icmpHdr.Ident()] = sourceAddr - 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()) } } @@ -94,25 +102,25 @@ func (m *Rewriter) WriteBack(packet []byte) (bool, error) { icmpHdr := header.ICMPv4(ipHdr.Payload()) m.access.Lock() ident := icmpHdr.Ident() - source, loaded := m.source4Address[ident] + source, loaded := m.sourceAddress[ident] if !loaded { m.access.Unlock() return false, nil } - delete(m.source4Address, icmpHdr.Ident()) - m.access.Lock() + 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.source6Address[ident] + source, loaded := m.sourceAddress[ident] if !loaded { m.access.Unlock() return false, nil } - delete(m.source6Address, icmpHdr.Ident()) - m.access.Lock() + delete(m.sourceAddress, icmpHdr.Ident()) + m.access.Unlock() routeSession.Source = source default: return false, nil @@ -129,6 +137,9 @@ func (m *Rewriter) WriteBack(packet []byte) (bool, error) { 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(0) @@ -137,6 +148,7 @@ func (m *Rewriter) WriteBack(packet []byte) (bool, error) { 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) } From e6c64e3f1823c92fabd0327879c7d08621e5936b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 25 Aug 2025 22:19:09 +0800 Subject: [PATCH 065/121] Update icmp network name --- go.mod | 2 +- go.sum | 4 ++-- network_name.go | 42 ------------------------------------------ ping/ping_test.go | 1 + ping/socket_unix.go | 1 + stack_gvisor_icmp.go | 4 ++-- stack_system.go | 4 ++-- 7 files changed, 9 insertions(+), 49 deletions(-) delete mode 100644 network_name.go diff --git a/go.mod b/go.mod index 672eb6ef..452ddac3 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/sagernet/nftables v0.3.0-beta.4 - github.com/sagernet/sing v0.7.6-0.20250825115037-1dbcbdf691a5 + github.com/sagernet/sing v0.7.6-0.20250825141840-811aa328e57b github.com/stretchr/testify v1.9.0 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 diff --git a/go.sum b/go.sum index 8f45d8cd..4b6325ce 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= -github.com/sagernet/sing v0.7.6-0.20250825115037-1dbcbdf691a5 h1:ozQMu9iZGuSNdNaoBTy2E9ukYsE0uY4p9U0jA0CqrsM= -github.com/sagernet/sing v0.7.6-0.20250825115037-1dbcbdf691a5/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.7.6-0.20250825141840-811aa328e57b h1:RCfo1Q6VDAXfumNupRyqTomKzDODhASswkxVCqM8l2M= +github.com/sagernet/sing v0.7.6-0.20250825141840-811aa328e57b/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= diff --git a/network_name.go b/network_name.go deleted file mode 100644 index fa487fb6..00000000 --- a/network_name.go +++ /dev/null @@ -1,42 +0,0 @@ -package tun - -import ( - "strconv" - - "github.com/sagernet/sing-tun/internal/gtcpip" - "github.com/sagernet/sing-tun/internal/gtcpip/header" - F "github.com/sagernet/sing/common/format" - N "github.com/sagernet/sing/common/network" -) - -func NetworkName(network uint8) string { - switch tcpip.TransportProtocolNumber(network) { - case header.TCPProtocolNumber: - return N.NetworkTCP - case header.UDPProtocolNumber: - return N.NetworkUDP - case header.ICMPv4ProtocolNumber: - return N.NetworkICMPv4 - case header.ICMPv6ProtocolNumber: - return N.NetworkICMPv6 - } - return F.ToString(network) -} - -func NetworkFromName(name string) uint8 { - switch name { - case N.NetworkTCP: - return uint8(header.TCPProtocolNumber) - case N.NetworkUDP: - return uint8(header.UDPProtocolNumber) - case N.NetworkICMPv4: - return uint8(header.ICMPv4ProtocolNumber) - case N.NetworkICMPv6: - return uint8(header.ICMPv6ProtocolNumber) - } - parseNetwork, err := strconv.ParseUint(name, 10, 8) - if err != nil { - return 0 - } - return uint8(parseNetwork) -} diff --git a/ping/ping_test.go b/ping/ping_test.go index 73a56b26..7ec291a3 100644 --- a/ping/ping_test.go +++ b/ping/ping_test.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing-tun/internal/gtcpip/header" "github.com/sagernet/sing-tun/ping" "github.com/sagernet/sing/common/buf" + "github.com/stretchr/testify/require" ) diff --git a/ping/socket_unix.go b/ping/socket_unix.go index 3d3c59be..3e9bc99d 100644 --- a/ping/socket_unix.go +++ b/ping/socket_unix.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" + "golang.org/x/sys/unix" ) diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index c2e369b6..cb464055 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -61,7 +61,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa 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.NetworkICMPv4, + N.NetworkICMP, M.SocksaddrFrom(sourceAddr, 0), M.SocksaddrFrom(destinationAddr, 0), &ICMPBackWriter{ @@ -120,7 +120,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa 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.NetworkICMPv6, + N.NetworkICMP, M.SocksaddrFrom(sourceAddr, 0), M.SocksaddrFrom(destinationAddr, 0), &ICMPBackWriter{ diff --git a/stack_system.go b/stack_system.go index 5797fa92..d09e7541 100644 --- a/stack_system.go +++ b/stack_system.go @@ -660,7 +660,7 @@ func (s *System) processIPv4ICMP(ipHdr header.IPv4, icmpHdr header.ICMPv4) (bool 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.NetworkICMPv4, + N.NetworkICMP, M.SocksaddrFrom(sourceAddr, 0), M.SocksaddrFrom(destinationAddr, 0), &systemICMPDirectPacketWriter4{s.tun, s.frontHeadroom + PacketOffset, sourceAddr}, @@ -732,7 +732,7 @@ func (s *System) processIPv6ICMP(ipHdr header.IPv6, icmpHdr header.ICMPv6) (bool 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.NetworkICMPv6, + N.NetworkICMP, M.SocksaddrFrom(sourceAddr, 0), M.SocksaddrFrom(destinationAddr, 0), &systemICMPDirectPacketWriter6{s.tun, s.frontHeadroom + PacketOffset, sourceAddr}, From 79e2d3b56d01d8878fb5e0aa4af7443fec7acd8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 26 Aug 2025 11:09:50 +0800 Subject: [PATCH 066/121] ping: Clean old requests --- ping/destination.go | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/ping/destination.go b/ping/destination.go index cf197a00..51511c61 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -27,7 +27,7 @@ type Destination struct { routeContext tun.DirectRouteContext timeout time.Duration requestAccess sync.Mutex - requests map[pingRequest]bool + requests map[pingRequest]time.Time } type pingRequest struct { @@ -68,7 +68,7 @@ func ConnectDestination( destination: destination, routeContext: routeContext, timeout: timeout, - requests: make(map[pingRequest]bool), + requests: make(map[pingRequest]time.Time), } go d.loopRead() return d, nil @@ -107,7 +107,8 @@ func (d *Destination) loopRead() { var requestExists bool request := pingRequest{Source: ipHdr.DestinationAddr(), Destination: ipHdr.SourceAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()} d.requestAccess.Lock() - if d.requests[request] { + _, loaded := d.requests[request] + if loaded { requestExists = true delete(d.requests, request) } @@ -133,7 +134,8 @@ func (d *Destination) loopRead() { var requestExists bool request := pingRequest{Source: ipHdr.DestinationAddr(), Destination: ipHdr.SourceAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()} d.requestAccess.Lock() - if d.requests[request] { + _, loaded := d.requests[request] + if loaded { requestExists = true delete(d.requests, request) } @@ -161,9 +163,7 @@ func (d *Destination) WritePacket(packet *buf.Buffer) error { return E.New("invalid ICMPv4 header") } icmpHdr := header.ICMPv4(ipHdr.Payload()) - d.requestAccess.Lock() - d.requests[pingRequest{Source: ipHdr.SourceAddr(), Destination: ipHdr.DestinationAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()}] = true - d.requestAccess.Unlock() + d.registerRequest(pingRequest{Source: ipHdr.SourceAddr(), Destination: ipHdr.DestinationAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()}) d.logger.TraceContext(d.ctx, "write ICMPv4 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } else { ipHdr := header.IPv6(packet.Bytes()) @@ -174,14 +174,24 @@ func (d *Destination) WritePacket(packet *buf.Buffer) error { return E.New("invalid ICMPv6 header") } icmpHdr := header.ICMPv6(ipHdr.Payload()) - d.requestAccess.Lock() - d.requests[pingRequest{Source: ipHdr.SourceAddr(), Destination: ipHdr.DestinationAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()}] = true - d.requestAccess.Unlock() + d.registerRequest(pingRequest{Source: ipHdr.SourceAddr(), Destination: ipHdr.DestinationAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()}) d.logger.TraceContext(d.ctx, "write ICMPv6 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } return d.conn.WriteIP(packet) } +func (d *Destination) registerRequest(request pingRequest) { + d.requestAccess.Lock() + defer d.requestAccess.Unlock() + now := time.Now() + for oldRequest, createdAt := range d.requests { + if now.Sub(createdAt) > d.timeout { + delete(d.requests, oldRequest) + } + } + d.requests[request] = time.Now() +} + func (d *Destination) Close() error { return d.conn.Close() } From 6e4e04562011e43164990a04cdbf47839bcc2b91 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 26 Aug 2025 11:38:38 +0800 Subject: [PATCH 067/121] ping: Limit old requests --- ping/destination.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ping/destination.go b/ping/destination.go index 51511c61..00966af7 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -181,15 +181,26 @@ func (d *Destination) WritePacket(packet *buf.Buffer) error { } func (d *Destination) registerRequest(request pingRequest) { + const requestsLimit = 1024 d.requestAccess.Lock() defer d.requestAccess.Unlock() now := time.Now() + var ( + oldestRequest pingRequest + oldestCreateAt = now + ) for oldRequest, createdAt := range d.requests { if now.Sub(createdAt) > d.timeout { delete(d.requests, oldRequest) + } else if createdAt.Before(oldestCreateAt) { + oldestRequest = oldRequest + oldestCreateAt = createdAt } } - d.requests[request] = time.Now() + if len(d.requests) > requestsLimit { + delete(d.requests, oldestRequest) + } + d.requests[request] = now } func (d *Destination) Close() error { From 4fb5702443a490759f9bd5b9fbbd9e4f114068bc Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 26 Aug 2025 13:20:50 +0800 Subject: [PATCH 068/121] ping: Add needFilter --- ping/destination.go | 72 ++++++++++++++++++++++++++------------------- ping/ping.go | 22 +++++++------- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/ping/destination.go b/ping/destination.go index 00966af7..23db6745 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -101,20 +101,22 @@ func (d *Destination) loopRead() { continue } icmpHdr := header.ICMPv4(ipHdr.Payload()) - 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 { - continue + 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 { + continue + } } d.logger.TraceContext(d.ctx, "read ICMPv4 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } else { @@ -128,20 +130,22 @@ func (d *Destination) loopRead() { continue } icmpHdr := header.ICMPv6(ipHdr.Payload()) - if icmpHdr.Type() != header.ICMPv6EchoReply { - 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 { - continue + if d.needFilter() { + if icmpHdr.Type() != header.ICMPv6EchoReply { + 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 { + continue + } } d.logger.TraceContext(d.ctx, "read ICMPv6 echo reply from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } @@ -163,7 +167,9 @@ func (d *Destination) WritePacket(packet *buf.Buffer) error { return E.New("invalid ICMPv4 header") } icmpHdr := header.ICMPv4(ipHdr.Payload()) - d.registerRequest(pingRequest{Source: ipHdr.SourceAddr(), Destination: ipHdr.DestinationAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()}) + if d.needFilter() { + d.registerRequest(pingRequest{Source: ipHdr.SourceAddr(), Destination: ipHdr.DestinationAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()}) + } d.logger.TraceContext(d.ctx, "write ICMPv4 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } else { ipHdr := header.IPv6(packet.Bytes()) @@ -174,12 +180,18 @@ func (d *Destination) WritePacket(packet *buf.Buffer) error { return E.New("invalid ICMPv6 header") } icmpHdr := header.ICMPv6(ipHdr.Payload()) - d.registerRequest(pingRequest{Source: ipHdr.SourceAddr(), Destination: ipHdr.DestinationAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()}) + if d.needFilter() { + d.registerRequest(pingRequest{Source: ipHdr.SourceAddr(), Destination: ipHdr.DestinationAddr(), Identifier: icmpHdr.Ident(), Sequence: icmpHdr.Sequence()}) + } d.logger.TraceContext(d.ctx, "write ICMPv6 echo request from ", ipHdr.SourceAddr(), " to ", ipHdr.DestinationAddr(), " id ", icmpHdr.Ident(), " seq ", icmpHdr.Sequence()) } return d.conn.WriteIP(packet) } +func (d *Destination) needFilter() bool { + return runtime.GOOS != "windows" && !d.conn.isLinuxUnprivileged() +} + func (d *Destination) registerRequest(request pingRequest) { const requestsLimit = 1024 d.requestAccess.Lock() diff --git a/ping/ping.go b/ping/ping.go index d6518521..069cc180 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -44,7 +44,7 @@ func Connect(ctx context.Context, privileged bool, controlFunc control.Func, des } func (c *Conn) connect(controlFunc control.Func) (err error) { - if c.IsLinuxUnprivileged() { + if c.isLinuxUnprivileged() { c.conn, err = newUnprivilegedConn(c.ctx, controlFunc, c.destination) } else { c.conn, err = connect(c.privileged, controlFunc, c.destination) @@ -52,12 +52,12 @@ func (c *Conn) connect(controlFunc control.Func) (err error) { return } -func (c *Conn) IsLinuxUnprivileged() bool { +func (c *Conn) isLinuxUnprivileged() bool { return (runtime.GOOS == "linux" || runtime.GOOS == "android") && !c.privileged } func (c *Conn) ReadIP(buffer *buf.Buffer) error { - if c.destination.Is6() || c.IsLinuxUnprivileged() { + if c.destination.Is6() || c.isLinuxUnprivileged() { var readMsg func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) switch conn := c.conn.(type) { case *net.IPConn: @@ -104,7 +104,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { } ttl = controlMessage.TTL } - if !c.IsLinuxUnprivileged() { + if !c.isLinuxUnprivileged() { icmpHdr := header.ICMPv4(buffer.Bytes()) icmpHdr.SetIdent(^icmpHdr.Ident()) icmpHdr.SetChecksum(0) @@ -142,7 +142,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { trafficClass = controlMessage.TrafficClass } icmpHdr := header.ICMPv6(buffer.Bytes()) - if !c.IsLinuxUnprivileged() { + if !c.isLinuxUnprivileged() { icmpHdr.SetIdent(^icmpHdr.Ident()) } icmpHdr.SetChecksum(0) @@ -182,7 +182,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { ipHdr.SetChecksum(0) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) icmpHdr := header.ICMPv4(ipHdr.Payload()) - if !c.IsLinuxUnprivileged() { + if !c.isLinuxUnprivileged() { icmpHdr.SetIdent(^icmpHdr.Ident()) } icmpHdr.SetChecksum(0) @@ -194,7 +194,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { } ipHdr.SetDestinationAddr(c.source.Load()) icmpHdr := header.ICMPv6(ipHdr.Payload()) - if !c.IsLinuxUnprivileged() { + if !c.isLinuxUnprivileged() { icmpHdr.SetIdent(^icmpHdr.Ident()) } icmpHdr.SetChecksum(0) @@ -213,7 +213,7 @@ func (c *Conn) ReadICMP(buffer *buf.Buffer) error { if err != nil { return err } - if !c.IsLinuxUnprivileged() { + if !c.isLinuxUnprivileged() { if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) buffer.Advance(int(ipHdr.HeaderLength())) @@ -240,7 +240,7 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { defer buffer.Release() if !c.destination.Is6() { ipHdr := header.IPv4(buffer.Bytes()) - if !c.IsLinuxUnprivileged() { + if !c.isLinuxUnprivileged() { icmpHdr := header.ICMPv4(ipHdr.Payload()) icmpHdr.SetIdent(^icmpHdr.Ident()) icmpHdr.SetChecksum(0) @@ -250,7 +250,7 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { return common.Error(c.conn.Write(ipHdr.Payload())) } else { ipHdr := header.IPv6(buffer.Bytes()) - if !c.IsLinuxUnprivileged() { + if !c.isLinuxUnprivileged() { icmpHdr := header.ICMPv6(ipHdr.Payload()) icmpHdr.SetIdent(^icmpHdr.Ident()) icmpHdr.SetChecksum(0) @@ -267,7 +267,7 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { func (c *Conn) WriteICMP(buffer *buf.Buffer) error { defer buffer.Release() - if !c.IsLinuxUnprivileged() { + if !c.isLinuxUnprivileged() { if !c.destination.Is6() { icmpHdr := header.ICMPv4(buffer.Bytes()) icmpHdr.SetIdent(^icmpHdr.Ident()) From b5f3fecc25df3b25e34658f6b08abb51b303b02e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 26 Aug 2025 14:29:48 +0800 Subject: [PATCH 069/121] ping: Fix linux route rules --- redirect_nftables.go | 25 ++++++++++++++++++++----- tun_linux.go | 18 ------------------ 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/redirect_nftables.go b/redirect_nftables.go index ec4bf5d8..01239532 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -143,12 +143,26 @@ func (r *autoRedirect) setupNFTables() error { } } chainPreRoutingUDP := nft.AddChain(&nftables.Chain{ - Name: "prerouting_udp", + Name: "prerouting_udp_icmp", Table: table, Hooknum: nftables.ChainHookPrerouting, Priority: nftables.ChainPriorityRef(*nftables.ChainPriorityNATDest + 2), Type: nftables.ChainTypeFilter, }) + ipProto := &nftables.Set{ + Table: table, + Anonymous: true, + Constant: true, + KeyType: nftables.TypeInetProto, + } + err = nft.AddSet(ipProto, []nftables.SetElement{ + {Key: []byte{unix.IPPROTO_UDP}}, + {Key: []byte{unix.IPPROTO_ICMP}}, + {Key: []byte{unix.IPPROTO_ICMPV6}}, + }) + if err != nil { + return err + } nft.AddRule(&nftables.Rule{ Table: table, Chain: chainPreRoutingUDP, @@ -157,10 +171,11 @@ func (r *autoRedirect) setupNFTables() error { Key: expr.MetaKeyL4PROTO, Register: 1, }, - &expr.Cmp{ - Op: expr.CmpOpNeq, - Register: 1, - Data: []byte{unix.IPPROTO_UDP}, + &expr.Lookup{ + SourceRegister: 1, + SetID: ipProto.ID, + SetName: ipProto.Name, + Invert: true, }, &expr.Verdict{ Kind: expr.VerdictReturn, diff --git a/tun_linux.go b/tun_linux.go index 6d7dfed9..b19d5daa 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -816,14 +816,6 @@ func (t *NativeTun) rules() []*netlink.Rule { it.Family = unix.AF_INET rules = append(rules, it) } - if p4 && !t.options.StrictRoute { - it = netlink.NewRule() - it.Priority = priority - it.IPProto = syscall.IPPROTO_ICMP - it.Goto = nopPriority - it.Family = unix.AF_INET - rules = append(rules, it) - } if p6 { it = netlink.NewRule() it.Priority = priority6 @@ -834,16 +826,6 @@ func (t *NativeTun) rules() []*netlink.Rule { it.Family = unix.AF_INET6 rules = append(rules, it) } - - if p6 && !t.options.StrictRoute { - it = netlink.NewRule() - it.Priority = priority6 - it.IPProto = syscall.IPPROTO_ICMPV6 - it.Goto = nopPriority - it.Family = unix.AF_INET6 - rules = append(rules, it) - priority6++ - } } if p4 { it = netlink.NewRule() From a24ab73aca0c3a19c1ebd12152abdd47db607f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 26 Aug 2025 14:41:14 +0800 Subject: [PATCH 070/121] ping: Increate mapping capacity --- route_direct.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/route_direct.go b/route_direct.go index 444eb5e0..b001f19a 100644 --- a/route_direct.go +++ b/route_direct.go @@ -1,6 +1,7 @@ package tun import ( + "math" "net/netip" "time" @@ -29,7 +30,7 @@ type DirectRouteMapping struct { } func NewDirectRouteMapping(timeout time.Duration) *DirectRouteMapping { - mapping := common.Must1(freelru.NewSharded[DirectRouteSession, DirectRouteDestination](1024, maphash.NewHasher[DirectRouteSession]().Hash32)) + mapping := common.Must1(freelru.NewSharded[DirectRouteSession, DirectRouteDestination](math.MaxUint16, maphash.NewHasher[DirectRouteSession]().Hash32)) mapping.SetHealthCheck(func(session DirectRouteSession, action DirectRouteDestination) bool { if action != nil { return !action.IsClosed() From e8d7fc1bb29747e334e7b08ee7cadf351bcc1e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 26 Aug 2025 23:16:58 +0800 Subject: [PATCH 071/121] ping: Do not use connected socket on macOS --- ping/ping.go | 15 +++++++-------- ping/socket_unix.go | 32 ++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/ping/ping.go b/ping/ping.go index 069cc180..c9fdc526 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -59,28 +59,27 @@ func (c *Conn) isLinuxUnprivileged() bool { func (c *Conn) ReadIP(buffer *buf.Buffer) error { if c.destination.Is6() || c.isLinuxUnprivileged() { var readMsg func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) - switch conn := c.conn.(type) { - case *net.IPConn: + if ipConn, isIPConn := common.Cast[*net.IPConn](c.conn); isIPConn { readMsg = func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) { var ipAddr *net.IPAddr - n, oobn, _, ipAddr, err = conn.ReadMsgIP(b, oob) + n, oobn, _, ipAddr, err = ipConn.ReadMsgIP(b, oob) if err == nil { addr = M.AddrFromNet(ipAddr) } return } - case *net.UDPConn: + } else if udpConn, isUDPConn := common.Cast[*net.UDPConn](c.conn); isUDPConn { readMsg = func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) { var addrPort netip.AddrPort - n, oobn, _, addrPort, err = conn.ReadMsgUDPAddrPort(b, oob) + n, oobn, _, addrPort, err = udpConn.ReadMsgUDPAddrPort(b, oob) if err == nil { addr = addrPort.Addr() } return } - case *UnprivilegedConn: - readMsg = conn.ReadMsg - default: + } else if unprivilegedConn, isUnprivilegedConn := c.conn.(*UnprivilegedConn); isUnprivilegedConn { + readMsg = unprivilegedConn.ReadMsg + } else { return E.New("unsupported conn type: ", reflect.TypeOf(c.conn)) } if !c.destination.Is6() { diff --git a/ping/socket_unix.go b/ping/socket_unix.go index 3e9bc99d..1eec10a5 100644 --- a/ping/socket_unix.go +++ b/ping/socket_unix.go @@ -9,6 +9,7 @@ import ( "runtime" "syscall" + "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" @@ -76,23 +77,34 @@ func connect(privileged bool, controlFunc control.Func, destination netip.Addr) var bindAddress netip.Addr if !destination.Is6() { - bindAddress = netip.AddrFrom4([4]byte{0, 0, 0, 0}) + bindAddress = netip.AddrFrom4([4]byte{}) } else { bindAddress = netip.AddrFrom16([16]byte{}) } + err = unix.Bind(fd, M.AddrPortToSockaddr(netip.AddrPortFrom(bindAddress, 0))) if err != nil { return nil, err } - err = unix.Connect(fd, M.AddrPortToSockaddr(netip.AddrPortFrom(destination, 0))) - if err != nil { - return nil, E.Cause(err, "connect()") - } - - conn, err := net.FileConn(file) - if err != nil { - return nil, err + if runtime.GOOS == "darwin" && !privileged { + // When running in NetworkExtension on macOS, write to connected socket results in 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 + } + return conn, nil } - return conn, nil } From 055fe13ec0d608b657c4e35dde91cff22832b2f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 26 Aug 2025 23:54:57 +0800 Subject: [PATCH 072/121] Revert "ping: Increate mapping capacity" This reverts commit a24ab73aca0c3a19c1ebd12152abdd47db607f5a. --- route_direct.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/route_direct.go b/route_direct.go index b001f19a..444eb5e0 100644 --- a/route_direct.go +++ b/route_direct.go @@ -1,7 +1,6 @@ package tun import ( - "math" "net/netip" "time" @@ -30,7 +29,7 @@ type DirectRouteMapping struct { } func NewDirectRouteMapping(timeout time.Duration) *DirectRouteMapping { - mapping := common.Must1(freelru.NewSharded[DirectRouteSession, DirectRouteDestination](math.MaxUint16, maphash.NewHasher[DirectRouteSession]().Hash32)) + 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() From 4c43f4af12bf57b3184db7728cac7d82d196a950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 27 Aug 2025 00:12:17 +0800 Subject: [PATCH 073/121] ping: Fix not handling reject --- stack.go | 5 ++++- stack_gvisor_icmp.go | 11 +++++++++-- stack_system.go | 10 ++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/stack.go b/stack.go index f6affa04..7014c6d3 100644 --- a/stack.go +++ b/stack.go @@ -12,7 +12,10 @@ import ( "github.com/sagernet/sing/common/logger" ) -var ErrDrop = E.New("drop connections by rule") +var ( + ErrDrop = E.New("drop by rule") + ErrReset = E.New("reset by rule") +) type Stack interface { Start() error diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index cb464055..78177d79 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -4,6 +4,7 @@ package tun import ( "context" + "errors" "net/netip" "sync" "time" @@ -73,7 +74,10 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa timeout, ) }) - if err != nil { + if errors.Is(err, ErrReset) { + gWriteUnreachable(f.stack, pkt) + return true + } else if errors.Is(err, ErrDrop) { return true } if action != nil { @@ -132,7 +136,10 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa timeout, ) }) - if err != nil { + if errors.Is(err, ErrReset) { + gWriteUnreachable(f.stack, pkt) + return true + } else if errors.Is(err, ErrDrop) { return true } if action != nil { diff --git a/stack_system.go b/stack_system.go index d09e7541..c262c5da 100644 --- a/stack_system.go +++ b/stack_system.go @@ -668,7 +668,11 @@ func (s *System) processIPv4ICMP(ipHdr header.IPv4, icmpHdr header.ICMPv4) (bool ) }) if err != nil { - return false, nil + if errors.Is(err, ErrReset) { + return false, s.rejectIPv4WithICMP(ipHdr, header.ICMPv4PortUnreachable) + } else if errors.Is(err, ErrDrop) { + return false, nil + } } if action != nil { return false, action.WritePacket(buf.As(ipHdr).ToOwned()) @@ -739,7 +743,9 @@ func (s *System) processIPv6ICMP(ipHdr header.IPv6, icmpHdr header.ICMPv6) (bool timeout, ) }) - if err != nil { + if errors.Is(err, ErrReset) { + return false, s.rejectIPv6WithICMP(ipHdr, header.ICMPv6PortUnreachable) + } else if errors.Is(err, ErrDrop) { return false, nil } if action != nil { From f9bbb15bfb7afaf488009c009747f66186e5233f Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Wed, 27 Aug 2025 08:45:12 +0800 Subject: [PATCH 074/121] ping: Reduce the cost of readMsg --- ping/ping.go | 58 ++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/ping/ping.go b/ping/ping.go index c9fdc526..5140c2da 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 + readMsg func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) } func Connect(ctx context.Context, privileged bool, controlFunc control.Func, destination netip.Addr) (*Conn, error) { @@ -49,6 +50,32 @@ func (c *Conn) connect(controlFunc control.Func) (err error) { } else { c.conn, err = connect(c.privileged, controlFunc, c.destination) } + if err != nil { + return err + } + if ipConn, isIPConn := common.Cast[*net.IPConn](c.conn); isIPConn { + 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) + if err == nil { + addr = M.AddrFromNet(ipAddr) + } + return + } + } else if udpConn, isUDPConn := common.Cast[*net.UDPConn](c.conn); isUDPConn { + 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) + if err == nil { + addr = addrPort.Addr() + } + return + } + } else if unprivilegedConn, isUnprivilegedConn := c.conn.(*UnprivilegedConn); isUnprivilegedConn { + c.readMsg = unprivilegedConn.ReadMsg + } else { + return E.New("unsupported conn type: ", reflect.TypeOf(c.conn)) + } return } @@ -58,39 +85,12 @@ func (c *Conn) isLinuxUnprivileged() bool { func (c *Conn) ReadIP(buffer *buf.Buffer) error { if c.destination.Is6() || c.isLinuxUnprivileged() { - var readMsg func(b, oob []byte) (n, oobn int, addr netip.Addr, err error) - if ipConn, isIPConn := common.Cast[*net.IPConn](c.conn); isIPConn { - 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) - if err == nil { - addr = M.AddrFromNet(ipAddr) - } - return - } - } else if udpConn, isUDPConn := common.Cast[*net.UDPConn](c.conn); isUDPConn { - 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) - if err == nil { - addr = addrPort.Addr() - } - return - } - } else if unprivilegedConn, isUnprivilegedConn := c.conn.(*UnprivilegedConn); isUnprivilegedConn { - readMsg = unprivilegedConn.ReadMsg - } else { - return E.New("unsupported conn type: ", reflect.TypeOf(c.conn)) - } if !c.destination.Is6() { oob := ipv4.NewControlMessage(ipv4.FlagTTL) buffer.Advance(header.IPv4MinimumSize) var ttl int // tos int - n, oobn, addr, err := readMsg(buffer.FreeBytes(), oob) - if err != nil { - return err - } + n, oobn, addr, err := c.readMsg(buffer.FreeBytes(), oob) if err != nil { return err } @@ -126,7 +126,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { hopLimit int trafficClass int ) - n, oobn, addr, err := readMsg(buffer.FreeBytes(), oob) + n, oobn, addr, err := c.readMsg(buffer.FreeBytes(), oob) if err != nil { return err } From adc106bcf62d1e1049b3b3d86e17f54d29a13d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 27 Aug 2025 11:03:17 +0800 Subject: [PATCH 075/121] Improve checksum usages --- internal/gtcpip/header/ipv4.go | 4 +++- internal/gtcpip/header/tcp.go | 4 +++- internal/gtcpip/header/udp.go | 6 ++++-- ping/ping.go | 22 +++++----------------- ping/rewriter.go | 4 ---- ping/socket_linux_unprivileged.go | 4 +--- stack_gvisor_icmp.go | 9 +++++---- stack_gvisor_tcp.go | 2 -- stack_system.go | 13 ++----------- 9 files changed, 23 insertions(+), 45 deletions(-) diff --git a/internal/gtcpip/header/ipv4.go b/internal/gtcpip/header/ipv4.go index 82539363..d1cbf7c6 100644 --- a/internal/gtcpip/header/ipv4.go +++ b/internal/gtcpip/header/ipv4.go @@ -479,7 +479,9 @@ func (b IPv4) SetDestinationAddress(addr tcpip.Address) { // CalculateChecksum calculates the checksum of the IPv4 header. func (b IPv4) CalculateChecksum() uint16 { - return checksum.Checksum(b[:b.HeaderLength()], 0) + xsum0 := checksum.Checksum(b[:xsum], 0) + xsum0 = checksum.Checksum(b[xsum+2:b.HeaderLength()], xsum0) + return xsum0 } // Encode encodes all the fields of the IPv4 header. diff --git a/internal/gtcpip/header/tcp.go b/internal/gtcpip/header/tcp.go index 58552538..da5d3d8a 100644 --- a/internal/gtcpip/header/tcp.go +++ b/internal/gtcpip/header/tcp.go @@ -351,7 +351,9 @@ func (b TCP) SetUrgentPointer(urgentPointer uint16) { // and the checksum of the segment data. func (b TCP) CalculateChecksum(partialChecksum uint16) uint16 { // Calculate the rest of the checksum. - return checksum.Checksum(b[:b.DataOffset()], partialChecksum) + xsum := checksum.Checksum(b[:TCPChecksumOffset], partialChecksum) + xsum = checksum.Checksum(b[TCPChecksumOffset+2:b.DataOffset()], xsum) + return xsum } // IsChecksumValid returns true iff the TCP header's checksum is valid. diff --git a/internal/gtcpip/header/udp.go b/internal/gtcpip/header/udp.go index 080a97fd..eac9d637 100644 --- a/internal/gtcpip/header/udp.go +++ b/internal/gtcpip/header/udp.go @@ -113,8 +113,10 @@ func (b UDP) SetLength(length uint16) { // CalculateChecksum calculates the checksum of the UDP packet, given the // checksum of the network-layer pseudo-header and the checksum of the payload. func (b UDP) CalculateChecksum(partialChecksum uint16) uint16 { - // Calculate the rest of the checksum. - return checksum.Checksum(b[:UDPMinimumSize], partialChecksum) + // Calculate the rest of the checksum.\ + xsum := checksum.Checksum(b[:udpChecksum], partialChecksum) + xsum = checksum.Checksum(b[udpChecksum+2:], xsum) + return xsum } // IsChecksumValid returns true iff the UDP header's checksum is valid. diff --git a/ping/ping.go b/ping/ping.go index 5140c2da..eab977b7 100644 --- a/ping/ping.go +++ b/ping/ping.go @@ -9,7 +9,6 @@ import ( "sync/atomic" "time" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" "github.com/sagernet/sing-tun/internal/gtcpip/header" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" @@ -106,8 +105,7 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { if !c.isLinuxUnprivileged() { icmpHdr := header.ICMPv4(buffer.Bytes()) icmpHdr.SetIdent(^icmpHdr.Ident()) - icmpHdr.SetChecksum(0) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) } ipHdr := header.IPv4(buffer.ExtendHeader(header.IPv4MinimumSize)) ipHdr.Encode(&header.IPv4Fields{ @@ -144,7 +142,6 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { if !c.isLinuxUnprivileged() { icmpHdr.SetIdent(^icmpHdr.Ident()) } - icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmpHdr, Src: addr.AsSlice(), @@ -178,14 +175,12 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { return E.New("invalid IPv4 header received") } ipHdr.SetDestinationAddr(c.source.Load()) - ipHdr.SetChecksum(0) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) icmpHdr := header.ICMPv4(ipHdr.Payload()) if !c.isLinuxUnprivileged() { icmpHdr.SetIdent(^icmpHdr.Ident()) } - icmpHdr.SetChecksum(0) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) } else { ipHdr := header.IPv6(buffer.Bytes()) if !ipHdr.IsValid(buffer.Len()) { @@ -196,7 +191,6 @@ func (c *Conn) ReadIP(buffer *buf.Buffer) error { if !c.isLinuxUnprivileged() { icmpHdr.SetIdent(^icmpHdr.Ident()) } - icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmpHdr, Src: ipHdr.SourceAddressSlice(), @@ -219,12 +213,10 @@ func (c *Conn) ReadICMP(buffer *buf.Buffer) error { icmpHdr := header.ICMPv4(buffer.Bytes()) icmpHdr.SetIdent(^icmpHdr.Ident()) - icmpHdr.SetChecksum(0) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) } else { icmpHdr := header.ICMPv6(buffer.Bytes()) icmpHdr.SetIdent(^icmpHdr.Ident()) - icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmpHdr, Src: c.destination.AsSlice(), @@ -242,8 +234,7 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { if !c.isLinuxUnprivileged() { icmpHdr := header.ICMPv4(ipHdr.Payload()) icmpHdr.SetIdent(^icmpHdr.Ident()) - icmpHdr.SetChecksum(0) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) } c.source.Store(M.AddrFromIP(ipHdr.SourceAddressSlice())) return common.Error(c.conn.Write(ipHdr.Payload())) @@ -252,7 +243,6 @@ func (c *Conn) WriteIP(buffer *buf.Buffer) error { if !c.isLinuxUnprivileged() { icmpHdr := header.ICMPv6(ipHdr.Payload()) icmpHdr.SetIdent(^icmpHdr.Ident()) - icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmpHdr, Src: ipHdr.SourceAddressSlice(), @@ -270,12 +260,10 @@ func (c *Conn) WriteICMP(buffer *buf.Buffer) error { if !c.destination.Is6() { icmpHdr := header.ICMPv4(buffer.Bytes()) icmpHdr.SetIdent(^icmpHdr.Ident()) - icmpHdr.SetChecksum(0) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) } else { icmpHdr := header.ICMPv6(buffer.Bytes()) icmpHdr.SetIdent(^icmpHdr.Ident()) - icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmpHdr, Src: c.source.Load().AsSlice(), diff --git a/ping/rewriter.go b/ping/rewriter.go index 52666b74..4d0dcd8d 100644 --- a/ping/rewriter.go +++ b/ping/rewriter.go @@ -59,7 +59,6 @@ func (m *Rewriter) RewritePacket(packet []byte) { sourceAddr := ipHdr.SourceAddr() ipHdr.SetSourceAddr(bindAddr) if ipHdr4, isIPv4 := ipHdr.(header.IPv4); isIPv4 { - ipHdr4.SetChecksum(0) ipHdr4.SetChecksum(^ipHdr4.CalculateChecksum()) } switch ipHdr.TransportProtocol() { @@ -71,7 +70,6 @@ func (m *Rewriter) RewritePacket(packet []byte) { 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(0) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmpHdr, Src: ipHdr.SourceAddressSlice(), @@ -133,7 +131,6 @@ func (m *Rewriter) WriteBack(packet []byte) (bool, error) { } ipHdr.SetDestinationAddr(routeSession.Source) if ipHdr4, isIPv4 := ipHdr.(header.IPv4); isIPv4 { - ipHdr4.SetChecksum(0) ipHdr4.SetChecksum(^ipHdr4.CalculateChecksum()) } switch ipHdr.TransportProtocol() { @@ -142,7 +139,6 @@ func (m *Rewriter) WriteBack(packet []byte) (bool, error) { 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(0) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmpHdr, Src: ipHdr.SourceAddressSlice(), diff --git a/ping/socket_linux_unprivileged.go b/ping/socket_linux_unprivileged.go index 4e17945b..79fd682d 100644 --- a/ping/socket_linux_unprivileged.go +++ b/ping/socket_linux_unprivileged.go @@ -8,7 +8,6 @@ import ( "sync" "time" - "github.com/sagernet/sing-tun/internal/gtcpip/checksum" "github.com/sagernet/sing-tun/internal/gtcpip/header" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" @@ -130,8 +129,7 @@ func (c *UnprivilegedConn) fetchResponse(conn *net.UDPConn, identifier uint16) { if !c.destination.Is6() { icmpHdr := header.ICMPv4(buffer.Bytes()) icmpHdr.SetIdent(identifier) - icmpHdr.SetChecksum(0) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) } else { icmpHdr := header.ICMPv6(buffer.Bytes()) icmpHdr.SetIdent(identifier) diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index 78177d79..160d3ec4 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -90,7 +90,6 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa sourceAddress := ipHdr.SourceAddress() ipHdr.SetSourceAddress(ipHdr.DestinationAddress()) ipHdr.SetDestinationAddress(sourceAddress) - icmpHdr.SetChecksum(0) icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], pkt.Data().Checksum())) ipHdr.SetChecksum(0) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) @@ -154,9 +153,11 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa ipHdr.SetSourceAddress(ipHdr.DestinationAddress()) ipHdr.SetDestinationAddress(sourceAddress) icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ - Header: icmpHdr, - Src: ipHdr.SourceAddress(), - Dst: ipHdr.DestinationAddress(), + Header: icmpHdr, + Src: ipHdr.SourceAddress(), + Dst: ipHdr.DestinationAddress(), + PayloadCsum: pkt.Data().Checksum(), + PayloadLen: pkt.Data().Size(), })) outgoingEP, gErr := f.stack.GetNetworkEndpoint(DefaultNIC, header.IPv4ProtocolNumber) if gErr != nil { diff --git a/stack_gvisor_tcp.go b/stack_gvisor_tcp.go index 15927999..024f4b4a 100644 --- a/stack_gvisor_tcp.go +++ b/stack_gvisor_tcp.go @@ -51,7 +51,6 @@ func (f *TCPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac ipHdr.SetDestinationAddressWithChecksumUpdate(ipHdr.SourceAddress()) ipHdr.SetSourceAddressWithChecksumUpdate(inet4LoopbackAddress) tcpHdr := header.TCP(pkt.TransportHeader().Slice()) - tcpHdr.SetChecksum(0) tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress(), ipHdr.PayloadLength()), ))) @@ -65,7 +64,6 @@ func (f *TCPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac ipHdr.SetDestinationAddress(ipHdr.SourceAddress()) ipHdr.SetSourceAddress(inet6LoopbackAddress) tcpHdr := header.TCP(pkt.TransportHeader().Slice()) - tcpHdr.SetChecksum(0) tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress(), ipHdr.PayloadLength()), ))) diff --git a/stack_system.go b/stack_system.go index c262c5da..c354bda5 100644 --- a/stack_system.go +++ b/stack_system.go @@ -430,14 +430,12 @@ func (s *System) processIPv4TCP(ipHdr header.IPv4, tcpHdr header.TCP) (bool, err } } if !s.txChecksumOffload { - tcpHdr.SetChecksum(0) tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), ipHdr.PayloadLength()), ))) } else { tcpHdr.SetChecksum(0) } - ipHdr.SetChecksum(0) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) return true, nil } @@ -478,7 +476,6 @@ func (s *System) resetIPv4TCP(origIPHdr header.IPv4, origTCPHdr header.TCP) erro if !s.txChecksumOffload { tcpHdr.SetChecksum(^tcpHdr.CalculateChecksum(header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), header.TCPMinimumSize))) } - ipHdr.SetChecksum(0) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) if PacketOffset > 0 { PacketFillHeader(newPacket.ExtendHeader(PacketOffset), header.IPv4Version) @@ -528,7 +525,6 @@ func (s *System) processIPv6TCP(ipHdr header.IPv6, tcpHdr header.TCP) (bool, err } } if !s.txChecksumOffload { - tcpHdr.SetChecksum(0) tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), ipHdr.PayloadLength()), ))) @@ -682,8 +678,7 @@ func (s *System) processIPv4ICMP(ipHdr header.IPv4, icmpHdr header.ICMPv4) (bool sourceAddress := ipHdr.SourceAddr() ipHdr.SetSourceAddr(ipHdr.DestinationAddr()) ipHdr.SetDestinationAddr(sourceAddress) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(icmpHdr.Payload(), 0))) - ipHdr.SetChecksum(0) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) return true, nil } @@ -717,7 +712,7 @@ func (s *System) rejectIPv4WithICMP(ipHdr header.IPv4, code header.ICMPv4Code) e icmpHdr := header.ICMPv4(newIPHdr.Payload()) icmpHdr.SetType(header.ICMPv4DstUnreachable) icmpHdr.SetCode(code) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr[:header.ICMPv4MinimumSize], checksum.Checksum(payload, 0))) + icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) copy(icmpHdr.Payload(), payload) if PacketOffset > 0 { newPacket.ExtendHeader(PacketOffset)[3] = syscall.AF_INET @@ -831,14 +826,12 @@ func (w *systemUDPPacketWriter4) WritePacket(buffer *buf.Buffer, destination M.S udpHdr.SetSourcePort(destination.Port) udpHdr.SetLength(uint16(buffer.Len() + header.UDPMinimumSize)) if !w.txChecksumOffload { - udpHdr.SetChecksum(0) udpHdr.SetChecksum(^checksum.Checksum(udpHdr.Payload(), udpHdr.CalculateChecksum( header.PseudoHeaderChecksum(header.UDPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), ipHdr.PayloadLength()), ))) } else { udpHdr.SetChecksum(0) } - ipHdr.SetChecksum(0) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) if PacketOffset > 0 { PacketFillHeader(newPacket.ExtendHeader(PacketOffset), header.IPv4Version) @@ -872,7 +865,6 @@ func (w *systemUDPPacketWriter6) WritePacket(buffer *buf.Buffer, destination M.S udpHdr.SetSourcePort(destination.Port) udpHdr.SetLength(udpLen) if !w.txChecksumOffload { - udpHdr.SetChecksum(0) udpHdr.SetChecksum(^checksum.Checksum(udpHdr.Payload(), udpHdr.CalculateChecksum( header.PseudoHeaderChecksum(header.UDPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), ipHdr.PayloadLength()), ))) @@ -900,7 +892,6 @@ func (w *systemICMPDirectPacketWriter4) WritePacket(p []byte) error { newPacket.Write(p) ipHdr := header.IPv4(newPacket.Bytes()) ipHdr.SetDestinationAddr(w.source) - ipHdr.SetChecksum(0) ipHdr.SetChecksum(^ipHdr.CalculateChecksum()) if PacketOffset > 0 { PacketFillHeader(newPacket.ExtendHeader(PacketOffset), header.IPv4Version) From ff49ece55d9d67f7043be5374711d8f267077091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 27 Aug 2025 15:45:18 +0800 Subject: [PATCH 076/121] Fix checksum changes --- internal/gtcpip/header/ipv4.go | 4 +++- internal/gtcpip/header/tcp.go | 4 +++- internal/gtcpip/header/udp.go | 5 +++-- stack_gvisor_tcp.go | 2 ++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/internal/gtcpip/header/ipv4.go b/internal/gtcpip/header/ipv4.go index d1cbf7c6..ad06f38c 100644 --- a/internal/gtcpip/header/ipv4.go +++ b/internal/gtcpip/header/ipv4.go @@ -479,6 +479,7 @@ func (b IPv4) SetDestinationAddress(addr tcpip.Address) { // CalculateChecksum calculates the checksum of the IPv4 header. func (b IPv4) CalculateChecksum() uint16 { + // return checksum.Checksum(b[:b.HeaderLength()], 0) xsum0 := checksum.Checksum(b[:xsum], 0) xsum0 = checksum.Checksum(b[xsum+2:b.HeaderLength()], xsum0) return xsum0 @@ -573,7 +574,8 @@ func (b IPv4) IsChecksumValid() bool { // same set of octets, including the checksum field. If the result // is all 1 bits (-0 in 1's complement arithmetic), the check // succeeds. - return b.CalculateChecksum() == 0xffff + //return b.CalculateChecksum() == 0xffff + return checksum.Checksum(b[:b.HeaderLength()], 0) == 0xffff } // IsV4MulticastAddress determines if the provided address is an IPv4 multicast diff --git a/internal/gtcpip/header/tcp.go b/internal/gtcpip/header/tcp.go index da5d3d8a..1b58df86 100644 --- a/internal/gtcpip/header/tcp.go +++ b/internal/gtcpip/header/tcp.go @@ -351,6 +351,7 @@ func (b TCP) SetUrgentPointer(urgentPointer uint16) { // and the checksum of the segment data. func (b TCP) CalculateChecksum(partialChecksum uint16) uint16 { // Calculate the rest of the checksum. + // return checksum.Checksum(b[:b.DataOffset()], partialChecksum) xsum := checksum.Checksum(b[:TCPChecksumOffset], partialChecksum) xsum = checksum.Checksum(b[TCPChecksumOffset+2:b.DataOffset()], xsum) return xsum @@ -360,7 +361,8 @@ func (b TCP) CalculateChecksum(partialChecksum uint16) uint16 { func (b TCP) IsChecksumValid(src, dst tcpip.Address, payloadChecksum, payloadLength uint16) bool { xsum := PseudoHeaderChecksum(TCPProtocolNumber, src.AsSlice(), dst.AsSlice(), uint16(b.DataOffset())+payloadLength) xsum = checksum.Combine(xsum, payloadChecksum) - return b.CalculateChecksum(xsum) == 0xffff + // return b.CalculateChecksum(xsum) == 0xffff + return checksum.Checksum(b[:b.DataOffset()], xsum) == 0xffff } // Options returns a slice that holds the unparsed TCP options in the segment. diff --git a/internal/gtcpip/header/udp.go b/internal/gtcpip/header/udp.go index eac9d637..a995a172 100644 --- a/internal/gtcpip/header/udp.go +++ b/internal/gtcpip/header/udp.go @@ -114,8 +114,9 @@ func (b UDP) SetLength(length uint16) { // checksum of the network-layer pseudo-header and the checksum of the payload. func (b UDP) CalculateChecksum(partialChecksum uint16) uint16 { // Calculate the rest of the checksum.\ + // return checksum.Checksum(b[:UDPMinimumSize], partialChecksum) xsum := checksum.Checksum(b[:udpChecksum], partialChecksum) - xsum = checksum.Checksum(b[udpChecksum+2:], xsum) + xsum = checksum.Checksum(b[udpChecksum+2:UDPMinimumSize], xsum) return xsum } @@ -123,7 +124,7 @@ func (b UDP) CalculateChecksum(partialChecksum uint16) uint16 { func (b UDP) IsChecksumValid(src, dst tcpip.Address, payloadChecksum uint16) bool { xsum := PseudoHeaderChecksum(UDPProtocolNumber, dst.AsSlice(), src.AsSlice(), b.Length()) xsum = checksum.Combine(xsum, payloadChecksum) - return b.CalculateChecksum(xsum) == 0xffff + return checksum.Checksum(b[:UDPMinimumSize], xsum) == 0xffff } // Encode encodes all the fields of the UDP header. diff --git a/stack_gvisor_tcp.go b/stack_gvisor_tcp.go index 024f4b4a..15927999 100644 --- a/stack_gvisor_tcp.go +++ b/stack_gvisor_tcp.go @@ -51,6 +51,7 @@ func (f *TCPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac ipHdr.SetDestinationAddressWithChecksumUpdate(ipHdr.SourceAddress()) ipHdr.SetSourceAddressWithChecksumUpdate(inet4LoopbackAddress) tcpHdr := header.TCP(pkt.TransportHeader().Slice()) + tcpHdr.SetChecksum(0) tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress(), ipHdr.PayloadLength()), ))) @@ -64,6 +65,7 @@ func (f *TCPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac ipHdr.SetDestinationAddress(ipHdr.SourceAddress()) ipHdr.SetSourceAddress(inet6LoopbackAddress) tcpHdr := header.TCP(pkt.TransportHeader().Slice()) + tcpHdr.SetChecksum(0) tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress(), ipHdr.PayloadLength()), ))) From d5865f21354247a9ef80f7470fc0b0fd4cd8b4db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 27 Aug 2025 16:35:53 +0800 Subject: [PATCH 077/121] Fix gvisor loopback address --- stack_gvisor_tcp.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stack_gvisor_tcp.go b/stack_gvisor_tcp.go index 15927999..0c63ee11 100644 --- a/stack_gvisor_tcp.go +++ b/stack_gvisor_tcp.go @@ -52,7 +52,7 @@ func (f *TCPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac ipHdr.SetSourceAddressWithChecksumUpdate(inet4LoopbackAddress) tcpHdr := header.TCP(pkt.TransportHeader().Slice()) tcpHdr.SetChecksum(0) - tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( + tcpHdr.SetChecksum(^checksum.Combine(pkt.Data().Checksum(), tcpHdr.CalculateChecksum( header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress(), ipHdr.PayloadLength()), ))) f.tun.WritePacket(pkt) @@ -66,7 +66,7 @@ func (f *TCPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac ipHdr.SetSourceAddress(inet6LoopbackAddress) tcpHdr := header.TCP(pkt.TransportHeader().Slice()) tcpHdr.SetChecksum(0) - tcpHdr.SetChecksum(^checksum.Checksum(tcpHdr.Payload(), tcpHdr.CalculateChecksum( + tcpHdr.SetChecksum(^checksum.Combine(pkt.Data().Checksum(), tcpHdr.CalculateChecksum( header.PseudoHeaderChecksum(header.TCPProtocolNumber, ipHdr.SourceAddress(), ipHdr.DestinationAddress(), ipHdr.PayloadLength()), ))) f.tun.WritePacket(pkt) From b76e852f59b07354f0851a2feeac4f8ade9b66b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 27 Aug 2025 20:28:59 +0800 Subject: [PATCH 078/121] ping: Fix reject --- stack_system.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stack_system.go b/stack_system.go index c354bda5..f74fef96 100644 --- a/stack_system.go +++ b/stack_system.go @@ -665,7 +665,7 @@ func (s *System) processIPv4ICMP(ipHdr header.IPv4, icmpHdr header.ICMPv4) (bool }) if err != nil { if errors.Is(err, ErrReset) { - return false, s.rejectIPv4WithICMP(ipHdr, header.ICMPv4PortUnreachable) + return false, s.rejectIPv4WithICMP(ipHdr, header.ICMPv4HostUnreachable) } else if errors.Is(err, ErrDrop) { return false, nil } @@ -712,7 +712,7 @@ func (s *System) rejectIPv4WithICMP(ipHdr header.IPv4, code header.ICMPv4Code) e icmpHdr := header.ICMPv4(newIPHdr.Payload()) icmpHdr.SetType(header.ICMPv4DstUnreachable) icmpHdr.SetCode(code) - icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0)) + 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 @@ -739,7 +739,7 @@ func (s *System) processIPv6ICMP(ipHdr header.IPv6, icmpHdr header.ICMPv6) (bool ) }) if errors.Is(err, ErrReset) { - return false, s.rejectIPv6WithICMP(ipHdr, header.ICMPv6PortUnreachable) + return false, s.rejectIPv6WithICMP(ipHdr, header.ICMPv6AddressUnreachable) } else if errors.Is(err, ErrDrop) { return false, nil } From a8cb01e6df93ee6bb43760fa7c08f399f5e039de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 9 Sep 2025 14:48:31 +0800 Subject: [PATCH 079/121] Prevent panic when wintun dll fails to load --- internal/wintun/wintun_windows.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/wintun/wintun_windows.go b/internal/wintun/wintun_windows.go index a817e6c5..288d364b 100644 --- a/internal/wintun/wintun_windows.go +++ b/internal/wintun/wintun_windows.go @@ -39,6 +39,10 @@ func closeAdapter(wintun *Adapter) { // deterministically. If it is set to nil, the GUID is chosen by the system at random, // and hence a new NLA entry is created for each new adapter. func CreateAdapter(name string, tunnelType string, requestedGUID *windows.GUID) (wintun *Adapter, err error) { + err = procWintunCloseAdapter.Find() + if err != nil { + return + } var name16 *uint16 name16, err = windows.UTF16PtrFromString(name) if err != nil { From 7de8ff7f20a352c5a19c6e65bd3ca742c335b8b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 25 Aug 2025 22:19:09 +0800 Subject: [PATCH 080/121] ping: Fix read ICMPv6 from gVisor --- go.mod | 2 +- go.sum | 4 ++-- stack_gvisor_icmp.go | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 452ddac3..92005bf9 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/go-ole/go-ole v1.3.0 github.com/google/btree v1.1.3 github.com/sagernet/fswatch v0.1.1 - github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 + github.com/sagernet/gvisor v0.0.0-20250909151924-850a370d8506 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/sagernet/nftables v0.3.0-beta.4 github.com/sagernet/sing v0.7.6-0.20250825141840-811aa328e57b diff --git a/go.sum b/go.sum index 4b6325ce..8aff582e 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= -github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 h1:SUPFNB+vSP4RBPrSEgNII+HkfqC8hKMpYLodom4o4EU= -github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4= +github.com/sagernet/gvisor v0.0.0-20250909151924-850a370d8506 h1:x/t3XqWshOlWqRuumpvbUvjtEr/6mJuBXAVovPefbUg= +github.com/sagernet/gvisor v0.0.0-20250909151924-850a370d8506/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index 160d3ec4..da5549b6 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -226,6 +226,7 @@ func (w *ICMPBackWriter) WritePacket(p []byte) error { packet := stack.NewPacketBuffer(stack.PacketBufferOptions{ Payload: buffer.MakeWithData(p), }) + parse.IPv6(packet) defer packet.DecRef() err = route.WritePacketDirect(packet) if err != nil { From 960457abba74af84020acf92914ca38288e52f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 10 Sep 2025 22:52:15 +0800 Subject: [PATCH 081/121] Update dependencies --- go.mod | 4 ++-- go.sum | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 92005bf9..0d34981e 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,8 @@ require ( github.com/sagernet/gvisor v0.0.0-20250909151924-850a370d8506 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/sagernet/nftables v0.3.0-beta.4 - github.com/sagernet/sing v0.7.6-0.20250825141840-811aa328e57b - github.com/stretchr/testify v1.9.0 + github.com/sagernet/sing v0.8.0-beta.1 + github.com/stretchr/testify v1.11.1 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 golang.org/x/net v0.43.0 diff --git a/go.sum b/go.sum index 8aff582e..1032ca34 100644 --- a/go.sum +++ b/go.sum @@ -24,10 +24,12 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= -github.com/sagernet/sing v0.7.6-0.20250825141840-811aa328e57b h1:RCfo1Q6VDAXfumNupRyqTomKzDODhASswkxVCqM8l2M= -github.com/sagernet/sing v0.7.6-0.20250825141840-811aa328e57b/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.0-beta.1 h1:tBOdh/K/EBdXWuBxUJsZONyxDzyfzjdCF1Yq57QtpE4= +github.com/sagernet/sing v0.8.0-beta.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= From 0381a06643bc35d7fa48786fde8bc6109b6066be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 11 Sep 2025 18:51:00 +0800 Subject: [PATCH 082/121] ping: Add destination rewriter --- ping/destination_gvisor.go | 4 +- ping/destination_rewriter.go | 79 ++++++++++++++++++++++++ ping/{rewriter.go => source_rewriter.go} | 14 ++--- 3 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 ping/destination_rewriter.go rename ping/{rewriter.go => source_rewriter.go} (89%) diff --git a/ping/destination_gvisor.go b/ping/destination_gvisor.go index 2a2d0e5c..0bac8036 100644 --- a/ping/destination_gvisor.go +++ b/ping/destination_gvisor.go @@ -27,7 +27,7 @@ type GVisorDestination struct { logger logger.ContextLogger endpoint tcpip.Endpoint conn *gonet.TCPConn - rewriter *Rewriter + rewriter *SourceRewriter timeout time.Duration } @@ -76,7 +76,7 @@ func ConnectGVisor( return nil, gonet.TranslateNetstackError(gErr) } endpoint.SocketOptions().SetHeaderIncluded(true) - rewriter := NewRewriter(ctx, logger, bindAddress4, bindAddress6) + rewriter := NewSourceRewriter(ctx, logger, bindAddress4, bindAddress6) rewriter.CreateSession(tun.DirectRouteSession{Source: sourceAddress, Destination: destinationAddress}, routeContext) destination := &GVisorDestination{ ctx: ctx, diff --git a/ping/destination_rewriter.go b/ping/destination_rewriter.go new file mode 100644 index 00000000..a61e1556 --- /dev/null +++ b/ping/destination_rewriter.go @@ -0,0 +1,79 @@ +package ping + +import ( + "net/netip" + + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/internal/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/rewriter.go b/ping/source_rewriter.go similarity index 89% rename from ping/rewriter.go rename to ping/source_rewriter.go index 4d0dcd8d..480c6a78 100644 --- a/ping/rewriter.go +++ b/ping/source_rewriter.go @@ -10,7 +10,7 @@ import ( "github.com/sagernet/sing/common/logger" ) -type Rewriter struct { +type SourceRewriter struct { ctx context.Context logger logger.ContextLogger access sync.RWMutex @@ -20,8 +20,8 @@ type Rewriter struct { inet6Address netip.Addr } -func NewRewriter(ctx context.Context, logger logger.ContextLogger, inet4Address netip.Addr, inet6Address netip.Addr) *Rewriter { - return &Rewriter{ +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), @@ -31,19 +31,19 @@ func NewRewriter(ctx context.Context, logger logger.ContextLogger, inet4Address } } -func (m *Rewriter) CreateSession(session tun.DirectRouteSession, context tun.DirectRouteContext) { +func (m *SourceRewriter) CreateSession(session tun.DirectRouteSession, context tun.DirectRouteContext) { m.access.Lock() m.sessions[session] = context m.access.Unlock() } -func (m *Rewriter) DeleteSession(session tun.DirectRouteSession) { +func (m *SourceRewriter) DeleteSession(session tun.DirectRouteSession) { m.access.Lock() delete(m.sessions, session) m.access.Unlock() } -func (m *Rewriter) RewritePacket(packet []byte) { +func (m *SourceRewriter) RewritePacket(packet []byte) { var ipHdr header.Network var bindAddr netip.Addr switch header.IPVersion(packet) { @@ -82,7 +82,7 @@ func (m *Rewriter) RewritePacket(packet []byte) { } } -func (m *Rewriter) WriteBack(packet []byte) (bool, error) { +func (m *SourceRewriter) WriteBack(packet []byte) (bool, error) { var ipHdr header.Network var routeSession tun.DirectRouteSession switch header.IPVersion(packet) { From 67013b321e6051ca02ed723833a8653e026631b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 12 Sep 2025 18:02:37 +0800 Subject: [PATCH 083/121] Fix race codes --- stack_system_nat.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/stack_system_nat.go b/stack_system_nat.go index 6a6d6b97..636b561d 100644 --- a/stack_system_nat.go +++ b/stack_system_nat.go @@ -11,6 +11,7 @@ import ( ) type TCPNat struct { + timeout time.Duration portIndex uint16 portAccess sync.RWMutex addrAccess sync.RWMutex @@ -19,6 +20,7 @@ type TCPNat struct { } type TCPSession struct { + sync.Mutex Source netip.AddrPort Destination netip.AddrPort LastActive time.Time @@ -26,38 +28,41 @@ type TCPSession struct { func NewNat(ctx context.Context, timeout time.Duration) *TCPNat { natMap := &TCPNat{ + timeout: timeout, portIndex: 10000, addrMap: make(map[netip.AddrPort]uint16), portMap: make(map[uint16]*TCPSession), } - go natMap.loopCheckTimeout(ctx, timeout) + go natMap.loopCheckTimeout(ctx) return natMap } -func (n *TCPNat) loopCheckTimeout(ctx context.Context, timeout time.Duration) { - ticker := time.NewTicker(timeout) +func (n *TCPNat) loopCheckTimeout(ctx context.Context) { + ticker := time.NewTicker(n.timeout) defer ticker.Stop() for { select { case <-ticker.C: - n.checkTimeout(timeout) + n.checkTimeout() case <-ctx.Done(): return } } } -func (n *TCPNat) checkTimeout(timeout time.Duration) { +func (n *TCPNat) checkTimeout() { now := time.Now() n.portAccess.Lock() defer n.portAccess.Unlock() n.addrAccess.Lock() defer n.addrAccess.Unlock() for natPort, session := range n.portMap { - if now.Sub(session.LastActive) > timeout { + session.Lock() + if now.Sub(session.LastActive) > n.timeout { delete(n.addrMap, session.Source) delete(n.portMap, natPort) } + session.Unlock() } } @@ -66,7 +71,11 @@ func (n *TCPNat) LookupBack(port uint16) *TCPSession { session := n.portMap[port] n.portAccess.RUnlock() if session != nil { - session.LastActive = time.Now() + session.Lock() + if time.Since(session.LastActive) > time.Second { + session.LastActive = time.Now() + } + session.Unlock() } return session } From 09d1d97313f719705eebb2150ffbf6df9b207ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 15 Sep 2025 23:55:16 +0800 Subject: [PATCH 084/121] Update gVisor to v20250811.0 --- go.mod | 6 +++--- go.sum | 10 ++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 0d34981e..ff61d7c1 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,15 @@ module github.com/sagernet/sing-tun -go 1.23.1 +go 1.24.7 require ( github.com/go-ole/go-ole v1.3.0 github.com/google/btree v1.1.3 github.com/sagernet/fswatch v0.1.1 - github.com/sagernet/gvisor v0.0.0-20250909151924-850a370d8506 + github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/sagernet/nftables v0.3.0-beta.4 - github.com/sagernet/sing v0.8.0-beta.1 + github.com/sagernet/sing v0.8.0-beta.2 github.com/stretchr/testify v1.11.1 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 diff --git a/go.sum b/go.sum index 1032ca34..e3c486b3 100644 --- a/go.sum +++ b/go.sum @@ -18,16 +18,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= -github.com/sagernet/gvisor v0.0.0-20250909151924-850a370d8506 h1:x/t3XqWshOlWqRuumpvbUvjtEr/6mJuBXAVovPefbUg= -github.com/sagernet/gvisor v0.0.0-20250909151924-850a370d8506/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4= +github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 h1:AzCE2RhBjLJ4WIWc/GejpNh+z30d5H1hwaB0nD9eY3o= +github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1/go.mod h1:NJKBtm9nVEK3iyOYWsUlrDQuoGh4zJ4KOPhSYVidvQ4= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= -github.com/sagernet/sing v0.8.0-beta.1 h1:tBOdh/K/EBdXWuBxUJsZONyxDzyfzjdCF1Yq57QtpE4= -github.com/sagernet/sing v0.8.0-beta.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/sagernet/sing v0.8.0-beta.2 h1:3khO2eE5LMylD/v47+pnVMtFzl6lBY2v/b/V+79qpsE= +github.com/sagernet/sing v0.8.0-beta.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= From b42efe251683807d22f83c0213f51dabc2d72da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 22 Sep 2025 12:51:12 +0800 Subject: [PATCH 085/121] Update golangci-lint --- .github/workflows/lint.yml | 16 ++++------ .gitignore | 2 ++ .golangci.yml | 61 ++++++++++++++++++++++++++++---------- tun_offload_linux.go | 6 ++-- 4 files changed, 56 insertions(+), 29 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c682b258..7d5cea88 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,21 +20,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ^1.23 - - name: Cache go module - uses: actions/cache@v4 - with: - path: | - ~/go/pkg/mod - key: go-${{ hashFiles('**/go.sum') }} + go-version: ^1.25 - name: golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v8 with: version: latest - args: . \ No newline at end of file + args: --timeout=30m + install-mode: binary + verify: false \ No newline at end of file diff --git a/.gitignore b/.gitignore index f1298ea2..3f3c51ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /.idea/ /vendor/ .DS_Store +!/README.md +/*.md diff --git a/.golangci.yml b/.golangci.yml index eecfec35..7a8a771d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,20 +1,49 @@ +version: "2" +run: + go: "1.25" linters: - disable-all: true + default: none enable: - - gofumpt - govet - - gci - - staticcheck - - paralleltest - ineffassign - -linters-settings: - gci: - custom-order: true - sections: - - standard - - prefix(github.com/sagernet/) - - default - -run: - go: "1.23" \ No newline at end of file + - paralleltest + - staticcheck + settings: + staticcheck: + checks: + - all + - -S1000 + - -S1008 + - -S1017 + - -ST1003 + - -QF1001 + - -QF1003 + - -QF1008 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gci + - gofumpt + settings: + gci: + sections: + - standard + - prefix(github.com/sagernet/) + - default + custom-order: true + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/tun_offload_linux.go b/tun_offload_linux.go index 7e44a553..77337607 100644 --- a/tun_offload_linux.go +++ b/tun_offload_linux.go @@ -149,12 +149,12 @@ func (t *tcpGROTable) insert(pkt []byte, srcAddrOffset, dstAddrOffset, tcphOffse } func (t *tcpGROTable) updateAt(item tcpGROItem, i int) { - items, _ := t.itemsByFlow[item.key] + items := t.itemsByFlow[item.key] items[i] = item } func (t *tcpGROTable) deleteAt(key tcpFlowKey, i int) { - items, _ := t.itemsByFlow[key] + items := t.itemsByFlow[key] items = append(items[:i], items[i+1:]...) t.itemsByFlow[key] = items } @@ -254,7 +254,7 @@ func (u *udpGROTable) insert(pkt []byte, srcAddrOffset, dstAddrOffset, udphOffse } func (u *udpGROTable) updateAt(item udpGROItem, i int) { - items, _ := u.itemsByFlow[item.key] + items := u.itemsByFlow[item.key] items[i] = item } From b49e63f8efdbe7071ec0784829833010fbfb4567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 17 Oct 2025 16:26:45 +0800 Subject: [PATCH 086/121] Fix compatibility with MPTCP --- redirect_nftables_rules.go | 37 +++++++++++++++++++++++++++++++++++++ tun.go | 1 + 2 files changed, 38 insertions(+) diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index ba4ee872..2d71fff1 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -534,6 +534,43 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft nftablesCreateExcludeDestinationIPSet(nft, table, chain, 4, "inet6_route_exclude_address_set", nftables.TableFamilyIPv6, false) } + mptcpVerdict := expr.VerdictDrop + if r.tunOptions.ExcludeMPTCP { + mptcpVerdict = expr.VerdictReturn + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyL4PROTO, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{unix.IPPROTO_TCP}, + }, + &expr.Exthdr{ + DestRegister: 1, + Type: 30, + Offset: 0, + Len: 1, + Flags: unix.NFT_EXTHDR_F_PRESENT, + Op: expr.ExthdrOpTcpopt, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{1}, + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: mptcpVerdict, + }, + }, + }) + return nil } diff --git a/tun.go b/tun.go index d831742f..c32cd8ad 100644 --- a/tun.go +++ b/tun.go @@ -83,6 +83,7 @@ type Options struct { AutoRedirectMarkMode bool AutoRedirectInputMark uint32 AutoRedirectOutputMark uint32 + ExcludeMPTCP bool Inet4LoopbackAddress []netip.Addr Inet6LoopbackAddress []netip.Addr StrictRoute bool From 20161f3059d5286694848ffa357c4f3726a59895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 21 Oct 2025 20:41:16 +0800 Subject: [PATCH 087/121] redirect: Fix compatibility with `/product/bin/su` --- redirect_linux.go | 1 + 1 file changed, 1 insertion(+) diff --git a/redirect_linux.go b/redirect_linux.go index 5441bc10..8c97d07b 100644 --- a/redirect_linux.go +++ b/redirect_linux.go @@ -69,6 +69,7 @@ func (r *autoRedirect) Start() error { r.androidSu = true for _, suPath := range []string{ "su", + "/product/bin/su", "/system/bin/su", } { r.suPath, err = exec.LookPath(suPath) From e9e3fbf0c15e4502baec235066f9dee903ac2b50 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Mon, 1 Dec 2025 08:47:38 +0800 Subject: [PATCH 088/121] Apply ping destination filter for Windows --- ping/destination.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ping/destination.go b/ping/destination.go index 23db6745..60decb4c 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -189,7 +189,7 @@ func (d *Destination) WritePacket(packet *buf.Buffer) error { } func (d *Destination) needFilter() bool { - return runtime.GOOS != "windows" && !d.conn.isLinuxUnprivileged() + return !d.conn.isLinuxUnprivileged() } func (d *Destination) registerRequest(request pingRequest) { From 6516c2d8f1dee003e0612055c5849b66c8b12021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 17 Dec 2025 19:44:39 +0800 Subject: [PATCH 089/121] Fix race condition in ReadPacket --- tun_windows.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tun_windows.go b/tun_windows.go index e62789ca..f33addf1 100644 --- a/tun_windows.go +++ b/tun_windows.go @@ -395,15 +395,16 @@ retry: func (t *NativeTun) ReadPacket() ([]byte, func(), error) { t.running.Add(1) - defer t.running.Done() retry: if t.close.Load() == 1 { + t.running.Done() return nil, nil, os.ErrClosed } start := nanotime() shouldSpin := t.rate.current.Load() >= spinloopRateThreshold && uint64(start-t.rate.nextStartTime.Load()) <= rateMeasurementGranularity*2 for { if t.close.Load() == 1 { + t.running.Done() return nil, nil, os.ErrClosed } packet, err := t.session.ReceivePacket() @@ -411,7 +412,10 @@ retry: case nil: packetSize := len(packet) t.rate.update(uint64(packetSize)) - return packet, func() { t.session.ReleaseReceivePacket(packet) }, nil + return packet, func() { + t.session.ReleaseReceivePacket(packet) + t.running.Done() + }, nil case windows.ERROR_NO_MORE_ITEMS: if !shouldSpin || uint64(nanotime()-start) >= spinloopDuration { windows.WaitForSingleObject(t.readWait, windows.INFINITE) @@ -420,10 +424,13 @@ retry: procyield(1) continue case windows.ERROR_HANDLE_EOF: + t.running.Done() return nil, nil, os.ErrClosed case windows.ERROR_INVALID_DATA: + t.running.Done() return nil, nil, errors.New("send ring corrupt") } + t.running.Done() return nil, nil, fmt.Errorf("read failed: %w", err) } } From a850c4f8a1c85fcd00bf9bc0cd430ce5b86cd0d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 26 Dec 2025 05:56:29 +0800 Subject: [PATCH 090/121] Add pre-matching support for auto redirect --- go.mod | 3 +- go.sum | 2 + nfqueue_linux.go | 244 +++++++++++++++++++++++++++++++++++++ redirect.go | 5 +- redirect_linux.go | 52 +++++++- redirect_nftables.go | 137 ++++++++++++++++++++- redirect_nftables_rules.go | 42 +++++++ stack.go | 5 +- tun.go | 2 + 9 files changed, 480 insertions(+), 12 deletions(-) create mode 100644 nfqueue_linux.go diff --git a/go.mod b/go.mod index ff61d7c1..86d0da1f 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,10 @@ module github.com/sagernet/sing-tun go 1.24.7 require ( + github.com/florianl/go-nfqueue/v2 v2.0.2 github.com/go-ole/go-ole v1.3.0 github.com/google/btree v1.1.3 + github.com/mdlayher/netlink v1.7.2 github.com/sagernet/fswatch v0.1.1 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a @@ -22,7 +24,6 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/vishvananda/netns v0.0.4 // indirect diff --git a/go.sum b/go.sum index e3c486b3..d9664e2f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE= +github.com/florianl/go-nfqueue/v2 v2.0.2/go.mod h1:VA09+iPOT43OMoCKNfXHyzujQUty2xmzyCRkBOlmabc= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= diff --git a/nfqueue_linux.go b/nfqueue_linux.go new file mode 100644 index 00000000..10f253f6 --- /dev/null +++ b/nfqueue_linux.go @@ -0,0 +1,244 @@ +//go:build linux + +package tun + +import ( + "context" + "errors" + "sync" + "sync/atomic" + + "github.com/sagernet/sing-tun/internal/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" + "golang.org/x/sys/unix" +) + +const nfqueueMaxPacketLen = 512 + +type nfqueueHandler struct { + ctx context.Context + cancel context.CancelFunc + handler Handler + logger logger.Logger + nfq *nfqueue.Nfqueue + queue uint16 + outputMark uint32 + resetMark uint32 + wg sync.WaitGroup + closed atomic.Bool +} + +type nfqueueOptions struct { + Context context.Context + Handler Handler + Logger logger.Logger + Queue uint16 + OutputMark uint32 + ResetMark uint32 +} + +func newNFQueueHandler(options nfqueueOptions) (*nfqueueHandler, error) { + ctx, cancel := context.WithCancel(options.Context) + return &nfqueueHandler{ + ctx: ctx, + cancel: cancel, + handler: options.Handler, + logger: options.Logger, + queue: options.Queue, + outputMark: options.OutputMark, + resetMark: options.ResetMark, + }, nil +} + +func (h *nfqueueHandler) setVerdict(packetID uint32, verdict int, mark uint32) { + var err error + if mark != 0 { + err = h.nfq.SetVerdictWithOption(packetID, verdict, nfqueue.WithMark(mark)) + } else { + err = h.nfq.SetVerdict(packetID, verdict) + } + if err != nil && !h.closed.Load() && h.ctx.Err() == nil { + h.logger.Trace(E.Cause(err, "set verdict")) + } +} + +func (h *nfqueueHandler) Start() error { + config := nfqueue.Config{ + NfQueue: h.queue, + MaxPacketLen: nfqueueMaxPacketLen, + MaxQueueLen: 4096, + Copymode: nfqueue.NfQnlCopyPacket, + AfFamily: unix.AF_UNSPEC, + Flags: nfqueue.NfQaCfgFlagFailOpen, + } + + nfq, err := nfqueue.Open(&config) + if err != nil { + return E.Cause(err, "open nfqueue") + } + h.nfq = nfq + + if err = nfq.SetOption(netlink.NoENOBUFS, true); err != nil { + h.nfq.Close() + return E.Cause(err, "set nfqueue option") + } + + h.wg.Add(1) + go func() { + defer h.wg.Done() + err := nfq.RegisterWithErrorFunc(h.ctx, h.handlePacket, func(e error) int { + if h.ctx.Err() != nil { + return 1 + } + h.logger.Error("nfqueue error: ", e) + return 0 + }) + if err != nil && h.ctx.Err() == nil { + h.logger.Error("nfqueue register error: ", err) + } + }() + + return nil +} + +func parseIPv6TransportHeader(payload []byte) (transportProto uint8, transportOffset int, ok bool) { + if len(payload) < header.IPv6MinimumSize { + return 0, 0, false + } + + ipv6 := header.IPv6(payload) + nextHeader := ipv6.NextHeader() + offset := header.IPv6MinimumSize + + for { + switch nextHeader { + case unix.IPPROTO_HOPOPTS, + unix.IPPROTO_ROUTING, + unix.IPPROTO_DSTOPTS: + if len(payload) < offset+2 { + return 0, 0, false + } + nextHeader = payload[offset] + extLen := int(payload[offset+1]+1) * 8 + if len(payload) < offset+extLen { + return 0, 0, false + } + offset += extLen + + case unix.IPPROTO_FRAGMENT: + if len(payload) < offset+8 { + return 0, 0, false + } + nextHeader = payload[offset] + offset += 8 + + case unix.IPPROTO_AH: + if len(payload) < offset+2 { + return 0, 0, false + } + nextHeader = payload[offset] + extLen := int(payload[offset+1]+2) * 4 + if len(payload) < offset+extLen { + return 0, 0, false + } + offset += extLen + + case unix.IPPROTO_NONE: + return 0, 0, false + + default: + return nextHeader, offset, true + } + } +} + +func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int { + if h.closed.Load() { + return 0 + } + if attr.PacketID == nil || attr.Payload == nil { + return 0 + } + + packetID := *attr.PacketID + payload := *attr.Payload + + if len(payload) < header.IPv4MinimumSize { + h.setVerdict(packetID, nfqueue.NfAccept, 0) + return 0 + } + + var srcAddr, dstAddr M.Socksaddr + var tcpOffset int + + version := payload[0] >> 4 + if version == 4 { + ipv4 := header.IPv4(payload) + if !ipv4.IsValid(len(payload)) || ipv4.Protocol() != uint8(unix.IPPROTO_TCP) { + h.setVerdict(packetID, nfqueue.NfAccept, 0) + return 0 + } + srcAddr = M.SocksaddrFrom(ipv4.SourceAddr(), 0) + dstAddr = M.SocksaddrFrom(ipv4.DestinationAddr(), 0) + tcpOffset = int(ipv4.HeaderLength()) + } else if version == 6 { + transportProto, transportOffset, ok := parseIPv6TransportHeader(payload) + if !ok || transportProto != unix.IPPROTO_TCP { + h.setVerdict(packetID, nfqueue.NfAccept, 0) + return 0 + } + ipv6 := header.IPv6(payload) + srcAddr = M.SocksaddrFrom(ipv6.SourceAddr(), 0) + dstAddr = M.SocksaddrFrom(ipv6.DestinationAddr(), 0) + tcpOffset = transportOffset + } else { + h.setVerdict(packetID, nfqueue.NfAccept, 0) + return 0 + } + + if len(payload) < tcpOffset+header.TCPMinimumSize { + h.setVerdict(packetID, nfqueue.NfAccept, 0) + return 0 + } + + 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) { + h.setVerdict(packetID, nfqueue.NfAccept, 0) + return 0 + } + + _, pErr := h.handler.PrepareConnection(N.NetworkTCP, srcAddr, dstAddr, nil, 0) + + switch { + case errors.Is(pErr, ErrBypass): + h.setVerdict(packetID, nfqueue.NfAccept, h.outputMark) + case errors.Is(pErr, ErrReset): + h.setVerdict(packetID, nfqueue.NfAccept, h.resetMark) + case errors.Is(pErr, ErrDrop): + h.setVerdict(packetID, nfqueue.NfDrop, 0) + default: + h.setVerdict(packetID, nfqueue.NfAccept, 0) + } + + return 0 +} + +func (h *nfqueueHandler) Close() error { + h.closed.Store(true) + h.cancel() + if h.nfq != nil { + h.nfq.Close() + } + h.wg.Wait() + return nil +} diff --git a/redirect.go b/redirect.go index 0569eb36..dcf3e720 100644 --- a/redirect.go +++ b/redirect.go @@ -5,7 +5,6 @@ import ( "github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/logger" - N "github.com/sagernet/sing/common/network" "go4.org/netipx" ) @@ -13,6 +12,8 @@ import ( const ( DefaultAutoRedirectInputMark = 0x2023 DefaultAutoRedirectOutputMark = 0x2024 + DefaultAutoRedirectResetMark = 0x2025 + DefaultAutoRedirectNFQueue = 100 ) type AutoRedirect interface { @@ -24,7 +25,7 @@ type AutoRedirect interface { type AutoRedirectOptions struct { TunOptions *Options Context context.Context - Handler N.TCPConnectionHandlerEx + Handler Handler Logger logger.Logger NetworkMonitor NetworkUpdateMonitor InterfaceFinder control.InterfaceFinder diff --git a/redirect_linux.go b/redirect_linux.go index 8c97d07b..2a638ee4 100644 --- a/redirect_linux.go +++ b/redirect_linux.go @@ -13,7 +13,6 @@ import ( 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/sagernet/sing/common/x/list" "go4.org/netipx" @@ -22,7 +21,7 @@ import ( type autoRedirect struct { tunOptions *Options ctx context.Context - handler N.TCPConnectionHandlerEx + handler Handler logger logger.Logger tableName string networkMonitor NetworkUpdateMonitor @@ -41,6 +40,8 @@ type autoRedirect struct { suPath string routeAddressSet *[]*netipx.IPSet routeExcludeAddressSet *[]*netipx.IPSet + nfqueueHandler *nfqueueHandler + nfqueueEnabled bool } func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { @@ -125,13 +126,30 @@ func (r *autoRedirect) Start() error { listenAddr = netip.IPv4Unspecified() } server := newRedirectServer(r.ctx, r.handler, r.logger, listenAddr) - err := server.Start() + err = server.Start() if err != nil { return E.Cause(err, "start redirect server") } 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: ", err) + } else if err = handler.Start(); err != nil { + r.logger.Warn("nfqueue start failed, pre-match disabled: ", err) + } else { + r.nfqueueHandler = handler + r.nfqueueEnabled = true + } r.cleanupNFTables() err = r.setupNFTables() } else { @@ -142,6 +160,9 @@ func (r *autoRedirect) Start() error { } func (r *autoRedirect) Close() error { + if r.nfqueueHandler != nil { + r.nfqueueHandler.Close() + } if r.useNFTables { r.cleanupNFTables() } else { @@ -181,3 +202,28 @@ func (r *autoRedirect) redirectPort() uint16 { } return M.AddrPortFromNet(r.redirectServer.listener.Addr()).Port() } + +func (r *autoRedirect) effectiveOutputMark() uint32 { + if r.tunOptions.AutoRedirectOutputMark != 0 { + return r.tunOptions.AutoRedirectOutputMark + } + return DefaultAutoRedirectOutputMark +} + +func (r *autoRedirect) effectiveResetMark() uint32 { + if r.tunOptions.AutoRedirectResetMark != 0 { + return r.tunOptions.AutoRedirectResetMark + } + return DefaultAutoRedirectResetMark +} + +func (r *autoRedirect) effectiveNFQueue() uint16 { + if r.tunOptions.AutoRedirectNFQueue != 0 { + return r.tunOptions.AutoRedirectNFQueue + } + return DefaultAutoRedirectNFQueue +} + +func (r *autoRedirect) shouldSkipOutputChain() bool { + return len(r.tunOptions.IncludeInterface) > 0 && !common.Contains(r.tunOptions.IncludeInterface, "lo") || common.Contains(r.tunOptions.ExcludeInterface, "lo") +} diff --git a/redirect_nftables.go b/redirect_nftables.go index 01239532..4dea1c1d 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -51,13 +51,23 @@ func (r *autoRedirect) setupNFTables() error { return err } - skipOutput := len(r.tunOptions.IncludeInterface) > 0 && !common.Contains(r.tunOptions.IncludeInterface, "lo") || common.Contains(r.tunOptions.ExcludeInterface, "lo") - if !skipOutput { + if r.nfqueueEnabled { + err = r.nftablesCreatePreMatchChains(nft, table) + if err != nil { + return err + } + } + + if !r.shouldSkipOutputChain() { + outputNATPriority := nftables.ChainPriorityMangle + if r.nfqueueEnabled { + outputNATPriority = nftables.ChainPriorityRef(*nftables.ChainPriorityMangle + 1) + } chainOutput := nft.AddChain(&nftables.Chain{ Name: "output", Table: table, Hooknum: nftables.ChainHookOutput, - Priority: nftables.ChainPriorityMangle, + Priority: outputNATPriority, Type: nftables.ChainTypeNAT, }) if r.tunOptions.AutoRedirectMarkMode { @@ -267,7 +277,7 @@ func (r *autoRedirect) setupNFTables() error { return nil } -// TODO; test is this works +// TODO: test if this works func (r *autoRedirect) nftablesUpdateLocalAddressSet() error { newLocalAddresses := common.FlatMap(r.interfaceFinder.Interfaces(), func(it control.Interface) []netip.Prefix { return common.Filter(it.Addresses, func(prefix netip.Prefix) bool { @@ -327,3 +337,122 @@ func (r *autoRedirect) cleanupNFTables() { _ = nft.Flush() _ = nft.CloseLasting() } + +func (r *autoRedirect) nftablesCreatePreMatchChains(nft *nftables.Conn, table *nftables.Table) error { + chainPreroutingPreMatch := nft.AddChain(&nftables.Chain{ + Name: "prerouting_prematch", + Table: table, + Hooknum: nftables.ChainHookPrerouting, + Priority: nftables.ChainPriorityRef(*nftables.ChainPriorityNATDest - 1), + Type: nftables.ChainTypeFilter, + }) + r.nftablesAddPreMatchRules(nft, table, chainPreroutingPreMatch, true) + + if !r.shouldSkipOutputChain() { + chainOutputPreMatch := nft.AddChain(&nftables.Chain{ + Name: "output_prematch", + Table: table, + Hooknum: nftables.ChainHookOutput, + Priority: nftables.ChainPriorityRef(*nftables.ChainPriorityMangle - 1), + Type: nftables.ChainTypeFilter, + }) + r.nftablesAddPreMatchRules(nft, table, chainOutputPreMatch, false) + } + + return nil +} + +func (r *autoRedirect) nftablesAddPreMatchRules(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, isPrerouting bool) { + ifnameKey := expr.MetaKeyOIFNAME + if isPrerouting { + ifnameKey = expr.MetaKeyIIFNAME + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: ifnameKey, Register: 1}, + &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: nftablesIfname(r.tunOptions.Name)}, + &expr.Verdict{Kind: expr.VerdictReturn}, + }, + }) + + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1}, + &expr.Cmp{Op: expr.CmpOpNeq, Register: 1, Data: []byte{unix.IPPROTO_TCP}}, + &expr.Verdict{Kind: expr.VerdictReturn}, + }, + }) + + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeyMARK, Register: 1}, + &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.effectiveOutputMark())}, + &expr.Verdict{Kind: expr.VerdictReturn}, + }, + }) + + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Ct{Key: expr.CtKeyMARK, Register: 1}, + &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.effectiveOutputMark())}, + &expr.Verdict{Kind: expr.VerdictReturn}, + }, + }) + + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseTransportHeader, + Offset: 13, + Len: 1, + }, + &expr.Bitwise{ + SourceRegister: 1, + DestRegister: 1, + Len: 1, + Mask: []byte{0x12}, + Xor: []byte{0x00}, + }, + &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{0x02}}, + &expr.Counter{}, + &expr.Queue{ + Num: r.effectiveNFQueue(), + Flag: expr.QueueFlagBypass, + }, + }, + }) + + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeyMARK, Register: 1}, + &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.effectiveResetMark())}, + &expr.Counter{}, + &expr.Reject{Type: unix.NFT_REJECT_TCP_RST}, + }, + }) + + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeyMARK, Register: 1}, + &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.effectiveOutputMark())}, + &expr.Ct{Key: expr.CtKeyMARK, Register: 1, SourceRegister: true}, + &expr.Counter{}, + }, + }) +} diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index 2d71fff1..ea03d405 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -213,6 +213,48 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft }) } } + if r.nfqueueEnabled && chain.Hooknum == nftables.ChainHookPrerouting && chain.Type == nftables.ChainTypeNAT { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Ct{ + Key: expr.CtKeyMARK, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: binaryutil.NativeEndian.PutUint32(r.effectiveOutputMark()), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } + if r.nfqueueEnabled && chain.Hooknum == nftables.ChainHookOutput && chain.Type == nftables.ChainTypeNAT { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Ct{ + Key: expr.CtKeyMARK, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: binaryutil.NativeEndian.PutUint32(r.effectiveOutputMark()), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } if chain.Hooknum == nftables.ChainHookPrerouting { nft.AddRule(&nftables.Rule{ Table: table, diff --git a/stack.go b/stack.go index 7014c6d3..7c34c798 100644 --- a/stack.go +++ b/stack.go @@ -13,8 +13,9 @@ import ( ) var ( - ErrDrop = E.New("drop by rule") - ErrReset = E.New("reset by rule") + ErrDrop = E.New("drop by rule") + ErrReset = E.New("reset by rule") + ErrBypass = E.New("bypass by rule") ) type Stack interface { diff --git a/tun.go b/tun.go index c32cd8ad..f4b60c12 100644 --- a/tun.go +++ b/tun.go @@ -83,6 +83,8 @@ type Options struct { AutoRedirectMarkMode bool AutoRedirectInputMark uint32 AutoRedirectOutputMark uint32 + AutoRedirectResetMark uint32 + AutoRedirectNFQueue uint16 ExcludeMPTCP bool Inet4LoopbackAddress []netip.Addr Inet6LoopbackAddress []netip.Addr From a5db80d710814a5d0209cfd7d9b9b40f24975b9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 31 Dec 2025 02:18:51 +0800 Subject: [PATCH 091/121] Fix nfqueue fallback when kernel module unavailable --- nfqueue_linux.go | 30 ++++++++++++------------------ redirect_linux.go | 4 ++-- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/nfqueue_linux.go b/nfqueue_linux.go index 10f253f6..e52cd468 100644 --- a/nfqueue_linux.go +++ b/nfqueue_linux.go @@ -5,7 +5,6 @@ package tun import ( "context" "errors" - "sync" "sync/atomic" "github.com/sagernet/sing-tun/internal/gtcpip/header" @@ -30,7 +29,6 @@ type nfqueueHandler struct { queue uint16 outputMark uint32 resetMark uint32 - wg sync.WaitGroup closed atomic.Bool } @@ -82,28 +80,25 @@ func (h *nfqueueHandler) Start() error { if err != nil { return E.Cause(err, "open nfqueue") } - h.nfq = nfq if err = nfq.SetOption(netlink.NoENOBUFS, true); err != nil { - h.nfq.Close() + nfq.Close() return E.Cause(err, "set nfqueue option") } - h.wg.Add(1) - go func() { - defer h.wg.Done() - err := nfq.RegisterWithErrorFunc(h.ctx, h.handlePacket, func(e error) int { - if h.ctx.Err() != nil { - return 1 - } - h.logger.Error("nfqueue error: ", e) - return 0 - }) - if err != nil && h.ctx.Err() == nil { - h.logger.Error("nfqueue register error: ", err) + err = nfq.RegisterWithErrorFunc(h.ctx, h.handlePacket, func(e error) int { + if h.ctx.Err() != nil { + return 1 } - }() + h.logger.Error("nfqueue error: ", e) + return 0 + }) + if err != nil { + nfq.Close() + return E.Cause(err, "register nfqueue") + } + h.nfq = nfq return nil } @@ -239,6 +234,5 @@ func (h *nfqueueHandler) Close() error { if h.nfq != nil { h.nfq.Close() } - h.wg.Wait() return nil } diff --git a/redirect_linux.go b/redirect_linux.go index 2a638ee4..5bea0c45 100644 --- a/redirect_linux.go +++ b/redirect_linux.go @@ -143,9 +143,9 @@ func (r *autoRedirect) Start() error { ResetMark: r.effectiveResetMark(), }) if err != nil { - r.logger.Warn("nfqueue not available, pre-match disabled: ", err) + 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: ", err) + 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 From 525f783d005b617df46ec38c269880398ff4aae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 7 Jan 2026 14:05:47 +0800 Subject: [PATCH 092/121] Add EXP_ExternalConfiguration for Tailscale --- tun.go | 3 ++- tun_darwin.go | 14 +++++++++++++- tun_linux.go | 43 ++++++++++++++++++++++++++++--------------- tun_windows.go | 25 +++++++++++++++++++++++-- 4 files changed, 66 insertions(+), 19 deletions(-) diff --git a/tun.go b/tun.go index f4b60c12..ff77cf70 100644 --- a/tun.go +++ b/tun.go @@ -109,7 +109,8 @@ type Options struct { _TXChecksumOffload bool // For library usages. - EXP_DisableDNSHijack bool + EXP_DisableDNSHijack bool + EXP_ExternalConfiguration bool // For gvisor stack, it should be enabled when MTU is less than 32768; otherwise it should be less than or equal to 8192. // The above condition is just an estimate and not exact, calculated on M4 pro. diff --git a/tun_darwin.go b/tun_darwin.go index 45efa291..36a0b115 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -146,11 +146,17 @@ func New(options Options) (Tun, error) { } func (t *NativeTun) Start() error { + if t.options.EXP_ExternalConfiguration { + return nil + } t.options.InterfaceMonitor.RegisterMyInterface(t.options.Name) return t.setRoutes() } func (t *NativeTun) Close() error { + if t.options.EXP_ExternalConfiguration { + return t.tunFile.Close() + } defer flushDNSCache() return E.Errors(t.unsetRoutes(), t.tunFile.Close()) } @@ -232,6 +238,9 @@ func create(tunFd int, ifIndex int, name string, options Options) error { if err != nil { return os.NewSyscallError("IoctlSetIfreqMTU", err) } + if options.EXP_ExternalConfiguration { + return nil + } if len(options.Inet4Address) > 0 { for _, address := range options.Inet4Address { ifReq := ifAliasReq{ @@ -396,11 +405,14 @@ func (t *NativeTun) TXChecksumOffload() bool { } func (t *NativeTun) UpdateRouteOptions(tunOptions Options) error { + t.options = tunOptions + if t.options.EXP_ExternalConfiguration { + return nil + } err := t.unsetRoutes() if err != nil { return err } - t.options = tunOptions return t.setRoutes() } diff --git a/tun_linux.go b/tun_linux.go index b19d5daa..7f22644e 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -125,22 +125,23 @@ func (t *NativeTun) configure(tunLink netlink.Link) error { } else if err != nil { return err } - - if len(t.options.Inet4Address) > 0 { - for _, address := range t.options.Inet4Address { - addr4, _ := netlink.ParseAddr(address.String()) - err = netlink.AddrAdd(tunLink, addr4) - if err != nil { - return err + if !t.options.EXP_ExternalConfiguration { + if len(t.options.Inet4Address) > 0 { + for _, address := range t.options.Inet4Address { + addr4, _ := netlink.ParseAddr(address.String()) + err = netlink.AddrAdd(tunLink, addr4) + if err != nil { + return err + } } } - } - if len(t.options.Inet6Address) > 0 { - for _, address := range t.options.Inet6Address { - addr6, _ := netlink.ParseAddr(address.String()) - err = netlink.AddrAdd(tunLink, addr6) - if err != nil { - return err + if len(t.options.Inet6Address) > 0 { + for _, address := range t.options.Inet6Address { + addr6, _ := netlink.ParseAddr(address.String()) + err = netlink.AddrAdd(tunLink, addr6) + if err != nil { + return err + } } } } @@ -257,7 +258,9 @@ func (t *NativeTun) Start() error { if t.options.FileDescriptor != 0 { return nil } - t.options.InterfaceMonitor.RegisterMyInterface(t.options.Name) + if !t.options.EXP_ExternalConfiguration { + t.options.InterfaceMonitor.RegisterMyInterface(t.options.Name) + } tunLink, err := netlink.LinkByName(t.options.Name) if err != nil { return err @@ -277,6 +280,10 @@ func (t *NativeTun) Start() error { } } + if t.options.EXP_ExternalConfiguration { + return nil + } + if t.options.IPRoute2TableIndex == 0 { for { t.options.IPRoute2TableIndex = int(rand.Uint32()) @@ -315,6 +322,9 @@ func (t *NativeTun) Close() error { if t.interfaceCallback != nil { t.options.InterfaceMonitor.UnregisterCallback(t.interfaceCallback) } + if t.options.EXP_ExternalConfiguration { + return common.Close(common.PtrOrNil(t.tunFile)) + } return E.Errors(t.unsetRoute(), t.unsetRules(), common.Close(common.PtrOrNil(t.tunFile))) } @@ -476,6 +486,9 @@ func prefixToIPNet(prefix netip.Prefix) *net.IPNet { func (t *NativeTun) UpdateRouteOptions(tunOptions Options) error { if t.options.FileDescriptor > 0 { return nil + } else if t.options.EXP_ExternalConfiguration { + t.options = tunOptions + return nil } else if !t.options.AutoRoute { t.options = tunOptions return nil diff --git a/tun_windows.go b/tun_windows.go index f33addf1..60c43e31 100644 --- a/tun_windows.go +++ b/tun_windows.go @@ -65,6 +65,9 @@ func New(options Options) (WinTun, error) { } func (t *NativeTun) configure() error { + if t.options.EXP_ExternalConfiguration { + return nil + } luid := winipcfg.LUID(t.adapter.LUID()) if len(t.options.Inet4Address) > 0 { err := luid.SetIPAddressesForFamily(winipcfg.AddressFamily(windows.AF_INET), t.options.Inet4Address) @@ -162,10 +165,10 @@ func (t *NativeTun) Name() (string, error) { } func (t *NativeTun) Start() error { - t.options.InterfaceMonitor.RegisterMyInterface(t.options.Name) - if !t.options.AutoRoute { + if t.options.EXP_ExternalConfiguration || !t.options.AutoRoute { return nil } + t.options.InterfaceMonitor.RegisterMyInterface(t.options.Name) luid := winipcfg.LUID(t.adapter.LUID()) gateway4, gateway6 := t.options.Inet4GatewayAddr(), t.options.Inet6GatewayAddr() routeRanges, err := t.options.BuildAutoRouteRanges(false) @@ -393,6 +396,21 @@ retry: } } +func (t *NativeTun) MTU() (int, error) { + return int(t.options.MTU), nil +} + +func (t *NativeTun) ForceMTU(mtu int) { + if mtu <= 0 { + return + } + t.options.MTU = uint32(mtu) +} + +func (t *NativeTun) LUID() uint64 { + return t.adapter.LUID() +} + func (t *NativeTun) ReadPacket() ([]byte, func(), error) { t.running.Add(1) retry: @@ -543,6 +561,9 @@ func (t *NativeTun) Close() error { func (t *NativeTun) UpdateRouteOptions(tunOptions Options) error { t.options = tunOptions + if t.options.EXP_ExternalConfiguration { + return nil + } if !t.options.AutoRoute { return nil } From 9d97d95a9cd52050b83a836e14172e98a9ab04e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 4 Jan 2026 23:24:29 +0800 Subject: [PATCH 093/121] Fix TUN interface restart fails with existing addresses --- tun_linux.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tun_linux.go b/tun_linux.go index 7f22644e..abca009a 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -130,7 +130,7 @@ func (t *NativeTun) configure(tunLink netlink.Link) error { for _, address := range t.options.Inet4Address { addr4, _ := netlink.ParseAddr(address.String()) err = netlink.AddrAdd(tunLink, addr4) - if err != nil { + if err != nil && !errors.Is(err, unix.EEXIST) { return err } } @@ -139,7 +139,7 @@ func (t *NativeTun) configure(tunLink netlink.Link) error { for _, address := range t.options.Inet6Address { addr6, _ := netlink.ParseAddr(address.String()) err = netlink.AddrAdd(tunLink, addr6) - if err != nil { + if err != nil && !errors.Is(err, unix.EEXIST) { return err } } @@ -325,6 +325,7 @@ func (t *NativeTun) Close() error { if t.options.EXP_ExternalConfiguration { return common.Close(common.PtrOrNil(t.tunFile)) } + t.unsetAddresses() return E.Errors(t.unsetRoute(), t.unsetRules(), common.Close(common.PtrOrNil(t.tunFile))) } @@ -1016,6 +1017,24 @@ func (t *NativeTun) unsetRules() error { return nil } +func (t *NativeTun) unsetAddresses() { + if t.options.FileDescriptor > 0 { + return + } + tunLink, err := netlink.LinkByName(t.options.Name) + if err != nil { + return + } + for _, address := range t.options.Inet4Address { + addr, _ := netlink.ParseAddr(address.String()) + _ = netlink.AddrDel(tunLink, addr) + } + for _, address := range t.options.Inet6Address { + addr, _ := netlink.ParseAddr(address.String()) + _ = netlink.AddrDel(tunLink, addr) + } +} + func (t *NativeTun) resetRules() error { t.unsetRules() return t.setRules() From 2f53768be2c9a79a0a8335cf3ab228821c41a794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 15 Jan 2026 02:01:35 +0800 Subject: [PATCH 094/121] Fix darwin batch read not exit on stop --- tun_darwin.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tun_darwin.go b/tun_darwin.go index 36a0b115..8aa6923f 100644 --- a/tun_darwin.go +++ b/tun_darwin.go @@ -158,7 +158,10 @@ func (t *NativeTun) Close() error { return t.tunFile.Close() } defer flushDNSCache() - return E.Errors(t.unsetRoutes(), t.tunFile.Close()) + t.stopFd.Stop() + err := E.Errors(t.unsetRoutes(), t.tunFile.Close()) + t.stopFd.Close() + return err } func (t *NativeTun) Read(p []byte) (n int, err error) { @@ -356,6 +359,9 @@ func (t *NativeTun) BatchRead() ([]*buf.Buffer, error) { t.buffers = t.buffers[:0] return nil, errno } + if n < 0 { + return nil, os.ErrClosed + } if n < 1 { return nil, nil } From a23b66f7213b855f362e64201c71d10c3455eabb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 17 Jan 2026 18:16:51 +0800 Subject: [PATCH 095/121] Skip strict routing on Windows versions below 10 --- tun_windows.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tun_windows.go b/tun_windows.go index 60c43e31..54e73954 100644 --- a/tun_windows.go +++ b/tun_windows.go @@ -184,6 +184,13 @@ func (t *NativeTun) Start() error { return err } if t.options.StrictRoute { + major, _, _ := windows.RtlGetNtVersionNumbers() + if major < 10 { + if t.options.Logger != nil { + t.options.Logger.Warn("strict routing is not supported on Windows versions below 10") + } + return nil + } var engine uintptr session := &winsys.FWPM_SESSION0{Flags: winsys.FWPM_SESSION_FLAG_DYNAMIC} err := winsys.FwpmEngineOpen0(nil, winsys.RPC_C_AUTHN_DEFAULT, nil, session, unsafe.Pointer(&engine)) From 2377e62d4a4ecce2b84281d16d7de612c498cd1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 17 Jan 2026 20:42:31 +0800 Subject: [PATCH 096/121] Fix IPv6 ULA addresses excluded from local address set The filter condition `IsGlobalUnicast() && !IsPrivate()` incorrectly excluded ULA addresses (fc00::/7) from inet6_local_address_set, causing DNS hijack to fail for IPv6 ULA addresses. Fixes: sagernet/sing-box#3698 --- redirect_nftables_rules.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index ea03d405..08f6baa6 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -103,10 +103,6 @@ func (r *autoRedirect) nftablesCreateLocalAddressSets( update = true } } - localAddresses6 = common.Filter(localAddresses6, func(it netip.Prefix) bool { - address := it.Addr() - return address.IsLoopback() || address.IsGlobalUnicast() && !address.IsPrivate() - }) if len(lastAddresses) == 0 || update { _, err := nftablesCreateIPSet(nft, table, 6, "inet6_local_address_set", nftables.TableFamilyIPv6, nil, localAddresses6, false, update) if err != nil { From 381bf9d40d6b828d3d218d29ec520a04b38c051a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 28 Jan 2026 14:34:10 +0800 Subject: [PATCH 097/121] Skip tun interface traffic in prerouting UDP/ICMP chain --- redirect_nftables.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/redirect_nftables.go b/redirect_nftables.go index 4dea1c1d..f7253ccc 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -192,6 +192,25 @@ func (r *autoRedirect) setupNFTables() error { }, }, }) + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chainPreRoutingUDP, + Exprs: []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyIIFNAME, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: nftablesIfname(r.tunOptions.Name), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) nft.AddRule(&nftables.Rule{ Table: table, Chain: chainPreRoutingUDP, From e88ed52dbc1e402f503fd8ce225318ce961abfa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 28 Jan 2026 15:47:10 +0800 Subject: [PATCH 098/121] Fix auto_redirect on IPv6-only or IPv4-only servers --- tun_linux.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tun_linux.go b/tun_linux.go index abca009a..e4d1044b 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -630,6 +630,23 @@ func (t *NativeTun) rules() []*netlink.Rule { it.Family = unix.AF_INET6 rules = append(rules, it) } + // Fallback rules after system default rules (32766: main, 32767: default) + // Only reached when main and default tables have no route + const fallbackPriority = 32768 + if p4 { + it = netlink.NewRule() + it.Priority = fallbackPriority + it.Table = t.options.IPRoute2TableIndex + it.Family = unix.AF_INET + rules = append(rules, it) + } + if p6 { + it = netlink.NewRule() + it.Priority = fallbackPriority + it.Table = t.options.IPRoute2TableIndex + it.Family = unix.AF_INET6 + rules = append(rules, it) + } return rules } From 1d02d635b95b5f057adaa168193256e4a06df260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 29 Jan 2026 11:41:27 +0800 Subject: [PATCH 099/121] Fix auto_redirect fallback rule --- tun.go | 78 +++++++++++++++++++++++++++------------------------- tun_linux.go | 18 ++---------- 2 files changed, 43 insertions(+), 53 deletions(-) diff --git a/tun.go b/tun.go index ff77cf70..35cd0956 100644 --- a/tun.go +++ b/tun.go @@ -63,47 +63,49 @@ type DarwinTUN interface { } const ( - DefaultIPRoute2TableIndex = 2022 - DefaultIPRoute2RuleIndex = 9000 + DefaultIPRoute2TableIndex = 2022 + DefaultIPRoute2RuleIndex = 9000 + DefaultIPRoute2AutoRedirectFallbackRuleIndex = 32768 ) type Options struct { - Name string - Inet4Address []netip.Prefix - Inet6Address []netip.Prefix - MTU uint32 - GSO bool - AutoRoute bool - InterfaceScope bool - Inet4Gateway netip.Addr - Inet6Gateway netip.Addr - DNSServers []netip.Addr - IPRoute2TableIndex int - IPRoute2RuleIndex int - AutoRedirectMarkMode bool - AutoRedirectInputMark uint32 - AutoRedirectOutputMark uint32 - AutoRedirectResetMark uint32 - AutoRedirectNFQueue uint16 - ExcludeMPTCP bool - Inet4LoopbackAddress []netip.Addr - Inet6LoopbackAddress []netip.Addr - StrictRoute bool - Inet4RouteAddress []netip.Prefix - Inet6RouteAddress []netip.Prefix - Inet4RouteExcludeAddress []netip.Prefix - Inet6RouteExcludeAddress []netip.Prefix - IncludeInterface []string - ExcludeInterface []string - IncludeUID []ranges.Range[uint32] - ExcludeUID []ranges.Range[uint32] - IncludeAndroidUser []int - IncludePackage []string - ExcludePackage []string - InterfaceFinder control.InterfaceFinder - InterfaceMonitor DefaultInterfaceMonitor - FileDescriptor int - Logger logger.Logger + Name string + Inet4Address []netip.Prefix + Inet6Address []netip.Prefix + MTU uint32 + GSO bool + AutoRoute bool + InterfaceScope bool + Inet4Gateway netip.Addr + Inet6Gateway netip.Addr + DNSServers []netip.Addr + IPRoute2TableIndex int + IPRoute2RuleIndex int + IPRoute2AutoRedirectFallbackRuleIndex int + AutoRedirectMarkMode bool + AutoRedirectInputMark uint32 + AutoRedirectOutputMark uint32 + AutoRedirectResetMark uint32 + AutoRedirectNFQueue uint16 + ExcludeMPTCP bool + Inet4LoopbackAddress []netip.Addr + Inet6LoopbackAddress []netip.Addr + StrictRoute bool + Inet4RouteAddress []netip.Prefix + Inet6RouteAddress []netip.Prefix + Inet4RouteExcludeAddress []netip.Prefix + Inet6RouteExcludeAddress []netip.Prefix + IncludeInterface []string + ExcludeInterface []string + IncludeUID []ranges.Range[uint32] + ExcludeUID []ranges.Range[uint32] + IncludeAndroidUser []int + IncludePackage []string + ExcludePackage []string + InterfaceFinder control.InterfaceFinder + InterfaceMonitor DefaultInterfaceMonitor + FileDescriptor int + Logger logger.Logger // No work for TCP, do not use. _TXChecksumOffload bool diff --git a/tun_linux.go b/tun_linux.go index e4d1044b..a1b5560f 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -3,7 +3,6 @@ package tun import ( "errors" "fmt" - "math/rand" "net" "net/netip" "os" @@ -284,16 +283,6 @@ func (t *NativeTun) Start() error { return nil } - if t.options.IPRoute2TableIndex == 0 { - for { - t.options.IPRoute2TableIndex = int(rand.Uint32()) - routeList, fErr := netlink.RouteListFiltered(netlink.FAMILY_ALL, &netlink.Route{Table: t.options.IPRoute2TableIndex}, netlink.RT_FILTER_TABLE) - if len(routeList) == 0 || fErr != nil { - break - } - } - } - err = t.setRoute(tunLink) if err != nil { _ = t.unsetRoute0(tunLink) @@ -632,17 +621,16 @@ func (t *NativeTun) rules() []*netlink.Rule { } // Fallback rules after system default rules (32766: main, 32767: default) // Only reached when main and default tables have no route - const fallbackPriority = 32768 if p4 { it = netlink.NewRule() - it.Priority = fallbackPriority + it.Priority = t.options.IPRoute2AutoRedirectFallbackRuleIndex it.Table = t.options.IPRoute2TableIndex it.Family = unix.AF_INET rules = append(rules, it) } if p6 { it = netlink.NewRule() - it.Priority = fallbackPriority + it.Priority = t.options.IPRoute2AutoRedirectFallbackRuleIndex it.Table = t.options.IPRoute2TableIndex it.Family = unix.AF_INET6 rules = append(rules, it) @@ -1020,7 +1008,7 @@ func (t *NativeTun) unsetRules() error { for _, rule := range ruleList { ruleStart := t.options.IPRoute2RuleIndex ruleEnd := ruleStart + 10 - if rule.Priority >= ruleStart && rule.Priority <= ruleEnd { + if rule.Priority >= ruleStart && rule.Priority <= ruleEnd || (t.options.AutoRedirectMarkMode && rule.Priority == t.options.IPRoute2AutoRedirectFallbackRuleIndex) { ruleToDel := netlink.NewRule() ruleToDel.Family = rule.Family ruleToDel.Priority = rule.Priority From 983b3caf40d6bd83d1bc6930facd6a83314eef5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 1 Feb 2026 10:16:17 +0800 Subject: [PATCH 100/121] Disable rp filter atomically --- tun_linux.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tun_linux.go b/tun_linux.go index a1b5560f..42714b53 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -254,6 +254,10 @@ func (t *NativeTun) Name() (string, error) { } func (t *NativeTun) Start() error { + err := t.disableReversePathFilter() + if err != nil && t.options.Logger != nil && t.options.FileDescriptor == 0 { + t.options.Logger.Warn(E.Cause(err, "disable reverse path filter")) + } if t.options.FileDescriptor != 0 { return nil } @@ -1083,3 +1087,15 @@ func (t *NativeTun) setSearchDomainForSystemdResolved() { _ = shell.Exec(ctlPath, append([]string{"dns", t.options.Name}, common.Map(dnsServer, netip.Addr.String)...)...).Run() }() } + +func (t *NativeTun) disableReversePathFilter() error { + err := os.WriteFile("/proc/sys/net/ipv4/conf/all/rp_filter", []byte{'0'}, 0o644) + if err != nil { + return err + } + err = os.WriteFile("/proc/sys/net/ipv4/conf/"+t.options.Name+"/rp_filter", []byte{'0'}, 0o644) + if err != nil { + return err + } + return nil +} From 2e21cd99ef2d928e62ca8981efac2620fa7b9e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 2 Feb 2026 14:15:10 +0800 Subject: [PATCH 101/121] Revert "Disable rp filter atomically" This reverts commit 983b3caf40d6bd83d1bc6930facd6a83314eef5e. --- tun_linux.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tun_linux.go b/tun_linux.go index 42714b53..a1b5560f 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -254,10 +254,6 @@ func (t *NativeTun) Name() (string, error) { } func (t *NativeTun) Start() error { - err := t.disableReversePathFilter() - if err != nil && t.options.Logger != nil && t.options.FileDescriptor == 0 { - t.options.Logger.Warn(E.Cause(err, "disable reverse path filter")) - } if t.options.FileDescriptor != 0 { return nil } @@ -1087,15 +1083,3 @@ func (t *NativeTun) setSearchDomainForSystemdResolved() { _ = shell.Exec(ctlPath, append([]string{"dns", t.options.Name}, common.Map(dnsServer, netip.Addr.String)...)...).Run() }() } - -func (t *NativeTun) disableReversePathFilter() error { - err := os.WriteFile("/proc/sys/net/ipv4/conf/all/rp_filter", []byte{'0'}, 0o644) - if err != nil { - return err - } - err = os.WriteFile("/proc/sys/net/ipv4/conf/"+t.options.Name+"/rp_filter", []byte{'0'}, 0o644) - if err != nil { - return err - } - return nil -} From 635920688d0a7aa2171afa56be2a0760a69fecb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 2 Feb 2026 14:10:06 +0800 Subject: [PATCH 102/121] Add back random iproute2 table index --- tun_linux.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tun_linux.go b/tun_linux.go index a1b5560f..f35921eb 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -3,6 +3,7 @@ package tun import ( "errors" "fmt" + "math/rand" "net" "net/netip" "os" @@ -283,6 +284,16 @@ func (t *NativeTun) Start() error { return nil } + if t.options.IPRoute2TableIndex == 0 { + for { + t.options.IPRoute2TableIndex = int(rand.Uint32()) + routeList, fErr := netlink.RouteListFiltered(netlink.FAMILY_ALL, &netlink.Route{Table: t.options.IPRoute2TableIndex}, netlink.RT_FILTER_TABLE) + if len(routeList) == 0 || fErr != nil { + break + } + } + } + err = t.setRoute(tunLink) if err != nil { _ = t.unsetRoute0(tunLink) From 3144f43fc2a603c0cba76b6756339cd98a0c4b51 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Fri, 13 Feb 2026 00:11:53 +0800 Subject: [PATCH 103/121] Fix udp/icmp not work on gso with system stack --- tun_linux.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tun_linux.go b/tun_linux.go index f35921eb..8e1e5d50 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -17,7 +17,6 @@ import ( "github.com/sagernet/sing-tun/internal/gtcpip/checksum" "github.com/sagernet/sing-tun/internal/gtcpip/header" "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/rw" @@ -393,10 +392,7 @@ func handleVirtioRead(in []byte, bufs [][]byte, sizes []int, offset int) (int, e func (t *NativeTun) Write(p []byte) (n int, err error) { if t.vnetHdr { - buffer := buf.Get(virtioNetHdrLen + len(p)) - copy(buffer[virtioNetHdrLen:], p) - _, err = t.BatchWrite([][]byte{buffer}, virtioNetHdrLen) - buf.Put(buffer) + _, err = t.BatchWrite([][]byte{p}, virtioNetHdrLen) if err != nil { return } From 2a33d64abc6b161229a534f3380597d25424c806 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Fri, 13 Feb 2026 00:28:14 +0800 Subject: [PATCH 104/121] Fix logging maybe panic --- tun_linux.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tun_linux.go b/tun_linux.go index 8e1e5d50..ac36af2f 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -148,7 +148,9 @@ func (t *NativeTun) configure(tunLink netlink.Link) error { if t.options.GSO { err = t.enableGSO() if err != nil { - t.options.Logger.Warn(err) + if t.options.Logger != nil { + t.options.Logger.Warn(err) + } } } @@ -275,7 +277,9 @@ func (t *NativeTun) Start() error { if err != nil { t.gro.disableTCPGRO() t.gro.disableUDPGRO() - t.options.Logger.Warn(E.Cause(err, "disabled TUN TCP & UDP GRO due to GRO probe error")) + if t.options.Logger != nil { + t.options.Logger.Warn(E.Cause(err, "disabled TUN TCP & UDP GRO due to GRO probe error")) + } } } From 5715a3919a7fdbd38272fefd4da4eb57593b671a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Feb 2026 17:12:39 +0800 Subject: [PATCH 105/121] Fix nftablesCreateLocalAddressSets --- redirect_nftables.go | 10 ++++++++++ redirect_nftables_rules.go | 14 ++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/redirect_nftables.go b/redirect_nftables.go index f7253ccc..3154101a 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -4,6 +4,7 @@ package tun import ( "net/netip" + "strings" "github.com/sagernet/nftables" "github.com/sagernet/nftables/binaryutil" @@ -298,6 +299,10 @@ func (r *autoRedirect) setupNFTables() error { // TODO: test if this works func (r *autoRedirect) nftablesUpdateLocalAddressSet() error { + err := r.interfaceFinder.Update() + if err != nil { + return err + } newLocalAddresses := common.FlatMap(r.interfaceFinder.Interfaces(), func(it control.Interface) []netip.Prefix { return common.Filter(it.Addresses, func(prefix netip.Prefix) bool { return it.Name == "lo" || prefix.Addr().IsGlobalUnicast() @@ -306,6 +311,11 @@ func (r *autoRedirect) nftablesUpdateLocalAddressSet() error { if slices.Equal(newLocalAddresses, r.localAddresses) { return nil } + if r.logger != nil { + r.logger.Debug("updating local address set to [", strings.Join(common.Map(newLocalAddresses, func(it netip.Prefix) string { + return it.String() + }), ", ")+"]") + } nft, err := nftables.New() if err != nil { return err diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index 08f6baa6..fe432f58 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -74,12 +74,11 @@ func (r *autoRedirect) nftablesCreateLocalAddressSets( localAddresses4 := common.Filter(localAddresses, func(it netip.Prefix) bool { return it.Addr().Is4() }) - updateAddresses4 := common.Filter(localAddresses, func(it netip.Prefix) bool { - return it.Addr().Is4() - }) var update bool if len(lastAddresses) != 0 { - if !slices.Equal(localAddresses4, updateAddresses4) { + if !slices.Equal(localAddresses4, common.Filter(lastAddresses, func(it netip.Prefix) bool { + return it.Addr().Is4() + })) { update = true } } @@ -94,12 +93,11 @@ func (r *autoRedirect) nftablesCreateLocalAddressSets( localAddresses6 := common.Filter(localAddresses, func(it netip.Prefix) bool { return it.Addr().Is6() }) - updateAddresses6 := common.Filter(localAddresses, func(it netip.Prefix) bool { - return it.Addr().Is6() - }) var update bool if len(lastAddresses) != 0 { - if !slices.Equal(localAddresses6, updateAddresses6) { + if !slices.Equal(localAddresses6, common.Filter(lastAddresses, func(it netip.Prefix) bool { + return it.Addr().Is6() + })) { update = true } } From 1fb84560211b918e5411399f39a1d20431ad21b1 Mon Sep 17 00:00:00 2001 From: eronez <55025357+eronez@users.noreply.github.com> Date: Fri, 13 Feb 2026 03:40:09 +0000 Subject: [PATCH 106/121] Fix DNS not revert while close tun --- tun_linux.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tun_linux.go b/tun_linux.go index ac36af2f..06c4f927 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -328,6 +328,7 @@ func (t *NativeTun) Close() error { if t.options.EXP_ExternalConfiguration { return common.Close(common.PtrOrNil(t.tunFile)) } + t.unsetSearchDomainForSystemdResolved() t.unsetAddresses() return E.Errors(t.unsetRoute(), t.unsetRules(), common.Close(common.PtrOrNil(t.tunFile))) } @@ -1094,3 +1095,14 @@ func (t *NativeTun) setSearchDomainForSystemdResolved() { _ = shell.Exec(ctlPath, append([]string{"dns", t.options.Name}, common.Map(dnsServer, netip.Addr.String)...)...).Run() }() } + +func (t *NativeTun) unsetSearchDomainForSystemdResolved() { + if t.options.EXP_DisableDNSHijack { + return + } + ctlPath, err := exec.LookPath("resolvectl") + if err != nil { + return + } + _ = shell.Exec(ctlPath, "revert", t.options.Name).Run() +} From 6ee3db839d7b12188f7303435452ed2432b5673b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 2 Mar 2026 06:50:15 +0800 Subject: [PATCH 107/121] Update dependencies --- go.mod | 12 ++++++------ go.sum | 12 ++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 86d0da1f..3ac25d2c 100644 --- a/go.mod +++ b/go.mod @@ -6,25 +6,25 @@ require ( github.com/florianl/go-nfqueue/v2 v2.0.2 github.com/go-ole/go-ole v1.3.0 github.com/google/btree v1.1.3 - github.com/mdlayher/netlink v1.7.2 + github.com/mdlayher/netlink v1.9.0 github.com/sagernet/fswatch v0.1.1 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/sagernet/nftables v0.3.0-beta.4 - github.com/sagernet/sing v0.8.0-beta.2 + github.com/sagernet/sing v0.8.0 github.com/stretchr/testify v1.11.1 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 - golang.org/x/net v0.43.0 - golang.org/x/sys v0.35.0 + golang.org/x/net v0.50.0 + golang.org/x/sys v0.41.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/mdlayher/socket v0.4.1 // indirect + github.com/mdlayher/socket v0.5.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/vishvananda/netns v0.0.4 // indirect golang.org/x/sync v0.7.0 // indirect diff --git a/go.sum b/go.sum index d9664e2f..7d2e3fa6 100644 --- a/go.sum +++ b/go.sum @@ -10,12 +10,18 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco= +github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= @@ -28,6 +34,8 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/sing v0.8.0-beta.2 h1:3khO2eE5LMylD/v47+pnVMtFzl6lBY2v/b/V+79qpsE= github.com/sagernet/sing v0.8.0-beta.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.0 h1:OwLEwbcYfZHvu4olZVljxxC1XRicBqJ1HfiFr6F2WEE= +github.com/sagernet/sing v0.8.0/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= @@ -38,11 +46,15 @@ golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0J golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= From 90ea7a0b697746d4e3fb7ebda592a4a6f4420be5 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 3 Mar 2026 23:48:15 +0800 Subject: [PATCH 108/121] Fix udp/icmp not work on gso with mixed stack --- tun_linux.go | 1 + tun_linux_gvisor.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/tun_linux.go b/tun_linux.go index 06c4f927..5d7b9c6a 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -39,6 +39,7 @@ type NativeTun struct { writeAccess sync.Mutex vnetHdr bool writeBuffer []byte + vnetHdrWriteBuf []byte gsoToWrite []int tcpGROTable *tcpGROTable udpGroAccess sync.Mutex diff --git a/tun_linux_gvisor.go b/tun_linux_gvisor.go index cb0561b6..8adf4c5d 100644 --- a/tun_linux_gvisor.go +++ b/tun_linux_gvisor.go @@ -3,6 +3,8 @@ package tun import ( + "fmt" + "github.com/sagernet/gvisor/pkg/rawfile" "github.com/sagernet/gvisor/pkg/tcpip/link/fdbased" "github.com/sagernet/gvisor/pkg/tcpip/stack" @@ -18,6 +20,37 @@ var _ GVisorTun = (*NativeTun)(nil) func (t *NativeTun) WritePacket(pkt *stack.PacketBuffer) (int, error) { iovecs := t.iovecsOutputDefault + if t.vnetHdr { + if t.vnetHdrWriteBuf == nil { + t.vnetHdrWriteBuf = make([]byte, virtioNetHdrLen) + } + vnetHdr := virtioNetHdr{} + if pkt.GSOOptions.Type != stack.GSONone { + vnetHdr.hdrLen = uint16(pkt.HeaderSize()) + if pkt.GSOOptions.NeedsCsum { + vnetHdr.flags = unix.VIRTIO_NET_HDR_F_NEEDS_CSUM + vnetHdr.csumStart = pkt.GSOOptions.L3HdrLen + vnetHdr.csumOffset = pkt.GSOOptions.CsumOffset + } + if uint16(pkt.Data().Size()) > pkt.GSOOptions.MSS { + switch pkt.GSOOptions.Type { + case stack.GSOTCPv4: + vnetHdr.gsoType = unix.VIRTIO_NET_HDR_GSO_TCPV4 + case stack.GSOTCPv6: + vnetHdr.gsoType = unix.VIRTIO_NET_HDR_GSO_TCPV6 + default: + panic(fmt.Sprintf("Unknown gso type: %v", pkt.GSOOptions.Type)) + } + vnetHdr.gsoSize = pkt.GSOOptions.MSS + } + } + if err := vnetHdr.encode(t.vnetHdrWriteBuf); err != nil { + return 0, err + } + iovec := unix.Iovec{Base: &t.vnetHdrWriteBuf[0]} + iovec.SetLen(virtioNetHdrLen) + iovecs = append(iovecs, iovec) + } var dataLen int for _, packetSlice := range pkt.AsSlices() { dataLen += len(packetSlice) From c81ce6d358ee984cdcc94389643a2d87d768d5a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 5 Mar 2026 20:32:32 +0800 Subject: [PATCH 109/121] Fix darwin batch loop not exit on EBADF --- stack_mixed.go | 5 ++++- stack_system.go | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/stack_mixed.go b/stack_mixed.go index 083ae973..6518cd01 100644 --- a/stack_mixed.go +++ b/stack_mixed.go @@ -3,6 +3,9 @@ package tun import ( + "errors" + "syscall" + "github.com/sagernet/gvisor/pkg/buffer" "github.com/sagernet/gvisor/pkg/tcpip" gHdr "github.com/sagernet/gvisor/pkg/tcpip/header" @@ -169,7 +172,7 @@ func (m *Mixed) batchLoopDarwin(darwinTUN DarwinTUN) { for { buffers, err := darwinTUN.BatchRead() if err != nil { - if E.IsClosed(err) { + if E.IsClosed(err) || errors.Is(err, syscall.EBADF) { return } m.logger.Error(E.Cause(err, "batch read packet")) diff --git a/stack_system.go b/stack_system.go index f74fef96..fa54db38 100644 --- a/stack_system.go +++ b/stack_system.go @@ -269,7 +269,7 @@ func (s *System) batchLoopDarwin(darwinTUN DarwinTUN) { for { buffers, err := darwinTUN.BatchRead() if err != nil { - if E.IsClosed(err) { + if E.IsClosed(err) || errors.Is(err, syscall.EBADF) { return } s.logger.Error(E.Cause(err, "batch read packet")) From caaf8469e09e309557b217c8997e0bfc55ebad91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 11 Mar 2026 15:06:55 +0800 Subject: [PATCH 110/121] Fix auto_redirect dropping SO_BINDTODEVICE traffic REDIRECT in the OUTPUT chain rewrites the destination to 127.0.0.1, then ip_route_me_harder() reroutes with the socket's bound interface constraint (flowi4_oif). Since 127.0.0.1 is only reachable via lo, the routing lookup fails and the packet is silently dropped. Add a fallback routing table with `local 127.0.0.1` entries for each non-loopback interface. When the local table lookup fails due to OIF mismatch, the fallback table provides a matching RTN_LOCAL route. The kernel then overrides dev_out to loopback (route.c:2857), so the packet is delivered locally to the redirect server as intended. This fixes NetworkManager connectivity checks and other tools that use SO_BINDTODEVICE (e.g. curl --interface). --- redirect_linux.go | 55 +++++++------ redirect_nftables.go | 6 ++ redirect_route_linux.go | 174 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+), 23 deletions(-) create mode 100644 redirect_route_linux.go diff --git a/redirect_linux.go b/redirect_linux.go index 5bea0c45..237b02ae 100644 --- a/redirect_linux.go +++ b/redirect_linux.go @@ -19,29 +19,31 @@ import ( ) type autoRedirect struct { - tunOptions *Options - ctx context.Context - handler Handler - logger logger.Logger - tableName string - networkMonitor NetworkUpdateMonitor - networkListener *list.Element[NetworkUpdateCallback] - interfaceFinder control.InterfaceFinder - localAddresses []netip.Prefix - customRedirectPortFunc func() int - customRedirectPort int - redirectServer *redirectServer - enableIPv4 bool - enableIPv6 bool - iptablesPath string - ip6tablesPath string - useNFTables bool - androidSu bool - suPath string - routeAddressSet *[]*netipx.IPSet - routeExcludeAddressSet *[]*netipx.IPSet - nfqueueHandler *nfqueueHandler - nfqueueEnabled bool + tunOptions *Options + ctx context.Context + handler Handler + logger logger.Logger + tableName string + networkMonitor NetworkUpdateMonitor + networkListener *list.Element[NetworkUpdateCallback] + interfaceFinder control.InterfaceFinder + localAddresses []netip.Prefix + customRedirectPortFunc func() int + customRedirectPort int + redirectServer *redirectServer + enableIPv4 bool + enableIPv6 bool + iptablesPath string + ip6tablesPath string + useNFTables bool + androidSu bool + suPath string + routeAddressSet *[]*netipx.IPSet + routeExcludeAddressSet *[]*netipx.IPSet + nfqueueHandler *nfqueueHandler + nfqueueEnabled bool + redirectRouteTableIndex int + redirectInterfaces []control.Interface } func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { @@ -152,6 +154,12 @@ func (r *autoRedirect) Start() error { } r.cleanupNFTables() err = r.setupNFTables() + if err == nil && r.tunOptions.AutoRedirectMarkMode { + err = r.setupRedirectRoutes() + if err != nil { + r.cleanupNFTables() + } + } } else { r.cleanupIPTables() err = r.setupIPTables() @@ -164,6 +172,7 @@ func (r *autoRedirect) Close() error { r.nfqueueHandler.Close() } if r.useNFTables { + r.cleanupRedirectRoutes() r.cleanupNFTables() } else { r.cleanupIPTables() diff --git a/redirect_nftables.go b/redirect_nftables.go index 3154101a..3da8576f 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -293,6 +293,12 @@ func (r *autoRedirect) setupNFTables() error { if err != nil { r.logger.Error("update local address set: ", err) } + if r.tunOptions.AutoRedirectMarkMode { + err = r.updateRedirectRoutes() + if err != nil { + r.logger.Error("update redirect routes: ", err) + } + } }) return nil } diff --git a/redirect_route_linux.go b/redirect_route_linux.go new file mode 100644 index 00000000..726ec1dc --- /dev/null +++ b/redirect_route_linux.go @@ -0,0 +1,174 @@ +//go:build linux + +package tun + +import ( + "math/rand" + "net" + + "github.com/sagernet/netlink" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/control" + + "golang.org/x/sys/unix" +) + +const redirectRouteRulePriority = 1 + +func (r *autoRedirect) setupRedirectRoutes() error { + for { + r.redirectRouteTableIndex = int(rand.Uint32()) + if r.redirectRouteTableIndex == r.tunOptions.IPRoute2TableIndex { + continue + } + routeList, fErr := netlink.RouteListFiltered(netlink.FAMILY_ALL, + &netlink.Route{Table: r.redirectRouteTableIndex}, + netlink.RT_FILTER_TABLE) + if len(routeList) == 0 || fErr != nil { + break + } + } + err := r.interfaceFinder.Update() + if err != nil { + return err + } + tunName := r.tunOptions.Name + r.redirectInterfaces = common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool { + return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0 + }) + r.cleanupRedirectRoutes() + for _, iface := range r.redirectInterfaces { + err = r.addRedirectRoutes(iface.Index) + if err != nil { + return err + } + } + if r.enableIPv4 { + rule := netlink.NewRule() + rule.Priority = redirectRouteRulePriority + rule.Table = r.redirectRouteTableIndex + rule.Family = unix.AF_INET + err = netlink.RuleAdd(rule) + if err != nil { + return err + } + } + if r.enableIPv6 { + rule := netlink.NewRule() + rule.Priority = redirectRouteRulePriority + rule.Table = r.redirectRouteTableIndex + rule.Family = unix.AF_INET6 + err = netlink.RuleAdd(rule) + if err != nil { + return err + } + } + return nil +} + +func (r *autoRedirect) addRedirectRoutes(linkIndex int) error { + if r.enableIPv4 { + err := netlink.RouteAppend(&netlink.Route{ + LinkIndex: linkIndex, + Dst: &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(32, 32)}, + Table: r.redirectRouteTableIndex, + Type: unix.RTN_LOCAL, + Scope: netlink.SCOPE_HOST, + }) + if err != nil { + return err + } + } + if r.enableIPv6 { + err := netlink.RouteAppend(&netlink.Route{ + LinkIndex: linkIndex, + Dst: &net.IPNet{IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)}, + Table: r.redirectRouteTableIndex, + Type: unix.RTN_LOCAL, + Scope: netlink.SCOPE_HOST, + }) + if err != nil { + return err + } + } + return nil +} + +func (r *autoRedirect) removeRedirectRoutes(linkIndex int) { + if r.enableIPv4 { + _ = netlink.RouteDel(&netlink.Route{ + LinkIndex: linkIndex, + Dst: &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(32, 32)}, + Table: r.redirectRouteTableIndex, + Type: unix.RTN_LOCAL, + }) + } + if r.enableIPv6 { + _ = netlink.RouteDel(&netlink.Route{ + LinkIndex: linkIndex, + Dst: &net.IPNet{IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)}, + Table: r.redirectRouteTableIndex, + Type: unix.RTN_LOCAL, + }) + } +} + +func (r *autoRedirect) updateRedirectRoutes() error { + err := r.interfaceFinder.Update() + if err != nil { + return err + } + tunName := r.tunOptions.Name + newInterfaces := common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool { + return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0 + }) + oldMap := make(map[int]bool, len(r.redirectInterfaces)) + for _, iface := range r.redirectInterfaces { + oldMap[iface.Index] = true + } + newMap := make(map[int]bool, len(newInterfaces)) + for _, iface := range newInterfaces { + newMap[iface.Index] = true + } + for _, iface := range newInterfaces { + if !oldMap[iface.Index] { + err = r.addRedirectRoutes(iface.Index) + if err != nil { + return err + } + } + } + for _, iface := range r.redirectInterfaces { + if !newMap[iface.Index] { + r.removeRedirectRoutes(iface.Index) + } + } + r.redirectInterfaces = newInterfaces + return nil +} + +func (r *autoRedirect) cleanupRedirectRoutes() { + if r.redirectRouteTableIndex == 0 { + return + } + routes, _ := netlink.RouteListFiltered(netlink.FAMILY_ALL, + &netlink.Route{Table: r.redirectRouteTableIndex}, + netlink.RT_FILTER_TABLE) + for _, route := range routes { + _ = netlink.RouteDel(&route) + } + if r.enableIPv4 { + rule := netlink.NewRule() + rule.Priority = redirectRouteRulePriority + rule.Table = r.redirectRouteTableIndex + rule.Family = unix.AF_INET + _ = netlink.RuleDel(rule) + } + if r.enableIPv6 { + rule := netlink.NewRule() + rule.Priority = redirectRouteRulePriority + rule.Table = r.redirectRouteTableIndex + rule.Family = unix.AF_INET6 + _ = netlink.RuleDel(rule) + } +} From 0329538ecd5e33f3f1365ff2d404069be3378a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 14 Mar 2026 20:43:28 +0800 Subject: [PATCH 111/121] Fix "Fix auto_redirect dropping SO_BINDTODEVICE traffic" --- redirect_route_linux.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/redirect_route_linux.go b/redirect_route_linux.go index 726ec1dc..6b86da12 100644 --- a/redirect_route_linux.go +++ b/redirect_route_linux.go @@ -5,6 +5,7 @@ package tun import ( "math/rand" "net" + "net/netip" "github.com/sagernet/netlink" "github.com/sagernet/sing/common" @@ -38,7 +39,7 @@ func (r *autoRedirect) setupRedirectRoutes() error { }) r.cleanupRedirectRoutes() for _, iface := range r.redirectInterfaces { - err = r.addRedirectRoutes(iface.Index) + err = r.addRedirectRoutes(iface) if err != nil { return err } @@ -66,10 +67,12 @@ func (r *autoRedirect) setupRedirectRoutes() error { return nil } -func (r *autoRedirect) addRedirectRoutes(linkIndex int) error { - if r.enableIPv4 { +func (r *autoRedirect) addRedirectRoutes(iface control.Interface) error { + if r.enableIPv4 && common.Any(iface.Addresses, func(it netip.Prefix) bool { + return it.Addr().Is4() + }) { err := netlink.RouteAppend(&netlink.Route{ - LinkIndex: linkIndex, + LinkIndex: iface.Index, Dst: &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(32, 32)}, Table: r.redirectRouteTableIndex, Type: unix.RTN_LOCAL, @@ -79,9 +82,11 @@ func (r *autoRedirect) addRedirectRoutes(linkIndex int) error { return err } } - if r.enableIPv6 { + if r.enableIPv6 && common.Any(iface.Addresses, func(it netip.Prefix) bool { + return it.Addr().Is6() && !it.Addr().Is4In6() + }) { err := netlink.RouteAppend(&netlink.Route{ - LinkIndex: linkIndex, + LinkIndex: iface.Index, Dst: &net.IPNet{IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)}, Table: r.redirectRouteTableIndex, Type: unix.RTN_LOCAL, @@ -132,7 +137,7 @@ func (r *autoRedirect) updateRedirectRoutes() error { } for _, iface := range newInterfaces { if !oldMap[iface.Index] { - err = r.addRedirectRoutes(iface.Index) + err = r.addRedirectRoutes(iface) if err != nil { return err } From 82d14b26ded586ff2aa8ce3312a0291b02f975d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 15 Mar 2026 14:15:22 +0800 Subject: [PATCH 112/121] Fix nftables single include_uid not working The single UID code path incorrectly used BigEndian to encode the UID for nft_cmp, but the kernel stores SKUIDs in native endian. --- redirect_nftables_rules.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index fe432f58..968a27da 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -424,7 +424,7 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft &expr.Cmp{ Op: expr.CmpOpNeq, Register: 1, - Data: binaryutil.BigEndian.PutUint32(r.tunOptions.IncludeUID[0].Start), + Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.IncludeUID[0].Start), }, &expr.Counter{}, &expr.Verdict{ From 1ab008e1e61589e828429cc0a7a39877eb59d1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 15 Mar 2026 14:43:38 +0800 Subject: [PATCH 113/121] Improve error messages for Linux TUN and redirect operations --- monitor_android.go | 6 ++--- monitor_linux.go | 4 ++-- monitor_linux_default.go | 4 ++-- redirect_iptables.go | 4 ++-- redirect_linux.go | 11 +++++++-- redirect_nftables.go | 49 +++++++++++++++++++------------------- redirect_nftables_rules.go | 42 ++++++++++++++++---------------- redirect_route_linux.go | 17 ++++++------- tun_linux.go | 45 ++++++++++++++++++---------------- 9 files changed, 98 insertions(+), 84 deletions(-) diff --git a/monitor_android.go b/monitor_android.go index 1c7e711b..c83440d9 100644 --- a/monitor_android.go +++ b/monitor_android.go @@ -8,7 +8,7 @@ import ( func (m *defaultInterfaceMonitor) checkUpdate() error { ruleList, err := netlink.RuleList(netlink.FAMILY_ALL) if err != nil { - return err + return E.Cause(err, "list rules") } oldVPNEnabled := m.androidVPNEnabled @@ -38,7 +38,7 @@ func (m *defaultInterfaceMonitor) checkUpdate() error { routes, err := netlink.RouteListFiltered(netlink.FAMILY_ALL, &netlink.Route{Table: defaultTableIndex}, netlink.RT_FILTER_TABLE) if err != nil { - return err + return E.Cause(err, "list routes") } if len(routes) == 0 { @@ -48,7 +48,7 @@ func (m *defaultInterfaceMonitor) checkUpdate() error { var link netlink.Link link, err = netlink.LinkByIndex(routes[0].LinkIndex) if err != nil { - return err + return E.Cause(err, "find link by index") } newInterface, err := m.interfaceFinder.ByIndex(link.Attrs().Index) diff --git a/monitor_linux.go b/monitor_linux.go index 86dd28b3..4725c166 100644 --- a/monitor_linux.go +++ b/monitor_linux.go @@ -57,11 +57,11 @@ func NewNetworkUpdateMonitor(logger logger.Logger) (NetworkUpdateMonitor, error) func (m *networkUpdateMonitor) Start() error { err := netlink.RouteSubscribe(m.routeUpdate, m.close) if err != nil { - return err + return E.Cause(err, "subscribe route updates") } err = netlink.LinkSubscribe(m.linkUpdate, m.close) if err != nil { - return err + return E.Cause(err, "subscribe link updates") } go m.loopUpdate() return nil diff --git a/monitor_linux_default.go b/monitor_linux_default.go index 72ba1be3..ce6b36dd 100644 --- a/monitor_linux_default.go +++ b/monitor_linux_default.go @@ -12,7 +12,7 @@ import ( func (m *defaultInterfaceMonitor) checkUpdate() error { routes, err := netlink.RouteListFiltered(netlink.FAMILY_ALL, &netlink.Route{Table: unix.RT_TABLE_MAIN}, netlink.RT_FILTER_TABLE) if err != nil { - return err + return E.Cause(err, "list routes") } for _, route := range routes { if route.Dst != nil { @@ -22,7 +22,7 @@ func (m *defaultInterfaceMonitor) checkUpdate() error { var link netlink.Link link, err = netlink.LinkByIndex(route.LinkIndex) if err != nil { - return err + return E.Cause(err, "find link by index") } newInterface, err := m.interfaceFinder.ByIndex(link.Attrs().Index) diff --git a/redirect_iptables.go b/redirect_iptables.go index 2c6e2e29..d918363d 100644 --- a/redirect_iptables.go +++ b/redirect_iptables.go @@ -14,13 +14,13 @@ func (r *autoRedirect) setupIPTables() error { if r.enableIPv4 { err := r.setupIPTablesForFamily(r.iptablesPath) if err != nil { - return err + return E.Cause(err, "setup iptables") } } if r.enableIPv6 { err := r.setupIPTablesForFamily(r.ip6tablesPath) if err != nil { - return err + return E.Cause(err, "setup ip6tables") } } return nil diff --git a/redirect_linux.go b/redirect_linux.go index 237b02ae..e9c892c8 100644 --- a/redirect_linux.go +++ b/redirect_linux.go @@ -154,17 +154,24 @@ func (r *autoRedirect) Start() error { } r.cleanupNFTables() err = r.setupNFTables() - if err == nil && r.tunOptions.AutoRedirectMarkMode { + if err != nil { + return E.Cause(err, "setup nftables") + } + if r.tunOptions.AutoRedirectMarkMode { err = r.setupRedirectRoutes() if err != nil { r.cleanupNFTables() + return E.Cause(err, "setup redirect routes") } } } else { r.cleanupIPTables() err = r.setupIPTables() + if err != nil { + return E.Cause(err, "setup iptables") + } } - return err + return nil } func (r *autoRedirect) Close() error { diff --git a/redirect_nftables.go b/redirect_nftables.go index 3da8576f..f45f5df2 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -11,6 +11,7 @@ import ( "github.com/sagernet/nftables/expr" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" "golang.org/x/exp/slices" "golang.org/x/sys/unix" @@ -19,7 +20,7 @@ import ( func (r *autoRedirect) setupNFTables() error { nft, err := nftables.New() if err != nil { - return err + return E.Cause(err, "create nftables connection") } defer nft.CloseLasting() @@ -30,12 +31,12 @@ func (r *autoRedirect) setupNFTables() error { err = r.nftablesCreateAddressSets(nft, table, false) if err != nil { - return err + return E.Cause(err, "create address sets") } err = r.interfaceFinder.Update() if err != nil { - return err + return E.Cause(err, "update interfaces") } r.localAddresses = common.FlatMap(r.interfaceFinder.Interfaces(), func(it control.Interface) []netip.Prefix { return common.Filter(it.Addresses, func(prefix netip.Prefix) bool { @@ -44,18 +45,18 @@ func (r *autoRedirect) setupNFTables() error { }) err = r.nftablesCreateLocalAddressSets(nft, table, r.localAddresses, nil) if err != nil { - return err + return E.Cause(err, "create local address sets") } err = r.nftablesCreateLoopbackAddressSets(nft, table) if err != nil { - return err + return E.Cause(err, "create loopback address sets") } if r.nfqueueEnabled { err = r.nftablesCreatePreMatchChains(nft, table) if err != nil { - return err + return E.Cause(err, "create pre-match chains") } } @@ -74,12 +75,12 @@ func (r *autoRedirect) setupNFTables() error { if r.tunOptions.AutoRedirectMarkMode { err = r.nftablesCreateExcludeRules(nft, table, chainOutput) if err != nil { - return err + return E.Cause(err, "create output exclude rules") } r.nftablesCreateUnreachable(nft, table, chainOutput) err = r.nftablesCreateRedirect(nft, table, chainOutput) if err != nil { - return err + return E.Cause(err, "create output redirect") } if len(r.tunOptions.Inet4LoopbackAddress) > 0 || len(r.tunOptions.Inet6LoopbackAddress) > 0 { chainOutputRoute := nft.AddChain(&nftables.Chain{ @@ -91,7 +92,7 @@ func (r *autoRedirect) setupNFTables() error { }) err = r.nftablesCreateLoopbackReroute(nft, table, chainOutputRoute) if err != nil { - return err + return E.Cause(err, "create output loopback reroute") } } chainOutputUDP := nft.AddChain(&nftables.Chain{ @@ -103,7 +104,7 @@ func (r *autoRedirect) setupNFTables() error { }) err = r.nftablesCreateExcludeRules(nft, table, chainOutputUDP) if err != nil { - return err + return E.Cause(err, "create output udp exclude rules") } r.nftablesCreateUnreachable(nft, table, chainOutputUDP) r.nftablesCreateMark(nft, table, chainOutputUDP) @@ -117,7 +118,7 @@ func (r *autoRedirect) setupNFTables() error { Data: nftablesIfname(r.tunOptions.Name), }) if err != nil { - return err + return E.Cause(err, "create output redirect") } } } @@ -131,12 +132,12 @@ func (r *autoRedirect) setupNFTables() error { }) err = r.nftablesCreateExcludeRules(nft, table, chainPreRouting) if err != nil { - return err + return E.Cause(err, "create prerouting exclude rules") } r.nftablesCreateUnreachable(nft, table, chainPreRouting) err = r.nftablesCreateRedirect(nft, table, chainPreRouting) if err != nil { - return err + return E.Cause(err, "create prerouting redirect") } if r.tunOptions.AutoRedirectMarkMode { r.nftablesCreateMark(nft, table, chainPreRouting) @@ -150,7 +151,7 @@ func (r *autoRedirect) setupNFTables() error { }) err = r.nftablesCreateLoopbackReroute(nft, table, chainPreRoutingFilter) if err != nil { - return err + return E.Cause(err, "create prerouting loopback reroute") } } chainPreRoutingUDP := nft.AddChain(&nftables.Chain{ @@ -172,7 +173,7 @@ func (r *autoRedirect) setupNFTables() error { {Key: []byte{unix.IPPROTO_ICMPV6}}, }) if err != nil { - return err + return E.Cause(err, "add ip protocol set") } nft.AddRule(&nftables.Rule{ Table: table, @@ -280,12 +281,12 @@ func (r *autoRedirect) setupNFTables() error { err = r.configureOpenWRTFirewall4(nft, false) if err != nil { - return err + return E.Cause(err, "configure openwrt firewall4") } err = nft.Flush() if err != nil { - return err + return E.Cause(err, "flush nftables") } r.networkListener = r.networkMonitor.RegisterCallback(func() { @@ -307,7 +308,7 @@ func (r *autoRedirect) setupNFTables() error { func (r *autoRedirect) nftablesUpdateLocalAddressSet() error { err := r.interfaceFinder.Update() if err != nil { - return err + return E.Cause(err, "update interfaces") } newLocalAddresses := common.FlatMap(r.interfaceFinder.Interfaces(), func(it control.Interface) []netip.Prefix { return common.Filter(it.Addresses, func(prefix netip.Prefix) bool { @@ -324,16 +325,16 @@ func (r *autoRedirect) nftablesUpdateLocalAddressSet() error { } nft, err := nftables.New() if err != nil { - return err + return E.Cause(err, "create nftables connection") } defer nft.CloseLasting() table, err := nft.ListTableOfFamily(r.tableName, nftables.TableFamilyINet) if err != nil { - return err + return E.Cause(err, "list nftables table") } err = r.nftablesCreateLocalAddressSets(nft, table, newLocalAddresses, r.localAddresses) if err != nil { - return err + return E.Cause(err, "create local address sets") } r.localAddresses = newLocalAddresses return nft.Flush() @@ -342,16 +343,16 @@ func (r *autoRedirect) nftablesUpdateLocalAddressSet() error { func (r *autoRedirect) nftablesUpdateRouteAddressSet() error { nft, err := nftables.New() if err != nil { - return err + return E.Cause(err, "create nftables connection") } defer nft.CloseLasting() table, err := nft.ListTableOfFamily(r.tableName, nftables.TableFamilyINet) if err != nil { - return err + return E.Cause(err, "list nftables table") } err = r.nftablesCreateAddressSets(nft, table, true) if err != nil { - return err + return E.Cause(err, "create address sets") } return nft.Flush() } diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index 968a27da..9f950a38 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -13,6 +13,8 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/ranges" + E "github.com/sagernet/sing/common/exceptions" + "golang.org/x/exp/slices" "golang.org/x/sys/unix" ) @@ -38,13 +40,13 @@ func (r *autoRedirect) nftablesCreateAddressSets( if r.enableIPv4 { _, err := nftablesCreateIPSet(nft, table, 1, "inet4_route_address_set", nftables.TableFamilyIPv4, routeAddressSet, nil, true, update) if err != nil { - return err + return E.Cause(err, "create ipv4 route address set") } } if r.enableIPv6 { _, err := nftablesCreateIPSet(nft, table, 2, "inet6_route_address_set", nftables.TableFamilyIPv6, routeAddressSet, nil, true, update) if err != nil { - return err + return E.Cause(err, "create ipv6 route address set") } } } @@ -53,13 +55,13 @@ func (r *autoRedirect) nftablesCreateAddressSets( if r.enableIPv4 { _, err := nftablesCreateIPSet(nft, table, 3, "inet4_route_exclude_address_set", nftables.TableFamilyIPv4, routeExcludeAddressSet, nil, false, update) if err != nil { - return err + return E.Cause(err, "create ipv4 route exclude address set") } } if r.enableIPv6 { _, err := nftablesCreateIPSet(nft, table, 4, "inet6_route_exclude_address_set", nftables.TableFamilyIPv6, routeExcludeAddressSet, nil, false, update) if err != nil { - return err + return E.Cause(err, "create ipv6 route exclude address set") } } } @@ -85,7 +87,7 @@ func (r *autoRedirect) nftablesCreateLocalAddressSets( if len(lastAddresses) == 0 || update { _, err := nftablesCreateIPSet(nft, table, 5, "inet4_local_address_set", nftables.TableFamilyIPv4, nil, localAddresses4, false, update) if err != nil { - return err + return E.Cause(err, "create ipv4 local address set") } } } @@ -104,7 +106,7 @@ func (r *autoRedirect) nftablesCreateLocalAddressSets( if len(lastAddresses) == 0 || update { _, err := nftablesCreateIPSet(nft, table, 6, "inet6_local_address_set", nftables.TableFamilyIPv6, nil, localAddresses6, false, update) if err != nil { - return err + return E.Cause(err, "create ipv6 local address set") } } } @@ -117,13 +119,13 @@ func (r *autoRedirect) nftablesCreateLoopbackAddressSets( if r.enableIPv4 && len(r.tunOptions.Inet4LoopbackAddress) > 0 { _, err := nftablesCreateIPConst(nft, table, 7, "inet4_local_redirect_address_set", nftables.TableFamilyIPv4, r.tunOptions.Inet4LoopbackAddress) if err != nil { - return err + return E.Cause(err, "create ipv4 loopback address set") } } if r.enableIPv6 && len(r.tunOptions.Inet6LoopbackAddress) > 0 { _, err := nftablesCreateIPConst(nft, table, 8, "inet6_local_redirect_address_set", nftables.TableFamilyIPv6, r.tunOptions.Inet6LoopbackAddress) if err != nil { - return err + return E.Cause(err, "create ipv6 loopback address set") } } return nil @@ -144,7 +146,7 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft {Key: []byte{unix.IPPROTO_ICMPV6}}, }) if err != nil { - return err + return E.Cause(err, "add ip protocol set") } nft.AddRule(&nftables.Rule{ Table: table, @@ -283,7 +285,7 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft } })) if err != nil { - return err + return E.Cause(err, "add include interface set") } nft.AddRule(&nftables.Rule{ Table: table, @@ -336,7 +338,7 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft } })) if err != nil { - return err + return E.Cause(err, "add exclude interface set") } nft.AddRule(&nftables.Rule{ Table: table, @@ -395,7 +397,7 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft } })) if err != nil { - return err + return E.Cause(err, "add include uid set") } nft.AddRule(&nftables.Rule{ Table: table, @@ -456,7 +458,7 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft } })) if err != nil { - return err + return E.Cause(err, "add exclude uid set") } nft.AddRule(&nftables.Rule{ Table: table, @@ -499,7 +501,7 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft if len(r.tunOptions.Inet4RouteAddress) > 0 { inet4RouteAddress, err := nftablesCreateIPSet(nft, table, 0, "", nftables.TableFamilyIPv4, nil, r.tunOptions.Inet4RouteAddress, false, false) if err != nil { - return err + return E.Cause(err, "create ipv4 route address set") } nftablesCreateExcludeDestinationIPSet(nft, table, chain, inet4RouteAddress.ID, inet4RouteAddress.Name, nftables.TableFamilyIPv4, true) } @@ -507,7 +509,7 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft if len(r.tunOptions.Inet6RouteAddress) > 0 { inet6RouteAddress, err := nftablesCreateIPSet(nft, table, 0, "", nftables.TableFamilyIPv6, nil, r.tunOptions.Inet6RouteAddress, false, false) if err != nil { - return err + return E.Cause(err, "create ipv6 route address set") } nftablesCreateExcludeDestinationIPSet(nft, table, chain, inet6RouteAddress.ID, inet6RouteAddress.Name, nftables.TableFamilyIPv6, true) } @@ -515,7 +517,7 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft if len(r.tunOptions.Inet4RouteExcludeAddress) > 0 { inet4RouteExcludeAddress, err := nftablesCreateIPSet(nft, table, 0, "", nftables.TableFamilyIPv4, nil, r.tunOptions.Inet4RouteExcludeAddress, false, false) if err != nil { - return err + return E.Cause(err, "create ipv4 route exclude address set") } nftablesCreateExcludeDestinationIPSet(nft, table, chain, inet4RouteExcludeAddress.ID, inet4RouteExcludeAddress.Name, nftables.TableFamilyIPv4, false) } @@ -523,7 +525,7 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft if len(r.tunOptions.Inet6RouteExcludeAddress) > 0 { inet6RouteExcludeAddress, err := nftablesCreateIPSet(nft, table, 0, "", nftables.TableFamilyIPv6, nil, r.tunOptions.Inet6RouteExcludeAddress, false, false) if err != nil { - return err + return E.Cause(err, "create ipv6 route exclude address set") } nftablesCreateExcludeDestinationIPSet(nft, table, chain, inet6RouteExcludeAddress.ID, inet6RouteExcludeAddress.Name, nftables.TableFamilyIPv6, false) } @@ -533,13 +535,13 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft if r.enableIPv4 { err := r.nftablesCreateDNSHijackRulesForFamily(nft, table, chain, nftables.TableFamilyIPv4, 5, "inet4_local_address_set") if err != nil { - return err + return E.Cause(err, "create ipv4 dns hijack rules") } } if r.enableIPv6 { err := r.nftablesCreateDNSHijackRulesForFamily(nft, table, chain, nftables.TableFamilyIPv6, 6, "inet6_local_address_set") if err != nil { - return err + return E.Cause(err, "create ipv6 dns hijack rules") } } } @@ -848,7 +850,7 @@ func (r *autoRedirect) nftablesCreateDNSHijackRulesForFamily( {Key: []byte{unix.IPPROTO_UDP}}, }) if err != nil { - return err + 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) diff --git a/redirect_route_linux.go b/redirect_route_linux.go index 6b86da12..db79cac6 100644 --- a/redirect_route_linux.go +++ b/redirect_route_linux.go @@ -8,6 +8,7 @@ import ( "net/netip" "github.com/sagernet/netlink" + E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" @@ -31,7 +32,7 @@ func (r *autoRedirect) setupRedirectRoutes() error { } err := r.interfaceFinder.Update() if err != nil { - return err + return E.Cause(err, "update interfaces") } tunName := r.tunOptions.Name r.redirectInterfaces = common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool { @@ -41,7 +42,7 @@ func (r *autoRedirect) setupRedirectRoutes() error { for _, iface := range r.redirectInterfaces { err = r.addRedirectRoutes(iface) if err != nil { - return err + return E.Cause(err, "add redirect routes for ", iface.Name) } } if r.enableIPv4 { @@ -51,7 +52,7 @@ func (r *autoRedirect) setupRedirectRoutes() error { rule.Family = unix.AF_INET err = netlink.RuleAdd(rule) if err != nil { - return err + return E.Cause(err, "add ipv4 redirect rule") } } if r.enableIPv6 { @@ -61,7 +62,7 @@ func (r *autoRedirect) setupRedirectRoutes() error { rule.Family = unix.AF_INET6 err = netlink.RuleAdd(rule) if err != nil { - return err + return E.Cause(err, "add ipv6 redirect rule") } } return nil @@ -79,7 +80,7 @@ func (r *autoRedirect) addRedirectRoutes(iface control.Interface) error { Scope: netlink.SCOPE_HOST, }) if err != nil { - return err + return E.Cause(err, "append ipv4 loopback route") } } if r.enableIPv6 && common.Any(iface.Addresses, func(it netip.Prefix) bool { @@ -93,7 +94,7 @@ func (r *autoRedirect) addRedirectRoutes(iface control.Interface) error { Scope: netlink.SCOPE_HOST, }) if err != nil { - return err + return E.Cause(err, "append ipv6 loopback route") } } return nil @@ -121,7 +122,7 @@ func (r *autoRedirect) removeRedirectRoutes(linkIndex int) { func (r *autoRedirect) updateRedirectRoutes() error { err := r.interfaceFinder.Update() if err != nil { - return err + return E.Cause(err, "update interfaces") } tunName := r.tunOptions.Name newInterfaces := common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool { @@ -139,7 +140,7 @@ func (r *autoRedirect) updateRedirectRoutes() error { if !oldMap[iface.Index] { err = r.addRedirectRoutes(iface) if err != nil { - return err + return E.Cause(err, "add redirect routes for ", iface.Name) } } } diff --git a/tun_linux.go b/tun_linux.go index 5d7b9c6a..20fdce23 100644 --- a/tun_linux.go +++ b/tun_linux.go @@ -53,7 +53,7 @@ func New(options Options) (Tun, error) { if options.FileDescriptor == 0 { tunFd, err := open(options.Name, options.GSO) if err != nil { - return nil, err + return nil, E.Cause(err, "open tun") } tunLink, err := netlink.LinkByName(options.Name) if err != nil { @@ -93,12 +93,12 @@ func init() { func open(name string, vnetHdr bool) (int, error) { fd, err := unix.Open(controlPath, unix.O_RDWR, 0) if err != nil { - return -1, err + return -1, E.Cause(err, "open ", controlPath) } ifr, err := unix.NewIfreq(name) if err != nil { unix.Close(fd) - return 0, err + return 0, E.Cause(err, "create ifreq") } flags := unix.IFF_TUN | unix.IFF_NO_PI if vnetHdr { @@ -108,12 +108,12 @@ func open(name string, vnetHdr bool) (int, error) { err = unix.IoctlIfreq(fd, unix.TUNSETIFF, ifr) if err != nil { unix.Close(fd) - return 0, err + return 0, E.Cause(err, "TUNSETIFF") } err = unix.SetNonblock(fd, true) if err != nil { unix.Close(fd) - return 0, err + return 0, E.Cause(err, "set nonblock") } return fd, nil } @@ -123,7 +123,7 @@ func (t *NativeTun) configure(tunLink netlink.Link) error { if errors.Is(err, unix.EPERM) { return nil } else if err != nil { - return err + return E.Cause(err, "set mtu") } if !t.options.EXP_ExternalConfiguration { if len(t.options.Inet4Address) > 0 { @@ -131,7 +131,7 @@ func (t *NativeTun) configure(tunLink netlink.Link) error { addr4, _ := netlink.ParseAddr(address.String()) err = netlink.AddrAdd(tunLink, addr4) if err != nil && !errors.Is(err, unix.EEXIST) { - return err + return E.Cause(err, "add address ", address) } } } @@ -140,7 +140,7 @@ func (t *NativeTun) configure(tunLink netlink.Link) error { addr6, _ := netlink.ParseAddr(address.String()) err = netlink.AddrAdd(tunLink, addr6) if err != nil && !errors.Is(err, unix.EEXIST) { - return err + return E.Cause(err, "add address ", address) } } } @@ -165,12 +165,12 @@ func (t *NativeTun) configure(tunLink netlink.Link) error { var txChecksumOffload bool txChecksumOffload, err = checkChecksumOffload(t.options.Name, unix.ETHTOOL_GTXCSUM) if err != nil { - return err + return E.Cause(err, "check tx checksum offload") } if !txChecksumOffload { err = setChecksumOffload(t.options.Name, unix.ETHTOOL_STXCSUM) if err != nil { - return err + return E.Cause(err, "set tx checksum offload") } } t.txChecksumOffload = true @@ -239,7 +239,10 @@ func (t *NativeTun) probeTCPGRO() error { tcpH.SetChecksum(^tcpH.CalculateChecksum(pseudoCsum)) } _, err := t.BatchWrite(bufs, virtioNetHdrLen) - return err + if err != nil { + return E.Cause(err, "batch write") + } + return nil } func (t *NativeTun) Name() (string, error) { @@ -265,12 +268,12 @@ func (t *NativeTun) Start() error { } tunLink, err := netlink.LinkByName(t.options.Name) if err != nil { - return err + return E.Cause(err, "find tun interface") } err = netlink.LinkSetUp(tunLink) if err != nil { - return err + return E.Cause(err, "set tun up") } if t.vnetHdr && len(t.options.Inet4Address) > 0 { @@ -301,7 +304,7 @@ func (t *NativeTun) Start() error { err = t.setRoute(tunLink) if err != nil { _ = t.unsetRoute0(tunLink) - return err + return E.Cause(err, "set routes") } err = t.unsetRules() @@ -311,7 +314,7 @@ func (t *NativeTun) Start() error { err = t.setRules() if err != nil { _ = t.unsetRules() - return err + return E.Cause(err, "set rules") } t.setSearchDomainForSystemdResolved() @@ -498,11 +501,11 @@ func (t *NativeTun) UpdateRouteOptions(tunOptions Options) error { } tunLink, err := netlink.LinkByName(t.options.Name) if err != nil { - return err + return E.Cause(err, "find tun interface") } err = t.unsetRoute0(tunLink) if err != nil { - return err + return E.Cause(err, "unset old routes") } t.options = tunOptions return t.setRoute(tunLink) @@ -511,7 +514,7 @@ func (t *NativeTun) UpdateRouteOptions(tunOptions Options) error { func (t *NativeTun) routes(tunLink netlink.Link) ([]netlink.Route, error) { routeRanges, err := t.options.BuildAutoRouteRanges(false) if err != nil { - return nil, err + return nil, E.Cause(err, "build auto route ranges") } // Do not create gateway on linux by default gateway4, gateway6 := t.options.Inet4GatewayAddr(), t.options.Inet6GatewayAddr() @@ -956,7 +959,7 @@ func (t *NativeTun) rules() []*netlink.Rule { func (t *NativeTun) setRoute(tunLink netlink.Link) error { routes, err := t.routes(tunLink) if err != nil { - return err + return E.Cause(err, "build routes") } for i, route := range routes { err := netlink.RouteAdd(&route) @@ -983,7 +986,7 @@ func (t *NativeTun) unsetRoute() error { } tunLink, err := netlink.LinkByName(t.options.Name) if err != nil { - return err + return E.Cause(err, "find tun interface") } return t.unsetRoute0(tunLink) } @@ -1016,7 +1019,7 @@ func (t *NativeTun) unsetRules() error { if t.options.AutoRoute { ruleList, err := netlink.RuleList(netlink.FAMILY_ALL) if err != nil { - return err + return E.Cause(err, "list rules") } for _, rule := range ruleList { ruleStart := t.options.IPRoute2RuleIndex From 0e4cdbbc6196b3e85e7a6c86ec23350a2a81e97b Mon Sep 17 00:00:00 2001 From: Andrew Novikov <68810358+npokc123@users.noreply.github.com> Date: Tue, 17 Mar 2026 05:57:47 +0300 Subject: [PATCH 114/121] fix: use NF_REPEAT for NFQUEUE bypass/reset verdicts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NF_ACCEPT is a terminal verdict in nftables — when a packet returns from NFQUEUE with NF_ACCEPT, it exits the current chain immediately and continues to the next hook priority. Rules placed after the queue statement in the same chain are never evaluated. This meant that the `ct mark set meta mark` rule (which saves the bypass decision to conntrack for subsequent packets) was dead code. The first SYN packet received the correct mark from NFQUEUE, but conntrack never stored it, so all subsequent packets of the same connection were redirected to sing-box userspace. Fix: use NF_REPEAT instead of NF_ACCEPT for bypass and reset verdicts. NF_REPEAT re-enters the chain from the beginning with the mark already set on skb->mark. Reorder the prematch chain rules so mark-checking rules (ct mark set, reject) come before the queue statement: 1. meta mark == outputMark → ct mark set meta mark, return 2. meta mark == resetMark → reject with tcp reset 3. ct mark == outputMark → return 4. TCP SYN → queue to NFQUEUE This is the standard pattern used by Suricata and other NFQUEUE-based systems (NF_REPEAT + mark-based skip). Tested on Orange Pi Zero 3 (arm64, kernel 6.12.58) with sing-box 1.13.3. Bypass correctly saves ct mark, subsequent packets skip NFQUEUE entirely. --- nfqueue_linux.go | 8 ++++++-- redirect_nftables.go | 46 +++++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/nfqueue_linux.go b/nfqueue_linux.go index e52cd468..baaefb54 100644 --- a/nfqueue_linux.go +++ b/nfqueue_linux.go @@ -214,11 +214,15 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int { _, pErr := h.handler.PrepareConnection(N.NetworkTCP, srcAddr, dstAddr, nil, 0) + // 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): - h.setVerdict(packetID, nfqueue.NfAccept, h.outputMark) + h.setVerdict(packetID, nfqueue.NfRepeat, h.outputMark) case errors.Is(pErr, ErrReset): - h.setVerdict(packetID, nfqueue.NfAccept, h.resetMark) + h.setVerdict(packetID, nfqueue.NfRepeat, h.resetMark) case errors.Is(pErr, ErrDrop): h.setVerdict(packetID, nfqueue.NfDrop, 0) default: diff --git a/redirect_nftables.go b/redirect_nftables.go index f45f5df2..266bbe91 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -423,16 +423,39 @@ func (r *autoRedirect) nftablesAddPreMatchRules(nft *nftables.Conn, table *nftab }, }) + // Bypass mark: save to conntrack and return. + // When the NFQUEUE handler returns NF_REPEAT with the output mark, + // the packet re-enters this chain from the beginning. This rule + // catches it, saves the mark to conntrack (so subsequent packets + // of the same connection are bypassed via ct mark check below), + // and returns. nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: []expr.Any{ &expr.Meta{Key: expr.MetaKeyMARK, Register: 1}, &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.effectiveOutputMark())}, + &expr.Ct{Key: expr.CtKeyMARK, Register: 1, SourceRegister: true}, + &expr.Counter{}, &expr.Verdict{Kind: expr.VerdictReturn}, }, }) + // Reset mark: reject with TCP RST. + // When the NFQUEUE handler returns NF_REPEAT with the reset mark, + // the packet re-enters this chain and is rejected here. + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeyMARK, Register: 1}, + &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.effectiveResetMark())}, + &expr.Counter{}, + &expr.Reject{Type: unix.NFT_REJECT_TCP_RST}, + }, + }) + + // Already-tracked bypass connections: return immediately. nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, @@ -443,6 +466,7 @@ func (r *autoRedirect) nftablesAddPreMatchRules(nft *nftables.Conn, table *nftab }, }) + // TCP SYN: send to NFQUEUE for pre-match evaluation. nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, @@ -469,26 +493,4 @@ func (r *autoRedirect) nftablesAddPreMatchRules(nft *nftables.Conn, table *nftab }, }, }) - - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chain, - Exprs: []expr.Any{ - &expr.Meta{Key: expr.MetaKeyMARK, Register: 1}, - &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.effectiveResetMark())}, - &expr.Counter{}, - &expr.Reject{Type: unix.NFT_REJECT_TCP_RST}, - }, - }) - - nft.AddRule(&nftables.Rule{ - Table: table, - Chain: chain, - Exprs: []expr.Any{ - &expr.Meta{Key: expr.MetaKeyMARK, Register: 1}, - &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.effectiveOutputMark())}, - &expr.Ct{Key: expr.CtKeyMARK, Register: 1, SourceRegister: true}, - &expr.Counter{}, - }, - }) } From 0e624a007c0cc1e596c57242fc2014b65de14479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Mar 2026 18:24:30 +0800 Subject: [PATCH 115/121] Add PackagesByID for android package manager --- packages.go | 1 + packages_android.go | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages.go b/packages.go index 3ec46544..72932b16 100644 --- a/packages.go +++ b/packages.go @@ -8,6 +8,7 @@ type PackageManager interface { IDByPackage(packageName string) (uint32, bool) IDBySharedPackage(sharedPackage string) (uint32, bool) PackageByID(id uint32) (string, bool) + PackagesByID(id uint32) ([]string, bool) SharedPackageByID(id uint32) (string, bool) } diff --git a/packages_android.go b/packages_android.go index 11de01f1..af79d544 100644 --- a/packages_android.go +++ b/packages_android.go @@ -20,7 +20,7 @@ type packageManager struct { watcher *fswatch.Watcher idByPackage map[string]uint32 sharedByPackage map[string]uint32 - packageById map[uint32]string + packageById map[uint32][]string sharedById map[uint32]string } @@ -83,8 +83,16 @@ func (m *packageManager) IDBySharedPackage(sharedPackage string) (uint32, bool) } func (m *packageManager) PackageByID(id uint32) (string, bool) { - packageName, loaded := m.packageById[id] - return packageName, loaded + packageNames, loaded := m.packageById[id] + if !loaded || len(packageNames) == 0 { + return "", false + } + return packageNames[0], true +} + +func (m *packageManager) PackagesByID(id uint32) ([]string, bool) { + packageNames, loaded := m.packageById[id] + return packageNames, loaded } func (m *packageManager) SharedPackageByID(id uint32) (string, bool) { @@ -110,7 +118,7 @@ func (m *packageManager) updatePackages() error { func (m *packageManager) decodePackages(decoder *xml.Decoder) error { idByPackage := make(map[string]uint32) sharedByPackage := make(map[string]uint32) - packageById := make(map[uint32]string) + packageById := make(map[uint32][]string) sharedById := make(map[uint32]string) for { token, err := decoder.Token() @@ -144,7 +152,7 @@ func (m *packageManager) decodePackages(decoder *xml.Decoder) error { continue } idByPackage[name] = uint32(userID) - packageById[uint32(userID)] = name + packageById[uint32(userID)] = append(packageById[uint32(userID)], name) case "shared-user": var name string var userID uint64 @@ -157,7 +165,6 @@ func (m *packageManager) decodePackages(decoder *xml.Decoder) error { if err != nil { return err } - packageById[uint32(userID)] = name } } if userID == 0 && name == "" { From 24b12270dbbef075427b93b43dfc276cb6027453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Mar 2026 19:37:57 +0800 Subject: [PATCH 116/121] Fix system stack rewriting TUN subnet destinations to loopback The acceptLoop was rewriting any TCP destination within the TUN address prefix to 127.0.0.1/::1. This incorrectly caught the gateway address and other subnet addresses, not just the interface address itself. --- stack_system.go | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/stack_system.go b/stack_system.go index fa54db38..e2cdd45e 100644 --- a/stack_system.go +++ b/stack_system.go @@ -330,23 +330,7 @@ func (s *System) acceptLoop(listener net.Listener) { s.logger.Trace(E.New("unknown session with port ", connPort)) continue } - destination := M.SocksaddrFromNetIP(session.Destination) - if destination.Addr.Is4() { - for _, prefix := range s.inet4Prefixes { - if prefix.Contains(destination.Addr) { - destination.Addr = netip.AddrFrom4([4]byte{127, 0, 0, 1}) - break - } - } - } else { - for _, prefix := range s.inet6Prefixes { - if prefix.Contains(destination.Addr) { - destination.Addr = netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) - break - } - } - } - go s.handler.NewConnectionEx(s.ctx, conn, M.SocksaddrFromNetIP(session.Source), destination, nil) + go s.handler.NewConnectionEx(s.ctx, conn, M.SocksaddrFromNetIP(session.Source), M.SocksaddrFromNetIP(session.Destination), nil) } } From 1664d083fb1751c06b49d02ab77942a3b43a6de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 7 Apr 2026 14:14:19 +0800 Subject: [PATCH 117/121] Reduce iOS TCP buffers --- stack_gvisor.go | 63 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/stack_gvisor.go b/stack_gvisor.go index cc488f65..ca7dcdc5 100644 --- a/stack_gvisor.go +++ b/stack_gvisor.go @@ -87,7 +87,7 @@ func (t *GVisor) Start() error { return err } linkEndpoint = &LinkEndpointFilter{linkEndpoint, t.broadcastAddr, t.tun} - ipStack, err := NewGVisorStackWithOptions(linkEndpoint, nicOptions, false) + ipStack, err := newGVisorStack(linkEndpoint, nicOptions, false, true) if err != nil { return err } @@ -135,6 +135,10 @@ func NewGVisorStack(ep stack.LinkEndpoint) (*stack.Stack, error) { } func NewGVisorStackWithOptions(ep stack.LinkEndpoint, opts stack.NICOptions, allowRawEndpoint bool) (*stack.Stack, error) { + return newGVisorStack(ep, opts, allowRawEndpoint, false) +} + +func newGVisorStack(ep stack.LinkEndpoint, opts stack.NICOptions, allowRawEndpoint bool, isLocalStack bool) (*stack.Stack, error) { stackOptions := stack.Options{ NetworkProtocols: []stack.NetworkProtocolFactory{ ipv4.NewProtocol, @@ -175,23 +179,46 @@ func NewGVisorStackWithOptions(ep stack.LinkEndpoint, opts stack.NICOptions, all tcpRecoveryOpt := tcpip.TCPRecovery(0) err = ipStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpRecoveryOpt) } - tcpRXBufOpt := tcpip.TCPReceiveBufferSizeRangeOption{ - Min: tcpRXBufMinSize, - Default: tcpRXBufDefSize, - Max: tcpRXBufMaxSize, - } - err = ipStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpRXBufOpt) - if err != nil { - return nil, gonet.TranslateNetstackError(err) - } - tcpTXBufOpt := tcpip.TCPSendBufferSizeRangeOption{ - Min: tcpTXBufMinSize, - Default: tcpTXBufDefSize, - Max: tcpTXBufMaxSize, - } - err = ipStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpTXBufOpt) - if err != nil { - return nil, gonet.TranslateNetstackError(err) + if isLocalStack || runtime.GOOS == "ios" { + const iOSBufferDefault = 1 << 15 + const iOSBufferMax = 1 << 17 + tcpRXBufOpt := tcpip.TCPReceiveBufferSizeRangeOption{ + Min: tcpRXBufMinSize, + Default: iOSBufferDefault, + Max: iOSBufferMax, + } + err = ipStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpRXBufOpt) + if err != nil { + return nil, gonet.TranslateNetstackError(err) + } + tcpTXBufOpt := tcpip.TCPSendBufferSizeRangeOption{ + Min: tcpTXBufMinSize, + Default: iOSBufferDefault, + Max: iOSBufferMax, + } + err = ipStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpTXBufOpt) + if err != nil { + return nil, gonet.TranslateNetstackError(err) + } + } else { + tcpRXBufOpt := tcpip.TCPReceiveBufferSizeRangeOption{ + Min: tcpRXBufMinSize, + Default: tcpRXBufDefSize, + Max: tcpRXBufMaxSize, + } + err = ipStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpRXBufOpt) + if err != nil { + return nil, gonet.TranslateNetstackError(err) + } + tcpTXBufOpt := tcpip.TCPSendBufferSizeRangeOption{ + Min: tcpTXBufMinSize, + Default: tcpTXBufDefSize, + Max: tcpTXBufMaxSize, + } + err = ipStack.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpTXBufOpt) + if err != nil { + return nil, gonet.TranslateNetstackError(err) + } } return ipStack, nil } From 31c44bc478f96b14dcd31ee6d40345f937aabd5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 7 Apr 2026 20:34:55 +0800 Subject: [PATCH 118/121] Fix buffer not released in batchLoopDarwin --- stack_mixed.go | 2 ++ stack_system.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/stack_mixed.go b/stack_mixed.go index 6518cd01..8836d6ba 100644 --- a/stack_mixed.go +++ b/stack_mixed.go @@ -184,6 +184,7 @@ func (m *Mixed) batchLoopDarwin(darwinTUN DarwinTUN) { for _, buffer := range buffers { packetSize := buffer.Len() if packetSize < header.IPv4MinimumSize { + buffer.Release() continue } if m.processPacket(buffer.Bytes()) { @@ -197,6 +198,7 @@ func (m *Mixed) batchLoopDarwin(darwinTUN DarwinTUN) { if err != nil { m.logger.Trace(E.Cause(err, "batch write packet")) } + buf.ReleaseMulti(writeBuffers) } } } diff --git a/stack_system.go b/stack_system.go index e2cdd45e..ef8b709a 100644 --- a/stack_system.go +++ b/stack_system.go @@ -281,6 +281,7 @@ func (s *System) batchLoopDarwin(darwinTUN DarwinTUN) { for _, buffer := range buffers { packetSize := buffer.Len() if packetSize < header.IPv4MinimumSize { + buffer.Release() continue } if s.processPacket(buffer.Bytes()) { @@ -294,6 +295,7 @@ func (s *System) batchLoopDarwin(darwinTUN DarwinTUN) { if err != nil { s.logger.Trace(E.Cause(err, "batch write packet")) } + buf.ReleaseMulti(writeBuffers) } } } From 4cd8fef58105b9237637842e3b17f24cdfacc583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 7 Apr 2026 23:12:58 +0800 Subject: [PATCH 119/121] Fix UDP forwarder slice allocation --- stack_gvisor_udp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack_gvisor_udp.go b/stack_gvisor_udp.go index 2e8ff3e7..f91a2b3e 100644 --- a/stack_gvisor_udp.go +++ b/stack_gvisor_udp.go @@ -46,7 +46,7 @@ func (f *UDPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac source := M.SocksaddrFrom(AddrFromAddress(id.RemoteAddress), id.RemotePort) destination := M.SocksaddrFrom(AddrFromAddress(id.LocalAddress), id.LocalPort) bufferRange := pkt.Data().AsRange() - bufferSlices := make([][]byte, bufferRange.Size()) + var bufferSlices [][]byte rangeIterate(bufferRange, func(view *buffer.View) { bufferSlices = append(bufferSlices, view.AsSlice()) }) From 1a80fcd6800f5c4291a55f56e570f8fe44b162ab 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 120/121] 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 9f950a38..27765f5b 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -3,6 +3,7 @@ package tun import ( + "net" "net/netip" _ "unsafe" @@ -375,6 +376,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 018f5eaae6956814f1bbb092a1984ffbd53f1476 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 121/121] Make gtcpip public for external use --- {internal/gtcpip => gtcpip}/README.md | 0 {internal/gtcpip => gtcpip}/checksum/checksum.go | 0 {internal/gtcpip => gtcpip}/checksum/checksum_default.go | 0 {internal/gtcpip => gtcpip}/checksum/checksum_ts.go | 0 {internal/gtcpip => gtcpip}/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 ++-- {internal/gtcpip => gtcpip}/header/interfaces.go | 2 +- {internal/gtcpip => gtcpip}/header/ipv4.go | 4 ++-- {internal/gtcpip => gtcpip}/header/ipv6.go | 2 +- .../gtcpip => gtcpip}/header/ipv6_extension_headers.go | 2 +- {internal/gtcpip => gtcpip}/header/ipv6_fragment.go | 2 +- {internal/gtcpip => gtcpip}/header/ndp_neighbor_advert.go | 2 +- {internal/gtcpip => gtcpip}/header/ndp_neighbor_solicit.go | 2 +- {internal/gtcpip => gtcpip}/header/ndp_options.go | 2 +- {internal/gtcpip => gtcpip}/header/ndp_router_advert.go | 0 {internal/gtcpip => gtcpip}/header/ndp_router_solicit.go | 0 .../gtcpip => gtcpip}/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 | 2 +- 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 +- redirect_nftables_rules.go | 3 +-- redirect_route_linux.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 +++--- 44 files changed, 45 insertions(+), 46 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 ba293398..365329a2 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 1b58df86..824b08c8 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 35ee021c..2d07fff6 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 baaefb54..9eed52fc 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 60decb4c..ada5258c 100644 --- a/ping/destination.go +++ b/ping/destination.go @@ -10,7 +10,7 @@ import ( "time" "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" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" 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 eab977b7..09979997 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 7ec291a3..163dbd4c 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 79fd682d..5e703e6e 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/redirect_nftables_rules.go b/redirect_nftables_rules.go index 27765f5b..7e47be08 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -12,9 +12,8 @@ import ( "github.com/sagernet/nftables/expr" "github.com/sagernet/nftables/userdata" "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/ranges" - E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/ranges" "golang.org/x/exp/slices" "golang.org/x/sys/unix" diff --git a/redirect_route_linux.go b/redirect_route_linux.go index db79cac6..7e0868c6 100644 --- a/redirect_route_linux.go +++ b/redirect_route_linux.go @@ -8,9 +8,9 @@ import ( "net/netip" "github.com/sagernet/netlink" - E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" "golang.org/x/sys/unix" ) 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 ef8b709a..f475767a 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 8aa6923f..f4ca0edd 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" "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 20fdce23..27c81c41 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 a0eee82f..83c833af 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" ) const ( diff --git a/tun_offload_linux.go b/tun_offload_linux.go index 77337607..a3085304 100644 --- a/tun_offload_linux.go +++ b/tun_offload_linux.go @@ -13,9 +13,9 @@ import ( "io" "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" )