Follow One Packet
Pick a source, a destination, a protocol and a port, and follow a single packet from send() through the socket, transport, IP and link layers, out of the NIC, through a NAT router and every hop after it, watching which headers exist, which fields change and which stay fixed — and what a router does in the microseconds it holds the packet.
The problem
Choose the packet, then follow it
The interactive asks for four things: a client address (say 192.168.1.42 behind a home router), a server address (203.0.113.10), a protocol (TCP or UDP) and a destination port (443). Everything that follows is determined by those choices plus the state of the devices on the way. This lesson is the narration: at each stop, which headers exist, which fields change, and what the device is doing. The model is conceptual — a real path has more hops, and the numbers are illustrative — but every field behaviour described here is what happens on a real network.
The layers that build the packet are the subject of Encapsulation: Data, Segment, Packet, Frame; the Packet Inspector: Read a Frame Field by Field shows the bytes. Here the focus is movement: the same packet, re-examined at every hop.
- Application`send(fd, buf, len)` — bytes, no headers yet↓
- Socketthe descriptor maps to a kernel socket with a 4-tuple: 192.168.1.42:51742 → 203.0.113.10:443↓
- TransportTCP header: src/dst port, seq, ack, flags, window, checksum — ~20–32 bytes↓
- IPIP header: src 192.168.1.42, dst 203.0.113.10, TTL 64, protocol 6, checksum — 20 bytes↓
- Linkrouting table says via 192.168.1.1; ARP gives its MAC; Ethernet frame: dst MAC = router, src MAC = laptop↓
- NICserialises the frame onto the wire or the air; may compute checksums in hardware↓
- Home router (NAT)rewrites src to 198.51.100.77:40012, records the mapping, TTL 64 → 63, new MACs toward the ISP↓
- ISP routerlongest-prefix match on 203.0.113.10; TTL 63 → 62; new src/dst MACs; queue on the egress port↓
- Next hop … × nevery hop: lookup, decrement, checksum, re-frame, queue; IP addresses and TCP header untouched↓
- DestinationNIC → kernel → TCP finds the listening socket → data lands in the receive buffer → `recv()` returns
What changes and what does not
The packet is three nested envelopes. The link-layer frame is rewritten completely at every hop: the source MAC becomes the forwarding router’s egress interface and the destination MAC becomes the next hop’s, because MACs only mean something within one link (MAC Addresses: Identity for One Hop). The IP header is mostly preserved end to end — the destination address never changes, and the source changes only at a NAT — but TTL decrements by one per hop and the IPv4 header checksum is recomputed each time because TTL changed (IPv6 has no header checksum, one reason it forwards cheaper). The TCP or UDP header and payload are untouched by ordinary routers; the only middlebox that edits them is NAT, which must rewrite the source port and the transport checksum because the checksum covers a pseudo-header containing the IP addresses.
So at a router in the middle of the internet, the packet still says "from 198.51.100.77:40012 to 203.0.113.10:443, TCP seq 3948211", exactly as the NAT sent it; only the MACs and the TTL reveal how far it has travelled. This is why the destination can reply — the addresses it sees are routable — and why the home router can deliver the reply to the right laptop: the mapping 198.51.100.77:40012 ↔ 192.168.1.42:51742 it recorded on the way out.
| Field | At the laptop | At the NAT router | At every internet router | At the server |
|---|---|---|---|---|
| Dst MAC / src MAC | router MAC / laptop MAC | rewritten: ISP next hop / router WAN | rewritten every hop | server NIC / last router |
| IP src | 192.168.1.42 | → 198.51.100.77 (NAT) | unchanged | 198.51.100.77 (the reply goes here) |
| IP dst | 203.0.113.10 | unchanged | unchanged — the lookup key | 203.0.113.10: mine, deliver up |
| TTL | 64 | 63 | 62, 61, … one less per hop | ~50; only matters if it hit 0 |
| IP header checksum | computed | recomputed (src and TTL changed) | recomputed (TTL changed) | verified |
| TCP src port | 51742 | → 40012 (NAT) | unchanged | 40012 |
| TCP dst port, seq, ack, flags | 443, seq 3948211 | unchanged | unchanged — routers do not read them | selects the socket; seq orders the bytes |
| TCP checksum | computed (pseudo-header) | recomputed (addresses/port changed) | unchanged | verified |
| Payload | bytes from send() | unchanged | unchanged | copied into the receive buffer |
What a router does in the microseconds it holds the packet
A router receives the frame on an ingress port, checks the frame’s checksum and that the destination MAC is its own, strips the frame, and looks at the IP header. It verifies the header checksum, decrements TTL (if it becomes 0: discard, send ICMP Time Exceeded — the traceroute: Discovering the Path Hop by Hop mechanism), and looks the destination address up in the forwarding table with longest-prefix match: among all prefixes that contain 203.0.113.10, the most specific wins (The Routing Table and Longest-Prefix Match; a Trie is the classic structure, and hardware routers use TCAM or compressed tries to do it in a fixed number of memory accesses). The result is an egress interface and a next-hop address. It recomputes the IP checksum, resolves the next hop’s MAC (usually already cached), builds a new frame, and places it in the egress port’s queue.
Time budget: on a 10 Gbit/s port, a 1500-byte packet takes 1.2 µs to serialise, so the lookup and rewrite have to fit in about that long to keep up with line rate — which is why they are done in hardware. Queueing is where variable latency comes from: if the egress port is busy, the packet waits, and under sustained overload the queue fills and the router drops — the loss that Congestion Control: Protecting the Network reacts to. Bufferbloat is the opposite failure: queues so deep the packet waits tens of milliseconds instead of being dropped.
What a router does not do: read ports, sequence numbers or payload (a stateless router forwards on the IP destination alone), reassemble fragments, or keep per-connection state. Anything that does those things — NAT, a stateful firewall, a load balancer — is a middlebox, and middleboxes are where most surprising packet rewrites happen.
- Per packet: verify → decrement TTL → longest-prefix lookup → recompute checksum → re-frame with new MACs → enqueue.
- ~1 µs of work at 10 Gbit/s; queueing adds 0–tens of ms depending on load.
- IPv6 drops the header checksum and forbids router fragmentation, trimming the per-hop work.
The reply, and what the lab lets you change
The server’s reply carries dst 198.51.100.77:40012 — the NAT’s public address — and travels an independently routed path back. At the home router the mapping turns it into dst 192.168.1.42:51742 and the laptop’s kernel delivers it to the socket whose 4-tuple matches. No device on the return path knows the laptop’s private address ever existed. Switch the lab to UDP and the transport header shrinks to 8 bytes with no sequence numbers, so the destination can no longer tell whether datagrams were lost or reordered — that is the UDP: Datagrams and the Contract You Choose contract. Switch to a destination in the same subnet and the router disappears: ARP resolves the server’s MAC directly and the frame is delivered by the switch with no TTL decrement at all.
This is also the "send a packet" journey seen from the packet’s point of view rather than the OS’s; together they cover both halves of the question "what happens between send() and recv()".
- Same subnet → no router, no TTL change; the switch forwards by MAC.
- Different subnet → default gateway, NAT if the source is private, longest-prefix match at every hop after.
- UDP → 8-byte header, no seq/ack; the lab shows nothing to reorder by.
Key points
- A packet is nested envelopes: the frame is rewritten every hop, the IP header is preserved except TTL (and src at NAT), the transport header and payload are untouched by routers.
- The destination IP is the lookup key at every hop and never changes; MACs are per-link and change at every hop.
- NAT rewrites source IP and port, recomputes the IP and transport checksums, and records a mapping for the reply.
- A router: verify, decrement TTL, longest-prefix match, recompute checksum, re-frame, enqueue — about a microsecond, plus queueing.
- Routers do not read ports or payload; anything that does is a middlebox and the usual source of surprises.
- The reply is routed independently and reaches the private client only because of the NAT mapping created on the way out.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why do MACs change at every hop but IPs do not?
A MAC identifies an interface on one link; it has no meaning beyond it. IP addresses are the end-to-end names. Each layer solves delivery at its own scope, and re-framing at each hop is how the link layer hands the same IP packet across a chain of links.
▸Why decrement TTL at all?
Routing loops happen during convergence. Without TTL a looping packet would circulate forever and the loop would fill with copies; TTL bounds its life to ~64 hops, and the ICMP it triggers gave us traceroute for free.
▸Why does NAT have to touch the TCP checksum?
The TCP and UDP checksums cover a pseudo-header with the IP addresses, so changing the source address invalidates them. NAT therefore rewrites layer 4 too — one reason NAT breaks protocols that embed addresses in their payload.
Follow one packet
How it fails
What the failure looks like from inside real software.
- A TTL that hits zero on a looping path during a routing convergence; the client sees ICMP Time Exceeded, or nothing.
- A NAT table that fills or expires a mapping mid-connection; replies arrive at the public IP and are dropped — see the NAT table exhaustion challenge.
- A packet larger than a link’s MTU with DF set: dropped with ICMP Fragmentation Needed, which a firewall then filters — the black hole.
- A middlebox that rewrites payload it does not understand (an ALG mangling SIP or FTP), corrupting the very thing it tried to help.
- A checksum offload misconfiguration showing "bad checksum" in a capture on the sending host — an artefact of capturing before the NIC computed it, not corruption.
Follow it through every layer
This lesson is one node of a longer journey. Zoom out, then zoom back in.