summary refs log tree commit diff
path: root/src/devices/nm-device-wireguard.c
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2019-03-26 23:25:23 +0100
committerMichael Biebl <biebl@debian.org>2019-03-26 23:25:23 +0100
commit9a6dcbf895f9da01768e64b73cec88c16157d91e (patch)
treea359958930d731e9f1b59344642e10754419fe84 /src/devices/nm-device-wireguard.c
parent964ae8cc391520440cf5aa13e2b9cc34850ea6c2 (diff)
New upstream version 1.16.0 upstream/1.16.0
Diffstat (limited to 'src/devices/nm-device-wireguard.c')
-rw-r--r--src/devices/nm-device-wireguard.c1501
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;
 )