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 {