Home IoT: an ESPHome fleet on a Flint-bridged VLAN
How a home IoT segment ended up shaped the way it is: a GL.iNet Flint 2 broadcasting the IoT SSID, bridged untagged into a switch port with PVID 40, onto a router-owned 10.40.0.0/24 where kea pins every device and nftables lets the segment talk to exactly one host. For anyone running ESPHome devices who has wondered whether the hub itself can double as the access point - measured answer: not this hub.
Provenance. Everything marked measured was observed on this rig, 2026-08-10 and 2026-08-11: a Raspberry Pi 4B (BCM43455 radio, Argon ONE aluminum case, NixOS) as the hub, a GL.iNet Flint 2 as the wireless access point, a managed switch, a mini PC running NixOS with kea + nftables as the edge router, and four Athom Smart Plug V3 (ESP32-C3, ESPHome) as the fleet. Claims that come from documentation rather than measurement are marked as such, and one mechanism is marked as inferred where the behavior was measured but the cause was not proven.
TL;DR:
- A Pi 4B’s onboard radio in a metal case could not hold four ESP32 plugs in the same room: associate, handshake, disassociate, repeat, forever. This failure mode is not exotic - the same connect-then-disconnect loop on the same chip is an open linux-kernel report, and ESP32 clients specifically trigger a brcmfmac firmware crash tracked by the Raspberry Pi kernel tree (in concurrent STA+AP mode, a harder configuration than the AP-only one used here).12
- The fix was not a better Pi antenna. It was moving the SSID to the Flint 2, whose only job here is radios, and bridging that SSID at layer 2 into a VLAN the router owns. DHCP, reservations, and firewall policy all live on the router; the Flint runs
proto='none'and forwards frames. - The adoption path that works is the device’s own fallback hotspot plus captive portal at
http://192.168.4.1- but the fallback never fires while the old AP is still beaconing badly, because every flap-loop association counts as a connection and resets the timer. Retire the old SSID first, then the escape hatch opens by itself.34 - Server-side DHCP reservations (kea), not ESPHome
manual_ip: the pin survives reflashing and factory resets, lives in git, and makesuse_addressstable. Same reason you do not bake the SSID’s future into firmware you cannot reach.
Topology
Section titled “Topology”The Flint owns no IP on br-iot, runs no DHCP, no NAT, no firewall for this segment. It is a media converter: RF to untagged Ethernet on a port the switch has already decided is VLAN 40.
Which hardware should host the IoT SSID
Section titled “Which hardware should host the IoT SSID”| Option | Cost | Measured outcome | Ops burden |
|---|---|---|---|
| Pi 4B onboard BCM43455 as AP | $0 (owned) | Flap loop; 45-290 ms pings with loss to a plug in the same room; tx failed: 0 but multi-second RTTs | Two NixOS module traps on day one (HT capabilities, dnsmasq bind timing); every rebuild risked the AP |
| USB mt76 dongle on the Pi (ALFA AWUS036ACM class) | ~2-4x normal price (scalper listings at decision time) | Not bought, so not measured | Second radio to own; still terminates on the hub |
| Flint 2 (already the home WAP) hosts the SSID | $0 (owned) | Plug signal -70 dBm through a wall; ping across the full path 0.734/1.018/1.460 ms min/avg/max, 0% loss | UCI is live state, not config-managed - see the gotcha |
The Flint 2’s radios are MediaTek MT7986 (the UCI device names are mt798611/mt798612), and the stock GL firmware drives them with MediaTek’s own mt_wifi/mtk_warp modules, not mainline mt76 - measured via lsmod on the Flint. The dongle would have been mainline mt76 on the Pi. The driver differs; the point does not: known-good silicon with real antennas, in a case that is not a Faraday cage, already on the wall.
Why the Pi access point failed
Section titled “Why the Pi access point failed”The Pi hosted pi-iot for one evening. The measured failure sequence, per device, in the hostapd journal:
wlan0: STA cc:8d:a2:00:00:01 IEEE 802.11: associatedwlan0: STA cc:8d:a2:00:00:01 WPA: pairwise key handshake completed (RSN)wlan0: STA cc:8d:a2:00:00:01 IEEE 802.11: disassociatedwlan0: AP-STA-DISCONNECTED cc:8d:a2:00:00:01then again, seconds later, indefinitely. Authentication succeeded; the link would not hold. This was not a passphrase or AKM problem - both WPA-PSK and WPA-PSK-SHA256 were offered, and a PMF-capable phone completed the full 4-way handshake, took a lease, and passed NAT’d traffic during the same window.
Two external data points say this was not a local misconfiguration. A linux-kernel report from April 2025 describes the same connect-then-disconnect loop on the same chip with current firmware, unresolved in the thread.1 A Raspberry Pi kernel issue tracks a BCM43455 firmware crash triggered specifically by ESP32-class clients that reconnect without clean deauth frames - exactly what a flapping plug does - though in concurrent STA+AP mode, a harder configuration than the AP-only one used here.2 The Argon ONE’s aluminum case over a PCB antenna did the rest: even when associated, the link budget left nothing.
The segment design
Section titled “The segment design”One SSID, one VLAN, one owner per job:
| Job | Owner | Detail |
|---|---|---|
| Radios | Flint 2 | SSID home-iot, WPA2-PSK (psk2), 2.4GHz, client isolation on |
| L2 transport | Switch port 2 | Access port, PVID 40; the AP tags nothing |
| DHCP + pins | kea on the router | One reservation per device MAC, 10.40.0.20-23 |
| Policy | nftables on the router | Forward chain: IoT -> pi-hub only (6053 API, 3232 OTA, 80 web UI the other way), plus NTP (udp/123) to the internet. No other internet, no other segment. DNS/DHCP arrive as input to the router and are unaffected |
| Hub | pi-hub, admin segment | Home Assistant + ESPHome at 10.10.0.7; the only host the fleet can reach |
The pins, live in kea as measured from /var/lib/kea/leases4.csv:
| Device | MAC | Pin |
|---|---|---|
| athom-plug-1 | cc:8d:a2:00:00:01 | 10.40.0.20 |
| athom-plug-2 | cc:8d:a2:00:00:02 | 10.40.0.21 |
| athom-plug-3 | cc:8d:a2:00:00:03 | 10.40.0.22 |
| athom-plug-4 | cc:8d:a2:00:00:04 | 10.40.0.23 |
Reservations over manual_ip, deliberately: an address baked into firmware needs a reflash to change and dies on a factory reset, while a server-side pin lives in git, survives reflashing, and keeps one list of who-is-who. The pin is also what makes ESPHome’s use_address stable - mDNS across segments cannot be relied on.
Converting the GL-stock IoT network
Section titled “Converting the GL-stock IoT network”The Flint’s stock IoT network is its own little router: NAT’d 192.168.10.0/24 with a DHCP server that turns itself on when the network is enabled.5 Left as-is it would have fought kea for every DHCP discover on the segment. The conversion is four UCI statements, measured live:
network.iot.proto='none'- the interface becomes an unmanaged bridge member, no address, no routingdhcp.iot.ignore='1'- the stock dnsmasq instance stands downlan1moves frombr-lantobr-iot- the wired uplink for the segmentwireless.iot2g: enabled,ssid='home-iot',encryption='psk2', same passphrase the fleet already knew,isolate='1'kept
The L2 proof was a temporary address on the Flint’s bridge: ip addr add 10.40.0.250/24 dev br-iot, ping the router at 10.40.0.1 (3/3, rtt min/avg/max 0.734/1.018/1.460 ms), ARP resolved to the router’s MAC, then delete the address. That exercises the entire path - lan1, switch PVID, trunk, VLAN interface - with no config left behind.
The adoption path that works
Section titled “The adoption path that works”The plan was to OTA each plug with the new SSID while it sat on the old one. The flap loop made that untenable - an OTA needs minutes of stable link, and the plugs could not hold seconds. The path that worked needs no link to the old AP at all:
- Switch the old AP off. This is the step the docs do not spell out. ESPHome opens its fallback hotspot only after
ap_timeoutwith no successful connection - but a flap loop produces a successful association every few seconds, which resets the timer, so the fallback never fires while a sick AP keeps beaconing. Observed: over an hour of flapping with the old AP up produced no fallback SSID; once the old AP was off (and each plug had rebooted into the silence), the fallback SSIDs appeared within minutes. The timer-reset mechanism itself is inferred from the ESPHome source and docs, not proven on this rig.3 - Join the fallback hotspot from a phone, open
http://192.168.4.1, enter the new SSID and passphrase. The captive portal stores the credentials in flash and the device reboots onto the new network.4 - The pin does the rest. kea recognizes the MAC, hands over the reserved address, and the device appears at exactly the
use_addressthe dashboard already points at. - OTA once, over the good link, to bake the new SSID into firmware. Portal-stored credentials survive reboots but a later serial flash would overwrite them; the bake keeps firmware and YAML saying the same thing.4
Two measured surprises on this path: the stock Athom firmware accepted the OTA of the replacement config with no password prompt, and the portal-set credentials survived every reboot without the bake - but the bake is still worth doing for the YAML-firmware match.
Reading the numbers
Section titled “Reading the numbers”| Claim in this doc | How it was checked |
|---|---|
| Flap loop: associate / handshake completed / disassociated, repeating | Measured 2026-08-10/11: hostapd journal on pi-hub, both plug MACs |
| 45-290 ms pings with loss to a same-room plug on the Pi AP | Measured 2026-08-10: ping from pi-hub over pi-iot |
| Pi AP verified working for a phone (handshake, lease, NAT flows) | Measured 2026-08-10: hostapd journal + dnsmasq leases + /proc/net/nf_conntrack ASSURED entries |
| New path latency 0.734/1.018/1.460 ms, 0% loss | Measured 2026-08-11: temporary address on br-iot, ping 10.40.0.1 |
| Plug signal -70 dBm on the Flint | Measured 2026-08-11: ESPHome log stream, athom-plug-1 WiFi Signal sensor |
| All four kea pins taken by the correct MACs | Measured 2026-08-11: /var/lib/kea/leases4.csv on the router |
| Pi -> plug :6053 and :3232 reachable through the forward rule | Measured 2026-08-11: TCP connect from pi-hub to 10.40.0.20/.21/.23 |
| Stock Athom OTA accepts the replacement config, no password | Measured 2026-08-11: dashboard Install to 10.40.0.22/.23 |
| Fallback hotspot never fires during the flap loop; fires once the old AP is off | Measured behavior 2026-08-11; timer-reset mechanism inferred from ESPHome docs and source |
| brcmfmac cannot maintain an AP on this chip | Documented: linux-kernel thread, same chip, same symptom shape; ESP32-triggered firmware crash in raspberrypi/linux#7092 (concurrent STA+AP mode there; AP-only here) |
Flint radios are MT7986 driven by MediaTek mt_wifi/mtk_warp, not mainline mt76 | Measured 2026-08-11: lsmod on the Flint |
| Stock GL IoT network NATs 192.168.10.0/24 and auto-enables its DHCP server | Documented: GL.iNet IoT Network guide; dhcp config with force measured in the Flint’s UCI |
| Switch port 2 is access PVID 40 | Documented in the infra repo’s switch migration record; live CLI re-verification was blocked by a switch login lockout at write time |
What generalizes
Section titled “What generalizes”- The hub is not the AP. A gateway radio in a metal case, an inch from a heatsink, running a driver with open AP-mode bugs, is three problems renting one antenna. Any dedicated WAP with its radios in the clear beats it, and if the WAP is already on the wall the marginal cost is four UCI statements.
- Bridge, don’t route, at the AP.
proto='none'plus a PVID’d switch port keeps DHCP, reservations, DNS, and policy in one place on the router. The AP becomes replaceable: any box that can bridge an SSID to a wire fits the slot. - Pins on the server, not the device. Every property baked into firmware is a property you change by reflashing. Every property on the DHCP server is a line in git.
- The escape hatch has a precondition. ESPHome’s fallback hotspot is the right adoption path for devices you cannot reach - but it only opens when the device stays disconnected. A sick AP that almost works is worse than no AP: it keeps the device connected just enough to never ask for help.
References
Section titled “References”-
KeithG, “Cannot maintain an ap with brcmfmac,” linux-kernel mailing list, April 2025. https://lists.openwall.net/linux-kernel/2025/04/20/201 ↩ ↩2
-
mshioji, “BCM43455 firmware crash with concurrent STA+AP mode on kernel 6.12,” raspberrypi/linux issue 7092, filed 2025-10-17. https://github.com/raspberrypi/linux/issues/7092 ↩ ↩2
-
ESPHome, “WiFi Component,” ESPHome Docs. https://esphome.io/components/wifi/ ↩ ↩2
-
ESPHome, “Captive Portal,” ESPHome Docs. https://esphome.io/components/captive_portal/ ↩ ↩2 ↩3
-
GL.iNet, “IoT Network,” GL.iNet Router Docs 4. https://docs.gl-inet.com/router/en/4/interface_guide/iot_network/ ↩