Compare commits

...

18 Commits

Author SHA1 Message Date
世界
470c605118 option: add round-trip test for DNSRuleAction with evaluate action 2026-03-31 17:52:54 +08:00
世界
3795ac1503 dns: add evaluate integration tests for response_rcode, response_ns, response_extra 2026-03-31 17:45:35 +08:00
世界
8be4f38eaf dns: remove redundant DNSResponse assignment in addressLimitResponseCheck
MatchAddressLimit internally copies metadata and sets DNSResponse,
making the prior assignment in the closure unnecessary.
2026-03-31 17:39:51 +08:00
世界
4152022b89 dns: remove redundant queryOptions variable 2026-03-31 17:31:09 +08:00
世界
5b01eaa149 dns: remove dead lookupStrategyAllowsQueryType helper 2026-03-31 17:28:52 +08:00
世界
3d32ad79cd dns: remove dead lookup strategy guard in lookupWithRulesType 2026-03-31 17:24:03 +08:00
世界
e49a7bcc86 adapter: remove unused DestinationAddressesForMatch 2026-03-31 17:21:06 +08:00
世界
c94696df9e dns: fix variable shadowing in matchDNSHeadlessRuleStatesForMatch 2026-03-31 17:17:26 +08:00
世界
a7c4096a07 dns: fix err shadowing in buildRules
Reuse the outer err variable in the rule-construction and rule-startup
loops instead of redeclaring it with :=, and declare dnsRule separately.
2026-03-31 17:13:18 +08:00
世界
0e87476ee5 dns: return immediately on context cancellation in evaluate exchange 2026-03-31 17:08:52 +08:00
世界
21f3acef81 dns: reject method reply is not supported for DNS rules
Add config-time validation in NewDNSRule that rejects
RejectMethodReply for both default and logical DNS rules,
matching the existing TCP/UDP validation in route/route.go.
2026-03-31 16:53:57 +08:00
世界
861fa897e0 dns: improve test coverage and cleanup
- Add t.Cleanup(router.Close) in newTestRouter for automatic cleanup
- Remove unnecessary testCase loop variable capture (Go 1.22+)
- Add tests for reject drop action, route_options effect, and
  chained evaluate response overwrite
2026-03-31 15:53:38 +08:00
世界
c471d7fee7 dns: fix test style issues in repro_test.go
- Rename addrs to addresses per naming conventions
- Replace errors.New with E.New per error-handling rules
2026-03-31 15:53:30 +08:00
世界
7de00c7cd1 fix: add missing EnvName, document Strategy invariant, improve rcode display
- Add EnvName to four new deprecation constants so users can suppress
  warnings via ENABLE_DEPRECATED_* environment variables
- Add comment explaining why applyDNSRouteOptions skips Strategy
- Use dns.RcodeToString in DNSResponseRCodeItem.String() for readability
- Remove redundant Fqdn(FqdnToDomain(domain)) round-trip
2026-03-31 15:47:29 +08:00
世界
29024191ee docs: fix strategy deprecation format, explain legacyDNSMode, unify CN/EN order
- Use standard !!! failure block for strategy deprecation notice
- Add Legacy DNS Mode section explaining automatic mode detection
- Reorder ip_accept_any/rule_set_ip_cidr_accept_empty in Chinese docs
  to match English
2026-03-31 15:43:04 +08:00
世界
16480095f7 dns: populate reverse mapping for legacy predefined responses
The legacy path returned predefined responses early, bypassing the
reverse mapping cache. Use goto to reach the shared post-exchange
block so both legacy and new paths record predefined A/AAAA answers.
2026-03-31 15:37:10 +08:00
世界
cf33f1f375 route/rule: remove dead IgnoreDestinationIPCIDRMatch field
The field was never set to true after the legacy pre-match refactor
in 3549c02b8. Remove the declaration, guard check, and redundant
false assignments.
2026-03-31 15:29:50 +08:00
世界
1d872a6835 dns: use refcounted snapshot to narrow rule lock scope
Exchange and Lookup held rulesAccess.RLock across all DNS network I/O,
blocking rebuildRules from swapping in new rules until every in-flight
query finished. Replace the RWMutex with an atomic pointer to a
refcounted rulesSnapshot so queries only hold a snapshot reference
during execution, allowing concurrent rule rebuilds.
2026-03-31 15:29:16 +08:00
14 changed files with 908 additions and 152 deletions

View File

@@ -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)
}

View File

@@ -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")
}
},
}

View File

@@ -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)
}
}

View File

@@ -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{

View File

@@ -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.

View File

@@ -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 起"

View File

@@ -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`.

View File

@@ -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`

View File

@@ -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/",
}

View File

@@ -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)
}

View File

@@ -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")
}

View File

@@ -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 {

View File

@@ -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()
}

View File

@@ -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])
}