Prufy notes
DNS ยท 8 min

Asking DNS

packages/dns gathers evidence and never judges it. Here's who it asks, what it asks them, and how it knows who to ask.

Why ask two kinds of server?

Two kinds of witnessClick a side
  • Asks each nameserver directly with its own Resolver and setServers([ip])
  • So if a provider's servers disagree, we can see it
  • IPv4 first, then IPv6. 2.5 seconds per address, 2 tries
  • The answer is the truth, right now
Weakness Plain UDP, so a reply could be forged by someone on the path.
Together, one can't be fooled by a cache and the other can't be fooled by a forged packet.

What gets asked

Every name one check asks aboutFrom the real diagnosticNames
NameCatchesAsked of
_prufy-challenge.mail.example.comThe record itselfEvery server
_prufy-challenge.mail.example.com.example.comThe domain typed twiceUp to 2 authoritative
mail.example.comThe host field left blankUp to 2 authoritative
example.comThe host field left blankUp to 2 authoritative
prufy-challenge.mail.example.comThe underscore left offUp to 2 authoritative
_prufy-challenge.example.comThe subdomain left offUp to 2 authoritative
Asking about the record only tells you it's missing. Asking about the likely mistakes tells you why.
Predict it first
A provider has 13 nameservers. How many of them get asked about the record itself?

Finding the zone

The engine needs to know which zone a name is in, so it can tell you what to type and who to ask. It walks up from the name until something answers with nameservers. This runs on every check, because nameservers change.

Finding the zoneChoose a name
mail.example.comNo nameservers here, keep going
Step 1 of 2
Most subdomains are just names inside their parent's zone.

Who's the provider?

Nameserver hostnames give the provider away. The catalog maps those patterns to each provider's own field names and steps.

From a nameserver to instructionsReads the real provider catalog
Detected
Cloudflare
Calls the fields Name and Content. Adds your domain automatically.
Steps we show
  1. Open Cloudflare and click your domain.
  2. In the menu, go to DNS, then Records.
  3. Click Add record and choose TXT.

A query never rejects

One timeout shouldn't sink a whole check, so every DnsSource returns its failure as data:

interface DnsAnswer {
  rcode: DnsRcode | null        // null means no response arrived
  answers: readonly string[]
  cnameChain: readonly string[]
  ad: boolean | null            // DNSSEC flag, null if the source can't say
  serverIp: string | null
  error: LookupErrorKind | null // timeout, servfail, refused, network...
}

A wrinkle found by looking at real data

Long TXT values get split into several strings of up to 255 characters, meant to be joined with nothing between them. The two public resolvers report this differently, which only showed up by recording real responses.

Google joins them for you
"data": "v=DKIM1; k=rsa; p=MIIB...GqMo..."
Cloudflare shows them quoted
"data": "\"v=DKIM1; k=rsa; p=MIIB...\" \"GqMo...\""

A test checks that one real multi-string record reads identically through both parsers.

Faking DNS for tests and demos

Tokens are random, so a fixture can't hard-code a right answer. Record values are templates instead, filled in for each check. Set DNS_MODE=fixtures and every failure can be staged on demand.

PlaceholderRenders as
{{expected}}The right value
{{expected_lookalike}}One character swapped for a lookalike
{{expected_truncated}}Cut off
{{previous}}An older value for this domain
{{other}}A different, well-formed code

Records can also be limited by audience and by check number, which is how "propagating, then verified" is staged. See packages/dns/fixtures/scenarios.json.