DNS
etemenanki-app has one resolver per running configuration. When an outbound has to turn a name into an address, for example a freedom outbound dialling example.com or a VLESS client connecting to proxy.example.com, it asks this resolver. The one exception is the WireGuard endpoint, described under Who does not. The [dns] table chooses where the resolver gets its answers: the host’s own resolver (the default), a DNS server over plain UDP, DNS over TLS, or DNS over HTTPS. Whatever the backend, a cache sits in front of it.
Read this page if you want lookups to go to a specific server, to be encrypted, or to reach a private resolver, or if you need to know why a name resolved the way it did.
Choosing a backend
Section titled “Choosing a backend”| Backend | Transport | Reads /etc/hosts, nsswitch.conf, search domains |
How long an answer is cached | Keys it needs |
|---|---|---|---|---|
system (default) |
The host resolver (getaddrinfo) |
Yes | 60 s | none |
udp |
Plain DNS, UDP | No | The record TTL, clamped to 5 s–3600 s | server |
tls |
DNS over TLS (RFC 7858) | No | The record TTL, clamped to 5 s–3600 s | server, server_name |
https |
DNS over HTTPS (RFC 8484) | No | The record TTL, clamped to 5 s–3600 s | server, url |
system is the default because it is the only backend that honours the host’s own name configuration. Switching to a DNS client changes what some names resolve to: an entry in /etc/hosts or a search domain no longer applies. Pick tls or https when the lookups themselves must not be readable on the network, and udp when you want a particular server and privacy does not matter.
A minimal example
Section titled “A minimal example”This configuration runs a local SOCKS proxy that dials destinations directly and resolves every name over DNS over HTTPS:
# A local SOCKS proxy that sends traffic straight out and looks up every# name over DNS over HTTPS instead of the host resolver.
# DNS over HTTPS (RFC 8484). `server` is the address that is dialed and must be# an IP literal with a port; `url` supplies the TLS name, the Host header and# the request path. The certificate is checked against the system CA store.[dns]backend = "https"server = "1.1.1.1:443"url = "https://cloudflare-dns.com/dns-query"
# Accept SOCKS 4/4a/5 clients on the loopback interface only.[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080
# Dial the requested destination directly. When the client asks for a name,# freedom resolves it through [dns] above.[[outbound]]tag = "direct"protocol = "freedom"server is the address etemenanki-app connects to. url gives the name the certificate is checked against, the Host header, and the path of the request. The two are separate on purpose: taking the address from the URL would mean resolving the resolver, and there is nothing to resolve it with.
Settings
Section titled “Settings”The [dns] table is optional. Without it, the resolver uses the system backend. The table accepts exactly the keys below; any other key is a parse error that names the line, for example unknown field `backends`, expected one of `backend`, `server`, `server_name`, `url`, `ca_file` .
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
backend | string (enum) | no | "system" | Where answers come from: system (the host resolver, getaddrinfo), udp (plain DNS over UDP), tls (DNS over TLS, RFC 7858) or https (DNS over HTTPS, RFC 8484). Lower case only; any other value fails with dns: unknown backend "…" (expected "system", "udp", "tls" or "https"). |
server | string | depends | — | Required for udp, tls and https. The resolver's socket address as an IP literal with a port, such as "192.0.2.53:53" or "[2001:db8::53]:853". A host name or a missing port is refused with dns: invalid server address: invalid socket address syntax, because there is nothing yet to resolve it with. For https this is the address that is dialed, whatever host and port url names. Ignored by system. |
server_name | string | depends | — | Required for tls. The name sent as SNI and checked against the resolver's certificate. It is never guessed from server; a missing value fails with dns: the tls backend needs a server name to verify against. Ignored by every other backend, including https, which takes the name from url. |
url | string | depends | — | Required for https. Must start with https://. The host is sent as SNI and as the Host header and is what the certificate is checked against; everything from the first / on is the request path, and /dns-query is used when there is none. A port in the URL is ignored, and the host is cut at the first :, so write it as a name: an IPv6 literal passes the config check but fails every query. A URL with userinfo (user@) or an empty host is refused. Ignored by every other backend. |
ca_file | path | no | — | PEM file of extra CA certificates that tls and https trust in addition to the system store, for a resolver with a private certificate. The file is read whenever the key is set, whatever the backend, so a missing file fails the config even with system or udp; only tls and https parse it, and a file with no certificate then fails with no certificate in CA PEM bundle. A relative path resolves against the working directory. |
Which keys each backend reads
Section titled “Which keys each backend reads”| Key | system |
udp |
tls |
https |
|---|---|---|---|---|
server |
ignored | required | required | required |
server_name |
ignored | ignored | required | ignored |
url |
ignored | ignored | ignored | required |
ca_file |
read, not used | read, not used | used | used |
A key the chosen backend does not use is accepted and ignored, even when its value would be invalid for another backend: server = "garbage" passes --test with backend = "system". The exception is ca_file, which is always read from disk when it is set, so a path that does not exist fails the configuration whatever the backend.
Backend by backend
Section titled “Backend by backend”# Same as leaving [dns] out.[dns]backend = "system"Every lookup goes through getaddrinfo, so the host’s resolver configuration, its timeouts and its retries apply. The host resolver reports no TTL, so each answer is cached for a fixed 60 seconds.
[dns]backend = "udp"server = "192.0.2.53:53"Each query is a single UDP datagram to server, from a new socket of the same address family. Write an IPv6 server as "[2001:db8::53]:53".
[dns]backend = "tls"server = "192.0.2.53:853"server_name = "dns.example.com"server_name is required and is never taken from server: checking a resolver’s certificate against the wrong name is exactly the failure DNS over TLS is meant to prevent.
[dns]backend = "https"server = "192.0.2.53:443"url = "https://dns.example.com/dns-query"The host part of url is the TLS name and the Host header. The path is everything from the first /, or /dns-query if the URL has none. A port written in the URL is ignored; the port in server is the one dialled.
Write the host in url as a name. The host is cut at the first :, so an IPv6 literal such as https://[2001:db8::53]/dns-query passes --test but leaves [2001 as the TLS name and Host header, and every query then fails in the TLS handshake.
A resolver with a private certificate
Section titled “A resolver with a private certificate”If your resolver’s certificate is issued by your own CA, point ca_file at that CA. It is trusted in addition to the system store, not instead of it:
[dns]backend = "tls"server = "192.0.2.53:853"server_name = "dns.example.com"ca_file = "/etc/etemenanki/dns-ca.pem"Use an absolute path. A relative one resolves against the process’s working directory, which is often / under a service manager.
How a lookup works
Section titled “How a lookup works”flowchart TB
Q["Name to resolve"] --> LIT{"IP literal?"}
LIT -- yes --> USE["Use it as is"]
LIT -- no --> HIT{"Fresh entry in the cache?"}
HIT -- yes --> FILTER
HIT -- no --> BACK["Ask the backend"]
BACK --> ANY{"Any address?"}
ANY -- no --> ERR["Lookup fails, nothing cached"]
ANY -- yes --> STORE["Cache with the clamped TTL"]
STORE --> FILTER["Outbound applies its address_family"]
- Address literals skip the resolver. A destination or server written as an IP address is used directly, with no query and no cache entry.
- Names are cached case-insensitively.
Example.COMandexample.comshare one entry. - Only answers are cached. A failed lookup, or one that returned no address, is not remembered, so the next request for that name asks the backend again.
- The cache holds both address families. The outbound that asked then filters and orders the addresses by its
address_familypolicy, described on Outbounds. Outbounds with different policies can share one cache entry. - Concurrent misses are not merged. If several connections ask for the same uncached name at the same moment, each one queries the backend.
| Property | Value |
|---|---|
| Capacity | 8192 names. When it is full, the cache keeps the names that are used most often: a new name may push out an older entry before its TTL ends, or may not be stored at all. |
| Scope | One cache for the whole configuration, shared by every outbound and every balancer probe. |
| Lifetime | The cache belongs to the running configuration. A reload builds a new resolver with an empty cache. |
For udp, tls and https, an answer is cached for the smallest TTL among its A and AAAA records, clamped to at least 5 seconds and at most 3600 seconds. The floor stops a zero TTL from defeating the cache; the ceiling stops a very long TTL from pinning a stale address for hours. For system, which reports no TTL, the fixed 60 seconds applies.
Queries
Section titled “Queries”The udp, tls and https backends share one query path:
- A and AAAA are asked in parallel. One failing does not discard the other: a host that has only an A record still resolves when the AAAA query fails. The lookup fails only when neither query produced an address. The IPv4 answers come first in the result, then the IPv6 answers, without duplicates.
- Each query has 5 seconds. The limit covers the whole exchange, including the TCP connect and TLS handshake for
tlsandhttps. On expiry the query fails withdns: query timed out. There is no retry. - Each query opens its own connection. Nothing is pooled or kept open between queries, so a lookup that misses the cache costs two connections, one for A and one for AAAA, and for
tlsandhttpstwo TLS handshakes. Once the cache is warm, the resolver is consulted rarely. - The query is a standard recursive question with a random ID. The CNAME chain is left to the server; the resolver reads the A and AAAA records in the answer and skips any other record type. A response with a different ID, or with a non-zero response code such as NXDOMAIN, is an error.
Details that differ per backend:
| Backend | On the wire |
|---|---|
udp |
One datagram to server, answer read into a 512-byte buffer. The query carries no EDNS, so a conforming server keeps its answer within 512 bytes. The truncation (TC) flag is not checked and there is no retry over TCP: a truncated answer yields only the address records it still contains. |
tls |
TLS 1.2 or newer, no ALPN, server_name as SNI. The message is sent with a 2-byte length prefix, as in DNS over TCP. |
https |
TLS 1.2 or newer, ALPN http/1.1. An HTTP/1.1 POST to the URL’s path with Content-Type: application/dns-message, Accept: application/dns-message and Connection: close. The response body is everything after the headers, read to the end of the connection; any status other than 200 is an error. |
The DNS over HTTPS client speaks only HTTP/1.1, so the server must accept an HTTP/1.1 request. It also does not decode Transfer-Encoding: chunked: a server that sends a chunked response has the chunk framing read as part of the DNS message, and the query fails.
Who uses the resolver
Section titled “Who uses the resolver”| Where | What is looked up |
|---|---|
freedom outbound, TCP |
The destination name, when the client asked for a name. |
freedom outbound, UDP |
Each destination name, once per UDP flow. The first usable address is kept for the rest of that flow, even past its TTL; a name that fails to resolve has its datagrams dropped for the rest of the flow. A flow looks up one name at a time, so datagrams to a second name wait until the first lookup finishes. |
| SOCKS, HTTP, Shadowsocks, Trojan, VLESS and VMess outbounds | The outbound’s own server, to connect to the upstream proxy. |
| Hysteria 2 outbound | The outbound’s server, when it opens its QUIC connection. |
| WireGuard outbound | Destination names inside the tunnel. UDP destinations behave as for freedom: one lookup per name per flow. |
| Balancer health probes | The probed member’s server address. The lookup counts against the probe’s timeout. |
Who does not
Section titled “Who does not”- The WireGuard
endpoint. The tunnel’s peer address is looked up with the host resolver, bypassing[dns]and its cache, and the first address returned is used. Write the endpoint as an IP address to be independent of either resolver. See WireGuard. - Destinations sent to an upstream proxy. A SOCKS, HTTP, Shadowsocks, Trojan, VLESS, VMess or Hysteria 2 outbound passes the destination name to the proxy server unchanged; the server resolves it.
- Routing. The router never resolves a name. IP and GeoIP rules match only when the destination is an IP address. A name found by sniffing is used for matching domain rules and is not looked up either. See Routing.
Reloading
Section titled “Reloading”A change to [dns], like any other change, takes effect on the next reload: the new configuration gets a new resolver, and its cache starts empty. If the new [dns] table is invalid, the reload is refused with reload: build failed, keeping current config: … and the running configuration keeps its resolver and cache. The reload summary in the log lists only inbound, outbound, route and log changes, so a reload that changes only [dns] logs config reload: no changes even though it applied. A ca_file is read when the configuration is built, so editing the CA file alone takes effect only when the configuration file itself changes. See Hot reload.
Errors
Section titled “Errors”When the configuration is checked
Section titled “When the configuration is checked”These appear from --test, at startup, and in the log of a reload that was refused.
| Error | Cause | Fix |
|---|---|---|
unknown field `…`, expected one of `backend`, `server`, `server_name`, `url`, `ca_file` |
A misspelt key in [dns]. |
Use one of the listed keys. |
dns: unknown backend "UDP" (expected "system", "udp", "tls" or "https") |
backend is not one of the four values, or is not lower case. |
Write the value in lower case. |
dns: the udp backend needs a server address |
udp, tls or https without server. The message names the backend. |
Add server. |
dns: invalid server address: invalid socket address syntax |
server is a host name, or has no port. |
Write an IP address and a port, such as "192.0.2.53:53". |
dns: the tls backend needs a server name to verify against |
tls without server_name. |
Add the name on the resolver’s certificate. |
dns: the https backend needs the resolver's url |
https without url. |
Add url. |
dns: "http://dns.example.com/dns-query" is not an https:// url |
url does not start with https://. |
Use an https:// URL. |
dns: "https://user@dns.example.com/dns-query" has no usable host |
url contains userinfo, or has an empty host. |
Remove user@, or add the host. |
No such file or directory (os error 2) |
ca_file does not exist. The message does not name the file. |
Check the path; use an absolute one. |
no certificate in CA PEM bundle |
ca_file contains no PEM certificate (tls and https only). |
Point it at a PEM file. |
When a lookup fails
Section titled “When a lookup fails”A failed lookup fails the TCP connection that needed it. On a TCP inbound, the error is logged at debug level as the connection ends, for example socks connection from Some(127.0.0.1) ended: dns: query timed out. In a freedom UDP flow, a failed lookup does not end the flow; its datagrams are dropped, and each drop is logged at debug as freedom: dropping a datagram to an unresolvable …. Set [log].level = "debug" to see these lines.
| Error | Meaning |
|---|---|
failed to lookup address information: Name or service not known |
The system backend could not resolve the name. |
dns: query timed out |
The server did not answer within 5 seconds, or for tls and https, the connection or handshake did not finish in time. |
Connection refused (os error 111) |
Nothing listens on server’s port (udp), or the TCP connection was refused (tls, https). |
dns: server returned rcode 3 |
The server answered with an error code: 3 is NXDOMAIN (no such name), 2 is SERVFAIL. |
dns: example.com did not resolve |
The server answered, but with no A or AAAA record. |
dns: doh server answered "HTTP/1.1 502 Bad Gateway" |
The DNS over HTTPS server replied with a status other than 200. Check the path in url. |
dns: no http header |
The DNS over HTTPS server closed the connection without a complete HTTP response head. |
dns: response id … does not match query … |
The reply is not the answer to this query, for example a chunked DNS over HTTPS body. |
dial: no usable ipv6_only destination address for example.com:443 |
The name resolved, but to no address that the outbound’s address_family allows. See Outbounds. |
An OpenSSL error mentioning certificate verify failed |
The resolver’s certificate does not chain to a trusted CA or does not match server_name or the host in url. Add its CA with ca_file, or correct the name. |