DHCP
By the end of this lesson you will be able to describe the four DHCP messages in order, explain leases and renewal times, read DHCP options, say why relay agents are needed, and simulate a small DHCP server in Python.
When you join the Wi-Fi at a café in Pune, your phone gets an IP address, a subnet mask, a gateway and DNS servers in about a second, without you typing anything. That is DHCP, the Dynamic Host Configuration Protocol (RFC 2131). Without it, someone would have to configure every device by hand.
What DHCP hands out
A DHCP server gives each client a set of settings, called a lease:
- an IP address from a pool (also called a scope);
- the subnet mask;
- the default gateway (router);
- DNS server addresses;
- the lease time: how long the client may keep the address.
Think of a library lending books. You borrow a book for 14 days. Before the due date you can renew it. If you never come back, the book returns to the shelf for someone else. IP addresses work the same way: borrowed, renewed, and eventually returned to the pool.
The DORA exchange
A new client has no IP address and does not know where the server is. So DHCP starts with broadcasts, over UDP: servers listen on port 67, clients on port 68.
- Discover. The client broadcasts: "Is there a DHCP server here?" Source address
0.0.0.0, destination255.255.255.255. - Offer. A server replies with an offer: "You can have 192.168.1.23, mask /24, gateway .1, DNS, for 24 hours."
- Request. The client broadcasts: "I accept the offer from that server." It broadcasts so that any other servers that made offers know they were not chosen.
- Acknowledge (ACK). The chosen server confirms. The client may now use the address.
The first letters make DORA, an easy way to remember the order. Many clients also check that nobody else is using the address (with ARP) before using it.
A DHCP server in Python
Let us build a small server that hands out addresses from a pool and remembers which device has which, using each device's MAC address as the key.
import ipaddress
class DhcpServer:
def __init__(self, network, first, last, gateway, dns, lease):
net = ipaddress.ip_network(network)
self.mask = net.netmask
self.free = [ipaddress.ip_address(first) + i
for i in range(int(ipaddress.ip_address(last)) - int(ipaddress.ip_address(first)) + 1)]
self.leases = {}
self.gateway, self.dns, self.lease = gateway, dns, lease
def dora(self, mac):
print(f"DISCOVER from {mac}")
ip = self.leases.get(mac) or self.free.pop(0)
print(f"OFFER {ip} mask {self.mask} gw {self.gateway} dns {self.dns} lease {self.lease}s")
print(f"REQUEST {mac} wants {ip}")
self.leases[mac] = ip
print(f"ACK {ip} is leased to {mac}\n")
server = DhcpServer("192.168.1.0/24", "192.168.1.100", "192.168.1.199",
gateway="192.168.1.1", dns="192.168.1.1", lease=86400)
server.dora("3c:22:fb:12:34:56")
server.dora("da:a1:19:7c:20:01")
server.dora("3c:22:fb:12:34:56") # same laptop rejoins
print("Free addresses left:", len(server.free))DISCOVER from 3c:22:fb:12:34:56 OFFER 192.168.1.100 mask 255.255.255.0 gw 192.168.1.1 dns 192.168.1.1 lease 86400s REQUEST 3c:22:fb:12:34:56 wants 192.168.1.100 ACK 192.168.1.100 is leased to 3c:22:fb:12:34:56 DISCOVER from da:a1:19:7c:20:01 OFFER 192.168.1.101 mask 255.255.255.0 gw 192.168.1.1 dns 192.168.1.1 lease 86400s REQUEST da:a1:19:7c:20:01 wants 192.168.1.101 ACK 192.168.1.101 is leased to da:a1:19:7c:20:01 DISCOVER from 3c:22:fb:12:34:56 OFFER 192.168.1.100 mask 255.255.255.0 gw 192.168.1.1 dns 192.168.1.1 lease 86400s REQUEST 3c:22:fb:12:34:56 wants 192.168.1.100 ACK 192.168.1.100 is leased to 3c:22:fb:12:34:56 Free addresses left: 98
When the first laptop rejoins, the server gives it the same address again, because the lease is still recorded. Real servers try to do this too, which is why your laptop often keeps the same address at home.
Notice the pool runs from .100 to .199. Addresses below .100 are left out of the pool on purpose, for devices that need fixed addresses such as the router and a printer. Keeping fixed addresses outside the pool avoids conflicts.
Lease renewal
A client does not wait until its lease runs out. RFC 2131 sets two timers by default:
- T1 = 50% of the lease time: the client asks its own server directly (unicast) to renew.
- T2 = 87.5% of the lease time: if the server has not answered, the client broadcasts to any server.
- If the lease ends with no renewal, the client must stop using the address and start DORA again.
lease = 24 * 3600 # 24 hours, in seconds
t1 = lease * 0.5
t2 = lease * 0.875
for name, t in [("T1 (renew)", t1), ("T2 (rebind)", t2), ("Lease ends", lease)]:
print(f"{name:<12} after {t / 3600:>5.1f} hours")T1 (renew) after 12.0 hours T2 (rebind) after 21.0 hours Lease ends after 24.0 hours
DHCP options
All the extra settings travel as options at the end of the DHCP message. Each option is coded as type, length, value (TLV): one byte for the option number, one byte for the length, then the value. Some common options:
| Option | Meaning |
|---|---|
| 1 | Subnet mask |
| 3 | Router (default gateway) |
| 6 | DNS servers |
| 51 | Lease time in seconds |
| 53 | DHCP message type (1 Discover, 2 Offer, 3 Request, 5 ACK) |
| 255 | End of options |
Let us decode a small block of options.
import ipaddress, struct
raw = bytes.fromhex("350105" "0104ffffff00" "0304c0a80101" "3304" "00015180" "ff")
types = {1: "Discover", 2: "Offer", 3: "Request", 5: "ACK"}
i = 0
while raw[i] != 255:
code, length = raw[i], raw[i + 1]
value = raw[i + 2:i + 2 + length]
if code == 53:
print("Message type:", types[value[0]])
elif code in (1, 3):
name = "Subnet mask" if code == 1 else "Router"
print(f"{name}:", ipaddress.ip_address(value))
elif code == 51:
print("Lease time:", struct.unpack("!I", value)[0], "seconds")
i += 2 + lengthMessage type: ACK Subnet mask: 255.255.255.0 Router: 192.168.1.1 Lease time: 86400 seconds
DHCP across routers: relay agents
DORA starts with a broadcast, and routers do not forward broadcasts. So a DHCP server in one subnet cannot hear clients in other subnets. Instead of putting a server in every VLAN, network engineers configure a DHCP relay agent on the router (on Cisco, the ip helper-address command). The relay receives the client's broadcast and forwards it as a unicast to the central server, adding the subnet it came from, so the server picks the right pool.
Reservations and useful commands
A reservation ties an address to a MAC address, so a device such as a shop's billing printer always gets the same address while still using DHCP.
On Windows you can release and renew your own lease:
ipconfig /release
ipconfig /renew
ipconfig /allipconfig /all shows the DHCP server, the lease obtained and the lease expiry time. On Linux, ip addr shows the address and NetworkManager tools show the lease.
Common mistakes
- Getting the DORA order wrong. It is Discover, Offer, Request, Acknowledge.
- Thinking DHCP uses TCP. It uses UDP: port 67 for servers and 68 for clients.
- Putting static addresses inside the pool. The server may hand the same address to someone else.
- Forgetting the relay. Without a relay agent, clients in other subnets never reach the server.
Key points
- DHCP gives clients an address, mask, gateway, DNS servers and a lease time automatically.
- The exchange is Discover, Offer, Request, Acknowledge (DORA), starting with broadcasts over UDP 67/68.
- Clients renew at T1 (50%) and rebind at T2 (87.5%) of the lease.
- Options are type-length-value fields such as 1 (mask), 3 (router), 6 (DNS), 51 (lease) and 53 (message type).
- Relay agents forward DHCP between subnets because routers do not pass broadcasts.
- Use reservations for fixed devices and DHCP snooping against rogue servers.
Exercise
Extend DhcpServer with a release(mac) method that puts the address back at the front of the free list and deletes the lease. Lease addresses to three devices, release the second one, then lease to a fourth device. Which address does the fourth device get?
Show solution
import ipaddress
class DhcpServer:
def __init__(self, first, count):
start = ipaddress.ip_address(first)
self.free = [start + i for i in range(count)]
self.leases = {}
def lease(self, mac):
ip = self.leases.get(mac) or self.free.pop(0)
self.leases[mac] = ip
print(f"{mac} -> {ip}")
def release(self, mac):
ip = self.leases.pop(mac)
self.free.insert(0, ip)
print(f"{mac} released {ip}")
s = DhcpServer("192.168.1.100", 10)
for mac in ["aa:01", "aa:02", "aa:03"]:
s.lease(mac)
s.release("aa:02")
s.lease("aa:04")aa:01 -> 192.168.1.100 aa:02 -> 192.168.1.101 aa:03 -> 192.168.1.102 aa:02 released 192.168.1.101 aa:04 -> 192.168.1.101
The released address goes back to the front of the pool, so the fourth device receives 192.168.1.101. Real servers often prefer to hand out addresses that have been free the longest, to avoid confusing old connections.
Quick Quiz
1. What is the correct order of DHCP messages?
DORA: Discover, Offer, Request, Acknowledge.
2. Which UDP port does a DHCP server listen on?
Servers use 67 and clients use 68.
3. With an 8-hour lease, when does the client first try to renew (T1)?
T1 is 50% of the lease time.
4. Why is a DHCP relay agent needed?
The relay turns the broadcast into a unicast towards the central server.
Learn Computer Networks with a mentor
Free lessons get you started. Our Computer Science Fundamentals training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
