Before795d1c289, nested rule-set evaluation reused the parent rule match cache. In practice, this meant these fields leaked across nested evaluation: - SourceAddressMatch - SourcePortMatch - DestinationAddressMatch - DestinationPortMatch - DidMatch That leak had two opposite effects. First, it made included rule-sets partially behave like the docs' "merged" semantics. For example, if an outer route rule had: rule_set = ["geosite-additional-!cn"] ip_cidr = 104.26.10.0/24 and the inline rule-set matched `domain_suffix = speedtest.net`, the inner match could set `DestinationAddressMatch = true` and the outer rule would then pass its destination-address group check. This is why some `rule_set + ip_cidr` combinations used to work. But the same leak also polluted sibling rules and sibling rule-sets. A branch could partially match one group, then fail later, and still leave that group cache set for the next branch. This broke cases such as gh-3485: with `rule_set = [test1, test2]`, `test1` could touch destination-address cache before an AdGuard `@@` exclusion made the whole branch fail, and `test2` would then run against dirty state.795d1c289fixed that by cloning metadata for nested rule-set/rule evaluation and resetting the rule match cache for each branch. That stopped sibling pollution, but it also removed the only mechanism by which a successful nested branch could affect the parent rule's grouped matching state. As a result, nested rule-sets became pure boolean sub-items against the outer rule. The previous example stopped working: the inner `domain_suffix = speedtest.net` still matched, but the outer rule no longer observed any destination-address-group success, so it fell through to `final`. This change makes the semantics explicit instead of relying on cache side effects: - `rule_set: ["a", "b"]` is OR - rules inside one rule-set are OR - each nested branch is evaluated in isolation - failed branches contribute no grouped match state - a successful branch contributes its grouped match state back to the parent rule - grouped state from different rule-sets must not be combined together to satisfy one outer rule In other words, rule-sets now behave as "OR branches whose successful group matches merge into the outer rule", which matches the documented intent without reintroducing cross-branch cache leakage.
290 lines
7.4 KiB
Go
290 lines
7.4 KiB
Go
package rule
|
|
|
|
import (
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/sagernet/sing-box/adapter"
|
|
C "github.com/sagernet/sing-box/constant"
|
|
"github.com/sagernet/sing/common"
|
|
F "github.com/sagernet/sing/common/format"
|
|
)
|
|
|
|
type abstractDefaultRule struct {
|
|
items []RuleItem
|
|
sourceAddressItems []RuleItem
|
|
sourcePortItems []RuleItem
|
|
destinationAddressItems []RuleItem
|
|
destinationIPCIDRItems []RuleItem
|
|
destinationPortItems []RuleItem
|
|
allItems []RuleItem
|
|
ruleSetItem RuleItem
|
|
invert bool
|
|
action adapter.RuleAction
|
|
}
|
|
|
|
func (r *abstractDefaultRule) Type() string {
|
|
return C.RuleTypeDefault
|
|
}
|
|
|
|
func (r *abstractDefaultRule) Start() error {
|
|
for _, item := range r.allItems {
|
|
if starter, isStarter := item.(interface {
|
|
Start() error
|
|
}); isStarter {
|
|
err := starter.Start()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *abstractDefaultRule) Close() error {
|
|
for _, item := range r.allItems {
|
|
err := common.Close(item)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
|
|
return !r.matchStates(metadata).isEmpty()
|
|
}
|
|
|
|
func (r *abstractDefaultRule) destinationIPCIDRMatchesSource(metadata *adapter.InboundContext) bool {
|
|
return !metadata.IgnoreDestinationIPCIDRMatch && metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0
|
|
}
|
|
|
|
func (r *abstractDefaultRule) destinationIPCIDRMatchesDestination(metadata *adapter.InboundContext) bool {
|
|
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 {
|
|
if len(r.allItems) == 0 {
|
|
return emptyRuleMatchState()
|
|
}
|
|
var baseState ruleMatchState
|
|
if len(r.sourceAddressItems) > 0 {
|
|
metadata.DidMatch = true
|
|
if matchAnyItem(r.sourceAddressItems, metadata) {
|
|
baseState |= ruleMatchSourceAddress
|
|
}
|
|
}
|
|
if r.destinationIPCIDRMatchesSource(metadata) && !baseState.has(ruleMatchSourceAddress) {
|
|
metadata.DidMatch = true
|
|
if matchAnyItem(r.destinationIPCIDRItems, metadata) {
|
|
baseState |= ruleMatchSourceAddress
|
|
}
|
|
} else if r.destinationIPCIDRMatchesSource(metadata) {
|
|
metadata.DidMatch = true
|
|
}
|
|
if len(r.sourcePortItems) > 0 {
|
|
metadata.DidMatch = true
|
|
if matchAnyItem(r.sourcePortItems, metadata) {
|
|
baseState |= ruleMatchSourcePort
|
|
}
|
|
}
|
|
if len(r.destinationAddressItems) > 0 {
|
|
metadata.DidMatch = true
|
|
if matchAnyItem(r.destinationAddressItems, metadata) {
|
|
baseState |= ruleMatchDestinationAddress
|
|
}
|
|
}
|
|
if r.destinationIPCIDRMatchesDestination(metadata) && !baseState.has(ruleMatchDestinationAddress) {
|
|
metadata.DidMatch = true
|
|
if matchAnyItem(r.destinationIPCIDRItems, metadata) {
|
|
baseState |= ruleMatchDestinationAddress
|
|
}
|
|
} else if r.destinationIPCIDRMatchesDestination(metadata) {
|
|
metadata.DidMatch = true
|
|
}
|
|
if len(r.destinationPortItems) > 0 {
|
|
metadata.DidMatch = true
|
|
if matchAnyItem(r.destinationPortItems, metadata) {
|
|
baseState |= ruleMatchDestinationPort
|
|
}
|
|
}
|
|
for _, item := range r.items {
|
|
metadata.DidMatch = true
|
|
if !item.Match(metadata) {
|
|
return r.invertedFailure()
|
|
}
|
|
}
|
|
stateSet := singleRuleMatchState(baseState)
|
|
if r.ruleSetItem != nil {
|
|
metadata.DidMatch = true
|
|
ruleSetStates := matchRuleItemStates(r.ruleSetItem, metadata)
|
|
if ruleSetStates.isEmpty() {
|
|
return r.invertedFailure()
|
|
}
|
|
stateSet = ruleSetStates.withBase(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()
|
|
}
|
|
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 0
|
|
}
|
|
return stateSet
|
|
}
|
|
|
|
func (r *abstractDefaultRule) invertedFailure() ruleMatchStateSet {
|
|
if r.invert {
|
|
return emptyRuleMatchState()
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (r *abstractDefaultRule) Action() adapter.RuleAction {
|
|
return r.action
|
|
}
|
|
|
|
func (r *abstractDefaultRule) String() string {
|
|
if !r.invert {
|
|
return strings.Join(F.MapToString(r.allItems), " ")
|
|
} else {
|
|
return "!(" + strings.Join(F.MapToString(r.allItems), " ") + ")"
|
|
}
|
|
}
|
|
|
|
type abstractLogicalRule struct {
|
|
rules []adapter.HeadlessRule
|
|
mode string
|
|
invert bool
|
|
action adapter.RuleAction
|
|
}
|
|
|
|
func (r *abstractLogicalRule) Type() string {
|
|
return C.RuleTypeLogical
|
|
}
|
|
|
|
func (r *abstractLogicalRule) Start() error {
|
|
for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (interface {
|
|
Start() error
|
|
}, bool,
|
|
) {
|
|
rule, loaded := it.(interface {
|
|
Start() error
|
|
})
|
|
return rule, loaded
|
|
}) {
|
|
err := rule.Start()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *abstractLogicalRule) Close() error {
|
|
for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (io.Closer, bool) {
|
|
rule, loaded := it.(io.Closer)
|
|
return rule, loaded
|
|
}) {
|
|
err := rule.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool {
|
|
return !r.matchStates(metadata).isEmpty()
|
|
}
|
|
|
|
func (r *abstractLogicalRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
|
|
var stateSet ruleMatchStateSet
|
|
if r.mode == C.LogicalTypeAnd {
|
|
stateSet = emptyRuleMatchState()
|
|
for _, rule := range r.rules {
|
|
nestedMetadata := *metadata
|
|
nestedMetadata.ResetRuleCache()
|
|
nestedStateSet := matchHeadlessRuleStates(rule, &nestedMetadata)
|
|
if nestedStateSet.isEmpty() {
|
|
if r.invert {
|
|
return emptyRuleMatchState()
|
|
}
|
|
return 0
|
|
}
|
|
stateSet = stateSet.combine(nestedStateSet)
|
|
}
|
|
} else {
|
|
for _, rule := range r.rules {
|
|
nestedMetadata := *metadata
|
|
nestedMetadata.ResetRuleCache()
|
|
stateSet = stateSet.merge(matchHeadlessRuleStates(rule, &nestedMetadata))
|
|
}
|
|
if stateSet.isEmpty() {
|
|
if r.invert {
|
|
return emptyRuleMatchState()
|
|
}
|
|
return 0
|
|
}
|
|
}
|
|
if r.invert {
|
|
return 0
|
|
}
|
|
return stateSet
|
|
}
|
|
|
|
func (r *abstractLogicalRule) Action() adapter.RuleAction {
|
|
return r.action
|
|
}
|
|
|
|
func (r *abstractLogicalRule) String() string {
|
|
var op string
|
|
switch r.mode {
|
|
case C.LogicalTypeAnd:
|
|
op = "&&"
|
|
case C.LogicalTypeOr:
|
|
op = "||"
|
|
}
|
|
if !r.invert {
|
|
return strings.Join(F.MapToString(r.rules), " "+op+" ")
|
|
} else {
|
|
return "!(" + strings.Join(F.MapToString(r.rules), " "+op+" ") + ")"
|
|
}
|
|
}
|
|
|
|
func matchAnyItem(items []RuleItem, metadata *adapter.InboundContext) bool {
|
|
return common.Any(items, func(it RuleItem) bool {
|
|
return it.Match(metadata)
|
|
})
|
|
}
|
|
|
|
func (s ruleMatchState) has(target ruleMatchState) bool {
|
|
return s&target != 0
|
|
}
|