VPN: a WireGuard tunnel (#8) #87

Merged
twisla merged 2 commits from vpn-wireguard into main 2026-10-07 23:52:09 +00:00
Owner

For #8: a WireGuard client. One tunnel to one peer, IPv4, over whatever Wi-Fi the device is on.

Using it

  • Copy a client's .conf to the card as /vpn/wg0.conf, then Settings > VPN > Import (or vpn import). The configuration, private key included, is kept in the device and never shown or printed. Settings offers to delete the file.
  • A switch brings it up until the next restart; "Start with Wi-Fi" does it every time. It waits for the clock.
  • VPN in the Status Bar, bright once the server has answered. Toasts when it comes up and when the server stops answering.
  • vpn status | up [seconds] | down | import [path] | forget | auto on|off.

Two things differ from the design round

  • Routing is one of two cases, not "whatever AllowedIPs say". Everything (0.0.0.0/0), or the one subnet the device's tunnel address is in. lwIP routes by an interface's subnet or by default and has no table for more. A home LAN behind the server needs the full tunnel; the import says how many ranges it can't reach.
  • With everything through the tunnel, nothing leaves while the server is silent. The round said "no kill switch"; in that mode there is one, by construction.

How

  • esphome/wireguard 0.4.8, pinned. It calls lwIP without lwIP's lock, which this build checks (the device stopped on the first try): every call into it is made with the lock held, on our side.
  • lib/net/src/wg_config.h: the .conf reader and the routing decision, host-tested.
  • src/services/vpn_service: when the tunnel is up, the allowed ranges, the default route, DNS in and out.

Checked

  • 534 host tests pass (6 new).
  • On the device, against a WireGuard peer in a container with throwaway keys: import, up, pings and TCP through the tunnel, DNS in and out, the full tunnel carrying an HTTPS update check while the device stays reachable on Wi-Fi, a timed vpn up 100, start with Wi-Fi after a restart, a silenced peer and its return, vpn forget.

Not checked

  • The usual direction: the device calling the server. The test network didn't allow it, so the test peer called the device. It needs a server the device can reach.
  • A server given by name, PresharedKey and MTU on the air, roaming between networks, IRC and Gemini through the tunnel, long uptime.

Costs 63 KB of flash, 1.2 KB of static RAM, 1.8 KB of heap while up.

Design, measurements and what went wrong: docs/milestones/N1.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT

For #8: a WireGuard client. One tunnel to one peer, IPv4, over whatever Wi-Fi the device is on. **Using it** - Copy a client's `.conf` to the card as `/vpn/wg0.conf`, then Settings > VPN > Import (or `vpn import`). The configuration, private key included, is kept in the device and never shown or printed. Settings offers to delete the file. - A switch brings it up until the next restart; "Start with Wi-Fi" does it every time. It waits for the clock. - `VPN` in the Status Bar, bright once the server has answered. Toasts when it comes up and when the server stops answering. - `vpn status | up [seconds] | down | import [path] | forget | auto on|off`. **Two things differ from the design round** - **Routing is one of two cases, not "whatever AllowedIPs say".** Everything (`0.0.0.0/0`), or the one subnet the device's tunnel address is in. lwIP routes by an interface's subnet or by default and has no table for more. A home LAN behind the server needs the full tunnel; the import says how many ranges it can't reach. - **With everything through the tunnel, nothing leaves while the server is silent.** The round said "no kill switch"; in that mode there is one, by construction. **How** - `esphome/wireguard` 0.4.8, pinned. It calls lwIP without lwIP's lock, which this build checks (the device stopped on the first try): every call into it is made with the lock held, on our side. - `lib/net/src/wg_config.h`: the `.conf` reader and the routing decision, host-tested. - `src/services/vpn_service`: when the tunnel is up, the allowed ranges, the default route, DNS in and out. **Checked** - 534 host tests pass (6 new). - On the device, against a WireGuard peer in a container with throwaway keys: import, up, pings and TCP through the tunnel, DNS in and out, the full tunnel carrying an HTTPS update check while the device stays reachable on Wi-Fi, a timed `vpn up 100`, start with Wi-Fi after a restart, a silenced peer and its return, `vpn forget`. **Not checked** - **The usual direction: the device calling the server.** The test network didn't allow it, so the test peer called the device. It needs a server the device can reach. - A server given by name, PresharedKey and MTU on the air, roaming between networks, IRC and Gemini through the tunnel, long uptime. Costs 63 KB of flash, 1.2 KB of static RAM, 1.8 KB of heap while up. Design, measurements and what went wrong: docs/milestones/N1.md. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
twisla added 2 commits 2026-10-07 23:49:50 +00:00
The device joins a WireGuard network over whatever Wi-Fi it is on: one
peer, IPv4. A client's .conf is imported from the card (/vpn/wg0.conf) and
kept in the device's settings, private key included, never shown; Settings
offers to delete the file. A switch brings the tunnel up until the next
restart, "Start with Wi-Fi" every time; it waits for the clock, which a
handshake needs. VPN shows in the Status Bar.

The protocol is esphome/wireguard 0.4.8. It calls lwIP without lwIP's lock,
which this framework checks: every call into it is made with the lock held.

What goes through the tunnel is everything (AllowedIPs 0.0.0.0/0) or the
one subnet the device's tunnel address is in: lwIP routes by an
interface's subnet or by default, nothing finer. The import says how many
ranges it can't reach.

Checked against a test peer in both directions and against a real server,
with a configuration uploaded from a phone (docs/milestones/N1.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
Docs: caught up with v0.13.0 to v0.19.0
Site / build (pull_request) Successful in 15s
CI / build (pull_request) Successful in 1m33s
e00fff670f
The documentation had kept up feature page by feature page and nowhere
else. Now also:

- the home page's App cards (notes of any size, pictures, sharing) and its
  note (the Shell, the VPN, the screenshot key);
- the guide: VPN in the Status Bar and in Settings, the three keys that work
  everywhere, screenshots of the Shell, a picture, sharing, the VPN page and
  the help panel;
- three how-tos: move files with your phone, set up the VPN, take a
  screenshot; where the files are and what things cost in memory;
- the FAQ: screenshots, and what listens on the network;
- the README's opening: what the firmware does today;
- the glossary: Tunnel, Sharing, Screenshot;
- every milestone's status line, with the version each thing shipped in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
twisla force-pushed vpn-wireguard from 454e879e60 to e00fff670f 2026-10-07 23:49:50 +00:00 Compare
twisla merged commit 86172c0342 into main 2026-10-07 23:52:09 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: twisla/roro9stack#87