aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorflu0r1ne <flu0r1ne@flu0r1ne.net>2023-08-17 15:56:11 -0500
committerflu0r1ne <flu0r1ne@flu0r1ne.net>2023-08-17 15:56:11 -0500
commit34779155d5b69b51aa301e7c111f95ea9c840589 (patch)
tree7289232dc3c4cd187e065792191dd707f08d6e79 /src
downloadwg2nd-34779155d5b69b51aa301e7c111f95ea9c840589.tar.xz
wg2nd-34779155d5b69b51aa301e7c111f95ea9c840589.zip
init commit
Diffstat (limited to 'src')
-rw-r--r--src/main.cpp202
-rw-r--r--src/wg2sd.cpp503
-rw-r--r--src/wg2sd.hpp134
3 files changed, 839 insertions, 0 deletions
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..724b479
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,202 @@
+// =====================================
+// ERROR HANDING - FROM FCUTILS
+// =====================================
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <errno.h>
+#include <string.h>
+#include <stdlib.h>
+
+#define FORMAT_MAX_SIZE 1024
+extern int errno;
+
+void format_with_errno(char * buf, size_t n, char const * format) {
+ char const * errstr = strerror(errno);
+ char fmt_err[256];
+
+ size_t i = 0,
+ j = 0;
+ while(errstr[j] && i < sizeof(fmt_err) - 1) {
+ if((fmt_err[i++] = errstr[j++]) != '%')
+ continue;
+
+ if(i < sizeof(fmt_err) - 1) {
+ fmt_err[i++] = '%';
+ } else {
+ i--;
+ }
+ }
+
+ fmt_err[i] = '\0';
+
+ snprintf(buf, n, "%s: %s", format, fmt_err);
+}
+
+
+[[noreturn]] void die_errno(char const * format, ...) {
+ va_list vargs;
+ va_start (vargs, format);
+
+ char buf[FORMAT_MAX_SIZE];
+ format_with_errno(buf, sizeof(buf), format);
+ vfprintf(stderr, buf, vargs);
+ fprintf(stderr, "\n");
+ exit(1);
+
+ va_end(vargs);
+}
+
+[[noreturn]] void die(char const * format, ...) {
+ va_list vargs;
+ va_start (vargs, format);
+
+ vfprintf(stderr, format, vargs);
+ fprintf(stderr, "\n");
+
+ va_end(vargs);
+ exit(1);
+}
+
+void err(char const * format, ...) {
+ va_list vargs;
+ va_start (vargs, format);
+
+ vfprintf(stderr, format, vargs);
+ fprintf(stderr, "\n");
+
+ va_end(vargs);
+}
+
+// =============================================
+// COMMAND LINE UTILITY
+// =============================================
+
+
+#include "wg2sd.hpp"
+
+#include <filesystem>
+#include <fstream>
+#include <iostream>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <grp.h>
+
+[[noreturn]] void die_usage(char const * prog) {
+ err("Usage: %s [ -o OUTPUT_PATH ] CONFIG_FILE", prog);
+ die("Use -h for help");
+}
+
+void print_help(char const * prog) {
+ err("Usage: %s [ -o OUTPUT_PATH ] CONFIG_FILE", prog);
+ err("Options:");
+ err("-o OUTPUT_PATH\tSet the output path (default is /etc/systemd/network)");
+ err("-h\t\tDisplay this help message");
+ exit(EXIT_SUCCESS);
+}
+
+using namespace wg2sd;
+
+void write_systemd_file(SystemdFilespec const & filespec, std::string output_path, bool secure) {
+ std::string full_path = output_path + "/" + filespec.name;
+ std::ofstream ofs;
+
+ if (secure) {
+ // Set permissions to 0640 before writing the file
+ umask(0027);
+
+ // Open the file
+ ofs.open(full_path, std::ios::out | std::ios::trunc);
+ if (!ofs.is_open()) {
+ die("Failed to open file %s for writing", full_path.c_str());
+ }
+
+ // Change ownership to root:systemd-network
+ struct group *grp;
+ grp = getgrnam("systemd-network");
+ if (grp == nullptr) {
+ die_errno("Failed to find the 'systemd-network' group");
+ }
+ if (chown(full_path.c_str(), 0, grp->gr_gid) != 0) {
+ die_errno("Failed to change ownership of file %s", full_path.c_str());
+ }
+
+ // Set permissions
+ if (chmod(full_path.c_str(), S_IRUSR | S_IWUSR | S_IRGRP) != 0) {
+ die_errno("Failed to set permissions for file %s", full_path.c_str());
+ }
+ } else {
+ ofs.open(full_path, std::ios::out | std::ios::trunc);
+ if (!ofs.is_open()) {
+ die_errno("Failed to open file %s for writing", full_path.c_str());
+ }
+ }
+
+ // Write the contents to the file
+ ofs << filespec.contents;
+
+ if (ofs.fail()) {
+ die("Failed to write to file %s", full_path.c_str());
+ }
+}
+
+int main(int argc, char ** argv) {
+ int opt;
+ std::filesystem::path output_path = "/etc/systemd/network";
+
+ while ((opt = getopt(argc, argv, "o:h")) != -1) {
+ switch (opt) {
+ case 'o':
+ output_path = optarg;
+ break;
+ case 'h':
+ print_help(argv[0]);
+ break;
+ default: /* '?' */
+ die_usage(argv[0]);
+ }
+ }
+
+ if (optind >= argc) {
+ die_usage(argv[0]);
+ }
+
+ std::filesystem::path config_path = argv[optind];
+ std::fstream cfg_stream { config_path };
+
+ if(!cfg_stream.is_open()) {
+ die("Failed to open config file %s", config_path.string().c_str());
+ }
+
+ std::string interface_name = interface_name_from_filename(config_path);
+
+ SystemdConfig cfg;
+
+ try {
+ cfg = wg2sd::wg2sd(interface_name, cfg_stream, output_path);
+ } catch(ConfigurationException const & cex) {
+
+ const ParsingException * pex = dynamic_cast<const ParsingException *>(&cex);
+ if(pex && pex->line_no().has_value()) {
+ die("parsing error (line %llu): %s", pex->line_no().value(), pex->what());
+ } else {
+ die("configuration error: %s", cex.what());
+ }
+
+ }
+
+ if(!std::filesystem::path(output_path).is_absolute()) {
+ output_path = std::filesystem::absolute(output_path);
+ }
+
+ write_systemd_file(cfg.netdev, output_path, false);
+ write_systemd_file(cfg.network, output_path, false);
+ write_systemd_file(cfg.private_keyfile, output_path, true);
+
+ for(SystemdFilespec const & spec : cfg.symmetric_keyfiles) {
+ write_systemd_file(spec, output_path, true);
+ }
+
+ return 0;
+}
diff --git a/src/wg2sd.cpp b/src/wg2sd.cpp
new file mode 100644
index 0000000..2571908
--- /dev/null
+++ b/src/wg2sd.cpp
@@ -0,0 +1,503 @@
+#include "wg2sd.hpp"
+
+#include <exception>
+#include <sstream>
+#include <random>
+#include <regex>
+
+#include <argon2.h>
+
+std::string hashed_keyfile_name(std::string const & priv_key) {
+ constexpr uint8_t const SALT[] = {
+ 0x1, 0x6, 0x1, 0x5, 0x5, 0x8, 0x3, 0xd, 0x2, 0x7,
+ 0x5, 0xc, 0x8, 0x8, 0x7, 0x2, 0x7, 0xa, 0xf, 0x5,
+ 0xa, 0x6, 0xc, 0x5, 0xf, 0xe, 0x6, 0x7, 0xf, 0xd,
+ 0x1, 0x5
+ };
+
+ uint8_t const * key = reinterpret_cast<uint8_t const *>(priv_key.c_str());
+ uint32_t keylen = priv_key.size();
+
+ uint8_t t_cost = 2; // 2-pass computation
+ uint32_t m_cost = 1 << 17; // 128 mebibytes memory
+ uint32_t parallelism = 1; // single thread
+
+ constexpr size_t HASHLEN = 32;
+
+ uint8_t hash[HASHLEN];
+
+ argon2i_hash_raw(t_cost, m_cost, parallelism, key, keylen, SALT, sizeof(SALT), hash, HASHLEN);
+
+ constexpr char KEYFILE_EXT[] = ".keyfile";
+
+ char filename[HASHLEN + sizeof(KEYFILE_EXT)];
+
+ constexpr char const HEX[] = "0123456789abcdefghijklmnopqrstuv";
+
+ for(size_t i = 0; i < HASHLEN; i++) {
+ filename[i] = HEX[hash[i] & 0x1F];
+ }
+
+ // copy null terminator
+ for(size_t i = 0; i < sizeof(KEYFILE_EXT); i++) {
+ filename[HASHLEN + i] = KEYFILE_EXT[i];
+ }
+
+ return std::string { filename } ;
+}
+
+
+namespace wg2sd {
+
+ std::string interface_name_from_filename(std::filesystem::path config_path) {
+ std::string interface_name = config_path.filename().string();
+ interface_name = interface_name.substr(
+ 0, interface_name.size() - config_path.extension().string().size()
+ );
+ return interface_name;
+ }
+
+ bool _is_default_route(std::string const & cidr) {
+ static std::regex ipv4_wildcard("0(\\.0){0,3}\\/0");
+ static std::regex ipv6_wildcard("(0{0,4}:){0,7}0{0,4}\\/0{1,4}");
+
+ return std::regex_match(cidr, ipv4_wildcard) or std::regex_match(cidr, ipv6_wildcard);
+ }
+
+ bool _is_ipv4_route(std::string const & cidr) {
+ static std::regex ipv4("\\d{1,3}(\\.\\d{1,3}){0,3}(\\/\\d{1,2})?");
+
+ return std::regex_match(cidr, ipv4);
+ }
+
+ constexpr uint32_t MAIN_TABLE = 254;
+ constexpr uint32_t LOCAL_TABLE = 255;
+
+ // Parse the wireguard configuration from an input stream
+ // into a Config object. If an invalid key or section occurs,
+ // exists, a ParsingException is thrown.
+ Config parse_config(std::string const & interface_name, std::istream & stream) {
+ Config cfg;
+
+ cfg.intf.name = interface_name;
+ cfg.has_default_route = false;
+ cfg.intf.should_create_routes = true;
+
+ std::string line;
+ uint64_t line_no = 0;
+
+ enum class Section {
+ Interface,
+ Peer,
+ None
+ };
+
+ Section section = Section::None;
+
+ bool peer_has_default_route = false;
+
+ while (std::getline(stream, line)) {
+ ++line_no;
+
+ // Strip whitespace (\t) from line in-place
+ {
+ size_t i = 0, j = 0;
+ for(; i < line.size(); i++) {
+ if(line[i] != ' ' and line[i] != '\t') {
+ line[j] = line[i];
+ j++;
+ }
+ }
+ line.erase(j);
+ }
+
+ // Remove content exceeding a comment
+ size_t comment_start = line.find('#');
+ if(comment_start != std::string::npos) {
+ line.erase(comment_start);
+ }
+
+ // Ignore empty lines
+ if (line.empty()) {
+ continue;
+ }
+
+ // Handle section: [Interface] or [Peer] specifies further
+ // configuration concerns an interface or peer respectively
+
+ bool interface_sec_wanted = line == "[Interface]";
+ bool peer_sec_wanted = line == "[Peer]";
+
+ if(interface_sec_wanted || peer_sec_wanted) {
+ cfg.has_default_route = cfg.has_default_route or peer_has_default_route;
+ peer_has_default_route = false;
+ }
+
+ if (interface_sec_wanted) {
+ section = Section::Interface;
+ continue;
+ } else if (peer_sec_wanted) {
+ section = Section::Peer;
+ cfg.peers.emplace_back();
+ continue;
+ }
+
+ // Split key
+ size_t pos = line.find('=');
+ if(pos == std::string::npos) {
+ throw ParsingException("Expected key-value pair, got \"" + line + "\"", line_no);
+ }
+
+ std::string key = line.substr(0, pos);
+ std::string value = line.substr(pos + 1);
+
+ // Read keys according to corresponding section
+ switch (section) {
+ case Section::Interface: {
+ if (key == "PrivateKey") {
+ cfg.intf.private_key = value;
+ } else if (key == "DNS") {
+ std::istringstream dnsStream(value);
+ std::string dnsIp;
+ while (std::getline(dnsStream, dnsIp, ',')) {
+ cfg.intf.DNS.push_back(dnsIp);
+ }
+ } else if (key == "Address") {
+ std::istringstream addressStream(value);
+ std::string address;
+ while (std::getline(addressStream, address, ',')) {
+ cfg.intf.addresses.push_back(address);
+ }
+ } else if (key == "Table") {
+ if(value == "off") {
+ cfg.intf.table = 0;
+ cfg.intf.should_create_routes = false;
+ } else {
+ cfg.intf.should_create_routes = true;
+ if(value == "auto") {
+ cfg.intf.table = 0;
+ } else if(value == "main") {
+ cfg.intf.table = MAIN_TABLE;
+ } else if(value == "local") {
+ cfg.intf.table = LOCAL_TABLE;
+ } else {
+ long long table;
+ try {
+ table = std::stoll(value);
+ } catch(std::exception const & e) {
+ table = -1;
+ }
+
+ if(table < 1 || table > UINT32_MAX) {
+ throw ParsingException("Invalid option to \"Table\", must be one of \"off\", \"auto\" or a table number", line_no);
+ }
+
+ cfg.intf.table = table;
+ }
+ }
+ } else if (key == "ListenPort") {
+ int port;
+ try {
+ port = std::stoi(value);
+ } catch(std::exception & e) {
+ port = -1;
+ }
+
+ if(port < 0 || port > UINT16_MAX) {
+ throw ParsingException("Invalid port: " + key, line_no);
+ }
+
+ cfg.intf.listen_port = port;
+ } else {
+ throw ParsingException("Invalid key in [Interface] section: " + key, line_no);
+ }
+ break;
+ }
+ case Section::Peer: {
+ if (key == "Endpoint") {
+ cfg.peers.back().endpoint = value;
+ } else if (key == "AllowedIPs") {
+ std::istringstream allowedIpsStream(value);
+ std::string allowedIp;
+
+ while (std::getline(allowedIpsStream, allowedIp, ',')) {
+ bool is_default_route = _is_default_route(allowedIp);
+
+ if(is_default_route and cfg.has_default_route) {
+ throw ParsingException("Default routes exist on multiple peers");
+ }
+
+ cfg.peers.back().allowed_ips.push_back(Cidr {
+ .route = allowedIp,
+ .is_default_route = is_default_route,
+ .is_ipv4 = _is_ipv4_route(allowedIp),
+ });
+
+ peer_has_default_route = peer_has_default_route or is_default_route;
+ }
+
+ } else if (key == "PublicKey") {
+ cfg.peers.back().public_key = value;
+ } else if (key == "PersistentKeepalive") {
+ cfg.peers.back().persistent_keepalive = value;
+ } else if (key == "PresharedKey") {
+ cfg.peers.back().preshared_key = value;
+ } else {
+ throw ParsingException("Invalid key in [Peer] section: " + key, line_no);
+ }
+ break;
+ }
+ case Section::None:
+ throw ParsingException("Unexpected key outside of section: " + key, line_no);
+ }
+ }
+
+ cfg.has_default_route = cfg.has_default_route or peer_has_default_route;
+
+#define MissingField(section, key) \
+ ConfigurationException("[" section "] section missing essential field \"" key "\"")
+
+ // Ensure PrivateKey, Address, PublicKey, and AllowedIPs are present
+
+ if(cfg.intf.private_key.empty()) {
+ throw MissingField("Interface", "PrivateKey");
+ }
+
+ if(cfg.intf.addresses.empty()) {
+ throw MissingField("Interface", "Address");
+ }
+
+ for(Peer const & peer : cfg.peers) {
+ if(peer.public_key.empty()) {
+ throw MissingField("Peer", "PublicKey");
+ }
+
+ if(peer.allowed_ips.empty()) {
+ throw MissingField("Peer", "AllowedIPs");
+ }
+ }
+
+#undef MissingField
+
+ return cfg;
+ }
+
+ static std::string _gen_netdev_cfg(Config const & cfg, uint32_t fwd_table, std::string const & private_keyfile,
+ std::vector<SystemdFilespec> & symmetric_keyfiles, std::string const & output_path) {
+ std::stringstream netdev;
+
+ netdev << "# Autogenerated by wg2sd\n";
+ netdev << "[NetDev]\n";
+ netdev << "Name = " << cfg.intf.name << "\n";
+ netdev << "Kind = wireguard\n";
+ netdev << "Description = " << cfg.intf.name << " - wireguard tunnel\n";
+
+ if(!cfg.intf.mtu.empty()) {
+ netdev << "MTUBytes = " << cfg.intf.mtu << "\n";
+ }
+
+ netdev << "\n";
+
+ netdev << "[WireGuard]\n";
+ netdev << "PrivateKeyFile = " << output_path;
+
+ if(output_path.back() != '/') {
+ netdev << "/";
+ }
+
+ netdev << private_keyfile << "\n";
+
+ if(cfg.intf.should_create_routes and cfg.intf.table != 0) {
+ netdev << "RouteTable = ";
+
+ switch(cfg.intf.table) {
+ case LOCAL_TABLE:
+ netdev << "local";
+ break;
+ case MAIN_TABLE:
+ netdev << "main";
+ break;
+ default:
+ netdev << cfg.intf.table;
+ break;
+ }
+
+ netdev << "\n";
+ }
+
+ if(cfg.intf.should_create_routes and cfg.has_default_route) {
+ netdev << "FirewallMark = 0x" << std::hex << fwd_table << std::dec << "\n";
+ }
+
+ netdev << "\n";
+
+ for(Peer const & peer : cfg.peers) {
+ netdev << "[WireGuardPeer]\n";
+
+ netdev << "Endpoint = " << peer.endpoint << "\n";
+ netdev << "PublicKey = " << peer.public_key << "\n";
+
+ if(!peer.preshared_key.empty()) {
+ std::string filename = hashed_keyfile_name(peer.preshared_key);
+
+ symmetric_keyfiles.push_back(SystemdFilespec {
+ .name = filename,
+ .contents = peer.preshared_key + "\n",
+ });
+
+ netdev << "PresharedKeyFile = " << filename << "\n";
+ }
+
+ for(Cidr const & cidr : peer.allowed_ips) {
+ netdev << "AllowedIPs = " << cidr.route << "\n";
+ }
+
+ if(!peer.persistent_keepalive.empty()) {
+ netdev << "PersistentKeepalive = " << peer.persistent_keepalive << "\n";
+ }
+
+ netdev << "\n";
+ }
+
+ return netdev.str();
+ }
+
+ static std::string _gen_network_cfg(Config const & cfg, uint32_t fwd_table) {
+ std::stringstream network;
+
+ network << "# Autogenerated by wg2sd\n";
+ network << "[Match]\n";
+ network << "Name = " << cfg.intf.name << "\n";
+ network << "\n";
+
+ network << "[Network]\n";
+ for(std::string const & addr : cfg.intf.addresses) {
+ network << "Address = " << addr << "\n";
+ }
+
+ for(std::string const & dns : cfg.intf.DNS) {
+ network << "DNS = " << dns << "\n";
+ }
+
+ if(cfg.has_default_route and cfg.intf.DNS.size() > 0) {
+ network << "Domains = ~." << "\n";
+ }
+
+ network << "\n";
+
+ if(!cfg.intf.should_create_routes) {
+ return network.str();
+ }
+
+ constexpr uint8_t POLICY_ROUTE_NONE = 0;
+ constexpr uint8_t POLICY_ROUTE_V4 = 1 << 0;
+ constexpr uint8_t POLICY_ROUTE_V6 = 1 << 1;
+ constexpr uint8_t POLICY_ROUTE_BOTH = POLICY_ROUTE_V4 | POLICY_ROUTE_V6;
+
+ uint8_t policy_route = POLICY_ROUTE_NONE;
+
+ for(Peer const & peer : cfg.peers) {
+ for(Cidr const & cidr : peer.allowed_ips) {
+ if(cidr.is_default_route) {
+ policy_route |= cidr.is_ipv4 ? POLICY_ROUTE_V4 : POLICY_ROUTE_V6;
+ }
+
+ network << "[Route]\n";
+ network << "Destination = " << cidr.route << "\n";
+ uint32_t table = cfg.has_default_route ? fwd_table : cfg.intf.table;
+ if(table) {
+ network << "Table = " << table << "\n";
+ }
+ network << "\n";
+ }
+ }
+
+ if(policy_route != POLICY_ROUTE_NONE) {
+
+ char const * family = nullptr;
+
+ switch(policy_route) {
+ case POLICY_ROUTE_V4:
+ family = "ipv6";
+ break;
+ case POLICY_ROUTE_V6:
+ family = "ipv4";
+ break;
+ case POLICY_ROUTE_BOTH:
+ family = "both";
+ break;
+ }
+
+ network << "[RoutingPolicyRule]\n";
+ network << "SuppressPrefixLength = 0\n";
+ network << "Family = " << family << "\n";
+ network << "Priority = 32764\n";
+ network << "\n";
+
+ network << "[RoutingPolicyRule]\n";
+ network << "FirewallMark = 0x" << std::hex << fwd_table << std::dec << "\n";
+ network << "InvertRule = true\n";
+ network << "Table = " << fwd_table << "\n";
+ network << "Family = " << family << "\n";
+ network << "Priority = 32765\n";
+ network << "\n";
+
+ }
+
+ return network.str();
+ }
+
+ static uint32_t _random_table() {
+ std::random_device rd;
+ std::mt19937 rng { rd() };
+
+ uint32_t table = 0;
+ while(table == 0 or table == MAIN_TABLE or table == LOCAL_TABLE) {
+ table = rng() % UINT32_MAX;
+ }
+
+ return table;
+ }
+
+ SystemdConfig gen_systemd_config(Config const & cfg, std::string const & output_path) {
+
+ // If the table is explicitly specified with Table=<number>,
+ // all routes are added to this table.
+ //
+ // If Table=auto and a default route exists, this
+ // table is used by the default route to supersede
+ // non-encrypted traffic traveling to /0 routes in the
+ // main routing table by using suppress_prefix policy rules.
+ // These routes match a fwmark which is identical to the
+ // table name. All other routes are placed in the main routing
+ // table.
+ //
+ // If Table=off, no routes are added.
+ uint32_t fwd_table = _random_table();
+
+ std::vector<SystemdFilespec> symmetric_keyfiles;
+
+ std::string private_keyfile = hashed_keyfile_name(cfg.intf.private_key);
+
+ return SystemdConfig {
+ .netdev = {
+ .name = cfg.intf.name + ".netdev",
+ .contents = _gen_netdev_cfg(cfg, fwd_table, private_keyfile, symmetric_keyfiles, output_path),
+ },
+ .network = {
+ .name = cfg.intf.name + ".network",
+ .contents = _gen_network_cfg(cfg, fwd_table)
+ },
+ .private_keyfile = {
+ .name = private_keyfile,
+ .contents = cfg.intf.private_key + "\n",
+ },
+ .symmetric_keyfiles = std::move(symmetric_keyfiles)
+ };
+ }
+
+ SystemdConfig wg2sd(std::string const & interface_name, std::istream & stream, std::string const & output_path) {
+ return gen_systemd_config(parse_config(interface_name, stream), output_path);
+ }
+
+}
diff --git a/src/wg2sd.hpp b/src/wg2sd.hpp
new file mode 100644
index 0000000..00028b4
--- /dev/null
+++ b/src/wg2sd.hpp
@@ -0,0 +1,134 @@
+#pragma once
+
+#include <istream>
+#include <exception>
+#include <optional>
+#include <string>
+#include <vector>
+#include <filesystem>
+
+#include <cstdint>
+
+namespace wg2sd {
+
+ struct Interface {
+ // File name, or defaults to "wg"
+ std::string name;
+ // Address=...
+ // List of ip addresses to be assigned to the interface
+ std::vector<std::string> addresses;
+ // PrivateKey=...
+ // Base64-encoded private key string
+ std::string private_key;
+ // MTu=..
+ std::string mtu;
+ // DNS=...
+ // DNS consists of a comma-separated list of IP addresses of DNS servers
+ std::vector<std::string> DNS;
+ // Table=...
+ // By default, wireguard creates routes. This is disabled, when Table=off
+ bool should_create_routes;
+ // Table number (if specific), 0 if auto
+ uint32_t table;
+ // ListenPort=...
+ // The port number on which the interface will listen
+ std::optional<uint16_t> listen_port;
+
+ Interface()
+ : should_create_routes { false }
+ , table { 0 }
+ , listen_port { }
+ { }
+ };
+
+ struct Cidr {
+ std::string route;
+ bool is_default_route;
+ bool is_ipv4;
+ };
+
+ struct Peer {
+ // Endpoint=...
+ // IP and port of the peer
+ std::string endpoint;
+ // PublicKey=...
+ std::string public_key;
+ // AllowedIPs=...
+ // Comma separated list of allowed ips
+ // Each allowed ip is a CIDR block
+ std::vector<Cidr> allowed_ips;
+ // PersistentAlive=...
+ std::string persistent_keepalive;
+ // PresharedKey=...
+ std::string preshared_key;
+ };
+
+ struct Config {
+ // [Interface]
+ Interface intf;
+ // [Peer]
+ std::vector<Peer> peers;
+ // If one of the peers has a default route
+ bool has_default_route;
+
+ Config()
+ : has_default_route { false }
+ { }
+ };
+
+ class ConfigurationException : public std::exception {
+
+ public:
+
+ ConfigurationException(std::string const & message)
+ : _message { message }
+ {}
+
+ char const * what() const noexcept override {
+ return _message.c_str();
+ }
+
+ private:
+ std::string _message;
+ };
+
+ class ParsingException : public ConfigurationException {
+
+ public:
+
+ ParsingException(std::string const & message, std::optional<uint64_t> line_no = {})
+ : ConfigurationException(message)
+ , _line_no { line_no }
+ {}
+
+
+ std::optional<uint64_t> line_no() const noexcept {
+ return _line_no;
+ }
+
+ private:
+ std::string _message;
+ std::optional<uint64_t> _line_no;
+ };
+
+ struct SystemdFilespec {
+ std::string name;
+ std::string contents;
+ };
+
+ struct SystemdConfig {
+ SystemdFilespec netdev;
+ SystemdFilespec network;
+ SystemdFilespec private_keyfile;
+ std::vector<SystemdFilespec> symmetric_keyfiles;
+ };
+
+ std::string interface_name_from_filename(std::filesystem::path config_path);
+
+ Config parse_config(std::string const & interface_name, std::istream & stream);
+
+ SystemdConfig gen_systemd_config(Config const & cfg, std::string const & output_path);
+
+ SystemdConfig wg2sd(std::string const & interface_name, std::istream & stream, std::string const & output_path);
+
+};