Skip to content

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 ​

FieldTypeDefaultAllowed valuesDescription
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.
finalstring(unset)<server tag>Default DNS server when no rule matches. Empty falls back to the first server in servers[].
reverse_mappingboolfalsetrue | falseMaintain 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:

FieldTypeDefaultAllowed valuesDescription
strategyDomainStrategy(prefer_ipv4)prefer_ipv4 | prefer_ipv6 | ipv4_only | ipv6_onlyDefault address-family preference.
timeoutbadoption.Duration10s<duration>Default timeout for each DNS query. Overridable per rule (DNS rule action timeout) and in domain_resolver.
disable_cacheboolfalsetrue | falseDisable the in-memory answer cache.
disable_expireboolfalsetrue | falseDon't evict cached entries by TTL — keep them indefinitely.
independent_cacheboolfalsetrue | falseDeprecated: the cache is always keyed by server, so this option has no effect.
cache_capacityuint321024<int>LRU cache capacity (entries). Values below 1024 are raised to 1024.
optimistic*OptimisticDNSOptionsfalsetrue | 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).

FieldTypeDefaultAllowed valuesDescription
prefer_goboolfalsetrue | falseUse 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_domainbadoption.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 ​

json
{ "type": "udp", "tag": "local-udp", "server": "8.8.8.8", "server_port": 53 }
FieldTypeDefaultAllowed valuesDescription
serverstring(required)<host>Server hostname or IP.
server_portuint1653 (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 ​

FieldTypeDefaultAllowed valuesDescription
pathstring/dns-query/<path>DoH endpoint path.
methodstringPOSTPOST | GETHTTP method used for DoH queries.
headersbadoption.HTTPHeader{}{<header>: <value>}Extra HTTP headers.

Source: option/dns.go:204-209 · pinned at v1.14.2 (af6e64c)

type: "hosts" — local hosts file ​

FieldTypeDefaultAllowed valuesDescription
pathbadoption.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" ​

FieldTypeDefaultAllowed valuesDescription
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" ​

FieldTypeDefaultAllowed valuesDescription
interfacestring(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 optional interface list. Usually unnecessary next to a local server (which already routes *.local. via mDNS); add one to reference it from preferred_by.
  • type: "openvpn" / type: "openconnect" — use the resolvers pushed by an OpenVPN client / OpenConnect endpoint (endpoint: "<tag>"), with accept_default_resolvers and accept_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 actionMeaning
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-optionsApply DNS-route options (same optional fields) to subsequent rule matches.
evaluateQuery 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.
respondReturn the response kept by a preceding evaluate, without a new query.
rejectRefuse the query. method: "default" replies REFUSED; method: "drop" sends no response.
predefinedReturn 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:

json
{
  "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:

json
{
  "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:

json
{
  "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 without type (the untyped address: "tls://…" format) are not supported — both fail at startup. Use typed entries (type: "fakeip", type: "tls", …).
  • reverse_mapping: true lets 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_cache has 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_version and query_type also apply to internal lookups that do not target a specific server (e.g. a route resolve action without server). Combining them with the deprecated legacy address-filter fields in the same DNS config is rejected at startup.
  • The deprecated outbound DNS rule item still parses, but should be replaced by route.default_domain_resolver or a per-outbound domain_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 from systemd-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 with servers[] carrying URL strings (or NameServerConfig objects) — no type field, the URL scheme decides the transport. See DNS — Xray-core.
  • mihomo has a 25-field dns: block with separate nameserver / fallback lists and a nameserver-policy map. See DNS — mihomo.

Source: option/dns.go:18-224 · v1.14.2 (af6e64c)

Core Tutorial by Argsment