diff options
Diffstat (limited to 'src/devices/nm-device-wireguard.c')
| -rw-r--r-- | src/devices/nm-device-wireguard.c | 1501 |
1 files changed, 1485 insertions, 16 deletions
diff --git a/src/devices/nm-device-wireguard.c b/src/devices/nm-device-wireguard.c index 62ec0274..a9eb1ab4 100644 --- a/src/devices/nm-device-wireguard.c +++ b/src/devices/nm-device-wireguard.c @@ -21,24 +21,128 @@ #include "nm-device-wireguard.h" +#include "nm-setting-wireguard.h" +#include "nm-core-internal.h" +#include "nm-utils/nm-secret-utils.h" #include "nm-device-private.h" #include "platform/nm-platform.h" +#include "platform/nmp-object.h" #include "nm-device-factory.h" +#include "nm-active-connection.h" +#include "nm-act-request.h" +#include "dns/nm-dns-manager.h" #include "nm-device-logging.h" _LOG_DECLARE_SELF(NMDeviceWireGuard); /*****************************************************************************/ +/* TODO: activate profile with peer preshared-key-flags=2. On first activation, the secret is + * requested (good). Enter it and connect. Reactivate the profile, now there is no password + * prompt, as the secret is cached (good??). */ + +/* TODO: unlike for other VPNs, we don't inject a direct route to the peers. That means, + * you might get a routing sceneraio where the peer (VPN server) is reachable via the VPN. + * How we handle adding routes to external gateway for other peers, has severe issues + * as well. We may use policy-routing like wg-quick does. See also disussions at + * https://www.wireguard.com/netns/#improving-the-classic-solutions */ + +/* TODO: honor the TTL of DNS to determine when to retry resolving endpoints. */ + +/* TODO: when we get multiple IP addresses when resolving a peer endpoint. We currently + * just take the first from GAI. We should only accept AAAA/IPv6 if we also have a suitable + * IPv6 address. The problem is, that we have to recheck that when IP addressing on other + * interfaces changes. This makes it almost too cumbersome to implement. */ + +/*****************************************************************************/ + +G_STATIC_ASSERT (NM_WIREGUARD_PUBLIC_KEY_LEN == NMP_WIREGUARD_PUBLIC_KEY_LEN); +G_STATIC_ASSERT (NM_WIREGUARD_SYMMETRIC_KEY_LEN == NMP_WIREGUARD_SYMMETRIC_KEY_LEN); + +/*****************************************************************************/ + +#define LINK_CONFIG_RATE_LIMIT_NSEC (50 * NM_UTILS_NS_PER_MSEC) + +/* a special @next_try_at_nsec timestamp indicating that we should try again as soon as possible. */ +#define NEXT_TRY_AT_NSEC_ASAP ((gint64) G_MAXINT64) + +/* a special @next_try_at_nsec timestamp that is + * - positive (indicating resolve-checks are enabled) + * - already in the past (we use the absolute timestamp of 1nsec for that). */ +#define NEXT_TRY_AT_NSEC_PAST ((gint64) 1) + +/* like %NEXT_TRY_AT_NSEC_ASAP, but used for indicating to retry ASAP for a @retry_in_msec value. + * That is a relative time duraction, contrary to @next_try_at_nsec which is an absolute + * timestamp. */ +#define RETRY_IN_MSEC_ASAP ((gint64) G_MAXINT64) + +#define RETRY_IN_MSEC_MAX ((gint64) (30 * 60 * 1000)) + +typedef enum { + LINK_CONFIG_MODE_FULL, + LINK_CONFIG_MODE_REAPPLY, + LINK_CONFIG_MODE_ASSUME, + LINK_CONFIG_MODE_ENDPOINTS, +} LinkConfigMode; + +typedef struct { + GCancellable *cancellable; + + NMSockAddrUnion sockaddr; + + /* the timestamp (in nm_utils_get_monotonic_timestamp_ns() scale) when we want + * to retry resolving the endpoint (again). + * + * It may be set to %NEXT_TRY_AT_NSEC_ASAP to indicate to re-resolve as soon as possible. + * + * A @sockaddr is either fixed or it has + * - @cancellable set to indicate an ongoing request + * - @next_try_at_nsec set to a positive value, indicating when + * we ought to retry. */ + gint64 next_try_at_nsec; + + guint resolv_fail_count; +} PeerEndpointResolveData; + +typedef struct { + NMWireGuardPeer *peer; + + NMDeviceWireGuard *self; + + CList lst_peers; + + PeerEndpointResolveData ep_resolv; + + /* dirty flag used during _peers_update_all(). */ + bool dirty_update_all:1; +} PeerData; + NM_GOBJECT_PROPERTIES_DEFINE (NMDeviceWireGuard, PROP_PUBLIC_KEY, PROP_LISTEN_PORT, PROP_FWMARK, ); +typedef struct { + + NMDnsManager *dns_manager; + + NMPlatformLnkWireGuard lnk_curr; + NMActRequestGetSecretsCallId *secrets_call_id; + + CList lst_peers_head; + GHashTable *peers; + + gint64 resolve_next_try_at; + guint resolve_next_try_id; + + gint64 link_config_last_at; + guint link_config_delayed_id; +} NMDeviceWireGuardPrivate; + struct _NMDeviceWireGuard { NMDevice parent; - NMPlatformLnkWireGuard props; + NMDeviceWireGuardPrivate _priv; }; struct _NMDeviceWireGuardClass { @@ -47,25 +151,710 @@ struct _NMDeviceWireGuardClass { G_DEFINE_TYPE (NMDeviceWireGuard, nm_device_wireguard, NM_TYPE_DEVICE) -/******************************************************************/ +#define NM_DEVICE_WIREGUARD_GET_PRIVATE(self) _NM_GET_PRIVATE (self, NMDeviceWireGuard, NM_IS_DEVICE_WIREGUARD, NMDevice) + +/*****************************************************************************/ + +static void _peers_resolve_start (NMDeviceWireGuard *self, + PeerData *peer_data); + +static void _peers_resolve_retry_reschedule (NMDeviceWireGuard *self, + gint64 new_next_try_at_nsec); + +static gboolean link_config_delayed_resolver_cb (gpointer user_data); + +static gboolean link_config_delayed_ratelimit_cb (gpointer user_data); + +/*****************************************************************************/ + +NM_UTILS_LOOKUP_STR_DEFINE_STATIC (_link_config_mode_to_string, LinkConfigMode, + NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT (NULL), + NM_UTILS_LOOKUP_ITEM (LINK_CONFIG_MODE_FULL, "full"), + NM_UTILS_LOOKUP_ITEM (LINK_CONFIG_MODE_REAPPLY, "reapply"), + NM_UTILS_LOOKUP_ITEM (LINK_CONFIG_MODE_ASSUME, "assume"), + NM_UTILS_LOOKUP_ITEM (LINK_CONFIG_MODE_ENDPOINTS, "endpoints"), +); + +/*****************************************************************************/ + +static gboolean +_peer_data_equal (gconstpointer ptr_a, gconstpointer ptr_b) +{ + const PeerData *peer_data_a = ptr_a; + const PeerData *peer_data_b = ptr_b; + + return nm_streq (nm_wireguard_peer_get_public_key (peer_data_a->peer), + nm_wireguard_peer_get_public_key (peer_data_b->peer)); +} + +static guint +_peer_data_hash (gconstpointer ptr) +{ + const PeerData *peer_data = ptr; + + return nm_hash_str (nm_wireguard_peer_get_public_key (peer_data->peer)); +} + +static PeerData * +_peers_find (NMDeviceWireGuardPrivate *priv, + NMWireGuardPeer *peer) +{ + nm_assert (peer); + + G_STATIC_ASSERT_EXPR (G_STRUCT_OFFSET (PeerData, peer) == 0); + + return g_hash_table_lookup (priv->peers, &peer); +} + +static void +_peers_remove (NMDeviceWireGuardPrivate *priv, + PeerData *peer_data) +{ + nm_assert (peer_data); + nm_assert (g_hash_table_lookup (priv->peers, peer_data) == peer_data); + + if (!g_hash_table_remove (priv->peers, peer_data)) + nm_assert_not_reached (); + + c_list_unlink_stale (&peer_data->lst_peers); + nm_wireguard_peer_unref (peer_data->peer); + nm_clear_g_cancellable (&peer_data->ep_resolv.cancellable); + g_slice_free (PeerData, peer_data); + + if (c_list_is_empty (&peer_data->lst_peers)) { + nm_clear_g_source (&priv->resolve_next_try_id); + nm_clear_g_source (&priv->link_config_delayed_id); + } +} + +static PeerData * +_peers_add (NMDeviceWireGuard *self, + NMWireGuardPeer *peer) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); + PeerData *peer_data; + + nm_assert (peer); + nm_assert (nm_wireguard_peer_is_sealed (peer)); + nm_assert (!_peers_find (priv, peer)); + + peer_data = g_slice_new (PeerData); + *peer_data = (PeerData) { + .self = self, + .peer = nm_wireguard_peer_ref (peer), + .ep_resolv = { + .sockaddr = NM_SOCK_ADDR_UNION_INIT_UNSPEC, + }, + }; + + c_list_link_tail (&priv->lst_peers_head, &peer_data->lst_peers); + if (!nm_g_hash_table_add (priv->peers, peer_data)) + nm_assert_not_reached (); + return peer_data; +} + +static gboolean +_peers_resolve_retry_timeout (gpointer user_data) +{ + NMDeviceWireGuard *self = user_data; + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); + PeerData *peer_data; + gint64 now; + gint64 next; + + priv->resolve_next_try_id = 0; + + _LOGT (LOGD_DEVICE, "wireguard-peers: rechecking peer endpoints..."); + + now = nm_utils_get_monotonic_timestamp_ns (); + next = G_MAXINT64; + c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) { + if (peer_data->ep_resolv.next_try_at_nsec <= 0) + continue; + + if (peer_data->ep_resolv.cancellable) { + /* we are currently resolving a name. We don't need the global + * watchdog to guard this peer. No need to adjust @next for + * this one, when the currently ongoing resolving completes, we + * may reschedule. Skip. */ + continue; + } + + if ( peer_data->ep_resolv.next_try_at_nsec == NEXT_TRY_AT_NSEC_ASAP + || now >= peer_data->ep_resolv.next_try_at_nsec) { + _peers_resolve_start (self, peer_data); + /* same here. Now we are resolving. We don't need the global + * watchdog. Skip w.r.t. finding @next. */ + continue; + } + + if (next > peer_data->ep_resolv.next_try_at_nsec) + next = peer_data->ep_resolv.next_try_at_nsec; + } + if (next < G_MAXINT64) + _peers_resolve_retry_reschedule (self, next); + + return G_SOURCE_REMOVE; +} + +static void +_peers_resolve_retry_reschedule (NMDeviceWireGuard *self, + gint64 new_next_try_at_nsec) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); + guint32 interval_ms; + gint64 now; + + nm_assert (new_next_try_at_nsec > 0); + nm_assert (new_next_try_at_nsec != NEXT_TRY_AT_NSEC_ASAP); + + if ( priv->resolve_next_try_id + && priv->resolve_next_try_at <= new_next_try_at_nsec) { + /* we already have an earlier timeout scheduled (possibly for + * another peer that expires sooner). Don't reschedule now. + * Even if the scheduled timeout expires too early, we will + * compute the right next-timeout and reschedule then. */ + return; + } + + now = nm_utils_get_monotonic_timestamp_ns (); + + /* schedule at most one day ahead. No problem if we expire earlier + * than expected. Also, rate-limit to 500 msec. */ + interval_ms = NM_CLAMP ((new_next_try_at_nsec - now) / NM_UTILS_NS_PER_MSEC, + (gint64) 500, + (gint64) (24*60*60*1000)); + + _LOGT (LOGD_DEVICE, "wireguard-peers: schedule rechecking peer endpoints in %u msec", + interval_ms); + + nm_clear_g_source (&priv->resolve_next_try_id); + priv->resolve_next_try_at = new_next_try_at_nsec; + priv->resolve_next_try_id = g_timeout_add (interval_ms, + _peers_resolve_retry_timeout, + self); +} + +static void +_peers_resolve_retry_reschedule_for_peer (NMDeviceWireGuard *self, + PeerData *peer_data, + gint64 retry_in_msec) +{ + nm_assert (retry_in_msec >= 0); + + if (retry_in_msec == RETRY_IN_MSEC_ASAP) { + _peers_resolve_start (self, peer_data); + return; + } + + peer_data->ep_resolv.next_try_at_nsec = nm_utils_get_monotonic_timestamp_ns () + + (retry_in_msec * NM_UTILS_NS_PER_MSEC); + _peers_resolve_retry_reschedule (self, peer_data->ep_resolv.next_try_at_nsec); +} + +static gint64 +_peers_retry_in_msec (PeerData *peer_data, + gboolean after_failure) +{ + if (peer_data->ep_resolv.next_try_at_nsec == NEXT_TRY_AT_NSEC_ASAP) { + peer_data->ep_resolv.resolv_fail_count = 0; + return RETRY_IN_MSEC_ASAP; + } + + if (after_failure) { + if (peer_data->ep_resolv.resolv_fail_count < G_MAXUINT) + peer_data->ep_resolv.resolv_fail_count++; + } else + peer_data->ep_resolv.resolv_fail_count = 0; + + if (!after_failure) + return RETRY_IN_MSEC_MAX; -static GVariant * -get_public_key_as_variant (const NMDeviceWireGuard *self) + if (peer_data->ep_resolv.resolv_fail_count > 20) + return RETRY_IN_MSEC_MAX; + + /* double the retry-time, starting with one second. */ + return NM_MIN (RETRY_IN_MSEC_MAX, + (1u << peer_data->ep_resolv.resolv_fail_count) * 500); +} + +static void +_peers_resolve_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + NMDeviceWireGuard *self; + PeerData *peer_data; + gs_free_error GError *resolv_error = NULL; + GList *list; + gboolean changed = FALSE; + NMSockAddrUnion sockaddr; + gint64 retry_in_msec; + char s_sockaddr[100]; + char s_retry[100]; + + list = g_resolver_lookup_by_name_finish (G_RESOLVER (source_object), res, &resolv_error); + + if (nm_utils_error_is_cancelled (resolv_error, FALSE)) + return; + + peer_data = user_data; + self = peer_data->self; + + g_clear_object (&peer_data->ep_resolv.cancellable); + + nm_assert ((!resolv_error) != (!list)); + +#define _retry_in_msec_to_string(retry_in_msec, s_retry) \ + ({ \ + gint64 _retry_in_msec = (retry_in_msec); \ + \ + _retry_in_msec == RETRY_IN_MSEC_ASAP \ + ? "right away" \ + : nm_sprintf_buf (s_retry, "in %"G_GINT64_FORMAT" msec", _retry_in_msec); \ + }) + + if ( resolv_error + && !g_error_matches (resolv_error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_NOT_FOUND)) { + retry_in_msec = _peers_retry_in_msec (peer_data, TRUE); + + _LOGT (LOGD_DEVICE, "wireguard-peer[%s]: failure to resolve endpoint \"%s\": %s (retry %s)", + nm_wireguard_peer_get_public_key (peer_data->peer), + nm_wireguard_peer_get_endpoint (peer_data->peer), + resolv_error->message, + _retry_in_msec_to_string (retry_in_msec, s_retry)); + + _peers_resolve_retry_reschedule_for_peer (self, peer_data, retry_in_msec); + return; + } + + sockaddr = (NMSockAddrUnion) NM_SOCK_ADDR_UNION_INIT_UNSPEC; + + if (!resolv_error) { + GList *iter; + + for (iter = list; iter; iter = iter->next) { + GInetAddress *a = iter->data; + GSocketFamily f = g_inet_address_get_family (a); + + if (f == G_SOCKET_FAMILY_IPV4) { + nm_assert (g_inet_address_get_native_size (a) == sizeof (struct in_addr)); + sockaddr.in = (struct sockaddr_in) { + .sin_family = AF_INET, + .sin_port = htons (nm_sock_addr_endpoint_get_port (_nm_wireguard_peer_get_endpoint (peer_data->peer))), + }; + memcpy (&sockaddr.in.sin_addr, g_inet_address_to_bytes (a), sizeof (struct in_addr)); + break; + } + if (f == G_SOCKET_FAMILY_IPV6) { + nm_assert (g_inet_address_get_native_size (a) == sizeof (struct in6_addr)); + sockaddr.in6 = (struct sockaddr_in6) { + .sin6_family = AF_INET6, + .sin6_port = htons (nm_sock_addr_endpoint_get_port (_nm_wireguard_peer_get_endpoint (peer_data->peer))), + .sin6_scope_id = 0, + .sin6_flowinfo = 0, + }; + memcpy (&sockaddr.in6.sin6_addr, g_inet_address_to_bytes (a), sizeof (struct in6_addr)); + break; + } + } + + g_list_free_full (list, g_object_unref); + } + + if (sockaddr.sa.sa_family == AF_UNSPEC) { + /* we failed to resolve the name. There is no need to reset the previous + * sockaddr. Either it was already AF_UNSPEC, or we had a good name + * from resolving before. In that case, we don't want to throw away + * a possibly good IP address, since WireGuard supports automatic roaming + * anyway. Either the IP address is still good (and we would wrongly + * reject it), or it isn't -- in which case it does not hurt much. */ + } else { + if (nm_sock_addr_union_cmp (&peer_data->ep_resolv.sockaddr, &sockaddr) != 0) + changed = TRUE; + peer_data->ep_resolv.sockaddr = sockaddr; + } + + if ( resolv_error + || peer_data->ep_resolv.sockaddr.sa.sa_family == AF_UNSPEC) { + /* while it technically did not fail, something is probably odd. Retry frequently to + * resolve the name, like we would do for normal failures. */ + retry_in_msec = _peers_retry_in_msec (peer_data, TRUE); + _LOGT (LOGD_DEVICE, "wireguard-peer[%s]: no %sresults for endpoint \"%s\" (retry %s)", + nm_wireguard_peer_get_public_key (peer_data->peer), + resolv_error ? "" : "suitable ", + nm_wireguard_peer_get_endpoint (peer_data->peer), + _retry_in_msec_to_string (retry_in_msec, s_retry)); + } else { + retry_in_msec = _peers_retry_in_msec (peer_data, FALSE); + _LOGT (LOGD_DEVICE, "wireguard-peer[%s]: endpoint \"%s\" resolved to %s (retry %s)", + nm_wireguard_peer_get_public_key (peer_data->peer), + nm_wireguard_peer_get_endpoint (peer_data->peer), + nm_sock_addr_union_to_string (&peer_data->ep_resolv.sockaddr, s_sockaddr, sizeof (s_sockaddr)), + _retry_in_msec_to_string (retry_in_msec, s_retry)); + } + + _peers_resolve_retry_reschedule_for_peer (self, peer_data, retry_in_msec); + + if (changed) { + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); + + /* schedule the job in the background, to give multiple resolve events time + * to complete. */ + nm_clear_g_source (&priv->link_config_delayed_id); + priv->link_config_delayed_id = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE + 1, + link_config_delayed_resolver_cb, + self, + NULL); + } +} + +static void +_peers_resolve_start (NMDeviceWireGuard *self, + PeerData *peer_data) +{ + gs_unref_object GResolver *resolver = NULL; + const char *host; + + resolver = g_resolver_get_default (); + + nm_assert (!peer_data->ep_resolv.cancellable); + + peer_data->ep_resolv.cancellable = g_cancellable_new (); + + /* set a special next-try timestamp. It is positive, and indicates + * that we are in the process of trying. + * This timestamp however already lies in the past, but that is correct, + * because we are currently in the process of trying. We will determine + * a next-try timestamp once the try completes. */ + peer_data->ep_resolv.next_try_at_nsec = NEXT_TRY_AT_NSEC_PAST; + + host = nm_sock_addr_endpoint_get_host (_nm_wireguard_peer_get_endpoint (peer_data->peer)); + + g_resolver_lookup_by_name_async (resolver, + host, + peer_data->ep_resolv.cancellable, + _peers_resolve_cb, + peer_data); + + _LOGT (LOGD_DEVICE, "wireguard-peer[%s]: resolving name \"%s\" for endpoint \"%s\"...", + nm_wireguard_peer_get_public_key (peer_data->peer), + host, + nm_wireguard_peer_get_endpoint (peer_data->peer)); +} + +static void +_peers_resolve_reresolve_all (NMDeviceWireGuard *self) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); + PeerData *peer_data; + + c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) { + if (peer_data->ep_resolv.cancellable) { + /* remember to retry when the currently ongoing request completes. */ + peer_data->ep_resolv.next_try_at_nsec = NEXT_TRY_AT_NSEC_ASAP; + } else if (peer_data->ep_resolv.next_try_at_nsec <= 0) { + /* this peer does not require resolving the name. Skip it. */ + } else { + /* we have a next-try scheduled. Restart right away. */ + peer_data->ep_resolv.resolv_fail_count = 0; + _peers_resolve_start (self, peer_data); + } + } +} + +static gboolean +_peers_update (NMDeviceWireGuard *self, + PeerData *peer_data, + NMWireGuardPeer *peer, + gboolean force_update) +{ + nm_auto_unref_wgpeer NMWireGuardPeer *old_peer = NULL; + NMSockAddrEndpoint *old_endpoint; + NMSockAddrEndpoint *endpoint; + gboolean endpoint_changed = FALSE; + gboolean changed; + NMSockAddrUnion sockaddr; + gboolean sockaddr_fixed; + char sockaddr_sbuf[100]; + + nm_assert (peer); + nm_assert (nm_wireguard_peer_is_sealed (peer)); + + if ( peer == peer_data->peer + && !force_update) + return FALSE; + + changed = (nm_wireguard_peer_cmp (peer, + peer_data->peer, + NM_SETTING_COMPARE_FLAG_EXACT) != 0); + + old_peer = peer_data->peer; + peer_data->peer = nm_wireguard_peer_ref (peer); + + old_endpoint = old_peer ? _nm_wireguard_peer_get_endpoint (old_peer) : NULL; + endpoint = peer ? _nm_wireguard_peer_get_endpoint (peer) : NULL; + + endpoint_changed = ( endpoint != old_endpoint + && ( !old_endpoint + || !endpoint + || !nm_streq (nm_sock_addr_endpoint_get_endpoint (old_endpoint), + nm_sock_addr_endpoint_get_endpoint (endpoint)))); + + if ( !force_update + && !endpoint_changed) { + /* nothing to do. */ + return changed; + } + + sockaddr = (NMSockAddrUnion) NM_SOCK_ADDR_UNION_INIT_UNSPEC; + sockaddr_fixed = TRUE; + if ( endpoint + && nm_sock_addr_endpoint_get_host (endpoint)) { + if (!nm_sock_addr_endpoint_get_fixed_sockaddr (endpoint, &sockaddr)) { + /* we have an endpoint, but it's not a static IP address. We need to resolve + * the names. */ + sockaddr_fixed = FALSE; + } + } + + if (nm_sock_addr_union_cmp (&peer_data->ep_resolv.sockaddr, &sockaddr) != 0) + changed = TRUE; + + nm_clear_g_cancellable (&peer_data->ep_resolv.cancellable); + + peer_data->ep_resolv = (PeerEndpointResolveData) { + .sockaddr = sockaddr, + .resolv_fail_count = 0, + .cancellable = NULL, + .next_try_at_nsec = 0, + }; + + if (!endpoint) { + _LOGT (LOGD_DEVICE, "wireguard-peer[%s]: no endpoint configured", + nm_wireguard_peer_get_public_key (peer_data->peer)); + } else if (!nm_sock_addr_endpoint_get_host (endpoint)) { + _LOGT (LOGD_DEVICE, "wireguard-peer[%s]: invalid endpoint \"%s\"", + nm_wireguard_peer_get_public_key (peer_data->peer), + nm_sock_addr_endpoint_get_endpoint (endpoint)); + } else if (sockaddr_fixed) { + _LOGT (LOGD_DEVICE, "wireguard-peer[%s]: fixed endpoint \"%s\" (%s)", + nm_wireguard_peer_get_public_key (peer_data->peer), + nm_sock_addr_endpoint_get_endpoint (endpoint), + nm_sock_addr_union_to_string (&peer_data->ep_resolv.sockaddr, sockaddr_sbuf, sizeof (sockaddr_sbuf))); + } else + _peers_resolve_start (self, peer_data); + + return changed; +} + +static void +_peers_remove_all (NMDeviceWireGuardPrivate *priv) { - return g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, - self->props.public_key, sizeof (self->props.public_key), 1); + PeerData *peer_data; + + while ((peer_data = c_list_first_entry (&priv->lst_peers_head, PeerData, lst_peers))) + _peers_remove (priv, peer_data); } static void +_peers_update_all (NMDeviceWireGuard *self, + NMSettingWireGuard *s_wg, + gboolean *out_peers_removed) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); + PeerData *peer_data_safe; + PeerData *peer_data; + guint i, n; + gboolean peers_removed = FALSE; + + c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) + peer_data->dirty_update_all = TRUE; + + n = nm_setting_wireguard_get_peers_len (s_wg); + for (i = 0; i < n; i++) { + NMWireGuardPeer *peer = nm_setting_wireguard_get_peer (s_wg, i); + gboolean added = FALSE; + + peer_data = _peers_find (priv, peer); + if (!peer_data) { + peer_data = _peers_add (self, peer); + added = TRUE; + } + _peers_update (self, peer_data, peer, added); + peer_data->dirty_update_all = FALSE; + } + + c_list_for_each_entry_safe (peer_data, peer_data_safe, &priv->lst_peers_head, lst_peers) { + if (peer_data->dirty_update_all) { + _peers_remove (priv, peer_data); + peers_removed = TRUE; + } + } + + NM_SET_OUT (out_peers_removed, peers_removed); +} + +static void +_peers_get_platform_list (NMDeviceWireGuardPrivate *priv, + LinkConfigMode config_mode, + NMPWireGuardPeer **out_peers, + NMPlatformWireGuardChangePeerFlags **out_peer_flags, + guint *out_len, + GArray **out_allowed_ips_data) +{ + gs_free NMPWireGuardPeer *plpeers = NULL; + gs_free NMPlatformWireGuardChangePeerFlags *plpeer_flags = NULL; + gs_unref_array GArray *allowed_ips = NULL; + PeerData *peer_data; + guint i_good; + guint n_aip; + guint i_aip; + guint len; + guint i; + + nm_assert (out_peers && !*out_peers); + nm_assert (out_peer_flags && !*out_peer_flags); + nm_assert (out_len && *out_len == 0); + nm_assert (out_allowed_ips_data && !*out_allowed_ips_data); + + len = g_hash_table_size (priv->peers); + + nm_assert (len == c_list_length (&priv->lst_peers_head)); + + if (len == 0) + return; + + plpeers = g_new0 (NMPWireGuardPeer, len); + plpeer_flags = g_new0 (NMPlatformWireGuardChangePeerFlags, len); + + i_good = 0; + c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) { + NMPlatformWireGuardChangePeerFlags *plf = &plpeer_flags[i_good]; + NMPWireGuardPeer *plp = &plpeers[i_good]; + NMSettingSecretFlags psk_secret_flags; + + if (!nm_utils_base64secret_decode (nm_wireguard_peer_get_public_key (peer_data->peer), + sizeof (plp->public_key), + plp->public_key)) + continue; + + *plf = NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_NONE; + + plp->persistent_keepalive_interval = nm_wireguard_peer_get_persistent_keepalive (peer_data->peer); + if (NM_IN_SET (config_mode, LINK_CONFIG_MODE_FULL, + LINK_CONFIG_MODE_REAPPLY)) + *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_KEEPALIVE_INTERVAL; + + /* if the peer has an endpoint but it is not yet resolved (not ready), + * we still configure it and leave the endpoint unspecified. Later, + * when we can resolve the endpoint, we will update. */ + plp->endpoint = peer_data->ep_resolv.sockaddr; + if (plp->endpoint.sa.sa_family == AF_UNSPEC) { + /* we don't actually ever clear endpoints, if we don't have better information. */ + } else + *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_ENDPOINT; + + if (NM_IN_SET (config_mode, LINK_CONFIG_MODE_FULL, + LINK_CONFIG_MODE_REAPPLY)) { + psk_secret_flags = nm_wireguard_peer_get_preshared_key_flags (peer_data->peer); + if (!NM_FLAGS_HAS (psk_secret_flags, NM_SETTING_SECRET_FLAG_NOT_REQUIRED)) { + if ( !nm_utils_base64secret_decode (nm_wireguard_peer_get_preshared_key (peer_data->peer), + sizeof (plp->preshared_key), + plp->preshared_key) + && config_mode == LINK_CONFIG_MODE_FULL) + goto skip; + } + *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_PRESHARED_KEY; + } + + if ( NM_IN_SET (config_mode, LINK_CONFIG_MODE_FULL, + LINK_CONFIG_MODE_REAPPLY) + && ((n_aip = nm_wireguard_peer_get_allowed_ips_len (peer_data->peer)) > 0)) { + if (!allowed_ips) + allowed_ips = g_array_new (FALSE, FALSE, sizeof (NMPWireGuardAllowedIP)); + + *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_ALLOWEDIPS + | NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_REPLACE_ALLOWEDIPS; + + plp->_construct_idx_start = allowed_ips->len; + for (i_aip = 0; i_aip < n_aip; i_aip++) { + const char *aip; + NMIPAddr addrbin = { }; + int addr_family; + gboolean valid; + int prefix; + + aip = nm_wireguard_peer_get_allowed_ip (peer_data->peer, i_aip, &valid); + if ( !valid + || !nm_utils_parse_inaddr_prefix_bin (AF_UNSPEC, + aip, + &addr_family, + &addrbin, + &prefix)) { + /* the address is really not expected to be invalid, because then + * the connection would not verify. Anyway, silently skip it. */ + continue; + } + + if (prefix == -1) + prefix = addr_family == AF_INET ? 32 : 128; + + g_array_append_val (allowed_ips, + ((NMPWireGuardAllowedIP) { + .family = addr_family, + .mask = prefix, + .addr = addrbin, + })); + } + plp->_construct_idx_end = allowed_ips->len; + } + + i_good++; + continue; + +skip: + memset (plp, 0, sizeof (*plp)); + } + + if (i_good == 0) + return; + + for (i = 0; i < i_good; i++) { + NMPWireGuardPeer *plp = &plpeers[i]; + guint l; + + if (plp->_construct_idx_end == 0) { + nm_assert (plp->_construct_idx_start == 0); + plp->allowed_ips = NULL; + plp->allowed_ips_len = 0; + } else { + nm_assert (plp->_construct_idx_start < plp->_construct_idx_end); + l = plp->_construct_idx_end - plp->_construct_idx_start; + plp->allowed_ips = &g_array_index (allowed_ips, NMPWireGuardAllowedIP, plp->_construct_idx_start); + plp->allowed_ips_len = l; + } + } + *out_peers = g_steal_pointer (&plpeers); + *out_peer_flags = g_steal_pointer (&plpeer_flags);; + *out_len = i_good; + *out_allowed_ips_data = g_steal_pointer (&allowed_ips); +} + +/*****************************************************************************/ + +static void update_properties (NMDevice *device) { NMDeviceWireGuard *self; + NMDeviceWireGuardPrivate *priv; const NMPlatformLink *plink; const NMPlatformLnkWireGuard *props = NULL; int ifindex; g_return_if_fail (NM_IS_DEVICE_WIREGUARD (device)); self = NM_DEVICE_WIREGUARD (device); + priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); ifindex = nm_device_get_ifindex (device); props = nm_platform_link_get_lnk_wireguard (nm_device_get_platform (device), ifindex, &plink); @@ -78,16 +867,16 @@ update_properties (NMDevice *device) #define CHECK_PROPERTY_CHANGED(field, prop) \ G_STMT_START { \ - if (self->props.field != props->field) { \ - self->props.field = props->field; \ + if (priv->lnk_curr.field != props->field) { \ + priv->lnk_curr.field = props->field; \ _notify (self, prop); \ } \ } G_STMT_END #define CHECK_PROPERTY_CHANGED_ARRAY(field, prop) \ G_STMT_START { \ - if (memcmp (&self->props.field, &props->field, sizeof (props->field)) != 0) { \ - memcpy (&self->props.field, &props->field, sizeof (props->field)); \ + if (memcmp (&priv->lnk_curr.field, &props->field, sizeof (priv->lnk_curr.field)) != 0) { \ + memcpy (&priv->lnk_curr.field, &props->field, sizeof (priv->lnk_curr.field)); \ _notify (self, prop); \ } \ } G_STMT_END @@ -107,24 +896,655 @@ link_changed (NMDevice *device, update_properties (device); } +static NMDeviceCapabilities +get_generic_capabilities (NMDevice *dev) +{ + return NM_DEVICE_CAP_IS_SOFTWARE; +} + +/*****************************************************************************/ + +static gboolean +create_and_realize (NMDevice *device, + NMConnection *connection, + NMDevice *parent, + const NMPlatformLink **out_plink, + GError **error) +{ + const char *iface = nm_device_get_iface (device); + int r; + + g_return_val_if_fail (iface, FALSE); + + r = nm_platform_link_wireguard_add (nm_device_get_platform (device), iface, out_plink); + if (r < 0) { + g_set_error (error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create WireGuard interface '%s' for '%s': %s", + iface, + nm_connection_get_id (connection), + nm_strerror (r)); + return FALSE; + } + + return TRUE; +} + +/*****************************************************************************/ + +static void +_secrets_cancel (NMDeviceWireGuard *self) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); + + if (priv->secrets_call_id) + nm_act_request_cancel_secrets (NULL, priv->secrets_call_id); + nm_assert (!priv->secrets_call_id); +} + +static void +_secrets_cb (NMActRequest *req, + NMActRequestGetSecretsCallId *call_id, + NMSettingsConnection *connection, + GError *error, + gpointer user_data) +{ + NMDeviceWireGuard *self = NM_DEVICE_WIREGUARD (user_data); + NMDevice *device = NM_DEVICE (self); + NMDeviceWireGuardPrivate *priv; + + g_return_if_fail (NM_IS_DEVICE_WIREGUARD (self)); + g_return_if_fail (NM_IS_ACT_REQUEST (req)); + + priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); + + g_return_if_fail (priv->secrets_call_id == call_id); + + priv->secrets_call_id = NULL; + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_return_if_fail (req == nm_device_get_act_request (device)); + g_return_if_fail (nm_device_get_state (device) == NM_DEVICE_STATE_NEED_AUTH); + g_return_if_fail (nm_act_request_get_settings_connection (req) == connection); + + if (error) { + _LOGW (LOGD_ETHER, "%s", error->message); + nm_device_state_changed (device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_NO_SECRETS); + } else + nm_device_activate_schedule_stage1_device_prepare (device); +} + +static void +_secrets_get_secrets (NMDeviceWireGuard *self, + const char *setting_name, + NMSecretAgentGetSecretsFlags flags, + const char *const*hints) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); + NMActRequest *req; + + _secrets_cancel (self); + + req = nm_device_get_act_request (NM_DEVICE (self)); + g_return_if_fail (NM_IS_ACT_REQUEST (req)); + + priv->secrets_call_id = nm_act_request_get_secrets (req, + TRUE, + setting_name, + flags, + hints, + _secrets_cb, + self); + g_return_if_fail (priv->secrets_call_id); +} + +static NMActStageReturn +_secrets_handle_auth_or_fail (NMDeviceWireGuard *self, + NMActRequest *req, + gboolean new_secrets) +{ + NMConnection *applied_connection; + const char *setting_name; + gs_unref_ptrarray GPtrArray *hints = NULL; + + if (!nm_device_auth_retries_try_next (NM_DEVICE (self))) + return NM_ACT_STAGE_RETURN_FAILURE; + + nm_device_state_changed (NM_DEVICE (self), NM_DEVICE_STATE_NEED_AUTH, NM_DEVICE_STATE_REASON_NONE); + + nm_active_connection_clear_secrets (NM_ACTIVE_CONNECTION (req)); + + applied_connection = nm_act_request_get_applied_connection (req); + setting_name = nm_connection_need_secrets (applied_connection, &hints); + if (!setting_name) { + _LOGI (LOGD_DEVICE, "Cleared secrets, but setting didn't need any secrets."); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + if (hints) + g_ptr_array_add (hints, NULL); + + _secrets_get_secrets (self, + setting_name, + NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION + | (new_secrets ? NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW : 0), + ( hints + ? (const char *const*) hints->pdata + : NULL)); + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +/*****************************************************************************/ + +static void +_dns_config_changed (NMDnsManager *dns_manager, NMDeviceWireGuard *self) +{ + /* when the DNS configuration changes, we re-resolve the peer addresses. + * + * Possibly, we should also do that when the default-route changes, but it's + * hard to figure out when that happens. */ + _peers_resolve_reresolve_all (self); +} + +/*****************************************************************************/ + +static NMActStageReturn +link_config (NMDeviceWireGuard *self, + const char *reason, + LinkConfigMode config_mode, + NMDeviceStateReason *out_failure_reason) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); + nm_auto_bzero_secret_ptr NMSecretPtr wg_lnk_clear_private_key = NM_SECRET_PTR_INIT (); + NMSettingWireGuard *s_wg; + NMConnection *connection; + NMActStageReturn ret; + gs_unref_array GArray *allowed_ips_data = NULL; + NMPlatformLnkWireGuard wg_lnk; + gs_free NMPWireGuardPeer *plpeers = NULL; + gs_free NMPlatformWireGuardChangePeerFlags *plpeer_flags = NULL; + guint plpeers_len = 0; + const char *setting_name; + gboolean peers_removed; + NMPlatformWireGuardChangeFlags wg_change_flags; + int ifindex; + int r; + + NM_SET_OUT (out_failure_reason, NM_DEVICE_STATE_REASON_NONE); + + connection = nm_device_get_applied_connection (NM_DEVICE (self)); + s_wg = NM_SETTING_WIREGUARD (nm_connection_get_setting (connection, NM_TYPE_SETTING_WIREGUARD)); + g_return_val_if_fail (s_wg, NM_ACT_STAGE_RETURN_FAILURE); + + priv->link_config_last_at = nm_utils_get_monotonic_timestamp_ns (); + + _LOGT (LOGD_DEVICE, "wireguard link config (%s, %s)...", + reason, _link_config_mode_to_string (config_mode)); + + if (!priv->dns_manager) { + priv->dns_manager = g_object_ref (nm_dns_manager_get ()); + g_signal_connect (priv->dns_manager, NM_DNS_MANAGER_CONFIG_CHANGED, G_CALLBACK (_dns_config_changed), self); + } + + if ( NM_IN_SET (config_mode, LINK_CONFIG_MODE_FULL) + && (setting_name = nm_connection_need_secrets (connection, NULL))) { + NMActRequest *req = nm_device_get_act_request (NM_DEVICE (self)); + + _LOGD (LOGD_DEVICE, + "Activation: connection '%s' has security, but secrets are required.", + nm_connection_get_id (connection)); + + ret = _secrets_handle_auth_or_fail (self, req, FALSE); + if (ret != NM_ACT_STAGE_RETURN_SUCCESS) { + if (ret != NM_ACT_STAGE_RETURN_POSTPONE) { + nm_assert (ret == NM_ACT_STAGE_RETURN_FAILURE); + NM_SET_OUT (out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS); + } + return ret; + } + } + + ifindex = nm_device_get_ip_ifindex (NM_DEVICE (self)); + if (ifindex <= 0) { + NM_SET_OUT (out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + _peers_update_all (self, s_wg, &peers_removed); + + wg_lnk = (NMPlatformLnkWireGuard) { }; -/******************************************************************/ + wg_change_flags = NM_PLATFORM_WIREGUARD_CHANGE_FLAG_NONE; + + if ( NM_IN_SET (config_mode, LINK_CONFIG_MODE_FULL) + || ( NM_IN_SET (config_mode, LINK_CONFIG_MODE_REAPPLY) + && peers_removed)) + wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_REPLACE_PEERS; + + if (NM_IN_SET (config_mode, LINK_CONFIG_MODE_FULL, + LINK_CONFIG_MODE_REAPPLY)) { + + wg_lnk.listen_port = nm_setting_wireguard_get_listen_port (s_wg), + wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_HAS_LISTEN_PORT; + + wg_lnk.fwmark = nm_setting_wireguard_get_fwmark (s_wg), + wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_HAS_FWMARK; + + if (nm_utils_base64secret_decode (nm_setting_wireguard_get_private_key (s_wg), + sizeof (wg_lnk.private_key), + wg_lnk.private_key)) { + wg_lnk_clear_private_key = NM_SECRET_PTR_ARRAY (wg_lnk.private_key); + wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_HAS_PRIVATE_KEY; + } else { + if (NM_IN_SET (config_mode, LINK_CONFIG_MODE_FULL)) { + _LOGD (LOGD_DEVICE, "the provided private-key is invalid"); + NM_SET_OUT (out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS); + return NM_ACT_STAGE_RETURN_FAILURE; + } + } + } + + _peers_get_platform_list (priv, + config_mode, + &plpeers, + &plpeer_flags, + &plpeers_len, + &allowed_ips_data); + + r = nm_platform_link_wireguard_change (nm_device_get_platform (NM_DEVICE (self)), + ifindex, + &wg_lnk, + plpeers, + plpeer_flags, + plpeers_len, + wg_change_flags); + + nm_explicit_bzero (plpeers, sizeof (plpeers) * plpeers_len); + + if (r < 0) { + NM_SET_OUT (out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static void +link_config_delayed (NMDeviceWireGuard *self, + const char *reason) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); + gint64 now; + + priv->link_config_delayed_id = 0; + + if (priv->link_config_last_at != 0) { + now = nm_utils_get_monotonic_timestamp_ns (); + if (now < priv->link_config_last_at + LINK_CONFIG_RATE_LIMIT_NSEC) { + /* we ratelimit calls to link_config(), because we call this whenever a resolver + * completes. */ + _LOGT (LOGD_DEVICE, "wireguard link config (%s) (postponed)", reason); + priv->link_config_delayed_id = g_timeout_add (NM_MAX ((priv->link_config_last_at + LINK_CONFIG_RATE_LIMIT_NSEC - now) / NM_UTILS_NS_PER_MSEC, + (gint64) 1), + link_config_delayed_ratelimit_cb, + self); + return; + } + } + + link_config (self, reason, LINK_CONFIG_MODE_ENDPOINTS, NULL); +} + +static gboolean +link_config_delayed_ratelimit_cb (gpointer user_data) +{ + link_config_delayed (user_data, "after-ratelimiting"); + return G_SOURCE_REMOVE; +} + +static gboolean +link_config_delayed_resolver_cb (gpointer user_data) +{ + link_config_delayed (user_data, "resolver-update"); + return G_SOURCE_REMOVE; +} + +static NMActStageReturn +act_stage2_config (NMDevice *device, + NMDeviceStateReason *out_failure_reason) +{ + NMDeviceSysIfaceState sys_iface_state; + NMDeviceStateReason failure_reason; + NMActStageReturn ret; + + sys_iface_state = nm_device_sys_iface_state_get (device); + + if (sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_EXTERNAL) { + NM_SET_OUT (out_failure_reason, NM_DEVICE_STATE_REASON_NONE); + return NM_ACT_STAGE_RETURN_SUCCESS; + } + + ret = link_config (NM_DEVICE_WIREGUARD (device), + "configure", + (sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_ASSUME) + ? LINK_CONFIG_MODE_ASSUME + : LINK_CONFIG_MODE_FULL, + &failure_reason); + + if (sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_ASSUME) { + /* this never fails. */ + NM_SET_OUT (out_failure_reason, NM_DEVICE_STATE_REASON_NONE); + return NM_ACT_STAGE_RETURN_SUCCESS; + } + + if (ret != NM_ACT_STAGE_RETURN_FAILURE) { + NM_SET_OUT (out_failure_reason, NM_DEVICE_STATE_REASON_NONE); + return ret; + } + + nm_device_state_changed (device, + NM_DEVICE_STATE_FAILED, + failure_reason); + NM_SET_OUT (out_failure_reason, failure_reason); + return NM_ACT_STAGE_RETURN_FAILURE; +} + +static NMIPConfig * +_get_dev2_ip_config (NMDeviceWireGuard *self, + int addr_family) +{ + gs_unref_object NMIPConfig *ip_config = NULL; + NMConnection *connection; + NMSettingWireGuard *s_wg; + guint n_peers; + guint i; + int ip_ifindex; + guint32 route_metric; + guint32 route_table_coerced; + + connection = nm_device_get_applied_connection (NM_DEVICE (self)); + + s_wg = NM_SETTING_WIREGUARD (nm_connection_get_setting (connection, NM_TYPE_SETTING_WIREGUARD)); + + /* Differences to `wg-quick`. + * + * `wg-quick` supports the "Table" setting with 3 modes: + * + * a1) "off": this is what we do with "peer-routes" disabled. + * + * a2) an explicit routing table. This is our behavior with "peer-routes" on. In this case + * we honor the "ipv4.route-table" and "ipv6.route-table" settings. One difference is that + * `wg-quick` would resolve table names from /etc/iproute2/rt_tables. Our connection profiles + * only contain table numbers, so that conversion from name to table must have happened + * before already. + * + * a3) "auto" (the default). In this case, `wg-quick` would only add the route to the + * main table, if the AllowedIP range is not yet reachable on the link. With "peer-routes" + * enabled, we don't check for that and always add the routes to the main-table + * (with 'ipv4.route-table' and 'ipv6.route-table' set to zero or RT_TABLE_MAIN (254)). + * + * Also, in "auto" mode, `wg-quick` would add special handling for /0 routes and pick + * an empty table to configure policy routing to avoid routing loops. This handling + * of routing-loops via policy routing is not yet done, and requires a separate solution + * from constructing the peer-routes here. + */ + if (!nm_setting_wireguard_get_peer_routes (s_wg)) + return NULL; + + ip_ifindex = nm_device_get_ip_ifindex (NM_DEVICE (self)); + + if (ip_ifindex <= 0) + return NULL; + + route_metric = nm_device_get_route_metric (NM_DEVICE (self), addr_family); + + route_table_coerced = nm_platform_route_table_coerce (nm_device_get_route_table (NM_DEVICE (self), addr_family, TRUE)); + + n_peers = nm_setting_wireguard_get_peers_len (s_wg); + for (i = 0; i < n_peers; i++) { + NMWireGuardPeer *peer = nm_setting_wireguard_get_peer (s_wg, i); + guint n_aips; + guint j; + + n_aips = nm_wireguard_peer_get_allowed_ips_len (peer); + for (j = 0; j < n_aips; j++) { + NMPlatformIPXRoute rt; + NMIPAddr addrbin; + const char *aip; + gboolean valid; + int prefix; + + aip = nm_wireguard_peer_get_allowed_ip (peer, j, &valid); + + if ( !valid + || !nm_utils_parse_inaddr_prefix_bin (addr_family, + aip, + NULL, + &addrbin, + &prefix)) + continue; + + if (prefix < 0) + prefix = (addr_family == AF_INET) ? 32 : 128; + + if (!ip_config) + ip_config = nm_device_ip_config_new (NM_DEVICE (self), addr_family); + + nm_utils_ipx_address_clear_host_address (addr_family, &addrbin, NULL, prefix); + + if (addr_family == AF_INET) { + rt.r4 = (NMPlatformIP4Route) { + .network = addrbin.addr4, + .plen = prefix, + .ifindex = ip_ifindex, + .rt_source = NM_IP_CONFIG_SOURCE_USER, + .table_coerced = route_table_coerced, + .metric = route_metric, + }; + } else { + rt.r6 = (NMPlatformIP6Route) { + .network = addrbin.addr6, + .plen = prefix, + .ifindex = ip_ifindex, + .rt_source = NM_IP_CONFIG_SOURCE_USER, + .table_coerced = route_table_coerced, + .metric = route_metric, + }; + } + + nm_ip_config_add_route (ip_config, &rt.rx, NULL); + } + } + + return g_steal_pointer (&ip_config); +} + +static NMActStageReturn +act_stage3_ip_config_start (NMDevice *device, + int addr_family, + gpointer *out_config, + NMDeviceStateReason *out_failure_reason) +{ + gs_unref_object NMIPConfig *ip_config = NULL; + + ip_config = _get_dev2_ip_config (NM_DEVICE_WIREGUARD (device), addr_family); + + nm_device_set_dev2_ip_config (device, addr_family, ip_config); + + return NM_DEVICE_CLASS (nm_device_wireguard_parent_class)->act_stage3_ip_config_start (device, addr_family, out_config, out_failure_reason); +} + +static guint32 +get_configured_mtu (NMDevice *device, NMDeviceMtuSource *out_source) +{ + /* When "MTU" for `wg-quick up` is unset, it calls `ip route get` for + * each configured endpoint, to determine the suitable MTU how to reach + * each endpoint. + * For `wg-quick` this works very well, because whenever the script runs it + * determines the best setting at that point in time. It's simply not concerned + * with what happens later (and it's not around anyway). + * + * NetworkManager sticks around, so the right MTU would need to be re-determined + * whenever anything relevant changes. Which basically means, to re-evaluate whenever + * something related to addresses or routing changes (which happens all the time). + * + * The correct MTU indeed depends on the MTU setting of other interfaces (or routes). + * But it's still odd, that activating/deactivating a seemingly unrelated interface + * would trigger an MTU change. It's odd to explain/document and odd to implemented + * -- despite this being the reality. + * + * For now, only support configuring an explicit MTU, or leave the setting untouched. + * The same limitation also applies to other "ip-tunnel" types, where we could use + * similar smarts for autodetecting the MTU. + */ + return nm_device_get_configured_mtu_from_connection (device, + NM_TYPE_SETTING_WIREGUARD, + out_source); +} + +static void +device_state_changed (NMDevice *device, + NMDeviceState new_state, + NMDeviceState old_state, + NMDeviceStateReason reason) +{ + NMDeviceWireGuardPrivate *priv; + + if (new_state <= NM_DEVICE_STATE_ACTIVATED) + return; + + priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (device); + + _peers_remove_all (priv); + _secrets_cancel (NM_DEVICE_WIREGUARD (device)); +} + +/*****************************************************************************/ + +static gboolean +can_reapply_change (NMDevice *device, + const char *setting_name, + NMSetting *s_old, + NMSetting *s_new, + GHashTable *diffs, + GError **error) +{ + if (nm_streq (setting_name, NM_SETTING_WIREGUARD_SETTING_NAME)) { + /* Most, but not all WireGuard settings can be reapplied. Whitelist. + * + * MTU cannot be reapplied. */ + return nm_device_hash_check_invalid_keys (diffs, + NM_SETTING_WIREGUARD_SETTING_NAME, + error, + NM_SETTING_WIREGUARD_FWMARK, + NM_SETTING_WIREGUARD_LISTEN_PORT, + NM_SETTING_WIREGUARD_PEERS, + NM_SETTING_WIREGUARD_PEER_ROUTES, + NM_SETTING_WIREGUARD_PRIVATE_KEY, + NM_SETTING_WIREGUARD_PRIVATE_KEY_FLAGS); + } + + return NM_DEVICE_CLASS (nm_device_wireguard_parent_class)->can_reapply_change (device, + setting_name, + s_old, + s_new, + diffs, + error); +} + +static void +reapply_connection (NMDevice *device, + NMConnection *con_old, + NMConnection *con_new) +{ + NMDeviceWireGuard *self = NM_DEVICE_WIREGUARD (device); + gs_unref_object NMIPConfig *ip4_config = NULL; + gs_unref_object NMIPConfig *ip6_config = NULL; + + ip4_config = _get_dev2_ip_config (self, AF_INET); + ip6_config = _get_dev2_ip_config (self, AF_INET6); + + nm_device_set_dev2_ip_config (device, AF_INET, ip4_config); + nm_device_set_dev2_ip_config (device, AF_INET6, ip6_config); + + NM_DEVICE_CLASS (nm_device_wireguard_parent_class)->reapply_connection (device, + con_old, + con_new); + + link_config (NM_DEVICE_WIREGUARD (device), + "reapply", + LINK_CONFIG_MODE_REAPPLY, + NULL); +} + +/*****************************************************************************/ + +static void +update_connection (NMDevice *device, NMConnection *connection) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (device); + NMSettingWireGuard *s_wg = NM_SETTING_WIREGUARD (nm_connection_get_setting (connection, NM_TYPE_SETTING_WIREGUARD)); + const NMPObject *obj_wg; + const NMPObjectLnkWireGuard *olnk_wg; + guint i; + + if (!s_wg) { + s_wg = NM_SETTING_WIREGUARD (nm_setting_wireguard_new ()); + nm_connection_add_setting (connection, NM_SETTING (s_wg)); + } + + g_object_set (s_wg, + NM_SETTING_WIREGUARD_FWMARK, + (guint) priv->lnk_curr.fwmark, + NM_SETTING_WIREGUARD_LISTEN_PORT, + (guint) priv->lnk_curr.listen_port, + NULL); + + obj_wg = NMP_OBJECT_UP_CAST (nm_platform_link_get_lnk_wireguard (nm_device_get_platform (device), + nm_device_get_ip_ifindex (device), + NULL)); + if (!obj_wg) + return; + + olnk_wg = &obj_wg->_lnk_wireguard; + + for (i = 0; i < olnk_wg->peers_len; i++) { + nm_auto_unref_wgpeer NMWireGuardPeer *peer = NULL; + const NMPWireGuardPeer *ppeer = &olnk_wg->peers[i]; + + peer = nm_wireguard_peer_new (); + + _nm_wireguard_peer_set_public_key_bin (peer, ppeer->public_key); + + nm_setting_wireguard_append_peer (s_wg, peer); + } +} + +/*****************************************************************************/ static void get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { NMDeviceWireGuard *self = NM_DEVICE_WIREGUARD (object); + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); switch (prop_id) { case PROP_PUBLIC_KEY: - g_value_take_variant (value, get_public_key_as_variant (self)); + g_value_take_variant (value, + g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, + priv->lnk_curr.public_key, + sizeof (priv->lnk_curr.public_key), + 1)); break; case PROP_LISTEN_PORT: - g_value_set_uint (value, self->props.listen_port); + g_value_set_uint (value, priv->lnk_curr.listen_port); break; case PROP_FWMARK: - g_value_set_uint (value, self->props.fwmark); + g_value_set_uint (value, priv->lnk_curr.fwmark); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); @@ -132,9 +1552,44 @@ get_property (GObject *object, guint prop_id, } } +/*****************************************************************************/ + static void nm_device_wireguard_init (NMDeviceWireGuard *self) { + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); + + c_list_init (&priv->lst_peers_head); + priv->peers = g_hash_table_new (_peer_data_hash, _peer_data_equal); +} + +static void +dispose (GObject *object) +{ + NMDeviceWireGuard *self = NM_DEVICE_WIREGUARD (object); + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); + + _secrets_cancel (self); + + _peers_remove_all (priv); + + G_OBJECT_CLASS (nm_device_wireguard_parent_class)->dispose (object); +} + +static void +finalize (GObject *object) +{ + NMDeviceWireGuard *self = NM_DEVICE_WIREGUARD (object); + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self); + + nm_explicit_bzero (priv->lnk_curr.private_key, sizeof (priv->lnk_curr.private_key)); + + if (priv->dns_manager) { + g_signal_handlers_disconnect_by_func (priv->dns_manager, _dns_config_changed, self); + g_object_unref (priv->dns_manager); + } + + G_OBJECT_CLASS (nm_device_wireguard_parent_class)->finalize (object); } static const NMDBusInterfaceInfoExtended interface_info_device_wireguard = { @@ -156,13 +1611,26 @@ nm_device_wireguard_class_init (NMDeviceWireGuardClass *klass) NMDeviceClass *device_class = NM_DEVICE_CLASS (klass); object_class->get_property = get_property; + object_class->dispose = dispose; + object_class->finalize = finalize; dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS (&interface_info_device_wireguard); - device_class->connection_type_supported = NULL; + device_class->connection_type_supported = NM_SETTING_WIREGUARD_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_WIREGUARD_SETTING_NAME; device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES (NM_LINK_TYPE_WIREGUARD); + device_class->state_changed = device_state_changed; + device_class->create_and_realize = create_and_realize; + device_class->act_stage2_config = act_stage2_config; + device_class->act_stage2_config_also_for_external_or_assume = TRUE; + device_class->act_stage3_ip_config_start = act_stage3_ip_config_start; + device_class->get_generic_capabilities = get_generic_capabilities; device_class->link_changed = link_changed; + device_class->update_connection = update_connection; + device_class->can_reapply_change = can_reapply_change; + device_class->reapply_connection = reapply_connection; + device_class->get_configured_mtu = get_configured_mtu; obj_properties[PROP_PUBLIC_KEY] = g_param_spec_variant (NM_DEVICE_WIREGUARD_PUBLIC_KEY, @@ -207,6 +1675,7 @@ create_device (NMDeviceFactory *factory, } NM_DEVICE_FACTORY_DEFINE_INTERNAL (WIREGUARD, WireGuard, wireguard, - NM_DEVICE_FACTORY_DECLARE_LINK_TYPES (NM_LINK_TYPE_WIREGUARD), + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES (NM_LINK_TYPE_WIREGUARD) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES (NM_SETTING_WIREGUARD_SETTING_NAME), factory_class->create_device = create_device; ) |