#include "wg_config.h" #include #include "ipv4.h" namespace roro::net { namespace { std::string trim(const std::string& s) { size_t a = s.find_first_not_of(" \t\r"), b = s.find_last_not_of(" \t\r"); return a == std::string::npos ? "" : s.substr(a, b - a + 1); } std::string lower(std::string s) { for (char& c : s) if (c >= 'A' && c <= 'Z') c = static_cast(c + 32); return s; } bool number(const std::string& s, long& out, long max) { if (s.empty() || s.size() > 6) return false; out = 0; for (char c : s) { if (c < '0' || c > '9') return false; out = out * 10 + (c - '0'); } return out <= max; } // "10.9.0.2/24", or an address alone (then /32). False for anything else, IPv6 included. bool range(const std::string& text, WgRange& out) { size_t slash = text.find('/'); long prefix = 32; if (slash != std::string::npos && !number(text.substr(slash + 1), prefix, 32)) return false; if (!parseIpv4(text.substr(0, slash), out.address)) return false; out.prefix = static_cast(prefix); return true; } template void eachItem(const std::string& list, Each each) { size_t at = 0; while (at <= list.size()) { size_t comma = list.find(',', at); if (comma == std::string::npos) comma = list.size(); std::string item = trim(list.substr(at, comma - at)); if (!item.empty()) each(item); at = comma + 1; } } bool inRange(uint32_t address, const WgRange& r) { return (address & maskOf(r.prefix)) == (r.address & maskOf(r.prefix)); } } // namespace bool validWgKey(const std::string& key) { if (key.size() != 44 || key[43] != '=') return false; for (size_t i = 0; i < 43; i++) { char c = key[i]; if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '+' || c == '/')) return false; } // 43 characters carry 258 bits: the last one's two low bits belong to no byte and are zero. static const std::string kLast = "AEIMQUYcgkosw048"; return kLast.find(key[42]) != std::string::npos; } std::string parseWgConf(const std::string& text, WgConfig& out) { WgConfig c; enum { None, Interface, Peer, OtherPeer } section = None; bool hasAddress = false, hasEndpoint = false, hasKeepalive = false; int lineNo = 0; std::string problem; auto fail = [&](const std::string& what) { if (problem.empty()) problem = "line " + std::to_string(lineNo) + ": " + what; }; for (size_t at = 0; at <= text.size() && problem.empty();) { size_t end = text.find('\n', at); if (end == std::string::npos) end = text.size(); std::string line = text.substr(at, end - at); at = end + 1; lineNo++; size_t hash = line.find_first_of("#;"); if (hash != std::string::npos) line.resize(hash); line = trim(line); if (line.empty()) continue; if (line[0] == '[') { std::string name = lower(line); if (name == "[interface]") section = Interface; else if (name == "[peer]") section = section == Peer || section == OtherPeer ? OtherPeer : Peer; else fail("a section this doesn't know"); if (section == OtherPeer) fail("a second peer: this device has one tunnel to one peer"); continue; } size_t eq = line.find('='); if (eq == std::string::npos) { fail("not a setting"); continue; } std::string key = lower(trim(line.substr(0, eq))), value = trim(line.substr(eq + 1)); long n = 0; if (section == Interface) { if (key == "privatekey") { if (!validWgKey(value)) fail("PrivateKey isn't a key"); c.privateKey = value; } else if (key == "address") { eachItem(value, [&](const std::string& item) { WgRange r; if (!hasAddress && range(item, r)) { c.address = r.address; c.prefix = r.prefix; hasAddress = true; } }); if (!hasAddress) fail("Address has no IPv4 address"); } else if (key == "dns") { int count = 0; eachItem(value, [&](const std::string& item) { // names and IPv6 servers are left out uint32_t ip; if (count < 2 && parseIpv4(item, ip)) c.dns[count++] = ip; }); } else if (key == "mtu") { if (!number(value, n, 1500) || n < 576) fail("MTU must be 576 to 1500"); c.mtu = static_cast(n); } else if (key == "listenport") { if (!number(value, n, 65535)) fail("ListenPort must be a port"); c.listenPort = static_cast(n); } // Table, PostUp and the rest mean nothing here } else if (section == Peer) { if (key == "publickey") { if (!validWgKey(value)) fail("PublicKey isn't a key"); c.peerKey = value; } else if (key == "presharedkey") { if (!validWgKey(value)) fail("PresharedKey isn't a key"); c.presharedKey = value; } else if (key == "endpoint") { size_t colon = value.rfind(':'); if (value.empty() || value[0] == '[') fail("an IPv6 Endpoint: IPv4 or a name only"); else if (colon == std::string::npos || colon == 0 || !number(value.substr(colon + 1), n, 65535) || n == 0) fail("Endpoint must be host:port"); else if (value.find_first_of(" \t,/") != std::string::npos || colon > 253) fail("Endpoint must be host:port"); else { c.endpointHost = value.substr(0, colon); c.endpointPort = static_cast(n); hasEndpoint = true; } } else if (key == "allowedips") { eachItem(value, [&](const std::string& item) { WgRange r; if (item.find(':') != std::string::npos) return; // IPv6: not routed here if (!range(item, r)) return fail("AllowedIPs has something that isn't an address range"); if (c.allowedCount == WgConfig::kMaxRanges) return fail("AllowedIPs: four IPv4 ranges at most"); r.address &= maskOf(r.prefix); c.allowed[c.allowedCount++] = r; }); } else if (key == "persistentkeepalive") { if (lower(value) == "off") n = 0; else if (!number(value, n, 65535)) fail("PersistentKeepalive must be seconds"); c.keepalive = static_cast(n); hasKeepalive = true; } } else if (section == None) { fail("a setting before [Interface]"); } } (void)hasKeepalive; if (!problem.empty()) return problem; if (c.privateKey.empty()) return "no PrivateKey under [Interface]"; if (!hasAddress) return "no Address under [Interface]"; if (c.peerKey.empty()) return "no PublicKey under [Peer]"; if (!hasEndpoint) return "no Endpoint under [Peer]"; if (!c.allowedCount) return "no IPv4 range in AllowedIPs"; out = c; return ""; } std::string toWgConf(const WgConfig& c) { std::string s = "[Interface]\nPrivateKey = " + c.privateKey + "\nAddress = " + formatIpv4(c.address) + "/" + std::to_string(c.prefix) + "\n"; if (c.dns[0]) s += "DNS = " + formatIpv4(c.dns[0]) + (c.dns[1] ? ", " + formatIpv4(c.dns[1]) : "") + "\n"; if (c.mtu) s += "MTU = " + std::to_string(c.mtu) + "\n"; if (c.listenPort) s += "ListenPort = " + std::to_string(c.listenPort) + "\n"; s += "[Peer]\nPublicKey = " + c.peerKey + "\n"; if (!c.presharedKey.empty()) s += "PresharedKey = " + c.presharedKey + "\n"; s += "Endpoint = " + c.endpointHost + ":" + std::to_string(c.endpointPort) + "\nAllowedIPs = "; for (int i = 0; i < c.allowedCount; i++) s += (i ? ", " : "") + formatIpv4(c.allowed[i].address) + "/" + std::to_string(c.allowed[i].prefix); s += "\nPersistentKeepalive = " + std::to_string(c.keepalive) + "\n"; return s; } WgRouting routingOf(const WgConfig& c) { WgRouting r; for (int i = 0; i < c.allowedCount; i++) if (c.allowed[i].prefix == 0) r.full = true; if (r.full) return r; // The widest allowed range this device's own address is in is the interface's subnet; with // none, the Address line's own. r.prefix = c.prefix; bool found = false; for (int i = 0; i < c.allowedCount; i++) if (inRange(c.address, c.allowed[i]) && (!found || c.allowed[i].prefix < r.prefix)) { r.prefix = c.allowed[i].prefix; found = true; } WgRange subnet{c.address, r.prefix}; for (int i = 0; i < c.allowedCount; i++) if (c.allowed[i].prefix < r.prefix || !inRange(c.allowed[i].address, subnet)) r.unreachable++; return r; } bool wgReaches(const WgConfig& c, uint32_t address) { WgRouting r = routingOf(c); if (r.full) return true; return inRange(address, WgRange{c.address, r.prefix}); } std::string describeWgRouting(const WgConfig& c) { WgRouting r = routingOf(c); if (r.full) return "everything"; std::string s = formatIpv4(c.address & maskOf(r.prefix)) + "/" + std::to_string(r.prefix); if (r.unreachable) s += ", not " + std::to_string(r.unreachable) + " other range" + (r.unreachable > 1 ? "s" : ""); return s; } } // namespace roro::net