feat: add configurable rule matching with improved code structure

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.
This commit is contained in:
Cuong Manh Le
2025-09-16 18:52:42 +07:00
committed by Cuong Manh Le
parent 6294ba4028
commit c365051732
5 changed files with 220 additions and 126 deletions
+31 -38
View File
@@ -29,8 +29,7 @@ func NewMatchingEngine(config *MatchingConfig) *MatchingEngine {
}
// FindUpstreams determines which upstreams should handle a request based on policy rules
// It evaluates rules in the configured order and returns the first match (if StopOnFirstMatch is true)
// or all matches (if StopOnFirstMatch is false)
// It implements the original behavior where MAC and domain rules can override network rules
func (e *MatchingEngine) FindUpstreams(ctx context.Context, req *MatchRequest) *MatchingResult {
result := &MatchingResult{
Upstreams: []string{},
@@ -49,9 +48,11 @@ func (e *MatchingEngine) FindUpstreams(ctx context.Context, req *MatchRequest) *
result.MatchedPolicy = req.Policy.Name
var allMatches []*MatchResult
var networkMatch *MatchResult
var macMatch *MatchResult
var domainMatch *MatchResult
// Evaluate rules in the configured order
// Check all rule types and store matches
for _, ruleType := range e.config.Order {
matcher, exists := e.matchers[ruleType]
if !exists {
@@ -60,46 +61,38 @@ func (e *MatchingEngine) FindUpstreams(ctx context.Context, req *MatchRequest) *
matchResult := matcher.Match(ctx, req)
if matchResult.Matched {
allMatches = append(allMatches, matchResult)
// If we should stop on first match, return immediately
if e.config.StopOnFirstMatch {
result.Upstreams = matchResult.Targets
result.Matched = true
result.MatchedRuleType = string(matchResult.RuleType)
// Set the appropriate matched field based on rule type
switch matchResult.RuleType {
case RuleTypeNetwork:
result.MatchedNetwork = matchResult.MatchedRule
case RuleTypeMac:
result.MatchedNetwork = matchResult.MatchedRule
case RuleTypeDomain:
result.MatchedRule = matchResult.MatchedRule
}
return result
switch matchResult.RuleType {
case RuleTypeNetwork:
networkMatch = matchResult
case RuleTypeMac:
macMatch = matchResult
case RuleTypeDomain:
domainMatch = matchResult
}
}
}
// If we get here, either no matches were found or StopOnFirstMatch is false
if len(allMatches) > 0 {
// For now, we'll use the first match's targets
// In the future, we could implement more sophisticated target merging
result.Upstreams = allMatches[0].Targets
// Determine the final match based on original logic:
// Domain rules override everything, MAC rules override network rules
if domainMatch != nil {
result.Upstreams = domainMatch.Targets
result.Matched = true
result.MatchedRuleType = string(allMatches[0].RuleType)
// Set the appropriate matched field based on rule type
switch allMatches[0].RuleType {
case RuleTypeNetwork:
result.MatchedNetwork = allMatches[0].MatchedRule
case RuleTypeMac:
result.MatchedNetwork = allMatches[0].MatchedRule
case RuleTypeDomain:
result.MatchedRule = allMatches[0].MatchedRule
result.MatchedRuleType = string(domainMatch.RuleType)
result.MatchedRule = domainMatch.MatchedRule
// Special case: domain rules override network rules
if networkMatch != nil {
result.MatchedNetwork = networkMatch.MatchedRule + " (unenforced)"
}
} else if macMatch != nil {
result.Upstreams = macMatch.Targets
result.Matched = true
result.MatchedRuleType = string(macMatch.RuleType)
result.MatchedNetwork = macMatch.MatchedRule
} else if networkMatch != nil {
result.Upstreams = networkMatch.Targets
result.Matched = true
result.MatchedRuleType = string(networkMatch.RuleType)
result.MatchedNetwork = networkMatch.MatchedRule
}
return result
+15 -15
View File
@@ -40,13 +40,13 @@ func TestMatchingEngine(t *testing.T) {
Config: cfg,
},
expected: &MatchingResult{
Upstreams: []string{"upstream.1", "upstream.0"},
Upstreams: []string{"upstream.1"},
MatchedPolicy: "My Policy",
MatchedNetwork: "network.0",
MatchedRule: "no rule",
MatchedNetwork: "network.0 (unenforced)",
MatchedRule: "*.ru",
Matched: true,
SrcAddr: "192.168.0.1",
MatchedRuleType: "network",
MatchedRuleType: "domain",
MatchingOrder: []RuleType{RuleTypeNetwork, RuleTypeMac, RuleTypeDomain},
},
},
@@ -66,7 +66,7 @@ func TestMatchingEngine(t *testing.T) {
expected: &MatchingResult{
Upstreams: []string{"upstream.1"},
MatchedPolicy: "My Policy",
MatchedNetwork: "no network",
MatchedNetwork: "network.0 (unenforced)",
MatchedRule: "*.ru",
Matched: true,
SrcAddr: "192.168.0.1",
@@ -88,13 +88,13 @@ func TestMatchingEngine(t *testing.T) {
Config: cfg,
},
expected: &MatchingResult{
Upstreams: []string{"upstream.2"},
Upstreams: []string{"upstream.1"},
MatchedPolicy: "My Policy",
MatchedNetwork: "14:45:a0:67:83:0a",
MatchedRule: "no rule",
MatchedNetwork: "network.0 (unenforced)",
MatchedRule: "*.ru",
Matched: true,
SrcAddr: "192.168.0.1",
MatchedRuleType: "mac",
MatchedRuleType: "domain",
MatchingOrder: []RuleType{RuleTypeMac, RuleTypeNetwork, RuleTypeDomain},
},
},
@@ -141,23 +141,23 @@ func TestMatchingEngine(t *testing.T) {
},
},
{
name: "Nil config uses default",
config: nil,
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.ru",
Domain: "example.com", // This domain doesn't match any domain rules
Policy: cfg.Listener["0"].Policy,
Config: cfg,
},
expected: &MatchingResult{
Upstreams: []string{"upstream.1", "upstream.0"},
Upstreams: []string{"upstream.2"},
MatchedPolicy: "My Policy",
MatchedNetwork: "network.0",
MatchedNetwork: "14:45:a0:67:83:0a",
MatchedRule: "no rule",
Matched: true,
SrcAddr: "192.168.0.1",
MatchedRuleType: "network",
MatchedRuleType: "mac",
MatchingOrder: []RuleType{RuleTypeNetwork, RuleTypeMac, RuleTypeDomain},
},
},