Usage

Assuming that you’ve followed the installation steps, you’re now ready to use this package.

Quick start

aiodhcpwatcher watches the local network for DHCP REQUEST packets and invokes a callback for each one. The callback receives a DHCPRequest carrying the requested IP, the client hostname (decoded leniently — see Hostname decoding), and the source MAC address.

import asyncio
import aiodhcpwatcher

def on_dhcp_request(request: aiodhcpwatcher.DHCPRequest) -> None:
    print(f"{request.mac_address} requested {request.ip_address} ({request.hostname!r})")

async def run() -> None:
    shutdown = await aiodhcpwatcher.async_start(on_dhcp_request)
    try:
        await asyncio.Event().wait()
    finally:
        shutdown()

asyncio.run(run())

async_start returns the watcher’s shutdown callable — call it (or rely on process exit) to stop watching, close the underlying sockets, and cancel any pending auto-recovery.

Permissions

Packet capture requires CAP_NET_RAW (or running as root). Without it, aiodhcpwatcher logs a debug message and returns without raising — the callback simply will not fire. To grant the capability without running as root, on Linux:

sudo setcap cap_net_raw=eip $(readlink -f $(which python))

Selecting interfaces

By default, the watcher listens on scapy’s default interface. To watch specific interfaces, pass their indexes:

import socket
indexes = [socket.if_nametoindex("eth0"), socket.if_nametoindex("wlan0")]
shutdown = await aiodhcpwatcher.async_start(on_dhcp_request, if_indexes=indexes)

If any interface fails to open (e.g. invalid index, missing capability), the watcher logs and stops — it does not partially start. Interfaces that open successfully but cannot be registered with the event loop (e.g. an add_reader failure on Windows) are closed individually while the rest keep running.

Pre-initializing scapy

Importing scapy is slow because it probes the system for routing and interface information. async_start triggers this lazily on first use, which can stall the event loop. If you know you will start the watcher later, you can warm up scapy in the executor first:

await aiodhcpwatcher.async_init()
# ... later ...
shutdown = await aiodhcpwatcher.async_start(on_dhcp_request)

async_init is a no-op after the first successful call.

Auto-recovery

If the underlying socket raises an OSError while reading (e.g. an interface goes down), the watcher stops, then schedules a restart 30 seconds later. Callers do not need to handle transient socket failures manually.

Hostname decoding

DHCP hostnames are supposed to be encoded with IDNA, but many clients send raw UTF-8 (or worse). The handler tries idna first and falls back to utf-8 with errors="replace", so untrusted DHCP traffic cannot crash the handler. The hostname field on DHCPRequest is always a str (possibly empty if no hostname option was present).

Building a standalone packet handler

If you have your own packet source (a pcap file, a different sniffer), you can reuse the parsing logic directly:

handler = aiodhcpwatcher.make_packet_handler(on_dhcp_request)
for packet in my_packet_source:
    handler(packet)

make_packet_handler returns a function that takes a scapy Packet and calls the callback only when the packet is a valid DHCP REQUEST with both an IP and a MAC address.