DHCP: How Devices Get an Address
From Plugged In to Online
A device joining a network knows its own MAC address and nothing else: not its IP address, subnet, gateway or DNS server. The Dynamic Host Configuration Protocol (DHCP) fills all of that in within a fraction of a second. When it fails, the symptoms (a 169.254.x.x address, "no internet" on one VLAN, a whole office using the wrong gateway) are easy to diagnose once you know the exchange. The address formats themselves are covered in IPv4 and IPv6 Addressing.
The DORA Exchange, Packet by Packet
DHCP runs over UDP: servers listen on port 67, clients on port 68. A client without an address cannot unicast to anyone, so the first messages are broadcasts.
| Step | From → To | Key contents |
|---|---|---|
| Discover | 0.0.0.0:68 → 255.255.255.255:67 |
transaction ID (xid), client MAC (chaddr), list of wanted options |
| Offer | server → client (broadcast or unicast) | offered address (yiaddr), mask, router, DNS, lease time, server identifier |
| Request | 0.0.0.0:68 → 255.255.255.255:67 |
the chosen server identifier and the requested address |
| Acknowledge | server → client | final confirmation, with the full set of options |
The Request is broadcast even though the client has chosen a server, because several servers may have made offers: the chosen server identifier (option 54) inside it tells the others to return their reserved addresses to the pool.
Other message types appear in real captures:
- NAK: the server refuses a request, typically for an address from a network the client has left (a laptop moved from home to office). The client restarts from Discover.
- DECLINE: after the ACK, many clients ARP-probe the address (see Ethernet, ARP and Switching); if another host answers, they decline it and start again.
- RELEASE returns the address early; INFORM lets a statically addressed host ask only for options.
What a DHCP Message Looks Like
DHCP reuses the older BOOTP format: a fixed 236-byte header followed by a "magic cookie" (63 82 53 63) and a list of options, each encoded as code, length, value. Building and parsing one with the standard library makes the format concrete:
import ipaddress, struct
MAGIC = b"\x63\x82\x53\x63" # marks the start of DHCP options
TYPES = {1: "DISCOVER", 2: "OFFER", 3: "REQUEST", 4: "DECLINE",
5: "ACK", 6: "NAK", 7: "RELEASE", 8: "INFORM"}
ADDR_OPTS = {1: "subnet mask", 3: "router", 6: "dns", 54: "server id"}
def build_offer(xid, mac, yiaddr, options):
header = struct.pack("!BBBBIHH4s4s4s4s16s192x",
2, 1, 6, 0, xid, 0, 0, # op=2 (reply), Ethernet, 6-byte MAC, hops, xid
bytes(4), ipaddress.IPv4Address(yiaddr).packed, bytes(4), bytes(4),
bytes.fromhex(mac.replace(":", "")))
body = bytes([53, 1, 2]) # option 53 (message type) = OFFER
for code, value in options:
body += bytes([code, len(value)]) + value
return header + MAGIC + body + b"\xff" # 255 = end of options
def parse(pkt):
xid = struct.unpack("!I", pkt[4:8])[0]
yiaddr, giaddr = (ipaddress.IPv4Address(pkt[o:o + 4]) for o in (16, 24))
print(f"xid={xid:#x} client={pkt[28:34].hex(':')} yiaddr={yiaddr} giaddr={giaddr}")
assert pkt[236:240] == MAGIC
i = 240
while pkt[i] != 255:
code, length = pkt[i], pkt[i + 1]
value = pkt[i + 2:i + 2 + length]
if code == 53:
print(" type:", TYPES[value[0]])
elif code in ADDR_OPTS:
addrs = [str(ipaddress.IPv4Address(value[j:j + 4])) for j in range(0, length, 4)]
print(f" {ADDR_OPTS[code]}: {', '.join(addrs)}")
elif code == 51:
print(" lease:", struct.unpack("!I", value)[0], "seconds")
i += 2 + length
ip = lambda *a: b"".join(ipaddress.IPv4Address(x).packed for x in a)
offer = build_offer(0x3903F326, "a4:5e:60:d1:22:9b", "192.168.10.57", [
(1, ip("255.255.255.0")), (3, ip("192.168.10.1")), (6, ip("192.168.10.1", "9.9.9.9")),
(51, struct.pack("!I", 43200)), (54, ip("192.168.10.1"))])
print(len(offer), "bytes")
parse(offer)
278 bytes
xid=0x3903f326 client=a4:5e:60:d1:22:9b yiaddr=192.168.10.57 giaddr=0.0.0.0
type: OFFER
subnet mask: 255.255.255.0
router: 192.168.10.1
dns: 192.168.10.1, 9.9.9.9
lease: 43200 seconds
server id: 192.168.10.1
The client matches replies to its request by xid, because on a broadcast network it hears everyone's traffic. yiaddr ("your address") carries the offered IP, and the message type lives in option 53, not in the header. The giaddr field matters for relays, below. A production parser must also skip pad bytes (option 0) and bounds-check lengths, since a malformed packet is attacker-controlled input.
Options Worth Knowing
| Code | Option | Why it matters |
|---|---|---|
| 1 | Subnet mask | defines the local network; wrong mask breaks on-link reachability |
| 3 | Router | the default gateway |
| 6 | DNS servers | what the client resolves names with |
| 42 | NTP servers | time sync |
| 51 | Lease time | seconds the address is valid |
| 55 | Parameter request list | which options the client wants; differs by OS, so it is also a device fingerprint |
| 61 | Client identifier | identifies the client instead of the MAC when present |
| 66 / 67 | TFTP server / boot file | network booting (PXE) |
| 82 | Relay agent information | which switch and port the request came from |
| 121 | Classless static routes | pushes extra routes; if sent, clients ignore option 3 |
The last row catches people: RFC 3442 says a client that receives option 121 must ignore the router option, so a server sending both needs the default route included in option 121 as well.
Leases and Their Timers
An address is leased, not given. Two timers run from the moment of the ACK:
- T1, by default 50% of the lease: the client renews by unicasting a Request to the server that granted the lease. A healthy network sees these quietly all day.
- T2, by default 87.5%: if renewals went unanswered, the client rebinds by broadcasting, accepting an extension from any server.
- Expiry: the client must stop using the address. Connections drop.
So a DHCP server outage is not noticed immediately: existing clients keep working until expiry, while new devices fail at once. On reboot, a client first asks for its previous address, which is why laptops usually keep the same IP.
Lease length is a trade-off. Guest Wi-Fi wants short leases (an hour or less) so phones that left do not exhaust the pool; wired office networks can use a day or more, keeping addresses stable in logs. Shorten leases a few days before a planned renumbering.
Relays: DHCP Across Routers
Broadcasts stop at the router, yet one central server usually serves dozens of VLANs. The router interface on each VLAN runs a DHCP relay (Cisco's ip helper-address): it catches the broadcast, writes its own interface address into giaddr, and unicasts the message to the server, which picks the pool whose subnet contains giaddr.
The classic failure: a new VLAN gets a pool on the server but no relay on its router interface. Every client self-assigns 169.254.x.x, and the server logs show nothing because no request ever reached it.
Reservations versus Static Addresses
Printers, servers and access points need predictable addresses. A reservation always hands the same address to a given client, keeping the inventory on the server and still delivering DNS and gateway changes automatically. A minimal dnsmasq configuration shows both a pool and a reservation:
interface=eth1
dhcp-range=192.168.10.100,192.168.10.200,12h
dhcp-option=option:router,192.168.10.1
dhcp-option=option:dns-server,192.168.10.1
dhcp-host=a4:5e:60:d1:22:9b,192.168.10.20,printer-2f
Keep reservations and static addresses outside the dynamic range (here, .20 sits below .100) so the server never hands them to someone else. Two modern complications: phones and laptops randomise their MAC per Wi-Fi network by default, so MAC-based reservations for personal devices are unreliable; and some Linux clients (systemd-networkd, for example) send a DUID-based client identifier, so a server that matches on option 61 will not recognise a MAC-only reservation. For larger deployments, ISC's current server is Kea; the older ISC DHCP server is no longer maintained.
IPv6: SLAAC, DHCPv6, or Both
IPv6 moves the decision into router advertisements. Two flags tell hosts what to do:
| RA flags | Address from | DNS from |
|---|---|---|
| no M, no O | SLAAC (host builds it from the /64 prefix) | RDNSS option in the RA |
| O set | SLAAC | stateless DHCPv6 |
| M set | DHCPv6 (stateful) | DHCPv6 |
SLAAC itself is governed by a separate flag on each advertised prefix, so M-flag networks often give hosts both kinds of address. DHCPv6 uses UDP ports 546 (client) and 547 (server), multicast (ff02::1:2) instead of broadcast, and a Solicit, Advertise, Request, Reply exchange. Clients identify themselves with a DUID, not a MAC, which complicates matching logs to switch tables. And DHCPv6 has no default-gateway option: the route always comes from router advertisements, so a network with a DHCPv6 server but filtered RAs gives hosts addresses and no way out. Android does not support stateful DHCPv6 address assignment, so networks with Android clients need SLAAC. ISPs use DHCPv6 prefix delegation to hand a home router a whole prefix (commonly a /56) to subnet internally.
Troubleshooting
Start with what the client actually has:
ipconfig /all # Windows: "DHCP Server", "Lease Obtained", "Lease Expires"
ipconfig /release && ipconfig /renew
nmcli device show eth0 # NetworkManager: IP4.ADDRESS, IP4.GATEWAY, DHCP4 options
sudo tcpdump -n -e -i eth0 'udp port 67 or udp port 68' -v
-e prints MAC addresses, which identifies which device answered. In the verbose output, look for the DHCP-Message option on each packet to follow Discover, Offer, Request, ACK.
| Symptom | Likely cause | Check |
|---|---|---|
169.254.x.x (APIPA) |
no Offer received | tcpdump: Discovers leaving, nothing back? Relay config, VLAN, server pool |
| Offers arrive, client keeps sending Discover | client software rejects or never sees them | host firewall, VPN or security agent, Offer malformed by a relay |
| NAK in a loop | client requesting an address from another subnet; two servers disagree | server logs; overlapping pools |
| Pool exhausted | too many devices, leases too long, or a starvation attack | server lease table; shorten leases; port security |
| Correct address, wrong gateway or subnet | rogue DHCP server | "DHCP Server" in ipconfig /all; the Offer's source MAC |
A rogue server is usually innocent: a consumer router plugged in with its LAN port facing the office. Find its MAC from the Offer, then look it up in the switch's MAC address table to find the port. The permanent fix is DHCP snooping: switches drop server messages on untrusted ports, and the binding table it builds also feeds dynamic ARP inspection. The Network Security and Wireless and LAN lessons place this among the other LAN defences.
Practice
On a network you administer, run the tcpdump command above and renew your lease. Label each packet with its DORA step, note the server identifier and lease time, and confirm the next renewal is a unicast at half the lease. Then decode one captured UDP payload with the parser above.