From 08986ad405bf5e1237dae38aa1f8920010a31a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 22 Aug 2026 13:37:11 +0800 Subject: [PATCH] Fix address filter matching for inverted legacy DNS rules Inverted rules whose address filter conditions come from rule sets or logical rules were rejected during pre-lookup matching, so the lookup was never sent and the rules never matched; for plain ip rule sets this was a regression from the rule-set invert fix in 1.12.22. Pre-lookup matching now tracks whether the result depends on deferred address filter conditions and defers such rules to the response phase, while results already determined by evaluated conditions are still decided during pre-lookup. --- adapter/inbound.go | 4 +- route/rule/rule_abstract.go | 50 ++- route/rule/rule_dns.go | 34 +- route/rule/rule_dns_address_filter_test.go | 386 +++++++++++++++++++++ route/rule/rule_item_rule_set.go | 43 ++- 5 files changed, 460 insertions(+), 57 deletions(-) create mode 100644 route/rule/rule_dns_address_filter_test.go diff --git a/adapter/inbound.go b/adapter/inbound.go index e5c3fd9c..42f0825c 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -108,7 +108,7 @@ type InboundContext struct { SourcePortMatch bool DestinationAddressMatch bool DestinationPortMatch bool - DidMatch bool + DeferredIPCIDRMatchGroups uint8 IgnoreDestinationIPCIDRMatch bool } @@ -123,7 +123,7 @@ func (c *InboundContext) ResetRuleMatchCache() { c.SourcePortMatch = false c.DestinationAddressMatch = false c.DestinationPortMatch = false - c.DidMatch = false + c.DeferredIPCIDRMatchGroups = 0 } func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr { diff --git a/route/rule/rule_abstract.go b/route/rule/rule_abstract.go index ce56a58d..f21df788 100644 --- a/route/rule/rule_abstract.go +++ b/route/rule/rule_abstract.go @@ -57,10 +57,11 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool { } matched := r.matchInner(metadata) if r.invert { - if matched && metadata.IgnoreDestinationIPCIDRMatch && !metadata.DidMatch && len(r.destinationIPCIDRItems) > 0 { + if !matched { + metadata.DeferredIPCIDRMatchGroups = 0 return true } - return !matched + return metadata.DeferredIPCIDRMatchGroups != 0 } return matched } @@ -68,22 +69,25 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool { 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 } } + var matched bool if r.ruleSetItem != nil { - metadata.DidMatch = true - return r.ruleSetItem.matchWithOuterGroups(metadata, groups) + matched = r.ruleSetItem.matchWithOuterGroups(metadata, groups) + } else { + matched = groups.done() } - return groups.done() + if matched { + metadata.DeferredIPCIDRMatchGroups &^= uint8(groups.satisfied) + } + return matched } 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 } @@ -92,7 +96,7 @@ func (r *abstractDefaultRule) evaluateForMerge(metadata *adapter.InboundContext) } func (r *abstractDefaultRule) destinationIPCIDRMatchesSource(metadata *adapter.InboundContext) bool { - return !metadata.IgnoreDestinationIPCIDRMatch && metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0 + return metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0 } func (r *abstractDefaultRule) destinationIPCIDRMatchesDestination(metadata *adapter.InboundContext) bool { @@ -102,47 +106,44 @@ func (r *abstractDefaultRule) destinationIPCIDRMatchesDestination(metadata *adap 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) { groups.satisfied |= ruleMatchSourceAddress } } if r.destinationIPCIDRMatchesSource(metadata) { - metadata.DidMatch = true groups.required |= ruleMatchSourceAddress if !groups.satisfied.has(ruleMatchSourceAddress) && matchAnyItem(r.destinationIPCIDRItems, metadata) { groups.satisfied |= ruleMatchSourceAddress } } if len(r.sourcePortItems) > 0 { - metadata.DidMatch = true groups.required |= ruleMatchSourcePort if matchAnyItem(r.sourcePortItems, metadata) { groups.satisfied |= ruleMatchSourcePort } } if len(r.destinationAddressItems) > 0 { - metadata.DidMatch = true groups.required |= ruleMatchDestinationAddress if matchAnyItem(r.destinationAddressItems, metadata) { groups.satisfied |= ruleMatchDestinationAddress } } if r.destinationIPCIDRMatchesDestination(metadata) { - metadata.DidMatch = true groups.required |= ruleMatchDestinationAddress if !groups.satisfied.has(ruleMatchDestinationAddress) && matchAnyItem(r.destinationIPCIDRItems, metadata) { groups.satisfied |= ruleMatchDestinationAddress } } if len(r.destinationPortItems) > 0 { - metadata.DidMatch = true groups.required |= ruleMatchDestinationPort if matchAnyItem(r.destinationPortItems, metadata) { groups.satisfied |= ruleMatchDestinationPort } } + if metadata.IgnoreDestinationIPCIDRMatch && !metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0 && len(r.destinationAddressItems) == 0 { + metadata.DeferredIPCIDRMatchGroups |= uint8(ruleMatchDestinationAddress) + } return groups } @@ -201,7 +202,10 @@ func (r *abstractLogicalRule) Close() error { } func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { - var matched bool + var ( + matched bool + deferredGroups uint8 + ) if r.mode == C.LogicalTypeAnd { matched = true for _, rule := range r.rules { @@ -209,8 +213,10 @@ func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { nestedMetadata.ResetRuleCache() if !rule.Match(&nestedMetadata) { matched = false + deferredGroups = 0 break } + deferredGroups |= nestedMetadata.DeferredIPCIDRMatchGroups } } else { for _, rule := range r.rules { @@ -218,12 +224,22 @@ func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { nestedMetadata.ResetRuleCache() if rule.Match(&nestedMetadata) { matched = true - break + if nestedMetadata.DeferredIPCIDRMatchGroups == 0 { + deferredGroups = 0 + break + } + deferredGroups |= nestedMetadata.DeferredIPCIDRMatchGroups } } } + if matched { + metadata.DeferredIPCIDRMatchGroups |= deferredGroups + } if r.invert { - return !matched + if !matched { + return true + } + return deferredGroups != 0 } return matched } diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index 4fc4797d..a7f3bcf5 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -462,6 +462,7 @@ func (r *DefaultDNSRule) matchForMatch(metadata *adapter.InboundContext) bool { func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool { matchMetadata := *metadata + matchMetadata.ResetRuleCache() matchMetadata.DNSResponse = response matchMetadata.DestinationAddressMatchFromResponse = true return r.abstractDefaultRule.Match(&matchMetadata) @@ -492,34 +493,6 @@ func (r *LogicalDNSRule) Race() bool { return r.race } -func (r *LogicalDNSRule) matchForMatch(metadata *adapter.InboundContext) bool { - var matched bool - if r.mode == C.LogicalTypeAnd { - matched = true - for _, rule := range r.rules { - nestedMetadata := *metadata - nestedMetadata.ResetRuleCache() - if !rule.Match(&nestedMetadata) { - matched = false - break - } - } - } else { - for _, rule := range r.rules { - nestedMetadata := *metadata - nestedMetadata.ResetRuleCache() - if rule.Match(&nestedMetadata) { - matched = true - break - } - } - } - if r.invert { - return !matched - } - return matched -} - func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options option.LogicalDNSRule, legacyDNSMode bool) (*LogicalDNSRule, error) { r := &LogicalDNSRule{ abstractLogicalRule: abstractLogicalRule{ @@ -574,10 +547,6 @@ func (r *LogicalDNSRule) WithAddressLimit() bool { return false } -func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool { - return r.matchForMatch(metadata) -} - func (r *LogicalDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool { metadata.IgnoreDestinationIPCIDRMatch = true defer func() { metadata.IgnoreDestinationIPCIDRMatch = false }() @@ -586,6 +555,7 @@ func (r *LogicalDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool { func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool { matchMetadata := *metadata + matchMetadata.ResetRuleCache() matchMetadata.DNSResponse = response matchMetadata.DestinationAddressMatchFromResponse = true return r.abstractLogicalRule.Match(&matchMetadata) diff --git a/route/rule/rule_dns_address_filter_test.go b/route/rule/rule_dns_address_filter_test.go new file mode 100644 index 00000000..3b86a867 --- /dev/null +++ b/route/rule/rule_dns_address_filter_test.go @@ -0,0 +1,386 @@ +package rule + +import ( + "context" + "net" + "testing" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/json" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/service" + + "github.com/miekg/dns" + "github.com/stretchr/testify/require" +) + +type addressFilterRouter struct { + adapter.Router + ruleSets map[string]adapter.RuleSet +} + +func (r *addressFilterRouter) RuleSet(tag string) (adapter.RuleSet, bool) { + ruleSet, loaded := r.ruleSets[tag] + return ruleSet, loaded +} + +func addressFilterContext(t *testing.T, ruleSetConfigs map[string]string) context.Context { + t.Helper() + router := &addressFilterRouter{ruleSets: make(map[string]adapter.RuleSet)} + ctx := service.ContextWith[adapter.Router](context.Background(), router) + for tag, config := range ruleSetConfigs { + var plainOptions option.PlainRuleSetCompat + err := json.UnmarshalContext(ctx, []byte(config), &plainOptions) + require.NoError(t, err) + ruleSet, err := NewLocalRuleSet(ctx, log.NewNOPFactory().Logger(), tag, option.RuleSet{ + Type: C.RuleSetTypeInline, + InlineOptions: plainOptions.Options, + }) + require.NoError(t, err) + router.ruleSets[tag] = ruleSet + } + return ctx +} + +func addressFilterDNSRule(t *testing.T, ctx context.Context, config string) adapter.DNSRule { + t.Helper() + var ruleOptions option.DNSRule + err := json.UnmarshalContext(ctx, []byte(config), &ruleOptions) + require.NoError(t, err) + rule, err := NewDNSRule(ctx, log.NewNOPFactory().NewLogger("test"), ruleOptions, true, true) + require.NoError(t, err) + require.NoError(t, rule.Start()) + return rule +} + +func addressFilterResponse(address string) *dns.Msg { + response := &dns.Msg{} + response.Rcode = dns.RcodeSuccess + response.Answer = append(response.Answer, &dns.A{ + Hdr: dns.RR_Header{Rrtype: dns.TypeA, Class: dns.ClassINET}, + A: net.ParseIP(address).To4(), + }) + return response +} + +// addressFilterFlow mirrors dns/router.go: LegacyPreMatch under +// IgnoreDestinationIPCIDRMatch, then addressLimitResponseCheck against the +// response. +func addressFilterFlow(rule adapter.DNSRule, domain string, responseAddress string) (preMatched bool, routed bool) { + metadata := adapter.InboundContext{ + Domain: domain, + QueryType: dns.TypeA, + Source: M.ParseSocksaddrHostPort("192.168.1.10", 5353), + } + metadata.ResetRuleCache() + preMatched = rule.LegacyPreMatch(&metadata) + if !preMatched { + return false, false + } + if !rule.WithAddressLimit() { + return true, true + } + checkMetadata := metadata + return true, rule.MatchAddressLimit(&checkMetadata, addressFilterResponse(responseAddress)) +} + +func TestDNSAddressFilterInvert(t *testing.T) { + t.Parallel() + ctx := addressFilterContext(t, map[string]string{ + "mixed": `{"version": 3, "rules": [{"domain_suffix": ["ads.example"]}, {"ip_cidr": ["1.1.1.0/24"]}]}`, + "cn-ip": `{"version": 3, "rules": [{"ip_cidr": ["1.1.1.0/24"]}]}`, + "lan-ip": `{"version": 3, "rules": [{"ip_cidr": ["192.168.0.0/16"]}]}`, + "other-net": `{"version": 3, "rules": [{"ip_cidr": ["10.99.0.0/16"]}]}`, + "cn-domain": `{"version": 3, "rules": [{"domain_suffix": ["cn.example"]}]}`, + }) + testCases := []struct { + name string + rule string + domain string + responseAddress string + expectPreMatch bool + expectRouted bool + }{ + { + name: "direct mixed invert, domain hit", + rule: `{"domain_suffix": ["lookup.example"], "ip_cidr": ["1.1.1.0/24"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + }, + { + name: "direct mixed invert, both miss", + rule: `{"domain_suffix": ["lookup.example"], "ip_cidr": ["1.1.1.0/24"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "other.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "direct mixed invert, ip hit", + rule: `{"domain_suffix": ["lookup.example"], "ip_cidr": ["1.1.1.0/24"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "other.example", + responseAddress: "1.1.1.5", + expectPreMatch: true, + }, + { + name: "ip rule-set invert, ip miss", + rule: `{"rule_set": ["cn-ip"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "ip rule-set invert, ip hit", + rule: `{"rule_set": ["cn-ip"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "1.1.1.5", + expectPreMatch: true, + }, + { + name: "mixed rule-set invert, both miss", + rule: `{"rule_set": ["mixed"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "mixed rule-set invert, domain hit skips pre-lookup", + rule: `{"rule_set": ["mixed"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "ads.example", + responseAddress: "8.8.8.8", + }, + { + name: "mixed rule-set invert, ip hit", + rule: `{"rule_set": ["mixed"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "1.1.1.5", + expectPreMatch: true, + }, + { + name: "logical invert, ip miss", + rule: `{"type": "logical", "mode": "or", "invert": true, "rules": [{"rule_set": ["cn-ip"]}], "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "logical invert, ip hit", + rule: `{"type": "logical", "mode": "or", "invert": true, "rules": [{"rule_set": ["cn-ip"]}], "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "1.1.1.5", + expectPreMatch: true, + }, + { + name: "logical and with inverted ip rule-set, ip miss", + rule: `{"type": "logical", "mode": "and", "rules": [{"domain_suffix": ["lookup.example"]}, {"rule_set": ["cn-ip"], "invert": true}], "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "logical and with inverted ip rule-set, ip hit", + rule: `{"type": "logical", "mode": "and", "rules": [{"domain_suffix": ["lookup.example"]}, {"rule_set": ["cn-ip"], "invert": true}], "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "1.1.1.5", + expectPreMatch: true, + }, + { + name: "logical or invert, domain hit skips pre-lookup", + rule: `{"type": "logical", "mode": "or", "invert": true, "rules": [{"domain_suffix": ["ads.example"]}, {"rule_set": ["cn-ip"]}], "action": "route", "server": "proxy"}`, + domain: "ads.example", + responseAddress: "8.8.8.8", + }, + { + name: "logical or invert, both miss", + rule: `{"type": "logical", "mode": "or", "invert": true, "rules": [{"domain_suffix": ["ads.example"]}, {"rule_set": ["cn-ip"]}], "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "logical or invert, ip hit", + rule: `{"type": "logical", "mode": "or", "invert": true, "rules": [{"domain_suffix": ["ads.example"]}, {"rule_set": ["cn-ip"]}], "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "1.1.1.5", + expectPreMatch: true, + }, + { + name: "outer domain with ip rule-set invert, domain hit skips pre-lookup", + rule: `{"domain_suffix": ["cn.example"], "rule_set": ["cn-ip"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "cn.example", + responseAddress: "8.8.8.8", + }, + { + name: "outer domain with ip rule-set invert, both miss", + rule: `{"domain_suffix": ["cn.example"], "rule_set": ["cn-ip"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "other.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "source ip with ip invert, source hit ip miss", + rule: `{"source_ip_cidr": ["192.168.1.0/24"], "ip_cidr": ["1.1.1.0/24"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "source ip with ip invert, source hit ip hit", + rule: `{"source_ip_cidr": ["192.168.1.0/24"], "ip_cidr": ["1.1.1.0/24"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "1.1.1.5", + expectPreMatch: true, + }, + { + name: "source ip with ip invert, source miss", + rule: `{"source_ip_cidr": ["10.99.0.0/16"], "ip_cidr": ["1.1.1.0/24"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "ip rule-set without invert, ip hit", + rule: `{"rule_set": ["cn-ip"], "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "1.1.1.5", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "ip rule-set without invert, ip miss", + rule: `{"rule_set": ["cn-ip"], "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + }, + { + name: "mixed rule-set without invert, ip hit", + rule: `{"rule_set": ["mixed"], "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "1.1.1.5", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "mixed rule-set without invert, both miss", + rule: `{"rule_set": ["mixed"], "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + }, + { + name: "direct ip invert, ip miss", + rule: `{"ip_cidr": ["1.1.1.0/24"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "direct ip invert, ip hit", + rule: `{"ip_cidr": ["1.1.1.0/24"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "1.1.1.5", + expectPreMatch: true, + }, + { + name: "nested logical invert, ip miss", + rule: `{"type": "logical", "mode": "or", "invert": true, "rules": [{"type": "logical", "mode": "or", "rules": [{"rule_set": ["cn-ip"]}]}], "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "nested logical invert, ip hit", + rule: `{"type": "logical", "mode": "or", "invert": true, "rules": [{"type": "logical", "mode": "or", "rules": [{"rule_set": ["cn-ip"]}]}], "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "1.1.1.5", + expectPreMatch: true, + }, + { + name: "nested logical and invert, domain hit ip miss", + rule: `{"type": "logical", "mode": "or", "invert": true, "rules": [{"type": "logical", "mode": "and", "rules": [{"domain_suffix": ["lookup.example"]}, {"rule_set": ["cn-ip"]}]}], "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "nested logical and invert, domain hit ip hit", + rule: `{"type": "logical", "mode": "or", "invert": true, "rules": [{"type": "logical", "mode": "and", "rules": [{"domain_suffix": ["lookup.example"]}, {"rule_set": ["cn-ip"]}]}], "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "1.1.1.5", + expectPreMatch: true, + }, + { + name: "match-source rule-set invert, source in set", + rule: `{"rule_set": ["lan-ip"], "rule_set_ip_cidr_match_source": true, "invert": true, "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + }, + { + name: "match-source rule-set invert, source not in set", + rule: `{"rule_set": ["other-net"], "rule_set_ip_cidr_match_source": true, "invert": true, "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "match-source rule-set, source in set", + rule: `{"rule_set": ["lan-ip"], "rule_set_ip_cidr_match_source": true, "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "match-source rule-set, source not in set", + rule: `{"rule_set": ["other-net"], "rule_set_ip_cidr_match_source": true, "action": "route", "server": "proxy"}`, + domain: "lookup.example", + responseAddress: "8.8.8.8", + }, + { + name: "direct ip with domain rule-set invert, domain hit skips pre-lookup", + rule: `{"ip_cidr": ["1.1.1.0/24"], "rule_set": ["cn-domain"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "cn.example", + responseAddress: "8.8.8.8", + }, + { + name: "direct ip with domain rule-set invert, both miss", + rule: `{"ip_cidr": ["1.1.1.0/24"], "rule_set": ["cn-domain"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "other.example", + responseAddress: "8.8.8.8", + expectPreMatch: true, + expectRouted: true, + }, + { + name: "direct ip with domain rule-set invert, ip hit", + rule: `{"ip_cidr": ["1.1.1.0/24"], "rule_set": ["cn-domain"], "invert": true, "action": "route", "server": "proxy"}`, + domain: "other.example", + responseAddress: "1.1.1.5", + expectPreMatch: true, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + rule := addressFilterDNSRule(t, ctx, testCase.rule) + preMatched, routed := addressFilterFlow(rule, testCase.domain, testCase.responseAddress) + require.Equal(t, testCase.expectPreMatch, preMatched, "pre-lookup match") + require.Equal(t, testCase.expectRouted, routed, "routed") + }) + } +} diff --git a/route/rule/rule_item_rule_set.go b/route/rule/rule_item_rule_set.go index 505650b1..eb7c74e1 100644 --- a/route/rule/rule_item_rule_set.go +++ b/route/rule/rule_item_rule_set.go @@ -63,23 +63,43 @@ func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { func (r *RuleSetItem) matchWithOuterGroups(metadata *adapter.InboundContext, outerGroups ruleGroupMatch) bool { outerDone := outerGroups.done() + var ( + matched bool + deferredGroups uint8 + ) 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 + if branchMatched { + merged := outerGroups.mergeWith(branchGroups) + if merged.done() { + branchDeferredGroups := nestedMetadata.DeferredIPCIDRMatchGroups &^ uint8(merged.satisfied) + if branchDeferredGroups == 0 { + metadata.DeferredIPCIDRMatchGroups &^= uint8(merged.satisfied) + return true + } + matched = true + deferredGroups |= branchDeferredGroups + } } continue } } if outerDone && ruleSet.Match(&nestedMetadata) { - return true + if nestedMetadata.DeferredIPCIDRMatchGroups == 0 { + return true + } + matched = true + deferredGroups |= nestedMetadata.DeferredIPCIDRMatchGroups } } - return false + if matched { + metadata.DeferredIPCIDRMatchGroups |= deferredGroups + } + return matched } func (r *RuleSetItem) nestedMetadata(metadata *adapter.InboundContext) adapter.InboundContext { @@ -106,14 +126,25 @@ func mergeableRuleIn(rules []adapter.HeadlessRule) *DefaultHeadlessRule { } func matchAnyHeadlessRule(rules []adapter.HeadlessRule, metadata *adapter.InboundContext) bool { + var ( + matched bool + deferredGroups uint8 + ) for _, rule := range rules { nestedMetadata := *metadata nestedMetadata.ResetRuleMatchCache() if rule.Match(&nestedMetadata) { - return true + if nestedMetadata.DeferredIPCIDRMatchGroups == 0 { + return true + } + matched = true + deferredGroups |= nestedMetadata.DeferredIPCIDRMatchGroups } } - return false + if matched { + metadata.DeferredIPCIDRMatchGroups |= deferredGroups + } + return matched } func (r *RuleSetItem) ContainsDestinationIPCIDRRule() bool {