From 50df8f91a90d8c91676b81cc9668b58914f85b08 Mon Sep 17 00:00:00 2001 From: flu0r1ne Date: Mon, 20 Nov 2023 17:45:24 -0600 Subject: Allow configurable activation policy, allow -h before dropping caps --- README.md | 32 +++++---- src/main.cpp | 217 ++++++++++++++++++++++++++++++++++++---------------------- src/wg2nd.cpp | 33 +++++++-- src/wg2nd.hpp | 12 +++- 4 files changed, 191 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index 9664eb4..36ad5e4 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,8 @@ wg2nd generate -t nft /etc/wireguard/wg0.conf >> /etc/nftables.conf networkctl up wg0 ``` -To enable automatic starting, ensure that the `ActivationPolicy` is removed from the generated `network` configuration. +To enable automatic starting, use `wg2nd install -a up /etc/wireguard/wg0.conf`. This sets the +default [activation policy](https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#ActivationPolicy=) to `up`. ### Batch Conversion @@ -136,7 +137,7 @@ Usage: wg2nd version ``` ```plaintext -Usage: wg2nd install [ -h ] [ -f FILE_NAME ] [ -o OUTPUT_PATH ] CONFIG_FILE +Usage: ./wg2nd install [ -h ] [ -a ACTIVATION_POLICY ] [ -f FILE_NAME ] [ -o OUTPUT_PATH ] CONFIG_FILE `wg2nd install` translates `wg-quick(8)` configuration into corresponding `networkd` configuration and installs the resulting files in `OUTPUT_PATH`. @@ -152,6 +153,10 @@ Usage: wg2nd install [ -h ] [ -f FILE_NAME ] [ -o OUTPUT_PATH ] CONFIG_FILE `wg2nd generate -t nft CONFIG_FILE`. Options: + -a ACTIVATION_POLICY + manual Require manual activation (default) + up Automatically set the link "up" + -o OUTPUT_PATH The installation path (default is /etc/systemd/network) -f FILE_NAME The base name for the installed configuration files. The @@ -166,18 +171,21 @@ Options: ``` ```plaintext -Usage: wg2nd generate [ -h ] [ -t { network, netdev, keyfile, nft } ] CONFIG_FILE - -`wg2nd generate` translates `wg-quick(8)` configuration into the equivalent -`systemd-networkd` configuration. The results are printed to `stdout`. Users -are responsible for installing these files correctly and restricting access privileges. +Usage: ./wg2nd generate [ -h ] [ -a ACTIVATION_POLICY ] [ -k KEYPATH ] [ -t { network, netdev, keyfile, nft } ] CONFIG_FILE Options: + -a ACTIVATION_POLICY + manual Require manual activation (default) + up Automatically set the link "up" + -t FILE_TYPE - network Generate a Network Configuration File (see systemd.network(8)) - netdev Generate a Virtual Device File (see systemd.netdev(8)) - keyfile Print the interface's private key - nft Print the netfilter table `nft(8)` installed by `wg-quick(8)` + network Generate a Network Configuration File (see systemd.network(8)) + netdev Generate a Virtual Device File (see systemd.netdev(8)) + keyfile Print the interface's private key + nft Print the netfilter table `nft(8)` installed by `wg-quick(8)` + + -k KEYPATH Full path to the keyfile (a path relative to /etc/systemd/network is generated + if unspecified) - -h Display this help + -h Print this help ``` diff --git a/src/main.cpp b/src/main.cpp index 36226c7..7cc8d13 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -134,13 +134,16 @@ void print_help(const char *prog) { } void die_usage_generate(const char *prog) { - err("Usage: %s generate [ -h ] [ -k KEYPATH ] [ -t { network, netdev, keyfile, nft } ] CONFIG_FILE", prog); + err("Usage: %s generate [ -h ] [ -k KEYPATH ] [ -t { network, netdev, keyfile, nft } ] [ -a ACTIVATION_POLICY ] CONFIG_FILE\n", prog); die("Use -h for help"); } void print_help_generate(const char *prog) { - err("Usage: %s generate [ -h ] [ -k KEYPATH ] [ -t { network, netdev, keyfile, nft } ] CONFIG_FILE\n", prog); + err("Usage: %s generate [ -h ] [ -a ACTIVATION_POLICY ] [ -k KEYPATH ] [ -t { network, netdev, keyfile, nft } ] CONFIG_FILE\n", prog); err("Options:"); + err(" -a ACTIVATION_POLICY"); + err(" manual Require manual activation (default)"); + err(" up Automatically set the link \"up\"\n"); 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))"); @@ -153,12 +156,12 @@ void print_help_generate(const char *prog) { } void die_usage_install(const char *prog) { - err("Usage: %s install [ -h ] [ -f FILE_NAME ] [ -k KEYFILE ] [ -o OUTPUT_PATH ] CONFIG_FILE", prog); + err("Usage: %s install [ -h ] [ -a ACTIVATION_POLICY ] [ -f FILE_NAME ] [ -o OUTPUT_PATH ] CONFIG_FILE\n", 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("Usage: %s install [ -h ] [ -a ACTIVATION_POLICY ] [ -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"); @@ -170,6 +173,9 @@ void print_help_install(const char *prog) { err(" with `wg2nd install`. The equivalent firewall can be generated with"); err(" `wg2nd generate -t nft CONFIG_FILE`.\n"); err("Options:"); + err(" -a ACTIVATION_POLICY"); + err(" manual Require manual activation (default)"); + err(" up Automatically set the link \"up\"\n"); 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"); @@ -211,7 +217,7 @@ enum class FileType { using namespace wg2nd; -void write_systemd_file(SystemdFilespec const & filespec, std::string output_path, bool secure) { +static void write_systemd_file(SystemdFilespec const & filespec, std::string output_path, bool secure) { std::string full_path = output_path + "/" + filespec.name; std::ofstream ofs; @@ -254,10 +260,11 @@ void write_systemd_file(SystemdFilespec const & filespec, std::string output_pat } } -SystemdConfig generate_cfg_or_die( +static SystemdConfig generate_cfg_or_die( std::filesystem::path && config_path, std::filesystem::path const & keyfile_or_output_path, - std::optional const & filename + std::optional const & filename, + ActivationPolicy activation_policy ) { std::fstream cfg_stream { config_path, std::ios_base::in }; @@ -270,7 +277,13 @@ SystemdConfig generate_cfg_or_die( std::string interface_name = interface_name_from_filename(config_path); try { - cfg = wg2nd::wg2nd(interface_name, cfg_stream, keyfile_or_output_path, filename); + cfg = wg2nd::wg2nd( + interface_name, + cfg_stream, + keyfile_or_output_path, + filename, + activation_policy + ); } catch(ConfigurationException const & cex) { const ParsingException * pex = dynamic_cast(&cex); @@ -286,8 +299,9 @@ SystemdConfig generate_cfg_or_die( } -void wg2nd_install_internal(std::optional && filename, std::string && keyfile_name, - std::filesystem::path && output_path, std::filesystem::path && config_path) { +static void wg2nd_install_internal(std::optional && filename, std::string && keyfile_name, + std::filesystem::path && output_path, std::filesystem::path && config_path, + ActivationPolicy activation_policy) { if(!std::filesystem::path(output_path).is_absolute()) { output_path = std::filesystem::absolute(output_path); @@ -299,7 +313,12 @@ void wg2nd_install_internal(std::optional && filename, std::string keyfile_or_output_path /= keyfile_name; } - SystemdConfig cfg = generate_cfg_or_die(std::move(config_path), keyfile_or_output_path, std::move(filename)); + SystemdConfig cfg = generate_cfg_or_die( + std::move(config_path), + keyfile_or_output_path, + std::move(filename), + activation_policy + ); for(std::string const & warning : cfg.warnings) { err("warning: %s", warning.c_str()); @@ -314,11 +333,15 @@ void wg2nd_install_internal(std::optional && filename, std::string } } -void wg2nd_generate_internal(FileType type, std::string && config_file, - std::optional && keyfile_path) { +static void wg2nd_generate_internal(FileType type, std::string && config_file, + std::optional && keyfile_path, + ActivationPolicy activation_policy) { SystemdConfig cfg = generate_cfg_or_die( - std::move(config_file), std::move(keyfile_path.value_or(DEFAULT_OUTPUT_PATH)), {} + std::move(config_file), + std::move(keyfile_path.value_or(DEFAULT_OUTPUT_PATH)), + {}, + activation_policy ); switch(type) { @@ -339,15 +362,79 @@ void wg2nd_generate_internal(FileType type, std::string && config_file, } } +#ifdef HAVE_LIBCAP + +// Drop excess capabilities and ensure the process have proper capabilities upfront +static void drop_excess_capabilities(std::vector cap_required) { + + cap_t current_cap = cap_get_proc(); + cap_t wanted_cap = cap_get_proc(); + + if(!current_cap || !wanted_cap) { + goto die_cap_failure; + } + + if(cap_clear(wanted_cap)) { + goto die_cap_failure; + } + + // Add required capabilities to the wanted set + for(cap_value_t cap : cap_required) { + cap_flag_value_t is_set; + + // Print nice error message if the user does not have the required permissions + if(cap_get_flag(current_cap, cap, CAP_PERMITTED, &is_set)) { + goto die_cap_failure; + } + + if(is_set != CAP_SET) { + char * name = cap_to_name(cap); + die("Failed to obtain capability \"%s\": do you need to elevate permissions?", name); + } + + if(cap_set_flag(wanted_cap, CAP_PERMITTED, 1, &cap, CAP_SET)) { + goto die_cap_failure; + } + + if(cap_set_flag(wanted_cap, CAP_EFFECTIVE, 1, &cap, CAP_SET)) { + goto die_cap_failure; + } + } + + if(cap_set_proc(wanted_cap)) { + goto die_cap_failure; + } + + if(cap_free(wanted_cap) || cap_free(current_cap)) { + goto die_cap_failure; + } + + return; + +die_cap_failure: + die_errno("Failed to drop capabilities"); +} +#endif /* HAVE_LIBCAP */ + +static ActivationPolicy activation_policy_from_argument(std::string const & arg) { + if(arg == "manual") { + return ActivationPolicy::MANUAL; + } else if (arg == "up") { + return ActivationPolicy::UP; + } else { + die("Unknown activation policy: \"%s\"", arg.c_str()); + } +} static int wg2nd_generate(char const * prog, int argc, char **argv) { std::filesystem::path config_path = ""; FileType type = FileType::NONE; std::optional keyfile_path = {}; + ActivationPolicy activation_policy = ActivationPolicy::MANUAL; int opt; - while ((opt = getopt(argc, argv, "ht:k:")) != -1) { + while ((opt = getopt(argc, argv, "ht:k:a:")) != -1) { switch (opt) { case 't': if (strcmp(optarg, "network") == 0) { @@ -365,6 +452,9 @@ static int wg2nd_generate(char const * prog, int argc, char **argv) { case 'k': keyfile_path = optarg; break; + case 'a': + activation_policy = activation_policy_from_argument(optarg); + break; case 'h': print_help_generate(prog); break; @@ -377,9 +467,18 @@ static int wg2nd_generate(char const * prog, int argc, char **argv) { die_usage_generate(prog); } +#ifdef HAVE_LIBCAP + drop_excess_capabilities({}); +#endif /* HAVE_LIBCAP */ + config_path = argv[optind]; - wg2nd_generate_internal(type, std::move(config_path), std::move(keyfile_path)); + wg2nd_generate_internal( + type, + std::move(config_path), + std::move(keyfile_path), + activation_policy + ); return 0; } @@ -390,9 +489,10 @@ static int wg2nd_install(char const * prog, int argc, char **argv) { std::optional filename = {}; std::filesystem::path output_path = DEFAULT_OUTPUT_PATH; std::string keyfile_name = ""; + ActivationPolicy activation_policy = ActivationPolicy::MANUAL; int opt; - while ((opt = getopt(argc, argv, "o:f:k:h")) != -1) { + while ((opt = getopt(argc, argv, "o:f:k:a:h")) != -1) { switch (opt) { case 'o': { std::string path = optarg; @@ -411,6 +511,9 @@ static int wg2nd_install(char const * prog, int argc, char **argv) { case 'k': keyfile_name = optarg; break; + case 'a': + activation_policy = activation_policy_from_argument(optarg); + break; default: die_usage_install(prog); } @@ -420,66 +523,25 @@ static int wg2nd_install(char const * prog, int argc, char **argv) { 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; -} - #ifdef HAVE_LIBCAP + drop_excess_capabilities({{ + CAP_CHOWN, + CAP_DAC_OVERRIDE + }}); +#endif /* HAVE_LIBCAP */ -// Drop excess capabilities and ensure the process have proper capabilities upfront -void drop_excess_capabilities(std::vector cap_required) { - - cap_t current_cap = cap_get_proc(); - cap_t wanted_cap = cap_get_proc(); - - if(!current_cap || !wanted_cap) { - goto die_cap_failure; - } - - if(cap_clear(wanted_cap)) { - goto die_cap_failure; - } - - // Add required capabilities to the wanted set - for(cap_value_t cap : cap_required) { - cap_flag_value_t is_set; - - // Print nice error message if the user does not have the required permissions - if(cap_get_flag(current_cap, cap, CAP_PERMITTED, &is_set)) { - goto die_cap_failure; - } - - if(is_set != CAP_SET) { - char * name = cap_to_name(cap); - die("Failed to obtain capability \"%s\": do you need to elevate permissions?", name); - } - - if(cap_set_flag(wanted_cap, CAP_PERMITTED, 1, &cap, CAP_SET)) { - goto die_cap_failure; - } - - if(cap_set_flag(wanted_cap, CAP_EFFECTIVE, 1, &cap, CAP_SET)) { - goto die_cap_failure; - } - } - - if(cap_set_proc(wanted_cap)) { - goto die_cap_failure; - } - - if(cap_free(wanted_cap) || cap_free(current_cap)) { - goto die_cap_failure; - } + config_path = argv[optind]; - return; + wg2nd_install_internal( + std::move(filename), + std::move(keyfile_name), + std::move(output_path), + std::move(config_path), + activation_policy + ); -die_cap_failure: - die_errno("Failed to drop capabilities"); + return 0; } -#endif /* HAVE_LIBCAP */ int main(int argc, char **argv) { char const * prog = "wg2nd"; @@ -494,17 +556,6 @@ int main(int argc, char **argv) { std::string action = argv[1]; -#ifdef HAVE_LIBCAP - std::vector required_caps; - - if(action == "install") { - required_caps.push_back(CAP_CHOWN); - required_caps.push_back(CAP_DAC_OVERRIDE); - } - - drop_excess_capabilities(required_caps); -#endif /* HAVE_LIBCAP */ - if (action == "generate") { return wg2nd_generate(prog, argc - 1, argv + 1); } else if (action == "install") { diff --git a/src/wg2nd.cpp b/src/wg2nd.cpp index deb3212..afe3132 100644 --- a/src/wg2nd.cpp +++ b/src/wg2nd.cpp @@ -446,7 +446,18 @@ namespace wg2nd { return netdev.str(); } - static std::string _gen_network_cfg(Config const & cfg, uint32_t fwd_table) { + static std::string_view activation_policy_keyword(ActivationPolicy activation_policy) { + switch(activation_policy) { + case ActivationPolicy::MANUAL: + return "manual"; + case ActivationPolicy::UP: + return "up"; + } + + return "none"; + } + + static std::string _gen_network_cfg(Config const & cfg, uint32_t fwd_table, ActivationPolicy activation_policy) { std::stringstream network; network << "# Autogenerated by wg2nd\n"; @@ -455,7 +466,9 @@ namespace wg2nd { network << "\n"; network << "[Link]" << "\n"; - network << "ActivationPolicy = manual\n"; + + network << "ActivationPolicy = " << activation_policy_keyword(activation_policy) << "\n"; + if(!cfg.intf.mtu.empty()) { network << "MTUBytes = " << cfg.intf.mtu << "\n"; } @@ -551,7 +564,8 @@ namespace wg2nd { SystemdConfig gen_systemd_config( Config const & cfg, std::filesystem::path const & keyfile_or_output_path, - std::optional const & filename + std::optional const & filename, + ActivationPolicy activation_policy ) { // If the table is explicitly specified with Table=, @@ -608,7 +622,7 @@ if(!cfg.intf.field_.empty()) { \ }, .network = { .name = basename + ".network", - .contents = _gen_network_cfg(cfg, fwd_table) + .contents = _gen_network_cfg(cfg, fwd_table, activation_policy) }, .private_keyfile = { .name = keyfile_path.filename(), @@ -621,8 +635,15 @@ if(!cfg.intf.field_.empty()) { \ } 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); + std::filesystem::path const & keyfile_or_output_path, + std::optional const & filename, + ActivationPolicy activation_policy) { + return gen_systemd_config( + parse_config(interface_name, stream), + keyfile_or_output_path, + filename, + activation_policy + ); } } diff --git a/src/wg2nd.hpp b/src/wg2nd.hpp index 4e3e18a..fbded13 100644 --- a/src/wg2nd.hpp +++ b/src/wg2nd.hpp @@ -17,6 +17,11 @@ namespace wg2nd { + enum class ActivationPolicy { + MANUAL, + UP, + }; + struct Interface { // File name, or defaults to "wg" std::string name; @@ -143,11 +148,14 @@ namespace wg2nd { SystemdConfig gen_systemd_config( Config const & cfg, std::filesystem::path const & keyfile_or_output_path, - std::optional const & filename + std::optional const & filename, + ActivationPolicy activation_policy = ActivationPolicy::MANUAL ); SystemdConfig wg2nd(std::string const & interface_name, std::istream & stream, std::filesystem::path const & keyfile_or_output_path, - std::optional const & filename); + std::optional const & filename, + ActivationPolicy activation_policy = ActivationPolicy::MANUAL + ); }; -- cgit v1.2.3