Wire up on-demand deep-check: API route + dashboard button
CI / web (push) Successful in 18s
CI / api (push) Successful in 24s

POST /api/devices/:ip/deep-check runs deep-check-device.sh on the
CT122 host via SSH (reaches its own LAN IP), returns mDNS/SSDP/port
scan results. "Deep check" button on unknown device rows in the
dashboard shows results inline below the row.

Verified end-to-end via SSH before wiring into the API: correctly
identified Home Assistant via SSDP (friendlyName/manufacturer/model),
and confirmed both a shell-injection attempt and an out-of-subnet IP
get rejected cleanly by the forced command's input validation.

Closes #15 (all four pieces: OUI, mDNS, manual labels, deep check).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 23:15:24 -06:00
parent e0ef618f0a
commit 542a3d8ce0
9 changed files with 421 additions and 28 deletions
+62 -8
View File
@@ -23,8 +23,10 @@ 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}]`).
- `scripts/discover-devices.sh` — ping sweep + `ip neigh show` parse, then an
`avahi-resolve` mDNS reverse lookup per discovered IP (parallel, 2s timeout each so
one non-mDNS device can't stall the run). Writes
`/opt/homelab-monitor/data/devices-raw.json` (`[{ip, mac, state, mdnsHostname?}]`).
- `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
@@ -33,17 +35,69 @@ periodic jobs like the rclone backups on CT105.
- `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).
## Identifying unknown devices
Three passive/config-driven layers, plus one on-demand active one:
1. **MAC OUI vendor lookup** (`mac-oui-lookup` npm package, `apps/api/src/routes/devices.ts`)
— computed on every read from the MAC prefix, no storage needed. Already resolves
most smart-home gear to a vendor (e.g. "Amazon Technologies Inc.", "Ring LLC",
"Tuya Smart Inc." — a very common IoT chipset vendor).
2. **mDNS hostname** — see `discover-devices.sh` above, stored in `devices.mdns_hostname`.
3. **Manual labels**`device_labels` table, keyed by **MAC** (survives DHCP IP
changes, unlike `knownDevices` in `config/hosts.yaml` — see "Known limitation"
below). `PUT`/`DELETE /api/devices/:mac/label`, inline-editable in the dashboard's
Name column. Manual label > `knownDevices` config name > mDNS hostname (shown as an
*italic hint*, not treated as "known" — nobody's actually confirmed it yet).
4. **On-demand deep check** — admin-triggered, single device, not automatic (avoids the
noise/risk of doing this for the whole subnet on every poll). See below.
### Deep check
`scripts/deep-check-device.sh` runs, on request, against one target IP: mDNS resolve,
a targeted SSDP/UPnP query (many smart-home devices announce a `friendlyName` /
`manufacturer` / `modelName` this way — confirmed working against Home Assistant),
reverse DNS, and a small curated TCP port scan (21,22,23,80,443,554,5000,8000,8008,
8009,8060,8080,8443,9100,32400,62078) with an HTTP title/server grab on anything open.
Bounded timeouts throughout, finishes in well under 15s.
Same multicast-needs-real-network-access constraint as the ping sweep applies, so this
also runs on the CT122 **host**, not in the container — but unlike the scheduled
discovery script, this is triggered on demand from `POST /api/devices/:ip/deep-check`.
The API reaches it via SSH into CT122's own LAN IP (192.168.1.103), using the same
`monitor_ed25519` key as `SshHostCollector` but a **separate `authorized_keys` entry on
CT122 itself**. That entry's forced command is *parameterized*: forced commands ignore
whatever the client literally requests, but OpenSSH still exposes it via
`$SSH_ORIGINAL_COMMAND`, which the script reads and validates strictly (regex-anchored
`192.168.1.<1-254>`, never passed to a shell) before using it — confirmed a shell
injection attempt (`; rm -rf / #`) and an out-of-subnet IP both get rejected cleanly.
`deepCheck:` in `config/hosts.yaml` declares the target host (CT122 itself);
`apps/api/src/discovery/deepCheck.ts` does the SSH round-trip and JSON parsing.
Requires `avahi-utils` and `miniupnpc` installed on the CT122 host (`apt-get install
avahi-utils miniupnpc` — a one-time host package install, not part of any deploy
script, so re-provisioning CT122 from scratch would need to redo this step).
## Deploying/updating
```bash
scp scripts/discover-devices.sh homelab-monitor:/opt/homelab-monitor/scripts/
scp scripts/discover-devices.sh scripts/deep-check-device.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 && \
ssh homelab-monitor "chmod +x /opt/homelab-monitor/scripts/*.sh && \
systemctl daemon-reload && systemctl enable --now homelab-monitor-discover.timer"
```
## Known limitation
(In practice, `git pull` on CT122 already updates the script files at their deployed
path — the above is only needed for the systemd units or a from-scratch setup.)
`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.
## Known limitations
- `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. (Manual labels don't have this
problem — they're keyed by MAC.)
- Deep check's SSDP/port-scan often finds nothing for cloud-connected devices (Ring,
Echo) that deliberately minimize their LAN footprint — OUI vendor + mDNS are the
primary identification layers for those; deep check helps most for devices that run
a local web UI or SSDP responder (smart TVs, media devices, printers, Home Assistant).