Networking Fundamentals · beginner · ~12 min

DNS: how names become addresses

- Trace a full DNS resolution step by step, from your resolver to the authoritative server. - Name and use the record types that matter most (A, AAAA, CNAME, MX, NS, TXT, PTR) and explain what each reveals. - Explain how TTL and caching change what you see and how fresh it is. - Read a DNS answer on the wire: how a domain name is encoded as length-prefixed labels. - Use `dig`, `nslookup`, and `host` to query specific record types and read their output. - Apply DNS as a source of passive reconnaissance while staying defensive and lab-only.

Overview

Almost everything you type into a browser, an email client, or a network tool is a name: example.com, mail.google.com, api.github.com. But computers route packets using IP addresses, not names. The Domain Name System (DNS) is the service that turns a human-friendly name into the address the network can actually use.

Think of DNS as the phone book of the internet — except no single book holds every number. Instead, the data is spread across millions of servers arranged in a tree. To answer one question, your computer walks down that tree until it reaches the server that is authoritative (the official source) for the name you asked about.

This builds directly on IP addresses: IPv4, IPv6, public vs private. There you learned what an address like 93.184.216.34 or 2606:2800:220:1:248:1893:25c8:1946 looks like and how it identifies a host. DNS is the layer that finds those addresses for you. Once you understand that names and addresses are two different things linked by DNS, a lot of networking stops feeling like magic.

In plain language first: you ask a helper (a resolver) for a name, it asks a chain of servers on your behalf, and it hands back an address plus an expiry time. In proper terminology: a stub resolver sends a recursive query to a recursive resolver, which performs iterative queries against the root, TLD, and authoritative name servers, then returns a resource record with a TTL.

Why it matters

DNS matters because it sits in front of nearly every network action. If DNS is slow, the whole internet feels slow. If DNS is wrong or poisoned, users get sent to the wrong server even though the address bar looks correct. Understanding it helps you debug "the site is down for me but not for you" problems, which are very often DNS or caching issues rather than the server itself.

For anyone learning defensive security, DNS is one of the richest sources of information about how an organization is built. Subdomain enumeration and DNS record gathering are core parts of passive reconnaissance — building a map of a target's public attack surface. The DNS records for a domain quietly announce its mail servers, name servers, hosted services, and even hints about the technologies and cloud providers in use. Much of this can be collected from public infrastructure and logs without ever sending a packet to the organization's own machines.

On the defensive side, the same knowledge tells you what your own organization is leaking. Every record you publish is public. Knowing how attackers read DNS helps you decide what to expose, what to keep on internal-only resolvers, and how to monitor for suspicious lookups.

Core concepts

1. The DNS hierarchy (root -> TLD -> authoritative)

Definition. DNS is a distributed, hierarchical database. The namespace is a tree that is read right-to-left: www.example.com. belongs to the com. zone, which belongs to the root (.).

Plain-language explanation. No single server knows every name. Instead, servers know who to ask next. The root knows where each top-level domain (TLD) lives. The TLD server for .com knows who runs example.com. That final server — the authoritative one — actually holds the answer.

How it works internally. Your resolver starts at the top and follows referrals downward:

        Your computer (stub resolver)
                 |  "What is www.example.com?"
                 v
        Recursive resolver  (e.g. 8.8.8.8 or your ISP)
           |   |   |
   (1) ask root: "where is .com?"        -> "ask the .com TLD servers"
   (2) ask .com TLD: "where is example.com?" -> "ask ns1.example.com"
   (3) ask example.com authoritative:
           "what is www.example.com A?"   -> "93.184.216.34"
                 |
                 v
        Answer returned + cached along the way

When it matters / when it does not. You rely on the hierarchy every time a name is not already cached. For a name you looked up seconds ago, the resolver skips the whole walk and answers from cache.

Common pitfall. Assuming your computer talks to the authoritative server directly. It almost never does — a recursive resolver does the walking, and the cached copy you receive may be slightly out of date.

Knowledge check: In mail.corp.example.co.uk., which part is closest to the root of the tree, and which server is authoritative for the full name?

2. Recursive vs iterative resolution

Definition. A recursive query says "do all the work and give me the final answer." An iterative query says "give me your best answer or a referral to who to ask next."

Plain-language explanation. Your laptop is lazy — it sends one recursive query to its resolver and waits for a complete answer. The resolver is the busy one: it sends a series of iterative queries up and down the hierarchy until it has the address.

Recursive query Iterative query
Who sends it Stub resolver (your OS) Recursive resolver
Expected reply The final answer Answer or a referral
Work done by asker Almost none All of the walking
Typical target Your configured resolver Root, TLD, authoritative

When to use which. You do not choose — the roles are fixed by design. But knowing the split explains why one server (the resolver) is a caching bottleneck and a security-sensitive component.

Common pitfall. Confusing the resolver's IP with the authoritative server's IP when debugging. The resolver you set in your network config is not the source of truth for any domain.

3. Resource records and record types

Definition. A resource record (RR) is one entry in DNS: a name, a type, a TTL, and data. The type decides what the data means.

Plain-language explanation. One domain has many records. The address lives in an A record, the mail routing in an MX record, and so on. When you query, you ask for a specific type.

Type Holds What it reveals in recon
A IPv4 address Where a host actually lives
AAAA IPv6 address IPv6-reachable hosts (often forgotten by defenders)
CNAME Alias to another name Third-party/cloud services (e.g. a CDN or SaaS host)
MX Mail server + priority Email provider, on-prem vs hosted mail
NS Authoritative name servers DNS provider, delegation structure
TXT Free text SPF/DKIM, domain-verification tokens, tech hints
PTR IP -> name (reverse) Naming conventions, ownership of an IP block

How it works internally. Each record carries a TTL and a class (almost always IN for internet). A CNAME is special: it says "this name is really that name," so the resolver restarts the lookup at the target.

When to use which. Ask for the exact type you need. A/AAAA for connecting, MX for mail, NS for delegation, TXT for verification and policy records.

Common pitfall. Expecting a name to have both a CNAME and other records at the same level. By the rules of DNS, a name with a CNAME should not also carry an A or MX at that exact name — everything must follow the alias.

Knowledge check: You run a mail lookup and get back a name, not an IP. Which record type did you query, and what would you look up next to actually connect?

4. TTL and caching

Definition. TTL (time to live) is a number of seconds attached to every record telling resolvers how long they may cache the answer before asking again.

Plain-language explanation. A low TTL (say 60) means changes propagate fast but there is more query traffic. A high TTL (say 86400 = one day) means fewer lookups but stale answers linger.

TTL countdown in a resolver cache:

  example.com A 93.184.216.34   TTL 300
   t=0s   -> answer cached, TTL 300
   t=120s -> served from cache, remaining TTL ~180
   t=305s -> expired, resolver walks the hierarchy again

When it matters. During a migration you lower TTLs beforehand so the switch is quick. In recon, a low TTL can hint at load balancing or an active service; a stale cache can make you see an old address.

Common pitfall. Believing a DNS change is "live" instantly. Until the old TTL expires in every cache, some users still see the old record.

Knowledge check: A record has TTL 3600. You change its address. Roughly how long might some users keep hitting the old address, and why?

5. How a name is encoded on the wire

Definition. In a DNS packet a domain name is not stored as www.example.com. It is stored as a sequence of length-prefixed labels ending in a single zero byte.

Plain-language explanation. Each label (the part between dots) is written as one length byte followed by that many characters. A 0x00 marks the end of the name.

Encoding of www.example.com in a DNS message:

  03 'w' 'w' 'w'  07 'e' 'x' 'a' 'm' 'p' 'l' 'e'  03 'c' 'o' 'm'  00
  |   3 bytes |   |       7 bytes            |   |  3 bytes  |   end

  length=3    length=7                        length=3      terminator

How it works internally. A parser reads one length byte, copies that many bytes as a label, inserts a ., and repeats until it reads a length of 0. Each label is at most 63 bytes (because the two high bits of the length byte are reserved for message compression pointers).

When it matters. You need this to decode DNS traffic by hand, which is exactly what the related exercises ask for. It is also where a lot of parser bugs and vulnerabilities live.

Common pitfall. Forgetting the maximum label length or trusting the length byte blindly. A malicious length that runs past the end of the buffer is a classic way to crash or exploit a naive parser.

Knowledge check: Given the bytes 02 'g' 'o' 03 'd' 'e' 'v' 00, what dotted name do they decode to?

Syntax notes

There is no C "syntax" for DNS itself — it is a protocol — but two things have a precise shape worth memorizing.

1. Querying with command-line tools:

# dig <name> <TYPE>            ask for a specific record type
dig example.com A
dig example.com MX
dig +short example.com A       # just the answer, no ceremony
dig @1.1.1.1 example.com A     # ask a specific resolver

# host and nslookup do similar jobs, simpler output
host -t MX example.com
nslookup -type=TXT example.com

2. The wire format of a name (length-prefixed labels):

[len1][label1 bytes...][len2][label2 bytes...] ... [0x00]
  |         |                                        |
  |         the ASCII characters of one label        end-of-name marker
  one length byte (0..63) for the label that follows

Rules to keep in mind: each label is 1..63 bytes; the total encoded name is at most 255 bytes; the name always ends with a single 0x00; and a length byte whose top two bits are 11 is a compression pointer, not a length (the exercises stick to the no-pointer case).

Lesson

What DNS is

DNS (Domain Name System) translates names like example.com into IP addresses. It is a distributed, hierarchical database — no single server holds all the answers.

How a lookup resolves

  1. Your resolver asks a root server: "where is .com?"
  2. The TLD server answers: "ask example.com's authoritative server."
  3. The authoritative server returns the A record (the IP address).

Each answer is cached along the way for the length of its TTL, so repeat lookups are faster.

Record types you'll meet

Type Holds
A / AAAA IPv4 / IPv6 address
CNAME Alias to another name
MX Mail server
NS Authoritative name servers
TXT Free text (SPF, domain verification)
PTR Reverse: IP -> name

DNS in recon

DNS is a goldmine for passive reconnaissance — gathering information without touching the target's own servers.

With DNS you can:

  • Enumerate subdomains.
  • Find mail and name servers.
  • Read TXT records for technology hints.

Useful tools are dig, nslookup, and host.

Certificate Transparency logs and dig any can reveal much of a target's attack surface — all without sending a single packet to the target's own servers.

Code examples

Here is a small, complete C11 program that decodes a DNS-wire name (a sequence of length-prefixed labels, no compression pointers) into a dotted string — the exact skill the related exercises build on. It validates every length against the buffer bounds so a malformed packet cannot walk off the end.

#include <stdio.h>
#include <string.h>
#include <stddef.h>

/*
 * Decode a DNS-wire name from `in` (length `in_len`) into `out`.
 * Format: [len][bytes...][len][bytes...]...[0x00], no compression.
 * Returns the number of bytes consumed from `in` on success,
 * or -1 on any malformed / out-of-bounds input.
 */
static int decode_dns_name(const unsigned char *in, size_t in_len,
                           char *out, size_t out_cap)
{
    size_t pos = 0;      /* read cursor into `in`  */
    size_t w = 0;        /* write cursor into `out` */

    if (in_len == 0) return -1;              /* need at least the 0x00 */

    while (pos < in_len) {
        unsigned char len = in[pos++];       /* read one length byte   */

        if (len == 0) {                      /* terminator: name done  */
            out[w] = '\0';
            return (int)pos;                 /* bytes consumed          */
        }
        if (len > 63) return -1;             /* label too long / pointer */
        if (pos + len > in_len) return -1;   /* would read past buffer   */
        if (w != 0) {                        /* dot between labels       */
            if (w + 1 >= out_cap) return -1;
            out[w++] = '.';
        }
        if (w + len >= out_cap) return -1;   /* leave room for '\0'      */
        memcpy(out + w, in + pos, len);      /* copy the label bytes     */
        w += len;
        pos += len;
    }
    return -1;                               /* ran out before 0x00      */
}

int main(void)
{
    /* Wire bytes for "www.example.com" */
    const unsigned char packet[] = {
        3, 'w','w','w',
        7, 'e','x','a','m','p','l','e',
        3, 'c','o','m',
        0
    };
    char name[256];

    int used = decode_dns_name(packet, sizeof packet, name, sizeof name);
    if (used < 0) {
        fprintf(stderr, "decode failed: malformed DNS name\n");
        return 1;
    }
    printf("name    = %s\n", name);
    printf("consumed = %d bytes\n", used);
    return 0;
}

What it does. It reads the packet one label at a time, guarding every length against the buffer size, and joins the labels with dots. When it reads the terminating 0x00, it null-terminates the output and returns how many bytes it consumed (useful because a real DNS message has more fields right after the name).

Expected output:

name    = www.example.com
consumed = 17 bytes

Edge cases to notice. A label length greater than 63 is rejected (it would actually be a compression pointer in a real packet). A length that points past the end of the buffer is rejected instead of causing an over-read. A buffer with no terminating 0x00 returns -1 rather than looping forever. An output buffer too small to hold the name also returns -1.

Line by line

Walkthrough of the key example, decode_dns_name, using the input for www.example.com.

Step Code State after it runs
Init pos = 0; w = 0; Read and write cursors at the start
Guard if (in_len == 0) return -1; Empty input rejected early
Read len len = in[pos++] -> 3 pos = 1; next label is 3 bytes
Bounds len > 63? no; pos+len > in_len? no Safe to copy 3 bytes
Dot? w == 0, skip dot No leading dot
Copy memcpy(out, in+1, 3) out = "www", w = 3, pos = 4
Read len len = in[4] -> 7 pos = 5; next label is 7 bytes
Dot? w != 0, write . out = "www.", w = 4
Copy memcpy(out+4, in+5, 7) out = "www.example", w = 11, pos = 12
Read len len = in[12] -> 3 pos = 13
Dot + copy write . then 3 bytes out = "www.example.com", w = 15, pos = 16
Read len len = in[16] -> 0 Terminator reached
Finish out[15] = '\0'; return 16 (pos is 17 after pos++) Returns bytes consumed

Key ideas the trace makes concrete:

  • The length byte drives everything — the parser never guesses where a label ends; the byte before it says exactly how long it is.
  • The dot is inserted between labels, never before the first one, which is why the w != 0 check exists.
  • Every copy is checked against both buffers first, so no read or write can escape its array.
  • The function returns the number of bytes consumed, so a caller parsing a whole DNS message knows where the name ends and the next field begins.

Common mistakes

Mistake 1: Trusting the length byte without bounds-checking.

/* WRONG: copies `len` bytes with no check that they exist */
unsigned char len = in[pos++];
memcpy(out + w, in + pos, len);   /* len could run past the buffer! */

Why it is wrong: a malformed or malicious packet can set len so the copy reads past the end of in (an out-of-bounds read) or overflows out. This is a real class of DNS parser vulnerability. Fix: check pos + len > in_len and w + len >= out_cap before copying, as the lesson code does. Recognize it by fuzzing with truncated packets — a correct parser returns an error, a broken one crashes.

Mistake 2: Forgetting the terminating zero byte.

Learners write the loop as "while there are bytes left" and never handle a missing 0x00. On a truncated packet the loop keeps reading garbage. Fix: treat "ran off the end before seeing 0x00" as an error (return -1), and treat len == 0 as the one and only success exit.

Mistake 3: Confusing the resolver with the authoritative source.

# WRONG assumption: "dig said 1.2.3.4, so that IS the record"
dig example.com A          # may be a *cached* answer from your resolver

Why it is wrong: your resolver may serve a stale cached copy. Fix: query the authoritative server directly and compare:

dig NS example.com                 # find the authoritative servers
dig @ns1.example.com example.com A # ask one of them, bypassing cache

Mistake 4: Expecting instant DNS changes. Editing a record and reloading immediately, then panicking that nothing changed. The old value lives in caches until its TTL expires. Fix: lower the TTL before the change, wait out the old TTL, then switch.

Mistake 5: Assuming a name has only one record type. A host can have A, AAAA, MX, and TXT all at once; a CNAME name should not have a sibling A. Query the exact type you mean instead of assuming dig name (default A) tells the whole story.

Debugging tips

Compiler errors (for the C example):

  • implicit declaration of 'memcpy' — you forgot #include <string.h>.
  • comparison of integer expressions of different signedness — mixing int and size_t; keep cursors as size_t and cast only when returning.

Runtime errors:

  • A segfault while parsing almost always means a missing bounds check — an index ran past the buffer. Run under valgrind ./a.out or compile with -fsanitize=address to see the exact out-of-bounds access.
  • Garbage in the output string usually means you forgot the '\0' terminator or wrote past out_cap.

Logic errors in DNS itself:

  • "It resolves for me but not my colleague" — suspect caching or TTL. Compare dig +short name on both machines and check which resolver each uses (dig prints SERVER: in its output).
  • "NXDOMAIN" means the name does not exist at all; "SERVFAIL" means the resolver could not complete the lookup (often DNSSEC or a broken authoritative server) — they are different failures.
  • Empty answer but no error — you probably queried the wrong record type. Try the specific type (dig name AAAA, dig name MX).

Questions to ask when it does not work:

  1. Which resolver am I actually using, and is it giving me a cached answer?
  2. What does the authoritative server say (dig @<ns> name TYPE)?
  3. Am I asking for the right record type?
  4. Has the old TTL expired everywhere yet?
  5. For a parser: does every length come from the packet, and did I bound-check it before using it?

Memory safety

DNS parsing is a classic source of memory-safety bugs, so this topic is worth taking seriously even though the track is conceptual.

Bounds. Every length byte in a DNS name comes from untrusted network data. Never copy len bytes without first checking pos + len <= in_len (source) and w + len < out_cap (destination). Both an over-read (leaking adjacent memory) and an over-write (corrupting the stack/heap) are possible if you skip these.

Termination and loops. A parser must have a guaranteed exit: reaching 0x00, or running out of input. Real DNS also has compression pointers (a length byte with the top two bits set points backward into the message). A naive pointer-follower can be sent into an infinite loop by a packet whose pointer references itself or an earlier pointer — a denial-of-service. Safe parsers cap the number of pointer jumps and never allow a pointer to move forward. The lesson exercises deliberately exclude compression so you can master the base case first.

Initialization. Always null-terminate the output before using it as a C string; an uninitialized out printed with %s reads until it randomly finds a zero byte — undefined behaviour.

Overflow. Enforce the protocol limits: label <= 63 bytes, total name <= 255 bytes. Rejecting oversized values early prevents both integer surprises and buffer growth attacks.

Security / defensive practices (this topic is recon-adjacent):

  • Treat DNS reconnaissance as passive information gathering — practice only against domains you own or lab targets, never live third-party infrastructure without written authorization.
  • Defensively, remember that every published record is public. Do not put secrets in TXT records, and keep internal-only names on split-horizon / internal resolvers rather than public DNS.
  • Validate and rate-limit DNS input in any service you write; use well-tested libraries (getaddrinfo, libresolv, c-ares) for production rather than hand-rolled parsers, which are the safe API choice here.
  • Prefer least privilege: a program that only needs to resolve names does not need raw-socket access to craft packets.

Real-world uses

Concrete real-world use case. Content delivery networks (CDNs) like Cloudflare and Akamai lean entirely on DNS. When you request assets.example.com, a low-TTL DNS answer steers you to the nearest edge server, often via a CNAME to the CDN's own domain. The same mechanism powers global load balancing, blue/green deployments, and failover — all by changing which address a name resolves to. Email delivery depends on MX records; domain ownership verification for cloud services depends on TXT records; and reverse PTR records are checked by mail servers to fight spam.

In security work. Defenders build asset inventories from their own DNS zones and monitor resolver logs for signs of malware calling home (DNS is a common covert channel). Certificate Transparency logs plus DNS enumeration reveal forgotten subdomains that need patching — the same information an attacker would collect during passive recon, used to shrink the attack surface first.

Beginner best-practice habits:

  • Query the specific record type you need instead of relying on the default.
  • Use dig +short for clean, scriptable output and read the SERVER: line to know who answered.
  • Always confirm surprising answers against the authoritative server before acting.

Advanced best-practice habits:

  • Manage TTLs deliberately: lower them before planned changes, raise them for stable, high-traffic records.
  • Use split-horizon DNS to keep internal names off public resolvers.
  • In code, use vetted resolver libraries, validate every field from the wire, and add fuzz tests for your parsers.
  • Monitor for anomalous query volume and unusual record types as an early breach signal.

Practice tasks

Beginner 1 — Read the record types. Using dig (or a captured sample your instructor provides), look up the A, AAAA, and MX records for a domain you own or a lab domain. Write one sentence per record explaining what it tells you. Requirements: show the exact command and the answer for each. Concepts: record types, dig usage. Hint: dig example.com MX +short.

Beginner 2 — Trace the hierarchy. On paper or in a text file, draw the resolution path for shop.example.co.uk from your resolver through root, TLD, and authoritative servers. Requirements: label which query is recursive and which are iterative, and mark where caching could short-circuit the walk. Concepts: hierarchy, recursive vs iterative, caching. Hint: read the name right-to-left.

Intermediate 1 — Decode a name by hand, then in code. Given the bytes 03 'd' 'e' 'v' 04 'm' 'a' 'i' 'l' 00, first decode them on paper to a dotted string, then write a short function that reproduces your answer. Input: the byte array above. Expected output: dev.mail. Requirements: insert dots only between labels; stop at 0x00. Concepts: wire format, length-prefixed labels.

Intermediate 2 — Make the parser safe. Extend your decoder to reject: a label length above 63, a length that runs past the end of the buffer, and input with no terminating 0x00. Requirements: return an error value (e.g. -1) for each bad case instead of crashing; add at least three test inputs, one per failure. Constraints: no reads or writes outside either buffer. Concepts: bounds checking, memory safety. Hint: check before every memcpy.

Challenge — TTL and cache reasoning. Without touching production, write a short explanation (and, optionally, a simulation in code) of what happens to users over time when you change an A record that has a TTL of 3600. Requirements: describe the timeline for a user whose cache is fresh vs one whose cache just expired, explain why some users see the old address for up to an hour, and recommend a TTL strategy for a planned migration. Concepts: TTL, caching, propagation. Hint: the worst-case stale window equals the old TTL, not the new one.

Summary

  • DNS turns names into IP addresses using a distributed, hierarchical database resolved through a root -> TLD -> authoritative chain, connecting the names you type to the addresses you learned about in IP addresses: IPv4, IPv6, public vs private.
  • Your computer sends one recursive query to a resolver; the resolver does the iterative walking and returns a cached copy of the answer.
  • Record types each expose a different facet of a domain: A/AAAA (addresses), CNAME (aliases), MX (mail), NS (delegation), TXT (verification/policy), PTR (reverse). Query the exact type you need.
  • TTL controls caching: changes are not instant, and stale answers linger until the old TTL expires. Lower TTLs before planned changes.
  • On the wire, a name is length-prefixed labels ending in 0x00 — and parsing it safely means bounds-checking every length, which is the heart of the related exercises.
  • Most important syntax to remember: dig <name> <TYPE>, dig @server, and the wire pattern [len][bytes...]...[0x00].
  • Common mistakes: trusting a length byte without bounds-checking, forgetting the terminator, confusing the resolver with the authoritative source, and expecting instant propagation.
  • DNS is a top source for passive reconnaissance — powerful, and to be practiced only on domains you own or authorized labs, always defensively.

Practice with these exercises