DNS
Every [dns] field is optional. With no [dns] section at all:
- names that route
DIRECTare resolved by your system's resolvers; - other names Core has to resolve itself use
udp://8.8.8.8; - names sent through a proxy on the local HTTP/SOCKS5 port are passed to the proxy as names and resolved on the proxy side;
- Enhanced Mode answers with Fake-IP for names that a rule sends through a proxy.
Start without [dns], and add settings only when you have a reason to.
When DNS matters
Local HTTP/SOCKS5 port. Applications send the destination name (CONNECT github.com:443). Domain rules match the name directly and no local lookup is needed.
Enhanced Mode. Applications resolve the name before they connect, so Core must answer their DNS queries to know which name an IP belongs to. On Linux, the eBPF backend redirects queries to Core automatically. [dns.tun] controls how Core answers. Read DNS for transparent routing for the full picture.
Fake-IP
[dns.tun]
mode = "fake-ip"
fake-ip-range = "198.18.0.1/16"
fake-ip-filter-mode = "blacklist"
fake-ip-filter = ["+.lan", "+.local"]For a name that a rule sends through a proxy, Core answers with an address from the Fake-IP pool. When the application connects to that address, Core maps it back to the name and gives the name to the proxy. Names routed DIRECT get their real address. Names routed to REJECT get REFUSED.
| Field | Default | Notes |
|---|---|---|
mode | "fake-ip" | fake-ip, or redir-host to always answer with real addresses |
fake-ip-range | "198.18.0.1/16" | IPv4 pool, inside 198.18.0.0/15 |
fake-ip-range-v6 | "fd00::1/18" | IPv6 pool |
fake-ip-filter-mode | "blacklist" | blacklist: listed names get real addresses; whitelist: only listed names get Fake-IP |
fake-ip-filter | [] | names: example.com, +.example.com (and subdomains), *.example.com, geosite:<code> |
fake-ip-ttl | 300 | seconds |
fake-ip-capacity | 65535 | mappings kept; oldest are evicted |
store-fake-ip | false | keep mappings across restarts |
fake-ip-store-path | "" | file for stored mappings |
Put local names (+.lan, +.local, your company's internal zone if it must resolve locally) in fake-ip-filter.
Upstream resolvers
[dns.upstream]
default = ["https://1.1.1.1/dns-query", "tls://8.8.8.8"]
direct = ["udp://192.168.1.1"]
fallback = ["tls://9.9.9.9"]| Field | Default | Used for |
|---|---|---|
default | ["udp://8.8.8.8"] | names Core resolves that do not route direct |
direct | system resolvers | names that route DIRECT |
bootstrap | system resolvers | resolving the host names of DoT/DoH resolvers |
proxy-server | bootstrap | resolving the host names of your [[proxy]] servers |
fallback | [] | used when the primary resolvers fail, or as [dns.fallback-filter] decides |
direct-follow-policy | false | also apply nameserver-policy to direct names |
Resolver formats:
| Format | Protocol | Default port |
|---|---|---|
192.168.1.1 or udp://192.168.1.1 | UDP | 53 |
tcp://192.168.1.1 | TCP | 53 |
tls://dns.example.com (or dot://) | DNS over TLS | 853 |
https://dns.example.com/dns-query | DNS over HTTPS | 443 |
Add :port to override the port. With the eBPF backend, names that route DIRECT are always resolved by the system resolvers, so direct traffic behaves as it does without Specola.
Per-domain resolvers
[dns.nameserver-policy]
"+.corp.example.com" = "udp://10.0.0.53"
"build.internal.example.org" = ["udp://10.1.0.53", "udp://10.1.0.54"]Keys are an exact name, +. followed by a suffix (the name and its subdomains), or geosite:<code>. The longest matching key wins. Policies apply to names that do not route direct, and also to direct names when direct-follow-policy = true.
Fallback filter
[dns.fallback-filter] decides when fallback resolvers are used even though the primary ones answered. Fields: geoip (boolean), geoip-code (default "CN"), ipcidr and domain (lists).
Route resolution
Should Core look up a name just to test IP rules? route-resolution decides, separately for the local port ([dns.system-proxy]) and Enhanced Mode ([dns.tun]):
| Value | Behavior |
|---|---|
domain-only (default) | never; names that match no domain rule go to FINAL |
unknown-domain | only when no domain rule matched and FINAL is a proxy |
always | look up every name and compare domain and IP rules in list order |
unknown-domain and always need at least two probe-resolver entries, queried in parallel; with fewer, Core logs a warning and uses domain-only.
[dns.system-proxy]
route-resolution = "unknown-domain"
probe-resolver = ["udp://1.1.1.1", "udp://8.8.8.8"]
probe-timeout-ms = 1500Prefer writing a domain rule over enabling lookups.
General fields
| Field | Default | Notes |
|---|---|---|
ipv6 | false | answer AAAA queries; keep false with the eBPF backend, which captures IPv4 only |
respect-rules | true | send a name's DNS query through the proxy its rule selects |
disable-cache | false | |
cache-capacity | 4096 | minimum 128 |
cache-min-ttl | 10 | seconds |
negative-cache-ttl | 30 | seconds |
query-timeout-ms | 5000 | minimum 100 |
use-hosts | true | |
use-system-hosts | true | read /etc/hosts |
[dns.hosts] enable = true is not implemented and is rejected; use the system hosts file.
Local DNS listener
Core can also serve DNS on a port, for other programs you point at it manually. It is off by default and is not needed for Enhanced Mode.
[dns.listener]
enable = true
address = "127.0.0.1:1053"Older field names
Flat fields such as [dns] enable, listen, nameserver, default-nameserver, enhanced-mode and fake-ip-range are still read, with a warning. Move them into the tables above: nameserver → [dns.upstream] default, default-nameserver → bootstrap, listen → [dns.listener] address, enhanced-mode → [dns.tun] mode.