A fast, single-binary DHCPv4 server written in Go (built on insomniacslk/dhcp) with:
- JSON configuration (strictly validated; shows line/column on errors)
- Address pools, exclusions, reservations (with notes & metadata)
- Sticky leases, optional compaction, and tolerant migration of older lease formats
- Per-device overrides (DNS/TFTP/Bootfile)
- Classless static routes (121), optional mirror to 249, custom vendor option 43 payloads
- Live config reload (watcher +
SIGHUP) - Banned MAC handling (from config and/or env var)
- Rich CLI: run server, view leases, stats, full subnet grid, config checking, config management (add/remove), search IPs, and reload
- Clear logging (file and/or console), PID file, graceful shutdown
- Background monitoring: ARP anomaly detection and rogue DHCP server detection
- Console access via UNIX socket or TCP/IP for remote management
- Quick start
- Build
- Why root or CAP_NET_BIND_SERVICE?
- Configuration
- Leases DB
- Running
- Commands
- Flags
- Environment variables
- Signals & lifecycle
- Logging
- Security notes
- Appendix: IP selection policy
# 1) Build
go build -o dhcplane .
# 2) Prepare config and empty lease DB
cp config.example.json dhcplane.config
echo '{"by_ip":{},"by_mac":{}}' > dhcplane.leases
# 3) Allow binding to UDP:67 without root (Linux)
sudo setcap 'cap_net_bind_service=+ep' "$(pwd)/dhcplane"
# 4) Run
./dhcplane serve --consoleTip: add
--consoleto expose the interactive console socket (or TCP ifconsole_tcp_addressis configured) while still seeing logs on stdout/stderr.
Requires Go 1.21+.
go build -o dhcplane .Binary is self-contained; no external services needed.
DHCPv4 servers listen on UDP port 67 (privileged). Options:
- Run as root, or
- Grant the binary capability (Linux):
sudo setcap 'cap_net_bind_service=+ep' /path/to/dhcplaneThis attaches to that specific binary. Recompiling creates a new file, so you must re-apply setcap to the new binary path.
The server loads a strict JSON file (unknown fields are rejected). On serve startup and on every reload/watch event, config is validated.
{
"interface": "eth0",
"server_ip": "192.168.178.1",
"subnet_cidr": "192.168.178.0/24",
"gateway": "192.168.178.1",
"compact_on_load": false,
"dns": ["1.1.1.1", "9.9.9.9"],
"domain": "lan",
"lease_db_path": "dhcplane.leases",
"pid_file": "dhcplane.pid",
"lease_seconds": 86400,
"lease_sticky_seconds": 86400,
"auto_reload": true,
"pools": [
{"start": "192.168.178.50", "end": "192.168.178.199"}
],
"exclusions": ["192.168.178.100"],
"reservations_path": "dhcplane.reservations",
"ntp": ["192.168.178.1"],
"mtu": 1500,
"tftp_server_name": "192.168.178.2",
"bootfile_name": "pxelinux.0",
"wpad_url": "http://wpad.lan/wpad.dat",
"wins": ["192.168.178.3"],
"domain_search": ["lan", "corp.lan"],
"static_routes": [
{"cidr": "10.10.0.0/16", "gateway": "192.168.178.254"}
],
"mirror_routes_to_249": true,
"vendor_specific_43_hex": "01:04:de:ad:be:ef",
"device_overrides": {
"00-11-22-33-44-55": {
"dns": ["192.168.178.53"],
"tftp_server_name": "192.168.178.2",
"bootfile_name": "special.efi"
}
},
"banned_macs": {
"dc:ed:83:f3:68:5b": {
"first_seen": 1725550000,
"note": "guest device blocked",
"equipment_type": "Gateway",
"manufacturer": "Unknown"
}
},
"equipment_types": ["Switch","Router","AP","Modem","Gateway","Printer"],
"management_types": ["ssh","web","telnet","serial","console"],
"console_max_lines": 10000,
"console_tcp_address": "",
"logging": {
"path": "",
"filename": "dhcplane.log",
"max_size": 20,
"max_backups": 5,
"max_age": 0,
"compress": true
},
"detect_dhcp_servers": {
"enabled": true,
"active_probe": "off",
"probe_interval": 600,
"first_scan": 60,
"rate_limit": 6,
"whitelist_servers": []
},
"arp_anomaly_detection": {
"enabled": false,
"probe_interval": 1800,
"first_scan": 60
}
}Defaults:
lease_seconds:86400(24h) if ≤ 0lease_sticky_seconds:86400(sticky window) if ≤ 0- At least one pool is required
- Unknown fields are rejected (strict mode)
lease_db_path: defaults todhcplane.leaseswhen omittedpid_file: defaults todhcplane.pidwhen omittedreservations_path: defaults todhcplane.reservationswhen omitted (relative paths are resolved relative to config file directory)authoritative: defaults totruewhen unsetequipment_types: defaults to["Switch","Router","AP","Modem","Gateway"]when emptymanagement_types: defaults to["ssh","web","telnet","serial","console"]when emptyconsole_max_lines: defaults to10000when ≤ 0detect_dhcp_servers.enabled: defaults totrueif other detection fields are setdetect_dhcp_servers.probe_interval: defaults to600seconds (minimum 60)detect_dhcp_servers.first_scan: defaults to60seconds (minimum 10)detect_dhcp_servers.rate_limit: defaults to6events per minutearp_anomaly_detection.probe_interval: defaults to1800secondsarp_anomaly_detection.first_scan: defaults to60seconds
{
"interface": "",
"server_ip": "192.168.178.1",
"subnet_cidr": "192.168.178.0/24",
"gateway": "192.168.178.1",
"dns": ["1.1.1.1","9.9.9.9"],
"lease_db_path": "dhcplane.leases",
"lease_seconds": 86400,
"lease_sticky_seconds": 86400,
"auto_reload": true,
"pools": [
{"start":"192.168.178.50","end":"192.168.178.199"}
],
"exclusions": ["192.168.178.100"],
"reservations_path": "dhcplane.reservations",
"domain": "lan",
"ntp": ["192.168.178.1"],
"mtu": 1500,
"tftp_server_name": "192.168.178.2",
"bootfile_name": "pxelinux.0",
"wpad_url": "",
"wins": [],
"domain_search": ["lan"],
"static_routes": [],
"mirror_routes_to_249": false,
"vendor_specific_43_hex": "",
"device_overrides": {},
"banned_macs": {},
"equipment_types": ["Switch","Router","AP","Modem","Gateway"],
"management_types": ["ssh","web","telnet","serial","console"],
"console_max_lines": 10000,
"console_tcp_address": "",
"logging": {
"path": "",
"filename": "dhcplane.log",
"max_size": 20,
"max_backups": 5,
"max_age": 0,
"compress": true
},
"detect_dhcp_servers": {
"enabled": true,
"active_probe": "off",
"probe_interval": 600,
"first_scan": 60,
"rate_limit": 6,
"whitelist_servers": []
},
"arp_anomaly_detection": {
"enabled": false,
"probe_interval": 1800,
"first_scan": 60
}
}Reservations are stored in a separate JSON file (default: dhcplane.reservations, configurable via reservations_path). The file format is a map of MAC addresses to reservation objects:
{
"aa:bb:cc:dd:ee:ff": {
"ip": "192.168.178.10",
"note": "human note",
"first_seen": 1725550000,
"equipment_type": "Switch",
"manufacturer": "Ubiquiti",
"management_type": "web",
"management_interface": "https://192.168.178.10"
}
}MAC keys accept aa:bb:..., aa-bb-..., or aabb... formats.
Backwards compatible: the legacy
{"mac":"ip"}style is also accepted on load.
Banned MACs can be declared in config under banned_macs (with optional metadata), and/or via env var dhcplane_BANNED_MACS (comma/space/newline-separated list, any delimiter style, e.g. aabbccddeeff, aa:bb:..., aa-bb-...).
Banned MACs:
- Are logged on contact
- Receive a NAK on DHCP REQUEST when the config’s
authoritativeflag is true (default) - Are marked in the grid and details outputs
device_overrides lets you override only:
- DNS servers (option 6)
- TFTP server name (66)
- Bootfile name (67)
Global config remains in effect for everything else.
static_routes becomes Option 121 (RFC 3442). If mirror_routes_to_249 is true, the same payload is also sent as proprietary Microsoft option 249.
"static_routes": [
{"cidr":"10.10.0.0/16","gateway":"192.168.178.254"}
]Provide a raw hex payload in many styles: "01:04:de:ad:be:ef", "01 04 de ad be ef", "hex:0104deadbeef", "0x01,0x04,0xDE...".
dhcplane.leases (or the path specified in lease_db_path) is a simple map persisted by the server:
{
"by_ip": {
"192.168.178.100": {
"mac": "aa:bb:cc:dd:ee:ff",
"ip": "192.168.178.100",
"hostname": "host-name",
"allocated_at": 1725551111,
"expiry": 1725637511,
"first_seen": 1725550000
}
},
"by_mac": {
"aa:bb:cc:dd:ee:ff": { /* same structure as above */ }
}
}All timestamps are epoch seconds. Formatting to local time happens only when printing.
If you used a prior version that stored RFC3339 timestamps, the server will tolerantly read and coerce them to epoch on load.
- Sticky window (
lease_sticky_seconds): influences IP selection so a device tends to get the same address again (even after expiry within the sticky window), as long as it’s safe. - Compaction removes leases that expired longer than the sticky window ago. If
compact_on_loadis true, compaction runs at startup and on reload.
The server listens on UDP:67 on the configured interface (or all interfaces if empty). It is authoritative by default (sends NAKs on invalid requests).
Serve with console echo and file log:
./dhcplane serve \
--config ./dhcplane.config \
--consoleCheck the config:
./dhcplane check -c ./dhcplane.configLive reload via PID file (default dhcplane.pid):
./dhcplane reload -c ./dhcplane.configList current leases (pretty JSON):
./dhcplane leases -c ./dhcplane.configSearch for an IP address:
./dhcplane search 192.168.178.100 -c ./dhcplane.configStats & tables:
# Summary + leased/expiring/expired tables
./dhcplane stats -c ./dhcplane.config
# Full subnet table (hides free addresses) with Type column
./dhcplane stats -c ./dhcplane.config --details
# colour grid of the whole subnet
./dhcplane stats -c ./dhcplane.config --gridAdd or update a reservation:
# MAC, IP, then an optional free-form note
./dhcplane manage add dc:ed:83:f3:68:5b 192.168.178.55 "kitchen display"Remove a reservation:
./dhcplane manage remove dc:ed:83:f3:68:5bStart the DHCP server. Validates config before binding. Creates/updates a PID file. Supports SIGHUP reload and graceful termination.
Key features at runtime:
- Sticky leases
- Reservation enforcement over leases
- Per-device overrides (DNS/TFTP/Bootfile)
- Vendor option 43
- Routes 121 (+249 mirror)
- WPAD (252), WINS (44), MTU (26), Domain(15), Domain search (119)
- Auto-reload (filesystem watcher) when
auto_reloadis true
Print leases from the lease database (default: dhcplane.leases) as a JSON array with formatted timestamps (local time), including AllocatedAt, Expiry, and FirstSeen.
Print allocation rates for the last 1m/1h/24h/7d/30d and lease groupings.
Flags:
--details: show a unified table across the whole subnet with Type classification:leased(active leases)reserved(IP is fixed in config, not currently leased)banned-mac(active lease owned by a banned MAC)banned-ip(network/broadcast/server/gateway/exclusions/declined quarantine)free(hidden in details output)
--grid: render a colour block grid (green free, red leased, brown reserved, light-gray banned-mac, dark-gray banned/excluded IPs). Requires a colour terminal.
Strictly validate the config file (default: dhcplane.config). Unknown fields, wrong types, etc., return precise line/column.
Reads PID from pid_file in the config and sends SIGHUP. Before signaling, re-validates the config and refuses to reload if invalid.
Manipulate reservations in the reservations file (default: dhcplane.reservations):
manage add <mac> <ip> [note...]- Validates MAC and IPv4
- Warns if IP is out of subnet or currently leased to a different MAC
- Inserts/updates with
first_seen(epoch) if missing, preserves any existing metadata
manage remove <mac>- Deletes the reservation if present
Edits are written atomically via a temporary file → rename.
Search for an IP address in reservations and leases:
search <ip>- Displays all information about the IP address
- Shows reservation details (MAC, note, metadata) if found
- Shows lease details (MAC, hostname, timestamps, expiry status) if found
- Warns if MAC addresses don't match between reservation and lease
- Suggests running an ARP scan if the IP is not found in either reservations or leases
Global flags (apply to all commands unless noted):
-c, --config stringPath to JSON config (defaultdhcplane.config)--consoleServe the interactive console over UNIX socket (logs always print to stdout/stderr)
Command-specific flags:
stats --detailsstats --gridconsole attach --transparentconsole attach --tcp <address>(e.g.,--tcp localhost:9090)
-
dhcplane_BANNED_MACSExtra banned MACs; separated by comma/space/newline. Acceptsaa:bb:...,aa-bb-..., oraabb.... -
COLUMNSIf set, used as terminal width hint for grid layout.
SIGHUPReload config. Ifcompact_on_loadis true, compaction runs post-reload.SIGINT/SIGTERMGraceful shutdown: server stops, leases DB is saved, watcher/log files closed.
When auto_reload is true, the process also watches the config file and reservations file directories and applies changes shortly after writes (with validation and first-seen stamping for reservations/banned MACs where missing).
- File logger is created using the
loggingsection (defaultdhcplane.log); logs are always mirrored to stdout/stderr. - Internal errors are additionally printed to stderr in red for operator visibility.
- PID file is written at the
pid_filepath in the config.
Example log lines:
START iface="" bind=0.0.0.0:67 server_ip=192.168.178.1 subnet=192.168.178.0/24 gateway=192.168.178.1 lease=24h0m0s sticky=24h0m0s
AUTO-RELOAD: watching ./dhcplane.config
CONSOLE listening on UNIX socket + TCP 0.0.0.0:9090
DETECT start mode=off interval=600s rate_limit=6/min iface=eth0 whitelist=0
DETECT scan started iface=eth0
DETECT scan completed iface=eth0 (no foreign servers found)
DISCOVER from aa:bb:cc:dd:ee:ff hostname="printer" xid=0x12345678
OFFER aa:bb:cc:dd:ee:ff -> 192.168.178.100
ACK aa:bb:cc:dd:ee:ff <- 192.168.178.100 lease=24h0m0s (alloc=2025/09/05 20:40:01, exp=2025/09/06 20:40:01)
DETECT scan started iface=eth0
FOREIGN-DHCP-SERVER detected server_ip=192.168.178.254 from=192.168.178.254 iface=eth0
DETECT scan completed iface=eth0 (found 1 server(s))
ARP-ANOMALY ip=192.168.178.107 mac=d0:27:03:4f:22:60 iface=eth0 reason=unknown found=arp reserved=false leased=false excluded=false
The server includes two optional background monitoring tasks:
Detects devices on the network that aren't managed by the DHCP server:
- Scans ARP tables periodically
- Identifies devices with IPs that aren't leased or reserved
- Detects MAC address mismatches
- Reports banned MACs found in ARP
Configuration:
"arp_anomaly_detection": {
"enabled": false,
"probe_interval": 1800,
"first_scan": 60
}Detects rogue/unauthorized DHCP servers on the network:
- Sends periodic DHCP DISCOVER probes (using MAC
00:00:00:00:00:00which is silently ignored by the server) - Listens for OFFER responses from other servers
- Logs when scans start and complete (even if no servers found)
- Logs foreign DHCP servers not in the whitelist
- Helps identify network conflicts
Configuration:
"detect_dhcp_servers": {
"enabled": true,
"active_probe": "off",
"probe_interval": 600,
"first_scan": 60,
"rate_limit": 6,
"whitelist_servers": []
}Both tasks run independently in background goroutines and can be enabled/disabled via config.
The interactive console can be accessed via:
-
UNIX socket (default): Local access via socket file
./dhcplane console attach
-
TCP/IP (when configured): Remote access over network
"console_tcp_address": "0.0.0.0:9090"
./dhcplane console attach --tcp 192.168.1.2:9090
When console_tcp_address is set, the server listens on both UNIX socket and TCP simultaneously, allowing both local and remote access.
- Only enable
authoritativein the config on networks where this server should NAK competing/invalid requests. - Use
exclusionsto protect infrastructure IPs that are inside pools. - Banned MACs are enforced at request time; keep in mind MAC spoofing is possible on L2.
- Consider running under a service account with only
cap_net_bind_servicecapability if possible, not full root. - Always validate the config file (default:
dhcplane.config) withcheckbefore deploying edits in production. - If enabling TCP console access, consider firewall rules to restrict access to trusted networks.
- Reservation wins: If the MAC has a reservation, use it (unless actively leased to a different MAC).
- Sticky preference: If the MAC had a prior lease, try that IP again, subject to safety (in-subnet, not excluded/declined/reserved for someone else, not actively leased by another MAC).
- Brand-new IPs: Prefer addresses never seen in the DB.
- Recycle expired: If no new IPs, reuse expired, safe IPs (respecting reservations).
- If none apply: pool exhausted → NAK (when authoritative) or silent failure.