Before running a mail server you check whether your addresses are on email blocklists. A sweep of both gateways against ten lists came back clean on all ten, IPv4 and IPv6, and that went into the record as a finding.
Drafted by an AI agent (claude-opus-5) from this lab’s own runbooks, deployment log and errata. Reviewed before publication by the site owner. How this site is written →
It was not a measurement. It was the sound of nothing happening.
Why absence was the wrong thing to trust
dig does not work from the machine the agent runs on. Outbound UDP/53 is blocked by the network path it sits behind, so every query returned empty.
On a blocklist, empty is precisely what not listed looks like.
That makes this class of check uniquely dangerous: the success condition is the absence of an answer, so a completely blind check returns the reassuring result every single time, for every address, forever.
The tell was incidental. A control query for the lab’s own domain came back with no nameservers — a domain whose services the same session had been reaching over HTTPS all day.
dig +short A example.net -> <EMPTY> # a domain that demonstrably works
getent hosts ntfy.example.net -> 198.51.100.10 ntfy.example.net
getent works because it goes through the system resolver. dig speaks UDP/53 directly and was being dropped. Two tools, same question, opposite answers, and only one of them was being used for the check.
The re-run, and why the first control still was not good enough
Redone from a host with working DNS, this time with a control address that must come back listed — 127.0.0.2, which every blocklist is required to return as listed.
| List | Control 127.0.0.2 | Verdict |
|---|---|---|
b.barracudacentral.org | 127.0.0.2 | usable — both addresses genuinely clean |
bl.spamcop.net | 127.0.0.2 | usable — both addresses genuinely clean |
zen.spamhaus.org | 127.255.255.254 | unusable |
127.255.255.254 is Spamhaus’s code for query arrived via a public resolver, refused. It came back for the control and for both host addresses. The “result” for that list was an error message wearing the shape of an answer.
The control logic had asserted only that the response was non-empty. An error code is non-empty.
A control must assert the expected value, not merely that something came back. That is the second lesson and the more portable one. Adding a control was the right instinct and it still let a broken list through, because the assertion was too weak to distinguish a listing from a refusal.
Spamhaus needs querying from a non-public recursive resolver, or with a subscription key. Any pre-flight that asks it through 1.1.1.1 or 8.8.8.8 gets this same non-answer.
The retraction
The original “clean on all ten lists” finding was withdrawn from the record rather than quietly overwritten, in the same way an earlier misdiagnosis was struck through instead of edited away.
Two of the three lists that could be checked properly do report both addresses clean. The third remains unknown. “Clean on ten” has become “clean on two, unknown on one, and the other seven need re-running from somewhere with working DNS” — which is less satisfying and actually true.
Rules
Never run DNS checks from a machine whose DNS you have not verified, and record where the check ran. Provenance is part of the result.
Every blocklist query needs a positive control, and the control must match the expected listing code rather than merely being non-empty.
When absence is the success condition, prove the mechanism works before trusting an absence. This is the third time in this project that the same shape has produced a comforting wrong answer rather than an obviously broken one: discarded error output read as a count of zero, an intrusion-detection ban read as a security policy, and now a blocked resolver read as a clean bill of health.
Obviously-broken checks get fixed. Comfortably-wrong ones get filed as findings.
An unrelated thing it found
Querying the zones from a resolver that actually worked showed that one of the lab’s three domains is at a different registrar than the documentation claimed — still at the provider the project believed it had migrated away from.
That matters more than it sounds. The dynamic-address tracker on both gateways resolves a name in that zone every five minutes, so a mechanism the whole inbound tunnel depends on was pointed at infrastructure the architecture documents had already written off. Nothing was broken. The documentation was simply wrong about where a load-bearing dependency lived, which is the kind of error that stays harmless right up until someone acts on it.
Found by accident, while checking something else, using a tool that had just been discovered not to work.