Skip to content

DNS Parsing the Header

Elbasiouny, Mahmoud edited this page Aug 5, 2026 · 3 revisions

The DNS header is the first 12 bytes of every DNS message. It contains a 16-bit transaction ID, a packed 16-bit flags field (with several sub-byte fields), and four 16-bit counters that determine how many records appear in the body.

This makes the header a great place to practice:

  • Bit-level parsing with h_bits
  • Grammar-level validation with h_attr_bool
  • Context-driven parsing by storing counts using h_action
  • Sequencing with h_sequence

Source file: dns.c, headerParser()
Hammer concepts: h_bits · h_attr_bool · h_action · h_sequence


DNS Header Layout (12 bytes)

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                ID             |QR|Opcode|AA|TC|RD|RA|Z| RCode |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Total Questions       |         Total Answers         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Total Auth Resource Record   |  Total Add'l Resource Record  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Field Bits Type Valid Range Description
ID 16 unsigned any Transaction identifier matching query/response
QR 1 unsigned 0–1 Query (0) or Response (1)
Opcode 4 unsigned 0–15 Operation code
AA 1 unsigned 0–1 Authoritative answer (responses)
TC 1 unsigned 0–1 Truncation flag
RD 1 unsigned 0–1 Recursion desired
RA 1 unsigned 0–1 Recursion available
Z 3 unsigned must be 0 Reserved bits (must be zero)
RCODE 4 unsigned 0–15 Response code
QDCOUNT 16 unsigned practical bounds Number of questions
ANCOUNT 16 unsigned practical bounds Number of answers
NSCOUNT 16 unsigned practical bounds Number of authority records
ARCOUNT 16 unsigned practical bounds Number of additional records

Note: DNS specs define “practical” limits via message size rather than small numeric ranges. This example stores the counts and uses them to drive exact repetition in the body parser.


Step 1: Fixed-Width Fields with h_uint16

The DNS header begins with a 16-bit ID:

HParser *ID = h_uint16();

h_uint16() reads exactly 2 bytes and interprets them as an unsigned integer. (Hammer’s integer parsers are byte-based; use h_bits for sub-byte fields.)


Step 2: Sub-Byte Flags with h_bits

The next 16 bits are not a single “flags number” in this parser. Instead, we split the field into its spec-defined bit ranges using h_bits:

HParser *QR     = h_bits(1, false);
HParser *opcode = h_bits(4, false);
HParser *AA     = h_bits(1, false);
HParser *TC     = h_bits(1, false);
HParser *RD     = h_bits(1, false);
HParser *RA     = h_bits(1, false);
HParser *zero   = h_bits(3, false);
HParser *rCode  = h_bits(4, false);

h_bits(len, signed)

Parameter Type Description
len size_t Number of bits to read
signed bool Whether to interpret the extracted bits as signed

For DNS flags, every sub-field is naturally unsigned, so signed should be false. h_bits returns a 64-bit integer token; it is best suited for sub-byte and other integer fields up to 64 bits. For wider fields, use h_bytes() or split the field into smaller integer parsers.


Step 3: Reserved-Zero Validation with h_attr_bool

The DNS header contains 3 reserved Z bits that must be 0 in standard DNS messages. Instead of parsing them and validating later, we validate directly in the grammar with h_attr_bool.

h_attr_bool(parser, predicate, user_data)

Parameter Type Description
parser HParser * Inner parser producing a value
predicate bool (*)(HParseResult*, void*) Returns true to accept, false to fail
user_data void * Context pointer passed to the predicate

In the DNS parser:

HParser *verified_zero = h_attr_bool(zero, is_zero, ctx);

The predicate:

bool is_zero(HParseResult *result, void *user_data) {
    return result && result->ast && result->ast->uint == 0;
}

If those bits are non-zero, the parse fails immediately. This is the core pattern for grammar-level invariants: malformed packets do not “partially parse” - they are rejected.


Step 4: Combine Flag Fields with h_sequence

Once each flag sub-field is defined, we combine them in order:

HParser *flags = h_sequence(
    QR, opcode, AA, TC, RD, RA, verified_zero, rCode,
    NULL
);

h_sequence(p1, p2, ..., NULL)

Parses p1 then p2 then ... consecutively. The list must end with NULL. If any element fails, the whole sequence fails.

This produces a structured “flags” match without needing to post-process a 16-bit integer.


Step 5: Store Section Counts with h_action

The four count fields determine how many items appear in the body sections. A validator should not “guess” where the body ends; it should use these values to drive repetition.

This parser stores the values into a DNSContext using h_action:

HParser *QDCount = h_action(h_uint16(), store_qdcount, ctx);
HParser *ANCount = h_action(h_uint16(), store_ancount, ctx);
HParser *NSCount = h_action(h_uint16(), store_nscount, ctx);
HParser *ARCount = h_action(h_uint16(), store_arcount, ctx);

h_action(parser, action_fn, user_data)

Parameter Type Description
parser HParser * Inner parser
action_fn HParsedToken *(*)(const HParseResult*, void*) Called on success
user_data void * Context pointer passed to the action

The action functions are simple “store and return” helpers:

HParsedToken *store_qdcount(const HParseResult *r, void *user) {
    ((DNSContext*)user)->qdcount = (uint16_t)r->ast->uint;
    return (HParsedToken*)r->ast;
}

Why store counts? The body parser uses h_repeat_n(..., ctx->qdcount) (and similarly for AN/NS/AR) so it parses exactly the declared number of entries; no more, no less.


Step 6: Assemble the Full Header Parser

Finally, we sequence the entire header:

HParser *header = h_sequence(
    ID, flags, QDCount, ANCount, NSCount, ARCount,
    NULL
);

When header runs, it:

  1. Reads 16 bits -> ID
  2. Reads 16 bits -> flags (bit-sliced)
  3. Reads 4 × 16-bit counters -> stores into context

If any part fails (e.g., Z bits are non-zero), the header parse fails and the packet is rejected before any body parsing happens.


Worked Example (Flags)

Suppose the flags field is 0x8180:

  • QR = 1 (response)
  • Opcode = 0
  • AA = 0
  • TC = 0
  • RD = 1
  • RA = 1
  • Z = 0 (required)
  • RCODE = 0

Because Z=0, verified_zero succeeds. If Z were non-zero, the entire header would fail immediately.


Summary

Concept Hammer Function Why It Matters in DNS
Parse sub-byte fields h_bits(n, false) DNS flags are bit-packed
Enforce invariants in-grammar h_attr_bool(p, pred, ctx) Reject malformed packets early (Z bits, pointers, etc.)
Capture context h_action(p, store, ctx) Store QD/AN/NS/AR counts to drive body parsing
Parse fields in order h_sequence(a, b, ..., NULL) Header is fixed layout and must match exactly

Next: Parsing the Body
Previous: DNS Overview

Clone this wiki locally