Files
roro9stack/test/test_wg_config/test_wg_config.cpp
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

163 lines
8.2 KiB
C++

#include <unity.h>
#include <string>
#include "ipv4.h"
#include "wg_config.h"
using namespace roro::net;
void setUp() {}
void tearDown() {}
namespace {
// Keys made for these tests: they open nothing.
const char* const kPriv = "aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789+/aBcDE=";
const char* const kPub = "h+vdhuWJykaesw515qrYYGNdg2pGvE7JU5PXIAV2YzE=";
const char* const kPsk = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
std::string conf(const std::string& allowed = "10.9.0.0/24", const std::string& more = "") {
return std::string("[Interface]\nPrivateKey = ") + kPriv + "\nAddress = 10.9.0.2/24\nDNS = 10.9.0.1\n" + more + "\n[Peer]\nPublicKey = " + kPub +
"\nEndpoint = vpn.example.org:51820\nAllowedIPs = " + allowed + "\n";
}
uint32_t ip(const char* text) {
uint32_t v = 0;
TEST_ASSERT_TRUE(parseIpv4(text, v));
return v;
}
} // namespace
void test_a_usual_file() {
WgConfig c;
TEST_ASSERT_EQUAL_STRING("", parseWgConf(conf(), c).c_str());
TEST_ASSERT_EQUAL_STRING(kPriv, c.privateKey.c_str());
TEST_ASSERT_EQUAL_STRING(kPub, c.peerKey.c_str());
TEST_ASSERT_EQUAL_UINT32(ip("10.9.0.2"), c.address);
TEST_ASSERT_EQUAL_INT(24, c.prefix);
TEST_ASSERT_EQUAL_UINT32(ip("10.9.0.1"), c.dns[0]);
TEST_ASSERT_EQUAL_UINT32(0, c.dns[1]);
TEST_ASSERT_EQUAL_STRING("vpn.example.org", c.endpointHost.c_str());
TEST_ASSERT_EQUAL_UINT16(51820, c.endpointPort);
TEST_ASSERT_EQUAL_INT(1, c.allowedCount);
TEST_ASSERT_EQUAL_INT(25, c.keepalive); // none given: this device is behind a NAT
TEST_ASSERT_EQUAL_INT(0, c.mtu);
TEST_ASSERT_TRUE(c.presharedKey.empty());
}
void test_as_people_write_them() {
std::string text = std::string("# my phone's old config\r\n[interface]\r\n privatekey=") + kPriv +
" ; secret\r\nAddress = fd00::2/64, 192.168.77.5\r\nDNS = dns.example, 2001:db8::1, 9.9.9.9, 1.1.1.1, 8.8.8.8\r\nMTU = 1280\r\nListenPort = 51820\r\n"
"PostUp = iptables -A FORWARD\r\n\r\n[PEER]\r\nPublicKey = " + kPub + "\r\nPresharedKey = " + kPsk +
"\r\nEndpoint = 203.0.113.9:4500\r\nAllowedIPs = 0.0.0.0/0, ::/0\r\nPersistentKeepalive = 0\r\n";
WgConfig c;
TEST_ASSERT_EQUAL_STRING("", parseWgConf(text, c).c_str());
TEST_ASSERT_EQUAL_UINT32(ip("192.168.77.5"), c.address); // the IPv4 one, and alone it is a /32
TEST_ASSERT_EQUAL_INT(32, c.prefix);
TEST_ASSERT_EQUAL_UINT32(ip("9.9.9.9"), c.dns[0]); // names and IPv6 left out, two kept
TEST_ASSERT_EQUAL_UINT32(ip("1.1.1.1"), c.dns[1]);
TEST_ASSERT_EQUAL_INT(1280, c.mtu);
TEST_ASSERT_EQUAL_UINT16(51820, c.listenPort);
TEST_ASSERT_EQUAL_STRING(kPsk, c.presharedKey.c_str());
TEST_ASSERT_EQUAL_STRING("203.0.113.9", c.endpointHost.c_str());
TEST_ASSERT_EQUAL_UINT16(4500, c.endpointPort);
TEST_ASSERT_EQUAL_INT(1, c.allowedCount);
TEST_ASSERT_EQUAL_INT(0, c.allowed[0].prefix);
TEST_ASSERT_EQUAL_INT(0, c.keepalive); // said so
}
void test_it_survives_being_stored() {
WgConfig a, b;
TEST_ASSERT_EQUAL_STRING("", parseWgConf(conf("10.9.0.0/24, 192.168.1.77/24", std::string("MTU = 1300\nListenPort = 4242\n")), a).c_str());
a.presharedKey = kPsk;
std::string stored = toWgConf(a);
TEST_ASSERT_EQUAL_STRING("", parseWgConf(stored, b).c_str());
TEST_ASSERT_EQUAL_STRING(stored.c_str(), toWgConf(b).c_str());
TEST_ASSERT_EQUAL_UINT32(ip("192.168.1.0"), b.allowed[1].address); // a range is kept as its network
TEST_ASSERT_EQUAL_INT(1300, b.mtu);
TEST_ASSERT_EQUAL_UINT16(4242, b.listenPort);
TEST_ASSERT_EQUAL_STRING(kPsk, b.presharedKey.c_str());
}
void test_what_is_refused_and_why() {
WgConfig c;
c.endpointHost = "untouched";
auto why = [&](const std::string& text) { return parseWgConf(text, c); };
TEST_ASSERT_EQUAL_STRING("no PrivateKey under [Interface]", why("[Interface]\nAddress = 10.0.0.2/24\n").c_str());
TEST_ASSERT_EQUAL_STRING("line 2: PrivateKey isn't a key", why("[Interface]\nPrivateKey = tooshort=\n").c_str());
TEST_ASSERT_EQUAL_STRING("line 3: Address has no IPv4 address", why(std::string("[Interface]\nPrivateKey = ") + kPriv + "\nAddress = fd00::2/64\n").c_str());
std::string base = std::string("[Interface]\nPrivateKey = ") + kPriv + "\nAddress = 10.0.0.2/24\n[Peer]\nPublicKey = " + kPub + "\n";
TEST_ASSERT_EQUAL_STRING("no Endpoint under [Peer]", why(base + "AllowedIPs = 10.0.0.0/24\n").c_str());
TEST_ASSERT_EQUAL_STRING("no IPv4 range in AllowedIPs", why(base + "Endpoint = a.example:1\nAllowedIPs = ::/0\n").c_str());
TEST_ASSERT_EQUAL_STRING("line 6: an IPv6 Endpoint: IPv4 or a name only", why(base + "Endpoint = [2001:db8::1]:51820\n").c_str());
TEST_ASSERT_EQUAL_STRING("line 6: Endpoint must be host:port", why(base + "Endpoint = vpn.example.org\n").c_str());
TEST_ASSERT_EQUAL_STRING("line 6: AllowedIPs has something that isn't an address range", why(base + "AllowedIPs = 10.0.0.0/33\n").c_str());
TEST_ASSERT_EQUAL_STRING("line 6: AllowedIPs: four IPv4 ranges at most", why(base + "AllowedIPs = 10.0.0.0/24, 10.0.1.0/24, 10.0.2.0/24, 10.0.3.0/24, 10.0.4.0/24\n").c_str());
TEST_ASSERT_EQUAL_STRING("line 8: a second peer: this device has one tunnel to one peer",
why(base + "Endpoint = a.example:1\nAllowedIPs = 10.0.0.0/24\n[Peer]\nPublicKey = " + kPub + "\n").c_str());
TEST_ASSERT_EQUAL_STRING("line 1: a setting before [Interface]", why("PrivateKey = x\n").c_str());
TEST_ASSERT_EQUAL_STRING("line 4: MTU must be 576 to 1500", why(std::string("[Interface]\nPrivateKey = ") + kPriv + "\nAddress = 10.0.0.2\nMTU = 9000\n").c_str());
TEST_ASSERT_EQUAL_STRING("untouched", c.endpointHost.c_str()); // a refused file changes nothing
// No message carries a key.
std::string bad = std::string("[Interface]\nPrivateKey = ") + kPriv + "x\n";
TEST_ASSERT_TRUE(why(bad).find("aBcD") == std::string::npos);
}
void test_keys() {
TEST_ASSERT_TRUE(validWgKey(kPriv));
TEST_ASSERT_TRUE(validWgKey(kPub));
TEST_ASSERT_FALSE(validWgKey(""));
TEST_ASSERT_FALSE(validWgKey(std::string(kPub).substr(0, 43)));
TEST_ASSERT_FALSE(validWgKey(std::string(kPub).substr(0, 43) + "A")); // no padding
TEST_ASSERT_FALSE(validWgKey("h+vdhuWJykaesw515qrYYGNdg2pGvE7JU5PXIAV2Yz!=")); // not base64
TEST_ASSERT_FALSE(validWgKey("h+vdhuWJykaesw515qrYYGNdg2pGvE7JU5PXIAV2YzF=")); // bits past the 32nd byte
}
void test_what_goes_through_it() {
WgConfig c;
parseWgConf(conf("10.9.0.0/24"), c);
WgRouting r = routingOf(c);
TEST_ASSERT_FALSE(r.full);
TEST_ASSERT_EQUAL_INT(24, r.prefix);
TEST_ASSERT_EQUAL_INT(0, r.unreachable);
TEST_ASSERT_TRUE(wgReaches(c, ip("10.9.0.1")));
TEST_ASSERT_FALSE(wgReaches(c, ip("10.9.1.1")));
TEST_ASSERT_FALSE(wgReaches(c, ip("93.184.216.34")));
TEST_ASSERT_EQUAL_STRING("10.9.0.0/24", describeWgRouting(c).c_str());
parseWgConf(conf("0.0.0.0/0"), c);
TEST_ASSERT_TRUE(routingOf(c).full);
TEST_ASSERT_TRUE(wgReaches(c, ip("93.184.216.34")));
TEST_ASSERT_EQUAL_STRING("everything", describeWgRouting(c).c_str());
// A home network behind the server can't be reached without the full tunnel: said, not hidden.
parseWgConf(conf("10.9.0.0/24, 192.168.1.0/24"), c);
r = routingOf(c);
TEST_ASSERT_EQUAL_INT(24, r.prefix);
TEST_ASSERT_EQUAL_INT(1, r.unreachable);
TEST_ASSERT_FALSE(wgReaches(c, ip("192.168.1.10")));
TEST_ASSERT_EQUAL_STRING("10.9.0.0/24, not 1 other range", describeWgRouting(c).c_str());
// The widest allowed range that holds this device's address is the tunnel's subnet.
parseWgConf(conf("10.0.0.0/8"), c);
TEST_ASSERT_EQUAL_INT(8, routingOf(c).prefix);
TEST_ASSERT_TRUE(wgReaches(c, ip("10.200.3.4")));
TEST_ASSERT_EQUAL_INT(0, routingOf(c).unreachable);
// Only the server itself allowed: the Address line's own subnet, and that one host outside it.
parseWgConf(conf("172.16.5.1/32"), c);
r = routingOf(c);
TEST_ASSERT_EQUAL_INT(24, r.prefix);
TEST_ASSERT_EQUAL_INT(1, r.unreachable);
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_a_usual_file);
RUN_TEST(test_as_people_write_them);
RUN_TEST(test_it_survives_being_stored);
RUN_TEST(test_what_is_refused_and_why);
RUN_TEST(test_keys);
RUN_TEST(test_what_goes_through_it);
return UNITY_END();
}