Entirely vibe-coded, self-contained Go daemon that brings up a captive-portal wifi access point when a Linux host has no internet, lets a user pick a wifi network from their phone, applies it via NetworkManager, then steps aside.
You've got a headless Linux box — no screen, no keyboard. You've just plugged it in somewhere new : a venue for an event, a customer site, a closet in a building you don't own. It boots fine but it's offline, because nobody ever told it which wifi to join, and there's nowhere on the box itself to type that.
That's the gap wifi-connect fills. When the box has no internet, it broadcasts an open wifi called wifi-setup. You hop on with your phone, your OS goes "oh hey, a captive portal!" and pops a page open all by itself. You pick the wifi you want from the scanned list, type the password, hit submit. The box jumps onto the chosen wifi and the AP disappears. ✨
If nobody interacts, the box retries every 10 min : drops the AP, asks NetworkManager nicely if any known wifi is in range, brings the AP back if not. Patient little thing.
Grab a release binary straight from GitHub releases :
# latest stable
curl -L -o wifi-connect https://github.com/asso-syntac/wifi-connect/releases/latest/download/wifi-connect
chmod +x wifi-connect
sudo install -m 0755 wifi-connect /usr/local/bin/Statically linked, no extra runtime to install.
- Linux/amd64
- NetworkManager + nftables (default on modern distros)
- A wifi adapter that does AP mode —
iw listshould show* APunder "Supported interface modes" - Root, or
CAP_NET_ADMIN(we touch nftables and NM)
| Flag | Env var | Default |
|---|---|---|
--debug |
— | false |
--interface=<name> |
WIFI_CONNECT_IFACE |
auto-detect |
--ssid=<name> |
WIFI_CONNECT_SSID |
wifi-setup |
--recovery-interval=<dur> |
WIFI_CONNECT_RECOVERY_INTERVAL |
10m |
Durations accept the usual Go format : 30s, 5m, 1h30m.
Logs are JSON on stdout, slurped happily by journald or any shipper.
The same binary doubles as a CLI client over /run/wifi-connect.sock :
sudo wifi-connect status # current state + last scan + last error
sudo wifi-connect monitor # tail state transitions live (Ctrl-C to stop)
sudo wifi-connect force-portal # bring up the AP right now — great for tests
sudo wifi-connect versionEverything except version needs root (the socket is 0600).
Docker only. No local Go install — everything happens inside golang:1.23-alpine.
make buildSpits out a ~6.5 MB static binary at bin/wifi-connect. To bump Go, edit GO_IMAGE in the Makefile.
- NetworkManager creates the AP in
sharedmode → its internal dnsmasq handles DHCP. We never run our own dnsmasq. (That was the bug we were running away from.) nftablesredirects DNS (UDP/TCP 53) and HTTP (TCP 80) from the AP interface to in-process servers : a tiny DNS hijack that answers192.168.42.1to absolutely everything, and an HTTP server that 302's any request whoseHostisn't the portal. That 302 is what flips the "Sign in to network" notif on Android, the auto-popup on iOS, etc.- HTML/JS/CSS are embedded via
go:embed— nothing to ship next to the binary. - One supervisor goroutine, four states (
STARTING→MONITORING→PORTAL_UP→CONNECTING), channels for everything else. No global mutex hell.
That's it. Have fun. 📶