SagerNet / SagerNet/sing-box

No supported way to route dial-side domain resolution through DNS rules

Open
#4,444 0 comments 4 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
38.1k
Forks
4.6k
Avg merge
19d 15h
Merged PRs (30d)
1

Description

This is a feature request, not a bug report. The repository only exposes a bug-report template, so this is filed as a plain issue.

Relation to existing issues

#3596 and #3668 describe the same underlying behavior for the request domain handled by a direct outbound: a named domain_resolver supplies a non-nil DNS transport and therefore bypasses dns.rules. In that case, a route resolve action without server is a possible workaround.

This request is about a different lookup: the server address of an outbound or endpoint itself. That lookup occurs in the dialer while the outbound/endpoint is being established, before an inbound route flow exists. A route resolve action cannot reach it.

Summary

sing-box 1.14 provides evaluate, race, match_response, and respond, which together can express parallel multi-upstream resolution with response-based filtering.

However, dial-side resolution of an outbound/endpoint server domain cannot use those DNS rules. Every supported domain_resolver shape selects one named DNS transport, while omitting it is deprecated and already rejected by default.

The rule engine has the required behavior. What is missing is an explicit, supported way for dial-side lookup to enter it.

Current code path

common/dialer/dialer.go resolves domain_resolver.server to a concrete transport and stores it in DNSQueryOptions.Transport:

https://github.com/SagerNet/sing-box/blob/v1.14.0-beta.15/common/dialer/dialer.go#L65-L130

dns.Router.Lookup then takes the direct transport branch whenever options.Transport != nil; lookupWithRules is only used when it is nil:

https://github.com/SagerNet/sing-box/blob/v1.14.0-beta.15/dns/router.go#L1208-L1260

The object form cannot intentionally request the nil-transport path because DomainResolveOptions rejects an empty server:

https://github.com/SagerNet/sing-box/blob/v1.14.0-beta.15/option/outbound.go#L113-L164

On v1.14.0-beta.15, a minimal config with two DNS transports and an outbound whose server is a domain but has no domain_resolver is rejected by sing-box check:

ERROR missing `route.default_domain_resolver` or `domain_resolver` in dial fields is deprecated in sing-box 1.12.0 and will be removed in sing-box 1.14.0
FATAL to continuing using this feature, set environment variable ENABLE_DEPRECATED_MISSING_DOMAIN_RESOLVER=true

Setting that environment variable only permits a deprecated configuration; it is not a durable configuration API.

Use case

Resolve the domain of a proxy server or endpoint on a network with unreliable or hostile DNS:

  • query several upstreams concurrently;
  • accept the first response containing a usable address;
  • reject responses containing known poisoned/decoy address ranges;
  • distinguish SERVFAIL/timeout from NXDOMAIN or NODATA;
  • start a slower fallback tier only after the first tier has no acceptable result.

A single named DNS transport cannot express these semantics. The 1.14 DNS rule actions can. The current workaround is to run a local UDP DNS service that implements the race/filter logic and point domain_resolver to that service. This duplicates logic already present in the core and adds a local listener, lifecycle supervision, port coordination, and another failure mode.

The native race/barrier behavior is also covered by the beta.15 test suite:

https://github.com/SagerNet/sing-box/blob/v1.14.0-beta.15/dns/router_race_test.go

Requested API

Please provide an explicit opt-in for dial-side resolution through dns.rules, while preserving the current named-transport behavior by default.

For example:

{
  "domain_resolver": {
    "resolve_with_rules": true,
    "timeout": "2s",
    "strategy": "prefer_ipv4"
  }
}

resolve_with_rules should be mutually exclusive with server. Internally it would construct DNSQueryOptions with a nil Transport, allowing the existing Router.Lookup rule path to run. timeout, strategy, client_subnet, and cache options should remain usable.

Alternative configuration shapes would also solve the problem:

  1. a dial field such as resolve_with_rules: true;
  2. a reserved value such as domain_resolver: "rules";
  3. allowing the object form to omit server, meaning rule-based resolution.

The first form appears least disruptive and keeps the current loop-safety default: DNS rules are used for dial-side bootstrap only when the operator explicitly opts in.

Loop-safety consideration

Requiring a named transport currently prevents a DNS bootstrap cycle by construction. This request does not propose changing that default. An explicit opt-in makes the operator responsible for ensuring that the DNS servers selected by their rules have IP-literal addresses or otherwise possess a non-recursive bootstrap path.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with common/dialer/dialer.go, dns/router.go, and option/outbound.go to trace how domain_resolver becomes DNSQueryOptions.Transport and selects lookupWithRules. Run the DNS rule behavior covered by dns/router_race_test.go. Done means an explicit opt-in can use dial-side DNS rules while named transports remain the default and existing bootstrap safeguards are preserved.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend-api-design, networking
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.