Recorded because these outlived the session that produced them and every one is
about code already committed.
Six findings, most severe first. The reconciler as written would revoke every guest
authorization on the whole UniFi site, not just the ones this portal created —
front-desk vouchers, contractors, anything an admin authorized by hand — and
re-authorize anything an admin deliberately revoked. That is why "not scheduled" is
currently load-bearing rather than cautious. The pre-DHCP retry cannot succeed
because all three attempts read one cached client snapshot, and the first failure
warms that cache with a snapshot lacking the device, so "try again" stays refused
for the TTL while holding a worker 1.5s per attempt. Every portal visit blanks the
device fingerprint, because the controller writes three request parameters UniFi
never sends. Session windows slide on every visit, so a bookmarked callback never
ages out. And with no unique index on (user_id, mac_address) and no locking,
concurrent callbacks can produce either two authorizations against a one-device
limit or two rows for one phone that permanently consume two slots.
Four tests pass for the wrong reason, including the headline quota-leak regression
test: it asserts a component name that four different branches render, so it stays
green with no role and no mapping at all.
TWO CORRECTIONS from production evidence the operator supplied.
The earlier claim that the request IP is forgeable was demonstrated by sending a
header directly to nginx on :80. Clients cannot do that — the Wi-Fi VLAN is
firewalled to Traefik only — so that demonstration does not show a live exploit and
must not be cited as one. What survives is narrower: the app believes any
X-Forwarded-For that reaches it, and whether a client can deliver a forged one
depends on one forwardedHeaders setting on a Traefik host that is not reachable from
here.
And spec section E had its release-gate risk backwards. It feared TrustProxies
would be too strict and lock everybody out. Production shows the opposite: the
Logged-in Users list reports real per-client addresses, 10.22.5.x for on-site staff
and a public address for an off-site phone. The IP-to-MAC approach is viable, with
evidence, and that item is not a blocker.
Also added: device type should come from the User-Agent, not from UniFi. The
platform already parses "Safari · iPhone" and "Chrome · macOS" in
ActiveSessionController::device(), and its OS table already separates iPhone, iPad
and Android from the desktop platforms. That is available at authorization time,
which UniFi's traffic-inferred dev_cat is not, and it is known to be populated,
which dev_cat in this environment never was.
Found while implementing the resolver. The spec claimed the difference between "no
client matched this IP" and "this IP is the proxy" made a TrustProxies
misconfiguration one glance to diagnose. That holds only for the case where the
forwarded header arrives and TrustProxies fails to apply it.
If Traefik is misconfigured such that no X-Forwarded-For is sent at all, the
resolver sees a plain unmatched IP and reports no_client — which reads to the
person in front of it as "your device is not on the network", and to an operator
as nothing at all. The alarm is silent in precisely one of the two ways the thing
it guards can break.
Recorded rather than patched, because the fix belongs to Plan 2's health line: it
should show the resolved client IP and say what it is, instead of reporting a
reason code. And the hardware validation step now has to confirm the true client
IP actually arrives — confirming the alarm is quiet is not evidence, since a quiet
alarm is what this blind spot produces.
Six TDD tasks covering the back-end half of the approved spec: the live-authorization
quota scope, dropping network presence as an eviction trigger, the role allow-list
with `default` removed, the IP-derived device resolver, randomized-address
detection, and the reconciler.
Split from the UI deliberately. Plan 1 leaves the portal correct and safe while
still being configured through the existing mappings CRUD, so it is worth shipping
on its own; Plan 2 covers the settings screen, the denial-page coaching copy and
the Wi-Fi Devices page.
Three things the self-review caught and the plan now handles:
· the resolver had no retry, so a device that had only just associated — and so
had no IP in UniFi's table yet — would have been told it was not connected at
the exact moment it was connecting;
· nothing captured the device fingerprint, which the report-only device-type
decision depends on. The reconciler now does it, and only ever fills in: an
unfingerprinted client must not blank a type already recorded, or the answer
is lost every time the device happens to be quiet;
· one task's test needed a method a later task introduced, and the first draft
said to leave it red. Under subagent-driven execution a fresh agent per task
would have seen red and tried to fix it, so the test now uses the parameter
flow that exists at that point and the later task migrates it.
Also records what NOT to do: do not schedule the reconciler (it calls
unauthorizeGuest against a live controller that nothing here has been tested
against), do not register the unifi app, and do not touch disconnectSession.
Two changes from the operator.
Session length and devices-per-role are set in the settings UI, on the existing
Portal settings page rather than a new one. Two presentation decisions that are
really correctness decisions:
· the role is picked from a DROPDOWN of seeded roles, not typed. match_value is
compared exactly, so "Student" or "studnet" matches nobody and the failure
surfaces as a pupil with no Wi-Fi and nothing logged anywhere. A dropdown
makes an unmatchable value impossible to enter.
· session length is entered in DAYS with presets. session_minutes stays the
stored unit, but a term is 172,800 minutes and nobody should type that — the
same reasoning that presents cron schedules as presets instead of raw strings.
And access is now an explicit allow-list: no role added means no Wi-Fi. That makes
`default` an anti-feature, because a default row matches everybody and turns the
allow-list into a suggestion, so it is removed from the available match types.
resolveMapping() already returns null when nothing matches and null already
produces the "No WiFi access configured for your account." denial, so
deny-by-default is the behaviour as soon as the catch-all cannot be created.
Safe to do now specifically because unifi_vlan_mappings has ZERO rows in every
environment — nothing is revoked from anybody. The same change made after a school
had leaned on a catch-all would cut off every unmapped person at once.
VLAN stays required per row. It was not in the request, so it is called out in the
spec rather than quietly dropped: the network still has to know where to put the
device.
Brainstormed spec for registered guest/BYOD Wi-Fi. The headline finding is that
this is not a new feature: the portal, per-cohort device limits, per-cohort VLANs,
session length, self-service eviction and admin eviction all already exist here,
and CaptureSnapshots is already collecting device fingerprints. So the spec is
about what is broken and what is missing, not what to build from scratch.
Two defects in quota accounting, in opposite directions, both of which get worse
the longer session_minutes is — and long sessions are the whole point of "do not
make users re-do the portal every connection":
· a slot is freed when isClientConnected() is false, so a phone that leaves the
building frees its slot while UniFi still holds it authorized — the user then
authorizes a second device and one of them is an orphan nothing tracks;
· expires_at is written and never read, and no job expires sessions, so a row
keeps consuming a slot long after UniFi stopped honouring it.
The fix is to count live AUTHORIZATIONS rather than present devices, and to let a
reconciler repair both directions.
Also specified: closing the MAC-ownership hole the original author documented and
declined to guess at, by deriving the MAC from the request's source IP and
verifying AP association instead of trusting ?mac=. That fails closed, which is
correct — and the spec is explicit that it can therefore fail closed for
EVERYONE if TrustProxies is wrong behind Traefik, so the resolver has to report
that case distinctly and it is the one thing to verify against real hardware
before release.
Device-type detection (keeping students off phones) is specified as report-only.
UniFi's fingerprint is derived from observed traffic and so is not available at
the moment of authorization, and unifi_client_snapshots has zero rows on dev, so
there is no evidence dev_cat is populated in this environment. Enforcing on
unvalidated data would lock out the wrong people. MAC OUI is rejected outright:
a randomized MAC has no real OUI, so it would fail on phones while working on the
laptops that should not be blocked.
No version bump: documentation only, no runtime change, and bumping would put the
repo ahead of prod for a file that cannot affect it.