From 660af7ce5966d82b584469cebc23ef89ff2cacbe Mon Sep 17 00:00:00 2001 From: iBug Date: Sat, 26 Sep 2026 22:00:37 +0800 Subject: [PATCH] feat(bypass): Make `network` respect `whitelist` --- bypass/bypass.go | 59 ++++++++++++++++--------------------------- bypass/bypass_test.go | 49 +++++++++++++++++++++++++++++------ 2 files changed, 63 insertions(+), 45 deletions(-) diff --git a/bypass/bypass.go b/bypass/bypass.go index 4c02281..abf54b5 100644 --- a/bypass/bypass.go +++ b/bypass/bypass.go @@ -44,13 +44,13 @@ var ErrBypass = errors.New("bypass") type options struct { // whitelist toggles between blacklist and whitelist mode. // When false (default): matching addresses bypass the proxy. - // When true: only matching addresses bypass the proxy. + // When true: matching addresses use the proxy and all others bypass it. whitelist bool // network restricts this bypass to a specific network protocol. - // When set, the incoming network must match before address matchers - // are evaluated. Recognized values include "tcp" and "udp". - // An empty value disables the network check. + // When set, the incoming network must match as part of the base rule. + // Recognized values include "tcp" and "udp". An empty value + // disables the network check. network string // matchers holds static patterns provided at construction time @@ -89,8 +89,8 @@ func WhitelistOption(whitelist bool) Option { } // NetworkOption restricts this bypass to a specific network protocol. -// When set, Contains returns false when the incoming network does not -// match, regardless of address matchers. Accepts values like "tcp" or "udp". +// Network and address predicates are combined before whitelist mode is +// applied to the resulting match. Accepts values like "tcp" or "udp". func NetworkOption(network string) Option { return func(opts *options) { opts.network = network @@ -427,43 +427,28 @@ func (p *localBypass) Contains(ctx context.Context, network, addr string, opts . return decision == decisionBypass } -// decide returns the bypass decision for the given address, applying the -// whitelist or blacklist mode to the pattern match result. +// decide returns the bypass decision for the given address. Network and +// address predicates form one base match, then whitelist mode is applied once +// to that combined result. func (p *localBypass) decide(network string, addr string) bypassDecision { p.mu.RLock() defer p.mu.RUnlock() - if p.options.network != "" && p.options.network != network { + matched := p.options.network == "" || p.options.network == network + if p.patterns != nil { + matched = matched && p.patterns.matchAny(addr) + } else if p.options.network == "" { + // A bypass with no predicates has no base match. + matched = false + } + + // Whitelist is a top-level negation of the normal blacklist decision: + // blacklist mode proxies a failed combined match, while whitelist mode + // proxies a successful combined match. + if matched == p.options.whitelist { return decisionProxy } - - if p.patterns == nil { - if p.options.network != "" { - if p.options.whitelist { - return decisionProxy - } - return decisionBypass - } - - return decisionProxy - } - - matched := p.patterns.matchAny(addr) - - if p.options.whitelist { - // Whitelist mode: the pattern set specifies addresses that MUST use the proxy. - // Matched addresses go through the proxy; unmatched addresses bypass. - if matched { - return decisionProxy - } - return decisionBypass - } - // Blacklist mode: the pattern set specifies addresses that should bypass the proxy. - // Matched addresses bypass; unmatched addresses go through the proxy. - if matched { - return decisionBypass - } - return decisionProxy + return decisionBypass } func (p *localBypass) IsWhitelist() bool { diff --git a/bypass/bypass_test.go b/bypass/bypass_test.go index bb4f8e1..7075646 100644 --- a/bypass/bypass_test.go +++ b/bypass/bypass_test.go @@ -765,7 +765,40 @@ func TestContains_NetworkMatch_WhitelistBypass(t *testing.T) { assert.True(t, b.Contains(context.Background(), "udp", "10.0.0.1")) } -func TestContains_NetworkMismatch_ReturnsFalse(t *testing.T) { +func TestContains_WhitelistNegatesCombinedMatch(t *testing.T) { + tests := []struct { + name string + whitelist bool + network string + addr string + wantProxy bool + }{ + {name: "blacklist both match", network: "tcp", addr: "192.168.1.1", wantProxy: false}, + {name: "blacklist network fails", network: "udp", addr: "192.168.1.1", wantProxy: true}, + {name: "blacklist matcher fails", network: "tcp", addr: "10.0.0.1", wantProxy: true}, + {name: "blacklist both fail", network: "udp", addr: "10.0.0.1", wantProxy: true}, + {name: "whitelist both match", whitelist: true, network: "tcp", addr: "192.168.1.1", wantProxy: true}, + {name: "whitelist network fails", whitelist: true, network: "udp", addr: "192.168.1.1", wantProxy: false}, + {name: "whitelist matcher fails", whitelist: true, network: "tcp", addr: "10.0.0.1", wantProxy: false}, + {name: "whitelist both fail", whitelist: true, network: "udp", addr: "10.0.0.1", wantProxy: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := newSyncedBypass( + WhitelistOption(tt.whitelist), + NetworkOption("tcp"), + MatchersOption([]string{"192.168.1.1"}), + ) + defer b.Close() + + gotProxy := !b.Contains(context.Background(), tt.network, tt.addr) + assert.Equal(t, tt.wantProxy, gotProxy) + }) + } +} + +func TestContains_NetworkMismatch_BlacklistReturnsFalse(t *testing.T) { b := newSyncedBypass( NetworkOption("tcp"), MatchersOption([]string{"192.168.1.1"}), @@ -789,7 +822,7 @@ func TestContains_NetworkMismatch_EmptyAddr(t *testing.T) { assert.False(t, b.Contains(context.Background(), "tcp", "")) } -func TestContains_NetworkMismatch_WhitelistReturnsFalse(t *testing.T) { +func TestContains_NetworkMismatch_WhitelistBypasses(t *testing.T) { b := newSyncedBypass( WhitelistOption(true), NetworkOption("tcp"), @@ -797,9 +830,9 @@ func TestContains_NetworkMismatch_WhitelistReturnsFalse(t *testing.T) { ) defer b.Close() - // Network does not match → bypass returns false even in whitelist mode - assert.False(t, b.Contains(context.Background(), "udp", "10.0.0.1")) - assert.False(t, b.Contains(context.Background(), "udp", "192.168.1.1")) + // Network fails the combined predicate, then whitelist mode negates it. + assert.True(t, b.Contains(context.Background(), "udp", "10.0.0.1")) + assert.True(t, b.Contains(context.Background(), "udp", "192.168.1.1")) } func TestContains_PatternSetPassesThroughNetworkFilter(t *testing.T) { @@ -846,15 +879,15 @@ func TestNetworkOnly_Whitelist_ProxiesMatchingNetwork(t *testing.T) { assert.False(t, b.Contains(context.Background(), "tcp", "10.0.0.1")) } -func TestNetworkOnly_Whitelist_NonMatchingNotAffected(t *testing.T) { +func TestNetworkOnly_Whitelist_NonMatchingBypasses(t *testing.T) { b := newSyncedBypass( WhitelistOption(true), NetworkOption("tcp"), ) defer b.Close() - // network: tcp 是作用域限定,非 TCP 流量不受影响 - assert.False(t, b.Contains(context.Background(), "udp", "192.168.1.1")) + // The network predicate fails, then whitelist mode negates it. + assert.True(t, b.Contains(context.Background(), "udp", "192.168.1.1")) } func TestNetworkOnly_NoNetwork_BehavesAsBefore(t *testing.T) {