{"id":35492676,"url":"https://github.com/network-plane/dhcplane","last_synced_at":"2026-01-18T11:56:56.290Z","repository":{"id":313396140,"uuid":"1051264639","full_name":"network-plane/dhcplane","owner":"network-plane","description":"A highly configurable and efficient DHCP Server with multiple features","archived":false,"fork":false,"pushed_at":"2026-01-03T13:58:04.000Z","size":188,"stargazers_count":0,"open_issues_count":5,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-07T02:28:38.660Z","etag":null,"topics":["cli","console","dhcp","dhcp-server","monitor","pools","server","tui"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/network-plane.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-05T17:44:03.000Z","updated_at":"2026-01-03T13:58:08.000Z","dependencies_parsed_at":"2025-09-05T20:24:56.299Z","dependency_job_id":null,"html_url":"https://github.com/network-plane/dhcplane","commit_stats":null,"previous_names":["earentir/dhcplane","network-plane/dhcplane"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/network-plane/dhcplane","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/network-plane%2Fdhcplane","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/network-plane%2Fdhcplane/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/network-plane%2Fdhcplane/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/network-plane%2Fdhcplane/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/network-plane","download_url":"https://codeload.github.com/network-plane/dhcplane/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/network-plane%2Fdhcplane/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28399500,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-13T14:36:09.778Z","status":"ssl_error","status_checked_at":"2026-01-13T14:35:19.697Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","console","dhcp","dhcp-server","monitor","pools","server","tui"],"created_at":"2026-01-03T16:12:14.315Z","updated_at":"2026-01-18T11:56:56.272Z","avatar_url":"https://github.com/network-plane.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# dhcplane\n![dhcplane](https://github.com/user-attachments/assets/9ce25e91-fa42-427c-bfe3-c76e0b3198c1)\n\nA fast, single-binary **DHCPv4 server** written in Go (built on `insomniacslk/dhcp`) with:\n\n- JSON configuration (strictly validated; shows line/column on errors)\n- Address pools, exclusions, reservations (with notes \u0026 metadata)\n- Sticky leases, optional compaction, and tolerant migration of older lease formats\n- Per-device overrides (DNS/TFTP/Bootfile)\n- Classless static routes (121), optional mirror to 249, custom vendor option 43 payloads\n- Live config reload (watcher + `SIGHUP`)\n- Banned MAC handling (from config and/or env var)\n- Rich CLI: run server, view leases, stats, full subnet grid, config checking, config management (add/remove), search IPs, and reload\n- Clear logging (file and/or console), PID file, graceful shutdown\n- Background monitoring: ARP anomaly detection and rogue DHCP server detection\n- Console access via UNIX socket or TCP/IP for remote management\n\n---\n\n## Table of contents\n\n- [Quick start](#quick-start)\n- [Build](#build)\n- [Why root or CAP_NET_BIND_SERVICE?](#why-root-or-cap_net_bind_service)\n- [Configuration](#configuration)\n  - [Full schema](#full-schema)\n  - [Example config](#example-config)\n  - [Reservations format (with metadata)](#reservations-format-with-metadata)\n  - [Banned MACs](#banned-macs)\n  - [Per-device overrides](#per-device-overrides)\n  - [Static routes](#static-routes)\n  - [Vendor option 43](#vendor-option-43)\n- [Leases DB](#leases-db)\n  - [Format](#format)\n  - [Backwards-compatible loading](#backwards-compatible-loading)\n  - [Sticky window \u0026 compaction](#sticky-window--compaction)\n- [Running](#running)\n  - [Examples](#examples)\n- [Commands](#commands)\n  - [`serve`](#serve)\n  - [`leases`](#leases)\n  - [`stats`](#stats)\n  - [`check`](#check)\n  - [`reload`](#reload)\n  - [`manage add/remove`](#manage-addremove)\n  - [`search`](#search)\n- [Flags](#flags)\n- [Environment variables](#environment-variables)\n- [Signals \u0026 lifecycle](#signals--lifecycle)\n- [Logging](#logging)\n- [Security notes](#security-notes)\n- [Appendix: IP selection policy](#appendix-ip-selection-policy-summary)\n\n---\n\n## Quick start\n\n```bash\n# 1) Build\ngo build -o dhcplane .\n\n# 2) Prepare config and empty lease DB\ncp config.example.json dhcplane.config\necho '{\"by_ip\":{},\"by_mac\":{}}' \u003e dhcplane.leases\n\n# 3) Allow binding to UDP:67 without root (Linux)\nsudo setcap 'cap_net_bind_service=+ep' \"$(pwd)/dhcplane\"\n\n# 4) Run\n./dhcplane serve --console\n```\n\n\u003e Tip: add `--console` to expose the interactive console socket (or TCP if `console_tcp_address` is configured) while still seeing logs on stdout/stderr.\n\n---\n\n## Build\n\nRequires Go 1.21+.\n\n```bash\ngo build -o dhcplane .\n```\n\nBinary is self-contained; no external services needed.\n\n---\n\n## Why root or `CAP_NET_BIND_SERVICE`?\n\nDHCPv4 servers listen on UDP port **67** (privileged). Options:\n\n- Run as root, **or**\n- Grant the binary capability (Linux):\n\n```bash\nsudo setcap 'cap_net_bind_service=+ep' /path/to/dhcplane\n```\n\nThis attaches to that specific binary. **Recompiling creates a new file**, so you must re-apply `setcap` to the new binary path.\n\n---\n\n## Configuration\n\nThe server loads a strict JSON file (unknown fields are rejected). On `serve` startup and on every reload/watch event, config is validated.\n\n### Full schema\n\n```json\n{\n  \"interface\": \"eth0\",\n  \"server_ip\": \"192.168.178.1\",\n  \"subnet_cidr\": \"192.168.178.0/24\",\n  \"gateway\": \"192.168.178.1\",\n  \"compact_on_load\": false,\n  \"dns\": [\"1.1.1.1\", \"9.9.9.9\"],\n  \"domain\": \"lan\",\n  \"lease_db_path\": \"dhcplane.leases\",\n  \"pid_file\": \"dhcplane.pid\",\n  \"lease_seconds\": 86400,\n  \"lease_sticky_seconds\": 86400,\n  \"auto_reload\": true,\n\n  \"pools\": [\n    {\"start\": \"192.168.178.50\", \"end\": \"192.168.178.199\"}\n  ],\n  \"exclusions\": [\"192.168.178.100\"],\n  \"reservations_path\": \"dhcplane.reservations\",\n\n  \"ntp\": [\"192.168.178.1\"],\n  \"mtu\": 1500,\n  \"tftp_server_name\": \"192.168.178.2\",\n  \"bootfile_name\": \"pxelinux.0\",\n  \"wpad_url\": \"http://wpad.lan/wpad.dat\",\n  \"wins\": [\"192.168.178.3\"],\n\n  \"domain_search\": [\"lan\", \"corp.lan\"],\n  \"static_routes\": [\n    {\"cidr\": \"10.10.0.0/16\", \"gateway\": \"192.168.178.254\"}\n  ],\n  \"mirror_routes_to_249\": true,\n  \"vendor_specific_43_hex\": \"01:04:de:ad:be:ef\",\n\n  \"device_overrides\": {\n    \"00-11-22-33-44-55\": {\n      \"dns\": [\"192.168.178.53\"],\n      \"tftp_server_name\": \"192.168.178.2\",\n      \"bootfile_name\": \"special.efi\"\n    }\n  },\n\n  \"banned_macs\": {\n    \"dc:ed:83:f3:68:5b\": {\n      \"first_seen\": 1725550000,\n      \"note\": \"guest device blocked\",\n      \"equipment_type\": \"Gateway\",\n      \"manufacturer\": \"Unknown\"\n    }\n  },\n\n  \"equipment_types\": [\"Switch\",\"Router\",\"AP\",\"Modem\",\"Gateway\",\"Printer\"],\n  \"management_types\": [\"ssh\",\"web\",\"telnet\",\"serial\",\"console\"],\n\n  \"console_max_lines\": 10000,\n  \"console_tcp_address\": \"\",\n\n  \"logging\": {\n    \"path\": \"\",\n    \"filename\": \"dhcplane.log\",\n    \"max_size\": 20,\n    \"max_backups\": 5,\n    \"max_age\": 0,\n    \"compress\": true\n  },\n\n  \"detect_dhcp_servers\": {\n    \"enabled\": true,\n    \"active_probe\": \"off\",\n    \"probe_interval\": 600,\n    \"first_scan\": 60,\n    \"rate_limit\": 6,\n    \"whitelist_servers\": []\n  },\n\n  \"arp_anomaly_detection\": {\n    \"enabled\": false,\n    \"probe_interval\": 1800,\n    \"first_scan\": 60\n  }\n}\n```\n\nDefaults:\n\n- `lease_seconds`: `86400` (24h) if ≤ 0\n- `lease_sticky_seconds`: `86400` (sticky window) if ≤ 0\n- At least one pool is required\n- Unknown fields are rejected (strict mode)\n- `lease_db_path`: defaults to `dhcplane.leases` when omitted\n- `pid_file`: defaults to `dhcplane.pid` when omitted\n- `reservations_path`: defaults to `dhcplane.reservations` when omitted (relative paths are resolved relative to config file directory)\n- `authoritative`: defaults to `true` when unset\n- `equipment_types`: defaults to `[\"Switch\",\"Router\",\"AP\",\"Modem\",\"Gateway\"]` when empty\n- `management_types`: defaults to `[\"ssh\",\"web\",\"telnet\",\"serial\",\"console\"]` when empty\n- `console_max_lines`: defaults to `10000` when ≤ 0\n- `detect_dhcp_servers.enabled`: defaults to `true` if other detection fields are set\n- `detect_dhcp_servers.probe_interval`: defaults to `600` seconds (minimum 60)\n- `detect_dhcp_servers.first_scan`: defaults to `60` seconds (minimum 10)\n- `detect_dhcp_servers.rate_limit`: defaults to `6` events per minute\n- `arp_anomaly_detection.probe_interval`: defaults to `1800` seconds\n- `arp_anomaly_detection.first_scan`: defaults to `60` seconds\n\n### Example config\n\n```json\n{\n  \"interface\": \"\",\n  \"server_ip\": \"192.168.178.1\",\n  \"subnet_cidr\": \"192.168.178.0/24\",\n  \"gateway\": \"192.168.178.1\",\n  \"dns\": [\"1.1.1.1\",\"9.9.9.9\"],\n  \"lease_db_path\": \"dhcplane.leases\",\n  \"lease_seconds\": 86400,\n  \"lease_sticky_seconds\": 86400,\n  \"auto_reload\": true,\n  \"pools\": [\n    {\"start\":\"192.168.178.50\",\"end\":\"192.168.178.199\"}\n  ],\n  \"exclusions\": [\"192.168.178.100\"],\n  \"reservations_path\": \"dhcplane.reservations\",\n  \"domain\": \"lan\",\n  \"ntp\": [\"192.168.178.1\"],\n  \"mtu\": 1500,\n  \"tftp_server_name\": \"192.168.178.2\",\n  \"bootfile_name\": \"pxelinux.0\",\n  \"wpad_url\": \"\",\n  \"wins\": [],\n  \"domain_search\": [\"lan\"],\n  \"static_routes\": [],\n  \"mirror_routes_to_249\": false,\n  \"vendor_specific_43_hex\": \"\",\n  \"device_overrides\": {},\n  \"banned_macs\": {},\n  \"equipment_types\": [\"Switch\",\"Router\",\"AP\",\"Modem\",\"Gateway\"],\n  \"management_types\": [\"ssh\",\"web\",\"telnet\",\"serial\",\"console\"],\n  \"console_max_lines\": 10000,\n  \"console_tcp_address\": \"\",\n  \"logging\": {\n    \"path\": \"\",\n    \"filename\": \"dhcplane.log\",\n    \"max_size\": 20,\n    \"max_backups\": 5,\n    \"max_age\": 0,\n    \"compress\": true\n  },\n  \"detect_dhcp_servers\": {\n    \"enabled\": true,\n    \"active_probe\": \"off\",\n    \"probe_interval\": 600,\n    \"first_scan\": 60,\n    \"rate_limit\": 6,\n    \"whitelist_servers\": []\n  },\n  \"arp_anomaly_detection\": {\n    \"enabled\": false,\n    \"probe_interval\": 1800,\n    \"first_scan\": 60\n  }\n}\n```\n\n### Reservations\n\nReservations 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:\n\n```json\n{\n  \"aa:bb:cc:dd:ee:ff\": {\n  \"ip\": \"192.168.178.10\",\n  \"note\": \"human note\",\n  \"first_seen\": 1725550000,\n  \"equipment_type\": \"Switch\",\n  \"manufacturer\": \"Ubiquiti\",\n  \"management_type\": \"web\",\n  \"management_interface\": \"https://192.168.178.10\"\n  }\n}\n```\n\nMAC keys accept `aa:bb:...`, `aa-bb-...`, or `aabb...` formats.\n\n\u003e Backwards compatible: the legacy `{\"mac\":\"ip\"}` style is also accepted on load.\n\n### Banned MACs\n\nBanned 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-...`).\nBanned MACs:\n\n- Are logged on contact\n- Receive a NAK on DHCP REQUEST when the config’s `authoritative` flag is true (default)\n- Are marked in the **grid** and **details** outputs\n\n### Per-device overrides\n\n`device_overrides` lets you override **only**:\n\n- DNS servers (option 6)\n- TFTP server name (66)\n- Bootfile name (67)\n\nGlobal config remains in effect for everything else.\n\n### Static routes\n\n`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.\n\n```json\n\"static_routes\": [\n  {\"cidr\":\"10.10.0.0/16\",\"gateway\":\"192.168.178.254\"}\n]\n```\n\n### Vendor option 43\n\nProvide a raw hex payload in many styles: `\"01:04:de:ad:be:ef\"`, `\"01 04 de ad be ef\"`, `\"hex:0104deadbeef\"`, `\"0x01,0x04,0xDE...\"`.\n\n---\n\n## Leases DB\n\n### Format\n\n`dhcplane.leases` (or the path specified in `lease_db_path`) is a simple map persisted by the server:\n\n```json\n{\n  \"by_ip\": {\n    \"192.168.178.100\": {\n      \"mac\": \"aa:bb:cc:dd:ee:ff\",\n      \"ip\": \"192.168.178.100\",\n      \"hostname\": \"host-name\",\n      \"allocated_at\": 1725551111,\n      \"expiry\": 1725637511,\n      \"first_seen\": 1725550000\n    }\n  },\n  \"by_mac\": {\n    \"aa:bb:cc:dd:ee:ff\": { /* same structure as above */ }\n  }\n}\n```\n\nAll timestamps are **epoch seconds**. Formatting to local time happens only when printing.\n\n### Backwards-compatible loading\n\nIf you used a prior version that stored RFC3339 timestamps, the server will **tolerantly** read and coerce them to epoch on load.\n\n### Sticky window \u0026 compaction\n\n- **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.\n- **Compaction** removes leases that expired longer than the sticky window ago. If `compact_on_load` is true, compaction runs at startup and on reload.\n\n---\n\n## Running\n\nThe server listens on UDP:67 on the configured interface (or all interfaces if empty). It is **authoritative** by default (sends NAKs on invalid requests).\n\n### Examples\n\nServe with console echo and file log:\n\n```bash\n./dhcplane serve \\\n  --config ./dhcplane.config \\\n  --console\n```\n\nCheck the config:\n\n```bash\n./dhcplane check -c ./dhcplane.config\n```\n\nLive reload via PID file (default `dhcplane.pid`):\n\n```bash\n./dhcplane reload -c ./dhcplane.config\n```\n\nList current leases (pretty JSON):\n\n```bash\n./dhcplane leases -c ./dhcplane.config\n```\n\nSearch for an IP address:\n\n```bash\n./dhcplane search 192.168.178.100 -c ./dhcplane.config\n```\n\nStats \u0026 tables:\n\n```bash\n# Summary + leased/expiring/expired tables\n./dhcplane stats -c ./dhcplane.config\n\n# Full subnet table (hides free addresses) with Type column\n./dhcplane stats -c ./dhcplane.config --details\n\n# colour grid of the whole subnet\n./dhcplane stats -c ./dhcplane.config --grid\n```\n\nAdd or update a reservation:\n\n```bash\n# MAC, IP, then an optional free-form note\n./dhcplane manage add dc:ed:83:f3:68:5b 192.168.178.55 \"kitchen display\"\n```\n\nRemove a reservation:\n\n```bash\n./dhcplane manage remove dc:ed:83:f3:68:5b\n```\n\n---\n\n## Commands\n\n### `serve`\n\nStart the DHCP server. Validates config before binding. Creates/updates a PID file. Supports `SIGHUP` reload and graceful termination.\n\nKey features at runtime:\n\n- Sticky leases\n- Reservation enforcement over leases\n- Per-device overrides (DNS/TFTP/Bootfile)\n- Vendor option 43\n- Routes 121 (+249 mirror)\n- WPAD (252), WINS (44), MTU (26), Domain(15), Domain search (119)\n- Auto-reload (filesystem watcher) when `auto_reload` is true\n\n### `leases`\n\nPrint leases from the lease database (default: `dhcplane.leases`) as a JSON array with formatted timestamps (local time), including `AllocatedAt`, `Expiry`, and `FirstSeen`.\n\n### `stats`\n\nPrint allocation rates for the last 1m/1h/24h/7d/30d and lease groupings.\n\nFlags:\n\n- `--details`: show a unified table across the whole subnet with Type classification:\n  - `leased` (active leases)\n  - `reserved` (IP is fixed in config, not currently leased)\n  - `banned-mac` (active lease owned by a banned MAC)\n  - `banned-ip` (network/broadcast/server/gateway/exclusions/declined quarantine)\n  - `free` (hidden in details output)\n- `--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.\n\n### `check`\n\nStrictly validate the config file (default: `dhcplane.config`). Unknown fields, wrong types, etc., return precise line/column.\n\n### `reload`\n\nReads PID from `pid_file` in the config and sends `SIGHUP`. Before signaling, re-validates the config and refuses to reload if invalid.\n\n### `manage add/remove`\n\nManipulate reservations in the reservations file (default: `dhcplane.reservations`):\n\n- `manage add \u003cmac\u003e \u003cip\u003e [note...]`\n  - Validates MAC and IPv4\n  - Warns if IP is out of subnet or currently leased to a different MAC\n  - Inserts/updates with `first_seen` (epoch) if missing, preserves any existing metadata\n- `manage remove \u003cmac\u003e`\n  - Deletes the reservation if present\n\nEdits are written atomically via a temporary file → rename.\n\n### `search`\n\nSearch for an IP address in reservations and leases:\n\n- `search \u003cip\u003e`\n  - Displays all information about the IP address\n  - Shows reservation details (MAC, note, metadata) if found\n  - Shows lease details (MAC, hostname, timestamps, expiry status) if found\n  - Warns if MAC addresses don't match between reservation and lease\n  - Suggests running an ARP scan if the IP is not found in either reservations or leases\n\n---\n\n## Flags\n\nGlobal flags (apply to all commands unless noted):\n\n- `-c, --config string`\n  Path to JSON config (default `dhcplane.config`)\n- `--console`\n  Serve the interactive console over UNIX socket (logs always print to stdout/stderr)\n\nCommand-specific flags:\n\n- `stats --details`\n- `stats --grid`\n- `console attach --transparent`\n- `console attach --tcp \u003caddress\u003e` (e.g., `--tcp localhost:9090`)\n\n---\n\n## Environment variables\n\n- `dhcplane_BANNED_MACS`\n  Extra banned MACs; separated by comma/space/newline. Accepts `aa:bb:...`, `aa-bb-...`, or `aabb...`.\n\n- `COLUMNS`\n  If set, used as terminal width hint for grid layout.\n\n---\n\n## Signals \u0026 lifecycle\n\n- `SIGHUP`\n  Reload config. If `compact_on_load` is true, compaction runs post-reload.\n- `SIGINT`/`SIGTERM`\n  Graceful shutdown: server stops, leases DB is saved, watcher/log files closed.\n\nWhen `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).\n\n---\n\n## Logging\n\n- File logger is created using the `logging` section (default `dhcplane.log`); logs are always mirrored to stdout/stderr.\n- Internal errors are additionally printed to stderr in red for operator visibility.\n- PID file is written at the `pid_file` path in the config.\n\nExample log lines:\n\n```log\nSTART 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\nAUTO-RELOAD: watching ./dhcplane.config\nCONSOLE listening on UNIX socket + TCP 0.0.0.0:9090\nDETECT start mode=off interval=600s rate_limit=6/min iface=eth0 whitelist=0\nDETECT scan started iface=eth0\nDETECT scan completed iface=eth0 (no foreign servers found)\nDISCOVER from aa:bb:cc:dd:ee:ff hostname=\"printer\" xid=0x12345678\nOFFER aa:bb:cc:dd:ee:ff -\u003e 192.168.178.100\nACK aa:bb:cc:dd:ee:ff \u003c- 192.168.178.100 lease=24h0m0s (alloc=2025/09/05 20:40:01, exp=2025/09/06 20:40:01)\nDETECT scan started iface=eth0\nFOREIGN-DHCP-SERVER detected server_ip=192.168.178.254 from=192.168.178.254 iface=eth0\nDETECT scan completed iface=eth0 (found 1 server(s))\nARP-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\n```\n\n---\n\n## Background monitoring\n\nThe server includes two optional background monitoring tasks:\n\n### ARP Anomaly Detection\n\nDetects devices on the network that aren't managed by the DHCP server:\n- Scans ARP tables periodically\n- Identifies devices with IPs that aren't leased or reserved\n- Detects MAC address mismatches\n- Reports banned MACs found in ARP\n\nConfiguration:\n```json\n\"arp_anomaly_detection\": {\n  \"enabled\": false,\n  \"probe_interval\": 1800,\n  \"first_scan\": 60\n}\n```\n\n### DHCP Server Detection\n\nDetects rogue/unauthorized DHCP servers on the network:\n- Sends periodic DHCP DISCOVER probes (using MAC `00:00:00:00:00:00` which is silently ignored by the server)\n- Listens for OFFER responses from other servers\n- Logs when scans start and complete (even if no servers found)\n- Logs foreign DHCP servers not in the whitelist\n- Helps identify network conflicts\n\nConfiguration:\n```json\n\"detect_dhcp_servers\": {\n  \"enabled\": true,\n  \"active_probe\": \"off\",\n  \"probe_interval\": 600,\n  \"first_scan\": 60,\n  \"rate_limit\": 6,\n  \"whitelist_servers\": []\n}\n```\n\nBoth tasks run independently in background goroutines and can be enabled/disabled via config.\n\n## Console access\n\nThe interactive console can be accessed via:\n\n- **UNIX socket** (default): Local access via socket file\n  ```bash\n  ./dhcplane console attach\n  ```\n\n- **TCP/IP** (when configured): Remote access over network\n  ```json\n  \"console_tcp_address\": \"0.0.0.0:9090\"\n  ```\n  ```bash\n  ./dhcplane console attach --tcp 192.168.1.2:9090\n  ```\n\nWhen `console_tcp_address` is set, the server listens on **both** UNIX socket and TCP simultaneously, allowing both local and remote access.\n\n## Security notes\n\n- Only enable `authoritative` in the config on networks where this server should NAK competing/invalid requests.\n- Use `exclusions` to protect infrastructure IPs that are inside pools.\n- Banned MACs are enforced at request time; keep in mind MAC spoofing is possible on L2.\n- Consider running under a service account with only `cap_net_bind_service` capability if possible, not full root.\n- Always validate the config file (default: `dhcplane.config`) with `check` before deploying edits in production.\n- If enabling TCP console access, consider firewall rules to restrict access to trusted networks.\n\n---\n\n## Appendix: IP selection policy (summary)\n\n1. **Reservation wins**: If the MAC has a reservation, use it (unless actively leased to a different MAC).\n2. **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).\n3. **Brand-new** IPs: Prefer addresses never seen in the DB.\n4. **Recycle expired**: If no new IPs, reuse expired, safe IPs (respecting reservations).\n5. If none apply: pool exhausted → NAK (when authoritative) or silent failure.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnetwork-plane%2Fdhcplane","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnetwork-plane%2Fdhcplane","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnetwork-plane%2Fdhcplane/lists"}