Public Access
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
134 lines
4.9 KiB
C++
134 lines
4.9 KiB
C++
#include "apps/vpn_page.h"
|
|
|
|
#include <SD.h>
|
|
|
|
#include "app_keys.h"
|
|
#include "ipv4.h"
|
|
#include "ui/fonts.h"
|
|
#include "ui/theme.h"
|
|
#include "ui/widgets.h"
|
|
|
|
namespace roro {
|
|
|
|
void VpnPage::enter() {
|
|
list_.setCount(kRows);
|
|
confirm_.reset();
|
|
message_.clear();
|
|
}
|
|
|
|
bool VpnPage::onKey(const KeyEvent& e) {
|
|
if (confirm_) {
|
|
confirm_->onKey(e);
|
|
int result = confirm_->result();
|
|
if (result == DialogModel::kPending) return true;
|
|
Ask asked = ask_;
|
|
ask_ = Ask::None;
|
|
confirm_.reset();
|
|
if (result == 1 && asked == Ask::DeleteFile) {
|
|
storage_.runJob([]() { SD.remove(kConfPath); });
|
|
message_ = "Imported, and the file is deleted";
|
|
} else if (result == 1 && asked == Ask::Forget) {
|
|
vpn_.forget();
|
|
message_ = "Forgotten";
|
|
}
|
|
return true;
|
|
}
|
|
switch (e.key) {
|
|
case Key::Up: list_.up(); break;
|
|
case Key::Down: list_.down(); break;
|
|
case Key::Back: return false;
|
|
case Key::Left:
|
|
case Key::Right:
|
|
case Key::Select:
|
|
if (e.key != Key::Select && list_.selected() > kAuto) break;
|
|
message_.clear();
|
|
switch (list_.selected()) {
|
|
case kSwitch:
|
|
if (!vpn_.configured()) message_ = "Import a .conf first";
|
|
else vpn_.want(!vpn_.wanted());
|
|
break;
|
|
case kAuto:
|
|
if (!vpn_.configured()) message_ = "Import a .conf first";
|
|
else settings_.setBool(Setting::VpnAuto, !settings_.getBool(Setting::VpnAuto));
|
|
break;
|
|
case kImport: {
|
|
std::string why = vpn_.importFile(storage_, kConfPath);
|
|
if (!why.empty()) {
|
|
message_ = why;
|
|
break;
|
|
}
|
|
message_ = "Imported";
|
|
ask_ = Ask::DeleteFile; // the card can be taken out, and the key is in that file
|
|
confirm_.reset(new DialogModel({"Keep it", "Delete it"}));
|
|
break;
|
|
}
|
|
case kForget:
|
|
if (!vpn_.configured()) break;
|
|
ask_ = Ask::Forget;
|
|
confirm_.reset(new DialogModel({"Cancel", "Forget"}));
|
|
break;
|
|
default: break;
|
|
}
|
|
break;
|
|
default: break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void VpnPage::help(std::vector<KeyHelp>& out) const {
|
|
if (confirm_) return keys::add(out, keys::kDialog);
|
|
keys::add(out, keys::kVpn);
|
|
}
|
|
|
|
void VpnPage::draw(Canvas& c) {
|
|
const auto& area = theme::kContent;
|
|
c.setTextDatum(top_left);
|
|
bool set = vpn_.configured();
|
|
widgets::list(
|
|
c, list_, {area.x, area.y, area.w, kRows * theme::kLineHeight},
|
|
[](int i) -> std::string {
|
|
switch (i) {
|
|
case kSwitch: return "VPN";
|
|
case kAuto: return "Start with Wi-Fi";
|
|
case kImport: return "Import /vpn/wg0.conf";
|
|
default: return "Forget it";
|
|
}
|
|
},
|
|
[&](int i) -> std::string {
|
|
switch (i) {
|
|
case kSwitch: return !set ? "Not set" : vpn_.wanted() ? "On" : "Off";
|
|
case kAuto: return settings_.getBool(Setting::VpnAuto) ? "On" : "Off";
|
|
default: return "";
|
|
}
|
|
});
|
|
|
|
int y = area.y + kRows * theme::kLineHeight + 4;
|
|
c.setFont(&fonts::small);
|
|
auto line = [&](const std::string& text, uint16_t colour) {
|
|
c.setTextColor(colour);
|
|
c.drawString(text.c_str(), 4, y);
|
|
y += 10;
|
|
};
|
|
if (set) {
|
|
const net::WgConfig& k = vpn_.config();
|
|
std::string state = std::string("It is ") + vpn_.stateText();
|
|
int64_t now = clock_.utcNow(), last = vpn_.lastHandshake();
|
|
if (vpn_.state() == VpnService::State::Up && now >= 0 && last > 0 && now >= last) state += ", heard " + std::to_string(now - last) + " s ago";
|
|
line(state, vpn_.state() == VpnService::State::Up ? theme::kAccent : theme::kText);
|
|
line("Server " + k.endpointHost + ":" + std::to_string(k.endpointPort), theme::kMuted);
|
|
line("This device " + net::formatIpv4(k.address) + ", through it " + net::describeWgRouting(k), theme::kMuted);
|
|
} else {
|
|
line("Copy a WireGuard .conf to the card as", theme::kMuted);
|
|
line(std::string(kConfPath) + ", then import it.", theme::kMuted);
|
|
}
|
|
if (!message_.empty()) line(message_, theme::kWarning);
|
|
else if (!vpn_.lastError().empty()) line(vpn_.lastError(), theme::kWarning);
|
|
|
|
if (confirm_ && ask_ == Ask::DeleteFile)
|
|
widgets::dialog(c, "Delete the file?", "It is stored in the device now. The file on the card still holds the private key.", *confirm_);
|
|
else if (confirm_)
|
|
widgets::dialog(c, "Forget the VPN?", "The tunnel stops and its keys are erased from the device.", *confirm_);
|
|
}
|
|
|
|
} // namespace roro
|