route: merge rule_set branches into outer rules

Treat rule_set items as merged branches instead of standalone boolean
sub-items.

Evaluate each branch inside a referenced rule-set as if it were merged
into the outer rule and keep OR semantics between branches. This lets
outer grouped fields satisfy matching groups inside a branch without
introducing a standalone outer fallback or cross-branch state union.

Keep inherited grouped state outside inverted default and logical
branches. Negated rule-set branches now evaluate !(...) against their
own conditions and only reapply the outer grouped match after negation
succeeds, so configs like outer-group && !inner-condition continue to
work.

Add regression tests for same-group merged matches, cross-group and
extra-AND failures, DNS merged-branch behaviour, and inverted merged
branches. Update the route and DNS rule docs to clarify that rule-set
branches merge into the outer rule while keeping OR semantics between
branches.
This commit is contained in:
世界
2026-03-25 10:32:09 +08:00
parent fcdea284e1
commit fdfd0ffb07
10 changed files with 308 additions and 32 deletions

View File

@@ -72,10 +72,18 @@ func (r *abstractDefaultRule) requiresDestinationAddressMatch(metadata *adapter.
}
func (r *abstractDefaultRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.matchStatesWithBase(metadata, 0)
}
func (r *abstractDefaultRule) matchStatesWithBase(metadata *adapter.InboundContext, inheritedBase ruleMatchState) ruleMatchStateSet {
if len(r.allItems) == 0 {
return emptyRuleMatchState()
return emptyRuleMatchState().withBase(inheritedBase)
}
var baseState ruleMatchState
evaluationBase := inheritedBase
if r.invert {
evaluationBase = 0
}
baseState := evaluationBase
if len(r.sourceAddressItems) > 0 {
metadata.DidMatch = true
if matchAnyItem(r.sourceAddressItems, metadata) {
@@ -119,17 +127,15 @@ func (r *abstractDefaultRule) matchStates(metadata *adapter.InboundContext) rule
for _, item := range r.items {
metadata.DidMatch = true
if !item.Match(metadata) {
return r.invertedFailure()
return r.invertedFailure(inheritedBase)
}
}
stateSet := singleRuleMatchState(baseState)
var stateSet ruleMatchStateSet
if r.ruleSetItem != nil {
metadata.DidMatch = true
ruleSetStates := matchRuleItemStates(r.ruleSetItem, metadata)
if ruleSetStates.isEmpty() {
return r.invertedFailure()
}
stateSet = ruleSetStates.withBase(baseState)
stateSet = matchRuleItemStatesWithBase(r.ruleSetItem, metadata, baseState)
} else {
stateSet = singleRuleMatchState(baseState)
}
stateSet = stateSet.filter(func(state ruleMatchState) bool {
if r.requiresSourceAddressMatch(metadata) && !state.has(ruleMatchSourceAddress) {
@@ -147,21 +153,21 @@ func (r *abstractDefaultRule) matchStates(metadata *adapter.InboundContext) rule
return true
})
if stateSet.isEmpty() {
return r.invertedFailure()
return r.invertedFailure(inheritedBase)
}
if r.invert {
// DNS pre-lookup defers destination address-limit checks until the response phase.
if metadata.IgnoreDestinationIPCIDRMatch && stateSet == emptyRuleMatchState() && !metadata.DidMatch && len(r.destinationIPCIDRItems) > 0 {
return emptyRuleMatchState()
return emptyRuleMatchState().withBase(inheritedBase)
}
return 0
}
return stateSet
}
func (r *abstractDefaultRule) invertedFailure() ruleMatchStateSet {
func (r *abstractDefaultRule) invertedFailure(base ruleMatchState) ruleMatchStateSet {
if r.invert {
return emptyRuleMatchState()
return emptyRuleMatchState().withBase(base)
}
return 0
}
@@ -225,16 +231,24 @@ func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool {
}
func (r *abstractLogicalRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.matchStatesWithBase(metadata, 0)
}
func (r *abstractLogicalRule) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
evaluationBase := base
if r.invert {
evaluationBase = 0
}
var stateSet ruleMatchStateSet
if r.mode == C.LogicalTypeAnd {
stateSet = emptyRuleMatchState()
stateSet = emptyRuleMatchState().withBase(evaluationBase)
for _, rule := range r.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleCache()
nestedStateSet := matchHeadlessRuleStates(rule, &nestedMetadata)
nestedStateSet := matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase)
if nestedStateSet.isEmpty() {
if r.invert {
return emptyRuleMatchState()
return emptyRuleMatchState().withBase(base)
}
return 0
}
@@ -244,11 +258,11 @@ func (r *abstractLogicalRule) matchStates(metadata *adapter.InboundContext) rule
for _, rule := range r.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleCache()
stateSet = stateSet.merge(matchHeadlessRuleStates(rule, &nestedMetadata))
stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase))
}
if stateSet.isEmpty() {
if r.invert {
return emptyRuleMatchState()
return emptyRuleMatchState().withBase(base)
}
return 0
}