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.
This commit is contained in:
世界
2026-08-30 17:41:46 +08:00
parent aea28ca6dc
commit 08986ad405
5 changed files with 460 additions and 57 deletions
+2 -2
View File
@@ -108,7 +108,7 @@ type InboundContext struct {
SourcePortMatch bool SourcePortMatch bool
DestinationAddressMatch bool DestinationAddressMatch bool
DestinationPortMatch bool DestinationPortMatch bool
DidMatch bool DeferredIPCIDRMatchGroups uint8
IgnoreDestinationIPCIDRMatch bool IgnoreDestinationIPCIDRMatch bool
} }
@@ -123,7 +123,7 @@ func (c *InboundContext) ResetRuleMatchCache() {
c.SourcePortMatch = false c.SourcePortMatch = false
c.DestinationAddressMatch = false c.DestinationAddressMatch = false
c.DestinationPortMatch = false c.DestinationPortMatch = false
c.DidMatch = false c.DeferredIPCIDRMatchGroups = 0
} }
func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr { func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr {
+33 -17
View File
@@ -57,10 +57,11 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
} }
matched := r.matchInner(metadata) matched := r.matchInner(metadata)
if r.invert { if r.invert {
if matched && metadata.IgnoreDestinationIPCIDRMatch && !metadata.DidMatch && len(r.destinationIPCIDRItems) > 0 { if !matched {
metadata.DeferredIPCIDRMatchGroups = 0
return true return true
} }
return !matched return metadata.DeferredIPCIDRMatchGroups != 0
} }
return matched return matched
} }
@@ -68,22 +69,25 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
func (r *abstractDefaultRule) matchInner(metadata *adapter.InboundContext) bool { func (r *abstractDefaultRule) matchInner(metadata *adapter.InboundContext) bool {
groups := r.evaluateGroups(metadata) groups := r.evaluateGroups(metadata)
for _, item := range r.items { for _, item := range r.items {
metadata.DidMatch = true
if !item.Match(metadata) { if !item.Match(metadata) {
return false return false
} }
} }
var matched bool
if r.ruleSetItem != nil { if r.ruleSetItem != nil {
metadata.DidMatch = true matched = r.ruleSetItem.matchWithOuterGroups(metadata, groups)
return 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) { func (r *abstractDefaultRule) evaluateForMerge(metadata *adapter.InboundContext) (ruleGroupMatch, bool) {
groups := r.evaluateGroups(metadata) groups := r.evaluateGroups(metadata)
for _, item := range r.items { for _, item := range r.items {
metadata.DidMatch = true
if !item.Match(metadata) { if !item.Match(metadata) {
return ruleGroupMatch{}, false return ruleGroupMatch{}, false
} }
@@ -92,7 +96,7 @@ func (r *abstractDefaultRule) evaluateForMerge(metadata *adapter.InboundContext)
} }
func (r *abstractDefaultRule) destinationIPCIDRMatchesSource(metadata *adapter.InboundContext) bool { 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 { 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 { func (r *abstractDefaultRule) evaluateGroups(metadata *adapter.InboundContext) ruleGroupMatch {
var groups ruleGroupMatch var groups ruleGroupMatch
if len(r.sourceAddressItems) > 0 { if len(r.sourceAddressItems) > 0 {
metadata.DidMatch = true
groups.required |= ruleMatchSourceAddress groups.required |= ruleMatchSourceAddress
if matchAnyItem(r.sourceAddressItems, metadata) { if matchAnyItem(r.sourceAddressItems, metadata) {
groups.satisfied |= ruleMatchSourceAddress groups.satisfied |= ruleMatchSourceAddress
} }
} }
if r.destinationIPCIDRMatchesSource(metadata) { if r.destinationIPCIDRMatchesSource(metadata) {
metadata.DidMatch = true
groups.required |= ruleMatchSourceAddress groups.required |= ruleMatchSourceAddress
if !groups.satisfied.has(ruleMatchSourceAddress) && matchAnyItem(r.destinationIPCIDRItems, metadata) { if !groups.satisfied.has(ruleMatchSourceAddress) && matchAnyItem(r.destinationIPCIDRItems, metadata) {
groups.satisfied |= ruleMatchSourceAddress groups.satisfied |= ruleMatchSourceAddress
} }
} }
if len(r.sourcePortItems) > 0 { if len(r.sourcePortItems) > 0 {
metadata.DidMatch = true
groups.required |= ruleMatchSourcePort groups.required |= ruleMatchSourcePort
if matchAnyItem(r.sourcePortItems, metadata) { if matchAnyItem(r.sourcePortItems, metadata) {
groups.satisfied |= ruleMatchSourcePort groups.satisfied |= ruleMatchSourcePort
} }
} }
if len(r.destinationAddressItems) > 0 { if len(r.destinationAddressItems) > 0 {
metadata.DidMatch = true
groups.required |= ruleMatchDestinationAddress groups.required |= ruleMatchDestinationAddress
if matchAnyItem(r.destinationAddressItems, metadata) { if matchAnyItem(r.destinationAddressItems, metadata) {
groups.satisfied |= ruleMatchDestinationAddress groups.satisfied |= ruleMatchDestinationAddress
} }
} }
if r.destinationIPCIDRMatchesDestination(metadata) { if r.destinationIPCIDRMatchesDestination(metadata) {
metadata.DidMatch = true
groups.required |= ruleMatchDestinationAddress groups.required |= ruleMatchDestinationAddress
if !groups.satisfied.has(ruleMatchDestinationAddress) && matchAnyItem(r.destinationIPCIDRItems, metadata) { if !groups.satisfied.has(ruleMatchDestinationAddress) && matchAnyItem(r.destinationIPCIDRItems, metadata) {
groups.satisfied |= ruleMatchDestinationAddress groups.satisfied |= ruleMatchDestinationAddress
} }
} }
if len(r.destinationPortItems) > 0 { if len(r.destinationPortItems) > 0 {
metadata.DidMatch = true
groups.required |= ruleMatchDestinationPort groups.required |= ruleMatchDestinationPort
if matchAnyItem(r.destinationPortItems, metadata) { if matchAnyItem(r.destinationPortItems, metadata) {
groups.satisfied |= ruleMatchDestinationPort groups.satisfied |= ruleMatchDestinationPort
} }
} }
if metadata.IgnoreDestinationIPCIDRMatch && !metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0 && len(r.destinationAddressItems) == 0 {
metadata.DeferredIPCIDRMatchGroups |= uint8(ruleMatchDestinationAddress)
}
return groups return groups
} }
@@ -201,7 +202,10 @@ func (r *abstractLogicalRule) Close() error {
} }
func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool {
var matched bool var (
matched bool
deferredGroups uint8
)
if r.mode == C.LogicalTypeAnd { if r.mode == C.LogicalTypeAnd {
matched = true matched = true
for _, rule := range r.rules { for _, rule := range r.rules {
@@ -209,8 +213,10 @@ func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool {
nestedMetadata.ResetRuleCache() nestedMetadata.ResetRuleCache()
if !rule.Match(&nestedMetadata) { if !rule.Match(&nestedMetadata) {
matched = false matched = false
deferredGroups = 0
break break
} }
deferredGroups |= nestedMetadata.DeferredIPCIDRMatchGroups
} }
} else { } else {
for _, rule := range r.rules { for _, rule := range r.rules {
@@ -218,12 +224,22 @@ func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool {
nestedMetadata.ResetRuleCache() nestedMetadata.ResetRuleCache()
if rule.Match(&nestedMetadata) { if rule.Match(&nestedMetadata) {
matched = true matched = true
break if nestedMetadata.DeferredIPCIDRMatchGroups == 0 {
deferredGroups = 0
break
}
deferredGroups |= nestedMetadata.DeferredIPCIDRMatchGroups
} }
} }
} }
if matched {
metadata.DeferredIPCIDRMatchGroups |= deferredGroups
}
if r.invert { if r.invert {
return !matched if !matched {
return true
}
return deferredGroups != 0
} }
return matched return matched
} }
+2 -32
View File
@@ -462,6 +462,7 @@ func (r *DefaultDNSRule) matchForMatch(metadata *adapter.InboundContext) bool {
func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool { func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool {
matchMetadata := *metadata matchMetadata := *metadata
matchMetadata.ResetRuleCache()
matchMetadata.DNSResponse = response matchMetadata.DNSResponse = response
matchMetadata.DestinationAddressMatchFromResponse = true matchMetadata.DestinationAddressMatchFromResponse = true
return r.abstractDefaultRule.Match(&matchMetadata) return r.abstractDefaultRule.Match(&matchMetadata)
@@ -492,34 +493,6 @@ func (r *LogicalDNSRule) Race() bool {
return r.race 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) { func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options option.LogicalDNSRule, legacyDNSMode bool) (*LogicalDNSRule, error) {
r := &LogicalDNSRule{ r := &LogicalDNSRule{
abstractLogicalRule: abstractLogicalRule{ abstractLogicalRule: abstractLogicalRule{
@@ -574,10 +547,6 @@ func (r *LogicalDNSRule) WithAddressLimit() bool {
return false return false
} }
func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool {
return r.matchForMatch(metadata)
}
func (r *LogicalDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool { func (r *LogicalDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool {
metadata.IgnoreDestinationIPCIDRMatch = true metadata.IgnoreDestinationIPCIDRMatch = true
defer func() { metadata.IgnoreDestinationIPCIDRMatch = false }() 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 { func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool {
matchMetadata := *metadata matchMetadata := *metadata
matchMetadata.ResetRuleCache()
matchMetadata.DNSResponse = response matchMetadata.DNSResponse = response
matchMetadata.DestinationAddressMatchFromResponse = true matchMetadata.DestinationAddressMatchFromResponse = true
return r.abstractLogicalRule.Match(&matchMetadata) return r.abstractLogicalRule.Match(&matchMetadata)
+386
View File
@@ -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")
})
}
}
+37 -6
View File
@@ -63,23 +63,43 @@ func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool {
func (r *RuleSetItem) matchWithOuterGroups(metadata *adapter.InboundContext, outerGroups ruleGroupMatch) bool { func (r *RuleSetItem) matchWithOuterGroups(metadata *adapter.InboundContext, outerGroups ruleGroupMatch) bool {
outerDone := outerGroups.done() outerDone := outerGroups.done()
var (
matched bool
deferredGroups uint8
)
for _, ruleSet := range r.setList { for _, ruleSet := range r.setList {
nestedMetadata := r.nestedMetadata(metadata) nestedMetadata := r.nestedMetadata(metadata)
if provider, isProvider := ruleSet.(mergeableRuleProvider); isProvider { if provider, isProvider := ruleSet.(mergeableRuleProvider); isProvider {
branch := provider.mergeableRule() branch := provider.mergeableRule()
if branch != nil { if branch != nil {
branchGroups, branchMatched := branch.evaluateForMerge(&nestedMetadata) branchGroups, branchMatched := branch.evaluateForMerge(&nestedMetadata)
if branchMatched && outerGroups.mergeWith(branchGroups).done() { if branchMatched {
return true 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 continue
} }
} }
if outerDone && ruleSet.Match(&nestedMetadata) { 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 { 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 { func matchAnyHeadlessRule(rules []adapter.HeadlessRule, metadata *adapter.InboundContext) bool {
var (
matched bool
deferredGroups uint8
)
for _, rule := range rules { for _, rule := range rules {
nestedMetadata := *metadata nestedMetadata := *metadata
nestedMetadata.ResetRuleMatchCache() nestedMetadata.ResetRuleMatchCache()
if rule.Match(&nestedMetadata) { 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 { func (r *RuleSetItem) ContainsDestinationIPCIDRRule() bool {