diff --git a/adapter/router.go b/adapter/router.go index ba5fd3612..a87912b23 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -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. diff --git a/common/trafficcontrol/tracker.go b/common/trafficcontrol/tracker.go index 259f1a530..34820f30e 100644 --- a/common/trafficcontrol/tracker.go +++ b/common/trafficcontrol/tracker.go @@ -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 diff --git a/daemon/started_service.go b/daemon/started_service.go index 5dbc9a7de..a0cf0b6f8 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -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 } diff --git a/docs/configuration/shared/pre-match.md b/docs/configuration/shared/pre-match.md index 80738b707..6ad77ebfd 100644 --- a/docs/configuration/shared/pre-match.md +++ b/docs/configuration/shared/pre-match.md @@ -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" diff --git a/docs/configuration/shared/pre-match.zh.md b/docs/configuration/shared/pre-match.zh.md index 9f096f909..5f56e55ac 100644 --- a/docs/configuration/shared/pre-match.zh.md +++ b/docs/configuration/shared/pre-match.zh.md @@ -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 起" diff --git a/experimental/v2rayapi/stats.go b/experimental/v2rayapi/stats.go index c7b2b49f3..6f38fdd2d 100644 --- a/experimental/v2rayapi/stats.go +++ b/experimental/v2rayapi/stats.go @@ -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] diff --git a/go.mod b/go.mod index 1801c74a3..b3df9b860 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index d12351d9d..0ee4c81aa 100644 --- a/go.sum +++ b/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= diff --git a/protocol/cloudflare/inbound.go b/protocol/cloudflare/inbound.go index c33674475..6902b0065 100644 --- a/protocol/cloudflare/inbound.go +++ b/protocol/cloudflare/inbound.go @@ -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) diff --git a/protocol/tailscale/port.go b/protocol/tailscale/port.go index 3e5b7a70f..cac1e15b8 100644 --- a/protocol/tailscale/port.go +++ b/protocol/tailscale/port.go @@ -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 { diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index cb5251655..3aaddc166 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -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) { diff --git a/protocol/wireguard/endpoint.go b/protocol/wireguard/endpoint.go index 013a99ad0..c8d5bf747 100644 --- a/protocol/wireguard/endpoint.go +++ b/protocol/wireguard/endpoint.go @@ -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 { diff --git a/route/flow_tracker.go b/route/flow_tracker.go new file mode 100644 index 000000000..44c71913c --- /dev/null +++ b/route/flow_tracker.go @@ -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) + } +} diff --git a/route/route.go b/route/route.go index db1332d9b..f6c4e6591 100644 --- a/route/route.go +++ b/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 { diff --git a/route/rule/rule_item_rule_set_test.go b/route/rule/rule_item_rule_set_test.go index 70d440f41..d6457fbd1 100644 --- a/route/rule/rule_item_rule_set_test.go +++ b/route/rule/rule_item_rule_set_test.go @@ -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{} }