Fix legacy DNS rule_set accept_empty matching

This commit is contained in:
世界
2026-03-27 15:24:09 +08:00
parent 495ec64da2
commit 44a487b7e9
5 changed files with 98 additions and 2 deletions

View File

@@ -66,6 +66,9 @@ type RuleSet interface {
type RuleSetUpdateCallback func(it RuleSet)
// Rule-set metadata only exposes headless-rule capabilities that outer routers
// need before evaluating nested matches. Headless rules do not support
// ip_version, so there is intentionally no ContainsIPVersionRule flag here.
type RuleSetMetadata struct {
ContainsProcessRule bool
ContainsWIFIRule bool

View File

@@ -1058,6 +1058,9 @@ func dnsRuleModeRequirementsInDefaultRule(router adapter.Router, rule option.Def
return false, false, E.New("rule-set not found: ", tag)
}
metadata := ruleSet.Metadata()
// Rule sets are built from headless rules, so query_type is the only
// per-query DNS predicate they can contribute here. ip_version is not a
// headless-rule item and is therefore intentionally absent from metadata.
forceNew = forceNew || metadata.ContainsDNSQueryTypeRule
if !rule.RuleSetIPCIDRMatchSource && metadata.ContainsIPCIDRRule {
needsLegacy = true

View File

@@ -661,6 +661,88 @@ func TestLookupLegacyModeFallsBackAfterRejectedAddressLimitResponse(t *testing.T
require.Equal(t, []string{"private", "default"}, lookups)
}
func TestLookupLegacyModeRuleSetAcceptEmptyDoesNotTreatMismatchAsEmpty(t *testing.T) {
t.Parallel()
ctx := context.Background()
ruleSet, err := rulepkg.NewRuleSet(ctx, log.NewNOPFactory().NewLogger("router"), option.RuleSet{
Type: C.RuleSetTypeInline,
Tag: "legacy-ipcidr-set",
InlineOptions: option.PlainRuleSet{
Rules: []option.HeadlessRule{{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultHeadlessRule{
IPCIDR: badoption.Listable[string]{"10.0.0.0/8"},
},
}},
},
})
require.NoError(t, err)
ctx = service.ContextWith[adapter.Router](ctx, &fakeRouter{
ruleSets: map[string]adapter.RuleSet{
"legacy-ipcidr-set": ruleSet,
},
})
defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}
privateTransport := &fakeDNSTransport{tag: "private", transportType: C.DNSTypeUDP}
var lookups []string
router := newTestRouterWithContext(t, ctx, []option.DNSRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
RawDefaultDNSRule: option.RawDefaultDNSRule{
RuleSet: badoption.Listable[string]{"legacy-ipcidr-set"},
RuleSetIPCIDRAcceptEmpty: true,
},
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeRoute,
RouteOptions: option.DNSRouteActionOptions{Server: "private"},
},
},
},
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeRoute,
RouteOptions: option.DNSRouteActionOptions{Server: "default"},
},
},
},
}, &fakeDNSTransportManager{
defaultTransport: defaultTransport,
transports: map[string]adapter.DNSTransport{
"default": defaultTransport,
"private": privateTransport,
},
}, &fakeDNSClient{
lookup: func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) {
require.Equal(t, "example.com", domain)
require.Equal(t, C.DomainStrategyIPv4Only, options.LookupStrategy)
lookups = append(lookups, transport.Tag())
switch transport.Tag() {
case "private":
response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60)
return MessageToAddresses(response), response, nil
case "default":
response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("9.9.9.9")}, 60)
return MessageToAddresses(response), response, nil
}
return nil, nil, errors.New("unexpected transport")
},
})
require.True(t, router.legacyAddressFilterMode)
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{
LookupStrategy: C.DomainStrategyIPv4Only,
})
require.NoError(t, err)
require.Equal(t, []netip.Addr{netip.MustParseAddr("9.9.9.9")}, addresses)
require.Equal(t, []string{"private", "default"}, lookups)
}
func TestDNSResponseAddressesMatchesMessageToAddressesForHTTPSHints(t *testing.T) {
t.Parallel()

View File

@@ -77,12 +77,18 @@ func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool {
return r.ipSet.Contains(metadata.Source.Addr)
}
if metadata.DestinationAddressMatchFromResponse {
for _, address := range metadata.DNSResponseAddressesForMatch() {
addresses := metadata.DNSResponseAddressesForMatch()
if len(addresses) == 0 {
// Legacy rule_set_ip_cidr_accept_empty only applies when the DNS response
// does not expose any address answers for matching.
return metadata.IPCIDRAcceptEmpty
}
for _, address := range addresses {
if r.ipSet.Contains(address) {
return true
}
}
return metadata.IPCIDRAcceptEmpty
return false
}
if metadata.Destination.IsIP() {
return r.ipSet.Contains(metadata.Destination.Addr)

View File

@@ -612,6 +612,8 @@ func TestDNSRuleSetSemantics(t *testing.T) {
ipCidrAcceptEmpty: true,
})
})
require.True(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1"))))
require.False(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest(netip.MustParseAddr("8.8.8.8"))))
require.True(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest()))
require.False(t, metadata.IPCIDRMatchSource)
require.False(t, metadata.IPCIDRAcceptEmpty)