mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-05-27 12:52:27 +02:00
6294ba4028
Implement MatchingEngine in internal/rulematcher package to enable configurable DNS policy rule evaluation order and behavior. New components: - MatchingConfig: Configuration for rule order and stop behavior - MatchingEngine: Orchestrates rule matching with configurable order - MatchingResult: Standardized result structure - DefaultMatchingConfig(): Maintains backward compatibility Key features: - Configurable rule evaluation order (e.g., domain-first, MAC-first) - StopOnFirstMatch configuration option - Graceful handling of invalid rule types - Comprehensive test coverage for all scenarios The engine supports custom matching strategies while preserving the default Networks → Macs → Domains order for backward compatibility. This enables future configuration-driven rule matching without breaking existing functionality.
221 lines
6.2 KiB
Go
221 lines
6.2 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", "upstream.0"},
|
|
MatchedPolicy: "My Policy",
|
|
MatchedNetwork: "network.0",
|
|
MatchedRule: "no rule",
|
|
Matched: true,
|
|
SrcAddr: "192.168.0.1",
|
|
MatchedRuleType: "network",
|
|
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: "no network",
|
|
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.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{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: "Nil config uses default",
|
|
config: nil,
|
|
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", "upstream.0"},
|
|
MatchedPolicy: "My Policy",
|
|
MatchedNetwork: "network.0",
|
|
MatchedRule: "no rule",
|
|
Matched: true,
|
|
SrcAddr: "192.168.0.1",
|
|
MatchedRuleType: "network",
|
|
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)
|
|
}
|