route: simplify rule_set matching semantics

Since b0c6762bc, every rule inside a referenced rule-set was evaluated
as if merged into the outer rule, which required tracking per-branch
group states and let outer rules and rule-set rules satisfy each
other's grouped conditions in both directions.

Restrict merging to the only designed case: a rule-set containing
exactly one non-inverted default rule is merged into the outer rule as
before. Any other rule-set now matches as an ordinary condition of the
outer rule: it matches when any of its rules matches on its own, and
its rules no longer exchange grouped match state with the outer rule
in either direction. Multiple referenced rule-sets keep OR semantics.

Flat address rule-sets such as generated geosite/geoip sets contain a
single default rule, so their behavior is unchanged. The group-state
set machinery is replaced by a single required/satisfied mask pair.

Also update the route and DNS rule docs.
This commit is contained in:
世界
2026-07-23 11:09:48 +08:00
parent 153b42ba28
commit 52e72736ad
13 changed files with 445 additions and 338 deletions
+1 -1
View File
@@ -242,7 +242,7 @@ icon: material/alert-decagram
(`source_port` || `source_port_range`) &&
`other fields`
Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics.
When a rule-set contains only a single default rule without `invert`, its fields are considered merged into the outer rule per the logic above; otherwise, it is matched as an `other field`; different rule-sets always keep OR semantics.
#### inbound
+2 -2
View File
@@ -240,9 +240,9 @@ icon: material/alert-decagram
(`port` || `port_range`) &&
(`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) &&
(`source_port` || `source_port_range`) &&
`other fields`
`其他字段`
另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义
当规则集仅包含一条默认规则且非 invert 时,其中字段视为按以上规则与外层规则合并;否则,作为一条 `其他字段` 匹配;不同规则集之间始终保持 or
#### inbound
+1 -1
View File
@@ -214,7 +214,7 @@ icon: material/new-box
(`source_port` || `source_port_range`) &&
`other fields`
Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics.
When a rule-set contains only a single default rule without `invert`, its fields are considered merged into the outer rule per the logic above; otherwise, it is matched as an `other field`; different rule-sets always keep OR semantics.
#### inbound
+2 -2
View File
@@ -210,9 +210,9 @@ icon: material/new-box
(`port` || `port_range`) &&
(`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) &&
(`source_port` || `source_port_range`) &&
`other fields`
`其他字段`
另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义
当规则集仅包含一条默认规则且非 invert 时,其中字段视为按以上规则与外层规则合并;否则,作为一条 `其他字段` 匹配;不同规则集之间始终保持 or
#### inbound
+9 -101
View File
@@ -1,7 +1,5 @@
package rule
import "github.com/sagernet/sing-box/adapter"
type ruleMatchState uint8
const (
@@ -11,108 +9,18 @@ const (
ruleMatchDestinationPort
)
type ruleMatchStateSet uint16
func singleRuleMatchState(state ruleMatchState) ruleMatchStateSet {
return 1 << state
type ruleGroupMatch struct {
required ruleMatchState
satisfied ruleMatchState
}
func emptyRuleMatchState() ruleMatchStateSet {
return singleRuleMatchState(0)
func (g ruleGroupMatch) done() bool {
return g.required&^g.satisfied == 0
}
func (s ruleMatchStateSet) isEmpty() bool {
return s == 0
}
func (s ruleMatchStateSet) contains(state ruleMatchState) bool {
return s&(1<<state) != 0
}
func (s ruleMatchStateSet) add(state ruleMatchState) ruleMatchStateSet {
return s | singleRuleMatchState(state)
}
func (s ruleMatchStateSet) merge(other ruleMatchStateSet) ruleMatchStateSet {
return s | other
}
func (s ruleMatchStateSet) combine(other ruleMatchStateSet) ruleMatchStateSet {
if s.isEmpty() || other.isEmpty() {
return 0
func (g ruleGroupMatch) mergeWith(other ruleGroupMatch) ruleGroupMatch {
return ruleGroupMatch{
required: g.required | other.required,
satisfied: g.satisfied | other.satisfied,
}
var combined ruleMatchStateSet
for left := range ruleMatchState(16) {
if !s.contains(left) {
continue
}
for right := range ruleMatchState(16) {
if !other.contains(right) {
continue
}
combined = combined.add(left | right)
}
}
return combined
}
func (s ruleMatchStateSet) withBase(base ruleMatchState) ruleMatchStateSet {
if s.isEmpty() {
return 0
}
var withBase ruleMatchStateSet
for state := range ruleMatchState(16) {
if !s.contains(state) {
continue
}
withBase = withBase.add(state | base)
}
return withBase
}
func (s ruleMatchStateSet) filter(allowed func(ruleMatchState) bool) ruleMatchStateSet {
var filtered ruleMatchStateSet
for state := range ruleMatchState(16) {
if !s.contains(state) {
continue
}
if allowed(state) {
filtered = filtered.add(state)
}
}
return filtered
}
type ruleStateMatcher interface {
matchStates(metadata *adapter.InboundContext) ruleMatchStateSet
}
type ruleStateMatcherWithBase interface {
matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet
}
func matchHeadlessRuleStatesWithBase(rule adapter.HeadlessRule, metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
if matcher, isStateMatcher := rule.(ruleStateMatcherWithBase); isStateMatcher {
return matcher.matchStatesWithBase(metadata, base)
}
if matcher, isStateMatcher := rule.(ruleStateMatcher); isStateMatcher {
return matcher.matchStates(metadata).withBase(base)
}
if rule.Match(metadata) {
return emptyRuleMatchState().withBase(base)
}
return 0
}
func matchRuleItemStatesWithBase(item RuleItem, metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
if matcher, isStateMatcher := item.(ruleStateMatcherWithBase); isStateMatcher {
return matcher.matchStatesWithBase(metadata, base)
}
if matcher, isStateMatcher := item.(ruleStateMatcher); isStateMatcher {
return matcher.matchStates(metadata).withBase(base)
}
if item.Match(metadata) {
return emptyRuleMatchState().withBase(base)
}
return 0
}
+67 -111
View File
@@ -18,7 +18,7 @@ type abstractDefaultRule struct {
destinationIPCIDRItems []RuleItem
destinationPortItems []RuleItem
allItems []RuleItem
ruleSetItem RuleItem
ruleSetItem *RuleSetItem
invert bool
action adapter.RuleAction
}
@@ -52,7 +52,43 @@ func (r *abstractDefaultRule) Close() error {
}
func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
return !r.matchStates(metadata).isEmpty()
if len(r.allItems) == 0 {
return true
}
matched := r.matchInner(metadata)
if r.invert {
if matched && metadata.IgnoreDestinationIPCIDRMatch && !metadata.DidMatch && len(r.destinationIPCIDRItems) > 0 {
return true
}
return !matched
}
return matched
}
func (r *abstractDefaultRule) matchInner(metadata *adapter.InboundContext) bool {
groups := r.evaluateGroups(metadata)
for _, item := range r.items {
metadata.DidMatch = true
if !item.Match(metadata) {
return false
}
}
if r.ruleSetItem != nil {
metadata.DidMatch = true
return r.ruleSetItem.matchWithOuterGroups(metadata, groups)
}
return groups.done()
}
func (r *abstractDefaultRule) evaluateForMerge(metadata *adapter.InboundContext) (ruleGroupMatch, bool) {
groups := r.evaluateGroups(metadata)
for _, item := range r.items {
metadata.DidMatch = true
if !item.Match(metadata) {
return ruleGroupMatch{}, false
}
}
return groups, true
}
func (r *abstractDefaultRule) destinationIPCIDRMatchesSource(metadata *adapter.InboundContext) bool {
@@ -63,112 +99,51 @@ func (r *abstractDefaultRule) destinationIPCIDRMatchesDestination(metadata *adap
return !metadata.IgnoreDestinationIPCIDRMatch && !metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0
}
func (r *abstractDefaultRule) requiresSourceAddressMatch(metadata *adapter.InboundContext) bool {
return len(r.sourceAddressItems) > 0 || r.destinationIPCIDRMatchesSource(metadata)
}
func (r *abstractDefaultRule) requiresDestinationAddressMatch(metadata *adapter.InboundContext) bool {
return len(r.destinationAddressItems) > 0 || r.destinationIPCIDRMatchesDestination(metadata)
}
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().withBase(inheritedBase)
}
evaluationBase := inheritedBase
if r.invert {
evaluationBase = 0
}
baseState := evaluationBase
func (r *abstractDefaultRule) evaluateGroups(metadata *adapter.InboundContext) ruleGroupMatch {
var groups ruleGroupMatch
if len(r.sourceAddressItems) > 0 {
metadata.DidMatch = true
groups.required |= ruleMatchSourceAddress
if matchAnyItem(r.sourceAddressItems, metadata) {
baseState |= ruleMatchSourceAddress
groups.satisfied |= ruleMatchSourceAddress
}
}
if r.destinationIPCIDRMatchesSource(metadata) && !baseState.has(ruleMatchSourceAddress) {
if r.destinationIPCIDRMatchesSource(metadata) {
metadata.DidMatch = true
if matchAnyItem(r.destinationIPCIDRItems, metadata) {
baseState |= ruleMatchSourceAddress
groups.required |= ruleMatchSourceAddress
if !groups.satisfied.has(ruleMatchSourceAddress) && matchAnyItem(r.destinationIPCIDRItems, metadata) {
groups.satisfied |= ruleMatchSourceAddress
}
} else if r.destinationIPCIDRMatchesSource(metadata) {
metadata.DidMatch = true
}
if len(r.sourcePortItems) > 0 {
metadata.DidMatch = true
groups.required |= ruleMatchSourcePort
if matchAnyItem(r.sourcePortItems, metadata) {
baseState |= ruleMatchSourcePort
groups.satisfied |= ruleMatchSourcePort
}
}
if len(r.destinationAddressItems) > 0 {
metadata.DidMatch = true
groups.required |= ruleMatchDestinationAddress
if matchAnyItem(r.destinationAddressItems, metadata) {
baseState |= ruleMatchDestinationAddress
groups.satisfied |= ruleMatchDestinationAddress
}
}
if r.destinationIPCIDRMatchesDestination(metadata) && !baseState.has(ruleMatchDestinationAddress) {
if r.destinationIPCIDRMatchesDestination(metadata) {
metadata.DidMatch = true
if matchAnyItem(r.destinationIPCIDRItems, metadata) {
baseState |= ruleMatchDestinationAddress
groups.required |= ruleMatchDestinationAddress
if !groups.satisfied.has(ruleMatchDestinationAddress) && matchAnyItem(r.destinationIPCIDRItems, metadata) {
groups.satisfied |= ruleMatchDestinationAddress
}
} else if r.destinationIPCIDRMatchesDestination(metadata) {
metadata.DidMatch = true
}
if len(r.destinationPortItems) > 0 {
metadata.DidMatch = true
groups.required |= ruleMatchDestinationPort
if matchAnyItem(r.destinationPortItems, metadata) {
baseState |= ruleMatchDestinationPort
groups.satisfied |= ruleMatchDestinationPort
}
}
for _, item := range r.items {
metadata.DidMatch = true
if !item.Match(metadata) {
return r.invertedFailure(inheritedBase)
}
}
var stateSet ruleMatchStateSet
if r.ruleSetItem != nil {
metadata.DidMatch = true
stateSet = matchRuleItemStatesWithBase(r.ruleSetItem, metadata, baseState)
} else {
stateSet = singleRuleMatchState(baseState)
}
stateSet = stateSet.filter(func(state ruleMatchState) bool {
if r.requiresSourceAddressMatch(metadata) && !state.has(ruleMatchSourceAddress) {
return false
}
if len(r.sourcePortItems) > 0 && !state.has(ruleMatchSourcePort) {
return false
}
if r.requiresDestinationAddressMatch(metadata) && !state.has(ruleMatchDestinationAddress) {
return false
}
if len(r.destinationPortItems) > 0 && !state.has(ruleMatchDestinationPort) {
return false
}
return true
})
if stateSet.isEmpty() {
return r.invertedFailure(inheritedBase)
}
if r.invert {
if metadata.IgnoreDestinationIPCIDRMatch && stateSet == emptyRuleMatchState() && !metadata.DidMatch && len(r.destinationIPCIDRItems) > 0 {
return emptyRuleMatchState().withBase(inheritedBase)
}
return 0
}
return stateSet
}
func (r *abstractDefaultRule) invertedFailure(base ruleMatchState) ruleMatchStateSet {
if r.invert {
return emptyRuleMatchState().withBase(base)
}
return 0
return groups
}
func (r *abstractDefaultRule) Action() adapter.RuleAction {
@@ -226,50 +201,31 @@ func (r *abstractLogicalRule) Close() error {
}
func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool {
return !r.matchStates(metadata).isEmpty()
}
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
var matched bool
if r.mode == C.LogicalTypeAnd {
stateSet = emptyRuleMatchState().withBase(evaluationBase)
matched = true
for _, rule := range r.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleCache()
nestedStateSet := matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase)
if nestedStateSet.isEmpty() {
if r.invert {
return emptyRuleMatchState().withBase(base)
}
return 0
if !rule.Match(&nestedMetadata) {
matched = false
break
}
stateSet = stateSet.combine(nestedStateSet)
}
} else {
for _, rule := range r.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleCache()
stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase))
}
if stateSet.isEmpty() {
if r.invert {
return emptyRuleMatchState().withBase(base)
if rule.Match(&nestedMetadata) {
matched = true
break
}
return 0
}
}
if r.invert {
return 0
return !matched
}
return stateSet
return matched
}
func (r *abstractLogicalRule) Action() adapter.RuleAction {
-8
View File
@@ -47,10 +47,6 @@ type DefaultRule struct {
abstractDefaultRule
}
func (r *DefaultRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.abstractDefaultRule.matchStates(metadata)
}
type RuleItem interface {
Match(metadata *adapter.InboundContext) bool
String() string
@@ -309,10 +305,6 @@ type LogicalRule struct {
abstractLogicalRule
}
func (r *LogicalRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.abstractLogicalRule.matchStates(metadata)
}
func NewLogicalRule(ctx context.Context, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) {
action, err := NewRuleAction(ctx, logger, options.RuleAction)
if err != nil {
+26 -47
View File
@@ -72,10 +72,6 @@ type DefaultDNSRule struct {
matchResponse bool
}
func (r *DefaultDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.abstractDefaultRule.matchStates(metadata)
}
func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options option.DefaultDNSRule, legacyDNSMode bool) (*DefaultDNSRule, error) {
rule := &DefaultDNSRule{
abstractDefaultRule: abstractDefaultRule{
@@ -365,17 +361,11 @@ func (r *DefaultDNSRule) WithAddressLimit() bool {
if len(r.destinationIPCIDRItems) > 0 {
return true
}
if r.ruleSetItem != nil {
ruleSet, isRuleSet := r.ruleSetItem.(*RuleSetItem)
if isRuleSet && ruleSet.ContainsDestinationIPCIDRRule() {
return true
}
}
return false
return r.ruleSetItem != nil && r.ruleSetItem.ContainsDestinationIPCIDRRule()
}
func (r *DefaultDNSRule) Match(metadata *adapter.InboundContext) bool {
return !r.matchStatesForMatch(metadata).isEmpty()
return r.matchForMatch(metadata)
}
func (r *DefaultDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool {
@@ -384,26 +374,26 @@ func (r *DefaultDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool {
}
metadata.IgnoreDestinationIPCIDRMatch = true
defer func() { metadata.IgnoreDestinationIPCIDRMatch = false }()
return !r.abstractDefaultRule.matchStates(metadata).isEmpty()
return r.abstractDefaultRule.Match(metadata)
}
func (r *DefaultDNSRule) matchStatesForMatch(metadata *adapter.InboundContext) ruleMatchStateSet {
func (r *DefaultDNSRule) matchForMatch(metadata *adapter.InboundContext) bool {
if r.matchResponse {
if metadata.DNSResponse == nil {
return r.abstractDefaultRule.invertedFailure(0)
return r.invert
}
matchMetadata := *metadata
matchMetadata.DestinationAddressMatchFromResponse = true
return r.abstractDefaultRule.matchStates(&matchMetadata)
return r.abstractDefaultRule.Match(&matchMetadata)
}
return r.abstractDefaultRule.matchStates(metadata)
return r.abstractDefaultRule.Match(metadata)
}
func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool {
matchMetadata := *metadata
matchMetadata.DNSResponse = response
matchMetadata.DestinationAddressMatchFromResponse = true
return !r.abstractDefaultRule.matchStates(&matchMetadata).isEmpty()
return r.abstractDefaultRule.Match(&matchMetadata)
}
var _ adapter.DNSRule = (*LogicalDNSRule)(nil)
@@ -412,54 +402,43 @@ type LogicalDNSRule struct {
abstractLogicalRule
}
func (r *LogicalDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.abstractLogicalRule.matchStates(metadata)
}
func matchDNSHeadlessRuleStatesForMatch(rule adapter.HeadlessRule, metadata *adapter.InboundContext) ruleMatchStateSet {
func matchDNSHeadlessRuleForMatch(rule adapter.HeadlessRule, metadata *adapter.InboundContext) bool {
switch typedRule := rule.(type) {
case *DefaultDNSRule:
return typedRule.matchStatesForMatch(metadata)
return typedRule.matchForMatch(metadata)
case *LogicalDNSRule:
return typedRule.matchStatesForMatch(metadata)
return typedRule.matchForMatch(metadata)
default:
return matchHeadlessRuleStatesWithBase(typedRule, metadata, 0)
return typedRule.Match(metadata)
}
}
func (r *LogicalDNSRule) matchStatesForMatch(metadata *adapter.InboundContext) ruleMatchStateSet {
var stateSet ruleMatchStateSet
func (r *LogicalDNSRule) matchForMatch(metadata *adapter.InboundContext) bool {
var matched bool
if r.mode == C.LogicalTypeAnd {
stateSet = emptyRuleMatchState()
matched = true
for _, rule := range r.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleCache()
nestedStateSet := matchDNSHeadlessRuleStatesForMatch(rule, &nestedMetadata)
if nestedStateSet.isEmpty() {
if r.invert {
return emptyRuleMatchState()
}
return 0
if !matchDNSHeadlessRuleForMatch(rule, &nestedMetadata) {
matched = false
break
}
stateSet = stateSet.combine(nestedStateSet)
}
} else {
for _, rule := range r.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleCache()
stateSet = stateSet.merge(matchDNSHeadlessRuleStatesForMatch(rule, &nestedMetadata))
}
if stateSet.isEmpty() {
if r.invert {
return emptyRuleMatchState()
if matchDNSHeadlessRuleForMatch(rule, &nestedMetadata) {
matched = true
break
}
return 0
}
}
if r.invert {
return 0
return !matched
}
return stateSet
return matched
}
func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options option.LogicalDNSRule, legacyDNSMode bool) (*LogicalDNSRule, error) {
@@ -513,18 +492,18 @@ func (r *LogicalDNSRule) WithAddressLimit() bool {
}
func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool {
return !r.matchStatesForMatch(metadata).isEmpty()
return r.matchForMatch(metadata)
}
func (r *LogicalDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool {
metadata.IgnoreDestinationIPCIDRMatch = true
defer func() { metadata.IgnoreDestinationIPCIDRMatch = false }()
return !r.abstractLogicalRule.matchStates(metadata).isEmpty()
return r.abstractLogicalRule.Match(metadata)
}
func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool {
matchMetadata := *metadata
matchMetadata.DNSResponse = response
matchMetadata.DestinationAddressMatchFromResponse = true
return !r.abstractLogicalRule.matchStates(&matchMetadata).isEmpty()
return r.abstractLogicalRule.Match(&matchMetadata)
}
-8
View File
@@ -34,10 +34,6 @@ type DefaultHeadlessRule struct {
abstractDefaultRule
}
func (r *DefaultHeadlessRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.abstractDefaultRule.matchStates(metadata)
}
func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) {
networkManager := service.FromContext[adapter.NetworkManager](ctx)
rule := &DefaultHeadlessRule{
@@ -216,10 +212,6 @@ type LogicalHeadlessRule struct {
abstractLogicalRule
}
func (r *LogicalHeadlessRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.abstractLogicalRule.matchStates(metadata)
}
func NewLogicalHeadlessRule(ctx context.Context, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) {
r := &LogicalHeadlessRule{
abstractLogicalRule{
+58 -13
View File
@@ -52,23 +52,68 @@ func (r *RuleSetItem) Close() error {
}
func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool {
return !r.matchStates(metadata).isEmpty()
}
func (r *RuleSetItem) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.matchStatesWithBase(metadata, 0)
}
func (r *RuleSetItem) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
var stateSet ruleMatchStateSet
for _, ruleSet := range r.setList {
nestedMetadata := r.nestedMetadata(metadata)
if ruleSet.Match(&nestedMetadata) {
return true
}
}
return false
}
func (r *RuleSetItem) matchWithOuterGroups(metadata *adapter.InboundContext, outerGroups ruleGroupMatch) bool {
outerDone := outerGroups.done()
for _, ruleSet := range r.setList {
nestedMetadata := r.nestedMetadata(metadata)
if provider, isProvider := ruleSet.(mergeableRuleProvider); isProvider {
branch := provider.mergeableRule()
if branch != nil {
branchGroups, branchMatched := branch.evaluateForMerge(&nestedMetadata)
if branchMatched && outerGroups.mergeWith(branchGroups).done() {
return true
}
continue
}
}
if outerDone && ruleSet.Match(&nestedMetadata) {
return true
}
}
return false
}
func (r *RuleSetItem) nestedMetadata(metadata *adapter.InboundContext) adapter.InboundContext {
nestedMetadata := *metadata
nestedMetadata.ResetRuleMatchCache()
nestedMetadata.IPCIDRMatchSource = r.ipCidrMatchSource
nestedMetadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty
return nestedMetadata
}
type mergeableRuleProvider interface {
mergeableRule() *DefaultHeadlessRule
}
func mergeableRuleIn(rules []adapter.HeadlessRule) *DefaultHeadlessRule {
if len(rules) != 1 {
return nil
}
rule, isDefault := rules[0].(*DefaultHeadlessRule)
if !isDefault || rule.invert || rule.ruleSetItem != nil {
return nil
}
return rule
}
func matchAnyHeadlessRule(rules []adapter.HeadlessRule, metadata *adapter.InboundContext) bool {
for _, rule := range rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleMatchCache()
nestedMetadata.IPCIDRMatchSource = r.ipCidrMatchSource
nestedMetadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty
stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(ruleSet, &nestedMetadata, base))
if rule.Match(&nestedMetadata) {
return true
}
}
return stateSet
return false
}
func (r *RuleSetItem) ContainsDestinationIPCIDRRule() bool {
+3 -13
View File
@@ -199,19 +199,9 @@ func (s *LocalRuleSet) Close() error {
}
func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool {
return !s.matchStates(metadata).isEmpty()
return matchAnyHeadlessRule(s.rules, metadata)
}
func (s *LocalRuleSet) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return s.matchStatesWithBase(metadata, 0)
}
func (s *LocalRuleSet) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
var stateSet ruleMatchStateSet
for _, rule := range s.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleMatchCache()
stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base))
}
return stateSet
func (s *LocalRuleSet) mergeableRule() *DefaultHeadlessRule {
return mergeableRuleIn(s.rules)
}
+3 -13
View File
@@ -297,19 +297,9 @@ func (s *RemoteRuleSet) Close() error {
}
func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool {
return !s.matchStates(metadata).isEmpty()
return matchAnyHeadlessRule(s.rules, metadata)
}
func (s *RemoteRuleSet) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
return s.matchStatesWithBase(metadata, 0)
}
func (s *RemoteRuleSet) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
var stateSet ruleMatchStateSet
for _, rule := range s.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleMatchCache()
stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base))
}
return stateSet
func (s *RemoteRuleSet) mergeableRule() *DefaultHeadlessRule {
return mergeableRuleIn(s.rules)
}
+273 -18
View File
@@ -295,11 +295,11 @@ func TestRouteRuleSetOrSemantics(t *testing.T) {
})
require.True(t, rule.Match(&metadata))
})
t.Run("later rule in same set can satisfy outer group", func(t *testing.T) {
t.Run("multi rule set does not satisfy outer group", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.example.com")
ruleSet := newLocalRuleSetForTest(
"rule-set-or",
"rule-set-and",
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP}))
}),
@@ -311,7 +311,7 @@ func TestRouteRuleSetOrSemantics(t *testing.T) {
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
})
require.True(t, rule.Match(&metadata))
require.False(t, rule.Match(&metadata))
})
t.Run("cross ruleset union is not allowed", func(t *testing.T) {
t.Parallel()
@@ -333,7 +333,7 @@ func TestRouteRuleSetOrSemantics(t *testing.T) {
func TestRouteRuleSetLogicalSemantics(t *testing.T) {
t.Parallel()
t.Run("logical or keeps all successful branch states", func(t *testing.T) {
t.Run("logical set does not satisfy outer group", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.example.com")
ruleSet := newLocalRuleSetForTest("logical-or", headlessLogicalRule(
@@ -350,9 +350,9 @@ func TestRouteRuleSetLogicalSemantics(t *testing.T) {
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
})
require.True(t, rule.Match(&metadata))
require.False(t, rule.Match(&metadata))
})
t.Run("logical and unions child states", func(t *testing.T) {
t.Run("logical branch does not lift outer group requirements", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.example.com")
ruleSet := newLocalRuleSetForTest("logical-and", headlessLogicalRule(
@@ -370,9 +370,28 @@ func TestRouteRuleSetLogicalSemantics(t *testing.T) {
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
addSourcePortItem(rule, []uint16{2000})
})
require.False(t, rule.Match(&metadata))
})
t.Run("logical branch matches on its own conditions", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.example.com")
ruleSet := newLocalRuleSetForTest("logical-and-self", headlessLogicalRule(
C.LogicalTypeAnd,
false,
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
}),
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addSourcePortItem(rule, []uint16{1000})
}),
))
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
addSourcePortItem(rule, []uint16{1000})
})
require.True(t, rule.Match(&metadata))
})
t.Run("invert success does not contribute positive state", func(t *testing.T) {
t.Run("inverted set does not satisfy outer group", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.example.com")
ruleSet := newLocalRuleSetForTest("invert", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
@@ -387,9 +406,240 @@ func TestRouteRuleSetLogicalSemantics(t *testing.T) {
})
}
func TestRouteRuleSetInvertMergedBranchSemantics(t *testing.T) {
func TestRuleSetShapeBoundary(t *testing.T) {
t.Parallel()
t.Run("default invert keeps inherited group outside grouped predicate", func(t *testing.T) {
buildOuter := func(ruleSet *LocalRuleSet) *DefaultRule {
return routeRuleForTest(func(rule *abstractDefaultRule) {
addDestinationAddressItem(t, rule, []string{"extra.example.org"}, nil)
addDestinationPortItem(rule, []uint16{443})
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
})
}
singleShape := buildOuter(newLocalRuleSetForTest("flat-single", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationAddressItem(t, rule, nil, []string{"a.example.com", "b.example.com"})
})))
multiShape := buildOuter(newLocalRuleSetForTest(
"flat-multi",
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationAddressItem(t, rule, nil, []string{"a.example.com"})
}),
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationAddressItem(t, rule, nil, []string{"b.example.com"})
}),
))
queries := []struct {
name string
domain string
port uint16
singleResult bool
multiResult bool
}{
{"in set", "www.b.example.com", 443, true, false},
{"outer own domain", "extra.example.org", 443, true, false},
{"neither", "other.example.net", 443, false, false},
{"in set with wrong port", "www.b.example.com", 80, false, false},
}
for _, query := range queries {
t.Run(query.name, func(t *testing.T) {
t.Parallel()
singleMetadata := testMetadata(query.domain)
singleMetadata.Destination.Port = query.port
multiMetadata := testMetadata(query.domain)
multiMetadata.Destination.Port = query.port
require.Equal(t, query.singleResult, singleShape.Match(&singleMetadata))
require.Equal(t, query.multiResult, multiShape.Match(&multiMetadata))
})
}
}
func TestRuleSetCaseBoundary(t *testing.T) {
t.Parallel()
t.Run("single cross group rule merges into outer", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.example.com")
ruleSet := newLocalRuleSetForTest("port-single", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationPortRangeItem(t, rule, []string{"400:500"})
}))
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
addDestinationPortItem(rule, []uint16{8080})
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
})
require.True(t, rule.Match(&metadata))
})
t.Run("multi rule set keeps outer group absolute", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.example.com")
ruleSet := newLocalRuleSetForTest(
"port-multi",
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationPortRangeItem(t, rule, []string{"400:500"})
}),
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationAddressItem(t, rule, nil, []string{"never.example"})
}),
)
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
addDestinationPortItem(rule, []uint16{8080})
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
})
require.False(t, rule.Match(&metadata))
})
}
func TestRuleSetLogicalBranchSelfContained(t *testing.T) {
t.Parallel()
newRuleSet := func() *LocalRuleSet {
return newLocalRuleSetForTest("and-branch", headlessLogicalRule(
C.LogicalTypeAnd,
false,
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationAddressItem(t, rule, nil, []string{"b.example.com"})
}),
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationPortRangeItem(t, rule, []string{"800:900"})
}),
))
}
t.Run("branch matches only on its own conditions", func(t *testing.T) {
t.Parallel()
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{newRuleSet()}})
})
matchedMetadata := testMetadata("www.b.example.com")
matchedMetadata.Destination.Port = 850
require.True(t, rule.Match(&matchedMetadata))
unmatchedMetadata := testMetadata("www.b.example.com")
require.False(t, rule.Match(&unmatchedMetadata))
})
t.Run("outer condition and set are both required", func(t *testing.T) {
t.Parallel()
matchedRule := routeRuleForTest(func(rule *abstractDefaultRule) {
addSourcePortItem(rule, []uint16{1000})
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{newRuleSet()}})
})
metadata := testMetadata("www.b.example.com")
metadata.Destination.Port = 850
require.True(t, matchedRule.Match(&metadata))
setMissMetadata := testMetadata("other.example.net")
setMissMetadata.Destination.Port = 850
require.False(t, matchedRule.Match(&setMissMetadata))
outerMissRule := routeRuleForTest(func(rule *abstractDefaultRule) {
addSourcePortItem(rule, []uint16{2000})
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{newRuleSet()}})
})
outerMissMetadata := testMetadata("www.b.example.com")
outerMissMetadata.Destination.Port = 850
require.False(t, outerMissRule.Match(&outerMissMetadata))
})
}
func TestRuleSetMixedReference(t *testing.T) {
t.Parallel()
t.Run("single rule set merges as or", func(t *testing.T) {
t.Parallel()
ruleSet := newLocalRuleSetForTest("mixed-single", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationAddressItem(t, rule, nil, []string{"set.example.com"})
}))
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
addDestinationAddressItem(t, rule, []string{"extra.example.org"}, nil)
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
})
extraMetadata := testMetadata("extra.example.org")
require.True(t, rule.Match(&extraMetadata))
setMetadata := testMetadata("www.set.example.com")
require.True(t, rule.Match(&setMetadata))
otherMetadata := testMetadata("other.example.net")
require.False(t, rule.Match(&otherMetadata))
})
t.Run("multi rule set is an independent condition", func(t *testing.T) {
t.Parallel()
ruleSet := newLocalRuleSetForTest(
"mixed-multi",
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationAddressItem(t, rule, nil, []string{"set.example.com"})
}),
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationAddressItem(t, rule, nil, []string{"set2.example.com"})
}),
)
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
addDestinationPortItem(rule, []uint16{443})
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
})
matchedMetadata := testMetadata("www.set2.example.com")
require.True(t, rule.Match(&matchedMetadata))
portMissMetadata := testMetadata("www.set2.example.com")
portMissMetadata.Destination.Port = 80
require.False(t, rule.Match(&portMissMetadata))
setMissMetadata := testMetadata("other.example.net")
require.False(t, rule.Match(&setMissMetadata))
})
}
func TestRuleSetStandaloneReference(t *testing.T) {
t.Parallel()
ruleSet := newLocalRuleSetForTest(
"standalone",
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationAddressItem(t, rule, nil, []string{"a.example.net"})
}),
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationPortRangeItem(t, rule, []string{"400:500"})
}),
)
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
})
domainMetadata := testMetadata("www.a.example.net")
domainMetadata.Destination.Port = 80
require.True(t, rule.Match(&domainMetadata))
portMetadata := testMetadata("other.example.org")
require.True(t, rule.Match(&portMetadata))
missMetadata := testMetadata("other.example.org")
missMetadata.Destination.Port = 80
require.False(t, rule.Match(&missMetadata))
}
func TestRuleSetInvertedSingleRuleIsBoolean(t *testing.T) {
t.Parallel()
ruleSet := newLocalRuleSetForTest("inverted-single", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
rule.invert = true
addDestinationAddressItem(t, rule, nil, []string{"blocked.example"})
}))
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
})
allowedMetadata := testMetadata("good.example.org")
require.True(t, rule.Match(&allowedMetadata))
blockedMetadata := testMetadata("www.blocked.example")
require.False(t, rule.Match(&blockedMetadata))
}
func TestRuleSetEmptySetNeverMatches(t *testing.T) {
t.Parallel()
emptySet := newLocalRuleSetForTest("empty")
t.Run("outer own group does not bypass the set", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.example.com")
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{emptySet}})
})
require.False(t, rule.Match(&metadata))
})
t.Run("standalone reference does not match", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.example.com")
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{emptySet}})
})
require.False(t, rule.Match(&metadata))
})
}
func TestRouteRuleSetInvertBranchSemantics(t *testing.T) {
t.Parallel()
t.Run("inverted default branch acts as boolean term", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.example.com")
ruleSet := newLocalRuleSetForTest("invert-grouped", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
@@ -402,7 +652,7 @@ func TestRouteRuleSetInvertMergedBranchSemantics(t *testing.T) {
})
require.True(t, rule.Match(&metadata))
})
t.Run("default invert keeps inherited group after negation succeeds", func(t *testing.T) {
t.Run("inverted default branch with non grouped condition acts as boolean term", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.example.com")
ruleSet := newLocalRuleSetForTest("invert-network", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
@@ -415,7 +665,7 @@ func TestRouteRuleSetInvertMergedBranchSemantics(t *testing.T) {
})
require.True(t, rule.Match(&metadata))
})
t.Run("logical invert keeps inherited group outside grouped predicate", func(t *testing.T) {
t.Run("inverted logical branch acts as boolean term", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.example.com")
ruleSet := newLocalRuleSetForTest("logical-invert-grouped", headlessLogicalRule(
@@ -431,7 +681,7 @@ func TestRouteRuleSetInvertMergedBranchSemantics(t *testing.T) {
})
require.True(t, rule.Match(&metadata))
})
t.Run("logical invert keeps inherited group after negation succeeds", func(t *testing.T) {
t.Run("inverted logical branch with non grouped condition acts as boolean term", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.example.com")
ruleSet := newLocalRuleSetForTest("logical-invert-network", headlessLogicalRule(
@@ -498,21 +748,26 @@ func TestDefaultRuleDoesNotReuseGroupedMatchCacheAcrossEvaluations(t *testing.T)
func TestRouteRuleSetRemoteUsesSameSemantics(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.example.com")
ruleSet := newRemoteRuleSetForTest(
"remote",
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP}))
addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP}))
}),
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
}),
)
rule := routeRuleForTest(func(rule *abstractDefaultRule) {
standaloneRule := routeRuleForTest(func(rule *abstractDefaultRule) {
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
})
standaloneMetadata := testMetadata("www.example.com")
require.True(t, standaloneRule.Match(&standaloneMetadata))
combinedRule := routeRuleForTest(func(rule *abstractDefaultRule) {
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
})
require.True(t, rule.Match(&metadata))
combinedMetadata := testMetadata("www.example.com")
require.False(t, combinedRule.Match(&combinedMetadata))
}
func TestDNSRuleSetSemantics(t *testing.T) {
@@ -541,7 +796,7 @@ func TestDNSRuleSetSemantics(t *testing.T) {
})
require.False(t, rule.Match(&metadata))
})
t.Run("outer destination group stays outside inverted grouped branch", func(t *testing.T) {
t.Run("inverted branch acts as boolean term", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.baidu.com")
ruleSet := newLocalRuleSetForTest("dns-invert-grouped", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
@@ -554,7 +809,7 @@ func TestDNSRuleSetSemantics(t *testing.T) {
})
require.True(t, rule.Match(&metadata))
})
t.Run("outer destination group stays outside inverted logical branch", func(t *testing.T) {
t.Run("inverted logical branch acts as boolean term", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("www.example.com")
ruleSet := newLocalRuleSetForTest("dns-logical-invert-network", headlessLogicalRule(