mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-08-04 14:36:07 +00:00
Imrpove flow tracking & sniff action
This commit is contained in:
parent
9b5dee7dcf
commit
f8f408feff
15 changed files with 368 additions and 53 deletions
|
|
@ -17,7 +17,7 @@ import (
|
|||
type Router interface {
|
||||
Lifecycle
|
||||
ConnectionRouter
|
||||
PreMatch(metadata InboundContext) PreMatchResult
|
||||
PreMatch(metadata InboundContext, firstPacket []byte) PreMatchResult
|
||||
ConnectionRouterEx
|
||||
RuleSet(tag string) (RuleSet, bool)
|
||||
Rules() []Rule
|
||||
|
|
@ -42,9 +42,10 @@ type PreMatchResult struct {
|
|||
Action PreMatchAction
|
||||
Outbound Outbound
|
||||
Destination netip.AddrPort
|
||||
NewTracker func() tun.FlowTracker
|
||||
}
|
||||
|
||||
func JudgeFlow(router Router, inbound string, inboundType string, network uint8, source netip.AddrPort, destination netip.AddrPort) tun.FlowVerdict {
|
||||
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):
|
||||
|
|
@ -67,14 +68,14 @@ func JudgeFlow(router Router, inbound string, inboundType string, network uint8,
|
|||
metadata.Source.Port = 0
|
||||
metadata.Destination.Port = 0
|
||||
}
|
||||
result := router.PreMatch(metadata)
|
||||
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}
|
||||
verdict := tun.FlowVerdict{Action: tun.ActionFlow, Port: port, NewTracker: result.NewTracker}
|
||||
if result.Destination.IsValid() {
|
||||
destinationPort := result.Destination.Port()
|
||||
if networkName == N.NetworkICMP {
|
||||
|
|
@ -97,6 +98,7 @@ func JudgeFlow(router Router, inbound string, inboundType string, network uint8,
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -68,6 +69,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 (
|
||||
|
|
@ -135,6 +143,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
|
||||
|
|
|
|||
|
|
@ -986,8 +986,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ icon: material/new-box
|
|||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-alert: [route](#route)
|
||||
:material-alert: [route](#route)
|
||||
:material-plus: [sniff](#sniff)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
|
|
@ -16,11 +17,11 @@ Pre-match is rule matching that runs before the connection is established.
|
|||
|
||||
### How it works
|
||||
|
||||
When an L3 inbound (TUN, WireGuard, or Tailscale) 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
|
||||
|
||||
|
|
@ -53,6 +54,19 @@ 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"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ icon: material/new-box
|
|||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-alert: [route](#route)
|
||||
:material-alert: [route](#route)
|
||||
:material-plus: [sniff](#sniff)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
|
|
@ -16,9 +17,9 @@ icon: material/new-box
|
|||
|
||||
### 工作原理
|
||||
|
||||
当 L3 入站(TUN、WireGuard 或 Tailscale)收到连接请求时,连接尚未建立,因此无法读取连接数据。在此阶段,sing-box 在预匹配模式下运行路由规则。
|
||||
当 L3 入站(TUN、WireGuard 或 Tailscale)收到连接请求时,连接尚未建立:对于 TCP 连接,无连接数据可用;对于 UDP 连接,仅首个数据包可用。在此阶段,sing-box 在预匹配模式下运行路由规则。
|
||||
|
||||
由于连接数据不可用,只有不需要连接数据的动作才能执行。当规则匹配到需要已建立连接的动作时,预匹配将在该规则处停止。
|
||||
当规则匹配到需要比当前可用数据更多连接数据的动作时,预匹配将在该规则处停止。
|
||||
|
||||
### 支持的动作
|
||||
|
||||
|
|
@ -47,6 +48,18 @@ 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 起"
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -55,7 +55,7 @@ require (
|
|||
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.9
|
||||
github.com/sagernet/wireguard-go v0.0.5-0.20260706130655-57baac9504a8
|
||||
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
|
||||
github.com/stretchr/testify v1.11.1
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -302,6 +302,8 @@ github.com/sagernet/wireguard-go v0.0.4 h1:w/vHtk7AzMG37+D/uQROBL72Gj/Gj+FHMY5kg
|
|||
github.com/sagernet/wireguard-go v0.0.4/go.mod h1:hEqi4y5czEg6LYtX2Bpjg+lV0b/J1n+5rA885Z66Mx0=
|
||||
github.com/sagernet/wireguard-go v0.0.5-0.20260706130655-57baac9504a8 h1:gfukXANr9v5kcrKGeoCV5c+IdessM9NRGzWE7Aot9sw=
|
||||
github.com/sagernet/wireguard-go v0.0.5-0.20260706130655-57baac9504a8/go.mod h1:hEqi4y5czEg6LYtX2Bpjg+lV0b/J1n+5rA885Z66Mx0=
|
||||
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/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ func (h *icmpRouterHandler) RouteICMPFlow(source netip.Addr, destination netip.A
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ func (t *Endpoint) PortMTU() uint32 {
|
|||
return uint32(tsTUN.DefaultTUNMTU())
|
||||
}
|
||||
|
||||
func (t *Endpoint) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort) tun.FlowVerdict {
|
||||
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}
|
||||
|
|
@ -70,7 +70,7 @@ func (t *Endpoint) JudgeFlow(network uint8, source netip.AddrPort, destination n
|
|||
}
|
||||
}
|
||||
}
|
||||
return adapter.JudgeFlow(t.router, t.Tag(), t.Type(), network, source, destination)
|
||||
return adapter.JudgeFlow(t.router, t.Tag(), t.Type(), network, source, destination, firstPacket)
|
||||
}
|
||||
|
||||
func (t *Endpoint) AttachReturn(returnPath tun.Return) error {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/netip"
|
||||
"os"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -479,8 +480,11 @@ func (t *Inbound) Close() error {
|
|||
)
|
||||
}
|
||||
|
||||
func (t *Inbound) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort) tun.FlowVerdict {
|
||||
return adapter.JudgeFlow(t.router, t.tag, C.TypeTun, network, source, destination)
|
||||
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}
|
||||
}
|
||||
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) {
|
||||
|
|
@ -490,10 +494,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)
|
||||
|
|
@ -527,8 +529,8 @@ func (t *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
|||
|
||||
type autoRedirectHandler Inbound
|
||||
|
||||
func (t *autoRedirectHandler) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort) tun.FlowVerdict {
|
||||
return (*Inbound)(t).JudgeFlow(network, source, destination)
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -161,13 +161,13 @@ 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) tun.FlowVerdict {
|
||||
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 adapter.JudgeFlow(w.router, w.Tag(), w.Type(), network, source, destination)
|
||||
return adapter.JudgeFlow(w.router, w.Tag(), w.Type(), network, source, destination, firstPacket)
|
||||
}
|
||||
|
||||
func (w *Endpoint) WritePackets(packets [][]byte) error {
|
||||
|
|
|
|||
101
route/flow_tracker.go
Normal file
101
route/flow_tracker.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
var (
|
||||
_ tun.FlowTracker = (*flowLogger)(nil)
|
||||
_ tun.FlowTracker = (multiFlowTracker)(nil)
|
||||
)
|
||||
|
||||
type flowLogger struct {
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
network string
|
||||
source string
|
||||
destination string
|
||||
outbound adapter.Outbound
|
||||
createdAt time.Time
|
||||
upload atomic.Int64
|
||||
download atomic.Int64
|
||||
}
|
||||
|
||||
func newFlowLogger(ctx context.Context, logger log.ContextLogger, metadata adapter.InboundContext, outbound adapter.Outbound) *flowLogger {
|
||||
var source, destination string
|
||||
if metadata.Network == N.NetworkICMP {
|
||||
source = metadata.Source.AddrString()
|
||||
destination = metadata.Destination.AddrString()
|
||||
} else {
|
||||
source = metadata.Source.String()
|
||||
destination = metadata.Destination.String()
|
||||
}
|
||||
return &flowLogger{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
network: metadata.Network,
|
||||
source: source,
|
||||
destination: destination,
|
||||
outbound: outbound,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *flowLogger) AttachFlow(handle tun.FlowHandle) {
|
||||
l.createdAt = time.Now()
|
||||
}
|
||||
|
||||
func (l *flowLogger) CountForward(n int) {
|
||||
l.upload.Add(int64(n))
|
||||
}
|
||||
|
||||
func (l *flowLogger) CountReverse(n int) {
|
||||
l.download.Add(int64(n))
|
||||
}
|
||||
|
||||
func (l *flowLogger) FlowEstablished() {
|
||||
}
|
||||
|
||||
func (l *flowLogger) CloseFlow(reason tun.FlowCloseReason) {
|
||||
l.logger.DebugContext(l.ctx, "flow closed: ", reason,
|
||||
", upload ", byteformats.FormatBytes(uint64(l.upload.Load())), ", download ", byteformats.FormatBytes(uint64(l.download.Load())))
|
||||
}
|
||||
|
||||
type multiFlowTracker []tun.FlowTracker
|
||||
|
||||
func (t multiFlowTracker) AttachFlow(handle tun.FlowHandle) {
|
||||
for _, tracker := range t {
|
||||
tracker.AttachFlow(handle)
|
||||
}
|
||||
}
|
||||
|
||||
func (t multiFlowTracker) CountForward(n int) {
|
||||
for _, tracker := range t {
|
||||
tracker.CountForward(n)
|
||||
}
|
||||
}
|
||||
|
||||
func (t multiFlowTracker) CountReverse(n int) {
|
||||
for _, tracker := range t {
|
||||
tracker.CountReverse(n)
|
||||
}
|
||||
}
|
||||
|
||||
func (t multiFlowTracker) FlowEstablished() {
|
||||
for _, tracker := range t {
|
||||
tracker.FlowEstablished()
|
||||
}
|
||||
}
|
||||
|
||||
func (t multiFlowTracker) CloseFlow(reason tun.FlowCloseReason) {
|
||||
for _, tracker := range t {
|
||||
tracker.CloseFlow(reason)
|
||||
}
|
||||
}
|
||||
111
route/route.go
111
route/route.go
|
|
@ -12,8 +12,10 @@ import (
|
|||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"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-vmess"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
|
|
@ -28,6 +30,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)
|
||||
|
|
@ -294,7 +306,8 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) PreMatch(metadata adapter.InboundContext) adapter.PreMatchResult {
|
||||
func (r *Router) PreMatch(metadata adapter.InboundContext, firstPacket []byte) adapter.PreMatchResult {
|
||||
ctx := log.ContextWithNewID(r.ctx)
|
||||
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) {
|
||||
|
|
@ -319,46 +332,89 @@ func (r *Router) PreMatch(metadata adapter.InboundContext) adapter.PreMatchResul
|
|||
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
|
||||
}
|
||||
return continueResult
|
||||
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
|
||||
}
|
||||
continue
|
||||
}
|
||||
//goland:noinspection GoDeprecation
|
||||
if action.OverrideDestination && M.IsDomainName(metadata.Domain) {
|
||||
metadata.Destination = M.Socksaddr{
|
||||
Fqdn: metadata.Domain,
|
||||
Port: metadata.Destination.Port,
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
case *R.RuleActionRouteOptions:
|
||||
applyRouteOptionsOverride(&metadata, action)
|
||||
case *R.RuleActionRoute:
|
||||
applyRouteOptionsOverride(&metadata, &action.RuleActionRouteOptions)
|
||||
r.logger.Debug("pre-match[", currentRuleIndex, "] ", currentRule, " => ", action)
|
||||
return r.preMatchFlow(&metadata, packetDestination, action.Outbound)
|
||||
return r.preMatchFlow(ctx, &metadata, packetDestination, currentRule, action.Outbound)
|
||||
case *R.RuleActionBypass:
|
||||
applyRouteOptionsOverride(&metadata, &action.RuleActionRouteOptions)
|
||||
r.logger.Debug("pre-match[", currentRuleIndex, "] ", currentRule, " => ", action)
|
||||
if action.Outbound == "" {
|
||||
if metadata.Destination.IsDomain() || metadata.Destination != packetDestination {
|
||||
return continueResult
|
||||
}
|
||||
return adapter.PreMatchResult{Action: adapter.PreMatchBypass}
|
||||
}
|
||||
return r.preMatchFlow(&metadata, packetDestination, action.Outbound)
|
||||
return r.preMatchFlow(ctx, &metadata, packetDestination, currentRule, action.Outbound)
|
||||
case *R.RuleActionReject:
|
||||
r.logger.Debug("pre-match[", currentRuleIndex, "] ", currentRule, " => ", action)
|
||||
rejectErr := action.Error(r.ctx)
|
||||
if errors.Is(rejectErr, R.ErrDrop) {
|
||||
return adapter.PreMatchResult{Action: adapter.PreMatchDrop}
|
||||
}
|
||||
return adapter.PreMatchResult{Action: adapter.PreMatchReject}
|
||||
case *R.RuleActionResolve:
|
||||
resolveErr := r.actionResolve(adapter.WithContext(r.ctx, &metadata), &metadata, action)
|
||||
resolveErr := r.actionResolve(adapter.WithContext(ctx, &metadata), &metadata, action)
|
||||
if resolveErr != nil {
|
||||
r.logger.Debug("pre-match[", currentRuleIndex, "] ", currentRule, " => ", action, ": ", resolveErr)
|
||||
r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] ", currentRule, " => ", action, ": ", resolveErr)
|
||||
return adapter.PreMatchResult{Action: adapter.PreMatchReject}
|
||||
}
|
||||
default:
|
||||
return continueResult
|
||||
}
|
||||
}
|
||||
return r.preMatchFlow(&metadata, packetDestination, "")
|
||||
return r.preMatchFlow(ctx, &metadata, packetDestination, nil, "")
|
||||
}
|
||||
|
||||
func applyRouteOptionsOverride(metadata *adapter.InboundContext, routeOptions *R.RuleActionRouteOptions) {
|
||||
|
|
@ -378,7 +434,7 @@ func applyRouteOptionsOverride(metadata *adapter.InboundContext, routeOptions *R
|
|||
}
|
||||
}
|
||||
|
||||
func (r *Router) preMatchFlow(metadata *adapter.InboundContext, packetDestination M.Socksaddr, outboundTag string) adapter.PreMatchResult {
|
||||
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 == "" {
|
||||
|
|
@ -409,7 +465,7 @@ func (r *Router) preMatchFlow(metadata *adapter.InboundContext, packetDestinatio
|
|||
if outbound.Type() == C.TypeDirect {
|
||||
directDialer, isDirectDialer := outbound.(dialer.DirectDialer)
|
||||
if isDirectDialer && directDialer.IsEmpty() && !metadata.Destination.IsDomain() && metadata.Destination == packetDestination {
|
||||
r.logger.Debug("pre-match bypass ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString())
|
||||
r.logger.DebugContext(ctx, "pre-match bypass ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString())
|
||||
return adapter.PreMatchResult{Action: adapter.PreMatchBypass, Outbound: outbound}
|
||||
}
|
||||
}
|
||||
|
|
@ -429,9 +485,9 @@ func (r *Router) preMatchFlow(metadata *adapter.InboundContext, packetDestinatio
|
|||
}
|
||||
if !newDestination.IsValid() {
|
||||
if len(metadata.DestinationAddresses) == 0 {
|
||||
r.logger.Warn("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(), "]")
|
||||
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 {
|
||||
r.logger.Debug("pre-match: reject ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to fake destination ", metadata.Destination.Fqdn, ": no resolved address for this address family")
|
||||
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}
|
||||
}
|
||||
|
|
@ -439,7 +495,22 @@ func (r *Router) preMatchFlow(metadata *adapter.InboundContext, packetDestinatio
|
|||
} else if metadata.Destination != packetDestination {
|
||||
result.Destination = metadata.Destination.AddrPort()
|
||||
}
|
||||
r.logger.Debug("pre-match forward ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString(), " via outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||
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)
|
||||
}
|
||||
}
|
||||
if len(flowTrackers) == 1 {
|
||||
return flowTrackers[0]
|
||||
}
|
||||
return multiFlowTracker(flowTrackers)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
@ -678,15 +749,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 {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ 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) adapter.PreMatchResult {
|
||||
func (r *ruleSetItemTestRouter) PreMatch(adapter.InboundContext, []byte) adapter.PreMatchResult {
|
||||
return adapter.PreMatchResult{}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue