Fix DNS evaluate regressions

This commit is contained in:
世界
2026-03-24 21:38:12 +08:00
parent 2d40044978
commit b74ac6a178
4 changed files with 373 additions and 39 deletions

View File

@@ -232,9 +232,11 @@ func (r *Router) applyTransportDefaults(transport adapter.DNSTransport, options
}
}
func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOptions R.RuleActionDNSRouteOptions) {
func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOptions R.RuleActionDNSRouteOptions) bool {
var strategyOverridden bool
if routeOptions.Strategy != C.DomainStrategyAsIS {
options.Strategy = routeOptions.Strategy
strategyOverridden = true
}
if routeOptions.DisableCache {
options.DisableCache = true
@@ -245,23 +247,32 @@ func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOpt
if routeOptions.ClientSubnet.IsValid() {
options.ClientSubnet = routeOptions.ClientSubnet
}
return strategyOverridden
}
func (r *Router) resolveDNSRoute(action *R.RuleActionDNSRoute, allowFakeIP bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, bool) {
type dnsRouteStatus uint8
const (
dnsRouteStatusMissing dnsRouteStatus = iota
dnsRouteStatusSkipped
dnsRouteStatusResolved
)
func (r *Router) resolveDNSRoute(action *R.RuleActionDNSRoute, allowFakeIP bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, dnsRouteStatus, bool) {
transport, loaded := r.transport.Transport(action.Server)
if !loaded {
return nil, false
return nil, dnsRouteStatusMissing, false
}
isFakeIP := transport.Type() == C.DNSTypeFakeIP
if isFakeIP && !allowFakeIP {
return transport, false
return transport, dnsRouteStatusSkipped, false
}
r.applyDNSRouteOptions(options, action.RuleActionDNSRouteOptions)
strategyOverridden := r.applyDNSRouteOptions(options, action.RuleActionDNSRouteOptions)
if isFakeIP {
options.DisableCache = true
}
r.applyTransportDefaults(transport, options)
return transport, true
return transport, dnsRouteStatusResolved, strategyOverridden
}
func (r *Router) logRuleMatch(ctx context.Context, ruleIndex int, currentRule adapter.DNSRule) {
@@ -276,12 +287,13 @@ func (r *Router) logRuleMatch(ctx context.Context, ruleIndex int, currentRule ad
}
}
func (r *Router) exchangeWithRules(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions, allowFakeIP bool) (*mDNS.Msg, adapter.DNSTransport, adapter.DNSQueryOptions, error) {
func (r *Router) exchangeWithRules(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions, allowFakeIP bool) (*mDNS.Msg, adapter.DNSTransport, adapter.DNSQueryOptions, bool, error) {
metadata := adapter.ContextFrom(ctx)
if metadata == nil {
panic("no context")
}
effectiveOptions := options
effectiveStrategyOverridden := false
var savedResponse *mDNS.Msg
for currentRuleIndex, currentRule := range r.rules {
metadata.ResetRuleCache()
@@ -293,24 +305,26 @@ func (r *Router) exchangeWithRules(ctx context.Context, message *mDNS.Msg, optio
r.logRuleMatch(ctx, currentRuleIndex, currentRule)
switch action := currentRule.Action().(type) {
case *R.RuleActionDNSRouteOptions:
r.applyDNSRouteOptions(&effectiveOptions, *action)
effectiveStrategyOverridden = r.applyDNSRouteOptions(&effectiveOptions, *action) || effectiveStrategyOverridden
case *R.RuleActionEvaluate:
queryOptions := effectiveOptions
transport, loaded := r.resolveDNSRoute(&R.RuleActionDNSRoute{
transport, status, _ := r.resolveDNSRoute(&R.RuleActionDNSRoute{
Server: action.Server,
RuleActionDNSRouteOptions: action.RuleActionDNSRouteOptions,
}, allowFakeIP, &queryOptions)
if !loaded {
if transport == nil {
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
}
switch status {
case dnsRouteStatusMissing:
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
savedResponse = nil
continue
case dnsRouteStatusSkipped:
continue
}
if queryOptions.Strategy == C.DomainStrategyAsIS {
queryOptions.Strategy = r.defaultDomainStrategy
exchangeOptions := queryOptions
if exchangeOptions.Strategy == C.DomainStrategyAsIS {
exchangeOptions.Strategy = r.defaultDomainStrategy
}
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, queryOptions, nil)
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil)
if err != nil {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
savedResponse = nil
@@ -319,18 +333,20 @@ func (r *Router) exchangeWithRules(ctx context.Context, message *mDNS.Msg, optio
savedResponse = response
case *R.RuleActionDNSRoute:
queryOptions := effectiveOptions
transport, loaded := r.resolveDNSRoute(action, allowFakeIP, &queryOptions)
if !loaded {
if transport == nil {
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
}
transport, status, strategyOverridden := r.resolveDNSRoute(action, allowFakeIP, &queryOptions)
switch status {
case dnsRouteStatusMissing:
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
continue
case dnsRouteStatusSkipped:
continue
}
if queryOptions.Strategy == C.DomainStrategyAsIS {
queryOptions.Strategy = r.defaultDomainStrategy
exchangeOptions := queryOptions
if exchangeOptions.Strategy == C.DomainStrategyAsIS {
exchangeOptions.Strategy = r.defaultDomainStrategy
}
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, queryOptions, nil)
return response, transport, queryOptions, err
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil)
return response, transport, queryOptions, effectiveStrategyOverridden || strategyOverridden, err
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
@@ -341,27 +357,29 @@ func (r *Router) exchangeWithRules(ctx context.Context, message *mDNS.Msg, optio
Response: true,
},
Question: []mDNS.Question{message.Question[0]},
}, nil, effectiveOptions, nil
}, nil, effectiveOptions, effectiveStrategyOverridden, nil
case C.RuleActionRejectMethodDrop:
return nil, nil, effectiveOptions, tun.ErrDrop
return nil, nil, effectiveOptions, effectiveStrategyOverridden, tun.ErrDrop
}
case *R.RuleActionPredefined:
return action.Response(message), nil, effectiveOptions, nil
return action.Response(message), nil, effectiveOptions, effectiveStrategyOverridden, nil
}
}
queryOptions := effectiveOptions
transport := r.transport.Default()
r.applyTransportDefaults(transport, &queryOptions)
if queryOptions.Strategy == C.DomainStrategyAsIS {
queryOptions.Strategy = r.defaultDomainStrategy
exchangeOptions := queryOptions
if exchangeOptions.Strategy == C.DomainStrategyAsIS {
exchangeOptions.Strategy = r.defaultDomainStrategy
}
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, queryOptions, nil)
return response, transport, queryOptions, err
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil)
return response, transport, queryOptions, effectiveStrategyOverridden, err
}
type lookupWithRulesResponse struct {
addresses []netip.Addr
strategy C.DomainStrategy
addresses []netip.Addr
strategy C.DomainStrategy
explicitStrategy C.DomainStrategy
}
func (r *Router) resolveLookupStrategy(options adapter.DNSQueryOptions, strategies ...C.DomainStrategy) C.DomainStrategy {
@@ -390,6 +408,13 @@ func lookupStrategyAllowsQueryType(strategy C.DomainStrategy, qType uint16) bool
}
}
func lookupStrategyOverride(queryOptions adapter.DNSQueryOptions, strategyOverridden bool) C.DomainStrategy {
if !strategyOverridden {
return C.DomainStrategyAsIS
}
return queryOptions.Strategy
}
func (r *Router) lookupWithRules(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
lookupOptions := options
if options.LookupStrategy != C.DomainStrategyAsIS {
@@ -419,7 +444,7 @@ func (r *Router) lookupWithRules(ctx context.Context, domain string, options ada
return err
})
err := group.Run(ctx)
strategy := r.resolveLookupStrategy(options, response4.strategy, response6.strategy)
strategy := r.resolveLookupStrategy(options, response4.explicitStrategy, response6.explicitStrategy)
if !lookupStrategyAllowsQueryType(strategy, mDNS.TypeA) {
response4.addresses = nil
}
@@ -443,9 +468,11 @@ func (r *Router) lookupWithRulesType(ctx context.Context, domain string, qType u
Qclass: mDNS.ClassINET,
}},
}
response, _, queryOptions, err := r.exchangeWithRules(adapter.OverrideContext(ctx), request, options, false)
response, _, queryOptions, strategyOverridden, err := r.exchangeWithRules(adapter.OverrideContext(ctx), request, options, false)
explicitStrategy := lookupStrategyOverride(queryOptions, strategyOverridden)
result := lookupWithRulesResponse{
strategy: r.resolveLookupStrategy(options, queryOptions.Strategy),
strategy: r.resolveLookupStrategy(options, explicitStrategy),
explicitStrategy: explicitStrategy,
}
if err != nil {
return result, err
@@ -500,7 +527,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
}
response, err = r.client.Exchange(ctx, transport, message, options, nil)
} else if !r.legacyAddressFilterMode {
response, transport, _, err = r.exchangeWithRules(ctx, message, options, true)
response, transport, _, _, err = r.exchangeWithRules(ctx, message, options, true)
} else {
var (
rule adapter.DNSRule

View File

@@ -583,6 +583,86 @@ func TestLookupNewModeSkipsFakeIPRule(t *testing.T) {
require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addresses)
}
func TestLookupNewModeEvaluateSkipFakeIPPreservesResponse(t *testing.T) {
t.Parallel()
defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}
router := newTestRouter(t, []option.DNSRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
RawDefaultDNSRule: option.RawDefaultDNSRule{
Domain: badoption.Listable[string]{"example.com"},
},
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeEvaluate,
RouteOptions: option.DNSRouteActionOptions{Server: "upstream"},
},
},
},
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
RawDefaultDNSRule: option.RawDefaultDNSRule{
Domain: badoption.Listable[string]{"example.com"},
},
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeEvaluate,
RouteOptions: option.DNSRouteActionOptions{Server: "fake"},
},
},
},
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
RawDefaultDNSRule: option.RawDefaultDNSRule{
MatchResponse: true,
ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")},
},
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeRoute,
RouteOptions: option.DNSRouteActionOptions{Server: "selected"},
},
},
},
}, &fakeDNSTransportManager{
defaultTransport: defaultTransport,
transports: map[string]adapter.DNSTransport{
"default": defaultTransport,
"upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP},
"fake": &fakeDNSTransport{tag: "fake", transportType: C.DNSTypeFakeIP},
"selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP},
},
}, &fakeDNSClient{
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
switch transport.Tag() {
case "upstream":
if message.Question[0].Qtype == mDNS.TypeA {
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil
}
return FixedResponse(0, message.Question[0], nil, 60), nil
case "selected":
if message.Question[0].Qtype == mDNS.TypeA {
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil
}
return FixedResponse(0, message.Question[0], nil, 60), nil
case "default":
if message.Question[0].Qtype == mDNS.TypeA {
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("4.4.4.4")}, 60), nil
}
return FixedResponse(0, message.Question[0], nil, 60), nil
default:
return nil, errors.New("unexpected transport")
}
},
})
router.legacyAddressFilterMode = false
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{})
require.NoError(t, err)
require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addresses)
}
func TestLookupNewModeDoesNotUseQueryTypeRule(t *testing.T) {
t.Parallel()
@@ -685,6 +765,159 @@ func TestLookupNewModeAppliesRouteStrategyAfterEvaluate(t *testing.T) {
require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addresses)
}
func TestLookupNewModePrefersExplicitBranchStrategyOverDefault(t *testing.T) {
t.Parallel()
defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}
router := newTestRouter(t, []option.DNSRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
RawDefaultDNSRule: option.RawDefaultDNSRule{
Domain: badoption.Listable[string]{"example.com"},
},
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeEvaluate,
RouteOptions: option.DNSRouteActionOptions{Server: "upstream"},
},
},
},
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
RawDefaultDNSRule: option.RawDefaultDNSRule{
MatchResponse: true,
ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN AAAA 2001:db8::1")},
},
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeRoute,
RouteOptions: option.DNSRouteActionOptions{
Server: "selected",
Strategy: option.DomainStrategy(C.DomainStrategyIPv6Only),
},
},
},
},
}, &fakeDNSTransportManager{
defaultTransport: defaultTransport,
transports: map[string]adapter.DNSTransport{
"default": defaultTransport,
"upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP},
"selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP},
},
}, &fakeDNSClient{
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
switch transport.Tag() {
case "upstream":
if message.Question[0].Qtype == mDNS.TypeA {
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil
}
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::1")}, 60), nil
case "selected":
if message.Question[0].Qtype == mDNS.TypeA {
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil
}
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::2")}, 60), nil
case "default":
if message.Question[0].Qtype == mDNS.TypeA {
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("4.4.4.4")}, 60), nil
}
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::4")}, 60), nil
default:
return nil, errors.New("unexpected transport")
}
},
})
router.defaultDomainStrategy = C.DomainStrategyIPv4Only
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{})
require.NoError(t, err)
require.Equal(t, []netip.Addr{netip.MustParseAddr("2001:db8::2")}, addresses)
}
func TestLookupNewModeKeepsExplicitBranchStrategyMatchingInput(t *testing.T) {
t.Parallel()
defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}
router := newTestRouter(t, []option.DNSRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
RawDefaultDNSRule: option.RawDefaultDNSRule{
Domain: badoption.Listable[string]{"example.com"},
},
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeEvaluate,
RouteOptions: option.DNSRouteActionOptions{Server: "upstream"},
},
},
},
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
RawDefaultDNSRule: option.RawDefaultDNSRule{
MatchResponse: true,
ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")},
},
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeRoute,
RouteOptions: option.DNSRouteActionOptions{
Server: "selected4",
Strategy: option.DomainStrategy(C.DomainStrategyIPv4Only),
},
},
},
},
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
RawDefaultDNSRule: option.RawDefaultDNSRule{
MatchResponse: true,
ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN AAAA 2001:db8::1")},
},
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeRoute,
RouteOptions: option.DNSRouteActionOptions{
Server: "selected6",
Strategy: option.DomainStrategy(C.DomainStrategyIPv6Only),
},
},
},
},
}, &fakeDNSTransportManager{
defaultTransport: defaultTransport,
transports: map[string]adapter.DNSTransport{
"default": defaultTransport,
"upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP},
"selected4": &fakeDNSTransport{tag: "selected4", transportType: C.DNSTypeUDP},
"selected6": &fakeDNSTransport{tag: "selected6", transportType: C.DNSTypeUDP},
},
}, &fakeDNSClient{
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
switch transport.Tag() {
case "upstream":
if message.Question[0].Qtype == mDNS.TypeA {
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil
}
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::1")}, 60), nil
case "selected4":
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil
case "selected6":
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::2")}, 60), nil
default:
return nil, errors.New("unexpected transport")
}
},
})
router.legacyAddressFilterMode = false
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{
Strategy: C.DomainStrategyIPv4Only,
})
require.NoError(t, err)
require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addresses)
}
func TestExchangeNewModeLogicalMatchResponseIPCIDRFallsThrough(t *testing.T) {
t.Parallel()

View File

@@ -65,9 +65,21 @@ func (o *DNSOptions) UnmarshalJSONContext(ctx context.Context, content []byte) e
}
if !dontUpgrade {
rcodeMap := make(map[string]int)
for _, server := range o.Servers {
if server.Type == C.DNSTypeLegacyRcode {
rcodeMap[server.Tag] = server.Options.(int)
}
}
if len(rcodeMap) > 0 {
for i := 0; i < len(o.Rules); i++ {
err = rejectEvaluateLegacyRcode(rcodeMap, o.Rules[i])
if err != nil {
return E.Cause(err, "dns rule[", i, "]")
}
}
}
o.Servers = common.Filter(o.Servers, func(it DNSServerOptions) bool {
if it.Type == C.DNSTypeLegacyRcode {
rcodeMap[it.Tag] = it.Options.(int)
return false
}
return true
@@ -81,6 +93,35 @@ func (o *DNSOptions) UnmarshalJSONContext(ctx context.Context, content []byte) e
return nil
}
func rejectEvaluateLegacyRcode(rcodeMap map[string]int, rule DNSRule) error {
switch rule.Type {
case C.RuleTypeDefault:
return rejectEvaluateLegacyRcodeAction(rcodeMap, &rule.DefaultOptions.DNSRuleAction)
case C.RuleTypeLogical:
err := rejectEvaluateLegacyRcodeAction(rcodeMap, &rule.LogicalOptions.DNSRuleAction)
if err != nil {
return err
}
for i, subRule := range rule.LogicalOptions.Rules {
err = rejectEvaluateLegacyRcode(rcodeMap, subRule)
if err != nil {
return E.Cause(err, "sub rule[", i, "]")
}
}
}
return nil
}
func rejectEvaluateLegacyRcodeAction(rcodeMap map[string]int, ruleAction *DNSRuleAction) error {
if ruleAction.Action != C.RuleActionTypeEvaluate {
return nil
}
if _, loaded := rcodeMap[ruleAction.RouteOptions.Server]; loaded {
return E.New("evaluate action cannot reference legacy rcode server: ", ruleAction.RouteOptions.Server)
}
return nil
}
func rewriteRcode(rcodeMap map[string]int, rule *DNSRule) {
switch rule.Type {
case C.RuleTypeDefault:

33
option/dns_test.go Normal file
View File

@@ -0,0 +1,33 @@
package option
import (
"context"
"testing"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/service"
"github.com/stretchr/testify/require"
)
type stubDNSTransportOptionsRegistry struct{}
func (stubDNSTransportOptionsRegistry) CreateOptions(string) (any, bool) {
return nil, false
}
func TestDNSOptionsRejectsEvaluateLegacyRcodeServer(t *testing.T) {
t.Parallel()
ctx := service.ContextWith[DNSTransportOptionsRegistry](context.Background(), stubDNSTransportOptionsRegistry{})
var options DNSOptions
err := json.UnmarshalContext(ctx, []byte(`{
"servers": [
{"tag": "legacy-rcode", "address": "rcode://success"},
{"tag": "default", "address": "1.1.1.1"}
],
"rules": [
{"domain": ["example.com"], "action": "evaluate", "server": "legacy-rcode"}
]
}`), &options)
require.ErrorContains(t, err, "evaluate action cannot reference legacy rcode server: legacy-rcode")
}