Decision 0093
0093. A scan is asked of the supplicant that owns the radio
Architecture decision record 0093: 0093. A scan is asked of the supplicant that owns the radio
Context
ADR-0085 gave this
machine the read: system.wireless speaks nl80211 by hand and returns what the kernel already
heard. It left the write, and said why in one sentence — triggering a scan “needs CAP_NET_ADMIN,
which means it belongs to the stem and wants its own record.”
That sentence is right about the half it names, and wireless.rs’s own header, written the same
day and three files away, carried the other half: triggering “needs CAP_NET_ADMIN, which nothing
here has, and would fight with the supplicant that owns the interface”. The capability was
never what was stopping it.
iwd owns the interface on a Tacid machine. It scans on its own every few seconds while a station is
disconnected — most often exactly when a person is looking, which is why the read is useful at all.
Two scan requests on one interface is -EBUSY for whoever asked second, and the second can be this
machine’s own supplicant, failing at the moment somebody is standing there waiting for a list. A
stem holding CAP_NET_ADMIN does not fix that; it just makes this machine the one that wins the
race sometimes.
So the question was never “how do we get the capability”. It was which door to knock on, and there were three:
NL80211_CMD_TRIGGER_SCANfrom the stem. Shortest, no dependency, and what ADR-0085’s sentence assumes. It races iwd on every machine where iwd is what makes the answer useful.- iwd’s own D-Bus interface,
net.connman.iwd.Station.Scan. The supplicant is asked rather than raced, and it already serialises its own scans. iwctl station <device> scan. Consistent with how the stem runs tools, and not a violation of ADR-0028 — that rule forbids parsing prose, not invoking programs — butiwctlis a readline shell with a command mode bolted on, which is a poor foundation for a guarded operation.
Decision
Ask iwd, over its own D-Bus interface, and reach that interface with busctl.
The first half is about correctness: the process that owns the radio is the one that should be told to use it, and it is the only one in a position to serialise the request against the scans it starts itself.
The second half is about where a dependency would land. ADR-0085 declined zbus for the read
because it is a large dependency for one question and there was a kernel socket to use instead.
That argument does not transfer to a write — there is no kernel socket here that does not race —
so it would have had to be made again, and it would have been made about the stem, whose
Cargo.toml lists five crates and is that short on purpose: it is the only process here that runs
as root.
It does not have to be made at all. busctl is systemd’s own D-Bus client, systemd is this
machine’s init (ADR-0002), and the stem already reaches systemd-run from
the same directory through the same helper. iwd’s door costs no dependency in the one process where
a dependency costs the most.
Operation::NetworkScan takes an interface name, never a D-Bus object path. iwd names a
station /net/connman/iwd/<wiphy>/<ifindex>, and both numbers are files the kernel writes under
/sys/class/net/<name>/ — so the path is derived where the work happens and nothing a caller
supplies reaches the bus as a destination. That is the same shape as NetworkKnow, which takes an
SSID and builds the filename itself rather than being handed one. Asking the bus what objects it has
would mean reading busctl tree, which prints a tree for a person, and that is the one thing
ADR-0028 is about.
It is not guarded: a scan changes nothing a later generation would have to undo. A radio looks,
the kernel’s list is newer, and that is the whole of the effect. What stands in front of it is the
keeper, refusing a principal who could not approve a change — the same standard as NetworkKnow.
And it is asked, not waited on. iwd answers when it has started a scan; what the radio hears
arrives over the seconds after. tacid network scan returns then, and tacid network a moment
later says what was heard. A verb that blocked until a list was ready would be a verb that lies
about what a scan is.
Consequences
The two halves of one question now go two different ways, and that is right rather than untidy:
system.wireless reads the kernel directly because reading is not changing, and a scan goes
keeper-then-stem because it is. Nothing in the fact layer gained a capability.
tacid network scan takes an interface only when it needs one. A laptop has one radio, and naming
it every time would be a verb asking a person for what their machine already knows; a machine with
two is asked which, because scanning on an interface somebody did not mean is worse than a
question. An interface is wireless when it has a phy80211, which is the kernel’s own answer —
there is no name pattern that is reliable, since wlan0, wlp2s0 and wlan-home are all real and
so is an ethernet port somebody renamed.
The stem grew its twenty-fourth operation and its twenty-fourth tool, and finding room for the tool found something else: the guard that exists to stop a tool arriving unaccounted for could not see a tool whose call rustfmt had wrapped across two lines. Six had. That is written up in the build log and fixed in the same change; it is mentioned here because this ADR is the reason anybody looked.
What is proven and what is not, exactly. The interface resolution, the refusals and the authority
chain are held by the lifecycle leg on a machine with four wireless interfaces — hostapd’s three
virtual ones and a station — which is the awkward case rather than the easy one. The busctl call
itself is described there and not made, because that leg’s stem is the stand-in
(TACID_STEM_DRY_RUN). A real scan on a real radio wants a machine with a wireless card iwd can
have to itself, which is the same thing the association half of ADR-0085 is still waiting for.