➡️ How to check a forwarder
A forwarder is a server you hand certain queries to instead of looking them up yourself. When a forward zone misbehaves, it is almost always one of four things, and each has a one-command test.
If you are not sure what a forward zone is, read forward zones first. This page assumes you have one configured and it is not doing what you expected.
The four things that go wrong#
- The forward zone is not matching, so queries never get forwarded at all
- The upstream forwarder cannot or will not answer
- The forward works, but falls back to the public internet when it should fail
- DNSSEC validation rejects the answers
Check them in that order. Each one rules out the next.
Step 1: is the upstream actually answering?#
Skip your own server entirely and ask the upstream directly. If this fails, nothing else matters.
dig @192.0.2.53 internal.partner.example AReplace 192.0.2.53 with your forwarder's IP and internal.partner.example with a name inside the forwarded zone.
What you want to see: status: NOERROR and an answer.
What each failure tells you:
| What comes back | What it means | What to do |
|---|---|---|
| an answer | the upstream is fine, go to step 2 | move on |
REFUSED | the upstream will not do this for you | it is not a recursive resolver, or it does not host the zone, or its access list excludes your server's IP |
SERVFAIL | the upstream tried and failed | the problem is upstream of your upstream |
timed out | nothing there | wrong IP, the box is down, or a firewall is blocking port 53 |
REFUSED here is the single most common forwarder mistake. Pointing a forward zone at a server that is authoritative-only, and does not host that particular zone, gets you REFUSED every time. A forwarder has to be either recursive, or authoritative for the exact zone you are forwarding.
Step 2: is your own server actually forwarding?#
Now ask your resolver for the same name:
dig @10.0.0.53 internal.partner.example AReplace 10.0.0.53 with your own resolver's IP.
What you want to see: the same answer the upstream gave you in step 1, and importantly, no aa flag.
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, ...Why no aa: your server did not answer from its own data, it passed the question along and relayed what came back. aa means "I own this zone" (RFC 1035 §4.1.1), and a forwarding server does not own the zone. On a working forward, aa is always absent, even when the upstream itself was authoritative, because the forwarding server strips it.
If you see aa=1, your zone classification is wrong. Your server thinks it owns this zone rather than forwarding it. You have accidentally created an auth zone. That is the bug.
If you get NXDOMAIN from your server but a real answer from the upstream, your forward zone is not matching. The query never got forwarded. Check the zone name for a typo, and remember you cannot use a wildcard: you forward internal.partner.example and everything underneath follows automatically. *.internal.partner.example is not a valid forward zone name.
nslookupnever prints the flags line, so it cannot show youaadirectly. Its only hint is theNon-authoritative answer:label. Usedigfor this check. See nslookup-and-dig.
Step 3: does it fail the way you want?#
This is the check almost nobody runs, and it is the one that causes 2am incidents.
When the forwarder is unreachable, your resolver does one of two things, and the default disagrees between implementations:
- BIND defaults to
forward first: try the forwarder, and if that does not work, go resolve it from the public root servers yourself (BIND 9 ARM) - Unbound defaults the other way:
forward-firstisno, so a failed forward stays failed (unbound.conf(5))
For an internal namespace, falling back is almost always wrong. If the forwarders for internal.partner.example are down, you want SERVFAIL. You do not want your resolver asking the public root servers about your partner's internal names, finding they do not exist out there, and caching NXDOMAIN. Now the name is broken even after the forwarder comes back, until that cache entry expires.
To test it: make the forwarder unreachable on purpose (block it at the firewall, or point the zone at an address with nothing on it), then ask your resolver for a name in that zone.
dig @10.0.0.53 internal.partner.example A| What comes back | Verdict |
|---|---|
SERVFAIL | correct. It failed closed, exactly as an internal namespace should |
NXDOMAIN | wrong. It fell back to the public internet, did not find the name there, and is about to cache that |
The fix is to set forwarders-only explicitly on every internal-namespace forward zone rather than trusting the default. BIND calls it forward only;. Unbound gets it by leaving forward-first at its default of no.
Set it explicitly even when the default is already what you want, so the next person reading the config knows it was a decision.
Step 4: is DNSSEC rejecting the answers?#
Two different failures hide here.
If the forwarded zone is signed, your resolver sets the DO bit on its upstream queries and expects RRSIG records back. A forwarder that strips them turns every answer into SERVFAIL. Test whether signatures survive the trip:
dig @192.0.2.53 internal.partner.example A +dnssecLook for RRSIG records in the answer. If the same query direct to the upstream returns RRSIG but through your resolver does not, something in between is stripping them.
If the forwarded zone is private and unsigned, like consul. or cluster.local., the failure is the opposite. The signed public DNS tree can prove those names do not exist, so a validating resolver judges the answers bogus and returns SERVFAIL.
The fix is to carve that one name out of validation, not to turn validation off:
- BIND:
validate-except - Unbound:
domain-insecure
See dnssec and dnssec-troubleshooting for the full picture.
Quick reference#
| Symptom | Most likely cause |
|---|---|
REFUSED from the upstream | forwarder is not recursive and does not host the zone |
NXDOMAIN from yours, answer from upstream | forward zone is not matching, check the name |
aa=1 on the answer | it is an auth zone, not a forward zone |
SERVFAIL on a signed zone | signatures are being stripped in transit |
SERVFAIL on a private zone | validation rejecting an unsigned private namespace |
NXDOMAIN when the forwarder is down | fallback to root is on, set forwarders-only |
| latency spikes and log noise | forwarding loop, A to B to A |
See also#
- Forward zones: what they are and when to use one
- check-an-auth: the other end of the path
- Stub zones: one alternative to forwarding
- delegations: the other alternative to forwarding
- dot-doh: encrypting the hop to your forwarder