From df8e2edfd587cbf02e15094fd94c3e8187044fad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 6 Oct 2026 20:44:55 +0800 Subject: [PATCH] Rework forward NAT with UDP mapping and fragment support --- common/kernelports/pool.go | 47 +++++++++ common/kernelports/pool_darwin.go | 17 ++++ common/kernelports/pool_linux.go | 29 ++++++ common/kernelports/pool_unix.go | 87 ++++++++++++++++ common/kernelports/pool_unix_other.go | 7 ++ common/kernelports/pool_windows.go | 121 +++++++++++++++++++++++ protocol/bridge/backend_windows.go | 132 +++++++++++++++++++------ protocol/bridge/outbound.go | 7 +- protocol/bridge/reservation_windows.go | 73 -------------- protocol/masque/client.go | 4 + protocol/masque/server.go | 4 + protocol/openconnect/client.go | 4 + protocol/openvpn/client.go | 4 + protocol/openvpn/server.go | 4 + protocol/tailscale/port.go | 7 ++ protocol/wireguard/endpoint.go | 4 + route/route.go | 2 +- transport/device/device.go | 1 + transport/device/stack.go | 4 + transport/device/system.go | 34 ++++++- transport/wireguard/device.go | 1 + transport/wireguard/device_stack.go | 4 + transport/wireguard/device_system.go | 22 +++++ transport/wireguard/port.go | 4 + 24 files changed, 507 insertions(+), 116 deletions(-) create mode 100644 common/kernelports/pool.go create mode 100644 common/kernelports/pool_darwin.go create mode 100644 common/kernelports/pool_linux.go create mode 100644 common/kernelports/pool_unix.go create mode 100644 common/kernelports/pool_unix_other.go create mode 100644 common/kernelports/pool_windows.go delete mode 100644 protocol/bridge/reservation_windows.go diff --git a/common/kernelports/pool.go b/common/kernelports/pool.go new file mode 100644 index 000000000..242fbd64c --- /dev/null +++ b/common/kernelports/pool.go @@ -0,0 +1,47 @@ +package kernelports + +import ( + "net/netip" + "slices" + "sync/atomic" + + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/gtcpip/header" + E "github.com/sagernet/sing/common/exceptions" +) + +var ErrExhausted = E.New("kernel ports: no more ports can be reserved") + +func (p *Pool) ranges(protocol uint8) *atomic.Pointer[[]tun.SelectorRange] { + if protocol == uint8(header.UDPProtocolNumber) { + return &p.udp + } + return &p.tcp +} + +func (p *Pool) PortSelectorRanges(protocol uint8) []tun.SelectorRange { + ranges := p.ranges(protocol).Load() + if ranges == nil { + return nil + } + return *ranges +} + +func (p *Pool) ExpandSelectorRanges(protocol uint8) bool { + _, err := p.Acquire(protocol) + return err == nil +} + +func (p *Pool) Contains(protocol uint8, port uint16) bool { + return slices.ContainsFunc(p.PortSelectorRanges(protocol), func(selectorRange tun.SelectorRange) bool { + return port >= selectorRange.Start && port-selectorRange.Start < selectorRange.Count + }) +} + +func (p *Pool) ReleaseSelector(protocol uint8, address netip.AddrPort) { +} + +var ( + _ tun.PortWithSelectorRange = (*Pool)(nil) + _ tun.PortWithSelectorReservation = (*Pool)(nil) +) diff --git a/common/kernelports/pool_darwin.go b/common/kernelports/pool_darwin.go new file mode 100644 index 000000000..c0a220e92 --- /dev/null +++ b/common/kernelports/pool_darwin.go @@ -0,0 +1,17 @@ +package kernelports + +import "golang.org/x/sys/unix" + +const ( + defaultEphemeralFirst = 49152 + defaultEphemeralLast = 65535 +) + +func ephemeralRange() (uint16, uint16) { + first, firstErr := unix.SysctlUint32("net.inet.ip.portrange.first") + last, lastErr := unix.SysctlUint32("net.inet.ip.portrange.last") + if firstErr != nil || lastErr != nil || first > last || last > 65535 { + return defaultEphemeralFirst, defaultEphemeralLast + } + return uint16(first), uint16(last) +} diff --git a/common/kernelports/pool_linux.go b/common/kernelports/pool_linux.go new file mode 100644 index 000000000..db87d13ec --- /dev/null +++ b/common/kernelports/pool_linux.go @@ -0,0 +1,29 @@ +package kernelports + +import ( + "os" + "strconv" + "strings" +) + +const ( + defaultEphemeralFirst = 32768 + defaultEphemeralLast = 60999 +) + +func ephemeralRange() (uint16, uint16) { + content, err := os.ReadFile("/proc/sys/net/ipv4/ip_local_port_range") + if err != nil { + return defaultEphemeralFirst, defaultEphemeralLast + } + fields := strings.Fields(string(content)) + if len(fields) != 2 { + return defaultEphemeralFirst, defaultEphemeralLast + } + first, firstErr := strconv.ParseUint(fields[0], 10, 16) + last, lastErr := strconv.ParseUint(fields[1], 10, 16) + if firstErr != nil || lastErr != nil || first > last { + return defaultEphemeralFirst, defaultEphemeralLast + } + return uint16(first), uint16(last) +} diff --git a/common/kernelports/pool_unix.go b/common/kernelports/pool_unix.go new file mode 100644 index 000000000..b2af15055 --- /dev/null +++ b/common/kernelports/pool_unix.go @@ -0,0 +1,87 @@ +//go:build !windows + +package kernelports + +import ( + "errors" + "net/netip" + "sync/atomic" + "syscall" + + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/gtcpip/header" + + "golang.org/x/sys/unix" +) + +const lowestSelector = 1024 + +type Pool struct { + tcp atomic.Pointer[[]tun.SelectorRange] + udp atomic.Pointer[[]tun.SelectorRange] +} + +func New() (*Pool, error) { + first, last := ephemeralRange() + below := tun.SelectorRange{Start: lowestSelector} + if first > lowestSelector { + below.Count = first - lowestSelector + } + above := tun.SelectorRange{Start: last + 1, Count: 65535 - last} + selected := below + if above.Count > below.Count { + selected = above + } + if selected.Count == 0 { + selected = tun.SelectorRange{Start: 49152, Count: 65535 - 49152 + 1} + } + ranges := []tun.SelectorRange{selected} + pool := &Pool{} + pool.tcp.Store(&ranges) + pool.udp.Store(&ranges) + return pool, nil +} + +func (p *Pool) Acquire(protocol uint8) (tun.SelectorRange, error) { + return tun.SelectorRange{}, ErrExhausted +} + +func (p *Pool) ReserveSelector(protocol uint8, address netip.AddrPort) bool { + var socketType int + switch protocol { + case uint8(header.TCPProtocolNumber): + socketType = unix.SOCK_STREAM + case uint8(header.UDPProtocolNumber): + socketType = unix.SOCK_DGRAM + default: + return true + } + family := unix.AF_INET + var sockaddr unix.Sockaddr + if address.Addr().Is4() { + sockaddr4 := &unix.SockaddrInet4{Port: int(address.Port())} + sockaddr4.Addr = address.Addr().As4() + sockaddr = sockaddr4 + } else { + family = unix.AF_INET6 + sockaddr6 := &unix.SockaddrInet6{Port: int(address.Port())} + sockaddr6.Addr = address.Addr().As16() + sockaddr = sockaddr6 + } + syscall.ForkLock.RLock() + fd, err := unix.Socket(family, socketType, 0) + if err == nil { + unix.CloseOnExec(fd) + } + syscall.ForkLock.RUnlock() + if err != nil { + return true + } + defer unix.Close(fd) + err = unix.Bind(fd, sockaddr) + return !errors.Is(err, unix.EADDRINUSE) +} + +func (p *Pool) Close() error { + return nil +} diff --git a/common/kernelports/pool_unix_other.go b/common/kernelports/pool_unix_other.go new file mode 100644 index 000000000..00358c825 --- /dev/null +++ b/common/kernelports/pool_unix_other.go @@ -0,0 +1,7 @@ +//go:build !windows && !linux && !darwin + +package kernelports + +func ephemeralRange() (uint16, uint16) { + return 49152, 65535 +} diff --git a/common/kernelports/pool_windows.go b/common/kernelports/pool_windows.go new file mode 100644 index 000000000..a506ae545 --- /dev/null +++ b/common/kernelports/pool_windows.go @@ -0,0 +1,121 @@ +package kernelports + +import ( + "encoding/binary" + "net/netip" + "sync" + "sync/atomic" + + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/gtcpip/header" + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/sys/windows" +) + +const ( + blockSize uint16 = 1024 + maxBlocks int = 8 +) + +// SIO_ACQUIRE_PORT_RESERVATION = _WSAIOW(IOC_VENDOR, 100): +// IOC_IN | IOC_VENDOR | 100. Despite the write-only direction code, the +// reservation result is written to the WSAIoctl output buffer; using +// _WSAIORW instead is rejected with WSAEOPNOTSUPP. +const sioAcquirePortReservation uint32 = 0x80000000 | 0x18000000 | 100 + +type Pool struct { + tcp atomic.Pointer[[]tun.SelectorRange] + udp atomic.Pointer[[]tun.SelectorRange] + + access sync.Mutex + blocks map[uint8][]windows.Handle +} + +func New() (*Pool, error) { + pool := &Pool{blocks: make(map[uint8][]windows.Handle)} + for _, protocol := range []uint8{uint8(header.TCPProtocolNumber), uint8(header.UDPProtocolNumber)} { + _, err := pool.Acquire(protocol) + if err != nil { + pool.Close() + return nil, err + } + } + return pool, nil +} + +// A runtime reservation only binds the protocol of the socket it was +// acquired on: both ephemeral auto-assignment and explicit binds of that +// protocol skip the block, while sockets of the other protocol still receive +// ports inside it (observed on Windows 11; the documentation does not say). +// Repeated wildcard requests from one process return distinct, +// non-contiguous blocks. Closing the socket releases the reservation. +func (p *Pool) Acquire(protocol uint8) (tun.SelectorRange, error) { + socketType, socketProtocol := windows.SOCK_STREAM, windows.IPPROTO_TCP + if protocol == uint8(header.UDPProtocolNumber) { + socketType, socketProtocol = windows.SOCK_DGRAM, windows.IPPROTO_UDP + } else { + protocol = uint8(header.TCPProtocolNumber) + } + p.access.Lock() + defer p.access.Unlock() + if len(p.blocks[protocol]) >= maxBlocks { + return tun.SelectorRange{}, ErrExhausted + } + socket, err := windows.Socket(windows.AF_INET, socketType, socketProtocol) + if err != nil { + return tun.SelectorRange{}, E.Cause(err, "create reservation socket") + } + // INET_PORT_RANGE { USHORT StartPort; USHORT NumberOfPorts; }. + // StartPort 0 requests a runtime (wildcard) reservation. + var in [4]byte + binary.LittleEndian.PutUint16(in[0:2], 0) + binary.LittleEndian.PutUint16(in[2:4], blockSize) + // INET_PORT_RESERVATION_INSTANCE { + // INET_PORT_RESERVATION { USHORT StartPort; USHORT NumberOfPorts; }; + // INET_PORT_RESERVATION_TOKEN { ULONG64 Token; }; + // } — the ULONG64 forces 8-byte alignment, so Token sits at offset 8. + var out [16]byte + var returned uint32 + err = windows.WSAIoctl(socket, sioAcquirePortReservation, + &in[0], uint32(len(in)), &out[0], uint32(len(out)), &returned, nil, 0) + if err != nil { + windows.Closesocket(socket) + return tun.SelectorRange{}, E.Cause(err, "acquire port reservation") + } + // StartPort is returned in network byte order (as documented for + // INET_PORT_RANGE); NumberOfPorts is a plain host-order count. + startPort := binary.BigEndian.Uint16(out[0:2]) + reservedCount := binary.LittleEndian.Uint16(out[2:4]) + if startPort == 0 || reservedCount < blockSize { + windows.Closesocket(socket) + return tun.SelectorRange{}, E.New("acquire port reservation: stack returned ", reservedCount, " of ", blockSize, " ports") + } + p.blocks[protocol] = append(p.blocks[protocol], socket) + block := tun.SelectorRange{Start: startPort, Count: blockSize} + ranges := p.ranges(protocol) + var updated []tun.SelectorRange + current := ranges.Load() + if current != nil { + updated = append(updated, *current...) + } + updated = append(updated, block) + ranges.Store(&updated) + return block, nil +} + +func (p *Pool) ReserveSelector(protocol uint8, address netip.AddrPort) bool { + return true +} + +func (p *Pool) Close() error { + p.access.Lock() + defer p.access.Unlock() + for _, sockets := range p.blocks { + for _, socket := range sockets { + windows.Closesocket(socket) + } + } + clear(p.blocks) + return nil +} diff --git a/protocol/bridge/backend_windows.go b/protocol/bridge/backend_windows.go index 9709a1801..e978083ed 100644 --- a/protocol/bridge/backend_windows.go +++ b/protocol/bridge/backend_windows.go @@ -14,8 +14,10 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/kernelports" "github.com/sagernet/sing-box/common/windivert" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun/gtcpip" "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common/control" @@ -26,8 +28,6 @@ import ( ) const ( - bridgeReservedPortCount uint16 = 1024 - bridgeICMPFlowTimeout = time.Minute bridgeDivertPriority int16 = 0 @@ -123,8 +123,9 @@ type backendWindows struct { egress atomic.Pointer[egressState] - reservation *portReservation - reservedStart uint16 + ports *kernelports.Pool + tcpRanges atomic.Pointer[[]tun.SelectorRange] + udpRanges atomic.Pointer[[]tun.SelectorRange] icmp4, icmp6 *icmpTable @@ -153,14 +154,19 @@ func (b *backendWindows) start(scope *adapter.Scope) error { state := b.currentEgressState() b.egress.Store(state) - err := b.acquireReservations() + ports, err := kernelports.New() if err != nil { - return err + return E.Cause(err, "bridge: reserve ports") } - scope.Add(func() error { - b.reservation.Close() - return nil - }) + b.ports = ports + scope.Add(ports.Close) + for _, protocol := range []uint8{uint8(header.TCPProtocolNumber), uint8(header.UDPProtocolNumber)} { + for _, block := range ports.PortSelectorRanges(protocol) { + b.publishRange(protocol, block) + } + } + b.icmp4 = newICMPTable(bridgeICMPFlowTimeout) + b.icmp6 = newICMPTable(bridgeICMPFlowTimeout) injectHandle, err := windivert.Open(nil, windivert.LayerNetwork, windivert.PriorityHighest, windivert.FlagSendOnly) if err != nil { @@ -201,20 +207,62 @@ func (b *backendWindows) egressLabel() string { return "auto" } -func (b *backendWindows) acquireReservations() error { - reservation, err := acquirePortReservation(windows.AF_INET, windows.SOCK_STREAM, windows.IPPROTO_TCP, bridgeReservedPortCount) - if err != nil { - return E.Cause(err, "bridge: reserve ports") +func (b *backendWindows) selectorRanges(protocol uint8) *atomic.Pointer[[]tun.SelectorRange] { + if protocol == uint8(header.UDPProtocolNumber) { + return &b.udpRanges } - b.reservation = reservation - b.reservedStart = reservation.startPort - b.icmp4 = newICMPTable(bridgeICMPFlowTimeout) - b.icmp6 = newICMPTable(bridgeICMPFlowTimeout) - return nil + return &b.tcpRanges } -func (b *backendWindows) PortSelectorRange() (uint16, uint16) { - return b.reservedStart, bridgeReservedPortCount +func (b *backendWindows) PortSelectorRanges(protocol uint8) []tun.SelectorRange { + ranges := b.selectorRanges(protocol).Load() + if ranges == nil { + return nil + } + return *ranges +} + +func (b *backendWindows) publishRange(protocol uint8, block tun.SelectorRange) { + ranges := b.selectorRanges(protocol) + var updated []tun.SelectorRange + current := ranges.Load() + if current != nil { + updated = append(updated, *current...) + } + updated = append(updated, block) + ranges.Store(&updated) +} + +func (b *backendWindows) ExpandSelectorRanges(protocol uint8) bool { + b.egressAccess.Lock() + defer b.egressAccess.Unlock() + block, err := b.ports.Acquire(protocol) + if err != nil { + b.logger.Debug(E.Cause(err, "bridge: expand selector range")) + return false + } + state := b.egress.Load() + for _, isV6 := range []bool{false, true} { + addresses := state.divertAddresses(isV6) + if len(addresses) == 0 { + continue + } + err = b.openTransportDiverter(addresses, isV6, protocol, block) + if err != nil { + b.logger.Warn(E.Cause(err, "bridge: divert expanded selector range")) + return false + } + } + b.publishRange(protocol, block) + b.logger.Info("bridge: reserved ", block.Count, " more ", transportName(protocol), " ports") + return true +} + +func transportName(protocol uint8) string { + if protocol == uint8(header.UDPProtocolNumber) { + return "UDP" + } + return "TCP" } func (b *backendWindows) rebuildDivertersLocked(state *egressState) error { @@ -245,19 +293,37 @@ func (b *backendWindows) closeDivertersLocked() { b.diverters = nil } +func (b *backendWindows) openTransportDiverter(addresses []netip.Addr, isV6 bool, protocol uint8, block tun.SelectorRange) error { + portHigh := uint16(uint32(block.Start) + uint32(block.Count) - 1) + var ( + filter *windivert.Filter + err error + ) + if protocol == uint8(header.UDPProtocolNumber) { + filter, err = windivert.InboundUDPPortRange(addresses, block.Start, portHigh) + } else { + filter, err = windivert.InboundTCPPortRange(addresses, block.Start, portHigh) + } + if err != nil { + return E.Cause(err, "bridge: build ", transportName(protocol), " divert filter") + } + return b.openDiverter(filter, divertTransport, isV6) +} + func (b *backendWindows) openFamilyDiverters(addresses []netip.Addr, isV6 bool) error { - portHigh := uint16(uint32(b.reservedStart) + uint32(bridgeReservedPortCount) - 1) + for _, protocol := range []uint8{uint8(header.TCPProtocolNumber), uint8(header.UDPProtocolNumber)} { + for _, block := range b.PortSelectorRanges(protocol) { + err := b.openTransportDiverter(addresses, isV6, protocol, block) + if err != nil { + return err + } + } + } entries := []struct { what string kind divertKind build func() (*windivert.Filter, error) }{ - {"TCP", divertTransport, func() (*windivert.Filter, error) { - return windivert.InboundTCPPortRange(addresses, b.reservedStart, portHigh) - }}, - {"UDP", divertTransport, func() (*windivert.Filter, error) { - return windivert.InboundUDPPortRange(addresses, b.reservedStart, portHigh) - }}, {"ICMP echo", divertICMPEcho, func() (*windivert.Filter, error) { return windivert.InboundICMPEchoReply(addresses) }}, @@ -474,7 +540,7 @@ func (b *backendWindows) embeddedFlowActive(protocol tcpip.TransportProtocolNumb if len(transport) < 4 { return false } - return b.portReserved(binary.BigEndian.Uint16(transport[0:2])) + return b.selectorReserved(uint8(protocol), binary.BigEndian.Uint16(transport[0:2])) case header.ICMPv4ProtocolNumber: if isV6 || len(transport) < header.ICMPv4MinimumSize { return false @@ -500,8 +566,10 @@ func (b *backendWindows) embeddedFlowActive(protocol tcpip.TransportProtocolNumb } } -func (b *backendWindows) portReserved(port uint16) bool { - return port >= b.reservedStart && uint32(port) < uint32(b.reservedStart)+uint32(bridgeReservedPortCount) +func (b *backendWindows) selectorReserved(protocol uint8, port uint16) bool { + return slices.ContainsFunc(b.PortSelectorRanges(protocol), func(block tun.SelectorRange) bool { + return port >= block.Start && port-block.Start < block.Count + }) } func (b *backendWindows) deliver(packets [][]byte) { @@ -659,7 +727,7 @@ func (b *backendWindows) prepareOutbound(packet []byte, state *egressState) bool if len(info.transport) < 4 { return false } - if !b.portReserved(binary.BigEndian.Uint16(info.transport[0:2])) { + if !b.selectorReserved(uint8(info.protocol), binary.BigEndian.Uint16(info.transport[0:2])) { b.logger.Debug("bridge: dropping outbound packet with source port outside the reserved block") return false } diff --git a/protocol/bridge/outbound.go b/protocol/bridge/outbound.go index e4db72d43..eaaaa6333 100644 --- a/protocol/bridge/outbound.go +++ b/protocol/bridge/outbound.go @@ -106,11 +106,8 @@ func (o *Outbound) PortMTU() uint32 { return o.backend.PortMTU() } -func (o *Outbound) PortSelectorRange() (uint16, uint16) { - if rangedBackend, isRanged := o.backend.(tun.PortWithSelectorRange); isRanged { - return rangedBackend.PortSelectorRange() - } - return 0, 0 +func (o *Outbound) UpstreamPort() any { + return o.backend } func (o *Outbound) AttachReturn(returnPath tun.Return) error { diff --git a/protocol/bridge/reservation_windows.go b/protocol/bridge/reservation_windows.go deleted file mode 100644 index 22f848cb3..000000000 --- a/protocol/bridge/reservation_windows.go +++ /dev/null @@ -1,73 +0,0 @@ -//go:build windows && (amd64 || 386) - -package bridge - -import ( - "encoding/binary" - - E "github.com/sagernet/sing/common/exceptions" - - "golang.org/x/sys/windows" -) - -// SIO_ACQUIRE_PORT_RESERVATION = _WSAIOW(IOC_VENDOR, 100): -// IOC_IN | IOC_VENDOR | 100. Despite the write-only direction code, the -// reservation result is written to the WSAIoctl output buffer; using -// _WSAIORW instead is rejected with WSAEOPNOTSUPP. -const sioAcquirePortReservation uint32 = 0x80000000 | 0x18000000 | 100 - -// portReservation holds a runtime port block acquired from the host TCP/IP -// stack. Runtime reservation records are protocol- and family-agnostic: -// one reservation excludes the block from ephemeral auto-assignment for -// TCP and UDP sockets of both address families (and a specific reservation -// request for numbers covered by any existing record fails with -// WSAEADDRINUSE, whatever its protocol). Explicit binds inside the block -// are rejected for the reserving protocol but still allowed for others. -// Closing the socket releases the reservation. -type portReservation struct { - socket windows.Handle - startPort uint16 -} - -func acquirePortReservation(family, socketType, protocol int, count uint16) (*portReservation, error) { - socket, err := windows.Socket(family, socketType, protocol) - if err != nil { - return nil, E.Cause(err, "create reservation socket") - } - // INET_PORT_RANGE { USHORT StartPort; USHORT NumberOfPorts; }. - // StartPort 0 requests a runtime (wildcard) reservation. - var in [4]byte - binary.LittleEndian.PutUint16(in[0:2], 0) - binary.LittleEndian.PutUint16(in[2:4], count) - // INET_PORT_RESERVATION_INSTANCE { - // INET_PORT_RESERVATION { USHORT StartPort; USHORT NumberOfPorts; }; - // INET_PORT_RESERVATION_TOKEN { ULONG64 Token; }; - // } — the ULONG64 forces 8-byte alignment, so Token sits at offset 8. - var out [16]byte - var returned uint32 - err = windows.WSAIoctl(socket, sioAcquirePortReservation, - &in[0], uint32(len(in)), &out[0], uint32(len(out)), &returned, nil, 0) - if err != nil { - windows.Closesocket(socket) - return nil, E.Cause(err, "acquire port reservation") - } - // StartPort is returned in network byte order (as documented for - // INET_PORT_RANGE); NumberOfPorts is a plain host-order count. - startPort := binary.BigEndian.Uint16(out[0:2]) - reservedCount := binary.LittleEndian.Uint16(out[2:4]) - if startPort == 0 || reservedCount < count { - windows.Closesocket(socket) - return nil, E.New("acquire port reservation: stack returned ", reservedCount, " of ", count, " ports") - } - return &portReservation{ - socket: socket, - startPort: startPort, - }, nil -} - -func (r *portReservation) Close() { - if r == nil { - return - } - windows.Closesocket(r.socket) -} diff --git a/protocol/masque/client.go b/protocol/masque/client.go index 27ec286d1..8403f4c53 100644 --- a/protocol/masque/client.go +++ b/protocol/masque/client.go @@ -250,6 +250,10 @@ func (c *ClientEndpoint) PortMTU() uint32 { return c.device.PortMTU() } +func (c *ClientEndpoint) UpstreamPort() any { + return c.device +} + func (c *ClientEndpoint) AttachReturn(returnPath tun.Return) error { return c.device.AttachReturn(returnPath) } diff --git a/protocol/masque/server.go b/protocol/masque/server.go index 2640832dc..f400a10a8 100644 --- a/protocol/masque/server.go +++ b/protocol/masque/server.go @@ -230,6 +230,10 @@ func (s *ServerEndpoint) PortMTU() uint32 { return s.device.PortMTU() } +func (s *ServerEndpoint) UpstreamPort() any { + return s.device +} + func (s *ServerEndpoint) AttachReturn(returnPath tun.Return) error { return s.device.AttachReturn(returnPath) } diff --git a/protocol/openconnect/client.go b/protocol/openconnect/client.go index c192ad2bc..85c893035 100644 --- a/protocol/openconnect/client.go +++ b/protocol/openconnect/client.go @@ -556,6 +556,10 @@ func (e *Endpoint) PortMTU() uint32 { return e.device.PortMTU() } +func (e *Endpoint) UpstreamPort() any { + return e.device +} + func (e *Endpoint) AttachReturn(returnPath tun.Return) error { return e.device.AttachReturn(returnPath) } diff --git a/protocol/openvpn/client.go b/protocol/openvpn/client.go index f6ebbbff4..b07ad149f 100644 --- a/protocol/openvpn/client.go +++ b/protocol/openvpn/client.go @@ -739,6 +739,10 @@ func (c *ClientEndpoint) PortMTU() uint32 { return c.device.PortMTU() } +func (c *ClientEndpoint) UpstreamPort() any { + return c.device +} + func (c *ClientEndpoint) AttachReturn(returnPath tun.Return) error { return c.device.AttachReturn(returnPath) } diff --git a/protocol/openvpn/server.go b/protocol/openvpn/server.go index 9d21d493c..1026579f5 100644 --- a/protocol/openvpn/server.go +++ b/protocol/openvpn/server.go @@ -655,6 +655,10 @@ func (s *ServerEndpoint) PortMTU() uint32 { return s.device.PortMTU() } +func (s *ServerEndpoint) UpstreamPort() any { + return s.device +} + func (s *ServerEndpoint) AttachReturn(returnPath tun.Return) error { return s.device.AttachReturn(returnPath) } diff --git a/protocol/tailscale/port.go b/protocol/tailscale/port.go index 00587c3a4..fb382504e 100644 --- a/protocol/tailscale/port.go +++ b/protocol/tailscale/port.go @@ -36,6 +36,13 @@ func (t *Endpoint) PortMTU() uint32 { return uint32(tsTUN.DefaultTUNMTU()) } +func (t *Endpoint) UpstreamPort() any { + if !t.started.Load() { + return nil + } + return t.stack +} + func (t *Endpoint) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict { inet4Address, inet6Address := t.PortAddresses() if destination.Addr() == inet4Address || destination.Addr() == inet6Address { diff --git a/protocol/wireguard/endpoint.go b/protocol/wireguard/endpoint.go index 926ab771b..cb50d754a 100644 --- a/protocol/wireguard/endpoint.go +++ b/protocol/wireguard/endpoint.go @@ -202,6 +202,10 @@ func (w *Endpoint) PortMTU() uint32 { return w.endpoint.PortMTU() } +func (w *Endpoint) UpstreamPort() any { + return w.endpoint +} + func (w *Endpoint) AttachReturn(returnPath tun.Return) error { return w.endpoint.AttachReturn(returnPath) } diff --git a/route/route.go b/route/route.go index af0b785f0..24befe383 100644 --- a/route/route.go +++ b/route/route.go @@ -547,7 +547,7 @@ func (r *Router) preMatchFlow(ctx context.Context, metadata *adapter.InboundCont metadata.OutboundChain = chain metadataCopy := *metadata result.NewTracker = func() tun.FlowTracker { - r.logger.InfoContext(ctx, "pre-match: forward ", metadataCopy.Network, " connection from ", metadataCopy.Source.AddrString(), " to ", metadataCopy.Destination.AddrString(), " via outbound/", outbound.Type(), "[", outbound.Tag(), "]") + r.logger.InfoContext(ctx, "pre-match: forward ", metadataCopy.Network, " connection from ", metadataCopy.Source, " to ", metadataCopy.Destination, " via outbound/", outbound.Type(), "[", outbound.Tag(), "]") flowTrackers := make([]tun.FlowTracker, 0, len(r.trackers)+2) flowTrackers = append(flowTrackers, newFlowLogger(ctx, r.logger, metadataCopy, outbound)) flowInterrupter := newFlowInterrupter(chain) diff --git a/transport/device/device.go b/transport/device/device.go index ee9d4428c..46c5041ec 100644 --- a/transport/device/device.go +++ b/transport/device/device.go @@ -27,6 +27,7 @@ type Device interface { PortMTU() uint32 AttachReturn(returnPath tun.Return) error DetachReturn(returnPath tun.Return) error + UpstreamPort() any FrontHeadroom() int NewOutboundQueue(handler func(packetBuffers []*buf.Buffer)) *tun.OutboundQueue Close() error diff --git a/transport/device/stack.go b/transport/device/stack.go index 4721120b6..d92e66516 100644 --- a/transport/device/stack.go +++ b/transport/device/stack.go @@ -145,6 +145,10 @@ func (d *stackDevice) PortMTU() uint32 { return d.options.MTU } +func (d *stackDevice) UpstreamPort() any { + return d.stack +} + func (d *stackDevice) NewOutboundQueue(handler func(packetBuffers []*buf.Buffer)) *tun.OutboundQueue { return d.memoryTun.NewOutboundQueue(handler) } diff --git a/transport/device/system.go b/transport/device/system.go index e4740a181..016e9bc8f 100644 --- a/transport/device/system.go +++ b/transport/device/system.go @@ -10,6 +10,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/kernelports" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun/gtcpip/header" @@ -29,6 +30,7 @@ type systemDevice struct { options Options dialer N.Dialer device tun.Tun + ports *kernelports.Pool inet4Address netip.Addr inet6Address netip.Addr closed bool @@ -58,7 +60,16 @@ func newSystemDevice(options Options) (*systemDevice, error) { func (d *systemDevice) Start() error { d.stateAccess.Lock() defer d.stateAccess.Unlock() - return d.startLocked() + err := d.startLocked() + if err != nil { + return err + } + ports, err := kernelports.New() + if err != nil { + d.options.Logger.Warn(E.Cause(err, "reserve selector ports")) + } + d.ports = ports + return nil } func (d *systemDevice) startLocked() error { @@ -365,15 +376,28 @@ func (d *systemDevice) PortMTU() uint32 { return d.options.MTU } +func (d *systemDevice) UpstreamPort() any { + d.stateAccess.RLock() + defer d.stateAccess.RUnlock() + if d.ports == nil { + return nil + } + return d.ports +} + func (d *systemDevice) Close() error { d.stateAccess.Lock() defer d.stateAccess.Unlock() d.closed = true - if d.device == nil { - return nil + var err error + if d.device != nil { + err = d.device.Close() + d.device = nil + } + if d.ports != nil { + err = E.Errors(err, d.ports.Close()) + d.ports = nil } - err := d.device.Close() - d.device = nil return err } diff --git a/transport/wireguard/device.go b/transport/wireguard/device.go index a9367ea7d..3988fc2cf 100644 --- a/transport/wireguard/device.go +++ b/transport/wireguard/device.go @@ -21,6 +21,7 @@ type Device interface { SetDevice(device *device.Device, peers []*device.Peer) Inet4Address() netip.Addr Inet6Address() netip.Addr + UpstreamPort() any } type DeviceOptions struct { diff --git a/transport/wireguard/device_stack.go b/transport/wireguard/device_stack.go index 564b3908d..620a1789b 100644 --- a/transport/wireguard/device_stack.go +++ b/transport/wireguard/device_stack.go @@ -107,6 +107,10 @@ func (w *stackDevice) Inet6Address() netip.Addr { return w.inet6Address } +func (w *stackDevice) UpstreamPort() any { + return w.stack +} + func (w *stackDevice) SetDevice(device *device.Device, peers []*device.Peer) { w.router.setPeers(device.AllowedIPs(), peers) } diff --git a/transport/wireguard/device_system.go b/transport/wireguard/device_system.go index 93ac3c14f..54d82fed9 100644 --- a/transport/wireguard/device_system.go +++ b/transport/wireguard/device_system.go @@ -8,10 +8,13 @@ import ( "os" "runtime" "sync" + "sync/atomic" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/kernelports" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" @@ -25,6 +28,7 @@ type systemDevice struct { options DeviceOptions dialer N.Dialer device tun.Tun + ports atomic.Pointer[kernelports.Pool] batchDevice tun.LinuxTUN events chan wgTun.Event closeOnce sync.Once @@ -65,6 +69,14 @@ func (w *systemDevice) Inet6Address() netip.Addr { func (w *systemDevice) SetDevice(device *device.Device, peers []*device.Peer) { } +func (w *systemDevice) UpstreamPort() any { + ports := w.ports.Load() + if ports == nil { + return nil + } + return ports +} + func (w *systemDevice) Start() error { networkManager := service.FromContext[adapter.NetworkManager](w.options.Context) tunOptions := tun.Options{ @@ -100,6 +112,12 @@ func (w *systemDevice) Start() error { tunInterface.Close() return err } + ports, err := kernelports.New() + if err != nil { + w.options.Logger.Warn(E.Cause(err, "reserve selector ports")) + } else { + w.ports.Store(ports) + } w.options.Logger.Info("started at ", w.options.Name) w.device = tunInterface batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN) @@ -170,6 +188,10 @@ func (w *systemDevice) Close() error { if w.device != nil { err = w.device.Close() } + ports := w.ports.Load() + if ports != nil { + err = E.Errors(err, ports.Close()) + } }) return err } diff --git a/transport/wireguard/port.go b/transport/wireguard/port.go index ecd72bce1..b0896da1d 100644 --- a/transport/wireguard/port.go +++ b/transport/wireguard/port.go @@ -18,6 +18,10 @@ func (e *Endpoint) PortMTU() uint32 { return e.options.MTU } +func (e *Endpoint) UpstreamPort() any { + return e.tunDevice +} + func (e *Endpoint) WritePackets(packets [][]byte) error { wgDevice := e.device.Load() if wgDevice == nil {