mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-11 17:47:20 +10:00
Compare commits
18 Commits
fb19bf6111
...
470c605118
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
470c605118 | ||
|
|
3795ac1503 | ||
|
|
8be4f38eaf | ||
|
|
4152022b89 | ||
|
|
5b01eaa149 | ||
|
|
3d32ad79cd | ||
|
|
e49a7bcc86 | ||
|
|
c94696df9e | ||
|
|
a7c4096a07 | ||
|
|
0e87476ee5 | ||
|
|
21f3acef81 | ||
|
|
861fa897e0 | ||
|
|
c471d7fee7 | ||
|
|
7de00c7cd1 | ||
|
|
29024191ee | ||
|
|
16480095f7 | ||
|
|
cf33f1f375 | ||
|
|
1d872a6835 |
@@ -99,10 +99,9 @@ type InboundContext struct {
|
||||
|
||||
SourceAddressMatch bool
|
||||
SourcePortMatch bool
|
||||
DestinationAddressMatch bool
|
||||
DestinationPortMatch bool
|
||||
DidMatch bool
|
||||
IgnoreDestinationIPCIDRMatch bool
|
||||
DestinationAddressMatch bool
|
||||
DestinationPortMatch bool
|
||||
DidMatch bool
|
||||
}
|
||||
|
||||
func (c *InboundContext) ResetRuleCache() {
|
||||
@@ -119,13 +118,6 @@ func (c *InboundContext) ResetRuleMatchCache() {
|
||||
c.DidMatch = false
|
||||
}
|
||||
|
||||
func (c *InboundContext) DestinationAddressesForMatch() []netip.Addr {
|
||||
if c.DestinationAddressMatchFromResponse {
|
||||
return DNSResponseAddresses(c.DNSResponse)
|
||||
}
|
||||
return c.DestinationAddresses
|
||||
}
|
||||
|
||||
func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr {
|
||||
return DNSResponseAddresses(c.DNSResponse)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
@@ -35,12 +35,12 @@ func TestReproLookupWithRulesUsesRequestStrategy(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
addrs, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{
|
||||
Strategy: C.DomainStrategyIPv4Only,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []uint16{mDNS.TypeA}, qTypes)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addrs)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addresses)
|
||||
}
|
||||
|
||||
func TestReproLogicalMatchResponseIPCIDR(t *testing.T) {
|
||||
@@ -62,7 +62,7 @@ func TestReproLogicalMatchResponseIPCIDR(t *testing.T) {
|
||||
case "selected":
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil
|
||||
default:
|
||||
return nil, errors.New("unexpected transport")
|
||||
return nil, E.New("unexpected transport")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
200
dns/router.go
200
dns/router.go
@@ -6,6 +6,7 @@ import (
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -37,6 +38,42 @@ type dnsRuleSetCallback struct {
|
||||
element *list.Element[adapter.RuleSetUpdateCallback]
|
||||
}
|
||||
|
||||
type rulesSnapshot struct {
|
||||
rules []adapter.DNSRule
|
||||
legacyDNSMode bool
|
||||
references atomic.Int64
|
||||
}
|
||||
|
||||
func newRulesSnapshot(rules []adapter.DNSRule, legacyDNSMode bool) *rulesSnapshot {
|
||||
snapshot := &rulesSnapshot{
|
||||
rules: rules,
|
||||
legacyDNSMode: legacyDNSMode,
|
||||
}
|
||||
snapshot.references.Store(1)
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func (s *rulesSnapshot) retain() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.references.Add(1)
|
||||
}
|
||||
|
||||
func (s *rulesSnapshot) release() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
references := s.references.Add(-1)
|
||||
switch {
|
||||
case references > 0:
|
||||
case references == 0:
|
||||
closeRules(s.rules)
|
||||
default:
|
||||
panic("dns: negative rules snapshot references")
|
||||
}
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
@@ -44,13 +81,12 @@ type Router struct {
|
||||
outbound adapter.OutboundManager
|
||||
client adapter.DNSClient
|
||||
rawRules []option.DNSRule
|
||||
rules []adapter.DNSRule
|
||||
currentRules atomic.Pointer[rulesSnapshot]
|
||||
defaultDomainStrategy C.DomainStrategy
|
||||
dnsReverseMapping freelru.Cache[netip.Addr, string]
|
||||
platformInterface adapter.PlatformInterface
|
||||
legacyDNSMode bool
|
||||
rulesAccess sync.RWMutex
|
||||
rebuildAccess sync.Mutex
|
||||
stateAccess sync.Mutex
|
||||
closing bool
|
||||
ruleSetCallbacks []dnsRuleSetCallback
|
||||
addressFilterDeprecatedReported bool
|
||||
@@ -64,9 +100,9 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
|
||||
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
||||
rawRules: make([]option.DNSRule, 0, len(options.Rules)),
|
||||
rules: make([]adapter.DNSRule, 0, len(options.Rules)),
|
||||
defaultDomainStrategy: C.DomainStrategy(options.Strategy),
|
||||
}
|
||||
router.currentRules.Store(newRulesSnapshot(make([]adapter.DNSRule, 0, len(options.Rules)), false))
|
||||
router.client = NewClient(ClientOptions{
|
||||
DisableCache: options.DNSClientOptions.DisableCache,
|
||||
DisableExpire: options.DNSClientOptions.DisableExpire,
|
||||
@@ -134,26 +170,21 @@ func (r *Router) Start(stage adapter.StartStage) error {
|
||||
}
|
||||
|
||||
func (r *Router) Close() error {
|
||||
monitor := taskmonitor.New(r.logger, C.StopTimeout)
|
||||
r.rulesAccess.Lock()
|
||||
r.stateAccess.Lock()
|
||||
if r.closing {
|
||||
r.stateAccess.Unlock()
|
||||
return nil
|
||||
}
|
||||
r.closing = true
|
||||
callbacks := r.ruleSetCallbacks
|
||||
r.ruleSetCallbacks = nil
|
||||
runtimeRules := r.rules
|
||||
r.rules = nil
|
||||
oldSnapshot := r.currentRules.Swap(nil)
|
||||
for _, callback := range callbacks {
|
||||
callback.ruleSet.UnregisterCallback(callback.element)
|
||||
}
|
||||
r.rulesAccess.Unlock()
|
||||
var err error
|
||||
for i, rule := range runtimeRules {
|
||||
monitor.Start("close dns rule[", i, "]")
|
||||
err = E.Append(err, rule.Close(), func(err error) error {
|
||||
return E.Cause(err, "close dns rule[", i, "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
return err
|
||||
r.stateAccess.Unlock()
|
||||
oldSnapshot.release()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) rebuildRules(startRules bool) error {
|
||||
@@ -177,23 +208,22 @@ func (r *Router) rebuildRules(startRules bool) error {
|
||||
legacyDNSMode &&
|
||||
!r.ruleStrategyDeprecatedReported &&
|
||||
hasDNSRuleActionStrategy(r.rawRules)
|
||||
r.rulesAccess.Lock()
|
||||
newSnapshot := newRulesSnapshot(newRules, legacyDNSMode)
|
||||
r.stateAccess.Lock()
|
||||
if r.closing {
|
||||
r.rulesAccess.Unlock()
|
||||
closeRules(newRules)
|
||||
r.stateAccess.Unlock()
|
||||
newSnapshot.release()
|
||||
return nil
|
||||
}
|
||||
oldRules := r.rules
|
||||
r.rules = newRules
|
||||
r.legacyDNSMode = legacyDNSMode
|
||||
if shouldReportAddressFilterDeprecated {
|
||||
r.addressFilterDeprecatedReported = true
|
||||
}
|
||||
if shouldReportRuleStrategyDeprecated {
|
||||
r.ruleStrategyDeprecatedReported = true
|
||||
}
|
||||
r.rulesAccess.Unlock()
|
||||
closeRules(oldRules)
|
||||
oldSnapshot := r.currentRules.Swap(newSnapshot)
|
||||
r.stateAccess.Unlock()
|
||||
oldSnapshot.release()
|
||||
if shouldReportAddressFilterDeprecated {
|
||||
deprecated.Report(r.ctx, deprecated.OptionLegacyDNSAddressFilter)
|
||||
}
|
||||
@@ -204,11 +234,19 @@ func (r *Router) rebuildRules(startRules bool) error {
|
||||
}
|
||||
|
||||
func (r *Router) isClosing() bool {
|
||||
r.rulesAccess.RLock()
|
||||
defer r.rulesAccess.RUnlock()
|
||||
r.stateAccess.Lock()
|
||||
defer r.stateAccess.Unlock()
|
||||
return r.closing
|
||||
}
|
||||
|
||||
func (r *Router) acquireRulesSnapshot() *rulesSnapshot {
|
||||
r.stateAccess.Lock()
|
||||
defer r.stateAccess.Unlock()
|
||||
snapshot := r.currentRules.Load()
|
||||
snapshot.retain()
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func (r *Router) buildRules(startRules bool) ([]adapter.DNSRule, bool, error) {
|
||||
for i, ruleOptions := range r.rawRules {
|
||||
err := R.ValidateNoNestedDNSRuleActions(ruleOptions)
|
||||
@@ -229,7 +267,8 @@ func (r *Router) buildRules(startRules bool) ([]adapter.DNSRule, bool, error) {
|
||||
}
|
||||
newRules := make([]adapter.DNSRule, 0, len(r.rawRules))
|
||||
for i, ruleOptions := range r.rawRules {
|
||||
dnsRule, err := R.NewDNSRule(r.ctx, r.logger, ruleOptions, true, legacyDNSMode)
|
||||
var dnsRule adapter.DNSRule
|
||||
dnsRule, err = R.NewDNSRule(r.ctx, r.logger, ruleOptions, true, legacyDNSMode)
|
||||
if err != nil {
|
||||
closeRules(newRules)
|
||||
return nil, false, E.Cause(err, "parse dns rule[", i, "]")
|
||||
@@ -238,7 +277,7 @@ func (r *Router) buildRules(startRules bool) ([]adapter.DNSRule, bool, error) {
|
||||
}
|
||||
if startRules {
|
||||
for i, rule := range newRules {
|
||||
err := rule.Start()
|
||||
err = rule.Start()
|
||||
if err != nil {
|
||||
closeRules(newRules)
|
||||
return nil, false, E.Cause(err, "initialize DNS rule[", i, "]")
|
||||
@@ -259,12 +298,12 @@ func (r *Router) registerRuleSetCallbacks() (bool, error) {
|
||||
if len(tags) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
r.rulesAccess.RLock()
|
||||
r.stateAccess.Lock()
|
||||
if len(r.ruleSetCallbacks) > 0 {
|
||||
r.rulesAccess.RUnlock()
|
||||
r.stateAccess.Unlock()
|
||||
return true, nil
|
||||
}
|
||||
r.rulesAccess.RUnlock()
|
||||
r.stateAccess.Unlock()
|
||||
router := service.FromContext[adapter.Router](r.ctx)
|
||||
if router == nil {
|
||||
return false, E.New("router service not found")
|
||||
@@ -289,19 +328,19 @@ func (r *Router) registerRuleSetCallbacks() (bool, error) {
|
||||
element: element,
|
||||
})
|
||||
}
|
||||
r.rulesAccess.Lock()
|
||||
r.stateAccess.Lock()
|
||||
if len(r.ruleSetCallbacks) == 0 {
|
||||
r.ruleSetCallbacks = callbacks
|
||||
callbacks = nil
|
||||
}
|
||||
r.rulesAccess.Unlock()
|
||||
r.stateAccess.Unlock()
|
||||
for _, callback := range callbacks {
|
||||
callback.ruleSet.UnregisterCallback(callback.element)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) {
|
||||
func (r *Router) matchDNS(ctx context.Context, rules []adapter.DNSRule, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) {
|
||||
metadata := adapter.ContextFrom(ctx)
|
||||
if metadata == nil {
|
||||
panic("no context")
|
||||
@@ -310,8 +349,8 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
|
||||
if ruleIndex != -1 {
|
||||
currentRuleIndex = ruleIndex + 1
|
||||
}
|
||||
for ; currentRuleIndex < len(r.rules); currentRuleIndex++ {
|
||||
currentRule := r.rules[currentRuleIndex]
|
||||
for ; currentRuleIndex < len(rules); currentRuleIndex++ {
|
||||
currentRule := rules[currentRuleIndex]
|
||||
if currentRule.WithAddressLimit() && !isAddressQuery {
|
||||
continue
|
||||
}
|
||||
@@ -372,6 +411,9 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
|
||||
}
|
||||
|
||||
func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOptions R.RuleActionDNSRouteOptions) {
|
||||
// Strategy is intentionally skipped here. A non-default DNS rule action strategy
|
||||
// forces legacy mode via resolveLegacyDNSMode, so this path is only reachable
|
||||
// when strategy remains at its default value.
|
||||
if routeOptions.DisableCache {
|
||||
options.DisableCache = true
|
||||
}
|
||||
@@ -422,14 +464,14 @@ type exchangeWithRulesResult struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *Router) exchangeWithRules(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions, allowFakeIP bool) exchangeWithRulesResult {
|
||||
func (r *Router) exchangeWithRules(ctx context.Context, rules []adapter.DNSRule, message *mDNS.Msg, options adapter.DNSQueryOptions, allowFakeIP bool) exchangeWithRulesResult {
|
||||
metadata := adapter.ContextFrom(ctx)
|
||||
if metadata == nil {
|
||||
panic("no context")
|
||||
}
|
||||
effectiveOptions := options
|
||||
var savedResponse *mDNS.Msg
|
||||
for currentRuleIndex, currentRule := range r.rules {
|
||||
for currentRuleIndex, currentRule := range rules {
|
||||
metadata.ResetRuleCache()
|
||||
metadata.DNSResponse = savedResponse
|
||||
metadata.DestinationAddressMatchFromResponse = false
|
||||
@@ -460,6 +502,9 @@ func (r *Router) exchangeWithRules(ctx context.Context, message *mDNS.Msg, optio
|
||||
}
|
||||
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil)
|
||||
if err != nil {
|
||||
if E.IsClosedOrCanceled(err) {
|
||||
return exchangeWithRulesResult{err: err}
|
||||
}
|
||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
|
||||
savedResponse = nil
|
||||
continue
|
||||
@@ -511,9 +556,8 @@ func (r *Router) exchangeWithRules(ctx context.Context, message *mDNS.Msg, optio
|
||||
}
|
||||
}
|
||||
}
|
||||
queryOptions := effectiveOptions
|
||||
transport := r.transport.Default()
|
||||
exchangeOptions := queryOptions
|
||||
exchangeOptions := effectiveOptions
|
||||
if exchangeOptions.Strategy == C.DomainStrategyAsIS {
|
||||
exchangeOptions.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
@@ -539,17 +583,6 @@ func (r *Router) resolveLookupStrategy(options adapter.DNSQueryOptions) C.Domain
|
||||
return r.defaultDomainStrategy
|
||||
}
|
||||
|
||||
func lookupStrategyAllowsQueryType(strategy C.DomainStrategy, qType uint16) bool {
|
||||
switch strategy {
|
||||
case C.DomainStrategyIPv4Only:
|
||||
return qType == mDNS.TypeA
|
||||
case C.DomainStrategyIPv6Only:
|
||||
return qType == mDNS.TypeAAAA
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func withLookupQueryMetadata(ctx context.Context, qType uint16) context.Context {
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
metadata.QueryType = qType
|
||||
@@ -578,18 +611,18 @@ func filterAddressesByQueryType(addresses []netip.Addr, qType uint16) []netip.Ad
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) lookupWithRules(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
||||
func (r *Router) lookupWithRules(ctx context.Context, rules []adapter.DNSRule, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
||||
strategy := r.resolveLookupStrategy(options)
|
||||
lookupOptions := options
|
||||
if strategy != C.DomainStrategyAsIS {
|
||||
lookupOptions.Strategy = strategy
|
||||
}
|
||||
if strategy == C.DomainStrategyIPv4Only {
|
||||
response, err := r.lookupWithRulesType(ctx, domain, mDNS.TypeA, lookupOptions)
|
||||
response, err := r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeA, lookupOptions)
|
||||
return response.addresses, err
|
||||
}
|
||||
if strategy == C.DomainStrategyIPv6Only {
|
||||
response, err := r.lookupWithRulesType(ctx, domain, mDNS.TypeAAAA, lookupOptions)
|
||||
response, err := r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeAAAA, lookupOptions)
|
||||
return response.addresses, err
|
||||
}
|
||||
var (
|
||||
@@ -598,12 +631,12 @@ func (r *Router) lookupWithRules(ctx context.Context, domain string, options ada
|
||||
)
|
||||
var group task.Group
|
||||
group.Append("exchange4", func(ctx context.Context) error {
|
||||
result, err := r.lookupWithRulesType(ctx, domain, mDNS.TypeA, lookupOptions)
|
||||
result, err := r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeA, lookupOptions)
|
||||
response4 = result
|
||||
return err
|
||||
})
|
||||
group.Append("exchange6", func(ctx context.Context) error {
|
||||
result, err := r.lookupWithRulesType(ctx, domain, mDNS.TypeAAAA, lookupOptions)
|
||||
result, err := r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeAAAA, lookupOptions)
|
||||
response6 = result
|
||||
return err
|
||||
})
|
||||
@@ -614,18 +647,18 @@ func (r *Router) lookupWithRules(ctx context.Context, domain string, options ada
|
||||
return sortAddresses(response4.addresses, response6.addresses, strategy), nil
|
||||
}
|
||||
|
||||
func (r *Router) lookupWithRulesType(ctx context.Context, domain string, qType uint16, options adapter.DNSQueryOptions) (lookupWithRulesResponse, error) {
|
||||
func (r *Router) lookupWithRulesType(ctx context.Context, rules []adapter.DNSRule, domain string, qType uint16, options adapter.DNSQueryOptions) (lookupWithRulesResponse, error) {
|
||||
request := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
RecursionDesired: true,
|
||||
},
|
||||
Question: []mDNS.Question{{
|
||||
Name: mDNS.Fqdn(FqdnToDomain(domain)),
|
||||
Name: mDNS.Fqdn(domain),
|
||||
Qtype: qType,
|
||||
Qclass: mDNS.ClassINET,
|
||||
}},
|
||||
}
|
||||
exchangeResult := r.exchangeWithRules(withLookupQueryMetadata(ctx, qType), request, options, false)
|
||||
exchangeResult := r.exchangeWithRules(withLookupQueryMetadata(ctx, qType), rules, request, options, false)
|
||||
result := lookupWithRulesResponse{}
|
||||
if exchangeResult.rejectAction != nil {
|
||||
return result, exchangeResult.rejectAction.Error(ctx)
|
||||
@@ -636,9 +669,6 @@ func (r *Router) lookupWithRulesType(ctx context.Context, domain string, qType u
|
||||
if exchangeResult.response.Rcode != mDNS.RcodeSuccess {
|
||||
return result, RcodeError(exchangeResult.response.Rcode)
|
||||
}
|
||||
if !lookupStrategyAllowsQueryType(r.resolveLookupStrategy(options), qType) {
|
||||
return result, nil
|
||||
}
|
||||
result.addresses = filterAddressesByQueryType(MessageToAddresses(exchangeResult.response), qType)
|
||||
return result, nil
|
||||
}
|
||||
@@ -656,8 +686,16 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
}
|
||||
return &responseMessage, nil
|
||||
}
|
||||
r.rulesAccess.RLock()
|
||||
defer r.rulesAccess.RUnlock()
|
||||
snapshot := r.acquireRulesSnapshot()
|
||||
defer snapshot.release()
|
||||
var (
|
||||
rules []adapter.DNSRule
|
||||
legacyDNSMode bool
|
||||
)
|
||||
if snapshot != nil {
|
||||
rules = snapshot.rules
|
||||
legacyDNSMode = snapshot.legacyDNSMode
|
||||
}
|
||||
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
|
||||
var (
|
||||
response *mDNS.Msg
|
||||
@@ -683,8 +721,8 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
options.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
response, err = r.client.Exchange(ctx, transport, message, options, nil)
|
||||
} else if !r.legacyDNSMode {
|
||||
exchangeResult := r.exchangeWithRules(ctx, message, options, true)
|
||||
} else if !legacyDNSMode {
|
||||
exchangeResult := r.exchangeWithRules(ctx, rules, message, options, true)
|
||||
response, transport, err = exchangeResult.response, exchangeResult.transport, exchangeResult.err
|
||||
} else {
|
||||
var (
|
||||
@@ -695,7 +733,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
for {
|
||||
dnsCtx := adapter.OverrideContext(ctx)
|
||||
dnsOptions := options
|
||||
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
|
||||
transport, rule, ruleIndex = r.matchDNS(ctx, rules, true, ruleIndex, isAddressQuery(message), &dnsOptions)
|
||||
if rule != nil {
|
||||
switch action := rule.Action().(type) {
|
||||
case *R.RuleActionReject:
|
||||
@@ -713,7 +751,9 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
return nil, tun.ErrDrop
|
||||
}
|
||||
case *R.RuleActionPredefined:
|
||||
return action.Response(message), nil
|
||||
err = nil
|
||||
response = action.Response(message)
|
||||
goto done
|
||||
}
|
||||
}
|
||||
responseCheck := addressLimitResponseCheck(rule, metadata)
|
||||
@@ -741,6 +781,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
break
|
||||
}
|
||||
}
|
||||
done:
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -760,8 +801,16 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
}
|
||||
|
||||
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
||||
r.rulesAccess.RLock()
|
||||
defer r.rulesAccess.RUnlock()
|
||||
snapshot := r.acquireRulesSnapshot()
|
||||
defer snapshot.release()
|
||||
var (
|
||||
rules []adapter.DNSRule
|
||||
legacyDNSMode bool
|
||||
)
|
||||
if snapshot != nil {
|
||||
rules = snapshot.rules
|
||||
legacyDNSMode = snapshot.legacyDNSMode
|
||||
}
|
||||
var (
|
||||
responseAddrs []netip.Addr
|
||||
err error
|
||||
@@ -797,8 +846,8 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
||||
options.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
responseAddrs, err = r.client.Lookup(ctx, transport, domain, options, nil)
|
||||
} else if !r.legacyDNSMode {
|
||||
responseAddrs, err = r.lookupWithRules(ctx, domain, options)
|
||||
} else if !legacyDNSMode {
|
||||
responseAddrs, err = r.lookupWithRules(ctx, rules, domain, options)
|
||||
} else {
|
||||
var (
|
||||
transport adapter.DNSTransport
|
||||
@@ -809,7 +858,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
||||
for {
|
||||
dnsCtx := adapter.OverrideContext(ctx)
|
||||
dnsOptions := options
|
||||
transport, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true, &dnsOptions)
|
||||
transport, rule, ruleIndex = r.matchDNS(ctx, rules, false, ruleIndex, true, &dnsOptions)
|
||||
if rule != nil {
|
||||
switch action := rule.Action().(type) {
|
||||
case *R.RuleActionReject:
|
||||
@@ -867,7 +916,6 @@ func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundCo
|
||||
responseMetadata := *metadata
|
||||
return func(response *mDNS.Msg) bool {
|
||||
checkMetadata := responseMetadata
|
||||
checkMetadata.DNSResponse = response
|
||||
return rule.MatchAddressLimit(&checkMetadata, response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,11 @@ type fakeDNSClient struct {
|
||||
lookup func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error)
|
||||
}
|
||||
|
||||
type recordingExchangeDNSClient struct {
|
||||
beforeExchange func(ctx context.Context, transport adapter.DNSTransport, message *mDNS.Msg, options adapter.DNSQueryOptions)
|
||||
exchange func(transport adapter.DNSTransport, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error)
|
||||
}
|
||||
|
||||
type fakeDeprecatedManager struct {
|
||||
features []deprecated.Note
|
||||
}
|
||||
@@ -113,6 +118,7 @@ func (r *fakeRouter) RuleSet(tag string) (adapter.RuleSet, bool) {
|
||||
ruleSet, loaded := r.ruleSets[tag]
|
||||
return ruleSet, loaded
|
||||
}
|
||||
|
||||
func (r *fakeRouter) setRuleSet(tag string, ruleSet adapter.RuleSet) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
@@ -135,7 +141,7 @@ type fakeRuleSet struct {
|
||||
match func(*adapter.InboundContext) bool
|
||||
callbacks list.List[adapter.RuleSetUpdateCallback]
|
||||
refs int
|
||||
afterIncrementReference func()
|
||||
afterIncrementReference func()
|
||||
beforeDecrementReference func()
|
||||
}
|
||||
|
||||
@@ -255,10 +261,54 @@ func (c *fakeDNSClient) Lookup(_ context.Context, transport adapter.DNSTransport
|
||||
return MessageToAddresses(response), nil
|
||||
}
|
||||
|
||||
func (c *recordingExchangeDNSClient) Start() {}
|
||||
|
||||
func (c *recordingExchangeDNSClient) Exchange(ctx context.Context, transport adapter.DNSTransport, message *mDNS.Msg, options adapter.DNSQueryOptions, _ func(*mDNS.Msg) bool) (*mDNS.Msg, error) {
|
||||
if c.beforeExchange != nil {
|
||||
c.beforeExchange(ctx, transport, message, options)
|
||||
}
|
||||
if c.exchange == nil {
|
||||
return nil, E.New("unused client exchange")
|
||||
}
|
||||
return c.exchange(transport, message, options)
|
||||
}
|
||||
|
||||
func (c *recordingExchangeDNSClient) Lookup(context.Context, adapter.DNSTransport, string, adapter.DNSQueryOptions, func(*mDNS.Msg) bool) ([]netip.Addr, error) {
|
||||
return nil, E.New("unused client lookup")
|
||||
}
|
||||
|
||||
func (c *fakeDNSClient) ClearCache() {}
|
||||
|
||||
func (c *recordingExchangeDNSClient) ClearCache() {}
|
||||
|
||||
func newTestRouter(t *testing.T, rules []option.DNSRule, transportManager *fakeDNSTransportManager, client *fakeDNSClient) *Router {
|
||||
return newTestRouterWithContext(t, context.Background(), rules, transportManager, client)
|
||||
router := newTestRouterWithContext(t, context.Background(), rules, transportManager, client)
|
||||
t.Cleanup(func() {
|
||||
router.Close()
|
||||
})
|
||||
return router
|
||||
}
|
||||
|
||||
func newTestRouterWithDNSClient(t *testing.T, rules []option.DNSRule, transportManager *fakeDNSTransportManager, client adapter.DNSClient) *Router {
|
||||
router := &Router{
|
||||
ctx: context.Background(),
|
||||
logger: log.NewNOPFactory().NewLogger("dns"),
|
||||
transport: transportManager,
|
||||
client: client,
|
||||
rawRules: make([]option.DNSRule, 0, len(rules)),
|
||||
defaultDomainStrategy: C.DomainStrategyAsIS,
|
||||
}
|
||||
router.currentRules.Store(newRulesSnapshot(make([]adapter.DNSRule, 0, len(rules)), false))
|
||||
if rules != nil {
|
||||
err := router.Initialize(rules)
|
||||
require.NoError(t, err)
|
||||
err = router.Start(adapter.StartStateStart)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
router.Close()
|
||||
})
|
||||
return router
|
||||
}
|
||||
|
||||
func newTestRouterWithContext(t *testing.T, ctx context.Context, rules []option.DNSRule, transportManager *fakeDNSTransportManager, client *fakeDNSClient) *Router {
|
||||
@@ -273,9 +323,9 @@ func newTestRouterWithContextAndLogger(t *testing.T, ctx context.Context, rules
|
||||
transport: transportManager,
|
||||
client: client,
|
||||
rawRules: make([]option.DNSRule, 0, len(rules)),
|
||||
rules: make([]adapter.DNSRule, 0, len(rules)),
|
||||
defaultDomainStrategy: C.DomainStrategyAsIS,
|
||||
}
|
||||
router.currentRules.Store(newRulesSnapshot(make([]adapter.DNSRule, 0, len(rules)), false))
|
||||
if rules != nil {
|
||||
err := router.Initialize(rules)
|
||||
require.NoError(t, err)
|
||||
@@ -427,9 +477,9 @@ func TestInitializeRejectsInvalidDNSRuleParseError(t *testing.T) {
|
||||
transport: &fakeDNSTransportManager{},
|
||||
client: &fakeDNSClient{},
|
||||
rawRules: make([]option.DNSRule, 0, 1),
|
||||
rules: make([]adapter.DNSRule, 0, 1),
|
||||
defaultDomainStrategy: C.DomainStrategyAsIS,
|
||||
}
|
||||
router.currentRules.Store(newRulesSnapshot(make([]adapter.DNSRule, 0, 1), false))
|
||||
err := router.Initialize([]option.DNSRule{{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
@@ -474,9 +524,9 @@ func TestInitializeRejectsDirectLegacyRuleWhenRuleSetForcesNew(t *testing.T) {
|
||||
transport: &fakeDNSTransportManager{},
|
||||
client: &fakeDNSClient{},
|
||||
rawRules: make([]option.DNSRule, 0, 2),
|
||||
rules: make([]adapter.DNSRule, 0, 2),
|
||||
defaultDomainStrategy: C.DomainStrategyAsIS,
|
||||
}
|
||||
router.currentRules.Store(newRulesSnapshot(make([]adapter.DNSRule, 0, 2), false))
|
||||
err = router.Initialize([]option.DNSRule{
|
||||
{
|
||||
Type: C.RuleTypeDefault,
|
||||
@@ -557,7 +607,7 @@ func TestLookupLegacyDNSModeDefersRuleSetDestinationIPMatch(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
require.True(t, router.legacyDNSMode)
|
||||
require.True(t, router.currentRules.Load().legacyDNSMode)
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{
|
||||
LookupStrategy: C.DomainStrategyIPv4Only,
|
||||
@@ -700,7 +750,7 @@ func TestRuleSetUpdateKeepsLastSuccessfullyCompiledRuleGraphWhenRebuildFails(t *
|
||||
require.NoError(t, closeErr)
|
||||
})
|
||||
|
||||
require.True(t, router.legacyDNSMode)
|
||||
require.True(t, router.currentRules.Load().legacyDNSMode)
|
||||
require.Equal(t, 1, callbackRuleSet.refCount())
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{})
|
||||
@@ -723,7 +773,7 @@ func TestRuleSetUpdateKeepsLastSuccessfullyCompiledRuleGraphWhenRebuildFails(t *
|
||||
})
|
||||
rebuildErrorEntry := waitForLogMessageContaining(t, logEntries, logDone, "rebuild DNS rules after rule-set update")
|
||||
require.Contains(t, rebuildErrorEntry.Message, "ip_cidr and ip_is_private require match_response")
|
||||
require.True(t, router.legacyDNSMode)
|
||||
require.True(t, router.currentRules.Load().legacyDNSMode)
|
||||
require.Equal(t, 1, callbackRuleSet.refCount())
|
||||
require.Zero(t, rebuildTargetRuleSet.refCount())
|
||||
|
||||
@@ -992,7 +1042,7 @@ func TestCloseDuringRebuildDiscardsResult(t *testing.T) {
|
||||
}
|
||||
},
|
||||
})
|
||||
require.True(t, router.legacyDNSMode)
|
||||
require.True(t, router.currentRules.Load().legacyDNSMode)
|
||||
require.Equal(t, 1, fakeSet.refCount())
|
||||
|
||||
callbacks := fakeSet.snapshotCallbacks()
|
||||
@@ -1031,12 +1081,11 @@ func TestCloseDuringRebuildDiscardsResult(t *testing.T) {
|
||||
|
||||
fakeSet.metadataRead = nil
|
||||
|
||||
router.rulesAccess.RLock()
|
||||
router.stateAccess.Lock()
|
||||
require.True(t, router.closing)
|
||||
require.Nil(t, router.rules)
|
||||
require.Empty(t, router.ruleSetCallbacks)
|
||||
router.rulesAccess.RUnlock()
|
||||
require.True(t, router.legacyDNSMode)
|
||||
router.stateAccess.Unlock()
|
||||
require.Nil(t, router.currentRules.Load())
|
||||
require.Zero(t, fakeSet.refCount())
|
||||
}
|
||||
|
||||
@@ -1098,11 +1147,218 @@ func TestCloseIgnoresSnapshottedRuleSetCallback(t *testing.T) {
|
||||
}
|
||||
callbacks[0](fakeSet)
|
||||
|
||||
router.rulesAccess.RLock()
|
||||
defer router.rulesAccess.RUnlock()
|
||||
router.stateAccess.Lock()
|
||||
require.True(t, router.closing)
|
||||
require.Nil(t, router.rules)
|
||||
require.Empty(t, router.ruleSetCallbacks)
|
||||
router.stateAccess.Unlock()
|
||||
require.Nil(t, router.currentRules.Load())
|
||||
}
|
||||
|
||||
func TestRuleSetUpdateDoesNotBlockOnInFlightLookup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fakeSet := &fakeRuleSet{
|
||||
metadata: adapter.RuleSetMetadata{
|
||||
ContainsIPCIDRRule: true,
|
||||
},
|
||||
}
|
||||
ctx := service.ContextWith[adapter.Router](context.Background(), &fakeRouter{
|
||||
ruleSets: map[string]adapter.RuleSet{
|
||||
"dynamic-set": fakeSet,
|
||||
},
|
||||
})
|
||||
defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}
|
||||
selectedTransport := &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}
|
||||
lookupStarted := make(chan struct{})
|
||||
releaseLookup := make(chan struct{})
|
||||
router := newTestRouterWithContext(t, ctx, []option.DNSRule{{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
RawDefaultDNSRule: option.RawDefaultDNSRule{
|
||||
RuleSet: badoption.Listable[string]{"dynamic-set"},
|
||||
},
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeRoute,
|
||||
RouteOptions: option.DNSRouteActionOptions{Server: "selected"},
|
||||
},
|
||||
},
|
||||
}}, &fakeDNSTransportManager{
|
||||
defaultTransport: defaultTransport,
|
||||
transports: map[string]adapter.DNSTransport{
|
||||
"default": defaultTransport,
|
||||
"selected": selectedTransport,
|
||||
},
|
||||
}, &fakeDNSClient{
|
||||
lookup: func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) {
|
||||
require.Equal(t, "selected", transport.Tag())
|
||||
require.Equal(t, "example.com", domain)
|
||||
require.Equal(t, C.DomainStrategyIPv4Only, options.LookupStrategy)
|
||||
close(lookupStarted)
|
||||
<-releaseLookup
|
||||
response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("10.0.0.1")}, 60)
|
||||
return MessageToAddresses(response), response, nil
|
||||
},
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
closeErr := router.Close()
|
||||
require.NoError(t, closeErr)
|
||||
})
|
||||
|
||||
require.True(t, router.currentRules.Load().legacyDNSMode)
|
||||
require.Equal(t, 1, fakeSet.refCount())
|
||||
|
||||
var (
|
||||
addresses []netip.Addr
|
||||
err error
|
||||
)
|
||||
lookupDone := make(chan struct{})
|
||||
go func() {
|
||||
addresses, err = router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{
|
||||
LookupStrategy: C.DomainStrategyIPv4Only,
|
||||
})
|
||||
close(lookupDone)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-lookupStarted:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("lookup did not reach DNS client")
|
||||
}
|
||||
|
||||
rebuildDone := make(chan struct{})
|
||||
go func() {
|
||||
fakeSet.updateMetadata(adapter.RuleSetMetadata{
|
||||
ContainsIPCIDRRule: true,
|
||||
})
|
||||
close(rebuildDone)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-rebuildDone:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("rebuild blocked on in-flight lookup")
|
||||
}
|
||||
|
||||
require.Equal(t, 2, fakeSet.refCount())
|
||||
|
||||
select {
|
||||
case <-lookupDone:
|
||||
t.Fatal("lookup finished before release")
|
||||
default:
|
||||
}
|
||||
|
||||
close(releaseLookup)
|
||||
|
||||
select {
|
||||
case <-lookupDone:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("lookup did not finish after release")
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("10.0.0.1")}, addresses)
|
||||
require.Eventually(t, func() bool {
|
||||
return fakeSet.refCount() == 1
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestCloseReleasesSnapshottedRulesAfterInFlightLookup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fakeSet := &fakeRuleSet{
|
||||
metadata: adapter.RuleSetMetadata{
|
||||
ContainsIPCIDRRule: true,
|
||||
},
|
||||
}
|
||||
ctx := service.ContextWith[adapter.Router](context.Background(), &fakeRouter{
|
||||
ruleSets: map[string]adapter.RuleSet{
|
||||
"dynamic-set": fakeSet,
|
||||
},
|
||||
})
|
||||
defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}
|
||||
selectedTransport := &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}
|
||||
lookupStarted := make(chan struct{})
|
||||
releaseLookup := make(chan struct{})
|
||||
router := newTestRouterWithContext(t, ctx, []option.DNSRule{{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
RawDefaultDNSRule: option.RawDefaultDNSRule{
|
||||
RuleSet: badoption.Listable[string]{"dynamic-set"},
|
||||
},
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeRoute,
|
||||
RouteOptions: option.DNSRouteActionOptions{Server: "selected"},
|
||||
},
|
||||
},
|
||||
}}, &fakeDNSTransportManager{
|
||||
defaultTransport: defaultTransport,
|
||||
transports: map[string]adapter.DNSTransport{
|
||||
"default": defaultTransport,
|
||||
"selected": selectedTransport,
|
||||
},
|
||||
}, &fakeDNSClient{
|
||||
lookup: func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) {
|
||||
require.Equal(t, "selected", transport.Tag())
|
||||
require.Equal(t, "example.com", domain)
|
||||
require.Equal(t, C.DomainStrategyIPv4Only, options.LookupStrategy)
|
||||
close(lookupStarted)
|
||||
<-releaseLookup
|
||||
response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("10.0.0.1")}, 60)
|
||||
return MessageToAddresses(response), response, nil
|
||||
},
|
||||
})
|
||||
|
||||
require.True(t, router.currentRules.Load().legacyDNSMode)
|
||||
require.Equal(t, 1, fakeSet.refCount())
|
||||
|
||||
var (
|
||||
addresses []netip.Addr
|
||||
lookupErr error
|
||||
closeErr error
|
||||
)
|
||||
lookupDone := make(chan struct{})
|
||||
go func() {
|
||||
addresses, lookupErr = router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{
|
||||
LookupStrategy: C.DomainStrategyIPv4Only,
|
||||
})
|
||||
close(lookupDone)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-lookupStarted:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("lookup did not reach DNS client")
|
||||
}
|
||||
|
||||
closeDone := make(chan struct{})
|
||||
go func() {
|
||||
closeErr = router.Close()
|
||||
close(closeDone)
|
||||
}()
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
return router.currentRules.Load() == nil && fakeSet.refCount() == 1
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
|
||||
close(releaseLookup)
|
||||
|
||||
select {
|
||||
case <-lookupDone:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("lookup did not finish after release")
|
||||
}
|
||||
select {
|
||||
case <-closeDone:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("close did not finish")
|
||||
}
|
||||
|
||||
require.NoError(t, lookupErr)
|
||||
require.NoError(t, closeErr)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("10.0.0.1")}, addresses)
|
||||
require.Eventually(t, func() bool {
|
||||
return fakeSet.refCount() == 0
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestLookupLegacyDNSModeDefersDirectDestinationIPMatch(t *testing.T) {
|
||||
@@ -1143,7 +1399,7 @@ func TestLookupLegacyDNSModeDefersDirectDestinationIPMatch(t *testing.T) {
|
||||
},
|
||||
}, client)
|
||||
|
||||
require.True(t, router.legacyDNSMode)
|
||||
require.True(t, router.currentRules.Load().legacyDNSMode)
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{
|
||||
LookupStrategy: C.DomainStrategyIPv4Only,
|
||||
@@ -1295,7 +1551,7 @@ func TestLookupLegacyDNSModeRuleSetAcceptEmptyDoesNotTreatMismatchAsEmpty(t *tes
|
||||
},
|
||||
})
|
||||
|
||||
require.True(t, router.legacyDNSMode)
|
||||
require.True(t, router.currentRules.Load().legacyDNSMode)
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{
|
||||
LookupStrategy: C.DomainStrategyIPv4Only,
|
||||
@@ -1375,6 +1631,206 @@ func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseRoute(t *testing.T) {
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response))
|
||||
}
|
||||
|
||||
func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseRcodeRoute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
transportManager := &fakeDNSTransportManager{
|
||||
defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
|
||||
transports: map[string]adapter.DNSTransport{
|
||||
"upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP},
|
||||
"selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP},
|
||||
"default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
|
||||
},
|
||||
}
|
||||
client := &fakeDNSClient{
|
||||
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
switch transport.Tag() {
|
||||
case "upstream":
|
||||
return &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Response: true,
|
||||
Rcode: mDNS.RcodeNameError,
|
||||
},
|
||||
Question: []mDNS.Question{message.Question[0]},
|
||||
}, nil
|
||||
case "selected":
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil
|
||||
default:
|
||||
return nil, E.New("unexpected transport")
|
||||
}
|
||||
},
|
||||
}
|
||||
rcode := option.DNSRCode(mDNS.RcodeNameError)
|
||||
rules := []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,
|
||||
ResponseRcode: &rcode,
|
||||
},
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeRoute,
|
||||
RouteOptions: option.DNSRouteActionOptions{Server: "selected"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
router := newTestRouter(t, rules, transportManager, client)
|
||||
|
||||
response, err := router.Exchange(context.Background(), &mDNS.Msg{
|
||||
Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)},
|
||||
}, adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response))
|
||||
}
|
||||
|
||||
func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseNsRoute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
transportManager := &fakeDNSTransportManager{
|
||||
defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
|
||||
transports: map[string]adapter.DNSTransport{
|
||||
"upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP},
|
||||
"selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP},
|
||||
"default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
|
||||
},
|
||||
}
|
||||
nsRecord := mustRecord(t, "example.com. IN NS ns1.example.com.")
|
||||
client := &fakeDNSClient{
|
||||
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
switch transport.Tag() {
|
||||
case "upstream":
|
||||
return &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Response: true,
|
||||
Rcode: mDNS.RcodeSuccess,
|
||||
},
|
||||
Question: []mDNS.Question{message.Question[0]},
|
||||
Ns: []mDNS.RR{nsRecord.Build()},
|
||||
}, nil
|
||||
case "selected":
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil
|
||||
default:
|
||||
return nil, E.New("unexpected transport")
|
||||
}
|
||||
},
|
||||
}
|
||||
rules := []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,
|
||||
ResponseNs: badoption.Listable[option.DNSRecordOptions]{nsRecord},
|
||||
},
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeRoute,
|
||||
RouteOptions: option.DNSRouteActionOptions{Server: "selected"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
router := newTestRouter(t, rules, transportManager, client)
|
||||
|
||||
response, err := router.Exchange(context.Background(), &mDNS.Msg{
|
||||
Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)},
|
||||
}, adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response))
|
||||
}
|
||||
|
||||
func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseExtraRoute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
transportManager := &fakeDNSTransportManager{
|
||||
defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
|
||||
transports: map[string]adapter.DNSTransport{
|
||||
"upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP},
|
||||
"selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP},
|
||||
"default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
|
||||
},
|
||||
}
|
||||
extraRecord := mustRecord(t, "ns1.example.com. IN A 192.0.2.53")
|
||||
client := &fakeDNSClient{
|
||||
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
switch transport.Tag() {
|
||||
case "upstream":
|
||||
return &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Response: true,
|
||||
Rcode: mDNS.RcodeSuccess,
|
||||
},
|
||||
Question: []mDNS.Question{message.Question[0]},
|
||||
Extra: []mDNS.RR{extraRecord.Build()},
|
||||
}, nil
|
||||
case "selected":
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil
|
||||
default:
|
||||
return nil, E.New("unexpected transport")
|
||||
}
|
||||
},
|
||||
}
|
||||
rules := []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,
|
||||
ResponseExtra: badoption.Listable[option.DNSRecordOptions]{extraRecord},
|
||||
},
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeRoute,
|
||||
RouteOptions: option.DNSRouteActionOptions{Server: "selected"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
router := newTestRouter(t, rules, transportManager, client)
|
||||
|
||||
response, err := router.Exchange(context.Background(), &mDNS.Msg{
|
||||
Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)},
|
||||
}, adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response))
|
||||
}
|
||||
|
||||
func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseRouteIgnoresTTL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1696,6 +2152,98 @@ func TestExchangeLegacyDNSModeDisabledEvaluateRouteResolutionFailureClearsRespon
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("4.4.4.4")}, MessageToAddresses(response))
|
||||
}
|
||||
|
||||
func TestExchangeLegacyDNSModeDisabledSecondEvaluateOverwritesFirstResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
transportManager := &fakeDNSTransportManager{
|
||||
defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
|
||||
transports: map[string]adapter.DNSTransport{
|
||||
"default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
|
||||
"first-upstream": &fakeDNSTransport{tag: "first-upstream", transportType: C.DNSTypeUDP},
|
||||
"second-upstream": &fakeDNSTransport{tag: "second-upstream", transportType: C.DNSTypeUDP},
|
||||
"first-match": &fakeDNSTransport{tag: "first-match", transportType: C.DNSTypeUDP},
|
||||
"second-match": &fakeDNSTransport{tag: "second-match", transportType: C.DNSTypeUDP},
|
||||
},
|
||||
}
|
||||
client := &fakeDNSClient{
|
||||
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
switch transport.Tag() {
|
||||
case "first-upstream":
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil
|
||||
case "second-upstream":
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil
|
||||
case "first-match":
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("7.7.7.7")}, 60), nil
|
||||
case "second-match":
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil
|
||||
case "default":
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("4.4.4.4")}, 60), nil
|
||||
default:
|
||||
return nil, E.New("unexpected transport")
|
||||
}
|
||||
},
|
||||
}
|
||||
rules := []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: "first-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: "second-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: "first-match"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
RawDefaultDNSRule: option.RawDefaultDNSRule{
|
||||
MatchResponse: true,
|
||||
ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 2.2.2.2")},
|
||||
},
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeRoute,
|
||||
RouteOptions: option.DNSRouteActionOptions{Server: "second-match"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
router := newTestRouter(t, rules, transportManager, client)
|
||||
|
||||
response, err := router.Exchange(context.Background(), &mDNS.Msg{
|
||||
Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)},
|
||||
}, adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response))
|
||||
}
|
||||
|
||||
func TestExchangeLegacyDNSModeDisabledEvaluateExchangeFailureUsesMatchResponseBooleanSemantics(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1715,7 +2263,6 @@ func TestExchangeLegacyDNSModeDisabledEvaluateExchangeFailureUsesMatchResponseBo
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1801,7 +2348,7 @@ func TestLookupLegacyDNSModeDisabledAllowsPartialSuccess(t *testing.T) {
|
||||
}
|
||||
},
|
||||
})
|
||||
router.legacyDNSMode = false
|
||||
router.currentRules.Load().legacyDNSMode = false
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -1838,7 +2385,7 @@ func TestLookupLegacyDNSModeDisabledSkipsFakeIPRule(t *testing.T) {
|
||||
return FixedResponse(0, message.Question[0], nil, 60), nil
|
||||
},
|
||||
})
|
||||
router.legacyDNSMode = false
|
||||
router.currentRules.Load().legacyDNSMode = false
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -1918,7 +2465,7 @@ func TestLookupLegacyDNSModeDisabledEvaluateSkipFakeIPPreservesResponse(t *testi
|
||||
}
|
||||
},
|
||||
})
|
||||
router.legacyDNSMode = false
|
||||
router.currentRules.Load().legacyDNSMode = false
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -1961,7 +2508,7 @@ func TestLookupLegacyDNSModeDisabledUsesQueryTypeRule(t *testing.T) {
|
||||
}
|
||||
},
|
||||
})
|
||||
require.False(t, router.legacyDNSMode)
|
||||
require.False(t, router.currentRules.Load().legacyDNSMode)
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -2027,7 +2574,7 @@ func TestLookupLegacyDNSModeDisabledUsesRuleSetQueryTypeRule(t *testing.T) {
|
||||
}
|
||||
},
|
||||
})
|
||||
require.False(t, router.legacyDNSMode)
|
||||
require.False(t, router.currentRules.Load().legacyDNSMode)
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -2076,7 +2623,7 @@ func TestLookupLegacyDNSModeDisabledUsesIPVersionRule(t *testing.T) {
|
||||
}
|
||||
},
|
||||
})
|
||||
require.False(t, router.legacyDNSMode)
|
||||
require.False(t, router.currentRules.Load().legacyDNSMode)
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -2092,9 +2639,9 @@ func TestInitializeRejectsDNSRuleStrategyWhenLegacyDNSModeIsDisabledByEvaluate(t
|
||||
transport: &fakeDNSTransportManager{},
|
||||
client: &fakeDNSClient{},
|
||||
rawRules: make([]option.DNSRule, 0, 1),
|
||||
rules: make([]adapter.DNSRule, 0, 1),
|
||||
defaultDomainStrategy: C.DomainStrategyAsIS,
|
||||
}
|
||||
router.currentRules.Store(newRulesSnapshot(make([]adapter.DNSRule, 0, 1), false))
|
||||
err := router.Initialize([]option.DNSRule{{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
@@ -2122,9 +2669,9 @@ func TestInitializeRejectsDNSRuleStrategyWhenLegacyDNSModeIsDisabledByMatchRespo
|
||||
transport: &fakeDNSTransportManager{},
|
||||
client: &fakeDNSClient{},
|
||||
rawRules: make([]option.DNSRule, 0, 1),
|
||||
rules: make([]adapter.DNSRule, 0, 1),
|
||||
defaultDomainStrategy: C.DomainStrategyAsIS,
|
||||
}
|
||||
router.currentRules.Store(newRulesSnapshot(make([]adapter.DNSRule, 0, 1), false))
|
||||
err := router.Initialize([]option.DNSRule{{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
@@ -2142,6 +2689,68 @@ func TestInitializeRejectsDNSRuleStrategyWhenLegacyDNSModeIsDisabledByMatchRespo
|
||||
require.ErrorContains(t, err, "legacyDNSMode")
|
||||
}
|
||||
|
||||
func TestExchangeLegacyDNSModeDisabledRouteOptionsApplyQueryOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}
|
||||
rewriteTTL := uint32(30)
|
||||
var capturedOptions adapter.DNSQueryOptions
|
||||
router := newTestRouterWithDNSClient(t, []option.DNSRule{
|
||||
{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
RawDefaultDNSRule: option.RawDefaultDNSRule{
|
||||
QueryType: badoption.Listable[option.DNSQueryType]{option.DNSQueryType(mDNS.TypeA)},
|
||||
},
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeRouteOptions,
|
||||
RouteOptionsOptions: option.DNSRouteOptionsActionOptions{
|
||||
DisableCache: true,
|
||||
RewriteTTL: &rewriteTTL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
RawDefaultDNSRule: option.RawDefaultDNSRule{
|
||||
Domain: badoption.Listable[string]{"example.com"},
|
||||
},
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeRoute,
|
||||
RouteOptions: option.DNSRouteActionOptions{Server: "selected"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, &fakeDNSTransportManager{
|
||||
defaultTransport: defaultTransport,
|
||||
transports: map[string]adapter.DNSTransport{
|
||||
"default": defaultTransport,
|
||||
"selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP},
|
||||
},
|
||||
}, &recordingExchangeDNSClient{
|
||||
beforeExchange: func(ctx context.Context, transport adapter.DNSTransport, message *mDNS.Msg, options adapter.DNSQueryOptions) {
|
||||
require.Equal(t, "selected", transport.Tag())
|
||||
require.Equal(t, []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, message.Question)
|
||||
capturedOptions = options
|
||||
},
|
||||
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) {
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil
|
||||
},
|
||||
})
|
||||
require.False(t, router.currentRules.Load().legacyDNSMode)
|
||||
|
||||
response, err := router.Exchange(context.Background(), &mDNS.Msg{
|
||||
Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)},
|
||||
}, adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, MessageToAddresses(response))
|
||||
require.True(t, capturedOptions.DisableCache)
|
||||
require.NotNil(t, capturedOptions.RewriteTTL)
|
||||
require.Equal(t, rewriteTTL, *capturedOptions.RewriteTTL)
|
||||
}
|
||||
|
||||
func TestLookupLegacyDNSModeUsesRouteStrategy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -2175,7 +2784,7 @@ func TestLookupLegacyDNSModeUsesRouteStrategy(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
require.True(t, router.legacyDNSMode)
|
||||
require.True(t, router.currentRules.Load().legacyDNSMode)
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -2207,7 +2816,7 @@ func TestLookupLegacyDNSModeDisabledReturnsRejectedErrorForRejectAction(t *testi
|
||||
"default": defaultTransport,
|
||||
},
|
||||
}, &fakeDNSClient{})
|
||||
require.False(t, router.legacyDNSMode)
|
||||
require.False(t, router.currentRules.Load().legacyDNSMode)
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{})
|
||||
require.Nil(t, addresses)
|
||||
@@ -2240,7 +2849,7 @@ func TestExchangeLegacyDNSModeDisabledReturnsRefusedResponseForRejectAction(t *t
|
||||
"default": defaultTransport,
|
||||
},
|
||||
}, &fakeDNSClient{})
|
||||
require.False(t, router.legacyDNSMode)
|
||||
require.False(t, router.currentRules.Load().legacyDNSMode)
|
||||
|
||||
response, err := router.Exchange(context.Background(), &mDNS.Msg{
|
||||
Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)},
|
||||
@@ -2250,6 +2859,40 @@ func TestExchangeLegacyDNSModeDisabledReturnsRefusedResponseForRejectAction(t *t
|
||||
require.Equal(t, []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, response.Question)
|
||||
}
|
||||
|
||||
func TestExchangeLegacyDNSModeDisabledReturnsDropErrorForRejectDropAction(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.RuleActionTypeReject,
|
||||
RejectOptions: option.RejectActionOptions{
|
||||
Method: C.RuleActionRejectMethodDrop,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, &fakeDNSTransportManager{
|
||||
defaultTransport: defaultTransport,
|
||||
transports: map[string]adapter.DNSTransport{
|
||||
"default": defaultTransport,
|
||||
},
|
||||
}, &fakeDNSClient{})
|
||||
require.False(t, router.currentRules.Load().legacyDNSMode)
|
||||
|
||||
response, err := router.Exchange(context.Background(), &mDNS.Msg{
|
||||
Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)},
|
||||
}, adapter.DNSQueryOptions{})
|
||||
require.Nil(t, response)
|
||||
require.ErrorIs(t, err, tun.ErrDrop)
|
||||
}
|
||||
|
||||
func TestLookupLegacyDNSModeDisabledFiltersPerQueryTypeAddressesBeforeMerging(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -2278,7 +2921,7 @@ func TestLookupLegacyDNSModeDisabledFiltersPerQueryTypeAddressesBeforeMerging(t
|
||||
"default": defaultTransport,
|
||||
},
|
||||
}, &fakeDNSClient{})
|
||||
require.False(t, router.legacyDNSMode)
|
||||
require.False(t, router.currentRules.Load().legacyDNSMode)
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -2318,7 +2961,7 @@ func TestLookupLegacyDNSModeDisabledUsesInputStrategy(t *testing.T) {
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::2")}, 60), nil
|
||||
},
|
||||
})
|
||||
router.legacyDNSMode = false
|
||||
router.currentRules.Load().legacyDNSMode = false
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{
|
||||
Strategy: C.DomainStrategyIPv4Only,
|
||||
@@ -2359,7 +3002,7 @@ func TestLookupLegacyDNSModeDisabledUsesDefaultStrategy(t *testing.T) {
|
||||
},
|
||||
})
|
||||
router.defaultDomainStrategy = C.DomainStrategyIPv4Only
|
||||
router.legacyDNSMode = false
|
||||
router.currentRules.Load().legacyDNSMode = false
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -2445,9 +3088,9 @@ func TestLegacyDNSModeReportsLegacyAddressFilterDeprecation(t *testing.T) {
|
||||
ctx: ctx,
|
||||
logger: log.NewNOPFactory().NewLogger("dns"),
|
||||
client: &fakeDNSClient{},
|
||||
rules: make([]adapter.DNSRule, 0, 1),
|
||||
defaultDomainStrategy: C.DomainStrategyAsIS,
|
||||
}
|
||||
router.currentRules.Store(newRulesSnapshot(make([]adapter.DNSRule, 0, 1), false))
|
||||
err := router.Initialize([]option.DNSRule{{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
@@ -2477,9 +3120,9 @@ func TestLegacyDNSModeReportsDNSRuleStrategyDeprecation(t *testing.T) {
|
||||
ctx: ctx,
|
||||
logger: log.NewNOPFactory().NewLogger("dns"),
|
||||
client: &fakeDNSClient{},
|
||||
rules: make([]adapter.DNSRule, 0, 1),
|
||||
defaultDomainStrategy: C.DomainStrategyAsIS,
|
||||
}
|
||||
router.currentRules.Store(newRulesSnapshot(make([]adapter.DNSRule, 0, 1), false))
|
||||
err := router.Initialize([]option.DNSRule{{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
|
||||
@@ -544,6 +544,15 @@ See [DNS Rule Actions](../rule_action/) for details.
|
||||
|
||||
Moved to [DNS Rule Action](../rule_action#route).
|
||||
|
||||
### Legacy DNS Mode
|
||||
|
||||
`legacyDNSMode` is an internal compatibility mode that is automatically detected from your DNS rule
|
||||
configuration. It is disabled when any rule uses features introduced in sing-box 1.14.0 such as
|
||||
`evaluate`, `match_response`, response fields (`response_rcode`, `response_answer`, etc.),
|
||||
`query_type`, or `ip_version`. When disabled, `ip_cidr` and `ip_is_private` require `match_response`
|
||||
to be set, and deprecated fields like `strategy`, `ip_accept_any`, and `rule_set_ip_cidr_accept_empty`
|
||||
are no longer accepted.
|
||||
|
||||
### Address Filter Fields
|
||||
|
||||
Only takes effect for address requests (A/AAAA/HTTPS). When the query results do not match the address filtering rule items, the current rule will be skipped.
|
||||
|
||||
@@ -542,6 +542,14 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
已移动到 [DNS 规则动作](../rule_action#route).
|
||||
|
||||
### Legacy DNS Mode
|
||||
|
||||
`legacyDNSMode` 是一种内部兼容模式,会根据 DNS 规则配置自动检测。
|
||||
当任何规则使用了 sing-box 1.14.0 引入的特性(如 `evaluate`、`match_response`、
|
||||
响应字段(`response_rcode`、`response_answer` 等)、`query_type` 或 `ip_version`)时,
|
||||
该模式将被自动禁用。禁用后,`ip_cidr` 和 `ip_is_private` 需要设置 `match_response`,
|
||||
且已废弃的字段(如 `strategy`、`ip_accept_any`、`rule_set_ip_cidr_accept_empty`)将不再被接受。
|
||||
|
||||
### 地址筛选字段
|
||||
|
||||
仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
|
||||
@@ -579,17 +587,6 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
当 `legacyDNSMode` 未启用时,`match_response` 必须设为 `true`。
|
||||
|
||||
#### ip_accept_any
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`ip_accept_any` 已废弃且将在 sing-box 1.16.0 中被移除。
|
||||
仅在 `legacyDNSMode` 中可用。请使用 `match_response` 和响应项替代。
|
||||
|
||||
匹配任意 IP。
|
||||
|
||||
#### rule_set_ip_cidr_accept_empty
|
||||
|
||||
!!! question "自 sing-box 1.10.0 起"
|
||||
@@ -601,6 +598,17 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
使规则集中的 `ip_cidr` 规则接受空查询响应。
|
||||
|
||||
#### ip_accept_any
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`ip_accept_any` 已废弃且将在 sing-box 1.16.0 中被移除。
|
||||
仅在 `legacyDNSMode` 中可用。请使用 `match_response` 和响应项替代。
|
||||
|
||||
匹配任意 IP。
|
||||
|
||||
### 响应字段
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
@@ -39,11 +39,11 @@ Tag of target server.
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
!!! warning
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
`strategy` is deprecated and only supported in `legacyDNSMode`.
|
||||
`strategy` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0.
|
||||
|
||||
Set domain strategy for this query in `legacyDNSMode`.
|
||||
Set domain strategy for this query. Only supported when `legacyDNSMode` is active.
|
||||
|
||||
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
||||
|
||||
|
||||
@@ -39,11 +39,11 @@ icon: material/new-box
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
!!! warning
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`strategy` 已废弃,且仅在 `legacyDNSMode` 中可用。
|
||||
`strategy` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
在 `legacyDNSMode` 中为此查询设置域名策略。
|
||||
为此查询设置域名策略。仅在 `legacyDNSMode` 启用时可用。
|
||||
|
||||
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ var OptionIPAcceptAny = Note{
|
||||
Description: "`ip_accept_any` in DNS rules",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "DNS_RULE_IP_ACCEPT_ANY",
|
||||
MigrationLink: "https://sing-box.sagernet.org/configuration/dns/rule/",
|
||||
}
|
||||
|
||||
@@ -106,6 +107,7 @@ var OptionRuleSetIPCIDRAcceptEmpty = Note{
|
||||
Description: "`rule_set_ip_cidr_accept_empty` in DNS rules",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "DNS_RULE_RULE_SET_IP_CIDR_ACCEPT_EMPTY",
|
||||
MigrationLink: "https://sing-box.sagernet.org/configuration/dns/rule/",
|
||||
}
|
||||
|
||||
@@ -114,6 +116,7 @@ var OptionLegacyDNSAddressFilter = Note{
|
||||
Description: "legacy address filter DNS rule items",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "LEGACY_DNS_ADDRESS_FILTER",
|
||||
MigrationLink: "https://sing-box.sagernet.org/configuration/dns/rule/",
|
||||
}
|
||||
|
||||
@@ -122,6 +125,7 @@ var OptionLegacyDNSRuleStrategy = Note{
|
||||
Description: "`strategy` in DNS rule actions",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "LEGACY_DNS_RULE_STRATEGY",
|
||||
MigrationLink: "https://sing-box.sagernet.org/configuration/dns/rule_action/",
|
||||
}
|
||||
|
||||
|
||||
@@ -70,3 +70,22 @@ func TestDNSOptionsAcceptsTypedServers(t *testing.T) {
|
||||
require.Equal(t, "1.1.1.1", options.Servers[0].Options.(*RemoteDNSServerOptions).Server)
|
||||
require.Equal(t, C.DNSTypeFakeIP, options.Servers[1].Type)
|
||||
}
|
||||
|
||||
func TestDNSRuleActionEvaluateRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
action := DNSRuleAction{
|
||||
Action: C.RuleActionTypeEvaluate,
|
||||
RouteOptions: DNSRouteActionOptions{
|
||||
Server: "default",
|
||||
},
|
||||
}
|
||||
|
||||
content, err := json.Marshal(action)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded DNSRuleAction
|
||||
err = json.UnmarshalContext(context.Background(), content, &decoded)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, action, decoded)
|
||||
}
|
||||
|
||||
@@ -135,3 +135,23 @@ func TestNewDNSRuleRejectsNestedRuleAction(t *testing.T) {
|
||||
}, true, false)
|
||||
require.ErrorContains(t, err, dnsRuleActionNestedUnsupportedMessage)
|
||||
}
|
||||
|
||||
func TestNewDNSRuleRejectsReplyRejectMethod(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := NewDNSRule(context.Background(), log.NewNOPFactory().NewLogger("dns"), option.DNSRule{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
RawDefaultDNSRule: option.RawDefaultDNSRule{
|
||||
Domain: []string{"example.com"},
|
||||
},
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeReject,
|
||||
RejectOptions: option.RejectActionOptions{
|
||||
Method: C.RuleActionRejectMethodReply,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, false, false)
|
||||
require.ErrorContains(t, err, "reject method `reply` is not supported for DNS rules")
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func (r *abstractDefaultRule) destinationIPCIDRMatchesSource(metadata *adapter.I
|
||||
}
|
||||
|
||||
func (r *abstractDefaultRule) destinationIPCIDRMatchesDestination(metadata *adapter.InboundContext) bool {
|
||||
return !metadata.IgnoreDestinationIPCIDRMatch && !metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0
|
||||
return !metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0
|
||||
}
|
||||
|
||||
func (r *abstractDefaultRule) requiresSourceAddressMatch(metadata *adapter.InboundContext) bool {
|
||||
|
||||
@@ -24,6 +24,10 @@ func NewDNSRule(ctx context.Context, logger log.ContextLogger, options option.DN
|
||||
if !checkServer && options.DefaultOptions.Action == C.RuleActionTypeEvaluate {
|
||||
return nil, E.New(options.DefaultOptions.Action, " is only allowed on top-level DNS rules")
|
||||
}
|
||||
err := validateDNSRuleAction(options.DefaultOptions.DNSRuleAction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch options.DefaultOptions.Action {
|
||||
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
|
||||
if options.DefaultOptions.RouteOptions.Server == "" && checkServer {
|
||||
@@ -38,6 +42,10 @@ func NewDNSRule(ctx context.Context, logger log.ContextLogger, options option.DN
|
||||
if !checkServer && options.LogicalOptions.Action == C.RuleActionTypeEvaluate {
|
||||
return nil, E.New(options.LogicalOptions.Action, " is only allowed on top-level DNS rules")
|
||||
}
|
||||
err := validateDNSRuleAction(options.LogicalOptions.DNSRuleAction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch options.LogicalOptions.Action {
|
||||
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
|
||||
if options.LogicalOptions.RouteOptions.Server == "" && checkServer {
|
||||
@@ -50,6 +58,13 @@ func NewDNSRule(ctx context.Context, logger log.ContextLogger, options option.DN
|
||||
}
|
||||
}
|
||||
|
||||
func validateDNSRuleAction(action option.DNSRuleAction) error {
|
||||
if action.Action == C.RuleActionTypeReject && action.RejectOptions.Method == C.RuleActionRejectMethodReply {
|
||||
return E.New("reject method `reply` is not supported for DNS rules")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ adapter.DNSRule = (*DefaultDNSRule)(nil)
|
||||
|
||||
type DefaultDNSRule struct {
|
||||
@@ -379,7 +394,6 @@ func (r *DefaultDNSRule) matchStatesForMatchWithMissingResponse(metadata *adapte
|
||||
return 0
|
||||
}
|
||||
matchMetadata := *metadata
|
||||
matchMetadata.IgnoreDestinationIPCIDRMatch = false
|
||||
matchMetadata.DestinationAddressMatchFromResponse = true
|
||||
return r.abstractDefaultRule.matchStates(&matchMetadata)
|
||||
}
|
||||
@@ -389,7 +403,6 @@ func (r *DefaultDNSRule) matchStatesForMatchWithMissingResponse(metadata *adapte
|
||||
func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool {
|
||||
matchMetadata := *metadata
|
||||
matchMetadata.DNSResponse = response
|
||||
matchMetadata.IgnoreDestinationIPCIDRMatch = false
|
||||
matchMetadata.DestinationAddressMatchFromResponse = true
|
||||
return !r.abstractDefaultRule.matchStates(&matchMetadata).isEmpty()
|
||||
}
|
||||
@@ -405,13 +418,13 @@ func (r *LogicalDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatch
|
||||
}
|
||||
|
||||
func matchDNSHeadlessRuleStatesForMatch(rule adapter.HeadlessRule, metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
switch rule := rule.(type) {
|
||||
switch typedRule := rule.(type) {
|
||||
case *DefaultDNSRule:
|
||||
return rule.matchStatesForMatch(metadata)
|
||||
return typedRule.matchStatesForMatch(metadata)
|
||||
case *LogicalDNSRule:
|
||||
return rule.matchStatesForMatch(metadata)
|
||||
return typedRule.matchStatesForMatch(metadata)
|
||||
default:
|
||||
return matchHeadlessRuleStates(rule, metadata)
|
||||
return matchHeadlessRuleStates(typedRule, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,7 +524,6 @@ func (r *LogicalDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool {
|
||||
func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool {
|
||||
matchMetadata := *metadata
|
||||
matchMetadata.DNSResponse = response
|
||||
matchMetadata.IgnoreDestinationIPCIDRMatch = false
|
||||
matchMetadata.DestinationAddressMatchFromResponse = true
|
||||
return !r.abstractLogicalRule.matchStates(&matchMetadata).isEmpty()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
@@ -20,5 +21,5 @@ func (r *DNSResponseRCodeItem) Match(metadata *adapter.InboundContext) bool {
|
||||
}
|
||||
|
||||
func (r *DNSResponseRCodeItem) String() string {
|
||||
return F.ToString("response_rcode=", r.rcode)
|
||||
return F.ToString("response_rcode=", dns.RcodeToString[r.rcode])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user