Rework forward NAT with UDP mapping and fragment support

This commit is contained in:
世界 2026-10-06 20:44:55 +08:00
parent a2b0923e01
commit df8e2edfd5
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
24 changed files with 507 additions and 116 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
//go:build !windows && !linux && !darwin
package kernelports
func ephemeralRange() (uint16, uint16) {
return 49152, 65535
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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