Of course! Creating a DHCP client in Python is an excellent way to understand networking protocols at a low level. We'll build a simple one from scratch using Python's built-in socket library.

What is a DHCP Client?
A DHCP (Dynamic Host Configuration Protocol) client is a program that automatically configures a device's network settings by requesting them from a DHCP server. The key settings it requests are:
- IP Address
- Subnet Mask
- Default Gateway (Router)
- DNS Server
The process happens through a series of messages, most commonly over UDP port 67 (server) and 68 (client).
The DHCP Process in a Nutshell
A standard DHCP client-server interaction involves four steps (often called the DORA process):
- Discover: The client broadcasts a "DHCP Discover" message to find any available DHCP servers on the network.
- Offer: A DHCP server that receives the Discover message responds with a "DHCP Offer". This offer includes an available IP address and other network configuration details.
- Request: The client then broadcasts a "DHCP Request" message, telling the server it wants to accept the specific offer it received.
- Acknowledge (ACK): The server finalizes the configuration by sending a "DHCP Acknowledge" message. The client can now use the assigned IP address.
We will simulate this entire process.

Prerequisites
You need to run this script on a network where you have a DHCP server running (e.g., your home or office router). You should not run this on a critical system, as it will attempt to acquire a new lease, potentially interfering with an existing one.
The Python Code
This script will construct and send the raw DHCP packets. It's a simplified version but captures the essence of the protocol.
import socket
import struct
import random
import time
import ipaddress
# --- DHCP Constants ---
# DHCP Message Types
DHCPDISCOVER = 1
DHCPOFFER = 2
DHCPREQUEST = 3
DHCPACK = 5
DHCPNAK = 6
DHCPRELEASE = 7
# DHCP Options
DHCP_MESSAGE_TYPE = 53
DHCP_SERVER_ID = 54 # IP address of the DHCP server
DHCP_LEASE_TIME = 51
DHCP_SUBNET_MASK = 1
DHCP_ROUTER = 3
DHCP_DNS_SERVER = 6
DHCP_END = 255
# --- Network Configuration ---
# Use a broadcast address to find the DHCP server
BROADCAST_IP = '255.255.255.255'
DHCP_SERVER_PORT = 67
DHCP_CLIENT_PORT = 68
def create_dhcp_discover(xid):
"""
Creates a DHCP Discover packet.
"""
# Create a BOOTP header (the basis for DHCP)
# See RFC 2131 for details on the BOOTP/DHCP header format
bootp = struct.pack(
'!BBBBHHHHH',
1, # op (1 = BOOTREQUEST)
1, # htype (1 = 10mb ethernet)
6, # hlen (6 bytes for MAC address)
0, # hops
xid, # transaction ID (a random number)
0, # seconds
0, # flags
0, # ciaddr (client IP - 0.0.0.0)
0, # yiaddr (your IP - 0.0.0.0)
0, # siaddr (server IP - 0.0.0.0)
0, # giaddr (relay agent IP - 0.0.0.0)
# MAC address placeholder - will be filled by OS
b'\x00' * 16 # chaddr (client hardware address)
)
# DHCP Options
options = struct.pack(
'!BB',
DHCP_MESSAGE_TYPE,
DHCPDISCOVER
)
options += struct.pack('!BB', DHCP_END, 0) # End of options + padding for BOOTP
# The OS will fill in the MAC address in the chaddr field of the BOOTP header.
# We just need to make sure the total length is correct.
# A BOOTP packet is 300 bytes. Our bootp struct is 44 + 16 = 60.
# The options field needs to be padded to 300 - 60 = 240 bytes.
# However, the OS handles this. We just send the BOOTP header and options.
# The total packet will be assembled by the socket sendto function.
return bootp + options
def parse_dhcp_packet(data):
"""
Parses a DHCP packet to extract the message type and server IP.
"""
# Parse BOOTP header
# op, htype, hlen, hops, xid, secs, flags, ciaddr, yiaddr, siaddr, giaddr, chaddr
bootp_header = struct.unpack('!BBBBHHHHH16s208s', data[:236])
op = bootp_header[0]
server_ip = socket.inet_ntoa(bootp_header[9]) # siaddr
# Parse DHCP Options
options_data = data[240:]
options = {}
i = 0
while i < len(options_data):
opt_code = options_data[i]
if opt_code == DHCP_END:
break
opt_len = options_data[i+1]
opt_value = options_data[i+2:i+2+opt_len]
options[opt_code] = opt_value
i += 2 + opt_len
message_type = options.get(DHCP_MESSAGE_TYPE, b'')[0] if DHCP_MESSAGE_TYPE in options else 0
return {
'op': op,
'server_ip': server_ip,
'message_type': message_type,
'options': options
}
def main():
"""
Main function to run the DHCP client.
"""
# Generate a random Transaction ID (XID) to identify our request
xid = random.randint(1, 0xFFFFFFFF)
print(f"Starting DHCP Client with Transaction ID: {xid:x}\n")
# Create a UDP socket
# We need to allow broadcasting
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
client_socket.bind(('', DHCP_CLIENT_PORT))
client_socket.settimeout(10) # Wait for 10 seconds for a response
# 1. DHCP DISCOVER
print("1. Sending DHCP DISCOVER...")
discover_packet = create_dhcp_discover(xid)
client_socket.sendto(discover_packet, (BROADCAST_IP, DHCP_SERVER_PORT))
# 2. DHCPOFFER
try:
print(" Waiting for DHCPOFFER...")
offer_data, server_addr = client_socket.recvfrom(1024)
offer = parse_dhcp_packet(offer_data)
if offer['message_type'] == DHCPOFFER:
print(f" Received DHCPOFFER from server: {offer['server_ip']}")
server_ip = offer['server_ip']
# Extract the offered IP address from the yiaddr field
offered_ip = socket.inet_ntoa(offer_data[16:20])
print(f" Offered IP Address: {offered_ip}")
else:
print(" Received unexpected message type. Exiting.")
return
except socket.timeout:
print(" Timeout waiting for DHCPOFFER. Exiting.")
return
except Exception as e:
print(f" An error occurred: {e}")
return
# 3. DHCP REQUEST
print("\n2. Sending DHCP REQUEST to accept the offer...")
# For simplicity, we will reuse the discover packet and just change the message type.
# A real client would build a new request packet with more details.
request_packet = create_dhcp_discover(xid)
# Overwrite the message type option
# Find the start of the options section and replace the type
request_packet = request_packet[:240] + struct.pack('!BB', DHCP_MESSAGE_TYPE, DHCPREQUEST) + request_packet[242:]
# The request is sent to the specific server that made the offer
client_socket.sendto(request_packet, (server_ip, DHCP_SERVER_PORT))
# 4. DHCP ACK
try:
print(" Waiting for DHCP ACK...")
ack_data, server_addr = client_socket.recvfrom(1024)
ack = parse_dhcp_packet(ack_data)
if ack['message_type'] == DHCPACK:
print(f" Received DHCP ACK from server: {ack['server_ip']}")
# Parse the offered IP from the yiaddr field
assigned_ip = socket.inet_ntoa(ack_data[16:20])
print(f"\n --- Configuration Successful ---")
print(f" Assigned IP Address: {assigned_ip}")
# Parse other options
if DHCP_LEASE_TIME in ack['options']:
lease_time = int.from_bytes(ack['options'][DHCP_LEASE_TIME], 'big')
print(f" Lease Time: {lease_time} seconds")
if DHCP_SUBNET_MASK in ack['options']:
subnet_mask = socket.inet_ntoa(ack['options'][DHCP_SUBNET_MASK])
print(f" Subnet Mask: {subnet_mask}")
if DHCP_ROUTER in ack['options']:
router_ip = socket.inet_ntoa(ack['options'][DHCP_ROUTER])
print(f" Default Gateway: {router_ip}")
if DHCP_DNS_SERVER in ack['options']:
dns_ip = socket.inet_ntoa(ack['options'][DHCP_DNS_SERVER])
print(f" DNS Server: {dns_ip}")
print(" ------------------------------")
elif ack['message_type'] == DHCPNAK:
print(" Received DHCP NAK (Negative Acknowledgement). The server denied the request.")
else:
print(" Received unexpected message type. Exiting.")
except socket.timeout:
print(" Timeout waiting for DHCP ACK. Exiting.")
except Exception as e:
print(f" An error occurred: {e}")
finally:
client_socket.close()
if __name__ == "__main__":
main()
How to Run the Script
- Save the code above as a Python file (e.g.,
dhcp_client.py). - Open your terminal or command prompt.
- Run the script:
python dhcp_client.py
You should see output similar to this (your IPs and server address will be different):
Starting DHCP Client with Transaction ID: 5a1b2c3d
1. Sending DHCP DISCOVER...
Waiting for DHCPOFFER...
Received DHCPOFFER from server: 192.168.1.1
Offered IP Address: 192.168.1.105
2. Sending DHCP REQUEST to accept the offer...
Waiting for DHCP ACK...
Received DHCP ACK from server: 192.168.1.1
--- Configuration Successful ---
Assigned IP Address: 192.168.1.105
Lease Time: 86400 seconds
Subnet Mask: 255.255.255.0
Default Gateway: 192.168.1.1
DNS Server: 8.8.8.8
------------------------------
Important Considerations and Limitations
- MAC Address: The script relies on the underlying operating system to fill in the MAC address (the
chaddrfield in the BOOTP header). We don't need to manually set it. - Simplified Packet Creation: The
create_dhcp_discoverfunction reuses the BOOTP header for both theDISCOVERandREQUESTpackets. A full-fledged client would build more distinctREQUESTpackets, often including the "server identifier" option to specify which offer it's accepting. - No
RELEASE: This client does not send aDHCPRELEASEpacket when it's done. It's a "fire and forget" client. - No Renew/Rebind: It only performs the initial DORA process. It doesn't handle renewing the lease before it expires.
- Permissions: On some systems (especially Linux), you might need root/administrator privileges to create a socket that can send and receive on raw-like ports or to bind to a low port. You may need to run
sudo python dhcp_client.py. - Network Interference: As mentioned, running this will make your machine attempt to get a new IP. It might briefly disconnect you from your network if you already have a lease from the same server. Use it in a safe environment.
