Files
homelab-monitor/docs/device-discovery.md
T
jhodgkin 04282232cc
CI / web (push) Successful in 17s
CI / api (push) Successful in 23s
Add LAN device discovery (ping sweep + ARP, known/unknown labeling)
Runs as a host-level systemd timer on CT122 (scripts/discover-devices.sh)
rather than inside the api container, since real ARP entries live in the
host's network namespace, not Docker's bridge network. See
docs/device-discovery.md for the full writeup, including why literal
passive-only ARP reading was dropped (near-empty result in practice).

API reads the resulting JSON file each poll cycle, cross-references
config/hosts.yaml's knownDevices list by IP, and serves /api/devices.
Dashboard gets a new "Network Devices" table.

Closes #9.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:49:22 -06:00

50 lines
2.4 KiB
Markdown

# LAN device discovery
Shows every device currently on the LAN, labeled known/unknown, in the dashboard's
"Network Devices" section.
## Why this isn't pure passive ARP reading
The original idea was to just read the ARP table (no active network traffic). In
practice that produces a nearly empty list: a host's ARP cache only contains entries
for peers it has actually exchanged traffic with, and the dashboard container has no
reason to talk to most LAN devices on its own. So discovery does a lightweight ICMP
ping sweep (no port scanning) first, to populate the cache, then reads it.
## Why this runs outside the Docker container
Real ARP entries live in the network namespace of whichever host is directly on the
LAN interface. The `api` container sits behind Docker's own bridge network — pinging
from inside it and reading `/proc/net/arp` there would show Docker's internal network,
not the actual LAN. Rather than switch the containers to `network_mode: host` (loses
container network isolation), discovery runs as a small script directly on the CT122
host via a systemd timer — the same pattern the homelab already uses for host-level
periodic jobs like the rclone backups on CT105.
## Pieces
- `scripts/discover-devices.sh` — ping sweep + `ip neigh show` parse, writes
`/opt/homelab-monitor/data/devices-raw.json` (`[{ip, mac, state}]`).
- `deploy/systemd/homelab-monitor-discover.{service,timer}` — runs the script every
5 minutes on the CT122 host (not in Docker).
- `apps/api/src/discovery/index.ts` — reads that JSON file each poll cycle, labels
entries against `knownDevices` in `config/hosts.yaml` (matched by IP), upserts into
the `devices` SQLite table.
- `GET /api/devices` — serves devices seen in the last 24h (older entries are dropped
rather than shown as stale-known, since there's no per-device up/down tracking).
## Deploying/updating
```bash
scp scripts/discover-devices.sh homelab-monitor:/opt/homelab-monitor/scripts/
scp deploy/systemd/homelab-monitor-discover.* homelab-monitor:/etc/systemd/system/
ssh homelab-monitor "chmod +x /opt/homelab-monitor/scripts/discover-devices.sh && \
systemctl daemon-reload && systemctl enable --now homelab-monitor-discover.timer"
```
## Known limitation
`knownDevices` in `config/hosts.yaml` is matched by **IP**, not MAC — fine as long as
DHCP reservations don't change, but a device losing its reservation would show up as
"unknown" until the config is updated by hand.