/* SPDX-License-Identifier: GPL-2.0-or-later */ /* * Copyright (C) 2013 - 2015 Red Hat, Inc. */ /** * SECTION:nm-vpn-helpers * @short_description: VPN-related utilities */ #include "nm-default.h" #include "nm-vpn-helpers.h" #include #include #include "nm-client-utils.h" #include "nm-utils.h" #include "nm-glib-aux/nm-io-utils.h" #include "nm-glib-aux/nm-secret-utils.h" /*****************************************************************************/ NMVpnEditorPlugin * nm_vpn_get_editor_plugin(const char *service_type, GError **error) { NMVpnEditorPlugin *plugin = NULL; NMVpnPluginInfo * plugin_info; gs_free_error GError *local = NULL; g_return_val_if_fail(service_type, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); plugin_info = nm_vpn_plugin_info_list_find_by_service(nm_vpn_get_plugin_infos(), service_type); if (!plugin_info) { g_set_error(error, NM_VPN_PLUGIN_ERROR, NM_VPN_PLUGIN_ERROR_FAILED, _("unknown VPN plugin \"%s\""), service_type); return NULL; } plugin = nm_vpn_plugin_info_get_editor_plugin(plugin_info); if (!plugin) plugin = nm_vpn_plugin_info_load_editor_plugin(plugin_info, &local); if (!plugin) { if (!nm_vpn_plugin_info_get_plugin(plugin_info) && nm_vpn_plugin_info_lookup_property(plugin_info, NM_VPN_PLUGIN_INFO_KF_GROUP_GNOME, "properties")) { g_set_error(error, NM_VPN_PLUGIN_ERROR, NM_VPN_PLUGIN_ERROR_FAILED, _("cannot load legacy-only VPN plugin \"%s\" for \"%s\""), nm_vpn_plugin_info_get_name(plugin_info), nm_vpn_plugin_info_get_filename(plugin_info)); } else if (g_error_matches(local, G_FILE_ERROR, G_FILE_ERROR_NOENT)) { g_set_error( error, NM_VPN_PLUGIN_ERROR, NM_VPN_PLUGIN_ERROR_FAILED, _("cannot load VPN plugin \"%s\" due to missing \"%s\". Missing client plugin?"), nm_vpn_plugin_info_get_name(plugin_info), nm_vpn_plugin_info_get_plugin(plugin_info)); } else { g_set_error(error, NM_VPN_PLUGIN_ERROR, NM_VPN_PLUGIN_ERROR_FAILED, _("failed to load VPN plugin \"%s\": %s"), nm_vpn_plugin_info_get_name(plugin_info), local->message); } return NULL; } return plugin; } GSList * nm_vpn_get_plugin_infos(void) { static bool plugins_loaded; static GSList *plugins = NULL; if (G_LIKELY(plugins_loaded)) return plugins; plugins_loaded = TRUE; plugins = nm_vpn_plugin_info_list_load(); return plugins; } gboolean nm_vpn_supports_ipv6(NMConnection *connection) { NMSettingVpn * s_vpn; const char * service_type; NMVpnEditorPlugin *plugin; guint32 capabilities; s_vpn = nm_connection_get_setting_vpn(connection); g_return_val_if_fail(s_vpn != NULL, FALSE); service_type = nm_setting_vpn_get_service_type(s_vpn); if (!service_type) return FALSE; plugin = nm_vpn_get_editor_plugin(service_type, NULL); if (!plugin) return FALSE; capabilities = nm_vpn_editor_plugin_get_capabilities(plugin); return NM_FLAGS_HAS(capabilities, NM_VPN_EDITOR_PLUGIN_CAPABILITY_IPV6); } const NmcVpnPasswordName * nm_vpn_get_secret_names(const char *service_type) { const char *type; if (!service_type) return NULL; if (!NM_STR_HAS_PREFIX(service_type, NM_DBUS_INTERFACE) || service_type[NM_STRLEN(NM_DBUS_INTERFACE)] != '.') { /* all our well-known, hard-coded vpn-types start with NM_DBUS_INTERFACE. */ return NULL; } type = service_type + (NM_STRLEN(NM_DBUS_INTERFACE) + 1); #define _VPN_PASSWORD_LIST(...) \ ({ \ static const NmcVpnPasswordName _arr[] = { \ __VA_ARGS__{0}, \ }; \ _arr; \ }) if (NM_IN_STRSET(type, "pptp", "iodine", "ssh", "l2tp", "fortisslvpn")) { return _VPN_PASSWORD_LIST({"password", N_("Password")}, ); } if (NM_IN_STRSET(type, "openvpn")) { return _VPN_PASSWORD_LIST({"password", N_("Password")}, {"cert-pass", N_("Certificate password")}, {"http-proxy-password", N_("HTTP proxy password")}, ); } if (NM_IN_STRSET(type, "vpnc")) { return _VPN_PASSWORD_LIST({"Xauth password", N_("Password")}, {"IPSec secret", N_("Group password")}, ); }; if (NM_IN_STRSET(type, "openswan", "libreswan", "strongswan")) { return _VPN_PASSWORD_LIST({"xauthpassword", N_("Password")}, {"pskvalue", N_("Group password")}, ); }; if (NM_IN_STRSET(type, "openconnect")) { return _VPN_PASSWORD_LIST({"gateway", N_("Gateway")}, {"cookie", N_("Cookie")}, {"gwcert", N_("Gateway certificate hash")}, ); }; return NULL; } static gboolean _extract_variable_value(char *line, const char *tag, char **value) { char *p1, *p2; if (!g_str_has_prefix(line, tag)) return FALSE; p1 = line + strlen(tag); p2 = line + strlen(line) - 1; if ((*p1 == '\'' || *p1 == '"') && (*p1 == *p2)) { p1++; *p2 = '\0'; } NM_SET_OUT(value, g_strdup(p1)); return TRUE; } gboolean nm_vpn_openconnect_authenticate_helper(const char *host, char ** cookie, char ** gateway, char ** gwcert, int * status, GError ** error) { gs_free char * output = NULL; gs_free const char **output_v = NULL; const char *const * iter; const char * path; const char *const DEFAULT_PATHS[] = { "/sbin/", "/usr/sbin/", "/usr/local/sbin/", "/bin/", "/usr/bin/", "/usr/local/bin/", NULL, }; path = nm_utils_file_search_in_paths("openconnect", "/usr/sbin/openconnect", DEFAULT_PATHS, G_FILE_TEST_IS_EXECUTABLE, NULL, NULL, error); if (!path) return FALSE; if (!g_spawn_sync(NULL, (char **) NM_MAKE_STRV(path, "--authenticate", host), NULL, G_SPAWN_SEARCH_PATH | G_SPAWN_CHILD_INHERITS_STDIN, NULL, NULL, &output, NULL, status, error)) return FALSE; /* Parse output and set cookie, gateway and gwcert * output example: * COOKIE='loremipsum' * HOST='1.2.3.4' * FINGERPRINT='sha1:32bac90cf09a722e10ecc1942c67fe2ac8c21e2e' */ output_v = nm_utils_strsplit_set_with_empty(output, "\r\n"); for (iter = output_v; iter && *iter; iter++) { char *s_mutable = (char *) *iter; _extract_variable_value(s_mutable, "COOKIE=", cookie); _extract_variable_value(s_mutable, "HOST=", gateway); _extract_variable_value(s_mutable, "FINGERPRINT=", gwcert); } 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_search = 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_ifname_valid(ifname, NMU_IFACE_KERNEL, NULL)) ifname_valid = TRUE; } } if (!ifname_valid) { nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, _("The name of the WireGuard config 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, NULL, error)) 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 explicit 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)) { GPtrArray **p_data_dns; NMIPAddr addr_bin; int addr_family; if (nm_utils_parse_inaddr_bin(AF_UNSPEC, value_word, &addr_family, &addr_bin)) { 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); g_ptr_array_add(*p_data_dns, nm_utils_inet_ntop_dup(addr_family, &addr_bin)); continue; } if (!data_dns_search) data_dns_search = g_ptr_array_new_with_free_func(g_free); g_ptr_array_add(data_dns_search, g_strdup(value_word)); } 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 parameters. */ 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_DISABLED; 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; GPtrArray * data_dns_search2 = data_dns_search; 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; data_dns_search2 = NULL; } g_object_set(s_ip, NM_SETTING_IP_CONFIG_METHOD, data_addr ? method_manual : method_disabled, NULL); /* For WireGuard profiles, always set dns-priority to a negative value, * so that DNS servers on other profiles get ignored. This is also what * wg-quick does, by calling `resolvconf -x`. */ g_object_set(s_ip, NM_SETTING_IP_CONFIG_DNS_PRIORITY, (int) -50, 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]); /* Of the wg-quick doesn't specify a search domain, assume the user * wants to use the domain server for all searches. */ if (!data_dns_search2) nm_setting_ip_config_add_dns_search(s_ip, "~"); } if (data_dns_search2) { for (i = 0; i < data_dns_search2->len; i++) nm_setting_ip_config_add_dns_search(s_ip, data_dns_search2->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. * The imported connection profile will have wireguard.ip4-auto-default-route and * wireguard.ip6-auto-default-route set to "default". It will thus configure wg-quick's * policy routing if the profile has any AllowedIPs ranges with /0. */ } 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); }