mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-05-27 12:52:27 +02:00
c365051732
Implement configurable DNS policy rule matching order and refactor upstreamFor method for better maintainability. New features: - Add MatchingConfig to ListenerPolicyConfig for rule order configuration - Support custom rule evaluation order (network, mac, domain) - Add stop_on_first_match configuration option - Hidden from config files (mapstructure:"-" toml:"-") for future release Code improvements: - Create upstreamForRequest struct to reduce method parameter count - Refactor upstreamForWithConfig to use single struct parameter - Improve code readability and maintainability - Maintain full backward compatibility Technical details: - String-based configuration converted to RuleType enum internally - Default behavior preserved (network → mac → domain order) - Domain rules still override MAC/network rules regardless of order - Comprehensive test coverage for configuration integration The matching configuration is programmatically accessible but hidden from user configuration files until ready for public release.
221 lines
6.3 KiB
Go
221 lines
6.3 KiB
Go
package rulematcher
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/Control-D-Inc/ctrld/testhelper"
|
|
)
|
|
|
|
func TestMatchingEngine(t *testing.T) {
|
|
cfg := testhelper.SampleConfig(t)
|
|
// Convert Cidrs to IPNets like in the original test
|
|
for _, nc := range cfg.Network {
|
|
for _, cidr := range nc.Cidrs {
|
|
_, ipNet, err := net.ParseCIDR(cidr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
nc.IPNets = append(nc.IPNets, ipNet)
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
config *MatchingConfig
|
|
request *MatchRequest
|
|
expected *MatchingResult
|
|
}{
|
|
{
|
|
name: "Default config - network match first",
|
|
config: DefaultMatchingConfig(),
|
|
request: &MatchRequest{
|
|
SourceIP: net.ParseIP("192.168.0.1"),
|
|
SourceMac: "14:45:A0:67:83:0A",
|
|
Domain: "example.ru",
|
|
Policy: cfg.Listener["0"].Policy,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchingResult{
|
|
Upstreams: []string{"upstream.1"},
|
|
MatchedPolicy: "My Policy",
|
|
MatchedNetwork: "network.0 (unenforced)",
|
|
MatchedRule: "*.ru",
|
|
Matched: true,
|
|
SrcAddr: "192.168.0.1",
|
|
MatchedRuleType: "domain",
|
|
MatchingOrder: []RuleType{RuleTypeNetwork, RuleTypeMac, RuleTypeDomain},
|
|
},
|
|
},
|
|
{
|
|
name: "Custom order - domain first",
|
|
config: &MatchingConfig{
|
|
Order: []RuleType{RuleTypeDomain, RuleTypeNetwork, RuleTypeMac},
|
|
StopOnFirstMatch: true,
|
|
},
|
|
request: &MatchRequest{
|
|
SourceIP: net.ParseIP("192.168.0.1"),
|
|
SourceMac: "14:45:A0:67:83:0A",
|
|
Domain: "example.ru",
|
|
Policy: cfg.Listener["0"].Policy,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchingResult{
|
|
Upstreams: []string{"upstream.1"},
|
|
MatchedPolicy: "My Policy",
|
|
MatchedNetwork: "network.0 (unenforced)",
|
|
MatchedRule: "*.ru",
|
|
Matched: true,
|
|
SrcAddr: "192.168.0.1",
|
|
MatchedRuleType: "domain",
|
|
MatchingOrder: []RuleType{RuleTypeDomain, RuleTypeNetwork, RuleTypeMac},
|
|
},
|
|
},
|
|
{
|
|
name: "Custom order - MAC first",
|
|
config: &MatchingConfig{
|
|
Order: []RuleType{RuleTypeMac, RuleTypeNetwork, RuleTypeDomain},
|
|
StopOnFirstMatch: true,
|
|
},
|
|
request: &MatchRequest{
|
|
SourceIP: net.ParseIP("192.168.0.1"),
|
|
SourceMac: "14:45:A0:67:83:0A",
|
|
Domain: "example.ru",
|
|
Policy: cfg.Listener["0"].Policy,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchingResult{
|
|
Upstreams: []string{"upstream.1"},
|
|
MatchedPolicy: "My Policy",
|
|
MatchedNetwork: "network.0 (unenforced)",
|
|
MatchedRule: "*.ru",
|
|
Matched: true,
|
|
SrcAddr: "192.168.0.1",
|
|
MatchedRuleType: "domain",
|
|
MatchingOrder: []RuleType{RuleTypeMac, RuleTypeNetwork, RuleTypeDomain},
|
|
},
|
|
},
|
|
{
|
|
name: "No policy",
|
|
config: DefaultMatchingConfig(),
|
|
request: &MatchRequest{
|
|
SourceIP: net.ParseIP("192.168.0.1"),
|
|
SourceMac: "14:45:A0:67:83:0A",
|
|
Domain: "example.ru",
|
|
Policy: nil,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchingResult{
|
|
Upstreams: []string{},
|
|
MatchedPolicy: "no policy",
|
|
MatchedNetwork: "no network",
|
|
MatchedRule: "no rule",
|
|
Matched: false,
|
|
SrcAddr: "192.168.0.1",
|
|
MatchedRuleType: "",
|
|
MatchingOrder: []RuleType{RuleTypeNetwork, RuleTypeMac, RuleTypeDomain},
|
|
},
|
|
},
|
|
{
|
|
name: "No matches",
|
|
config: DefaultMatchingConfig(),
|
|
request: &MatchRequest{
|
|
SourceIP: net.ParseIP("10.0.0.1"),
|
|
SourceMac: "00:11:22:33:44:55",
|
|
Domain: "example.com",
|
|
Policy: cfg.Listener["0"].Policy,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchingResult{
|
|
Upstreams: []string{},
|
|
MatchedPolicy: "My Policy",
|
|
MatchedNetwork: "no network",
|
|
MatchedRule: "no rule",
|
|
Matched: false,
|
|
SrcAddr: "10.0.0.1",
|
|
MatchedRuleType: "",
|
|
MatchingOrder: []RuleType{RuleTypeNetwork, RuleTypeMac, RuleTypeDomain},
|
|
},
|
|
},
|
|
{
|
|
name: "MAC rule overrides network rule",
|
|
config: DefaultMatchingConfig(),
|
|
request: &MatchRequest{
|
|
SourceIP: net.ParseIP("192.168.0.1"),
|
|
SourceMac: "14:45:A0:67:83:0A",
|
|
Domain: "example.com", // This domain doesn't match any domain rules
|
|
Policy: cfg.Listener["0"].Policy,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchingResult{
|
|
Upstreams: []string{"upstream.2"},
|
|
MatchedPolicy: "My Policy",
|
|
MatchedNetwork: "14:45:a0:67:83:0a",
|
|
MatchedRule: "no rule",
|
|
Matched: true,
|
|
SrcAddr: "192.168.0.1",
|
|
MatchedRuleType: "mac",
|
|
MatchingOrder: []RuleType{RuleTypeNetwork, RuleTypeMac, RuleTypeDomain},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
engine := NewMatchingEngine(tc.config)
|
|
result := engine.FindUpstreams(context.Background(), tc.request)
|
|
|
|
assert.Equal(t, tc.expected.Upstreams, result.Upstreams)
|
|
assert.Equal(t, tc.expected.MatchedPolicy, result.MatchedPolicy)
|
|
assert.Equal(t, tc.expected.MatchedNetwork, result.MatchedNetwork)
|
|
assert.Equal(t, tc.expected.MatchedRule, result.MatchedRule)
|
|
assert.Equal(t, tc.expected.Matched, result.Matched)
|
|
assert.Equal(t, tc.expected.SrcAddr, result.SrcAddr)
|
|
assert.Equal(t, tc.expected.MatchedRuleType, result.MatchedRuleType)
|
|
assert.Equal(t, tc.expected.MatchingOrder, result.MatchingOrder)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDefaultMatchingConfig(t *testing.T) {
|
|
config := DefaultMatchingConfig()
|
|
|
|
assert.Equal(t, []RuleType{RuleTypeNetwork, RuleTypeMac, RuleTypeDomain}, config.Order)
|
|
assert.True(t, config.StopOnFirstMatch)
|
|
}
|
|
|
|
func TestMatchingEngineWithInvalidRuleType(t *testing.T) {
|
|
cfg := testhelper.SampleConfig(t)
|
|
// Convert Cidrs to IPNets like in the original test
|
|
for _, nc := range cfg.Network {
|
|
for _, cidr := range nc.Cidrs {
|
|
_, ipNet, err := net.ParseCIDR(cidr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
nc.IPNets = append(nc.IPNets, ipNet)
|
|
}
|
|
}
|
|
|
|
config := &MatchingConfig{
|
|
Order: []RuleType{RuleType("invalid"), RuleTypeNetwork},
|
|
StopOnFirstMatch: true,
|
|
}
|
|
|
|
engine := NewMatchingEngine(config)
|
|
request := &MatchRequest{
|
|
SourceIP: net.ParseIP("192.168.0.1"),
|
|
Policy: cfg.Listener["0"].Policy,
|
|
Config: cfg,
|
|
}
|
|
|
|
result := engine.FindUpstreams(context.Background(), request)
|
|
|
|
// Should still work, just skip the invalid rule type
|
|
assert.True(t, result.Matched)
|
|
assert.Equal(t, "network", result.MatchedRuleType)
|
|
}
|