Skip to content

DNS Assembling the Parser

Elbasiouny, Mahmoud edited this page May 29, 2026 · 5 revisions

DNS -- Assembling the Parser

In the DNS example, "assembling the parser" is less about building one giant HParser * and more about wiring together:

  1. A shared context (DNSContext) that holds header-derived information\
  2. A header parse that fills that context\
  3. A body parse that uses the stored values to validate the rest of the message

This document explains how main.c, dns.h, and the DNS parser entrypoints work together.


The Glue: DNSContext (dns.h)

DNS is a header-driven protocol. The header tells you how many Questions/Answers/etc. you must parse. To support that, the parser uses a shared context struct between header and body parsing:

typedef struct {
    uint16_t qdcount;
    uint16_t ancount;
    uint16_t nscount;
    uint16_t arcount;
    uint16_t packet_len;
    uint16_t curr_storage;
} DNSContext;

What each field is used for

Field Set By Used By Meaning
qdcount header parser (h_action) body parser (h_repeat_n) number of Question entries
ancount header parser (h_action) body parser (h_repeat_n) number of Answer RRs
nscount header parser (h_action) body parser (h_repeat_n) number of Authority RRs
arcount header parser (h_action) body parser (h_repeat_n) number of Additional RRs
packet_len main.c pointer validation total message length in bytes
curr_storage body parser (h_tell + h_action) pointer validation "current position" used to validate compression pointers

Key idea: the header parser doesn't just "recognize bytes" - it also initializes state the body parser depends on.


The Entry Point: main.c

The current implementation performs orchestration directly inside main.c.

#include "dns.h"
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    uint8_t input[1024] = {0};
    FILE *f_input = stdin;

    if (argc > 1) {
        f_input = fopen(argv[1], "rb");
        if (!f_input) {
            perror("Failed to open file");
            return 1;
        }
    } else {
        puts("Reading from stdin");
    }

    size_t input_size = fread(input, 1, sizeof(input), f_input);
    if (f_input != stdin) fclose(f_input);

    DNSContext ctx = {0};
    ctx.packet_len = input_size;

    // Header
    HParser *header = headerParser(&ctx);
    HParseResult *header_result =
        h_parse(header, input, input_size);

    if (!header_result){
        puts("Bad header, Packet Failed");
        return 1;
    }

    // Body
    if (input_size < 12){
        puts("Bad body, Packet Failed");
        h_parse_result_free(header_result);
        return 1;
    }

    HParser *body = bodyParser(&ctx);
    HParseResult *body_result =
        h_parse(body, input + 12, input_size - 12);

    if (!body_result){
        puts("Bad body, Packet Failed");
        h_parse_result_free(header_result);
        return 1;
    }

    puts("Packet Passed");
    h_parse_result_free(header_result);
    h_parse_result_free(body_result);
    return 0;
}

Parsing Flow Explained

1. Read Input

  • Reads up to 1024 bytes
  • Accepts either a file argument or stdin
  • Stores total size in ctx.packet_len

2. Parse Header First

HParser *header = headerParser(&ctx);
HParseResult *header_result = h_parse(header, input, input_size);

If successful:

  • Validates the fixed 12-byte DNS header
  • Stores qdcount, ancount, nscount, and arcount in ctx

If it fails → packet is rejected immediately.


3. Guard Against Too-Short Packets

if (input_size < 12)

Ensures safe slicing for the body parse.


4. Parse Body Using the Stored Counts

The body parser is run against the bytes after the header:

HParser *body = bodyParser(&ctx);
HParseResult *body_result =
    h_parse(body, input + 12, input_size - 12);

The body parser now relies on the stored counts:

  • qdcount → drives question parsing
  • ancount, nscount, arcount → drive RR parsing
  • packet_len + curr_storage → validate compression pointer bounds

If body parsing fails → packet is rejected.


5. Cleanup

If both parses succeed, the function frees the parse results and returns 0 (success):

h_parse_result_free(header_result);
h_parse_result_free(body_result);

Hammer allocates structures for parse results. Even if you don't use the AST, you should free the results to avoid leaks.

Summary

What makes this assembly pattern work is the shared context:

  • The header parse validates the fixed structure and populates DNSContext
  • The body parse uses that stored information to validate variable-length sections
  • main.c accepts a packet only if both parses succeed

This is a clean, practical approach for a validator:

  • It rejects invalid packets early
  • It keeps header and body logic modular
  • It uses the DNS spec's "counts drive structure" rule directly in the grammar

Next: DNS-Running-and-Testing Previous: DNS-Parsing-the-Body

Clone this wiki locally