Networking for beginners. Part 6: IPv4 — header, routing and NAT
All parts of the series “Networking for beginners”In the fifth part we covered addresses and masks and got to the point where a host decides: neighbour or gateway. If it is the gateway, the packet goes off to a router, and then begins what the network layer exists for: routing, passing a packet from network to network until it ends up in the recipient's segment. Today: what the router reads in the packet while doing this, how it chooses the path and what it changes, what happens to a packet that does not fit in a frame, and how a home router hides a whole flat behind one address.
The IPv4 header
Twenty bytes without options. It is traditionally drawn in rows of 32 bits:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
┌───────┬───────┬───────────┬───┬───────────────────────────────┐
│Version│ IHL │ DSCP │ECN│ Total Length │
├───────┴───────┴───────────┴───┼─────┬─────────────────────────┤
│ Identification │Flags│ Fragment Offset │
├───────────────┬───────────────┼─────┴─────────────────────────┤
│ TTL │ Protocol │ Header Checksum │
├───────────────┴───────────────┴───────────────────────────────┤
│ Source Address │
├───────────────────────────────────────────────────────────────┤
│ Destination Address │
├───────────────────────────────────────────────────────────────┤
│ Options (if IHL > 5) │
└───────────────────────────────────────────────────────────────┘
Field by field:
- Version (4 bits) —
4. The stack uses this field to tell IPv4 from IPv6, even if the EtherType has already said the same thing. - IHL (4 bits) — the header length in 32-bit words. Usually
5, that is, 20 bytes; more only with options. - DSCP (6 bits) — the class of service: a label by which routers can serve voice traffic ahead of file downloads, if configured to. On the internet it is most often ignored and zeroed. ECN (2 bits) — a router whose queue is overflowing can, instead of dropping the packet, set a "congestion" mark here, and the receiver passes it back to the sender via TCP. It works when enabled on both sides.
- Total Length (16 bits) — the length of the whole packet including the header, up to 65,535 bytes. This is how the stack knows where the data ends: Ethernet pads with zeros up to 46 bytes, and without this field they could not be told apart from data.
- Identification, Flags, Fragment Offset — for fragmentation, see the section below. Of the flags, DF (Don't Fragment) is the important one.
- TTL (8 bits) — Time To Live. Despite the name, it is not a time but a hop counter: each router decrements it by one, and a packet whose TTL reaches zero is dropped, and an ICMP message goes back to the sender (part 8). Without it, a packet caught in a routing loop would circle forever, like a frame in an Ethernet loop. Initial values: 64 on Linux and macOS, 128 on Windows, 255 on network equipment; from the TTL of a reply you can often guess the OS and the number of routers to it.
- Protocol (8 bits) — the "what's inside" field:
1ICMP,6TCP,17UDP. - Header Checksum (16 bits) — a checksum of the header only. IP does not check the data: that is the job of TCP and UDP. Since TTL changes at every hop, every router recomputes the checksum.
- Source, Destination — the addresses from the previous part. They reach the recipient unchanged — unless NAT gets involved.
- Options — so rare that many firewalls drop packets carrying them. Historical ones: record the route, add timestamps, specify the path explicitly.
With -v, tcpdump shows these fields:
$ sudo tcpdump -i eth0 -n -v -c 1 icmp
12:00:00.000000 IP (tos 0x0, ttl 64, id 54321, offset 0, flags [DF], proto ICMP (1), length 84)
192.168.1.10 > 203.0.113.10: ICMP echo request, id 7, seq 1, length 64
TTL 64, the DF flag, an 84-byte packet: 20 of header and 64 of data.
The routing table
A router is a machine with several interfaces and forwarding enabled: a packet that arrived on one interface and is not addressed to the machine itself gets sent onwards. The "where to" decision is made using the routing table. Each row is a prefix and what to do with it:
$ ip route
default via 198.51.100.1 dev eth1
10.0.0.0/8 via 192.168.1.254 dev eth0
10.1.0.0/16 via 192.168.1.253 dev eth0
10.1.2.0/24 dev eth2 proto kernel scope link src 10.1.2.1
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.1
198.51.100.0/30 dev eth1 proto kernel scope link src 198.51.100.2
Kinds of rows:
- Connected (
dev eth0withoutvia): a network directly behind an interface; they appear by themselves when an address is assigned. The packet is handed to the recipient directly. - Via a gateway (
via): the next router, the next hop, which must be on a connected network, otherwise there is no way to reach it. - Default (
default, also0.0.0.0/0): where to send everything that did not match any row. For a home router this is the ISP; routers in the core of the internet have no default route — they have a full table of ~a million prefixes.
Where the rows come from: connected ones from addresses, static ones from the administrator's hands (ip route add), dynamic ones from routing protocols, by which routers tell each other about their networks. Inside an organisation this is most often OSPF: each router floods a description of its links, everyone assembles the same map of the network and computes the shortest paths over it. Between ISPs it is BGP: routers exchange prefixes along with the list of networks a prefix has passed through, and choose a path based on that list and policy. Both protocols get their own parts in this series — OSPF and BGP; for BGP in practice I also have a separate series. What matters for this article is that they put the very same rows into the table.
The longest prefix
One address can match several rows. A packet to 10.1.2.50 matches 10.0.0.0/8, 10.1.0.0/16, 10.1.2.0/24 and default. There is one rule: the longest prefix wins (longest prefix match). The logic is that a longer prefix is more precise knowledge: "all of 10.0.0.0/8 is over there" is a general rule, while "10.1.2.0/24 is right behind eth2" is an exception to it.
| Destination | Matching rows | Chosen | Where |
|---|---|---|---|
10.1.2.50 |
/8, /16, /24, default | 10.1.2.0/24 |
directly via eth2 |
10.1.7.1 |
/8, /16, default | 10.1.0.0/16 |
to gateway 192.168.1.253 |
10.9.9.9 |
/8, default | 10.0.0.0/8 |
to gateway 192.168.1.254 |
203.0.113.10 |
default | 0.0.0.0/0 |
to gateway 198.51.100.1 |
If the prefix is the same too, the metric is compared — a number set by whoever added the route (lower is better). That is how a laptop with a cable and Wi-Fi keeps two default routes and uses the wired one while it is there: its metric is 100, the wireless one's 600.
The host from the previous part that made the "neighbour or gateway" decision was doing exactly the same thing: its table is the connected network plus the default route, and the algorithm is the same for everyone.
What a router does with a packet
Let's follow a packet from laptop 192.168.1.10 to server 203.0.113.10 through home router R1 and the ISP's router R2. R1 has interfaces 192.168.1.1 (towards the flat) and 198.51.100.2 (towards the ISP); R2 has 198.51.100.1 and onwards into the internet.
laptop ──────── R1 ─────────── R2 ───── … ───── server
192.168.1.10 .1 | .2 .1
192.168.1.0/24 198.51.100.0/30
The same thing happens at every hop:
- Strip the Ethernet header, check the IP header checksum.
- Is the destination address mine? No — forward it.
- TTL − 1. Reached zero — drop it, send the sender ICMP Time Exceeded.
- Find a route by the longest prefix: the outgoing interface and next hop. None at all — drop it, send the sender ICMP Destination Unreachable.
- Recompute the Header Checksum.
- Wrap it in a new frame: the source MAC is its own outgoing interface, the destination MAC is the next hop (or the recipient itself, if the network is connected). Send.
| Leg | MAC src → dst | IP src → dst | TTL |
|---|---|---|---|
| laptop → R1 | laptop → R1 eth0 | 192.168.1.10 → 203.0.113.10 | 64 |
| R1 → R2 | R1 eth1 → R2 | 192.168.1.10 → 203.0.113.10 | 63 |
| R2 → onwards | R2 → next | 192.168.1.10 → 203.0.113.10 | 62 |
A router keeps nothing about a packet after sending it: every packet is an independent decision based on the table. It knows nothing about connections, does not remember the previous packet and does not care whether this one arrived. This is the main property of IP — best effort, "we'll try": delivery without guarantees, and the guarantees, if needed, are built by TCP.
A consequence people trip over follows from this too: the path there and the path back are two independent decisions by different routers. The server's reply may take a different road, and that is fine as long as nothing along the way expects to see both directions. In the table above, R1 sent out a private address 192.168.1.10 in the source field. Such a packet will not be let out onto the internet, and if it were, the reply to 192.168.1.10 would not find its way back. That is why R1 needs NAT, more on which below.
Fragmentation
The Ethernet MTU is 1500 bytes, and that is the most common case, but not the only one: PPPoE has 1492, tunnels less, and old networks sometimes had 576. What does a router do if a packet is longer than the MTU of the outgoing interface?
If the DF flag is not set, it cuts the packet into fragments. Each gets a copy of the IP header with the same Identification; Fragment Offset holds the offset of the fragment's data from the start of the original data in eight-byte units; and the MF (More Fragments) flag is set on all fragments except the last. A 4000-byte packet over an MTU of 1500:
| Fragment | Total Length | Data | Offset (×8) | MF |
|---|---|---|---|---|
| 1 | 1500 | 1480 | 0 | 1 |
| 2 | 1500 | 1480 | 185 | 1 |
| 3 | 1040 | 1020 | 370 | 0 |
A fragment carries 1480 bytes of data because 20 bytes go to the header, and 1480 is divisible by 8. Only the recipient reassembles the fragments: routers along the path do not, and may cut already-cut fragments further. The recipient waits for all the pieces by Identification, puts them together by Offset and, having received the last one (MF = 0) with no gaps, hands the packet to the transport layer.
Why fragmentation is avoided:
- Losing one fragment means losing the whole packet, and TCP will retransmit it whole, and it will be cut again.
- The transport header (ports) is only in the first fragment, so a firewall, NAT or load balancer has to either reassemble the packet itself or let fragments through blindly. Many simply drop them.
- Reassembly means buffers and timers on the recipient; in the 1990s attacks were built on this.
That is why modern stacks set DF on everything. A router drops a DF packet that does not fit in the MTU and sends the sender an ICMP "Fragmentation Needed" with the MTU value, and the sender reduces the size. This is Path MTU Discovery, and it is covered in detail — together with what happens when the ICMP does not get through — in part 8.
NAT
Private addresses are not routed on the internet, and there are not enough public ones for every device. NAT (Network Address Translation) is the rewriting of addresses in a packet at the boundary between a private network and the internet. In everyday speech "NAT" means its most common kind, formally called NAPT or PAT (with ports), and masquerade in Linux.
The translation table
Router R1 has the public address 198.51.100.2. Laptop 192.168.1.10 opens a connection from port 51234 to server 203.0.113.10:443.
┌──────────────┐ ┌────────────── R1 ──────────────┐ ┌──────────────┐
│ 192.168.1.10 │ ──── (1) ────► │ 192.168.1.1 198.51.100.2 │ ──── (2) ────► │ 203.0.113.10 │
│ :51234 │ │ │ │ :443 │
│ │ ◄─── (4) ───── │ translation table │ ◄─── (3) ───── │ │
└──────────────┘ └────────────────────────────────┘ └──────────────┘
(1) 192.168.1.10:51234 → 203.0.113.10:443
(2) 198.51.100.2:51234 → 203.0.113.10:443 ← src replaced
(3) 203.0.113.10:443 → 198.51.100.2:51234 ← the server replies to the router
(4) 203.0.113.10:443 → 192.168.1.10:51234 ← dst restored from the table
- The packet arrives at R1 from the private network. R1 looks up an entry for
192.168.1.10:51234 → 203.0.113.10:443in the translation table. None — it creates one: "192.168.1.10:51234↔198.51.100.2:51234, to203.0.113.10:443, protocol TCP". If port 51234 on the public address is already taken by another internal machine, R1 picks a different free one, and the external entry will have, for example,198.51.100.2:40001. - R1 rewrites the source address to
198.51.100.2and, if needed, the source port. It recomputes the IP header checksum and — note this — the TCP checksum, because it covers the addresses from the IP header. The router reaches into the layer above its own: this is where the abstraction from the first part leaks. - The server knows nothing about
192.168.1.10. It sees a connection from198.51.100.2:51234and replies to it. - The reply arrives at R1. Using the pair "public port 51234, from
203.0.113.10:443", R1 finds the entry and rewrites the destination address back to192.168.1.10:51234. Again two checksums. It sends the packet into the private network.
Entries live as long as traffic flows through them and are removed on a timeout: for an established TCP connection in Linux it is five days, for UDP 30 seconds if traffic went one way, and 180 seconds if it went both ways. That is why "quiet" connections through NAT have to be kept alive with keepalive packets; otherwise the entry disappears, and the router will have nowhere to deliver the next packet from the server.
One public address means 65 thousand ports, that is, tens of thousands of simultaneous connections for the whole flat. For a home that is infinity, but an ISP doing the same thing for thousands of subscribers on one address (CGNAT, Carrier-Grade NAT, with 100.64.0.0/10 addresses between the subscriber and the ISP) already has to divide ports into quotas.
What breaks
NAT relies on the connection having been started by someone inside. A packet from the internet with no entry in the table has nowhere to go at R1: there are many internal addresses, and the packet only carries the public one. It is dropped. Hence:
- Incoming connections are impossible without a manual entry. Such an entry is called port forwarding (DNAT): "send everything that arrives at
198.51.100.2:8080to192.168.1.50:80". It is a permanent row in the table, independent of traffic. - One port, one internal server. You cannot put two web servers on 443 behind one address.
- Protocols that carry addresses in their data. Old FTP tells the server "connect to me at
192.168.1.10:5000", SIP telephony inserts its address into the message. NAT does not see them. For them, routers have ALGs (application layer gateways): modules that parse the data of a specific protocol and rewrite the addresses there too. They work every other time. - Two devices behind NATs trying to connect directly (a call, a game), each behind its own router. Neither can start. The hole punching technique — both simultaneously send packets outwards so that each NAT creates an entry for the other, detailed in part 9 — does not always help, and then traffic goes through an intermediary server.
- Hairpin. Reaching your own public address from inside the network: the packet arrives at R1 from the inside and is addressed to R1 itself. Not all home routers can turn it back inside.
- The end of end-to-end connectivity. The idea of the internet was that any address is reachable from any other. NAT cancelled it, and a significant share of modern protocols is about working around this fact.
NAT is often confused with a firewall. It really does not let anything unrequested in, but not by policy — simply because it does not know where to send it. The actual firewall is a separate set of rules, usually on the same router.
In Linux, NAT is done by the conntrack module, which maintains the table, and it is enabled with one rule:
$ sysctl -w net.ipv4.ip_forward=1
$ iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
"Rewrite everything leaving through eth1 to the address of eth1". The table can be seen like this:
$ conntrack -L
tcp 6 431999 ESTABLISHED src=192.168.1.10 dst=203.0.113.10 sport=51234 dport=443 \
src=203.0.113.10 dst=198.51.100.2 sport=443 dport=51234 [ASSURED] use=1
The first half of the line is how the packet looks from the inside, the second is how the reply should look from the outside. 431999 is the number of seconds until the entry is removed.
Seeing it for yourself
The routing table's decision for a specific address, with the interface, next hop and the source address that will be used:
$ ip route get 203.0.113.10
203.0.113.10 via 192.168.1.1 dev eth0 src 192.168.1.10 uid 1000
Adding and removing a route:
$ sudo ip route add 10.1.0.0/16 via 192.168.1.253
$ sudo ip route del 10.1.0.0/16
Turning a machine into a router means enabling forwarding (it is off by default, and the machine silently drops other people's packets):
$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
traceroute will help you see routing through the eyes of a packet — it gets a section in part 8, when we get to ICMP, which it is built on. For now one observation in tcpdump -v is enough: if you capture the same packet on the way into and out of a router, its TTL, checksum and both MAC addresses will change, and its IP addresses only if the router does NAT.
Key takeaways
- The IPv4 header is 20 bytes: addresses, TTL, Protocol, length, fragmentation fields, a checksum of the header only.
- TTL is a hop counter against loops; every router decrements it and recomputes the checksum.
- A router looks up the longest matching prefix in the table, and on a tie the lower metric; connected networks, static routes and the default route are the three kinds of rows. Every packet is an independent, stateless decision.
- A packet longer than the MTU is cut into fragments with a shared Identification, and only the recipient reassembles them; modern stacks set DF and rely on ICMP.
- NAT rewrites the source address and port, keeps a translation table and restores the address in replies; incoming connections need port forwarding, protocols with addresses inside break, there is no end-to-end connectivity.
Next — Part 7: ARP: how a host that knows a neighbour's IP address learns its MAC address, what sits in the neighbour cache and for how long, what gratuitous ARP is and why it is so easy to hijack someone else's address on a local network.
Only registered users can leave a comment.
Sign in or register to leave a comment.