A name, asked and answered
A stub resolver: the stack writes a query for a host name's IPv4 address into a datagram to a server that recurses (QEMU's is 10.0.2.3), and reads the answer when it comes back. As everywhere else in the stack, it keeps nothing: the caller keeps the name, the query's id and its port, and hands them back with the answer. The answer is untrusted bytes from the network, and DNS is where network stacks have most often read past their buffers or looped forever; every name here is read so that neither can happen, and the proofs say so.
dns.syspAsking
dns-pick gives a query's id and source port from the time and the next IPv4 identification, under the key the caller lends with TCP's memory, so someone off the path cannot guess either and forge the answer (RFC 5452). dns-ask writes the query straight into the datagram it sends, with no buffer of its own: a header asking for recursion, and one question, the name in labels, type A, class IN. A name that is not a host name (labels of 1 to 63 letters, digits or hyphens, 255 bytes in all) is not asked for.
An answer, byte by byte
The answer QEMU's server gave the recording, laid out as dns-answer reads it: the header, the question it repeats, then the records. Each record's owner is a pointer, two bytes whose top bits are 11, to a name earlier in the message: the arrows. That is compression (RFC 1035 4.1.4), and it is the part of DNS that has gone wrong most often.
Names that cannot loop
Nor can the answers. The records are read in one scan, and two invariants of dns-records are proven (sysn 028): each record moves the scan on by 11 bytes at least, its owner's end, type, class, TTL and length, so the scan leaves the message after at most a twelfth of its bytes in records, whatever the header's count says; and the name a CNAME leads to is always behind the scan, so following one never goes back over a record. A chain that loops, a to b and b to a, ends with no address: none. Every function here is pure, reading only its arguments, so the same bytes give the same answer; and the resolver is a stub, asking one server that recurses, so it never follows a delegation of its own. That the resolver ends is tested, not proven, since sysp proves no termination: every loop in net/dns.sysp is a for over a bound, and tests/dns-test.rkt fails if the file gains a while.
In 2021 Forescout's NAME:WRECK found the same mistakes in the DNS code of four TCP/IP stacks (FreeBSD's, Nucleus NET, NetX and IPnet): pointers followed in a loop forever, label lengths trusted past the message's end, names longer than any buffer, record counts believed. RFC 9267 collects them as anti-patterns, with the remedies taken here. A pointer must point before the last place a pointer on that side of the comparison led, so each one followed moves strictly backwards and a name ends; a label's length is checked against the message before its bytes are read; a name over 255 bytes is no name; and the counts in the header only bound the loop over records, each of which must fit. A name is never copied out: it is compared where it lies, label by label, without case.
What an answer says
| Answer | When |
|---|---|
| (addr ip ttl) | the first A record for the name, or for the name a CNAME before it led to; the TTL in seconds, 0 when its top bit is set (RFC 2181) |
| (none) | an answer with no address for the name: none at all, or only other types |
| (failed rcode) | the server says it failed: 3 for no such name, 2 for a server failure |
| (truncated) | the answer did not fit 512 bytes; it should be asked over TCP, which this resolver does not do |
| (bad not-ours) | not an answer to the query: another id, not a response, another question |
| (bad malformed), (bad short) | a message that does not hold together, or shorter than a header |
What netd does
netd has the clock, the ports and the jobs, so asking again, giving up and keeping answers for their TTL are its. Since sysn 021 it serves the resolver as /net/dns, a protocol of conversations as icmp and tcp are (sysk: Names, and host): connect NAME to a conversation's ctl, a write to its data asks, a read is the answer. In Plan 9 /net/dns is one file, the query kept with the open file; netd keeps nothing per fid, so it keeps the query with a conversation. Its loop:
on a write of data: (id, sport) = dns-pick(now); dns-ask(10.0.2.3, sport, id, name)
asking: the ARP request goes, and the query is owed
on an ARP reply: from 10.0.2.3, the query owed goes now
on a datagram: from 10.0.2.3's port 53 to the kept sport: a = dns-answer(payload, id, name)
(addr ip ttl), none, (failed 3), truncated: the answer, read once
(bad not-ours) or (bad malformed): let go; the read waits on
The reader's wait is the timeout: after it, (lost), and the reader may ask again, as host does not. netd keeps no cache yet. netd's /net/cs asks too (sysn 025), with a record of its own, for the shell, which dials a program given a host's name in place of an address, as Plan 9's dial asks its connection server.
Left out, and why
AAAA (there is no IPv6 yet), EDNS and answers over TCP (a stub asking for one address does not need more than 512 bytes, and a truncated answer is said to be one), DNSSEC (validation belongs to a server the caller trusts, or to a later feature with sysc), a cache (the caller's, with the TTL given), search lists, and the 0x20 trick of mixing a query's case (RFC 5452 says id and port are the main defence; the case would need the stack to remember it).
Taken from
| From | Taken |
|---|---|
| RFC 1035 | the message, names in labels, compression, 512 bytes over UDP |
| RFC 9267, NAME:WRECK | the anti-patterns found in four stacks' DNS code, and their remedies: pointers that must point backwards, lengths checked before reading, names at most 255 bytes, counts not believed |
| RFC 5452 | an unpredictable id and source port, here from SipHash under the stack's key |
| RFC 1123, 2181, 4343 | host names in letters, digits and hyphens; the TTL's top bit; names compared without case |
| djbdns | a resolver that parses with every length checked against the packet, and answers that stay where they are |
| Plan 9 | /net/dns (netd's since sysn 021, as conversations); /net/cs, netd's (sysn 025) |
| smoltcp, lwIP | (looked at) both keep a query table inside the stack; here the caller keeps the query |