Legacy DNS address-filter mode still accepts destination-side IP predicates with a deprecation warning, but the recent evaluate/ match_response refactor started evaluating those predicates during pre-response Match(). That broke rules whose transport selection must be deferred until MatchAddressLimit() can inspect the upstream reply. Restore the old defer behavior by reintroducing an internal IgnoreDestinationIPCIDRMatch flag on InboundContext and using it only for legacy pre-response DNS matching. Default and logical DNS rules now carry the legacy mode bit, set the ignore flag on metadata copies while performing pre-response Match(), and explicitly clear it again for match_response and MatchAddressLimit() so response-phase matching still checks the returned addresses. Add regression coverage for direct legacy destination-IP rules, rule_set-backed CIDR rules, logical wrappers, and the legacy Lookup router path, including fallback after a rejected response. This keeps legacy configs working without changing new-mode evaluate semantics. Tests: go test ./route/rule ./dns Tests: make
300 lines
7.7 KiB
Go
300 lines
7.7 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.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 {
|
|
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
|
|
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(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 {
|
|
return 0
|
|
}
|
|
return stateSet
|
|
}
|
|
|
|
func (r *abstractDefaultRule) invertedFailure(base ruleMatchState) ruleMatchStateSet {
|
|
if r.invert {
|
|
return emptyRuleMatchState().withBase(base)
|
|
}
|
|
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 {
|
|
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().withBase(evaluationBase)
|
|
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
|
|
}
|
|
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)
|
|
}
|
|
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
|
|
}
|