From 84ba3607bf4b87e2f872e960c957d7d860acd83d Mon Sep 17 00:00:00 2001 From: flu0r1ne Date: Fri, 25 Aug 2023 19:05:30 -0500 Subject: Rename project wg2sd -> wg2nd, new CLI + generate 1. Renamed the project from wg2sd to wg2nd 2. Modified the _gen_netdev_cfg() function to handle the MTUBytes field. 3. Add new CLI with `generate` and `install` commands 4. Modified the gen_systemd_config() function to accept keyfile_or_output_path and filename parameters. - user can choose the name of the keyfile on the CLI - user can choose alternative output filename (instead of just using the interface name) --- src/main.cpp | 327 +++++++++++++++++++++++++----- src/wg2nd.cpp | 632 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/wg2nd.hpp | 147 ++++++++++++++ src/wg2sd.cpp | 598 ------------------------------------------------------ src/wg2sd.hpp | 141 ------------- 5 files changed, 1055 insertions(+), 790 deletions(-) create mode 100644 src/wg2nd.cpp create mode 100644 src/wg2nd.hpp delete mode 100644 src/wg2sd.cpp delete mode 100644 src/wg2sd.hpp (limited to 'src') diff --git a/src/main.cpp b/src/main.cpp index 19b5ced..b8c8ec6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,14 @@ +#include +#include +#include +#include +#include +#include +#include +#include + // ===================================== -// ERROR HANDING - FROM FCUTILS +// ERROR HANDING - FROM FCUTILS // ===================================== #include @@ -69,35 +78,122 @@ void err(char const * format, ...) { } // ============================================= -// COMMAND LINE UTILITY +// COMMAND LINE UTILITY // ============================================= +constexpr char const * DEFAULT_OUTPUT_PATH = "/etc/systemd/network/"; -#include "wg2sd.hpp" +/* + * HELP AND USAGE + */ -#include -#include -#include -#include -#include -#include -#include +void die_usage(const char *prog) { + err("Usage: %s { install, generate } [ OPTIONS ] { -h, CONFIG_FILE }", prog); + die("Use -h for help"); +} + +void print_help(const char *prog) { + err("Usage: %s { install, generate } [ OPTIONS ] { -h, CONFIG_FILE }\n", prog); + err(" CONFIG_FILE is the complete path to a WireGuard configuration file, used by"); + err(" `wg-quick`. `wg2nd` will convert the WireGuard configuration to networkd"); + err(" files.\n"); + err(" The generated configurations are functionally equivalent to `wg-quick(8)`"); + err(" with the following exceptions:\n"); + err(" 1. When unspecified, `wg-quick` determines whether `FwMark` and `Table` are available dynamically,"); + err(" ensuring that the routing table and `fwmark` are not already in use. `wg2nd` sets"); + err(" the `fwmark` to a random number (deterministically generated from the interface"); + err(" name). If more than 500 `fwmarks` are in use, there is a non-negligible chance of a"); + err(" collision. This would occur when there are more than 500 active WireGuard interfaces.\n"); + err(" 2. The PreUp, PostUp, PreDown, and PostDown script snippets are ignored.\n"); + err(" 3. `wg-quick(8)` installs a firewall when a default route is specified (i.e., when `0.0.0.0/0`"); + err(" or `::/0` are specified in `AllowedIPs`). This is not installed by"); + err(" default with `wg2nd install`. The equivalent firewall can be generated with"); + err(" `wg2nd generate -t nft CONFIG_FILE`. Refer to `nft(8)` for details.\n"); + err(" Actions:"); + err(" install Generate and install the configuration with restricted permissions"); + err(" generate Generate specific configuration files and write the results to stdout\n"); + err(" Options:"); + err(" -h Print this help"); + exit(EXIT_SUCCESS); +} -[[noreturn]] void die_usage(char const * prog) { - err("Usage: %s [ -o OUTPUT_PATH ] CONFIG_FILE", prog); +void die_usage_generate(const char *prog) { + err("Usage: %s generate [ -h ] [ -k KEYPATH ] [ -t { network, netdev, keyfile, nft } ] CONFIG_FILE", prog); die("Use -h for help"); } -void print_help(char const * prog) { - err("Usage: %s [ -h | -f | -o OUTPUT_PATH ] CONFIG_FILE", prog); +void print_help_generate(const char *prog) { + err("Usage: %s generate [ -h ] [ -k KEYPATH ] [ -t { network, netdev, keyfile, nft } ] CONFIG_FILE\n", prog); err("Options:"); - err("-o OUTPUT_PATH\tSet the output path (default is /etc/systemd/network)"); - err("-f \tOutput firewall rules"); - err("-h \tDisplay this help message"); + err(" -t FILE_TYPE"); + err(" network Generate a Network Configuration File (see systemd.network(8))"); + err(" netdev Generate a Virtual Device File (see systemd.netdev(8))"); + err(" keyfile Print the interface's private key"); + err(" nft Print the netfilter table `nft(8)` installed by `wg-quick(8)`\n"); + err(" -k KEYPATH Full path to the keyfile (a path relative to /etc/systemd/network is generated"); + err(" if unspecified)\n"); + err(" -h Print this help"); exit(EXIT_SUCCESS); } -using namespace wg2sd; +void die_usage_install(const char *prog) { + err("Usage: %s install [ -h ] [ -f FILE_NAME ] [ -k KEYFILE ] [ -o OUTPUT_PATH ] CONFIG_FILE", prog); + die("Use -h for help"); +} + +void print_help_install(const char *prog) { + err("Usage: %s install [ -h ] [ -f FILE_NAME ] [ -o OUTPUT_PATH ] CONFIG_FILE\n", prog); + err(" `wg2nd install` translates `wg-quick(8)` configuration into corresponding"); + err(" `networkd` configuration and installs the resulting files in `OUTPUT_PATH`.\n"); + err(" `wg2nd install` generates a `netdev`, `network`, and `keyfile` for each"); + err(" CONFIG_FILE. Links will be installed with a `manual` `ActivationPolicy`."); + err(" The interface can be brought up with `networkctl up INTERFACE` and down"); + err(" with `networkctl down INTERFACE`.\n"); + err(" `wg-quick(8)` installs a firewall when a default route (i.e., when `0.0.0.0/0`"); + err(" or `::/0` is specified in `AllowedIPs`). This is not installed by default"); + err(" with `wg2nd install`. The equivalent firewall can be generated with"); + err(" `wg2nd generate -t nft CONFIG_FILE`.\n"); + err("Options:"); + err(" -o OUTPUT_PATH The installation path (default is /etc/systemd/network)\n"); + err(" -f FILE_NAME The base name for the installed configuration files. The"); + err(" networkd-specific configuration suffix will be added"); + err(" (FILE_NAME.netdev for systemd-netdev(8) files,"); + err(" FILE_NAME.network for systemd-network(8) files,"); + err(" and FILE_NAME.keyfile for keyfiles)\n"); + err(" -k KEYFILE The name of the private keyfile\n"); + err(" -h Print this help"); + exit(EXIT_SUCCESS); +} + + +/* + * PARSING + */ + +enum class FileType { + NONE = 0, + NETWORK, + NETDEV, + KEYFILE, + NFT +}; + + +/* + * INTERNAL LOGIC + */ + +#include "wg2nd.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace wg2nd; void write_systemd_file(SystemdFilespec const & filespec, std::string output_path, bool secure) { std::string full_path = output_path + "/" + filespec.name; @@ -142,44 +238,23 @@ void write_systemd_file(SystemdFilespec const & filespec, std::string output_pat } } -int main(int argc, char ** argv) { - int opt; - std::filesystem::path output_path = "/etc/systemd/network"; - bool print_firewall_rules = false; - - while ((opt = getopt(argc, argv, "o:fh")) != -1) { - switch (opt) { - case 'o': - output_path = optarg; - break; - case 'f': - print_firewall_rules = true; - 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]; +SystemdConfig generate_cfg_or_die( + std::filesystem::path && config_path, + std::filesystem::path const & keyfile_or_output_path, + std::optional const & filename + ) { 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; + std::string interface_name = interface_name_from_filename(config_path); + try { - cfg = wg2sd::wg2sd(interface_name, cfg_stream, output_path); + cfg = wg2nd::wg2nd(interface_name, cfg_stream, keyfile_or_output_path, filename); } catch(ConfigurationException const & cex) { const ParsingException * pex = dynamic_cast(&cex); @@ -191,16 +266,25 @@ int main(int argc, char ** argv) { } - if(print_firewall_rules) { - fprintf(stdout, "%s", cfg.firewall.c_str()); + return cfg; +} - return 0; - } + +void wg2nd_install_internal(std::optional && filename, std::string && keyfile_name, + std::filesystem::path && output_path, std::filesystem::path && config_path) { if(!std::filesystem::path(output_path).is_absolute()) { output_path = std::filesystem::absolute(output_path); } + std::filesystem::path keyfile_or_output_path = output_path; + + if(!keyfile_name.empty()) { + keyfile_or_output_path /= keyfile_name; + } + + SystemdConfig cfg = generate_cfg_or_die(std::move(config_path), keyfile_or_output_path, std::move(filename)); + for(std::string const & warning : cfg.warnings) { err("warning: %s", warning.c_str()); } @@ -212,6 +296,147 @@ int main(int argc, char ** argv) { for(SystemdFilespec const & spec : cfg.symmetric_keyfiles) { write_systemd_file(spec, output_path, true); } +} + +void wg2nd_generate_internal(FileType type, std::string && config_file, + std::optional && keyfile_path) { + + SystemdConfig cfg = generate_cfg_or_die( + std::move(config_file), std::move(keyfile_path.value_or(DEFAULT_OUTPUT_PATH)), {} + ); + + switch(type) { + case FileType::NFT: + printf("%s", cfg.firewall.c_str()); + break; + case FileType::NETWORK: + printf("%s", cfg.network.contents.c_str()); + break; + case FileType::NETDEV: + printf("%s", cfg.netdev.contents.c_str()); + break; + case FileType::KEYFILE: + printf("%s", cfg.private_keyfile.contents.c_str()); + break; + default: + break; + } +} + + +static int wg2nd_generate(char const * prog, int argc, char **argv) { + std::filesystem::path config_path = ""; + + FileType type = FileType::NONE; + std::optional keyfile_path = {}; + + int opt; + while ((opt = getopt(argc, argv, "ht:k:")) != -1) { + switch (opt) { + case 't': + if (strcmp(optarg, "network") == 0) { + type = FileType::NETWORK; + } else if (strcmp(optarg, "netdev") == 0) { + type = FileType::NETDEV; + } else if (strcmp(optarg, "keyfile") == 0) { + type = FileType::KEYFILE; + } else if (strcmp(optarg, "nft") == 0) { + type = FileType::NFT; + } else { + die("Unknown file type: %s", optarg); + } + break; + case 'k': + keyfile_path = optarg; + break; + case 'h': + print_help_generate(prog); + break; + default: + die_usage_generate(prog); + } + } + + if (optind >= argc) { + die_usage_generate(prog); + } + + config_path = argv[optind]; + + wg2nd_generate_internal(type, std::move(config_path), std::move(keyfile_path)); + + return 0; +} + +static int wg2nd_install(char const * prog, int argc, char **argv) { + std::filesystem::path config_path = ""; + + std::optional filename = {}; + std::filesystem::path output_path = DEFAULT_OUTPUT_PATH; + std::string keyfile_name = ""; + + int opt; + while ((opt = getopt(argc, argv, "o:f:k:h")) != -1) { + switch (opt) { + case 'o': { + std::string path = optarg; + if(path[path.size() - 1] != '/') { + path.push_back('/'); + } + output_path = std::move(path); + break; + } + case 'f': + filename = optarg; + break; + case 'h': + print_help_install(prog); + break; + case 'k': + keyfile_name = optarg; + break; + default: + die_usage_install(prog); + } + } + + if (optind >= argc) { + die_usage_install(prog); + } + + config_path = argv[optind]; + + wg2nd_install_internal(std::move(filename), std::move(keyfile_name), std::move(output_path), std::move(config_path)); + + return 0; +} + +// The main function remains the same as before + + +int main(int argc, char **argv) { + char const * prog = "wg2nd"; + + if(argc > 0) { + prog = argv[0]; + } + + if (argc < 2) { + die_usage(prog); + } + + std::string action = argv[1]; + + if (action == "generate") { + return wg2nd_generate(prog, argc - 1, argv + 1); + } else if (action == "install") { + return wg2nd_install(prog, argc - 1, argv + 1); + } else if (action == "-h" || action == "--help") { + print_help(prog); + } else { + err("Unknown action: %s", action.c_str()); + die_usage(prog); + } return 0; } diff --git a/src/wg2nd.cpp b/src/wg2nd.cpp new file mode 100644 index 0000000..c53df04 --- /dev/null +++ b/src/wg2nd.cpp @@ -0,0 +1,632 @@ +#include "wg2nd.hpp" + +#include +#include +#include +#include + +#include +#include + +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(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]; + + argon2id_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 } ; +} + +uint32_t deterministic_fwmark(std::string const & interface_name) { + constexpr uint8_t const SALT[] = { + 0x90, 0x08, 0x82, 0xd7, 0x75, 0x68, 0xf4, 0x8e, + 0x90, 0x74, 0x0c, 0x74, 0x0d, 0xf4, 0xfb, 0x91, + 0xe5, 0x44, 0x87, 0x7e, 0xce, 0x48, 0xcf, 0x01, + }; + + uint8_t const * key = reinterpret_cast(interface_name.c_str()); + uint32_t keylen = interface_name.size(); + + uint32_t mark; + + uint8_t t_cost = 2; + uint32_t m_cost = 1 << 10; + uint32_t parallelism = 1; + + argon2id_hash_raw(t_cost, m_cost, parallelism, key, keylen, SALT, sizeof(SALT), &mark, sizeof(mark)); + + return mark; +} + + +namespace wg2nd { + + 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); + } + + std::string_view _get_addr(std::string_view const & cidr) { + size_t suffix = cidr.rfind('/'); + + if(suffix == std::string::npos) { + return cidr; + } else { + return cidr.substr(0, suffix); + } + } + + 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 if (key == "MTU") { + cfg.intf.mtu = value; + } else if (key == "PreUp") { + cfg.intf.preup = value; + } else if (key == "PostUp") { + cfg.intf.postup = value; + } else if (key == "PreDown") { + cfg.intf.predown = value; + } else if (key == "PostDown") { + cfg.intf.postdown = value; + } else if (key == "SaveConfig") { + cfg.intf.save_config = value; + } 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 void _write_table(std::stringstream & firewall, Config const & cfg, std::vector addrs, bool ipv4, uint32_t fwd_table) { + char const * ip = ipv4 ? "ip" : "ip6"; + + firewall << "table " << ip << " " << cfg.intf.name << " {\n" + << " chain preraw {\n" + << " type filter hook prerouting priority raw; policy accept;\n"; + + for(std::string_view const & addr : addrs) { + firewall << " iifname != \"" << cfg.intf.name << "\" " << ip << " daddr " << addr << " fib saddr type != local drop;\n"; + } + + firewall << " }\n" + << "\n" + << " chain premangle {\n" + << " type filter hook prerouting priority mangle; policy accept;\n" + << " meta l4proto udp meta mark set ct mark;\n" + << " }\n" + << "\n" + << " chain postmangle {\n" + << " type filter hook postrouting priority mangle; policy accept;\n" + << " meta l4proto udp meta mark 0x" << std::hex << fwd_table << std::dec << " ct mark set meta mark;\n" + << " }\n" + << "}\n"; + + } + + std::string _gen_nftables_firewall(Config const & cfg, uint32_t fwd_table) { + std::stringstream firewall; + + std::vector ipv4_addrs; + std::vector ipv6_addrs; + + for(std::string const & addr : cfg.intf.addresses) { + if(_is_ipv4_route(addr)) { + ipv4_addrs.push_back(_get_addr(addr)); + } else { + ipv6_addrs.push_back(_get_addr(addr)); + } + } + + if(ipv4_addrs.size() > 0) { + _write_table(firewall, cfg, ipv4_addrs, true, fwd_table); + firewall << "\n"; + } + + if(ipv6_addrs.size() > 0) { + _write_table(firewall, cfg, ipv6_addrs, false, fwd_table); + } + + return firewall.str(); + } + + static std::string _gen_netdev_cfg(Config const & cfg, uint32_t fwd_table, std::filesystem::path const & private_keyfile, + std::vector & symmetric_keyfiles) { + std::stringstream netdev; + + netdev << "# Autogenerated by wg2nd\n"; + netdev << "[NetDev]\n"; + netdev << "Name = " << cfg.intf.name << "\n"; + netdev << "Kind = wireguard\n"; + netdev << "Description = " << cfg.intf.name << " - wireguard tunnel\n"; + netdev << "\n"; + + netdev << "[WireGuard]\n"; + netdev << "PrivateKeyFile = " << private_keyfile.string() << "\n"; + + if(cfg.intf.listen_port.has_value()) { + netdev << "ListenPort = " << cfg.intf.listen_port.value() << "\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 << "PublicKey = " << peer.public_key << "\n"; + + if(!peer.endpoint.empty()) { + netdev << "Endpoint = " << peer.endpoint << "\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 wg2nd\n"; + network << "[Match]\n"; + network << "Name = " << cfg.intf.name << "\n"; + network << "\n"; + + network << "[Link]" << "\n"; + network << "ActivationPolicy = manual\n"; + if(!cfg.intf.mtu.empty()) { + network << "MTUBytes = " << cfg.intf.mtu << "\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 _deterministic_random_table(std::string const & interface_name) { + + uint32_t table = 0; + while(table == 0 or table == MAIN_TABLE or table == LOCAL_TABLE) { + table = deterministic_fwmark(interface_name); + } + + return table; + } + + SystemdConfig gen_systemd_config( + Config const & cfg, + std::filesystem::path const & keyfile_or_output_path, + std::optional const & filename + ) { + + // If the table is explicitly specified with Table=, + // 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 = _deterministic_random_table(cfg.intf.name); + + std::vector symmetric_keyfiles; + + std::filesystem::path keyfile_path; + + if(keyfile_or_output_path.has_filename()) { + keyfile_path = keyfile_or_output_path; + } else { + std::string private_keyfile = hashed_keyfile_name(cfg.intf.private_key); + keyfile_path = keyfile_or_output_path / private_keyfile; + } + + std::vector warnings; + +#define WarnOnIntfField(field_, field_name) \ +if(!cfg.intf.field_.empty()) { \ + warnings.push_back("[Interface] section contains a field \"" field_name "\" which does not have a systemd-networkd analog, omitting"); \ +} + + WarnOnIntfField(preup, "PreUp") + WarnOnIntfField(postup, "PostUp") + WarnOnIntfField(predown, "PreDown") + WarnOnIntfField(postdown, "PostDown") + WarnOnIntfField(save_config, "SaveConfig") + + if(!cfg.intf.preup.empty()) { + warnings.push_back("[Interface] section contains a field \"PreUp\" which does not have a systemd-networkd analog"); + } + + std::string const & basename = filename.value_or(cfg.intf.name); + + return SystemdConfig { + .netdev = { + .name = basename + ".netdev", + .contents = _gen_netdev_cfg(cfg, fwd_table, keyfile_path, symmetric_keyfiles), + }, + .network = { + .name = basename + ".network", + .contents = _gen_network_cfg(cfg, fwd_table) + }, + .private_keyfile = { + .name = keyfile_path.filename(), + .contents = cfg.intf.private_key + "\n", + }, + .symmetric_keyfiles = std::move(symmetric_keyfiles), + .warnings = std::move(warnings), + .firewall = _gen_nftables_firewall(cfg, fwd_table), + }; + } + + SystemdConfig wg2nd(std::string const & interface_name, std::istream & stream, + std::filesystem::path const & keyfile_or_output_path, std::optional const & filename) { + return gen_systemd_config(parse_config(interface_name, stream), keyfile_or_output_path, filename); + } + +} diff --git a/src/wg2nd.hpp b/src/wg2nd.hpp new file mode 100644 index 0000000..7bf7f54 --- /dev/null +++ b/src/wg2nd.hpp @@ -0,0 +1,147 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace wg2nd { + + struct Interface { + // File name, or defaults to "wg" + std::string name; + // Address=... + // List of ip addresses to be assigned to the interface + std::vector 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 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 listen_port; + // PreUp, PostUp, PreDown PostDown + std::string preup, postup, predown, postdown; + // SaveConfig + std::string save_config; + + 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 allowed_ips; + // PersistentAlive=... + std::string persistent_keepalive; + // PresharedKey=... + std::string preshared_key; + }; + + struct Config { + // [Interface] + Interface intf; + // [Peer] + std::vector 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 line_no = {}) + : ConfigurationException(message) + , _line_no { line_no } + {} + + + std::optional line_no() const noexcept { + return _line_no; + } + + private: + std::string _message; + std::optional _line_no; + }; + + struct SystemdFilespec { + std::string name; + std::string contents; + }; + + struct SystemdConfig { + SystemdFilespec netdev; + SystemdFilespec network; + SystemdFilespec private_keyfile; + std::vector symmetric_keyfiles; + + std::vector warnings; + std::string firewall; + }; + + 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::filesystem::path const & keyfile_or_output_path, + std::optional const & filename + ); + + SystemdConfig wg2nd(std::string const & interface_name, std::istream & stream, + std::filesystem::path const & keyfile_or_output_path, + std::optional const & filename); + +}; diff --git a/src/wg2sd.cpp b/src/wg2sd.cpp deleted file mode 100644 index 4c39a03..0000000 --- a/src/wg2sd.cpp +++ /dev/null @@ -1,598 +0,0 @@ -#include "wg2sd.hpp" - -#include -#include -#include -#include - -#include -#include - -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(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); - } - - std::string_view _get_addr(std::string_view const & cidr) { - size_t suffix = cidr.rfind('/'); - - if(suffix == std::string::npos) { - return cidr; - } else { - return cidr.substr(0, suffix); - } - } - - 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 if (key == "PreUp") { - cfg.intf.preup = value; - } else if (key == "PostUp") { - cfg.intf.postup = value; - } else if (key == "PreDown") { - cfg.intf.predown = value; - } else if (key == "PostDown") { - cfg.intf.postdown = value; - } else if (key == "SaveConfig") { - cfg.intf.save_config = value; - } 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 void _write_table(std::stringstream & firewall, Config const & cfg, std::vector addrs, bool ipv4) { - char const * ip = ipv4 ? "ip" : "ip6"; - - firewall << "table " << ip << " " << cfg.intf.name << " {\n" - << " chain preraw {\n" - << " type filter hook prerouting priority raw; policy accept;\n"; - - for(std::string_view const & addr : addrs) { - firewall << " iifname != \"" << cfg.intf.name << "\" " << ip << " daddr " << addr << " fib saddr type != local drop;\n"; - } - - firewall << " }\n" - << "\n" - << " chain premangle {\n" - << " type filter hook prerouting priority mangle; policy accept;\n" - << " meta l4proto udp meta mark set ct mark;\n" - << " }\n" - << "\n" - << " chain postmangle {\n" - << " type filter hook postrouting priority mangle; policy accept;\n" - << " meta l4proto udp meta mark " << std::hex << cfg.intf.table << std::dec << "ct mark set meta mark;\n" - << " }\n" - << "}\n"; - - } - - std::string _gen_nftables_firewall(Config const & cfg) { - std::stringstream firewall; - - std::vector ipv4_addrs; - std::vector ipv6_addrs; - - for(std::string const & addr : cfg.intf.addresses) { - if(_is_ipv4_route(addr)) { - ipv4_addrs.push_back(_get_addr(addr)); - } else { - ipv6_addrs.push_back(_get_addr(addr)); - } - } - - _write_table(firewall, cfg, ipv4_addrs, true); - - firewall << "\n"; - - _write_table(firewall, cfg, ipv6_addrs, false); - - return firewall.str(); - } - - static std::string _gen_netdev_cfg(Config const & cfg, uint32_t fwd_table, std::string const & private_keyfile, - std::vector & 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.listen_port.has_value()) { - netdev << "ListenPort = " << cfg.intf.listen_port.value() << "\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 << "PublicKey = " << peer.public_key << "\n"; - - if(!peer.endpoint.empty()) { - netdev << "Endpoint = " << peer.endpoint << "\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=, - // 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 symmetric_keyfiles; - - std::string private_keyfile = hashed_keyfile_name(cfg.intf.private_key); - - std::vector warnings; - -#define WarnOnIntfField(field_, field_name) \ -if(!cfg.intf.field_.empty()) { \ - warnings.push_back("[Interface] section contains a field \"" field_name "\" which does not have a systemd-networkd analog, omitting"); \ -} - - WarnOnIntfField(preup, "PreUp") - WarnOnIntfField(postup, "PostUp") - WarnOnIntfField(predown, "PreDown") - WarnOnIntfField(postdown, "PostDown") - WarnOnIntfField(save_config, "SaveConfig") - - if(!cfg.intf.preup.empty()) { - warnings.push_back("[Interface] section contains a field \"PreUp\" which does not have a systemd-networkd analog"); - } - - 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), - .warnings = std::move(warnings), - .firewall = _gen_nftables_firewall(cfg), - }; - } - - 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 deleted file mode 100644 index e80a284..0000000 --- a/src/wg2sd.hpp +++ /dev/null @@ -1,141 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include - -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 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 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 listen_port; - // PreUp, PostUp, PreDown PostDown - std::string preup, postup, predown, postdown; - // SaveConfig - std::string save_config; - - 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 allowed_ips; - // PersistentAlive=... - std::string persistent_keepalive; - // PresharedKey=... - std::string preshared_key; - }; - - struct Config { - // [Interface] - Interface intf; - // [Peer] - std::vector 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 line_no = {}) - : ConfigurationException(message) - , _line_no { line_no } - {} - - - std::optional line_no() const noexcept { - return _line_no; - } - - private: - std::string _message; - std::optional _line_no; - }; - - struct SystemdFilespec { - std::string name; - std::string contents; - }; - - struct SystemdConfig { - SystemdFilespec netdev; - SystemdFilespec network; - SystemdFilespec private_keyfile; - std::vector symmetric_keyfiles; - - std::vector warnings; - std::string firewall; - }; - - 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); - -}; -- cgit v1.2.3