Bypass with network (#101)
* Fix logger init before use it * Support specific network protocol validation in bypass
This commit is contained in:
+31
-5
@@ -47,6 +47,12 @@ type options struct {
|
|||||||
// When true: only matching addresses bypass the proxy.
|
// When true: only matching addresses bypass the proxy.
|
||||||
whitelist bool
|
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.
|
||||||
|
network string
|
||||||
|
|
||||||
// matchers holds static patterns provided at construction time
|
// matchers holds static patterns provided at construction time
|
||||||
// (e.g. from config file or command-line arguments).
|
// (e.g. from config file or command-line arguments).
|
||||||
matchers []string
|
matchers []string
|
||||||
@@ -82,6 +88,15 @@ 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".
|
||||||
|
func NetworkOption(network string) Option {
|
||||||
|
return func(opts *options) {
|
||||||
|
opts.network = network
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MatchersOption sets the static bypass patterns (CIDR, IP range, wildcard, or address).
|
// MatchersOption sets the static bypass patterns (CIDR, IP range, wildcard, or address).
|
||||||
func MatchersOption(matchers []string) Option {
|
func MatchersOption(matchers []string) Option {
|
||||||
return func(opts *options) {
|
return func(opts *options) {
|
||||||
@@ -272,10 +287,10 @@ func (p *localBypass) reload(ctx context.Context) error {
|
|||||||
patterns := append(p.options.matchers, v...)
|
patterns := append(p.options.matchers, v...)
|
||||||
p.logger.Debugf("load items %d", len(patterns))
|
p.logger.Debugf("load items %d", len(patterns))
|
||||||
|
|
||||||
ps := classifyPatterns(patterns, p.logger)
|
|
||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
p.patterns = ps
|
if len(patterns) > 0 {
|
||||||
|
p.patterns = classifyPatterns(patterns, p.logger)
|
||||||
|
}
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -402,7 +417,7 @@ func (p *localBypass) Contains(ctx context.Context, network, addr string, opts .
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
decision := p.decide(addr)
|
decision := p.decide(network, addr)
|
||||||
|
|
||||||
log := p.logger.WithFields(map[string]any{
|
log := p.logger.WithFields(map[string]any{
|
||||||
"sid": ctxvalue.SidFromContext(ctx),
|
"sid": ctxvalue.SidFromContext(ctx),
|
||||||
@@ -414,11 +429,22 @@ func (p *localBypass) Contains(ctx context.Context, network, addr string, opts .
|
|||||||
|
|
||||||
// decide returns the bypass decision for the given address, applying the
|
// decide returns the bypass decision for the given address, applying the
|
||||||
// whitelist or blacklist mode to the pattern match result.
|
// whitelist or blacklist mode to the pattern match result.
|
||||||
func (p *localBypass) decide(addr string) bypassDecision {
|
func (p *localBypass) decide(network string, addr string) bypassDecision {
|
||||||
p.mu.RLock()
|
p.mu.RLock()
|
||||||
defer p.mu.RUnlock()
|
defer p.mu.RUnlock()
|
||||||
|
|
||||||
|
if p.options.network != "" && p.options.network != network {
|
||||||
|
return decisionProxy
|
||||||
|
}
|
||||||
|
|
||||||
if p.patterns == nil {
|
if p.patterns == nil {
|
||||||
|
if p.options.network != "" {
|
||||||
|
if p.options.whitelist {
|
||||||
|
return decisionProxy
|
||||||
|
}
|
||||||
|
return decisionBypass
|
||||||
|
}
|
||||||
|
|
||||||
return decisionProxy
|
return decisionProxy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+194
-14
@@ -288,32 +288,32 @@ func TestMatched_IP(t *testing.T) {
|
|||||||
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1"}))
|
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1"}))
|
||||||
defer b.Close()
|
defer b.Close()
|
||||||
|
|
||||||
assert.Equal(t, decisionBypass, b.decide("192.168.1.1"))
|
assert.Equal(t, decisionBypass, b.decide("", "192.168.1.1"))
|
||||||
assert.Equal(t, decisionProxy, b.decide("10.0.0.1"))
|
assert.Equal(t, decisionProxy, b.decide("", "10.0.0.1"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMatched_CIDR(t *testing.T) {
|
func TestMatched_CIDR(t *testing.T) {
|
||||||
b := newSyncedBypass(MatchersOption([]string{"10.0.0.0/8"}))
|
b := newSyncedBypass(MatchersOption([]string{"10.0.0.0/8"}))
|
||||||
defer b.Close()
|
defer b.Close()
|
||||||
|
|
||||||
assert.Equal(t, decisionBypass, b.decide("10.0.0.1"))
|
assert.Equal(t, decisionBypass, b.decide("", "10.0.0.1"))
|
||||||
assert.Equal(t, decisionProxy, b.decide("192.168.1.1"))
|
assert.Equal(t, decisionProxy, b.decide("", "192.168.1.1"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMatched_Wildcard(t *testing.T) {
|
func TestMatched_Wildcard(t *testing.T) {
|
||||||
b := newSyncedBypass(MatchersOption([]string{"*.example.com"}))
|
b := newSyncedBypass(MatchersOption([]string{"*.example.com"}))
|
||||||
defer b.Close()
|
defer b.Close()
|
||||||
|
|
||||||
assert.Equal(t, decisionBypass, b.decide("foo.example.com"))
|
assert.Equal(t, decisionBypass, b.decide("", "foo.example.com"))
|
||||||
assert.Equal(t, decisionProxy, b.decide("foo.other.com"))
|
assert.Equal(t, decisionProxy, b.decide("", "foo.other.com"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMatched_IPRange(t *testing.T) {
|
func TestMatched_IPRange(t *testing.T) {
|
||||||
b := newSyncedBypass(MatchersOption([]string{"10.0.0.1-10.0.0.100"}))
|
b := newSyncedBypass(MatchersOption([]string{"10.0.0.1-10.0.0.100"}))
|
||||||
defer b.Close()
|
defer b.Close()
|
||||||
|
|
||||||
assert.Equal(t, decisionBypass, b.decide("10.0.0.50"))
|
assert.Equal(t, decisionBypass, b.decide("", "10.0.0.50"))
|
||||||
assert.Equal(t, decisionProxy, b.decide("10.0.0.200"))
|
assert.Equal(t, decisionProxy, b.decide("", "10.0.0.200"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- reload tests ---
|
// --- reload tests ---
|
||||||
@@ -683,7 +683,20 @@ func TestNewBypass_ReloadError(t *testing.T) {
|
|||||||
|
|
||||||
// --- Network parameter ignored ---
|
// --- Network parameter ignored ---
|
||||||
|
|
||||||
func TestContains_NetworkIgnoredForMatching(t *testing.T) {
|
// --- Network option tests ---
|
||||||
|
|
||||||
|
func TestNetworkOption(t *testing.T) {
|
||||||
|
var opts options
|
||||||
|
NetworkOption("tcp")(&opts)
|
||||||
|
assert.Equal(t, "tcp", opts.network)
|
||||||
|
|
||||||
|
NetworkOption("")(&opts)
|
||||||
|
assert.Equal(t, "", opts.network)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Network pre-filter tests ---
|
||||||
|
|
||||||
|
func TestContains_NetworkNotSet_BehavesAsBefore(t *testing.T) {
|
||||||
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1"}))
|
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1"}))
|
||||||
defer b.Close()
|
defer b.Close()
|
||||||
|
|
||||||
@@ -692,6 +705,173 @@ func TestContains_NetworkIgnoredForMatching(t *testing.T) {
|
|||||||
assert.True(t, b.Contains(context.Background(), "ip4", "192.168.1.1"))
|
assert.True(t, b.Contains(context.Background(), "ip4", "192.168.1.1"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContains_NetworkMatch_BlacklistBypass(t *testing.T) {
|
||||||
|
b := newSyncedBypass(
|
||||||
|
NetworkOption("tcp"),
|
||||||
|
MatchersOption([]string{"192.168.1.1"}),
|
||||||
|
)
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
// Network matches and addr matches → bypass
|
||||||
|
assert.True(t, b.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContains_NetworkMatch_BlacklistPass(t *testing.T) {
|
||||||
|
b := newSyncedBypass(
|
||||||
|
NetworkOption("tcp"),
|
||||||
|
MatchersOption([]string{"192.168.1.1"}),
|
||||||
|
)
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
// Network matches but addr does not match → proxy
|
||||||
|
assert.False(t, b.Contains(context.Background(), "tcp", "10.0.0.1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContains_NetworkMatch_WhitelistProxy(t *testing.T) {
|
||||||
|
b := newSyncedBypass(
|
||||||
|
WhitelistOption(true),
|
||||||
|
NetworkOption("udp"),
|
||||||
|
MatchersOption([]string{"192.168.1.1"}),
|
||||||
|
)
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
// Network matches and addr matches → in whitelist, addr goes through proxy
|
||||||
|
assert.False(t, b.Contains(context.Background(), "udp", "192.168.1.1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContains_NetworkMatch_WhitelistBypass(t *testing.T) {
|
||||||
|
b := newSyncedBypass(
|
||||||
|
WhitelistOption(true),
|
||||||
|
NetworkOption("udp"),
|
||||||
|
MatchersOption([]string{"192.168.1.1"}),
|
||||||
|
)
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
// Network matches but addr does not match → whitelist bypass
|
||||||
|
assert.True(t, b.Contains(context.Background(), "udp", "10.0.0.1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContains_NetworkMismatch_ReturnsFalse(t *testing.T) {
|
||||||
|
b := newSyncedBypass(
|
||||||
|
NetworkOption("tcp"),
|
||||||
|
MatchersOption([]string{"192.168.1.1"}),
|
||||||
|
)
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
// Network does not match → bypass returns false regardless of addr
|
||||||
|
assert.False(t, b.Contains(context.Background(), "udp", "192.168.1.1"))
|
||||||
|
assert.False(t, b.Contains(context.Background(), "udp", ""))
|
||||||
|
assert.False(t, b.Contains(context.Background(), "sctp", "192.168.1.1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContains_NetworkMismatch_EmptyAddr(t *testing.T) {
|
||||||
|
b := newSyncedBypass(
|
||||||
|
NetworkOption("tcp"),
|
||||||
|
MatchersOption([]string{"192.168.1.1"}),
|
||||||
|
)
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
// addr is empty → early return false
|
||||||
|
assert.False(t, b.Contains(context.Background(), "tcp", ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContains_NetworkMismatch_WhitelistReturnsFalse(t *testing.T) {
|
||||||
|
b := newSyncedBypass(
|
||||||
|
WhitelistOption(true),
|
||||||
|
NetworkOption("tcp"),
|
||||||
|
MatchersOption([]string{"192.168.1.1"}),
|
||||||
|
)
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContains_PatternSetPassesThroughNetworkFilter(t *testing.T) {
|
||||||
|
b := newSyncedBypass(
|
||||||
|
NetworkOption("tcp"),
|
||||||
|
MatchersOption([]string{"192.168.1.1", "10.0.0.0/8", "*.example.com"}),
|
||||||
|
)
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
// Network matches → pattern set is evaluated
|
||||||
|
assert.True(t, b.Contains(context.Background(), "tcp", "10.0.0.1"))
|
||||||
|
assert.True(t, b.Contains(context.Background(), "tcp", "foo.example.com"))
|
||||||
|
assert.False(t, b.Contains(context.Background(), "tcp", "8.8.8.8"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Network-only tests (no address matchers) ---
|
||||||
|
|
||||||
|
func TestNetworkOnly_Blacklist_BypassesMatchingNetwork(t *testing.T) {
|
||||||
|
b := newSyncedBypass(NetworkOption("tcp"))
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
// Network matches → bypass
|
||||||
|
assert.True(t, b.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||||
|
assert.True(t, b.Contains(context.Background(), "tcp", "10.0.0.1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNetworkOnly_Blacklist_DoesNotBypassNonMatching(t *testing.T) {
|
||||||
|
b := newSyncedBypass(NetworkOption("tcp"))
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
// Network does not match → not bypassed
|
||||||
|
assert.False(t, b.Contains(context.Background(), "udp", "192.168.1.1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNetworkOnly_Whitelist_ProxiesMatchingNetwork(t *testing.T) {
|
||||||
|
b := newSyncedBypass(
|
||||||
|
WhitelistOption(true),
|
||||||
|
NetworkOption("tcp"),
|
||||||
|
)
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
// In whitelist mode, matched network → proxy (not bypassed)
|
||||||
|
assert.False(t, b.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||||
|
assert.False(t, b.Contains(context.Background(), "tcp", "10.0.0.1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNetworkOnly_Whitelist_NonMatchingNotAffected(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"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNetworkOnly_NoNetwork_BehavesAsBefore(t *testing.T) {
|
||||||
|
b := newSyncedBypass()
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
// No network configured, no matchers → never bypass
|
||||||
|
assert.False(t, b.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||||
|
assert.False(t, b.Contains(context.Background(), "udp", "192.168.1.1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNetworkOnly_EmptyAddr_ReturnsFalse(t *testing.T) {
|
||||||
|
b := newSyncedBypass(NetworkOption("tcp"))
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
assert.False(t, b.Contains(context.Background(), "tcp", ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNetworkOption_DoesNotAffectGroupBypass(t *testing.T) {
|
||||||
|
b := newSyncedBypass(
|
||||||
|
NetworkOption("tcp"),
|
||||||
|
MatchersOption([]string{"192.168.1.1"}),
|
||||||
|
)
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
// bypassGroup embeds the same localBypass and respects the network filter
|
||||||
|
g := BypassGroup(b)
|
||||||
|
assert.False(t, g.Contains(context.Background(), "udp", "192.168.1.1"))
|
||||||
|
assert.True(t, g.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||||
|
}
|
||||||
|
|
||||||
// --- Debug logging doesn't panic ---
|
// --- Debug logging doesn't panic ---
|
||||||
|
|
||||||
func TestContains_DenyLogs(t *testing.T) {
|
func TestContains_DenyLogs(t *testing.T) {
|
||||||
@@ -916,33 +1096,33 @@ func TestDecide_BlacklistMatched(t *testing.T) {
|
|||||||
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1"}))
|
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1"}))
|
||||||
defer b.Close()
|
defer b.Close()
|
||||||
|
|
||||||
assert.Equal(t, decisionBypass, b.decide("192.168.1.1"))
|
assert.Equal(t, decisionBypass, b.decide("", "192.168.1.1"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDecide_BlacklistNotMatched(t *testing.T) {
|
func TestDecide_BlacklistNotMatched(t *testing.T) {
|
||||||
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1"}))
|
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1"}))
|
||||||
defer b.Close()
|
defer b.Close()
|
||||||
|
|
||||||
assert.Equal(t, decisionProxy, b.decide("10.0.0.1"))
|
assert.Equal(t, decisionProxy, b.decide("", "10.0.0.1"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDecide_WhitelistMatched(t *testing.T) {
|
func TestDecide_WhitelistMatched(t *testing.T) {
|
||||||
b := newSyncedBypass(WhitelistOption(true), MatchersOption([]string{"192.168.1.1"}))
|
b := newSyncedBypass(WhitelistOption(true), MatchersOption([]string{"192.168.1.1"}))
|
||||||
defer b.Close()
|
defer b.Close()
|
||||||
|
|
||||||
assert.Equal(t, decisionProxy, b.decide("192.168.1.1"))
|
assert.Equal(t, decisionProxy, b.decide("", "192.168.1.1"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDecide_WhitelistNotMatched(t *testing.T) {
|
func TestDecide_WhitelistNotMatched(t *testing.T) {
|
||||||
b := newSyncedBypass(WhitelistOption(true), MatchersOption([]string{"192.168.1.1"}))
|
b := newSyncedBypass(WhitelistOption(true), MatchersOption([]string{"192.168.1.1"}))
|
||||||
defer b.Close()
|
defer b.Close()
|
||||||
|
|
||||||
assert.Equal(t, decisionBypass, b.decide("10.0.0.1"))
|
assert.Equal(t, decisionBypass, b.decide("", "10.0.0.1"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDecide_NilPatterns(t *testing.T) {
|
func TestDecide_NilPatterns(t *testing.T) {
|
||||||
lb := &localBypass{logger: xlogger.Nop()}
|
lb := &localBypass{logger: xlogger.Nop()}
|
||||||
assert.Equal(t, decisionProxy, lb.decide("anything"))
|
assert.Equal(t, decisionProxy, lb.decide("", "anything"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- evaluate (bypassGroup) tests ---
|
// --- evaluate (bypassGroup) tests ---
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ type BypassConfig struct {
|
|||||||
// Deprecated: use whitelist instead
|
// Deprecated: use whitelist instead
|
||||||
Reverse bool `yaml:",omitempty" json:"reverse,omitempty"`
|
Reverse bool `yaml:",omitempty" json:"reverse,omitempty"`
|
||||||
Whitelist bool `yaml:",omitempty" json:"whitelist,omitempty"`
|
Whitelist bool `yaml:",omitempty" json:"whitelist,omitempty"`
|
||||||
|
Network string `yaml:",omitempty" json:"network,omitempty"`
|
||||||
Matchers []string `yaml:",omitempty" json:"matchers,omitempty"`
|
Matchers []string `yaml:",omitempty" json:"matchers,omitempty"`
|
||||||
Reload time.Duration `yaml:",omitempty" json:"reload,omitempty"`
|
Reload time.Duration `yaml:",omitempty" json:"reload,omitempty"`
|
||||||
File *FileLoader `yaml:",omitempty" json:"file,omitempty"`
|
File *FileLoader `yaml:",omitempty" json:"file,omitempty"`
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ func ParseBypass(cfg *config.BypassConfig) bypass.Bypass {
|
|||||||
opts := []xbypass.Option{
|
opts := []xbypass.Option{
|
||||||
xbypass.MatchersOption(cfg.Matchers),
|
xbypass.MatchersOption(cfg.Matchers),
|
||||||
xbypass.WhitelistOption(cfg.Reverse || cfg.Whitelist),
|
xbypass.WhitelistOption(cfg.Reverse || cfg.Whitelist),
|
||||||
|
xbypass.NetworkOption(cfg.Network),
|
||||||
xbypass.ReloadPeriodOption(cfg.Reload),
|
xbypass.ReloadPeriodOption(cfg.Reload),
|
||||||
xbypass.LoggerOption(logger.Default().WithFields(map[string]any{
|
xbypass.LoggerOption(logger.Default().WithFields(map[string]any{
|
||||||
"kind": "bypass",
|
"kind": "bypass",
|
||||||
|
|||||||
Reference in New Issue
Block a user