Views and forwarding
Answers scoped to client networks, per-zone upstreams, and whole-server forwarder mode.
Views
A view serves particular answers to particular client networks. Everything a view does not match falls through to normal resolution.
[[views]]
zone = "lannet"
networks = ["192.168.1.0/24"]
answers = [
"*.example.lan. 60 IN A 192.168.1.3",
"*.example.lan. 60 IN AAAA fd00::3",
]
[[views]]
zone = "vpnnet"
networks = ["100.64.0.0/24"]
answers = ["*.example.lan. 60 IN A 100.64.0.2"]
Answers are zone-file lines. Wildcards work, and an exact owner overrides a
covering wildcard. A wildcard matches names below its suffix only:
*.example.lan. does not match example.lan. itself.
The first view, in declaration order, whose networks contain the client is the only one consulted. If it has no record of the asked type for the name, the query resolves normally, and later views are not tried. Matching is by exact type: a view with only A records lets an AAAA question for the same name resolve publicly.
Views run early in the chain, after the hosts file and ahead of the blocklist, RPZ and the cache, so a view answer is not subject to policy or caching. A name the hosts file answers never reaches the views.
The example above is the common case: one internal name that must resolve to different addresses depending on which network the client is on.
Per-zone forwarding
Sends one zone’s queries to its own upstreams while everything else still resolves recursively.
[[forward_zone]]
name = "corp.example."
servers = ["10.0.0.53:53", "tls://10.0.0.54:853"]
The most specific matching zone wins. A zone must name itself and at least one
server or startup fails, an omitted name would forward every query. A zone
whose upstreams all turn out unusable fails its own queries rather than falling
back to forwarderservers, because sending an internal zone’s questions to a
public resolver is worse than failing them.
Servers take the same forms as forwarderservers, so DoT and DoH work per zone.
This is forwarding, not delegation
The query goes out with RD=1 to a resolver that answers on your behalf, in the RFC 9499 sense. The upstreams must be recursive resolvers, not the zone’s authoritative servers.
A forwarded zone is not validated here
Answers carry whatever the upstream asserted, exactly as in whole-server forwarder mode. Pointing a signed public zone at an upstream gives up local DNSSEC validation for it. The intended use is the opposite case: an internal zone the public namespace cannot resolve at all.
Whole-server forwarder mode
forwarderservers = [
"8.8.8.8:53",
"tls://8.8.8.8:853",
"tls://9.9.9.9:853#dns.quad9.net",
"https://cloudflare-dns.com/dns-query",
]
With this set, sdns stops resolving from the root and forwards everything.
Plain DNS, DoT (tls://) and DoH (https://, RFC 8484) are all accepted.
A DoT upstream is an IP address and port, and by default its certificate must
be valid for that address. Some providers’ certificates carry only their
service name. For those, add the name after #:
tls://9.9.9.9:853#dns.quad9.net connects to 9.9.9.9, sends dns.quad9.net
as SNI, and accepts only a certificate valid for that name. The name is never
resolved: the address says where to connect, and the name what must answer
there. This is the IP address plus authentication domain name configuration
of RFC 8310, and it works in forward zones too. A certificate that does not
match fails that upstream, and the next configured one is tried as usual;
sdns never retries it without the name.
DoH URLs may use an IP literal or a hostname. A hostname is resolved once at startup through the system resolver and the resulting addresses are pinned for the life of the process, so there is no per-query DNS dependency and no bootstrap loop.
Note what this costs: forwarding means trusting the upstream’s answers rather than validating them yourself, and it means no learned delegation cuts, which in turn weakens the bound on serve-stale.
Fallback servers
fallbackservers = ["8.8.8.8:53"]
Used when normal resolution of a query with RD=1 has returned SERVFAIL because
it could not get an answer (a lame delegation, an unreachable upstream, a
network fault), not only when the root is unreachable. Unlike
forwarderservers this does not change the normal mode of operation.
Some SERVFAILs are passed to the client unchanged, without trying a fallback:
- a query with RD=0;
- a DNSSEC validation failure, so a bogus answer is never sent to a fallback server to be answered unvalidated;
- a request the recursion firewall stopped in
enforcemode; - a request whose own time has run out;
- a query shed while it waited on the retry of an expired cached resolution failure (RFC 9520), since a fallback query would bypass that bound;
- a SERVFAIL answered from the RFC 9520 failure cache: the cache sits ahead of the fallback in the chain, so while a failure is cached, its queries get it without a fallback attempt.
Three limits worth knowing. Fallback servers are queried over plain UDP, and
over TCP when a reply comes back truncated, with a hard five-second ceiling per
endpoint. Their answers are cached like any other. And, the one that matters
most, a fallback answer is not validated here: it is written as the
upstream asserted it, so configuring fallbackservers means a resolution that
failed locally can be answered by an upstream you are trusting rather than
checking. Note also that a per-zone forward that fails writes SERVFAIL, which
is itself a fallback trigger, so with fallbackservers set, an internal zone’s
questions can reach them.