mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-15 21:00:27 +00:00
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:
+2
-2
@@ -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 {
|
||||
|
||||
+33
-17
@@ -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
|
||||
}
|
||||
|
||||
+2
-32
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user