Skip to content

feat: optional per-device IP address tracking via periodic ARP scan - #1

Draft
mwpastore wants to merge 5 commits into
masterfrom
claude/upsnap-magic-packet-routing-dc13ba
Draft

feat: optional per-device IP address tracking via periodic ARP scan#1
mwpastore wants to merge 5 commits into
masterfrom
claude/upsnap-magic-packet-routing-dc13ba

Conversation

@mwpastore

@mwpastore mwpastore commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Summary

Adds an opt-in, per-device IP address tracking feature: when enabled, UpSnap periodically ARP-scans the local subnets of opted-in devices and updates a device's IP address whenever its MAC address shows up at a different IP. This keeps WOL and ping working for DHCP devices whose leases change.

  • Per-device toggle (disabled by default): "Track IP address" in Device settings → General (track_ip bool on the devices collection).
  • Global interval in Settings under Ping interval (track_ip_interval on settings_private, cron syntax, same validation UX as the ping interval). Empty = feature off.
  • The sweep (registered in SetPingJobs, so it re-arms on settings changes): fetches devices with track_ip = true, computes each device's subnet from ip+netmask, keeps only subnets directly attached to the host (a host interface address lies inside the subnet — routed devices are never scanned), skips /32 and anything broader than /16, dedupes, runs one nmap -sn per subnet, and updates any opted-in device whose MAC now sits at a different IP. Saves use IgnoreUnchangedFields(true) so concurrent status writes from the ping/wake crons are never clobbered. No devices opted in or no local subnets → the tick is a no-op.

Related work

This PR is intended as complementary to seriousm4x#1760, not competing: name resolution covers devices that have a resolvable name (AD, avahi, dynamic DNS) and works across routed subnets, while ARP tracking covers devices with no name at all (headless/IoT boxes, plain DHCP networks without dynamic DNS) but only on directly attached subnets. A device answers to a MAC even when nothing answers for its name. The two features touch adjacent parts of the device form, so whichever lands second will need a small rebase — happy to be the one to rebase over seriousm4x#1760 if it merges first.

Design notes

  • ARP scan, not broadcast ping: modern OSes ship with ICMP-echo-to-broadcast disabled, and passive ARP-table reads miss hosts the daemon hasn't talked to (arp_accept=0 ignores gratuitous ARP). A privileged nmap -sn on the local segment does the ARP probing directly and reports MACs in its XML output — the same mechanism the existing network scan feature relies on.
  • No new privileges or dependencies: requires nmap + CAP_NET_RAW, exactly like the existing network scan. Docker images already ship both; host network mode is already required for WOL. The docker-compose capability comment and README are updated accordingly.
  • Wake path untouched: tracking is periodic-only; WakeDevice/SendMagicPacket are unchanged.
  • Linux-first: capability raising is build-tagged (scan_linux.go / scan_other.go), mirroring the existing ping/scan pattern.

Deliberate duplication / follow-up refactor opportunity

networking/scan.go + scan_linux.go intentionally duplicate the nmap exec + capability-raising code from pb/handlerscan_linux.go so that no existing files in backend/pb are touched by this PR. If desired, a natural follow-up (or squash into this PR) would collapse handlerscan_linux.go/handlerscan_other.go into a single cross-platform handler that calls the shared networking.NmapScan, removing ~120 lines of duplicated handler code. I have that refactor working and can push it as a separate commit/PR on request — kept it out of this one to keep the diff strictly feature-scoped.

Translations

The 4 new UI strings are added to en-US.json only; other locales fall back to English for these keys until translated. Happy to add translations for the other 22 locales if you'd prefer complete files.

Testing

  • go build for darwin and linux/amd64, go vet, gofmt, and existing networking tests all pass.
  • Booted the compiled backend against a fresh pb_data: both migrations apply and the new track_ip / track_ip_interval columns are created with correct defaults (off / empty).
  • Frontend: pnpm build and eslint pass; svelte-check clean apart from a pre-existing error in paraglide's generated output.
  • An adversarial code review pass flagged a lost-update race between this cron's saves and the ping cron's status saves (PocketBase Save() writes all columns); fixed via IgnoreUnchangedFields(true).
  • Soak-tested in production: I deployed a beta build of this branch to my own UpSnap instance (bare-metal systemd install on a Proxmox guest). The migrations applied cleanly to the live database, and the tracker followed a real device through an actual IP round trip (.142.143.142, driven by netplan changes on the device) within one sweep interval each way. Running with --dev SQL logging confirmed the tracking save writes only the ip column while the ping cron's concurrent full-row status updates continued unharmed, and state survived service restarts.

AI disclosure

This feature was developed with Claude Code (Anthropic). The design decisions, scope choices, and review/testing direction were mine; I've reviewed the changes and stand behind them.

🤖 Generated with Claude Code

@mwpastore
mwpastore force-pushed the claude/upsnap-magic-packet-routing-dc13ba branch from a99973f to d49656a Compare August 7, 2026 22:40
Adds a per-device "Track IP address" toggle (disabled by default) and a
global cron interval setting. When enabled, upsnap periodically arp-scans
the local subnets of opted-in devices with nmap and updates a device's ip
address whenever its mac address is found at a different one. Subnets are
skipped unless directly attached to the host, so routed devices are never
scanned. Uses the same nmap + CAP_NET_RAW requirements as the existing
network scan feature; no new privileges needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mwpastore
mwpastore force-pushed the claude/upsnap-magic-packet-routing-dc13ba branch from d49656a to c828f3a Compare August 7, 2026 23:00
mwpastore and others added 4 commits August 7, 2026 18:29
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…verage

Move the arp scan sweep out of cronjobs into a new iptracking package with
two entry points: TrackAllSubnets (the cron sweep) and TrackOneSubnet (a
single validated scan). Device lookup is now by mac address across all
tracked devices, with updates constrained to the device's own subnet.

The scannability check becomes networking.ValidateScannableSubnet, which
reports why a subnet can't be scanned and accepts any subnet overlapping a
directly attached network, so sub-blocks of an attached prefix are now
scannable. The mac-to-ip mapping moves to networking as Nmaprun.MacToIP.

Cover the subnet guard, mac normalization, skip paths, and sweep
orchestration with tests against a stubbed scanner and a real migrated
schema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With lazy_ping turned on, the periodic tracking sweep now skips its tick
while no realtime clients are connected, just like the ping cron, and owes
a catch-up sweep to the next client that connects: the realtime connect
hook runs one sweep only when a tick was actually skipped (or none has run
since boot), so page reloads during active use trigger nothing. This
extends lazy_ping's meaning from "pause pings when idle" to "pause
periodic network activity when idle"; the setting's description is updated
accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant