Files
roro9stack/src/services/vpn_service.h
T
twislaandClaude Opus 5.5 4404dd9380 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
2026-10-08 01:49:47 +02:00

75 lines
2.8 KiB
C++

#pragma once
#include <string>
#include "event_bus.h"
#include "service.h"
#include "services/clock_service.h"
#include "services/storage_service.h"
#include "services/wifi_service.h"
#include "settings.h"
#include "wg_config.h"
namespace roro {
// The WireGuard tunnel (issue #8, docs/milestones/N1.md): one peer, IPv4, over whatever Wi-Fi the
// device is on. The protocol is the `esphome/wireguard` library's; this decides when the tunnel
// is up, takes lwIP's lock around every call into it (the library takes none), and puts the
// tunnel's DNS servers in and out.
//
// It starts once Wi-Fi is connected and the clock is set: a handshake carries the time, and a
// server refuses one older than the last it saw from this key.
class VpnService : public Service {
public:
enum class State { NoConfig, Off, WaitingWifi, WaitingClock, Resolving, Trying, Up };
VpnService(Settings& settings, WifiService& wifi, ClockService& clock, EventBus& bus)
: settings_(settings), wifi_(wifi), clock_(clock), bus_(bus) {}
const char* name() const override { return "vpn"; }
void start() override;
void stop() override { takeDown(); }
void tick(uint32_t nowMs) override;
State state() const { return state_; }
const char* stateText() const;
bool configured() const { return configured_; }
const net::WgConfig& config() const { return config_; } // its keys are for the library only
bool wanted() const { return wanted_; }
// On or off, until the next restart; `seconds`: on for that long, then off by itself (for
// trying a configuration from afar, when a wrong one would cut the connection it was sent over).
void want(bool on, uint32_t seconds = 0);
// A `.conf`'s text, or the file itself. "" or why it wasn't taken. A tunnel that is up starts
// again with the new one.
std::string import(const std::string& confText);
std::string importFile(StorageService& storage, const std::string& path);
void forget();
bool dnsThroughIt() const; // the tunnel's DNS servers are the ones in use
int64_t lastHandshake() const { return lastHandshake_; } // UTC seconds, 0: none yet
const std::string& lastError() const { return error_; }
private:
struct Tunnel; // the library's structures, kept out of this header
void bringUp();
void takeDown();
void loadConfig();
void keepDns(); // puts the tunnel's servers back in if a DHCP renewal replaced them
Settings& settings_;
WifiService& wifi_;
ClockService& clock_;
EventBus& bus_;
net::WgConfig config_;
bool configured_ = false, wanted_ = false, announced_ = false;
State state_ = State::NoConfig;
Tunnel* tunnel_ = nullptr;
uint32_t untilMs_ = 0, retryMs_ = 0;
bool timed_ = false;
int64_t lastHandshake_ = 0;
std::string error_;
};
} // namespace roro