diff --git a/dns/router.go b/dns/router.go index bc16fad0f..fd68d1cd8 100644 --- a/dns/router.go +++ b/dns/router.go @@ -810,6 +810,11 @@ func validateNonLegacyAddressFilterDefaultRule(rule option.DefaultDNSRule) (bool if (len(rule.IPCIDR) > 0 || rule.IPIsPrivate) && !rule.MatchResponse { return false, E.New("ip_cidr and ip_is_private require match_response in DNS evaluate mode") } + // Intentionally do not reject rule_set here. A referenced rule set may mix + // destination-IP predicates with pre-response predicates such as domain items. + // When match_response is false, those destination-IP branches fail closed during + // pre-response evaluation instead of consuming DNS response state, while sibling + // non-response branches remain matchable. if rule.IPAcceptAny { return false, E.New("ip_accept_any is removed in DNS evaluate mode, use ip_cidr with match_response") } diff --git a/route/rule/rule_set_semantics_test.go b/route/rule/rule_set_semantics_test.go index d2a865bb3..f599adc3b 100644 --- a/route/rule/rule_set_semantics_test.go +++ b/route/rule/rule_set_semantics_test.go @@ -625,6 +625,9 @@ func TestDNSRuleSetSemantics(t *testing.T) { rule := dnsRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) + // This is accepted without match_response so mixed rule_set deployments keep + // working; the destination-IP-only branch simply cannot match before a DNS + // response is available. require.False(t, rule.Match(&metadata)) }) t.Run("pre lookup ruleset destination cidr does not fall back to other predicates", func(t *testing.T) { @@ -655,6 +658,9 @@ func TestDNSRuleSetSemantics(t *testing.T) { rule := dnsRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) + // Destination-IP predicates inside rule_set fail closed before the DNS response, + // but they must not force validation errors or suppress sibling non-response + // branches. require.True(t, rule.Match(&metadata)) }) }