Module 06 — DNS Parser
What you learn
How to parse a complete DNS message from raw wire bytes — queries and responses, over UDP and TCP, including pointer compression and CNAME chains.
After this module you have everything needed to extract the domain name and query type from any DNS packet entering the pipeline, which is the prerequisite for both policy lookup (Module 17) and DNS sinkholing (Module 18).
Where this fits in the real application
UDP payload (or TCP payload after stripping 2-byte length prefix)
│
├─► dns_parse_message(wire, wire_len, &msg)
│
│ msg.qname = "blocked-malware.example.com"
│ msg.qtype = DNS_TYPE_A (or DNS_TYPE_AAAA)
│ msg.id = 0xcafe
│ msg.question_wire_end = offset after question section
│
├─► dns_normalize_name(msg.qname) → lowercase
│
├─► rte_hash_lookup_data(domain_details_table, msg.qname, &fd)
│ hit → apply filter_details (Module 17)
│ miss → hs_scan_domain_group(msg.qname, ...) (Module 17)
│
└─► if blocked:
qtype A → inject A answer (walled-garden IPv4) (Module 18)
qtype AAAA → inject AAAA answer (walled-garden IPv6) (Module 18)
Files
| File | Purpose |
|---|---|
dns_parser.h |
dns_message_t, dns_answer_t, API declarations |
dns_parser.c |
Parser + 5 tests (A query, AAAA with normalization, A response, CNAME+AAAA chain, DNS over TCP) |
Makefile |
Build rules (includes ../05-packet-structs for packet_structs.h) |
Build and run
make
./dns_parser
Expected output:
=== Module 06: DNS Parser ===
--- Test 1: A query for blocked-malware.example.com ---
ID : 0xcafe
Type : QUERY
[Question]
qname : blocked-malware.example.com
qtype : A (1)
→ policy engine would look up: "blocked-malware.example.com" (type A)
PASS
--- Test 2: AAAA query — uppercase normalization ---
'GOOGLE.COM' normalized to 'google.com'
PASS
--- Test 3: A response with pointer compression ---
Pointer compression decoded correctly: www.example.com → 93.184.216.34
PASS
--- Test 4: CNAME + AAAA chain response ---
CNAME decoded: cdn.example.com → origin.example.com
PASS
--- Test 5: DNS over TCP ---
TCP length prefix: 38 bytes
Extracted domain: "ads.tracker.io"
PASS
Key concepts in the code
1. DNS qname wire format — label encoding
DNS names are not null-terminated strings in the wire format. They use length-prefixed labels:
"www.example.com" on the wire:
Offset Value Meaning
------ ----- -------
0 0x03 length = 3
1-3 "www"
4 0x07 length = 7
5-11 "example"
12 0x03 length = 3
13-15 "com"
16 0x00 root label = end of name
2. Pointer compression (RFC 1035 §4.1.4)
In DNS responses, names are often back-referenced to save space.
If the top 2 bits of a length byte are 11, it’s a pointer:
0xC0 0x0C = 1100_0000 0000_1100
^^ top 2 bits set → pointer
^^^^^^^^^^^^^^ 14-bit offset = 12
So 0xC0 0x0C means “the name is at offset 12 in this DNS message”.
Without handling this, answer section parsing breaks for virtually all real responses.
3. DNS over TCP — 2-byte length prefix
TCP stream (DNS): +-----+-----+---DNS message---+
| len (2B) | DNS header + qname + ... |
+-----+-----+---DNS message---+
uint16_t dns_len = read_u16_be(tcp_payload);
uint8_t *dns_msg = tcp_payload + 2;
dns_parse_message(dns_msg, dns_len, &msg);
4. qtype matters for sinkholing
The query type drives what kind of fake DNS answer to inject:
Client sends: www.blocked.com A query
the DP application injects: A answer with 10.0.0.1 (walled-garden IPv4)
Client sends: www.blocked.com AAAA query
the DP application injects: AAAA answer with fd00::1 (walled-garden IPv6)
5. Name normalisation
DNS names are case-insensitive (RFC 1034 §3.1). A client may send
GOOGLE.COM, Google.Com, or google.com — all mean the same thing.
dns_normalize_name() lowercases the qname before any hash table
lookup, so the policy table only needs to store lowercase entries.
6. question_wire_end offset
After parsing, msg.question_wire_end holds the byte offset where the
question section ends. Module 18 (DNS sinkhole) uses this to know where
to write the injected answer section in the in-place mbuf rewrite:
[DNS header 12B][question section][ ← inject answer here]
^
question_wire_end
DNS record types reference
| Type | Value | rdata | Used for |
|---|---|---|---|
| A | 1 | 4-byte IPv4 | Most queries, sinkhole target |
| AAAA | 28 | 16-byte IPv6 | IPv6 queries, sinkhole target |
| CNAME | 5 | label-encoded name | CDN aliases — follow the chain |
| MX | 15 | priority + name | Email — not filtered in the DP application |
| NS | 2 | label-encoded name | Nameserver delegation |
Next module
Module 07 — TLS SNI Extractor: Parse a TLS ClientHello from a TCP payload to extract the Server Name Indication (SNI) field — the domain name for HTTPS connections.
Source files
| File | Download |
|---|---|
dns_parser.h |
dns_parser.h |
dns_parser.c |
dns_parser.c |
Makefile |
Makefile |