Skip to content

DNS — Xray-core

The dns block configures Xray's internal resolver: a list of servers, optional static hosts, and a handful of caching / fallback toggles. The DNS engine also exposes itself as an internal inbound (tagged dns by default), so DNS traffic can be routed like any other connection.

Top-level options

FieldTypeDefaultAllowed valuesDescription
servers[]*NameServerConfig[][NameServerConfig | <string>]Resolvers list. Each entry is either an `address` string (`8.8.8.8`, `https://dns.google/dns-query`, `tcp+local://`, `fakedns`, …) or a full NameServerConfig object.
hosts*HostsWrapper{}{<domain or pattern>: <IP | [IP, …]>}Static hosts. Keys support the same `full:`, `domain:`, `regexp:`, `keyword:`, `geosite:` prefixes as routing rules.
clientIp*Address(unset)<IP>ECS (EDNS Client Subnet) advertised on outgoing queries. Helps CDN-aware resolvers return a geographically near answer.
tagstringdns<inbound tag>Internal tag for the DNS-engine inbound. Used by routing rules that want to single out DNS traffic.
queryStrategystringUseIPUseIP | UseIPv4 | UseIPv6Default query strategy applied when a per-server strategy isn't set. `UseIP` queries both A and AAAA; `UseIPv4`/`UseIPv6` skips the other family.
disableCacheboolfalsetrue | falseDisable the in-memory answer cache. Each query goes out fresh.
serveStaleboolfalsetrue | falseServe expired cached entries while a refresh runs in the background. Improves perceived latency.
serveExpiredTTLuint320<seconds>Maximum seconds an expired entry is served when `serveStale` is on. 0 means unbounded.
disableFallbackboolfalsetrue | falseDon't fall back to later servers when the first server doesn't return a usable answer.
disableFallbackIfMatchboolfalsetrue | falseSame, but only when the matched server itself has a `domains` list — i.e. matched-by-domain servers never trigger fallback.
enableParallelQueryboolfalsetrue | falseQuery all matching servers in parallel; first valid answer wins.
useSystemHostsboolfalsetrue | falseAlso consult the OS's `/etc/hosts` (or platform equivalent) before going to network.

Source: infra/conf/dns.go:160-173 · pinned at v26.6.1 (94ffd50)

servers[] — NameServerConfig

Each entry is either a bare address string (parsed into address) or the verbose object form:

FieldTypeDefaultAllowed valuesDescription
address*Address(required)<DNS URL>Resolver address. Bare IP (`8.8.8.8` = UDP/53), `tcp://`, `tls://`, `https://`, `quic://`, `tcp+local://...` (use local DNS without proxy), `localhost`, or `fakedns`.
clientIp*Address(inherit)<IP>Per-server ECS override.
portuint1653<port>Server port (only for bare IP addresses).
skipFallbackboolfalsetrue | falseWhen true and this server returns no answer, the fallback chain is *not* tried — the query simply fails.
domainsStringList[]<domain pattern>Domains this server should answer. Same syntax as routing rules. If non-empty, the server is consulted only for these domains.
expectedIPsStringList[]<IP / CIDR / geoip:>Accept this server's answer only when the resolved IP matches one of these patterns. Used to filter out censored or hijacked responses.
expectIPsStringList(alias)(alias of expectedIPs)Compatibility alias for `expectedIPs`.
queryStrategystring(inherit)UseIP | UseIPv4 | UseIPv6Per-server strategy override.
tagstring(unset)<string>Server tag — referenced by routing rules that route the resolver's outgoing traffic.
timeoutMsuint644000<ms>Per-query timeout in milliseconds.
disableCache*bool(inherit)true | falsePer-server cache override.
serveStale*bool(inherit)true | falsePer-server serve-stale override.
serveExpiredTTL*uint32(inherit)<seconds>Per-server stale-TTL ceiling.
finalQueryboolfalsetrue | falseMark this server as the final fallback — if it answers, no further resolution happens.
unexpectedIPsStringList[]<IP / CIDR / geoip:>Reject this server's answer if the resolved IP matches any of these patterns. Complement of `expectedIPs`.

Source: infra/conf/dns.go:19-35 · pinned at v26.6.1 (94ffd50)

hosts

The hosts field is a flat map. Keys support the same prefixes as routing rules; values are either a single IP or an array of IPs.

json
{
  "dns": {
    "hosts": {
      "domain:example.com": "203.0.113.10",
      "domain:internal.corp": ["10.0.0.1", "10.0.0.2"],
      "geosite:cn": "8.8.8.8"
    }
  }
}

A value of "fakedns" for the IP makes Xray hand back a fake address from the FakeDNS pool.

Examples

Standard split-resolver — CN domains via local DoH, everything else via Cloudflare DoH:

json
{
  "dns": {
    "servers": [
      {
        "address": "https://doh.pub/dns-query",
        "domains": ["geosite:cn"],
        "expectedIPs": ["geoip:cn"]
      },
      "https://1.1.1.1/dns-query",
      {
        "address": "tcp+local://223.5.5.5",
        "domains": ["geosite:cn"]
      }
    ],
    "hosts": {
      "domain:example.com": "203.0.113.10"
    },
    "queryStrategy": "UseIP",
    "disableFallbackIfMatch": true,
    "enableParallelQuery": false
  }
}

FakeDNS routing:

json
{
  "dns": {
    "servers": ["fakedns", "1.1.1.1"]
  },
  "fakeDns": { "ipPool": "198.18.0.0/15", "poolSize": 32768 },
  "inbounds": [{
    "port": 1080,
    "protocol": "socks",
    "sniffing": {
      "enabled": true,
      "destOverride": ["http", "tls", "fakedns"]
    }
  }]
}

Notes

  • A bare 8.8.8.8 string in servers[] is shorthand for {"address": "8.8.8.8"}NameServerConfig's UnmarshalJSON (infra/conf/dns.go:36-43) accepts both shapes.
  • address: "localhost" uses the OS resolver (handy on platforms with a stub resolver like systemd-resolved).
  • address: "tcp+local://..." queries via TCP bypassing the routing engine — useful for the bootstrap resolver that the rest of the DNS setup depends on.
  • domains patterns: bare strings are suffix match by default; prefix with full: for exact, regexp: for regex, keyword: for substring, geosite: for GeoSite category.
  • expectedIPs / unexpectedIPs together implement the "reject poisoned answers" pattern — set expectedIPs: ["geoip:cn"] on the domestic resolver and unexpectedIPs: ["geoip:cn"] on the public one.
  • clientIp (and per-server clientIp) sets the EDNS Client Subnet option. Use a realistic value (e.g. one of your IPs) to get CDN-aware answers.
  • finalQuery: true marks a server as terminal — once it answers, no fallback runs.

dns outbound

Distinct from the dns block above, Xray ships a dns outbound ("protocol": "dns"). Route DNS traffic to it and it either forwards each query to a fixed resolver or answers it according to a rule list. Recent Xray added destination rewrite fields and a rules engine (the older nonIPQuery / blockTypes knobs are now legacy).

FieldTypeDefaultAllowed valuesDescription
rewriteNetworkNetwork(unset)tcp | udp | tcp,udpTransport queries are rewritten to. Canonical name for the legacy `network`.
rewriteAddress*Address(unset)<host>Resolver that intercepted DNS queries are rewritten to. Canonical name for the legacy `address`.
rewritePortuint160<port>Port queries are rewritten to. Canonical name for the legacy `port`.
networkNetwork(alias)(alias of rewriteNetwork)Legacy alias for `rewriteNetwork`.
address*Address(alias)(alias of rewriteAddress)Legacy alias for `rewriteAddress`.
portuint16(alias)(alias of rewritePort)Legacy alias for `rewritePort`.
userLeveluint320<uint32>Policy level applied to this outbound's connections.
rules[]*DNSOutboundRuleConfig(unset)[DNSOutboundRuleConfig]Per-query rules evaluated in order; the first match decides how the query is handled.
nonIPQuery*string(legacy)reject | drop | skipLegacy non-IP-query handling. Deprecated in favour of `rules`, and cannot be combined with it.
blockTypes*[]int32(legacy)[<qType number>]Legacy list of DNS query-type numbers to block. Deprecated in favour of `rules`.

Source: infra/conf/dns_proxy.go:60-71 · pinned at v26.6.1 (94ffd50)

rules[]

FieldTypeDefaultAllowed valuesDescription
actionstring(required)direct | drop | return | hijackWhat to do with a matching query. `direct` forwards as-is; `drop` discards it; `return` answers locally with `rCode`; `hijack` redirects it to the rewrite server.
qType*PortList(any)<DNS type number / range>Match these DNS query types by number (1 = A, 28 = AAAA, 65 = HTTPS, …). Accepts comma/range lists.
domain*StringList(any)<domain pattern>Match these domains (substring by default; the same pattern prefixes as routing rules).
rCodeuint3200-65535Response code returned when `action` is `return` (e.g. 5 = REFUSED, 3 = NXDOMAIN).

Source: infra/conf/dns_proxy.go:13-18 · pinned at v26.6.1 (94ffd50)

Hijack A/AAAA to a fixed resolver, drop HTTPS-record (type 65) lookups, and refuse anything else:

json
{
  "outbounds": [
    {
      "tag": "dns-out",
      "protocol": "dns",
      "settings": {
        "rewriteAddress": "1.1.1.1",
        "rewritePort": 53,
        "rewriteNetwork": "udp",
        "rules": [
          { "action": "hijack", "qType": "1,28" },
          { "action": "drop",   "qType": "65" },
          { "action": "return", "rCode": 5 }
        ]
      }
    }
  ]
}

nonIPQuery and blockTypes cannot be mixed with rules — doing so is a startup error. Migrate legacy configs to rules.

Cross-core notes

  • sing-box uses a polymorphic server-by-type schema (type: "https", type: "tls", type: "fakeip", …) plus structured DNS rules with action: "route"|"reject"|"predefined". See DNS — sing-box.
  • mihomo uses a flat dns: block with separate nameserver / fallback lists plus a powerful nameserver-policy map for per-domain routing. See DNS — mihomo.

Source: infra/conf/dns.go:19-173 · v26.6.1 (94ffd50)

Core Tutorial by Argsment