feat: optional per-device IP address tracking via periodic ARP scan - #1763
feat: optional per-device IP address tracking via periodic ARP scan#1763mwpastore wants to merge 5 commits into
Conversation
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>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
I haven't looked closely at the code but from the description, this is a great feature addition. However, it's subject to the same problems that #1760 runs into with dynamically obtaining a device's IP.... (Wanting to prevent/reduce flooding the network, while still keeping the device's info relatively updated) Your implementation, for example, arp scans every 15 minutes, which keeps network traffic to a minimum. However, if during that 15 minute window a device gets powered off and then another device gets assigned that IP address, UpSnap won't know for 15 minutes, and in an extreme/edge case, if the user has a shutdown command scheduled during that 15 minute window, it may send the command to the new (and incorrect) device using that IP address. It's unlikely but possible. |
|
@invario Thank you for the feedback. One thing I considered was triggering the arp scan before/during/after other events. For example, after wakeUDP runs (perhaps plus a small delay). I suppose another opportunity would be right before any action that uses the IP address, e.g. a scheduled shutdown. I'll work on this locally and can submit a follow-up PR (or add it onto this one if y'all want). |
…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>
|
The latest commit (f9945b7) restructures the backend half of this feature and adds test coverage for the tracking logic. No frontend or schema changes. Structure. The sweep moved out of Two behavior fixes:
Tests. The entry points take Net: |
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>
|
I hope I haven't moved the cheese too badly on anyone who might have started to review this, but the refactoring and lazy_ping integration seemed integral. I think I'm done now. 😅 |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
track_ipbool on thedevicescollection).track_ip_intervalonsettings_private, cron syntax, same validation UX as the ping interval). Empty = feature off.SetPingJobs, so it re-arms on settings changes; implemented in thebackend/iptrackingpackage): fetches devices withtrack_ip = true, computes each device's subnet from ip+netmask, keeps only subnets that overlap a directly attached network (routed devices are never scanned), skips /32s, non-contiguous netmasks, and anything broader than /16, dedupes, runs onenmap -snper subnet, and updates any opted-in device whose MAC now sits at a different IP — with the new IP constrained to the device's own subnet, so a scan can never relocate a device across subnets. Saves useIgnoreUnchangedFields(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. With lazy_ping turned on, sweeps also pause while no realtime clients are connected; a catch-up sweep runs when the next client connects.Related work
This PR is intended as complementary to #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 #1760 if it merges first.
Design notes
arp_accept=0ignores gratuitous ARP). A privilegednmap -snon 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.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.WakeDevice/SendMagicPacketare unchanged.scan_linux.go/scan_other.go), mirroring the existing ping/scan pattern.Deliberate duplication / follow-up refactor opportunity
networking/scan.go+scan_linux.gointentionally duplicate the nmap exec + capability-raising code frompb/handlerscan_linux.goso that no existing files inbackend/pbare touched by this PR. If desired, a natural follow-up (or squash into this PR) would collapsehandlerscan_linux.go/handlerscan_other.gointo a single cross-platform handler that calls the sharednetworking.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 and the updated lazy ping description are in
en-US.jsononly; other locales fall back to English for the new keys until translated. Happy to add translations for the other 22 locales if you'd prefer complete files.Testing
go buildfor darwin and linux/amd64,go vet,gofmt, and all tests pass. New suites cover the subnet validation and mac→ip mapping innetworking, and the tracking behavior itself iniptracking(same-subnet guard, mac format normalization, skip paths, sweep dedupe) against a stubbed scanner and a real migrated PocketBase schema.pb_data: both migrations apply and the newtrack_ip/track_ip_intervalcolumns are created with correct defaults (off / empty).pnpm buildandeslintpass;svelte-checkclean apart from a pre-existing error in paraglide's generated output.Save()writes all columns); fixed viaIgnoreUnchangedFields(true)..142→.143→.142, driven by netplan changes on the device) within one sweep interval each way. Running with--devSQL logging confirmed the tracking save writes only theipcolumn 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