DHCP Explained: How a Machine With No IP Address Gets One in Four Messages
This post is part of Protocol Lab, a free, hands-on series for learning networking protocols by building and breaking them in a container lab. All the lab material — topologies, configs, and scripts — lives in the repo: github.com/pathvector-studio/protocol-lab.
Every host in the labs so far had its address configured for it. But how does a machine that just booted — with no IP at all — get one? The answer is DHCP, and it takes exactly four messages, often called DORA:
- Discover — the client shouts "is there a DHCP server?" (broadcast, from
0.0.0.0), - Offer — a server replies "you can have
10.0.0.193", - Request — the client says "yes, I'll take
10.0.0.193", - Ack — the server confirms "it's yours, for 3600 seconds".
In this lab you'll start a client with an address-less link, run a DHCP client, and capture the whole DORA exchange on the wire.
Reading guide: rfc-notes/dhcp-dora.md
Prerequisite: TCP Lab 07: Handshake and Teardown (reading captures)
Expected time: 40–55 minutes.
The Goal
By the end, you should be able to label this exchange:
| Step | From → To | Meaning |
|---|---|---|
| Discover | 0.0.0.0:68 → 255.255.255.255:67 |
any server out there? |
| Offer | server:67 → client:68 | here is an address you can use |
| Request | 0.0.0.0:68 → 255.255.255.255:67 |
I request that offered address |
| Ack | server:67 → client:68 | confirmed, with lease time |
What You Will Learn
- Why a host with no address must use broadcast to reach a server it does not yet know.
- The four DHCP messages (DORA) and what each one carries.
- What a lease is and why addresses are temporary.
- What options DHCP delivers besides the address (router, DNS, lease time).
- Why the DHCP ports are 67 (server) and 68 (client).
This lab does not cover:
- DHCP relay (across subnets), reservations, or failover.
- DHCPv6 or IPv6 SLAAC (a different mechanism).
- Rebinding/renewing timers (T1/T2) in depth.
Where to Read in the RFCs
| RFC | Section | What to focus on |
|---|---|---|
| RFC 2131 | 3.1 | How a client obtains an address (the DORA flow) |
| RFC 2131 | 2 | DHCP message format (built on BOOTP) |
| RFC 2131 | 4.1 | How broadcast is used, and ports 67/68 |
| RFC 2132 | 3, 9 | DHCP options (subnet, router, DNS, lease time, message type 53) |
The Big Picture
A client (with no address) and a server (10.0.0.1, running udhcpd) are connected by a single link.
client (no IP) ==== eth1/eth1 ==== server (10.0.0.1)
udhcpc udhcpd, pool 10.0.0.100-.200
The client's eth1 has only its link brought up — no IP. It gets one via DHCP.
sequenceDiagram
participant C as client (0.0.0.0)
participant S as server (10.0.0.1)
C->>S: DISCOVER (broadcast 255.255.255.255:67)
S->>C: OFFER 10.0.0.193 (to :68)
C->>S: REQUEST 10.0.0.193 (broadcast :67)
S->>C: ACK 10.0.0.193, lease 3600s (to :68)
Note over C: configures eth1 = 10.0.0.193
Note:
10.0.0.0/24is a local, closed range — nothing in this lab touches the real internet.
What You Need
Recommended environment:
- Linux / WSL2 / a Linux VM
- Docker
- containerlab
Images used:
nicolaka/netshoot:latest— it bundlesudhcpd(the server),udhcpc(the client), andtcpdump.
No additional images are required. The DHCP server configuration lives at examples/dhcp-22/udhcpd.conf.
Running the Lab
The quick path, which deploys, verifies, and tears down for you:
./scripts/labctl.sh run dhcp-22
Or step through it manually:
1. Move into the working directory
cd protocol-lab/examples/dhcp-22
2. Deploy and start the DHCP server
sudo containerlab deploy -t dhcp-22.clab.yml
docker exec clab-dhcp-22-server sh -c ": > /tmp/udhcpd.leases; udhcpd -f /etc/udhcpd.conf &"
udhcpd.conf hands out 10.0.0.100–.200 and passes the router, DNS, and lease time as options.
3. Set up the capture and have the client get an address
docker exec -d clab-dhcp-22-client tcpdump -i eth1 -n "udp port 67 or udp port 68"
docker exec clab-dhcp-22-client udhcpc -i eth1 -q -f -n
docker exec clab-dhcp-22-client ip -4 addr show eth1
udhcpc's output should include a line like lease of 10.0.0.193 obtained from 10.0.0.1, and ip addr should show an address on eth1.
4. Read the DORA exchange in the capture
docker exec clab-dhcp-22-client tcpdump -n -vv -r /tmp/dhcp.pcap
0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP ... DHCP-Message: Discover
... > ...68: DHCP-Message: Offer
0.0.0.0.68 > 255.255.255.255.67: DHCP-Message: Request, Requested-IP 10.0.0.193
... > ...68: DHCP-Message: ACK
Expected Output
udhcpc:lease of 10.0.0.1xx obtained from 10.0.0.1.ip addr show eth1:inet 10.0.0.1xx.- The capture: all four messages —
Discover→Offer→Request(with a Requested-IP) →ACK.
Why It Works
DHCP is a mechanism for handing an address — and the settings that go with it — to a host that doesn't have one. The hard part is the starting point: the client doesn't know the server, and doesn't have an address of its own.
- Why broadcast. The client knows neither the server's IP nor its own. So it sends to
255.255.255.255(broadcast) from source0.0.0.0, reaching everyone on the link. Only the server responds. - The four DORA steps:
- Discover: "Is there a DHCP server?" Broadcast.
- Offer: The server proposes a candidate — "how about this address?" If there are multiple servers, multiple Offers can arrive.
- Request: The client asks for that address, including which server's Offer it accepts. This is also broadcast — so the servers that weren't chosen learn their offers were declined.
- Ack: The server finalizes it, attaching the lease time and options.
- The lease. The address isn't bought outright — it's borrowed, with an expiry. Before the lease runs out, the client renews it. This lets addresses of departed hosts be reclaimed and reused, keeping a limited address space in circulation.
- More than just an address. DHCP also delivers the subnet mask, default router, DNS server, lease time, and more as options (RFC 2132). That's why connecting via DHCP configures your gateway and DNS automatically.
- Ports 67/68. The server uses 67, the client 68 — both UDP. Because the ports are fixed, the exchange works even before any IP is settled.
The key insight: starting from "I know neither my own address nor the server's," broadcast plus four messages is enough to borrow a complete address setup.
Common Pitfalls
- Thinking the R in DORA comes from the server. The Request is sent by the client (requesting the offered address).
- Thinking the Offer is final. The Ack is what finalizes it. An Offer is a candidate — with multiple servers, several may arrive.
- Missing why broadcast is needed. The client knows neither its own IP nor the server's.
- Assuming the address is permanent. A lease has an expiry. Fail to renew and you lose it.
- Assuming DHCP only hands out an address. It also delivers the router, DNS, lease time, and other options.
- Mixing up the ports. Server 67, client 68 — swap them and your capture filter won't match.
Cleanup
sudo containerlab destroy -t dhcp-22.clab.yml --cleanup
If you used labctl.sh run dhcp-22, the script runs destroy for you at the end.
Check Your Understanding
- What are the four DORA messages? Who sends each one?
- Why does a client with no address use broadcast? What is its source address?
- What's the difference between an Offer and an Ack? Which one finalizes the address?
- What is a lease, and why are addresses time-limited?
- Besides an address, what does DHCP deliver? (Name three things.)
- What are the DHCP server and client port numbers?
References
- RFC 2131: Dynamic Host Configuration Protocol
- RFC 2132: DHCP Options and BOOTP Vendor Extensions
- RFC 5737: IPv4 Address Blocks Reserved for Documentation
- udhcpd / udhcpc (BusyBox) documentation
Verified Run Log (2026-07-07)
This lab has been confirmed reproducible on real hardware.
Environment:
- Ubuntu 26.04 LTS (kernel 7.0.0-27-generic, x86_64)
- Docker 29.1.3
- containerlab 0.77.0
- client / server:
nicolaka/netshoot:latest(udhcpd 1.38.0 / udhcpc, with tcpdump)
Running PATH="/tmp/pl-shim:$PATH" ./scripts/labctl.sh run dhcp-22 performed deploy → verify → destroy, and verification.json returned "status": "verified".
The client obtains an address
$ docker exec clab-dhcp-22-client udhcpc -i eth1 -q -f -n
udhcpc: lease of 10.0.0.193 obtained from 10.0.0.1, lease time 3600
$ docker exec clab-dhcp-22-client ip -4 addr show eth1
inet 10.0.0.193/24 ... scope global eth1
DORA is visible on the wire
$ docker exec clab-dhcp-22-client tcpdump -n -vv -r dhcp.pcap
0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request ... DHCP-Message: Discover
... > ...68: DHCP-Message: Offer
0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request ... DHCP-Message: Request
Requested-IP: 10.0.0.193
... > ...68: DHCP-Message: ACK
Discover (source 0.0.0.0, destination broadcast 255.255.255.255:67) → Offer → Request (Requested-IP 10.0.0.193) → Ack. Starting with no address and no known server, broadcast plus four messages borrows a complete address setup — here including the router, DNS, and lease.
Cleanup
containerlab destroy -t dhcp-22.clab.yml --cleanup
That's DHCP: four messages — Discover, Offer, Request, Ack — that take a host from no address at all to a fully configured lease, with the gateway and DNS thrown in for free.
Explore the full Protocol Lab series here: github.com/pathvector-studio/protocol-lab. If these labs are useful to you, please ⭐ star the repo on GitHub — it genuinely helps others find the project.
Next up, we'll keep climbing the stack and look at what happens once a freshly configured host starts resolving names and moving packets.