Skip to content

DNS Features

import { Aside } from ‘@astrojs/starlight/components’;

homeDNS supports all major DNS-over-X transports. Configure listeners in listeners.* in your YAML config.

TransportProtocolDefault portYAML keyStatus
Do53DNS over UDP/TCP53do53✅ v1
DoTDNS over TLS (RFC 7858)853dot✅ v1
DoHDNS over HTTPS (RFC 8484) H1/H2443doh✅ v1
DoQDNS over QUIC (RFC 9250)853🗓 Roadmap
DoH3DNS over HTTP/3 (RFC 9230)443🗓 Roadmap

Encrypted transports require TLS material in tls.cert_file / tls.key_file. DoH supports both POST (application/dns-message) and GET (?dns=<base64url>) per RFC 8484.


homeDNS can serve as an authoritative name server for any zone you define.

TypeDescription
AIPv4 address
AAAAIPv6 address
CNAMECanonical name alias
MXMail exchanger
TXTText record (SPF, DKIM, DMARC, etc.)
SRVService locator
NSName server
SOAStart of authority
PTRPointer (reverse DNS)
CAACertification authority authorization
  • Forward and reverse zones supported (e.g. example.com and 1.0.10.in-addr.arpa)
  • Wildcards (*.example.com) expand correctly
  • NXDOMAIN vs NODATA distinction is preserved — a missing name returns NXDOMAIN; a name with no records of the requested type returns NODATA
  • Apex CNAME rejection — CNAME at the zone apex (@) is rejected per RFC 1034
  • SOA serial auto-increments on zone edits

Import and export use standard RFC 1035 zone file syntax. Import via API (POST /api/v1/zones/{zone}/import) or the web admin drag-and-drop.


The iterative recursive resolver is built into dnsd. It chases delegations from the root down to authoritative servers.

LimitConfig keyDefault
Max upstream queries per resolutionrecursion.max_queries50
Per-query timeoutrecursion.query_timeout3s
CNAME chain depthrecursion.max_cname_hops8

For each incoming query, dnsd evaluates:

  1. ACL — drop or reject based on acl.allow_query / acl.deny_query
  2. Rate limit — per-source token bucket
  3. Filter — blocklist/allowlist check
  4. Cache — return cached positive or negative response if present
  5. Authoritative — serve answer from local zone if hosted
  6. Forward — send to configured conditional forwarder matching the query zone
  7. Recurse — iterative resolution if recursion.enabled: true and no forwarder matched

Each conditional forwarder has a strategy that controls how its upstream chain is queried.

StrategyBehaviourHealth-aware?
failoverTry upstreams in order; skip failing onesYes — skips failing
round_robinRotate through upstreams in sequenceYes — skips failing
randomPick an upstream at randomYes — skips failing
parallelRace all upstreams; accept first valid response, cancel losersYes — deprioritises slow
hedgedFire first upstream, then fire next after a delay derived from observed latencyYes — uses RTT averages

A response is accepted as the winner only if:

RcodeAccepted?
NOERROR with answers
NOERROR (NODATA — no records of requested type)
NXDOMAIN✅ — legitimate authoritative answer
SERVFAIL❌ — keep waiting
REFUSED❌ — keep waiting
FORMERR❌ — keep waiting
Network / timeout error❌ — keep waiting

If all upstreams return errors, the last non-successful response is returned.

A background prober sends a lightweight query (health.check.dnsd.local. Type A, RD=0) to each upstream at probe_interval. The response time is stored in a rolling window of sample_window samples.

StatusCondition
HealthyAvg RTT < slow_above, no recent probe errors
SlowAvg RTT ≥ slow_above
FailingLast failing_after consecutive probes all errored

Probe notes:

  • The probe query uses a private-use name — never a public cacheable name, to avoid polluting forwarder caches.
  • Errors are not included in the RTT average (a 2s timeout would corrupt a sub-10ms rolling average).
  • Probe transport matches forwarder transport — a DoT forwarder is always probed over DoT.
  • One goroutine per forwarder, cancelled and restarted when config changes (no goroutine leaks).

The filter engine is a reversed-label trie — suffix matching runs in O(labels) time.

  • The query name is split into labels and traversed from the TLD inward.
  • A match on doubleclick.net will match ads.doubleclick.net (subdomain) but not notdoubleclick.net (no prefix collision).
  • Allowlist always beats blocklist, regardless of which list loaded first.
ActionDNS response
nxdomainNXDOMAIN — name does not exist
nodataNODATA — no records
sinkholeConfigured sinkhole IPs (configurable IPv4/IPv6)
refusedREFUSED rcode
custom_cnameRedirect to specified hostname
FormatDescription
hosts0.0.0.0 domain.com or 127.0.0.1 domain.com style
domain-onlyOne domain per line
AdBlock`
dnsmasqaddress=/domain.com/0.0.0.0 style
autoAuto-detected format

The cache stores both positive responses (records) and negative responses (NXDOMAIN, NODATA per RFC 2308).

(qname-lowercased, qtype, qclass, DO-bit)

The DO bit is part of the key — DNSSEC-aware and unaware queries are cached separately.

Never cached: SERVFAIL, REFUSED.


homeDNS accepts DNS UPDATE messages. Authentication is per-zone:

ModeTrust anchor
noneNo updates (default)
ip_aclSource IP in allowed CIDRs
tsig_requiredTSIG key required
tsig_optionalEither ACL or TSIG

Supported TSIG algorithms: HMAC-MD5, SHA-1, SHA-256, SHA-512. See Dynamic DNS for DHCP server setup.


DNSSEC validation stubs are present in v1. The dnssec_validate flag in recursion activates hook points but full DNSSEC chain validation is a v2 feature. Trust anchor management is exposed via the /api/v1/dnssec/* API and the DNSSEC page in the web admin.


Three CIDR lists in YAML:

  • acl.allow_query — who may submit any DNS query
  • acl.allow_recurse — who may trigger recursive resolution
  • acl.deny_query — explicitly rejected sources (evaluated first)

All lists match against the client’s IP address.

Per-source-IP token bucket. Queries exceeding the burst are dropped silently. Configure via rate_limit.* in YAML.