DNS — sing-box
sing-box's DNS engine is a list of typed servers plus a structured rule chain. Each server has a type selecting its transport (UDP, TCP, TLS, QUIC, HTTPS, HTTP/3, hosts file, fake-ip pool, DHCP-provided, mDNS, systemd-resolved, Tailscale embedded, or resolvers pushed by an OpenVPN / OpenConnect endpoint). Rules use the same match-field set as routing rules but with DNS-specific actions — and they can also match on the response.
Top-level options
| Field | Type | Default | Allowed values | Description |
|---|---|---|---|---|
servers | []DNSServerOptions | [] | [DNSServerOptions] | DNS server list. Each entry has a type (local, hosts, tcp, udp, tls, quic, https, h3, dhcp, mdns, fakeip, tailscale, openconnect, openvpn, resolved) and the matching set of options. Entries without type (the untyped address format) are rejected. |
rules | []DNSRule | [] | [DNSRule] | DNS-level routing rules. Same shape as routing rules but with DNS-specific action set. |
final | string | (unset) | <server tag> | Default DNS server when no rule matches. Empty falls back to the first server in servers[]. |
reverse_mapping | bool | false | true | false | Maintain a reverse map (IP → domain) so rules can match on the original domain after a connection resolves the destination to an IP. |
Source: option/dns.go:18-24 · pinned at v1.14.2 (af6e64c)
Plus the embedded DNSClientOptions:
| Field | Type | Default | Allowed values | Description |
|---|---|---|---|---|
strategy | DomainStrategy | (prefer_ipv4) | prefer_ipv4 | prefer_ipv6 | ipv4_only | ipv6_only | Default address-family preference. |
timeout | badoption.Duration | 10s | <duration> | Default timeout for each DNS query. Overridable per rule (DNS rule action timeout) and in domain_resolver. |
disable_cache | bool | false | true | false | Disable the in-memory answer cache. |
disable_expire | bool | false | true | false | Don't evict cached entries by TTL — keep them indefinitely. |
independent_cache | bool | false | true | false | Deprecated: the cache is always keyed by server, so this option has no effect. |
cache_capacity | uint32 | 1024 | <int> | LRU cache capacity (entries). Values below 1024 are raised to 1024. |
optimistic | *OptimisticDNSOptions | false | true | false | {enabled, timeout} | Optimistic caching: an expired entry still within timeout (default 3d) is returned immediately while a background refresh runs. Conflicts with disable_cache and disable_expire. |
client_subnet | *badoption.Prefixable | (unset) | <CIDR> | ECS (EDNS Client Subnet) advertised on outgoing queries. |
Source: option/dns.go:62-71 · pinned at v1.14.2 (af6e64c)
Server types
Every entry in servers[] has a type field. The matching fields:
type: "local"
Resolve via the OS resolver (handy on macOS/iOS where the system resolver is the authoritative path). Adds prefer_go to opt into the CGO-free Go resolver. neighbor_domain answers single-label LAN names from the neighbor resolver. On non-Apple platforms the local server also resolves *.local. and link-local reverse zones via mDNS (via the system resolver on Apple).
| Field | Type | Default | Allowed values | Description |
|---|---|---|---|---|
prefer_go | bool | false | true | false | Use Go's net.Resolver (CGO-free) instead of the platform-native resolver. Useful on systems where the native resolver is broken or rate-limited. |
neighbor_domain | badoption.Listable[string] | [] | [<.suffix>] | Domain suffixes (each starting with .) whose single-label A/AAAA queries are answered from the neighbor resolver (LAN hostnames from DHCP leases) instead of upstream. . matches any single-label name, e.g. [".", ".lan"]. |
Source: option/dns.go:188-192 · pinned at v1.14.2 (af6e64c)
type: "udp" and type: "tcp" — RemoteDNSServerOptions
{ "type": "udp", "tag": "local-udp", "server": "8.8.8.8", "server_port": 53 }| Field | Type | Default | Allowed values | Description |
|---|---|---|---|---|
server | string | (required) | <host> | Server hostname or IP. |
server_port | uint16 | 53 (udp/tcp), 853 (tls), 443 (https/h3) | <port> | Server port. |
Source: option/dns.go:158-161 · pinned at v1.14.2 (af6e64c)
type: "tls" — DNS-over-TLS
Same fields as udp/tcp plus the standard tls: block. type: "quic" (DNS-over-QUIC) takes exactly the same fields.
type: "https" and type: "h3" — DNS-over-HTTPS / -H3
| Field | Type | Default | Allowed values | Description |
|---|---|---|---|---|
path | string | /dns-query | /<path> | DoH endpoint path. |
method | string | POST | POST | GET | HTTP method used for DoH queries. |
headers | badoption.HTTPHeader | {} | {<header>: <value>} | Extra HTTP headers. |
Source: option/dns.go:204-209 · pinned at v1.14.2 (af6e64c)
type: "hosts" — local hosts file
| Field | Type | Default | Allowed values | Description |
|---|---|---|---|---|
path | badoption.Listable[string] | [/etc/hosts] | [<file path>] | List of hosts-file paths to merge. |
predefined | *badjson.TypedMap[string, badoption.Listable[netip.Addr]] | {} | {<domain>: [<IP>]} | Inline hosts table. Higher priority than path. |
Source: option/dns.go:179-182 · pinned at v1.14.2 (af6e64c)
type: "fakeip"
| Field | Type | Default | Allowed values | Description |
|---|---|---|---|---|
inet4_range | *badoption.Prefix | (required for v4) | <CIDR> | IPv4 CIDR allocated for fake IPs. |
inet6_range | *badoption.Prefix | (required for v6) | <CIDR> | IPv6 CIDR allocated for fake IPs. |
Source: option/dns.go:211-214 · pinned at v1.14.2 (af6e64c)
type: "dhcp"
| Field | Type | Default | Allowed values | Description |
|---|---|---|---|---|
interface | string | (auto) | <interface> | Interface whose DHCP-provided resolvers are used. |
Source: option/dns.go:216-219 · pinned at v1.14.2 (af6e64c)
Other types
type: "resolved"— read systemd-resolved's per-link nameservers (Linux only).type: "tailscale"— use the Tailscale daemon's MagicDNS resolver.type: "mdns"— multicast DNS on the local network, with an optionalinterfacelist. Usually unnecessary next to alocalserver (which already routes*.local.via mDNS); add one to reference it frompreferred_by.type: "openvpn"/type: "openconnect"— use the resolvers pushed by an OpenVPN client / OpenConnect endpoint (endpoint: "<tag>"), withaccept_default_resolversandaccept_search_domain. Pushed settings are not installed into the OS.
There is no predefined or rcode server type: fixed answers come from the predefined DNS rule action (below).
DNS rules
DNS rules use the same polymorphic _Rule shape as routing rules (type: "default" or type: "logical") with the 54 match keys of RawDefaultDNSRule — including query_client_subnet, query_dnssec, package_name_regex, source_mac_address, source_hostname, preferred_by (match names a listed server considers its own: hosts entries, mDNS names, Tailscale / VPN-pushed split-DNS and search domains) and the response-match fields below. The differences are at the action level:
| DNS action | Meaning |
|---|---|
route (default) | Send the query to server: "<tag>". Optional: disable_cache, disable_optimistic_cache, rewrite_ttl, timeout, client_subnet / remove_client_subnet, speculative; strategy is deprecated. |
route-options | Apply DNS-route options (same optional fields) to subsequent rule matches. |
evaluate | Query server and keep the response (optionally under a tag) for later rules to match via match_response; does not end rule evaluation. Top-level rules only. |
respond | Return the response kept by a preceding evaluate, without a new query. |
reject | Refuse the query. method: "default" replies REFUSED; method: "drop" sends no response. |
predefined | Return a hard-coded answer (rcode, answer, ns, extra). |
race: true (on route / respond / reject / predefined rules that use match_response) lets response-dependent rules be judged in parallel — the first one that matches wins and the remaining queries are cancelled.
Response matching
Rules can match the answer instead of only the query. An evaluate rule fetches a response; a later rule with match_response: true (or the evaluate rule's tag) matches it with ip_cidr / ip_is_private / ip_accept_any, an IP rule-set, or the response fields response_rcode, response_answer, response_ns, response_extra. Using ip_cidr / ip_is_private without match_response (the legacy address filter), the strategy action option and rule_set_ip_cidr_accept_empty are deprecated and scheduled for removal. The migration for a GeoIP-based split:
{
"dns": {
"rules": [
{ "action": "evaluate", "server": "remote" },
{ "match_response": true, "rule_set": "geoip-cn",
"action": "route", "server": "local" },
{ "action": "route", "server": "remote" }
]
}
}Examples
Two-server split — domestic over local DoH, everything else over Cloudflare DoH:
{
"dns": {
"servers": [
{ "type": "https", "tag": "local",
"server": "doh.pub", "path": "/dns-query" },
{ "type": "https", "tag": "remote",
"server": "cloudflare-dns.com", "path": "/dns-query",
"detour": "proxy" },
{ "type": "fakeip", "tag": "fakeip",
"inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18" }
],
"rules": [
{ "rule_set": ["geosite-cn"], "server": "local" }
],
"final": "remote",
"strategy": "prefer_ipv4"
}
}Block ads via DNS:
{
"dns": {
"servers": [
{ "type": "https", "tag": "main",
"server": "cloudflare-dns.com" }
],
"rules": [
{
"domain_keyword": ["ads", "doubleclick", "googlesyndication"],
"action": "reject",
"method": "default"
}
],
"final": "main"
}
}Notes
- A top-level
fakeip: {...}block and server entries withouttype(the untypedaddress: "tls://…"format) are not supported — both fail at startup. Use typed entries (type: "fakeip",type: "tls", …). reverse_mapping: truelets later routing rules match on the domain name even after sniffing has resolved it to an IP. Essential when using fake-ip alongside complex domain rules.independent_cachehas no effect: the cache is always keyed by server tag (and client subnet), so split-horizon servers never share answers. The option is deprecated — remove it.ip_versionandquery_typealso apply to internal lookups that do not target a specific server (e.g. a routeresolveaction withoutserver). Combining them with the deprecated legacy address-filter fields in the same DNS config is rejected at startup.- The deprecated
outboundDNS rule item still parses, but should be replaced byroute.default_domain_resolveror a per-outbounddomain_resolver. - DNS rules and routing rules share the same match-key vocabulary but operate at different times: DNS rules run on the resolver query; routing rules run on the resulting connection.
type: "resolved"reads fromsystemd-resolved's D-Bus API — works only on Linux systems where resolved is actually the running resolver.
Cross-core notes
- Xray-core uses a flat
dns:block withservers[]carrying URL strings (or NameServerConfig objects) — notypefield, the URL scheme decides the transport. See DNS — Xray-core. - mihomo has a 25-field
dns:block with separatenameserver/fallbacklists and anameserver-policymap. See DNS — mihomo.
Source: option/dns.go:18-224 · v1.14.2 (af6e64c)
