NextBSD WLAN plan PHASES 0–4 MERGED AWAITING HARDWARE

Grounded in a source-level review of nextbsd-redux/{nextbsd-userland, nextbsd-kernel, nextbsd-kernel-modules, nextbsd-freebsd-compat, nextbsd, nextbsd-overlays} and gershwin-components at 2026-07-13. Background: WLAN on NextBSD — the Apple stack, whose integration half this replaces. That half was written against the pre-Mach freebsd-* / pkgdemon era and assumed dhcpcd and Distributed Objects — neither of which NextBSD uses.

TL;DR

Status — 2026-07-14: Phases 0–4 are written, CI-green, and merged. Everything below describes what was built. What is not yet true is on the last line, and it is the important one.
PhaseWhat landedWhere
0 — kerneldevice wlan_xauth. WPA/WPA2/WPA3 can now associate at all; before this every association failed EINVAL at the first ioctl.kernel#54 (closes #51)
2a — ipconfigdMulti-interface: per-interface lease table + a dedicated DHCP worker thread per NIC; teardown (SIOCDIFADDR + RTM_DELETE) on lease loss and link-down; primary elected by rank (wired > wireless) instead of last-binder-wins. Two further latent bugs fixed in passing — a publish failure used to silently skip the lease loop, and /etc/resolv.conf was written per-interface (so wlan0 stomped a wired em0's DNS) and non-atomically.userland#43 (closes #37 #38 #39 #41)
2b — configdRTM_IFANNOUNCE in the link monitor, so a VAP cloned at runtime is visible and a destroyed one stops leaving a stale Link key. wland therefore needs no PF_ROUTE listener of its own.userland#44 (closes #42)
2c — the gateDHCP now requires Link{Active} and AirPort{Authenticated}. Key-absent-means-ready, so every wired NIC is bit-for-bit unchanged.userland#45
3 + 4 — wland/usr/sbin/wland (job org.nextbsd.wland, service org.nextbsd.wlan) + the wlan CLI. VAP lifecycle, stock wpa_supplicant driven over wpa_ctrl with an empty config, known networks in SCPreferences, autojoin, and the AirPort publish.userland#46

What CI proves: it all builds clean on amd64 and arm64, and the qemu boot-test passes with wland running under launchd (launchctl list shows 31 - org.nextbsd.wland, alive). Critically, the wired em0 DHCP path is untouched — which is the regression that mattered.

What CI cannot prove, and nobody should pretend otherwise: there is no 802.11 in qemu. Association, the 4-way handshake, the AirPort publish firing, and the DHCP gate actually releasing are reasoned, reviewed, and grounded in the kernel source — but they have never been executed. The first run on a real Intel radio is the real test, and it is the next thing that should happen.

1. Three blockers between "Intel card" and "IP address"

Each is independent. All three must be fixed before WLAN works at all.

1.1 wlan_xauth is missing BLOCKER SHIP THIS NOW kernel#51

This one is a self-contained bug fix. It does not depend on any other decision in this document, and it should not wait for one. It is a regression NextBSD introduced against stock FreeBSD, the fix is one additive line, the blast radius is zero, and it is independently testable. Everything else in this plan is about making WLAN good; this is the difference between WPA being impossible and WPA working.

Symptom

Every WPA / WPA2 / WPA3 network fails to associate. Open networks work fine. There is no useful error surfaced to the user — wpa_supplicant simply never leaves SCANNING.

Root cause

ioctl(SIOCS80211, IEEE80211_IOC_AUTHMODE) returns EINVAL, always. wpa_supplicant sets the authmode to IEEE80211_AUTH_WPA. ieee80211_ioctl.c:2858-2870 calls ieee80211_authenticator_get(), which (ieee80211_proto.c:471-478) looks up auth_modnames[IEEE80211_AUTH_WPA] = "wlan_xauth" and tries ieee80211_load_module("wlan_xauth")kldloadno such module on disk → returns NULL → EINVAL. Association never begins.

Two independent facts combine to produce this, and neither is wrong on its own:

  1. wlan_xauth is not in the kernel. NextBSD's config is include GENERIC plus additions (nextbsd-kernel/config/NEXTBSD:1) — nothing wireless is excluded, and device wlan + wlan_ccmp/wlan_tkip/wlan_wep/wlan_gcmp/wlan_amrr all arrive from GENERIC. But wlan_xauth is not in GENERIC either — upstream FreeBSD deliberately leaves it out, because net80211 autoloads it as a .ko on demand. On stock FreeBSD this is invisible and self-healing.
  2. NextBSD has no .ko tree to autoload from. nextbsd-kernel/ci/assemble-image.sh:168 does rm -f "$ROOTFS"/boot/kernel/*.ko — the entire module tree is deleted at image-assembly time; only .kext bundles survive. This is intentional and correct for the kext model.
The general lesson, beyond WLAN: on NextBSD, every lazy kldload path in the kernel is dead. Any subsystem that relies on autoloading a module on first use will fail silently and non-obviously, because the recovery mechanism upstream depends on has been removed wholesale. net80211 alone has four such call sites (ieee80211_proto.c:476, ieee80211_crypto.c:370, ieee80211_ratectl.c:125, ieee80211_scan.c:191). This is worth a separate auditwlan_xauth is simply the first instance anyone tripped over. Tracked as kernel#52.

Fix

# nextbsd-kernel/config/NEXTBSD
device      wlan_xauth   # 802.11 802.1x/WPA authenticator.
                         # net80211 autoloads this as a .ko on stock FreeBSD;
                         # NextBSD ships no .ko tree (assemble-image.sh:168),
                         # so it must be compiled in.

Risk: none. It is a purely additive kernel-config line that statically links a small, in-tree net80211 module that upstream ships and every other FreeBSD system loads on demand. It cannot regress the wired path, it changes no userland, and it touches no other subsystem. If WLAN is not present on a machine, the code is simply never entered.

Verification (standalone — no other phase required)

# Before: expect this to fail with "Invalid argument"
ifconfig wlan0 create wlandev iwlwifi0
wpa_supplicant -i wlan0 -c /tmp/wpa.conf -d      # never leaves SCANNING

# After: expect association to complete
wpa_cli -i wlan0 status                          # wpa_state=COMPLETED

# Sanity-check the authenticator actually registered:
sysctl net.wlan.devices                          # radio is visible
ifconfig wlan0 list caps                         # WPA/RSN advertised

wlan_acl and wlan_rssadapt are the same latent class of failure and would be reasonable to add in the same commit, but neither is needed for a station — they only matter for hostap mode and alternate rate control, so leaving them out is defensible.

1.2 Nothing creates the wlan0 VAP BLOCKER new work — wland

On FreeBSD, wlans_iwlwifi0="wlan0" in rc.conf clones the 802.11 VAP from the physical device. NextBSD has neither: nextbsd-freebsd-compat/.github/workflows/build.yml:127 hard-strips /stage/etc, installworld (not make distribution) is used deliberately so /etc content is never generated, and /sbin/init is in the superseded list because launchd is PID 1. There is no rc.d and never will be.

So ifconfig wlan0 create wlandev iwlwifi0 has no owner. This is a responsibility Apple's wifid never had — IO80211 presents en0 directly — and it means the phy→VAP lifecycle must belong to something on NextBSD. That something is wland.

1.3 ipconfigd DHCPs too early, then gives up forever BLOCKER userland#37

The trigger path has no interface-type check at all. The only filter anywhere is strncmp(ifname, "lo", 2).

wlan0 hits net80211 RUN (associated, NOT yet keyed) → if_link_state_change(LINK_STATE_UP) → RTM_IFINFO → configd config_link_monitor.c:269 publish_link("wlan0", 1) → State:/Network/Interface/wlan0/Link = {Active: true} → ipconfigd sc_link_watch.c:148 handle_link_key() ← "wlan0" is not "lo*" → ipconfigd.c:359 on_link_event(active=1) ← no type check → dhcp_run_on_interface("wlan0") DHCPDISCOVER on an unauthenticated port → dropped. {4,8,16}s ladder burns ~28s. IPCFG-BOUND-FAIL. → re-arm… but Link is ALREADY Active, so no new event ever arrives. → silence, forever.

Two aggravations. net80211 VAPs go through ether_ifattach, so they report IFT_ETHER — meaning even dhcp_pick_interface()'s sdl_type != 6 filter (dhcp_discover.c:111) will happily select wlan0. And the {4,8,16}s retransmit ladder means the user watches a 28-second hang before nothing happens.

1.4 The fix: gate DHCP on an AirPort key

Make ipconfigd's contract honest. Today it assumes Link{Active} means "L2 is usable for IP"; net80211's RUN state breaks that assumption for WPA. The repair is to require a second predicate — but only where a WLAN daemon has declared itself the authority.

Single file, src/IPConfiguration/sc_link_watch.c, ~70 LOC:

ChangeLOC
Second subscription pattern State:/Network/Interface/[^/]+/AirPort alongside the existing …/Link; widen the CFArrayCreate at :251-253 and the initial SCDynamicStoreCopyKeyList sweep at :282.~8
Rename handle_link_keyhandle_iface_key. Its ifname parse (:165-173) already works verbatim for both entities; route both into one evaluate_interface().~15
evaluate_interface(store, ifname) — read /Link {Active}, call wlan_ready(), fire schedule_link_event(ifname, 1) only when both hold. The admin-up path (active=0) is unchanged.~25
wlan_ready(store, ifname)SCDynamicStoreCopyValue on State:/Network/Interface/<if>/AirPort. Key absent ⇒ return 1.~25
Key-absent-means-ready is the load-bearing default. No WLAN daemon has claimed em0, so em0 behaves bit-for-bit as it does today. Zero regression risk on the existing wired CI path — which matters, because tests/boot-test.sh gates on it.

And the re-trigger falls out for free. wland publishes AirPort {Authenticated: false} on association and {Authenticated: true} on CTRL-EVENT-CONNECTED. That second configset is itself a change on the new pattern, so configd wakes the existing linkwatch session, evaluate_interface("wlan0") re-runs, /Link is already Active, the gate is now open → DHCP. Event-driven, no polling, no debounce, no new IPC. It also incidentally fixes the "one failed attempt then silence" hole: a re-association generates a fresh trigger even though /Link never moved.

No configd change. No ipconfig.defs change. No MIG regeneration.

2. What already exists (so we don't rebuild it)

PieceStatusEvidence
wpa_supplicant 2.11, wpa_cli, wpa_passphrase, hostapd, ifconfig, wlandebugSHIPSFreeBSD releng/15.1 base, untouched. Zero wireless paths in collisions or superseded; no WITHOUT_WIRELESS knob anywhere.
net80211 + wlan_ccmp/tkip/wep/gcmp/amrrIN KERNELInherited from GENERIC. config/NEXTBSD:1
LinuxKPI mac80211 shim (linux_80211.c)IN KERNELoptions COMPAT_LINUXKPI + optional compat_linuxkpi wlan
IntelWiFi.kext (if_iwlwifi) + 267 MB firmwareSHIPSBuilt by nextbsd-kernel-modules; firmware bundled in Contents/Resources/firmware, found via patch 0004's linker_file_foreach search. kext-smoke-test in CI.
SCDynamicStore (create/get/set/copy-key-list/notify, dispatch + runloop sources)WORKSlibSystemConfiguration/SCDynamicStore.c, SCNotify.c
SCPreferences — atomic, locked, commit-notifying plist storeWORKSSCPreferences.c: temp-file + rename(), lockfile, /Local/Library/Preferences/SystemConfiguration/preferences.plist
wlanN typed as kSCNetworkInterfaceTypeIEEE80211WORKSSCNetworkInterface.c:227-243 — name-prefix heuristic, because FreeBSD 802.11 clones report IFT_ETHER
CoreFoundation plist read/write (XML + binary)WORKSVendored swift-corelibs CFPropertyList.c
Gershwin Network.prefPane with WLAN tab (scan / connect-with-password / radio toggle)EXISTSgershwin-components/Network/NetworkBackend protocol + BSDBackend.m. Gershwin's to evolve, not this plan's — see §6 Phase 5.
kSCEntNetAirPort + 802.11 property keysABSENTSCSchemaDefinitions.h:71-78 stops at IPv4/IPv6/DHCP/DNS/Proxies/Interface/Link
Keychain / Security.framework / any secret storeABSENTNot in src/, not in PORTING.md, not planned
Objective-C runtime / Foundation in nextbsd-userlandABSENTNo libobjc2; zero .m files built; toolchains enable C/C++ only
Any Gershwin process speaking MachNONE, EVERZero mach/, bootstrap_look_up, xpc_, or SC references in the component tree

3. Approaches ruled out (do not relitigate)

3.1 iwd DEAD

Two independent fatal blockers. It is welded to nl80211/cfg80211 — there is no net80211 backend, and iwd deliberately leans on cfg80211 semantics rather than abstracting over them, so a FreeBSD port is a rewrite of the layer the daemon is built on, not a driver shim. And it is LGPL-2.1 (can't go in base), on top of dragging in ell and D-Bus.

What survives: iwd's object model — Station / Network / KnownNetwork / Agent — maps almost one-to-one onto CoreWLAN's CWInterface / CWNetwork / credential-prompt flow, and it already contains the autojoin brain wpa_supplicant lacks. iwd contributes ideas; wpa_supplicant contributes code.

3.2 A custom supplicant seeded from Apple's sources DEAD

There is no seed. eapolclient (eap8021x) does the EAP exchange only — it derives a PMK for Enterprise networks and hands it back. It does not scan, does not associate, and does not do the 4-way handshake. All of that lives in closed wifid and IO80211 firmware offload. And eap8021x is not in the NextBSD tree at all.

So "custom wland based on Apple's supplicant" means implementing SAE/Dragonfly, the 4-way handshake and key hierarchy, PMKSA caching, PMF/802.11w, and FT from scratch — the exact code that produced KRACK and Dragonblood — in a codebase where we are the only auditor. No.

3.3 Vendoring wpa_supplicant into nextbsd-userland/src and teaching it Mach REJECTED

Four reasons, any one sufficient:

Also don't add the port. security/wpa_supplicant is the same 2.11, but installs to /usr/local/sbin and shadows base for no benefit. (FreeBSD's official pkg repo is configured and enabled — nextbsd/build.sh:169-187 writes pkg+https://pkg.FreeBSD.org/FreeBSD:15:${ABIARCH}/latest and rebrands ABI precisely so FreeBSD:15 packages install cleanly — so ports remain available if we ever need them. We don't, here.)

wpa_supplicant never needs Mach. It speaks net80211 ioctls downward and a UNIX control socket upward. It does not link CoreFoundation and does not know configd exists. All Mach knowledge lives in wland, which gets it for free by linking libSystemConfiguration — the same way ipconfigd does.

4. The architecture

Gershwin (ObjC / GNUstep — no Mach, non-Mach libdispatch, own Foundation) ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ menubar WLAN status item — OUT OF SCOPE. Gershwin's call. └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ┌──────────────────────────────────────────────────────────────────┐ │ System Preferences → Network.prefPane [IN SCOPE, ph.5] │ │ EXISTS: WLAN tab + NetworkBackend protocol │ │ ADD: SCWLANBackend (3rd impl, beside NMBackend/BSDBackend) │ └───────────────────────────────┬──────────────────────────────────┘ │ NSTask + privileged helper │ (BSDBackend.m already does exactly this) ▼ ═══════════════════════════ Mach line ════════════════════════════════ /usr/sbin/wlan ← C CLI, MIG client │ (mirrors ipconfig.c) │ MIG / org.nextbsd.wlan ▼ /usr/sbin/wland ← C + CoreFoundation │ ┌───────────────┬───────────┴────────────┬────────────────────┐ │ │ │ │ VAP lifecycle own PF_ROUTE wpa_ctrl socket SCPreferences ifconfig create RTM_IFANNOUNCE ───────────────► known networks /destroy (configd does NOT stock wpa_supplicant preferences.plist handle this) 2.11 from base │ │ └──────────────► SCDynamicStore ◄──────────┘ State:/Network/Interface/wlan0/AirPort { Authenticated, SSID, BSSID, RSSI, Channel, Security } │ ▼ ipconfigd sc_link_watch.c — DHCP requires Link{Active:true} AND AirPort{Authenticated:true}

4.1 Why the CLI bridge, and not linking SC into the prefPane

No Gershwin process has ever spoken Mach — zero mach/, bootstrap_look_up, xpc_connection, or SystemConfiguration references anywhere in gershwin-components. And this is deliberate, not accidental: gershwin-developer/Library/Scripts/Install-System-Domain.sh:74-95 force-disables HAVE_MACH when building Gershwin's libdispatch on NextBSD, so the desktop's dispatch is isolated from the base stack's Mach one. Gershwin apps link gnustep-base's Foundation, not libCoreFoundation.so.6. PORTING.md:838-846 states the policy: "three CF lanes, one CF per binary."

Linking libSystemConfiguration + libmach into a GNUstep bundle collides head-on with that split. The cheap path is the one already in the tree: ipconfigd is a Mach daemon and ipconfig is a pure-C CLI Mach client (bootstrap_look_up at ipconfig.c:150-154). Do the same. BSDBackend.m:262-268 already invokes a privileged helper via NSTask, so a /usr/sbin/wlan binary drops straight into the existing invocation path with no ObjC↔Mach linkage problem at all.

The CoreWLAN-shaped ObjC API stays above the Mach line, where ObjC actually exists. That is also where it belongs architecturally.

5. The path DECIDED: BUILD IT PROPERLY

Decision (2026-07-13): we build wland. The shortcut is rejected. The table below is retained as the rationale, not as an open question — it records what we are choosing against and why, so this doesn't get relitigated.

The two options were never competing architectures — the shortcut is a strict subset of the real thing's first phases. The only question was where to stop, and the answer is: we don't.

Why the shortcut was rejected

It doesn't actually work. That is the decisive argument, and it isn't an aesthetic one. Without wland, nothing publishes an AirPort key — so the DHCP gate in §1.4 has nothing to gate on, and every alternative is worse: hold the interface admin-down until wpa_supplicant reports CONNECTED (ugly, and racy against ipconfigd's own iface_bring_up), or let the 28-second DISCOVER ladder fail and hope for a manual retry (which the sticky-failure bug means never comes). The minimum honest shim — something that watches wpa_ctrl and publishes AirPort{Authenticated}is wland v0. There is no cheaper thing that is also correct.

And the shortcut's end state is one we'd have to demolish. It leaves PSKs in /etc/wpa_supplicant.conf, autojoin as an integer, nothing in the dynamic store, and System Preferences shelling out through sudo -A to tools that don't exist on this system. Every one of those is later thrown away. The work isn't saved, it's deferred and then discarded.

Shortcut REJECTEDWhat we're building CHOSEN
Scopewlan_xauth + a launchd job that creates the VAP and runs wpa_supplicant + a DHCP gateAll of that, plus wland + the wlan CLI + an SC-backed System Preferences backend
EffortWeeksMonths
CredentialsPlaintext /etc/wpa_supplicant.conf, 0600, via wpa_cli save_config — what BSDBackend.m does todayPlaintext root-owned preferences.plist via SCPreferences. Still plaintext — there is no keychain either way
Autojoinwpa_supplicant's priority integerwland policy: RSSI ranking, last-used, per-network enable
Dynamic storeNothing published. DHCP gate has to be hacked another way (see below)State:/…/AirPort published; gate is clean and event-driven
System Preferencessudo -A + NSTask shell-outs — and to what is unclear, since dhclient and sysrc don't exist hereNetwork.prefPane gets a real backend: wlan CLI → MIG → wland
ShapeGhostBSDApple
What "decided" does and doesn't mean. It does not mean nothing lands until wland is finished. Phases 0–2 are independently useful and independently shippable — §1.1's wlan_xauth fix in particular should go in as its own PR this week, and Phase 2's ipconfigd repairs are worth doing on their own merits (they fix two latent bugs that have nothing to do with WLAN). What it means is that we don't stop there, and we don't build a throwaway credential store or a throwaway DHCP hack on the way.

6. Phases

Phase 0 — unblock the kernel 1 LINE · SHIP NOW

device wlan_xauth in nextbsd-kernel/config/NEXTBSD. This is not really a phase — it is a bug fix that happens to be a prerequisite. It has no dependency on Q1 (ship vs right), on wland, or on anything else in this document, and it should go in as its own PR rather than waiting for a WLAN decision. Full write-up, risk assessment, and standalone verification steps: §1.1. Ticket: nextbsd-kernel#51 (and the systemic audit, #52).

Phase 1 — prove the chain by hand

ifconfig wlan0 create wlandev iwlwifi0
ifconfig wlan0 up
wpa_supplicant -i wlan0 -c /tmp/wpa.conf -B
wpa_cli -i wlan0 status          # expect wpa_state=COMPLETED

Expect DHCP to fail here — that is the §1.3 race, and observing it is the point. Confirms driver, firmware, net80211, wlan_xauth, and the supplicant all work before any new code is written.

Phase 2 — ipconfigd: the gate + the latent bugs

The ~70-LOC sc_link_watch.c gate from §1.4, plus the pre-existing bugs in §8 — the gate does not work without them. Tickets: #37 (dead re-arm) and #38 (global single-flight) are hard blockers for the gate; #39, #40, #41 are the rest of the multi-interface debt.

Phase 3 — wland v1 (C + CoreFoundation)

/usr/sbin/wland, launchd job org.nextbsd.wland serving Mach service org.nextbsd.wlan. Responsibilities:

Why org.nextbsd.* and not com.apple.*. The shipped daemons (com.apple.configd, notifyd, IPConfiguration, …) keep Apple's exact labels, and that convention is documented in the Keychain plan — but it is the state of the shipped image, not an endpoint. The intent is to move to org.nextbsd.* across the board; what defers it is that those labels are baked into running jobs, plist filenames, and bootstrap_look_up() call sites, so the sweep is a real code-breaking migration that has to be scheduled.

wland doesn't have that problem, on two counts. Nothing Apple-derived looks its label up — we write the wlan CLI, we write SCWLANBackend, and the CoreWLAN-shaped ObjC API lives above the Mach line in Gershwin — so unlike securityd (whose label is a compatibility surface, because a clean-room Security.framework hardcodes bootstrap_look_up("com.apple.securityd")), the string is genuinely free. And Apple's daemon is wifid, not wland, so there is no Apple label to inherit anyway — com.apple.wland would be a fabricated Apple name for a daemon Apple never shipped. Nothing is built yet, so this costs nothing now and saves a migration later.

Phase 4 — the wlan CLI

/usr/sbin/wlan, a pure-C MIG client modeled on src/IPConfiguration/ipconfig.c. This is both the debugging tool (there is no scutil — see §7) and the desktop's bridge across the Mach line.

Phase 5 — System Preferences backend

A third NetworkBackend implementation (SCWLANBackend) in Network.prefPane, alongside NMBackend and BSDBackend, driving the wlan CLI from Phase 4.

This is needed rather than optional: the existing BSDBackend shells out to wpa_cli, dhclient and sysrc, none of which exist on NextBSD. So the NextBSD backend cannot be BSDBackend with tweaks — it needs something to talk to, and Phase 4 is what gives it one. The protocol itself doesn't change: scanForWLANs, connectToWLAN:withPassword:, setWLANEnabled:, savedConnections are already declared and already drive the WLAN tab.

Scope stops at System Preferences. A menubar WLAN status item is not part of this plan — that is Gershwin's to design and schedule, on Gershwin's own roadmap. This plan's obligation to it is only to make it possible: the wlan CLI and a stable State:/Network/Interface/<wlan>/AirPort schema are the contract, and anything Gershwin builds on top of them is out of scope here.

7. Constraints that shape the code

MIG out-of-line data is broken in this port's kernel. ipconfig.defs:19-22 says so explicitly — which is exactly why ipconfigd vendors only 2 of Apple's ~21 routines (Apple's ipconfig_set takes OOL xmlData). So wland cannot return a scan-results plist over MIG. Use the flat indexed pattern ipconfigd already established (ipconfig_if_count + ipconfig_if_addr): wlan_scan_count() then wlan_scan_entry(i).
Scan results cannot go in the dynamic store either. CONFIG_DATA_MAX (config_types.h:74) rejects any key or value over 8192 bytes, and a busy scan blows straight past that. Only the compact connected-state summary belongs in State:/…/AirPort. Scan results go over the MIG surface to the CLI.
There is no keychain, not started, not planned. WLAN passwords are plaintext-on-disk regardless of path. The only choice is which plaintext file. Decide it consciously (§9 Q2) rather than letting it happen by default.

Three more, less severe:

7.1 The daemon recipe, concretely

Binary to /usr/sbin/wland/System/Library holds data only (plists, kexts), never executables. A bsd.prog.mk Makefile, not CMake (CMake is used by exactly two components and the cross-toolchain file says so explicitly). Plist in overlay/System/Library/LaunchDaemons/, which ships verbatim via cp -aR overlay/. /stage/. Build block in build-userland.sh after IPConfiguration, generating MIG stubs externally and passing them in via MIGOUT=.

The link line matters and is easy to get wrong after the #378 rename:

LDADD+=  -llaunch -lsystem_kernel
LDADD+=  -lSystemConfiguration -lCoreFoundation
LDADD+=  -lsystem_dispatch -l:libsystem_blocks.so
CFLAGS+= -fblocks
WARNS=   0                       # CF public headers are not -Werror clean
BINDIR=  /usr/sbin
-ldispatch and -lBlocksRuntime no longer exist in base. Those names now belong to Gershwin's separate stock copies; linking them is what caused the rtld collision that crashed launchctl/dbus/sshd. Use -lsystem_dispatch and the exact-file -l:libsystem_blocks.so form.

8. ipconfigd debt that WLAN forces us to pay

Both bugs are pre-existing and independent of WLAN. Both will defeat the §1.4 gate if left alone. Neither has been noticed because NextBSD has only ever been tested on a single wired NIC.

The re-arm is dead code. bound_state_set(NULL, …) is never called anywhere — the only call site (ipconfigd.c:262) always passes a live lease. So after a lease expires, bound_state_any() stays true forever and the re-arm at ipconfigd.c:426 can never fire. Expire once ⇒ that ipconfigd never DHCPs again until restarted.
DHCP is a global single-flight. g_dhcp_started (ipconfigd.c:97-98) + bound_state (bound_state.c:12-17, one global struct, bound_state_count() returns 0 or 1) permit exactly one bound interface for the daemon's lifetime. On a laptop with em0 and wlan0, whichever links first wins and the other is ignored forever — so the WLAN gate will work correctly and still be defeated by em0 winning the race.

Related, and the next thing to break: State:/Network/Global/IPv4 is last-binder-wins. sc_publish.c:230-235 has ipconfigd declare whichever service just bound to be primary — the comment admits it. There is no IPMonitor, no service-order arbitration anywhere. mDNSResponder and hostnamed both read that key. Wired-only, nobody noticed. Add WLAN and "primary interface" is a race.

The honest read: ipconfigd is currently a single-interface wired-desktop daemon, and WLAN is what forces it to grow up. Also unhandled and worth knowing: no teardown on link-down (address and default route stay installed), no SIOCDIFADDR/RTM_DELETE/DHCPRELEASE anywhere, and /etc/resolv.conf is rewritten on bind but not on renew — so a renewal that changes DNS servers updates the store and not the resolver.

9. Hardware scope

TargetStatusWhat's needed
amd64 + Intel (iwlwifi)WORKS after Phase 0Nothing. Kext + firmware already ship.
amd64 + legacy Atheros (ath)SHOULD WORK after Phase 0Compiled into the kernel from GENERIC; needs no firmware. Untested.
Realtek (rtw88/rtw89), MediaTek (mt76), ath10k/11kNOEach needs a kext + firmware following the IntelWiFi pattern (personality generator + ko2kext + linux-firmware sparse-clone). No kernel work — LinuxKPI is already baked in.
USB WLAN (run, urtwn, otus, rsu)NOAbove, plus IOKit matcher work — the personality generator only emits IOProviderClass = IOPCIDevice.
arm64 — any WLAN at allNO net80211 WHATSOEVERarm64 GENERIC never sets device wlan. The entire 802.11 stack is absent from that kernel. Needs device wlan + the crypto set added explicitly to config/NEXTBSD.

10. Tickets

Filed 2026-07-13. Every one of these is a real bug independent of WLAN — they are all currently masked by the fact that NextBSD has only ever been exercised on a single wired NIC. WLAN is simply what makes them reachable. They are the foundation, and they go in before wland.

Foundation — must land first

TicketWhatBlocks
kernel#51 blocker wlan_xauth missing — every WPA/WPA2/WPA3 association fails EINVAL Everything. One additive config line; zero risk; ship independently. §1.1
userland#37 blocker ipconfigd: dead re-arm — bound_state_set(NULL,…) is never called, so after one lease expiry DHCP never runs again The DHCP gate. §8
userland#38 blocker ipconfigd: global single-flight — exactly one interface can ever bind, for the daemon's lifetime The DHCP gate. Without this, em0 wins the race and wlan0 is ignored forever even when the gate works correctly. §8
userland#42 strongly wanted configd: link monitor handles RTM_IFINFO only — no RTM_IFANNOUNCE, so runtime-created interfaces are invisible and destroyed ones leave stale Link keys Not a hard blocker, but if it lands, wland does not need to run a duplicate PF_ROUTE listener just to see the VAPs it creates. Worth doing first. Phase 3

Multi-interface debt — same root cause, do them together

TicketWhat
userland#39ipconfigd: no deconfigure path — address and default route survive link-down, lease expiry, and interface removal. Every time you walk out of WLAN range, a stale IP and a stale default route stay installed.
userland#40ipconfigd: /etc/resolv.conf not rewritten on renew (DNS change updates the store but not the resolver), and the write is non-atomic (no temp file, no rename(), no fsync).
userland#41ipconfigd: State:/Network/Global/IPv4 is last-binder-wins — no service-order arbitration, no IPMonitor. mDNSResponder and hostnamed both consume this key, so a wrong answer propagates into Bonjour and hostname resolution.

Systemic / scope

TicketWhat
kernel#52Every lazy kldload path in the kernel is deadassemble-image.sh:168 deletes the whole .ko tree, so any subsystem that autoloads a module on first use fails silently. wlan_xauth is just the first instance anyone tripped over; net80211 alone has four such call sites, and three of them work today only by the accident that GENERIC happens to compile them in. Deserves a real audit and a CI guard.
kernel#53arm64 has no net80211 whatsoever — arm64 GENERIC never sets device wlan. WLAN is amd64-only. Low priority; filed so the hardware matrix is honest. §9
Suggested order. kernel#51 first and alone — it is one line and unblocks WPA on hardware we already ship. Then #37 + #38 together (they are the same "ipconfigd assumes one interface forever" mistake seen from two angles), ideally with #39 since teardown is coupled to clearing the bound state. Then #42, which simplifies wland. Only then start wland itself.

11. Open questions

Q1 — Shortcut or build it properly? Answered 2026-07-13: build it properly. Not an open question. The rejected alternative is documented in full at §5, deliberately — so that it is on the record what we chose against and why, and so it does not get re-proposed as a fresh idea in six months. Short version: the shortcut doesn't work (nothing publishes the association-complete signal the DHCP gate needs), and its end state is entirely throwaway.
Q2 — Where do PSKs live, given there is no keychain? Plaintext either way. /etc/wpa_supplicant.conf (0600, root — what Gershwin's BSDBackend does today) or a root-owned preferences.plist under /Local/Library/Preferences/SystemConfiguration. The latter is Darwin-shaped and gives wland sole ownership of network blocks (which we want anyway, per §6 Phase 3); the former is what already works. Not a security difference — only an architecture one. A real keychain is greenfield and its own multi-month project.
Q3 — Does wland own the VAP, or does a separate one-shot job create it? A trivial launchd job could run ifconfig wlan0 create wlandev iwlwifi0 at boot and be done. But it can't handle hotplug (USB dongles), can't handle multiple radios, and leaves nobody owning teardown. Since wland must run a RTM_IFANNOUNCE listener regardless (configd doesn't), giving it the VAP lifecycle is barely more work than not.
Q4 — Enterprise (802.1X/EAP)? Comes free from wpa_supplicant's built-in EAP stack, or not at all — eap8021x is absent from the tree and porting it buys nothing wpa_supplicant lacks. Defer until someone actually needs WPA-Enterprise.
Q5 — ipconfigd multi-interface: how far? §8's two bugs are the minimum. But a laptop that roams between wired and WLAN really wants service-order arbitration (an IPMonitor), which is a genuinely larger piece. Is "both interfaces can bind, primary is whoever bound last" acceptable for v1?

12. Provenance

Written 2026-07-13 against these trees at these commits:

RepoHEAD
nextbsd-redux/nextbsd-userlanddf01921 (2026-07-10)
nextbsd-redux/nextbsd-kernel1b2eb00 (2026-07-10)
nextbsd-redux/nextbsd-kernel-modulesf9f065c (2026-07-12)
nextbsd-redux/nextbsd-freebsd-compatafeb233 (2026-07-05)
nextbsd-redux/nextbsdf2c647e (2026-07-10)
nextbsd-redux/nextbsd-overlays8fe48d0 (2026-07-03)
gershwin-desktop/gershwin-on-nextbsd436d423 (2026-07-13)

What this supersedes

The companion research doc — now WLAN on NextBSD — the Apple stack, and why we use wpa_supplicant, renamed from freebsd-wifi-management.htmlnextbsd-wifi-research.html → its current name — remains correct on Apple's stack, what's open source, the wpa_supplicant/iwd comparison, and the glossary. Its integration sections (old §4 "What this project ships today", §5 "Hypothetical wifid-fbsd", §7 "Open questions") have been deleted, not just flagged, because they asserted the following — all false on NextBSD:

Companion plans: configd (note: its repo pointer at pkgdemon/freebsd-launchd-mach is stale — the code now lives at nextbsd-redux/nextbsd-userland) · IPConfiguration · launchd (Mach) · Keychain