- Elixir 97.3%
- Shell 2.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Test / test (push) Successful in 2m0s
Reviewed-on: #1 |
||
| .forgejo/workflows | ||
| config | ||
| lib | ||
| rel | ||
| rootfs_overlay/etc | ||
| scripts | ||
| test | ||
| .formatter.exs | ||
| .gitignore | ||
| mise.toml | ||
| mix.exs | ||
| mix.lock | ||
| README.md | ||
gratwifi_probe
Fairphone 3 / 3+ firmware for testing captive-portal Wi-Fi networks such as GratWiFi. Scan for networks, join one and watch everything that happens:
- wpa_supplicant and VintageNet events (optionally wpa_supplicant
-dd) - the live signal of the connection (RSSI and link speed, every 2 s), logged when it changes by 5 dB or more and otherwise once a minute
- the battery level and whether it's charging, updated the moment the cable is plugged in or out
- every DHCP event and every option the server sends, including option 114 (Captive-Portal URI, RFC 8910)
- the Captive Portal API JSON (RFC 8908), re-read every 10 s while captive, with every deviation from the RFCs flagged
- a plain-HTTP probe (
generate_204) to see whether traffic is really intercepted, plus a warning when the API and the network disagree - a text browser to accept the portal's terms: links, forms, checkboxes, cookies and redirects, every request logged
- a touch UI on the phone itself: join networks with an on-screen keyboard, see the lease and the portal, accept the terms
Everything can be done on the phone's touchscreen, so no laptop is needed in the field. The same is available as a TUI over SSH.
On the phone
The top of the screen shows the battery (green, yellow below 30 %, red below
15 %, and "charging"), the network with its live signal and link speed
(updated every 2 s), the IPv4 address, DHCP state, option 114, the portal
state and the TUI's SSH command, with the phone's own nerves-XXXX.local
name. The TUI's header shows the same live signal and battery. The tab bar at the bottom
switches between:
- Wi-Fi: networks strongest first, with signal, channel, band and security; the connected one is green. Tap a network to join it; a WPA2/WPA3 network asks for its passphrase on the on-screen keyboard. The list fills itself at startup; Scan rescans, -dd restarts wpa_supplicant with verbose logging.
- DHCP: the lease with every option the server sent, and the recent DHCP events. Renew asks the server to renew; Release + restart starts over with a fresh DHCPDISCOVER.
- Portal: option 114, the API answer (parsed, with warnings, and raw), the plain-HTTP probe and the API's history. Re-read API, Probe HTTP, and Open portal, which opens the portal in the browser.
- Browser: Back, Reload, URL (type an address) and Portal,
plus three views:
- Page: the page text. Lines with a link or form control are blue; tap one to act on it (a line with several opens Elements instead).
- Elements: every link and form control as a row, with the page line it's on. Tap to follow a link, tick a checkbox, choose from a select, press a button, or edit a field on the keyboard.
- Details: status, the redirect chain, the response headers and the cookies.
- Log: everything that happened (without debug lines; the TUI has those). Tap the status block to filter by source.
Power off, top right on every tab, turns the phone off. It, Disconnect and Release + restart need a second tap: the button turns orange ("Confirm" / "Tap again to confirm"), and any other tap cancels it.
The keyboard has Shift (twice for caps lock), 123 and #+= layers that
together cover every printable ASCII character, Show/Hide for
passphrases, Cancel, and Join/Go/OK.
Lists, pages and the log scroll by dragging; Volume Up and Volume Down page. On the Log tab, while scrolled back, the log holds still and the bottom bar turns yellow, counting new entries; tap the bar to return to the live log. It also returns by itself after 30 s without input. The screen keeps the last 2,000 log entries.
Over SSH (the TUI)
- Connect the phone to your laptop over USB.
ssh -p 2222 nerves-XXXX.localopens the TUI (nerves.localworks when it's the only Nerves device around). Port 22 is still IEx.- Wi-Fi tab: ↑↓ to pick a network, Enter to join (it asks for the
passphrase of WPA2/WPA3 networks).
vrestarts wpa_supplicant with-dd. - DHCP tab: the lease with every option.
nrenews,Rreleases and starts over with a fresh DHCPDISCOVER. - Portal tab: option 114, the API response (parsed and raw), the probe.
oopens theuser-portal-urlin the browser (or wherever the probe was redirected),rre-reads the API,pre-runs the probe. - Browser tab: type a number and Enter to follow a link, edit a field,
tick a checkbox or press a button.
g URL,bback,rreload,sHTML source,istatus/redirects/headers,ccookies. - Log tab: everything, newest at the bottom.
ffilters by source.
←/→ switch tabs (1–5 too, outside the browser). q or Ctrl-C quits.
In IEx on port 22, GratwifiProbe.Log.tail(50) prints the log.
The touchscreen and the TUI use the same services and can be used at the same time; each has its own browser session.
The browser
Neither browser runs JavaScript. A portal page that can't be completed
without it can't be completed here; <noscript> content is shown. Forms are
submitted the way a browser would (hidden fields, ticked boxes, the pressed
button), cookies are kept, and every redirect is logged.
How the DHCP part works
VintageNet's DHCP client throws away options it doesn't know, 114 among them.
So wlan0 is configured with IPv4 disabled, and GratwifiProbe.DHCP.Client
runs busybox udhcpc -O 114 itself, with BEAMNotify as its hook script.
busybox passes options it doesn't know as optNNN=<hex>, which the probe
decodes. Interface setup (address, routes, DNS) is handed back to
VintageNet's udhcpc handler. That module is internal to VintageNet, so
vintage_net is pinned to ~> 0.13.13.
udhcpc starts when wpa_supplicant reports an association and stops when it is lost. It sends a DHCPRELEASE when it stops.
To ask for more options, change dhcp_request_options in config/config.exs.
Build and flash
Prerequisites are the same as for nerves_livebook_fp3: Erlang/OTP 29.1.1,
Elixir 1.20.4 (mise.toml), the nerves_bootstrap archive and an SSH
public key in ~/.ssh. That key is baked into the firmware for both SSH ports.
export MIX_TARGET=nerves_system_fp3
mix deps.get
mix firmware
Flash over USB with the phone in fastboot mode (power off, hold Volume Down, plug in USB):
scripts/flash-fp3.sh --build
After that, update over the network with mix upload nerves-XXXX.local. Use
the hostname, not an IP address. OTP 29's SSH server refuses the firmware
upload on a channel that has already had another request, such as an
environment variable, so your SSH client mustn't send any (SendEnv -* and
no SetEnv or ForwardAgent for these hosts in ~/.ssh/config). A
successful upload ends with "Success!"; check the new firmware UUID if it
doesn't.
Tests
mix test runs on the host, and on Forgejo CI for every push:
- DHCP option decoding and RFC 8908 parsing
- the HTML renderer and form submission
- the TUI, with
Breeze.Test - the touch UI: keyboard, tap regions and what each tap does
- an end-to-end portal flow against a fake portal server, through both the browser and the touch UI's own tap regions
The touch UI draws as a plain Scenic graph, so its tests need no screen.
Notes
- The signal in the Wi-Fi list comes from the last scan (Scan refreshes it).
The header's comes from wpa_supplicant's
SIGNAL_POLL, because VintageNet keeps the connected network's signal from the scan done when it joined. - TLS needs a sane clock. NTP is often blocked until the portal is accepted, so the clock may only be as recent as the firmware build. Certificate errors in the log say so.
- IPv6 router advertisements (RFC 8910 also defines a captive-portal RA option) aren't inspected.
- WEP and 802.1X networks aren't supported.