VPN: a WireGuard tunnel (#8)

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
This commit is contained in:
2026-10-08 01:49:47 +02:00
co-authored by Claude Opus 5.5
parent f0306dd880
commit 4404dd9380
31 changed files with 1309 additions and 12 deletions
+11 -1
View File
@@ -35,6 +35,9 @@ bool SettingsApp::onKey(const KeyEvent& e) {
case Page::Firmware:
if (!firmwarePage_.onKey(e)) page_ = Page::Menu;
return true;
case Page::Vpn:
if (!vpnPage_.onKey(e)) page_ = Page::Menu;
return true;
case Page::Debug:
if (!debugPage_.onKey(e)) page_ = Page::Menu;
return true;
@@ -79,6 +82,10 @@ bool SettingsApp::onMenuKey(const KeyEvent& e) {
page_ = Page::Firmware;
firmwarePage_.enter();
break;
case Row::Vpn:
page_ = Page::Vpn;
vpnPage_.enter();
break;
case Row::DebugConsole:
page_ = Page::Debug;
debugPage_.enter();
@@ -139,6 +146,7 @@ void SettingsApp::help(std::vector<KeyHelp>& out) const {
case Page::Wifi: wifiPage_.help(out); break;
case Page::Firmware: firmwarePage_.help(out); break;
case Page::Debug: debugPage_.help(out); break;
case Page::Vpn: vpnPage_.help(out); break;
}
}
@@ -147,6 +155,7 @@ const char* SettingsApp::helpTitle() const {
case Page::Wifi: return wifiPage_.helpTitle();
case Page::Firmware: return firmwarePage_.helpTitle();
case Page::Debug: return debugPage_.helpTitle();
case Page::Vpn: return "VPN";
case Page::About: return "About";
default: return nullptr;
}
@@ -154,7 +163,7 @@ const char* SettingsApp::helpTitle() const {
void SettingsApp::update(uint32_t nowMs) {
// Live values on About and Firmware.
bool live = page_ == Page::About || page_ == Page::Firmware || page_ == Page::Debug ||
bool live = page_ == Page::About || page_ == Page::Firmware || page_ == Page::Debug || page_ == Page::Vpn ||
(page_ == Page::Wifi && wifiPage_.live());
if (live && nowMs - lastRefreshMs_ >= 500) {
lastRefreshMs_ = nowMs;
@@ -213,6 +222,7 @@ void SettingsApp::draw(Canvas& c) {
case Page::Wifi: wifiPage_.draw(c); break;
case Page::Firmware: firmwarePage_.draw(c); break;
case Page::Debug: debugPage_.draw(c); break;
case Page::Vpn: vpnPage_.draw(c); break;
}
}