Originally published at series.
This post is part of Protocol Lab, a free, hands-on series for learning networking protocols by building and breaking them in a container lab. All the lab material — topologies, configs, and scripts — lives in the repo:
Prerequisite:
Verified Run Log (2026-07-05)
This lab has been confirmed reproducible on real hardware.
Environment:
- Ubuntu 26.04 LTS (kernel 7.0.0-27-generic, x86_64)
- Docker 29.1.3
- containerlab 0.77.0
- resolver/root/tld/auth:
protocol-lab/bind9:9.20, a thin wrapper aroundinternetsystemsconsortium/bind9:9.20(BIND 9.20.24) - client:
nicolaka/netshoot:latest(dig 9.20.23)
Running PATH="/tmp/pl-shim:$PATH" ./scripts/labctl.sh run dns-06 performed deploy → verify → destroy, and verification.json returned "status": "verified".
Keeping up with environment drift (fixes this verification required)
The upstream BIND image had switched to an Alpine base. Theexamples/dns-06/Dockerfilewas updated the same way as Lab 05's:apk add iproute2,ENTRYPOINT [], and a foregroundnamed -gCMD (see Lab 05's verification log for details).
The client's default route. The client in this lab only ever talks to the resolver (10.0.0.1, on the same eth1 subnet), soip route add default via 10.0.0.1— which collides with the management network's default route and fails withFile exists— isn't needed. It was removed to cut the noise.
The TTL countdown (it only shrinks while cached)
$ docker exec clab-dns-06-client dig @10.0.0.1 www.example.lab A
;; ANSWER SECTION:
www.example.lab. 60 IN A 203.0.113.10
# a few seconds later
$ docker exec clab-dns-06-client dig @10.0.0.1 www.example.lab A
;; ANSWER SECTION:
www.example.lab. 57 IN A 203.0.113.10
;; Query time: 0 msec
The same name's TTL dropped 60 → 57 — the remaining TTL shrinks by exactly the seconds elapsed since the resolver cached it. Query time: 0 msec signals a cache hit.
Contrast with a long-TTL name
$ docker exec clab-dns-06-client dig @10.0.0.1 stable.example.lab A
;; ANSWER SECTION:
stable.example.lab. 3600 IN A 203.0.113.20
stable carries TTL 3600 in the authoritative zone, so no re-query happens for a long while. The TTL expresses, in seconds, "how long you may trust this answer."
A nonexistent name → NXDOMAIN + SOA (negative caching)
$ docker exec clab-dns-06-client dig @10.0.0.1 missing.example.lab A
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 29784
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
;; AUTHORITY SECTION:
example.lab. 300 IN SOA ns.example.lab. admin.example.lab. 1 3600 900 604800 300
status: NXDOMAIN is the negative answer meaning "this name does not exist." The AUTHORITY section carries the example.lab. SOA, and the SOA's last field (300) is the negative TTL — how many seconds the resolver may cache this "doesn't exist" fact. A repeat of the same query is answered instantly from the negative cache (RFC 2308).
Cleanup
containerlab destroy -t dns-06.clab.yml --cleanup
That's DNS caching in a nutshell: the authoritative zone sets the clock, the resolver counts it down, and even "no such name" is an answer worth remembering — for exactly as long as the SOA allows.
Explore the full Protocol Lab series here: github.com/pathvector-studio/protocol-lab. If these labs are useful to you, please ⭐ star the repo on GitHub — it genuinely helps others find the project.
Next up, we'll keep climbing the DNS stack — think DNSSEC and how a resolver can prove an answer (or its absence) is genuine.
SOCIAL SHARE CARD GENERATOR