Networking · lesson 01

Bytes on a wire

You wrote 5 bytes. The wire carried 47. The other 42 were added by the kernel, one header at a time, and every machine between you and the destination reads a different slice of them.

in progress
12 min interactive

Where did the other 42 bytes come from?

You call send with the string "hello". Five bytes. Run tcpdump on the same machine and it reports a 47-byte frame leaving the network card. The five bytes you wrote are in there. They are the last five. Everything before them was written by the kernel.

Most networking courses start here with a diagram of seven layers and a paragraph about abstraction. This lesson starts with the bytes instead. Once you have seen the 47 bytes laid out in memory, the layers stop being a diagram and become what they actually are: headers, stacked in front of your data.

A header is a struct prepended to your bytes

Think of the frame as a byte array. Your payload sits at the end. In front of it the kernel places a small fixed-layout struct — a header — that describes the payload to the next machine. In front of that it places another header for a different reader. Three headers, three readers.

bytes header answers read by
0–13 Ethernet (14) which NIC on this cable? switch, next hop
14–33 IPv4 (20) which machine on the internet? every router
34–41 UDP (8) which program on that machine? destination kernel
42–46 payload (5) whatever you meant destination program

Each header has one job: get the bytes to the next reader. The Ethernet header only has to reach the router on your local cable. The IP header has to reach a machine on the other side of the planet. The UDP header has to reach one specific program on that machine. None of them look at your five bytes.

💡
Each header names the header after it. The Ethernet header has a 2-byte ethertype field: 0x0800 means "the next 20 bytes are an IPv4 header". The IPv4 header has a 1-byte protocol field: 17 means "the next 8 bytes are UDP" (6 means TCP). A receiver parses the frame front to back by following these fields. It never guesses.

The headers as C structs

Because a header is a fixed layout of integers, you can describe it as a struct. These are simplified versions of what the kernel uses. Note the sizes: every field is exactly as wide as it needs to be, and there is no padding anywhere — the designers chose offsets that keep every field aligned to its own size, which is why sizeof comes out to exactly 14, 20, and 8.

c
#include <stdint.h> struct eth_hdr { // 14 bytes — read by the switch uint8_t dst_mac[6]; uint8_t src_mac[6]; uint16_t ethertype; // 0x0800 = IPv4 follows }; struct ipv4_hdr { // 20 bytes — read by every router uint8_t version_ihl; // 0x45: version 4, header 5 words uint8_t dscp_ecn; uint16_t total_length; // header + everything after it uint16_t id; uint16_t flags_frag; uint8_t ttl; // decremented by each router; 0 → dropped uint8_t protocol; // 17 = UDP, 6 = TCP uint16_t checksum; uint32_t src_addr; // 192.168.1.7 is just a 32-bit integer uint32_t dst_addr; }; struct udp_hdr { // 8 bytes — read by the receiving kernel uint16_t src_port; uint16_t dst_port; // which program gets the payload uint16_t length; // 8 + payload bytes uint16_t checksum; }; // then: your payload bytes, up to the frame's end

Look at src_addr. An IPv4 address is a uint32_t. The dotted form 192.168.1.7 is just how humans print the four bytes c0 a8 01 07. A port is a uint16_t. These are integers, and the next two lessons treat them as integers.

⚠️
Multi-byte fields are big-endian on the wire. Port 8080 is 0x1F90. On the wire it appears as the bytes 1F 90: most significant byte first. Your x86 or ARM CPU stores the same uint16_t in memory as 90 1F. If you cast wire bytes to a uint16_t and read it directly, you get 0x901F = 36895, not 8080. Lesson 03 covers the conversion functions. For now, remember: the wire has one byte order, your CPU may have another.

Watch the frame grow

Step through the visualization below. It starts with the five bytes you wrote and prepends one header at a time, exactly as the kernel does on the way down to the network card. Each row is 16 bytes, the way tcpdump -xx and hexdump print them. Hover any byte to see which header it belongs to.

one frame, built byte by byte
Click next to start the walkthrough.

The frame grows in front. Your payload never moves. This is why the kernel allocates packet buffers with headroom before the data: it can prepend each header by moving a pointer backwards instead of copying the payload.

Who reads what

Follow the frame as it leaves your machine. The switch in your house reads bytes 0–13, finds the destination MAC, forwards the frame out the right port, and stops. It never reads byte 14. Your router reads bytes 14–33, finds the destination IP, decrements the TTL, picks the next hop, and — this is the part people miss — replaces the Ethernet header with a new one addressed to the next router. The IP header and everything after it travel untouched.

Twelve routers later the frame reaches the destination machine. Its kernel reads the IP header, sees protocol 17, reads the UDP header, sees port 8080, finds the program that owns port 8080, and copies bytes 42–46 into that program's buffer. Your five bytes arrive. Nothing else does: the program only ever sees "hello".

✅
Each layer reads its own header and hands the rest up, unchanged. This is the whole design. A router does not need to know UDP exists. Your program does not need to know Ethernet exists. When you debug a network problem, the first question is always: which header is wrong, and therefore which machine is confused?

The size limit you will hit

Ethernet frames carry at most 1500 bytes after the Ethernet header. This is the MTU (maximum transmission unit). Subtract 20 for IPv4 and 8 for UDP: your payload in a single frame is at most 1472 bytes. Send more and the kernel either splits it into fragments (slow, fragile, often dropped) or refuses. Every protocol you will meet in this track — DNS, QUIC, VPN tunnels — is designed around that 1472-byte budget.

See it yourself

sh
# terminal 1: capture UDP to port 8080, print every byte including Ethernet $ sudo tcpdump -i any -xx udp port 8080 # terminal 2: send "hello" with a tool that already speaks UDP $ printf hello | nc -u 93.184.216.34 8080 # terminal 1 prints the 47 bytes, 16 per line: 0x0000: 3c37 865a 1fe0 a483 e712 9b04 0800 4500 ← eth (14) + start of ip 0x0010: 0021 1c46 4000 4011 b1e6 c0a8 0107 5db8 ← ip continues 0x0020: d822 c822 1f90 000d 7a3c 6865 6c6c 6f ← ip ends, udp (8), "hello"

Find 1f90 on the last line: port 8080. Find 4011 on the middle line: TTL 64, protocol 17. Find 6865 6c6c 6f: your five bytes. Every number in the visualization above is on this screen. The layers are real, and they are nothing more than this.

one-line takeaway

A packet is your bytes with three fixed-layout headers prepended — Ethernet for the next hop, IP for the machine, UDP for the program — and each reader looks only at its own.