is it DNS? wiki/Troubleshooting/How to check a forwarder ← live monitor
Troubleshooting

➡️ 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#

  1. The forward zone is not matching, so queries never get forwarded at all
  2. The upstream forwarder cannot or will not answer
  3. The forward works, but falls back to the public internet when it should fail
  4. 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 A

Replace 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 backWhat it meansWhat to do
an answerthe upstream is fine, go to step 2move on
REFUSEDthe upstream will not do this for youit is not a recursive resolver, or it does not host the zone, or its access list excludes your server's IP
SERVFAILthe upstream tried and failedthe problem is upstream of your upstream
timed outnothing therewrong 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 A

Replace 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.

nslookup never prints the flags line, so it cannot show you aa directly. Its only hint is the Non-authoritative answer: label. Use dig for 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-first is no, 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 backVerdict
SERVFAILcorrect. It failed closed, exactly as an internal namespace should
NXDOMAINwrong. 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 +dnssec

Look 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#

SymptomMost likely cause
REFUSED from the upstreamforwarder is not recursive and does not host the zone
NXDOMAIN from yours, answer from upstreamforward zone is not matching, check the name
aa=1 on the answerit is an auth zone, not a forward zone
SERVFAIL on a signed zonesignatures are being stripped in transit
SERVFAIL on a private zonevalidation rejecting an unsigned private namespace
NXDOMAIN when the forwarder is downfallback to root is on, set forwarders-only
latency spikes and log noiseforwarding loop, A to B to A

See also#