Chapter 02 · Part I · The namespace

A DNS message, byte by byte

Every DNS lookup is a short message, usually a single UDP datagram, and every answer comes back in exactly the same format. This chapter takes both apart: the 12-byte header and its flags, the four sections, the trick that squeezes repeated names, and what happens when an answer is too big to fit. If ports, UDP and TCP are new to you, start with networks · TCP, UDP & ports; for what DNS is for and the "UDP, size limits and privacy" basics, see networks · DHCP & DNS. Here we open the envelope.

12 bytesthe fixed header at the front of every DNS message, query or response (RFC 1035 §4.1.1)
512 bytesthe largest DNS message allowed over plain UDP when the client has not used EDNS (RFC 1035 §4.2.1)
1232 bytesthe EDNS buffer size DNS Flag Day 2020 recommended, so answers fit in IPv6's 1280-byte minimum MTU

Common mix-up: 512 bytes is not the largest DNS answer. It is the largest answer a server may send over plain UDP to a client that never said it could take more. A client that uses EDNS can accept a bigger datagram, and over TCP a message can be up to 65,535 bytes, because its length travels in a 2-byte prefix. When an answer does not fit, the server does not fail: it sets the TC bit, and the client asks again over TCP.

The shape of every message

One format, five parts

When your laptop asks for www.example.com, the bytes that leave it are not text. There is no "GET", no colon, no line break. There is a binary structure that Paul Mockapetris laid out in RFC 1035 in November 1987, and that every DNS server and client since has had to read the same way. The design has aged remarkably well: the extensions that came later (DNSSEC, EDNS, extended errors) all fit inside the original frame instead of replacing it.

The single most useful fact about that frame is that queries and responses have the same layout. A response is the query handed back with the header changed and answers appended. A server copies your question into its reply word for word, so a client can check that the answer is for the question it asked. One parser reads both directions.

Every message has five parts, always in this order. The header is fixed at 12 bytes. Then come four sections of variable length: the question (what is being asked), the answer (records that answer it), the authority (records that point toward the servers responsible for the name, or prove that it does not exist) and the additional section (records that might save the client another lookup, and, since EDNS, a special pseudo-record carrying options). The header says how many entries each section holds; the sections themselves carry no markers or separators. A parser simply reads that many entries, one after another.

Header 12 bytes Question QDCOUNT entries Answer ANCOUNT records Authority NSCOUNT records Additional ARCOUNT records Header, 12 bytes as six 16-bit words: ID FLAGS QDCOUNT ANCOUNT NSCOUNT ARCOUNT bytes 0–1bytes 2–3bytes 4–5bytes 6–7bytes 8–9bytes 10–11
The layout of every DNS message, query or response (RFC 1035 §4.1). The four counts tell the parser how many entries to read from each section; nothing else marks where one ends. All multi-byte numbers are big-endian: most significant byte first, "network order".
Go deeper: why the question comes back in the answer

Echoing the question is a cheap integrity check. Over UDP there is no connection, so a reply is matched to its query by three things: the source address and port it came from, the 16-bit ID in the header, and the question section. A client that receives a reply whose question does not match what it sent must throw it away. That matters for security as well as correctness: an off-path attacker trying to slip a forged answer into a resolver's cache has to guess all of those at once. Chapter 13 covers that contest; the defense depends on these bytes being checked.

In theory QDCOUNT can be more than 1. In practice almost no server accepts more than one question per message, because the header has only one RCODE and one AA bit: there is no way to say "the first question failed and the second one is authoritative". Treat one question per message as the rule.

Instrument 1

Flip the header

These are real header bytes. Click any of the 16 flag bits to toggle it, change the ID, opcode, response code or counts, or type 24 hex digits straight into the box. Every control, the hex and the plain-English reading underneath are recomputed from the same 12 bytes, so they always agree. Try the presets, then set the Z bit and see why a strict server answers FORMERR.

Type or paste exactly 24 hex digits; spaces are ignored.

Flags word, most significant bit first. Blue underline: OPCODE bits. Green underline: RCODE bits.

    Questions and records

    Names as labels, records as fields

    A question has three fields. QNAME is the name being asked about. QTYPE is a 16-bit number for the record type (1 for A, 28 for AAAA, 15 for MX; IANA keeps the list). QCLASS is a 16-bit class, which in practice is always 1, IN, for internet. (Class 3, CH for Chaosnet, survives mostly as a way to ask a server which software it runs.)

    Names are not stored with dots. Each label is written as a length byte followed by that many bytes of characters, and the name ends with a zero-length label: a single 0 byte, which stands for the root of the tree from Chapter 1. So www.example.com becomes 3 www 7 example 3 com 0: 17 bytes for a 15-character name. Because the length byte comes first, a parser never searches for a terminator, and a dot inside a label is just another byte. The rules that fall out of this: a label holds at most 63 bytes, and a whole name, length bytes included, at most 255.

    Every record in the answer, authority and additional sections uses one layout, the resource record. Its owner NAME comes first, encoded the same way. Then TYPE and CLASS (2 bytes each), then a 32-bit TTL in seconds (how long a cache may keep it; Chapter 4), then RDLENGTH, a 16-bit count of the bytes that follow, and then the RDATA itself, whose shape depends on the type: four bytes for an A record, sixteen for AAAA, a name for CNAME or NS, a 2-byte preference plus a name for MX. RDLENGTH means a parser can skip any record it does not understand, which is how new record types could be added for decades without breaking old software.

    NAMElabels or pointer TYPE2 CLASS2 TTL4 RDLENGTH2 RDATARDLENGTH bytes says how long this is
    Every resource record, in every section, uses this layout (RFC 1035 §4.1.3). Numbers are sizes in bytes. A question is just NAME, TYPE and CLASS. Only the OPT pseudo-record of EDNS reuses the fields for other purposes.

    Here is a complete worked example: the query a stub sends for the A record of www.example.com, with ID 0x2A7F, recursion desired, and no EDNS. It is 33 bytes.

    ; offset  bytes                                   meaning
      0     2a 7f                                   ID = 10879
      2     01 00                                   flags: QR=0 query, OPCODE 0, RD=1
      4     00 01  00 00  00 00  00 00              QDCOUNT 1, AN 0, NS 0, AR 0
     12     03 77 77 77                             label of 3: "www"
     16     07 65 78 61 6d 70 6c 65                 label of 7: "example"
     24     03 63 6f 6d                             label of 3: "com"
     28     00                                      the root: end of name
     29     00 01                                   QTYPE 1 = A
     31     00 01                                   QCLASS 1 = IN
    ; 33 bytes of DNS; + 8 UDP + 20 IPv4 = 61 bytes on the wire
    Go deeper: case, odd bytes and the 0x20 trick

    DNS names compare case-insensitively for the ASCII letters A to Z, but the bytes on the wire keep whatever case was sent, and a server echoes the question exactly. Some resolvers exploit that: they randomize the case of each letter in the query (wWw.ExAmPlE.cOm) and reject replies whose question does not match bit for bit. That trick, called "0x20 encoding" after the bit that separates upper from lower case in ASCII, adds a few more bits for a forger to guess.

    The wire format itself allows any byte value in a label, including spaces, dots and zero bytes. Host names are much stricter (letters, digits and hyphens), and internationalised names are carried as ASCII "xn--" labels. A careful parser must still handle any byte, which is why dig prints odd characters as \DDD decimal escapes, and why the decoder below does too.

    Saying it once

    Name compression and its trap

    A typical response repeats the same names over and over. The question names www.example.com; the answer's owner is www.example.com again; its CNAME target is web.example.com; the authority section lists example.com and ns1.example.com. With only 512 bytes to play with in 1987, writing each one out in full was a waste. So RFC 1035 added name compression: anywhere a name, or the tail end of a name, has already appeared in the message, the encoder can write a 2-byte pointer to the earlier copy instead.

    The trick fits in the length byte. A label length is at most 63, which in binary is 00111111: the top two bits of a real length byte are always 00. If they are 11 instead, the byte is the start of a pointer, and the remaining 14 bits, across this byte and the next, are an offset counted from the first byte of the message. In the response to our query, the question's www.example.com starts at offset 12, so a later copy of it becomes C0 0C: binary 11000000 00001100, "pointer, offset 12". And because example.com starts four bytes later, at offset 16, the CNAME target web.example.com can be written as the label 03 web followed by C0 10. A name is a sequence of labels that may end in a pointer, so whole names and shared suffixes both compress.

    pointer = 11 + 14-bit offset → C0 0C = offset 12
    The first two bits 11 mark a pointer (00 is an ordinary length byte; 01 and 10 are reserved). The other 14 bits say where in the message, counting from byte 0, the rest of the name continues. 14 bits reach offsets up to 16,383.

    Compression saves a lot. In the simulated response in Instrument 2, the six names after the question would take 98 bytes written out; compressed, they take 20, and the whole response shrinks from 190 bytes to 112. But it also hands a parser a hazard. A pointer is an instruction to jump, and nothing in the format stops a malicious or broken message from pointing a name at itself, or two pointers at each other. A naive parser that just follows pointers will loop forever, or build a name until memory runs out. Such a message is a cheap denial-of-service against any server or client that reads it.

    The defense is simple and every serious implementation has it: count. Refuse to follow more pointers than a name could legitimately need, refuse any name whose expanded length passes 255 bytes, and refuse a pointer that lands outside the message. Many parsers also insist that a pointer points backwards, to an offset earlier than itself; RFC 1035 describes pointers as referring to a "prior occurrence" of the name, and a backwards-only rule makes loops impossible by construction. The bug class has a name in MITRE's catalog: CWE-835, "Loop with Unreachable Exit Condition". It is a classic trap for anyone writing a DNS parser by hand, and a pointer chain is one of the first things a fuzzer will try.

    Go deeper: where compression is and is not allowed

    Compression is allowed in owner names and in the RDATA of the record types defined in RFC 1035 that contain names: NS, CNAME, PTR, MX, SOA and a few others. RFC 3597 later fixed the rule for everything newer: new record types must not use compression inside their RDATA, because a server that does not know a type cannot know where the names in it are, and so could not safely rewrite or relocate them. That is why, for example, the target name inside an SRV or HTTPS record is always written out in full.

    Compression is also why a DNS message cannot be cut and spliced at arbitrary points. Move a record and its pointers may now aim at the wrong bytes. Servers build each response from scratch with a table of names already written, which is exactly what the encoder in Instrument 2 does.

    Instrument 2

    Build a message, then decode it

    Type a name and choose a type to build the exact query bytes a client would send. Each byte is tinted by section and named in the field list; click any byte or field to read it. Then load the simulated response: underlined bytes with an orange border are compression pointers, and clicking one shows where it jumps (dashed outline) and the name it expands to. The decoder is a real parser, so you can also edit the hex and decode it, or load the broken message and watch it refuse a pointer loop. All data here is simulated, using the reserved name example.com and documentation addresses.

    headerquestionanswer authorityadditionalcould not parse
    Message size–
    Pointers used–
    Bytes saved by compression–
    Parser verdict–
    
          
    When it does not fit

    512 bytes, the TC bit and TCP

    RFC 1035 capped a DNS message over UDP at 512 bytes. The figure sits neatly inside a rule IPv4 already had: every host must be able to accept a datagram of at least 576 bytes, and 512 bytes of DNS plus the IP and UDP headers fits within it (RFC 6891 spells out the pairing). It was plenty for an A record or two. It is not plenty for a signed answer with DNSSEC signatures, a TXT record full of email policy, or an IPv6-heavy list of name servers.

    When an answer will not fit, the server does not fragment it or give up. It sends what fits (often just the header and the question) with the TC bit set to 1. TC=1 is not an error. It means "this is incomplete; ask me again over TCP". The client opens a TCP connection to the same server on port 53 and sends the same query. Because TCP is a byte stream with no message boundaries, each DNS message on it is preceded by a 2-byte length prefix, so the reader knows where one message ends and the next begins. That prefix is the only difference in the bytes; the message itself is identical. The two bytes also set the ceiling: 65,535 bytes per message.

    The cost is time. A UDP lookup is one round trip. A truncated lookup is that round trip, plus one for TCP's handshake, plus one for the query and answer over TCP: three in all, before counting any TLS. How TCP's handshake works is covered on networks · TCP, UDP & ports; what matters here is that the fallback must work. For years, firewalls were configured on the folk belief that "DNS is UDP", and blocked TCP port 53. RFC 7766 (2016) put that to rest: every DNS implementation, authoritative and recursive, MUST support TCP. A server that cannot be reached over TCP will fail for exactly the large answers that need it.

    $ dig TXT big.example.com      (simulated output)
    ;; Truncated, retrying in TCP mode.
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 10879
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 6, AUTHORITY: 0, ADDITIONAL: 1
    ;; ...answer section...
    ;; SERVER: 192.0.2.53#53(192.0.2.53) (TCP)
    ;; MSG SIZE  rcvd: 1844
    Go deeper: TCP is not only a fallback

    RFC 7766 also tidied up how TCP should be used: a client may send several queries down one connection without waiting (pipelining), a server may answer them out of order (the ID tells them apart), and connections should be kept open for reuse rather than torn down after each answer. Zone transfers between servers (AXFR and IXFR, Chapter 6) have always used TCP, since a whole zone never fits in a datagram. The encrypted transports in Chapter 12 build on the same framing: DNS over TLS sends exactly these length-prefixed messages inside a TLS session on port 853.

    Room to grow

    EDNS(0) and the OPT record

    By the late 1990s the header had run out of room: no free flag bits, a 4-bit RCODE, and a 512-byte ceiling. Changing the header would have broken every parser on earth. EDNS, the Extension Mechanisms for DNS (first RFC 2671 in 1999, now RFC 6891 from 2013), found a cleverer way in: a fake record. A client that speaks EDNS adds one OPT pseudo-record to the additional section of its query. Old servers that do not understand type 41 ignore it or answer FORMERR, and the client then knows to fall back. New servers read it, and put an OPT record in their reply too.

    The OPT record reuses the ordinary record fields for new purposes. Its NAME is the root, a single 0 byte. Its TYPE is 41. Its CLASS field carries the sender's UDP payload size: "I can receive a DNS message this big over UDP". Its 32-bit TTL field is split into an 8-bit extension of the RCODE (giving 12 bits in all), an 8-bit EDNS version (0 is the only one in use, hence "EDNS(0)"), and 16 flag bits, of which only the top one is assigned: DO, "DNSSEC OK", meaning "please include signatures". Its RDATA holds a list of options, each a 2-byte code, a 2-byte length and the data.

    OPT fieldNormal meaningMeaning in OPTExample
    NAMEOwner nameAlways the root (one 0 byte)00
    TYPERecord type41 = OPT00 29
    CLASSClass (IN)Largest UDP payload the sender can receive04 d0 = 1232
    TTLCache lifetimeExtended RCODE (8) · version (8) · DO + 15 zero bits00 00 80 00 = version 0, DO=1
    RDLENGTHLength of RDATALength of the option list00 0c
    RDATARecord dataOptions: code, length, value …COOKIE (10), Padding (12), Client Subnet (8), EDE (15)

    The options are where EDNS keeps growing. DNS Cookies (option 10, RFC 7873) let a client and server recognize each other's later messages, which makes spoofed replies and reflection floods harder. Padding (option 12, RFC 7830) rounds encrypted messages up to fixed sizes so their length does not give away the name. Client Subnet (option 8, RFC 7871) lets a resolver tell a content network roughly where its client is, a privacy trade-off covered in Chapter 9. And Extended DNS Errors (option 15) explain failures, below.

    Fragments, and the 1232-byte compromise

    EDNS let clients advertise big buffers, and for years 4096 bytes was the common default. That created a new problem. Every network link has a maximum transmission unit (MTU), typically 1500 bytes on Ethernet and often less through tunnels and VPNs. A 3000-byte UDP answer does not fit one packet, so IP fragments it, and fragments behave badly. Many firewalls and middleboxes drop them. If any fragment is lost the whole datagram is lost, and the client sees nothing at all: no reply, no TC bit, just a timeout. Worse, fragments carry no UDP port and no DNS ID after the first one, so an attacker can try to splice a forged second fragment into a real reply, a cache-poisoning route that the usual ID and port randomization does not protect.

    The fix agreed by DNS software vendors and operators on DNS Flag Day 2020 was to stop asking for big UDP answers. They recommended a default EDNS buffer of 1232 bytes: IPv6 requires every link to carry at least 1280 bytes, and 1280 minus a 40-byte IPv6 header and an 8-byte UDP header leaves 1232. An answer that size fits nearly every path without fragmenting. Anything bigger is truncated and fetched over TCP, which handles large messages properly. RFC 9715 (2025) wrote the same advice into an RFC, with a recommended maximum of 1400 bytes and a firm preference for avoiding fragmentation altogether.

    1280 − 40 − 8 = 1232
    IPv6 minimum link MTU, minus the IPv6 header, minus the UDP header: the largest DNS message guaranteed to cross any IPv6 path in one unfragmented packet.
    Instrument 3

    Will it fit?

    Set the size of the answer, the buffer the client advertises and the path's MTU. The lab adds the UDP and IP headers, decides what the server sends (a whole answer, or TC=1), works out whether the packet must be fragmented and into how many pieces, and draws the round trips it takes to get an answer, including the timeout when a firewall eats a fragment. The timeout figure is an assumption for illustration; real resolvers differ.

    Limit in force–
    Packet on the wire–
    What happens–
    Time to answer–

    Saying why

    Extended DNS Errors

    For most of DNS's life, a failure was a four-bit shrug. SERVFAIL means "something went wrong while I was trying", and it covers an enormous range of causes: an expired DNSSEC signature, every authoritative server timing out, a broken delegation, a resolver policy blocking the name. Operators debugging an outage had to guess, or reproduce the lookup by hand.

    Extended DNS Errors (RFC 8914, 2020) put the reason in the message. An EDE is an EDNS option, code 15, carrying a 16-bit INFO-CODE and an optional line of text. It rides alongside the ordinary RCODE rather than replacing it, so old clients see the same SERVFAIL as ever while new tools can print the cause. It can also accompany a successful answer: code 3, "Stale Answer", says the resolver served an expired cache entry because the authoritative servers could not be reached.

    INFO-CODENameWhat it tells you
    0OtherCatch-all; read the text
    3Stale AnswerServed from expired cache because the authorities were unreachable
    6DNSSEC BogusValidation failed: the signatures do not check out (Chapter 11)
    7Signature ExpiredThe RRSIG's validity period has ended; someone forgot to re-sign
    9DNSKEY MissingThe parent's DS points at a key the zone does not publish
    15BlockedThe resolver's operator blocks this name by policy
    18ProhibitedThis client is not allowed to use this resolver
    22No Reachable AuthorityNo authoritative server answered
    23Network ErrorA network error reaching an authority
    $ dig A expired.example.com     (simulated output)
    ;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 4321
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
    ;; OPT PSEUDOSECTION:
    ; EDNS: version: 0, flags: do; udp: 1232
    ; EDE: 7 (Signature Expired)
    Go deeper: what an EDE is not

    RFC 8914 is careful about trust. An EDE is a diagnostic, not a security signal: it is not signed, it can be forged as easily as any other part of an unprotected reply, and a client must not change its behavior (for example, treat a failure as success) because of one. It tells a human where to look. The full list of codes, which has grown since 2020, is kept by IANA in the DNS parameters registry.

    Cheat sheet

    Terms from this chapter

    Message ID
    The 16-bit number a client puts in the header and the server copies into its reply, used to pair replies with queries. Should be random.
    Header flags
    The second 16-bit word of the header: QR, OPCODE, AA, TC, RD, RA, Z, AD, CD and RCODE packed together.
    RCODE
    The response code in the header: 0 NOERROR, 2 SERVFAIL, 3 NXDOMAIN, 5 REFUSED and others. EDNS extends it from 4 to 12 bits.
    Label
    One dot-separated piece of a name, sent as a length byte (at most 63) followed by its bytes. A name ends with the zero-length root label.
    Name compression
    Replacing a repeated name, or the end of one, with a 2-byte pointer to an earlier copy in the same message.
    Compression pointer
    Two bytes whose top two bits are 11; the other 14 bits give the offset, from the start of the message, where the name continues.
    TC bit
    "Truncated": the server could not fit the whole answer in the UDP reply. The client should repeat the query over TCP.
    Length prefix
    The 2-byte message length placed in front of every DNS message sent over TCP (and TLS), so the reader knows where each message ends.
    EDNS(0)
    Extension Mechanisms for DNS, version 0 (RFC 6891): bigger UDP messages, more flags and options, carried in an OPT record.
    OPT record
    The type 41 pseudo-record in the additional section that carries EDNS: its CLASS is the UDP payload size, its TTL holds the extended RCODE, version and DO bit.
    DO bit
    "DNSSEC OK": an EDNS flag asking the server to include DNSSEC signatures and keys in its answer.
    EDNS buffer size
    The largest DNS message over UDP the sender says it can receive. DNS Flag Day 2020 recommended 1232 bytes to avoid fragmentation.
    IP fragmentation
    Splitting a datagram too big for a link into pieces. Lost or dropped fragments lose the whole DNS reply, and spliced fragments are a poisoning risk.
    Extended DNS Error
    EDNS option 15 (RFC 8914): a numeric reason and optional text explaining a failure, such as 7 Signature Expired.
    Where this comes from

    Sources