-
Notifications
You must be signed in to change notification settings - Fork 1
DNS Parsing the Header
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
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.
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.)
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);| 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.
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.
| 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.
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
);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.
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);| 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.
Finally, we sequence the entire header:
HParser *header = h_sequence(
ID, flags, QDCount, ANCount, NSCount, ARCount,
NULL
);When header runs, it:
- Reads 16 bits -> ID
- Reads 16 bits -> flags (bit-sliced)
- 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.
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.
| 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
Learn Hammer
Protocol Examples
NTP
- NTP Overview
- Parsing the Header
- Parsing Data Fields
- Extension Fields and MAC
- Assembling the Parser
- Hex Input Preprocessing
- Running and Testing
DNS
TFTP
- TFTP Overview
- RRQ/WRQ Packets
- DATA Packets
- ACK Packets
- ERROR Packets
- Assembling the Parser
- Running and Testing
References
- Hammer Quick Reference
- Parsing Backends
- Unit Testing
- Using RTEMS
- Extending Hammer
- Adding a New Example
- Adding a New Binding
Further Reading