Files
SingboxForPanel/route/rule/rule_item_rule_set.go
世界 b0c6762bc1 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.
2026-03-25 14:00:29 +08:00

79 lines
2.1 KiB
Go

package rule
import (
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
var _ RuleItem = (*RuleSetItem)(nil)
type RuleSetItem struct {
router adapter.Router
tagList []string
setList []adapter.RuleSet
ipCidrMatchSource bool
ipCidrAcceptEmpty bool
}
func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool, ipCidrAcceptEmpty bool) *RuleSetItem {
return &RuleSetItem{
router: router,
tagList: tagList,
ipCidrMatchSource: ipCIDRMatchSource,
ipCidrAcceptEmpty: ipCidrAcceptEmpty,
}
}
func (r *RuleSetItem) Start() error {
for _, tag := range r.tagList {
ruleSet, loaded := r.router.RuleSet(tag)
if !loaded {
return E.New("rule-set not found: ", tag)
}
ruleSet.IncRef()
r.setList = append(r.setList, ruleSet)
}
return nil
}
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 := *metadata
nestedMetadata.ResetRuleMatchCache()
nestedMetadata.IPCIDRMatchSource = r.ipCidrMatchSource
nestedMetadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty
stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(ruleSet, &nestedMetadata, base))
}
return stateSet
}
func (r *RuleSetItem) ContainsDestinationIPCIDRRule() bool {
if r.ipCidrMatchSource {
return false
}
return common.Any(r.setList, func(ruleSet adapter.RuleSet) bool {
return ruleSet.Metadata().ContainsIPCIDRRule
})
}
func (r *RuleSetItem) String() string {
if len(r.tagList) == 1 {
return F.ToString("rule_set=", r.tagList[0])
} else {
return F.ToString("rule_set=[", strings.Join(r.tagList, " "), "]")
}
}