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:
@@ -94,6 +94,7 @@ type InboundContext struct {
|
||||
SourceHostname string
|
||||
QueryType uint16
|
||||
FakeIP bool
|
||||
PreMatch bool
|
||||
|
||||
// rule cache
|
||||
|
||||
|
||||
+5
-4
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{})),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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.
|
||||
@@ -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`。
|
||||
@@ -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.
|
||||
@@ -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`。
|
||||
@@ -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/) |
|
||||
|
||||
@@ -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/) |
|
||||
|
||||
@@ -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.
|
||||
@@ -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/)。
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -467,6 +467,7 @@ icon: material/new-box
|
||||
|-------------|--------------------------------|
|
||||
| `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs |
|
||||
| `wireguard` | 匹配对端的 allowed IPs |
|
||||
| `bridge` | 匹配除本机本地地址外的所有地址,仅在[预匹配](/zh/configuration/shared/pre-match/)中 |
|
||||
|
||||
#### source_mac_address
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user