mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
Extract DNS policy rule matching logic from dns_proxy.go into a dedicated internal/rulematcher package to improve code organization and maintainability. The new package provides: - RuleMatcher interface for extensible rule matching - NetworkRuleMatcher for IP-based network rules - MacRuleMatcher for MAC address-based rules - DomainRuleMatcher for domain/wildcard rules - Comprehensive unit tests for all matchers This refactoring improves: - Separation of concerns between DNS proxy and rule matching - Testability with isolated rule matcher components - Reusability of rule matching logic across the codebase - Maintainability with focused, single-responsibility modules
249 lines
5.9 KiB
Go
249 lines
5.9 KiB
Go
package rulematcher
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
"github.com/Control-D-Inc/ctrld/testhelper"
|
|
)
|
|
|
|
// Test NetworkRuleMatcher
|
|
func TestNetworkRuleMatcher(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)
|
|
}
|
|
}
|
|
matcher := &NetworkRuleMatcher{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
request *MatchRequest
|
|
expected *MatchResult
|
|
}{
|
|
{
|
|
name: "No policy",
|
|
request: &MatchRequest{
|
|
SourceIP: net.ParseIP("192.168.0.1"),
|
|
Policy: nil,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchResult{Matched: false, RuleType: RuleTypeNetwork},
|
|
},
|
|
{
|
|
name: "No network rules",
|
|
request: &MatchRequest{
|
|
SourceIP: net.ParseIP("192.168.0.1"),
|
|
Policy: &ctrld.ListenerPolicyConfig{},
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchResult{Matched: false, RuleType: RuleTypeNetwork},
|
|
},
|
|
{
|
|
name: "Match network rule",
|
|
request: &MatchRequest{
|
|
SourceIP: net.ParseIP("192.168.0.1"),
|
|
Policy: cfg.Listener["0"].Policy,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchResult{
|
|
Matched: true,
|
|
Targets: []string{"upstream.1", "upstream.0"},
|
|
MatchedRule: "network.0",
|
|
RuleType: RuleTypeNetwork,
|
|
},
|
|
},
|
|
{
|
|
name: "No match for IP",
|
|
request: &MatchRequest{
|
|
SourceIP: net.ParseIP("10.0.0.1"),
|
|
Policy: cfg.Listener["0"].Policy,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchResult{Matched: false, RuleType: RuleTypeNetwork},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := matcher.Match(context.Background(), tc.request)
|
|
assert.Equal(t, tc.expected.Matched, result.Matched)
|
|
assert.Equal(t, tc.expected.RuleType, result.RuleType)
|
|
if tc.expected.Matched {
|
|
assert.Equal(t, tc.expected.Targets, result.Targets)
|
|
assert.Equal(t, tc.expected.MatchedRule, result.MatchedRule)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test MacRuleMatcher
|
|
func TestMacRuleMatcher(t *testing.T) {
|
|
cfg := testhelper.SampleConfig(t)
|
|
matcher := &MacRuleMatcher{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
request *MatchRequest
|
|
expected *MatchResult
|
|
}{
|
|
{
|
|
name: "No policy",
|
|
request: &MatchRequest{
|
|
SourceMac: "14:45:A0:67:83:0A",
|
|
Policy: nil,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchResult{Matched: false, RuleType: RuleTypeMac},
|
|
},
|
|
{
|
|
name: "No MAC rules",
|
|
request: &MatchRequest{
|
|
SourceMac: "14:45:A0:67:83:0A",
|
|
Policy: &ctrld.ListenerPolicyConfig{},
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchResult{Matched: false, RuleType: RuleTypeMac},
|
|
},
|
|
{
|
|
name: "Match MAC rule - exact",
|
|
request: &MatchRequest{
|
|
SourceMac: "14:45:A0:67:83:0A",
|
|
Policy: cfg.Listener["0"].Policy,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchResult{
|
|
Matched: true,
|
|
Targets: []string{"upstream.2"},
|
|
MatchedRule: "14:45:a0:67:83:0a", // Config loading normalizes MAC addresses to lowercase
|
|
RuleType: RuleTypeMac,
|
|
},
|
|
},
|
|
{
|
|
name: "Match MAC rule - case insensitive",
|
|
request: &MatchRequest{
|
|
SourceMac: "14:54:4a:8e:08:2d",
|
|
Policy: cfg.Listener["0"].Policy,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchResult{
|
|
Matched: true,
|
|
Targets: []string{"upstream.2"},
|
|
MatchedRule: "14:54:4a:8e:08:2d",
|
|
RuleType: RuleTypeMac,
|
|
},
|
|
},
|
|
{
|
|
name: "No match for MAC",
|
|
request: &MatchRequest{
|
|
SourceMac: "00:11:22:33:44:55",
|
|
Policy: cfg.Listener["0"].Policy,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchResult{Matched: false, RuleType: RuleTypeMac},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := matcher.Match(context.Background(), tc.request)
|
|
assert.Equal(t, tc.expected.Matched, result.Matched)
|
|
assert.Equal(t, tc.expected.RuleType, result.RuleType)
|
|
if tc.expected.Matched {
|
|
assert.Equal(t, tc.expected.Targets, result.Targets)
|
|
assert.Equal(t, tc.expected.MatchedRule, result.MatchedRule)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test DomainRuleMatcher
|
|
func TestDomainRuleMatcher(t *testing.T) {
|
|
cfg := testhelper.SampleConfig(t)
|
|
matcher := &DomainRuleMatcher{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
request *MatchRequest
|
|
expected *MatchResult
|
|
}{
|
|
{
|
|
name: "No policy",
|
|
request: &MatchRequest{
|
|
Domain: "example.com",
|
|
Policy: nil,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchResult{Matched: false, RuleType: RuleTypeDomain},
|
|
},
|
|
{
|
|
name: "No domain rules",
|
|
request: &MatchRequest{
|
|
Domain: "example.com",
|
|
Policy: &ctrld.ListenerPolicyConfig{},
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchResult{Matched: false, RuleType: RuleTypeDomain},
|
|
},
|
|
{
|
|
name: "Match domain rule - exact",
|
|
request: &MatchRequest{
|
|
Domain: "example.ru",
|
|
Policy: cfg.Listener["0"].Policy,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchResult{
|
|
Matched: true,
|
|
Targets: []string{"upstream.1"},
|
|
MatchedRule: "*.ru",
|
|
RuleType: RuleTypeDomain,
|
|
},
|
|
},
|
|
{
|
|
name: "Match domain rule - wildcard",
|
|
request: &MatchRequest{
|
|
Domain: "test.ru",
|
|
Policy: cfg.Listener["0"].Policy,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchResult{
|
|
Matched: true,
|
|
Targets: []string{"upstream.1"},
|
|
MatchedRule: "*.ru",
|
|
RuleType: RuleTypeDomain,
|
|
},
|
|
},
|
|
{
|
|
name: "No match for domain",
|
|
request: &MatchRequest{
|
|
Domain: "example.com",
|
|
Policy: cfg.Listener["0"].Policy,
|
|
Config: cfg,
|
|
},
|
|
expected: &MatchResult{Matched: false, RuleType: RuleTypeDomain},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := matcher.Match(context.Background(), tc.request)
|
|
assert.Equal(t, tc.expected.Matched, result.Matched)
|
|
assert.Equal(t, tc.expected.RuleType, result.RuleType)
|
|
if tc.expected.Matched {
|
|
assert.Equal(t, tc.expected.Targets, result.Targets)
|
|
assert.Equal(t, tc.expected.MatchedRule, result.MatchedRule)
|
|
}
|
|
})
|
|
}
|
|
}
|