summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorThomas Haller <thaller@redhat.com>2019-04-30 17:48:46 +0200
committerThomas Haller <thaller@redhat.com>2019-07-29 18:00:18 +0200
commitdc27b510bb25581b0a5f6ef15536b2d74f3799e4 (patch)
tree575e17c9b3e5bd1b7224934cf5af8d5e82e2d835 /src
parentfa14429aa6f86d478cce81a3423f3040143e570f (diff)
downloadNetworkManager-th/wireguard-routing.tar.gz
wireguard: support configuring policy routing to avoid routing loopsth/wireguard-routing
For WireGuard (like for all IP-tunnels and IP-based VPNs), the IP addresses of the peers must be reached outside the tunnel/VPN itself. For VPN connections, NetworkManager usually adds a direct /32 route to the external VPN gateway to the underlying device. For WireGuard that is not done, because injecting a route to another device is ugly and error prone. Worse: WireGuard with automatic roaming and multiple peers makes this more complicated. This is commonly a problem when setting the default-route via the VPN, but there are also other subtle setups where special care must be taken to prevent such routing loops. WireGuard's wg-quick provides a simple, automatic solution by adding two policy routing rules and relying on the WireGuard packets having a fwmark set (see [1]). Let's also do that. Add new properties "wireguard.ip4-auto-default-route" and "wireguard.ip6-auto-default-route" to enable/disable this. Note that the default value lets NetworkManager automatically choose whether to enable it (depending on whether there are any peers that have a default route). This means, common scenarios should now work well without additional configuration. Note that this is also a change in behavior and upon package upgrade NetworkManager may start adding policy routes (if there are peers that have a default-route). This is a change in behavior, as the user already clearly had this setup working and configured some working solution already. The new automatism picks the rule priority automatically and adds the default-route to the routing table that has the same number as the fwmark. If any of this is unsuitable, then the user is free to disable this automatism. Note that since 1.18.0 NetworkManager supports policy routing (*). That means, what this automatism does can be also achieved via explicit configuration of the profile, which gives the user more flexibility to adjust all parameters explicitly). (*) but only since 1.20.0 NetworkManager supports the "suppress_prefixlength" rule attribute, which makes it impossible to configure exactly this rule-based solution with 1.18.0 NetworkManager. [1] https://www.wireguard.com/netns/#improved-rule-based-routing
Diffstat (limited to 'src')
-rw-r--r--src/devices/nm-device-wireguard.c424
-rw-r--r--src/devices/nm-device.c6
2 files changed, 425 insertions, 5 deletions
diff --git a/src/devices/nm-device-wireguard.c b/src/devices/nm-device-wireguard.c
index 714dbd4740..12fd9f3f5f 100644
--- a/src/devices/nm-device-wireguard.c
+++ b/src/devices/nm-device-wireguard.c
@@ -21,12 +21,16 @@
#include "nm-device-wireguard.h"
+#include <linux/rtnetlink.h>
+#include <linux/fib_rules.h>
+
#include "nm-setting-wireguard.h"
#include "nm-core-internal.h"
#include "nm-glib-aux/nm-secret-utils.h"
#include "nm-device-private.h"
#include "platform/nm-platform.h"
#include "platform/nmp-object.h"
+#include "platform/nmp-rules-manager.h"
#include "nm-device-factory.h"
#include "nm-active-connection.h"
#include "nm-act-request.h"
@@ -134,10 +138,21 @@ typedef struct {
GHashTable *peers;
gint64 resolve_next_try_at;
- guint resolve_next_try_id;
-
gint64 link_config_last_at;
+
+ guint resolve_next_try_id;
guint link_config_delayed_id;
+
+ guint32 auto_default_route_fwmark;
+
+ guint32 auto_default_route_priority;
+
+ bool auto_default_route_enabled_4:1;
+ bool auto_default_route_enabled_6:1;
+ bool auto_default_route_initialized:1;
+ bool auto_default_route_refresh:1;
+ bool auto_default_route_priority_initialized:1;
+
} NMDeviceWireGuardPrivate;
struct _NMDeviceWireGuard {
@@ -177,6 +192,372 @@ NM_UTILS_LOOKUP_STR_DEFINE_STATIC (_link_config_mode_to_string, LinkConfigMode,
/*****************************************************************************/
+static void
+_auto_default_route_get_enabled (NMSettingWireGuard *s_wg,
+ NMConnection *connection,
+ gboolean *out_enabled_v4,
+ gboolean *out_enabled_v6)
+{
+ NMTernary enabled_v4;
+ NMTernary enabled_v6;
+
+ enabled_v4 = nm_setting_wireguard_get_ip4_auto_default_route (s_wg);
+ enabled_v6 = nm_setting_wireguard_get_ip6_auto_default_route (s_wg);
+
+ if (enabled_v4 == NM_TERNARY_DEFAULT) {
+ if (nm_setting_ip_config_get_never_default (nm_connection_get_setting_ip_config (connection, AF_INET)))
+ enabled_v4 = FALSE;
+ }
+ if (enabled_v6 == NM_TERNARY_DEFAULT) {
+ if (nm_setting_ip_config_get_never_default (nm_connection_get_setting_ip_config (connection, AF_INET6)))
+ enabled_v6 = FALSE;
+ }
+
+ if ( enabled_v4 == NM_TERNARY_DEFAULT
+ || enabled_v6 == NM_TERNARY_DEFAULT) {
+ guint i, n_peers;
+
+ 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++) {
+ const char *aip;
+ gboolean valid;
+ int prefix;
+ int addr_family;
+
+ aip = nm_wireguard_peer_get_allowed_ip (peer, j, &valid);
+ if (!valid)
+ continue;
+ if (!nm_utils_parse_inaddr_prefix_bin (AF_UNSPEC,
+ aip,
+ &addr_family,
+ NULL,
+ &prefix))
+ continue;
+ if (prefix != 0)
+ continue;
+
+ if (addr_family == AF_INET) {
+ if (enabled_v4 == NM_TERNARY_DEFAULT) {
+ enabled_v4 = TRUE;
+ if (enabled_v6 != NM_TERNARY_DEFAULT)
+ goto done;
+ }
+ } else {
+ if (enabled_v6 == NM_TERNARY_DEFAULT) {
+ enabled_v6 = TRUE;
+ if (enabled_v4 != NM_TERNARY_DEFAULT)
+ goto done;
+ }
+ }
+ }
+ }
+done:
+ ;
+ }
+
+ *out_enabled_v4 = (enabled_v4 == TRUE);
+ *out_enabled_v6 = (enabled_v6 == TRUE);
+}
+
+static guint32
+_auto_default_route_find_unused_table (NMPlatform *platform)
+{
+ guint32 table;
+ int is_ipv4;
+
+ for (table = 51820; TRUE; table++) {
+ const NMDedupMultiHeadEntry *head_entry;
+ const guint32 table_coerced = nm_platform_route_table_coerce (table);
+ NMDedupMultiIter iter;
+ const NMPObject *plobj;
+
+ /* find a table/fwmark that is not yet in use. */
+
+ for (is_ipv4 = 0; is_ipv4 < 2; is_ipv4++) {
+ head_entry = nm_platform_lookup_object (platform,
+ is_ipv4
+ ? NMP_OBJECT_TYPE_IP4_ROUTE
+ : NMP_OBJECT_TYPE_IP6_ROUTE,
+ -1);
+ nmp_cache_iter_for_each (&iter, head_entry, &plobj) {
+ if (NMP_OBJECT_CAST_IP_ROUTE (plobj)->table_coerced == table_coerced)
+ goto try_next_table;
+ }
+ }
+
+ head_entry = nm_platform_lookup_object_by_addr_family (platform,
+ NMP_OBJECT_TYPE_ROUTING_RULE,
+ AF_UNSPEC);
+ nmp_cache_iter_for_each (&iter, head_entry, &plobj) {
+ const NMPlatformRoutingRule *rr = NMP_OBJECT_CAST_ROUTING_RULE (plobj);
+
+ if (rr->fwmark == table)
+ goto try_next_table;
+ }
+
+ head_entry = nm_platform_lookup_obj_type (platform, NMP_OBJECT_TYPE_LINK);
+ nmp_cache_iter_for_each (&iter, head_entry, &plobj) {
+ const NMPObject *lnk_wg;
+
+ if (plobj->link.type != NM_LINK_TYPE_WIREGUARD)
+ continue;
+
+ lnk_wg = plobj->_link.netlink.lnk;
+
+ if (!lnk_wg)
+ continue;
+
+ if (NMP_OBJECT_GET_TYPE (lnk_wg) != NMP_OBJECT_TYPE_LNK_WIREGUARD)
+ continue;
+
+ if (NMP_OBJECT_CAST_LNK_WIREGUARD (lnk_wg)->fwmark == table)
+ goto try_next_table;
+ }
+
+ return table;
+try_next_table:
+ ;
+ }
+}
+
+#define PRIO_WIDTH ((guint32) 2)
+
+static gboolean
+_auto_default_route_find_priority_exists (const NMDedupMultiHeadEntry *head_entry,
+ guint32 priority)
+{
+ NMDedupMultiIter iter;
+ const NMPObject *plobj;
+
+ nmp_cache_iter_for_each (&iter, head_entry, &plobj) {
+ const NMPlatformRoutingRule *rr = NMP_OBJECT_CAST_ROUTING_RULE (plobj);
+
+ /* we don't differenciate between IPv4 vs. IPv6. There should be no
+ * conflicting rules with the same priority. */
+ if ( rr->priority >= priority
+ && rr->priority < priority + PRIO_WIDTH)
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static guint32
+_auto_default_route_find_priority (NMPlatform *platform,
+ const char *uuid)
+{
+ const NMDedupMultiHeadEntry *head_entry;
+ guint64 rnd_seed;
+ guint32 RANGE_BASE = 100u / PRIO_WIDTH;
+ guint32 RANGE_LENGTH = ((32766u - PRIO_WIDTH) / PRIO_WIDTH) - RANGE_BASE;
+ guint32 PRIME_NUMBER = 1111567573u;
+ guint32 prio_candidate = 0;
+ guint32 i_step;
+ guint32 i;
+
+ /* For the auto-default-route policy routing rule we add 4 rules (2 Ipv4 and 2 IPv6).
+ * Hence, we choose a base priority for the first (of the two rules) and the second
+ * rule gets priortiy + 1.
+ * one base priority and then add the rules with priorities [priority, priority+3].
+ * For that, we pick a base-priority that does not yet exist.
+ * We also want to choose a priority that is individual for the profile.
+ * We do so by taking the UUID as seed and then sample the candidate priorities
+ * in a certain order. */
+
+ rnd_seed = c_siphash_hash ((const guint8 [16]) { 0xb9, 0x39, 0x8e, 0xed, 0x15, 0xb3, 0xd1, 0xc4, 0x5f, 0x45, 0x00, 0x4f, 0xec, 0xc2, 0x2b, 0x7e },
+ (const guint8 *) uuid,
+ uuid ? strlen (uuid) + 1u : 0u);
+
+ head_entry = nm_platform_lookup_object_by_addr_family (platform,
+ NMP_OBJECT_TYPE_ROUTING_RULE,
+ AF_UNSPEC);
+
+ i_step = ((guint32) rnd_seed) % RANGE_LENGTH;
+ for (i = 0; i < RANGE_LENGTH; i++) {
+
+ /* we sample the range in a stable, but somewhat arbitrary order to
+ * fine the priority. */
+ i_step = (i_step + PRIME_NUMBER) % RANGE_LENGTH;
+
+ prio_candidate = (RANGE_BASE + i_step) * PRIO_WIDTH;
+
+ if (!_auto_default_route_find_priority_exists (head_entry, prio_candidate))
+ return prio_candidate;
+ }
+
+ /* Couldn't find an unused one? Very odd, this really should not happen unless there
+ * are thousands of rules already. Just pick the last one we sampled. */
+ return prio_candidate;
+}
+
+static void
+_auto_default_route_init (NMDeviceWireGuard *self)
+{
+ NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self);
+ NMConnection *connection;
+ NMSettingWireGuard *s_wg;
+ gboolean enabled_v4;
+ gboolean enabled_v6;
+ gboolean refreshing_only;
+ guint32 old_fwmark;
+ char sbuf1[100];
+
+ if (G_LIKELY ( priv->auto_default_route_initialized
+ && !priv->auto_default_route_refresh))
+ return;
+
+ refreshing_only = priv->auto_default_route_initialized
+ && priv->auto_default_route_refresh;
+ priv->auto_default_route_refresh = FALSE;
+
+ connection = nm_device_get_applied_connection (NM_DEVICE (self));
+
+ s_wg = _nm_connection_get_setting (connection, NM_TYPE_SETTING_WIREGUARD);
+
+ old_fwmark = priv->auto_default_route_fwmark;
+
+ priv->auto_default_route_fwmark = nm_setting_wireguard_get_fwmark (s_wg);
+
+ _auto_default_route_get_enabled (s_wg,
+ connection,
+ &enabled_v4,
+ &enabled_v6);
+ priv->auto_default_route_enabled_4 = enabled_v4;
+ priv->auto_default_route_enabled_6 = enabled_v6;
+ priv->auto_default_route_initialized = TRUE;
+
+ if ( ( priv->auto_default_route_enabled_4
+ || priv->auto_default_route_enabled_6)
+ && priv->auto_default_route_fwmark == 0u) {
+ if (refreshing_only)
+ priv->auto_default_route_fwmark = old_fwmark;
+ else
+ priv->auto_default_route_fwmark = _auto_default_route_find_unused_table (nm_device_get_platform (NM_DEVICE (self)));
+ }
+
+ _LOGT (LOGD_DEVICE,
+ "auto-default-route is %s for IPv4 and %s for IPv6%s",
+ priv->auto_default_route_enabled_4 ? "enabled" : "disabled",
+ priv->auto_default_route_enabled_6 ? "enabled" : "disabled",
+ priv->auto_default_route_enabled_4 || priv->auto_default_route_enabled_6
+ ? nm_sprintf_buf (sbuf1, " (fwmark 0x%x)", priv->auto_default_route_fwmark)
+ : "");
+}
+
+static GPtrArray *
+get_extra_rules (NMDevice *device)
+{
+ NMDeviceWireGuard *self = NM_DEVICE_WIREGUARD (device);
+ NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self);
+ gs_unref_ptrarray GPtrArray *extra_rules = NULL;
+ guint32 priority = 0;
+ int is_ipv4;
+ NMConnection *connection;
+
+ _auto_default_route_init (self);
+
+ connection = nm_device_get_applied_connection (device);
+ if (!connection)
+ return NULL;
+
+ for (is_ipv4 = 0; is_ipv4 < 2; is_ipv4++) {
+ NMSettingIPConfig *s_ip;
+ int addr_family = is_ipv4 ? AF_INET : AF_INET6;
+ guint32 table_main;
+ guint32 fwmark;
+
+ if (is_ipv4) {
+ if (!priv->auto_default_route_enabled_4)
+ continue;
+ } else {
+ if (!priv->auto_default_route_enabled_6)
+ continue;
+ }
+
+ if (!extra_rules) {
+ if (priv->auto_default_route_priority_initialized)
+ priority = priv->auto_default_route_priority;
+ else {
+ priority = _auto_default_route_find_priority (nm_device_get_platform (device),
+ nm_connection_get_uuid (connection));
+ priv->auto_default_route_priority = priority;
+ priv->auto_default_route_priority_initialized = TRUE;
+ }
+ extra_rules = g_ptr_array_new_with_free_func ((GDestroyNotify) nmp_object_unref);
+ }
+
+ s_ip = nm_connection_get_setting_ip_config (connection, addr_family);
+ table_main = nm_setting_ip_config_get_route_table (s_ip);
+ if (table_main == 0)
+ table_main = RT_TABLE_MAIN;
+
+ fwmark = priv->auto_default_route_fwmark;
+
+ G_STATIC_ASSERT_EXPR (PRIO_WIDTH == 2);
+
+ g_ptr_array_add (extra_rules,
+ nmp_object_new (NMP_OBJECT_TYPE_ROUTING_RULE,
+ &((const NMPlatformRoutingRule) {
+ .priority = priority,
+ .addr_family = addr_family,
+ .action = FR_ACT_TO_TBL,
+ .table = table_main,
+ .suppress_prefixlen_inverse = ~((guint32) 0u),
+ })));
+
+ g_ptr_array_add (extra_rules,
+ nmp_object_new (NMP_OBJECT_TYPE_ROUTING_RULE,
+ &((const NMPlatformRoutingRule) {
+ .priority = priority + 1u,
+ .addr_family = addr_family,
+ .action = FR_ACT_TO_TBL,
+ .table = fwmark,
+ .flags = FIB_RULE_INVERT,
+ .fwmark = fwmark,
+ .fwmask = 0xFFFFFFFFu,
+ })));
+ }
+
+ return g_steal_pointer (&extra_rules);
+}
+
+static guint32
+coerce_route_table (NMDevice *device,
+ int addr_family,
+ guint32 route_table,
+ gboolean is_user_config)
+{
+ NMDeviceWireGuard *self = NM_DEVICE_WIREGUARD (device);
+ NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self);
+ gboolean auto_default_route_enabled;
+
+ if (route_table != 0u)
+ return route_table;
+
+ _auto_default_route_init (self);
+
+ auto_default_route_enabled = (addr_family == AF_INET)
+ ? priv->auto_default_route_enabled_4
+ : priv->auto_default_route_enabled_6;
+
+ if (auto_default_route_enabled) {
+ /* we need to enable full-sync mode of all routing tables. */
+ _LOGT (LOGD_DEVICE, "coerce ipv%c.route-table setting to \"main\" (table 254) as we enable auto-default-route handling",
+ nm_utils_addr_family_to_char (addr_family));
+ return RT_TABLE_MAIN;
+ }
+
+ return 0;
+}
+
+/*****************************************************************************/
+
static gboolean
_peer_data_equal (gconstpointer ptr_a, gconstpointer ptr_b)
{
@@ -1084,6 +1465,8 @@ link_config (NMDeviceWireGuard *self,
_LOGT (LOGD_DEVICE, "wireguard link config (%s, %s)...",
reason, _link_config_mode_to_string (config_mode));
+ _auto_default_route_init (self);
+
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);
@@ -1130,7 +1513,7 @@ link_config (NMDeviceWireGuard *self,
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_lnk.fwmark = priv->auto_default_route_fwmark;
wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_HAS_FWMARK;
if (nm_utils_base64secret_decode (nm_setting_wireguard_get_private_key (s_wg),
@@ -1256,6 +1639,7 @@ static NMIPConfig *
_get_dev2_ip_config (NMDeviceWireGuard *self,
int addr_family)
{
+ NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self);
gs_unref_object NMIPConfig *ip_config = NULL;
NMConnection *connection;
NMSettingWireGuard *s_wg;
@@ -1264,6 +1648,9 @@ _get_dev2_ip_config (NMDeviceWireGuard *self,
int ip_ifindex;
guint32 route_metric;
guint32 route_table_coerced;
+ gboolean auto_default_route_enabled;
+
+ _auto_default_route_init (self);
connection = nm_device_get_applied_connection (NM_DEVICE (self));
@@ -1303,6 +1690,10 @@ _get_dev2_ip_config (NMDeviceWireGuard *self,
route_table_coerced = nm_platform_route_table_coerce (nm_device_get_route_table (NM_DEVICE (self), addr_family));
+ auto_default_route_enabled = (addr_family == AF_INET)
+ ? priv->auto_default_route_enabled_4
+ : priv->auto_default_route_enabled_6;
+
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);
@@ -1316,6 +1707,7 @@ _get_dev2_ip_config (NMDeviceWireGuard *self,
const char *aip;
gboolean valid;
int prefix;
+ guint32 rtable_coerced;
aip = nm_wireguard_peer_get_allowed_ip (peer, j, &valid);
@@ -1335,13 +1727,24 @@ _get_dev2_ip_config (NMDeviceWireGuard *self,
nm_utils_ipx_address_clear_host_address (addr_family, &addrbin, NULL, prefix);
+ rtable_coerced = route_table_coerced;
+
+ if ( prefix == 0
+ && auto_default_route_enabled) {
+ /* In auto-default-route mode, we place the default route in a table that
+ * has the same number as the fwmark. wg-quick does that too. If you don't
+ * like that, configure the rules and the default-route explicitly in the
+ * connection profile. */
+ rtable_coerced = nm_platform_route_table_coerce (priv->auto_default_route_fwmark);
+ }
+
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,
+ .table_coerced = rtable_coerced,
.metric = route_metric,
};
} else {
@@ -1350,7 +1753,7 @@ _get_dev2_ip_config (NMDeviceWireGuard *self,
.plen = prefix,
.ifindex = ip_ifindex,
.rt_source = NM_IP_CONFIG_SOURCE_USER,
- .table_coerced = route_table_coerced,
+ .table_coerced = rtable_coerced,
.metric = route_metric,
};
}
@@ -1411,7 +1814,11 @@ _device_cleanup (NMDeviceWireGuard *self)
NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self);
_peers_remove_all (priv);
+
_secrets_cancel (self);
+
+ priv->auto_default_route_initialized = FALSE;
+ priv->auto_default_route_priority_initialized = FALSE;
}
static void
@@ -1444,6 +1851,8 @@ can_reapply_change (NMDevice *device,
NM_SETTING_WIREGUARD_SETTING_NAME,
error,
NM_SETTING_WIREGUARD_FWMARK,
+ NM_SETTING_WIREGUARD_IP4_AUTO_DEFAULT_ROUTE,
+ NM_SETTING_WIREGUARD_IP6_AUTO_DEFAULT_ROUTE,
NM_SETTING_WIREGUARD_LISTEN_PORT,
NM_SETTING_WIREGUARD_PEERS,
NM_SETTING_WIREGUARD_PEER_ROUTES,
@@ -1465,9 +1874,12 @@ reapply_connection (NMDevice *device,
NMConnection *con_new)
{
NMDeviceWireGuard *self = NM_DEVICE_WIREGUARD (device);
+ NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE (self);
gs_unref_object NMIPConfig *ip4_config = NULL;
gs_unref_object NMIPConfig *ip6_config = NULL;
+ priv->auto_default_route_refresh = TRUE;
+
ip4_config = _get_dev2_ip_config (self, AF_INET);
ip6_config = _get_dev2_ip_config (self, AF_INET6);
@@ -1634,6 +2046,8 @@ nm_device_wireguard_class_init (NMDeviceWireGuardClass *klass)
device_class->can_reapply_change = can_reapply_change;
device_class->reapply_connection = reapply_connection;
device_class->get_configured_mtu = get_configured_mtu;
+ device_class->get_extra_rules = get_extra_rules;
+ device_class->coerce_route_table = coerce_route_table;
obj_properties[PROP_PUBLIC_KEY] =
g_param_spec_variant (NM_DEVICE_WIREGUARD_PUBLIC_KEY,
diff --git a/src/devices/nm-device.c b/src/devices/nm-device.c
index 01e00a7107..feb5110d30 100644
--- a/src/devices/nm-device.c
+++ b/src/devices/nm-device.c
@@ -11567,6 +11567,12 @@ check_and_reapply_connection (NMDevice *self,
/**************************************************************************
* Reapply changes
+ *
+ * Note that reapply_connection() is called as very first. This is for example
+ * important for NMDeviceWireGuard, which implements coerce_route_table()
+ * and get_extra_rules().
+ * That is because NMDeviceWireGuard caches settings, so during reapply that
+ * cache must be updated *first*.
*************************************************************************/
klass->reapply_connection (self, con_old, con_new);