Networking Fundamentals · beginner · ~12 min
- 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.
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.
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.
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?
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.
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?
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?
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?
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).
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.
.com?"example.com's authoritative server."A record (the IP address).Each answer is cached along the way for the length of its TTL, so repeat lookups are faster.
| 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 is a goldmine for passive reconnaissance — gathering information without touching the target's own servers.
With DNS you can:
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.
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.
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:
w != 0 check exists.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.
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:
valgrind ./a.out or compile with -fsanitize=address to see the exact out-of-bounds access.'\0' terminator or wrote past out_cap.Logic errors in DNS itself:
dig +short name on both machines and check which resolver each uses (dig prints SERVER: in its output).dig name AAAA, dig name MX).Questions to ask when it does not work:
dig @<ns> name TYPE)?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):
getaddrinfo, libresolv, c-ares) for production rather than hand-rolled parsers, which are the safe API choice here.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:
dig +short for clean, scriptable output and read the SERVER: line to know who answered.Advanced best-practice habits:
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.
0x00 — and parsing it safely means bounds-checking every length, which is the heart of the related exercises.dig <name> <TYPE>, dig @server, and the wire pattern [len][bytes...]...[0x00].