The packet before the first packet

COMMIT536605dHEAD → main
PUBLISHEDOct 13, 20205y ago
UPDATEDJul 25, 20265d ago
READING12 min1,792 words
#networking#udp·by santiago toscanini

In the DNS post I called the name lookup the first packet of everything. That was a small lie. Before your machine can ask anyone anything, it needs an address to be asked back at, and it wakes up without one. No IP, no netmask, no idea who the router is. The only identity it owns is the MAC address burned into its network card.

DHCP fixes that with one of the internet's better magic tricks: a machine with no address sends a letter with no return address, shouted to everyone in the room, and gets back a working identity in about a tenth of a second. Four messages, remembered by the acronym DORA: Discover, Offer, Request, Ack. Then the machine is a citizen of the network, on a lease, with an expiry date and everything.

There's one honest confession to make up front. Unlike the DNS post, nothing on this page talks to the real network. Browsers can't open UDP sockets, can't broadcast, and there's no DoH-style escape hatch for DHCP, because a protocol whose whole job happens before you have an address is exactly the kind of thing a browser sandbox will never let you touch. So every byte below is a dated capture of a real exchange: the four DORA packets are from Wireshark's sample dhcp.pcap, captured 2004-12-05, a device leasing 192.168.0.10 from 192.168.0.1. I parsed the pcap myself, rebuilt every packet byte for byte with this post's own TypeScript encoder, and cross-checked the semantics against the last real ACK my own Mac received, which macOS will happily show you without root:

ipconfig getpacket en0

Everything is verified against RFC 2131 and RFC 2132, and the figures tell you which capture they're reading.

01 · Shouting from nowhere

The Discover leaves the machine as a UDP datagram from 0.0.0.0:68 to 255.255.255.255:67. Both halves of that are worth staring at.

The source is 0.0.0.0 because the client honestly has nothing to put there. The IP stack is empty; the packet is the request to fill it. The destination is 255.255.255.255, the limited broadcast, and not something like 192.168.0.255, because computing a subnet's broadcast address requires knowing the subnet mask, and the mask is literally one of the things the client is asking for. The limited broadcast needs zero knowledge and no router will ever forward it off the link. Everyone in the room hears you; nobody else does.

The two fixed ports are a 1985 story. RFC 951, BOOTP, explains why the client gets its own reserved port 68 instead of a random one: server replies are sometimes broadcast too, and a broadcast reply to a random port "could confuse other hosts that happened to be listening on that port". With a dedicated client port, every machine that isn't mid-boot filters the noise in the kernel. In DNS only the server needed a famous port; here both sides do, because here even the replies might be for everyone.

And since it's UDP, the correlation problem is the same one DNS has: no connection, so nothing pairs answers to questions except a number you invent. DNS uses a 16-bit ID; DHCP uses a 32-bit xid. The client picks it at random, every reply must echo it, and over a medium where every packet might reach every machine, that echo is the entire concept of "this one's for me".

02 · The skeleton is BOOTP

DHCP, protocol of 1993, doesn't have its own packet format. It wears BOOTP's, unchanged from 1985, on purpose. RFC 2131 keeps the layout so that BOOTP relay agents, the routers that forward these broadcasts between subnets, would forward DHCP without ever knowing it existed, and so old BOOTP clients could still talk to new servers.

So every DHCP message starts with the same fixed 236 bytes. All four messages below are the real 2004 exchange; hover every field, and pay attention to chaddr, xid and the flags:

A few things worth pulling out of those bytes.

The answer arrives in a fixed field, not an option. The address you're being offered rides in yiaddr, "your address", exactly where BOOTP put it in 1985. Meanwhile siaddr, "server address", doesn't mean the DHCP server: it's the next server for fetching boot files, because BOOTP's whole reason to exist was booting diskless workstations over the network. The actual DHCP server identifies itself in an option instead.

The broadcast flag is one bit of humility. The server has to reply to a client whose IP stack is still empty. Two ways out: the client can set the leftmost flags bit to say "shout the reply, I can't receive unicast yet", or leave it clear, meaning the server may write an ethernet frame addressed directly to chaddr carrying an IP destination that doesn't exist yet. It works because MAC-level delivery doesn't ask the IP stack's permission. This capture's client left it clear, which is why the Offer and Ack came back unicast to an address that was, at that moment, imaginary.

sname and file are 192 bytes of almost always nothing. A 64-byte server name and a 128-byte boot file path, kept because BOOTP had them, shipped as zeros in nearly every packet since. PXE network boot still fills them; everyone else pays 192 zeros per packet as a compatibility tax. DHCP even defines option 52 to recycle them as extra option space when the real options area runs out.

After byte 236, BOOTP had a 64-byte free-form "vendor area". DHCP moved in and turned it into its entire nervous system: everything DHCP adds over BOOTP travels as options, tagged TLV records exactly in the spirit of DNS's records, just with one-byte types and one-byte lengths.

To mark the squat, the options area must open with a magic cookie: 63 82 53 63. RFC 2132 specifies it, with a straight face, as the dotted decimal 99.130.83.99, an IP address that means nothing and routes nowhere. It exists so a parser can tell a DHCP packet from an old BOOTP one before reading a single option. Even the DHCP message type itself, the fact that this packet is a Discover and not an Ack, is just option 53. The fixed header never says.

Walk the real Discover's options area byte by byte, then switch tabs and walk the Ack's half, where the server signs the contract and the lease numbers live:

Two options in that walk deserve their own paragraph. Option 55, the parameter request list, is the client's wishlist: a bare list of option codes meaning "if you know these, tell me". This client asks for subnet mask, router, DNS and NTP. And options 0 and 255 are the grammar's punctuation: 0 is a one-byte no-op used as filler, 255 is the full stop that ends the option walk. Everything between the cookie and the ff is somebody's typed, length-prefixed fact.

04 · Build one

Everything above, hands on. Change the MAC, name yourself with option 12, tick and untick wishes, and read your own Discover. The defaults rebuild the captured 2004 packet byte for byte, seven pad bytes and all:

05 · Four packets to citizenship

Now the whole conversation, as it actually crossed a network. Watch who shouts and who whispers: the client broadcasts both its messages, the server unicasts both replies. The Request is broadcast on purpose, even though the client knows exactly who it's talking to by then: naming the chosen server in option 54, out loud, is how every losing server learns it can free its tentative offer. Rejection by press release.

That capture also keeps two honest wrinkles I refuse to sand off. First, the client mints a fresh xid for the Request, 0x3d1e after 0x3d1d, even though RFC 2131's own table says the Request should reuse the Offer's. Real clients have opinions; servers cope, because each reply just echoes whatever its request said. Second, look at what the wishlist got: the client asked for subnet mask, router, DNS and NTP, and the Ack grants only the subnet mask. Option 55 is a wishlist, not a contract. Servers answer what they can, and this one, apparently, couldn't say much.

My Mac's ACK from 2026 shows the other end of the spectrum: server 192.168.0.2 volunteered the mask, broadcast address, DNS, domain name and router. Same protocol, chattier landlord.

06 · A lease is a clock

The Ack's option 51 said 3600: this address is yours for one hour. Not a gift, a lease. The address belongs to the pool, and the client's claim on it decays unless renewed. Two more options, 58 and 59, arm the clocks called T1 and T2, and RFC 2131 §4.4.5 defaults them to 50% and 87.5% of the lease. This server sent exactly those defaults: 1800 and 3150. Press play and live the whole hour in twenty seconds, or scrub it by hand:

The escalation pattern is the protocol's whole personality in miniature. At T1 the client is polite: a unicast Request to the server that granted the lease, connection-style, with ciaddr filled in because now it has an address to speak from. At T2, after twenty-two and a half minutes of silence, loyalty expires before the lease does: it broadcasts the Request so any server on the link can save it. At zero, the machine is legally homeless, must stop using the address mid-connection if necessary, and starts over from Discover as if freshly booted.

Notice my Mac's real numbers in the figure's footnote: T1 at 12604 seconds of a 28800-second lease is 43.8%, not 50%. That's not a buggy router. RFC 2131 tells servers T1 and T2 "SHOULD be chosen with some random fuzz", so a building full of machines that all leased at 9:00 doesn't renew in one synchronized stampede. The lease can also be 0xffffffff, infinity, at which point DHCP quietly becomes static addressing with better paperwork.

07 · Capture your own

That's DHCP on the wire: a 1985 skeleton kept for compatibility, a magic cookie squatting in the vendor area, a wishlist option, and a four-packet ritual that turns a MAC address into a network citizen with a lease and two alarm clocks. It runs before everything, which means it runs under everything: DNS told you where things are, but DHCP is why you were able to ask.

Since this page can't send DHCP for you, here's how to watch your own machine do it. The no-root version, which prints your current lease parsed field by field, options and all:

ipconfig getpacket en0

And the full ritual, caught raw:

sudo tcpdump -i en0 -enx -w dhcp.pcap port 67 or port 68

Toggle your Wi-Fi off and on while it runs, and you'll own a DORA of your very own, four packets you can now read byte by byte. Same punchline as HTTP and DNS: it's bytes all the way down, and you can read every one of them.

$git log --oneline public/posts/dhcp-over-the-wire/
536605dNeural networks from scratch: a third post on pixels and vectorisationtoday
© 2026 · v2.0 · santiago toscanini