<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ece Kaptan</title>
    <description>The latest articles on DEV Community by Ece Kaptan (@ece_kaptan).</description>
    <link>https://dev.to/ece_kaptan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4090820%2F8149a993-88d2-4f77-b4cb-422b7fab13f7.png</url>
      <title>DEV Community: Ece Kaptan</title>
      <link>https://dev.to/ece_kaptan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ece_kaptan"/>
    <language>en</language>
    <item>
      <title>Understanding DNS by building dig</title>
      <dc:creator>Ece Kaptan</dc:creator>
      <pubDate>Sun, 23 Aug 2026 13:27:20 +0000</pubDate>
      <link>https://dev.to/ece_kaptan/understanding-dns-by-building-dig-36b8</link>
      <guid>https://dev.to/ece_kaptan/understanding-dns-by-building-dig-36b8</guid>
      <description>&lt;p&gt;Full source: &lt;a href="https://github.com/ecekaptan/dig-from-scratch" rel="noopener noreferrer"&gt;ecekaptan/dig-from-scratch&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everyone says DNS is like a phone book for the internet, but most people never stop to ask how it actually does that. The goal is simple: look at the raw bytes of a DNS packet and see what each one means: the header, the flags, the question, and the answer records that come back. The code in &lt;a href="https://github.com/ecekaptan/dig-from-scratch/blob/main/dig.c" rel="noopener noreferrer"&gt;&lt;code&gt;dig.c&lt;/code&gt;&lt;/a&gt; is the companion here, and every idea below maps to a function you can read and follow.&lt;/p&gt;

&lt;p&gt;A DNS lookup is really just &lt;strong&gt;two packets&lt;/strong&gt;: you send a &lt;em&gt;query&lt;/em&gt;, and the server sends back a &lt;em&gt;response&lt;/em&gt;. Both use the same basic structure, made of five parts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------------------+
|      Header       |  always 12 bytes
+-------------------+
|     Question      |  what you asked
+-------------------+
|      Answer       |  the records you wanted   (response only)
+-------------------+
|     Authority     |  which name servers are in charge   (response only)
+-------------------+
|     Additional    |  extra helpful records   (response only)
+-------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is &lt;strong&gt;big-endian&lt;/strong&gt; (most-significant byte first) and packed tightly: no padding, no separators, no wasted space. You know where one field ends because you already know how long it is.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The 12-byte header
&lt;/h2&gt;

&lt;p&gt;The header is six 16-bit numbers, in this exact order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;byte:  0   1   2   3   4   5   6   7   8   9  10  11
     +---+---+---+---+---+---+---+---+---+---+---+---+
     |  ID   | FLAGS |QDCOUNT|ANCOUNT|NSCOUNT|ARCOUNT|
     +---+---+---+---+---+---+---+---+---+---+---+---+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Bytes&lt;/th&gt;
&lt;th&gt;What it's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0-1&lt;/td&gt;
&lt;td&gt;A number you choose. The server copies it back into its reply so you can match your response to the query you sent.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FLAGS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2-3&lt;/td&gt;
&lt;td&gt;16 bits of yes/no switches and a couple of small values. We break those down below.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;QDCOUNT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4-5&lt;/td&gt;
&lt;td&gt;How many questions are in the packet. In our case, this is always &lt;code&gt;1&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ANCOUNT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6-7&lt;/td&gt;
&lt;td&gt;How many answer records are present. It is &lt;code&gt;0&lt;/code&gt; in a query; the reply fills it in.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NSCOUNT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8-9&lt;/td&gt;
&lt;td&gt;How many authority records are there.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ARCOUNT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10-11&lt;/td&gt;
&lt;td&gt;How many additional records are there.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the code, this is written with &lt;a href="https://github.com/ecekaptan/dig-from-scratch/blob/main/dig.c" rel="noopener noreferrer"&gt;&lt;code&gt;build_query&lt;/code&gt;&lt;/a&gt; and read back in &lt;a href="https://github.com/ecekaptan/dig-from-scratch/blob/main/dig.c" rel="noopener noreferrer"&gt;&lt;code&gt;check_response_header&lt;/code&gt;&lt;/a&gt;. Because the fields live at fixed offsets, they are read through the &lt;code&gt;put16&lt;/code&gt; and &lt;code&gt;get16&lt;/code&gt; helpers instead of raw pointer casts (more on that in §6).&lt;/p&gt;

&lt;h3&gt;
  
  
  The FLAGS field, bit by bit
&lt;/h3&gt;

&lt;p&gt;This is the part that often looks intimidating at first, but it becomes much easier once you see it as packed bits instead of one big number. Those two bytes are &lt;strong&gt;not&lt;/strong&gt; a single value; they are a collection of flags. Numbering the 16 bits from 15 (leftmost) down to 0 gives us this layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; bit  15  14 13 12 11  10   9   8   7   6  5  4   3  2  1  0
     +---+-----------+---+---+---+---+---+-------+-----------+
     |QR |  Opcode   |AA |TC |RD |RA |    Z      |   RCODE   |
     +---+-----------+---+---+---+---+---+-------+-----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bits&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;QR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0 = this packet is a query, 1 = a response.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14-11&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Opcode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The type of query. 0 means a standard lookup, which is what we use here.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;AA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Authoritative Answer&lt;/em&gt;: the responder actually owns the zone, rather than just giving a cached copy.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;TC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Truncated&lt;/em&gt;: the answer didn’t fit in the packet. &lt;strong&gt;This is the flag that triggers our TCP retry.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Recursion Desired&lt;/em&gt;: “please chase the full answer for me.” &lt;strong&gt;This is the bit we set.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Recursion Available&lt;/em&gt;: the server tells us whether it is willing to do recursion.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6-4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Z&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reserved and must be 0.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3-0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RCODE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Response Code&lt;/em&gt;: 0 means success, while other values mean an error (see the table below).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why our query’s flags are &lt;code&gt;0x0100&lt;/code&gt;:&lt;/strong&gt; we want exactly one bit set: RD (bit 8).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0x0100 = 0000 0001 0000 0000
                   ^
                   bit 8 (RD) = 1, everything else 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A typical successful reply has flags &lt;code&gt;0x8180&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0x8180 = 1000 0001 1000 0000
         ^        ^ ^
         |        | RA = 1  (server does recursion)
         |        RD = 1    (echoed back)
         QR = 1             (this is a response)
         ...RCODE = 0000 = NOERROR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code pulls the individual pieces back out with bit math:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;get16(response + 2) &amp;amp; 0xF&lt;/code&gt; gives the RCODE (the bottom 4 bits).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(get16(response + 2) &amp;gt;&amp;gt; 9) &amp;amp; 1&lt;/code&gt; gives the TC bit (shifted down, keeping only the one relevant bit). That one line is what decides whether we fall back to TCP.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  RCODE: what went wrong
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flags &amp;amp; 0x000F  -&amp;gt;  RCODE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RCODE&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;NOERROR&lt;/td&gt;
&lt;td&gt;Success.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;FORMERR&lt;/td&gt;
&lt;td&gt;The server couldn’t parse the query.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;SERVFAIL&lt;/td&gt;
&lt;td&gt;The server failed while trying to answer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;NXDOMAIN&lt;/td&gt;
&lt;td&gt;That name &lt;strong&gt;does not exist&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;NOTIMP&lt;/td&gt;
&lt;td&gt;The server does not support this kind of query.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;REFUSED&lt;/td&gt;
&lt;td&gt;The server refuses to answer (policy).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/ecekaptan/dig-from-scratch/blob/main/dig.c" rel="noopener noreferrer"&gt;&lt;code&gt;check_response_header&lt;/code&gt;&lt;/a&gt; prints these names and stops instead of trying to parse an answer section that is not there.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The question section
&lt;/h2&gt;

&lt;p&gt;Right after the header comes the question. One question has three pieces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;QNAME (variable)      the hostname, label-encoded
QTYPE (2 bytes)       what record you want (A = 1, MX = 15, ...)
QCLASS (2 bytes)      almost always 1 = IN (Internet)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How a name is encoded (QNAME)
&lt;/h3&gt;

&lt;p&gt;DNS does not store &lt;code&gt;www.example.com&lt;/code&gt; as plain text with dots. Instead, each dot-separated &lt;strong&gt;label&lt;/strong&gt; becomes a length byte followed by the characters in that label, and the whole name ends with a &lt;code&gt;0&lt;/code&gt; byte:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com
  -&amp;gt;  07 'e''x''a''m''p''l''e'   03 'c''o''m'   00
      ^^                          ^^            ^^
      7-char label                3-char label  root (end of name)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A label’s length byte can only be 0-63, because the top two bits are reserved for &lt;em&gt;compression pointers&lt;/em&gt; (§5). &lt;a href="https://github.com/ecekaptan/dig-from-scratch/blob/main/dig.c" rel="noopener noreferrer"&gt;&lt;code&gt;encode_name&lt;/code&gt;&lt;/a&gt; builds this format and rejects malformed input like &lt;code&gt;a..b&lt;/code&gt; (an empty label), while still accepting a trailing dot like &lt;code&gt;example.com.&lt;/code&gt;: the root that dot represents is just the final &lt;code&gt;00&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Resource records, the shape every answer shares
&lt;/h2&gt;

&lt;p&gt;The Answer, Authority, and Additional sections are all made of &lt;strong&gt;resource records (RRs)&lt;/strong&gt;, and every RR has the same six-field layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME      (variable)   which name this record is about
TYPE      (2 bytes)    A, AAAA, CNAME, ...
CLASS     (2 bytes)    1 = IN
TTL       (4 bytes)    how many seconds it may be cached
RDLENGTH  (2 bytes)    how many bytes of RDATA follow
RDATA     (RDLENGTH)   the payload, whose shape depends on TYPE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frame stays the same; only the &lt;strong&gt;RDATA&lt;/strong&gt; changes from one record type to another. That is the key idea: parse the frame the same way every time, then switch on the TYPE to interpret the payload. &lt;a href="https://github.com/ecekaptan/dig-from-scratch/blob/main/dig.c" rel="noopener noreferrer"&gt;&lt;code&gt;print_answers&lt;/code&gt;&lt;/a&gt; does exactly this: read the frame, then check &lt;code&gt;if (type == ...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RDLENGTH&lt;/code&gt; is what allows a parser to skip record types it does not understand. Even if you cannot decode the value, you still know it occupies exactly &lt;code&gt;RDLENGTH&lt;/code&gt; bytes, so you can jump to the next record safely.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Response types: what RDATA looks like for each one
&lt;/h2&gt;

&lt;p&gt;This is the other half of the story that is easy to gloss over. Here is the actual on-the-wire RDATA for every type this tool decodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  A, IPv4 address (TYPE 1)
&lt;/h3&gt;

&lt;p&gt;RDATA is &lt;strong&gt;exactly 4 raw bytes&lt;/strong&gt;, one per octet. No text at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RDATA = 5D B8 D8 22   -&amp;gt;   93.184.216.34
        93 184 216 34
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code checks &lt;code&gt;rdlength == 4&lt;/code&gt; and hands the 4 bytes to &lt;code&gt;inet_ntop&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  AAAA, IPv6 address (TYPE 28)
&lt;/h3&gt;

&lt;p&gt;Same idea, &lt;strong&gt;16 raw bytes&lt;/strong&gt;. &lt;code&gt;inet_ntop&lt;/code&gt; turns them into the &lt;code&gt;2a00:1450:...&lt;/code&gt;&lt;br&gt;
colon form.&lt;/p&gt;
&lt;h3&gt;
  
  
  CNAME / NS / PTR, a name (TYPES 5, 2, 12)
&lt;/h3&gt;

&lt;p&gt;RDATA is a single &lt;strong&gt;encoded name&lt;/strong&gt; (same label format as QNAME, and it may use&lt;br&gt;
compression). They differ only in meaning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CNAME&lt;/strong&gt;: "this name is an alias; the real records live under &lt;em&gt;that&lt;/em&gt; name."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NS&lt;/strong&gt;: "&lt;em&gt;that&lt;/em&gt; server is authoritative for this zone."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PTR&lt;/strong&gt;: a reverse lookup, where an IP's &lt;code&gt;x.x.x.x.in-addr.arpa&lt;/code&gt; name points
back to a hostname.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three decode by just calling &lt;a href="https://github.com/ecekaptan/dig-from-scratch/blob/main/dig.c" rel="noopener noreferrer"&gt;&lt;code&gt;read_name&lt;/code&gt;&lt;/a&gt; on the RDATA.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dig 8.8.8.8.in-addr.arpa PTR
&lt;span class="go"&gt;8.8.8.8.in-addr.arpa  18634  PTR  dns.google
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  MX, mail exchanger (TYPE 15)
&lt;/h3&gt;

&lt;p&gt;RDATA = a &lt;strong&gt;2-byte preference&lt;/strong&gt; (lower = higher priority) followed by a &lt;strong&gt;name&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RDATA = 00 0A  &amp;lt;name: alt1.gmail-smtp-in.l.google.com&amp;gt;
        ^^^^^
        preference 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dig gmail.com MX
&lt;span class="go"&gt;gmail.com  1873  MX  5 gmail-smtp-in.l.google.com
gmail.com  1873  MX  10 alt1.gmail-smtp-in.l.google.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SOA, start of authority (TYPE 6)
&lt;/h3&gt;

&lt;p&gt;The zone's "control record." RDATA packs &lt;strong&gt;two names then five 32-bit numbers&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MNAME    name   primary name server for the zone
RNAME    name   admin email (first dot = @: dns-admin.google.com -&amp;gt; dns-admin@google.com)
SERIAL   u32    version number, bumped on every change
REFRESH  u32    how often secondaries check for updates (s)
RETRY    u32    how long to wait after a failed refresh (s)
EXPIRE   u32    give up serving the zone after this long without contact (s)
MINIMUM  u32    default negative-cache TTL (s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dig google.com SOA
&lt;span class="go"&gt;google.com  5  SOA  ns1.google.com dns-admin.google.com 967105881 900 900 1800 60
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser reads MNAME, advances by however many bytes it consumed, reads&lt;br&gt;
RNAME, then reads five &lt;code&gt;get32&lt;/code&gt;s.&lt;/p&gt;
&lt;h3&gt;
  
  
  TXT, arbitrary text (TYPE 16)
&lt;/h3&gt;

&lt;p&gt;RDATA is &lt;strong&gt;one or more length-prefixed strings&lt;/strong&gt;: a length byte, then that many&lt;br&gt;
characters, repeated until RDLENGTH runs out. (One TXT record can hold several&lt;br&gt;
chunks, which is why the parser loops.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RDATA = 22 'v''=''s''p''f''1'' '...   1B ...
        ^^                            ^^
        0x22 = next 34 chars          next string starts here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TXT records are big, which is exactly what triggers truncation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dig google.com TXT
&lt;span class="go"&gt;(UDP reply truncated, retrying over TCP)
answers: 16
google.com  26  TXT  "v=spf1 include:_spf.google.com ~all"
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Name compression
&lt;/h2&gt;

&lt;p&gt;DNS responses often contain the same names multiple times. Instead of storing&lt;br&gt;
the same name again, DNS can use a &lt;strong&gt;pointer&lt;/strong&gt; to a name that already exists&lt;br&gt;
earlier in the packet.&lt;/p&gt;
&lt;h3&gt;
  
  
  5.1 Normal name
&lt;/h3&gt;

&lt;p&gt;A name like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is encoded as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;07 example 03 com 00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each label starts with its length, and &lt;code&gt;00&lt;/code&gt; means the name is finished.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 Compression pointer
&lt;/h3&gt;

&lt;p&gt;Suppose &lt;code&gt;example.com&lt;/code&gt; already starts at byte &lt;code&gt;16&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;offset 16:
07 example 03 com 00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of writing it again, another record can contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C0 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Go to byte 16 and continue reading the name there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C0 10
  ↓
byte 16
  ↓
07 example 03 com 00
  ↓
example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pointer is therefore &lt;strong&gt;a reference to another location&lt;/strong&gt;, not the name itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3 How do we recognize a pointer?
&lt;/h3&gt;

&lt;p&gt;The first two bits tell us what the byte means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00xxxxxx → normal label
11xxxxxx → pointer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;03 = 00000011
     ^^
     00 → normal label
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C0 = 11000000
     ^^
     11 → pointer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A pointer uses two bytes. The remaining 14 bits contain the offset.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C0 10
^^
11 = pointer

remaining bits → offset 16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.4 Partial compression
&lt;/h3&gt;

&lt;p&gt;A pointer can also represent only the &lt;strong&gt;rest&lt;/strong&gt; of a name.&lt;/p&gt;

&lt;p&gt;For:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ftp.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we could write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;03 ftp C0 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;C0 10&lt;/code&gt; points to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;03 ftp + pointer
        ↓
        example.com

= ftp.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.5 The important part: &lt;code&gt;cursor&lt;/code&gt; vs &lt;code&gt;consumed&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the tricky part.&lt;/p&gt;

&lt;p&gt;Suppose an answer contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C0 10 | 00 01 | ...
^^^^^   ^^^^^
 NAME    TYPE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The NAME physically occupies only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C0 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;so it occupies &lt;strong&gt;2 bytes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But the parser follows the pointer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C0 10
  ↓
byte 16
  ↓
07 example 03 com 00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser reads those bytes to &lt;strong&gt;discover the name&lt;/strong&gt;, but those bytes are &lt;strong&gt;not physically part of the NAME field in this record&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's why we track two things:&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;cursor&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Where are we currently reading?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C0 10
  ↓
jump to byte 16
  ↓
read example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;cursor&lt;/code&gt; can jump.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;consumed&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;How many bytes did the name occupy &lt;strong&gt;in the current record&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;For:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C0 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the answer is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;consumed = 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though we read more bytes after following the pointer.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.6 Why the code freezes &lt;code&gt;consumed&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;jumped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;consumed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;jumped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the first pointer is encountered, we calculate how many bytes the name occupied &lt;strong&gt;before the jump&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;03 ftp C0 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the name occupies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 + 3 + 2 = 6 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;consumed = 6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After jumping to &lt;code&gt;example.com&lt;/code&gt;, we &lt;strong&gt;do not increase &lt;code&gt;consumed&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is essential because the next field (&lt;code&gt;TYPE&lt;/code&gt;) comes immediately after the pointer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;03 ftp C0 10 | 00 01
^^^^^^^^^^^^   ^^^^^
 NAME           TYPE
  6 bytes       2 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.7 Mental model
&lt;/h3&gt;

&lt;p&gt;Think of a pointer as a book reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Current page:

NAME: "See page 16"
TYPE: A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You go to page 16 and read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the NAME on the original page still occupies only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"See page 16"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C0 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;occupies 2 bytes&lt;/strong&gt;, but tells us to go somewhere else to discover the actual name.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;cursor&lt;/code&gt; = where I'm reading.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;&lt;code&gt;consumed&lt;/code&gt; = how many bytes this field occupies here.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the key idea behind DNS compression.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.8 The safety guard
&lt;/h3&gt;

&lt;p&gt;A corrupt or hostile packet could point in a &lt;strong&gt;circle&lt;/strong&gt; (offset A holds a pointer to B, and B points back to A), or even point to itself. A naive parser would follow those forever and hang. &lt;a href="https://github.com/ecekaptan/dig-from-scratch/blob/main/dig.c" rel="noopener noreferrer"&gt;&lt;code&gt;read_name&lt;/code&gt;&lt;/a&gt; counts those jumps and gives up after 20, and it refuses any offset that lands outside the packet. The result is bounded work and no infinite loops, even on a malicious response.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Reading a whole packet at once
&lt;/h2&gt;

&lt;p&gt;Putting it all together, here is a complete &lt;code&gt;example.com A&lt;/code&gt; exchange, annotated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The query we send&lt;/strong&gt; (&lt;code&gt;dig example.com&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1A 2B                                ID = 0x1A2B (random)
01 00                                FLAGS: RD=1
00 01                                QDCOUNT = 1
00 00  00 00  00 00                  AN/NS/AR = 0
07 65 78 61 6D 70 6C 65              label "example"
03 63 6F 6D                          label "com"
00                                   end of name
00 01                                QTYPE = A
00 01                                QCLASS = IN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The response&lt;/strong&gt; (same header offsets, followed by the answer):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1A 2B                                ID echoed back
81 80                                FLAGS: QR=1 RD=1 RA=1, RCODE=0 (NOERROR)
00 01                                QDCOUNT = 1
00 01                                ANCOUNT = 1   (one answer)
00 00  00 00                         NS/AR = 0
07 65 ... 03 63 6F 6D 00  00 01 00 01   the question, echoed verbatim
=== answer starts here ===
C0 0C                                NAME = pointer to offset 12 (the question name)
00 01                                TYPE = A
00 01                                CLASS = IN
00 00 00 3C                          TTL = 60 seconds
00 04                                RDLENGTH = 4
5D B8 D8 22                          RDATA = 93.184.216.34
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read from top to bottom, and that is the full round trip: your ID comes back, the flags say “response, no error,” the count says there is one answer, and the answer’s 4 RDATA bytes are the address. Nothing magical is hiding in the bytes; it is just a packet with a known layout.&lt;/p&gt;

&lt;h3&gt;
  
  
  One small footnote: why the code never does &lt;code&gt;*(uint16_t *)p&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;It is tempting to read a 2-byte field as &lt;code&gt;ntohs(*(uint16_t *)(a + 8))&lt;/code&gt;. But &lt;code&gt;a + 8&lt;/code&gt; can land on an &lt;strong&gt;unaligned address&lt;/strong&gt;, and reading a &lt;code&gt;uint16_t&lt;/code&gt; from an unaligned address is undefined behavior in C. It may work on x86 or ARM, but it can crash on stricter CPUs, and the compiler is allowed to assume it never happens. That is why every field goes through &lt;code&gt;get16&lt;/code&gt; and &lt;code&gt;get32&lt;/code&gt;, which copy the bytes with &lt;code&gt;memcpy&lt;/code&gt; and then fix the endianness. A small helper, but it prevents a whole class of “works on my machine” bugs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Watch a real packet:&lt;/strong&gt; run &lt;code&gt;dig example.com&lt;/code&gt; in one terminal while &lt;code&gt;sudo tcpdump -X -n port 53&lt;/code&gt; runs in another, and match the hex to §6.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a record type:&lt;/strong&gt; &lt;code&gt;SRV&lt;/code&gt; (priority, weight, port, then a name) and &lt;code&gt;CAA&lt;/code&gt; are great next examples once the frame-parsing foundation is in place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The sections we ignore:&lt;/strong&gt; this tool only prints the Answer section. If you print the Authority and Additional sections too, you will see the NS records and glue records that make recursion work.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>c</category>
      <category>dns</category>
      <category>networking</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
