Files
ctrld/internal/rulematcher/engine.go
Cuong Manh Le adc0e1a51e feat: add configurable rule matching engine
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.
2025-10-09 19:12:06 +07:00

119 lines
3.2 KiB
Go

package rulematcher
import (
"context"
)
// MatchingEngine orchestrates rule matching based on configurable order
type MatchingEngine struct {
config *MatchingConfig
matchers map[RuleType]RuleMatcher
}
// NewMatchingEngine creates a new matching engine with the given configuration
func NewMatchingEngine(config *MatchingConfig) *MatchingEngine {
if config == nil {
config = DefaultMatchingConfig()
}
engine := &MatchingEngine{
config: config,
matchers: map[RuleType]RuleMatcher{
RuleTypeNetwork: &NetworkRuleMatcher{},
RuleTypeMac: &MacRuleMatcher{},
RuleTypeDomain: &DomainRuleMatcher{},
},
}
return engine
}
// 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)
func (e *MatchingEngine) FindUpstreams(ctx context.Context, req *MatchRequest) *MatchingResult {
result := &MatchingResult{
Upstreams: []string{},
MatchedPolicy: "no policy",
MatchedNetwork: "no network",
MatchedRule: "no rule",
Matched: false,
SrcAddr: req.SourceIP.String(),
MatchedRuleType: "",
MatchingOrder: e.config.Order,
}
if req.Policy == nil {
return result
}
result.MatchedPolicy = req.Policy.Name
var allMatches []*MatchResult
// Evaluate rules in the configured order
for _, ruleType := range e.config.Order {
matcher, exists := e.matchers[ruleType]
if !exists {
continue
}
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
}
}
}
// 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
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
}
}
return result
}
// MatchingResult represents the result of the matching engine
type MatchingResult struct {
Upstreams []string
MatchedPolicy string
MatchedNetwork string
MatchedRule string
Matched bool
SrcAddr string
MatchedRuleType string
MatchingOrder []RuleType
}