A single-file, single-client DHCP/BOOTP server. Give it a MAC address and an IP address; it waits for that client to ask for an address and hands it the one you specified. Everything else on the wire is ignored (but reported, so you can see what is out there).
Useful for bringing up embedded boards, netbooting a machine, or bootstrapping a device that only speaks DHCP — without touching the real DHCP server.
Runs on Linux and Windows with no dependencies beyond the Python 3 standard
library, and can be turned into a standalone pydhcp.exe that does not need
Python installed — see Building a standalone executable.
sudo ./pydhcp.py aa:bb:cc:dd:ee:ff 192.168.1.50 # Linux
pydhcp.exe aa:bb:cc:dd:ee:ff 192.168.1.50 # Windows (as Administrator)
MAC addresses may be written as aa:bb:cc:dd:ee:ff, aa-bb-cc-dd-ee-ff,
aabb.ccdd.eeff or bare hex.
Root (Linux) or an elevated prompt (Windows) is required, because DHCP servers listen on UDP port 67.
Run it with no arguments and it asks for what it needs, one step at a time:
sudo ./pydhcp.py # Linux
pydhcp.exe # Windows (as Administrator)
+------------------------------------------+
| pyDHCP 1.1.0 - interactive setup |
| answer the questions below; Ctrl-C quits |
+------------------------------------------+
09:15:02 info Step 1 of 3 MAC address of the device to serve
formats: aa:bb:cc:dd:ee:ff, aa-bb-cc-dd-ee-ff, aabb.ccdd.eeff or aabbccddeeff
client MAC address: aa:bb:cc:dd:ee:ff
09:15:06 ok client MAC set to aa:bb:cc:dd:ee:ff
09:15:06 info Step 2 of 3 IPv4 address to assign to that device
example: 192.168.1.50
IP address to assign: 192.168.1.50
09:15:09 ok will assign 192.168.1.50
09:15:09 info Step 3 of 3 network card to serve from
1) lo 127.0.0.1 / 255.0.0.0
2) enp2s0 192.168.1.220 / 255.255.255.0 <- same network as 192.168.1.50
3) eno1 172.20.154.99 / 255.255.255.0
4) (any) listen on every interface
select a network card by number [2]:
The card whose subnet contains the address you just typed is the default, so
Enter accepts it. A card can also be picked by name or by its own IP address.
Answers are validated as they are entered — a malformed MAC, an address that
cannot belong to a device (0.0.0.0, broadcast, multicast, loopback), or a
card on the wrong network each get an explanation and a second chance. A
summary is shown before the server starts.
Only the missing pieces are asked for, so sudo ./pydhcp.py -i eth0 prompts for
the MAC and the IP and skips the card list. When there is no console to prompt
on (a pipe, a service), the arguments remain required.
| Key | Action |
|---|---|
q |
quit |
s |
print a one-line status |
Ctrl-C |
quit |
-i, --interface IFACE serve on this interface only; accepts a name,
a Windows adapter description, or a local IP
--list-interfaces list this machine's interfaces and exit
-m, --subnet-mask MASK mask offered to the client (default: auto-detected)
-r, --router IP default gateway offered to the client
-d, --dns IP[,IP...] DNS servers offered to the client
-s, --server-ip IP this server's address (default: auto-detected)
-l, --lease-time SECS lease duration (default: 3600)
--next-server IP BOOTP siaddr, for netboot
--boot-file NAME BOOTP boot file name, for netboot
--keep-running keep serving after the lease (renewals, reboots)
--heartbeat SECS "still waiting" interval, 0 to disable (default: 30)
-V, --version print the version number and exit
--port / --client-port non-standard ports, handy for testing
--no-color plain output
Server IP, interface and subnet mask are worked out from the routing table, so in the common case only the MAC and the IP are needed.
- Answers
DISCOVERwith anOFFERandREQUESTwith anACK, both carrying the configured address, mask, lease time, and optional router/DNS. - Answers plain BOOTP requests (no option 53) with a BOOTP reply, including
siaddrand boot file for netboot. NAKs a client that insists on a different address, and reportsDECLINE(address already in use on the network) andRELEASE.- Replies via the relay agent (
giaddr) when the request came through one, unicasts tociaddron renewal, and otherwise broadcasts — so no ARP-table poking is needed for a client that has no address yet. - Exits 0 once the address was acknowledged, 1 otherwise.
+------------------------------------------------+
| pyDHCP 1.1.0 - single-client DHCP/BOOTP server |
| target MAC : aa:bb:cc:dd:ee:ff |
| assign IP : 192.168.1.50 |
| ... |
+------------------------------------------------+
09:15:10 wait listening on UDP port 67 for aa:bb:cc:dd:ee:ff...
09:15:11 info DISCOVER from aa:bb:cc:dd:ee:ff xid=0x12345678 (host=board)
09:15:11 ok OFFER sent to broadcast:68 192.168.1.50 -> aa:bb:cc:dd:ee:ff
09:15:11 wait waiting for the client to REQUEST the offer...
09:15:11 info REQUEST from aa:bb:cc:dd:ee:ff xid=0x12345678 (wants=192.168.1.50)
09:15:11 ok ACK sent to broadcast:68 192.168.1.50 -> aa:bb:cc:dd:ee:ff
09:15:11 ok LEASE ACTIVE - 192.168.1.50 is assigned for 3600s
PyInstaller bundles the interpreter and the script into one .exe, so the
target machine needs no Python installation. There is no cross-compiling —
build the Windows executable on a Windows machine (a VM works fine);
building on Linux produces a Linux binary.
On Windows, with Python 3.8+ installed:
py -m pip install --upgrade pyinstaller
py -m PyInstaller --onefile --console --name pydhcp --uac-admin pydhcp.pyThe result is dist\pydhcp.exe, a single self-contained file — copy it
anywhere and run it.
--onefile— one .exe instead of a folder. It unpacks to a temp directory at startup, costing about a second; drop the flag (givingdist\pydhcp\) if you prefer a faster start.--console— required. This is a console program;--noconsolewould hide the status output and break theqkey.--uac-admin— makes Windows prompt for elevation on launch, which the program needs to bind UDP port 67. Without it, run the .exe from an Administrator prompt.--icon app.ico— optional, if you want your own icon.
The same command on Linux produces a standalone dist/pydhcp ELF binary:
python3 -m pip install --user pyinstaller
python3 -m PyInstaller --onefile --console --name pydhcp pydhcp.pyPoints worth knowing:
- Antivirus and SmartScreen frequently flag fresh PyInstaller one-file executables as suspicious — the packaging pattern resembles malware droppers. Signing the .exe with a code-signing certificate is the real fix; otherwise expect a "Windows protected your PC → More info → Run anyway" prompt.
- The binary is tied to the OS and architecture it was built on. A 32-bit Python produces a 32-bit exe (which also runs on 64-bit Windows).
- Build with a Python that still supports your oldest target: the Windows builds of Python 3.9+ do not run on Windows 7.
build/,dist/andpydhcp.specare generated; they are already in.gitignore.
-
Run it elevated. Without Administrator rights the bind to port 67 fails, and the program says so rather than failing silently.
-
Windows Firewall will prompt on first run — allow it, or DHCP requests never reach the program. If the prompt was dismissed, add the rule manually from an elevated prompt:
netsh advfirewall firewall add rule name="pyDHCP" dir=in action=allow protocol=UDP localport=67
-
The built-in DHCP Client service uses port 68, not 67, so it does not conflict. The DHCP Server role does — stop it first if it is installed.
-
Interfaces are named by adapter description on Windows, so use
pydhcp.exe --list-interfacesto see them, then pass one to-i(a substring such as-i "USB 2.0 LAN"or the adapter's own IP works too). Pinning to an interface usesIP_UNICAST_IF; without-i, replies follow the normal routing table, which on a multi-homed machine may be the wrong card.
- Port 67 can only be held by one process: stop
dnsmasq/isc-dhcp-server(or the Windows DHCP Server role) first, or bind to a spare interface with-i. - Running this on a network that already has a DHCP server means the client
takes whichever offer arrives first. Use
-ion an isolated link, or take the other server down. - Leases are not persisted — this is a one-shot tool, not a replacement for a real DHCP server.