feat(wrt): identify unnamed devices by OS/vendor and retry silent mDNS lookups - #3623
Open
Dominion5254 wants to merge 5 commits into
Open
feat(wrt): identify unnamed devices by OS/vendor and retry silent mDNS lookups#3623Dominion5254 wants to merge 5 commits into
Dominion5254 wants to merge 5 commits into
Conversation
Some clients never advertise a hostname through any source devices.list consults — ChromeOS deliberately sends no DHCP option 12 and answers no reverse-mDNS query, and many stripped-down IoT DHCP clients behave the same — so they surfaced as an opaque `device-3af2b1` placeholder. Add a vendor-label rung to the name resolution chain, just above the placeholder: look up the MAC's OUI in an embedded snapshot of the IEEE registries (MA-L/MA-M/MA-S, longest-prefix match) and render e.g. "Apple device (3af2b1)", keeping the stable MAC suffix so identical unnamed devices stay distinguishable. Labels are derived, not learned: recomputed each poll, never written to the name cache, so any real hostname that later appears — or a user-assigned name — outranks them. Randomized (locally-administered) MACs are skipped — they have no registry entry by construction; on Ethernet, where MACs are burned in, the lookup essentially always resolves. "IEEE Registration Authority" parent rows and "Private" registrations are excluded at generation time so an unassigned sub-block falls through to the placeholder rather than yielding a meaningless label. The snapshot (52,889 entries, ~490 KB gzipped) is generated by build/gen-oui-table.sh; the registry is append-mostly, so the asset only ever develops misses on brand-new vendors — refresh it opportunistically by re-running the script. Version bumped to 1.1.0 (feature tier).
Layer an OS-recognition rung above the OUI vendor label: the order of a client's DHCP option-55 parameter-request list and its option-60 vendor class are characteristic of the OS's DHCP stack, ride the request itself (so they survive MAC randomization, where OUI lookup is blind), and cannot be withheld the way a hostname can. An unnamed Windows laptop now shows "Windows device (3af2b1)" rather than falling to the NIC vendor or the placeholder. Capture: a hook script sourced by dnsmasq's dhcp-script wrapper appends "MAC|options|vendor class" lines on every DHCP add/old event. The wrapper sources the script inside the dnsmasq ujail, which dictates the design: no `exit` (it would abort the wrapper's ubus hotplug notify), shell builtins only (the jail mounts no coreutils), and the output lands in /var/run/dnsmasq/ — the jail's only writable directory (/tmp is silently unwritable there). The daemon parses the file defensively (it is the validation layer for client-controlled bytes) and compacts it when it outgrows 64 KiB. Wiring: the daemon provisions the hook at every boot — writes the script, points every `config dnsmasq` section's `dhcpscript` at it via a minimal partial TypedSection (preserving all other options), and reloads dnsmasq only on change — so OTA-updated routers converge without a reflash; profile creation sets `dhcpscript` on new instances directly. Persistence: fingerprints are cached per-MAC in device_names.json (hostname becomes optional there — a Chromebook has a fingerprint but no name; legacy files load unchanged), so OS labels survive reboots even though the capture file is tmpfs. Labels themselves stay derived: recomputed each poll from fingerprint-else-OUI, never cached as names. The rule table is seeded from widely-documented signatures (MSFT/ android-dhcp/dhcpcd/udhcp vendor classes; Apple and Windows option-55 sequences) and is deliberately conservative — a miss falls through to the OUI rung. Live bench validation, documented in device_ident.rs: capture /var/run/dnsmasq/dhcp.fingerprints for Windows, macOS, iOS, Android, Debian, and above all ChromeOS (the client this feature exists for), whose vendor-class behavior is undocumented and decides whether it earns a dedicated rule or lands on "Linux"/the OUI rung.
Reverse-mDNS name recovery attempted each device exactly once per daemon run: a silent device went into MDNS_ATTEMPTED and was never queried again until restart — potentially months. That single attempt is a bad sampler: it usually fires the moment the device first appears (before its mDNS responder is up) or in the reassociation chaos right after a router reboot, and sleeping phones/laptops don't answer at all, so devices that do have Bonjour names got stuck on the generic label. Replace the attempted-set with a per-MAC attempt history and retry still-unnamed devices on a bounded backoff schedule: 6 attempts total, gated at 1 min -> 10 min -> 1 h -> 6 h -> 24 h after the previous try, then done until restart (the old behavior becomes the end state instead of the first). The shape follows RFC 6762 §5.2, mDNS's own doubling retry for unanswered queries; unlike a standing query we cap attempts, since many devices genuinely have no responder and each attempt spawns avahi-resolve. The early rungs catch responder-startup lag and boot chaos (avahi-daemon also caches answers that arrive after our 1.5 s timeout kills the CLI, so a quick retry often hits the cache); the late rungs catch sleepers. A permanently silent device now costs 6 spawns per daemon run instead of 1 — still nothing. Backoff runs on Instant, not wall time: routers boot with a wrong clock and NTP-jump minutes later, which would garble a wall-clock schedule. Retries ride the existing devices.list polls, so nothing fires while no client is viewing the device list, and success still persists to the name cache, which gates any further attempts. devices.forget now also drops the attempt history, so a forgotten device that reconnects starts a fresh schedule (matching the "appears as a new entry" docs contract).
Bench capture 2026-08-03 (Ubuntu, NetworkManager internal client): option-55 list 1,2,6,12,15,26,28,121,3,33,40,41,42,119,249,252,17 with no vendor class — the default DHCP client on Ubuntu/Fedora desktops. Exact-match rule in the table's conservative style; a drifted list in a future NM version just falls through to the OUI rung as before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Some clients never advertise a hostname through any source
devices.listconsults: ChromeOS withholds DHCP option 12 as a privacy decision, stripped-down
IoT clients can't send one, and phones with randomized Wi-Fi MACs defeat any
vendor lookup. Those devices showed up as an opaque
device-3af2b1. This PRgives them descriptive labels derived from what a device can't help
revealing, and fixes the mDNS name recovery that gave every device exactly one
chance to answer. Bumps 1.0.2 → 1.1.0.
The resolved name chain becomes: UCI static name → live DHCP hostname → live
mDNS name → cached hostname → DHCP-fingerprint OS label (
Windows device (3af2b1)) → MAC-OUI vendor label (Apple device (3af2b1)) → placeholder.Labels are derived, not learned — recomputed every poll, never cached as names —
so any real hostname outranks them the moment it appears, and user-assigned
names always win.
feat(wrt): label unnamed devices by MAC vendor instead of a placeholder.
New
device_identmodule with an embedded IEEE registry snapshot(MA-L/MA-M/MA-S, longest-prefix match, ~490 KB gzipped TSV) regenerated by
build/gen-oui-table.sh. Unassigned sub-blocks and anonymized registrationsare excluded at generation time so they fall through rather than mislabel.
Locally-administered (randomized) and multicast MACs return no vendor by
construction.
feat(wrt): recognize unnamed devices' OS via DHCP fingerprinting. The
option-55 parameter-request order and option-60 vendor class are
characteristic of the OS's DHCP client and survive MAC randomization.
Captured by a dnsmasq
dhcp-scripthook that runs sourced inside thednsmasq ujail (no
exit, builtins only,/var/run/dnsmasq/is the onlywritable path — verified against the shipped
dnsmasq.initand wrapper).The daemon provisions the script and the UCI
dhcpscriptoption on everydnsmasq section at boot (idempotent, reload only on change), so OTA-updated
routers converge without a fresh flash; profile-created sections get it via
ProfileDnsmasq. The fingerprint rung outranks OUI because it identifiesthe machine, not the NIC — a USB Ethernet dongle's OUI says "TP-Link" even
when the laptop behind it is a Mac. Fingerprints persist per-MAC in the
device_namescache (the capture file is tmpfs), so labels survive reboots;hostnamein the cache becomes optional, and legacy pre-fingerprint JSONloads unchanged. The daemon-side parser is the validation layer for the raw
hook output — a hostile vendor class can at worst forge a display label.
fix(wrt): retry silent mDNS name lookups on a backoff schedule. The
reverse-mDNS lookup fired at most once per device per daemon run, usually at
the worst moment (the instant a device first appeared, or in the
reassociation rush after a router reboot), so a missed answer meant a
generic label until restart. Silent devices are now retried on an RFC 6762
§5.2-shaped schedule (1 m → 10 m → 1 h → 6 h → 24 h, 6 attempts total, then
left alone).
Instant-based, since routers boot with a wrong clock andNTP-jump minutes later.
devices.forgetclears the attempt history so aforgotten device restarts a fresh schedule.
feat(wrt): recognize NetworkManager's DHCP fingerprint as Linux. Table
rule pinned to a bench capture from an Ubuntu client (NetworkManager's
internal DHCP client, the Ubuntu/Fedora desktop default).
Fingerprint table rules are deliberately conservative (a miss falls through to
the OUI rung, which is harmless; a wrong match mislabels a device). Labels are
English-only, matching the existing "VPN Device" convention.