diff options
Diffstat (limited to 'clients/common/nm-vpn-helpers.c')
-rw-r--r-- | clients/common/nm-vpn-helpers.c | 559 |
1 files changed, 559 insertions, 0 deletions
diff --git a/clients/common/nm-vpn-helpers.c b/clients/common/nm-vpn-helpers.c index a2e7dc0374..2353b7691e 100644 --- a/clients/common/nm-vpn-helpers.c +++ b/clients/common/nm-vpn-helpers.c @@ -25,7 +25,13 @@ #include "nm-vpn-helpers.h" +#include <arpa/inet.h> +#include <net/if.h> + +#include "nm-client-utils.h" #include "nm-utils.h" +#include "nm-utils/nm-io-utils.h" +#include "nm-utils/nm-secret-utils.h" /*****************************************************************************/ @@ -247,3 +253,556 @@ nm_vpn_openconnect_authenticate_helper (const char *host, return TRUE; } +static gboolean +_wg_complete_peer (GPtrArray **p_peers, + NMWireGuardPeer *peer_take, + gsize peer_start_line_nr, + const char *filename, + GError **error) +{ + nm_auto_unref_wgpeer NMWireGuardPeer *peer = peer_take; + gs_free_error GError *local = NULL; + + if (!peer) + return TRUE; + + if (!nm_wireguard_peer_is_valid (peer, TRUE, TRUE, &local)) { + nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN, + _("Invalid peer starting at %s:%zu: %s"), + filename, + peer_start_line_nr, + local->message); + return FALSE; + } + + if (!*p_peers) + *p_peers = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_wireguard_peer_unref); + g_ptr_array_add (*p_peers, g_steal_pointer (&peer)); + return TRUE; +} + +static gboolean +_line_match (char *line, const char *key, gsize key_len, const char **out_key, char **out_value) +{ + nm_assert (line); + nm_assert (key); + nm_assert (strlen (key) == key_len); + nm_assert (!strchr (key, '=')); + nm_assert (out_key && !*out_key); + nm_assert (out_value && !*out_value); + + /* Note that `wg-quick` (linux.bash) does case-insensitive comparison (shopt -s nocasematch). + * `wg setconf` does case-insensitive comparison too (with strncasecmp, which is locale dependent). + * + * We do a case-insensitive comparison of the key, however in a locale-independent manner. */ + + if (g_ascii_strncasecmp (line, key, key_len) != 0) + return FALSE; + + if (line[key_len] != '=') + return FALSE; + + *out_key = key; + *out_value = &line[key_len + 1]; + return TRUE; +} + +#define line_match(line, key, out_key, out_value) \ + _line_match ((line), ""key"", NM_STRLEN (key), (out_key), (out_value)) + +static gboolean +value_split_word (char **line_remainder, char **out_word) +{ + char *str; + + if ((*line_remainder)[0] == '\0') + return FALSE; + + *out_word = *line_remainder; + + str = strchrnul (*line_remainder, ','); + if (str[0] == ',') { + str[0] = '\0'; + *line_remainder = &str[1]; + } else + *line_remainder = str; + return TRUE; +} + +NMConnection * +nm_vpn_wireguard_import (const char *filename, + GError **error) +{ + nm_auto_clear_secret_ptr NMSecretPtr file_content = NM_SECRET_PTR_INIT (); + char ifname[IFNAMSIZ]; + gs_free char *uuid = NULL; + gboolean ifname_valid = FALSE; + const char *cstr; + char *line_remainder; + gs_unref_object NMConnection *connection = NULL; + NMSettingConnection *s_con; + NMSettingIPConfig *s_ip4; + NMSettingIPConfig *s_ip6; + NMSettingWireGuard *s_wg; + gs_free_error GError *local = NULL; + enum { + LINE_CONTEXT_INIT, + LINE_CONTEXT_INTERFACE, + LINE_CONTEXT_PEER, + } line_context; + gsize line_nr; + gsize current_peer_start_line_nr = 0; + nm_auto_unref_wgpeer NMWireGuardPeer *current_peer = NULL; + gs_unref_ptrarray GPtrArray *data_dns_v4 = NULL; + gs_unref_ptrarray GPtrArray *data_dns_v6 = NULL; + gs_unref_ptrarray GPtrArray *data_addr_v4 = NULL; + gs_unref_ptrarray GPtrArray *data_addr_v6 = NULL; + gs_unref_ptrarray GPtrArray *data_peers = NULL; + const char *data_private_key = NULL; + gint64 data_table; + guint data_listen_port = 0; + guint data_fwmark = 0; + guint data_mtu = 0; + int is_v4; + guint i; + + g_return_val_if_fail (filename, NULL); + g_return_val_if_fail (!error || !*error, NULL); + + /* contrary to "wg-quick", we never interpret the filename as "/etc/wireguard/$INTERFACE.conf". + * If the filename has no '/', it is interpreted as relative to the current working directory. + * However, we do require a suitable filename suffix and that the name corresponds to the interface + * name. */ + cstr = strrchr (filename, '/'); + cstr = cstr ? &cstr[1] : filename; + if (NM_STR_HAS_SUFFIX (cstr, ".conf")) { + gsize len = strlen (cstr) - NM_STRLEN (".conf"); + + if (len > 0 && len < sizeof (ifname)) { + memcpy (ifname, cstr, len); + ifname[len] = '\0'; + + if (nm_utils_is_valid_iface_name (ifname, NULL)) + ifname_valid = TRUE; + } + } + if (!ifname_valid) { + nm_utils_error_set_literal (error, NM_UTILS_ERROR_UNKNOWN, + _("The WireGuard config file must be a valid interface name followed by \".conf\"")); + return FALSE; + } + + if (nm_utils_file_get_contents (-1, + filename, + 10*1024*1024, + NM_UTILS_FILE_GET_CONTENTS_FLAG_SECRET, + &file_content.str, + &file_content.len, + error) < 0) + return NULL; + + /* We interpret the file like `wg-quick up` and `wg setconf` do. + * + * Of course the WireGuard scripts do something fundamentlly different. They + * perform actions to configure the WireGuard link in kernel, add routes and + * addresses, and call resolvconf. It all happens at the time when the script + * run. + * + * This code here instead generates a NetworkManager connection profile so that + * NetworkManager will apply a similar configuration when later activating the profile. */ + +#define _TABLE_AUTO ((gint64) -1) +#define _TABLE_OFF ((gint64) -2) + + data_table = _TABLE_AUTO; + + line_remainder = file_content.str; + line_context = LINE_CONTEXT_INIT; + line_nr = 0; + while (line_remainder[0] != '\0') { + const char *matched_key = NULL; + char *value = NULL; + char *line; + char ch; + gint64 i64; + + line_nr++; + + line = line_remainder; + line_remainder = strchrnul (line, '\n'); + if (line_remainder[0] != '\0') + (line_remainder++)[0] = '\0'; + + /* Drop all spaces and truncate at first '#'. + * See wg's config_read_line(). + * + * Note that wg-quick doesn't do that. + * + * Neither `wg setconf` nor `wg-quick` does a strict parsing. + * We don't either. Just try to interpret the file (mostly) the same as + * they would. + */ + { + gsize l, n; + + n = 0; + for (l = 0; (ch = line[l]); l++) { + if (g_ascii_isspace (ch)) { + /* wg-setconf strips all whitespace before parsing the content. That means, + * *[I nterface]" will be accepted. We do that too. */ + continue; + } + if (ch == '#') + break; + line[n++] = line[l]; + } + if (n == 0) + continue; + line[n] = '\0'; + } + + if (g_ascii_strcasecmp (line, "[Interface]") == 0) { + if (!_wg_complete_peer (&data_peers, + g_steal_pointer (¤t_peer), + current_peer_start_line_nr, + filename, + error)) + return FALSE; + line_context = LINE_CONTEXT_INTERFACE; + continue; + } + + if (g_ascii_strcasecmp (line, "[Peer]") == 0) { + if (!_wg_complete_peer (&data_peers, + g_steal_pointer (¤t_peer), + current_peer_start_line_nr, + filename, + error)) + return FALSE; + current_peer_start_line_nr = line_nr; + current_peer = nm_wireguard_peer_new (); + line_context = LINE_CONTEXT_PEER; + continue; + } + + if (line_context == LINE_CONTEXT_INTERFACE) { + + if (line_match (line, "Address", &matched_key, &value)) { + char *value_word; + + while (value_split_word (&value, &value_word)) { + GPtrArray **p_data_addr; + NMIPAddr addr_bin; + int addr_family; + int prefix_len; + + if (!nm_utils_parse_inaddr_prefix_bin (AF_UNSPEC, + value_word, + &addr_family, + &addr_bin, + &prefix_len)) + goto fail_invalid_value; + + p_data_addr = (addr_family == AF_INET) + ? &data_addr_v4 + : &data_addr_v6; + + if (!*p_data_addr) + *p_data_addr = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_ip_address_unref); + + g_ptr_array_add (*p_data_addr, + nm_ip_address_new_binary (addr_family, + &addr_bin, + prefix_len == -1 + ? ((addr_family == AF_INET) ? 32 : 128) + : prefix_len, + NULL)); + } + continue; + } + + if (line_match (line, "MTU", &matched_key, &value)) { + i64 = _nm_utils_ascii_str_to_int64 (value, 0, 0, G_MAXUINT32, -1); + if (i64 == -1) + goto fail_invalid_value; + + /* wg-quick accepts the "MTU" value, but it also fetches routes to + * autodetect it. NetworkManager won't do that, we can only configure + * an explict MTU or no autodetection will be performed. */ + data_mtu = i64; + continue; + } + + if (line_match (line, "DNS", &matched_key, &value)) { + char *value_word; + + while (value_split_word (&value, &value_word)) { + char addr_s[NM_CONST_MAX (INET_ADDRSTRLEN, INET6_ADDRSTRLEN)]; + GPtrArray **p_data_dns; + NMIPAddr addr_bin; + int addr_family; + + if (!nm_utils_parse_inaddr_bin (AF_UNSPEC, + value_word, + &addr_family, + &addr_bin)) + goto fail_invalid_value; + + p_data_dns = (addr_family == AF_INET) + ? &data_dns_v4 + : &data_dns_v6; + if (!*p_data_dns) + *p_data_dns = g_ptr_array_new_with_free_func (g_free); + + inet_ntop (addr_family, &addr_bin, addr_s, sizeof (addr_s)); + g_ptr_array_add (*p_data_dns, g_strdup (addr_s)); + } + continue; + } + + if (line_match (line, "Table", &matched_key, &value)) { + + if (nm_streq (value, "auto")) + data_table = _TABLE_AUTO; + else if (nm_streq (value, "off")) + data_table = _TABLE_OFF; + else { + /* we don't support table names from /etc/iproute2/rt_tables + * But we accept hex like `ip route add` would. */ + i64 = _nm_utils_ascii_str_to_int64 (value, 0, 0, G_MAXINT32, -1); + if (i64 == -1) + goto fail_invalid_value; + data_table = i64; + } + continue; + } + + if ( line_match (line, "PreUp", &matched_key, &value) + || line_match (line, "PreDown", &matched_key, &value) + || line_match (line, "PostUp", &matched_key, &value) + || line_match (line, "PostDown", &matched_key, &value)) { + /* we don't run any scripts. Silently ignore these paramters. */ + continue; + } + + if (line_match (line, "SaveConfig", &matched_key, &value)) { + /* we ignore the setting, but enforce that it's either true or false (like + * wg-quick. */ + if (!NM_IN_STRSET (value, "true", "false")) + goto fail_invalid_value; + continue; + } + + if (line_match (line, "ListenPort", &matched_key, &value)) { + /* we don't use getaddrinfo(), unlike `wg setconf`. Just interpret + * the port as plain decimal number. */ + i64 = _nm_utils_ascii_str_to_int64 (value, 10, 0, 0xFFFF, -1); + if (i64 == -1) + goto fail_invalid_value; + data_listen_port = i64; + continue; + } + + if (line_match (line, "FwMark", &matched_key, &value)) { + if (nm_streq (value, "off")) + data_fwmark = 0; + else { + i64 = _nm_utils_ascii_str_to_int64 (value, 0, 0, G_MAXINT32, -1); + if (i64 == -1) + goto fail_invalid_value; + data_fwmark = i64; + } + continue; + } + + if (line_match (line, "PrivateKey", &matched_key, &value)) { + if (!nm_utils_base64secret_decode (value, NM_WIREGUARD_PUBLIC_KEY_LEN, NULL)) + goto fail_invalid_secret; + data_private_key = value; + continue; + } + + goto fail_invalid_line; + } + + + if (line_context == LINE_CONTEXT_PEER) { + + if (line_match (line, "Endpoint", &matched_key, &value)) { + if (!nm_wireguard_peer_set_endpoint (current_peer, value, FALSE)) + goto fail_invalid_value; + continue; + } + + if (line_match (line, "PublicKey", &matched_key, &value)) { + if (!nm_wireguard_peer_set_public_key (current_peer, value, FALSE)) + goto fail_invalid_value; + continue; + } + + if (line_match (line, "AllowedIPs", &matched_key, &value)) { + char *value_word; + + while (value_split_word (&value, &value_word)) { + if (!nm_wireguard_peer_append_allowed_ip (current_peer, + value_word, + FALSE)) + goto fail_invalid_value; + } + continue; + } + + if (line_match (line, "PersistentKeepalive", &matched_key, &value)) { + if (nm_streq (value, "off")) + i64 = 0; + else { + i64 = _nm_utils_ascii_str_to_int64 (value, 10, 0, G_MAXUINT16, -1); + if (i64 == -1) + goto fail_invalid_value; + } + nm_wireguard_peer_set_persistent_keepalive (current_peer, i64); + continue; + } + + if (line_match (line, "PresharedKey", &matched_key, &value)) { + if (!nm_wireguard_peer_set_preshared_key (current_peer, value, FALSE)) + goto fail_invalid_secret; + nm_wireguard_peer_set_preshared_key_flags (current_peer, NM_SETTING_SECRET_FLAG_NONE); + continue; + } + + goto fail_invalid_line; + } + +fail_invalid_line: + nm_utils_error_set (error, NM_UTILS_ERROR_INVALID_ARGUMENT, + _("unrecognized line at %s:%zu"), + filename, line_nr); + return FALSE; +fail_invalid_value: + nm_utils_error_set (error, NM_UTILS_ERROR_INVALID_ARGUMENT, + _("invalid value for '%s' at %s:%zu"), + matched_key, filename, line_nr); + return FALSE; +fail_invalid_secret: + nm_utils_error_set (error, NM_UTILS_ERROR_INVALID_ARGUMENT, + _("invalid secret '%s' at %s:%zu"), + matched_key, filename, line_nr); + return FALSE; + } + + if (!_wg_complete_peer (&data_peers, + g_steal_pointer (¤t_peer), + current_peer_start_line_nr, + filename, + error)) + return FALSE; + + connection = nm_simple_connection_new (); + s_con = NM_SETTING_CONNECTION (nm_setting_connection_new ()); + nm_connection_add_setting (connection, NM_SETTING (s_con)); + s_ip4 = NM_SETTING_IP_CONFIG (nm_setting_ip4_config_new ()); + nm_connection_add_setting (connection, NM_SETTING (s_ip4)); + s_ip6 = NM_SETTING_IP_CONFIG (nm_setting_ip6_config_new ()); + nm_connection_add_setting (connection, NM_SETTING (s_ip6)); + s_wg = NM_SETTING_WIREGUARD (nm_setting_wireguard_new ()); + nm_connection_add_setting (connection, NM_SETTING (s_wg)); + + uuid = nm_utils_uuid_generate (); + + g_object_set (s_con, + NM_SETTING_CONNECTION_ID, + ifname, + NM_SETTING_CONNECTION_UUID, + uuid, + NM_SETTING_CONNECTION_TYPE, + NM_SETTING_WIREGUARD_SETTING_NAME, + NM_SETTING_CONNECTION_INTERFACE_NAME, + ifname, + NULL); + + g_object_set (s_wg, + NM_SETTING_WIREGUARD_PRIVATE_KEY, + data_private_key, + NM_SETTING_WIREGUARD_LISTEN_PORT, + data_listen_port, + NM_SETTING_WIREGUARD_FWMARK, + data_fwmark, + NM_SETTING_WIREGUARD_MTU, + data_mtu, + NULL); + + if (data_peers) { + for (i = 0; i < data_peers->len; i++) + nm_setting_wireguard_append_peer (s_wg, data_peers->pdata[i]); + } + + for (is_v4 = 0; is_v4 < 2; is_v4++) { + const char *method_disabled = is_v4 ? NM_SETTING_IP4_CONFIG_METHOD_DISABLED : NM_SETTING_IP6_CONFIG_METHOD_IGNORE; + const char *method_manual = is_v4 ? NM_SETTING_IP4_CONFIG_METHOD_MANUAL : NM_SETTING_IP6_CONFIG_METHOD_MANUAL; + NMSettingIPConfig *s_ip = is_v4 ? s_ip4 : s_ip6; + GPtrArray *data_dns = is_v4 ? data_dns_v4 : data_dns_v6; + GPtrArray *data_addr = is_v4 ? data_addr_v4 : data_addr_v6; + + if (data_dns && !data_addr) { + /* When specifying "DNS", we also require an "Address" for the same address + * family. That is because a NMSettingIPConfig cannot have @method_disabled + * and DNS settings at the same time. + * + * We don't have addresses. Silently ignore the DNS setting. */ + data_dns = NULL; + } + + g_object_set (s_ip, + NM_SETTING_IP_CONFIG_METHOD, + data_addr ? method_manual : method_disabled, + NULL); + + if (data_addr) { + for (i = 0; i < data_addr->len; i++) + nm_setting_ip_config_add_address (s_ip, data_addr->pdata[i]); + } + if (data_dns) { + for (i = 0; i < data_dns->len; i++) + nm_setting_ip_config_add_dns (s_ip, data_dns->pdata[i]); + } + + if (data_table == _TABLE_AUTO) { + /* in the "auto" setting, wg-quick adds peer-routes automatically to the main + * table. NetworkManager will do that too, but there are differences: + * + * - NetworkManager (contrary to wg-quick) does not check whether the peer-route is necessary. + * It will always add a route for each allowed-ips range, even if there is already another + * route that would ensure packets to the endpoint are routed via the WireGuard interface. + * If you don't want that, disable "wireguard.peer-routes", and add the necessary routes + * yourself to "ipv4.routes" and "ipv6.routes". + * + * - With "auto", wg-quick also configures policy routing to handle default-routes (/0) to + * avoid routing loops. That is not yet solved by NetworkManager, you need to configure + * that explicitly (for example, by adding a direct route to the gateway on the interface + * that has the default-route, or by using a script (possibly dispatcher script). + */ + } else if (data_table == _TABLE_OFF) { + if (is_v4) { + g_object_set (s_wg, + NM_SETTING_WIREGUARD_PEER_ROUTES, + FALSE, + NULL); + } + } else { + g_object_set (s_ip, + NM_SETTING_IP_CONFIG_ROUTE_TABLE, + (guint) data_table, + NULL); + } + } + + if (!nm_connection_normalize (connection, NULL, NULL, &local)) { + nm_utils_error_set (error, NM_UTILS_ERROR_INVALID_ARGUMENT, + _("Failed to create WireGuard connection: %s"), + local->message); + return FALSE; + } + + return g_steal_pointer (&connection); +} |