Merge upstream/testing (L3-forwarding, snell, bridge) into lx-1.14

Merges 14 upstream commits including L3-forwarding support (which bumped
wireguard-go v0.0.3->v0.0.5, already re-grafted in the prior commit),
snell protocol, bridge outbound, flow-tracking/sniff improvements, and
DNS/dialer fixes.

lx conflict resolutions:
- protocol/wireguard/endpoint.go: took upstream's new flow API
  (PreMatchFlow/PortAddresses/PortMTU/AttachReturn/DetachReturn/JudgeFlow),
  dropped our old PrepareConnection/NewDirectRouteConnection. SPEC 020
  idle-suspend wake guard (resumeOnDial) moved to WritePackets — the single
  point every L3-forwarded packet transits, incl. established flows that
  bypass DialContext.
- adapter/outbound.go: kept lx IdleSuspendable/ReachabilityInvalidator,
  restored 'time' import dropped by auto-merge.
- go.mod/go.sum + test/: took upstream dependency bumps (tailscale, sing,
  sing-tun); wireguard-go stays v0.0.5 with local submodule replace.

Green: full sing-box CLI with LX_TAGS (Go 1.24.7), libbox, wireguard/
adapter/dns/daemon packages, transport+protocol/wireguard tests, AWG
config validation.
This commit is contained in:
Leadaxe
2026-07-08 15:10:38 +03:00
84 changed files with 7100 additions and 736 deletions
+1
View File
@@ -94,6 +94,7 @@ type InboundContext struct {
SourceHostname string
QueryType uint16
FakeIP bool
PreMatch bool
// rule cache
+5 -4
View File
@@ -23,13 +23,14 @@ type Outbound interface {
type OutboundWithPreferredRoutes interface {
Outbound
PreferredDomain(domain string) bool
PreferredAddress(address netip.Addr) bool
PreferredDomain(metadata *InboundContext, domain string) bool
PreferredAddress(metadata *InboundContext, address netip.Addr) bool
}
type DirectRouteOutbound interface {
type FlowOutbound interface {
Outbound
NewDirectRouteConnection(metadata InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
tun.Port
PreMatchFlow(network string, destination netip.Addr) PreMatchAction
}
type OutboundRegistry interface {
+21
View File
@@ -51,6 +51,27 @@ type PlatformInterface interface {
LookupSFTPServer() (string, error)
ReadSystemSSHHostKey() ([]byte, error)
TailscaleHostname() string
UsePlatformBridge() bool
CreateBridge(options BridgeOptions) (BridgeSession, error)
}
type BridgeOptions struct {
BridgeName string
MTU uint32
Inet4Port netip.Addr
Inet6Port netip.Addr
Interface string
RuleIndex int
RouteTable int
}
type BridgeSession interface {
FileDescriptor() int
Name() string
Inet6Active() bool
SetEgress(interfaceName string) error
Close() error
}
type PlatformUser struct {
+73 -1
View File
@@ -3,9 +3,12 @@ package adapter
import (
"context"
"net"
"net/netip"
"time"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
@@ -15,7 +18,7 @@ import (
type Router interface {
Lifecycle
ConnectionRouter
PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration, supportBypass bool) (tun.DirectRouteDestination, error)
PreMatch(metadata InboundContext, firstPacket []byte) PreMatchResult
ConnectionRouterEx
RuleSet(tag string) (RuleSet, bool)
Rules() []Rule
@@ -26,9 +29,78 @@ type Router interface {
ResetNetwork()
}
type PreMatchAction uint8
const (
PreMatchContinue PreMatchAction = iota
PreMatchFlow
PreMatchReject
PreMatchDrop
PreMatchBypass
)
type PreMatchResult struct {
Action PreMatchAction
Outbound Outbound
Destination netip.AddrPort
UDPTimeout time.Duration
NewTracker func() tun.FlowTracker
}
func JudgeFlow(router Router, inbound string, inboundType string, network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict {
var networkName string
switch network {
case uint8(header.TCPProtocolNumber):
networkName = N.NetworkTCP
case uint8(header.UDPProtocolNumber):
networkName = N.NetworkUDP
case uint8(header.ICMPv4ProtocolNumber), uint8(header.ICMPv6ProtocolNumber):
networkName = N.NetworkICMP
default:
return tun.FlowVerdict{Action: tun.ActionAccept}
}
metadata := InboundContext{
Inbound: inbound,
InboundType: inboundType,
Network: networkName,
Source: M.SocksaddrFromNetIP(source),
Destination: M.SocksaddrFromNetIP(destination),
}
if networkName == N.NetworkICMP {
metadata.Source.Port = 0
metadata.Destination.Port = 0
}
result := router.PreMatch(metadata, firstPacket)
switch result.Action {
case PreMatchFlow:
port, isPort := result.Outbound.(tun.Port)
if !isPort {
return tun.FlowVerdict{Action: tun.ActionAccept}
}
verdict := tun.FlowVerdict{Action: tun.ActionFlow, Port: port, UDPTimeout: result.UDPTimeout, NewTracker: result.NewTracker}
if result.Destination.IsValid() {
destinationPort := result.Destination.Port()
if networkName == N.NetworkICMP {
destinationPort = destination.Port()
}
verdict.Destination = netip.AddrPortFrom(result.Destination.Addr(), destinationPort)
}
return verdict
case PreMatchReject:
return tun.FlowVerdict{Action: tun.ActionReject}
case PreMatchDrop:
return tun.FlowVerdict{Action: tun.ActionDrop}
case PreMatchBypass:
return tun.FlowVerdict{Action: tun.ActionBypass}
default:
return tun.FlowVerdict{Action: tun.ActionAccept}
}
}
type ConnectionTracker interface {
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) net.Conn
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) N.PacketConn
RoutedFlow(ctx context.Context, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) tun.FlowTracker
}
// Deprecated: Use ConnectionRouterEx instead.
@@ -19,6 +19,27 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di
return nil, false, E.New("no available network interface")
}
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
if len(primaryInterfaces)+len(fallbackInterfaces) == 1 {
var (
iif adapter.NetworkInterface
primary bool
)
if len(primaryInterfaces) == 1 {
iif = primaryInterfaces[0]
primary = true
} else {
iif = fallbackInterfaces[0]
}
perNetDialer := dialer
if defaultInterface == nil || iif.Index != defaultInterface.Index {
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
}
conn, err := perNetDialer.DialContext(ctx, network, addr)
if err != nil {
return nil, false, E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")")
}
return conn, primary, nil
}
if fallbackDelay == 0 {
fallbackDelay = N.DefaultFallbackDelay
}
@@ -93,6 +114,27 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d
return nil, false, E.New("no available network interface")
}
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
if len(primaryInterfaces)+len(fallbackInterfaces) == 1 {
var (
iif adapter.NetworkInterface
primary bool
)
if len(primaryInterfaces) == 1 {
iif = primaryInterfaces[0]
primary = true
} else {
iif = fallbackInterfaces[0]
}
perNetDialer := dialer
if defaultInterface == nil || iif.Index != defaultInterface.Index {
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
}
conn, err := perNetDialer.DialContext(ctx, network, addr)
if err != nil {
return nil, false, E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")")
}
return conn, primary, nil
}
if fallbackDelay == 0 {
fallbackDelay = N.DefaultFallbackDelay
}
+10 -21
View File
@@ -520,10 +520,6 @@ func (w *appleTLSReadWaiter) WaitReadBuffer() (*buf.Buffer, error) {
if c.readEOF {
return nil, io.EOF
}
maximumLen := readWaitFreeLen(w.options)
if maximumLen <= 0 {
return nil, io.ErrShortBuffer
}
timeoutMs, err := c.prepareReadTimeout()
if err != nil {
return nil, err
@@ -533,11 +529,18 @@ func (w *appleTLSReadWaiter) WaitReadBuffer() (*buf.Buffer, error) {
return nil, err
}
defer c.releaseClient()
buffer := w.options.NewBuffer()
if buffer.IsFull() {
buffer.Release()
return nil, io.ErrShortBuffer
}
maximumLen := buffer.FreeLen()
handle := cgo.NewHandle(w)
defer handle.Delete()
var errorPtr *C.char
if !bool(C.box_apple_tls_client_read_async(client, C.size_t(maximumLen), C.uintptr_t(handle), &errorPtr)) {
buffer.Release()
return nil, c.errorFromPointer(errorPtr)
}
@@ -553,22 +556,18 @@ func (w *appleTLSReadWaiter) WaitReadBuffer() (*buf.Buffer, error) {
if result != nil {
C.box_apple_tls_read_result_free(result)
}
buffer.Release()
c.markReadTimedOut()
return nil, os.ErrDeadlineExceeded
}
} else {
result = <-w.results
}
return c.readWaitResultToBuffer(result, w.options)
return c.readWaitResultToBuffer(result, buffer, w.options)
}
func (c *appleTLSConn) readWaitResultToBuffer(result *C.box_apple_tls_read_result_t, options N.ReadWaitOptions) (*buf.Buffer, error) {
func (c *appleTLSConn) readWaitResultToBuffer(result *C.box_apple_tls_read_result_t, buffer *buf.Buffer, options N.ReadWaitOptions) (*buf.Buffer, error) {
defer C.box_apple_tls_read_result_free(result)
buffer := options.NewBuffer()
if buffer.IsFull() {
buffer.Release()
return nil, io.ErrShortBuffer
}
startLen := buffer.Len()
var eof C.bool
var errorPtr *C.char
@@ -593,16 +592,6 @@ func (c *appleTLSConn) readWaitResultToBuffer(result *C.box_apple_tls_read_resul
return buffer, nil
}
func readWaitFreeLen(options N.ReadWaitOptions) int {
if options.IncreaseBuffer {
return 65535 - options.FrontHeadroom - options.RearHeadroom
}
if options.MTU > 0 {
return options.MTU
}
return buf.BufferSize - options.FrontHeadroom - options.RearHeadroom
}
//export box_apple_tls_read_callback
func box_apple_tls_read_callback(callbackHandle C.uintptr_t, result *C.box_apple_tls_read_result_t) {
handle := cgo.Handle(callbackHandle)
+55
View File
@@ -7,6 +7,7 @@ import (
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
N "github.com/sagernet/sing/common/network"
@@ -76,6 +77,13 @@ func (m *Manager) RoutedPacketConnection(ctx context.Context, conn N.PacketConn,
return tracker
}
func (m *Manager) RoutedFlow(ctx context.Context, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) tun.FlowTracker {
return &flowTracker{
metadata: m.newTrackerMetadata(metadata, matchedRule, matchOutbound, new(atomic.Int64), new(atomic.Int64)),
manager: m,
}
}
func (m *Manager) newTrackerMetadata(metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound, upload *atomic.Int64, download *atomic.Int64) TrackerMetadata {
id, _ := uuid.NewV4()
var (
@@ -186,6 +194,53 @@ func (t *connTracker) WriterReplaceable() bool {
return true
}
var (
_ Tracker = (*flowTracker)(nil)
_ tun.FlowTracker = (*flowTracker)(nil)
)
type flowTracker struct {
metadata TrackerMetadata
manager *Manager
handle tun.FlowHandle
}
func (t *flowTracker) Metadata() *TrackerMetadata {
return &t.metadata
}
func (t *flowTracker) AttachFlow(handle tun.FlowHandle) {
t.handle = handle
t.manager.join(t)
}
func (t *flowTracker) CountForward(n int) {
t.metadata.Upload.Add(int64(n))
t.manager.uploadTotal.Add(int64(n))
}
func (t *flowTracker) CountReverse(n int) {
t.metadata.Download.Add(int64(n))
t.manager.downloadTotal.Add(int64(n))
}
func (t *flowTracker) FlowEstablished() {
}
func (t *flowTracker) CloseFlow(reason tun.FlowCloseReason) {
t.manager.leave(t)
}
func (t *flowTracker) Close() error {
handle := t.handle
if handle != nil {
handle.CloseFlow()
} else {
t.manager.leave(t)
}
return nil
}
type packetConnTracker struct {
N.PacketConn
metadata TrackerMetadata
+89 -3
View File
@@ -21,27 +21,39 @@ const (
filterMaxInsts = 256
fieldZero = 0
fieldInbound = 1
fieldOutbound = 2
fieldIP = 5
fieldIPv6 = 6
fieldICMP = 7
fieldTCP = 8
fieldUDP = 9
fieldICMPv6 = 10
fieldIPSrcAddr = 21
fieldIPDstAddr = 22
fieldIPv6SrcAddr = 28
fieldIPv6DstAddr = 29
fieldICMPType = 30
fieldICMPv6Type = 34
fieldTCPSrcPort = 38
fieldTCPDstPort = 39
fieldUDPSrcPort = 53
fieldUDPDstPort = 54
testEQ = 0
testEQ = 0
testLEQ = 3
testGEQ = 5
resultAccept uint16 = 0x7FFE
resultReject uint16 = 0x7FFF
)
// Filter flags passed to IOCTL_WINDIVERT_STARTUP alongside the compiled
// filter. These tell the driver what *kinds* of packets the filter might
// match, used as a kernel-side fast-reject.
// filter. The driver installs WFP callouts only for the directions and
// address families named here (windivert_install_callouts), so a filter
// missing its direction flag never sees a packet.
const (
filterFlagInbound uint64 = 0x0010
filterFlagOutbound uint64 = 0x0020
filterFlagIP uint64 = 0x0040
filterFlagIPv6 uint64 = 0x0080
@@ -104,6 +116,80 @@ func OutboundTCP(src, dst netip.AddrPort) (*Filter, error) {
return f, nil
}
func inboundTo(destination netip.Addr) (*Filter, error) {
if !destination.IsValid() {
return nil, E.New("windivert: filter: invalid address")
}
f := &Filter{
flags: filterFlagInbound,
}
f.add(fieldInbound, testEQ, argUint32(1))
if destination.Is4() {
f.flags |= filterFlagIP
f.add(fieldIP, testEQ, argUint32(1))
f.add(fieldIPDstAddr, testEQ, argIPv4(destination))
} else {
f.flags |= filterFlagIPv6
f.add(fieldIPv6, testEQ, argUint32(1))
f.add(fieldIPv6DstAddr, testEQ, argIPv6(destination))
}
return f, nil
}
func InboundTCPPortRange(destination netip.Addr, portLow, portHigh uint16) (*Filter, error) {
f, err := inboundTo(destination)
if err != nil {
return nil, err
}
f.add(fieldTCP, testEQ, argUint32(1))
f.add(fieldTCPDstPort, testGEQ, argUint32(uint32(portLow)))
f.add(fieldTCPDstPort, testLEQ, argUint32(uint32(portHigh)))
return f, nil
}
func InboundUDPPortRange(destination netip.Addr, portLow, portHigh uint16) (*Filter, error) {
f, err := inboundTo(destination)
if err != nil {
return nil, err
}
f.add(fieldUDP, testEQ, argUint32(1))
f.add(fieldUDPDstPort, testGEQ, argUint32(uint32(portLow)))
f.add(fieldUDPDstPort, testLEQ, argUint32(uint32(portHigh)))
return f, nil
}
func InboundICMPEchoReply(destination netip.Addr) (*Filter, error) {
f, err := inboundTo(destination)
if err != nil {
return nil, err
}
if destination.Is4() {
f.add(fieldICMP, testEQ, argUint32(1))
f.add(fieldICMPType, testEQ, argUint32(0))
} else {
f.add(fieldICMPv6, testEQ, argUint32(1))
f.add(fieldICMPv6Type, testEQ, argUint32(129))
}
return f, nil
}
func InboundICMPError(destination netip.Addr) (*Filter, error) {
f, err := inboundTo(destination)
if err != nil {
return nil, err
}
if destination.Is4() {
f.add(fieldICMP, testEQ, argUint32(1))
f.add(fieldICMPType, testGEQ, argUint32(3))
f.add(fieldICMPType, testLEQ, argUint32(12))
} else {
f.add(fieldICMPv6, testEQ, argUint32(1))
f.add(fieldICMPv6Type, testGEQ, argUint32(1))
f.add(fieldICMPv6Type, testLEQ, argUint32(4))
}
return f, nil
}
func (f *Filter) add(field uint16, test uint8, arg [4]uint32) {
f.insts = append(f.insts, filterInst{field: field, test: test, arg: arg})
}
+75 -11
View File
@@ -26,11 +26,14 @@ import (
// because Go's escape analysis does not see the pointer through the
// unsafe.Pointer → uintptr → bytes conversion.
type Handle struct {
device windows.Handle
event windows.Handle
closing sync.Once
closeErr error
addr Address
device windows.Handle
event windows.Handle
closing sync.Once
closeErr error
addr Address
recvAddrs []Address
recvAddrsLen uint32
sendAddrs []Address
}
// Filter may be nil for "reject all", suitable for send-only handles.
@@ -169,10 +172,60 @@ func (h *Handle) Recv(buf []byte) (int, Address, error) {
return int(n), h.addr, nil
}
// BatchMax is WINDIVERT_BATCH_MAX: the driver caps both directions at 255
// packets per ioctl.
const BatchMax = 255
const addressSize = uint32(unsafe.Sizeof(Address{}))
// RecvBatch receives up to BatchMax packets in one ioctl. The driver packs
// packets back-to-back into buf with no padding and copies exactly each
// packet's IP total length, so boundaries are recovered by walking the IP
// length fields. It returns as soon as at least one packet is available;
// it never waits to fill the batch. The returned Address slice is owned by
// the Handle and is overwritten by the next RecvBatch.
func (h *Handle) RecvBatch(buf []byte) (int, []Address, error) {
if len(buf) < MTUMax {
return 0, nil, E.New("windivert: recv batch: buffer smaller than MTUMax")
}
if h.recvAddrs == nil {
h.recvAddrs = make([]Address, BatchMax)
}
h.recvAddrsLen = uint32(len(h.recvAddrs)) * addressSize
in := buildIoctlRecvBatch(&h.recvAddrs[0], &h.recvAddrsLen)
n, err := doIoctl(h.device, ioctlRecv, in[:], buf, h.event)
runtime.KeepAlive(h)
if err != nil {
return 0, nil, err
}
return int(n), h.recvAddrs[:h.recvAddrsLen/addressSize], nil
}
// SendBatch injects the packets packed back-to-back in buf, one Address per
// packet. The driver recovers packet boundaries from the IP total-length
// fields and rejects the whole batch if they do not add up to len(buf).
func (h *Handle) SendBatch(buf []byte, addrs []Address) (int, error) {
if len(addrs) == 0 || len(addrs) > BatchMax {
return 0, E.New("windivert: send batch: invalid packet count ", len(addrs))
}
if len(buf) == 0 {
return 0, E.New("windivert: send batch: empty buffer")
}
if h.sendAddrs == nil {
h.sendAddrs = make([]Address, BatchMax)
}
copy(h.sendAddrs, addrs)
in := buildIoctlSend(&h.sendAddrs[0], uint32(len(addrs))*addressSize)
n, err := doIoctl(h.device, ioctlSend, in[:], buf, h.event)
runtime.KeepAlive(h)
if err != nil {
return 0, err
}
return int(n), nil
}
// The address's Outbound flag controls whether the packet is sent toward
// the wire (outbound=true) or delivered up the stack (outbound=false).
// IfIdx and SubIfIdx can stay zero — the driver uses the routing table
// when IfIdx=0.
func (h *Handle) Send(packet []byte, addr *Address) (int, error) {
if len(packet) == 0 {
return 0, E.New("windivert: send: empty packet")
@@ -181,7 +234,7 @@ func (h *Handle) Send(packet []byte, addr *Address) (int, error) {
return 0, E.New("windivert: send: nil address")
}
h.addr = *addr
in := buildIoctlSend(&h.addr)
in := buildIoctlSend(&h.addr, addressSize)
n, err := doIoctl(h.device, ioctlSend, in[:], packet, h.event)
runtime.KeepAlive(h)
if err != nil {
@@ -316,9 +369,20 @@ func buildIoctlRecv(addr *Address) [ioctlSize]byte {
return buf
}
func buildIoctlSend(addr *Address) [ioctlSize]byte {
// buildIoctlRecvBatch additionally passes addr_len_ptr, a pointer to the
// Address array capacity in bytes; the driver overwrites it with the bytes
// actually written (packet count × 80). Caller must keep both pointees
// alive via runtime.KeepAlive.
func buildIoctlRecvBatch(addrs *Address, addrsLen *uint32) [ioctlSize]byte {
var buf [ioctlSize]byte
binary.LittleEndian.PutUint64(buf[0:8], uint64(uintptr(unsafe.Pointer(addr))))
binary.LittleEndian.PutUint64(buf[8:16], uint64(unsafe.Sizeof(Address{})))
binary.LittleEndian.PutUint64(buf[0:8], uint64(uintptr(unsafe.Pointer(addrs))))
binary.LittleEndian.PutUint64(buf[8:16], uint64(uintptr(unsafe.Pointer(addrsLen))))
return buf
}
func buildIoctlSend(addrs *Address, addrsLen uint32) [ioctlSize]byte {
var buf [ioctlSize]byte
binary.LittleEndian.PutUint64(buf[0:8], uint64(uintptr(unsafe.Pointer(addrs))))
binary.LittleEndian.PutUint64(buf[8:16], uint64(addrsLen))
return buf
}
+1 -1
View File
@@ -72,7 +72,7 @@ func TestBuildIoctlRecvEmbedsAddressPointer(t *testing.T) {
func TestBuildIoctlSendEmbedsAddressPointerAndSize(t *testing.T) {
t.Parallel()
addr := &Address{}
buf := buildIoctlSend(addr)
buf := buildIoctlSend(addr, addressSize)
require.Equal(t, uint64(uintptr(unsafe.Pointer(addr))),
binary.LittleEndian.Uint64(buf[0:8]))
require.Equal(t, uint64(unsafe.Sizeof(Address{})),
+18
View File
@@ -55,9 +55,11 @@ var _ [80]byte = [unsafe.Sizeof(Address{})]byte{}
// Bit positions inside the Address's packed flags word.
const (
addrBitOutbound = 17
addrBitIPv6 = 20
addrBitIPChecksum = 21
addrBitTCPChecksum = 22
addrBitUDPChecksum = 23
)
func getFlagBit(bits uint32, pos uint) bool { return bits&(1<<pos) != 0 }
@@ -69,6 +71,18 @@ func setFlagBit(bits uint32, pos uint, v bool) uint32 {
}
func (a *Address) IPv6() bool { return getFlagBit(a.bits, addrBitIPv6) }
// SetIPv6 declares the address family of a packet built for injection. The
// driver reads it to select the IPv6 network layer; a received address
// already carries it, but a from-scratch injection address must set it.
func (a *Address) SetIPv6(v bool) {
a.bits = setFlagBit(a.bits, addrBitIPv6, v)
}
func (a *Address) SetOutbound(v bool) {
a.bits = setFlagBit(a.bits, addrBitOutbound, v)
}
func (a *Address) SetIPChecksum(v bool) {
a.bits = setFlagBit(a.bits, addrBitIPChecksum, v)
}
@@ -76,3 +90,7 @@ func (a *Address) SetIPChecksum(v bool) {
func (a *Address) SetTCPChecksum(v bool) {
a.bits = setFlagBit(a.bits, addrBitTCPChecksum, v)
}
func (a *Address) SetUDPChecksum(v bool) {
a.bits = setFlagBit(a.bits, addrBitUDPChecksum, v)
}
+6
View File
@@ -5,12 +5,14 @@ const (
TypeRedirect = "redirect"
TypeTProxy = "tproxy"
TypeDirect = "direct"
TypeBridge = "bridge"
TypeBlock = "block"
TypeDNS = "dns"
TypeSOCKS = "socks"
TypeHTTP = "http"
TypeMixed = "mixed"
TypeShadowsocks = "shadowsocks"
TypeSnell = "snell"
TypeVMess = "vmess"
TypeTrojan = "trojan"
TypeNaive = "naive"
@@ -79,6 +81,8 @@ func ProxyDisplayName(proxyType string) string {
return "TProxy"
case TypeDirect:
return "Direct"
case TypeBridge:
return "Bridge"
case TypeBlock:
return "Block"
case TypeDNS:
@@ -91,6 +95,8 @@ func ProxyDisplayName(proxyType string) string {
return "Mixed"
case TypeShadowsocks:
return "Shadowsocks"
case TypeSnell:
return "Snell"
case TypeVMess:
return "VMess"
case TypeTrojan:
+11 -4
View File
@@ -4,6 +4,7 @@ import (
"context"
"os"
"runtime"
runtimeDebug "runtime/debug"
"sync"
"time"
@@ -190,6 +191,7 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov
s.updateStatus(ServiceStatus_STOPPING)
s.serviceAccess.Unlock()
_ = oldInstance.Close()
runtimeDebug.FreeOSMemory()
s.serviceAccess.Lock()
}
s.updateStatus(ServiceStatus_STARTING)
@@ -216,7 +218,7 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov
s.startedAt = time.Now()
s.updateStatus(ServiceStatus_STARTED)
s.serviceAccess.Unlock()
runtime.GC()
runtimeDebug.FreeOSMemory()
return nil
}
@@ -247,7 +249,7 @@ func (s *StartedService) CloseService() error {
s.startedAt = time.Time{}
s.updateStatus(ServiceStatus_IDLE)
s.serviceAccess.Unlock()
runtime.GC()
runtimeDebug.FreeOSMemory()
return nil
}
@@ -991,8 +993,13 @@ func (s *StartedService) CloseAllConnections(ctx context.Context, empty *emptypb
s.serviceAccess.RLock()
nowService := s.instance
s.serviceAccess.RUnlock()
if nowService != nil && nowService.connectionManager != nil {
nowService.connectionManager.CloseAll()
if nowService != nil {
if nowService.connectionManager != nil {
nowService.connectionManager.CloseAll()
}
if nowService.trafficManager != nil {
nowService.trafficManager.CloseAllConnections()
}
}
return &emptypb.Empty{}, nil
}
+4 -3
View File
@@ -15,7 +15,6 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
R "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
@@ -512,7 +511,7 @@ func (r *Router) exchangeWithRules(ctx context.Context, rules []adapter.DNSRule,
case C.RuleActionRejectMethodDrop:
return exchangeWithRulesResult{
rejectAction: action,
err: tun.ErrDrop,
err: R.ErrDrop,
}
}
case *R.RuleActionPredefined:
@@ -703,7 +702,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
return nil, R.ErrDrop
}
case *R.RuleActionPredefined:
err = nil
@@ -779,6 +778,8 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
r.logger.DebugContext(ctx, "response rejected for ", domain)
} else if R.IsRejected(err) {
r.logger.DebugContext(ctx, "lookup rejected for ", domain)
} else if errors.Is(err, ErrNotCached) {
r.logger.DebugContext(ctx, "cache-only lookup missed for ", domain)
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
}
+20 -5
View File
@@ -50,10 +50,12 @@ const (
mdnsResponderFlagMoreComing = 0x1
mdnsResponderFlagAdd = 0x2
mdnsResponderFlagReturnIntermediates = 0x1000
mdnsResponderFlagTimeout = 0x10000
mdnsResponderErrNoError = 0
mdnsResponderErrNoSuchName = -65538
mdnsResponderErrNoSuchRecord = -65554
mdnsResponderErrTimeout = -65568
)
func darwinLookupSystemDNS(ctx context.Context, name string, qtype, qclass uint16) (*mDNS.Msg, error) {
@@ -84,25 +86,36 @@ func darwinLookupSystemDNS(ctx context.Context, name string, qtype, qclass uint1
return nil, darwinResolverError(name, statusCode)
}
return readQueryResponse(ctx, conn, name, qtype, qclass)
}
func readQueryResponse(ctx context.Context, conn net.Conn, name string, qtype, qclass uint16) (*mDNS.Msg, error) {
var answers []mDNS.RR
var hasFinalAnswer bool
for {
reply, replyErr := readReply(conn)
if replyErr != nil {
return nil, contextError(ctx, E.Cause(replyErr, "read mDNSResponder reply"))
}
if reply.errorCode != mdnsResponderErrNoError {
if len(answers) > 0 {
break
if len(answers) == 0 {
return nil, darwinResolverError(name, reply.errorCode)
}
return nil, darwinResolverError(name, reply.errorCode)
break
}
if reply.flags&mdnsResponderFlagAdd != 0 && len(reply.rdata) > 0 {
record, buildErr := buildResourceRecord(reply)
if buildErr == nil {
answers = append(answers, record)
if record.Header().Rrtype == qtype {
hasFinalAnswer = true
}
}
}
if reply.flags&mdnsResponderFlagMoreComing == 0 {
if reply.flags&mdnsResponderFlagMoreComing != 0 {
continue
}
if hasFinalAnswer && reply.rrtype == qtype {
break
}
}
@@ -115,7 +128,7 @@ func darwinLookupSystemDNS(ctx context.Context, name string, qtype, qclass uint1
func buildQueryRequest(name string, qtype, qclass uint16) []byte {
payload := make([]byte, 0, 8+len(name)+1+4)
payload = binary.BigEndian.AppendUint32(payload, mdnsResponderFlagReturnIntermediates)
payload = binary.BigEndian.AppendUint32(payload, mdnsResponderFlagReturnIntermediates|mdnsResponderFlagTimeout)
payload = binary.BigEndian.AppendUint32(payload, 0) // interfaceIndex
payload = append(payload, name...)
payload = append(payload, 0) // C string terminator
@@ -204,6 +217,8 @@ func darwinResolverError(name string, code int32) error {
return dns.RcodeSuccess
case mdnsResponderErrNoSuchName:
return dns.RcodeNameError
case mdnsResponderErrTimeout:
return E.New("mDNSResponder query timeout for ", name)
default:
return E.New("mDNSResponder query failed for ", name, ": error ", code)
}
+30
View File
@@ -10,6 +10,36 @@ tracks only the fork. Versions are tagged `vX.Y.Z-lx.N`; releases are built by
`lx-release.yml`. Tags carrying an `-rc.N` / `-alpha.N` / `-beta.N` suffix publish
as GitHub **pre-releases** and never become "Latest".
#### v1.14.0-lx.3-rc.2
**Pre-release** — merges upstream `testing` (14 commits, incl. L3-forwarding,
snell, bridge outbound) and re-grafts AmneziaWG onto the wireguard-go bump that
came with it. Carries the `rc.1` DNS-multiplex payload forward unchanged. Ships a
new `libbox.aar`.
* **AmneziaWG re-grafted onto wireguard-go v0.0.5.** Upstream bumped
`sagernet/wireguard-go` v0.0.3 → v0.0.5 for L3-forwarding (batched
`InputPackets`, a size-based outbound buffer pool, Darwin batch UDP I/O). The
AWG 2.0 obfuscation graft was rebased onto that base (submodule
`e5feca7` → `1adc4c7`): 15 of 16 grafted files applied clean, only `send.go`
conflicted on a single backpressure line. The `MessageEncapsulatingTransportSize
= 0` invariant is preserved, so upstream's rewritten `InputPacket`/`InputPackets`
and buffer pool compose with the obfuscation hooks without a manual re-weave.
No config or behaviour change for AWG endpoints.
* **SPEC 020 idle-suspend re-homed onto the new L3-forwarding endpoint API.**
Upstream replaced the direct-route endpoint interface
(`PrepareConnection`/`NewDirectRouteConnection`) with a flow API
(`PreMatchFlow`/`PortAddresses`/`PortMTU`/`AttachReturn`/`DetachReturn`/`JudgeFlow`).
The idle-suspend wake guard (`resumeOnDial`) moved to `WritePackets` — the single
point every L3-forwarded packet (including established flows that bypass
`DialContext`) transits — so a suspended WG/AWG endpoint still wakes lazily on
first traffic. `Down`/`Up`/`BindUpdate` are unchanged on v0.0.5; the mechanism is
otherwise untouched.
* **Docs (SPEC 003, SPEC 020) rewritten to current-state methodology.** Both
SPEC.md files now describe the current architecture top-down; the chronology
(graft-base evolution, the idle-tick bug, the rejected GRO experiment, the
v0.0.3→v0.0.5 delta) moved to per-spec `HISTORY.md`.
#### v1.14.0-lx.3-rc.1
**Pre-release** — DNS-query stream (SPEC 018) re-architected onto the command
+45 -1
View File
@@ -2,10 +2,54 @@
icon: material/alert-decagram
---
#### 1.14.0-alpha.37
#### 1.14.0-alpha.40
* Add bridge outbound **1**
* Fixes and improvements
**1**:
The new `bridge` outbound is the L3 counterpart of `direct`: it forwards L3
traffic (TCP, UDP and ICMP) from a TUN or other L3 endpoints directly out of a
network interface, without going through L3 to L4 translation. It requires
privileges and is supported on Linux, macOS, rooted Android, and jailbroken iOS.
See [Bridge](/configuration/outbound/bridge/).
#### 1.14.0-alpha.39
* Add L3 forwarding support **1**
* Fixes and improvements
**1**:
Building on the ICMP proxy support introduced in sing-box 1.13.0, TCP and UDP
traffic from L3 inbounds (TUN, WireGuard, and Tailscale) can now be forwarded
directly to WireGuard and Tailscale endpoints at L3, without going through
L3 to L4 translation.
See [Pre-match](/configuration/shared/pre-match/).
#### 1.14.0-alpha.38
* Add Snell protocol support **1**
* Fixes and improvements
**1**:
Surge believes that being closed-source and not proliferated can keep
[Snell](https://kb.nssurge.com/surge-knowledge-base/release-notes/snell)
covert, but this is already impossible in 2026; considering that Snell still
has advantages that other random-traffic protocols do not possess, such as
multiplexing support with complete TCP semantics and traffic-characteristic
diversity, we [implemented it in Go](https://github.com/SagerNet/sing-snell)
instead of reinventing the wheel, with all features except the v5 QUIC proxy,
behavior as consistent with the official implementation as possible, and
performance at least on par with it.
See [Snell Inbound](/configuration/inbound/snell/) and
[Snell Outbound](/configuration/outbound/snell/).
#### 1.13.14
* Fixes and improvements
+1
View File
@@ -31,6 +31,7 @@
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
| `vless` | [VLESS](./vless/) | TCP |
| `anytls` | [AnyTLS](./anytls/) | TCP |
| `snell` | [Snell](./snell/) | TCP |
| `tun` | [Tun](./tun/) | :material-close: |
| `redirect` | [Redirect](./redirect/) | :material-close: |
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
+1
View File
@@ -31,6 +31,7 @@
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
| `vless` | [VLESS](./vless/) | TCP |
| `anytls` | [AnyTLS](./anytls/) | TCP |
| `snell` | [Snell](./snell/) | TCP |
| `tun` | [Tun](./tun/) | :material-close: |
| `redirect` | [Redirect](./redirect/) | :material-close: |
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
+96
View File
@@ -0,0 +1,96 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
### Structure
```json
{
"type": "snell",
"tag": "snell-in",
... // Listen Fields
"version": 5,
"psk": "password",
"users": [
{
"name": "sekai",
"userkey": "user-password"
}
],
"obfs_mode": ""
}
```
### Version 6 Structure
```json
{
"type": "snell",
"tag": "snell-in",
... // Listen Fields
"version": 6,
"psk": "password",
"users": [
{
"name": "sekai",
"userkey": "user-password"
}
],
"mode": ""
}
```
### Listen Fields
See [Listen Fields](/configuration/shared/listen/) for details.
### Fields
#### version
==Required==
The Snell protocol version, one of `5` `6`.
Version `5` supports HTTP obfuscation (`obfs_mode`); version `6` replaces it
with traffic shaping (`mode`) and requires a `psk` of 12 to 255 bytes.
!!! note
Since we intentionally do not support the QUIC proxy mode of Snell v5, the v5 wire protocol
is effectively identical to v4, so no separate v4 server or v5 client is provided.
#### psk
==Required==
The pre-shared key.
#### users
Snell users.
When set, the server runs in multi-user mode: each entry has a `name` (optional, used in
logs) and a `userkey` (the user's key). The top-level `psk` remains the server key.
#### obfs_mode
==Version 5 only==
HTTP obfuscation mode, one of `none` `http`.
`none` is used by default.
#### mode
==Version 6 only==
Traffic shaping mode, one of `default` `unshaped` `unsafe-raw`.
`default` is used by default.
+96
View File
@@ -0,0 +1,96 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
### 结构
```json
{
"type": "snell",
"tag": "snell-in",
... // 监听字段
"version": 5,
"psk": "password",
"users": [
{
"name": "sekai",
"userkey": "user-password"
}
],
"obfs_mode": ""
}
```
### 版本 6 结构
```json
{
"type": "snell",
"tag": "snell-in",
... // 监听字段
"version": 6,
"psk": "password",
"users": [
{
"name": "sekai",
"userkey": "user-password"
}
],
"mode": ""
}
```
### 监听字段
参阅 [监听字段](/zh/configuration/shared/listen/)。
### 字段
#### version
==必填==
Snell 协议版本,`5` `6` 之一。
版本 `5` 支持 HTTP 混淆(`obfs_mode`);版本 `6` 以流量整形(`mode`)取而代之,并要求
`psk` 长度为 12 到 255 字节。
!!! note
由于我们有意不支持 Snell v5 的 QUIC 代理模式,v5 的线路协议实际上与 v4 没有区别,
因此不提供独立的 v4 服务器和 v5 客户端。
#### psk
==必填==
预共享密钥。
#### users
Snell 用户。
设置后,服务器运行于多用户模式:每一项包含 `name`(可选,用于日志)和 `userkey`
(用户密钥)。顶层的 `psk` 仍作为服务器密钥。
#### obfs_mode
==仅版本 5==
HTTP 混淆模式,`none` `http` 之一。
默认为 `none`。
#### mode
==仅版本 6==
流量整形模式,`default` `unshaped` `unsafe-raw` 之一。
默认为 `default`。
+72
View File
@@ -0,0 +1,72 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
# Bridge
!!! quote ""
Requires privileges. Supported on Linux, macOS, rooted Android, and jailbroken iOS.
For graphical clients: on macOS, only available in the standalone version and requires the
Root Helper; on Android, requires root permission; on iOS, requires jailbreak.
`bridge` is the L3 counterpart of the `direct` outbound: it forwards L3 connections
(TCP, UDP and ICMP) directly out of a network interface. Route L3 traffic to it from a TUN
or other L3 endpoints via the `route` action in
[Pre-match](/configuration/shared/pre-match/); L4 connections will be rejected.
Traffic to local addresses of the machine (loopback, or addresses assigned to its
network interfaces) will be rejected.
### Structure
```json
{
"type": "bridge",
"tag": "bridge-out",
"interface": "",
"bridge_name": "",
"iproute2_table_index": 0,
"iproute2_rule_index": 0
}
```
### Fields
#### interface
Interface name for forwarded traffic to egress.
The default interface will be used by default.
Forwarded traffic will be dropped while the interface is unavailable.
#### bridge_name
Custom bridge TUN interface name prefix, `bridge` is used by default.
Not effective on Apple platforms.
#### iproute2_table_index
!!! quote ""
Only supported on Linux, and only takes effect when `interface` is set.
Linux iproute2 table index for pinned egress routes.
`2200` + instance index is used by default.
#### iproute2_rule_index
!!! quote ""
Only supported on Linux.
Linux iproute2 rule start index.
`100` is used by default.
+69
View File
@@ -0,0 +1,69 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
# Bridge
!!! quote ""
需要特权。支持 Linux、macOS、rooted Android 和越狱 iOS。
对于图形客户端:macOS 仅独立版本可用,且需要 Root Helper;Android 需要 root 权限;iOS 需要越狱。
`bridge` 是 `direct` 出站的 L3 版本:它将 L3 连接(TCP、UDP 和 ICMP)直接从网络接口转发出去。
通过[预匹配](/zh/configuration/shared/pre-match/)中的 `route` 动作,将 L3 流量从 TUN
或其他 L3 endpoints 路由到它;L4 连接将被拒绝。
到本机本地地址(loopback 或分配给本机网络接口的地址)的流量将被拒绝。
### 结构
```json
{
"type": "bridge",
"tag": "bridge-out",
"interface": "",
"bridge_name": "",
"iproute2_table_index": 0,
"iproute2_rule_index": 0
}
```
### 字段
#### interface
转发流量流出的网络接口名称。
默认使用默认接口。
接口不可用期间,转发流量将被丢弃。
#### bridge_name
自定义 bridge TUN 接口名前缀,默认使用 `bridge`。
在 Apple 平台上无效。
#### iproute2_table_index
!!! quote ""
仅支持 Linux,且仅在设置了 `interface` 时生效。
用于固定出口路由的 Linux iproute2 路由表索引。
默认使用 `2200` + 实例索引。
#### iproute2_rule_index
!!! quote ""
仅支持 Linux。
Linux iproute2 规则起始索引。
默认使用 `100`。
+2
View File
@@ -18,6 +18,7 @@
| Type | Format |
|----------------|--------------------------------|
| `direct` | [Direct](./direct/) |
| `bridge` | [Bridge](./bridge/) |
| `block` | [Block](./block/) |
| `socks` | [SOCKS](./socks/) |
| `http` | [HTTP](./http/) |
@@ -31,6 +32,7 @@
| `tuic` | [TUIC](./tuic/) |
| `hysteria2` | [Hysteria2](./hysteria2/) |
| `anytls` | [AnyTLS](./anytls/) |
| `snell` | [Snell](./snell/) |
| `tor` | [Tor](./tor/) |
| `ssh` | [SSH](./ssh/) |
| `dns` | [DNS](./dns/) |
+2
View File
@@ -18,6 +18,7 @@
| 类型 | 格式 |
|----------------|--------------------------------|
| `direct` | [Direct](./direct/) |
| `bridge` | [Bridge](./bridge/) |
| `block` | [Block](./block/) |
| `socks` | [SOCKS](./socks/) |
| `http` | [HTTP](./http/) |
@@ -31,6 +32,7 @@
| `tuic` | [TUIC](./tuic/) |
| `hysteria2` | [Hysteria2](./hysteria2/) |
| `anytls` | [AnyTLS](./anytls/) |
| `snell` | [Snell](./snell/) |
| `tor` | [Tor](./tor/) |
| `ssh` | [SSH](./ssh/) |
| `dns` | [DNS](./dns/) |
+125
View File
@@ -0,0 +1,125 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
### Structure
```json
{
"type": "snell",
"tag": "snell-out",
"server": "127.0.0.1",
"server_port": 1080,
"version": 4,
"psk": "password",
"userkey": "",
"reuse": false,
"network": "tcp",
"obfs_mode": "",
"obfs_host": "",
... // Dial Fields
}
```
### Version 6 Structure
```json
{
"type": "snell",
"tag": "snell-out",
"server": "127.0.0.1",
"server_port": 1080,
"version": 6,
"psk": "password",
"userkey": "",
"reuse": false,
"network": "tcp",
"mode": "",
... // Dial Fields
}
```
### Fields
#### server
==Required==
The server address.
#### server_port
==Required==
The server port.
#### version
==Required==
The Snell protocol version, one of `4` `6`.
Version `4` supports HTTP obfuscation (`obfs_mode` / `obfs_host`); version `6`
replaces it with traffic shaping (`mode`) and requires a `psk` of 12 to 255
bytes.
!!! note
Since we intentionally do not support the QUIC proxy mode of Snell v5, the v5 wire protocol
is effectively identical to v4, so no separate v4 server or v5 client is provided.
#### psk
==Required==
The pre-shared key.
#### userkey
The user key, used to authenticate against a multi-user server.
#### reuse
Enable connection reuse (the Snell v2 `CONNECT` command).
#### network
Enabled network
One of `tcp` `udp`.
Both is enabled by default.
#### obfs_mode
==Version 4 only==
HTTP obfuscation mode, one of `none` `http`.
`none` is used by default.
#### obfs_host
==Version 4 only==
The HTTP `Host` header sent when `obfs_mode` is `http`.
`bing.com` is used by default.
#### mode
==Version 6 only==
Traffic shaping mode, one of `default` `unshaped` `unsafe-raw`.
`default` is used by default.
### Dial Fields
See [Dial Fields](/configuration/shared/dial/) for details.
+124
View File
@@ -0,0 +1,124 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
### 结构
```json
{
"type": "snell",
"tag": "snell-out",
"server": "127.0.0.1",
"server_port": 1080,
"version": 4,
"psk": "password",
"userkey": "",
"reuse": false,
"network": "tcp",
"obfs_mode": "",
"obfs_host": "",
... // 拨号字段
}
```
### 版本 6 结构
```json
{
"type": "snell",
"tag": "snell-out",
"server": "127.0.0.1",
"server_port": 1080,
"version": 6,
"psk": "password",
"userkey": "",
"reuse": false,
"network": "tcp",
"mode": "",
... // 拨号字段
}
```
### 字段
#### server
==必填==
服务器地址。
#### server_port
==必填==
服务器端口。
#### version
==必填==
Snell 协议版本,`4` `6` 之一。
版本 `4` 支持 HTTP 混淆(`obfs_mode` / `obfs_host`);版本 `6` 以流量整形(`mode`)
取而代之,并要求 `psk` 长度为 12 到 255 字节。
!!! note
由于我们有意不支持 Snell v5 的 QUIC 代理模式,v5 的线路协议实际上与 v4 没有区别,
因此不提供独立的 v4 服务器和 v5 客户端。
#### psk
==必填==
预共享密钥。
#### userkey
用户密钥,用于向多用户服务器进行认证。
#### reuse
启用连接复用(Snell v2 `CONNECT` 命令)。
#### network
启用的网络协议。
`tcp` 或 `udp`。
默认所有。
#### obfs_mode
==仅版本 4==
HTTP 混淆模式,`none` `http` 之一。
默认为 `none`。
#### obfs_host
==仅版本 4==
`obfs_mode` 为 `http` 时发送的 HTTP `Host` 头。
默认为 `bing.com`。
#### mode
==仅版本 6==
流量整形模式,`default` `unshaped` `unsafe-raw` 之一。
默认为 `default`。
### 拨号字段
参阅 [拨号字段](/zh/configuration/shared/dial/)。
+5 -4
View File
@@ -465,10 +465,11 @@ See [Wi-Fi State](/configuration/shared/wifi-state/) for details.
Match specified outbounds' preferred routes.
| Type | Match |
|-------------|-----------------------------------------------|
| `tailscale` | Match MagicDNS domains and peers' allowed IPs |
| `wireguard` | Match peers's allowed IPs |
| Type | Match |
|-------------|----------------------------------------------------|
| `tailscale` | Match MagicDNS domains and peers' allowed IPs |
| `wireguard` | Match peers's allowed IPs |
| `bridge` | Match all addresses except local addresses of the machine, only in [pre-match](/configuration/shared/pre-match/) |
#### source_mac_address
+1
View File
@@ -467,6 +467,7 @@ icon: material/new-box
|-------------|--------------------------------|
| `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs |
| `wireguard` | 匹配对端的 allowed IPs |
| `bridge` | 匹配除本机本地地址外的所有地址,仅在[预匹配](/zh/configuration/shared/pre-match/)中 |
#### source_mac_address
+40 -5
View File
@@ -4,6 +4,11 @@ icon: material/new-box
# Pre-match
!!! quote "Changes in sing-box 1.14.0"
:material-alert: [route](#route)
:material-plus: [sniff](#sniff)
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [bypass](#bypass)
@@ -12,11 +17,11 @@ Pre-match is rule matching that runs before the connection is established.
### How it works
When TUN receives a connection request, the connection has not yet been established,
so no connection data can be read. In this phase, sing-box runs the routing rules in pre-match mode.
When an L3 inbound (TUN, WireGuard, or Tailscale) receives a connection request, the connection has not yet been established:
for TCP connections no connection data is available, while for UDP connections only the first packet is available.
In this phase, sing-box runs the routing rules in pre-match mode.
Since connection data is unavailable, only actions that do not require connection data can be executed.
When a rule matches an action that requires an established connection, pre-match stops at that rule.
When a rule matches an action that requires more connection data than available, pre-match stops at that rule.
### Supported actions
@@ -28,10 +33,40 @@ See [reject](/configuration/route/rule_action/#reject) for details.
#### route
Route ICMP connections to the specified outbound for direct reply.
!!! quote "Changes in sing-box 1.14.0"
Since sing-box 1.14.0, TCP and UDP connections can also be forwarded at L3;
previously only ICMP connections were supported.
Forward connections directly at L3 to the specified outbound,
without going through L3 to L4 translation.
Supported targets:
- ICMP connections: Direct and Bridge outbounds, and WireGuard / Tailscale endpoints.
- TCP and UDP connections: Bridge outbounds, and WireGuard / Tailscale endpoints.
L3 forwarding also applies when no rule matches and the default outbound is a supported
target; for outbound groups, the currently selected outbound is used.
FakeIP destinations require a `resolve` action performed in pre-match,
otherwise connections will be rejected.
See [route](/configuration/route/rule_action/#route) for details.
#### sniff
!!! question "Since sing-box 1.14.0"
For UDP connections, the first packet is available in pre-match,
so protocol sniffing runs on it directly and rule matching continues with the sniffed metadata.
When sniffers require more data (like a fragmented QUIC Client Hello), pre-match stops at that rule.
For TCP connections, pre-match always stops at that rule.
See [sniff](/configuration/route/rule_action/#sniff) for details.
#### bypass
!!! question "Since sing-box 1.13.0"
+33 -3
View File
@@ -4,6 +4,11 @@ icon: material/new-box
# 预匹配
!!! quote "sing-box 1.14.0 中的更改"
:material-alert: [route](#route)
:material-plus: [sniff](#sniff)
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [bypass](#bypass)
@@ -12,9 +17,9 @@ icon: material/new-box
### 工作原理
当 TUN 收到连接请求时,连接尚未建立,因此无法读取连接数据。在此阶段,sing-box 在预匹配模式下运行路由规则。
当 L3 入站(TUN、WireGuard 或 Tailscale)收到连接请求时,连接尚未建立:对于 TCP 连接,无连接数据可用;对于 UDP 连接,仅首个数据包可用。在此阶段,sing-box 在预匹配模式下运行路由规则。
由于连接数据不可用,只有不需要连接数据的动作才能执行。当规则匹配到需要已建立连接的动作时,预匹配将在该规则处停止。
当规则匹配到需要比当前可用数据更多连接数据的动作时,预匹配将在该规则处停止。
### 支持的动作
@@ -26,10 +31,35 @@ icon: material/new-box
#### route
将 ICMP 连接路由到指定出站以直接回复。
!!! quote "sing-box 1.14.0 中的更改"
自 sing-box 1.14.0 起,TCP 和 UDP 连接也可以在 L3 转发;此前仅支持 ICMP 连接。
将连接直接在 L3 转发到指定出站,不经过 L3 到 L4 转换。
支持的目标:
- ICMP 连接:direct 和 bridge 出站以及 WireGuard / Tailscale 端点。
- TCP 和 UDP 连接:bridge 出站以及 WireGuard / Tailscale 端点。
当没有规则匹配且默认出站为受支持的目标时,L3 转发同样生效;对于出站组,使用当前选中的出站。
FakeIP 目标需要在预匹配中先执行 `resolve` 动作,否则连接将被拒绝。
详情参阅 [route](/zh/configuration/route/rule_action/#route)。
#### sniff
!!! question "自 sing-box 1.14.0 起"
对于 UDP 连接,首个数据包在预匹配中可用,因此协议探测将直接在其上运行,随后规则匹配将携带探测结果继续。
当探测器需要更多数据时(如分片的 QUIC Client Hello),预匹配将在该规则处停止。
对于 TCP 连接,预匹配总是在该规则处停止。
详情参阅 [sniff](/zh/configuration/route/rule_action/#sniff)。
#### bypass
!!! question "自 sing-box 1.13.0 起"
@@ -0,0 +1,63 @@
//go:build darwin
package libbox
import (
"net/netip"
"github.com/sagernet/sing-box/protocol/bridge"
E "github.com/sagernet/sing/common/exceptions"
)
func NewBridgeService(options *BridgeOptions) (BridgeSession, error) {
if options == nil {
return nil, E.New("missing bridge options")
}
serviceOptions := bridge.ServiceOptions{
MTU: int(options.MTU),
Interface: options.Interface,
}
if options.Inet4Port != "" {
inet4Port, err := netip.ParseAddr(options.Inet4Port)
if err != nil {
return nil, E.Cause(err, "parse inet4 port address")
}
serviceOptions.Inet4Port = inet4Port
}
if options.Inet6Port != "" {
inet6Port, err := netip.ParseAddr(options.Inet6Port)
if err != nil {
return nil, E.Cause(err, "parse inet6 port address")
}
serviceOptions.Inet6Port = inet6Port
}
service, err := bridge.NewService(serviceOptions)
if err != nil {
return nil, err
}
return &bridgeServiceSession{service}, nil
}
type bridgeServiceSession struct {
service *bridge.Service
}
func (s *bridgeServiceSession) FileDescriptor() int32 {
return int32(s.service.FileDescriptor())
}
func (s *bridgeServiceSession) Name() string {
return s.service.Name()
}
func (s *bridgeServiceSession) Inet6Active() bool {
return s.service.Inet6Active()
}
func (s *bridgeServiceSession) SetEgress(interfaceName string) error {
return s.service.SetEgress(interfaceName)
}
func (s *bridgeServiceSession) Close() error {
return s.service.Close()
}
@@ -0,0 +1,65 @@
//go:build linux
package libbox
import (
"net/netip"
"github.com/sagernet/sing-box/protocol/bridge"
E "github.com/sagernet/sing/common/exceptions"
)
func NewBridgeService(options *BridgeOptions) (BridgeSession, error) {
if options == nil {
return nil, E.New("missing bridge options")
}
serviceOptions := bridge.ServiceOptions{
BridgeName: options.BridgeName,
MTU: int(options.MTU),
RuleIndex: int(options.RuleIndex),
RouteTable: int(options.RouteTable),
}
if options.Inet4Port != "" {
inet4Port, err := netip.ParseAddr(options.Inet4Port)
if err != nil {
return nil, E.Cause(err, "parse inet4 port address")
}
serviceOptions.Inet4Port = inet4Port
}
if options.Inet6Port != "" {
inet6Port, err := netip.ParseAddr(options.Inet6Port)
if err != nil {
return nil, E.Cause(err, "parse inet6 port address")
}
serviceOptions.Inet6Port = inet6Port
}
service, err := bridge.NewService(serviceOptions)
if err != nil {
return nil, err
}
return &bridgeServiceSession{service}, nil
}
type bridgeServiceSession struct {
service *bridge.Service
}
func (s *bridgeServiceSession) FileDescriptor() int32 {
return int32(s.service.FileDescriptor())
}
func (s *bridgeServiceSession) Name() string {
return s.service.Name()
}
func (s *bridgeServiceSession) Inet6Active() bool {
return s.service.Inet6Active()
}
func (s *bridgeServiceSession) SetEgress(interfaceName string) error {
return s.service.SetEgress(interfaceName)
}
func (s *bridgeServiceSession) Close() error {
return s.service.Close()
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !linux && !darwin
package libbox
import E "github.com/sagernet/sing/common/exceptions"
func NewBridgeService(options *BridgeOptions) (BridgeSession, error) {
return nil, E.New("bridge service not supported on this platform")
}
+8
View File
@@ -181,6 +181,14 @@ func (s *platformInterfaceStub) TailscaleHostname() string {
return ""
}
func (s *platformInterfaceStub) UsePlatformBridge() bool {
return false
}
func (s *platformInterfaceStub) CreateBridge(options adapter.BridgeOptions) (adapter.BridgeSession, error) {
return nil, os.ErrInvalid
}
func (s *platformInterfaceStub) LookupUser(username string) (*adapter.PlatformUser, error) {
return nil, os.ErrInvalid
}
+20
View File
@@ -27,6 +27,26 @@ type PlatformInterface interface {
LookupSFTPServer() (string, error)
ReadSystemSSHHostKey() (string, error)
TailscaleHostname() string
UsePlatformBridge() bool
CreateBridge(options *BridgeOptions) (BridgeSession, error)
}
type BridgeOptions struct {
BridgeName string
MTU int32
Inet4Port string
Inet6Port string
Interface string
RuleIndex int32
RouteTable int32
}
type BridgeSession interface {
FileDescriptor() int32
Name() string
Inet6Active() bool
SetEgress(interfaceName string) error
Close() error
}
type PlatformUser struct {
+49
View File
@@ -285,6 +285,55 @@ func (w *platformInterfaceWrapper) TailscaleHostname() string {
return w.iif.TailscaleHostname()
}
func (w *platformInterfaceWrapper) UsePlatformBridge() bool {
return w.iif.UsePlatformBridge()
}
func (w *platformInterfaceWrapper) CreateBridge(options adapter.BridgeOptions) (adapter.BridgeSession, error) {
bridgeOptions := &BridgeOptions{
BridgeName: options.BridgeName,
MTU: int32(options.MTU),
Interface: options.Interface,
RuleIndex: int32(options.RuleIndex),
RouteTable: int32(options.RouteTable),
}
if options.Inet4Port.IsValid() {
bridgeOptions.Inet4Port = options.Inet4Port.String()
}
if options.Inet6Port.IsValid() {
bridgeOptions.Inet6Port = options.Inet6Port.String()
}
session, err := w.iif.CreateBridge(bridgeOptions)
if err != nil {
return nil, err
}
return &bridgeSessionWrapper{session}, nil
}
type bridgeSessionWrapper struct {
session BridgeSession
}
func (w *bridgeSessionWrapper) FileDescriptor() int {
return int(w.session.FileDescriptor())
}
func (w *bridgeSessionWrapper) Name() string {
return w.session.Name()
}
func (w *bridgeSessionWrapper) Inet6Active() bool {
return w.session.Inet6Active()
}
func (w *bridgeSessionWrapper) SetEgress(interfaceName string) error {
return w.session.SetEgress(interfaceName)
}
func (w *bridgeSessionWrapper) Close() error {
return w.session.Close()
}
func (w *platformInterfaceWrapper) LookupUser(username string) (*adapter.PlatformUser, error) {
platformUser, err := w.iif.LookupUser(username)
if err != nil {
+58
View File
@@ -12,6 +12,7 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
@@ -118,6 +119,63 @@ func (s *StatsService) RoutedPacketConnection(ctx context.Context, conn N.Packet
return bufio.NewInt64CounterPacketConn(conn, readCounter, nil, writeCounter, nil)
}
func (s *StatsService) RoutedFlow(ctx context.Context, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) tun.FlowTracker {
inbound := metadata.Inbound
user := metadata.User
outbound := matchOutbound.Tag()
var uplinkCounter []*atomic.Int64
var downlinkCounter []*atomic.Int64
countInbound := inbound != "" && s.inbounds[inbound]
countOutbound := outbound != "" && s.outbounds[outbound]
countUser := user != "" && s.users[user]
if !countInbound && !countOutbound && !countUser {
return nil
}
s.access.Lock()
if countInbound {
uplinkCounter = append(uplinkCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>uplink"))
downlinkCounter = append(downlinkCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>downlink"))
}
if countOutbound {
uplinkCounter = append(uplinkCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>uplink"))
downlinkCounter = append(downlinkCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>downlink"))
}
if countUser {
uplinkCounter = append(uplinkCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>uplink"))
downlinkCounter = append(downlinkCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>downlink"))
}
s.access.Unlock()
return &statsFlowTracker{uplinkCounter: uplinkCounter, downlinkCounter: downlinkCounter}
}
var _ tun.FlowTracker = (*statsFlowTracker)(nil)
type statsFlowTracker struct {
uplinkCounter []*atomic.Int64
downlinkCounter []*atomic.Int64
}
func (t *statsFlowTracker) AttachFlow(handle tun.FlowHandle) {
}
func (t *statsFlowTracker) CountForward(n int) {
for _, counter := range t.uplinkCounter {
counter.Add(int64(n))
}
}
func (t *statsFlowTracker) CountReverse(n int) {
for _, counter := range t.downlinkCounter {
counter.Add(int64(n))
}
}
func (t *statsFlowTracker) FlowEstablished() {
}
func (t *statsFlowTracker) CloseFlow(reason tun.FlowCloseReason) {
}
func (s *StatsService) GetStats(ctx context.Context, request *GetStatsRequest) (*GetStatsResponse, error) {
s.access.Lock()
counter, loaded := s.counters[request.Name]
+7 -6
View File
@@ -40,19 +40,22 @@ require (
github.com/sagernet/gliderssh v0.3.4-0.20260531100337-2194faca5648
github.com/sagernet/gomobile v0.1.12
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-mod.3
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
github.com/sagernet/sing v0.8.12-0.20260701111927-87e1e819f10a
github.com/sagernet/sing-cloudflared v0.1.1
github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3
github.com/sagernet/sing-mux v0.3.5
github.com/sagernet/sing-quic v0.6.2-0.20260525051024-9467ede27fb7
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1
github.com/sagernet/sing-tun v0.8.12-0.20260629021427-b3c6babbd353
github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814
github.com/sagernet/sing-tun v0.8.12-0.20260708091449-be1a05a4c962
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
github.com/sagernet/smux v1.5.50-sing-box-mod.1
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260527101438-dc40932c32d9
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260706062137-ae2dde1295a3
github.com/sagernet/wireguard-go v0.0.5-0.20260706153856-2c27bbf4f97f
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/spf13/cobra v1.10.2
@@ -147,8 +150,6 @@ require (
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260620135226-def9ff0fb992 // indirect
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260620135226-def9ff0fb992 // indirect
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260620135226-def9ff0fb992 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-mod.2 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
+12 -10
View File
@@ -254,14 +254,14 @@ github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 h1:AzCE2RhBjLJ4WIWc/
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-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje+vW5Q0OQ=
github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ=
github.com/sagernet/nftables v0.3.0-mod.3 h1:CVfbVTd3Z/LQVc1Z3c1hpiriplJ4xDVHjfQCETiN9RA=
github.com/sagernet/nftables v0.3.0-mod.3/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
github.com/sagernet/sing v0.8.12-0.20260701111927-87e1e819f10a h1:MCid6UN8a7WZWziqlWbLRFOt2jsfNZ7HRYEbP+MxX0U=
github.com/sagernet/sing v0.8.12-0.20260701111927-87e1e819f10a/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
github.com/sagernet/sing-cloudflared v0.1.1 h1:By29ZWMJl8QU6UcC5pmBv803rYigAoSmzhDFOZc3h18=
github.com/sagernet/sing-cloudflared v0.1.1/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y=
github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d h1:BhsQU0Iug1tU4xR52cjm8Sc+LBo+KwdyLTRn3ie9moo=
github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8=
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg=
github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI=
github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-quic v0.6.2-0.20260525051024-9467ede27fb7 h1:hFLPJ21uNZSbRnzhOKz4Zv0b4F93mpDorWyN93BeRcM=
@@ -272,16 +272,18 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.1 h1:ZiHZdnEnP+YS73NMsxiZmIFCwNd0M4k7PkGCKNXhbaM=
github.com/sagernet/sing-shadowtls v0.2.1/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.8.12-0.20260629021427-b3c6babbd353 h1:HA0TGrBQSFfvcoVXL1DxzF+i8pmaGOGb33jdReB7L4s=
github.com/sagernet/sing-tun v0.8.12-0.20260629021427-b3c6babbd353/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ=
github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814 h1:xfnkRpjVRVeJhVvDZA8PzTLlKGTb1o2kdI4uv1YymXo=
github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814/go.mod h1:PcwzX/Xvqky0EP3kGt8OCjYb3R1pydenPHNQZcPZmXY=
github.com/sagernet/sing-tun v0.8.12-0.20260708091449-be1a05a4c962 h1:dmJoWdTQygt4P2rAwScy2IvHnFp1mKrW6OsI0qig6O8=
github.com/sagernet/sing-tun v0.8.12-0.20260708091449-be1a05a4c962/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ=
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb h1:KEMbfexD4DvrQGYWwx6r+AwH9Veh8z6cnBZmtCS2G+0=
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb/go.mod h1:D4CnJX3MNAAANhbQUxfIRgBdnvlTEaV7h6ojedcs+pw=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260527101438-dc40932c32d9 h1:jOkKeYI0A0M+jVEu2omQLId4q5GVP7G8FSZh1eUArIk=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260527101438-dc40932c32d9/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260706062137-ae2dde1295a3 h1:eczvica8YiS5j3GfpHg6JG1Icur4Z2D6ffSrLZTfD1E=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260706062137-ae2dde1295a3/go.mod h1:p8Ms8FbGlwQJyHb862XmdShTS50fFJ8C71VdO6xvWyk=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
+5
View File
@@ -21,6 +21,7 @@ import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/anytls"
"github.com/sagernet/sing-box/protocol/block"
"github.com/sagernet/sing-box/protocol/bridge"
"github.com/sagernet/sing-box/protocol/direct"
"github.com/sagernet/sing-box/protocol/group"
"github.com/sagernet/sing-box/protocol/http"
@@ -29,6 +30,7 @@ import (
"github.com/sagernet/sing-box/protocol/redirect"
"github.com/sagernet/sing-box/protocol/shadowsocks"
"github.com/sagernet/sing-box/protocol/shadowtls"
"github.com/sagernet/sing-box/protocol/snell"
"github.com/sagernet/sing-box/protocol/socks"
"github.com/sagernet/sing-box/protocol/ssh"
"github.com/sagernet/sing-box/protocol/tor"
@@ -60,6 +62,7 @@ func InboundRegistry() *inbound.Registry {
mixed.RegisterInbound(registry)
shadowsocks.RegisterInbound(registry)
snell.RegisterInbound(registry)
vmess.RegisterInbound(registry)
trojan.RegisterInbound(registry)
naive.RegisterInbound(registry)
@@ -78,6 +81,7 @@ func OutboundRegistry() *outbound.Registry {
registry := outbound.NewRegistry()
direct.RegisterOutbound(registry)
bridge.RegisterOutbound(registry)
block.RegisterOutbound(registry)
@@ -87,6 +91,7 @@ func OutboundRegistry() *outbound.Registry {
socks.RegisterOutbound(registry)
http.RegisterOutbound(registry)
shadowsocks.RegisterOutbound(registry)
snell.RegisterOutbound(registry)
vmess.RegisterOutbound(registry)
trojan.RegisterOutbound(registry)
registerNaiveOutbound(registry)
+3
View File
@@ -159,6 +159,7 @@ nav:
- TUIC: configuration/inbound/tuic.md
- Hysteria2: configuration/inbound/hysteria2.md
- AnyTLS: configuration/inbound/anytls.md
- Snell: configuration/inbound/snell.md
- Tun: configuration/inbound/tun.md
- Redirect: configuration/inbound/redirect.md
- TProxy: configuration/inbound/tproxy.md
@@ -166,6 +167,7 @@ nav:
- Outbound:
- configuration/outbound/index.md
- Direct: configuration/outbound/direct.md
- Bridge: configuration/outbound/bridge.md
- Block: configuration/outbound/block.md
- SOCKS: configuration/outbound/socks.md
- HTTP: configuration/outbound/http.md
@@ -180,6 +182,7 @@ nav:
- TUIC: configuration/outbound/tuic.md
- Hysteria2: configuration/outbound/hysteria2.md
- AnyTLS: configuration/outbound/anytls.md
- Snell: configuration/outbound/snell.md
- Tor: configuration/outbound/tor.md
- SSH: configuration/outbound/ssh.md
- DNS: configuration/outbound/dns.md
+8
View File
@@ -0,0 +1,8 @@
package option
type BridgeOutboundOptions struct {
Interface string `json:"interface,omitempty"`
BridgeName string `json:"bridge_name,omitempty"`
IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"`
IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"`
}
+118
View File
@@ -0,0 +1,118 @@
package option
import (
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
)
type _SnellInboundOptions struct {
ListenOptions
Version int `json:"version"`
PSK string `json:"psk"`
Users []SnellUser `json:"users,omitempty"`
ObfsOptions SnellObfsServerOptions `json:"-"`
V6Options SnellV6Options `json:"-"`
}
type SnellInboundOptions _SnellInboundOptions
func (o *SnellInboundOptions) UnmarshalJSON(content []byte) error {
err := json.Unmarshal(content, (*_SnellInboundOptions)(o))
if err != nil {
return err
}
var versionOptions any
switch o.Version {
case 5:
versionOptions = &o.ObfsOptions
case 6:
versionOptions = &o.V6Options
case 0:
return E.New("snell: missing version")
default:
return E.New("snell: unsupported version: ", o.Version)
}
return badjson.UnmarshallExcluded(content, (*_SnellInboundOptions)(o), versionOptions)
}
func (o SnellInboundOptions) MarshalJSON() ([]byte, error) {
var versionOptions any
switch o.Version {
case 5:
versionOptions = o.ObfsOptions
case 6:
versionOptions = o.V6Options
case 0:
return nil, E.New("snell: missing version")
default:
return nil, E.New("snell: unsupported version: ", o.Version)
}
return badjson.MarshallObjects((_SnellInboundOptions)(o), versionOptions)
}
type _SnellOutboundOptions struct {
DialerOptions
ServerOptions
Version int `json:"version"`
PSK string `json:"psk"`
UserKey string `json:"userkey,omitempty"`
Reuse bool `json:"reuse,omitempty"`
Network NetworkList `json:"network,omitempty"`
ObfsOptions SnellObfsClientOptions `json:"-"`
V6Options SnellV6Options `json:"-"`
}
type SnellOutboundOptions _SnellOutboundOptions
func (o *SnellOutboundOptions) UnmarshalJSON(content []byte) error {
err := json.Unmarshal(content, (*_SnellOutboundOptions)(o))
if err != nil {
return err
}
var versionOptions any
switch o.Version {
case 4:
versionOptions = &o.ObfsOptions
case 6:
versionOptions = &o.V6Options
case 0:
return E.New("snell: missing version")
default:
return E.New("snell: unsupported version: ", o.Version)
}
return badjson.UnmarshallExcluded(content, (*_SnellOutboundOptions)(o), versionOptions)
}
func (o SnellOutboundOptions) MarshalJSON() ([]byte, error) {
var versionOptions any
switch o.Version {
case 4:
versionOptions = o.ObfsOptions
case 6:
versionOptions = o.V6Options
case 0:
return nil, E.New("snell: missing version")
default:
return nil, E.New("snell: unsupported version: ", o.Version)
}
return badjson.MarshallObjects((_SnellOutboundOptions)(o), versionOptions)
}
type SnellObfsServerOptions struct {
ObfsMode string `json:"obfs_mode,omitempty"`
}
type SnellUser struct {
Name string `json:"name,omitempty"`
UserKey string `json:"userkey"`
}
type SnellObfsClientOptions struct {
ObfsMode string `json:"obfs_mode,omitempty"`
ObfsHost string `json:"obfs_host,omitempty"`
}
type SnellV6Options struct {
Mode string `json:"mode,omitempty"`
}
+205
View File
@@ -0,0 +1,205 @@
//go:build linux || darwin || (windows && (amd64 || 386))
//nolint:unused
package bridge
import (
"context"
"net/netip"
"slices"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
type sysctlState struct {
name string
value string
}
type backendBase struct {
ctx context.Context
logger logger.ContextLogger
networkManager adapter.NetworkManager
tag string
index uint32
bridgeName string
tunName string
inet4Port netip.Addr
inet6Port netip.Addr
boundInterface string
tunInterface tun.Tun
returnAccess sync.Mutex
returnPaths []tun.Return
egressAccess sync.Mutex
forwardingRestore []sysctlState
unregister func()
session adapter.BridgeSession
currentEgress string
closeOnce sync.Once
closed chan struct{}
readDone chan struct{}
}
func (b *backendBase) init(ctx context.Context, logger logger.ContextLogger, networkManager adapter.NetworkManager, tag string, options option.BridgeOutboundOptions) error {
index, err := allocateBridgeIndex()
if err != nil {
return err
}
b.ctx = ctx
b.logger = logger
b.networkManager = networkManager
b.tag = tag
b.index = index
b.bridgeName = options.BridgeName
if b.bridgeName == "" {
b.bridgeName = "bridge"
}
b.boundInterface = options.Interface
b.inet4Port = addressAt(bridgeInet4Base, index)
b.inet6Port = addressAt(bridgeInet6Base, index)
return nil
}
func (b *backendBase) PortAddresses() (netip.Addr, netip.Addr) {
return b.inet4Port, b.inet6Port
}
func (b *backendBase) AttachReturn(returnPath tun.Return) error {
b.returnAccess.Lock()
defer b.returnAccess.Unlock()
if slices.Contains(b.returnPaths, returnPath) {
return nil
}
b.returnPaths = append(b.returnPaths[:len(b.returnPaths):len(b.returnPaths)], returnPath)
return nil
}
func (b *backendBase) DetachReturn(returnPath tun.Return) error {
b.returnAccess.Lock()
defer b.returnAccess.Unlock()
returnPaths := make([]tun.Return, 0, len(b.returnPaths))
for _, existing := range b.returnPaths {
if existing != returnPath {
returnPaths = append(returnPaths, existing)
}
}
b.returnPaths = returnPaths
return nil
}
func (b *backendBase) registerMonitors(syncFunc func()) {
var unregisterFuncs []func()
networkMonitor := b.networkManager.NetworkMonitor()
if networkMonitor != nil {
networkElement := networkMonitor.RegisterCallback(syncFunc)
unregisterFuncs = append(unregisterFuncs, func() { networkMonitor.UnregisterCallback(networkElement) })
} else if b.boundInterface != "" {
b.logger.Debug("network monitor unavailable, pinned egress will not track interface changes")
}
if b.boundInterface == "" {
interfaceMonitor := b.networkManager.InterfaceMonitor()
if interfaceMonitor != nil {
interfaceElement := interfaceMonitor.RegisterCallback(func(_ *control.Interface, _ int) { syncFunc() })
unregisterFuncs = append(unregisterFuncs, func() { interfaceMonitor.UnregisterCallback(interfaceElement) })
}
}
if len(unregisterFuncs) > 0 {
b.unregister = func() {
for _, unregisterFunc := range unregisterFuncs {
unregisterFunc()
}
}
}
}
func (b *backendBase) syncSessionEgress() {
b.egressAccess.Lock()
defer b.egressAccess.Unlock()
select {
case <-b.closed:
return
default:
}
egress := b.resolveEgress()
if egress == b.currentEgress {
return
}
err := b.session.SetEgress(egress)
if err != nil {
b.logger.Debug(E.Cause(err, "apply bridge egress ", egress))
return
}
b.currentEgress = egress
if egress == "" {
b.logger.Debug("bridge egress unavailable, dropping forwarded traffic")
} else {
b.logger.Debug("bridge egress ", egress)
}
}
func (b *backendBase) resolveEgress() string {
if b.boundInterface != "" {
return b.boundInterface
}
monitor := b.networkManager.InterfaceMonitor()
if monitor == nil {
return ""
}
defaultInterface := monitor.DefaultInterface()
if defaultInterface == nil {
return ""
}
return defaultInterface.Name
}
func (b *backendBase) readLoop() {
defer close(b.readDone)
buffer := make([]byte, tun.PacketOffset+bridgeTunMTU)
for {
n, err := b.tunInterface.Read(buffer)
if err != nil {
select {
case <-b.closed:
default:
b.logger.Debug(E.Cause(err, "bridge tun read"))
}
return
}
if n <= tun.PacketOffset {
continue
}
packet := buffer[tun.PacketOffset:n]
// On checksum-offloading NICs (notably virtio) the kernel leaves the L4
// checksum uncomputed when the forwarding path TXes to a tun; recompute it.
fixReturnChecksum(packet)
b.deliverReturn(packet)
}
}
func (b *backendBase) deliverReturn(packet []byte) {
b.returnAccess.Lock()
returnPaths := b.returnPaths
b.returnAccess.Unlock()
for _, returnPath := range returnPaths {
headroom := returnPath.ReturnHeadroom()
buffer := make([]byte, headroom+len(packet))
copy(buffer[headroom:], packet)
unconsumed := returnPath.ReturnPackets([][]byte{buffer})
if len(unconsumed) == 0 {
return
}
}
}
+363
View File
@@ -0,0 +1,363 @@
package bridge
import (
"context"
"errors"
"net/netip"
"slices"
"sync"
"syscall"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
)
var (
bridgeInet4LocalBase = netip.MustParseAddr("198.51.100.1")
bridgeInet6LocalBase = netip.MustParseAddr("2001:db8:1::1")
)
type backendDarwin struct {
backendBase
// anchorName lives under com.apple/* so the stock pf.conf's wildcard
// nat/scrub/anchor references evaluate our rules without editing it.
anchorName string
inet4Local netip.Addr
inet6Local netip.Addr
batchTUN tun.DarwinTUN
writeAccess sync.Mutex
writeBatch []*buf.Buffer
pfDevice *pfDevice
pfToken uint64
currentRules []pfAnchorRule
platform adapter.PlatformInterface
}
func newBackend(ctx context.Context, logger logger.ContextLogger, networkManager adapter.NetworkManager, tag string, options option.BridgeOutboundOptions) (Backend, error) {
instance := &backendDarwin{
writeBatch: make([]*buf.Buffer, 0, bridgeWriteBatchSize),
}
err := instance.init(ctx, logger, networkManager, tag, options)
if err != nil {
return nil, err
}
instance.inet4Local = addressAt(bridgeInet4LocalBase, instance.index)
instance.inet6Local = addressAt(bridgeInet6LocalBase, instance.index)
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
if platformInterface != nil && platformInterface.UsePlatformBridge() {
instance.platform = platformInterface
}
return instance, nil
}
func (b *backendDarwin) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
err := b.start()
if err != nil {
b.Close()
return err
}
return nil
}
func (b *backendDarwin) start() error {
if b.platform != nil {
return b.startPlatform()
}
b.tunName = tun.CalculateInterfaceName(b.bridgeName)
b.anchorName = "com.apple/sing-box-" + b.tunName
tunInterface, err := tun.New(tun.Options{
Name: b.tunName,
MTU: bridgeTunMTU,
AutoRoute: false,
InterfaceMonitor: b.networkManager.InterfaceMonitor(),
Logger: b.logger,
EXP_ExternalConfiguration: true,
EXP_MultiPendingPackets: true,
})
if err != nil {
return E.Cause(err, "create bridge tun")
}
b.tunInterface = tunInterface
err = tunInterface.Start()
if err != nil {
return E.Cause(err, "start bridge tun")
}
b.forwardingRestore = enableDarwinForwarding(b.logger, b.inet4Port.IsValid(), b.inet6Port.IsValid())
err = assignBridgePortAddress(b.tunName, b.inet4Local, b.inet4Port)
if err != nil {
return E.Cause(err, "add bridge route")
}
err = assignBridgePortAddress(b.tunName, b.inet6Local, b.inet6Port)
if err != nil {
b.logger.Debug(E.Cause(err, "IPv6 bridge routing unavailable, disabling IPv6 forwarding"))
b.inet6Port = netip.Addr{}
}
err = b.enablePf()
if err != nil {
return E.Cause(err, "enable pf")
}
b.batchTUN = tunInterface.(tun.DarwinTUN)
b.closed = make(chan struct{})
b.readDone = make(chan struct{})
b.registerMonitors(b.syncEgress)
b.syncEgress()
go b.batchReadLoop()
b.logger.Info("bridge started at ", b.tunName, " (masquerade, egress ", b.egressLabel(), ")")
return nil
}
func (b *backendDarwin) startPlatform() error {
session, err := b.platform.CreateBridge(adapter.BridgeOptions{
BridgeName: b.bridgeName,
MTU: bridgeTunMTU,
Inet4Port: b.inet4Port,
Inet6Port: b.inet6Port,
Interface: b.boundInterface,
})
if err != nil {
return E.Cause(err, "create bridge")
}
b.session = session
b.tunName = session.Name()
if !session.Inet6Active() {
b.inet6Port = netip.Addr{}
}
tunInterface, err := tun.New(tun.Options{
Name: b.tunName,
MTU: bridgeTunMTU,
FileDescriptor: session.FileDescriptor(),
Logger: b.logger,
EXP_ExternalConfiguration: true,
EXP_MultiPendingPackets: true,
})
if err != nil {
return E.Cause(err, "create bridge tun")
}
b.tunInterface = tunInterface
err = tunInterface.Start()
if err != nil {
return E.Cause(err, "start bridge tun")
}
b.batchTUN = tunInterface.(tun.DarwinTUN)
b.closed = make(chan struct{})
b.readDone = make(chan struct{})
b.registerMonitors(b.syncSessionEgress)
b.syncSessionEgress()
go b.batchReadLoop()
b.logger.Info("bridge started at ", b.tunName, " (platform, egress ", b.egressLabel(), ")")
return nil
}
func (b *backendDarwin) egressLabel() string {
if b.boundInterface != "" {
return b.boundInterface
}
return "auto"
}
func (b *backendDarwin) Close() error {
b.closeOnce.Do(func() {
if b.closed != nil {
close(b.closed)
}
if b.unregister != nil {
b.unregister()
}
if b.pfDevice != nil && b.anchorName != "" {
b.egressAccess.Lock()
_ = b.pfDevice.LoadAnchor(b.anchorName, nil)
b.egressAccess.Unlock()
}
restoreDarwinForwarding(b.forwardingRestore)
b.forwardingRestore = nil
if b.pfDevice != nil {
if b.pfToken != 0 {
_ = b.pfDevice.StopReference(b.pfToken)
}
_ = b.pfDevice.Close()
}
if b.tunInterface != nil {
b.tunInterface.Close()
}
if b.readDone != nil {
<-b.readDone
}
if b.session != nil {
_ = b.session.Close()
}
releaseBridgeIndex(b.index)
})
return nil
}
// Zero tells the dispatcher not to clamp the TCP MSS or fragment; pf and the
// host kernel do both on the forwarding path instead (see buildBridgeAnchorRules).
func (b *backendDarwin) PortMTU() uint32 {
return 0
}
func (b *backendDarwin) WritePackets(packets [][]byte) error {
b.writeAccess.Lock()
defer b.writeAccess.Unlock()
for len(packets) > 0 {
chunk := packets
if len(chunk) > bridgeWriteBatchSize {
chunk = chunk[:bridgeWriteBatchSize]
}
packets = packets[len(chunk):]
batch := b.writeBatch[:0]
for _, packet := range chunk {
if len(packet) == 0 || len(packet) > maxPacketLength {
continue
}
ipVersion := header.IPVersion(packet)
if ipVersion != header.IPv4Version && ipVersion != header.IPv6Version {
continue
}
batch = append(batch, buf.As(packet))
}
if len(batch) == 0 {
continue
}
err := b.batchTUN.BatchWrite(batch)
if err != nil {
return err
}
}
return nil
}
func (b *backendDarwin) batchReadLoop() {
defer close(b.readDone)
headroom := -1
var buffers [][]byte
var batch [][]byte
for {
packets, err := b.batchTUN.BatchRead()
if err != nil {
select {
case <-b.closed:
return
default:
}
if E.IsClosed(err) || errors.Is(err, syscall.EBADF) {
return
}
b.logger.Debug(E.Cause(err, "bridge tun read"))
continue
}
if len(packets) == 0 {
continue
}
b.returnAccess.Lock()
returnPaths := b.returnPaths
b.returnAccess.Unlock()
if len(returnPaths) == 0 {
buf.ReleaseMulti(packets)
continue
}
pathHeadroom := returnPaths[0].ReturnHeadroom()
if pathHeadroom != headroom {
headroom = pathHeadroom
buffers = buffers[:0]
}
for len(buffers) < len(packets) {
buffers = append(buffers, make([]byte, headroom+bridgeTunMTU))
}
batch = batch[:0]
for _, packet := range packets {
payload := packet.Bytes()
if len(payload) == 0 {
continue
}
fixReturnChecksum(payload)
buffer := buffers[len(batch)][:headroom+len(payload)]
copy(buffer[headroom:], payload)
batch = append(batch, buffer)
}
buf.ReleaseMulti(packets)
if len(batch) == 0 {
continue
}
unconsumed := batch
currentHeadroom := headroom
for _, returnPath := range returnPaths {
if len(unconsumed) == 0 {
break
}
nextHeadroom := returnPath.ReturnHeadroom()
if nextHeadroom != currentHeadroom {
rebuffered := make([][]byte, 0, len(unconsumed))
for _, packet := range unconsumed {
payload := packet[currentHeadroom:]
buffer := make([]byte, nextHeadroom+len(payload))
copy(buffer[nextHeadroom:], payload)
rebuffered = append(rebuffered, buffer)
}
unconsumed = rebuffered
currentHeadroom = nextHeadroom
}
unconsumed = returnPath.ReturnPackets(unconsumed)
}
}
}
func (b *backendDarwin) syncEgress() {
b.egressAccess.Lock()
defer b.egressAccess.Unlock()
select {
case <-b.closed:
return
default:
}
egress := b.resolveEgress()
var rules []pfAnchorRule
if egress != "" {
rules = buildBridgeAnchorRules(b.logger, b.tunName, egress, b.boundInterface, b.inet4Port, b.inet6Port)
}
if slices.Equal(rules, b.currentRules) {
return
}
err := b.pfDevice.LoadAnchor(b.anchorName, rules)
if err != nil {
b.logger.Debug(E.Cause(err, "apply bridge egress ", egress))
return
}
b.currentRules = rules
if len(rules) == 0 {
b.logger.Debug("bridge egress unavailable, dropping forwarded traffic")
} else {
b.logger.Debug("bridge egress ", egress)
}
}
func (b *backendDarwin) enablePf() error {
device, err := openPfDevice()
if err != nil {
return err
}
token, err := device.StartReference()
if err != nil {
_ = device.Close()
return err
}
b.pfDevice = device
b.pfToken = token
return nil
}
+474
View File
@@ -0,0 +1,474 @@
package bridge
import (
"context"
"net/netip"
"sync"
"github.com/sagernet/netlink"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
"golang.org/x/sys/unix"
)
const (
defaultBridgeRuleIndex = 100
defaultBridgeTableIndexBase = 2200
)
type backendLinux struct {
backendBase
nftTableName string
routeTable int
ruleIndex int
platform adapter.PlatformInterface
batchTUN tun.LinuxTUN
writeAccess sync.Mutex
writeHeadroom int
writeBuffers [][]byte
clampMTU int
}
func newBackend(ctx context.Context, logger logger.ContextLogger, networkManager adapter.NetworkManager, tag string, options option.BridgeOutboundOptions) (Backend, error) {
instance := &backendLinux{}
err := instance.init(ctx, logger, networkManager, tag, options)
if err != nil {
return nil, err
}
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
if platformInterface != nil && platformInterface.UsePlatformBridge() {
instance.platform = platformInterface
}
instance.ruleIndex = options.IPRoute2RuleIndex
if instance.ruleIndex == 0 {
instance.ruleIndex = defaultBridgeRuleIndex
}
if instance.boundInterface != "" || instance.platform != nil {
instance.routeTable = options.IPRoute2TableIndex
if instance.routeTable == 0 {
instance.routeTable = defaultBridgeTableIndexBase + int(instance.index)
}
}
return instance, nil
}
func (b *backendLinux) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
err := b.start()
if err != nil {
b.Close()
return err
}
return nil
}
func (b *backendLinux) start() error {
if b.platform != nil {
return b.startPlatform()
}
b.tunName = tun.CalculateInterfaceName(b.bridgeName)
b.nftTableName = "sing-box-" + b.tunName
tunInterface, err := tun.New(tun.Options{
Name: b.tunName,
MTU: bridgeTunMTU,
GSO: true,
InterfaceMonitor: b.networkManager.InterfaceMonitor(),
Logger: b.logger,
EXP_ExternalConfiguration: true,
})
if err != nil {
return E.Cause(err, "create bridge tun")
}
b.tunInterface = tunInterface
err = tunInterface.Start()
if err != nil {
return E.Cause(err, "start bridge tun")
}
linuxTUN := tunInterface.(tun.LinuxTUN)
if linuxTUN.BatchSize() > 1 {
b.batchTUN = linuxTUN
b.writeHeadroom = linuxTUN.FrontHeadroom()
b.writeBuffers = make([][]byte, bridgeWriteBatchSize)
for i := range b.writeBuffers {
// handleGRO coalesces same-flow packets by appending into the first
// packet's buffer capacity, up to the 0xffff total length limit.
b.writeBuffers[i] = make([]byte, b.writeHeadroom+maxPacketLength)
}
}
inet6Active, err := setupBridgeNetfilter(b.logger, b.nftTableName, b.tunName, b.inet6Port.IsValid())
if err != nil {
return E.Cause(err, "set up bridge netfilter")
}
if !inet6Active {
b.inet6Port = netip.Addr{}
}
b.forwardingRestore = enableBridgeForwarding(b.logger, b.tunName, b.inet4Port.IsValid(), b.inet6Port.IsValid())
if b.boundInterface != "" {
b.syncEgress()
}
err = setupBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET, b.inet4Port)
if err != nil {
return E.Cause(err, "set up bridge routing")
}
err = setupBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET6, b.inet6Port)
if err != nil {
b.logger.Debug(E.Cause(err, "IPv6 bridge routing unavailable, disabling IPv6 forwarding"))
removeBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET6, b.inet6Port)
b.inet6Port = netip.Addr{}
}
b.closed = make(chan struct{})
b.readDone = make(chan struct{})
if b.batchTUN != nil {
go b.batchReadLoop()
} else {
go b.readLoop()
}
egress := "auto"
if b.boundInterface != "" {
egress = b.boundInterface
monitor := b.networkManager.NetworkMonitor()
if monitor != nil {
element := monitor.RegisterCallback(func() { b.syncEgress() })
b.unregister = func() { monitor.UnregisterCallback(element) }
} else {
b.logger.Debug("network monitor unavailable, pinned egress will not track interface changes")
}
b.syncEgress()
} else {
monitor := b.networkManager.InterfaceMonitor()
if monitor != nil {
element := monitor.RegisterCallback(func(_ *control.Interface, _ int) { b.updateClamp() })
b.unregister = func() { monitor.UnregisterCallback(element) }
}
b.updateClamp()
}
natMode := "masquerade"
if fullConeSupported() {
natMode = "full-cone NAT"
}
b.logger.Info("bridge started at ", b.tunName, " (", natMode, ", egress ", egress, ")")
return nil
}
func (b *backendLinux) startPlatform() error {
session, err := b.platform.CreateBridge(adapter.BridgeOptions{
BridgeName: b.bridgeName,
MTU: bridgeTunMTU,
Inet4Port: b.inet4Port,
Inet6Port: b.inet6Port,
RuleIndex: b.ruleIndex,
RouteTable: b.routeTable,
})
if err != nil {
return E.Cause(err, "create bridge")
}
b.session = session
b.tunName = session.Name()
if !session.Inet6Active() {
b.inet6Port = netip.Addr{}
}
tunInterface, err := tun.New(tun.Options{
Name: b.tunName,
MTU: bridgeTunMTU,
GSO: true,
FileDescriptor: session.FileDescriptor(),
Logger: b.logger,
})
if err != nil {
return E.Cause(err, "create bridge tun")
}
b.tunInterface = tunInterface
err = tunInterface.Start()
if err != nil {
return E.Cause(err, "start bridge tun")
}
linuxTUN := tunInterface.(tun.LinuxTUN)
if linuxTUN.BatchSize() > 1 {
b.batchTUN = linuxTUN
b.writeHeadroom = linuxTUN.FrontHeadroom()
b.writeBuffers = make([][]byte, bridgeWriteBatchSize)
for i := range b.writeBuffers {
b.writeBuffers[i] = make([]byte, b.writeHeadroom+maxPacketLength)
}
}
b.closed = make(chan struct{})
b.readDone = make(chan struct{})
if b.batchTUN != nil {
go b.batchReadLoop()
} else {
go b.readLoop()
}
monitor := b.networkManager.InterfaceMonitor()
if monitor != nil {
element := monitor.RegisterCallback(func(_ *control.Interface, _ int) { b.syncSessionEgress() })
b.unregister = func() { monitor.UnregisterCallback(element) }
}
b.syncSessionEgress()
egress := "auto"
if b.boundInterface != "" {
egress = b.boundInterface
}
b.logger.Info("bridge started at ", b.tunName, " (platform, egress ", egress, ")")
return nil
}
func (b *backendLinux) Close() error {
b.closeOnce.Do(func() {
if b.closed != nil {
close(b.closed)
}
if b.unregister != nil {
b.unregister()
}
if b.tunInterface != nil {
b.tunInterface.Close()
}
if b.readDone != nil {
<-b.readDone
}
if b.session != nil {
_ = b.session.Close()
} else {
b.egressAccess.Lock()
if b.tunName != "" {
cleanupBridgeNetfilter(b.nftTableName)
removeBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET, b.inet4Port)
removeBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET6, b.inet6Port)
}
if b.routeTable != 0 {
flushBridgeRouteTable(b.routeTable)
}
b.egressAccess.Unlock()
restoreBridgeForwarding(b.forwardingRestore)
b.forwardingRestore = nil
}
releaseBridgeIndex(b.index)
})
return nil
}
// Zero tells the dispatcher not to clamp the TCP MSS or fragment; the host kernel
// does both on the forwarding path instead (see setupBridgeClampRules).
func (b *backendLinux) PortMTU() uint32 {
return 0
}
func (b *backendLinux) WritePackets(packets [][]byte) error {
if b.batchTUN == nil {
for _, packet := range packets {
if len(packet) == 0 {
continue
}
_, err := b.tunInterface.Write(packet)
if err != nil {
return err
}
}
return nil
}
b.writeAccess.Lock()
defer b.writeAccess.Unlock()
for len(packets) > 0 {
chunk := packets
if len(chunk) > len(b.writeBuffers) {
chunk = chunk[:len(b.writeBuffers)]
}
packets = packets[len(chunk):]
batch := make([][]byte, 0, len(chunk))
for i, packet := range chunk {
if len(packet) == 0 || len(packet) > maxPacketLength {
continue
}
buffer := b.writeBuffers[i][:b.writeHeadroom+len(packet)]
copy(buffer[b.writeHeadroom:], packet)
batch = append(batch, buffer)
}
if len(batch) == 0 {
continue
}
_, err := b.batchTUN.BatchWrite(batch, b.writeHeadroom)
if err != nil {
return err
}
}
return nil
}
// BatchRead completes any kernel-deferred checksums while splitting GRO frames
// (virtio NEEDS_CSUM), so unlike readLoop no checksum fix is needed here.
func (b *backendLinux) batchReadLoop() {
defer close(b.readDone)
batchSize := b.batchTUN.BatchSize()
sizes := make([]int, batchSize)
batch := make([][]byte, 0, batchSize)
headroom := -1
var buffers [][]byte
for {
b.returnAccess.Lock()
returnPaths := b.returnPaths
b.returnAccess.Unlock()
pathHeadroom := 0
if len(returnPaths) > 0 {
pathHeadroom = returnPaths[0].ReturnHeadroom()
}
if pathHeadroom != headroom {
headroom = pathHeadroom
buffers = make([][]byte, batchSize)
for i := range buffers {
buffers[i] = make([]byte, headroom+bridgeTunMTU)
}
}
n, err := b.batchTUN.BatchRead(buffers, headroom, sizes)
if err != nil {
select {
case <-b.closed:
return
default:
}
if E.IsClosed(err) {
return
}
b.logger.Debug(E.Cause(err, "bridge tun read"))
continue
}
if n == 0 || len(returnPaths) == 0 {
continue
}
batch = batch[:0]
for i := range n {
if sizes[i] == 0 {
continue
}
batch = append(batch, buffers[i][:headroom+sizes[i]])
}
unconsumed := batch
currentHeadroom := headroom
for _, returnPath := range returnPaths {
if len(unconsumed) == 0 {
break
}
nextHeadroom := returnPath.ReturnHeadroom()
if nextHeadroom != currentHeadroom {
rebuffered := make([][]byte, 0, len(unconsumed))
for _, packet := range unconsumed {
payload := packet[currentHeadroom:]
buffer := make([]byte, nextHeadroom+len(payload))
copy(buffer[nextHeadroom:], payload)
rebuffered = append(rebuffered, buffer)
}
unconsumed = rebuffered
currentHeadroom = nextHeadroom
}
unconsumed = returnPath.ReturnPackets(unconsumed)
}
}
}
// The policy rules default to priority 100/101, ahead of sing-tun auto_route's rules,
// so forwarded packets egress the physical interface instead of looping back into
// a tun.
func (b *backendLinux) syncEgress() {
b.egressAccess.Lock()
defer b.egressAccess.Unlock()
select {
case <-b.closed:
return
default:
}
b.updateClampLocked()
flushBridgeRouteTable(b.routeTable)
link, err := netlink.LinkByName(b.boundInterface)
if err != nil {
for _, family := range activeBridgeFamilies(b.inet6Port) {
blackholeBridgeDefault(b.routeTable, family)
}
b.logger.Debug("pinned egress ", b.boundInterface, " absent, dropping forwarded traffic")
return
}
for _, family := range activeBridgeFamilies(b.inet6Port) {
b.syncEgressFamily(family, link.Attrs().Index)
}
}
func (b *backendLinux) syncEgressFamily(family int, linkIndex int) {
connected, err := netlink.RouteListFiltered(family, &netlink.Route{
LinkIndex: linkIndex,
Table: unix.RT_TABLE_MAIN,
}, netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE)
if err == nil {
for _, route := range connected {
if route.Gw != nil || route.Dst == nil {
continue
}
pinned := route
pinned.Table = b.routeTable
pinned.ILinkIndex = 0
_ = netlink.RouteReplace(&pinned)
}
}
resolved, err := netlink.RouteGetWithOptions(probeAddress(family), &netlink.RouteGetOptions{Oif: b.boundInterface})
if err == nil && len(resolved) > 0 {
defaultRoute := &netlink.Route{
LinkIndex: linkIndex,
Table: b.routeTable,
Dst: defaultDestination(family),
}
if len(resolved[0].Gw) > 0 {
defaultRoute.Gw = resolved[0].Gw
}
err = netlink.RouteReplace(defaultRoute)
if err == nil {
return
}
}
blackholeBridgeDefault(b.routeTable, family)
}
func (b *backendLinux) updateClamp() {
b.egressAccess.Lock()
defer b.egressAccess.Unlock()
select {
case <-b.closed:
return
default:
}
b.updateClampLocked()
}
func (b *backendLinux) updateClampLocked() {
mtu := bridgeTunMTU
egress := b.resolveEgress()
if egress != "" {
mtu = b.egressMTU(egress)
}
if mtu == b.clampMTU {
return
}
err := setupBridgeClamp(b.nftTableName, b.tunName, b.inet4Port, b.inet6Port, mtu)
if err != nil {
b.logger.Debug(E.Cause(err, "update bridge MSS clamp"))
return
}
b.clampMTU = mtu
}
func (b *backendLinux) egressMTU(egress string) int {
iface, err := b.networkManager.InterfaceFinder().ByName(egress)
if err != nil || iface.MTU < 576 || iface.MTU > bridgeTunMTU {
return bridgeTunMTU
}
return iface.MTU
}
+16
View File
@@ -0,0 +1,16 @@
//go:build !linux && !darwin && !(windows && (amd64 || 386))
package bridge
import (
"context"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
func newBackend(ctx context.Context, logger logger.ContextLogger, networkManager adapter.NetworkManager, tag string, options option.BridgeOutboundOptions) (Backend, error) {
return nil, E.New("bridge outbound is only supported on Linux, macOS, Windows (x86 and x64), rooted Android and jailbroken iOS")
}
+892
View File
@@ -0,0 +1,892 @@
//go:build windows && (amd64 || 386)
package bridge
import (
"context"
"encoding/binary"
"errors"
"net/netip"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/windivert"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun/gtcpip"
"github.com/sagernet/sing-tun/gtcpip/header"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"golang.org/x/sys/windows"
)
const (
bridgeReservedPortCount uint16 = 1024
bridgeICMPFlowTimeout = time.Minute
bridgeDivertPriority int16 = 0
bridgeDivertRetryDelayMin = 100 * time.Millisecond
bridgeDivertRetryDelayMax = 2 * time.Second
bridgeBatchBufferSize = 256 * 1024
)
type divertKind uint8
const (
divertTransport divertKind = iota
divertICMPEcho
divertICMPError
)
type egressState struct {
inet4 netip.Addr
inet6 netip.Addr
mtu uint32
}
type diverter struct {
handle *windivert.Handle
done chan struct{}
}
type backendWindows struct {
backendBase
writeAccess sync.Mutex
injectHandle *windivert.Handle
sendBuffer []byte
sendAddrs []windivert.Address
deliverAccess sync.Mutex
deliverBuffer []byte
deliverBuffered [][]byte
egress atomic.Pointer[egressState]
reservation *portReservation
reservedStart uint16
icmp4, icmp6 *icmpTable
diverters []*diverter
}
func newBackend(ctx context.Context, logger logger.ContextLogger, networkManager adapter.NetworkManager, tag string, options option.BridgeOutboundOptions) (Backend, error) {
instance := &backendWindows{}
err := instance.init(ctx, logger, networkManager, tag, options)
if err != nil {
return nil, err
}
return instance, nil
}
func (b *backendWindows) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
err := b.start()
if err != nil {
b.Close()
return err
}
return nil
}
func (b *backendWindows) start() error {
b.closed = make(chan struct{})
state := b.currentEgressState()
if !(b.inet4Port.IsValid() && state.inet4.IsValid()) {
b.inet4Port = netip.Addr{}
}
if !(b.inet6Port.IsValid() && state.inet6.IsValid()) {
b.inet6Port = netip.Addr{}
b.logger.Debug("bridge IPv6 egress unavailable, disabling IPv6 forwarding")
}
if !b.inet4Port.IsValid() && !b.inet6Port.IsValid() {
return E.New("bridge: no usable egress address; requires an interface with a routable address and Administrator")
}
b.egress.Store(state)
err := b.acquireReservations()
if err != nil {
return err
}
injectHandle, err := windivert.Open(nil, windivert.LayerNetwork, windivert.PriorityHighest, windivert.FlagSendOnly)
if err != nil {
return E.Cause(err, "bridge: open injection handle (Administrator required)")
}
b.injectHandle = injectHandle
b.sendBuffer = make([]byte, 0, bridgeBatchBufferSize)
b.sendAddrs = make([]windivert.Address, 0, windivert.BatchMax)
b.egressAccess.Lock()
err = b.rebuildDivertersLocked(state)
b.egressAccess.Unlock()
if err != nil {
return err
}
b.registerMonitors(b.syncEgress)
b.logger.Info("bridge started (WinDivert, egress ", b.egressLabel(), ")")
return nil
}
func (b *backendWindows) egressLabel() string {
if b.boundInterface != "" {
return b.boundInterface
}
return "auto"
}
func (b *backendWindows) acquireReservations() error {
family := windows.AF_INET
if !b.inet4Port.IsValid() {
family = windows.AF_INET6
}
reservation, err := acquirePortReservation(family, windows.SOCK_STREAM, windows.IPPROTO_TCP, bridgeReservedPortCount)
if err != nil {
return E.Cause(err, "bridge: reserve ports")
}
b.reservation = reservation
b.reservedStart = reservation.startPort
if b.inet4Port.IsValid() {
b.icmp4 = newICMPTable(bridgeICMPFlowTimeout)
}
if b.inet6Port.IsValid() {
b.icmp6 = newICMPTable(bridgeICMPFlowTimeout)
}
return nil
}
func (b *backendWindows) PortSelectorRange() (uint16, uint16) {
return b.reservedStart, bridgeReservedPortCount
}
func (b *backendWindows) rebuildDivertersLocked(state *egressState) error {
for _, existing := range b.diverters {
existing.handle.Close()
<-existing.done
}
b.diverters = nil
if b.inet4Port.IsValid() && state.inet4.IsValid() {
err := b.openFamilyDiverters(state.inet4, false)
if err != nil {
return err
}
}
if b.inet6Port.IsValid() && state.inet6.IsValid() {
err := b.openFamilyDiverters(state.inet6, true)
if err != nil {
return err
}
}
return nil
}
func (b *backendWindows) openFamilyDiverters(egressAddr netip.Addr, isV6 bool) error {
portHigh := uint16(uint32(b.reservedStart) + uint32(bridgeReservedPortCount) - 1)
entries := []struct {
what string
kind divertKind
build func() (*windivert.Filter, error)
}{
{"TCP", divertTransport, func() (*windivert.Filter, error) {
return windivert.InboundTCPPortRange(egressAddr, b.reservedStart, portHigh)
}},
{"UDP", divertTransport, func() (*windivert.Filter, error) {
return windivert.InboundUDPPortRange(egressAddr, b.reservedStart, portHigh)
}},
{"ICMP echo", divertICMPEcho, func() (*windivert.Filter, error) {
return windivert.InboundICMPEchoReply(egressAddr)
}},
{"ICMP error", divertICMPError, func() (*windivert.Filter, error) {
return windivert.InboundICMPError(egressAddr)
}},
}
for _, entry := range entries {
filter, err := entry.build()
if err != nil {
return E.Cause(err, "bridge: build ", entry.what, " divert filter")
}
err = b.openDiverter(filter, entry.kind, isV6)
if err != nil {
return err
}
}
return nil
}
func (b *backendWindows) openDiverter(filter *windivert.Filter, kind divertKind, isV6 bool) error {
handle, err := windivert.Open(filter, windivert.LayerNetwork, bridgeDivertPriority, 0)
if err != nil {
return E.Cause(err, "bridge: open divert handle")
}
d := &diverter{handle: handle, done: make(chan struct{})}
b.diverters = append(b.diverters, d)
go b.divertLoop(d, kind, isV6)
return nil
}
func (b *backendWindows) divertLoop(d *diverter, kind divertKind, isV6 bool) {
defer close(d.done)
buffer := make([]byte, bridgeBatchBufferSize)
deliverBatch := make([][]byte, 0, windivert.BatchMax)
retryDelay := bridgeDivertRetryDelayMin
for {
n, addrs, err := d.handle.RecvBatch(buffer)
if err != nil {
if errors.Is(err, windows.ERROR_OPERATION_ABORTED) || errors.Is(err, windows.ERROR_NO_DATA) || errors.Is(err, windows.ERROR_INVALID_HANDLE) {
return
}
select {
case <-b.closed:
return
default:
}
b.logger.Debug(E.Cause(err, "bridge divert recv"))
select {
case <-b.closed:
return
case <-time.After(retryDelay):
}
retryDelay = min(retryDelay*2, bridgeDivertRetryDelayMax)
continue
}
retryDelay = bridgeDivertRetryDelayMin
deliverBatch = deliverBatch[:0]
offset := 0
for i := range addrs {
packetLength := ipPacketLength(buffer[offset:n])
if packetLength <= 0 || offset+packetLength > n {
break
}
packet := buffer[offset : offset+packetLength]
offset += packetLength
if b.classifyInbound(packet, kind, isV6) {
deliverBatch = append(deliverBatch, packet)
} else {
b.reinject(d.handle, packet, &addrs[i])
}
}
if len(deliverBatch) > 0 {
b.deliver(deliverBatch)
}
}
}
func ipPacketLength(packet []byte) int {
switch header.IPVersion(packet) {
case header.IPv4Version:
if len(packet) < header.IPv4MinimumSize {
return 0
}
return int(header.IPv4(packet).TotalLength())
case header.IPv6Version:
if len(packet) < header.IPv6MinimumSize {
return 0
}
return header.IPv6MinimumSize + int(header.IPv6(packet).PayloadLength())
default:
return 0
}
}
func (b *backendWindows) classifyInbound(packet []byte, kind divertKind, isV6 bool) bool {
portAddress := b.inet4Port
if isV6 {
portAddress = b.inet6Port
}
if !portAddress.IsValid() {
return false
}
switch kind {
case divertTransport:
return rewriteAddress(packet, portAddress, false)
case divertICMPEcho:
table := b.icmpFor(isV6)
if table == nil {
return false
}
info, valid := parseTransport(packet, isV6)
if !valid || info.transport == nil {
return false
}
identifier, identifierValid := icmpIdentifier(info.transport, isV6)
if !identifierValid || !table.isActive(identifier, packetRemoteAddress(packet, isV6, true)) {
return false
}
return rewriteAddress(packet, portAddress, false)
case divertICMPError:
return b.classifyICMPError(packet, portAddress, isV6)
default:
return false
}
}
// classifyICMPError claims an inbound ICMP error whose embedded packet is
// one of our translated outbound packets, and prepares it for the
// dispatcher: only the embedded source address is rewritten back to the
// port address; the dispatcher's ICMP error return path matches the flow
// by the embedded tuple, rewrites everything else, and recomputes the
// outer checksums.
func (b *backendWindows) classifyICMPError(packet []byte, portAddress netip.Addr, isV6 bool) bool {
info, valid := parseTransport(packet, isV6)
if !valid || info.transport == nil || info.fragmented {
return false
}
var inner []byte
if isV6 {
if len(info.transport) < header.ICMPv6ErrorHeaderSize {
return false
}
if !header.ICMPv6(info.transport).Type().IsErrorType() {
return false
}
inner = info.transport[header.ICMPv6ErrorHeaderSize:]
} else {
if len(info.transport) < header.ICMPv4MinimumSize {
return false
}
switch header.ICMPv4(info.transport).Type() {
case header.ICMPv4DstUnreachable, header.ICMPv4SrcQuench, header.ICMPv4Redirect, header.ICMPv4TimeExceeded, header.ICMPv4ParamProblem:
default:
return false
}
inner = info.transport[header.ICMPv4MinimumSize:]
}
if isV6 {
return b.rewriteICMPErrorInner6(packet, inner, portAddress)
}
return b.rewriteICMPErrorInner4(packet, inner, portAddress)
}
func (b *backendWindows) rewriteICMPErrorInner4(packet, inner []byte, portAddress netip.Addr) bool {
if len(inner) < header.IPv4MinimumSize {
return false
}
innerHdr := header.IPv4(inner)
headerLength := int(innerHdr.HeaderLength())
if headerLength < header.IPv4MinimumSize || headerLength > len(inner) {
return false
}
outerDestination := header.IPv4(packet).DestinationAddr()
innerSource := innerHdr.SourceAddr()
if innerSource != outerDestination {
return false
}
transport := inner[headerLength:]
if !b.embeddedFlowActive(innerHdr.TransportProtocol(), transport, innerHdr.DestinationAddr(), false) {
return false
}
oldAddress := innerSource.As4()
newAddress := portAddress.As4()
innerHdr.SetSourceAddressWithChecksumUpdate(tcpip.AddrFrom4(newAddress))
adjustTransportChecksum(innerHdr.TransportProtocol(), transport, oldAddress[:], newAddress[:])
return true
}
func (b *backendWindows) rewriteICMPErrorInner6(packet, inner []byte, portAddress netip.Addr) bool {
if len(inner) < header.IPv6MinimumSize {
return false
}
innerHdr := header.IPv6(inner)
outerDestination := header.IPv6(packet).DestinationAddr()
innerSource := innerHdr.SourceAddr()
if innerSource != outerDestination {
return false
}
transport := inner[header.IPv6MinimumSize:]
if !b.embeddedFlowActive(innerHdr.TransportProtocol(), transport, innerHdr.DestinationAddr(), true) {
return false
}
oldAddress := innerSource.As16()
newAddress := portAddress.As16()
innerHdr.SetSourceAddress(tcpip.AddrFrom16(newAddress))
adjustTransportChecksum(innerHdr.TransportProtocol(), transport, oldAddress[:], newAddress[:])
return true
}
func (b *backendWindows) embeddedFlowActive(protocol tcpip.TransportProtocolNumber, transport []byte, remote netip.Addr, isV6 bool) bool {
switch protocol {
case header.TCPProtocolNumber, header.UDPProtocolNumber:
if len(transport) < 4 {
return false
}
return b.portReserved(binary.BigEndian.Uint16(transport[0:2]))
case header.ICMPv4ProtocolNumber:
if isV6 || len(transport) < header.ICMPv4MinimumSize {
return false
}
icmpHdr := header.ICMPv4(transport)
if icmpHdr.Type() != header.ICMPv4Echo {
return false
}
table := b.icmpFor(false)
return table != nil && table.isActive(icmpHdr.Ident(), remote)
case header.ICMPv6ProtocolNumber:
if !isV6 || len(transport) < header.ICMPv6MinimumSize {
return false
}
icmpHdr := header.ICMPv6(transport)
if icmpHdr.Type() != header.ICMPv6EchoRequest {
return false
}
table := b.icmpFor(true)
return table != nil && table.isActive(icmpHdr.Ident(), remote)
default:
return false
}
}
func (b *backendWindows) portReserved(port uint16) bool {
return port >= b.reservedStart && uint32(port) < uint32(b.reservedStart)+uint32(bridgeReservedPortCount)
}
func (b *backendWindows) deliver(packets [][]byte) {
b.deliverAccess.Lock()
defer b.deliverAccess.Unlock()
b.returnAccess.Lock()
returnPaths := b.returnPaths
b.returnAccess.Unlock()
if len(returnPaths) == 0 {
return
}
headroom := returnPaths[0].ReturnHeadroom()
// The return-path writeback copies synchronously, so the staging buffer is
// safe to reuse on the next batch.
total := 0
for _, packet := range packets {
total += headroom + len(packet)
}
if cap(b.deliverBuffer) < total {
b.deliverBuffer = make([]byte, total)
}
staging := b.deliverBuffer[:total]
buffered := b.deliverBuffered[:0]
offset := 0
for _, packet := range packets {
segment := staging[offset : offset+headroom+len(packet)]
copy(segment[headroom:], packet)
buffered = append(buffered, segment)
offset += headroom + len(packet)
}
b.deliverBuffered = buffered
unconsumed := buffered
currentHeadroom := headroom
for _, returnPath := range returnPaths {
if len(unconsumed) == 0 {
break
}
nextHeadroom := returnPath.ReturnHeadroom()
if nextHeadroom != currentHeadroom {
rebuffered := make([][]byte, 0, len(unconsumed))
for _, packet := range unconsumed {
payload := packet[currentHeadroom:]
buffer := make([]byte, nextHeadroom+len(payload))
copy(buffer[nextHeadroom:], payload)
rebuffered = append(rebuffered, buffer)
}
unconsumed = rebuffered
currentHeadroom = nextHeadroom
}
unconsumed = returnPath.ReturnPackets(unconsumed)
}
}
func (b *backendWindows) reinject(handle *windivert.Handle, packet []byte, addr *windivert.Address) {
_, err := handle.Send(packet, addr)
if err != nil {
select {
case <-b.closed:
default:
b.logger.Debug(E.Cause(err, "bridge reinject"))
}
}
}
func (b *backendWindows) icmpFor(isV6 bool) *icmpTable {
if isV6 {
return b.icmp6
}
return b.icmp4
}
func (b *backendWindows) PortMTU() uint32 {
state := b.egress.Load()
if state == nil {
return 0
}
return state.mtu
}
func (b *backendWindows) WritePackets(packets [][]byte) error {
state := b.egress.Load()
if state == nil {
return nil
}
b.writeAccess.Lock()
defer b.writeAccess.Unlock()
for _, packet := range packets {
if len(packet) == 0 || len(packet) > maxPacketLength {
continue
}
if !b.prepareOutbound(packet, state) {
continue
}
if len(b.sendAddrs) == windivert.BatchMax || len(b.sendBuffer)+len(packet) > cap(b.sendBuffer) {
b.flushOutboundLocked()
}
b.sendBuffer = append(b.sendBuffer, packet...)
var addr windivert.Address
addr.SetOutbound(true)
addr.SetIPv6(header.IPVersion(packet) == header.IPv6Version)
addr.SetIPChecksum(true)
addr.SetTCPChecksum(true)
addr.SetUDPChecksum(true)
b.sendAddrs = append(b.sendAddrs, addr)
}
b.flushOutboundLocked()
return nil
}
func (b *backendWindows) flushOutboundLocked() {
if len(b.sendAddrs) == 0 {
return
}
_, err := b.injectHandle.SendBatch(b.sendBuffer, b.sendAddrs)
if err != nil {
select {
case <-b.closed:
default:
b.logger.Debug(E.Cause(err, "bridge inject"))
}
}
b.sendBuffer = b.sendBuffer[:0]
b.sendAddrs = b.sendAddrs[:0]
}
func (b *backendWindows) prepareOutbound(packet []byte, state *egressState) bool {
var (
isV6 bool
egressAddr netip.Addr
)
switch header.IPVersion(packet) {
case header.IPv4Version:
egressAddr = state.inet4
case header.IPv6Version:
isV6 = true
egressAddr = state.inet6
default:
return false
}
if !egressAddr.IsValid() {
return false
}
// The batched injection ioctl walks the buffer by IP total length; a
// packet with trailing bytes would desynchronize the walk and fail the
// whole batch.
if ipPacketLength(packet) != len(packet) {
return false
}
info, valid := parseTransport(packet, isV6)
if !valid {
return false
}
switch info.protocol {
case header.TCPProtocolNumber, header.UDPProtocolNumber:
if info.transport != nil {
if len(info.transport) < 4 {
return false
}
if !b.portReserved(binary.BigEndian.Uint16(info.transport[0:2])) {
b.logger.Debug("bridge: dropping outbound packet with source port outside the reserved block")
return false
}
}
case header.ICMPv4ProtocolNumber, header.ICMPv6ProtocolNumber:
table := b.icmpFor(isV6)
if table == nil {
return false
}
if info.transport != nil {
identifier, identifierValid := icmpIdentifier(info.transport, isV6)
if !identifierValid {
return false
}
table.register(identifier, packetRemoteAddress(packet, isV6, false))
}
default:
return false
}
return rewriteAddressWithInfo(packet, info, egressAddr, true)
}
func (b *backendWindows) syncEgress() {
b.egressAccess.Lock()
defer b.egressAccess.Unlock()
select {
case <-b.closed:
return
default:
}
state := b.currentEgressState()
previous := b.egress.Load()
if previous != nil && previous.inet4 == state.inet4 && previous.inet6 == state.inet6 {
if *previous != *state {
b.egress.Store(state)
}
return
}
if (b.inet4Port.IsValid() && !state.inet4.IsValid()) || (b.inet6Port.IsValid() && !state.inet6.IsValid()) {
b.logger.Debug("bridge egress address unavailable, dropping affected traffic")
}
err := b.rebuildDivertersLocked(state)
if err != nil {
b.egress.Store(&egressState{})
b.logger.Debug(E.Cause(err, "bridge rebuild diverters"))
return
}
b.egress.Store(state)
b.logger.Debug("bridge egress ", b.egressLabel(), " updated")
}
func (b *backendWindows) currentEgressState() *egressState {
state := &egressState{}
egressName := b.resolveEgress()
if egressName == "" {
return state
}
finder := b.networkManager.InterfaceFinder()
if finder == nil {
return state
}
egressInterface, err := finder.ByName(egressName)
if err != nil {
return state
}
if egressInterface.MTU > 0 {
state.mtu = uint32(egressInterface.MTU)
}
for _, prefix := range egressInterface.Addresses {
address := prefix.Addr().Unmap()
if address.Is4() {
if !state.inet4.IsValid() && address.IsGlobalUnicast() {
state.inet4 = address
}
} else if !state.inet6.IsValid() && address.IsGlobalUnicast() {
state.inet6 = address
}
}
return state
}
func (b *backendWindows) Close() error {
b.closeOnce.Do(func() {
if b.closed != nil {
close(b.closed)
}
if b.unregister != nil {
b.unregister()
}
b.egressAccess.Lock()
for _, d := range b.diverters {
d.handle.Close()
<-d.done
}
b.diverters = nil
b.egressAccess.Unlock()
if b.injectHandle != nil {
b.injectHandle.Close()
}
b.reservation.Close()
b.reservation = nil
releaseBridgeIndex(b.index)
})
return nil
}
type transportInfo struct {
protocol tcpip.TransportProtocolNumber
transport []byte
fragmented bool
}
func parseTransport(packet []byte, isV6 bool) (transportInfo, bool) {
if !isV6 {
if len(packet) < header.IPv4MinimumSize {
return transportInfo{}, false
}
ipHdr := header.IPv4(packet)
if !ipHdr.IsValid(len(packet)) {
return transportInfo{}, false
}
info := transportInfo{
protocol: ipHdr.TransportProtocol(),
fragmented: ipHdr.More() || ipHdr.FragmentOffset() != 0,
}
if ipHdr.FragmentOffset() == 0 {
info.transport = ipHdr.Payload()
}
return info, true
}
if len(packet) < header.IPv6MinimumSize {
return transportInfo{}, false
}
ipHdr := header.IPv6(packet)
payloadLength := int(ipHdr.PayloadLength())
if payloadLength > len(packet)-header.IPv6MinimumSize {
return transportInfo{}, false
}
payload := packet[header.IPv6MinimumSize:][:payloadLength]
var info transportInfo
nextHeader := ipHdr.NextHeader()
offset := 0
for {
switch header.IPv6ExtensionHeaderIdentifier(nextHeader) {
case header.IPv6HopByHopOptionsExtHdrIdentifier, header.IPv6RoutingExtHdrIdentifier, header.IPv6DestinationOptionsExtHdrIdentifier:
if len(payload)-offset < 2 {
return transportInfo{}, false
}
extensionLength := (int(payload[offset+1]) + 1) * 8
if len(payload)-offset < extensionLength {
return transportInfo{}, false
}
nextHeader = payload[offset]
offset += extensionLength
case header.IPv6FragmentExtHdrIdentifier:
if len(payload)-offset < header.IPv6FragmentHeaderSize {
return transportInfo{}, false
}
fragmentHdr := header.IPv6Fragment(payload[offset : offset+header.IPv6FragmentHeaderSize])
info.fragmented = true
if fragmentHdr.FragmentOffset() != 0 {
info.protocol = fragmentHdr.TransportProtocol()
return info, true
}
nextHeader = fragmentHdr.NextHeader()
offset += header.IPv6FragmentHeaderSize
case header.IPv6NoNextHeaderIdentifier:
return transportInfo{}, false
default:
info.protocol = tcpip.TransportProtocolNumber(nextHeader)
info.transport = payload[offset:]
return info, true
}
}
}
func packetRemoteAddress(packet []byte, isV6, inbound bool) netip.Addr {
if isV6 {
ipHdr := header.IPv6(packet)
if inbound {
return ipHdr.SourceAddr()
}
return ipHdr.DestinationAddr()
}
ipHdr := header.IPv4(packet)
if inbound {
return ipHdr.SourceAddr()
}
return ipHdr.DestinationAddr()
}
func icmpIdentifier(transport []byte, isV6 bool) (uint16, bool) {
if isV6 {
if len(transport) < header.ICMPv6MinimumSize {
return 0, false
}
return header.ICMPv6(transport).Ident(), true
}
if len(transport) < header.ICMPv4MinimumSize {
return 0, false
}
return header.ICMPv4(transport).Ident(), true
}
func rewriteAddress(packet []byte, address netip.Addr, source bool) bool {
info, valid := parseTransport(packet, header.IPVersion(packet) == header.IPv6Version)
if !valid {
return false
}
return rewriteAddressWithInfo(packet, info, address, source)
}
func rewriteAddressWithInfo(packet []byte, info transportInfo, address netip.Addr, source bool) bool {
switch header.IPVersion(packet) {
case header.IPv4Version:
ipHdr := header.IPv4(packet)
newAddress := address.As4()
var oldAddress [4]byte
if source {
copy(oldAddress[:], ipHdr.SourceAddressSlice())
} else {
copy(oldAddress[:], ipHdr.DestinationAddressSlice())
}
if info.transport != nil {
adjustTransportChecksum(info.protocol, info.transport, oldAddress[:], newAddress[:])
}
if source {
ipHdr.SetSourceAddressWithChecksumUpdate(tcpip.AddrFrom4(newAddress))
} else {
ipHdr.SetDestinationAddressWithChecksumUpdate(tcpip.AddrFrom4(newAddress))
}
return true
case header.IPv6Version:
ipHdr := header.IPv6(packet)
newAddress := address.As16()
var oldAddress [16]byte
if source {
copy(oldAddress[:], ipHdr.SourceAddressSlice())
ipHdr.SetSourceAddress(tcpip.AddrFrom16(newAddress))
} else {
copy(oldAddress[:], ipHdr.DestinationAddressSlice())
ipHdr.SetDestinationAddress(tcpip.AddrFrom16(newAddress))
}
if info.transport != nil {
adjustTransportChecksum(info.protocol, info.transport, oldAddress[:], newAddress[:])
}
return true
default:
return false
}
}
func adjustTransportChecksum(protocol tcpip.TransportProtocolNumber, transport []byte, oldData, newData []byte) {
oldAddress := tcpip.AddrFromSlice(oldData)
newAddress := tcpip.AddrFromSlice(newData)
switch protocol {
case header.TCPProtocolNumber:
if len(transport) < header.TCPMinimumSize {
return
}
header.TCP(transport).UpdateChecksumPseudoHeaderAddress(oldAddress, newAddress, true)
case header.UDPProtocolNumber:
if len(transport) < header.UDPMinimumSize {
return
}
udpHdr := header.UDP(transport)
if udpHdr.Checksum() == 0 {
return
}
udpHdr.UpdateChecksumPseudoHeaderAddress(oldAddress, newAddress, true)
if udpHdr.Checksum() == 0 {
udpHdr.SetChecksum(0xffff)
}
case header.ICMPv6ProtocolNumber:
if len(transport) < header.ICMPv6MinimumSize {
return
}
header.ICMPv6(transport).UpdateChecksumPseudoHeaderAddress(oldAddress, newAddress)
}
}
+11
View File
@@ -0,0 +1,11 @@
package bridge
import (
"unsafe"
)
//go:linkname unixIoctlPtr golang.org/x/sys/unix.ioctlPtr
func unixIoctlPtr(fd int, request uint, arg unsafe.Pointer) error
//go:linkname unixSysctl golang.org/x/sys/unix.sysctl
func unixSysctl(mib []int32, old *byte, oldLen *uintptr, newValue *byte, newLen uintptr) error
+66
View File
@@ -0,0 +1,66 @@
//go:build windows && (amd64 || 386)
package bridge
import (
"net/netip"
"sync"
"time"
)
// icmpTable records liveness of ICMP echo flows by (identifier, remote
// address). The identifier passes through untranslated — the dispatcher NAT
// already set it to the selector — and Windows ping.exe uses a constant
// identifier, so the remote address is needed to tell a bridged reply from the
// host's own ping.
type icmpTable struct {
access sync.Mutex
timeout time.Duration
active map[icmpFlowKey]time.Time
lastSweep time.Time
}
type icmpFlowKey struct {
identifier uint16
remote netip.Addr
}
func newICMPTable(timeout time.Duration) *icmpTable {
return &icmpTable{
timeout: timeout,
active: make(map[icmpFlowKey]time.Time),
}
}
func (t *icmpTable) register(identifier uint16, remote netip.Addr) {
now := time.Now()
key := icmpFlowKey{identifier: identifier, remote: remote}
t.access.Lock()
defer t.access.Unlock()
if now.Sub(t.lastSweep) >= t.timeout {
t.lastSweep = now
for flow, lastActive := range t.active {
if now.Sub(lastActive) >= t.timeout {
delete(t.active, flow)
}
}
}
t.active[key] = now
}
func (t *icmpTable) isActive(identifier uint16, remote netip.Addr) bool {
now := time.Now()
key := icmpFlowKey{identifier: identifier, remote: remote}
t.access.Lock()
defer t.access.Unlock()
lastActive, loaded := t.active[key]
if !loaded {
return false
}
if now.Sub(lastActive) >= t.timeout {
delete(t.active, key)
return false
}
t.active[key] = now
return true
}
+540
View File
@@ -0,0 +1,540 @@
package bridge
import (
"net"
"net/netip"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
"github.com/sagernet/netlink"
"github.com/sagernet/nftables"
"github.com/sagernet/nftables/binaryutil"
"github.com/sagernet/nftables/expr"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"golang.org/x/sys/unix"
)
// fullcone is an out-of-tree nftables verb (the nft_fullcone module), absent on
// stock kernels.
var (
fullConeProbeOnce sync.Once
fullConeProbeResult bool
)
func enableBridgeForwarding(logger logger.ContextLogger, tunName string, inet4 bool, inet6 bool) []sysctlState {
var restore []sysctlState
enable := func(path string) {
content, err := os.ReadFile(path)
if err != nil {
logger.Debug(E.Cause(err, "read ", path))
return
}
value := strings.TrimSpace(string(content))
if value == "1" {
return
}
err = os.WriteFile(path, []byte("1"), 0o644)
if err != nil {
logger.Debug(E.Cause(err, "enable ", path))
return
}
restore = append(restore, sysctlState{name: path, value: value})
}
if inet4 {
enable("/proc/sys/net/ipv4/ip_forward")
}
if inet6 {
enable("/proc/sys/net/ipv6/conf/all/forwarding")
}
_ = os.WriteFile("/proc/sys/net/ipv4/conf/"+tunName+"/rp_filter", []byte("2"), 0o644)
return restore
}
func restoreBridgeForwarding(states []sysctlState) {
for _, state := range states {
_ = os.WriteFile(state.name, []byte(state.value), 0o644)
}
}
var (
nftablesProbeOnce sync.Once
nftablesMissing bool
)
// A kernel built without CONFIG_NF_TABLES (common on pre-GKI Android) answers a
// whole nfnetlink batch with a single EOPNOTSUPP ack, while the client waits for
// one ack per batched message and blocks forever; only non-batch requests are
// answered reliably, so probe with a dump before the first batch operation.
func bridgeUseIptables() bool {
nftablesProbeOnce.Do(func() {
nft, err := nftables.New()
if err != nil {
nftablesMissing = true
return
}
_, err = nft.ListTablesOfFamily(nftables.TableFamilyINet)
nftablesMissing = err != nil
})
return nftablesMissing
}
func setupBridgeNetfilter(logger logger.ContextLogger, tableName string, tunName string, inet6 bool) (bool, error) {
if bridgeUseIptables() {
return setupBridgeIptables(logger, tableName, tunName, inet6)
}
err := setupBridgeNftables(tableName, tunName)
if err != nil {
return false, err
}
return inet6, nil
}
func setupBridgeClamp(tableName string, tunName string, inet4Port netip.Addr, inet6Port netip.Addr, mtu int) error {
if bridgeUseIptables() {
return setupBridgeClampIptables(tableName, tunName, inet4Port, inet6Port, mtu)
}
return setupBridgeClampRules(tableName, tunName, inet4Port, inet6Port, mtu)
}
func cleanupBridgeNetfilter(tableName string) {
if bridgeUseIptables() {
cleanupBridgeIptables(tableName)
return
}
cleanupBridgeNftables(tableName)
}
// Bit 30 stays clear of Android netd's fwmark, which occupies bits 0-20 (netid,
// explicit, protected, permission, uid billing).
const bridgeIptablesMark = "0x40000000/0x40000000"
// The libsu root process inherits a PATH without /system/bin.
func iptablesPath(binary string) string {
path, err := exec.LookPath(binary)
if err == nil {
return path
}
if runtime.GOOS == "android" {
return "/system/bin/" + binary
}
return binary
}
func runIptables(binary string, args ...string) error {
output, err := exec.Command(iptablesPath(binary), args...).CombinedOutput()
if err != nil {
return E.Cause(err, binary, " ", strings.Join(args, " "), ": ", strings.TrimSpace(string(output)))
}
return nil
}
// iptables refuses input interface matches in nat POSTROUTING, so the mangle
// FORWARD chain marks packets entering from the bridge tun and the nat chain
// masquerades by mark.
func setupBridgeIptables(logger logger.ContextLogger, tableName string, tunName string, inet6 bool) (bool, error) {
cleanupBridgeIptables(tableName)
err := setupBridgeIptablesFamily("iptables", tableName, tunName)
if err != nil {
cleanupBridgeIptablesFamily("iptables", tableName)
return false, err
}
if !inet6 {
return false, nil
}
err = setupBridgeIptablesFamily("ip6tables", tableName, tunName)
if err != nil {
cleanupBridgeIptablesFamily("ip6tables", tableName)
logger.Debug(E.Cause(err, "IPv6 NAT unavailable, disabling IPv6 forwarding"))
return false, nil
}
return true, nil
}
func setupBridgeIptablesFamily(binary string, tableName string, tunName string) error {
err := runIptables(binary, "-t", "nat", "-N", tableName)
if err != nil {
return err
}
err = runIptables(binary, "-t", "nat", "-A", tableName, "-m", "mark", "--mark", bridgeIptablesMark, "-j", "MASQUERADE")
if err != nil {
return err
}
err = runIptables(binary, "-t", "nat", "-I", "POSTROUTING", "-j", tableName)
if err != nil {
return err
}
err = runIptables(binary, "-t", "mangle", "-N", tableName)
if err != nil {
return err
}
err = runIptables(binary, "-t", "mangle", "-A", tableName, "-i", tunName, "-j", "MARK", "--set-xmark", bridgeIptablesMark)
if err != nil {
return err
}
err = runIptables(binary, "-t", "mangle", "-I", "FORWARD", "-j", tableName)
if err != nil {
return err
}
return setupBridgeFilterAcceptFamily(binary, tableName, tunName)
}
// netd installs an unconditional DROP in the filter FORWARD chain
// (tetherctrl_FORWARD).
func setupBridgeFilterAcceptFamily(binary string, tableName string, tunName string) error {
err := runIptables(binary, "-t", "filter", "-N", tableName)
if err != nil {
return err
}
err = runIptables(binary, "-t", "filter", "-A", tableName, "-i", tunName, "-j", "ACCEPT")
if err != nil {
return err
}
err = runIptables(binary, "-t", "filter", "-A", tableName, "-o", tunName, "-j", "ACCEPT")
if err != nil {
return err
}
return runIptables(binary, "-t", "filter", "-I", "FORWARD", "-j", tableName)
}
func setupBridgeClampIptables(tableName string, tunName string, inet4Port netip.Addr, inet6Port netip.Addr, mtu int) error {
families := []struct {
binary string
port netip.Addr
headerSize int
}{
{"iptables", inet4Port, 40},
{"ip6tables", inet6Port, 60},
}
for _, family := range families {
if !family.port.IsValid() {
continue
}
err := runIptables(family.binary, "-t", "mangle", "-F", tableName)
if err != nil {
return err
}
err = runIptables(family.binary, "-t", "mangle", "-A", tableName, "-i", tunName, "-j", "MARK", "--set-xmark", bridgeIptablesMark)
if err != nil {
return err
}
err = runIptables(family.binary, "-t", "mangle", "-A", tableName, "-i", tunName,
"-p", "tcp", "--tcp-flags", "SYN,RST", "SYN",
"-j", "TCPMSS", "--set-mss", strconv.Itoa(mtu-family.headerSize))
if err != nil {
return err
}
}
return nil
}
func cleanupBridgeIptables(tableName string) {
cleanupBridgeIptablesFamily("iptables", tableName)
cleanupBridgeIptablesFamily("ip6tables", tableName)
}
func cleanupBridgeIptablesFamily(binary string, tableName string) {
cleanupBridgeIptablesTable(binary, "nat", "POSTROUTING", tableName)
cleanupBridgeIptablesTable(binary, "mangle", "FORWARD", tableName)
cleanupBridgeIptablesTable(binary, "filter", "FORWARD", tableName)
}
func cleanupBridgeIptablesTable(binary string, table string, hookChain string, tableName string) {
path := iptablesPath(binary)
_ = exec.Command(path, "-t", table, "-D", hookChain, "-j", tableName).Run()
_ = exec.Command(path, "-t", table, "-F", tableName).Run()
_ = exec.Command(path, "-t", table, "-X", tableName).Run()
}
func setupBridgeFamily(tunName string, ruleIndex int, routeTable int, family int, port netip.Addr) error {
if !port.IsValid() {
return nil
}
link, err := netlink.LinkByName(tunName)
if err != nil {
return err
}
err = netlink.RouteReplace(bridgeFamilyRoute(link.Attrs().Index, family, port))
if err != nil {
return E.Cause(err, "add route")
}
for _, rule := range bridgeFamilyRules(tunName, ruleIndex, routeTable, family, port) {
_ = netlink.RuleDel(rule)
err = netlink.RuleAdd(rule)
if err != nil {
return E.Cause(err, "add rule")
}
}
return nil
}
func removeBridgeFamily(tunName string, ruleIndex int, routeTable int, family int, port netip.Addr) {
if !port.IsValid() {
return
}
link, err := netlink.LinkByName(tunName)
if err == nil {
_ = netlink.RouteDel(bridgeFamilyRoute(link.Attrs().Index, family, port))
}
for _, rule := range bridgeFamilyRules(tunName, ruleIndex, routeTable, family, port) {
_ = netlink.RuleDel(rule)
}
}
func bridgeFamilyRoute(linkIndex int, family int, port netip.Addr) *netlink.Route {
bits := port.BitLen()
route := &netlink.Route{
LinkIndex: linkIndex,
Dst: &net.IPNet{IP: port.AsSlice(), Mask: net.CIDRMask(bits, bits)},
Table: unix.RT_TABLE_MAIN,
}
if family == unix.AF_INET {
route.Scope = netlink.Scope(unix.RT_SCOPE_LINK)
}
return route
}
func bridgeFamilyRules(tunName string, ruleIndex int, routeTable int, family int, port netip.Addr) []*netlink.Rule {
forwardTable := unix.RT_TABLE_MAIN
if routeTable != 0 {
forwardTable = routeTable
}
iifRule := netlink.NewRule()
iifRule.Priority = ruleIndex
iifRule.IifName = tunName
iifRule.Table = forwardTable
iifRule.Family = family
toRule := netlink.NewRule()
toRule.Priority = ruleIndex + 1
toRule.Dst = netip.PrefixFrom(port, port.BitLen())
toRule.Table = unix.RT_TABLE_MAIN
toRule.Family = family
return []*netlink.Rule{iifRule, toRule}
}
func flushBridgeRouteTable(routeTable int) {
for _, family := range []int{unix.AF_INET, unix.AF_INET6} {
routes, err := netlink.RouteListFiltered(family, &netlink.Route{Table: routeTable}, netlink.RT_FILTER_TABLE)
if err != nil {
continue
}
for _, route := range routes {
toDelete := route
_ = netlink.RouteDel(&toDelete)
}
}
}
func blackholeBridgeDefault(routeTable int, family int) {
_ = netlink.RouteReplace(&netlink.Route{
Table: routeTable,
Family: family,
Type: unix.RTN_BLACKHOLE,
Dst: defaultDestination(family),
})
}
func activeBridgeFamilies(inet6Port netip.Addr) []int {
families := []int{unix.AF_INET}
if inet6Port.IsValid() {
families = append(families, unix.AF_INET6)
}
return families
}
func probeAddress(family int) net.IP {
if family == unix.AF_INET6 {
return net.ParseIP("2000::")
}
return net.IPv4(1, 1, 1, 1)
}
func defaultDestination(family int) *net.IPNet {
if family == unix.AF_INET6 {
return &net.IPNet{IP: net.IPv6zero, Mask: net.CIDRMask(0, 128)}
}
return &net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)}
}
func setupBridgeNftables(tableName string, tunName string) error {
cleanupBridgeNftables(tableName)
nft, err := nftables.New()
if err != nil {
return err
}
table := nft.AddTable(&nftables.Table{
Family: nftables.TableFamilyINet,
Name: tableName,
})
chain := nft.AddChain(&nftables.Chain{
Name: "postrouting",
Table: table,
Type: nftables.ChainTypeNAT,
Hooknum: nftables.ChainHookPostrouting,
Priority: nftables.ChainPriorityNATSource,
})
// The nft_fullcone verb, like masquerade, sources from the routing-chosen egress
// interface.
var sourceNat expr.Any = &expr.Masq{}
if fullConeSupported() {
sourceNat = &expr.FullCone{}
}
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: nftIfname(tunName)},
sourceNat,
},
})
nft.AddChain(&nftables.Chain{
Name: "forward",
Table: table,
Type: nftables.ChainTypeFilter,
Hooknum: nftables.ChainHookForward,
Priority: nftables.ChainPriorityMangle,
})
return nft.Flush()
}
// nft_exthdr writes the MSS option unconditionally — unlike pf's max-mss or
// xt_TCPMSS it would also raise a smaller advertised MSS — so the rule matches
// only when the advertised MSS exceeds the clamp value.
func setupBridgeClampRules(tableName string, tunName string, inet4Port netip.Addr, inet6Port netip.Addr, mtu int) error {
nft, err := nftables.New()
if err != nil {
return err
}
table := &nftables.Table{
Family: nftables.TableFamilyINet,
Name: tableName,
}
chain := &nftables.Chain{
Name: "forward",
Table: table,
}
nft.FlushChain(chain)
families := []struct {
protocol byte
port netip.Addr
headerSize int
}{
{unix.NFPROTO_IPV4, inet4Port, 40},
{unix.NFPROTO_IPV6, inet6Port, 60},
}
for _, family := range families {
if !family.port.IsValid() {
continue
}
clamp := binaryutil.BigEndian.PutUint16(uint16(mtu - family.headerSize))
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyNFPROTO, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{family.protocol}},
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: nftIfname(tunName)},
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{unix.IPPROTO_TCP}},
&expr.Payload{DestRegister: 1, Base: expr.PayloadBaseTransportHeader, Offset: 13, Len: 1},
&expr.Bitwise{SourceRegister: 1, DestRegister: 1, Len: 1, Mask: []byte{0x02}, Xor: []byte{0x00}},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{0x02}},
&expr.Exthdr{DestRegister: 1, Type: 2, Offset: 2, Len: 2, Op: expr.ExthdrOpTcpopt},
&expr.Cmp{Op: expr.CmpOpGt, Register: 1, Data: clamp},
&expr.Immediate{Register: 1, Data: clamp},
&expr.Exthdr{SourceRegister: 1, Type: 2, Offset: 2, Len: 2, Op: expr.ExthdrOpTcpopt},
},
})
}
return nft.Flush()
}
func cleanupBridgeNftables(tableName string) {
nft, err := nftables.New()
if err != nil {
return
}
table, err := nft.ListTableOfFamily(tableName, nftables.TableFamilyINet)
if err != nil || table == nil {
return
}
nft.DelTable(table)
_ = nft.Flush()
}
func fullConeSupported() bool {
if runtime.GOOS == "android" {
return false
}
if bridgeUseIptables() {
return false
}
fullConeProbeOnce.Do(func() {
fullConeProbeResult = probeFullCone()
})
return fullConeProbeResult
}
const fullConeProbeTable = "sing-box-fullcone-probe"
// The kernel loads and validates the expression's module when the batch commits:
// a clean flush means the verb is available, a rejected one rolls back atomically.
func probeFullCone() bool {
deleteFullConeProbe()
nft, err := nftables.New()
if err != nil {
return false
}
table := nft.AddTable(&nftables.Table{
Family: nftables.TableFamilyINet,
Name: fullConeProbeTable,
})
chain := nft.AddChain(&nftables.Chain{
Name: "postrouting",
Table: table,
Type: nftables.ChainTypeNAT,
Hooknum: nftables.ChainHookPostrouting,
Priority: nftables.ChainPriorityNATSource,
})
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: nftIfname("sing-box-probe0")},
&expr.FullCone{},
},
})
supported := nft.Flush() == nil
deleteFullConeProbe()
return supported
}
func deleteFullConeProbe() {
nft, err := nftables.New()
if err != nil {
return
}
table, err := nft.ListTableOfFamily(fullConeProbeTable, nftables.TableFamilyINet)
if err != nil || table == nil {
return
}
nft.DelTable(table)
_ = nft.Flush()
}
func nftIfname(name string) []byte {
padded := make([]byte, 16)
copy(padded, name)
return padded
}
+138
View File
@@ -0,0 +1,138 @@
package bridge
import (
"context"
"net"
"net/netip"
"slices"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
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"
)
func RegisterOutbound(registry *outbound.Registry) {
outbound.Register[option.BridgeOutboundOptions](registry, C.TypeBridge, NewOutbound)
}
var (
_ adapter.Outbound = (*Outbound)(nil)
_ adapter.FlowOutbound = (*Outbound)(nil)
_ adapter.OutboundWithPreferredRoutes = (*Outbound)(nil)
_ adapter.Lifecycle = (*Outbound)(nil)
)
type Backend interface {
adapter.Lifecycle
tun.Port
}
type Outbound struct {
outbound.Adapter
logger log.ContextLogger
networkManager adapter.NetworkManager
platformInterface adapter.PlatformInterface
backend Backend
}
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.BridgeOutboundOptions) (adapter.Outbound, error) {
networkManager := service.FromContext[adapter.NetworkManager](ctx)
outboundBackend, err := newBackend(ctx, logger, networkManager, tag, options)
if err != nil {
return nil, err
}
return &Outbound{
Adapter: outbound.NewAdapter(C.TypeBridge, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil),
logger: logger,
networkManager: networkManager,
platformInterface: service.FromContext[adapter.PlatformInterface](ctx),
backend: outboundBackend,
}, nil
}
func (o *Outbound) Start(stage adapter.StartStage) error {
return o.backend.Start(stage)
}
func (o *Outbound) Close() error {
return o.backend.Close()
}
func (o *Outbound) PreferredDomain(metadata *adapter.InboundContext, domain string) bool {
return false
}
func (o *Outbound) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool {
return metadata.PreMatch && !o.isLocalDestination(address)
}
func (o *Outbound) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction {
if o.isLocalDestination(destination) {
o.logger.Warn("rejected connection to local destination ", destination, ": traffic to local addresses is not supported by bridge, exclude them in route rules")
return adapter.PreMatchReject
}
return adapter.PreMatchFlow
}
func (o *Outbound) isLocalDestination(destination netip.Addr) bool {
if !destination.IsValid() {
return false
}
destination = destination.Unmap()
if destination.IsLoopback() || destination.IsUnspecified() {
return true
}
if o.platformInterface != nil && slices.Contains(o.platformInterface.MyInterfaceAddress(), destination) {
return true
}
for _, netInterface := range o.networkManager.InterfaceFinder().Interfaces() {
for _, prefix := range netInterface.Addresses {
if prefix.Addr() == destination {
return true
}
}
}
return false
}
func (o *Outbound) PortAddresses() (netip.Addr, netip.Addr) {
return o.backend.PortAddresses()
}
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) AttachReturn(returnPath tun.Return) error {
return o.backend.AttachReturn(returnPath)
}
func (o *Outbound) DetachReturn(returnPath tun.Return) error {
return o.backend.DetachReturn(returnPath)
}
func (o *Outbound) WritePackets(packets [][]byte) error {
return o.backend.WritePackets(packets)
}
func (o *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
return nil, E.New("only L3 traffic is supported by bridge")
}
func (o *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return nil, E.New("only L3 traffic is supported by bridge")
}
+122
View File
@@ -0,0 +1,122 @@
//go:build linux || darwin || (windows && (amd64 || 386))
package bridge
import (
"net/netip"
"sync"
"github.com/sagernet/sing-tun/gtcpip"
"github.com/sagernet/sing-tun/gtcpip/checksum"
"github.com/sagernet/sing-tun/gtcpip/header"
E "github.com/sagernet/sing/common/exceptions"
)
const (
bridgeTunMTU = 1500
maxPacketLength = 0xffff
bridgeMaxInstances = 254
bridgeWriteBatchSize = 32
)
var (
bridgeInet4Base = netip.MustParseAddr("192.0.2.1")
bridgeInet6Base = netip.MustParseAddr("2001:db8::1")
bridgeIndexAccess sync.Mutex
bridgeIndexInUse [bridgeMaxInstances]bool
)
func allocateBridgeIndex() (uint32, error) {
bridgeIndexAccess.Lock()
defer bridgeIndexAccess.Unlock()
for index := range bridgeMaxInstances {
if !bridgeIndexInUse[index] {
bridgeIndexInUse[index] = true
return uint32(index), nil
}
}
return 0, E.New("too many bridge outbounds: limit is ", bridgeMaxInstances)
}
func releaseBridgeIndex(index uint32) {
bridgeIndexAccess.Lock()
defer bridgeIndexAccess.Unlock()
bridgeIndexInUse[index] = false
}
func addressAt(base netip.Addr, offset uint32) netip.Addr {
addr := base
for range offset {
addr = addr.Next()
}
return addr
}
func fixReturnChecksum(packet []byte) {
switch header.IPVersion(packet) {
case header.IPv4Version:
if len(packet) < header.IPv4MinimumSize {
return
}
ipHdr := header.IPv4(packet)
if !ipHdr.IsValid(len(packet)) {
return
}
if ipHdr.Flags()&header.IPv4FlagMoreFragments != 0 || ipHdr.FragmentOffset() != 0 {
return
}
ipHdr.SetChecksum(0)
ipHdr.SetChecksum(^ipHdr.CalculateChecksum())
recomputeTransportChecksum(ipHdr.TransportProtocol(), ipHdr.Payload(), ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice())
case header.IPv6Version:
if len(packet) < header.IPv6MinimumSize {
return
}
ipHdr := header.IPv6(packet)
recomputeTransportChecksum(ipHdr.TransportProtocol(), ipHdr.Payload(), ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice())
}
}
func recomputeTransportChecksum(protocol tcpip.TransportProtocolNumber, transport []byte, source []byte, destination []byte) {
switch protocol {
case header.TCPProtocolNumber:
if len(transport) < header.TCPMinimumSize {
return
}
tcpHdr := header.TCP(transport)
tcpHdr.SetChecksum(0)
payloadChecksum := checksum.Checksum(tcpHdr.Payload(), 0)
pseudoChecksum := header.PseudoHeaderChecksum(header.TCPProtocolNumber, source, destination, uint16(len(transport)))
tcpHdr.SetChecksum(^tcpHdr.CalculateChecksum(checksum.Combine(pseudoChecksum, payloadChecksum)))
case header.UDPProtocolNumber:
if len(transport) < header.UDPMinimumSize {
return
}
udpHdr := header.UDP(transport)
udpHdr.SetChecksum(0)
payloadChecksum := checksum.Checksum(udpHdr.Payload(), 0)
pseudoChecksum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, source, destination, udpHdr.Length())
udpChecksum := ^udpHdr.CalculateChecksum(checksum.Combine(pseudoChecksum, payloadChecksum))
if udpChecksum == 0 {
udpChecksum = 0xffff
}
udpHdr.SetChecksum(udpChecksum)
case header.ICMPv4ProtocolNumber:
if len(transport) < header.ICMPv4MinimumSize {
return
}
icmpHdr := header.ICMPv4(transport)
icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, 0))
case header.ICMPv6ProtocolNumber:
if len(transport) < header.ICMPv6MinimumSize {
return
}
icmpHdr := header.ICMPv6(transport)
icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
Header: icmpHdr,
Src: source,
Dst: destination,
}))
}
}
+379
View File
@@ -0,0 +1,379 @@
package bridge
import (
"net"
"net/netip"
"unsafe"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/sys/unix"
)
// Layouts and values mirror bsd/net/pfvar.h from xnu, which the SDKs do not
// ship; unchanged from xnu-4570.1.46 (macOS 10.13) through xnu-12377.1.9.
const (
pfRulesetScrub = 0
pfRulesetFilter = 1
pfRulesetNat = 2
pfActionPass = 0
pfActionScrub = 2
pfActionNat = 4
pfDirectionIn = 1
pfAddrTypeAddressMask = 0
pfAddrTypeDynamicInterface = 2
pfRouteActionRouteTo = 2
pfStateNormal = 1
pfNatProxyPortLow = 50001
pfNatProxyPortHigh = 65535
)
type pfAddr [16]byte
type pfAddrWrap struct {
Addr pfAddr
Mask pfAddr
_ uint64
Type uint8
IFlags uint8
_ [6]byte
}
type pfRuleAddr struct {
Addr pfAddrWrap
_ [8]byte
Neg uint8
_ [7]byte
}
type pfPool struct {
_ [2]uint64
_ uint64
_ [16]byte
_ pfAddr
TableIndex int32
ProxyPort [2]uint16
PortOp uint8
Opts uint8
AF uint8
_ [5]byte
}
type pfRuleUserGroup struct {
Range [2]uint32
Op uint8
_ [3]byte
}
type pfRule struct {
Src pfRuleAddr
Dst pfRuleAddr
_ [8]uint64
Label [64]byte
IfName [16]byte
QName [64]byte
PQName [64]byte
TagName [64]byte
MatchTagName [64]byte
OverloadTable [32]byte
_ [2]uint64
RPool pfPool
Evaluations uint64
Packets [2]uint64
Bytes [2]uint64
Ticket uint64
Owner [64]byte
Priority uint32
_ uint32
_ [3]uint64
OSFingerprint uint32
RouteTableID uint32
Timeout [26]uint32
States uint32
MaxStates uint32
SrcNodes uint32
MaxSrcNodes uint32
MaxSrcStates uint32
MaxSrcConn uint32
MaxSrcConnRate [2]uint32
QID uint32
PQID uint32
RouteListID uint32
Nr uint32
Prob uint32
CreatorUID uint32
CreatorPID uint32
ReturnICMP uint16
ReturnICMP6 uint16
MaxMSS uint16
Tag uint16
MatchTag uint16
_ uint16
UID pfRuleUserGroup
GID pfRuleUserGroup
RuleFlag uint32
Action uint8
Direction uint8
Log uint8
LogIf uint8
Quick uint8
IfNot uint8
MatchTagNot uint8
NatPass uint8
KeepState uint8
AF uint8
Proto uint8
Type uint8
Code uint8
Flags uint8
FlagSet uint8
MinTTL uint8
AllowOpts uint8
RouteAction uint8
ReturnTTL uint8
TOS uint8
AnchorRelative uint8
AnchorWildcard uint8
Flush uint8
ProtoVariant uint8
ExtFilter uint8
ExtMap uint8
_ uint16
DummynetPipe uint32
DummynetType uint32
}
type pfPoolAddr struct {
Addr pfAddrWrap
_ [2]uint64
IfName [16]byte
_ uint64
}
type pfiocRule struct {
Action uint32
Ticket uint32
PoolTicket uint32
Nr uint32
Anchor [1024]byte
AnchorCall [1024]byte
Rule pfRule
}
type pfiocPoolAddr struct {
Action uint32
Ticket uint32
Nr uint32
RNum uint32
RAction uint8
RLast uint8
AF uint8
Anchor [1024]byte
_ [5]byte
Addr pfPoolAddr
}
type pfiocTransElement struct {
RulesetIndex int32
Anchor [1024]byte
Ticket uint32
}
type pfiocTrans struct {
Size int32
ElementSize int32
Array *pfiocTransElement
}
type pfiocRemoveToken struct {
Token uint64
RefCount uint64
}
const (
iocParamMask = 0x1fff
iocOut = 0x40000000
iocIn = 0x80000000
iocInOut = iocIn | iocOut
)
const (
diocAddRule = iocInOut | (uint(unsafe.Sizeof(pfiocRule{}))&iocParamMask)<<16 | 'D'<<8 | 4
diocStartRef = iocOut | 8<<16 | 'D'<<8 | 8
diocStopRef = iocInOut | (uint(unsafe.Sizeof(pfiocRemoveToken{}))&iocParamMask)<<16 | 'D'<<8 | 9
diocBeginAddrs = iocInOut | (uint(unsafe.Sizeof(pfiocPoolAddr{}))&iocParamMask)<<16 | 'D'<<8 | 51
diocAddAddr = iocInOut | (uint(unsafe.Sizeof(pfiocPoolAddr{}))&iocParamMask)<<16 | 'D'<<8 | 52
diocXBegin = iocInOut | (uint(unsafe.Sizeof(pfiocTrans{}))&iocParamMask)<<16 | 'D'<<8 | 81
diocXCommit = iocInOut | (uint(unsafe.Sizeof(pfiocTrans{}))&iocParamMask)<<16 | 'D'<<8 | 82
diocXRollback = iocInOut | (uint(unsafe.Sizeof(pfiocTrans{}))&iocParamMask)<<16 | 'D'<<8 | 83
)
type pfAnchorRule struct {
RulesetIndex int32
Rule pfRule
Pool pfPoolAddr
}
type pfDevice struct {
fd int
}
func openPfDevice() (*pfDevice, error) {
fd, err := unix.Open("/dev/pf", unix.O_RDWR|unix.O_CLOEXEC, 0)
if err != nil {
return nil, E.Cause(err, "open /dev/pf")
}
return &pfDevice{fd: fd}, nil
}
func (d *pfDevice) Close() error {
return unix.Close(d.fd)
}
func (d *pfDevice) ioctl(request uint, pointer unsafe.Pointer) error {
return unixIoctlPtr(d.fd, request, pointer)
}
func (d *pfDevice) StartReference() (uint64, error) {
var token uint64
err := d.ioctl(uint(diocStartRef), unsafe.Pointer(&token))
if err != nil {
return 0, E.Cause(err, "DIOCSTARTREF")
}
return token, nil
}
func (d *pfDevice) StopReference(token uint64) error {
remove := pfiocRemoveToken{Token: token}
err := d.ioctl(uint(diocStopRef), unsafe.Pointer(&remove))
if err != nil {
return E.Cause(err, "DIOCSTOPREF")
}
return nil
}
// LoadAnchor atomically replaces the anchor's scrub, nat and filter rulesets;
// empty rules flush the anchor.
func (d *pfDevice) LoadAnchor(anchor string, rules []pfAnchorRule) error {
elements := [3]pfiocTransElement{
{RulesetIndex: pfRulesetScrub},
{RulesetIndex: pfRulesetNat},
{RulesetIndex: pfRulesetFilter},
}
for i := range elements {
copy(elements[i].Anchor[:], anchor)
}
trans := pfiocTrans{
Size: int32(len(elements)),
ElementSize: int32(unsafe.Sizeof(pfiocTransElement{})),
Array: &elements[0],
}
err := d.ioctl(uint(diocXBegin), unsafe.Pointer(&trans))
if err != nil {
return E.Cause(err, "DIOCXBEGIN")
}
for _, rule := range rules {
err = d.addRule(anchor, &elements, rule)
if err != nil {
_ = d.ioctl(uint(diocXRollback), unsafe.Pointer(&trans))
return err
}
}
err = d.ioctl(uint(diocXCommit), unsafe.Pointer(&trans))
if err != nil {
return E.Cause(err, "DIOCXCOMMIT")
}
return nil
}
func (d *pfDevice) addRule(anchor string, elements *[3]pfiocTransElement, rule pfAnchorRule) error {
var pool pfiocPoolAddr
err := d.ioctl(uint(diocBeginAddrs), unsafe.Pointer(&pool))
if err != nil {
return E.Cause(err, "DIOCBEGINADDRS")
}
if rule.Pool != (pfPoolAddr{}) {
pool.Addr = rule.Pool
pool.AF = rule.Rule.AF
err = d.ioctl(uint(diocAddAddr), unsafe.Pointer(&pool))
if err != nil {
return E.Cause(err, "DIOCADDADDR")
}
}
var ticket uint32
for _, element := range elements {
if element.RulesetIndex == rule.RulesetIndex {
ticket = element.Ticket
}
}
request := pfiocRule{
Ticket: ticket,
PoolTicket: pool.Ticket,
Rule: rule.Rule,
}
copy(request.Anchor[:], anchor)
err = d.ioctl(uint(diocAddRule), unsafe.Pointer(&request))
if err != nil {
return E.Cause(err, "DIOCADDRULE")
}
return nil
}
func pfAddrOf(address netip.Addr) (result pfAddr) {
if address.Is4() {
addr4 := address.As4()
copy(result[:], addr4[:])
} else {
addr16 := address.As16()
copy(result[:], addr16[:])
}
return
}
func pfMaskOf(bits int, is4 bool) (result pfAddr) {
totalBits := 128
if is4 {
totalBits = 32
}
copy(result[:], net.CIDRMask(bits, totalBits))
return
}
func pfHostAddress(address netip.Addr) pfAddrWrap {
return pfPrefixAddress(netip.PrefixFrom(address, address.BitLen()))
}
func pfPrefixAddress(prefix netip.Prefix) pfAddrWrap {
return pfAddrWrap{
Type: pfAddrTypeAddressMask,
Addr: pfAddrOf(prefix.Addr()),
Mask: pfMaskOf(prefix.Bits(), prefix.Addr().Is4()),
}
}
func pfDynamicInterfaceAddress(interfaceName string, is4 bool) pfAddrWrap {
wrap := pfAddrWrap{
Type: pfAddrTypeDynamicInterface,
}
if is4 {
wrap.Mask = pfMaskOf(32, true)
} else {
wrap.Mask = pfMaskOf(128, false)
}
copy(wrap.Addr[:], interfaceName)
return wrap
}
func pfFamily(is4 bool) uint8 {
if is4 {
return unix.AF_INET
}
return unix.AF_INET6
}
+73
View File
@@ -0,0 +1,73 @@
//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)
}
+223
View File
@@ -0,0 +1,223 @@
package bridge
import (
"net"
"net/netip"
"os"
"sync/atomic"
"syscall"
"unsafe"
"github.com/sagernet/sing-tun"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
)
var routeMessageSeq atomic.Int32
func interfaceGateway(interfaceIndex int, is4 bool) netip.Addr {
socketFd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
return netip.Addr{}
}
defer unix.Close(socketFd)
_ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 1})
var destination route.Addr
if is4 {
destination = &route.Inet4Addr{}
} else {
destination = &route.Inet6Addr{}
}
seq := int(routeMessageSeq.Add(1))
message := route.RouteMessage{
Type: unix.RTM_GET,
Version: unix.RTM_VERSION,
Flags: unix.RTF_IFSCOPE,
Index: interfaceIndex,
ID: uintptr(os.Getpid()),
Seq: seq,
Addrs: []route.Addr{syscall.RTAX_DST: destination},
}
request, err := message.Marshal()
if err != nil {
return netip.Addr{}
}
_, err = unix.Write(socketFd, request)
if err != nil {
return netip.Addr{}
}
buffer := make([]byte, 2048)
for {
n, err := unix.Read(socketFd, buffer)
if err != nil {
return netip.Addr{}
}
messages, err := route.ParseRIB(route.RIBTypeRoute, buffer[:n])
if err != nil {
continue
}
for _, routeMessage := range messages {
reply, isRoute := routeMessage.(*route.RouteMessage)
if !isRoute || reply.Seq != seq || reply.ID != uintptr(os.Getpid()) {
continue
}
if reply.Err != nil || reply.Flags&unix.RTF_GATEWAY == 0 || len(reply.Addrs) <= syscall.RTAX_GATEWAY {
return netip.Addr{}
}
switch gateway := reply.Addrs[syscall.RTAX_GATEWAY].(type) {
case *route.Inet4Addr:
return netip.AddrFrom4(gateway.IP)
case *route.Inet6Addr:
return netip.AddrFrom16(gateway.IP)
default:
return netip.Addr{}
}
}
}
}
func addInterfaceHostRoute(destination netip.Addr, interfaceName string) error {
tunInterface, err := net.InterfaceByName(interfaceName)
if err != nil {
return err
}
var destinationAddr, maskAddr route.Addr
if destination.Is4() {
destinationAddr = &route.Inet4Addr{IP: destination.As4()}
maskAddr = &route.Inet4Addr{IP: [4]byte{255, 255, 255, 255}}
} else {
destinationAddr = &route.Inet6Addr{IP: destination.As16()}
maskAddr = &route.Inet6Addr{IP: [16]byte{
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
}}
}
message := route.RouteMessage{
Type: unix.RTM_ADD,
Version: unix.RTM_VERSION,
Flags: unix.RTF_UP | unix.RTF_HOST | unix.RTF_STATIC,
Seq: int(routeMessageSeq.Add(1)),
Addrs: []route.Addr{
syscall.RTAX_DST: destinationAddr,
syscall.RTAX_GATEWAY: &route.LinkAddr{Index: tunInterface.Index},
syscall.RTAX_NETMASK: maskAddr,
},
}
request, err := message.Marshal()
if err != nil {
return err
}
socketFd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
return err
}
defer unix.Close(socketFd)
_, err = unix.Write(socketFd, request)
if err != nil && err != unix.EEXIST {
return E.Cause(err, "RTM_ADD")
}
return nil
}
type ifAliasRequest struct {
Name [unix.IFNAMSIZ]byte
Addr unix.RawSockaddrInet4
DstAddr unix.RawSockaddrInet4
Mask unix.RawSockaddrInet4
}
type inet6AddrLifetime struct {
Expire float64
Preferred float64
Vltime uint32
Pltime uint32
}
type ifAliasRequest6 struct {
Name [unix.IFNAMSIZ]byte
Addr unix.RawSockaddrInet6
DstAddr unix.RawSockaddrInet6
Mask unix.RawSockaddrInet6
Flags uint32
Lifetime inet6AddrLifetime
}
func assignPointToPointAddress(interfaceName string, local netip.Addr, peer netip.Addr) error {
if local.Is4() {
request := ifAliasRequest{
Addr: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: local.As4(),
},
DstAddr: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: peer.As4(),
},
Mask: unix.RawSockaddrInet4{
Len: unix.SizeofSockaddrInet4,
Family: unix.AF_INET,
Addr: [4]byte{255, 255, 255, 255},
},
}
copy(request.Name[:], interfaceName)
return interfaceIoctl(unix.AF_INET, uint(unix.SIOCAIFADDR), unsafe.Pointer(&request))
}
request := ifAliasRequest6{
Addr: unix.RawSockaddrInet6{
Len: unix.SizeofSockaddrInet6,
Family: unix.AF_INET6,
Addr: local.As16(),
},
DstAddr: unix.RawSockaddrInet6{
Len: unix.SizeofSockaddrInet6,
Family: unix.AF_INET6,
Addr: peer.As16(),
},
Mask: unix.RawSockaddrInet6{
Len: unix.SizeofSockaddrInet6,
Family: unix.AF_INET6,
Addr: [16]byte{
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
},
},
Flags: tun.IN6_IFF_NODAD | tun.IN6_IFF_SECURED,
Lifetime: inet6AddrLifetime{
Vltime: tun.ND6_INFINITE_LIFETIME,
Pltime: tun.ND6_INFINITE_LIFETIME,
},
}
copy(request.Name[:], interfaceName)
return interfaceIoctl(unix.AF_INET6, tun.SIOCAIFADDR_IN6, unsafe.Pointer(&request))
}
func interfaceIoctl(family int, request uint, pointer unsafe.Pointer) error {
socketFd, err := unix.Socket(family, unix.SOCK_DGRAM, 0)
if err != nil {
return err
}
defer unix.Close(socketFd)
return unixIoctlPtr(socketFd, request, pointer)
}
var forwardingMibs = map[string][]int32{
// CTL_NET, PF_INET, IPPROTO_IP, IPCTL_FORWARDING (netinet/in.h)
"net.inet.ip.forwarding": {syscall.CTL_NET, unix.AF_INET, 0, 1},
// CTL_NET, PF_INET6, IPPROTO_IPV6, IPV6CTL_FORWARDING (netinet6/in6.h)
"net.inet6.ip6.forwarding": {syscall.CTL_NET, unix.AF_INET6, unix.IPPROTO_IPV6, 1},
}
func getSysctlInt32(mib []int32) (int32, error) {
var value int32
valueLen := unsafe.Sizeof(value)
err := unixSysctl(mib, (*byte)(unsafe.Pointer(&value)), &valueLen, nil, 0)
return value, err
}
func setSysctlInt32(mib []int32, value int32) error {
return unixSysctl(mib, nil, nil, (*byte)(unsafe.Pointer(&value)), unsafe.Sizeof(value))
}
+250
View File
@@ -0,0 +1,250 @@
package bridge
import (
"net"
"net/netip"
"slices"
"strconv"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"golang.org/x/sys/unix"
)
func buildBridgeAnchorRules(ruleLogger logger.ContextLogger, tunName string, egress string, boundInterface string, inet4Port netip.Addr, inet6Port netip.Addr) []pfAnchorRule {
egressInterface, err := net.InterfaceByName(egress)
if err != nil {
return nil
}
mtu := egressInterface.MTU
if mtu < 576 || mtu > bridgeTunMTU {
mtu = bridgeTunMTU
}
localPrefixes, inet4Interfaces, inet6Interfaces := collectLocalSegments(egress, boundInterface, inet4Port.IsValid(), inet6Port.IsValid())
var rules []pfAnchorRule
if inet4Port.IsValid() {
rules = append(rules, pfScrubRule(egress, inet4Port, uint16(mtu-40)))
}
if inet6Port.IsValid() {
rules = append(rules, pfScrubRule(egress, inet6Port, uint16(mtu-60)))
}
if inet4Port.IsValid() {
rules = append(rules, pfNatRule(egress, inet4Port))
for _, name := range inet4Interfaces {
rules = append(rules, pfNatRule(name, inet4Port))
}
}
if inet6Port.IsValid() {
rules = append(rules, pfNatRule(egress, inet6Port))
for _, name := range inet6Interfaces {
rules = append(rules, pfNatRule(name, inet6Port))
}
}
// pf evaluates translation on the interface the routing table picks, and
// route-to on an out rule does not re-run it on the new interface: when
// another tun holds the default route the nat-on-egress rule never matches.
// route-to on the in side redirects before routing, so the packet actually
// leaves via the egress and the nat rule applies there.
if inet4Port.IsValid() {
gateway := interfaceGateway(egressInterface.Index, true)
if gateway.IsValid() {
rules = append(rules, pfRouteToRule(tunName, egress, gateway, inet4Port))
} else {
ruleLogger.Debug("no IPv4 gateway on ", egress, ", relying on the default route")
}
}
if inet6Port.IsValid() {
gateway := interfaceGateway(egressInterface.Index, false)
if gateway.IsValid() {
rules = append(rules, pfRouteToRule(tunName, egress, gateway, inet6Port))
} else {
ruleLogger.Debug("no IPv6 gateway on ", egress, ", relying on the default route")
}
}
// pf rules are last-match: the pass rules below override the route-to pin
// for destinations in connected subnets, so the routing table delivers them
// on their own interface.
for _, prefix := range localPrefixes {
port := inet4Port
if !prefix.Addr().Is4() {
port = inet6Port
}
rules = append(rules, pfPassInRule(tunName, port, prefix))
}
return rules
}
// collectLocalSegments returns the connected subnets whose destinations bypass
// the route-to pin so the routing table delivers them on their own interface,
// plus the non-egress interfaces that then need their own masquerade rule.
// With a pinned egress only its own subnets bypass, matching the Linux backend.
func collectLocalSegments(egress string, boundInterface string, inet4Active bool, inet6Active bool) (prefixes []netip.Prefix, inet4Interfaces []string, inet6Interfaces []string) {
localInterfaces, err := net.Interfaces()
if err != nil {
return
}
for _, localInterface := range localInterfaces {
if boundInterface != "" && localInterface.Name != boundInterface {
continue
}
if localInterface.Flags&net.FlagUp == 0 || localInterface.Flags&net.FlagBroadcast == 0 ||
localInterface.Flags&net.FlagLoopback != 0 || localInterface.Flags&net.FlagPointToPoint != 0 {
continue
}
interfaceAddrs, addrsErr := localInterface.Addrs()
if addrsErr != nil {
continue
}
var (
hasInet4 bool
hasInet6 bool
)
for _, interfaceAddr := range interfaceAddrs {
ipNet, isIPNet := interfaceAddr.(*net.IPNet)
if !isIPNet {
continue
}
address, valid := netip.AddrFromSlice(ipNet.IP)
if !valid {
continue
}
address = address.Unmap()
if address.IsLinkLocalUnicast() {
continue
}
if address.Is4() {
if !inet4Active {
continue
}
hasInet4 = true
} else {
if !inet6Active {
continue
}
hasInet6 = true
}
bits, _ := ipNet.Mask.Size()
prefix := netip.PrefixFrom(address, bits).Masked()
if !slices.Contains(prefixes, prefix) {
prefixes = append(prefixes, prefix)
}
}
if localInterface.Name == egress {
continue
}
if hasInet4 {
inet4Interfaces = append(inet4Interfaces, localInterface.Name)
}
if hasInet6 {
inet6Interfaces = append(inet6Interfaces, localInterface.Name)
}
}
return
}
func pfScrubRule(egress string, port netip.Addr, maxMSS uint16) pfAnchorRule {
rule := pfRule{
Action: pfActionScrub,
AF: pfFamily(port.Is4()),
Proto: unix.IPPROTO_TCP,
MaxMSS: maxMSS,
}
copy(rule.IfName[:], egress)
rule.Src.Addr = pfHostAddress(port)
return pfAnchorRule{RulesetIndex: pfRulesetScrub, Rule: rule}
}
func pfNatRule(interfaceName string, port netip.Addr) pfAnchorRule {
rule := pfRule{
Action: pfActionNat,
AF: pfFamily(port.Is4()),
}
rule.RPool.ProxyPort = [2]uint16{pfNatProxyPortLow, pfNatProxyPortHigh}
copy(rule.IfName[:], interfaceName)
rule.Src.Addr = pfHostAddress(port)
return pfAnchorRule{
RulesetIndex: pfRulesetNat,
Rule: rule,
Pool: pfPoolAddr{Addr: pfDynamicInterfaceAddress(interfaceName, port.Is4())},
}
}
func pfPassInRule(tunName string, port netip.Addr, destination netip.Prefix) pfAnchorRule {
rule := pfRule{
Action: pfActionPass,
Direction: pfDirectionIn,
AF: pfFamily(port.Is4()),
KeepState: pfStateNormal,
}
copy(rule.IfName[:], tunName)
rule.Src.Addr = pfHostAddress(port)
if destination.IsValid() {
rule.Dst.Addr = pfPrefixAddress(destination)
}
return pfAnchorRule{RulesetIndex: pfRulesetFilter, Rule: rule}
}
func pfRouteToRule(tunName string, egress string, gateway netip.Addr, port netip.Addr) pfAnchorRule {
anchorRule := pfPassInRule(tunName, port, netip.Prefix{})
anchorRule.Rule.RouteAction = pfRouteActionRouteTo
anchorRule.Pool = pfPoolAddr{Addr: pfHostAddress(gateway)}
copy(anchorRule.Pool.IfName[:], egress)
return anchorRule
}
// Assigning the port as the utun's point-to-point destination makes the kernel
// install the host route itself; a plain interface route against an address-less
// utun fails with ENETUNREACH.
func assignBridgePortAddress(tunName string, local netip.Addr, port netip.Addr) error {
if !port.IsValid() {
return nil
}
err := assignPointToPointAddress(tunName, local, port)
if err != nil {
return E.Cause(err, "assign bridge address")
}
err = addInterfaceHostRoute(port, tunName)
if err != nil {
return E.Cause(err, "add bridge host route")
}
return nil
}
func enableDarwinForwarding(forwardingLogger logger.ContextLogger, inet4Active bool, inet6Active bool) []sysctlState {
var restore []sysctlState
enable := func(name string) {
mib := forwardingMibs[name]
value, err := getSysctlInt32(mib)
if err != nil {
forwardingLogger.Debug(E.Cause(err, "read ", name))
return
}
if value == 1 {
return
}
err = setSysctlInt32(mib, 1)
if err != nil {
forwardingLogger.Debug(E.Cause(err, "enable ", name))
return
}
restore = append(restore, sysctlState{name: name, value: strconv.Itoa(int(value))})
}
if inet4Active {
enable("net.inet.ip.forwarding")
}
if inet6Active {
enable("net.inet6.ip6.forwarding")
}
return restore
}
func restoreDarwinForwarding(states []sysctlState) {
for _, state := range states {
value, err := strconv.Atoi(state.value)
if err != nil {
continue
}
_ = setSysctlInt32(forwardingMibs[state.name], int32(value))
}
}
+99
View File
@@ -0,0 +1,99 @@
//go:build linux || darwin
package bridge
import (
"net/netip"
"os"
"sync"
"github.com/sagernet/sing-tun"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/common/x/list"
)
type serviceBase struct {
logger logger.ContextLogger
mtu int
inet4Port netip.Addr
inet6Port netip.Addr
tunName string
tunFileDescriptor int
forwardingRestore []sysctlState
networkMonitor tun.NetworkUpdateMonitor
monitorElement *list.Element[tun.NetworkUpdateCallback]
access sync.Mutex
egressName string
closed bool
applyEgress func()
}
func (s *serviceBase) FileDescriptor() int {
return s.tunFileDescriptor
}
func (s *serviceBase) Name() string {
return s.tunName
}
func (s *serviceBase) Inet6Active() bool {
return s.inet6Port.IsValid()
}
func (s *serviceBase) SetEgress(interfaceName string) error {
s.access.Lock()
defer s.access.Unlock()
if s.closed {
return os.ErrClosed
}
s.egressName = interfaceName
s.applyEgress()
return nil
}
func (s *serviceBase) syncEgress() {
s.access.Lock()
defer s.access.Unlock()
if s.closed {
return
}
s.applyEgress()
}
func (s *serviceBase) startNetworkMonitor() {
networkMonitor, err := tun.NewNetworkUpdateMonitor(s.logger)
if err != nil {
s.logger.Debug(E.Cause(err, "create network monitor, egress will not track route changes"))
return
}
s.monitorElement = networkMonitor.RegisterCallback(func() { s.syncEgress() })
s.networkMonitor = networkMonitor
err = networkMonitor.Start()
if err != nil {
s.logger.Debug(E.Cause(err, "start network monitor, egress will not track route changes"))
}
}
func (s *serviceBase) beginClose() bool {
s.access.Lock()
if s.closed {
s.access.Unlock()
return false
}
s.closed = true
networkMonitor := s.networkMonitor
monitorElement := s.monitorElement
s.networkMonitor = nil
s.monitorElement = nil
s.access.Unlock()
if networkMonitor != nil {
if monitorElement != nil {
networkMonitor.UnregisterCallback(monitorElement)
}
_ = networkMonitor.Close()
}
return true
}
+208
View File
@@ -0,0 +1,208 @@
package bridge
import (
"net/netip"
"os"
"slices"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"golang.org/x/sys/unix"
)
type ServiceOptions struct {
MTU int
Inet4Port netip.Addr
Inet6Port netip.Addr
Interface string
Logger logger.ContextLogger
}
type Service struct {
serviceBase
boundInterface string
inet4Local netip.Addr
inet6Local netip.Addr
anchorName string
pfDevice *pfDevice
pfToken uint64
currentRules []pfAnchorRule
}
func NewService(options ServiceOptions) (*Service, error) {
if !options.Inet4Port.IsValid() {
return nil, E.New("missing bridge IPv4 port address")
}
serviceLogger := options.Logger
if serviceLogger == nil {
serviceLogger = logger.NOP()
}
instance := &Service{
serviceBase: serviceBase{
logger: serviceLogger,
mtu: options.MTU,
inet4Port: options.Inet4Port,
inet6Port: options.Inet6Port,
tunFileDescriptor: -1,
},
boundInterface: options.Interface,
}
instance.applyEgress = instance.syncEgressLocked
index, err := bridgeIndexOf(options.Inet4Port)
if err != nil {
return nil, err
}
instance.inet4Local = addressAt(bridgeInet4LocalBase, index)
instance.inet6Local = addressAt(bridgeInet6LocalBase, index)
err = instance.start()
if err != nil {
instance.Close()
return nil, err
}
return instance, nil
}
func bridgeIndexOf(inet4Port netip.Addr) (uint32, error) {
for index := range uint32(bridgeMaxInstances) {
if addressAt(bridgeInet4Base, index) == inet4Port {
return index, nil
}
}
return 0, E.New("unexpected bridge IPv4 port address: ", inet4Port)
}
func (s *Service) start() error {
tunFileDescriptor, tunName, err := createBridgeTun(s.mtu)
if err != nil {
return E.Cause(err, "create bridge tun")
}
s.tunFileDescriptor = tunFileDescriptor
s.tunName = tunName
s.anchorName = bridgeAnchor(tunName)
s.forwardingRestore = enableDarwinForwarding(s.logger, s.inet4Port.IsValid(), s.inet6Port.IsValid())
err = assignBridgePortAddress(tunName, s.inet4Local, s.inet4Port)
if err != nil {
return E.Cause(err, "add bridge route")
}
err = assignBridgePortAddress(tunName, s.inet6Local, s.inet6Port)
if err != nil {
s.logger.Debug(E.Cause(err, "IPv6 bridge routing unavailable, disabling IPv6 forwarding"))
s.inet6Port = netip.Addr{}
}
device, err := openPfDevice()
if err != nil {
return E.Cause(err, "enable pf")
}
s.pfDevice = device
token, err := device.StartReference()
if err != nil {
return E.Cause(err, "enable pf")
}
s.pfToken = token
s.startNetworkMonitor()
return nil
}
func (s *Service) syncEgressLocked() {
var rules []pfAnchorRule
if s.egressName != "" {
rules = buildBridgeAnchorRules(s.logger, s.tunName, s.egressName, s.boundInterface, s.inet4Port, s.inet6Port)
}
if slices.Equal(rules, s.currentRules) {
return
}
err := s.pfDevice.LoadAnchor(s.anchorName, rules)
if err != nil {
s.logger.Debug(E.Cause(err, "apply bridge egress ", s.egressName))
return
}
s.currentRules = rules
if len(rules) == 0 {
s.logger.Debug("bridge egress unavailable, dropping forwarded traffic")
} else {
s.logger.Debug("bridge egress ", s.egressName)
}
}
func (s *Service) Close() error {
if !s.beginClose() {
return nil
}
s.access.Lock()
defer s.access.Unlock()
if s.pfDevice != nil {
// anchorName is set before pfDevice is opened, so a non-nil pfDevice means
// it holds the intended target (the sub-anchor on macOS, "" on iOS).
_ = s.pfDevice.LoadAnchor(s.anchorName, nil)
if s.pfToken != 0 {
_ = s.pfDevice.StopReference(s.pfToken)
}
_ = s.pfDevice.Close()
s.pfDevice = nil
}
restoreDarwinForwarding(s.forwardingRestore)
s.forwardingRestore = nil
if s.tunFileDescriptor >= 0 {
_ = unix.Close(s.tunFileDescriptor)
s.tunFileDescriptor = -1
}
return nil
}
// The stock macOS /etc/pf.conf ends its main ruleset with wildcard
// nat/rdr/scrub/anchor references to "com.apple/*", so rules loaded into a
// sub-anchor below it are evaluated without editing the main ruleset. iOS ships
// no /etc/pf.conf and no such references, leaving the main ruleset empty and
// pf-unused; there an anchor is never traversed, so we own the main ruleset
// directly (anchor "") instead.
func bridgeAnchor(tunName string) string {
_, err := os.Stat("/etc/pf.conf")
if err != nil {
return ""
}
return "com.apple/sing-box-" + tunName
}
func createBridgeTun(mtu int) (int, string, error) {
tunFd, err := unix.Socket(unix.AF_SYSTEM, unix.SOCK_DGRAM, 2)
if err != nil {
return -1, "", os.NewSyscallError("socket", err)
}
ctlInfo := &unix.CtlInfo{}
copy(ctlInfo.Name[:], "com.apple.net.utun_control")
err = unix.IoctlCtlInfo(tunFd, ctlInfo)
if err != nil {
unix.Close(tunFd)
return -1, "", os.NewSyscallError("IoctlCtlInfo", err)
}
err = unix.Connect(tunFd, &unix.SockaddrCtl{ID: ctlInfo.Id, Unit: 0})
if err != nil {
unix.Close(tunFd)
return -1, "", os.NewSyscallError("Connect", err)
}
name, err := unix.GetsockoptString(
tunFd,
2, /* #define SYSPROTO_CONTROL 2 */
2, /* #define UTUN_OPT_IFNAME 2 */
)
if err != nil {
unix.Close(tunFd)
return -1, "", os.NewSyscallError("GetsockoptString", err)
}
socketFd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
if err != nil {
unix.Close(tunFd)
return -1, "", os.NewSyscallError("socket", err)
}
ifr := unix.IfreqMTU{MTU: int32(mtu)}
copy(ifr.Name[:], name)
err = unix.IoctlSetIfreqMTU(socketFd, &ifr)
unix.Close(socketFd)
if err != nil {
unix.Close(tunFd)
return -1, "", os.NewSyscallError("IoctlSetIfreqMTU", err)
}
return tunFd, name, nil
}
+242
View File
@@ -0,0 +1,242 @@
package bridge
import (
"net"
"net/netip"
_ "unsafe"
"github.com/sagernet/netlink"
"github.com/sagernet/sing-tun"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"golang.org/x/sys/unix"
)
type ServiceOptions struct {
BridgeName string
MTU int
Inet4Port netip.Addr
Inet6Port netip.Addr
RuleIndex int
RouteTable int
Logger logger.ContextLogger
}
type Service struct {
serviceBase
ruleIndex int
routeTable int
nftTableName string
clampMTU int
}
func NewService(options ServiceOptions) (*Service, error) {
if !options.Inet4Port.IsValid() {
return nil, E.New("missing bridge IPv4 port address")
}
if options.RouteTable == 0 {
return nil, E.New("missing bridge route table index")
}
serviceLogger := options.Logger
if serviceLogger == nil {
serviceLogger = logger.NOP()
}
instance := &Service{
serviceBase: serviceBase{
logger: serviceLogger,
mtu: options.MTU,
inet4Port: options.Inet4Port,
inet6Port: options.Inet6Port,
tunFileDescriptor: -1,
},
ruleIndex: options.RuleIndex,
routeTable: options.RouteTable,
}
instance.applyEgress = instance.syncEgressLocked
err := instance.start(options.BridgeName)
if err != nil {
instance.Close()
return nil, err
}
return instance, nil
}
func (s *Service) start(bridgeName string) error {
s.tunName = tun.CalculateInterfaceName(bridgeName)
s.nftTableName = "sing-box-" + s.tunName
tunFileDescriptor, err := openTUN(s.tunName, true)
if err != nil {
return E.Cause(err, "create bridge tun")
}
err = setTCPOffload(tunFileDescriptor)
if err != nil {
s.logger.Warn(E.Cause(err, "set TCP offload"))
}
err = setUDPOffload(tunFileDescriptor)
if err != nil {
s.logger.Warn(E.Cause(err, "set UDP offload"))
}
s.tunFileDescriptor = tunFileDescriptor
tunLink, err := netlink.LinkByName(s.tunName)
if err != nil {
return E.Cause(err, "find bridge tun")
}
err = netlink.LinkSetMTU(tunLink, s.mtu)
if err != nil {
return E.Cause(err, "set bridge tun mtu")
}
err = netlink.LinkSetUp(tunLink)
if err != nil {
return E.Cause(err, "set bridge tun up")
}
inet6Active, err := setupBridgeNetfilter(s.logger, s.nftTableName, s.tunName, s.inet6Port.IsValid())
if err != nil {
return E.Cause(err, "set up bridge netfilter")
}
if !inet6Active {
s.inet6Port = netip.Addr{}
}
s.forwardingRestore = enableBridgeForwarding(s.logger, s.tunName, s.inet4Port.IsValid(), s.inet6Port.IsValid())
err = setupBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, unix.AF_INET, s.inet4Port)
if err != nil {
return E.Cause(err, "set up bridge routing")
}
err = setupBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, unix.AF_INET6, s.inet6Port)
if err != nil {
s.logger.Debug(E.Cause(err, "IPv6 bridge routing unavailable, disabling IPv6 forwarding"))
removeBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, unix.AF_INET6, s.inet6Port)
s.inet6Port = netip.Addr{}
}
for _, family := range activeBridgeFamilies(s.inet6Port) {
blackholeBridgeDefault(s.routeTable, family)
}
s.startNetworkMonitor()
return nil
}
func (s *Service) syncEgressLocked() {
flushBridgeRouteTable(s.routeTable)
if s.egressName == "" {
for _, family := range activeBridgeFamilies(s.inet6Port) {
blackholeBridgeDefault(s.routeTable, family)
}
return
}
link, err := netlink.LinkByName(s.egressName)
if err != nil {
for _, family := range activeBridgeFamilies(s.inet6Port) {
blackholeBridgeDefault(s.routeTable, family)
}
s.logger.Debug("bridge egress ", s.egressName, " absent, dropping forwarded traffic")
return
}
for _, family := range activeBridgeFamilies(s.inet6Port) {
s.syncEgressFamilyLocked(family, link.Attrs().Index)
}
s.updateClampLocked(link.Attrs().MTU)
}
// Unlike the in-process backend this copies routes from every table: on Android
// netd leaves the main table empty and keeps each network's routes in its own
// table, resolvable only through fwmark rules that forwarded packets never carry.
func (s *Service) syncEgressFamilyLocked(family int, linkIndex int) {
routes, err := netlink.RouteListFiltered(family, &netlink.Route{
LinkIndex: linkIndex,
Table: unix.RT_TABLE_UNSPEC,
}, netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE)
if err != nil {
blackholeBridgeDefault(s.routeTable, family)
return
}
var defaultRoute *netlink.Route
for _, route := range routes {
if route.Table == unix.RT_TABLE_LOCAL || route.Table == s.routeTable {
continue
}
if route.Type != unix.RTN_UNICAST {
continue
}
if isDefaultDestination(route.Dst) {
if defaultRoute == nil {
pinned := route
defaultRoute = &pinned
}
continue
}
if route.Gw != nil {
continue
}
connected := route
connected.Table = s.routeTable
connected.ILinkIndex = 0
_ = netlink.RouteReplace(&connected)
}
if defaultRoute == nil {
blackholeBridgeDefault(s.routeTable, family)
s.logger.Debug("no default route on bridge egress ", s.egressName)
return
}
defaultRoute.Table = s.routeTable
defaultRoute.ILinkIndex = 0
err = netlink.RouteReplace(defaultRoute)
if err != nil {
blackholeBridgeDefault(s.routeTable, family)
s.logger.Debug(E.Cause(err, "pin bridge egress default route"))
}
}
func (s *Service) updateClampLocked(egressMTU int) {
mtu := s.mtu
if egressMTU >= 576 && egressMTU < mtu {
mtu = egressMTU
}
if mtu == s.clampMTU {
return
}
err := setupBridgeClamp(s.nftTableName, s.tunName, s.inet4Port, s.inet6Port, mtu)
if err != nil {
s.logger.Debug(E.Cause(err, "update bridge MSS clamp"))
return
}
s.clampMTU = mtu
}
func (s *Service) Close() error {
if !s.beginClose() {
return nil
}
s.access.Lock()
defer s.access.Unlock()
if s.tunName != "" {
cleanupBridgeNetfilter(s.nftTableName)
removeBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, unix.AF_INET, s.inet4Port)
removeBridgeFamily(s.tunName, s.ruleIndex, s.routeTable, unix.AF_INET6, s.inet6Port)
flushBridgeRouteTable(s.routeTable)
}
restoreBridgeForwarding(s.forwardingRestore)
s.forwardingRestore = nil
if s.tunFileDescriptor >= 0 {
_ = unix.Close(s.tunFileDescriptor)
s.tunFileDescriptor = -1
}
return nil
}
func isDefaultDestination(destination *net.IPNet) bool {
if destination == nil {
return true
}
ones, _ := destination.Mask.Size()
return ones == 0
}
//go:linkname openTUN github.com/sagernet/sing-tun.open
func openTUN(name string, vnetHdr bool) (int, error)
//go:linkname setTCPOffload github.com/sagernet/sing-tun.setTCPOffload
func setTCPOffload(fd int) error
//go:linkname setUDPOffload github.com/sagernet/sing-tun.setUDPOffload
func setUDPOffload(fd int) error
+39 -27
View File
@@ -5,6 +5,7 @@ package cloudflare
import (
"context"
"net"
"net/netip"
"time"
"github.com/sagernet/sing-box/adapter"
@@ -13,7 +14,6 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing-cloudflared"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/bufio"
@@ -137,32 +137,44 @@ type icmpRouterHandler struct {
tag string
}
func (h *icmpRouterHandler) RouteICMPConnection(ctx context.Context, session tun.DirectRouteSession, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
var ipVersion uint8
if session.Destination.Is4() {
ipVersion = 4
} else {
ipVersion = 6
}
destination := M.SocksaddrFrom(session.Destination, 0)
routeDestination, err := h.router.PreMatch(adapter.InboundContext{
Inbound: h.tag,
InboundType: C.TypeCloudflared,
IPVersion: ipVersion,
Network: N.NetworkICMP,
Source: M.SocksaddrFrom(session.Source, 0),
Destination: destination,
OriginDestination: destination,
}, routeContext, timeout, false)
if err != nil {
switch {
case rule.IsBypassed(err):
err = nil
case rule.IsRejected(err):
h.logger.Trace("reject ICMP connection from ", session.Source, " to ", session.Destination)
default:
h.logger.Warn(E.Cause(err, "link ICMP connection from ", session.Source, " to ", session.Destination))
func (h *icmpRouterHandler) RouteICMPFlow(source netip.Addr, destination netip.Addr) (tun.Port, error) {
result := h.router.PreMatch(adapter.InboundContext{
Inbound: h.tag,
InboundType: C.TypeCloudflared,
Network: N.NetworkICMP,
Source: M.SocksaddrFrom(source, 0),
Destination: M.SocksaddrFrom(destination, 0),
}, nil)
switch result.Action {
case adapter.PreMatchFlow:
flowOutbound, isFlowOutbound := result.Outbound.(adapter.FlowOutbound)
if !isFlowOutbound {
return nil, E.New("outbound is not a flow outbound")
}
if result.Destination.IsValid() && result.Destination.Addr() != destination {
h.logger.Trace("drop ICMP flow from ", source, " to ", destination, ": destination override is not supported from cloudflared")
return nil, E.New("destination override is not supported")
}
inet4Address, inet6Address := flowOutbound.PortAddresses()
var portAddress netip.Addr
if destination.Is4() {
portAddress = inet4Address
} else {
portAddress = inet6Address
}
if !portAddress.IsValid() || !portAddress.IsUnspecified() {
h.logger.Trace("drop ICMP flow from ", source, " to ", destination, ": forwarding ICMP to outbound/", result.Outbound.Type(), "[", result.Outbound.Tag(), "] is not supported from cloudflared")
return nil, E.New("unsupported flow outbound")
}
h.logger.Debug("link ICMP flow from ", source, " to ", destination, " via outbound/", result.Outbound.Type(), "[", result.Outbound.Tag(), "]")
return flowOutbound, nil
case adapter.PreMatchReject:
h.logger.Trace("reject ICMP flow from ", source, " to ", destination)
return nil, E.New("rejected")
case adapter.PreMatchDrop:
return nil, E.New("dropped")
default:
h.logger.Trace("drop ICMP flow from ", source, " to ", destination, ": no direct route")
return nil, E.New("no direct route")
}
return routeDestination, err
}
+39 -8
View File
@@ -16,6 +16,7 @@ import (
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/ping"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
@@ -31,7 +32,7 @@ var (
_ N.ParallelDialer = (*Outbound)(nil)
_ dialer.ParallelNetworkDialer = (*Outbound)(nil)
_ dialer.DirectDialer = (*Outbound)(nil)
_ adapter.DirectRouteOutbound = (*Outbound)(nil)
_ adapter.FlowOutbound = (*Outbound)(nil)
)
type Outbound struct {
@@ -44,6 +45,7 @@ type Outbound struct {
fallbackDelay time.Duration
isEmpty bool
myAddresses common.TypedValue[[]netip.Prefix]
icmpPort *ping.Port
}
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DirectOutboundOptions) (adapter.Outbound, error) {
@@ -75,6 +77,11 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
if options.ProxyProtocol != 0 {
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
}
if defaultDialer, isDefaultDialer := common.Cast[*dialer.DefaultDialer](outbound.dialer); isDefaultDialer {
outbound.icmpPort = ping.NewPort(ctx, logger, func(destination netip.Addr) control.Func {
return defaultDialer.DialerForICMPDestination(destination).Control
}, 0)
}
return outbound, nil
}
@@ -148,14 +155,38 @@ func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
return conn, nil
}
func (h *Outbound) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
ctx := log.ContextWithNewID(h.ctx)
destination, err := ping.ConnectDestination(ctx, h.logger, common.MustCast[*dialer.DefaultDialer](h.dialer).DialerForICMPDestination(metadata.Destination.Addr).Control, metadata.Destination.Addr, routeContext, timeout)
if err != nil {
return nil, err
func (h *Outbound) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction {
if network == N.NetworkICMP && h.icmpPort != nil {
return adapter.PreMatchFlow
}
h.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString())
return destination, nil
return adapter.PreMatchContinue
}
func (h *Outbound) PortAddresses() (netip.Addr, netip.Addr) {
return h.icmpPort.PortAddresses()
}
func (h *Outbound) PortMTU() uint32 {
return h.icmpPort.PortMTU()
}
func (h *Outbound) AttachReturn(returnPath tun.Return) error {
return h.icmpPort.AttachReturn(returnPath)
}
func (h *Outbound) DetachReturn(returnPath tun.Return) error {
return h.icmpPort.DetachReturn(returnPath)
}
func (h *Outbound) WritePackets(packets [][]byte) error {
return h.icmpPort.WritePackets(packets)
}
func (h *Outbound) Close() error {
if h.icmpPort != nil {
return h.icmpPort.Close()
}
return nil
}
func (h *Outbound) DialParallel(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr) (net.Conn, error) {
-10
View File
@@ -3,7 +3,6 @@ package group
import (
"context"
"net"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
@@ -12,7 +11,6 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
@@ -194,14 +192,6 @@ func (s *Selector) NewPacketConnection(ctx context.Context, conn N.PacketConn, m
}
}
func (s *Selector) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
selected := s.selected.Load()
if !common.Contains(selected.Network(), metadata.Network) {
return nil, E.New(metadata.Network, " is not supported by outbound: ", selected.Tag())
}
return selected.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout)
}
func RealTag(detour adapter.Outbound) string {
if group, isGroup := detour.(adapter.OutboundGroup); isGroup {
return group.Now()
-16
View File
@@ -14,7 +14,6 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/batch"
E "github.com/sagernet/sing/common/exceptions"
@@ -281,21 +280,6 @@ func (s *URLTest) NewPacketConnection(ctx context.Context, conn N.PacketConn, me
s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose)
}
func (s *URLTest) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
s.group.Touch()
selected := s.group.selectedOutboundTCP
if selected == nil {
selected, _ = s.group.Select(N.NetworkTCP)
}
if selected == nil {
return nil, E.New("missing supported outbound")
}
if !common.Contains(selected.Network(), metadata.Network) {
return nil, E.New(metadata.Network, " is not supported by outbound: ", selected.Tag())
}
return selected.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout)
}
type URLTestGroup struct {
ctx context.Context
outbound adapter.OutboundManager
+209
View File
@@ -0,0 +1,209 @@
package snell
import (
"context"
"net"
"os"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/common/listener"
"github.com/sagernet/sing-box/common/uot"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
snellprotocol "github.com/sagernet/sing-snell"
"github.com/sagernet/sing-snell/snellv5"
"github.com/sagernet/sing-snell/snellv6"
"github.com/sagernet/sing/common/auth"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func RegisterInbound(registry *inbound.Registry) {
inbound.Register[option.SnellInboundOptions](registry, C.TypeSnell, NewInbound)
}
var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
type Inbound struct {
inbound.Adapter
router adapter.ConnectionRouterEx
logger logger.ContextLogger
listener *listener.Listener
service snellprotocol.Service
users []option.SnellUser
}
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SnellInboundOptions) (adapter.Inbound, error) {
inbound := &Inbound{
Adapter: inbound.NewAdapter(C.TypeSnell, tag),
router: uot.NewRouter(router, logger),
logger: logger,
users: options.Users,
}
var userList []int
var keyList [][]byte
if len(options.Users) > 0 {
userList = make([]int, len(options.Users))
keyList = make([][]byte, len(options.Users))
for index, user := range options.Users {
userList[index] = index
keyList[index] = []byte(user.UserKey)
}
}
var err error
switch options.Version {
case 5:
var obfsMode snellprotocol.ObfsMode
obfsMode, err = snellprotocol.ParseObfsMode(options.ObfsOptions.ObfsMode)
if err != nil {
return nil, err
}
serviceOptions := snellv5.ServiceOptions{
PSK: []byte(options.PSK),
ObfsMode: obfsMode,
Handler: inbound,
}
if len(options.Users) > 0 {
var service *snellv5.MultiService[int]
service, err = snellv5.NewMultiService[int](serviceOptions)
if err != nil {
return nil, err
}
err = service.UpdateUsers(userList, keyList)
inbound.service = service
} else {
inbound.service, err = snellv5.NewService(serviceOptions)
}
case 6:
var mode snellv6.Mode
mode, err = snellv6.ParseMode(options.V6Options.Mode)
if err != nil {
return nil, err
}
serviceOptions := snellv6.ServerOptions{
PSK: []byte(options.PSK),
Mode: mode,
Handler: inbound,
}
if len(options.Users) > 0 {
var service *snellv6.MultiService[int]
service, err = snellv6.NewMultiService[int](serviceOptions)
if err != nil {
return nil, err
}
err = service.UpdateUsers(userList, keyList)
inbound.service = service
} else {
inbound.service, err = snellv6.NewService(serviceOptions)
}
case 0:
return nil, E.New("snell: missing version")
default:
return nil, E.New("snell: unsupported version: ", options.Version)
}
if err != nil {
return nil, err
}
inbound.listener = listener.New(listener.Options{
Context: ctx,
Logger: logger,
Network: []string{N.NetworkTCP},
Listen: options.ListenOptions,
ConnectionHandler: inbound,
})
return inbound, nil
}
func (h *Inbound) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
return h.listener.Start()
}
func (h *Inbound) Close() error {
return h.listener.Close()
}
func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose)
if err != nil {
N.CloseOnHandshakeFailure(conn, onClose, err)
if E.IsClosedOrCanceled(err) {
h.logger.DebugContext(ctx, "connection closed: ", err)
} else {
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
}
}
}
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
_, metadata := adapter.ExtendContext(ctx)
if source.IsValid() {
metadata.Source = source
}
if destination.IsValid() {
metadata.Destination = destination
}
h.newConnection(ctx, conn, *metadata, onClose)
}
func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
_, metadata := adapter.ExtendContext(ctx)
if source.IsValid() {
metadata.Source = source
}
if destination.IsValid() {
metadata.Destination = destination
}
h.newPacketConnection(ctx, conn, *metadata, onClose)
}
func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
metadata.Inbound = h.Tag()
metadata.InboundType = h.Type()
if len(h.users) > 0 {
userIndex, loaded := auth.UserFromContext[int](ctx)
if !loaded {
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
return
}
user := h.users[userIndex].Name
if user == "" {
user = F.ToString(userIndex)
} else {
metadata.User = user
}
h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination)
} else {
h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
}
h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
}
func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
metadata.Inbound = h.Tag()
metadata.InboundType = h.Type()
if len(h.users) > 0 {
userIndex, loaded := auth.UserFromContext[int](ctx)
if !loaded {
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
return
}
user := h.users[userIndex].Name
if user == "" {
user = F.ToString(userIndex)
} else {
metadata.User = user
}
h.logger.InfoContext(ctx, "[", user, "] inbound packet connection from ", metadata.Source)
} else {
h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
}
h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}
+141
View File
@@ -0,0 +1,141 @@
package snell
import (
"context"
"net"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/common/dialer"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
snellprotocol "github.com/sagernet/sing-snell"
"github.com/sagernet/sing-snell/snellv4"
"github.com/sagernet/sing-snell/snellv6"
"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"
)
func RegisterOutbound(registry *outbound.Registry) {
outbound.Register[option.SnellOutboundOptions](registry, C.TypeSnell, NewOutbound)
}
type Outbound struct {
outbound.Adapter
logger logger.ContextLogger
dialer N.Dialer
client snellClient
serverAddr M.Socksaddr
}
type snellClient interface {
snellprotocol.Method
DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error)
Close() error
}
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SnellOutboundOptions) (adapter.Outbound, error) {
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
if err != nil {
return nil, err
}
serverAddr := options.ServerOptions.Build()
var client snellClient
switch options.Version {
case 4:
var obfsMode snellprotocol.ObfsMode
obfsMode, err = snellprotocol.ParseObfsMode(options.ObfsOptions.ObfsMode)
if err != nil {
return nil, err
}
client, err = snellv4.NewClient(snellv4.ClientOptions{
PSK: []byte(options.PSK),
UserKey: []byte(options.UserKey),
Reuse: options.Reuse,
ObfsMode: obfsMode,
ObfsHost: options.ObfsOptions.ObfsHost,
Dialer: outboundDialer,
Server: serverAddr,
})
case 6:
var mode snellv6.Mode
mode, err = snellv6.ParseMode(options.V6Options.Mode)
if err != nil {
return nil, err
}
client, err = snellv6.NewClient(snellv6.ClientOptions{
PSK: []byte(options.PSK),
UserKey: []byte(options.UserKey),
Mode: mode,
Reuse: options.Reuse,
Dialer: outboundDialer,
Server: serverAddr,
})
case 0:
return nil, E.New("snell: missing version")
default:
return nil, E.New("snell: unsupported version: ", options.Version)
}
if err != nil {
return nil, err
}
outbound := &Outbound{
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSnell, tag, options.Network.Build(), options.DialerOptions),
logger: logger,
dialer: outboundDialer,
client: client,
serverAddr: serverAddr,
}
return outbound, nil
}
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
ctx, metadata := adapter.ExtendContext(ctx)
metadata.Outbound = h.Tag()
metadata.Destination = destination
networkName := N.NetworkName(network)
switch networkName {
case N.NetworkTCP:
h.logger.InfoContext(ctx, "outbound connection to ", destination)
return h.client.DialContext(ctx, destination)
case N.NetworkUDP:
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
conn, err := h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
if err != nil {
return nil, err
}
packetConn, err := h.client.DialPacketConn(conn)
if err != nil {
conn.Close()
return nil, err
}
return bufio.NewBindPacketConn(packetConn, destination), nil
default:
return nil, E.Extend(N.ErrUnknownNetwork, network)
}
}
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
ctx, metadata := adapter.ExtendContext(ctx)
metadata.Outbound = h.Tag()
metadata.Destination = destination
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
conn, err := h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
if err != nil {
return nil, err
}
packetConn, err := h.client.DialPacketConn(conn)
if err != nil {
conn.Close()
return nil, err
}
return packetConn, nil
}
func (h *Outbound) Close() error {
return h.client.Close()
}
+16 -106
View File
@@ -15,6 +15,7 @@ import (
"reflect"
"runtime"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
@@ -34,7 +35,6 @@ import (
"github.com/sagernet/sing-box/protocol/tailscale/tailssh"
R "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/ping"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/control"
@@ -56,7 +56,6 @@ import (
tsTUN "github.com/sagernet/tailscale/net/tstun"
"github.com/sagernet/tailscale/tailcfg"
"github.com/sagernet/tailscale/tsnet"
"github.com/sagernet/tailscale/types/ipproto"
"github.com/sagernet/tailscale/types/nettype"
"github.com/sagernet/tailscale/version"
"github.com/sagernet/tailscale/wgengine"
@@ -70,8 +69,8 @@ import (
var (
_ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil)
_ adapter.DirectRouteOutbound = (*Endpoint)(nil)
_ dialer.PacketDialerWithDestination = (*Endpoint)(nil)
_ tun.Port = (*Endpoint)(nil)
)
func init() {
@@ -95,6 +94,9 @@ type Endpoint struct {
stack *stack.Stack
icmpForwarder *tun.ICMPForwarder
filter *atomic.Pointer[filter.Filter]
returnAccess sync.Mutex
returnPath tun.Return
wgEngine wgengine.ExportedUserspaceEngine
onReconfigHook wgengine.ReconfigListener
sshReconfigHook wgengine.ReconfigListener
@@ -287,6 +289,7 @@ func (t *Endpoint) start() error {
if mtu == 0 {
mtu = uint32(tsTUN.DefaultTUNMTU())
}
t.systemInterfaceMTU = mtu
tunName := t.systemInterfaceName
if tunName == "" {
tunName = tun.CalculateInterfaceName("tailscale")
@@ -361,7 +364,9 @@ func (t *Endpoint) postStart() error {
}, true
})
}
t.server.ExportLocalBackend().ExportEngine().(wgengine.ExportedUserspaceEngine).SetOnReconfigListener(t.onReconfig)
wgEngine := t.server.ExportLocalBackend().ExportEngine().(wgengine.ExportedUserspaceEngine)
wgEngine.SetOnReconfigListener(t.onReconfig)
t.wgEngine = wgEngine
ipStack := t.server.ExportNetstack().ExportIPStack()
gErr := ipStack.SetSpoofing(tun.DefaultNIC, true)
@@ -372,7 +377,7 @@ func (t *Endpoint) postStart() error {
if gErr != nil {
return gonet.TranslateNetstackError(gErr)
}
icmpForwarder := tun.NewICMPForwarder(t.ctx, ipStack, t.logger, t, t.icmpTimeout)
icmpForwarder := tun.NewICMPForwarder(ipStack, t, t.logger)
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket)
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket)
t.stack = ipStack
@@ -622,6 +627,10 @@ func (t *Endpoint) Logout(ctx context.Context) error {
func (t *Endpoint) Close() error {
var err error
t.started.Store(false)
if t.icmpForwarder != nil {
t.icmpForwarder.Close()
t.icmpForwarder = nil
}
common.Close(common.PtrOrNil(t.sshServerInstance))
t.sshServerInstance = nil
if t.serverStarted {
@@ -776,62 +785,6 @@ func (t *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
return packetConn, nil
}
func (t *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
if !t.started.Load() {
return nil, E.New("Tailscale is not ready yet")
}
tsFilter := t.filter.Load()
if tsFilter != nil {
var ipProto ipproto.Proto
switch N.NetworkName(network) {
case N.NetworkTCP:
ipProto = ipproto.TCP
case N.NetworkUDP:
ipProto = ipproto.UDP
case N.NetworkICMP:
if !destination.IsIPv6() {
ipProto = ipproto.ICMPv4
} else {
ipProto = ipproto.ICMPv6
}
}
response := tsFilter.Check(source.Addr, destination.Addr, destination.Port, ipProto)
switch response {
case filter.Drop:
return nil, syscall.ECONNREFUSED
case filter.DropSilently:
return nil, tun.ErrDrop
}
}
var ipVersion uint8
if !destination.IsIPv6() {
ipVersion = 4
} else {
ipVersion = 6
}
routeDestination, err := t.router.PreMatch(adapter.InboundContext{
Inbound: t.Tag(),
InboundType: t.Type(),
IPVersion: ipVersion,
Network: network,
Source: source,
Destination: destination,
}, routeContext, timeout, false)
if err != nil {
switch {
case R.IsBypassed(err):
err = nil
case R.IsRejected(err):
t.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())
default:
if network == N.NetworkICMP {
t.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()))
}
}
}
return routeDestination, err
}
func (t *Endpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
var metadata adapter.InboundContext
metadata.Inbound = t.Tag()
@@ -872,41 +825,7 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}
func (t *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
if !t.started.Load() {
return nil, E.New("Tailscale is not ready yet")
}
ctx := log.ContextWithNewID(t.ctx)
var destination tun.DirectRouteDestination
var err error
if t.systemDialer != nil {
destination, err = ping.ConnectDestination(
ctx, t.logger,
t.systemDialer.DialerForICMPDestination(metadata.Destination.Addr).Control,
metadata.Destination.Addr, routeContext, timeout,
)
} else {
inet4Address, inet6Address := t.server.TailscaleIPs()
if metadata.Destination.Addr.Is4() && !inet4Address.IsValid() || metadata.Destination.Addr.Is6() && !inet6Address.IsValid() {
return nil, E.New("Tailscale is not ready yet")
}
destination, err = ping.ConnectGVisor(
ctx, t.logger,
metadata.Source.Addr, metadata.Destination.Addr,
routeContext,
t.stack,
inet4Address, inet6Address,
timeout,
)
}
if err != nil {
return nil, err
}
t.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString())
return destination, nil
}
func (t *Endpoint) PreferredDomain(domain string) bool {
func (t *Endpoint) PreferredDomain(metadata *adapter.InboundContext, domain string) bool {
routeDomains := t.routeDomains.Load()
if routeDomains == nil {
return false
@@ -914,7 +833,7 @@ func (t *Endpoint) PreferredDomain(domain string) bool {
return routeDomains[strings.ToLower(domain)]
}
func (t *Endpoint) PreferredAddress(address netip.Addr) bool {
func (t *Endpoint) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool {
routePrefixes := t.routePrefixes.Load()
if routePrefixes == nil {
return false
@@ -933,15 +852,6 @@ func (t *Endpoint) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCf
if (t.cfg != nil && reflect.DeepEqual(t.cfg, cfg)) && (t.dnsCfg != nil && reflect.DeepEqual(t.dnsCfg, dnsCfg)) {
return
}
var inet4Address, inet6Address netip.Addr
for _, address := range cfg.Addresses {
if address.Addr().Is4() {
inet4Address = address.Addr()
} else if address.Addr().Is6() {
inet6Address = address.Addr()
}
}
t.icmpForwarder.SetLocalAddresses(inet4Address, inet6Address)
t.cfg = cfg
t.dnsCfg = dnsCfg
+127
View File
@@ -0,0 +1,127 @@
//go:build with_gvisor
package tailscale
import (
"net/netip"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
E "github.com/sagernet/sing/common/exceptions"
tsTUN "github.com/sagernet/tailscale/net/tstun"
"github.com/sagernet/tailscale/types/ipproto"
"github.com/sagernet/tailscale/wgengine/filter"
)
func (t *Endpoint) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction {
return adapter.PreMatchFlow
}
func (t *Endpoint) PortAddresses() (netip.Addr, netip.Addr) {
if !t.started.Load() {
return netip.Addr{}, netip.Addr{}
}
return t.server.TailscaleIPs()
}
func (t *Endpoint) PortMTU() uint32 {
if t.systemInterface {
return t.systemInterfaceMTU
}
return uint32(tsTUN.DefaultTUNMTU())
}
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 {
return tun.FlowVerdict{Action: tun.ActionAccept}
}
if t.filter != nil {
tsFilter := t.filter.Load()
if tsFilter != nil {
var (
ipProto ipproto.Proto
destinationPort uint16
)
switch network {
case uint8(header.TCPProtocolNumber):
ipProto = ipproto.TCP
destinationPort = destination.Port()
case uint8(header.UDPProtocolNumber):
ipProto = ipproto.UDP
destinationPort = destination.Port()
case uint8(header.ICMPv4ProtocolNumber):
ipProto = ipproto.ICMPv4
case uint8(header.ICMPv6ProtocolNumber):
ipProto = ipproto.ICMPv6
}
switch tsFilter.Check(source.Addr(), destination.Addr(), destinationPort, ipProto) {
case filter.Drop:
return tun.FlowVerdict{Action: tun.ActionReject}
case filter.DropSilently:
return tun.FlowVerdict{Action: tun.ActionDrop}
}
}
}
return adapter.JudgeFlow(t.router, t.Tag(), t.Type(), network, source, destination, firstPacket)
}
func (t *Endpoint) AttachReturn(returnPath tun.Return) error {
t.returnAccess.Lock()
defer t.returnAccess.Unlock()
if t.returnPath == returnPath {
return nil
}
if t.returnPath != nil {
return E.New("return path already attached")
}
err := t.wgEngine.SetReturnPath(returnPath)
if err != nil {
return err
}
t.returnPath = returnPath
return nil
}
func (t *Endpoint) DetachReturn(returnPath tun.Return) error {
t.returnAccess.Lock()
defer t.returnAccess.Unlock()
if t.returnPath == returnPath {
t.returnPath = nil
}
return nil
}
func (t *Endpoint) WritePackets(packets [][]byte) error {
if !t.started.Load() {
return E.New("Tailscale is not ready yet")
}
unmatched, err := t.wgEngine.InputPackets(packets)
if err != nil || len(unmatched) == 0 {
return err
}
t.returnAccess.Lock()
returnPath := t.returnPath
t.returnAccess.Unlock()
if returnPath == nil {
return nil
}
headroom := returnPath.ReturnHeadroom()
inet4Address, inet6Address := t.PortAddresses()
var replies [][]byte
for _, packet := range unmatched {
source := inet4Address
if header.IPVersion(packet) == header.IPv6Version {
source = inet6Address
}
reply, replyOk := tun.BuildUnreachable(packet, source, headroom)
if replyOk {
replies = append(replies, reply)
}
}
if len(replies) > 0 {
returnPath.ReturnPackets(replies)
}
return nil
}
+39 -62
View File
@@ -6,6 +6,7 @@ import (
"net/netip"
"os"
"runtime"
"slices"
"strconv"
"strings"
"time"
@@ -16,7 +17,6 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
@@ -99,7 +99,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
tunMTU := options.MTU
enableGSO := C.IsLinux && options.Stack == "gvisor" && platformInterface == nil && tunMTU > 0 && tunMTU < 49152
if tunMTU == 0 {
if platformInterface != nil && platformInterface.UnderNetworkExtension() {
// In Network Extension, when MTU exceeds 4064 (4096-UTUN_IF_HEADROOM_SIZE), the performance of tun will drop significantly, which may be a system bug.
@@ -111,6 +110,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
tunMTU = 65535
}
}
var enableGSO bool
if C.IsLinux && platformInterface == nil {
enableGSO = (options.Stack == "gvisor" && tunMTU < 49152)
}
var udpTimeout time.Duration
if options.UDPTimeout != 0 {
udpTimeout = time.Duration(options.UDPTimeout)
@@ -178,7 +181,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
excludeMACAddress = append(excludeMACAddress, mac)
}
networkManager := service.FromContext[adapter.NetworkManager](ctx)
multiPendingPackets := C.IsDarwin && ((options.Stack == "gvisor" && tunMTU < 32768) || (options.Stack != "gvisor" && options.MTU <= 9000))
multiPendingPackets := C.IsDarwin && ((options.Stack == "gvisor" && tunMTU < 32768) || (options.Stack != "gvisor" && tunMTU <= 9000))
inbound := &Inbound{
tag: tag,
ctx: ctx,
@@ -320,6 +323,31 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
t.dnsHijackAddress = append(inet4DNSAddress, inet6DNSAddress...)
}
case adapter.StartStateStart:
if t.platformInterface == nil &&
((C.IsLinux && !t.tunOptions.GSO) || (C.IsDarwin && !t.tunOptions.EXP_MultiPendingPackets)) {
outboundManager := service.FromContext[adapter.OutboundManager](t.ctx)
endpointManager := service.FromContext[adapter.EndpointManager](t.ctx)
for _, outbound := range outboundManager.Outbounds() {
if _, isFlowOutbound := outbound.(adapter.FlowOutbound); isFlowOutbound {
if C.IsLinux {
t.tunOptions.GSO = true
} else {
t.tunOptions.EXP_MultiPendingPackets = true
}
break
}
}
for _, endpoint := range endpointManager.Endpoints() {
if _, isFlowOutbound := endpoint.(adapter.FlowOutbound); isFlowOutbound {
if C.IsLinux {
t.tunOptions.GSO = true
} else {
t.tunOptions.EXP_MultiPendingPackets = true
}
break
}
}
}
if C.IsAndroid && t.platformInterface == nil {
t.tunOptions.BuildAndroidRules(t.networkManager.PackageManager())
}
@@ -460,34 +488,11 @@ func (t *Inbound) Close() error {
)
}
func (t *Inbound) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
var ipVersion uint8
if !destination.IsIPv6() {
ipVersion = 4
} else {
ipVersion = 6
func (t *Inbound) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict {
if slices.Contains(t.dnsHijackAddress, destination.Addr()) {
return tun.FlowVerdict{Action: tun.ActionAccept}
}
routeDestination, err := t.router.PreMatch(adapter.InboundContext{
Inbound: t.tag,
InboundType: C.TypeTun,
IPVersion: ipVersion,
Network: network,
Source: source,
Destination: destination,
}, routeContext, timeout, false)
if err != nil {
switch {
case rule.IsBypassed(err):
err = nil
case rule.IsRejected(err):
t.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())
default:
if network == N.NetworkICMP {
t.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()))
}
}
}
return routeDestination, err
return adapter.JudgeFlow(t.router, t.tag, C.TypeTun, network, source, destination, firstPacket)
}
func (t *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
@@ -497,10 +502,8 @@ func (t *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.S
metadata.InboundType = C.TypeTun
metadata.Source = source
metadata.Destination = destination
for _, dnsHijackAddress := range t.dnsHijackAddress {
if destination.Addr == dnsHijackAddress {
metadata.Protocol = C.ProtocolDNS
}
if slices.Contains(t.dnsHijackAddress, destination.Addr) {
metadata.Protocol = C.ProtocolDNS
}
if metadata.Protocol == C.ProtocolDNS {
t.logger.InfoContext(ctx, "inbound DNS connection from ", metadata.Source)
@@ -534,34 +537,8 @@ func (t *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
type autoRedirectHandler Inbound
func (t *autoRedirectHandler) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
var ipVersion uint8
if !destination.IsIPv6() {
ipVersion = 4
} else {
ipVersion = 6
}
routeDestination, err := t.router.PreMatch(adapter.InboundContext{
Inbound: t.tag,
InboundType: C.TypeTun,
IPVersion: ipVersion,
Network: network,
Source: source,
Destination: destination,
}, routeContext, timeout, true)
if err != nil {
switch {
case rule.IsBypassed(err):
t.logger.Trace("bypass ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())
case rule.IsRejected(err):
t.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())
default:
if network == N.NetworkICMP {
t.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()))
}
}
}
return routeDestination, err
func (t *autoRedirectHandler) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict {
return (*Inbound)(t).JudgeFlow(network, source, destination, firstPacket)
}
func (t *autoRedirectHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
+34 -39
View File
@@ -15,7 +15,6 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing-box/transport/wireguard"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
@@ -340,37 +339,40 @@ func (w *Endpoint) Close() error {
return w.endpoint.Close()
}
func (w *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
if !w.resumeOnDial() { // lx: SPEC 020 — stamp activity + wake if idle-suspended
return nil, E.New("WireGuard is not ready yet")
}
var ipVersion uint8
if !destination.IsIPv6() {
ipVersion = 4
} else {
ipVersion = 6
}
routeDestination, err := w.router.PreMatch(adapter.InboundContext{
Inbound: w.Tag(),
InboundType: w.Type(),
IPVersion: ipVersion,
Network: network,
Source: source,
Destination: destination,
}, routeContext, timeout, false)
if err != nil {
switch {
case rule.IsBypassed(err):
err = nil
case rule.IsRejected(err):
w.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())
default:
if network == N.NetworkICMP {
w.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()))
}
func (w *Endpoint) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction {
return adapter.PreMatchFlow
}
func (w *Endpoint) PortAddresses() (netip.Addr, netip.Addr) {
return w.endpoint.PortAddresses()
}
func (w *Endpoint) PortMTU() uint32 {
return w.endpoint.PortMTU()
}
func (w *Endpoint) AttachReturn(returnPath tun.Return) error {
return w.endpoint.AttachReturn(returnPath)
}
func (w *Endpoint) DetachReturn(returnPath tun.Return) error {
return w.endpoint.DetachReturn(returnPath)
}
func (w *Endpoint) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict {
for _, localPrefix := range w.localAddresses {
if localPrefix.Contains(destination.Addr()) {
return tun.FlowVerdict{Action: tun.ActionAccept}
}
}
return routeDestination, err
return adapter.JudgeFlow(w.router, w.Tag(), w.Type(), network, source, destination, firstPacket)
}
func (w *Endpoint) WritePackets(packets [][]byte) error {
if !w.resumeOnDial() { // lx: SPEC 020 — stamp activity + wake if idle-suspended; L3-forward path (established flows transit here, bypassing DialContext)
return E.New("WireGuard is not ready yet")
}
return w.endpoint.WritePackets(packets)
}
func (w *Endpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
@@ -472,20 +474,13 @@ func (w *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
return packetConn, nil
}
func (w *Endpoint) PreferredDomain(domain string) bool {
func (w *Endpoint) PreferredDomain(metadata *adapter.InboundContext, domain string) bool {
return false
}
func (w *Endpoint) PreferredAddress(address netip.Addr) bool {
func (w *Endpoint) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool {
if !w.started.Load() {
return false
}
return w.endpoint.Lookup(address) != nil
}
func (w *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
if !w.resumeOnDial() { // lx: SPEC 020 — stamp activity + wake if idle-suspended
return nil, E.New("WireGuard is not ready yet")
}
return w.endpoint.NewDirectRouteConnection(metadata, routeContext, timeout)
}
+101
View File
@@ -0,0 +1,101 @@
package route
import (
"context"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/byteformats"
N "github.com/sagernet/sing/common/network"
)
var (
_ tun.FlowTracker = (*flowLogger)(nil)
_ tun.FlowTracker = (multiFlowTracker)(nil)
)
type flowLogger struct {
ctx context.Context
logger log.ContextLogger
network string
source string
destination string
outbound adapter.Outbound
createdAt time.Time
upload atomic.Int64
download atomic.Int64
}
func newFlowLogger(ctx context.Context, logger log.ContextLogger, metadata adapter.InboundContext, outbound adapter.Outbound) *flowLogger {
var source, destination string
if metadata.Network == N.NetworkICMP {
source = metadata.Source.AddrString()
destination = metadata.Destination.AddrString()
} else {
source = metadata.Source.String()
destination = metadata.Destination.String()
}
return &flowLogger{
ctx: ctx,
logger: logger,
network: metadata.Network,
source: source,
destination: destination,
outbound: outbound,
}
}
func (l *flowLogger) AttachFlow(handle tun.FlowHandle) {
l.createdAt = time.Now()
}
func (l *flowLogger) CountForward(n int) {
l.upload.Add(int64(n))
}
func (l *flowLogger) CountReverse(n int) {
l.download.Add(int64(n))
}
func (l *flowLogger) FlowEstablished() {
}
func (l *flowLogger) CloseFlow(reason tun.FlowCloseReason) {
l.logger.DebugContext(l.ctx, "flow closed: ", reason,
", upload ", byteformats.FormatBytes(uint64(l.upload.Load())), ", download ", byteformats.FormatBytes(uint64(l.download.Load())))
}
type multiFlowTracker []tun.FlowTracker
func (t multiFlowTracker) AttachFlow(handle tun.FlowHandle) {
for _, tracker := range t {
tracker.AttachFlow(handle)
}
}
func (t multiFlowTracker) CountForward(n int) {
for _, tracker := range t {
tracker.CountForward(n)
}
}
func (t multiFlowTracker) CountReverse(n int) {
for _, tracker := range t {
tracker.CountReverse(n)
}
}
func (t multiFlowTracker) FlowEstablished() {
for _, tracker := range t {
tracker.FlowEstablished()
}
}
func (t multiFlowTracker) CloseFlow(reason tun.FlowCloseReason) {
for _, tracker := range t {
tracker.CloseFlow(reason)
}
}
+229 -141
View File
@@ -11,10 +11,10 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
R "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing-mux"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/ping"
"github.com/sagernet/sing-vmess"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
@@ -29,6 +29,16 @@ import (
"golang.org/x/exp/slices"
)
var defaultPacketSniffers = []sniff.PacketSniffer{
sniff.DomainNameQuery,
sniff.QUICClientHello,
sniff.STUNMessage,
sniff.UTP,
sniff.UDPTracker,
sniff.DTLSRecord,
sniff.NTP,
}
// Deprecated: use RouteConnectionEx instead.
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
done := make(chan any)
@@ -101,7 +111,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
if deadline.NeedAdditionalReadDeadline(conn) {
conn = deadline.NewConn(conn)
}
selectedRule, _, buffers, _, err := r.matchRule(ctx, &metadata, false, false, conn, nil)
selectedRule, _, buffers, _, err := r.matchRule(ctx, &metadata, conn, nil)
if err != nil {
return err
}
@@ -235,7 +245,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
r.searchProcessInfo(ctx, &metadata)
return r.hijackDNSPacket(ctx, conn, nil, metadata, onClose)
}
selectedRule, _, _, packetBuffers, err := r.matchRule(ctx, &metadata, false, false, nil, conn)
selectedRule, _, _, packetBuffers, err := r.matchRule(ctx, &metadata, nil, conn)
if err != nil {
return err
}
@@ -304,119 +314,234 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
return nil
}
func (r *Router) PreMatch(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration, supportBypass bool) (tun.DirectRouteDestination, error) {
selectedRule, _, _, _, err := r.matchRule(r.ctx, &metadata, true, supportBypass, nil, nil)
if err != nil {
return nil, err
func (r *Router) PreMatch(metadata adapter.InboundContext, firstPacket []byte) adapter.PreMatchResult {
ctx := log.ContextWithNewID(r.ctx)
metadata.PreMatch = true
continueResult := adapter.PreMatchResult{Action: adapter.PreMatchContinue}
packetDestination := metadata.Destination
if metadata.Destination.Addr.IsValid() && r.dnsTransport.FakeIP() != nil && r.dnsTransport.FakeIP().Store().Contains(metadata.Destination.Addr) {
domain, loaded := r.dnsTransport.FakeIP().Store().Lookup(metadata.Destination.Addr)
if !loaded || domain == "" {
return continueResult
}
metadata.OriginDestination = metadata.Destination
metadata.Destination = M.Socksaddr{
Fqdn: domain,
Port: metadata.Destination.Port,
}
metadata.FakeIP = true
}
var directRouteOutbound adapter.DirectRouteOutbound
if selectedRule != nil {
switch action := selectedRule.Action().(type) {
case *R.RuleActionReject:
switch metadata.Network {
case N.NetworkTCP:
if action.Method == C.RuleActionRejectMethodReply {
return nil, E.New("reject method `reply` is not supported for TCP connections")
if metadata.Destination.IsIPv4() {
metadata.IPVersion = 4
} else if metadata.Destination.IsIPv6() {
metadata.IPVersion = 6
}
for currentRuleIndex, currentRule := range r.rules {
metadata.ResetRuleCache()
if !currentRule.Match(&metadata) {
continue
}
ruleDescription := currentRule.String()
if ruleDescription != "" {
r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action())
} else {
r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] => ", currentRule.Action())
}
switch action := currentRule.Action().(type) {
case *R.RuleActionSniff:
if metadata.Network == N.NetworkICMP {
continue
}
if metadata.Network != N.NetworkUDP || len(firstPacket) == 0 {
return continueResult
}
if sniff.Skip(&metadata) || metadata.Protocol != "" {
continue
}
if len(action.PacketSniffers) == 0 && len(action.StreamSniffers) > 0 {
continue
}
if slices.Equal(metadata.SnifferNames, action.SnifferNames) && metadata.SniffError != nil {
continue
}
packetSniffers := action.PacketSniffers
if len(packetSniffers) == 0 {
packetSniffers = defaultPacketSniffers
}
sniffErr := sniff.PeekPacket(ctx, &metadata, firstPacket, packetSniffers...)
metadata.SnifferNames = action.SnifferNames
metadata.SniffError = sniffErr
if sniffErr != nil {
if errors.Is(sniffErr, sniff.ErrNeedMoreData) {
return continueResult
}
case N.NetworkUDP:
if action.Method == C.RuleActionRejectMethodReply {
return nil, E.New("reject method `reply` is not supported for UDP connections")
continue
}
//goland:noinspection GoDeprecation
if action.OverrideDestination && M.IsDomainName(metadata.Domain) {
metadata.Destination = M.Socksaddr{
Fqdn: metadata.Domain,
Port: metadata.Destination.Port,
}
}
return nil, action.Error(context.Background())
case *R.RuleActionBypass:
if supportBypass {
return nil, &R.BypassedError{Cause: tun.ErrBypass}
if metadata.Domain != "" && metadata.Client != "" {
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client)
} else if metadata.Domain != "" {
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain)
} else if metadata.Client != "" {
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", client: ", metadata.Client)
} else {
r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol)
}
if routeContext == nil {
return nil, nil
}
outbound, loaded := r.outbound.Outbound(action.Outbound)
if !loaded {
return nil, E.New("outbound not found: ", action.Outbound)
}
if !common.Contains(outbound.Network(), metadata.Network) {
return nil, E.New(metadata.Network, " is not supported by outbound: ", action.Outbound)
}
directRouteOutbound = outbound.(adapter.DirectRouteOutbound)
case *R.RuleActionRouteOptions:
applyRouteOptionsOverride(&metadata, action)
case *R.RuleActionRoute:
if routeContext == nil {
return nil, nil
applyRouteOptionsOverride(&metadata, &action.RuleActionRouteOptions)
return r.preMatchFlow(ctx, &metadata, packetDestination, currentRule, action.Outbound)
case *R.RuleActionBypass:
applyRouteOptionsOverride(&metadata, &action.RuleActionRouteOptions)
if action.Outbound == "" {
if metadata.Destination.IsDomain() || metadata.Destination != packetDestination {
return continueResult
}
return adapter.PreMatchResult{Action: adapter.PreMatchBypass}
}
outbound, loaded := r.outbound.Outbound(action.Outbound)
if !loaded {
return nil, E.New("outbound not found: ", action.Outbound)
return r.preMatchFlow(ctx, &metadata, packetDestination, currentRule, action.Outbound)
case *R.RuleActionReject:
rejectErr := action.Error(r.ctx)
if errors.Is(rejectErr, R.ErrDrop) {
return adapter.PreMatchResult{Action: adapter.PreMatchDrop}
}
if !common.Contains(outbound.Network(), metadata.Network) {
return nil, E.New(metadata.Network, " is not supported by outbound: ", action.Outbound)
return adapter.PreMatchResult{Action: adapter.PreMatchReject}
case *R.RuleActionResolve:
resolveErr := r.actionResolve(adapter.WithContext(ctx, &metadata), &metadata, action)
if resolveErr != nil {
r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] ", currentRule, " => ", action, ": ", resolveErr)
return adapter.PreMatchResult{Action: adapter.PreMatchReject}
}
directRouteOutbound = outbound.(adapter.DirectRouteOutbound)
default:
return continueResult
}
}
if directRouteOutbound == nil {
if selectedRule != nil || metadata.Network != N.NetworkICMP {
return nil, nil
return r.preMatchFlow(ctx, &metadata, packetDestination, nil, "")
}
func applyRouteOptionsOverride(metadata *adapter.InboundContext, routeOptions *R.RuleActionRouteOptions) {
if routeOptions.OverrideAddress.IsValid() {
metadata.Destination = M.Socksaddr{
Addr: routeOptions.OverrideAddress.Addr,
Port: metadata.Destination.Port,
Fqdn: routeOptions.OverrideAddress.Fqdn,
}
defaultOutbound := r.outbound.Default()
if !common.Contains(defaultOutbound.Network(), metadata.Network) {
return nil, E.New(metadata.Network, " is not supported by default outbound: ", defaultOutbound.Tag())
}
if routeOptions.OverridePort > 0 {
metadata.Destination = M.Socksaddr{
Addr: metadata.Destination.Addr,
Port: routeOptions.OverridePort,
Fqdn: metadata.Destination.Fqdn,
}
}
if routeOptions.UDPTimeout > 0 {
metadata.UDPTimeout = routeOptions.UDPTimeout
}
}
func (r *Router) preMatchFlow(ctx context.Context, metadata *adapter.InboundContext, packetDestination M.Socksaddr, matchedRule adapter.Rule, outboundTag string) adapter.PreMatchResult {
continueResult := adapter.PreMatchResult{Action: adapter.PreMatchContinue}
var outbound adapter.Outbound
if outboundTag == "" {
outbound = r.outbound.Default()
} else {
var loaded bool
outbound, loaded = r.outbound.Outbound(outboundTag)
if !loaded {
return continueResult
}
}
for range 8 {
group, isGroup := outbound.(adapter.OutboundGroup)
if !isGroup {
break
}
selectedOutbound, selectedLoaded := r.outbound.Outbound(group.Now())
if !selectedLoaded {
return continueResult
}
outbound = selectedOutbound
}
if !common.Contains(outbound.Network(), metadata.Network) {
return continueResult
}
flowOutbound, isFlowOutbound := outbound.(adapter.FlowOutbound)
if !isFlowOutbound {
return continueResult
}
flowAction := flowOutbound.PreMatchFlow(metadata.Network, metadata.Destination.Addr)
if flowAction != adapter.PreMatchFlow {
return adapter.PreMatchResult{Action: flowAction, Outbound: outbound}
}
result := adapter.PreMatchResult{Action: adapter.PreMatchFlow, Outbound: outbound}
if metadata.Network == N.NetworkUDP {
if metadata.UDPTimeout > 0 {
result.UDPTimeout = metadata.UDPTimeout
} else {
protocol := metadata.Protocol
if protocol == "" {
protocol = C.PortProtocols[metadata.Destination.Port]
}
if protocol != "" {
result.UDPTimeout = C.ProtocolTimeouts[protocol]
}
}
directRouteOutbound = defaultOutbound.(adapter.DirectRouteOutbound)
}
if metadata.Destination.IsDomain() {
if len(metadata.DestinationAddresses) == 0 {
var strategy C.DomainStrategy
if metadata.Source.IsIPv4() {
strategy = C.DomainStrategyIPv4Only
} else {
strategy = C.DomainStrategyIPv6Only
}
err = r.actionResolve(r.ctx, &metadata, &R.RuleActionResolve{
Strategy: strategy,
})
if err != nil {
return nil, err
}
if !metadata.FakeIP {
return continueResult
}
var newDestination netip.Addr
if metadata.Source.IsIPv4() {
for _, address := range metadata.DestinationAddresses {
if address.Is4() {
newDestination = address
break
}
}
} else {
for _, address := range metadata.DestinationAddresses {
if address.Is6() {
newDestination = address
break
}
for _, address := range metadata.DestinationAddresses {
if address.Is4() == packetDestination.IsIPv4() {
newDestination = address
break
}
}
if !newDestination.IsValid() {
if metadata.Source.IsIPv4() {
return nil, E.New("no IPv4 address found for domain: ", metadata.Destination.Fqdn)
if len(metadata.DestinationAddresses) == 0 {
r.logger.WarnContext(ctx, "pre-match: reject ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to fake destination ", metadata.Destination.Fqdn, ": a resolve action is required before routing to outbound/", outbound.Type(), "[", outbound.Tag(), "]")
} else {
return nil, E.New("no IPv6 address found for domain: ", metadata.Destination.Fqdn)
r.logger.DebugContext(ctx, "pre-match: reject ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to fake destination ", metadata.Destination.Fqdn, ": no resolved address for this address family")
}
return adapter.PreMatchResult{Action: adapter.PreMatchReject}
}
flowAction = flowOutbound.PreMatchFlow(metadata.Network, newDestination)
if flowAction != adapter.PreMatchFlow {
return adapter.PreMatchResult{Action: flowAction, Outbound: outbound}
}
result.Destination = netip.AddrPortFrom(newDestination, metadata.Destination.Port)
} else if metadata.Destination != packetDestination {
result.Destination = metadata.Destination.AddrPort()
}
r.logger.InfoContext(ctx, "pre-match: forward ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString(), " via outbound/", outbound.Type(), "[", outbound.Tag(), "]")
metadataCopy := *metadata
result.NewTracker = func() tun.FlowTracker {
flowTrackers := make([]tun.FlowTracker, 0, len(r.trackers)+1)
flowTrackers = append(flowTrackers, newFlowLogger(ctx, r.logger, metadataCopy, outbound))
for _, tracker := range r.trackers {
flowTracker := tracker.RoutedFlow(ctx, metadataCopy, matchedRule, outbound)
if flowTracker != nil {
flowTrackers = append(flowTrackers, flowTracker)
}
}
metadata.Destination = M.Socksaddr{
Addr: newDestination,
if len(flowTrackers) == 1 {
return flowTrackers[0]
}
routeContext = ping.NewContextDestinationWriter(routeContext, metadata.OriginDestination.Addr)
var routeDestination tun.DirectRouteDestination
routeDestination, err = directRouteOutbound.NewDirectRouteConnection(metadata, routeContext, timeout)
if err != nil {
return nil, err
}
return ping.NewDestinationWriter(routeDestination, newDestination), nil
return multiFlowTracker(flowTrackers)
}
return directRouteOutbound.NewDirectRouteConnection(metadata, routeContext, timeout)
return result
}
func (r *Router) matchRule(
ctx context.Context, metadata *adapter.InboundContext, preMatch bool, supportBypass bool,
ctx context.Context, metadata *adapter.InboundContext,
inputConn net.Conn, inputPacketConn N.PacketConn,
) (
selectedRule adapter.Rule, selectedRuleIndex int,
@@ -474,23 +599,11 @@ match:
if !currentRule.Match(metadata) {
continue
}
if !preMatch {
ruleDescription := currentRule.String()
if ruleDescription != "" {
r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action())
} else {
r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] => ", currentRule.Action())
}
ruleDescription := currentRule.String()
if ruleDescription != "" {
r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action())
} else {
switch currentRule.Action().Type() {
case C.RuleActionTypeReject:
ruleDescription := currentRule.String()
if ruleDescription != "" {
r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action())
} else {
r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] => ", currentRule.Action())
}
}
r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] => ", currentRule.Action())
}
var routeOptions *R.RuleActionRouteOptions
switch action := currentRule.Action().(type) {
@@ -509,20 +622,9 @@ match:
metadata.RouteOriginalDestination = metadata.Destination
}
if routeOptions.OverrideAddress.IsValid() {
metadata.Destination = M.Socksaddr{
Addr: routeOptions.OverrideAddress.Addr,
Port: metadata.Destination.Port,
Fqdn: routeOptions.OverrideAddress.Fqdn,
}
metadata.DestinationAddresses = nil
}
if routeOptions.OverridePort > 0 {
metadata.Destination = M.Socksaddr{
Addr: metadata.Destination.Addr,
Port: routeOptions.OverridePort,
Fqdn: metadata.Destination.Fqdn,
}
}
applyRouteOptionsOverride(metadata, routeOptions)
if routeOptions.NetworkStrategy != nil {
metadata.NetworkStrategy = routeOptions.NetworkStrategy
}
@@ -558,21 +660,15 @@ match:
}
switch action := currentRule.Action().(type) {
case *R.RuleActionSniff:
if !preMatch {
newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn, buffers, packetBuffers)
if newBuffer != nil {
buffers = append(buffers, newBuffer)
} else if len(newPacketBuffers) > 0 {
packetBuffers = append(packetBuffers, newPacketBuffers...)
}
if newErr != nil {
fatalErr = newErr
return
}
} else if metadata.Network != N.NetworkICMP {
selectedRule = currentRule
selectedRuleIndex = currentRuleIndex
break match
newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn, buffers, packetBuffers)
if newBuffer != nil {
buffers = append(buffers, newBuffer)
} else if len(newPacketBuffers) > 0 {
packetBuffers = append(packetBuffers, newPacketBuffers...)
}
if newErr != nil {
fatalErr = newErr
return
}
case *R.RuleActionResolve:
fatalErr = r.actionResolve(ctx, metadata, action)
@@ -590,7 +686,7 @@ match:
}
if actionType == C.RuleActionTypeBypass {
bypassAction := currentRule.Action().(*R.RuleActionBypass)
if !supportBypass && bypassAction.Outbound == "" {
if bypassAction.Outbound == "" {
continue match
}
selectedRule = currentRule
@@ -679,15 +775,7 @@ func (r *Router) actionSniff(
if len(action.PacketSniffers) > 0 {
packetSniffers = action.PacketSniffers
} else {
packetSniffers = []sniff.PacketSniffer{
sniff.DomainNameQuery,
sniff.QUICClientHello,
sniff.STUNMessage,
sniff.UTP,
sniff.UDPTracker,
sniff.DTLSRecord,
sniff.NTP,
}
packetSniffers = defaultPacketSniffers
}
var err error
for _, packetBuffer := range inputPacketBuffers {
+8 -4
View File
@@ -14,7 +14,6 @@ import (
"github.com/sagernet/sing-box/common/tlsspoof"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
@@ -388,6 +387,11 @@ func (r *RuleActionDirect) String() string {
return "direct" + r.description
}
var (
ErrReset = E.New("connection reset")
ErrDrop = E.New("packet dropped")
)
type RejectedError struct {
Cause error
}
@@ -445,9 +449,9 @@ func (r *RuleActionReject) Error(ctx context.Context) error {
var returnErr error
switch r.Method {
case C.RuleActionRejectMethodDefault:
returnErr = &RejectedError{tun.ErrReset}
returnErr = &RejectedError{ErrReset}
case C.RuleActionRejectMethodDrop:
return &RejectedError{tun.ErrDrop}
return &RejectedError{ErrDrop}
case C.RuleActionRejectMethodReply:
return nil
default:
@@ -467,7 +471,7 @@ func (r *RuleActionReject) Error(ctx context.Context) error {
if ctx != nil {
r.logger.DebugContext(ctx, "dropped due to flooding")
}
return &RejectedError{tun.ErrDrop}
return &RejectedError{ErrDrop}
}
return returnErr
}
+3 -3
View File
@@ -50,14 +50,14 @@ func (r *PreferredByItem) Match(metadata *adapter.InboundContext) bool {
}
if domainHost != "" {
for _, outbound := range r.outbounds {
if outbound.PreferredDomain(domainHost) {
if outbound.PreferredDomain(metadata, domainHost) {
return true
}
}
}
if metadata.Destination.IsIP() {
for _, outbound := range r.outbounds {
if outbound.PreferredAddress(metadata.Destination.Addr) {
if outbound.PreferredAddress(metadata, metadata.Destination.Addr) {
return true
}
}
@@ -65,7 +65,7 @@ func (r *PreferredByItem) Match(metadata *adapter.InboundContext) bool {
if len(metadata.DestinationAddresses) > 0 {
for _, address := range metadata.DestinationAddresses {
for _, outbound := range r.outbounds {
if outbound.PreferredAddress(address) {
if outbound.PreferredAddress(metadata, address) {
return true
}
}
+2 -4
View File
@@ -5,10 +5,8 @@ import (
"net"
"sync/atomic"
"testing"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
@@ -22,8 +20,8 @@ type ruleSetItemTestRouter struct {
func (r *ruleSetItemTestRouter) Start(adapter.StartStage) error { return nil }
func (r *ruleSetItemTestRouter) Close() error { return nil }
func (r *ruleSetItemTestRouter) PreMatch(adapter.InboundContext, tun.DirectRouteContext, time.Duration, bool) (tun.DirectRouteDestination, error) {
return nil, nil
func (r *ruleSetItemTestRouter) PreMatch(adapter.InboundContext, []byte) adapter.PreMatchResult {
return adapter.PreMatchResult{}
}
func (r *ruleSetItemTestRouter) RouteConnection(context.Context, net.Conn, adapter.InboundContext) error {
+7 -6
View File
@@ -11,7 +11,7 @@ require (
github.com/docker/go-connections v0.5.0
github.com/gofrs/uuid/v5 v5.4.0
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
github.com/sagernet/sing v0.8.12-0.20260701111927-87e1e819f10a
github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d
github.com/sagernet/sing-quic v0.6.2-0.20260525051024-9467ede27fb7
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
@@ -139,16 +139,17 @@ require (
github.com/sagernet/gliderssh v0.3.4-0.20260531100337-2194faca5648 // indirect
github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-mod.2 // indirect
github.com/sagernet/sing-cloudflared v0.1.1 // indirect
github.com/sagernet/nftables v0.3.0-mod.3 // indirect
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 // indirect
github.com/sagernet/sing-mux v0.3.5 // indirect
github.com/sagernet/sing-shadowtls v0.2.1 // indirect
github.com/sagernet/sing-tun v0.8.12-0.20260629021427-b3c6babbd353 // indirect
github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814 // indirect
github.com/sagernet/sing-tun v0.8.12-0.20260708091449-be1a05a4c962 // indirect
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb // indirect
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 // indirect
github.com/sagernet/smux v1.5.50-sing-box-mod.1 // indirect
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260527101438-dc40932c32d9 // indirect
github.com/sagernet/wireguard-go v0.0.3 // indirect
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260706062137-ae2dde1295a3 // indirect
github.com/sagernet/wireguard-go v0.0.5-0.20260706153856-2c27bbf4f97f // indirect
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
+14 -12
View File
@@ -273,14 +273,14 @@ github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 h1:SUPFNB+vSP4RBPr
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-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje+vW5Q0OQ=
github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ=
github.com/sagernet/nftables v0.3.0-mod.3 h1:CVfbVTd3Z/LQVc1Z3c1hpiriplJ4xDVHjfQCETiN9RA=
github.com/sagernet/nftables v0.3.0-mod.3/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
github.com/sagernet/sing v0.8.12-0.20260701111927-87e1e819f10a h1:MCid6UN8a7WZWziqlWbLRFOt2jsfNZ7HRYEbP+MxX0U=
github.com/sagernet/sing v0.8.12-0.20260701111927-87e1e819f10a/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
github.com/sagernet/sing-cloudflared v0.1.1 h1:By29ZWMJl8QU6UcC5pmBv803rYigAoSmzhDFOZc3h18=
github.com/sagernet/sing-cloudflared v0.1.1/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y=
github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d h1:BhsQU0Iug1tU4xR52cjm8Sc+LBo+KwdyLTRn3ie9moo=
github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8=
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg=
github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI=
github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-quic v0.6.2-0.20260525051024-9467ede27fb7 h1:hFLPJ21uNZSbRnzhOKz4Zv0b4F93mpDorWyN93BeRcM=
@@ -291,18 +291,20 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.1 h1:ZiHZdnEnP+YS73NMsxiZmIFCwNd0M4k7PkGCKNXhbaM=
github.com/sagernet/sing-shadowtls v0.2.1/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.8.12-0.20260629021427-b3c6babbd353 h1:HA0TGrBQSFfvcoVXL1DxzF+i8pmaGOGb33jdReB7L4s=
github.com/sagernet/sing-tun v0.8.12-0.20260629021427-b3c6babbd353/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ=
github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814 h1:xfnkRpjVRVeJhVvDZA8PzTLlKGTb1o2kdI4uv1YymXo=
github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814/go.mod h1:PcwzX/Xvqky0EP3kGt8OCjYb3R1pydenPHNQZcPZmXY=
github.com/sagernet/sing-tun v0.8.12-0.20260708091449-be1a05a4c962 h1:dmJoWdTQygt4P2rAwScy2IvHnFp1mKrW6OsI0qig6O8=
github.com/sagernet/sing-tun v0.8.12-0.20260708091449-be1a05a4c962/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ=
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb h1:KEMbfexD4DvrQGYWwx6r+AwH9Veh8z6cnBZmtCS2G+0=
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb/go.mod h1:D4CnJX3MNAAANhbQUxfIRgBdnvlTEaV7h6ojedcs+pw=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260527101438-dc40932c32d9 h1:jOkKeYI0A0M+jVEu2omQLId4q5GVP7G8FSZh1eUArIk=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260527101438-dc40932c32d9/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc=
github.com/sagernet/wireguard-go v0.0.3 h1:6ebmwj/SFQRnYv6/nRCnwUzf+KFepF8tIBd57IAq1jE=
github.com/sagernet/wireguard-go v0.0.3/go.mod h1:hEqi4y5czEg6LYtX2Bpjg+lV0b/J1n+5rA885Z66Mx0=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260706062137-ae2dde1295a3 h1:eczvica8YiS5j3GfpHg6JG1Icur4Z2D6ffSrLZTfD1E=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260706062137-ae2dde1295a3/go.mod h1:p8Ms8FbGlwQJyHb862XmdShTS50fFJ8C71VdO6xvWyk=
github.com/sagernet/wireguard-go v0.0.5-0.20260706153856-2c27bbf4f97f h1:TzN97RL07xWb3gZtmqFhsdkud4f6G/pohiaOLiqSBj4=
github.com/sagernet/wireguard-go v0.0.5-0.20260706153856-2c27bbf4f97f/go.mod h1:hEqi4y5czEg6LYtX2Bpjg+lV0b/J1n+5rA885Z66Mx0=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
-6
View File
@@ -5,7 +5,6 @@ import (
"net/netip"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
@@ -45,8 +44,3 @@ func NewDevice(options DeviceOptions) (Device, error) {
return newSystemStackDevice(options)
}
}
type NatDevice interface {
Device
CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
}
-103
View File
@@ -1,103 +0,0 @@
package wireguard
import (
"context"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/ping"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/logger"
)
var _ Device = (*natDeviceWrapper)(nil)
type natDeviceWrapper struct {
Device
ctx context.Context
logger logger.ContextLogger
packetOutbound chan *buf.Buffer
rewriter *ping.SourceRewriter
buffer [][]byte
}
func NewNATDevice(ctx context.Context, logger logger.ContextLogger, upstream Device) NatDevice {
wrapper := &natDeviceWrapper{
Device: upstream,
ctx: ctx,
logger: logger,
packetOutbound: make(chan *buf.Buffer, 256),
rewriter: ping.NewSourceRewriter(ctx, logger, upstream.Inet4Address(), upstream.Inet6Address()),
}
return wrapper
}
func (d *natDeviceWrapper) Read(bufs [][]byte, sizes []int, offset int) (n int, err error) {
select {
case packet := <-d.packetOutbound:
defer packet.Release()
sizes[0] = copy(bufs[0][offset:], packet.Bytes())
return 1, nil
default:
}
return d.Device.Read(bufs, sizes, offset)
}
func (d *natDeviceWrapper) Write(bufs [][]byte, offset int) (int, error) {
for _, buffer := range bufs {
handled, err := d.rewriter.WriteBack(buffer[offset:])
if handled {
if err != nil {
return 0, err
}
} else {
d.buffer = append(d.buffer, buffer)
}
}
if len(d.buffer) > 0 {
_, err := d.Device.Write(d.buffer, offset)
if err != nil {
return 0, err
}
d.buffer = d.buffer[:0]
}
return 0, nil
}
func (d *natDeviceWrapper) CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
ctx := log.ContextWithNewID(d.ctx)
session := tun.DirectRouteSession{
Source: metadata.Source.Addr,
Destination: metadata.Destination.Addr,
}
d.rewriter.CreateSession(session, routeContext)
d.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString())
return &natDestination{device: d, session: session}, nil
}
var _ tun.DirectRouteDestination = (*natDestination)(nil)
type natDestination struct {
device *natDeviceWrapper
session tun.DirectRouteSession
closed atomic.Bool
}
func (d *natDestination) WritePacket(buffer *buf.Buffer) error {
d.device.rewriter.RewritePacket(buffer.Bytes())
d.device.packetOutbound <- buffer
return nil
}
func (d *natDestination) Close() error {
d.closed.Store(true)
d.device.rewriter.DeleteSession(d.session)
return nil
}
func (d *natDestination) IsClosed() bool {
return d.closed.Load()
}
+9 -36
View File
@@ -8,7 +8,6 @@ import (
"net/netip"
"os"
"sync"
"time"
"github.com/sagernet/gvisor/pkg/buffer"
"github.com/sagernet/gvisor/pkg/tcpip"
@@ -20,10 +19,7 @@ import (
"github.com/sagernet/gvisor/pkg/tcpip/transport/icmp"
"github.com/sagernet/gvisor/pkg/tcpip/transport/tcp"
"github.com/sagernet/gvisor/pkg/tcpip/transport/udp"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/ping"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
@@ -32,11 +28,9 @@ import (
wgTun "github.com/sagernet/wireguard-go/tun"
)
var _ NatDevice = (*stackDevice)(nil)
var _ Device = (*stackDevice)(nil)
type stackDevice struct {
ctx context.Context
logger log.ContextLogger
stack *stack.Stack
mtu uint32
events chan wgTun.Event
@@ -47,12 +41,11 @@ type stackDevice struct {
dispatcher stack.NetworkDispatcher
inet4Address netip.Addr
inet6Address netip.Addr
icmpForwarder *tun.ICMPForwarder
}
func newStackDevice(options DeviceOptions) (*stackDevice, error) {
tunDevice := &stackDevice{
ctx: options.Context,
logger: options.Logger,
mtu: options.MTU,
events: make(chan wgTun.Event, 1),
outbound: make(chan *stack.PacketBuffer, 256),
@@ -63,10 +56,6 @@ func newStackDevice(options DeviceOptions) (*stackDevice, error) {
if err != nil {
return nil, err
}
var (
inet4Address netip.Addr
inet6Address netip.Addr
)
for _, prefix := range options.Address {
addr := tun.AddressFromAddr(prefix.Addr())
protoAddr := tcpip.ProtocolAddress{
@@ -76,12 +65,10 @@ func newStackDevice(options DeviceOptions) (*stackDevice, error) {
},
}
if prefix.Addr().Is4() {
inet4Address = prefix.Addr()
tunDevice.inet4Address = inet4Address
tunDevice.inet4Address = prefix.Addr()
protoAddr.Protocol = ipv4.ProtocolNumber
} else {
inet6Address = prefix.Addr()
tunDevice.inet6Address = inet6Address
tunDevice.inet6Address = prefix.Addr()
protoAddr.Protocol = ipv6.ProtocolNumber
}
gErr := ipStack.AddProtocolAddress(tun.DefaultNIC, protoAddr, stack.AddressProperties{})
@@ -93,10 +80,10 @@ func newStackDevice(options DeviceOptions) (*stackDevice, error) {
if options.Handler != nil {
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket)
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket)
icmpForwarder := tun.NewICMPForwarder(options.Context, ipStack, options.Logger, options.Handler, options.ICMPTimeout)
icmpForwarder.SetLocalAddresses(inet4Address, inet6Address)
icmpForwarder := tun.NewICMPForwarder(ipStack, options.Handler, options.Logger)
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket)
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket)
tunDevice.icmpForwarder = icmpForwarder
}
return tunDevice, nil
}
@@ -255,6 +242,9 @@ func (w *stackDevice) Close() error {
w.closeOnce.Do(func() {
close(w.done)
close(w.events)
if w.icmpForwarder != nil {
w.icmpForwarder.Close()
}
w.stack.Close()
for _, endpoint := range w.stack.CleanupEndpoints() {
endpoint.Abort()
@@ -268,23 +258,6 @@ func (w *stackDevice) BatchSize() int {
return 1
}
func (w *stackDevice) CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
ctx := log.ContextWithNewID(w.ctx)
destination, err := ping.ConnectGVisor(
ctx, w.logger,
metadata.Source.Addr, metadata.Destination.Addr,
routeContext,
w.stack,
w.inet4Address, w.inet6Address,
timeout,
)
if err != nil {
return nil, err
}
w.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString())
return destination, nil
}
var _ stack.LinkEndpoint = (*wireEndpoint)(nil)
type wireEndpoint stackDevice
+19 -47
View File
@@ -3,10 +3,8 @@
package wireguard
import (
"context"
"net/netip"
"sync"
"time"
"github.com/sagernet/gvisor/pkg/buffer"
"github.com/sagernet/gvisor/pkg/tcpip"
@@ -17,12 +15,8 @@ import (
"github.com/sagernet/gvisor/pkg/tcpip/transport/icmp"
"github.com/sagernet/gvisor/pkg/tcpip/transport/tcp"
"github.com/sagernet/gvisor/pkg/tcpip/transport/udp"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/ping"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/wireguard-go/device"
)
@@ -30,12 +24,11 @@ var _ Device = (*systemStackDevice)(nil)
type systemStackDevice struct {
*systemDevice
ctx context.Context
logger logger.ContextLogger
stack *stack.Stack
endpoint *deviceEndpoint
writeBufs [][]byte
closeOnce sync.Once
stack *stack.Stack
endpoint *deviceEndpoint
icmpForwarder *tun.ICMPForwarder
writeBufs [][]byte
closeOnce sync.Once
}
func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) {
@@ -51,10 +44,6 @@ func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) {
if err != nil {
return nil, err
}
var (
inet4Address netip.Addr
inet6Address netip.Addr
)
for _, prefix := range options.Address {
addr := tun.AddressFromAddr(prefix.Addr())
protoAddr := tcpip.ProtocolAddress{
@@ -64,10 +53,8 @@ func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) {
},
}
if prefix.Addr().Is4() {
inet4Address = prefix.Addr()
protoAddr.Protocol = ipv4.ProtocolNumber
} else {
inet6Address = prefix.Addr()
protoAddr.Protocol = ipv6.ProtocolNumber
}
gErr := ipStack.AddProtocolAddress(tun.DefaultNIC, protoAddr, stack.AddressProperties{})
@@ -75,21 +62,20 @@ func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) {
return nil, E.New("parse local address ", protoAddr.AddressWithPrefix, ": ", gErr.String())
}
}
if options.Handler != nil {
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket)
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket)
icmpForwarder := tun.NewICMPForwarder(options.Context, ipStack, options.Logger, options.Handler, options.ICMPTimeout)
icmpForwarder.SetLocalAddresses(inet4Address, inet6Address)
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket)
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket)
}
return &systemStackDevice{
ctx: options.Context,
logger: options.Logger,
stackDevice := &systemStackDevice{
systemDevice: system,
stack: ipStack,
endpoint: endpoint,
}, nil
}
if options.Handler != nil {
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket)
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket)
icmpForwarder := tun.NewICMPForwarder(ipStack, options.Handler, options.Logger)
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket)
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket)
stackDevice.icmpForwarder = icmpForwarder
}
return stackDevice, nil
}
func (w *systemStackDevice) SetDevice(device *device.Device) {
@@ -129,6 +115,9 @@ func (w *systemStackDevice) Close() error {
var err error
w.closeOnce.Do(func() {
close(w.endpoint.done)
if w.icmpForwarder != nil {
w.icmpForwarder.Close()
}
w.stack.Close()
for _, endpoint := range w.stack.CleanupEndpoints() {
endpoint.Abort()
@@ -165,23 +154,6 @@ func (w *systemStackDevice) writeStack(packet []byte) bool {
return true
}
func (w *systemStackDevice) CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
ctx := log.ContextWithNewID(w.ctx)
destination, err := ping.ConnectGVisor(
ctx, w.logger,
metadata.Source.Addr, metadata.Destination.Addr,
routeContext,
w.stack,
w.inet4Address, w.inet6Address,
timeout,
)
if err != nil {
return nil, err
}
w.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString())
return destination, nil
}
type deviceEndpoint struct {
mtu uint32
done chan struct{}
+8 -24
View File
@@ -10,12 +10,9 @@ import (
"os"
"reflect"
"strings"
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
@@ -35,7 +32,7 @@ type Endpoint struct {
ipcConf string
allowedAddress []netip.Prefix
tunDevice Device
natDevice NatDevice
returnDevice *returnDeviceWrapper
device *device.Device
allowedIPs *device.AllowedIPs
pause pause.Manager
@@ -161,17 +158,13 @@ func NewEndpoint(options EndpointOptions) (*Endpoint, error) {
if err != nil {
return nil, E.Cause(err, "create WireGuard device")
}
natDevice, isNatDevice := tunDevice.(NatDevice)
if !isNatDevice {
natDevice = NewNATDevice(options.Context, options.Logger, tunDevice)
}
return &Endpoint{
options: options,
peers: peers,
ipcConf: ipcConf,
allowedAddress: allowedAddresses,
tunDevice: tunDevice,
natDevice: natDevice,
returnDevice: &returnDeviceWrapper{Device: tunDevice},
}, nil
}
@@ -198,7 +191,11 @@ func (e *Endpoint) Start(resolve bool) error {
var bind conn.Bind
wgListener, isWgListener := common.Cast[dialer.WireGuardListener](e.options.Dialer)
if isWgListener {
bind = conn.NewStdNetBind(wgListener.WireGuardControl())
stdBind := conn.NewStdNetBind(wgListener.WireGuardControl())
if e.options.ListenPort == 0 && len(e.peers) == 1 && e.peers[0].endpoint.IsValid() {
stdBind.(*conn.StdNetBind).SetSinglePeerMode()
}
bind = stdBind
} else {
var (
isConnect bool
@@ -231,13 +228,7 @@ func (e *Endpoint) Start(resolve bool) error {
e.options.Logger.Error(fmt.Sprintf(strings.ToLower(format), args...))
},
}
var deviceInput Device
if e.natDevice != nil {
deviceInput = e.natDevice
} else {
deviceInput = e.tunDevice
}
wgDevice := device.NewDevice(e.options.Context, deviceInput, bind, logger, e.options.Workers)
wgDevice := device.NewDevice(e.options.Context, e.returnDevice, bind, logger, e.options.Workers)
e.tunDevice.SetDevice(wgDevice)
var ipcConf strings.Builder
ipcConf.WriteString(e.ipcConf)
@@ -320,13 +311,6 @@ func (e *Endpoint) Lookup(address netip.Addr) *device.Peer {
return e.allowedIPs.Lookup(address.AsSlice())
}
func (e *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
if e.natDevice == nil {
return nil, os.ErrInvalid
}
return e.natDevice.CreateDestination(metadata, routeContext, timeout)
}
func (e *Endpoint) onPauseUpdated(event int) {
switch event {
case pause.EventDevicePaused, pause.EventNetworkPause:
+157
View File
@@ -0,0 +1,157 @@
package wireguard
import (
"net/netip"
"sync/atomic"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/wireguard-go/device"
)
func (e *Endpoint) PortAddresses() (netip.Addr, netip.Addr) {
return e.tunDevice.Inet4Address(), e.tunDevice.Inet6Address()
}
func (e *Endpoint) PortMTU() uint32 {
return e.options.MTU
}
func (e *Endpoint) WritePackets(packets [][]byte) error {
wgDevice := e.device
if wgDevice == nil {
return E.New("WireGuard device is not ready")
}
packetRefs := make([]*device.InputPacketRef, 0, len(packets))
refs := make([]device.InputPacketRef, len(packets))
packetSlices := make([][]byte, len(packets))
for i, packet := range packets {
if len(packet) == 0 {
continue
}
var destination []byte
switch header.IPVersion(packet) {
case header.IPv4Version:
if len(packet) < header.IPv4MinimumSize {
continue
}
destination = header.IPv4(packet).DestinationAddressSlice()
case header.IPv6Version:
if len(packet) < header.IPv6MinimumSize {
continue
}
destination = header.IPv6(packet).DestinationAddressSlice()
default:
continue
}
packetSlices[i] = packet
refs[i] = device.InputPacketRef{
Destination: destination,
PacketSlices: packetSlices[i : i+1],
}
packetRefs = append(packetRefs, &refs[i])
}
if len(packetRefs) == 0 {
return nil
}
unmatchedRefs := wgDevice.InputPackets(packetRefs)
if len(unmatchedRefs) == 0 {
return nil
}
state := e.returnDevice.state.Load()
if state == nil {
return nil
}
var replies [][]byte
for _, packetRef := range unmatchedRefs {
packet := packetRef.PacketSlices[0]
var source netip.Addr
if header.IPVersion(packet) == header.IPv4Version {
source = e.tunDevice.Inet4Address()
} else {
source = e.tunDevice.Inet6Address()
}
reply, replyOk := tun.BuildUnreachable(packet, source, state.headroom)
if replyOk {
replies = append(replies, reply)
}
}
if len(replies) > 0 {
state.returnPath.ReturnPackets(replies)
}
return nil
}
func (e *Endpoint) AttachReturn(returnPath tun.Return) error {
headroom := returnPath.ReturnHeadroom()
if headroom > device.MessageTransportOffsetContent {
return E.New("return path headroom ", headroom, " exceeds available ", device.MessageTransportOffsetContent)
}
newState := &returnPathState{
returnPath: returnPath,
headroom: headroom,
}
for {
currentState := e.returnDevice.state.Load()
if currentState != nil {
if currentState.returnPath == returnPath {
return nil
}
return E.New("return path already attached")
}
if e.returnDevice.state.CompareAndSwap(nil, newState) {
return nil
}
}
}
func (e *Endpoint) DetachReturn(returnPath tun.Return) error {
currentState := e.returnDevice.state.Load()
if currentState != nil && currentState.returnPath == returnPath {
e.returnDevice.state.CompareAndSwap(currentState, nil)
}
return nil
}
type returnPathState struct {
returnPath tun.Return
headroom int
}
type returnDeviceWrapper struct {
Device
state atomic.Pointer[returnPathState]
}
func (d *returnDeviceWrapper) Write(bufs [][]byte, offset int) (int, error) {
state := d.state.Load()
if state == nil || len(bufs) == 0 {
return d.Device.Write(bufs, offset)
}
packets := make([][]byte, len(bufs))
for i, packet := range bufs {
// wireguard-go leaves device.MessageTransportOffsetContent writable bytes in front of the decrypted packet.
packets[i] = packet[offset-state.headroom:]
}
unconsumed := state.returnPath.ReturnPackets(packets)
if len(unconsumed) == 0 {
return 0, nil
}
if len(unconsumed) == len(bufs) {
return d.Device.Write(bufs, offset)
}
remaining := make([][]byte, 0, len(unconsumed))
searchIndex := 0
for _, packet := range unconsumed {
for searchIndex < len(bufs) && &packet[0] != &bufs[searchIndex][offset-state.headroom] {
searchIndex++
}
if searchIndex == len(bufs) {
break
}
remaining = append(remaining, bufs[searchIndex])
searchIndex++
}
return d.Device.Write(remaining, offset)
}