Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

pyDHCP

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.

Usage

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.

Interactive mode

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.

Keys

Key Action
q quit
s print a one-line status
Ctrl-C quit

Options

-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.

What it does

  • Answers DISCOVER with an OFFER and REQUEST with an ACK, both carrying the configured address, mask, lease time, and optional router/DNS.
  • Answers plain BOOTP requests (no option 53) with a BOOTP reply, including siaddr and boot file for netboot.
  • NAKs a client that insists on a different address, and reports DECLINE (address already in use on the network) and RELEASE.
  • Replies via the relay agent (giaddr) when the request came through one, unicasts to ciaddr on 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.

Example session

+------------------------------------------------+
| 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

Building a standalone executable

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.py

The 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 (giving dist\pydhcp\) if you prefer a faster start.
  • --console — required. This is a console program; --noconsole would hide the status output and break the q key.
  • --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.py

Points 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/ and pydhcp.spec are generated; they are already in .gitignore.

Windows notes

  • 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-interfaces to 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 uses IP_UNICAST_IF; without -i, replies follow the normal routing table, which on a multi-homed machine may be the wrong card.

Notes

  • 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 -i on 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.

About

Python DHCP/BOOTP Server for Assigning IP to Devices

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages