Device Identity

A curfew rule is a promise attached to a MAC address. Both halves of that sentence rot. iOS and SteamOS rotate MACs by design. Mesh extenders re-translate them on every reconnect. And the labels every dashboard shows were copied from whichever system guessed first. On 2026-07-18 we found out how far the rot had gone — and rebuilt identity handling so it cannot recur silently.
The discovery: enforcement theater
Section titled “The discovery: enforcement theater”The consoles’ enrolled MACs — PS5, Switch — were absent from the Firewalla’s
entire post-April host history. 184 devices. Zero matches. Every console
“block” since the network migration had been a valid policy applied to an
address nothing wears: the closed loop verified the rule, never the device.
Meanwhile a live Xbox One sat ungoverned at 10.0.0.147, mislabeled as a
parent’s iPhone. Two TCL smart-TV panels streamed nightly past the 23:00
curfew — because the “TV” screens enforced an ESP32 sensor and a lawn sprinkler.
The rule fired. Nobody was home at the address.
Columbo screen pinning
Section titled “Columbo screen pinning”Consoles don’t advertise mDNS device-info. But their DHCP lease hostnames are
stable identities: XboxOne, PS5-*, steamdeck. Columbo
(~/.sanctum/bin/columbo/phonebook.py, launchd every 300 s) reads the box’s
dnsmasq leases; each screen’s hostname → current MAC gets pinned into
phonebook.json under _screens, with a rolling 14-day history of previous
MACs.
The engine enforces static config ∪ live pins at every site — curfew loop, holds, wake, reconciler ground truth, drift repair, daily reaper, and the parent-facing Block Now / override / homework handlers. Worst-case latency from a mid-curfew rotation to re-block: ~4 minutes (columbo pass ≤120 s + 60 s phonebook TTL + one reconciler tick). A never-seen console joining mid-curfew is faster. Discovery fires an immediate columbo pass — the “nudge” — and busts the phonebook TTL. The pin lands, and the normal curfew path blocks it in ~60–90 seconds.
Hardening that survived a 30-agent adversarial panel (25 confirmed findings, 3 critical, all fixed pre-deploy):
- Last-known-good reads — a corrupt/missing phonebook serves stale pins,
never
{}; columbo aborts its pass rather than save emptiness over pins. - Collision guard — a pin may never claim a family phone or static device MAC (hostname spoofing, extender aliasing). Refused + alerted.
- Stability under multi-lease — the current pin holds while its lease is live; extra matching MACs merge quietly into history. Notifications throttle to one per (screen, MAC) per 6 h.
- DHCPv6 IAID lines are skipped — field 2 must parse as a MAC.
One device, two network cards
Section titled “One device, two network cards”Pinning follows a name. And half the hardware a haus shares has none to follow — a console’s idle second NIC advertises nothing at all. That case, the single-target rule it broke, and how nameless TVs, receivers, speakers and printers stopped being treated as strangers are covered in Shared Hardware Identity.
The paused-MAC ledger
Section titled “The paused-MAC ledger”Firewalla pauses have no expiry. Before the ledger, a MAC that rotated away
while blocked could stay paused forever, with no code path left to release it.
Now every pause the engine applies is recorded in paused_macs and cleared
only on verified unpause. A per-tick sweep releases any ledger MAC that no
active enforcement state still claims. The ledger only ever contains Sanctum’s
own pauses. So the sweep can never release a block a parent set in the
Firewalla app.
The naming audit
Section titled “The naming audit”A 6-judge + adversarial-verify panel audited all 181 devices across every
naming surface. Thirteen Firewalla renames landed. Three devices.yaml
structural defects were fixed: an Apple TV slot collision, an Amazon Echo
masquerading as an Apple TV, a phantom Orbi-alias device. The classifier
gained a family-device catalog — console/streamer/TV OUIs plus hostname hints,
with a new tv category so smart-TV panels auto-enroll like consoles instead
of streaming silently.
Trust order for device identity (highest first):
- OUI vendor — authoritative for manufacturer, never for role.
- DHCP lease hostname — self-reported, strong (
XboxOne,bhyve-bh1-*), but spoofable: never let it unlock anything. - Live mDNS / port fingerprints — Cast 8009/8443, AirPlay 7000/62078, Roku 8060, Echo 55443+4070.
- HA registry — good for integrated devices.
- Firewalla user name — now corrected; keep it corrected.
- Firewalla auto-bname — junk. Historically the source of every impostor: the Xbox as “Lise’s iPhone”, the Lutron bridge as “Albert’s iPhone”, a Nuheat floor thermostat as “Bert’s Mac Mini M4 Pro”.
The Orbi holds no independent names — it mirrors DHCP. Its toolkit registry
(orbi-registry.sh) is now seeded with the canonical identities. Access the
Orbi only through the Network Control API (localhost:4007 on the Mini).
The admin refuses SOAP in AP mode, and direct scraping is what created stale
copies in the first place.
The evasion ladder
Section titled “The evasion ladder”Windu, who holds the security seat, models this as an adversary who wants one more hour of Fortnite. Every way a device can present itself lands on a blocking path, ordered here from most to least cooperative:
- Enrolled MAC — blocked directly by static config at curfew.
- Rotated MAC, recognizable hostname — columbo pin follows it; first-join gets the nudge (~60–90 s), mid-curfew rotation ≤4 min.
- Any MAC, recognizable vendor OUI — auto-assigned to shared devices, reconciler blocks within ~2 min. If the MAC is a NIC sibling of an already-enrolled screen it joins that screen instead, inheriting its schedule; appearing mid-curfew, it is blocked on the spot rather than at the next onset — that window is exactly when a kid switches cable.
- Spoofed MAC and renamed hostname, joining during curfew — the unknown-device backstop pauses it at the iptables level in ~30 s.
- Spoofed, renamed, and parked online before curfew — the curfew-start sweep catches it: at the rising edge of any child curfew, every present-but-never-reviewed unknown device gets the same guest pause (auto-released at wake, exempted by Holocron approval). This closed the last hole: Xbox’s manual “Alternate MAC address” setting plus a console rename plus patience used to survive the night.
The sweep is edge-triggered. A parent’s manual mid-curfew unpause is respected until the next curfew start. Devices that join during the day still sit un-paused in the review queue until curfew — a deliberate operator decision (2026-05-24: review-before-bed, not phone-buzzing). But curfew now catches them even when nobody reviewed.
Display-name renames cannot drop curfew
Section titled “Display-name renames cannot drop curfew”Renaming a device in the Firewalla app, on AirPlay, or via DHCP hostname
changes labels, not identity. Enforcement keys on the MAC (static
config ∪ columbo pins). A kid who renames Living Room Apple TV to
Den Room — or anything else — keeps the same enforce set.
What the engine does on every discovery tick when a known MAC presents a new name:
- Remember aliases — old and new labels land in
device_aliases(SQLite) so rotation matching still finds the device under either name. - Soft-update the in-memory display name — Holocron / status stay
truthful without rewriting
devices.yamlmid-flight. - Never remove the MAC from known or enforce sets.
Config can also list durable former names under also_known_as on a
screen or personal device. Hostname matching (columbo-style rotation,
owner re-pin after Private Wi-Fi Address shuffle) uses, in order:
hostname_patternsglobs- the screen/device
name also_known_as- remembered
device_aliasesfor that MAC
# devices.yaml (illustrative — real registry is gitignored haus config)screens: den_room_appletv: name: Den Room Apple TV also_known_as: - Living Room Apple TV - First Floor Apple TV macs: - "FA:CE:DE:CA:CA:01" # identity — rename-proof schedule: holiday: { curfew: "23:00", wake: "10:00" }Unknown OUI is not automatically an intruder
Section titled “Unknown OUI is not automatically an intruder”Private Wi-Fi Address and some classifiers report a new MAC with no
recognizable OUI. If that host still advertises an enrolled screen’s name
(or matches hostname_patterns / also_known_as), the engine pins the
MAC and does not guest-pause. Only true strangers hit the
unknown-during-curfew quarantine. Vacation TVs (shared devices on a
satellite Purple) stay media, not intruders.
Flow / OUI screen identity (2026-08-04)
Section titled “Flow / OUI screen identity (2026-08-04)”Some screens never give Columbo a usable name. Meta Quest Wi-Fi Private
Address is the archetype: no Meta OUI, no DHCP hostname, only
Unknown-… on the box — while live flows hit graph.oculus.com. Steam Deck
and Switch are better (Valve/Nintendo OUI + hostnames when awake) but still
rotate and can appear empty for days.
The engine now enrolls those devices the same way parents expect curfew to work: identity follows the hardware signal, then curfew follows the screen.
Config surface (devices.yaml screens)
Section titled “Config surface (devices.yaml screens)”| Field | Role |
|---|---|
macs | Static pin floor (current known MACs) |
hostname_patterns | DHCP / Bonjour name globs (*quest*, *steamdeck*, *switch*) |
oui_prefixes | Hardware vendor blocks (Valve 2C:A1:F2, Nintendo 48:F1:EB, …) |
flow_host_patterns | Distinctive DNS hosts (oculus.com, steampowered.com, nintendo.net) |
Columbo device-canon (~/.sanctum/firewalla/device-canon.json, exposed as
columbo-overrides.json) holds the Firewalla display name so rename survives
empty DHCP (Meta Quest (Albert), Nintendo Switch 1, …). Engine load seeds
overrides for every static screen MAC.
Adoption path
Section titled “Adoption path”Every discovery tick (host list) and every other tick (flow sample):
- Score screens that match hostname / OUI / flow host.
- Resolve one target via
_resolve_identity_screen:- already enrolled → refresh name only
- hostname hit → that screen
- NIC sibling of enrolled MAC → that screen
- empty
macsslot among matches → fill it (Switch 2 gets the next unenrolled Nintendo device when Switch 1 is already filled) - sole match (Quest, Deck) → adopt
- multi-filled ambiguous → do not auto-adopt; log
AMBIGUOUS … identity
_adopt_mac_to_screenwrites the MAC intodevices.yaml, Columbo override, bridge rename, clears guest-stranger state, and blocks immediately if that screen’s curfew is already active.
Curfew stranger rejoin (same ship)
Section titled “Curfew stranger rejoin (same ship)”Unknowns offline at curfew-start used to miss the rising-edge sweep, then
rejoin after 23:00 without re-triggering first-discovery (registry restore
puts them in _known_macs). Offline→online during child curfew now
auto-pauses unreviewed strangers (unknown-rejoin-during-curfew auto-pause).
Parent-approved guests and yaml-enrolled screens are exempt.
Albert’s screens (manoir, 2026-08-04)
Section titled “Albert’s screens (manoir, 2026-08-04)”| Screen key | Identity | Notes |
|---|---|---|
meta_quest | MAC + oculus flow hosts | Private MAC; flows are the reliable signal |
nintendo_switch_1 | MAC FA:CE:DE:CA:CA:05 + Nintendo OUI/flow | Nightly curfew already closed-loop |
nintendo_switch_2 | Empty slot + same Nintendo signals | First new Nintendo device fills here |
steam_deck | Empty + Valve OUI + Steam flows | Auto-adopts on first Deck Wi-Fi / Steam host |
Apple Vision Pro advertises RealityDevice — classified as Apple / personal,
not Meta Quest. Do not enroll it as Albert’s screen.
Operator checks after a rotation
Section titled “Operator checks after a rotation”# Screen inventory (Quest should show is_screen + Meta Quest name)curl -s http://127.0.0.1:4077/screen/devices | jq '.devices[] | select(.is_screen)'
# Status device_count > 0 for quest / switch_1; deck / switch_2 after first sightingcurl -s http://127.0.0.1:4077/screen/status | jq '.screens[] | select(.key|test("quest|switch|steam|ps5"))'
# Adopt / rejoin linesgrep -E 'SCREEN IDENTITY ADOPT|unknown-rejoin-during-curfew|AMBIGUOUS' ~/.sanctum/logs/force-flow.stderr.log | tail -20Unit coverage: tests/test_quest_flow_identity.py,
tests/test_unknown_rejoin_curfew.py in sanctum-screen-time.
Continuous label reconcile (2026-08-05)
Section titled “Continuous label reconcile (2026-08-05)”Columbo’s 10-minute apply tick now treats ~/.sanctum/screen-time/devices.yaml
as the enrollment name SoT:
- Load every screen / personal / shared MAC → display name.
- Merge into device-canon (
columbo-overridessymlink). - Force Firewalla renames for enrolled MACs (
source=sanctum-registry). - Repair vendor/name pollution (e.g. NETGEAR OUI labeled as an iPhone
from stolen Bonjour) with
vendor-mismatch-repair. - Force-flow discovery also runs
_reconcile_enrolled_labelseach tick so labels cannot drift between Columbo passes.
Placeholder yaml stubs like iPhone (private) never overwrite a better live
Bonjour name. Screens and intentional names (Albert’s iPhone, Meta Quest,
Switch, …) always win.
Verifying it works
Section titled “Verifying it works”# Columbo pins (should show every powered-on console)python3 ~/.sanctum/bin/columbo/phonebook.py report | jq ._screens
# A screen's live enforce set = static ∪ pinscurl -s -H "Authorization: Bearer $(cat ~/.sanctum/secrets/screen-control-token)" \ http://127.0.0.1:4077/screen/status | jq '.screens[] | select(.key=="xbox_one")'
# Ledger should be empty when nothing is intentionally blockedsqlite3 ~/.sanctum/screen-time/usage.db 'SELECT * FROM paused_macs'
# Rename aliases remembered for a MAC (after a live rename tick)sqlite3 ~/.sanctum/screen-time/usage.db \ "SELECT alias FROM device_aliases WHERE mac='FA:CE:DE:CA:CA:01'"Live E2E on ship day: block → box shows the pinned MAC paused → ledger row → expiry → verified release → ledger clear, in under 15 seconds — run on the Xbox and the TCL Google TV, on the deployed binary.
Rename-resilience E2E (2026-07-23): enrolled Apple TV renamed on the Gold Pro while curfew was active — Force Flow still reported blocked, the box kept streaming DNS blocks on the same MAC, and hostname match still resolved both the old and new labels to the same screen key.
The promise still lives at a MAC address. The difference now is that the MAC can’t quietly walk away from it.