From f7968c4ff7d87868732ba55cc5f28579b81f994c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 27 Dec 2025 13:33:58 +0800 Subject: [PATCH] Update bypass action behavior for auto redirect --- adapter/router.go | 2 +- docs/configuration/route/rule_action.md | 14 +++---- docs/configuration/route/rule_action.zh.md | 10 ++--- docs/configuration/shared/pre-match.md | 11 ++++++ docs/configuration/shared/pre-match.zh.md | 10 +++++ option/rule_action.go | 3 -- protocol/tailscale/endpoint.go | 2 +- protocol/tun/inbound.go | 4 +- protocol/wireguard/endpoint.go | 2 +- route/route.go | 43 ++++++++++++++++++---- route/rule/rule_action.go | 3 ++ 11 files changed, 75 insertions(+), 29 deletions(-) diff --git a/adapter/router.go b/adapter/router.go index 3cece2ee0..a0df7124d 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -21,7 +21,7 @@ import ( type Router interface { Lifecycle ConnectionRouter - PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) + PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration, supportBypass bool) (tun.DirectRouteDestination, error) ConnectionRouterEx RuleSet(tag string) (RuleSet, bool) Rules() []Rule diff --git a/docs/configuration/route/rule_action.md b/docs/configuration/route/rule_action.md index 241233b92..523ffec20 100644 --- a/docs/configuration/route/rule_action.md +++ b/docs/configuration/route/rule_action.md @@ -62,19 +62,19 @@ See `route-options` fields below. } ``` -`bypass` routes connection to the specified outbound. +`bypass` bypasses sing-box at the kernel level for auto redirect connections in pre-match. -For tun connections in [pre-match](/configuration/shared/pre-match/), -the connection will bypass sing-box and connect directly at the kernel level. - -For non-tun connections and already established connections, the behavior is the same as `route`. +For non-auto-redirect connections and already established connections, +if `outbound` is specified, the behavior is the same as `route`; +otherwise, the rule will be skipped. #### outbound -==Required== - Tag of target outbound. +If not specified, the rule only matches in [pre-match](/configuration/shared/pre-match/) +from auto redirect, and will be skipped in other contexts. + #### route-options Fields See `route-options` fields below. diff --git a/docs/configuration/route/rule_action.zh.md b/docs/configuration/route/rule_action.zh.md index c944d1a87..16efb53a8 100644 --- a/docs/configuration/route/rule_action.zh.md +++ b/docs/configuration/route/rule_action.zh.md @@ -58,18 +58,16 @@ icon: material/new-box } ``` -`bypass` 将连接路由到指定出站。 +`bypass` 在预匹配中为 auto redirect 连接在内核层面绕过 sing-box。 -对于[预匹配](/configuration/shared/pre-match/)中的 tun 连接,连接将在内核层面绕过 sing-box 直接连接。 - -对于非 tun 连接和已建立的连接,行为与 `route` 相同。 +对于非 auto redirect 连接和已建立的连接,如果指定了 `outbound`,行为与 `route` 相同;否则规则将被跳过。 #### outbound -==必填== - 目标出站的标签。 +如果未指定,规则仅在来自 auto redirect 的[预匹配](/configuration/shared/pre-match/)中匹配,在其他场景中将被跳过。 + #### route-options 字段 参阅下方的 `route-options` 字段。 diff --git a/docs/configuration/shared/pre-match.md b/docs/configuration/shared/pre-match.md index 180099aa6..a0faf5772 100644 --- a/docs/configuration/shared/pre-match.md +++ b/docs/configuration/shared/pre-match.md @@ -24,10 +24,14 @@ When a rule matches an action that requires an established connection, pre-match Reject with TCP RST / ICMP unreachable. +See [reject](/configuration/route/rule_action/#reject) for details. + #### route Route ICMP connections to the specified outbound for direct reply. +See [route](/configuration/route/rule_action/#route) for details. + #### bypass !!! question "Since sing-box 1.13.0" @@ -37,3 +41,10 @@ Route ICMP connections to the specified outbound for direct reply. Only supported on Linux with `auto_redirect` enabled. Bypass sing-box and connect directly at kernel level. + +If `outbound` is not specified, the rule only matches in pre-match from auto redirect, +and will be skipped in other contexts. + +For all other contexts, bypass with `outbound` behaves like `route` action. + +See [bypass](/configuration/route/rule_action/#bypass) for details. diff --git a/docs/configuration/shared/pre-match.zh.md b/docs/configuration/shared/pre-match.zh.md index c615070f4..615400b01 100644 --- a/docs/configuration/shared/pre-match.zh.md +++ b/docs/configuration/shared/pre-match.zh.md @@ -22,10 +22,14 @@ icon: material/new-box 以 TCP RST / ICMP 不可达拒绝。 +详情参阅 [reject](/configuration/route/rule_action/#reject)。 + #### route 将 ICMP 连接路由到指定出站以直接回复。 +详情参阅 [route](/configuration/route/rule_action/#route)。 + #### bypass !!! question "自 sing-box 1.13.0 起" @@ -35,3 +39,9 @@ icon: material/new-box 仅支持 Linux,且需要启用 `auto_redirect`。 在内核层面绕过 sing-box 直接连接。 + +如果未指定 `outbound`,规则仅在来自 auto redirect 的预匹配中匹配,在其他场景中将被跳过。 + +对于其他所有场景,指定了 `outbound` 的 bypass 行为与 `route` 相同。 + +详情参阅 [bypass](/configuration/route/rule_action/#bypass)。 diff --git a/option/rule_action.go b/option/rule_action.go index 48326aed2..431082552 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -93,9 +93,6 @@ func (r *RuleAction) UnmarshalJSON(data []byte) error { if err != nil { return err } - if r.Action == C.RuleActionTypeBypass && r.BypassOptions.Outbound == "" { - return E.New("missing outbound for bypass action") - } return nil } diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 294dd051d..56d6f3983 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -463,7 +463,7 @@ func (t *Endpoint) PrepareConnection(network string, source M.Socksaddr, destina Network: network, Source: source, Destination: destination, - }, routeContext, timeout) + }, routeContext, timeout, false) if err != nil { switch { case rule.IsBypassed(err): diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index 43f6680c1..2ab68b3ea 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -480,7 +480,7 @@ func (t *Inbound) PrepareConnection(network string, source M.Socksaddr, destinat Source: source, Destination: destination, InboundOptions: t.inboundOptions, - }, routeContext, timeout) + }, routeContext, timeout, false) if err != nil { switch { case rule.IsBypassed(err): @@ -541,7 +541,7 @@ func (t *autoRedirectHandler) PrepareConnection(network string, source M.Socksad Source: source, Destination: destination, InboundOptions: t.inboundOptions, - }, routeContext, timeout) + }, routeContext, timeout, true) if err != nil { switch { case rule.IsBypassed(err): diff --git a/protocol/wireguard/endpoint.go b/protocol/wireguard/endpoint.go index a68d3b368..35ffd19e3 100644 --- a/protocol/wireguard/endpoint.go +++ b/protocol/wireguard/endpoint.go @@ -140,7 +140,7 @@ func (w *Endpoint) PrepareConnection(network string, source M.Socksaddr, destina Network: network, Source: source, Destination: destination, - }, routeContext, timeout) + }, routeContext, timeout, false) if err != nil { switch { case rule.IsBypassed(err): diff --git a/route/route.go b/route/route.go index 2e4b854a2..fd025a1b3 100644 --- a/route/route.go +++ b/route/route.go @@ -95,7 +95,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, conn, nil) + selectedRule, _, buffers, _, err := r.matchRule(ctx, &metadata, false, false, conn, nil) if err != nil { return err } @@ -114,6 +114,9 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad return E.New("TCP is not supported by outbound: ", selectedOutbound.Tag()) } case *R.RuleActionBypass: + if action.Outbound == "" { + break + } var loaded bool selectedOutbound, loaded = r.outbound.Outbound(action.Outbound) if !loaded { @@ -223,7 +226,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m conn = deadline.NewPacketConn(bufio.NewNetPacketConn(conn)) }*/ - selectedRule, _, _, packetBuffers, err := r.matchRule(ctx, &metadata, false, nil, conn) + selectedRule, _, _, packetBuffers, err := r.matchRule(ctx, &metadata, false, false, nil, conn) if err != nil { return err } @@ -243,6 +246,9 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag()) } case *R.RuleActionBypass: + if action.Outbound == "" { + break + } var loaded bool selectedOutbound, loaded = r.outbound.Outbound(action.Outbound) if !loaded { @@ -289,8 +295,8 @@ 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) (tun.DirectRouteDestination, error) { - selectedRule, _, _, _, err := r.matchRule(r.ctx, &metadata, true, nil, 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 } @@ -310,7 +316,20 @@ func (r *Router) PreMatch(metadata adapter.InboundContext, routeContext tun.Dire } return nil, action.Error(context.Background()) case *R.RuleActionBypass: - return nil, &R.BypassedError{Cause: tun.ErrBypass} + if supportBypass { + return nil, &R.BypassedError{Cause: tun.ErrBypass} + } + 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.RuleActionRoute: if routeContext == nil { return nil, nil @@ -388,7 +407,7 @@ func (r *Router) PreMatch(metadata adapter.InboundContext, routeContext tun.Dire } func (r *Router) matchRule( - ctx context.Context, metadata *adapter.InboundContext, preMatch bool, + ctx context.Context, metadata *adapter.InboundContext, preMatch bool, supportBypass bool, inputConn net.Conn, inputPacketConn N.PacketConn, ) ( selectedRule adapter.Rule, selectedRuleIndex int, @@ -591,8 +610,16 @@ match: actionType := currentRule.Action().Type() if actionType == C.RuleActionTypeRoute || actionType == C.RuleActionTypeReject || - actionType == C.RuleActionTypeHijackDNS || - actionType == C.RuleActionTypeBypass { + actionType == C.RuleActionTypeHijackDNS { + selectedRule = currentRule + selectedRuleIndex = currentRuleIndex + break match + } + if actionType == C.RuleActionTypeBypass { + bypassAction := currentRule.Action().(*R.RuleActionBypass) + if !supportBypass && bypassAction.Outbound == "" { + continue match + } selectedRule = currentRule selectedRuleIndex = currentRuleIndex break match diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index f4608e0ae..cac814e76 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -183,6 +183,9 @@ func (r *RuleActionBypass) Type() string { } func (r *RuleActionBypass) String() string { + if r.Outbound == "" { + return "bypass()" + } var descriptions []string descriptions = append(descriptions, r.Outbound) descriptions = append(descriptions, r.Descriptions()...)