Public Access
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:
+49
-2
@@ -12,6 +12,7 @@
|
||||
#include "apps/demo_app.h"
|
||||
#include "apps/note_editor.h"
|
||||
#include "apps/shell_app.h"
|
||||
#include "services/vpn_service.h"
|
||||
#include "services/web_share.h"
|
||||
#include "apps/gemini_app.h"
|
||||
#include "apps/gnss_app.h"
|
||||
@@ -27,6 +28,7 @@
|
||||
#include "event_bus.h"
|
||||
#include "file_receiver.h"
|
||||
#include "ipv4.h"
|
||||
#include "wg_config.h"
|
||||
#include "traffic.h"
|
||||
#include "key_mapper.h"
|
||||
#include "platform/console.h"
|
||||
@@ -86,6 +88,7 @@ static WifiService* wifi;
|
||||
static IrcService* irc;
|
||||
static UpdateService* update;
|
||||
static DebugConsole* debugConsole;
|
||||
static VpnService* vpnService; // not in Safe Mode
|
||||
static Notifier* notifier;
|
||||
static LauncherApp launcher;
|
||||
static AppManager* apps;
|
||||
@@ -132,6 +135,8 @@ static StatusInfo currentStatus() {
|
||||
s.radio = last && millis() - last < 400 ? StatusInfo::Radio::Packet : StatusInfo::Radio::Listening;
|
||||
}
|
||||
s.capturing = loraCapture && loraCapture->capturing();
|
||||
if (vpnService && vpnService->wanted())
|
||||
s.vpn = vpnService->state() == VpnService::State::Up ? StatusInfo::Vpn::Up : StatusInfo::Vpn::Trying;
|
||||
s.debug = !debugConsole->on() ? StatusInfo::Debug::Off : debugConsole->clientConnected() ? StatusInfo::Debug::Client : StatusInfo::Debug::On;
|
||||
using WifiState = WifiController::State;
|
||||
switch (wifi->state()) {
|
||||
@@ -209,6 +214,8 @@ void setup() {
|
||||
services.add(*update);
|
||||
debugConsole = new DebugConsole(*wifi, *storageService, settings);
|
||||
services.add(*debugConsole);
|
||||
vpnService = new VpnService(settings, *wifi, *clockService, bus);
|
||||
services.add(*vpnService);
|
||||
|
||||
apps = new AppManager(launcher);
|
||||
launcher.setManager(*apps);
|
||||
@@ -227,7 +234,7 @@ void setup() {
|
||||
apps->registerApp({"system", "System", false,
|
||||
new SystemApp(*wifi, *battery, *storageService, *radioService, *gnssService, nvs)});
|
||||
apps->registerApp({"settings", "Settings", false,
|
||||
new SettingsApp({settings, bus, *apps, *battery, *storageService, *clockService, *wifi, *savedNetworks, *update})});
|
||||
new SettingsApp({settings, bus, *apps, *battery, *storageService, *clockService, *wifi, *savedNetworks, *update, *vpnService})});
|
||||
apps->registerApp({"demo", "Widget demo", true, new DemoApp(bus)});
|
||||
apps->registerApp({"setup", "Setup", true, new SetupApp(settings, *apps)});
|
||||
|
||||
@@ -667,6 +674,7 @@ static const char* const kHelp =
|
||||
"update check | update list | update status | update install <tag> the project's releases on Gitea\n"
|
||||
"sd card | sd list | cat <path> | log <text> | burst | sound on|off | short | normal\n"
|
||||
"Irc | Wifi | Gnss | Gemini | Lora | Storage | Notes | Shell | System | Settings open that App: a capital letter is an App, not a command\n"
|
||||
"vpn status | vpn up [seconds] | vpn down | vpn import [path] | vpn forget | vpn auto on|off the WireGuard tunnel (Settings > VPN); import reads /vpn/wg0.conf; with seconds, it goes down by itself\n"
|
||||
"debug status | debug off [seconds] the Debug Console over Wi-Fi (Settings > Debug Console); with seconds, it comes back\n"
|
||||
"debug on | debug token <16 to 64 characters> | debug token new (USB serial only) switch it on, set its token\n"
|
||||
"crash abort|wdt crash on purpose (to test crash reports and Safe Mode)\n"
|
||||
@@ -726,6 +734,44 @@ static void saveScreenshot() {
|
||||
});
|
||||
}
|
||||
|
||||
// `vpn ...` (issue #8). Nothing here prints a key.
|
||||
static void vpnCommand(const String& arg) {
|
||||
if (!vpnService) return (void)console.println("vpn: not available in Safe Mode");
|
||||
VpnService& v = *vpnService;
|
||||
if (arg == "up" || arg.startsWith("up ")) {
|
||||
if (!v.configured()) return (void)console.println("vpn: error not set: copy a .conf to /vpn/wg0.conf, then `vpn import`");
|
||||
uint32_t seconds = arg.length() > 3 ? constrain(arg.substring(3).toInt(), 0, 86400) : 0;
|
||||
v.want(true, seconds);
|
||||
if (seconds) console.printf("vpn: on for %lu s\n", (unsigned long)seconds);
|
||||
else console.println("vpn: on");
|
||||
} else if (arg == "down") {
|
||||
v.want(false);
|
||||
console.println("vpn: off");
|
||||
} else if (arg == "import" || arg.startsWith("import ")) {
|
||||
std::string path = arg.length() > 7 ? arg.substring(7).c_str() : "/vpn/wg0.conf";
|
||||
std::string why = v.importFile(*storageService, path);
|
||||
if (!why.empty()) return (void)console.printf("vpn: error %s\n", why.c_str());
|
||||
console.printf("vpn: imported, through it %s. %s still holds the private key: `rm -f` it\n", net::describeWgRouting(v.config()).c_str(), path.c_str());
|
||||
} else if (arg == "forget") {
|
||||
v.forget();
|
||||
console.println("vpn: forgotten");
|
||||
} else if (arg == "status") {
|
||||
if (!v.configured()) return (void)console.println("vpn: not set");
|
||||
const net::WgConfig& k = v.config();
|
||||
console.printf("vpn: %s%s, server %s:%u, this device %s/%d, through it %s\n", v.wanted() ? "" : "off, ", v.wanted() ? v.stateText() : "configured",
|
||||
k.endpointHost.c_str(), (unsigned)k.endpointPort, net::formatIpv4(k.address).c_str(), k.prefix, net::describeWgRouting(k).c_str());
|
||||
int64_t now = clockService->utcNow(), last = v.lastHandshake();
|
||||
if (last > 0 && now >= last) console.printf("vpn: last handshake %ld s ago\n", (long)(now - last));
|
||||
if (!v.lastError().empty()) console.printf("vpn: %s\n", v.lastError().c_str());
|
||||
console.printf("vpn: start with Wi-Fi %s\n", settings.getBool(Setting::VpnAuto) ? "on" : "off");
|
||||
} else if (arg == "auto on" || arg == "auto off") {
|
||||
settings.setBool(Setting::VpnAuto, arg == "auto on");
|
||||
console.printf("vpn: start with Wi-Fi %s\n", arg == "auto on" ? "on" : "off");
|
||||
} else {
|
||||
console.println("vpn: status | up [seconds] | down | import [path] | forget | auto on|off");
|
||||
}
|
||||
}
|
||||
|
||||
// Fn+p (issue #83): the screen as it is, dialog, help panel or Toast included. Not the page that
|
||||
// shows the Debug Console's token: a picture of it is a copy of the token in a file.
|
||||
static void screenshotKey() {
|
||||
@@ -820,6 +866,7 @@ static void runCommand(String line, bool fromSerial = false) {
|
||||
else console.println("Not now: Setup is running");
|
||||
return;
|
||||
}
|
||||
if (line == "vpn" || line.startsWith("vpn ")) return vpnCommand(line.length() > 4 ? line.substring(4) : String("status"));
|
||||
if (line.startsWith("debug ")) return debugCommand(line.substring(6), fromSerial);
|
||||
if (line == "screenshot" || line.startsWith("screenshot ")) {
|
||||
uint32_t seconds = constrain(line.substring(10).toInt(), 0, 60);
|
||||
@@ -1157,7 +1204,7 @@ static void runCommand(String line, bool fromSerial = false) {
|
||||
console.printf("wifi: address %s/%d (%s), gateway %s\n", c.address.c_str(), c.prefix, c.fixed ? "fixed" : "DHCP",
|
||||
c.gateway.empty() ? "none" : c.gateway.c_str());
|
||||
console.printf("wifi: dns %s %s (%s)\n", c.dns[0].empty() ? "none" : c.dns[0].c_str(), c.dns[1].c_str(),
|
||||
c.dnsFromSettings ? "Settings" : "DHCP");
|
||||
vpnService && vpnService->dnsThroughIt() ? "VPN" : c.dnsFromSettings ? "Settings" : "DHCP");
|
||||
console.print("wifi: ntp");
|
||||
for (int i = 0; i < c.ntpCount; i++) console.printf(" %s (%s%s)", c.ntp[i].server.c_str(), c.ntp[i].fromDhcp ? "DHCP" : "Settings", c.ntp[i].answered ? ", answered" : "");
|
||||
console.println(c.ntpCount ? "" : " none");
|
||||
|
||||
Reference in New Issue
Block a user