summary refs log tree commit diff
path: root/src/libnm-platform
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2022-08-16 18:24:19 +0200
committerMichael Biebl <biebl@debian.org>2022-08-16 18:24:19 +0200
commit0018d1f3cf71d680d7b6bceda55a5717244d8b26 (patch)
treea058f1d106d172d3354179437ef034c9355cdf9c /src/libnm-platform
parent6accbd3ec0e42d8633bbde4d47ed7bfe854e7e0b (diff)
New upstream version 1.39.90 upstream/1.39.90
Diffstat (limited to 'src/libnm-platform')
-rw-r--r--src/libnm-platform/meson.build2
-rw-r--r--src/libnm-platform/nm-linux-platform.c2328
-rw-r--r--src/libnm-platform/nm-netlink.c654
-rw-r--r--src/libnm-platform/nm-netlink.h170
-rw-r--r--src/libnm-platform/nm-platform.c696
-rw-r--r--src/libnm-platform/nm-platform.h230
-rw-r--r--src/libnm-platform/nmp-base.h3
-rw-r--r--src/libnm-platform/nmp-global-tracker.c1261
-rw-r--r--src/libnm-platform/nmp-global-tracker.h84
-rw-r--r--src/libnm-platform/nmp-object.c204
-rw-r--r--src/libnm-platform/nmp-object.h31
-rw-r--r--src/libnm-platform/nmp-route-manager.c844
-rw-r--r--src/libnm-platform/nmp-route-manager.h79
-rw-r--r--src/libnm-platform/tests/test-nm-platform.c5
-rw-r--r--src/libnm-platform/wifi/nm-wifi-utils-nl80211.c45
-rw-r--r--src/libnm-platform/wifi/nm-wifi-utils-nl80211.h2
-rw-r--r--src/libnm-platform/wifi/nm-wifi-utils.c6
-rw-r--r--src/libnm-platform/wifi/nm-wifi-utils.h5
-rw-r--r--src/libnm-platform/wpan/nm-wpan-utils.c40
-rw-r--r--src/libnm-platform/wpan/nm-wpan-utils.h5
20 files changed, 4432 insertions, 2262 deletions
diff --git a/src/libnm-platform/meson.build b/src/libnm-platform/meson.build
index d0a7c6c2..e273046f 100644
--- a/src/libnm-platform/meson.build
+++ b/src/libnm-platform/meson.build
@@ -7,9 +7,9 @@ libnm_platform = static_library(
     'nm-netlink.c',
     'nm-platform-utils.c',
     'nm-platform.c',
+    'nmp-global-tracker.c',
     'nmp-netns.c',
     'nmp-object.c',
-    'nmp-route-manager.c',
     'wifi/nm-wifi-utils-nl80211.c',
     'wifi/nm-wifi-utils.c',
     'wpan/nm-wpan-utils.c',
diff --git a/src/libnm-platform/nm-linux-platform.c b/src/libnm-platform/nm-linux-platform.c
index 40cb4396..cd685fe2 100644
--- a/src/libnm-platform/nm-linux-platform.c
+++ b/src/libnm-platform/nm-linux-platform.c
@@ -7,12 +7,13 @@
 
 #include "nm-linux-platform.h"
 
+#include "libnm-std-aux/nm-linux-compat.h"
+
 #include <arpa/inet.h>
 #include <dlfcn.h>
 #include <endian.h>
 #include <fcntl.h>
 #include <libudev.h>
-#include <net/ethernet.h>
 #include <linux/fib_rules.h>
 #include <linux/ip.h>
 #include <linux/if.h>
@@ -50,6 +51,14 @@
 
 /*****************************************************************************/
 
+G_STATIC_ASSERT(NM_MPTCP_PM_ADDR_FLAG_SIGNAL == MPTCP_PM_ADDR_FLAG_SIGNAL);
+G_STATIC_ASSERT(NM_MPTCP_PM_ADDR_FLAG_SUBFLOW == MPTCP_PM_ADDR_FLAG_SUBFLOW);
+G_STATIC_ASSERT(NM_MPTCP_PM_ADDR_FLAG_BACKUP == MPTCP_PM_ADDR_FLAG_BACKUP);
+G_STATIC_ASSERT(NM_MPTCP_PM_ADDR_FLAG_FULLMESH == MPTCP_PM_ADDR_FLAG_FULLMESH);
+G_STATIC_ASSERT(NM_MPTCP_PM_ADDR_FLAG_IMPLICIT == MPTCP_PM_ADDR_FLAG_IMPLICIT);
+
+/*****************************************************************************/
+
 /* re-implement <linux/tc_act/tc_defact.h> to build against kernel
  * headers that lack this. */
 
@@ -79,6 +88,25 @@ enum { TCA_DEF_UNSPEC, TCA_DEF_TM, TCA_DEF_PARMS, TCA_DEF_DATA, TCA_DEF_PAD, __T
 
 /*****************************************************************************/
 
+typedef enum _nm_packed {
+    _NMP_NETLINK_FIRST  = 0,
+    NMP_NETLINK_GENERIC = 0,
+    NMP_NETLINK_ROUTE,
+    _NMP_NETLINK_NUM,
+    _NMP_NETLINK_NONE = _NMP_NETLINK_NUM,
+} NMPNetlinkProtocol;
+
+#define nmp_netlink_protocol_check(netlink_protocol)                                       \
+    ({                                                                                     \
+        const NMPNetlinkProtocol _netlink_protocol_2 = (netlink_protocol);                 \
+                                                                                           \
+        nm_assert(NM_IN_SET(_netlink_protocol_2, NMP_NETLINK_ROUTE, NMP_NETLINK_GENERIC)); \
+                                                                                           \
+        _netlink_protocol_2;                                                               \
+    })
+
+/*****************************************************************************/
+
 #ifndef IFLA_PROMISCUITY
 #define IFLA_PROMISCUITY 30
 #endif
@@ -149,6 +177,12 @@ G_STATIC_ASSERT(RTA_MAX == (__RTA_MAX - 1));
 
 /*****************************************************************************/
 
+#define IFLA_BOND_PEER_NOTIF_DELAY 28
+
+#undef IFLA_BOND_MAX
+
+/*****************************************************************************/
+
 /* Appeared in the kernel prior to 3.13 dated 19 January, 2014 */
 #ifndef ARPHRD_6LOWPAN
 #define ARPHRD_6LOWPAN 825
@@ -279,6 +313,12 @@ struct _ifla_vf_vlan_info {
 
 /*****************************************************************************/
 
+typedef struct {
+    guint16 family_id;
+} GenlFamilyData;
+
+/*****************************************************************************/
+
 typedef enum {
     INFINIBAND_ACTION_CREATE_CHILD,
     INFINIBAND_ACTION_DELETE_CHILD,
@@ -302,15 +342,17 @@ typedef struct {
 typedef enum {
     _REFRESH_ALL_TYPE_FIRST = 0,
 
-    REFRESH_ALL_TYPE_LINKS             = 0,
-    REFRESH_ALL_TYPE_IP4_ADDRESSES     = 1,
-    REFRESH_ALL_TYPE_IP6_ADDRESSES     = 2,
-    REFRESH_ALL_TYPE_IP4_ROUTES        = 3,
-    REFRESH_ALL_TYPE_IP6_ROUTES        = 4,
-    REFRESH_ALL_TYPE_ROUTING_RULES_IP4 = 5,
-    REFRESH_ALL_TYPE_ROUTING_RULES_IP6 = 6,
-    REFRESH_ALL_TYPE_QDISCS            = 7,
-    REFRESH_ALL_TYPE_TFILTERS          = 8,
+    REFRESH_ALL_TYPE_RTNL_LINKS             = 0,
+    REFRESH_ALL_TYPE_RTNL_IP4_ADDRESSES     = 1,
+    REFRESH_ALL_TYPE_RTNL_IP6_ADDRESSES     = 2,
+    REFRESH_ALL_TYPE_RTNL_IP4_ROUTES        = 3,
+    REFRESH_ALL_TYPE_RTNL_IP6_ROUTES        = 4,
+    REFRESH_ALL_TYPE_RTNL_ROUTING_RULES_IP4 = 5,
+    REFRESH_ALL_TYPE_RTNL_ROUTING_RULES_IP6 = 6,
+    REFRESH_ALL_TYPE_RTNL_QDISCS            = 7,
+    REFRESH_ALL_TYPE_RTNL_TFILTERS          = 8,
+
+    REFRESH_ALL_TYPE_GENL_FAMILIES = 9,
 
     _REFRESH_ALL_TYPE_NUM,
 } RefreshAllType;
@@ -318,66 +360,79 @@ typedef enum {
 typedef struct {
     NMPObjectType obj_type;
 
-    /* for NLM_F_DUMP, which address family to request. */
-    int addr_family;
+    NMPNetlinkProtocol protocol;
+
+    /* For NLM_F_DUMP, which address family to request.
+     * Either AF_UNSPEC, AF_INET or AF_INET6. */
+    gint8 addr_family_for_dump;
 } RefreshAllInfo;
 
-typedef enum {
+typedef enum _nm_packed {
     DELAYED_ACTION_TYPE_NONE = 0,
 
 #define F(val, name) ((sizeof(char[(((val)) == (name)) ? 1 : -1]) * 0) + (val))
-    DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS             = 1 << F(0, REFRESH_ALL_TYPE_LINKS),
-    DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES     = 1 << F(1, REFRESH_ALL_TYPE_IP4_ADDRESSES),
-    DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES     = 1 << F(2, REFRESH_ALL_TYPE_IP6_ADDRESSES),
-    DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES        = 1 << F(3, REFRESH_ALL_TYPE_IP4_ROUTES),
-    DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES        = 1 << F(4, REFRESH_ALL_TYPE_IP6_ROUTES),
-    DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP4 = 1
-                                                        << F(5, REFRESH_ALL_TYPE_ROUTING_RULES_IP4),
-    DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP6 = 1
-                                                        << F(6, REFRESH_ALL_TYPE_ROUTING_RULES_IP6),
-    DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS   = 1 << F(7, REFRESH_ALL_TYPE_QDISCS),
-    DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS = 1 << F(8, REFRESH_ALL_TYPE_TFILTERS),
+    DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_LINKS = 1 << F(0, REFRESH_ALL_TYPE_RTNL_LINKS),
+    DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP4_ADDRESSES =
+        1 << F(1, REFRESH_ALL_TYPE_RTNL_IP4_ADDRESSES),
+    DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP6_ADDRESSES =
+        1 << F(2, REFRESH_ALL_TYPE_RTNL_IP6_ADDRESSES),
+    DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP4_ROUTES = 1 << F(3, REFRESH_ALL_TYPE_RTNL_IP4_ROUTES),
+    DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP6_ROUTES = 1 << F(4, REFRESH_ALL_TYPE_RTNL_IP6_ROUTES),
+    DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_IP4 =
+        1 << F(5, REFRESH_ALL_TYPE_RTNL_ROUTING_RULES_IP4),
+    DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_IP6 =
+        1 << F(6, REFRESH_ALL_TYPE_RTNL_ROUTING_RULES_IP6),
+    DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_QDISCS   = 1 << F(7, REFRESH_ALL_TYPE_RTNL_QDISCS),
+    DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_TFILTERS = 1 << F(8, REFRESH_ALL_TYPE_RTNL_TFILTERS),
+
+    DELAYED_ACTION_TYPE_REFRESH_ALL_GENL_FAMILIES = 1 << F(9, REFRESH_ALL_TYPE_GENL_FAMILIES),
 #undef F
 
-    DELAYED_ACTION_TYPE_REFRESH_LINK         = 1 << 9,
-    DELAYED_ACTION_TYPE_MASTER_CONNECTED     = 1 << 10,
-    DELAYED_ACTION_TYPE_READ_NETLINK         = 1 << 11,
-    DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE = 1 << 12,
+    DELAYED_ACTION_TYPE_READ_RTNL              = 1 << 10,
+    DELAYED_ACTION_TYPE_READ_GENL              = 1 << 11,
+    DELAYED_ACTION_TYPE_WAIT_FOR_RESPONSE_RTNL = 1 << 12,
+    DELAYED_ACTION_TYPE_WAIT_FOR_RESPONSE_GENL = 1 << 13,
+    DELAYED_ACTION_TYPE_REFRESH_LINK           = 1 << 14,
+    DELAYED_ACTION_TYPE_MASTER_CONNECTED       = 1 << 15,
 
     __DELAYED_ACTION_TYPE_MAX,
 
-    DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL =
-        DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP4
-        | DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP6,
+    DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_ALL =
+        DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_IP4
+        | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_IP6,
 
-    DELAYED_ACTION_TYPE_REFRESH_ALL =
-        DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS | DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES
-        | DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES | DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES
-        | DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES
-        | DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL | DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS
-        | DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS,
+    DELAYED_ACTION_TYPE_REFRESH_RTNL_ALL = DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_LINKS
+                                           | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP4_ADDRESSES
+                                           | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP6_ADDRESSES
+                                           | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP4_ROUTES
+                                           | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP6_ROUTES
+                                           | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_ALL
+                                           | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_QDISCS
+                                           | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_TFILTERS,
+
+    DELAYED_ACTION_TYPE_REFRESH_GENL_ALL = DELAYED_ACTION_TYPE_REFRESH_ALL_GENL_FAMILIES,
 
     DELAYED_ACTION_TYPE_MAX = __DELAYED_ACTION_TYPE_MAX - 1,
 } DelayedActionType;
 
-#define FOR_EACH_DELAYED_ACTION(iflags, flags_all)          \
-    for ((iflags) = (DelayedActionType) 0x1LL; ({           \
-             gboolean _good = FALSE;                        \
-                                                            \
-             nm_assert(nm_utils_is_power_of_two(iflags));   \
-                                                            \
-             while ((iflags) <= DELAYED_ACTION_TYPE_MAX) {  \
-                 if (NM_FLAGS_ANY((flags_all), (iflags))) { \
-                     _good = TRUE;                          \
-                     break;                                 \
-                 }                                          \
-                 (iflags) <<= 1;                            \
-             }                                              \
-             _good;                                         \
-         });                                                \
+#define FOR_EACH_DELAYED_ACTION(iflags, flags_all)                          \
+    for ((iflags) = (DelayedActionType) 0x1LL; ({                           \
+             gboolean _good = FALSE;                                        \
+                                                                            \
+             nm_assert((iflags) == 0 || nm_utils_is_power_of_two(iflags));  \
+                                                                            \
+             while ((iflags) != 0 && (iflags) <= DELAYED_ACTION_TYPE_MAX) { \
+                 if (NM_FLAGS_ANY((flags_all), (iflags))) {                 \
+                     _good = TRUE;                                          \
+                     break;                                                 \
+                 }                                                          \
+                 (iflags) <<= 1;                                            \
+             }                                                              \
+             _good;                                                         \
+         });                                                                \
          (iflags) <<= 1)
 
-typedef enum {
+typedef enum _nm_packed {
     /* Negative values are errors from kernel. Add dummy member to
      * make enum signed. */
     _WAIT_FOR_NL_RESPONSE_RESULT_SYSTEM_ERROR = G_MININT,
@@ -392,40 +447,57 @@ typedef enum {
     WAIT_FOR_NL_RESPONSE_RESULT_FAILED_SETNS,
 } WaitForNlResponseResult;
 
-typedef enum {
+typedef enum _nm_packed {
     DELAYED_ACTION_RESPONSE_TYPE_VOID                    = 0,
     DELAYED_ACTION_RESPONSE_TYPE_REFRESH_ALL_IN_PROGRESS = 1,
     DELAYED_ACTION_RESPONSE_TYPE_ROUTE_GET               = 2,
 } DelayedActionWaitForNlResponseType;
 
 typedef struct {
-    guint32                            seq_number;
-    WaitForNlResponseResult            seq_result;
-    DelayedActionWaitForNlResponseType response_type;
-    gint64                             timeout_abs_nsec;
-    WaitForNlResponseResult           *out_seq_result;
-    char                             **out_errmsg;
+    WaitForNlResponseResult *out_seq_result;
+    char                   **out_errmsg;
     union {
         int        *out_refresh_all_in_progress;
         NMPObject **out_route_get;
         gpointer    out_data;
     } response;
+    gint64                             timeout_abs_nsec;
+    guint32                            seq_number;
+    WaitForNlResponseResult            seq_result;
+    DelayedActionWaitForNlResponseType response_type;
 } DelayedActionWaitForNlResponseData;
 
 /*****************************************************************************/
 
 typedef struct {
-    struct nl_sock *genl;
-
-    struct nl_sock *nlh;
-
-    GSource *event_source;
-
     guint32 nlh_seq_next;
+    guint32 nlh_seq_last_seen;
 #if NM_MORE_LOGGING
     guint32 nlh_seq_last_handled;
 #endif
-    guint32 nlh_seq_last_seen;
+} NetlinkProtocolPrivData;
+
+typedef struct {
+    struct nl_sock *sk_genl_sync;
+
+    union {
+        struct {
+            struct nl_sock *sk_genl;
+            struct nl_sock *sk_rtnl;
+        };
+        struct nl_sock *sk_x[_NMP_NETLINK_NUM];
+    };
+
+    GSource *event_source_genl;
+    GSource *event_source_rtnl;
+
+    union {
+        struct {
+            NetlinkProtocolPrivData proto_data_genl;
+            NetlinkProtocolPrivData proto_data_rtnl;
+        };
+        NetlinkProtocolPrivData proto_data_x[_NMP_NETLINK_NUM];
+    };
 
     guint32 pruning[_REFRESH_ALL_TYPE_NUM];
 
@@ -446,7 +518,13 @@ typedef struct {
 
         GPtrArray *list_master_connected;
         GPtrArray *list_refresh_link;
-        GArray    *list_wait_for_nl_response;
+        union {
+            struct {
+                GArray *list_wait_for_response_genl;
+                GArray *list_wait_for_response_rtnl;
+            };
+            GArray *list_wait_for_response_x[_NMP_NETLINK_NUM];
+        };
 
         int is_handling;
     } delayed_action;
@@ -462,6 +540,8 @@ typedef struct {
         gsize          len;
     } netlink_recv_buf;
 
+    GenlFamilyData genl_family_data[_NMP_GENL_FAMILY_TYPE_NUM];
+
 } NMLinuxPlatformPrivate;
 
 struct _NMLinuxPlatform {
@@ -562,7 +642,7 @@ NM_LINUX_PLATFORM_FROM_PRIVATE(NMLinuxPlatformPrivate *priv)
 
 static void
 delayed_action_schedule(NMPlatform *platform, DelayedActionType action_type, gpointer user_data);
-static gboolean delayed_action_handle_all(NMPlatform *platform, gboolean read_netlink);
+static gboolean delayed_action_handle_all(NMPlatform *platform);
 static void do_request_link_no_delayed_actions(NMPlatform *platform, int ifindex, const char *name);
 static void do_request_all_no_delayed_actions(NMPlatform *platform, DelayedActionType action_type);
 static void cache_on_change(NMPlatform      *platform,
@@ -570,8 +650,36 @@ static void cache_on_change(NMPlatform      *platform,
                             const NMPObject *obj_old,
                             const NMPObject *obj_new);
 static void cache_prune_all(NMPlatform *platform);
-static gboolean        event_handler_read_netlink(NMPlatform *platform, gboolean wait_for_acks);
-static struct nl_sock *_genl_sock(NMLinuxPlatform *platform);
+static gboolean event_handler_read_netlink(NMPlatform        *platform,
+                                           NMPNetlinkProtocol netlink_protocol,
+                                           gboolean           wait_for_acks);
+
+/*****************************************************************************/
+
+static const struct {
+    gint8             netlink_protocol;
+    DelayedActionType delayed_action_type_read;
+    DelayedActionType delayed_action_type_wait_for_response;
+    const char        name[5];
+} _nmp_netlink_protocol_infos[_NMP_NETLINK_NUM] = {
+    [NMP_NETLINK_ROUTE] =
+        {
+            .netlink_protocol                      = NETLINK_ROUTE,
+            .name                                  = "rtnl",
+            .delayed_action_type_read              = DELAYED_ACTION_TYPE_READ_RTNL,
+            .delayed_action_type_wait_for_response = DELAYED_ACTION_TYPE_WAIT_FOR_RESPONSE_RTNL,
+        },
+    [NMP_NETLINK_GENERIC] =
+        {
+            .netlink_protocol                      = NETLINK_GENERIC,
+            .name                                  = "genl",
+            .delayed_action_type_read              = DELAYED_ACTION_TYPE_READ_GENL,
+            .delayed_action_type_wait_for_response = DELAYED_ACTION_TYPE_WAIT_FOR_RESPONSE_GENL,
+        },
+};
+
+#define nmp_netlink_protocol_info(netlink_protocol) \
+    (&_nmp_netlink_protocol_infos[nmp_netlink_protocol_check((netlink_protocol))])
 
 /*****************************************************************************/
 
@@ -1434,6 +1542,152 @@ _parse_lnk_bridge(const char *kind, struct nlattr *info_data)
 /***********************************************************************************/
 
 static NMPObject *
+_parse_lnk_bond(const char *kind, struct nlattr *info_data)
+{
+    static const struct nla_policy policy[] = {
+        [IFLA_BOND_MODE]              = {.type = NLA_U8},
+        [IFLA_BOND_ACTIVE_SLAVE]      = {.type = NLA_U32},
+        [IFLA_BOND_MIIMON]            = {.type = NLA_U32},
+        [IFLA_BOND_UPDELAY]           = {.type = NLA_U32},
+        [IFLA_BOND_DOWNDELAY]         = {.type = NLA_U32},
+        [IFLA_BOND_USE_CARRIER]       = {.type = NLA_U8},
+        [IFLA_BOND_ARP_INTERVAL]      = {.type = NLA_U32},
+        [IFLA_BOND_ARP_IP_TARGET]     = {.type = NLA_NESTED},
+        [IFLA_BOND_ARP_VALIDATE]      = {.type = NLA_U32},
+        [IFLA_BOND_ARP_ALL_TARGETS]   = {.type = NLA_U32},
+        [IFLA_BOND_PRIMARY]           = {.type = NLA_U32},
+        [IFLA_BOND_PRIMARY_RESELECT]  = {.type = NLA_U8},
+        [IFLA_BOND_FAIL_OVER_MAC]     = {.type = NLA_U8},
+        [IFLA_BOND_XMIT_HASH_POLICY]  = {.type = NLA_U8},
+        [IFLA_BOND_RESEND_IGMP]       = {.type = NLA_U32},
+        [IFLA_BOND_NUM_PEER_NOTIF]    = {.type = NLA_U8},
+        [IFLA_BOND_ALL_SLAVES_ACTIVE] = {.type = NLA_U8},
+        [IFLA_BOND_MIN_LINKS]         = {.type = NLA_U32},
+        [IFLA_BOND_LP_INTERVAL]       = {.type = NLA_U32},
+        [IFLA_BOND_PACKETS_PER_SLAVE] = {.type = NLA_U32},
+        [IFLA_BOND_AD_LACP_RATE]      = {.type = NLA_U8},
+        [IFLA_BOND_AD_SELECT]         = {.type = NLA_U8},
+        [IFLA_BOND_AD_ACTOR_SYS_PRIO] = {.type = NLA_U16},
+        [IFLA_BOND_AD_USER_PORT_KEY]  = {.type = NLA_U16},
+        [IFLA_BOND_AD_ACTOR_SYSTEM]   = {.minlen = sizeof(NMEtherAddr)},
+        [IFLA_BOND_TLB_DYNAMIC_LB]    = {.type = NLA_U8},
+        [IFLA_BOND_PEER_NOTIF_DELAY]  = {.type = NLA_U32},
+    };
+    NMPlatformLnkBond *props;
+    struct nlattr     *tb[G_N_ELEMENTS(policy)];
+    NMPObject         *obj = NULL;
+
+    if (!info_data || !nm_streq0(kind, "bond"))
+        return NULL;
+
+    if (nla_parse_nested_arr(tb, info_data, policy) < 0)
+        return NULL;
+
+    obj = nmp_object_new(NMP_OBJECT_TYPE_LNK_BOND, NULL);
+
+    props = &obj->lnk_bond;
+
+    if (tb[IFLA_BOND_MODE])
+        props->mode = nla_get_u8(tb[IFLA_BOND_MODE]);
+    if (tb[IFLA_BOND_PRIMARY]) {
+        props->primary = nla_get_u32(tb[IFLA_BOND_PRIMARY]);
+    } else if (tb[IFLA_BOND_ACTIVE_SLAVE]) {
+        props->primary = nla_get_u32(tb[IFLA_BOND_ACTIVE_SLAVE]);
+    }
+    if (tb[IFLA_BOND_MIIMON]) {
+        props->miimon     = nla_get_u32(tb[IFLA_BOND_MIIMON]);
+        props->miimon_has = TRUE;
+    } else {
+        props->miimon_has = FALSE;
+    }
+    if (tb[IFLA_BOND_UPDELAY]) {
+        props->updelay     = nla_get_u32(tb[IFLA_BOND_UPDELAY]);
+        props->updelay_has = TRUE;
+    } else {
+        props->updelay_has = FALSE;
+    }
+    if (tb[IFLA_BOND_DOWNDELAY]) {
+        props->downdelay     = nla_get_u32(tb[IFLA_BOND_DOWNDELAY]);
+        props->downdelay_has = TRUE;
+    } else {
+        props->downdelay_has = FALSE;
+    }
+    if (tb[IFLA_BOND_USE_CARRIER])
+        props->use_carrier = nla_get_u8(tb[IFLA_BOND_USE_CARRIER]);
+    if (tb[IFLA_BOND_ARP_INTERVAL])
+        props->arp_interval = nla_get_u32(tb[IFLA_BOND_ARP_INTERVAL]);
+    if (tb[IFLA_BOND_ARP_IP_TARGET]) {
+        struct nlattr *attr;
+        int            rem;
+
+        nla_for_each_nested (attr, tb[IFLA_BOND_ARP_IP_TARGET], rem) {
+            if (props->arp_ip_targets_num > NM_BOND_MAX_ARP_TARGETS - 1)
+                break;
+            if (nla_len(attr) < sizeof(in_addr_t))
+                break;
+
+            props->arp_ip_target[props->arp_ip_targets_num++] = nla_get_u32(attr);
+        }
+    }
+    if (tb[IFLA_BOND_ARP_VALIDATE])
+        props->arp_validate = nla_get_u32(tb[IFLA_BOND_ARP_VALIDATE]);
+    if (tb[IFLA_BOND_ARP_ALL_TARGETS]) {
+        props->arp_all_targets     = nla_get_u32(tb[IFLA_BOND_ARP_ALL_TARGETS]);
+        props->arp_all_targets_has = TRUE;
+    } else {
+        props->arp_all_targets_has = FALSE;
+    }
+    if (tb[IFLA_BOND_PRIMARY_RESELECT])
+        props->primary_reselect = nla_get_u8(tb[IFLA_BOND_PRIMARY_RESELECT]);
+    if (tb[IFLA_BOND_FAIL_OVER_MAC])
+        props->fail_over_mac = nla_get_u8(tb[IFLA_BOND_FAIL_OVER_MAC]);
+    if (tb[IFLA_BOND_XMIT_HASH_POLICY])
+        props->xmit_hash_policy = nla_get_u8(tb[IFLA_BOND_XMIT_HASH_POLICY]);
+    if (tb[IFLA_BOND_RESEND_IGMP]) {
+        props->resend_igmp     = nla_get_u32(tb[IFLA_BOND_RESEND_IGMP]);
+        props->resend_igmp_has = TRUE;
+    } else {
+        props->resend_igmp_has = FALSE;
+    }
+    if (tb[IFLA_BOND_NUM_PEER_NOTIF])
+        props->num_grat_arp = nla_get_u8(tb[IFLA_BOND_NUM_PEER_NOTIF]);
+    if (tb[IFLA_BOND_ALL_SLAVES_ACTIVE])
+        props->all_ports_active = nla_get_u8(tb[IFLA_BOND_ALL_SLAVES_ACTIVE]);
+    if (tb[IFLA_BOND_MIN_LINKS])
+        props->min_links = nla_get_u32(tb[IFLA_BOND_MIN_LINKS]);
+    if (tb[IFLA_BOND_LP_INTERVAL])
+        props->lp_interval = nla_get_u32(tb[IFLA_BOND_LP_INTERVAL]);
+    if (tb[IFLA_BOND_PACKETS_PER_SLAVE])
+        props->packets_per_port = nla_get_u32(tb[IFLA_BOND_PACKETS_PER_SLAVE]);
+    if (tb[IFLA_BOND_AD_LACP_RATE])
+        props->lacp_rate = nla_get_u8(tb[IFLA_BOND_AD_LACP_RATE]);
+    if (tb[IFLA_BOND_AD_SELECT])
+        props->ad_select = nla_get_u8(tb[IFLA_BOND_AD_SELECT]);
+    if (tb[IFLA_BOND_AD_ACTOR_SYS_PRIO])
+        props->ad_actor_sys_prio = nla_get_u16(tb[IFLA_BOND_AD_ACTOR_SYS_PRIO]);
+    if (tb[IFLA_BOND_AD_USER_PORT_KEY])
+        props->ad_user_port_key = nla_get_u16(tb[IFLA_BOND_AD_USER_PORT_KEY]);
+    if (tb[IFLA_BOND_AD_ACTOR_SYSTEM])
+        props->ad_actor_system = *nla_data_as(NMEtherAddr, tb[IFLA_BOND_AD_ACTOR_SYSTEM]);
+    if (tb[IFLA_BOND_TLB_DYNAMIC_LB]) {
+        props->tlb_dynamic_lb     = nla_get_u8(tb[IFLA_BOND_TLB_DYNAMIC_LB]);
+        props->tlb_dynamic_lb_has = TRUE;
+    } else {
+        props->tlb_dynamic_lb_has = FALSE;
+    }
+    if (tb[IFLA_BOND_PEER_NOTIF_DELAY]) {
+        props->peer_notif_delay     = nla_get_u32(tb[IFLA_BOND_PEER_NOTIF_DELAY]);
+        props->peer_notif_delay_has = TRUE;
+    } else {
+        props->peer_notif_delay_has = FALSE;
+    }
+
+    return obj;
+}
+
+/***********************************************************************************/
+
+static NMPObject *
 _parse_lnk_gre(const char *kind, struct nlattr *info_data)
 {
     static const struct nla_policy policy[] = {
@@ -2322,7 +2576,7 @@ typedef struct {
 } WireGuardParseData;
 
 static int
-_wireguard_get_device_cb(struct nl_msg *msg, void *arg)
+_wireguard_get_device_cb(const struct nl_msg *msg, void *arg)
 {
     static const struct nla_policy policy[] = {
         [WGDEVICE_A_IFINDEX]     = {.type = NLA_U32},
@@ -2400,7 +2654,7 @@ _wireguard_get_device_cb(struct nl_msg *msg, void *arg)
 static const NMPObject *
 _wireguard_read_info(NMPlatform     *platform /* used only as logging context */,
                      struct nl_sock *genl,
-                     int             wireguard_family_id,
+                     guint16         wireguard_family_id,
                      int             ifindex)
 {
     nm_auto_nlmsg struct nl_msg *msg = NULL;
@@ -2414,10 +2668,12 @@ _wireguard_read_info(NMPlatform     *platform /* used only as logging context */
     guint i;
 
     nm_assert(genl);
-    nm_assert(wireguard_family_id >= 0);
     nm_assert(ifindex > 0);
 
-    _LOGT("wireguard: fetching information for ifindex %d (genl-id %d)...",
+    if (wireguard_family_id == 0)
+        return NULL;
+
+    _LOGT("wireguard: fetching information for ifindex %d (genl-id 0x%x)...",
           ifindex,
           wireguard_family_id);
 
@@ -2530,25 +2786,8 @@ nla_put_failure:
     g_return_val_if_reached(NULL);
 }
 
-static int
-_wireguard_get_family_id(NMPlatform *platform, int ifindex_try)
-{
-    NMLinuxPlatformPrivate *priv                = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
-    int                     wireguard_family_id = -1;
-
-    if (ifindex_try > 0) {
-        const NMPlatformLink *plink;
-
-        if (nm_platform_link_get_lnk_wireguard(platform, ifindex_try, &plink))
-            wireguard_family_id = NMP_OBJECT_UP_CAST(plink)->_link.wireguard_family_id;
-    }
-    if (wireguard_family_id < 0)
-        wireguard_family_id = genl_ctrl_resolve(priv->genl, "wireguard");
-    return wireguard_family_id;
-}
-
 static const NMPObject *
-_wireguard_refresh_link(NMPlatform *platform, int wireguard_family_id, int ifindex)
+_wireguard_refresh_link(NMPlatform *platform, guint16 wireguard_family_id, int ifindex)
 {
     NMLinuxPlatformPrivate         *priv    = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
     nm_auto_nmpobj const NMPObject *obj_old = NULL;
@@ -2558,7 +2797,7 @@ _wireguard_refresh_link(NMPlatform *platform, int wireguard_family_id, int ifind
     const NMPObject                *plink = NULL;
     nm_auto_nmpobj NMPObject       *obj   = NULL;
 
-    nm_assert(wireguard_family_id >= 0);
+    nm_assert(wireguard_family_id > 0);
     nm_assert(ifindex > 0);
 
     nm_platform_process_events(platform);
@@ -2573,7 +2812,7 @@ _wireguard_refresh_link(NMPlatform *platform, int wireguard_family_id, int ifind
         if (NMP_OBJECT_GET_TYPE(plink->_link.netlink.lnk) == NMP_OBJECT_TYPE_LNK_WIREGUARD)
             lnk_new = nmp_object_ref(plink->_link.netlink.lnk);
     } else {
-        lnk_new = _wireguard_read_info(platform, priv->genl, wireguard_family_id, ifindex);
+        lnk_new = _wireguard_read_info(platform, priv->sk_genl_sync, wireguard_family_id, ifindex);
         if (!lnk_new) {
             if (NMP_OBJECT_GET_TYPE(plink->_link.netlink.lnk) == NMP_OBJECT_TYPE_LNK_WIREGUARD)
                 lnk_new = nmp_object_ref(plink->_link.netlink.lnk);
@@ -2583,15 +2822,13 @@ _wireguard_refresh_link(NMPlatform *platform, int wireguard_family_id, int ifind
         }
     }
 
-    if (plink->_link.wireguard_family_id == wireguard_family_id
-        && plink->_link.netlink.lnk == lnk_new)
+    if (plink->_link.netlink.lnk == lnk_new)
         return plink;
 
     /* we use nmp_cache_update_netlink() to re-inject the new object into the cache.
      * For that, we need to clone it, and tweak it so that it's suitable. It's a bit
      * of a hack, in particular that we need to clear driver and udev-device. */
-    obj                            = nmp_object_clone(plink, FALSE);
-    obj->_link.wireguard_family_id = wireguard_family_id;
+    obj = nmp_object_clone(plink, FALSE);
     nmp_object_unref(obj->_link.netlink.lnk);
     obj->_link.netlink.lnk = g_steal_pointer(&lnk_new);
     obj->link.driver       = NULL;
@@ -2617,7 +2854,7 @@ _wireguard_refresh_link(NMPlatform *platform, int wireguard_family_id, int ifind
 static int
 _wireguard_create_change_nlmsgs(NMPlatform                               *platform,
                                 int                                       ifindex,
-                                int                                       wireguard_family_id,
+                                guint16                                   wireguard_family_id,
                                 const NMPlatformLnkWireGuard             *lnk_wireguard,
                                 const NMPWireGuardPeer                   *peers,
                                 const NMPlatformWireGuardChangePeerFlags *peer_flags,
@@ -2636,14 +2873,6 @@ _wireguard_create_change_nlmsgs(NMPlatform                               *platfo
     struct nlattr                     *nest_curr_allowed_ip;
     NMPlatformWireGuardChangePeerFlags p_flags = NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_DEFAULT;
 
-#define _nla_nest_end(msg, nest_start)             \
-    G_STMT_START                                   \
-    {                                              \
-        if (nla_nest_end((msg), (nest_start)) < 0) \
-            g_return_val_if_reached(-NME_BUG);     \
-    }                                              \
-    G_STMT_END
-
     /* Adapted from LGPL-2.1+ code [1].
      *
      * [1] https://git.zx2c4.com/WireGuard/tree/contrib/examples/embeddable-wg-library/wireguard.c?id=5e99a6d43fe2351adf36c786f5ea2086a8fe7ab8#n1073 */
@@ -2799,36 +3028,36 @@ again:
                     if (nla_put_uint8(msg, WGALLOWEDIP_A_CIDR_MASK, aip->mask) < 0)
                         goto toobig_allowedips;
 
-                    _nla_nest_end(msg, nest_curr_allowed_ip);
+                    NLA_NEST_END(msg, nest_curr_allowed_ip);
                     nest_curr_allowed_ip = NULL;
                 }
                 idx_allowed_ips_curr = IDX_NIL;
 
-                _nla_nest_end(msg, nest_allowed_ips);
+                NLA_NEST_END(msg, nest_allowed_ips);
                 nest_allowed_ips = NULL;
             }
         }
 
-        _nla_nest_end(msg, nest_curr_peer);
+        NLA_NEST_END(msg, nest_curr_peer);
         nest_curr_peer = NULL;
     }
 
-    _nla_nest_end(msg, nest_peers);
+    NLA_NEST_END(msg, nest_peers);
     goto send;
 
 toobig_allowedips:
     if (nest_curr_allowed_ip)
         nla_nest_cancel(msg, nest_curr_allowed_ip);
     if (nest_allowed_ips)
-        _nla_nest_end(msg, nest_allowed_ips);
-    _nla_nest_end(msg, nest_curr_peer);
-    _nla_nest_end(msg, nest_peers);
+        NLA_NEST_END(msg, nest_allowed_ips);
+    NLA_NEST_END(msg, nest_curr_peer);
+    NLA_NEST_END(msg, nest_peers);
     goto send;
 
 toobig_peers:
     if (nest_curr_peer)
         nla_nest_cancel(msg, nest_curr_peer);
-    _nla_nest_end(msg, nest_peers);
+    NLA_NEST_END(msg, nest_peers);
     goto send;
 
 send:
@@ -2844,8 +3073,6 @@ send:
 
 nla_put_failure:
     g_return_val_if_reached(-NME_BUG);
-
-#undef _nla_nest_end
 }
 
 static int
@@ -2859,12 +3086,12 @@ link_wireguard_change(NMPlatform                               *platform,
 {
     NMLinuxPlatformPrivate      *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
     gs_unref_ptrarray GPtrArray *msgs = NULL;
-    int                          wireguard_family_id;
+    guint16                      wireguard_family_id;
     guint                        i;
     int                          r;
 
-    wireguard_family_id = _wireguard_get_family_id(platform, ifindex);
-    if (wireguard_family_id < 0)
+    wireguard_family_id = nm_platform_genl_get_family_id(platform, NMP_GENL_FAMILY_TYPE_WIREGUARD);
+    if (wireguard_family_id == 0)
         return -NME_PL_NO_FIRMWARE;
 
     r = _wireguard_create_change_nlmsgs(platform,
@@ -2882,14 +3109,14 @@ link_wireguard_change(NMPlatform                               *platform,
     }
 
     for (i = 0; i < msgs->len; i++) {
-        r = nl_send_auto(priv->genl, msgs->pdata[i]);
+        r = nl_send_auto(priv->sk_genl_sync, msgs->pdata[i]);
         if (r < 0) {
             _LOGW("wireguard: set-device, send netlink message #%u failed: %s", i, nm_strerror(r));
             return r;
         }
 
         do {
-            r = nl_recvmsgs(priv->genl, NULL);
+            r = nl_recvmsgs(priv->sk_genl_sync, NULL);
         } while (r == -EAGAIN);
         if (r < 0) {
             _LOGW("wireguard: set-device, message #%u was rejected: %s", i, nm_strerror(r));
@@ -2925,10 +3152,10 @@ _nmp_link_address_set(NMPLinkAddress *dst, const struct nlattr *nla)
 
 /* Copied and heavily modified from libnl3's link_msg_parser(). */
 static NMPObject *
-_new_from_nl_link(NMPlatform      *platform,
-                  const NMPCache  *cache,
-                  struct nlmsghdr *nlh,
-                  gboolean         id_only)
+_new_from_nl_link(NMPlatform            *platform,
+                  const NMPCache        *cache,
+                  const struct nlmsghdr *nlh,
+                  gboolean               id_only)
 {
     static const struct nla_policy policy[] = {
         [IFLA_IFNAME]        = {.type = NLA_STRING, .maxlen = IFNAMSIZ},
@@ -3117,6 +3344,9 @@ _new_from_nl_link(NMPlatform      *platform,
     case NM_LINK_TYPE_BRIDGE:
         lnk_data = _parse_lnk_bridge(nl_info_kind, nl_info_data);
         break;
+    case NM_LINK_TYPE_BOND:
+        lnk_data = _parse_lnk_bond(nl_info_kind, nl_info_data);
+        break;
     case NM_LINK_TYPE_GRE:
     case NM_LINK_TYPE_GRETAP:
         lnk_data = _parse_lnk_gre(nl_info_kind, nl_info_data);
@@ -3220,16 +3450,18 @@ _new_from_nl_link(NMPlatform      *platform,
         switch (obj->link.type) {
         case NM_LINK_TYPE_WIFI:
         case NM_LINK_TYPE_OLPC_MESH:
-            obj->_link.ext_data =
-                (GObject *) nm_wifi_utils_new(ifi->ifi_index,
-                                              _genl_sock(NM_LINUX_PLATFORM(platform)),
-                                              TRUE);
+            obj->_link.ext_data = (GObject *) nm_wifi_utils_new(
+                NM_LINUX_PLATFORM_GET_PRIVATE(platform)->sk_genl_sync,
+                nm_platform_genl_get_family_id(platform, NMP_GENL_FAMILY_TYPE_NL80211),
+                ifi->ifi_index,
+                TRUE);
             break;
         case NM_LINK_TYPE_WPAN:
-            obj->_link.ext_data =
-                (GObject *) nm_wpan_utils_new(ifi->ifi_index,
-                                              _genl_sock(NM_LINUX_PLATFORM(platform)),
-                                              TRUE);
+            obj->_link.ext_data = (GObject *) nm_wpan_utils_new(
+                NM_LINUX_PLATFORM_GET_PRIVATE(platform)->sk_genl_sync,
+                nm_platform_genl_get_family_id(platform, NMP_GENL_FAMILY_TYPE_NL802154),
+                ifi->ifi_index,
+                TRUE);
             break;
         default:
             g_assert_not_reached();
@@ -3238,28 +3470,17 @@ _new_from_nl_link(NMPlatform      *platform,
 
     if (obj->link.type == NM_LINK_TYPE_WIREGUARD) {
         const NMPObject *lnk_data_new = NULL;
-        struct nl_sock  *genl         = NM_LINUX_PLATFORM_GET_PRIVATE(platform)->genl;
+        struct nl_sock  *genl         = NM_LINUX_PLATFORM_GET_PRIVATE(platform)->sk_genl_sync;
 
         /* The WireGuard kernel module does not yet send link update
          * notifications, so we don't actually update the cache. For
          * now, always refetch link data here. */
 
-        _lookup_cached_link(cache, obj->link.ifindex, completed_from_cache, &link_cached);
-        if (link_cached && link_cached->_link.netlink.is_in_netlink
-            && link_cached->link.type == NM_LINK_TYPE_WIREGUARD)
-            obj->_link.wireguard_family_id = link_cached->_link.wireguard_family_id;
-        else
-            obj->_link.wireguard_family_id = -1;
-
-        if (obj->_link.wireguard_family_id < 0)
-            obj->_link.wireguard_family_id = genl_ctrl_resolve(genl, "wireguard");
-
-        if (obj->_link.wireguard_family_id >= 0) {
-            lnk_data_new = _wireguard_read_info(platform,
-                                                genl,
-                                                obj->_link.wireguard_family_id,
-                                                obj->link.ifindex);
-        }
+        lnk_data_new = _wireguard_read_info(
+            platform,
+            genl,
+            nm_platform_genl_get_family_id(platform, NMP_GENL_FAMILY_TYPE_WIREGUARD),
+            obj->link.ifindex);
 
         if (lnk_data_new && obj->_link.netlink.lnk
             && nmp_object_equal(obj->_link.netlink.lnk, lnk_data_new))
@@ -3276,7 +3497,7 @@ _new_from_nl_link(NMPlatform      *platform,
 
 /* Copied and heavily modified from libnl3's addr_msg_parser(). */
 static NMPObject *
-_new_from_nl_addr(struct nlmsghdr *nlh, gboolean id_only)
+_new_from_nl_addr(const struct nlmsghdr *nlh, gboolean id_only)
 {
     static const struct nla_policy policy[] = {
         [IFA_LABEL]     = {.type = NLA_STRING, .maxlen = IFNAMSIZ},
@@ -3391,7 +3612,7 @@ _new_from_nl_addr(struct nlmsghdr *nlh, gboolean id_only)
 
 /* Copied and heavily modified from libnl3's rtnl_route_parse() and parse_multipath(). */
 static NMPObject *
-_new_from_nl_route(struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter *parse_nlmsg_iter)
+_new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter *parse_nlmsg_iter)
 {
     static const struct nla_policy policy[] = {
         [RTA_TABLE]     = {.type = NLA_U32},
@@ -3420,13 +3641,15 @@ _new_from_nl_route(struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter *parse
         .found    = FALSE,
         .has_more = FALSE,
     };
-    guint32 mss;
-    guint32 window   = 0;
-    guint32 cwnd     = 0;
-    guint32 initcwnd = 0;
-    guint32 initrwnd = 0;
-    guint32 mtu      = 0;
-    guint32 lock     = 0;
+    guint32  mss;
+    guint32  window   = 0;
+    guint32  cwnd     = 0;
+    guint32  initcwnd = 0;
+    guint32  initrwnd = 0;
+    guint32  mtu      = 0;
+    guint32  rto_min  = 0;
+    guint32  lock     = 0;
+    gboolean quickack = FALSE;
 
     nm_assert((parse_nlmsg_iter->iter_more && parse_nlmsg_iter->ip6_route.next_multihop > 0)
               || (!parse_nlmsg_iter->iter_more && parse_nlmsg_iter->ip6_route.next_multihop == 0));
@@ -3604,6 +3827,8 @@ rta_multipath_done:
             [RTAX_INITCWND] = {.type = NLA_U32},
             [RTAX_INITRWND] = {.type = NLA_U32},
             [RTAX_MTU]      = {.type = NLA_U32},
+            [RTAX_QUICKACK] = {.type = NLA_U32},
+            [RTAX_RTO_MIN]  = {.type = NLA_U32},
         };
         struct nlattr *mtb[G_N_ELEMENTS(rtax_policy)];
 
@@ -3624,6 +3849,10 @@ rta_multipath_done:
             initrwnd = nla_get_u32(mtb[RTAX_INITRWND]);
         if (mtb[RTAX_MTU])
             mtu = nla_get_u32(mtb[RTAX_MTU]);
+        if (mtb[RTAX_RTO_MIN])
+            rto_min = nla_get_u32(mtb[RTAX_RTO_MIN]);
+        if (mtb[RTAX_QUICKACK])
+            quickack = !!nla_get_u32(mtb[RTAX_QUICKACK]);
     }
 
     /*****************************************************************/
@@ -3674,12 +3903,15 @@ rta_multipath_done:
     obj->ip_route.cwnd          = cwnd;
     obj->ip_route.initcwnd      = initcwnd;
     obj->ip_route.initrwnd      = initrwnd;
+    obj->ip_route.rto_min       = rto_min;
+    obj->ip_route.quickack      = quickack;
     obj->ip_route.mtu           = mtu;
     obj->ip_route.lock_window   = NM_FLAGS_HAS(lock, 1 << RTAX_WINDOW);
     obj->ip_route.lock_cwnd     = NM_FLAGS_HAS(lock, 1 << RTAX_CWND);
     obj->ip_route.lock_initcwnd = NM_FLAGS_HAS(lock, 1 << RTAX_INITCWND);
     obj->ip_route.lock_initrwnd = NM_FLAGS_HAS(lock, 1 << RTAX_INITRWND);
     obj->ip_route.lock_mtu      = NM_FLAGS_HAS(lock, 1 << RTAX_MTU);
+    obj->ip_route.lock_mss      = NM_FLAGS_HAS(lock, 1 << RTAX_ADVMSS);
 
     if (!IS_IPv4) {
         if (tb[RTA_PREF])
@@ -3699,7 +3931,7 @@ rta_multipath_done:
 }
 
 static NMPObject *
-_new_from_nl_routing_rule(struct nlmsghdr *nlh, gboolean id_only)
+_new_from_nl_routing_rule(const struct nlmsghdr *nlh, gboolean id_only)
 {
     static const struct nla_policy policy[] = {
         [FRA_UNSPEC] = {},
@@ -3975,7 +4207,7 @@ psched_tick_to_time(NMPlatform *platform, guint32 tick)
 }
 
 static NMPObject *
-_new_from_nl_qdisc(NMPlatform *platform, struct nlmsghdr *nlh, gboolean id_only)
+_new_from_nl_qdisc(NMPlatform *platform, const struct nlmsghdr *nlh, gboolean id_only)
 {
     static const struct nla_policy policy[] = {
         [TCA_KIND]    = {.type = NLA_STRING},
@@ -4088,7 +4320,7 @@ _new_from_nl_qdisc(NMPlatform *platform, struct nlmsghdr *nlh, gboolean id_only)
 }
 
 static NMPObject *
-_new_from_nl_tfilter(NMPlatform *platform, struct nlmsghdr *nlh, gboolean id_only)
+_new_from_nl_tfilter(NMPlatform *platform, const struct nlmsghdr *nlh, gboolean id_only)
 {
     static const struct nla_policy policy[] = {
         [TCA_KIND] = {.type = NLA_STRING},
@@ -4133,18 +4365,17 @@ _new_from_nl_tfilter(NMPlatform *platform, struct nlmsghdr *nlh, gboolean id_onl
  * Returns: %NULL or a newly created NMPObject instance.
  **/
 static NMPObject *
-nmp_object_new_from_nl(NMPlatform     *platform,
-                       const NMPCache *cache,
-                       struct nl_msg  *msg,
-                       gboolean        id_only,
-                       ParseNlmsgIter *parse_nlmsg_iter)
+nmp_object_new_from_nl(NMPlatform               *platform,
+                       const NMPCache           *cache,
+                       const struct nl_msg_lite *msg,
+                       gboolean                  id_only,
+                       ParseNlmsgIter           *parse_nlmsg_iter)
 {
-    struct nlmsghdr *msghdr;
+    const struct nlmsghdr *msghdr;
 
-    if (nlmsg_get_proto(msg) != NETLINK_ROUTE)
-        return NULL;
+    nm_assert(msg->nm_protocol == NETLINK_ROUTE);
 
-    msghdr = nlmsg_hdr(msg);
+    msghdr = msg->nm_nlh;
 
     switch (msghdr->nlmsg_type) {
     case RTM_NEWLINK:
@@ -4268,6 +4499,86 @@ _nl_msg_new_link_set_linkinfo(struct nl_msg *msg, NMLinkType link_type, gconstpo
         NLA_PUT_U64(msg, IFLA_BR_MCAST_STARTUP_QUERY_INTVL, props->mcast_startup_query_interval);
         break;
     }
+    case NM_LINK_TYPE_BOND:
+    {
+        const NMPlatformLnkBond *props = extra_data;
+        struct nlattr           *targets;
+        int                      i = 0;
+
+        nm_assert(extra_data);
+
+        if (!(data = nla_nest_start(msg, IFLA_INFO_DATA)))
+            goto nla_put_failure;
+
+        if (props->arp_ip_targets_num > 0) {
+            targets = nla_nest_start(msg, IFLA_BOND_ARP_IP_TARGET);
+            if (!targets)
+                goto nla_put_failure;
+
+            for (i = 0; i < props->arp_ip_targets_num; i++)
+                NLA_PUT_U32(msg, i, props->arp_ip_target[i]);
+
+            nla_nest_end(msg, targets);
+        }
+
+        if (props->arp_all_targets_has)
+            NLA_PUT_U32(msg, IFLA_BOND_ARP_ALL_TARGETS, props->arp_all_targets);
+        if (props->arp_interval)
+            NLA_PUT_U32(msg, IFLA_BOND_ARP_INTERVAL, props->arp_interval);
+        if (props->arp_validate)
+            NLA_PUT_U32(msg, IFLA_BOND_ARP_VALIDATE, props->arp_validate);
+        if (props->downdelay_has)
+            NLA_PUT_U32(msg, IFLA_BOND_DOWNDELAY, props->downdelay);
+        if (props->lp_interval_has)
+            NLA_PUT_U32(msg, IFLA_BOND_LP_INTERVAL, props->lp_interval);
+        if (props->miimon_has)
+            NLA_PUT_U32(msg, IFLA_BOND_MIIMON, props->miimon);
+        if (props->min_links)
+            NLA_PUT_U32(msg, IFLA_BOND_MIN_LINKS, props->min_links);
+        if (props->packets_per_port)
+            NLA_PUT_U32(msg, IFLA_BOND_PACKETS_PER_SLAVE, props->packets_per_port);
+        if (props->peer_notif_delay_has)
+            NLA_PUT_U32(msg, IFLA_BOND_PEER_NOTIF_DELAY, props->peer_notif_delay);
+        if (props->primary)
+            NLA_PUT_U32(msg, IFLA_BOND_PRIMARY, props->primary);
+        if (props->resend_igmp_has)
+            NLA_PUT_U32(msg, IFLA_BOND_RESEND_IGMP, props->resend_igmp);
+        if (props->updelay_has)
+            NLA_PUT_U32(msg, IFLA_BOND_UPDELAY, props->updelay);
+        if (props->ad_actor_sys_prio)
+            NLA_PUT_U16(msg, IFLA_BOND_AD_ACTOR_SYS_PRIO, props->ad_actor_sys_prio);
+        if (props->ad_user_port_key)
+            NLA_PUT_U16(msg, IFLA_BOND_AD_USER_PORT_KEY, props->ad_user_port_key);
+        if (!nm_ether_addr_equal(&props->ad_actor_system, &nm_ether_addr_zero))
+            NLA_PUT(msg,
+                    IFLA_BOND_AD_ACTOR_SYSTEM,
+                    sizeof(props->ad_actor_system),
+                    &props->ad_actor_system);
+        if (props->ad_select)
+            NLA_PUT_U8(msg, IFLA_BOND_AD_SELECT, props->ad_select);
+
+        NLA_PUT_U8(msg, IFLA_BOND_ALL_SLAVES_ACTIVE, props->all_ports_active);
+
+        if (props->fail_over_mac)
+            NLA_PUT_U8(msg, IFLA_BOND_FAIL_OVER_MAC, props->fail_over_mac);
+        if (props->lacp_rate)
+            NLA_PUT_U8(msg, IFLA_BOND_AD_LACP_RATE, props->lacp_rate);
+        if (props->num_grat_arp)
+            NLA_PUT_U8(msg, IFLA_BOND_NUM_PEER_NOTIF, props->num_grat_arp);
+
+        NLA_PUT_U8(msg, IFLA_BOND_MODE, props->mode);
+
+        if (props->primary_reselect)
+            NLA_PUT_U8(msg, IFLA_BOND_PRIMARY_RESELECT, props->primary_reselect);
+        if (props->xmit_hash_policy)
+            NLA_PUT_U8(msg, IFLA_BOND_XMIT_HASH_POLICY, props->xmit_hash_policy);
+        if (props->tlb_dynamic_lb_has)
+            NLA_PUT_U8(msg, IFLA_BOND_TLB_DYNAMIC_LB, !!props->tlb_dynamic_lb);
+
+        NLA_PUT_U8(msg, IFLA_BOND_USE_CARRIER, !!props->use_carrier);
+
+        break;
+    }
     case NM_LINK_TYPE_VLAN:
     {
         const NMPlatformLnkVlan *props = extra_data;
@@ -4655,8 +4966,8 @@ nla_put_failure:
 }
 
 static struct nl_msg *
-_nl_msg_new_link_full(int         nlmsg_type,
-                      int         nlmsg_flags,
+_nl_msg_new_link_full(uint16_t    nlmsg_type,
+                      uint16_t    nlmsg_flags,
                       int         ifindex,
                       const char *ifname,
                       guint8      family,
@@ -4688,22 +4999,22 @@ nla_put_failure:
 }
 
 static struct nl_msg *
-_nl_msg_new_link(int nlmsg_type, int nlmsg_flags, int ifindex, const char *ifname)
+_nl_msg_new_link(uint16_t nlmsg_type, uint16_t nlmsg_flags, int ifindex, const char *ifname)
 {
     return _nl_msg_new_link_full(nlmsg_type, nlmsg_flags, ifindex, ifname, AF_UNSPEC, 0, 0);
 }
 
 /* Copied and modified from libnl3's build_addr_msg(). */
 static struct nl_msg *
-_nl_msg_new_address(int           nlmsg_type,
-                    int           nlmsg_flags,
+_nl_msg_new_address(uint16_t      nlmsg_type,
+                    uint16_t      nlmsg_flags,
                     int           family,
                     int           ifindex,
                     gconstpointer address,
                     guint8        plen,
                     gconstpointer peer_address,
                     guint32       flags,
-                    int           scope,
+                    guint8        scope,
                     guint32       lifetime,
                     guint32       preferred,
                     in_addr_t     ip4_broadcast_address,
@@ -4715,6 +5026,7 @@ _nl_msg_new_address(int           nlmsg_type,
                      .ifa_index     = ifindex,
                      .ifa_prefixlen = plen,
                      .ifa_flags     = flags,
+                     .ifa_scope     = scope,
     };
     gsize addr_len;
 
@@ -4723,15 +5035,6 @@ _nl_msg_new_address(int           nlmsg_type,
 
     msg = nlmsg_alloc_simple(nlmsg_type, nlmsg_flags);
 
-    if (scope == -1) {
-        /* Allow having scope unset, and detect the scope (including IPv4 compatibility hack). */
-        if (family == AF_INET && address && *((char *) address) == 127)
-            scope = RT_SCOPE_HOST;
-        else
-            scope = RT_SCOPE_UNIVERSE;
-    }
-    am.ifa_scope = scope,
-
     addr_len = family == AF_INET ? sizeof(in_addr_t) : sizeof(struct in6_addr);
 
     if (nlmsg_append_struct(msg, &am) < 0)
@@ -4783,7 +5086,8 @@ ip_route_get_lock_flag(const NMPlatformIPRoute *route)
            | (((guint32) route->lock_cwnd) << RTAX_CWND)
            | (((guint32) route->lock_initcwnd) << RTAX_INITCWND)
            | (((guint32) route->lock_initrwnd) << RTAX_INITRWND)
-           | (((guint32) route->lock_mtu) << RTAX_MTU);
+           | (((guint32) route->lock_mtu) << RTAX_MTU)
+           | (((guint32) route->lock_mss) << RTAX_ADVMSS);
 }
 
 static gboolean
@@ -4809,7 +5113,7 @@ ip_route_ignored_protocol(const NMPlatformIPRoute *route)
 
 /* Copied and modified from libnl3's build_route_msg() and rtnl_route_build_msg(). */
 static struct nl_msg *
-_nl_msg_new_route(int nlmsg_type, guint16 nlmsgflags, const NMPObject *obj)
+_nl_msg_new_route(uint16_t nlmsg_type, uint16_t nlmsg_flags, const NMPObject *obj)
 {
     nm_auto_nlmsg struct nl_msg *msg     = NULL;
     const NMPClass              *klass   = NMP_OBJECT_GET_CLASS(obj);
@@ -4836,7 +5140,7 @@ _nl_msg_new_route(int nlmsg_type, guint16 nlmsgflags, const NMPObject *obj)
         NM_IN_SET(NMP_OBJECT_GET_TYPE(obj), NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE));
     nm_assert(NM_IN_SET(nlmsg_type, RTM_NEWROUTE, RTM_DELROUTE));
 
-    msg = nlmsg_alloc_simple(nlmsg_type, (int) nlmsgflags);
+    msg = nlmsg_alloc_simple(nlmsg_type, nlmsg_flags);
 
     if (nlmsg_append_struct(msg, &rtmsg) < 0)
         goto nla_put_failure;
@@ -4871,7 +5175,8 @@ _nl_msg_new_route(int nlmsg_type, guint16 nlmsgflags, const NMPObject *obj)
     }
 
     if (obj->ip_route.mss || obj->ip_route.window || obj->ip_route.cwnd || obj->ip_route.initcwnd
-        || obj->ip_route.initrwnd || obj->ip_route.mtu || lock) {
+        || obj->ip_route.initrwnd || obj->ip_route.mtu || obj->ip_route.quickack
+        || obj->ip_route.rto_min || lock) {
         struct nlattr *metrics;
 
         metrics = nla_nest_start(msg, RTA_METRICS);
@@ -4890,6 +5195,10 @@ _nl_msg_new_route(int nlmsg_type, guint16 nlmsgflags, const NMPObject *obj)
             NLA_PUT_U32(msg, RTAX_INITRWND, obj->ip_route.initrwnd);
         if (obj->ip_route.mtu)
             NLA_PUT_U32(msg, RTAX_MTU, obj->ip_route.mtu);
+        if (obj->ip_route.rto_min)
+            NLA_PUT_U32(msg, RTAX_RTO_MIN, obj->ip_route.rto_min);
+        if (obj->ip_route.quickack)
+            NLA_PUT_U32(msg, RTAX_QUICKACK, obj->ip_route.quickack);
         if (lock)
             NLA_PUT_U32(msg, RTAX_LOCK, lock);
 
@@ -4915,7 +5224,9 @@ nla_put_failure:
 }
 
 static struct nl_msg *
-_nl_msg_new_routing_rule(int nlmsg_type, int nlmsg_flags, const NMPlatformRoutingRule *routing_rule)
+_nl_msg_new_routing_rule(uint16_t                     nlmsg_type,
+                         uint16_t                     nlmsg_flags,
+                         const NMPlatformRoutingRule *routing_rule)
 {
     nm_auto_nlmsg struct nl_msg *msg = NULL;
     const guint8 addr_size           = nm_utils_addr_family_to_size(routing_rule->addr_family);
@@ -5028,7 +5339,7 @@ nla_put_failure:
 }
 
 static struct nl_msg *
-_nl_msg_new_qdisc(int nlmsg_type, int nlmsg_flags, const NMPlatformQdisc *qdisc)
+_nl_msg_new_qdisc(uint16_t nlmsg_type, uint16_t nlmsg_flags, const NMPlatformQdisc *qdisc)
 {
     nm_auto_nlmsg struct nl_msg *msg = NULL;
     struct nlattr               *tc_options;
@@ -5115,7 +5426,7 @@ nla_put_failure:
 }
 
 static struct nl_msg *
-_nl_msg_new_tfilter(int nlmsg_type, int nlmsg_flags, const NMPlatformTfilter *tfilter)
+_nl_msg_new_tfilter(uint16_t nlmsg_type, uint16_t nlmsg_flags, const NMPlatformTfilter *tfilter)
 {
     nm_auto_nlmsg struct nl_msg *msg = NULL;
     struct nlattr               *tc_options;
@@ -5154,7 +5465,7 @@ _nl_msg_new_tfilter(int nlmsg_type, int nlmsg_flags, const NMPlatformTfilter *tf
         if (nm_streq(action->kind, NM_PLATFORM_ACTION_KIND_SIMPLE)) {
             const NMPlatformActionSimple *simple = &action->simple;
             struct tc_defact              sel    = {
-                0,
+                                0,
             };
 
             if (!(act_options = nla_nest_start(msg, TCA_ACT_OPTIONS)))
@@ -5168,7 +5479,7 @@ _nl_msg_new_tfilter(int nlmsg_type, int nlmsg_flags, const NMPlatformTfilter *tf
         } else if (nm_streq(action->kind, NM_PLATFORM_ACTION_KIND_MIRRED)) {
             const NMPlatformActionMirred *mirred = &action->mirred;
             struct tc_mirred              sel    = {
-                0,
+                                0,
             };
 
             if (!(act_options = nla_nest_start(msg, TCA_ACT_OPTIONS)))
@@ -5204,14 +5515,6 @@ nla_put_failure:
 
 /*****************************************************************************/
 
-static struct nl_sock *
-_genl_sock(NMLinuxPlatform *platform)
-{
-    NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
-
-    return priv->genl;
-}
-
 #define ASSERT_SYSCTL_ARGS(pathid, dirfd, path)                                                 \
     G_STMT_START                                                                                \
     {                                                                                           \
@@ -5781,7 +6084,10 @@ sysctl_get(NMPlatform *platform, const char *pathid, int dirfd, const char *path
 static void
 process_events(NMPlatform *platform)
 {
-    delayed_action_handle_all(platform, TRUE);
+    delayed_action_schedule(platform,
+                            DELAYED_ACTION_TYPE_READ_RTNL | DELAYED_ACTION_TYPE_READ_GENL,
+                            NULL);
+    delayed_action_handle_all(platform);
 }
 
 /*****************************************************************************/
@@ -5789,27 +6095,34 @@ process_events(NMPlatform *platform)
 static const RefreshAllInfo *
 refresh_all_type_get_info(RefreshAllType refresh_all_type)
 {
+#define R(_protocol, _refresh_all_type, _obj_type, _addr_family) \
+    [_refresh_all_type] = {                                      \
+        .protocol             = _protocol,                       \
+        .obj_type             = _obj_type,                       \
+        .addr_family_for_dump = _addr_family,                    \
+    }
+#define R_ROUTE(...)   R(NMP_NETLINK_ROUTE, __VA_ARGS__)
+#define R_GENERIC(...) R(NMP_NETLINK_GENERIC, __VA_ARGS__)
     static const RefreshAllInfo infos[] = {
-#define R(_refresh_all_type, _obj_type, _addr_family) \
-    [_refresh_all_type] = {                           \
-        .obj_type    = _obj_type,                     \
-        .addr_family = _addr_family,                  \
-    }
-        R(REFRESH_ALL_TYPE_LINKS, NMP_OBJECT_TYPE_LINK, AF_UNSPEC),
-        R(REFRESH_ALL_TYPE_IP4_ADDRESSES, NMP_OBJECT_TYPE_IP4_ADDRESS, AF_UNSPEC),
-        R(REFRESH_ALL_TYPE_IP6_ADDRESSES, NMP_OBJECT_TYPE_IP6_ADDRESS, AF_UNSPEC),
-        R(REFRESH_ALL_TYPE_IP4_ROUTES, NMP_OBJECT_TYPE_IP4_ROUTE, AF_UNSPEC),
-        R(REFRESH_ALL_TYPE_IP6_ROUTES, NMP_OBJECT_TYPE_IP6_ROUTE, AF_UNSPEC),
-        R(REFRESH_ALL_TYPE_ROUTING_RULES_IP4, NMP_OBJECT_TYPE_ROUTING_RULE, AF_INET),
-        R(REFRESH_ALL_TYPE_ROUTING_RULES_IP6, NMP_OBJECT_TYPE_ROUTING_RULE, AF_INET6),
-        R(REFRESH_ALL_TYPE_QDISCS, NMP_OBJECT_TYPE_QDISC, AF_UNSPEC),
-        R(REFRESH_ALL_TYPE_TFILTERS, NMP_OBJECT_TYPE_TFILTER, AF_UNSPEC),
-#undef R
+        R_ROUTE(REFRESH_ALL_TYPE_RTNL_LINKS, NMP_OBJECT_TYPE_LINK, AF_UNSPEC),
+        R_ROUTE(REFRESH_ALL_TYPE_RTNL_IP4_ADDRESSES, NMP_OBJECT_TYPE_IP4_ADDRESS, AF_UNSPEC),
+        R_ROUTE(REFRESH_ALL_TYPE_RTNL_IP6_ADDRESSES, NMP_OBJECT_TYPE_IP6_ADDRESS, AF_UNSPEC),
+        R_ROUTE(REFRESH_ALL_TYPE_RTNL_IP4_ROUTES, NMP_OBJECT_TYPE_IP4_ROUTE, AF_UNSPEC),
+        R_ROUTE(REFRESH_ALL_TYPE_RTNL_IP6_ROUTES, NMP_OBJECT_TYPE_IP6_ROUTE, AF_UNSPEC),
+        R_ROUTE(REFRESH_ALL_TYPE_RTNL_ROUTING_RULES_IP4, NMP_OBJECT_TYPE_ROUTING_RULE, AF_INET),
+        R_ROUTE(REFRESH_ALL_TYPE_RTNL_ROUTING_RULES_IP6, NMP_OBJECT_TYPE_ROUTING_RULE, AF_INET6),
+        R_ROUTE(REFRESH_ALL_TYPE_RTNL_QDISCS, NMP_OBJECT_TYPE_QDISC, AF_UNSPEC),
+        R_ROUTE(REFRESH_ALL_TYPE_RTNL_TFILTERS, NMP_OBJECT_TYPE_TFILTER, AF_UNSPEC),
+        R_GENERIC(REFRESH_ALL_TYPE_GENL_FAMILIES, NMP_OBJECT_TYPE_UNKNOWN, AF_UNSPEC),
     };
+#undef R_GENERIC
+#undef R_ROUTE
+#undef R
 
     nm_assert(_NM_INT_NOT_NEGATIVE(refresh_all_type));
     nm_assert(refresh_all_type < G_N_ELEMENTS(infos));
-    nm_assert(nmp_class_from_type(infos[refresh_all_type].obj_type));
+    nm_assert(refresh_all_type == REFRESH_ALL_TYPE_GENL_FAMILIES
+              || nmp_class_from_type(infos[refresh_all_type].obj_type));
 
     return &infos[refresh_all_type];
 }
@@ -5819,19 +6132,24 @@ static NM_UTILS_LOOKUP_DEFINE(
     DelayedActionType,
     RefreshAllType,
     NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT(0),
-    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS, REFRESH_ALL_TYPE_LINKS),
-    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES,
-                         REFRESH_ALL_TYPE_IP4_ADDRESSES),
-    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES,
-                         REFRESH_ALL_TYPE_IP6_ADDRESSES),
-    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES, REFRESH_ALL_TYPE_IP4_ROUTES),
-    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES, REFRESH_ALL_TYPE_IP6_ROUTES),
-    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP4,
-                         REFRESH_ALL_TYPE_ROUTING_RULES_IP4),
-    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP6,
-                         REFRESH_ALL_TYPE_ROUTING_RULES_IP6),
-    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS, REFRESH_ALL_TYPE_QDISCS),
-    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS, REFRESH_ALL_TYPE_TFILTERS),
+    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_LINKS, REFRESH_ALL_TYPE_RTNL_LINKS),
+    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP4_ADDRESSES,
+                         REFRESH_ALL_TYPE_RTNL_IP4_ADDRESSES),
+    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP6_ADDRESSES,
+                         REFRESH_ALL_TYPE_RTNL_IP6_ADDRESSES),
+    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP4_ROUTES,
+                         REFRESH_ALL_TYPE_RTNL_IP4_ROUTES),
+    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP6_ROUTES,
+                         REFRESH_ALL_TYPE_RTNL_IP6_ROUTES),
+    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_IP4,
+                         REFRESH_ALL_TYPE_RTNL_ROUTING_RULES_IP4),
+    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_IP6,
+                         REFRESH_ALL_TYPE_RTNL_ROUTING_RULES_IP6),
+    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_QDISCS, REFRESH_ALL_TYPE_RTNL_QDISCS),
+    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_TFILTERS,
+                         REFRESH_ALL_TYPE_RTNL_TFILTERS),
+    NM_UTILS_LOOKUP_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_GENL_FAMILIES,
+                         REFRESH_ALL_TYPE_GENL_FAMILIES),
     NM_UTILS_LOOKUP_ITEM_IGNORE_OTHER(), );
 
 static DelayedActionType
@@ -5853,25 +6171,25 @@ refresh_all_type_from_needle_object(const NMPObject *obj_needle)
 {
     switch (NMP_OBJECT_GET_TYPE(obj_needle)) {
     case NMP_OBJECT_TYPE_LINK:
-        return REFRESH_ALL_TYPE_LINKS;
+        return REFRESH_ALL_TYPE_RTNL_LINKS;
     case NMP_OBJECT_TYPE_IP4_ADDRESS:
-        return REFRESH_ALL_TYPE_IP4_ADDRESSES;
+        return REFRESH_ALL_TYPE_RTNL_IP4_ADDRESSES;
     case NMP_OBJECT_TYPE_IP6_ADDRESS:
-        return REFRESH_ALL_TYPE_IP6_ADDRESSES;
+        return REFRESH_ALL_TYPE_RTNL_IP6_ADDRESSES;
     case NMP_OBJECT_TYPE_IP4_ROUTE:
-        return REFRESH_ALL_TYPE_IP4_ROUTES;
+        return REFRESH_ALL_TYPE_RTNL_IP4_ROUTES;
     case NMP_OBJECT_TYPE_IP6_ROUTE:
-        return REFRESH_ALL_TYPE_IP6_ROUTES;
+        return REFRESH_ALL_TYPE_RTNL_IP6_ROUTES;
     case NMP_OBJECT_TYPE_QDISC:
-        return REFRESH_ALL_TYPE_QDISCS;
+        return REFRESH_ALL_TYPE_RTNL_QDISCS;
     case NMP_OBJECT_TYPE_TFILTER:
-        return REFRESH_ALL_TYPE_TFILTERS;
+        return REFRESH_ALL_TYPE_RTNL_TFILTERS;
     case NMP_OBJECT_TYPE_ROUTING_RULE:
         switch (NMP_OBJECT_CAST_ROUTING_RULE(obj_needle)->addr_family) {
         case AF_INET:
-            return REFRESH_ALL_TYPE_ROUTING_RULES_IP4;
+            return REFRESH_ALL_TYPE_RTNL_ROUTING_RULES_IP4;
         case AF_INET6:
-            return REFRESH_ALL_TYPE_ROUTING_RULES_IP6;
+            return REFRESH_ALL_TYPE_RTNL_ROUTING_RULES_IP6;
         }
         nm_assert_not_reached();
         return 0;
@@ -5895,11 +6213,11 @@ refresh_all_type_init_lookup(RefreshAllType refresh_all_type, NMPLookup *lookup)
     if (NM_IN_SET(refresh_all_info->obj_type, NMP_OBJECT_TYPE_ROUTING_RULE)) {
         return nmp_lookup_init_object_by_addr_family(lookup,
                                                      refresh_all_info->obj_type,
-                                                     refresh_all_info->addr_family);
+                                                     refresh_all_info->addr_family_for_dump);
     }
 
     /* not yet implemented. */
-    nm_assert(refresh_all_info->addr_family == AF_UNSPEC);
+    nm_assert(refresh_all_info->addr_family_for_dump == AF_UNSPEC);
 
     return nmp_lookup_init_obj_type(lookup, refresh_all_info->obj_type);
 }
@@ -5915,28 +6233,42 @@ static NM_UTILS_LOOKUP_STR_DEFINE(
     delayed_action_to_string,
     DelayedActionType,
     NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT("unknown"),
-    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS, "refresh-all-links"),
-    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES,
-                             "refresh-all-ip4-addresses"),
-    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES,
-                             "refresh-all-ip6-addresses"),
-    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES, "refresh-all-ip4-routes"),
-    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES, "refresh-all-ip6-routes"),
-    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP4,
-                             "refresh-all-routing-rules-ip4"),
-    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP6,
-                             "refresh-all-routing-rules-ip6"),
-    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS, "refresh-all-qdiscs"),
-    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS, "refresh-all-tfilters"),
+    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_LINKS, "refresh-all-rtnl-links"),
+    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP4_ADDRESSES,
+                             "refresh-all-rtnl-ip4-addresses"),
+    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP6_ADDRESSES,
+                             "refresh-all-rtnl-ip6-addresses"),
+    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP4_ROUTES,
+                             "refresh-all-rtnl-ip4-routes"),
+    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP6_ROUTES,
+                             "refresh-all-rtnl-ip6-routes"),
+    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_IP4,
+                             "refresh-all-rtnl-routing-rules-ip4"),
+    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_IP6,
+                             "refresh-all-rtnl-routing-rules-ip6"),
+    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_QDISCS,
+                             "refresh-all-rtnl-qdiscs"),
+    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_TFILTERS,
+                             "refresh-all-rtnl-tfilters"),
+    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_ALL_GENL_FAMILIES,
+                             "refresh-all-genl-families"),
     NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_REFRESH_LINK, "refresh-link"),
     NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_MASTER_CONNECTED, "master-connected"),
-    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_READ_NETLINK, "read-netlink"),
-    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE, "wait-for-nl-response"),
+    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_READ_RTNL, "read-rtnl"),
+    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_READ_GENL, "read-genl"),
+    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_WAIT_FOR_RESPONSE_RTNL, "wait-for-response-rtnl"),
+    NM_UTILS_LOOKUP_STR_ITEM(DELAYED_ACTION_TYPE_WAIT_FOR_RESPONSE_GENL, "wait-for-response-genl"),
     NM_UTILS_LOOKUP_ITEM_IGNORE(DELAYED_ACTION_TYPE_NONE),
-    NM_UTILS_LOOKUP_ITEM_IGNORE(DELAYED_ACTION_TYPE_REFRESH_ALL),
-    NM_UTILS_LOOKUP_ITEM_IGNORE(DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL),
+    NM_UTILS_LOOKUP_ITEM_IGNORE(DELAYED_ACTION_TYPE_REFRESH_RTNL_ALL),
+    NM_UTILS_LOOKUP_ITEM_IGNORE(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_ALL),
     NM_UTILS_LOOKUP_ITEM_IGNORE(__DELAYED_ACTION_TYPE_MAX), );
 
+#define delayed_action_get_list_wait_for_resonse(priv, netlink_protocol, idx)                   \
+    (&g_array_index((priv)->delayed_action.list_wait_for_response_x[nmp_netlink_protocol_check( \
+                        (netlink_protocol))],                                                   \
+                    DelayedActionWaitForNlResponseData,                                         \
+                    (idx)))
+
 static const char *
 delayed_action_to_string_full(DelayedActionType action_type,
                               gpointer          user_data,
@@ -5947,6 +6279,7 @@ delayed_action_to_string_full(DelayedActionType action_type,
     const DelayedActionWaitForNlResponseData *data;
 
     nm_strbuf_append_str(&buf, &buf_size, delayed_action_to_string(action_type));
+
     switch (action_type) {
     case DELAYED_ACTION_TYPE_MASTER_CONNECTED:
         nm_strbuf_append(&buf, &buf_size, " (master-ifindex %d)", GPOINTER_TO_INT(user_data));
@@ -5954,7 +6287,8 @@ delayed_action_to_string_full(DelayedActionType action_type,
     case DELAYED_ACTION_TYPE_REFRESH_LINK:
         nm_strbuf_append(&buf, &buf_size, " (ifindex %d)", GPOINTER_TO_INT(user_data));
         break;
-    case DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE:
+    case DELAYED_ACTION_TYPE_WAIT_FOR_RESPONSE_RTNL:
+    case DELAYED_ACTION_TYPE_WAIT_FOR_RESPONSE_GENL:
         data = user_data;
 
         if (data) {
@@ -6005,8 +6339,8 @@ delayed_action_refresh_all_in_progress(NMPlatform *platform, DelayedActionType a
     RefreshAllType          refresh_all_type;
 
     nm_assert(nm_utils_is_power_of_two(action_type));
-    nm_assert(NM_FLAGS_ANY(action_type, DELAYED_ACTION_TYPE_REFRESH_ALL));
-    nm_assert(!NM_FLAGS_ANY(action_type, ~DELAYED_ACTION_TYPE_REFRESH_ALL));
+    nm_assert(NM_FLAGS_ANY(action_type, DELAYED_ACTION_TYPE_REFRESH_RTNL_ALL));
+    nm_assert(!NM_FLAGS_ANY(action_type, ~DELAYED_ACTION_TYPE_REFRESH_RTNL_ALL));
 
     if (NM_FLAGS_ANY(priv->delayed_action.flags, action_type))
         return TRUE;
@@ -6016,25 +6350,26 @@ delayed_action_refresh_all_in_progress(NMPlatform *platform, DelayedActionType a
 }
 
 static void
-delayed_action_wait_for_nl_response_complete(NMPlatform             *platform,
-                                             guint                   idx,
-                                             WaitForNlResponseResult seq_result)
+delayed_action_wait_for_response_complete(NMPlatform             *platform,
+                                          NMPNetlinkProtocol      netlink_protocol,
+                                          guint                   idx,
+                                          WaitForNlResponseResult seq_result)
 {
     NMLinuxPlatformPrivate             *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
     DelayedActionWaitForNlResponseData *data;
+    const DelayedActionType             ACTION_TYPE =
+        nmp_netlink_protocol_info(netlink_protocol)->delayed_action_type_wait_for_response;
 
-    nm_assert(NM_FLAGS_HAS(priv->delayed_action.flags, DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE));
-    nm_assert(idx < priv->delayed_action.list_wait_for_nl_response->len);
+    nm_assert(NM_FLAGS_ANY(priv->delayed_action.flags, ACTION_TYPE));
+    nm_assert(idx < priv->delayed_action.list_wait_for_response_x[netlink_protocol]->len);
     nm_assert(seq_result);
 
-    data = &g_array_index(priv->delayed_action.list_wait_for_nl_response,
-                          DelayedActionWaitForNlResponseData,
-                          idx);
+    data = delayed_action_get_list_wait_for_resonse(priv, netlink_protocol, idx);
 
-    _LOGt_delayed_action(DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE, data, "complete");
+    _LOGt_delayed_action(ACTION_TYPE, data, "complete");
 
-    if (priv->delayed_action.list_wait_for_nl_response->len <= 1)
-        priv->delayed_action.flags &= ~DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE;
+    if (priv->delayed_action.list_wait_for_response_x[netlink_protocol]->len <= 1)
+        priv->delayed_action.flags &= ~ACTION_TYPE;
     if (data->out_seq_result)
         *data->out_seq_result = seq_result;
     switch (data->response_type) {
@@ -6055,15 +6390,16 @@ delayed_action_wait_for_nl_response_complete(NMPlatform             *platform,
         break;
     }
 
-    g_array_remove_index_fast(priv->delayed_action.list_wait_for_nl_response, idx);
+    g_array_remove_index_fast(priv->delayed_action.list_wait_for_response_x[netlink_protocol], idx);
 }
 
 static void
-delayed_action_wait_for_nl_response_complete_check(NMPlatform             *platform,
-                                                   WaitForNlResponseResult force_result,
-                                                   guint32                *out_next_seq_number,
-                                                   gint64                 *out_next_timeout_abs_ns,
-                                                   gint64                 *p_now_nsec)
+delayed_action_wait_for_response_complete_check(NMPlatform             *platform,
+                                                NMPNetlinkProtocol      netlink_protocol,
+                                                WaitForNlResponseResult force_result,
+                                                guint32                *out_next_seq_number,
+                                                gint64                 *out_next_timeout_abs_ns,
+                                                gint64                 *p_now_nsec)
 {
     NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
     guint                   i;
@@ -6071,24 +6407,25 @@ delayed_action_wait_for_nl_response_complete_check(NMPlatform             *platf
     gint64                  next_timeout_abs_ns = 0;
     gint64                  now_nsec            = 0;
 
-    for (i = 0; i < priv->delayed_action.list_wait_for_nl_response->len;) {
+    for (i = 0; i < priv->delayed_action.list_wait_for_response_x[netlink_protocol]->len;) {
         const DelayedActionWaitForNlResponseData *data =
-            &g_array_index(priv->delayed_action.list_wait_for_nl_response,
-                           DelayedActionWaitForNlResponseData,
-                           i);
+            delayed_action_get_list_wait_for_resonse(priv, netlink_protocol, i);
 
         if (data->seq_result)
-            delayed_action_wait_for_nl_response_complete(platform, i, data->seq_result);
+            delayed_action_wait_for_response_complete(platform,
+                                                      netlink_protocol,
+                                                      i,
+                                                      data->seq_result);
         else if (p_now_nsec
                  && ((now_nsec ?: (now_nsec = nm_utils_get_monotonic_timestamp_nsec()))
                      >= data->timeout_abs_nsec)) {
             /* the caller can optionally check for timeout by providing a p_now_nsec argument. */
-            delayed_action_wait_for_nl_response_complete(
-                platform,
-                i,
-                WAIT_FOR_NL_RESPONSE_RESULT_FAILED_TIMEOUT);
+            delayed_action_wait_for_response_complete(platform,
+                                                      netlink_protocol,
+                                                      i,
+                                                      WAIT_FOR_NL_RESPONSE_RESULT_FAILED_TIMEOUT);
         } else if (force_result != WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN)
-            delayed_action_wait_for_nl_response_complete(platform, i, force_result);
+            delayed_action_wait_for_response_complete(platform, netlink_protocol, i, force_result);
         else {
             if (next_seq_number == 0 || next_timeout_abs_ns > data->timeout_abs_nsec) {
                 next_seq_number     = data->seq_number;
@@ -6099,9 +6436,10 @@ delayed_action_wait_for_nl_response_complete_check(NMPlatform             *platf
     }
 
     if (force_result != WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN) {
-        nm_assert(
-            !NM_FLAGS_HAS(priv->delayed_action.flags, DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE));
-        nm_assert(priv->delayed_action.list_wait_for_nl_response->len == 0);
+        nm_assert(!NM_FLAGS_ANY(
+            priv->delayed_action.flags,
+            nmp_netlink_protocol_info(netlink_protocol)->delayed_action_type_wait_for_response));
+        nm_assert(priv->delayed_action.list_wait_for_response_x[netlink_protocol]->len == 0);
     }
 
     NM_SET_OUT(out_next_seq_number, next_seq_number);
@@ -6111,9 +6449,15 @@ delayed_action_wait_for_nl_response_complete_check(NMPlatform             *platf
 
 static void
 delayed_action_wait_for_nl_response_complete_all(NMPlatform             *platform,
+                                                 NMPNetlinkProtocol      netlink_protocol,
                                                  WaitForNlResponseResult fallback_result)
 {
-    delayed_action_wait_for_nl_response_complete_check(platform, fallback_result, NULL, NULL, NULL);
+    delayed_action_wait_for_response_complete_check(platform,
+                                                    nmp_netlink_protocol_check(netlink_protocol),
+                                                    fallback_result,
+                                                    NULL,
+                                                    NULL,
+                                                    NULL);
 }
 
 /*****************************************************************************/
@@ -6148,15 +6492,15 @@ delayed_action_handle_REFRESH_ALL(NMPlatform *platform, DelayedActionType flags)
 }
 
 static void
-delayed_action_handle_READ_NETLINK(NMPlatform *platform)
+delayed_action_handle_READ_NETLINK(NMPlatform *platform, NMPNetlinkProtocol netlink_protocol)
 {
-    event_handler_read_netlink(platform, FALSE);
+    event_handler_read_netlink(platform, netlink_protocol, FALSE);
 }
 
 static void
-delayed_action_handle_WAIT_FOR_NL_RESPONSE(NMPlatform *platform)
+delayed_action_handle_WAIT_FOR_RESPONSE(NMPlatform *platform, NMPNetlinkProtocol netlink_protocol)
 {
-    event_handler_read_netlink(platform, TRUE);
+    event_handler_read_netlink(platform, netlink_protocol, TRUE);
 }
 
 static gboolean
@@ -6164,6 +6508,8 @@ delayed_action_handle_one(NMPlatform *platform)
 {
     NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
     gpointer                user_data;
+    NMPNetlinkProtocol      netlink_protocol;
+    DelayedActionType       iflags;
 
     if (priv->delayed_action.flags == DELAYED_ACTION_TYPE_NONE)
         return FALSE;
@@ -6190,28 +6536,46 @@ delayed_action_handle_one(NMPlatform *platform)
     }
     nm_assert(priv->delayed_action.list_master_connected->len == 0);
 
-    /* Next we prefer read-netlink, because the buffer size is limited and we want to process events
+    /* Next we prefer read-genl/read-rtnl, because the buffer size is limited and we want to process events
      * from netlink early. */
-    if (NM_FLAGS_HAS(priv->delayed_action.flags, DELAYED_ACTION_TYPE_READ_NETLINK)) {
-        _LOGt_delayed_action(DELAYED_ACTION_TYPE_READ_NETLINK, NULL, "handle");
-        priv->delayed_action.flags &= ~DELAYED_ACTION_TYPE_READ_NETLINK;
-        delayed_action_handle_READ_NETLINK(platform);
-        return TRUE;
+    for (netlink_protocol = _NMP_NETLINK_FIRST; netlink_protocol < _NMP_NETLINK_NUM;
+         netlink_protocol++) {
+        const DelayedActionType ACTION_TYPE =
+            nmp_netlink_protocol_info(netlink_protocol)->delayed_action_type_read;
+
+        if (NM_FLAGS_ANY(priv->delayed_action.flags, ACTION_TYPE)) {
+            _LOGt_delayed_action(ACTION_TYPE, NULL, "handle");
+            priv->delayed_action.flags &= ~ACTION_TYPE;
+            delayed_action_handle_READ_NETLINK(platform, netlink_protocol);
+            return TRUE;
+        }
     }
 
-    if (NM_FLAGS_ANY(priv->delayed_action.flags, DELAYED_ACTION_TYPE_REFRESH_ALL)) {
-        DelayedActionType flags, iflags;
+    if (NM_FLAGS_ANY(priv->delayed_action.flags, DELAYED_ACTION_TYPE_REFRESH_GENL_ALL)) {
+        const DelayedActionType FLAGS =
+            priv->delayed_action.flags & DELAYED_ACTION_TYPE_REFRESH_GENL_ALL;
 
-        flags = priv->delayed_action.flags & DELAYED_ACTION_TYPE_REFRESH_ALL;
+        if (_LOGt_ENABLED()) {
+            FOR_EACH_DELAYED_ACTION (iflags, FLAGS)
+                _LOGt_delayed_action(iflags, NULL, "handle");
+        }
 
-        priv->delayed_action.flags &= ~DELAYED_ACTION_TYPE_REFRESH_ALL;
+        priv->delayed_action.flags &= ~FLAGS;
+        delayed_action_handle_REFRESH_ALL(platform, FLAGS);
+        return TRUE;
+    }
+
+    if (NM_FLAGS_ANY(priv->delayed_action.flags, DELAYED_ACTION_TYPE_REFRESH_RTNL_ALL)) {
+        const DelayedActionType FLAGS =
+            (priv->delayed_action.flags & DELAYED_ACTION_TYPE_REFRESH_RTNL_ALL);
 
         if (_LOGt_ENABLED()) {
-            FOR_EACH_DELAYED_ACTION (iflags, flags)
+            FOR_EACH_DELAYED_ACTION (iflags, FLAGS)
                 _LOGt_delayed_action(iflags, NULL, "handle");
         }
 
-        delayed_action_handle_REFRESH_ALL(platform, flags);
+        priv->delayed_action.flags &= ~FLAGS;
+        delayed_action_handle_REFRESH_ALL(platform, FLAGS);
         return TRUE;
     }
 
@@ -6235,18 +6599,24 @@ delayed_action_handle_one(NMPlatform *platform)
         return TRUE;
     }
 
-    if (NM_FLAGS_HAS(priv->delayed_action.flags, DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE)) {
-        nm_assert(priv->delayed_action.list_wait_for_nl_response->len > 0);
-        _LOGt_delayed_action(DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE, NULL, "handle");
-        delayed_action_handle_WAIT_FOR_NL_RESPONSE(platform);
-        return TRUE;
+    for (netlink_protocol = _NMP_NETLINK_FIRST; netlink_protocol < _NMP_NETLINK_NUM;
+         netlink_protocol++) {
+        const DelayedActionType ACTION_TYPE =
+            nmp_netlink_protocol_info(netlink_protocol)->delayed_action_type_wait_for_response;
+
+        if (NM_FLAGS_ANY(priv->delayed_action.flags, ACTION_TYPE)) {
+            nm_assert(priv->delayed_action.list_wait_for_response_x[netlink_protocol]->len > 0);
+            _LOGt_delayed_action(ACTION_TYPE, NULL, "handle");
+            delayed_action_handle_WAIT_FOR_RESPONSE(platform, netlink_protocol);
+            return TRUE;
+        }
     }
 
     return FALSE;
 }
 
 static gboolean
-delayed_action_handle_all(NMPlatform *platform, gboolean read_netlink)
+delayed_action_handle_all(NMPlatform *platform)
 {
     NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
     gboolean                any  = FALSE;
@@ -6254,8 +6624,6 @@ delayed_action_handle_all(NMPlatform *platform, gboolean read_netlink)
     g_return_val_if_fail(priv->delayed_action.is_handling == 0, FALSE);
 
     priv->delayed_action.is_handling++;
-    if (read_netlink)
-        delayed_action_schedule(platform, DELAYED_ACTION_TYPE_READ_NETLINK, NULL);
     while (delayed_action_handle_one(platform))
         any = TRUE;
     priv->delayed_action.is_handling--;
@@ -6290,14 +6658,21 @@ delayed_action_schedule(NMPlatform *platform, DelayedActionType action_type, gpo
             < 0)
             g_ptr_array_add(priv->delayed_action.list_master_connected, user_data);
         break;
-    case DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE:
-        g_array_append_vals(priv->delayed_action.list_wait_for_nl_response, user_data, 1);
+    case DELAYED_ACTION_TYPE_WAIT_FOR_RESPONSE_RTNL:
+        g_array_append_vals(priv->delayed_action.list_wait_for_response_rtnl, user_data, 1);
+        break;
+    case DELAYED_ACTION_TYPE_WAIT_FOR_RESPONSE_GENL:
+        g_array_append_vals(priv->delayed_action.list_wait_for_response_genl, user_data, 1);
         break;
     default:
+        /* For other action types, we support setting multiple flags at once. They
+         * also don't have any user-data. */
         nm_assert(!user_data);
-        nm_assert(!NM_FLAGS_HAS(action_type, DELAYED_ACTION_TYPE_REFRESH_LINK));
-        nm_assert(!NM_FLAGS_HAS(action_type, DELAYED_ACTION_TYPE_MASTER_CONNECTED));
-        nm_assert(!NM_FLAGS_HAS(action_type, DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE));
+        nm_assert(!NM_FLAGS_ANY(action_type,
+                                DELAYED_ACTION_TYPE_REFRESH_LINK
+                                    | DELAYED_ACTION_TYPE_MASTER_CONNECTED
+                                    | DELAYED_ACTION_TYPE_WAIT_FOR_RESPONSE_RTNL
+                                    | DELAYED_ACTION_TYPE_WAIT_FOR_RESPONSE_GENL));
         break;
     }
 
@@ -6310,12 +6685,37 @@ delayed_action_schedule(NMPlatform *platform, DelayedActionType action_type, gpo
 }
 
 static void
-delayed_action_schedule_WAIT_FOR_NL_RESPONSE(NMPlatform                        *platform,
-                                             guint32                            seq_number,
-                                             WaitForNlResponseResult           *out_seq_result,
-                                             char                             **out_errmsg,
-                                             DelayedActionWaitForNlResponseType response_type,
-                                             gpointer                           response_out_data)
+delayed_action_schedule_refresh_all(NMPlatform *platform, NMPNetlinkProtocol netlink_protocol)
+{
+    DelayedActionType action_type;
+
+    if (netlink_protocol == NMP_NETLINK_ROUTE) {
+        action_type = DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_LINKS
+                      | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP4_ADDRESSES
+                      | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP6_ADDRESSES
+                      | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP4_ROUTES
+                      | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP6_ROUTES
+                      | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_ALL;
+        if (nm_platform_get_cache_tc(platform)) {
+            action_type |= (DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_QDISCS
+                            | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_TFILTERS);
+        }
+    } else {
+        nm_assert(netlink_protocol == NMP_NETLINK_GENERIC);
+        action_type = DELAYED_ACTION_TYPE_REFRESH_ALL_GENL_FAMILIES;
+    }
+
+    delayed_action_schedule(platform, action_type, NULL);
+}
+
+static void
+delayed_action_schedule_WAIT_FOR_RESPONSE(NMPlatform                        *platform,
+                                          NMPNetlinkProtocol                 netlink_protocol,
+                                          guint32                            seq_number,
+                                          WaitForNlResponseResult           *out_seq_result,
+                                          char                             **out_errmsg,
+                                          DelayedActionWaitForNlResponseType response_type,
+                                          gpointer                           response_out_data)
 {
     DelayedActionWaitForNlResponseData data = {
         .seq_number = seq_number,
@@ -6327,7 +6727,10 @@ delayed_action_schedule_WAIT_FOR_NL_RESPONSE(NMPlatform                        *
         .response.out_data = response_out_data,
     };
 
-    delayed_action_schedule(platform, DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE, &data);
+    delayed_action_schedule(
+        platform,
+        nmp_netlink_protocol_info(netlink_protocol)->delayed_action_type_wait_for_response,
+        &data);
 }
 
 /*****************************************************************************/
@@ -6379,6 +6782,9 @@ cache_prune_all(NMPlatform *platform)
 
         if (priv->pruning[refresh_all_type] == 0)
             continue;
+
+        nm_assert(refresh_all_type != REFRESH_ALL_TYPE_GENL_FAMILIES);
+
         priv->pruning[refresh_all_type] -= 1;
         if (priv->pruning[refresh_all_type] > 0)
             continue;
@@ -6462,15 +6868,18 @@ cache_on_change(NMPlatform      *platform,
                 ifindex = obj_new->link.ifindex;
 
             if (ifindex > 0) {
-                delayed_action_schedule(platform,
-                                        DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES
-                                            | DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES
-                                            | DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES
-                                            | DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES
-                                            | DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL
-                                            | DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS
-                                            | DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS,
-                                        NULL);
+                delayed_action_schedule(
+                    platform,
+                    DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP4_ADDRESSES
+                        | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP6_ADDRESSES
+                        | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP4_ROUTES
+                        | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP6_ROUTES
+                        | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_ALL
+                        | (nm_platform_get_cache_tc(platform)
+                               ? (DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_QDISCS
+                                  | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_TFILTERS)
+                               : DELAYED_ACTION_TYPE_NONE),
+                    NULL);
             }
         }
         {
@@ -6518,8 +6927,8 @@ cache_on_change(NMPlatform      *platform,
                  * think kernel does send RTM_DELROUTE events for IPv6 routes, so
                  * we might not need to refresh IPv6 routes. */
                 delayed_action_schedule(platform,
-                                        DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES
-                                            | DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES,
+                                        DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP4_ROUTES
+                                            | DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP6_ROUTES,
                                         NULL);
             }
         }
@@ -6627,8 +7036,8 @@ cache_on_change(NMPlatform      *platform,
         if (cache_op == NMP_CACHE_OPS_REMOVED) {
             delayed_action_schedule(platform,
                                     (klass->obj_type == NMP_OBJECT_TYPE_IP4_ADDRESS)
-                                        ? DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES
-                                        : DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES,
+                                        ? DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP4_ROUTES
+                                        : DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_IP6_ROUTES,
                                     NULL);
         }
     } break;
@@ -6640,12 +7049,14 @@ cache_on_change(NMPlatform      *platform,
 /*****************************************************************************/
 
 static guint32
-_nlh_seq_next_get(NMLinuxPlatformPrivate *priv)
+_nlh_seq_next_get(NMLinuxPlatformPrivate *priv, NMPNetlinkProtocol netlink_protocol)
 {
+    guint32 *p = &priv->proto_data_x[netlink_protocol].nlh_seq_next;
+
     /* generate a new sequence number, but never return zero.
      * Wrapping numbers are not a problem, because we don't rely
      * on strictly increasing sequence numbers. */
-    return (++priv->nlh_seq_next) ?: (++priv->nlh_seq_next);
+    return (++(*p)) ?: (++(*p));
 }
 
 /**
@@ -6672,7 +7083,7 @@ _nl_send_nlmsghdr(NMPlatform                        *platform,
 
     nm_assert(nlhdr);
 
-    seq              = _nlh_seq_next_get(priv);
+    seq              = _nlh_seq_next_get(priv, NMP_NETLINK_ROUTE);
     nlhdr->nlmsg_seq = seq;
 
     {
@@ -6689,12 +7100,12 @@ _nl_send_nlmsghdr(NMPlatform                        *platform,
         int try_count;
 
         if (!nlhdr->nlmsg_pid)
-            nlhdr->nlmsg_pid = nl_socket_get_local_port(priv->nlh);
+            nlhdr->nlmsg_pid = nl_socket_get_local_port(priv->sk_rtnl);
         nlhdr->nlmsg_flags |= (NLM_F_REQUEST | NLM_F_ACK);
 
         try_count = 0;
 again:
-        errsv = sendmsg(nl_socket_get_fd(priv->nlh), &msg, 0);
+        errsv = sendmsg(nl_socket_get_fd(priv->sk_rtnl), &msg, 0);
         if (errsv < 0) {
             errsv = errno;
             if (errsv == EINTR && try_count++ < 100)
@@ -6706,32 +7117,24 @@ again:
         }
     }
 
-    delayed_action_schedule_WAIT_FOR_NL_RESPONSE(platform,
-                                                 seq,
-                                                 out_seq_result,
-                                                 out_errmsg,
-                                                 response_type,
-                                                 response_out_data);
+    delayed_action_schedule_WAIT_FOR_RESPONSE(platform,
+                                              NMP_NETLINK_ROUTE,
+                                              seq,
+                                              out_seq_result,
+                                              out_errmsg,
+                                              response_type,
+                                              response_out_data);
     return 0;
 }
 
-/**
- * _nl_send_nlmsg:
- * @platform:
- * @nlmsg:
- * @out_seq_result:
- * @response_type:
- * @response_out_data:
- *
- * Returns: 0 on success, or a negative libnl3 error code (beware, it's not an errno).
- */
 static int
-_nl_send_nlmsg(NMPlatform                        *platform,
-               struct nl_msg                     *nlmsg,
-               WaitForNlResponseResult           *out_seq_result,
-               char                             **out_errmsg,
-               DelayedActionWaitForNlResponseType response_type,
-               gpointer                           response_out_data)
+_netlink_send_nlmsg(NMPlatform                        *platform,
+                    NMPNetlinkProtocol                 netlink_protocol,
+                    struct nl_msg                     *nlmsg,
+                    WaitForNlResponseResult           *out_seq_result,
+                    char                             **out_errmsg,
+                    DelayedActionWaitForNlResponseType response_type,
+                    gpointer                           response_out_data)
 {
     NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
     struct nlmsghdr        *nlhdr;
@@ -6739,24 +7142,40 @@ _nl_send_nlmsg(NMPlatform                        *platform,
     int                     nle;
 
     nlhdr            = nlmsg_hdr(nlmsg);
-    seq              = _nlh_seq_next_get(priv);
+    seq              = _nlh_seq_next_get(priv, netlink_protocol);
     nlhdr->nlmsg_seq = seq;
 
-    nle = nl_send_auto(priv->nlh, nlmsg);
+    nle = nl_send_auto(priv->sk_x[netlink_protocol], nlmsg);
     if (nle < 0) {
         _LOGD("netlink: nl-send-nlmsg: failed sending message: %s (%d)", nm_strerror(nle), nle);
         return nle;
     }
 
-    delayed_action_schedule_WAIT_FOR_NL_RESPONSE(platform,
-                                                 seq,
-                                                 out_seq_result,
-                                                 out_errmsg,
-                                                 response_type,
-                                                 response_out_data);
+    delayed_action_schedule_WAIT_FOR_RESPONSE(platform,
+                                              netlink_protocol,
+                                              seq,
+                                              out_seq_result,
+                                              out_errmsg,
+                                              response_type,
+                                              response_out_data);
     return 0;
 }
 
+static int
+_netlink_send_nlmsg_rtnl(NMPlatform              *platform,
+                         struct nl_msg           *nlmsg,
+                         WaitForNlResponseResult *out_seq_result,
+                         char                   **out_errmsg)
+{
+    return _netlink_send_nlmsg(platform,
+                               NMP_NETLINK_ROUTE,
+                               nlmsg,
+                               out_seq_result,
+                               out_errmsg,
+                               DELAYED_ACTION_RESPONSE_TYPE_VOID,
+                               NULL);
+}
+
 static void
 do_request_link_no_delayed_actions(NMPlatform *platform, int ifindex, const char *name)
 {
@@ -6776,16 +7195,16 @@ do_request_link_no_delayed_actions(NMPlatform *platform, int ifindex, const char
 
         entry = nmp_cache_lookup_entry_link(nm_platform_get_cache(platform), ifindex);
         if (entry) {
-            priv->pruning[REFRESH_ALL_TYPE_LINKS] += 1;
+            priv->pruning[REFRESH_ALL_TYPE_RTNL_LINKS] += 1;
             nm_dedup_multi_entry_set_dirty(entry, TRUE);
         }
     }
 
-    event_handler_read_netlink(platform, FALSE);
+    event_handler_read_netlink(platform, NMP_NETLINK_ROUTE, FALSE);
 
     nlmsg = _nl_msg_new_link(RTM_GETLINK, 0, ifindex, name);
     if (nlmsg) {
-        nle = _nl_send_nlmsg(platform, nlmsg, NULL, NULL, DELAYED_ACTION_RESPONSE_TYPE_VOID, NULL);
+        nle = _netlink_send_nlmsg_rtnl(platform, nlmsg, NULL, NULL);
         if (nle < 0) {
             _LOGE("do-request-link: %d %s: failed sending netlink request \"%s\" (%d)",
                   ifindex,
@@ -6801,11 +7220,11 @@ static void
 do_request_link(NMPlatform *platform, int ifindex, const char *name)
 {
     do_request_link_no_delayed_actions(platform, ifindex, name);
-    delayed_action_handle_all(platform, FALSE);
+    delayed_action_handle_all(platform);
 }
 
 static struct nl_msg *
-_nl_msg_new_dump(NMPObjectType obj_type, int preferred_addr_family)
+_nl_msg_new_dump_rtnl(NMPObjectType obj_type, int preferred_addr_family)
 {
     nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
     const NMPClass              *klass;
@@ -6855,6 +7274,26 @@ _nl_msg_new_dump(NMPObjectType obj_type, int preferred_addr_family)
     return g_steal_pointer(&nlmsg);
 }
 
+static struct nl_msg *
+_nl_msg_new_dump_genl_families(void)
+{
+    nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
+
+    nlmsg = nlmsg_alloc_size(nlmsg_total_size(GENL_HDRLEN));
+
+    if (!genlmsg_put(nlmsg,
+                     NL_AUTO_PORT,
+                     NL_AUTO_SEQ,
+                     GENL_ID_CTRL,
+                     0,
+                     NLM_F_DUMP,
+                     CTRL_CMD_GETFAMILY,
+                     1))
+        g_return_val_if_reached(NULL);
+
+    return g_steal_pointer(&nlmsg);
+}
+
 static void
 do_request_all_no_delayed_actions(NMPlatform *platform, DelayedActionType action_type)
 {
@@ -6862,29 +7301,36 @@ do_request_all_no_delayed_actions(NMPlatform *platform, DelayedActionType action
     DelayedActionType       action_type_prune;
     DelayedActionType       iflags;
 
-    nm_assert(!NM_FLAGS_ANY(action_type, ~DELAYED_ACTION_TYPE_REFRESH_ALL));
-    action_type &= DELAYED_ACTION_TYPE_REFRESH_ALL;
+    nm_assert((NM_FLAGS_ANY(action_type, DELAYED_ACTION_TYPE_REFRESH_RTNL_ALL)
+               && !NM_FLAGS_ANY(action_type, ~DELAYED_ACTION_TYPE_REFRESH_RTNL_ALL))
+              || (NM_FLAGS_ANY(action_type, DELAYED_ACTION_TYPE_REFRESH_GENL_ALL)
+                  && !NM_FLAGS_ANY(action_type, ~DELAYED_ACTION_TYPE_REFRESH_GENL_ALL)));
 
     action_type_prune = action_type;
 
-    /* calling nmp_cache_dirty_set_all_main() with a non-main lookup-index requires an extra
-     * cache lookup for every entry.
-     *
-     * Avoid that, by special casing routing-rules here. */
-    if (NM_FLAGS_ALL(action_type_prune, DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL)) {
+    if (NM_FLAGS_ALL(action_type_prune, DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_ALL)) {
         NMPLookup lookup;
 
-        priv->pruning[REFRESH_ALL_TYPE_ROUTING_RULES_IP4] += 1;
-        priv->pruning[REFRESH_ALL_TYPE_ROUTING_RULES_IP6] += 1;
+        /* calling nmp_cache_dirty_set_all_main() with a non-main lookup-index requires an extra
+         * cache lookup for every entry.
+         *
+         * Avoid that, by special casing routing-rules here. */
+        priv->pruning[REFRESH_ALL_TYPE_RTNL_ROUTING_RULES_IP4] += 1;
+        priv->pruning[REFRESH_ALL_TYPE_RTNL_ROUTING_RULES_IP6] += 1;
         nmp_lookup_init_obj_type(&lookup, NMP_OBJECT_TYPE_ROUTING_RULE);
         nmp_cache_dirty_set_all_main(nm_platform_get_cache(platform), &lookup);
-        action_type_prune &= ~DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL;
+        action_type_prune &= ~DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_ALL;
     }
-
     FOR_EACH_DELAYED_ACTION (iflags, action_type_prune) {
         RefreshAllType refresh_all_type = delayed_action_type_to_refresh_all_type(iflags);
         NMPLookup      lookup;
 
+        if (refresh_all_type == REFRESH_ALL_TYPE_GENL_FAMILIES) {
+            /* genl families are not tracked in the NMPObject cache, because there is
+             * only a static number of objects we care about (NMPGenlFamilyType). */
+            continue;
+        }
+
         priv->pruning[refresh_all_type] += 1;
         refresh_all_type_init_lookup(refresh_all_type, &lookup);
         nmp_cache_dirty_set_all_main(nm_platform_get_cache(platform), &lookup);
@@ -6905,7 +7351,7 @@ do_request_all_no_delayed_actions(NMPlatform *platform, DelayedActionType action
         priv->delayed_action.flags &= ~iflags;
         _LOGt_delayed_action(iflags, NULL, "handle (do-request-all)");
 
-        if (refresh_all_type == REFRESH_ALL_TYPE_LINKS) {
+        if (refresh_all_type == REFRESH_ALL_TYPE_RTNL_LINKS) {
             nm_assert(
                 (priv->delayed_action.list_refresh_link->len > 0)
                 == NM_FLAGS_HAS(priv->delayed_action.flags, DELAYED_ACTION_TYPE_REFRESH_LINK));
@@ -6918,23 +7364,30 @@ do_request_all_no_delayed_actions(NMPlatform *platform, DelayedActionType action
             }
         }
 
-        event_handler_read_netlink(platform, FALSE);
+        event_handler_read_netlink(platform, refresh_all_info->protocol, FALSE);
+
+        if (refresh_all_info->protocol == NMP_NETLINK_ROUTE) {
+            nlmsg = _nl_msg_new_dump_rtnl(refresh_all_info->obj_type,
+                                          refresh_all_info->addr_family_for_dump);
+        } else {
+            nm_assert(refresh_all_type == REFRESH_ALL_TYPE_GENL_FAMILIES);
+            nlmsg = _nl_msg_new_dump_genl_families();
+        }
 
-        nlmsg = _nl_msg_new_dump(refresh_all_info->obj_type, refresh_all_info->addr_family);
         if (!nlmsg)
             goto next_after_fail;
 
-        if (_nl_send_nlmsg(platform,
-                           nlmsg,
-                           NULL,
-                           NULL,
-                           DELAYED_ACTION_RESPONSE_TYPE_REFRESH_ALL_IN_PROGRESS,
-                           out_refresh_all_in_progress)
+        if (_netlink_send_nlmsg(platform,
+                                refresh_all_info->protocol,
+                                nlmsg,
+                                NULL,
+                                NULL,
+                                DELAYED_ACTION_RESPONSE_TYPE_REFRESH_ALL_IN_PROGRESS,
+                                out_refresh_all_in_progress)
             < 0)
             goto next_after_fail;
 
         continue;
-
 next_after_fail:
         nm_assert(*out_refresh_all_in_progress > 0);
         *out_refresh_all_in_progress -= 1;
@@ -6946,102 +7399,109 @@ do_request_one_type_by_needle_object(NMPlatform *platform, const NMPObject *obj_
 {
     do_request_all_no_delayed_actions(platform,
                                       delayed_action_refresh_from_needle_object(obj_needle));
-    delayed_action_handle_all(platform, FALSE);
+    delayed_action_handle_all(platform);
 }
 
 static void
-event_seq_check_refresh_all(NMPlatform *platform, guint32 seq_number)
+event_seq_check_refresh_all(NMPlatform        *platform,
+                            NMPNetlinkProtocol netlink_protocol,
+                            guint32            seq_number)
 {
-    NMLinuxPlatformPrivate             *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
-    DelayedActionWaitForNlResponseData *data;
-    guint                               i;
+    NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
+    guint                   i;
 
-    if (NM_IN_SET(seq_number, 0, priv->nlh_seq_last_seen))
+    if (NM_IN_SET(seq_number, 0, priv->proto_data_x[netlink_protocol].nlh_seq_last_seen))
         return;
 
-    if (NM_FLAGS_HAS(priv->delayed_action.flags, DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE)) {
-        nm_assert(priv->delayed_action.list_wait_for_nl_response->len > 0);
+    if (!NM_FLAGS_ANY(
+            priv->delayed_action.flags,
+            nmp_netlink_protocol_info(netlink_protocol)->delayed_action_type_wait_for_response))
+        goto out;
 
-        for (i = 0; i < priv->delayed_action.list_wait_for_nl_response->len; i++) {
-            data = &g_array_index(priv->delayed_action.list_wait_for_nl_response,
-                                  DelayedActionWaitForNlResponseData,
-                                  i);
+    nm_assert(priv->delayed_action.list_wait_for_response_x[netlink_protocol]->len > 0);
 
-            if (data->response_type == DELAYED_ACTION_RESPONSE_TYPE_REFRESH_ALL_IN_PROGRESS
-                && data->response.out_refresh_all_in_progress
-                && data->seq_number == priv->nlh_seq_last_seen) {
-                *data->response.out_refresh_all_in_progress -= 1;
-                data->response.out_refresh_all_in_progress = NULL;
-                break;
-            }
+    for (i = 0; i < priv->delayed_action.list_wait_for_response_x[netlink_protocol]->len; i++) {
+        DelayedActionWaitForNlResponseData *data =
+            delayed_action_get_list_wait_for_resonse(priv, netlink_protocol, i);
+
+        if (data->response_type == DELAYED_ACTION_RESPONSE_TYPE_REFRESH_ALL_IN_PROGRESS
+            && data->response.out_refresh_all_in_progress
+            && data->seq_number == priv->proto_data_x[netlink_protocol].nlh_seq_last_seen) {
+            *data->response.out_refresh_all_in_progress -= 1;
+            data->response.out_refresh_all_in_progress = NULL;
+            break;
         }
     }
 
-    priv->nlh_seq_last_seen = seq_number;
+out:
+    priv->proto_data_x[netlink_protocol].nlh_seq_last_seen = seq_number;
 }
 
 static void
 event_seq_check(NMPlatform             *platform,
+                NMPNetlinkProtocol      netlink_protocol,
                 guint32                 seq_number,
                 WaitForNlResponseResult seq_result,
                 const char             *msg)
 {
-    NMLinuxPlatformPrivate             *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
-    DelayedActionWaitForNlResponseData *data;
-    guint                               i;
+    NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
+    guint                   i;
 
     if (seq_number == 0)
         return;
 
-    if (NM_FLAGS_HAS(priv->delayed_action.flags, DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE)) {
-        nm_assert(priv->delayed_action.list_wait_for_nl_response->len > 0);
+    if (!NM_FLAGS_ANY(
+            priv->delayed_action.flags,
+            nmp_netlink_protocol_info(netlink_protocol)->delayed_action_type_wait_for_response))
+        goto out;
+
+    nm_assert(priv->delayed_action.list_wait_for_response_x[netlink_protocol]->len > 0);
 
-        for (i = 0; i < priv->delayed_action.list_wait_for_nl_response->len; i++) {
-            data = &g_array_index(priv->delayed_action.list_wait_for_nl_response,
-                                  DelayedActionWaitForNlResponseData,
-                                  i);
+    for (i = 0; i < priv->delayed_action.list_wait_for_response_x[netlink_protocol]->len; i++) {
+        DelayedActionWaitForNlResponseData *data =
+            delayed_action_get_list_wait_for_resonse(priv, netlink_protocol, i);
 
-            if (data->seq_number == seq_number) {
-                /* We potentially receive many parts partial responses for the same sequence number.
+        if (data->seq_number == seq_number) {
+            /* We potentially receive many parts partial responses for the same sequence number.
                  * Thus, we only remember the result, and collect it later. */
-                if (data->seq_result < 0) {
-                    /* we already saw an error for this sequence number.
+            if (data->seq_result < 0) {
+                /* we already saw an error for this sequence number.
                      * Preserve it. */
-                } else if (seq_result != WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_UNKNOWN
-                           || data->seq_result == WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN)
-                    data->seq_result = seq_result;
-                if (data->out_errmsg && !*data->out_errmsg)
-                    *data->out_errmsg = g_strdup(msg);
-                return;
-            }
+            } else if (seq_result != WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_UNKNOWN
+                       || data->seq_result == WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN)
+                data->seq_result = seq_result;
+            if (data->out_errmsg && !*data->out_errmsg)
+                *data->out_errmsg = g_strdup(msg);
+            return;
         }
     }
 
+out:
+
 #if NM_MORE_LOGGING
-    if (seq_number != priv->nlh_seq_last_handled)
+    if (seq_number != priv->proto_data_x[netlink_protocol].nlh_seq_last_handled)
         _LOGt("netlink: recvmsg: unwaited sequence number %u", seq_number);
-    priv->nlh_seq_last_handled = seq_number;
+    priv->proto_data_x[netlink_protocol].nlh_seq_last_handled = seq_number;
+#else
+    (void) 0;
 #endif
 }
 
 static void
-event_valid_msg(NMPlatform *platform, struct nl_msg *msg, gboolean handle_events)
+_rtnl_handle_msg(NMPlatform *platform, const struct nl_msg_lite *msg)
 {
     char                      sbuf1[NM_UTILS_TO_STRING_BUFFER_SIZE];
     NMLinuxPlatformPrivate   *priv;
     nm_auto_nmpobj NMPObject *obj = NULL;
     NMPCacheOpsType           cache_op;
-    struct nlmsghdr          *msghdr;
+    const struct nlmsghdr    *msghdr;
     char                      buf_nlmsghdr[400];
     gboolean                  is_del  = FALSE;
     gboolean                  is_dump = FALSE;
     NMPCache                 *cache   = nm_platform_get_cache(platform);
     ParseNlmsgIter            parse_nlmsg_iter;
 
-    if (!handle_events)
-        return;
-
-    msghdr = nlmsg_hdr(msg);
+    msghdr = msg->nm_nlh;
 
     if (NM_IN_SET(msghdr->nlmsg_type,
                   RTM_DELLINK,
@@ -7062,7 +7522,7 @@ event_valid_msg(NMPlatform *platform, struct nl_msg *msg, gboolean handle_events
     obj = nmp_object_new_from_nl(platform, cache, msg, is_del, &parse_nlmsg_iter);
     if (!obj) {
         _LOGT("event-notification: %s: ignore",
-              nl_nlmsghdr_to_str(msghdr, buf_nlmsghdr, sizeof(buf_nlmsghdr)));
+              nl_nlmsghdr_to_str(NETLINK_ROUTE, 0, msghdr, buf_nlmsghdr, sizeof(buf_nlmsghdr)));
         return;
     }
 
@@ -7080,7 +7540,7 @@ event_valid_msg(NMPlatform *platform, struct nl_msg *msg, gboolean handle_events
     }
 
     _LOGT("event-notification: %s%s: %s",
-          nl_nlmsghdr_to_str(msghdr, buf_nlmsghdr, sizeof(buf_nlmsghdr)),
+          nl_nlmsghdr_to_str(NETLINK_ROUTE, 0, msghdr, buf_nlmsghdr, sizeof(buf_nlmsghdr)),
           is_dump ? ", in-dump" : "",
           nmp_object_to_string(obj,
                                is_del ? NMP_OBJECT_TO_STRING_ID : NMP_OBJECT_TO_STRING_PUBLIC,
@@ -7119,20 +7579,18 @@ event_valid_msg(NMPlatform *platform, struct nl_msg *msg, gboolean handle_events
                 nm_assert(is_ipv6 || !nmp_object_is_alive(obj));
                 priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
                 if (NM_FLAGS_HAS(priv->delayed_action.flags,
-                                 DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE)) {
+                                 DELAYED_ACTION_TYPE_WAIT_FOR_RESPONSE_RTNL)) {
                     guint i;
 
-                    nm_assert(priv->delayed_action.list_wait_for_nl_response->len > 0);
-                    for (i = 0; i < priv->delayed_action.list_wait_for_nl_response->len; i++) {
+                    nm_assert(priv->delayed_action.list_wait_for_response_rtnl->len > 0);
+                    for (i = 0; i < priv->delayed_action.list_wait_for_response_rtnl->len; i++) {
                         DelayedActionWaitForNlResponseData *data =
-                            &g_array_index(priv->delayed_action.list_wait_for_nl_response,
-                                           DelayedActionWaitForNlResponseData,
-                                           i);
+                            delayed_action_get_list_wait_for_resonse(priv, NMP_NETLINK_ROUTE, i);
 
                         if (data->response_type == DELAYED_ACTION_RESPONSE_TYPE_ROUTE_GET
                             && data->response.out_route_get) {
                             nm_assert(!*data->response.out_route_get);
-                            if (data->seq_number == nlmsg_hdr(msg)->nlmsg_seq) {
+                            if (data->seq_number == msg->nm_nlh->nlmsg_seq) {
                                 *data->response.out_route_get = nmp_object_clone(obj, FALSE);
                                 data->response.out_route_get  = NULL;
                                 break;
@@ -7269,14 +7727,9 @@ do_add_link_with_lookup(NMPlatform            *platform,
     char                    s_buf[256];
     NMPCache               *cache = nm_platform_get_cache(platform);
 
-    event_handler_read_netlink(platform, FALSE);
+    event_handler_read_netlink(platform, NMP_NETLINK_ROUTE, FALSE);
 
-    nle = _nl_send_nlmsg(platform,
-                         nlmsg,
-                         &seq_result,
-                         &errmsg,
-                         DELAYED_ACTION_RESPONSE_TYPE_VOID,
-                         NULL);
+    nle = _netlink_send_nlmsg_rtnl(platform, nlmsg, &seq_result, &errmsg);
     if (nle < 0) {
         _LOGE("do-add-link[%s/%s]: failed sending netlink request \"%s\" (%d)",
               name,
@@ -7287,7 +7740,7 @@ do_add_link_with_lookup(NMPlatform            *platform,
         return nle;
     }
 
-    delayed_action_handle_all(platform, FALSE);
+    delayed_action_handle_all(platform);
 
     nm_assert(seq_result);
 
@@ -7323,14 +7776,9 @@ do_add_addrroute(NMPlatform      *platform,
                         NMP_OBJECT_TYPE_IP4_ROUTE,
                         NMP_OBJECT_TYPE_IP6_ROUTE));
 
-    event_handler_read_netlink(platform, FALSE);
+    event_handler_read_netlink(platform, NMP_NETLINK_ROUTE, FALSE);
 
-    nle = _nl_send_nlmsg(platform,
-                         nlmsg,
-                         &seq_result,
-                         &errmsg,
-                         DELAYED_ACTION_RESPONSE_TYPE_VOID,
-                         NULL);
+    nle = _netlink_send_nlmsg_rtnl(platform, nlmsg, &seq_result, &errmsg);
     if (nle < 0) {
         _LOGE("do-add-%s[%s]: failure sending netlink request \"%s\" (%d)",
               NMP_OBJECT_GET_CLASS(obj_id)->obj_type_name,
@@ -7340,7 +7788,7 @@ do_add_addrroute(NMPlatform      *platform,
         return -NME_PL_NETLINK;
     }
 
-    delayed_action_handle_all(platform, FALSE);
+    delayed_action_handle_all(platform);
 
     nm_assert(seq_result);
 
@@ -7379,14 +7827,9 @@ do_delete_object(NMPlatform *platform, const NMPObject *obj_id, struct nl_msg *n
     gboolean                success;
     const char             *log_detail = "";
 
-    event_handler_read_netlink(platform, FALSE);
+    event_handler_read_netlink(platform, NMP_NETLINK_ROUTE, FALSE);
 
-    nle = _nl_send_nlmsg(platform,
-                         nlmsg,
-                         &seq_result,
-                         &errmsg,
-                         DELAYED_ACTION_RESPONSE_TYPE_VOID,
-                         NULL);
+    nle = _netlink_send_nlmsg_rtnl(platform, nlmsg, &seq_result, &errmsg);
     if (nle < 0) {
         _LOGE("do-delete-%s[%s]: failure sending netlink request \"%s\" (%d)",
               NMP_OBJECT_GET_CLASS(obj_id)->obj_type_name,
@@ -7396,7 +7839,7 @@ do_delete_object(NMPlatform *platform, const NMPObject *obj_id, struct nl_msg *n
         return FALSE;
     }
 
-    delayed_action_handle_all(platform, FALSE);
+    delayed_action_handle_all(platform);
 
     nm_assert(seq_result);
 
@@ -7470,12 +7913,7 @@ do_change_link(NMPlatform           *platform,
     }
 
 retry:
-    nle = _nl_send_nlmsg(platform,
-                         nlmsg,
-                         &seq_result,
-                         &errmsg,
-                         DELAYED_ACTION_RESPONSE_TYPE_VOID,
-                         NULL);
+    nle = _netlink_send_nlmsg_rtnl(platform, nlmsg, &seq_result, &errmsg);
     if (nle < 0) {
         log_level = LOGL_ERR;
         log_detail_free =
@@ -7488,7 +7926,7 @@ retry:
      * and we sometimes lack events. Nuke it from the orbit... */
     delayed_action_schedule(platform, DELAYED_ACTION_TYPE_REFRESH_LINK, GINT_TO_POINTER(ifindex));
 
-    delayed_action_handle_all(platform, FALSE);
+    delayed_action_handle_all(platform);
 
     nm_assert(seq_result);
 
@@ -7760,7 +8198,7 @@ link_set_address(NMPlatform *platform, int ifindex, gconstpointer address, size_
     nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
     const ChangeLinkData         d     = {
                     .set_address =
-            {
+                        {
                             .address = address,
                             .length  = length,
             },
@@ -8596,11 +9034,11 @@ link_can_assume(NMPlatform *platform, int ifindex)
     if (link->link.master > 0)
         return TRUE;
 
-    nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP4_ADDRESS, ifindex);
+    nmp_lookup_init_object_by_ifindex(&lookup, NMP_OBJECT_TYPE_IP4_ADDRESS, ifindex);
     if (nmp_cache_lookup(cache, &lookup))
         return TRUE;
 
-    nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP6_ADDRESS, ifindex);
+    nmp_lookup_init_object_by_ifindex(&lookup, NMP_OBJECT_TYPE_IP6_ADDRESS, ifindex);
     nmp_cache_iter_for_each (&iter, nmp_cache_lookup(cache, &lookup), &o) {
         nm_assert(NMP_OBJECT_GET_TYPE(o) == NMP_OBJECT_TYPE_IP6_ADDRESS);
         if (!IN6_IS_ADDR_LINKLOCAL(&o->ip6_address.address))
@@ -8750,8 +9188,7 @@ ip4_address_add(NMPlatform *platform,
                                 plen,
                                 &peer_addr,
                                 flags,
-                                nm_utils_ip4_address_is_link_local(addr) ? RT_SCOPE_LINK
-                                                                         : RT_SCOPE_UNIVERSE,
+                                nm_platform_ip4_address_get_scope(addr),
                                 lifetime,
                                 preferred,
                                 broadcast_address,
@@ -8897,6 +9334,9 @@ object_delete(NMPlatform *platform, const NMPObject *obj)
     case NMP_OBJECT_TYPE_TFILTER:
         nlmsg = _nl_msg_new_tfilter(RTM_DELTFILTER, 0, NMP_OBJECT_CAST_TFILTER(obj));
         break;
+    case NMP_OBJECT_TYPE_MPTCP_ADDR:
+        return (nm_platform_mptcp_addr_update(platform, FALSE, NMP_OBJECT_CAST_MPTCP_ADDR(obj))
+                >= 0);
     default:
         break;
     }
@@ -8967,7 +9407,7 @@ ip_route_get(NMPlatform   *platform,
             return -NME_UNSPEC;
         }
 
-        delayed_action_handle_all(platform, FALSE);
+        delayed_action_handle_all(platform);
 
         /* Retry, if we failed due to a cache resync. That can happen when the netlink
          * socket fills up and we lost the response. */
@@ -9003,20 +9443,15 @@ routing_rule_add(NMPlatform *platform, NMPNlmFlags flags, const NMPlatformRoutin
 
     msg = _nl_msg_new_routing_rule(RTM_NEWRULE, flags, routing_rule);
 
-    event_handler_read_netlink(platform, FALSE);
+    event_handler_read_netlink(platform, NMP_NETLINK_ROUTE, FALSE);
 
-    nle = _nl_send_nlmsg(platform,
-                         msg,
-                         &seq_result,
-                         &errmsg,
-                         DELAYED_ACTION_RESPONSE_TYPE_VOID,
-                         NULL);
+    nle = _netlink_send_nlmsg_rtnl(platform, msg, &seq_result, &errmsg);
     if (nle < 0) {
         _LOGE("do-add-rule: failed sending netlink request \"%s\" (%d)", nm_strerror(nle), -nle);
         return -NME_PL_NETLINK;
     }
 
-    delayed_action_handle_all(platform, FALSE);
+    delayed_action_handle_all(platform);
 
     nm_assert(seq_result);
 
@@ -9047,20 +9482,15 @@ qdisc_add(NMPlatform *platform, NMPNlmFlags flags, const NMPlatformQdisc *qdisc)
 
     msg = _nl_msg_new_qdisc(RTM_NEWQDISC, flags, qdisc);
 
-    event_handler_read_netlink(platform, FALSE);
+    event_handler_read_netlink(platform, NMP_NETLINK_ROUTE, FALSE);
 
-    nle = _nl_send_nlmsg(platform,
-                         msg,
-                         &seq_result,
-                         &errmsg,
-                         DELAYED_ACTION_RESPONSE_TYPE_VOID,
-                         NULL);
+    nle = _netlink_send_nlmsg_rtnl(platform, msg, &seq_result, &errmsg);
     if (nle < 0) {
         _LOGE("do-add-qdisc: failed sending netlink request \"%s\" (%d)", nm_strerror(nle), -nle);
         return -NME_PL_NETLINK;
     }
 
-    delayed_action_handle_all(platform, FALSE);
+    delayed_action_handle_all(platform);
 
     nm_assert(seq_result);
 
@@ -9076,7 +9506,11 @@ qdisc_add(NMPlatform *platform, NMPNlmFlags flags, const NMPlatformQdisc *qdisc)
 }
 
 static int
-tc_delete(NMPlatform *platform, int nlmsgtype, int ifindex, guint32 parent, gboolean log_error)
+tc_delete(NMPlatform *platform,
+          uint16_t    nlmsg_type,
+          int         ifindex,
+          guint32     parent,
+          gboolean    log_error)
 {
     WaitForNlResponseResult      seq_result = WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN;
     gs_free char                *errmsg     = NULL;
@@ -9089,7 +9523,7 @@ tc_delete(NMPlatform *platform, int nlmsgtype, int ifindex, guint32 parent, gboo
                   .tcm_parent  = parent,
     };
 
-    switch (nlmsgtype) {
+    switch (nlmsg_type) {
     case RTM_DELQDISC:
         log_tag = "do-delete-qdisc";
         break;
@@ -9101,19 +9535,14 @@ tc_delete(NMPlatform *platform, int nlmsgtype, int ifindex, guint32 parent, gboo
         log_tag = "do-delete-tc";
     }
 
-    msg = nlmsg_alloc_simple(nlmsgtype, NMP_NLM_FLAG_F_ECHO);
+    msg = nlmsg_alloc_simple(nlmsg_type, NMP_NLM_FLAG_F_ECHO);
 
     if (nlmsg_append_struct(msg, &tcm) < 0)
         goto nla_put_failure;
 
-    event_handler_read_netlink(platform, FALSE);
+    event_handler_read_netlink(platform, NMP_NETLINK_ROUTE, FALSE);
 
-    nle = _nl_send_nlmsg(platform,
-                         msg,
-                         &seq_result,
-                         &errmsg,
-                         DELAYED_ACTION_RESPONSE_TYPE_VOID,
-                         NULL);
+    nle = _netlink_send_nlmsg_rtnl(platform, msg, &seq_result, &errmsg);
     if (nle < 0) {
         _NMLOG(log_error ? LOGL_ERR : LOGL_DEBUG,
                "%s: failed sending netlink request \"%s\" (%d)",
@@ -9123,7 +9552,7 @@ tc_delete(NMPlatform *platform, int nlmsgtype, int ifindex, guint32 parent, gboo
         return -NME_PL_NETLINK;
     }
 
-    delayed_action_handle_all(platform, FALSE);
+    delayed_action_handle_all(platform);
 
     nm_assert(seq_result);
 
@@ -9163,20 +9592,15 @@ tfilter_add(NMPlatform *platform, NMPNlmFlags flags, const NMPlatformTfilter *tf
 
     msg = _nl_msg_new_tfilter(RTM_NEWTFILTER, flags, tfilter);
 
-    event_handler_read_netlink(platform, FALSE);
+    event_handler_read_netlink(platform, NMP_NETLINK_ROUTE, FALSE);
 
-    nle = _nl_send_nlmsg(platform,
-                         msg,
-                         &seq_result,
-                         &errmsg,
-                         DELAYED_ACTION_RESPONSE_TYPE_VOID,
-                         NULL);
+    nle = _netlink_send_nlmsg_rtnl(platform, msg, &seq_result, &errmsg);
     if (nle < 0) {
         _LOGE("do-add-tfilter: failed sending netlink request \"%s\" (%d)", nm_strerror(nle), -nle);
         return -NME_PL_NETLINK;
     }
 
-    delayed_action_handle_all(platform, FALSE);
+    delayed_action_handle_all(platform);
 
     nm_assert(seq_result);
 
@@ -9199,93 +9623,210 @@ tfilter_delete(NMPlatform *platform, int ifindex, guint32 parent, gboolean log_e
 /*****************************************************************************/
 
 static gboolean
-event_handler(int fd, GIOCondition io_condition, gpointer user_data)
+_genl_family_id_update(NMPlatform *platform, NMPGenlFamilyType family_type, guint16 family_id)
 {
-    delayed_action_handle_all(NM_PLATFORM(user_data), TRUE);
+    NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
+
+    if (priv->genl_family_data[family_type].family_id == family_id)
+        return FALSE;
+
+    if (family_id != 0) {
+        _LOGD("genl:ctrl: new family-id for %s: 0x%x",
+              nmp_genl_family_infos[family_type].name,
+              family_id);
+    } else
+        _LOGD("genl:ctrl: del family-id for %s", nmp_genl_family_infos[family_type].name);
+    priv->genl_family_data[family_type].family_id = family_id;
     return TRUE;
 }
 
+static void
+_genl_handle_msg_ctrl(NMPlatform *platform, const struct nlmsghdr *hdr)
+{
+    const struct genlmsghdr *ghdr = nlmsg_data(hdr);
+
+    switch (ghdr->cmd) {
+    case CTRL_CMD_NEWFAMILY:
+    case CTRL_CMD_DELFAMILY:
+    {
+        struct nlattr    *tb[G_N_ELEMENTS(genl_ctrl_policy)];
+        NMPGenlFamilyType family_type;
+        const char       *name;
+        guint16           family_id = 0;
+
+        if (genlmsg_parse_arr(hdr, 0, tb, genl_ctrl_policy) < 0)
+            return;
+
+        name        = nla_get_string(tb[CTRL_ATTR_FAMILY_NAME]);
+        family_type = nmp_genl_family_type_from_name(name);
+
+        if (family_type == _NMP_GENL_FAMILY_TYPE_NONE)
+            return;
+
+        if (ghdr->cmd == CTRL_CMD_NEWFAMILY)
+            family_id = nla_get_u16(tb[CTRL_ATTR_FAMILY_ID]);
+
+        _genl_family_id_update(platform, family_type, family_id);
+    }
+    }
+}
+
+static void
+_genl_handle_msg(NMPlatform *platform, guint32 pktinfo_group, const struct nl_msg_lite *msg)
+{
+    const struct nlmsghdr *hdr = msg->nm_nlh;
+
+    if (!genlmsg_valid_hdr(hdr, 0))
+        return;
+
+    if (hdr->nlmsg_type == GENL_ID_CTRL)
+        _genl_handle_msg_ctrl(platform, hdr);
+}
+
 /*****************************************************************************/
 
-/* copied from libnl3's recvmsgs() */
 static int
-event_handler_recvmsgs(NMPlatform *platform, gboolean handle_events)
+_netlink_recv(NMPlatform         *platform,
+              struct nl_sock     *sk,
+              struct sockaddr_nl *nla,
+              struct ucred       *out_creds,
+              gboolean           *out_creds_has,
+              guint32            *out_pktinfo_group,
+              gboolean           *out_pktinfo_has)
 {
     NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
-    struct nl_sock         *sk   = priv->nlh;
+    unsigned char          *buf  = NULL;
     int                     n;
-    int                     err         = 0;
-    gboolean                multipart   = 0;
-    gboolean                interrupted = FALSE;
-    struct nlmsghdr        *hdr;
-    WaitForNlResponseResult seq_result;
-    struct sockaddr_nl      nla;
-    struct ucred            creds;
-    gboolean                creds_has;
-    unsigned char          *buf;
 
-continue_reading:
-    buf = NULL;
+    nm_assert(nla);
+    nm_assert(out_creds);
+    nm_assert(out_creds_has);
+
+    /* We use a pre-allocated receive buffer. We use it both for sk_rtnl
+     * and sk_genl. We can do that, because we are deep inside the netlink
+     * handling, and we never will need to use it for both sockets at the
+     * same time. */
 
     n = nl_recv(sk,
                 priv->netlink_recv_buf.buf,
                 priv->netlink_recv_buf.len,
-                &nla,
+                nla,
                 &buf,
-                &creds,
-                &creds_has);
+                out_creds,
+                out_creds_has,
+                out_pktinfo_group,
+                out_pktinfo_has);
 
     nm_assert((n <= 0 && !buf)
               || (n > 0 && n <= priv->netlink_recv_buf.len && buf == priv->netlink_recv_buf.buf));
 
-    if (n <= 0) {
-        if (n == -NME_NL_MSG_TRUNC) {
-            /* the message receive buffer was too small. We lost one message, which
-             * is unfortunate. Try to double the buffer size for the next time. */
-            priv->netlink_recv_buf.len *= 2;
-            priv->netlink_recv_buf.buf =
-                g_realloc(priv->netlink_recv_buf.buf, priv->netlink_recv_buf.len);
-            _LOGT("netlink: recvmsg: increase message buffer size for recvmsg() to %zu bytes",
-                  priv->netlink_recv_buf.len);
-            if (!handle_events)
-                goto continue_reading;
-        }
-
-        return n;
+    if (n == -NME_NL_MSG_TRUNC) {
+        /* the message receive buffer was too small. We lost one message, which
+         * is unfortunate. Try to double the buffer size for the next time. */
+        priv->netlink_recv_buf.len *= 2;
+        priv->netlink_recv_buf.buf =
+            g_realloc(priv->netlink_recv_buf.buf, priv->netlink_recv_buf.len);
+        _LOGT("netlink: recvmsg: increase message buffer size for recvmsg() to %zu bytes",
+              priv->netlink_recv_buf.len);
     }
 
-    hdr = (struct nlmsghdr *) buf;
-    while (nlmsg_ok(hdr, n)) {
-        nm_auto_nlmsg struct nl_msg *msg               = NULL;
-        gboolean                     abort_parsing     = FALSE;
-        gboolean                     process_valid_msg = FALSE;
-        guint32                      seq_number;
-        char                         buf_nlmsghdr[400];
-        const char                  *extack_msg = NULL;
+    return n;
+}
 
-        msg = nlmsg_alloc_convert(hdr);
+/*****************************************************************************/
 
-        nlmsg_set_proto(msg, NETLINK_ROUTE);
-        nlmsg_set_src(msg, &nla);
+static gboolean
+_nl_event_handler(NMPlatform *platform, DelayedActionType action_type)
+{
+    delayed_action_schedule(platform, action_type, NULL);
+    delayed_action_handle_all(platform);
+    return TRUE;
+}
 
-        if (!creds_has || creds.pid) {
-            if (!creds_has)
-                _LOGT("netlink: recvmsg: received message without credentials");
-            else
-                _LOGT("netlink: recvmsg: received non-kernel message (pid %d)", creds.pid);
-            err = 0;
-            goto stop;
-        }
+static gboolean
+_nl_event_handler_genl(int fd, GIOCondition io_condition, gpointer user_data)
+{
+    return _nl_event_handler(user_data, DELAYED_ACTION_TYPE_READ_GENL);
+}
+
+static gboolean
+_nl_event_handler_rtnl(int fd, GIOCondition io_condition, gpointer user_data)
+{
+    return _nl_event_handler(user_data, DELAYED_ACTION_TYPE_READ_RTNL);
+}
+
+/*****************************************************************************/
+
+static int
+_netlink_recv_handle(NMPlatform        *platform,
+                     NMPNetlinkProtocol netlink_protocol,
+                     gboolean           handle_events)
+{
+    NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
+    int                     n;
+    int                     retval      = 0;
+    gboolean                multipart   = 0;
+    gboolean                interrupted = FALSE;
+    struct nlmsghdr        *hdr;
+    struct sockaddr_nl      nla;
+    struct ucred            creds;
+    gboolean                creds_has;
+    guint32                 pktinfo_group = 0;
+    gboolean                pktinfo_has   = FALSE;
+    const char *const       log_prefix    = nmp_netlink_protocol_info(netlink_protocol)->name;
+
+continue_reading:
+
+    n = _netlink_recv(platform,
+                      priv->sk_x[netlink_protocol],
+                      &nla,
+                      &creds,
+                      &creds_has,
+                      &pktinfo_group,
+                      netlink_protocol == NMP_NETLINK_GENERIC ? &pktinfo_has : NULL);
+    if (n < 0) {
+        if (n == -NME_NL_MSG_TRUNC && !handle_events)
+            goto continue_reading;
+        return n;
+    }
+
+    if (!creds_has || creds.pid) {
+        if (!creds_has)
+            _LOGT("%s: recvmsg: received message without credentials", log_prefix);
+        else
+            _LOGT("%s: recvmsg: received non-kernel message (pid %d)", log_prefix, creds.pid);
+        goto stop;
+    }
 
-        _LOGt("netlink: recvmsg: new message %s",
-              nl_nlmsghdr_to_str(hdr, buf_nlmsghdr, sizeof(buf_nlmsghdr)));
+    hdr = (struct nlmsghdr *) priv->netlink_recv_buf.buf;
+    while (nlmsg_ok(hdr, n)) {
+        WaitForNlResponseResult  seq_result;
+        gboolean                 process_valid_msg = FALSE;
+        char                     buf_nlmsghdr[400];
+        const char              *extack_msg = NULL;
+        const struct nl_msg_lite msg        = {
+                   .nm_protocol = nmp_netlink_protocol_info(netlink_protocol)->netlink_protocol,
+                   .nm_src      = &nla,
+                   .nm_creds    = &creds,
+                   .nm_size     = NLMSG_ALIGN(hdr->nlmsg_len),
+                   .nm_nlh      = hdr,
+        };
+        const guint32 seq_number = msg.nm_nlh->nlmsg_seq;
+
+        nm_assert((((uintptr_t) (const void *) msg.nm_nlh) % NLMSG_ALIGNTO) == 0);
 
-        nlmsg_set_creds(msg, &creds);
+        _LOGt("%s: recvmsg: new message %s",
+              log_prefix,
+              nl_nlmsghdr_to_str(nmp_netlink_protocol_info(netlink_protocol)->netlink_protocol,
+                                 pktinfo_group,
+                                 msg.nm_nlh,
+                                 buf_nlmsghdr,
+                                 sizeof(buf_nlmsghdr)));
 
-        if (hdr->nlmsg_flags & NLM_F_MULTI)
+        if (msg.nm_nlh->nlmsg_flags & NLM_F_MULTI)
             multipart = TRUE;
 
-        if (hdr->nlmsg_flags & NLM_F_DUMP_INTR) {
+        if (msg.nm_nlh->nlmsg_flags & NLM_F_DUMP_INTR) {
             /*
              * We have to continue reading to clear
              * all messages until a NLMSG_DONE is
@@ -9294,105 +9835,75 @@ continue_reading:
             interrupted = TRUE;
         }
 
-        /* Other side wishes to see an ack for this message */
-        if (hdr->nlmsg_flags & NLM_F_ACK) {
-            /* FIXME: implement */
+        if (msg.nm_nlh->nlmsg_flags & NLM_F_ACK) {
+            /* TODO: Other side wishes to see an ack for this message */
         }
 
         seq_result = WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_UNKNOWN;
 
-        if (hdr->nlmsg_type == NLMSG_DONE) {
+        if (msg.nm_nlh->nlmsg_type == NLMSG_DONE) {
             /* messages terminates a multipart message, this is
              * usually the end of a message and therefore we slip
              * out of the loop by default. the user may overrule
              * this action by skipping this packet. */
             multipart  = FALSE;
             seq_result = WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK;
-        } else if (hdr->nlmsg_type == NLMSG_NOOP) {
+        } else if (msg.nm_nlh->nlmsg_type == NLMSG_NOOP) {
             /* Message to be ignored, the default action is to
              * skip this message if no callback is specified. The
              * user may overrule this action by returning
              * NL_PROCEED. */
-        } else if (hdr->nlmsg_type == NLMSG_OVERRUN) {
+        } else if (msg.nm_nlh->nlmsg_type == NLMSG_OVERRUN) {
             /* Data got lost, report back to user. The default action is to
              * quit parsing. The user may overrule this action by returning
              * NL_SKIP or NL_PROCEED (dangerous) */
-            err           = -NME_NL_MSG_OVERFLOW;
-            abort_parsing = TRUE;
-        } else if (hdr->nlmsg_type == NLMSG_ERROR) {
-            /* Message carries a nlmsgerr */
-            struct nlmsgerr *e = nlmsg_data(hdr);
-
-            if (hdr->nlmsg_len < nlmsg_size(sizeof(*e))) {
-                /* Truncated error message, the default action
-                 * is to stop parsing. The user may overrule
-                 * this action by returning NL_SKIP or
-                 * NL_PROCEED (dangerous) */
-                err           = -NME_NL_MSG_TRUNC;
-                abort_parsing = TRUE;
-            } else if (e->error) {
-                int errsv = nm_errno_native(e->error);
-
-                if (NM_FLAGS_HAS(hdr->nlmsg_flags, NLM_F_ACK_TLVS)
-                    && hdr->nlmsg_len >= sizeof(*e) + e->msg.nlmsg_len) {
-                    static const struct nla_policy policy[] = {
-                        [NLMSGERR_ATTR_MSG]  = {.type = NLA_STRING},
-                        [NLMSGERR_ATTR_OFFS] = {.type = NLA_U32},
-                    };
-                    struct nlattr *tb[G_N_ELEMENTS(policy)];
-                    struct nlattr *tlvs;
-
-                    tlvs = (struct nlattr *) ((char *) e + sizeof(*e) + e->msg.nlmsg_len
-                                              - NLMSG_HDRLEN);
-                    if (nla_parse_arr(tb,
-                                      tlvs,
-                                      hdr->nlmsg_len - sizeof(*e) - e->msg.nlmsg_len,
-                                      policy)
-                        >= 0) {
-                        if (tb[NLMSGERR_ATTR_MSG])
-                            extack_msg = nla_get_string(tb[NLMSGERR_ATTR_MSG]);
-                    }
-                }
+            retval = -NME_NL_MSG_OVERFLOW;
+        } else if (msg.nm_nlh->nlmsg_type == NLMSG_ERROR) {
+            int errsv;
 
-                /* Error message reported back from kernel. */
-                _LOGD("netlink: recvmsg: error message from kernel: %s (%d)%s%s%s for request %d",
-                      nm_strerror_native(errsv),
+            errsv = nlmsg_parse_error(msg.nm_nlh, &extack_msg);
+            if (errsv == 0)
+                seq_result = WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK;
+            else {
+                _LOGD("%s: recvmsg: error message from kernel: %s (%d)%s%s%s for request %d",
+                      log_prefix,
+                      nm_strerror(errsv),
                       errsv,
                       NM_PRINT_FMT_QUOTED(extack_msg, " \"", extack_msg, "\"", ""),
-                      nlmsg_hdr(msg)->nlmsg_seq);
-                seq_result = -NM_ERRNO_NATIVE(errsv);
-            } else
-                seq_result = WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK;
+                      msg.nm_nlh->nlmsg_seq);
+                seq_result = errsv;
+            }
         } else
             process_valid_msg = TRUE;
 
-        seq_number = nlmsg_hdr(msg)->nlmsg_seq;
-
         /* check whether the seq number is different from before, and
          * whether the previous number (@nlh_seq_last_seen) is a pending
          * refresh-all request. In that case, the pending request is thereby
          * completed.
          *
-         * We must do that before processing the message with event_valid_msg(),
+         * We must do that before processing the message with _rtnl_handle_msg(),
          * because we must track the completion of the pending request before that. */
-        event_seq_check_refresh_all(platform, seq_number);
+        event_seq_check_refresh_all(platform, netlink_protocol, seq_number);
 
         if (process_valid_msg) {
-            /* Valid message (not checking for MULTIPART bit to
-             * get along with broken kernels. NL_SKIP has no
-             * effect on this.  */
-
-            event_valid_msg(platform, msg, handle_events);
-
+            if (handle_events) {
+                /* Valid message (not checking for MULTIPART bit to
+                 * get along with broken kernels. NL_SKIP has no
+                 * effect on this.  */
+                if (netlink_protocol == NMP_NETLINK_ROUTE) {
+                    _rtnl_handle_msg(platform, &msg);
+                } else {
+                    _genl_handle_msg(platform, pktinfo_group, &msg);
+                }
+            }
             seq_result = WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK;
         }
 
-        event_seq_check(platform, seq_number, seq_result, extack_msg);
+        event_seq_check(platform, netlink_protocol, seq_number, seq_result, extack_msg);
 
-        if (abort_parsing)
+        if (retval != 0)
             goto stop;
 
-        err = 0;
         hdr = nlmsg_next(hdr, &n);
     }
 
@@ -9400,6 +9911,7 @@ continue_reading:
         /* Multipart message not yet complete, continue reading */
         goto continue_reading;
     }
+
 stop:
     if (!handle_events) {
         /* when we don't handle events, we want to drain all messages from the socket
@@ -9410,13 +9922,15 @@ stop:
 
     if (interrupted)
         return -NME_NL_DUMP_INTR;
-    return err;
+    return retval;
 }
 
 /*****************************************************************************/
 
 static gboolean
-event_handler_read_netlink(NMPlatform *platform, gboolean wait_for_acks)
+event_handler_read_netlink(NMPlatform        *platform,
+                           NMPNetlinkProtocol netlink_protocol,
+                           gboolean           wait_for_acks)
 {
     nm_auto_pop_netns NMPNetns *netns = NULL;
     NMLinuxPlatformPrivate     *priv  = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
@@ -9430,8 +9944,11 @@ event_handler_read_netlink(NMPlatform *platform, gboolean wait_for_acks)
         gint64  now_nsec;
     } next;
 
+    nmp_netlink_protocol_check(netlink_protocol);
+
     if (!nm_platform_netns_push(platform, &netns)) {
         delayed_action_wait_for_nl_response_complete_all(platform,
+                                                         netlink_protocol,
                                                          WAIT_FOR_NL_RESPONSE_RESULT_FAILED_SETNS);
         return FALSE;
     }
@@ -9440,20 +9957,24 @@ event_handler_read_netlink(NMPlatform *platform, gboolean wait_for_acks)
         for (;;) {
             int nle;
 
-            nle = event_handler_recvmsgs(platform, TRUE);
+            nle = _netlink_recv_handle(platform, netlink_protocol, TRUE);
 
             if (nle < 0) {
                 switch (nle) {
                 case -EAGAIN:
                     goto after_read;
                 case -NME_NL_DUMP_INTR:
-                    _LOGD("netlink: read: uncritical failure to retrieve incoming events: %s (%d)",
+                    _LOGD("netlink[%s]: read: uncritical failure to retrieve incoming events: %s "
+                          "(%d)",
+                          nmp_netlink_protocol_info(netlink_protocol)->name,
                           nm_strerror(nle),
                           nle);
                     break;
                 case -NME_NL_MSG_TRUNC:
                 case -ENOBUFS:
-                    _LOGI("netlink: read: %s. Need to resynchronize platform cache", ({
+                    _LOGI("netlink[%s]: read: %s. Need to resynchronize platform cache",
+                          nmp_netlink_protocol_info(netlink_protocol)->name,
+                          ({
                               const char *_reason = "unknown";
                               switch (nle) {
                               case -NME_NL_MSG_TRUNC:
@@ -9465,24 +9986,16 @@ event_handler_read_netlink(NMPlatform *platform, gboolean wait_for_acks)
                               }
                               _reason;
                           }));
-                    event_handler_recvmsgs(platform, FALSE);
+                    _netlink_recv_handle(platform, netlink_protocol, FALSE);
                     delayed_action_wait_for_nl_response_complete_all(
                         platform,
+                        netlink_protocol,
                         WAIT_FOR_NL_RESPONSE_RESULT_FAILED_RESYNC);
-
-                    delayed_action_schedule(platform,
-                                            DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS
-                                                | DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES
-                                                | DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES
-                                                | DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES
-                                                | DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES
-                                                | DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL
-                                                | DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS
-                                                | DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS,
-                                            NULL);
+                    delayed_action_schedule_refresh_all(platform, netlink_protocol);
                     break;
                 default:
-                    _LOGE("netlink: read: failed to retrieve incoming events: %s (%d)",
+                    _LOGE("netlink[%s]: read: failed to retrieve incoming events: %s (%d)",
+                          nmp_netlink_protocol_info(netlink_protocol)->name,
                           nm_strerror(nle),
                           nle);
                     break;
@@ -9493,17 +10006,22 @@ event_handler_read_netlink(NMPlatform *platform, gboolean wait_for_acks)
 
 after_read:
 
-        if (!NM_FLAGS_HAS(priv->delayed_action.flags, DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE))
+        if (!NM_FLAGS_ANY(
+                priv->delayed_action.flags,
+                nmp_netlink_protocol_info(netlink_protocol)->delayed_action_type_wait_for_response))
             return any;
 
-        delayed_action_wait_for_nl_response_complete_check(platform,
-                                                           WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN,
-                                                           &next.seq_number,
-                                                           &next.timeout_abs_nsec,
-                                                           &next.now_nsec);
+        delayed_action_wait_for_response_complete_check(platform,
+                                                        netlink_protocol,
+                                                        WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN,
+                                                        &next.seq_number,
+                                                        &next.timeout_abs_nsec,
+                                                        &next.now_nsec);
 
         if (!wait_for_acks
-            || !NM_FLAGS_HAS(priv->delayed_action.flags, DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE))
+            || !NM_FLAGS_ANY(
+                priv->delayed_action.flags,
+                nmp_netlink_protocol_info(netlink_protocol)->delayed_action_type_wait_for_response))
             return any;
 
         nm_assert(next.seq_number);
@@ -9521,7 +10039,7 @@ after_read:
               timeout_msec);
 
         memset(&pfd, 0, sizeof(pfd));
-        pfd.fd     = nl_socket_get_fd(priv->nlh);
+        pfd.fd     = nl_socket_get_fd(priv->sk_rtnl);
         pfd.events = POLLIN;
         r          = poll(&pfd, 1, timeout_msec);
 
@@ -9539,6 +10057,7 @@ after_read:
                 _LOGE("netlink: read: poll failed with %s", nm_strerror_native(errsv));
                 delayed_action_wait_for_nl_response_complete_all(
                     platform,
+                    netlink_protocol,
                     WAIT_FOR_NL_RESPONSE_RESULT_FAILED_POLL);
                 return any;
             }
@@ -9549,6 +10068,337 @@ after_read:
 
 /*****************************************************************************/
 
+static guint16
+genl_get_family_id(NMPlatform *platform, NMPGenlFamilyType family_type)
+{
+    NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
+    int                     family_id;
+
+    nm_assert(_NM_INT_NOT_NEGATIVE(family_type));
+    nm_assert(family_type < G_N_ELEMENTS(priv->genl_family_data));
+
+    if (priv->genl_family_data[family_type].family_id != 0)
+        goto out;
+
+    /* Unknown family ID... usually we expect to start using the protocol.
+     * Let's try harder and fetch the ID synchronously.
+     *
+     * Note that we might also be called back during delayed_action_handle_all(),
+     * when we add a WireGuard link, the module gets autoloaded, and we didn't
+     * yet process the genl notification about the new family. Let's not call
+     * delayed_action_handle_all() again, because that might emit various
+     * signals. */
+
+    family_id = genl_ctrl_resolve(priv->sk_genl_sync, nmp_genl_family_infos[family_type].name);
+    if (family_id < 0)
+        family_id = 0;
+
+    /* We cache the family ID and update it via genl notifications.
+     * Here we bypass the order of that, and update the cached value
+     * directly. */
+    _genl_family_id_update(platform, family_type, family_id);
+
+out:
+    return priv->genl_family_data[family_type].family_id;
+}
+
+/*****************************************************************************/
+
+static const NMPObject *
+_mptcp_addrs_dump_parse_addr(struct nlattr *attr)
+{
+    static const struct nla_policy policy[] = {
+        [MPTCP_PM_ADDR_ATTR_FAMILY] = {.type = NLA_U16},
+        [MPTCP_PM_ADDR_ATTR_ID]     = {.type = NLA_U8},
+        [MPTCP_PM_ADDR_ATTR_ADDR4]  = {.minlen = sizeof(in_addr_t)},
+        [MPTCP_PM_ADDR_ATTR_ADDR6]  = {.minlen = sizeof(struct in6_addr)},
+        [MPTCP_PM_ADDR_ATTR_PORT]   = {.type = NLA_U16},
+        [MPTCP_PM_ADDR_ATTR_FLAGS]  = {.type = NLA_U32},
+        [MPTCP_PM_ADDR_ATTR_IF_IDX] = {.type = NLA_S32},
+    };
+    struct nlattr            *tb[G_N_ELEMENTS(policy)];
+    nm_auto_nmpobj NMPObject *obj = NULL;
+    NMPlatformMptcpAddr      *mptcp_addr;
+    int                       addr_family;
+    int                       addr_attr;
+
+    if (nla_parse_nested_arr(tb, attr, policy) < 0)
+        return NULL;
+
+    if (!tb[MPTCP_PM_ADDR_ATTR_ID])
+        return NULL;
+
+    obj        = nmp_object_new(NMP_OBJECT_TYPE_MPTCP_ADDR, NULL);
+    mptcp_addr = &obj->mptcp_addr;
+
+    mptcp_addr->id = nla_get_u8(tb[MPTCP_PM_ADDR_ATTR_ID]);
+
+    if (!tb[MPTCP_PM_ADDR_ATTR_FAMILY]) {
+        /* If we don't have the family. Only create a stub object containing
+         * the ID. */
+        goto out;
+    }
+
+    addr_family = nla_get_u16(tb[MPTCP_PM_ADDR_ATTR_FAMILY]);
+
+    if (addr_family == AF_INET)
+        addr_attr = MPTCP_PM_ADDR_ATTR_ADDR4;
+    else if (addr_family == AF_INET6)
+        addr_attr = MPTCP_PM_ADDR_ATTR_ADDR6;
+    else
+        goto out;
+
+    if (!tb[addr_attr])
+        goto out;
+
+    mptcp_addr->addr_family = addr_family;
+    memcpy(&mptcp_addr->addr, nla_data(tb[addr_attr]), nm_utils_addr_family_to_size(addr_family));
+
+    if (tb[MPTCP_PM_ADDR_ATTR_PORT])
+        mptcp_addr->port = nla_get_u16(tb[MPTCP_PM_ADDR_ATTR_PORT]);
+
+    if (tb[MPTCP_PM_ADDR_ATTR_IF_IDX])
+        mptcp_addr->ifindex = nla_get_s32(tb[MPTCP_PM_ADDR_ATTR_IF_IDX]);
+
+    if (tb[MPTCP_PM_ADDR_ATTR_FLAGS])
+        mptcp_addr->flags = nla_get_u32(tb[MPTCP_PM_ADDR_ATTR_FLAGS]);
+
+out:
+
+    return g_steal_pointer(&obj);
+}
+
+typedef struct {
+    GPtrArray *addrs;
+} FetchMptcpAddrParseData;
+
+static int
+_mptcp_addrs_dump_parse_cb(const struct nl_msg *msg, void *arg)
+{
+    static const struct nla_policy policy[] = {
+        [MPTCP_PM_ATTR_ADDR] = {.type = NLA_NESTED},
+    };
+    struct nlattr           *tb[G_N_ELEMENTS(policy)];
+    FetchMptcpAddrParseData *parse_data = arg;
+
+    if (genlmsg_parse_arr(nlmsg_hdr(msg), 0, tb, policy) < 0)
+        return NL_SKIP;
+
+    if (tb[MPTCP_PM_ATTR_ADDR]) {
+        const NMPObject *obj;
+
+        obj = _mptcp_addrs_dump_parse_addr(tb[MPTCP_PM_ATTR_ADDR]);
+        if (obj)
+            g_ptr_array_add(parse_data->addrs, (gpointer) obj);
+    }
+
+    return NL_OK;
+}
+
+#define EXTACK_MSG_BUFSIZE 200
+
+static int
+_mptcp_addr_update_err_cb(const struct sockaddr_nl *nla, const struct nlmsgerr *nlerr, void *arg)
+{
+    char       *out_extack_msg = arg;
+    const char *extack_msg;
+    int         errsv;
+
+    errsv = nlmsg_parse_error(nlmsg_undata(nlerr), &extack_msg);
+    if (extack_msg)
+        g_strlcpy(out_extack_msg, extack_msg, EXTACK_MSG_BUFSIZE);
+    return errsv;
+}
+
+static int
+mptcp_addr_update(NMPlatform *platform, NMOptionBool add, const NMPlatformMptcpAddr *addr)
+{
+    NMLinuxPlatformPrivate      *priv  = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
+    nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
+    struct nlattr               *nla_nest;
+    char                         sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+    guint16                      genl_family_id;
+    guint8                       cmd_num;
+    const char                  *cmd_str;
+    int                          nle;
+    char                         extack_msg[EXTACK_MSG_BUFSIZE];
+    const struct nl_cb           cb = {
+                  .err_cb  = _mptcp_addr_update_err_cb,
+                  .err_arg = extack_msg,
+    };
+
+    extack_msg[0] = '\0';
+
+    if (add == NM_OPTION_BOOL_DEFAULT) {
+        cmd_num = MPTCP_PM_CMD_SET_FLAGS;
+        cmd_str = "update";
+    } else if (add) {
+        cmd_num = MPTCP_PM_CMD_ADD_ADDR;
+        cmd_str = "add";
+    } else {
+        cmd_num = MPTCP_PM_CMD_DEL_ADDR;
+        cmd_str = "delete";
+    }
+
+    nm_assert_addr_family_or_unspec(addr->addr_family);
+    nm_assert(cmd_num != MPTCP_PM_CMD_ADD_ADDR
+              || (addr->port == 0 && addr->addr_family != AF_UNSPEC));
+    nm_assert(cmd_num != MPTCP_PM_CMD_SET_FLAGS || (addr->id != 0 && addr->port == 0));
+    nm_assert(cmd_num != MPTCP_PM_CMD_DEL_ADDR || addr->id != 0);
+
+    genl_family_id = nm_platform_genl_get_family_id(platform, NMP_GENL_FAMILY_TYPE_MPTCP_PM);
+    if (genl_family_id == 0) {
+        _LOGT("mptcp: %s address %s fails because %s generic netlink family is unknown",
+              cmd_str,
+              nm_platform_mptcp_addr_to_string(addr, sbuf, sizeof(sbuf)),
+              nmp_genl_family_infos[NMP_GENL_FAMILY_TYPE_MPTCP_PM].name);
+        return -ENOSYS;
+    }
+
+    _LOGT("mptcp: %s address %s",
+          cmd_str,
+          nm_platform_mptcp_addr_to_string(addr, sbuf, sizeof(sbuf)));
+
+    nlmsg = nlmsg_alloc_size(nlmsg_total_size(GENL_HDRLEN) + 200);
+
+    if (!genlmsg_put(nlmsg,
+                     NL_AUTO_PORT,
+                     NL_AUTO_SEQ,
+                     genl_family_id,
+                     0,
+                     NLM_F_REQUEST,
+                     cmd_num,
+                     MPTCP_PM_VER))
+        goto nla_put_failure;
+
+    nla_nest = nla_nest_start(nlmsg, MPTCP_PM_ATTR_ADDR | NLA_F_NESTED);
+    if (!nla_nest)
+        goto nla_put_failure;
+
+    if (addr->id != 0)
+        NLA_PUT_U8(nlmsg, MPTCP_PM_ADDR_ATTR_ID, addr->id);
+    if (addr->flags != 0)
+        NLA_PUT_U32(nlmsg, MPTCP_PM_ADDR_ATTR_FLAGS, addr->flags);
+    if (addr->ifindex != 0)
+        NLA_PUT_S32(nlmsg, MPTCP_PM_ADDR_ATTR_IF_IDX, addr->ifindex);
+    if (addr->port != 0)
+        NLA_PUT_U16(nlmsg, MPTCP_PM_ADDR_ATTR_PORT, addr->port);
+    if (addr->addr_family != AF_UNSPEC) {
+        int addr_attr;
+
+        NLA_PUT_U16(nlmsg, MPTCP_PM_ADDR_ATTR_FAMILY, addr->addr_family);
+        if (addr->addr_family == AF_INET)
+            addr_attr = MPTCP_PM_ADDR_ATTR_ADDR4;
+        else if (addr->addr_family == AF_INET6)
+            addr_attr = MPTCP_PM_ADDR_ATTR_ADDR6;
+        else
+            g_return_val_if_reached(-NME_BUG);
+        NLA_PUT(nlmsg, addr_attr, nm_utils_addr_family_to_size(addr->addr_family), &addr->addr);
+    }
+
+    NLA_NEST_END(nlmsg, nla_nest);
+
+    nle = nl_send_auto(priv->sk_genl_sync, nlmsg);
+    if (nle < 0) {
+        _LOGT("mptcp: %s address %s: failed sending request: %s (%d)",
+              cmd_str,
+              nm_platform_mptcp_addr_to_string(addr, sbuf, sizeof(sbuf)),
+              nm_strerror(nle),
+              nle);
+        return nle;
+    }
+
+    do {
+        nle = nl_recvmsgs(priv->sk_genl_sync, &cb);
+    } while (nle == -EAGAIN);
+
+    if (nle < 0) {
+        _LOGT("mptcp: %s address %s: failed: %s (%d)%s%s%s",
+              cmd_str,
+              nm_platform_mptcp_addr_to_string(addr, sbuf, sizeof(sbuf)),
+              nm_strerror(nle),
+              nle,
+              NM_PRINT_FMT_QUOTED(extack_msg[0] != '\0', " \"", extack_msg, "\"", ""));
+        return nle;
+    }
+
+    _LOGT("mptcp: %s address %s: success",
+          cmd_str,
+          nm_platform_mptcp_addr_to_string(addr, sbuf, sizeof(sbuf)));
+
+    return 0;
+
+nla_put_failure:
+    g_return_val_if_reached(-NME_BUG);
+}
+
+static GPtrArray *
+mptcp_addrs_dump(NMPlatform *platform)
+{
+    NMLinuxPlatformPrivate      *priv  = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
+    gs_unref_ptrarray GPtrArray *addrs = NULL;
+    nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
+    FetchMptcpAddrParseData      parse_data;
+    guint16                      genl_family_id;
+    int                          r;
+    guint                        i;
+
+    genl_family_id = nm_platform_genl_get_family_id(platform, NMP_GENL_FAMILY_TYPE_MPTCP_PM);
+    if (genl_family_id == 0) {
+        _LOGT("mptcp: dump addresses fails because %s generic netlink family is unknown",
+              nmp_genl_family_infos[NMP_GENL_FAMILY_TYPE_MPTCP_PM].name);
+        return NULL;
+    }
+
+    nlmsg = nlmsg_alloc_size(nlmsg_total_size(GENL_HDRLEN));
+
+    if (!genlmsg_put(nlmsg,
+                     NL_AUTO_PORT,
+                     NL_AUTO_SEQ,
+                     genl_family_id,
+                     0,
+                     NLM_F_REQUEST | NLM_F_DUMP,
+                     MPTCP_PM_CMD_GET_ADDR,
+                     MPTCP_PM_VER))
+        g_return_val_if_reached(NULL);
+
+    r = nl_send_auto(priv->sk_genl_sync, nlmsg);
+    if (r < 0) {
+        _LOGT("mptcp: dump addresses failed to send dump request: %s", nm_strerror(r));
+        return NULL;
+    }
+
+    addrs = g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
+
+    parse_data = (FetchMptcpAddrParseData){
+        .addrs = addrs,
+    };
+
+    nl_recvmsgs(priv->sk_genl_sync,
+                &((const struct nl_cb){
+                    .valid_cb  = _mptcp_addrs_dump_parse_cb,
+                    .valid_arg = (gpointer) &parse_data,
+                }));
+
+    if (_LOGT_ENABLED()) {
+        _LOGT("mptcp: %u addresses dumped", addrs->len);
+        for (i = 0; i < addrs->len; i++) {
+            char sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+
+            _LOGT("mptcp: address[%04d]: %s",
+                  i,
+                  nmp_object_to_string(addrs->pdata[i],
+                                       NMP_OBJECT_TO_STRING_PUBLIC,
+                                       sbuf,
+                                       sizeof(sbuf)));
+        }
+    }
+
+    return g_steal_pointer(&addrs);
+}
+
+/*****************************************************************************/
+
 static void
 cache_update_link_udev(NMPlatform *platform, int ifindex, struct udev_device *udevice)
 {
@@ -9689,7 +10539,9 @@ nm_linux_platform_init(NMLinuxPlatform *self)
 
     priv->delayed_action.list_master_connected = g_ptr_array_new();
     priv->delayed_action.list_refresh_link     = g_ptr_array_new();
-    priv->delayed_action.list_wait_for_nl_response =
+    priv->delayed_action.list_wait_for_response_rtnl =
+        g_array_new(FALSE, TRUE, sizeof(DelayedActionWaitForNlResponseData));
+    priv->delayed_action.list_wait_for_response_genl =
         g_array_new(FALSE, TRUE, sizeof(DelayedActionWaitForNlResponseData));
 }
 
@@ -9721,43 +10573,53 @@ constructed(GObject *_object)
           nm_platform_get_use_udev(platform) ? "use" : "no",
           nm_platform_get_cache_tc(platform) ? "use" : "no");
 
-    priv->genl = nl_socket_alloc();
-    g_assert(priv->genl);
+    /*************************************************************************/
 
-    nle = nl_connect(priv->genl, NETLINK_GENERIC);
-    if (nle) {
-        _LOGE("unable to connect the generic netlink socket \"%s\" (%d)", nm_strerror(nle), -nle);
-        nl_socket_free(priv->genl);
-        priv->genl = NULL;
-    }
+    nle = nl_socket_new(&priv->sk_genl_sync, NETLINK_GENERIC, NL_SOCKET_FLAGS_NONE, 0, 0);
+    g_assert(!nle);
 
-    priv->nlh = nl_socket_alloc();
-    g_assert(priv->nlh);
+    _LOGD("genl: generic netlink socket for sync operations created: port=%u, fd=%d",
+          nl_socket_get_local_port(priv->sk_genl_sync),
+          nl_socket_get_fd(priv->sk_genl_sync));
 
-    nle = nl_connect(priv->nlh, NETLINK_ROUTE);
-    g_assert(!nle);
-    nle = nl_socket_set_passcred(priv->nlh, 1);
-    g_assert(!nle);
+    /*************************************************************************/
 
-    /* No blocking for event socket, so that we can drain it safely. */
-    nle = nl_socket_set_nonblocking(priv->nlh);
+    /* disable MSG_PEEK, we will handle lost messages ourselves. */
+    nle = nl_socket_new(&priv->sk_genl,
+                        NETLINK_GENERIC,
+                        NL_SOCKET_FLAGS_NONBLOCK | NL_SOCKET_FLAGS_PASSCRED
+                            | NL_SOCKET_FLAGS_DISABLE_MSG_PEEK,
+                        8 * 1024 * 1024,
+                        0);
     g_assert(!nle);
 
-    /* use 8 MB for receive socket kernel queue. */
-    nle = nl_socket_set_buffer_size(priv->nlh, 8 * 1024 * 1024, 0);
+    nle = nl_socket_add_memberships(priv->sk_genl, GENL_ID_CTRL, 0);
     g_assert(!nle);
 
-    nle = nl_socket_set_ext_ack(priv->nlh, TRUE);
-    if (nle)
-        _LOGD("could not enable extended acks on netlink socket");
+    fd = nl_socket_get_fd(priv->sk_genl);
 
-    /* explicitly set the msg buffer size and disable MSG_PEEK.
-     * We use our own receive buffer priv->netlink_recv_buf.
-     * If we encounter NME_NL_MSG_TRUNC, we will increase the buffer
-     * and resync (as we would have lost the message without NL_MSG_PEEK). */
-    nl_socket_disable_msg_peek(priv->nlh);
+    _LOGD("genl: generic netlink socket created: port=%u, fd=%d",
+          nl_socket_get_local_port(priv->sk_genl),
+          fd);
+
+    priv->event_source_genl =
+        nm_g_unix_fd_add_source(fd,
+                                G_IO_IN | G_IO_NVAL | G_IO_PRI | G_IO_ERR | G_IO_HUP,
+                                _nl_event_handler_genl,
+                                platform);
+
+    /*************************************************************************/
+
+    /* disable MSG_PEEK, we will handle lost messages ourselves. */
+    nle = nl_socket_new(&priv->sk_rtnl,
+                        NETLINK_ROUTE,
+                        NL_SOCKET_FLAGS_NONBLOCK | NL_SOCKET_FLAGS_PASSCRED
+                            | NL_SOCKET_FLAGS_DISABLE_MSG_PEEK,
+                        8 * 1024 * 1024,
+                        0);
+    g_assert(!nle);
 
-    nle = nl_socket_add_memberships(priv->nlh,
+    nle = nl_socket_add_memberships(priv->sk_rtnl,
                                     RTNLGRP_IPV4_IFADDR,
                                     RTNLGRP_IPV4_ROUTE,
                                     RTNLGRP_IPV4_RULE,
@@ -9769,37 +10631,32 @@ constructed(GObject *_object)
     g_assert(!nle);
 
     if (nm_platform_get_cache_tc(platform)) {
-        nle = nl_socket_add_memberships(priv->nlh, RTNLGRP_TC, 0);
+        nle = nl_socket_add_memberships(priv->sk_rtnl, RTNLGRP_TC, 0);
         nm_assert(!nle);
     }
 
-    fd = nl_socket_get_fd(priv->nlh);
+    fd = nl_socket_get_fd(priv->sk_rtnl);
 
-    _LOGD("Netlink socket for events established: port=%u, fd=%d",
-          nl_socket_get_local_port(priv->nlh),
+    _LOGD("rtnl: rtnetlink socket created: port=%u, fd=%d",
+          nl_socket_get_local_port(priv->sk_rtnl),
           fd);
 
-    priv->event_source =
+    priv->event_source_rtnl =
         nm_g_unix_fd_add_source(fd,
                                 G_IO_IN | G_IO_NVAL | G_IO_PRI | G_IO_ERR | G_IO_HUP,
-                                event_handler,
+                                _nl_event_handler_rtnl,
                                 platform);
 
+    /*************************************************************************/
+
     /* complete construction of the GObject instance before populating the cache. */
     G_OBJECT_CLASS(nm_linux_platform_parent_class)->constructed(_object);
 
     _LOGD("populate platform cache");
-    delayed_action_schedule(
-        platform,
-        DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS | DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES
-            | DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES
-            | DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES
-            | DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES
-            | DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL
-            | DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS | DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS,
-        NULL);
+    delayed_action_schedule_refresh_all(platform, NMP_NETLINK_ROUTE);
+    delayed_action_schedule_refresh_all(platform, NMP_NETLINK_GENERIC);
 
-    delayed_action_handle_all(platform, FALSE);
+    delayed_action_handle_all(platform);
 
     /* Set up udev monitoring */
     if (priv->udev_client) {
@@ -9881,6 +10738,11 @@ dispose(GObject *object)
     _LOGD("dispose");
 
     delayed_action_wait_for_nl_response_complete_all(platform,
+                                                     NMP_NETLINK_GENERIC,
+                                                     WAIT_FOR_NL_RESPONSE_RESULT_FAILED_DISPOSING);
+
+    delayed_action_wait_for_nl_response_complete_all(platform,
+                                                     NMP_NETLINK_ROUTE,
                                                      WAIT_FOR_NL_RESPONSE_RESULT_FAILED_DISPOSING);
 
     priv->delayed_action.flags = DELAYED_ACTION_TYPE_NONE;
@@ -9897,13 +10759,15 @@ finalize(GObject *object)
 
     g_ptr_array_unref(priv->delayed_action.list_master_connected);
     g_ptr_array_unref(priv->delayed_action.list_refresh_link);
-    g_array_unref(priv->delayed_action.list_wait_for_nl_response);
+    g_array_unref(priv->delayed_action.list_wait_for_response_rtnl);
+    g_array_unref(priv->delayed_action.list_wait_for_response_genl);
 
-    nl_socket_free(priv->genl);
+    nm_clear_g_source_inst(&priv->event_source_genl);
+    nm_clear_g_source_inst(&priv->event_source_rtnl);
 
-    nm_clear_g_source_inst(&priv->event_source);
-
-    nl_socket_free(priv->nlh);
+    nl_socket_free(priv->sk_genl_sync);
+    nl_socket_free(priv->sk_genl);
+    nl_socket_free(priv->sk_rtnl);
 
     {
         NM_G_MUTEX_LOCKED(&sysctl_clear_cache_lock);
@@ -10021,4 +10885,8 @@ nm_linux_platform_class_init(NMLinuxPlatformClass *klass)
     platform_class->tfilter_delete = tfilter_delete;
 
     platform_class->process_events = process_events;
+
+    platform_class->genl_get_family_id = genl_get_family_id;
+    platform_class->mptcp_addr_update  = mptcp_addr_update;
+    platform_class->mptcp_addrs_dump   = mptcp_addrs_dump;
 }
diff --git a/src/libnm-platform/nm-netlink.c b/src/libnm-platform/nm-netlink.c
index 697ae591..36841d56 100644
--- a/src/libnm-platform/nm-netlink.c
+++ b/src/libnm-platform/nm-netlink.c
@@ -18,10 +18,15 @@
 
 /*****************************************************************************/
 
-#define NL_SOCK_PASSCRED     (1 << 1)
-#define NL_MSG_PEEK          (1 << 3)
-#define NL_MSG_PEEK_EXPLICIT (1 << 4)
-#define NL_NO_AUTO_ACK       (1 << 5)
+#define nm_assert_sk(sk)                  \
+    G_STMT_START                          \
+    {                                     \
+        const struct nl_sock *_sk = (sk); \
+                                          \
+        nm_assert(_sk);                   \
+        nm_assert(_sk->s_fd >= 0);        \
+    }                                     \
+    G_STMT_END
 
 #ifndef NETLINK_EXT_ACK
 #define NETLINK_EXT_ACK 11
@@ -33,19 +38,20 @@ struct nl_msg {
     struct sockaddr_nl nm_dst;
     struct ucred       nm_creds;
     struct nlmsghdr   *nm_nlh;
-    size_t             nm_size;
+    uint32_t           nm_size;
     bool               nm_creds_has : 1;
 };
 
 struct nl_sock {
     struct sockaddr_nl s_local;
     struct sockaddr_nl s_peer;
+    size_t             s_bufsize;
     int                s_fd;
     int                s_proto;
     unsigned int       s_seq_next;
     unsigned int       s_seq_expect;
-    int                s_flags;
-    size_t             s_bufsize;
+    bool               s_msg_peek : 1;
+    bool               s_auto_ack : 1;
 };
 
 /*****************************************************************************/
@@ -71,13 +77,62 @@ NM_UTILS_FLAGS2STR_DEFINE(nl_nlmsg_flags2str,
                           NM_UTILS_FLAGS2STR(NLM_F_CREATE, "CREATE"),
                           NM_UTILS_FLAGS2STR(NLM_F_APPEND, "APPEND"), );
 
+static NM_UTILS_LOOKUP_STR_DEFINE(_rtnl_type_to_str,
+                                  guint16,
+                                  NM_UTILS_LOOKUP_DEFAULT(NULL),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_GETLINK, "RTM_GETLINK"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_NEWLINK, "RTM_NEWLINK"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_DELLINK, "RTM_DELLINK"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_SETLINK, "RTM_SETLINK"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_GETADDR, "RTM_GETADDR"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_NEWADDR, "RTM_NEWADDR"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_DELADDR, "RTM_DELADDR"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_GETROUTE, "RTM_GETROUTE"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_NEWROUTE, "RTM_NEWROUTE"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_DELROUTE, "RTM_DELROUTE"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_GETRULE, "RTM_GETRULE"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_NEWRULE, "RTM_NEWRULE"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_DELRULE, "RTM_DELRULE"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_GETQDISC, "RTM_GETQDISC"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_NEWQDISC, "RTM_NEWQDISC"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_DELQDISC, "RTM_DELQDISC"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_GETTFILTER, "RTM_GETTFILTER"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_NEWTFILTER, "RTM_NEWTFILTER"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(RTM_DELTFILTER, "RTM_DELTFILTER"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NLMSG_NOOP, "NLMSG_NOOP"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NLMSG_ERROR, "NLMSG_ERROR"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NLMSG_DONE, "NLMSG_DONE"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NLMSG_OVERRUN, "NLMSG_OVERRUN"), );
+
+static NM_UTILS_LOOKUP_STR_DEFINE(
+    _genl_ctrl_cmd_to_str,
+    guint8,
+    NM_UTILS_LOOKUP_DEFAULT(NULL),
+    NM_UTILS_LOOKUP_STR_ITEM(CTRL_CMD_UNSPEC, "CTRL_CMD_UNSPEC"),
+    NM_UTILS_LOOKUP_STR_ITEM(CTRL_CMD_NEWFAMILY, "CTRL_CMD_NEWFAMILY"),
+    NM_UTILS_LOOKUP_STR_ITEM(CTRL_CMD_DELFAMILY, "CTRL_CMD_DELFAMILY"),
+    NM_UTILS_LOOKUP_STR_ITEM(CTRL_CMD_GETFAMILY, "CTRL_CMD_GETFAMILY"),
+    NM_UTILS_LOOKUP_STR_ITEM(CTRL_CMD_NEWOPS, "CTRL_CMD_NEWOPS"),
+    NM_UTILS_LOOKUP_STR_ITEM(CTRL_CMD_DELOPS, "CTRL_CMD_DELOPS"),
+    NM_UTILS_LOOKUP_STR_ITEM(CTRL_CMD_GETOPS, "CTRL_CMD_GETOPS"),
+    NM_UTILS_LOOKUP_STR_ITEM(CTRL_CMD_NEWMCAST_GRP, "CTRL_CMD_NEWMCAST_GRP"),
+    NM_UTILS_LOOKUP_STR_ITEM(CTRL_CMD_DELMCAST_GRP, "CTRL_CMD_DELMCAST_GRP"),
+    NM_UTILS_LOOKUP_STR_ITEM(CTRL_CMD_GETMCAST_GRP, "CTRL_CMD_GETMCAST_GRP"),
+    /* CTRL_CMD_GETPOLICY was added in Linux 5.7 (released on 31 May, 2020),
+     * commit d07dcf9aadd6 ('netlink: add infrastructure to expose policies to userspace') */
+    NM_UTILS_LOOKUP_STR_ITEM(10 /* CTRL_CMD_GETPOLICY */, "CTRL_CMD_GETPOLICY"), );
+
 /*****************************************************************************/
 
 const char *
-nl_nlmsghdr_to_str(const struct nlmsghdr *hdr, char *buf, gsize len)
+nl_nlmsghdr_to_str(int                    netlink_protocol,
+                   guint32                pktinfo_group,
+                   const struct nlmsghdr *hdr,
+                   char                  *buf,
+                   gsize                  len)
 {
     const char *b;
-    const char *s;
+    const char *s = NULL;
     guint       flags, flags_before;
     const char *prefix;
 
@@ -86,86 +141,46 @@ nl_nlmsghdr_to_str(const struct nlmsghdr *hdr, char *buf, gsize len)
 
     b = buf;
 
-    switch (hdr->nlmsg_type) {
-    case RTM_GETLINK:
-        s = "RTM_GETLINK";
-        break;
-    case RTM_NEWLINK:
-        s = "RTM_NEWLINK";
-        break;
-    case RTM_DELLINK:
-        s = "RTM_DELLINK";
-        break;
-    case RTM_SETLINK:
-        s = "RTM_SETLINK";
-        break;
-    case RTM_GETADDR:
-        s = "RTM_GETADDR";
-        break;
-    case RTM_NEWADDR:
-        s = "RTM_NEWADDR";
-        break;
-    case RTM_DELADDR:
-        s = "RTM_DELADDR";
-        break;
-    case RTM_GETROUTE:
-        s = "RTM_GETROUTE";
-        break;
-    case RTM_NEWROUTE:
-        s = "RTM_NEWROUTE";
-        break;
-    case RTM_DELROUTE:
-        s = "RTM_DELROUTE";
-        break;
-    case RTM_GETRULE:
-        s = "RTM_GETRULE";
-        break;
-    case RTM_NEWRULE:
-        s = "RTM_NEWRULE";
-        break;
-    case RTM_DELRULE:
-        s = "RTM_DELRULE";
-        break;
-    case RTM_GETQDISC:
-        s = "RTM_GETQDISC";
-        break;
-    case RTM_NEWQDISC:
-        s = "RTM_NEWQDISC";
-        break;
-    case RTM_DELQDISC:
-        s = "RTM_DELQDISC";
-        break;
-    case RTM_GETTFILTER:
-        s = "RTM_GETTFILTER";
-        break;
-    case RTM_NEWTFILTER:
-        s = "RTM_NEWTFILTER";
-        break;
-    case RTM_DELTFILTER:
-        s = "RTM_DELTFILTER";
-        break;
-    case NLMSG_NOOP:
-        s = "NLMSG_NOOP";
-        break;
-    case NLMSG_ERROR:
-        s = "NLMSG_ERROR";
-        break;
-    case NLMSG_DONE:
-        s = "NLMSG_DONE";
-        break;
-    case NLMSG_OVERRUN:
-        s = "NLMSG_OVERRUN";
+    switch (netlink_protocol) {
+    case NETLINK_ROUTE:
+        s = _rtnl_type_to_str(hdr->nlmsg_type);
+        if (s)
+            nm_strbuf_append_str(&buf, &len, s);
+        else
+            nm_strbuf_append(&buf, &len, "(%u)", (unsigned) hdr->nlmsg_type);
         break;
     default:
+        nm_assert_not_reached();
+        /* fall-through */
+    case NETLINK_GENERIC:
+        if (pktinfo_group == 0)
+            nm_strbuf_append(&buf, &len, "group:unicast");
+        else
+            nm_strbuf_append(&buf, &len, "group:multicast(%u)", (unsigned) pktinfo_group);
+
         s = NULL;
+        if (hdr->nlmsg_type == GENL_ID_CTRL)
+            s = "GENL_ID_CTRL";
+        if (s)
+            nm_strbuf_append(&buf, &len, ", msg-type:%s", s);
+        else
+            nm_strbuf_append(&buf, &len, ", msg-type:(%u)", (unsigned) hdr->nlmsg_type);
+
+        if (genlmsg_valid_hdr(hdr, 0)) {
+            const struct genlmsghdr *ghdr;
+
+            ghdr = nlmsg_data(hdr);
+            s    = NULL;
+            if (hdr->nlmsg_type == GENL_ID_CTRL)
+                s = _genl_ctrl_cmd_to_str(ghdr->cmd);
+            if (s)
+                nm_strbuf_append(&buf, &len, ", cmd:%s", s);
+            else
+                nm_strbuf_append(&buf, &len, ", cmd:(%u)", (unsigned) ghdr->cmd);
+        }
         break;
     }
 
-    if (s)
-        nm_strbuf_append_str(&buf, &len, s);
-    else
-        nm_strbuf_append(&buf, &len, "(%u)", (unsigned) hdr->nlmsg_type);
-
     flags = hdr->nlmsg_flags;
 
     if (!flags) {
@@ -198,27 +213,30 @@ nl_nlmsghdr_to_str(const struct nlmsghdr *hdr, char *buf, gsize len)
     if (flags_before != flags)
         prefix = ";";
 
-    switch (hdr->nlmsg_type) {
-    case RTM_NEWLINK:
-    case RTM_NEWADDR:
-    case RTM_NEWROUTE:
-    case RTM_NEWQDISC:
-    case RTM_NEWTFILTER:
-        _F(NLM_F_REPLACE, "replace");
-        _F(NLM_F_EXCL, "excl");
-        _F(NLM_F_CREATE, "create");
-        _F(NLM_F_APPEND, "append");
-        break;
-    case RTM_GETLINK:
-    case RTM_GETADDR:
-    case RTM_GETROUTE:
-    case RTM_DELQDISC:
-    case RTM_DELTFILTER:
-        _F(NLM_F_DUMP, "dump");
-        _F(NLM_F_ROOT, "root");
-        _F(NLM_F_MATCH, "match");
-        _F(NLM_F_ATOMIC, "atomic");
-        break;
+    switch (netlink_protocol) {
+    case NETLINK_ROUTE:
+        switch (hdr->nlmsg_type) {
+        case RTM_NEWLINK:
+        case RTM_NEWADDR:
+        case RTM_NEWROUTE:
+        case RTM_NEWQDISC:
+        case RTM_NEWTFILTER:
+            _F(NLM_F_REPLACE, "replace");
+            _F(NLM_F_EXCL, "excl");
+            _F(NLM_F_CREATE, "create");
+            _F(NLM_F_APPEND, "append");
+            break;
+        case RTM_GETLINK:
+        case RTM_GETADDR:
+        case RTM_GETROUTE:
+        case RTM_DELQDISC:
+        case RTM_DELTFILTER:
+            _F(NLM_F_DUMP, "dump");
+            _F(NLM_F_ROOT, "root");
+            _F(NLM_F_MATCH, "match");
+            _F(NLM_F_ATOMIC, "atomic");
+            break;
+        }
     }
 
 #undef _F
@@ -237,29 +255,32 @@ flags_done:
 /*****************************************************************************/
 
 struct nlmsghdr *
-nlmsg_hdr(struct nl_msg *n)
+nlmsg_hdr(const struct nl_msg *n)
 {
     return n->nm_nlh;
 }
 
 void *
-nlmsg_reserve(struct nl_msg *n, size_t len, int pad)
+nlmsg_reserve(struct nl_msg *n, uint32_t len, uint32_t pad)
 {
-    char  *buf       = (char *) n->nm_nlh;
-    size_t nlmsg_len = n->nm_nlh->nlmsg_len;
-    size_t tlen;
-
-    nm_assert(pad >= 0);
+    char    *buf = (char *) n->nm_nlh;
+    uint32_t tlen;
 
-    if (len > n->nm_size)
-        return NULL;
+    nm_assert(n);
+    nm_assert(pad == 0 || nm_utils_is_power_of_two(pad));
+    nm_assert(n->nm_nlh->nlmsg_len <= n->nm_size);
 
-    tlen = pad ? ((len + (pad - 1)) & ~(pad - 1)) : len;
+    if (pad != 0) {
+        tlen = (len + (pad - 1u)) & ~(pad - 1u);
+        if (tlen < len)
+            return NULL;
+    } else
+        tlen = len;
 
-    if ((tlen + nlmsg_len) > n->nm_size)
+    if (tlen > n->nm_size - n->nm_nlh->nlmsg_len)
         return NULL;
 
-    buf += nlmsg_len;
+    buf += n->nm_nlh->nlmsg_len;
     n->nm_nlh->nlmsg_len += tlen;
 
     if (tlen > len)
@@ -270,6 +291,52 @@ nlmsg_reserve(struct nl_msg *n, size_t len, int pad)
 
 /*****************************************************************************/
 
+int
+nlmsg_parse_error(const struct nlmsghdr *nlh, const char **out_extack_msg)
+{
+    const struct nlmsgerr *e;
+
+    nm_assert(nlh);
+
+    NM_SET_OUT(out_extack_msg, NULL);
+
+    if (nlh->nlmsg_type != NLMSG_ERROR)
+        return -NME_NL_MSG_INVAL;
+
+    if (nlh->nlmsg_len < nlmsg_size(sizeof(struct nlmsgerr))) {
+        /* Truncated error message, the default action
+         * is to stop parsing. The user may overrule
+         * this action by returning NL_SKIP or
+         * NL_PROCEED (dangerous) */
+        return -NME_NL_MSG_TRUNC;
+    }
+
+    e = nlmsg_data(nlh);
+
+    if (!e->error)
+        return 0;
+
+    if (NM_FLAGS_HAS(nlh->nlmsg_flags, NLM_F_ACK_TLVS) && out_extack_msg
+        && nlh->nlmsg_len >= sizeof(*e) + e->msg.nlmsg_len) {
+        static const struct nla_policy policy[] = {
+            [NLMSGERR_ATTR_MSG]  = {.type = NLA_STRING},
+            [NLMSGERR_ATTR_OFFS] = {.type = NLA_U32},
+        };
+        struct nlattr *tb[G_N_ELEMENTS(policy)];
+        struct nlattr *tlvs;
+
+        tlvs = (struct nlattr *) ((char *) e + sizeof(*e) + e->msg.nlmsg_len - NLMSG_HDRLEN);
+        if (nla_parse_arr(tb, tlvs, nlh->nlmsg_len - sizeof(*e) - e->msg.nlmsg_len, policy) >= 0) {
+            if (tb[NLMSGERR_ATTR_MSG])
+                *out_extack_msg = nla_get_string(tb[NLMSGERR_ATTR_MSG]);
+        }
+    }
+
+    return -nm_errno_from_native(e->error);
+}
+
+/*****************************************************************************/
+
 struct nlattr *
 nla_reserve(struct nl_msg *msg, int attrtype, int attrlen)
 {
@@ -304,6 +371,8 @@ nlmsg_alloc_size(size_t len)
 
     if (len < sizeof(struct nlmsghdr))
         len = sizeof(struct nlmsghdr);
+    else if (len > UINT32_MAX)
+        g_return_val_if_reached(NULL);
 
     nm  = g_slice_new(struct nl_msg);
     *nm = (struct nl_msg){
@@ -341,7 +410,7 @@ nlmsg_alloc_convert(struct nlmsghdr *hdr)
 }
 
 struct nl_msg *
-nlmsg_alloc_simple(int nlmsgtype, int flags)
+nlmsg_alloc_simple(uint16_t nlmsgtype, uint16_t flags)
 {
     struct nl_msg *nm;
     struct nlmsghdr *new;
@@ -366,27 +435,27 @@ nlmsg_free(struct nl_msg *msg)
 /*****************************************************************************/
 
 int
-nlmsg_append(struct nl_msg *n, const void *data, size_t len, int pad)
+nlmsg_append(struct nl_msg *n, const void *data, uint32_t len, uint32_t pad)
 {
     void *tmp;
 
     nm_assert(n);
-    nm_assert(data);
-    nm_assert(len > 0);
-    nm_assert(pad >= 0);
+    nm_assert(len == 0 || data);
 
     tmp = nlmsg_reserve(n, len, pad);
-    if (tmp == NULL)
+    if (!tmp)
         return -ENOMEM;
 
-    memcpy(tmp, data, len);
+    if (len > 0)
+        memcpy(tmp, data, len);
+
     return 0;
 }
 
 /*****************************************************************************/
 
 int
-nlmsg_parse(struct nlmsghdr         *nlh,
+nlmsg_parse(const struct nlmsghdr   *nlh,
             int                      hdrlen,
             struct nlattr           *tb[],
             int                      maxtype,
@@ -399,14 +468,17 @@ nlmsg_parse(struct nlmsghdr         *nlh,
 }
 
 struct nlmsghdr *
-nlmsg_put(struct nl_msg *n, uint32_t pid, uint32_t seq, int type, int payload, int flags)
+nlmsg_put(struct nl_msg *n,
+          uint32_t       pid,
+          uint32_t       seq,
+          uint16_t       type,
+          uint32_t       payload,
+          uint16_t       flags)
 {
-    struct nlmsghdr *nlh;
+    struct nlmsghdr *nlh = (struct nlmsghdr *) n->nm_nlh;
 
-    if (n->nm_nlh->nlmsg_len < NLMSG_HDRLEN)
-        g_return_val_if_reached(NULL);
+    nm_assert(nlh->nlmsg_len >= NLMSG_HDRLEN);
 
-    nlh              = (struct nlmsghdr *) n->nm_nlh;
     nlh->nlmsg_type  = type;
     nlh->nlmsg_flags = flags;
     nlh->nlmsg_pid   = pid;
@@ -547,7 +619,8 @@ nla_nest_start(struct nl_msg *msg, int attrtype)
 static int
 _nest_end(struct nl_msg *msg, struct nlattr *start, int keep_empty)
 {
-    size_t pad, len;
+    size_t   len;
+    uint32_t pad;
 
     len = (char *) nlmsg_tail(msg->nm_nlh) - (char *) start;
 
@@ -566,14 +639,18 @@ _nest_end(struct nl_msg *msg, struct nlattr *start, int keep_empty)
 
     pad = NLMSG_ALIGN(msg->nm_nlh->nlmsg_len) - msg->nm_nlh->nlmsg_len;
     if (pad > 0) {
+        void *p;
+
         /*
          * Data inside attribute does not end at a alignment boundary.
          * Pad accordingly and account for the additional space in
          * the message. nlmsg_reserve() may never fail in this situation,
          * the allocate message buffer must be a multiple of NLMSG_ALIGNTO.
          */
-        if (!nlmsg_reserve(msg, pad, 0))
+        p = nlmsg_reserve(msg, pad, 0);
+        if (!p)
             g_return_val_if_reached(-NME_BUG);
+        memset(p, 0, pad);
     }
 
     return 0;
@@ -588,6 +665,7 @@ nla_nest_end(struct nl_msg *msg, struct nlattr *start)
 static const uint8_t nla_attr_minlen[NLA_TYPE_MAX + 1] = {
     [NLA_U8]     = sizeof(uint8_t),
     [NLA_U16]    = sizeof(uint16_t),
+    [NLA_S32]    = sizeof(int32_t),
     [NLA_U32]    = sizeof(uint32_t),
     [NLA_U64]    = sizeof(uint64_t),
     [NLA_STRING] = 1,
@@ -712,9 +790,9 @@ void *
 genlmsg_put(struct nl_msg *msg,
             uint32_t       port,
             uint32_t       seq,
-            int            family,
-            int            hdrlen,
-            int            flags,
+            uint16_t       family,
+            uint32_t       hdrlen,
+            uint16_t       flags,
             uint8_t        cmd,
             uint8_t        version)
 {
@@ -745,8 +823,8 @@ genlmsg_user_hdr(const struct genlmsghdr *gnlh)
     return genlmsg_data(gnlh);
 }
 
-struct genlmsghdr *
-genlmsg_hdr(struct nlmsghdr *nlh)
+const struct genlmsghdr *
+genlmsg_hdr(const struct nlmsghdr *nlh)
 {
     return nlmsg_data(nlh);
 }
@@ -779,7 +857,7 @@ genlmsg_attrlen(const struct genlmsghdr *gnlh, int hdrlen)
 }
 
 int
-genlmsg_valid_hdr(struct nlmsghdr *nlh, int hdrlen)
+genlmsg_valid_hdr(const struct nlmsghdr *nlh, int hdrlen)
 {
     struct genlmsghdr *ghdr;
 
@@ -794,13 +872,13 @@ genlmsg_valid_hdr(struct nlmsghdr *nlh, int hdrlen)
 }
 
 int
-genlmsg_parse(struct nlmsghdr         *nlh,
+genlmsg_parse(const struct nlmsghdr   *nlh,
               int                      hdrlen,
               struct nlattr           *tb[],
               int                      maxtype,
               const struct nla_policy *policy)
 {
-    struct genlmsghdr *ghdr;
+    const struct genlmsghdr *ghdr;
 
     if (!genlmsg_valid_hdr(nlh, hdrlen))
         return -NME_NL_MSG_TOOSHORT;
@@ -813,23 +891,24 @@ genlmsg_parse(struct nlmsghdr         *nlh,
                      policy);
 }
 
+const struct nla_policy genl_ctrl_policy[CTRL_ATTR_MCAST_GROUPS + 1] = {
+    [CTRL_ATTR_FAMILY_ID]    = {.type = NLA_U16},
+    [CTRL_ATTR_FAMILY_NAME]  = {.type = NLA_STRING, .maxlen = GENL_NAMSIZ},
+    [CTRL_ATTR_VERSION]      = {.type = NLA_U32},
+    [CTRL_ATTR_HDRSIZE]      = {.type = NLA_U32},
+    [CTRL_ATTR_MAXATTR]      = {.type = NLA_U32},
+    [CTRL_ATTR_OPS]          = {.type = NLA_NESTED},
+    [CTRL_ATTR_MCAST_GROUPS] = {.type = NLA_NESTED},
+};
+
 static int
-_genl_parse_getfamily(struct nl_msg *msg, void *arg)
+_genl_parse_getfamily(const struct nl_msg *msg, void *arg)
 {
-    static const struct nla_policy ctrl_policy[] = {
-        [CTRL_ATTR_FAMILY_ID]    = {.type = NLA_U16},
-        [CTRL_ATTR_FAMILY_NAME]  = {.type = NLA_STRING, .maxlen = GENL_NAMSIZ},
-        [CTRL_ATTR_VERSION]      = {.type = NLA_U32},
-        [CTRL_ATTR_HDRSIZE]      = {.type = NLA_U32},
-        [CTRL_ATTR_MAXATTR]      = {.type = NLA_U32},
-        [CTRL_ATTR_OPS]          = {.type = NLA_NESTED},
-        [CTRL_ATTR_MCAST_GROUPS] = {.type = NLA_NESTED},
-    };
-    struct nlattr   *tb[G_N_ELEMENTS(ctrl_policy)];
+    struct nlattr   *tb[G_N_ELEMENTS(genl_ctrl_policy)];
     struct nlmsghdr *nlh           = nlmsg_hdr(msg);
     gint32          *response_data = arg;
 
-    if (genlmsg_parse_arr(nlh, 0, tb, ctrl_policy) < 0)
+    if (genlmsg_parse_arr(nlh, 0, tb, genl_ctrl_policy) < 0)
         return NL_SKIP;
 
     if (tb[CTRL_ATTR_FAMILY_ID])
@@ -879,30 +958,14 @@ genl_ctrl_resolve(struct nl_sock *sk, const char *name)
 
 /*****************************************************************************/
 
-struct nl_sock *
-nl_socket_alloc(void)
-{
-    struct nl_sock *sk;
-
-    sk = g_slice_new0(struct nl_sock);
-
-    sk->s_fd              = -1;
-    sk->s_local.nl_family = AF_NETLINK;
-    sk->s_peer.nl_family  = AF_NETLINK;
-    sk->s_seq_expect = sk->s_seq_next = time(NULL);
-
-    return sk;
-}
-
 void
 nl_socket_free(struct nl_sock *sk)
 {
     if (!sk)
         return;
 
-    if (sk->s_fd >= 0)
-        nm_close(sk->s_fd);
-    g_slice_free(struct nl_sock, sk);
+    nm_close(sk->s_fd);
+    nm_g_slice_free(sk);
 }
 
 int
@@ -928,18 +991,24 @@ nl_socket_set_passcred(struct nl_sock *sk, int state)
 {
     int err;
 
-    if (sk->s_fd == -1)
-        return -NME_NL_BAD_SOCK;
+    nm_assert_sk(sk);
 
     err = setsockopt(sk->s_fd, SOL_SOCKET, SO_PASSCRED, &state, sizeof(state));
     if (err < 0)
         return -nm_errno_from_native(errno);
+    return 0;
+}
 
-    if (state)
-        sk->s_flags |= NL_SOCK_PASSCRED;
-    else
-        sk->s_flags &= ~NL_SOCK_PASSCRED;
+int
+nl_socket_set_pktinfo(struct nl_sock *sk, int state)
+{
+    int err;
 
+    nm_assert_sk(sk);
+
+    err = setsockopt(sk->s_fd, SOL_NETLINK, NETLINK_PKTINFO, &state, sizeof(state));
+    if (err < 0)
+        return -nm_errno_from_native(errno);
     return 0;
 }
 
@@ -960,8 +1029,7 @@ nlmsg_get_dst(struct nl_msg *msg)
 int
 nl_socket_set_nonblocking(const struct nl_sock *sk)
 {
-    if (sk->s_fd == -1)
-        return -NME_NL_BAD_SOCK;
+    nm_assert_sk(sk);
 
     if (fcntl(sk->s_fd, F_SETFL, O_NONBLOCK) < 0)
         return -nm_errno_from_native(errno);
@@ -974,15 +1042,14 @@ nl_socket_set_buffer_size(struct nl_sock *sk, int rxbuf, int txbuf)
 {
     int err;
 
+    nm_assert_sk(sk);
+
     if (rxbuf <= 0)
         rxbuf = 32768;
 
     if (txbuf <= 0)
         txbuf = 32768;
 
-    if (sk->s_fd == -1)
-        return -NME_NL_BAD_SOCK;
-
     err = setsockopt(sk->s_fd, SOL_SOCKET, SO_SNDBUF, &txbuf, sizeof(txbuf));
     if (err < 0) {
         return -nm_errno_from_native(errno);
@@ -1002,8 +1069,7 @@ nl_socket_add_memberships(struct nl_sock *sk, int group, ...)
     int     err;
     va_list ap;
 
-    if (sk->s_fd == -1)
-        return -NME_NL_BAD_SOCK;
+    nm_assert_sk(sk);
 
     va_start(ap, group);
 
@@ -1029,85 +1095,96 @@ nl_socket_add_memberships(struct nl_sock *sk, int group, ...)
     return 0;
 }
 
+/*****************************************************************************/
+
 int
-nl_socket_set_ext_ack(struct nl_sock *sk, gboolean enable)
+nl_socket_new(struct nl_sock **out_sk,
+              int              protocol,
+              NLSocketFlags    flags,
+              int              bufsize_rx,
+              int              bufsize_tx)
 {
-    int err, val;
-
-    if (sk->s_fd == -1)
-        return -NME_NL_BAD_SOCK;
-
-    val = !!enable;
-    err = setsockopt(sk->s_fd, SOL_NETLINK, NETLINK_EXT_ACK, &val, sizeof(val));
-    if (err < 0)
+    nm_auto_nlsock struct nl_sock *sk = NULL;
+    nm_auto_close int              fd = -1;
+    time_t                         t;
+    int                            err;
+    int                            nmerr;
+    socklen_t                      addrlen;
+    struct sockaddr_nl             local = {0};
+    int                            i_val;
+
+    nm_assert(out_sk && !*out_sk);
+
+    fd = socket(AF_NETLINK,
+                SOCK_RAW | SOCK_CLOEXEC
+                    | (NM_FLAGS_HAS(flags, NL_SOCKET_FLAGS_NONBLOCK) ? SOCK_NONBLOCK : 0),
+                protocol);
+    if (fd < 0)
         return -nm_errno_from_native(errno);
 
-    return 0;
-}
-
-void
-nl_socket_disable_msg_peek(struct nl_sock *sk)
-{
-    sk->s_flags |= NL_MSG_PEEK_EXPLICIT;
-    sk->s_flags &= ~NL_MSG_PEEK;
-}
+    t = time(NULL);
+
+    sk  = g_slice_new(struct nl_sock);
+    *sk = (struct nl_sock){
+        .s_fd = nm_steal_fd(&fd),
+        .s_local =
+            {
+                .nl_pid    = 0,
+                .nl_family = AF_NETLINK,
+                .nl_groups = 0,
+            },
+        .s_peer =
+            {
+                .nl_pid    = 0,
+                .nl_family = AF_NETLINK,
+                .nl_groups = 0,
+            },
+        .s_seq_expect = t,
+        .s_seq_next   = t,
+        .s_bufsize    = 0,
+        .s_msg_peek   = !NM_FLAGS_HAS(flags, NL_SOCKET_FLAGS_DISABLE_MSG_PEEK),
+        .s_auto_ack   = TRUE,
+    };
 
-int
-nl_connect(struct nl_sock *sk, int protocol)
-{
-    int                err, nmerr;
-    socklen_t          addrlen;
-    struct sockaddr_nl local = {0};
+    nmerr = nl_socket_set_buffer_size(sk, bufsize_rx, bufsize_tx);
+    if (nmerr < 0)
+        return nmerr;
 
-    if (sk->s_fd != -1)
-        return -NME_NL_BAD_SOCK;
+    i_val = 1;
+    (void) setsockopt(sk->s_fd, SOL_NETLINK, NETLINK_EXT_ACK, &i_val, sizeof(i_val));
 
-    sk->s_fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, protocol);
-    if (sk->s_fd < 0) {
-        nmerr = -nm_errno_from_native(errno);
-        goto errout;
+    if (NM_FLAGS_HAS(flags, NL_SOCKET_FLAGS_PASSCRED)) {
+        err = nl_socket_set_passcred(sk, 1);
+        if (err < 0)
+            return err;
     }
 
-    nmerr = nl_socket_set_buffer_size(sk, 0, 0);
-    if (nmerr < 0)
-        goto errout;
-
-    nm_assert(sk->s_local.nl_pid == 0);
+    if (NM_FLAGS_HAS(flags, NL_SOCKET_FLAGS_PKTINFO)) {
+        err = nl_socket_set_pktinfo(sk, 1);
+        if (err < 0)
+            return err;
+    }
 
     err = bind(sk->s_fd, (struct sockaddr *) &sk->s_local, sizeof(sk->s_local));
-    if (err != 0) {
-        nmerr = -nm_errno_from_native(errno);
-        goto errout;
-    }
+    if (err != 0)
+        return -nm_errno_from_native(errno);
 
     addrlen = sizeof(local);
     err     = getsockname(sk->s_fd, (struct sockaddr *) &local, &addrlen);
-    if (err < 0) {
-        nmerr = -nm_errno_from_native(errno);
-        goto errout;
-    }
+    if (err < 0)
+        return -nm_errno_from_native(errno);
 
-    if (addrlen != sizeof(local)) {
-        nmerr = -NME_UNSPEC;
-        goto errout;
-    }
+    if (addrlen != sizeof(local))
+        return -NME_UNSPEC;
 
-    if (local.nl_family != AF_NETLINK) {
-        nmerr = -NME_UNSPEC;
-        goto errout;
-    }
+    if (local.nl_family != AF_NETLINK)
+        return -NME_UNSPEC;
 
     sk->s_local = local;
     sk->s_proto = protocol;
 
+    *out_sk = g_steal_pointer(&sk);
     return 0;
-
-errout:
-    if (sk->s_fd != -1) {
-        close(sk->s_fd);
-        sk->s_fd = -1;
-    }
-    return nmerr;
 }
 
 /*****************************************************************************/
@@ -1124,7 +1201,7 @@ _cb_init(struct nl_cb *dst, const struct nl_cb *src)
 }
 
 static int
-ack_wait_handler(struct nl_msg *msg, void *arg)
+ack_wait_handler(const struct nl_msg *msg, void *arg)
 {
     return NL_STOP;
 }
@@ -1176,7 +1253,7 @@ nl_recvmsgs(struct nl_sock *sk, const struct nl_cb *cb)
     gboolean               creds_has;
 
 continue_reading:
-    n = nl_recv(sk, NULL, 0, &nla, &buf, &creds, &creds_has);
+    n = nl_recv(sk, NULL, 0, &nla, &buf, &creds, &creds_has, NULL, NULL);
     if (n <= 0)
         return n;
 
@@ -1193,7 +1270,7 @@ continue_reading:
         nrecv++;
 
         /* Only do sequence checking if auto-ack mode is enabled */
-        if (!(sk->s_flags & NL_NO_AUTO_ACK)) {
+        if (sk->s_auto_ack) {
             if (hdr->nlmsg_seq != sk->s_seq_expect) {
                 nmerr = -NME_NL_SEQ_MISMATCH;
                 goto out;
@@ -1378,7 +1455,7 @@ nl_complete_msg(struct nl_sock *sk, struct nl_msg *msg)
 
     nlh->nlmsg_flags |= NLM_F_REQUEST;
 
-    if (!(sk->s_flags & NL_NO_AUTO_ACK))
+    if (sk->s_auto_ack)
         nlh->nlmsg_flags |= NLM_F_ACK;
 }
 
@@ -1413,6 +1490,10 @@ nl_send_auto(struct nl_sock *sk, struct nl_msg *msg)
  *   on success.
  * @out_creds_has: (out) (allow-none): result indicating whether
  *   @out_creds was filled.
+* @out_pktinfo_group: (out) (allow-none): optional out buffer for NETLINK_PKTINFO
+*    group on success.
+ * @out_pktinfo_has: (out) (allow-none): result indicating whether
+ *   @out_pktinfo_group was filled.
  *
  * If @buf0_len is zero, the function will g_malloc() a new receive buffer of size
  * nl_socket_get_msg_buf_size(). If @buf0_len is larger than zero, then @buf0
@@ -1435,18 +1516,20 @@ nl_recv(struct nl_sock     *sk,
         struct sockaddr_nl *nla,
         unsigned char     **buf,
         struct ucred       *out_creds,
-        gboolean           *out_creds_has)
+        gboolean           *out_creds_has,
+        uint32_t           *out_pktinfo_group,
+        gboolean           *out_pktinfo_has)
 {
-    /* We really expect msg_contol_buf to be large enough and MSG_CTRUNC not
-     * happening. We nm_assert() against that. However, in release builds
-     * we don't assert, so add some extra safety space for the unexpected
-     * case where we might need more than CMSG_SPACE(sizeof(struct ucred)).
-     * It should not hurt and should not be necessary. It's just some
-     * extra defensive space. */
-#define _MSG_CONTROL_BUF_EXTRA_SPACE (NM_MORE_ASSERTS ? 512u : 0u)
     union {
-        struct cmsghdr cmsghdr;
-        char           buf[CMSG_SPACE(sizeof(struct ucred)) + _MSG_CONTROL_BUF_EXTRA_SPACE];
+        struct cmsghdr _dummy_for_alignment;
+        struct {
+            char buf[CMSG_SPACE(sizeof(struct ucred)) + CMSG_SPACE(sizeof(struct nl_pktinfo))];
+
+            /* We really expect that "buf" is large enough end even assert against
+             * that. We don't expect and don't want to handle MSG_CTRUNC error.
+             * Still, add some extra safety. This is on the stack and essentially for free. */
+            char _extra[512];
+        };
     } msg_contol_buf;
     ssize_t       n;
     int           flags = 0;
@@ -1459,17 +1542,16 @@ nl_recv(struct nl_sock     *sk,
         .msg_controllen = 0,
         .msg_control    = NULL,
     };
-    struct ucred tmpcreds;
-    gboolean     tmpcreds_has = FALSE;
-    int          retval;
-    int          errsv;
+    struct cmsghdr *cmsg;
+    int             retval;
+    int             errsv;
 
     nm_assert(nla);
     nm_assert(buf && !*buf);
-    nm_assert(!out_creds_has == !out_creds);
+    nm_assert(!out_creds_has || out_creds);
+    nm_assert(!out_pktinfo_has || out_pktinfo_group);
 
-    if ((sk->s_flags & NL_MSG_PEEK)
-        || (!(sk->s_flags & NL_MSG_PEEK_EXPLICIT) && sk->s_bufsize == 0))
+    if (sk->s_msg_peek)
         flags |= MSG_PEEK | MSG_TRUNC;
 
     if (buf0_len > 0) {
@@ -1480,7 +1562,7 @@ nl_recv(struct nl_sock     *sk,
         iov.iov_base = g_malloc(iov.iov_len);
     }
 
-    if (out_creds && (sk->s_flags & NL_SOCK_PASSCRED)) {
+    if (out_creds_has || out_pktinfo_has) {
         msg.msg_controllen = sizeof(msg_contol_buf);
         msg.msg_control    = msg_contol_buf.buf;
     }
@@ -1500,11 +1582,14 @@ retry:
         goto abort;
     }
 
+    nm_assert((gsize) n <= G_MAXINT);
+
     /* We really don't expect truncation of ancillary data. We provided a large
     * enough buffer, so this is likely a bug. In the worst case, we might lack
     * the requested credentials and the caller likely will reject the message
     * later. */
     nm_assert(!(msg.msg_flags & MSG_CTRUNC));
+    nm_assert(msg.msg_controllen <= G_STRUCT_OFFSET(typeof(msg_contol_buf), _extra));
 
     if (iov.iov_len < n || (msg.msg_flags & MSG_TRUNC)) {
         /* respond with error to an incomplete message */
@@ -1533,32 +1618,35 @@ retry:
         goto abort;
     }
 
-    if (out_creds && (sk->s_flags & NL_SOCK_PASSCRED)) {
-        struct cmsghdr *cmsg;
-
+    if (out_creds_has || out_pktinfo_has) {
+        NM_SET_OUT(out_creds_has, FALSE);
+        NM_SET_OUT(out_pktinfo_has, FALSE);
         for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
-            if (cmsg->cmsg_level != SOL_SOCKET)
-                continue;
-            if (cmsg->cmsg_type != SCM_CREDENTIALS)
-                continue;
-            memcpy(&tmpcreds, CMSG_DATA(cmsg), sizeof(tmpcreds));
-            tmpcreds_has = TRUE;
-            break;
+            switch (cmsg->cmsg_level) {
+            case SOL_SOCKET:
+                if (cmsg->cmsg_type == SCM_CREDENTIALS && out_creds_has) {
+                    memcpy(out_creds, CMSG_DATA(cmsg), sizeof(*out_creds));
+                    *out_creds_has = TRUE;
+                }
+                break;
+            case SOL_NETLINK:
+                if (cmsg->cmsg_type == NETLINK_PKTINFO && out_pktinfo_has) {
+                    struct nl_pktinfo p;
+
+                    memcpy(&p, CMSG_DATA(cmsg), sizeof(p));
+                    *out_pktinfo_group = p.group;
+                    *out_pktinfo_has   = TRUE;
+                }
+                break;
+            }
         }
     }
 
-    retval = n;
+    *buf = iov.iov_base;
+    return (int) n;
 
 abort:
-    if (retval <= 0) {
-        if (iov.iov_base != buf0)
-            g_free(iov.iov_base);
-        return retval;
-    }
-
-    *buf = iov.iov_base;
-    if (out_creds && tmpcreds_has)
-        *out_creds = tmpcreds;
-    NM_SET_OUT(out_creds_has, tmpcreds_has);
+    if (iov.iov_base != buf0)
+        g_free(iov.iov_base);
     return retval;
 }
diff --git a/src/libnm-platform/nm-netlink.h b/src/libnm-platform/nm-netlink.h
index bb1e41ee..44ed70af 100644
--- a/src/libnm-platform/nm-netlink.h
+++ b/src/libnm-platform/nm-netlink.h
@@ -31,6 +31,7 @@ enum {
     NLA_UNSPEC, /* Unspecified type, binary data chunk */
     NLA_U8,     /* 8 bit integer */
     NLA_U16,    /* 16 bit integer */
+    NLA_S32,    /* 32 bit integer */
     NLA_U32,    /* 32 bit integer */
     NLA_U64,    /* 64 bit integer */
     NLA_STRING, /* NUL terminated character string */
@@ -43,13 +44,37 @@ enum {
 
 struct nl_msg;
 
+/* This is similar to "struct nl_msg", in that it contains a
+ * netlink message including additional information like the
+ * src, creds, protocol.
+ *
+ * The difference is that "struct nl_msg" is an opaque type and
+ * contains a copy of the message (requiring two heap allocations).
+ * "struct nl_msg_lite" can be on the stack and it can directly
+ * point to the receive buffer, without need to copy the message.
+ * That can be useful, if you don't need to clone the message and
+ * just need to pass it "down the stack" for somebody to parse
+ * the message. */
+struct nl_msg_lite {
+    int                       nm_protocol;
+    const struct sockaddr_nl *nm_src;
+    const struct sockaddr_nl *nm_dst;
+    const struct ucred       *nm_creds;
+    const struct nlmsghdr    *nm_nlh;
+    uint32_t                  nm_size;
+};
+
 /*****************************************************************************/
 
 const char *nl_nlmsgtype2str(int type, char *buf, size_t size);
 
 const char *nl_nlmsg_flags2str(int flags, char *buf, size_t len);
 
-const char *nl_nlmsghdr_to_str(const struct nlmsghdr *hdr, char *buf, gsize len);
+const char *nl_nlmsghdr_to_str(int                    netlink_protocol,
+                               guint32                pktinfo_group,
+                               const struct nlmsghdr *hdr,
+                               char                  *buf,
+                               gsize                  len);
 
 /*****************************************************************************/
 
@@ -267,20 +292,21 @@ nla_put_uint32(struct nl_msg *msg, int attrtype, uint32_t val)
     return nla_put(msg, attrtype, sizeof(val), &val);
 }
 
-#define NLA_PUT(msg, attrtype, attrlen, data)          \
-    G_STMT_START                                       \
-    {                                                  \
-        if (nla_put(msg, attrtype, attrlen, data) < 0) \
-            goto nla_put_failure;                      \
-    }                                                  \
+#define NLA_PUT(msg, attrtype, attrlen, data)                  \
+    G_STMT_START                                               \
+    {                                                          \
+        if (nla_put((msg), (attrtype), (attrlen), (data)) < 0) \
+            goto nla_put_failure;                              \
+    }                                                          \
     G_STMT_END
 
-#define NLA_PUT_TYPE(msg, type, attrtype, value)          \
-    G_STMT_START                                          \
-    {                                                     \
-        type __nla_tmp = value;                           \
-        NLA_PUT(msg, attrtype, sizeof(type), &__nla_tmp); \
-    }                                                     \
+#define NLA_PUT_TYPE(msg, type, attrtype, value)                 \
+    G_STMT_START                                                 \
+    {                                                            \
+        type const _nla_tmp = value;                             \
+                                                                 \
+        NLA_PUT((msg), (attrtype), sizeof(_nla_tmp), &_nla_tmp); \
+    }                                                            \
     G_STMT_END
 
 #define NLA_PUT_U8(msg, attrtype, value) NLA_PUT_TYPE(msg, uint8_t, attrtype, value)
@@ -328,6 +354,14 @@ void           nla_nest_cancel(struct nl_msg *msg, const struct nlattr *attr);
 struct nlattr *nla_nest_start(struct nl_msg *msg, int attrtype);
 int            nla_nest_end(struct nl_msg *msg, struct nlattr *start);
 
+#define NLA_NEST_END(msg, nest_start)              \
+    G_STMT_START                                   \
+    {                                              \
+        if (nla_nest_end((msg), (nest_start)) < 0) \
+            goto nla_put_failure;                  \
+    }                                              \
+    G_STMT_END
+
 int nla_parse(struct nlattr           *tb[],
               int                      maxtype,
               struct nlattr           *head,
@@ -365,13 +399,13 @@ struct nl_msg *nlmsg_alloc_size(size_t max);
 
 struct nl_msg *nlmsg_alloc_convert(struct nlmsghdr *hdr);
 
-struct nl_msg *nlmsg_alloc_simple(int nlmsgtype, int flags);
+struct nl_msg *nlmsg_alloc_simple(uint16_t nlmsgtype, uint16_t flags);
 
-void *nlmsg_reserve(struct nl_msg *n, size_t len, int pad);
+void *nlmsg_reserve(struct nl_msg *n, uint32_t len, uint32_t pad);
 
-int nlmsg_append(struct nl_msg *n, const void *data, size_t len, int pad);
+int nlmsg_append(struct nl_msg *n, const void *data, uint32_t len, uint32_t pad);
 
-#define nlmsg_append_struct(n, data) nlmsg_append(n, (data), sizeof(*(data)), NLMSG_ALIGNTO)
+#define nlmsg_append_struct(n, data) (nlmsg_append((n), (data), sizeof(*(data)), NLMSG_ALIGNTO))
 
 void nlmsg_free(struct nl_msg *msg);
 
@@ -413,26 +447,29 @@ void nlmsg_set_src(struct nl_msg *msg, struct sockaddr_nl *addr);
 struct ucred *nlmsg_get_creds(struct nl_msg *msg);
 void          nlmsg_set_creds(struct nl_msg *msg, struct ucred *creds);
 
-static inline void
-_nm_auto_nl_msg_cleanup(struct nl_msg **ptr)
+NM_AUTO_DEFINE_FCN0(struct nl_msg *, _nm_auto_nl_msg_cleanup, nlmsg_free);
+#define nm_auto_nlmsg nm_auto(_nm_auto_nl_msg_cleanup)
+
+static inline const struct nlmsghdr *
+nlmsg_undata(const void *data)
 {
-    nlmsg_free(*ptr);
+    /* from the data, get back the header. It's the inverse of nlmsg_data(). */
+    return (void *) (((unsigned char *) data) - NLMSG_HDRLEN);
 }
-#define nm_auto_nlmsg nm_auto(_nm_auto_nl_msg_cleanup)
 
 static inline void *
 nlmsg_data(const struct nlmsghdr *nlh)
 {
-    return (unsigned char *) nlh + NLMSG_HDRLEN;
+    return ((unsigned char *) nlh) + NLMSG_HDRLEN;
 }
 
 static inline void *
 nlmsg_tail(const struct nlmsghdr *nlh)
 {
-    return (unsigned char *) nlh + NLMSG_ALIGN(nlh->nlmsg_len);
+    return ((unsigned char *) nlh) + NLMSG_ALIGN(nlh->nlmsg_len);
 }
 
-struct nlmsghdr *nlmsg_hdr(struct nl_msg *n);
+struct nlmsghdr *nlmsg_hdr(const struct nl_msg *n);
 
 static inline int
 nlmsg_valid_hdr(const struct nlmsghdr *nlh, int hdrlen)
@@ -468,7 +505,9 @@ nlmsg_find_attr(struct nlmsghdr *nlh, int hdrlen, int attrtype)
     return nla_find(nlmsg_attrdata(nlh, hdrlen), nlmsg_attrlen(nlh, hdrlen), attrtype);
 }
 
-int nlmsg_parse(struct nlmsghdr         *nlh,
+int nlmsg_parse_error(const struct nlmsghdr *nlh, const char **out_extack_msg);
+
+int nlmsg_parse(const struct nlmsghdr   *nlh,
                 int                      hdrlen,
                 struct nlattr           *tb[],
                 int                      maxtype,
@@ -482,20 +521,41 @@ int nlmsg_parse(struct nlmsghdr         *nlh,
         nlmsg_parse((nlh), (hdrlen), (tb), G_N_ELEMENTS(tb) - 1, (policy)); \
     })
 
-struct nlmsghdr *
-nlmsg_put(struct nl_msg *n, uint32_t pid, uint32_t seq, int type, int payload, int flags);
+struct nlmsghdr *nlmsg_put(struct nl_msg *n,
+                           uint32_t       pid,
+                           uint32_t       seq,
+                           uint16_t       type,
+                           uint32_t       payload,
+                           uint16_t       flags);
 
 /*****************************************************************************/
 
+typedef enum {
+    NL_SOCKET_FLAGS_NONE             = 0,
+    NL_SOCKET_FLAGS_NONBLOCK         = 0x1,
+    NL_SOCKET_FLAGS_PASSCRED         = 0x2,
+    NL_SOCKET_FLAGS_PKTINFO          = 0x4,
+    NL_SOCKET_FLAGS_DISABLE_MSG_PEEK = 0x8,
+
+    _NL_SOCKET_FLAGS_ALL = (NL_SOCKET_FLAGS_DISABLE_MSG_PEEK << 1) - 1,
+} NLSocketFlags;
+
 #define NL_AUTO_PORT 0
 #define NL_AUTO_SEQ  0
 
 struct nl_sock;
 
-struct nl_sock *nl_socket_alloc(void);
+int nl_socket_new(struct nl_sock **out_sk,
+                  int              protocol,
+                  NLSocketFlags    flags,
+                  int              bufsize_rx,
+                  int              bufsize_tx);
 
 void nl_socket_free(struct nl_sock *sk);
 
+NM_AUTO_DEFINE_FCN0(struct nl_sock *, _nm_auto_nlsock, nl_socket_free);
+#define nm_auto_nlsock nm_auto(_nm_auto_nlsock)
+
 int nl_socket_get_fd(const struct nl_sock *sk);
 
 struct sockaddr_nl *nlmsg_get_dst(struct nl_msg *msg);
@@ -507,9 +567,9 @@ int nl_socket_set_buffer_size(struct nl_sock *sk, int rxbuf, int txbuf);
 
 int nl_socket_set_passcred(struct nl_sock *sk, int state);
 
-int nl_socket_set_nonblocking(const struct nl_sock *sk);
+int nl_socket_set_pktinfo(struct nl_sock *sk, int state);
 
-void nl_socket_disable_msg_peek(struct nl_sock *sk);
+int nl_socket_set_nonblocking(const struct nl_sock *sk);
 
 uint32_t nl_socket_get_local_port(const struct nl_sock *sk);
 
@@ -523,7 +583,9 @@ int nl_recv(struct nl_sock     *sk,
             struct sockaddr_nl *nla,
             unsigned char     **buf,
             struct ucred       *out_creds,
-            gboolean           *out_creds_has);
+            gboolean           *out_creds_has,
+            uint32_t           *out_pktinfo_group,
+            gboolean           *out_pktinfo_has);
 
 int nl_send(struct nl_sock *sk, struct nl_msg *msg);
 
@@ -540,9 +602,11 @@ enum nl_cb_action {
     NL_STOP,
 };
 
-typedef int (*nl_recvmsg_msg_cb_t)(struct nl_msg *msg, void *arg);
+typedef int (*nl_recvmsg_msg_cb_t)(const struct nl_msg *msg, void *arg);
 
-typedef int (*nl_recvmsg_err_cb_t)(struct sockaddr_nl *nla, struct nlmsgerr *nlerr, void *arg);
+typedef int (*nl_recvmsg_err_cb_t)(const struct sockaddr_nl *nla,
+                                   const struct nlmsgerr    *nlerr,
+                                   void                     *arg);
 
 struct nl_cb {
     nl_recvmsg_msg_cb_t valid_cb;
@@ -568,28 +632,28 @@ int nl_recvmsgs(struct nl_sock *sk, const struct nl_cb *cb);
 
 int nl_wait_for_ack(struct nl_sock *sk, const struct nl_cb *cb);
 
-int nl_socket_set_ext_ack(struct nl_sock *sk, gboolean enable);
-
 /*****************************************************************************/
 
-void              *genlmsg_put(struct nl_msg *msg,
-                               uint32_t       port,
-                               uint32_t       seq,
-                               int            family,
-                               int            hdrlen,
-                               int            flags,
-                               uint8_t        cmd,
-                               uint8_t        version);
-void              *genlmsg_data(const struct genlmsghdr *gnlh);
-void              *genlmsg_user_hdr(const struct genlmsghdr *gnlh);
-struct genlmsghdr *genlmsg_hdr(struct nlmsghdr *nlh);
-void              *genlmsg_user_data(const struct genlmsghdr *gnlh, const int hdrlen);
-struct nlattr     *genlmsg_attrdata(const struct genlmsghdr *gnlh, int hdrlen);
-int                genlmsg_len(const struct genlmsghdr *gnlh);
-int                genlmsg_attrlen(const struct genlmsghdr *gnlh, int hdrlen);
-int                genlmsg_valid_hdr(struct nlmsghdr *nlh, int hdrlen);
-
-int genlmsg_parse(struct nlmsghdr         *nlh,
+extern const struct nla_policy genl_ctrl_policy[8];
+
+void                    *genlmsg_put(struct nl_msg *msg,
+                                     uint32_t       port,
+                                     uint32_t       seq,
+                                     uint16_t       family,
+                                     uint32_t       hdrlen,
+                                     uint16_t       flags,
+                                     uint8_t        cmd,
+                                     uint8_t        version);
+void                    *genlmsg_data(const struct genlmsghdr *gnlh);
+void                    *genlmsg_user_hdr(const struct genlmsghdr *gnlh);
+const struct genlmsghdr *genlmsg_hdr(const struct nlmsghdr *nlh);
+void                    *genlmsg_user_data(const struct genlmsghdr *gnlh, const int hdrlen);
+struct nlattr           *genlmsg_attrdata(const struct genlmsghdr *gnlh, int hdrlen);
+int                      genlmsg_len(const struct genlmsghdr *gnlh);
+int                      genlmsg_attrlen(const struct genlmsghdr *gnlh, int hdrlen);
+int                      genlmsg_valid_hdr(const struct nlmsghdr *nlh, int hdrlen);
+
+int genlmsg_parse(const struct nlmsghdr   *nlh,
                   int                      hdrlen,
                   struct nlattr           *tb[],
                   int                      maxtype,
diff --git a/src/libnm-platform/nm-platform.c b/src/libnm-platform/nm-platform.c
index 090af26d..df177485 100644
--- a/src/libnm-platform/nm-platform.c
+++ b/src/libnm-platform/nm-platform.c
@@ -7,6 +7,8 @@
 
 #include "nm-platform.h"
 
+#include "libnm-std-aux/nm-linux-compat.h"
+
 #include <stdlib.h>
 #include <unistd.h>
 #include <netinet/in.h>
@@ -113,24 +115,29 @@ nmp_link_address_get_as_bytes(const NMPLinkAddress *addr)
 #define _NMLOG_DOMAIN      LOGD_PLATFORM
 #define _NMLOG_PREFIX_NAME "platform"
 
-#define NMLOG_COMMON(level, name, ...)                                                \
-    char                    __prefix[32];                                             \
-    const char             *__p_prefix = _NMLOG_PREFIX_NAME;                          \
-    const NMPlatform *const __self     = (self);                                      \
-    const char             *__name     = name;                                        \
-                                                                                      \
-    if (__self && NM_PLATFORM_GET_PRIVATE(__self)->log_with_ptr) {                    \
-        g_snprintf(__prefix, sizeof(__prefix), "%s[%p]", _NMLOG_PREFIX_NAME, __self); \
-        __p_prefix = __prefix;                                                        \
-    }                                                                                 \
-    _nm_log(__level,                                                                  \
-            _NMLOG_DOMAIN,                                                            \
-            0,                                                                        \
-            __name,                                                                   \
-            NULL,                                                                     \
-            "%s: %s%s%s" _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                          \
-            __p_prefix,                                                               \
-            NM_PRINT_FMT_QUOTED(__name, "(", __name, ") ", "") _NM_UTILS_MACRO_REST(__VA_ARGS__));
+#define NMLOG_COMMON(level, name, ...)                                                    \
+    G_STMT_START                                                                          \
+    {                                                                                     \
+        char                    __prefix[32];                                             \
+        const char             *__p_prefix = _NMLOG_PREFIX_NAME;                          \
+        const NMPlatform *const __self     = (self);                                      \
+        const char             *__name     = name;                                        \
+                                                                                          \
+        if (__self && NM_PLATFORM_GET_PRIVATE(__self)->log_with_ptr) {                    \
+            g_snprintf(__prefix, sizeof(__prefix), "%s[%p]", _NMLOG_PREFIX_NAME, __self); \
+            __p_prefix = __prefix;                                                        \
+        }                                                                                 \
+        _nm_log((level),                                                                  \
+                _NMLOG_DOMAIN,                                                            \
+                0,                                                                        \
+                __name,                                                                   \
+                NULL,                                                                     \
+                "%s: %s%s%s" _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                          \
+                __p_prefix,                                                               \
+                NM_PRINT_FMT_QUOTED(__name, "(", __name, ") ", "")                        \
+                    _NM_UTILS_MACRO_REST(__VA_ARGS__));                                   \
+    }                                                                                     \
+    G_STMT_END
 
 #define _NMLOG(level, ...)                                \
     G_STMT_START                                          \
@@ -395,6 +402,76 @@ _nm_platform_kernel_support_init(NMPlatformKernelSupportType type, int value)
 
 /*****************************************************************************/
 
+const NMPGenlFamilyInfo nmp_genl_family_infos[_NMP_GENL_FAMILY_TYPE_NUM] = {
+    [NMP_GENL_FAMILY_TYPE_ETHTOOL] =
+        {
+            .name = "ethtool",
+        },
+    [NMP_GENL_FAMILY_TYPE_MPTCP_PM] =
+        {
+            .name = MPTCP_PM_NAME,
+        },
+    [NMP_GENL_FAMILY_TYPE_NL80211] =
+        {
+            .name = "nl80211",
+        },
+    [NMP_GENL_FAMILY_TYPE_NL802154] =
+        {
+            .name = "nl802154",
+        },
+    [NMP_GENL_FAMILY_TYPE_WIREGUARD] =
+        {
+            .name = "wireguard",
+        },
+};
+
+NMPGenlFamilyType
+nmp_genl_family_type_from_name(const char *name)
+{
+    int imin, imax, imid;
+
+    if (NM_MORE_ASSERT_ONCE(50)) {
+        int i;
+
+        for (i = 0; i < (int) G_N_ELEMENTS(nmp_genl_family_infos); i++) {
+            nm_assert(nmp_genl_family_infos[i].name);
+            if (i > 0)
+                nm_assert(strcmp(nmp_genl_family_infos[i - 1].name, nmp_genl_family_infos[i].name)
+                          < 0);
+        }
+    }
+
+    if (!name)
+        goto out;
+
+    imin = 0;
+    imax = G_N_ELEMENTS(nmp_genl_family_infos) - 1;
+    imid = imax / 2;
+
+    while (TRUE) {
+        int c;
+
+        c = strcmp(nmp_genl_family_infos[imid].name, name);
+        if (c == 0)
+            return (NMPGenlFamilyType) imid;
+
+        if (c < 0)
+            imin = imid + 1;
+        else
+            imax = imid - 1;
+
+        if (imin > imax)
+            break;
+
+        imid = (imax + imin) / 2;
+    }
+
+out:
+    return _NMP_GENL_FAMILY_TYPE_NONE;
+}
+
+/*****************************************************************************/
+
 /**
  * nm_platform_process_events:
  * @self: platform instance
@@ -1292,6 +1369,12 @@ nm_platform_link_add(NMPlatform            *self,
                                                      buf_p,
                                                      buf_len);
                    break;
+               case NM_LINK_TYPE_BOND:
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_platform_lnk_bond_to_string((const NMPlatformLnkBond *) extra_data,
+                                                  buf_p,
+                                                  buf_len);
+                   break;
                default:
                    nm_assert(!extra_data);
                    break;
@@ -1332,6 +1415,12 @@ nm_platform_link_change(NMPlatform *self, NMLinkType type, int ifindex, gconstpo
                                                     buf_p,
                                                     buf_len);
                    break;
+               case NM_LINK_TYPE_BOND:
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_platform_lnk_bond_to_string((const NMPlatformLnkBond *) extra_data,
+                                                  buf_p,
+                                                  buf_len);
+                   break;
                default:
                    nm_assert(!extra_data);
                    break;
@@ -2232,6 +2321,12 @@ _link_get_lnk(NMPlatform *self, int ifindex, NMLinkType link_type, const NMPlatf
     return lnk ? &lnk->object : NULL;
 }
 
+const NMPlatformLnkBond *
+nm_platform_link_get_lnk_bond(NMPlatform *self, int ifindex, const NMPlatformLink **out_link)
+{
+    return _link_get_lnk(self, ifindex, NM_LINK_TYPE_BOND, out_link);
+}
+
 const NMPlatformLnkBridge *
 nm_platform_link_get_lnk_bridge(NMPlatform *self, int ifindex, const NMPlatformLink **out_link)
 {
@@ -4133,7 +4228,7 @@ nm_platform_ip_address_sync(NMPlatform *self,
      * we check the "secondary" flag. */
     plat_addresses = nm_platform_lookup_clone(
         self,
-        nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4), ifindex),
+        nmp_lookup_init_object_by_ifindex(&lookup, NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4), ifindex),
         NULL,
         NULL);
 
@@ -4436,18 +4531,20 @@ next_plat:;
                     nm_platform_ip4_broadcast_address_from_addr(&known_address->a4),
                     lifetime,
                     preferred,
-                    IFA_F_NOPREFIXROUTE,
+                    known_address->ax.a_no_auto_noprefixroute ? 0 : IFA_F_NOPREFIXROUTE,
                     known_address->a4.label))
                 success = FALSE;
         } else {
-            if (!nm_platform_ip6_address_add(self,
-                                             ifindex,
-                                             known_address->a6.address,
-                                             known_address->a6.plen,
-                                             known_address->a6.peer_address,
-                                             lifetime,
-                                             preferred,
-                                             IFA_F_NOPREFIXROUTE | known_address->a6.n_ifa_flags))
+            if (!nm_platform_ip6_address_add(
+                    self,
+                    ifindex,
+                    known_address->a6.address,
+                    known_address->a6.plen,
+                    known_address->a6.peer_address,
+                    lifetime,
+                    preferred,
+                    (known_address->ax.a_no_auto_noprefixroute ? 0 : IFA_F_NOPREFIXROUTE)
+                        | known_address->a6.n_ifa_flags))
                 success = FALSE;
         }
     }
@@ -4545,7 +4642,9 @@ nm_platform_ip_address_get_prune_list(NMPlatform            *self,
     GPtrArray                     *result = NULL;
     CList                         *iter;
 
-    nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP_ADDRESS(NM_IS_IPv4(addr_family)), ifindex);
+    nmp_lookup_init_object_by_ifindex(&lookup,
+                                      NMP_OBJECT_TYPE_IP_ADDRESS(NM_IS_IPv4(addr_family)),
+                                      ifindex);
 
     head_entry = nm_platform_lookup(self, &lookup);
 
@@ -4626,7 +4725,9 @@ nm_platform_ip_route_get_prune_list(NMPlatform            *self,
                         NM_IP_ROUTE_TABLE_SYNC_MODE_ALL,
                         NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_PRUNE));
 
-    nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP_ROUTE(NM_IS_IPv4(addr_family)), ifindex);
+    nmp_lookup_init_object_by_ifindex(&lookup,
+                                      NMP_OBJECT_TYPE_IP_ROUTE(NM_IS_IPv4(addr_family)),
+                                      ifindex);
     head_entry = nm_platform_lookup(self, &lookup);
     if (!head_entry)
         return NULL;
@@ -5226,23 +5327,26 @@ nm_platform_object_delete(NMPlatform *self, const NMPObject *obj)
 
     _CHECK_SELF(self, klass, FALSE);
 
-    switch (NMP_OBJECT_GET_TYPE(obj)) {
-    case NMP_OBJECT_TYPE_ROUTING_RULE:
-        _LOGD("%s: delete %s",
-              NMP_OBJECT_GET_CLASS(obj)->obj_type_name,
-              nmp_object_to_string(obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
-        break;
-    case NMP_OBJECT_TYPE_IP4_ROUTE:
-    case NMP_OBJECT_TYPE_IP6_ROUTE:
-    case NMP_OBJECT_TYPE_QDISC:
-    case NMP_OBJECT_TYPE_TFILTER:
-        ifindex = NMP_OBJECT_CAST_OBJ_WITH_IFINDEX(obj)->ifindex;
-        _LOG3D("%s: delete %s",
-               NMP_OBJECT_GET_CLASS(obj)->obj_type_name,
-               nmp_object_to_string(obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
-        break;
-    default:
-        g_return_val_if_reached(FALSE);
+    if (_LOGD_ENABLED()) {
+        switch (NMP_OBJECT_GET_TYPE(obj)) {
+        case NMP_OBJECT_TYPE_ROUTING_RULE:
+        case NMP_OBJECT_TYPE_MPTCP_ADDR:
+            _LOGD("%s: delete %s",
+                  NMP_OBJECT_GET_CLASS(obj)->obj_type_name,
+                  nmp_object_to_string(obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
+            break;
+        case NMP_OBJECT_TYPE_IP4_ROUTE:
+        case NMP_OBJECT_TYPE_IP6_ROUTE:
+        case NMP_OBJECT_TYPE_QDISC:
+        case NMP_OBJECT_TYPE_TFILTER:
+            ifindex = NMP_OBJECT_CAST_OBJ_WITH_IFINDEX(obj)->ifindex;
+            _LOG3D("%s: delete %s",
+                   NMP_OBJECT_GET_CLASS(obj)->obj_type_name,
+                   nmp_object_to_string(obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
+            break;
+        default:
+            g_return_val_if_reached(FALSE);
+        }
     }
 
     return klass->object_delete(self, obj);
@@ -5972,6 +6076,123 @@ nm_platform_lnk_bridge_to_string(const NMPlatformLnkBridge *lnk, char *buf, gsiz
 }
 
 const char *
+nm_platform_lnk_bond_to_string(const NMPlatformLnkBond *lnk, char *buf, gsize len)
+{
+    char sbuf_miimon[30];
+    char sbuf_updelay[30];
+    char sbuf_downdelay[30];
+    char sbuf_peer_notif_delay[60];
+    char sbuf_arp_all_targets[30];
+    char sbuf_resend_igmp[30];
+    char sbuf_lp_interval[30];
+    char sbuf_tlb_dynamic_lb[30];
+    int  i;
+
+    if (!nm_utils_to_string_buffer_init_null(lnk, &buf, &len))
+        return buf;
+
+    nm_strbuf_append(
+        &buf,
+        &len,
+        "bond"
+        " mode %u"
+        " primary %u"
+        "%s" /* miimon */
+        "%s" /* updelay */
+        "%s" /* downdelay */
+        " arp_interval %u"
+        "%s" /* resend_igmp */
+        " min_links %u"
+        "%s" /* lp_interval */
+        " packets_per_port %u"
+        "%s" /* peer_notif_delay */
+        "%s" /* arp_all_targets */
+        " arp_validate %u"
+        " ad_actor_sys_prio %u"
+        " ad_user_port_key %u"
+        " ad_actor_system " NM_ETHER_ADDR_FORMAT_STR ""
+        " primary_reselect %u"
+        " fail_over_mac %u"
+        " xmit_hash_policy %u"
+        " num_gray_arp %u"
+        " all_ports_active %u"
+        " lacp_rate %u"
+        " ad_select %u"
+        " use_carrier %d"
+        "%s" /* tlb_dynamic_lb */,
+        lnk->mode,
+        lnk->primary,
+        lnk->miimon_has || lnk->miimon != 0
+            ? nm_sprintf_buf(sbuf_miimon, " miimon%s %u", !lnk->miimon_has ? "?" : "", lnk->miimon)
+            : "",
+        lnk->updelay_has || lnk->updelay != 0 ? nm_sprintf_buf(sbuf_updelay,
+                                                               " updelay%s %u",
+                                                               !lnk->updelay_has ? "?" : "",
+                                                               lnk->updelay)
+                                              : "",
+        lnk->downdelay_has || lnk->downdelay != 0 ? nm_sprintf_buf(sbuf_downdelay,
+                                                                   " downdelay%s %u",
+                                                                   !lnk->downdelay_has ? "?" : "",
+                                                                   lnk->downdelay)
+                                                  : "",
+        lnk->arp_interval,
+        lnk->resend_igmp_has || lnk->resend_igmp != 0
+            ? nm_sprintf_buf(sbuf_resend_igmp,
+                             " resend_igmp%s %u",
+                             !lnk->resend_igmp_has ? "?" : "",
+                             lnk->resend_igmp)
+            : "",
+        lnk->min_links,
+        lnk->lp_interval_has || lnk->lp_interval != 1
+            ? nm_sprintf_buf(sbuf_lp_interval,
+                             " lp_interval%s %u",
+                             !lnk->lp_interval_has ? "?" : "",
+                             lnk->lp_interval)
+            : "",
+        lnk->packets_per_port,
+        lnk->peer_notif_delay_has || lnk->peer_notif_delay != 0
+            ? nm_sprintf_buf(sbuf_peer_notif_delay,
+                             " peer_notif_delay%s %u",
+                             !lnk->peer_notif_delay_has ? "?" : "",
+                             lnk->peer_notif_delay)
+            : "",
+        lnk->arp_all_targets_has || lnk->arp_all_targets != 0
+            ? nm_sprintf_buf(sbuf_arp_all_targets,
+                             " arp_all_targets%s %u",
+                             !lnk->arp_all_targets_has ? "?" : "",
+                             lnk->arp_all_targets)
+            : "",
+        lnk->arp_validate,
+        lnk->ad_actor_sys_prio,
+        lnk->ad_user_port_key,
+        NM_ETHER_ADDR_FORMAT_VAL(&lnk->ad_actor_system),
+        lnk->primary_reselect,
+        lnk->fail_over_mac,
+        lnk->xmit_hash_policy,
+        lnk->num_grat_arp,
+        lnk->all_ports_active,
+        lnk->lacp_rate,
+        lnk->ad_select,
+        (int) lnk->use_carrier,
+        lnk->tlb_dynamic_lb_has ? nm_sprintf_buf(sbuf_tlb_dynamic_lb,
+                                                 " tlb_dynamic_lb%s %u",
+                                                 !lnk->tlb_dynamic_lb_has ? "?" : "",
+                                                 (int) lnk->tlb_dynamic_lb)
+                                : "");
+
+    if (lnk->arp_ip_targets_num > 0) {
+        nm_strbuf_append_str(&buf, &len, " arp_ip_target");
+        for (i = 0; i < lnk->arp_ip_targets_num; i++) {
+            char target[INET_ADDRSTRLEN];
+
+            nm_strbuf_append_c(&buf, &len, ' ');
+            nm_strbuf_append_str(&buf, &len, _nm_utils_inet4_ntop(lnk->arp_ip_target[i], target));
+        }
+    }
+    return buf;
+}
+
+const char *
 nm_platform_lnk_gre_to_string(const NMPlatformLnkGre *lnk, char *buf, gsize len)
 {
     char str_local[30];
@@ -6580,6 +6801,7 @@ nm_platform_ip4_address_to_string(const NMPlatformIP4Address *address, char *buf
         " src %s"
         "%s" /* a_acd_not_ready */
         "%s" /* a_force_commit */
+        "%s" /* a_no_auto_noprefixroute */
         "",
         s_address,
         address->plen,
@@ -6598,7 +6820,8 @@ nm_platform_ip4_address_to_string(const NMPlatformIP4Address *address, char *buf
         str_label,
         nmp_utils_ip_config_source_to_string(address->addr_source, s_source, sizeof(s_source)),
         address->a_acd_not_ready ? " ip4acd-not-ready" : "",
-        address->a_force_commit ? " force-commit" : "");
+        address->a_force_commit ? " force-commit" : "",
+        address->a_no_auto_noprefixroute ? " no-auto-noprefixroute" : "");
     g_free(str_peer);
     return buf;
 }
@@ -6719,6 +6942,7 @@ nm_platform_ip6_address_to_string(const NMPlatformIP6Address *address, char *buf
         len,
         "%s/%d lft %s pref %s%s%s%s%s src %s"
         "%s" /* a_force_commit */
+        "%s" /* a_no_auto_noprefixroute */
         "",
         s_address,
         address->plen,
@@ -6729,7 +6953,8 @@ nm_platform_ip6_address_to_string(const NMPlatformIP6Address *address, char *buf
         str_dev,
         _to_string_ifa_flags(address->n_ifa_flags, s_flags, sizeof(s_flags)),
         nmp_utils_ip_config_source_to_string(address->addr_source, s_source, sizeof(s_source)),
-        address->a_force_commit ? " force-commit" : "");
+        address->a_force_commit ? " force-commit" : "",
+        address->a_no_auto_noprefixroute ? " no-auto-noprefixroute" : "");
     g_free(str_peer);
     return buf;
 }
@@ -6787,6 +7012,7 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz
     char s_gateway[INET_ADDRSTRLEN];
     char s_pref_src[INET_ADDRSTRLEN];
     char str_dev[TO_STRING_DEV_BUF_SIZE];
+    char str_mss[32];
     char str_table[30];
     char str_scope[30];
     char s_source[50];
@@ -6795,6 +7021,7 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz
     char str_cwnd[32];
     char str_initcwnd[32];
     char str_initrwnd[32];
+    char str_rto_min[32];
     char str_mtu[32];
     char str_rtm_flags[_RTM_FLAGS_TO_STRING_MAXLEN];
     char str_type[30];
@@ -6821,18 +7048,20 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz
         "%s%s" /* gateway */
         "%s"
         " metric %s"
-        " mss %" G_GUINT32_FORMAT /* mss */
-        " rt-src %s"              /* protocol */
-        "%s"                      /* rtm_flags */
-        "%s%s"                    /* scope */
-        "%s%s"                    /* pref-src */
-        "%s"                      /* tos */
-        "%s"                      /* window */
-        "%s"                      /* cwnd */
-        "%s"                      /* initcwnd */
-        "%s"                      /* initrwnd */
-        "%s"                      /* mtu */
-        "%s"                      /* r_force_commit */
+        "%s"         /* mss */
+        " rt-src %s" /* protocol */
+        "%s"         /* rtm_flags */
+        "%s%s"       /* scope */
+        "%s%s"       /* pref-src */
+        "%s"         /* tos */
+        "%s"         /* window */
+        "%s"         /* cwnd */
+        "%s"         /* initcwnd */
+        "%s"         /* initrwnd */
+        "%s"         /* rto_min */
+        "%s"         /* quickack */
+        "%s"         /* mtu */
+        "%s"         /* r_force_commit */
         "",
         nm_net_aux_rtnl_rtntype_n2a_maybe_buf(nm_platform_route_type_uncoerce(route->type_coerced),
                                               str_type),
@@ -6851,7 +7080,10 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz
         route->metric_any
             ? (route->metric ? nm_sprintf_buf(str_metric, "??+%u", route->metric) : "??")
             : nm_sprintf_buf(str_metric, "%u", route->metric),
-        route->mss,
+        nm_sprintf_buf(str_mss,
+                       " mss %s%" G_GUINT32_FORMAT,
+                       route->lock_mss ? "lock " : "",
+                       route->mss),
         nmp_utils_ip_config_source_to_string(route->rt_source, s_source, sizeof(s_source)),
         _rtm_flags_to_string_full(str_rtm_flags, sizeof(str_rtm_flags), route->r_rtm_flags),
         route->scope_inv ? " scope " : "",
@@ -6885,6 +7117,9 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz
                              route->lock_initrwnd ? "lock " : "",
                              route->initrwnd)
             : "",
+        route->rto_min ? nm_sprintf_buf(str_rto_min, " rto_min %" G_GUINT32_FORMAT, route->rto_min)
+                       : "",
+        route->quickack ? " quickack 1" : "",
         route->mtu || route->lock_mtu ? nm_sprintf_buf(str_mtu,
                                                        " mtu %s%" G_GUINT32_FORMAT,
                                                        route->lock_mtu ? "lock " : "",
@@ -6919,11 +7154,13 @@ nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsiz
     char str_pref[40];
     char str_pref2[30];
     char str_dev[TO_STRING_DEV_BUF_SIZE];
+    char str_mss[32];
     char s_source[50];
     char str_window[32];
     char str_cwnd[32];
     char str_initcwnd[32];
     char str_initrwnd[32];
+    char str_rto_min[32];
     char str_mtu[32];
     char str_rtm_flags[_RTM_FLAGS_TO_STRING_MAXLEN];
     char str_metric[30];
@@ -6954,18 +7191,20 @@ nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsiz
         "%s%s" /* gateway */
         "%s"
         " metric %s"
-        " mss %" G_GUINT32_FORMAT /* mss */
-        " rt-src %s"              /* protocol */
-        "%s"                      /* source */
-        "%s"                      /* rtm_flags */
-        "%s%s"                    /* pref-src */
-        "%s"                      /* window */
-        "%s"                      /* cwnd */
-        "%s"                      /* initcwnd */
-        "%s"                      /* initrwnd */
-        "%s"                      /* mtu */
-        "%s"                      /* pref */
-        "%s"                      /* r_force_commit */
+        "%s"         /* mss */
+        " rt-src %s" /* protocol */
+        "%s"         /* source */
+        "%s"         /* rtm_flags */
+        "%s%s"       /* pref-src */
+        "%s"         /* window */
+        "%s"         /* cwnd */
+        "%s"         /* initcwnd */
+        "%s"         /* initrwnd */
+        "%s"         /* rto_min */
+        "%s"         /* quickack */
+        "%s"         /* mtu */
+        "%s"         /* pref */
+        "%s"         /* r_force_commit */
         "",
         nm_net_aux_rtnl_rtntype_n2a_maybe_buf(nm_platform_route_type_uncoerce(route->type_coerced),
                                               str_type),
@@ -6984,7 +7223,10 @@ nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsiz
         route->metric_any
             ? (route->metric ? nm_sprintf_buf(str_metric, "??+%u", route->metric) : "??")
             : nm_sprintf_buf(str_metric, "%u", route->metric),
-        route->mss,
+        nm_sprintf_buf(str_mss,
+                       " mss %s%" G_GUINT32_FORMAT,
+                       route->lock_mss ? "lock " : "",
+                       route->mss),
         nmp_utils_ip_config_source_to_string(route->rt_source, s_source, sizeof(s_source)),
         route->src_plen || !IN6_IS_ADDR_UNSPECIFIED(&route->src)
             ? nm_sprintf_buf(s_src_all,
@@ -7017,6 +7259,9 @@ nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsiz
                              route->lock_initrwnd ? "lock " : "",
                              route->initrwnd)
             : "",
+        route->rto_min ? nm_sprintf_buf(str_rto_min, " rto_min %" G_GUINT32_FORMAT, route->rto_min)
+                       : "",
+        route->quickack ? " quickack 1" : "",
         route->mtu || route->lock_mtu ? nm_sprintf_buf(str_mtu,
                                                        " mtu %s%" G_GUINT32_FORMAT,
                                                        route->lock_mtu ? "lock " : "",
@@ -7492,6 +7737,107 @@ nm_platform_tfilter_cmp(const NMPlatformTfilter *a, const NMPlatformTfilter *b)
     return 0;
 }
 
+static NM_UTILS_FLAGS2STR_DEFINE(_mptcp_flags_to_string,
+                                 guint32,
+                                 NM_UTILS_FLAGS2STR(NM_MPTCP_PM_ADDR_FLAG_SIGNAL, "signal"),
+                                 NM_UTILS_FLAGS2STR(NM_MPTCP_PM_ADDR_FLAG_SUBFLOW, "subflow"),
+                                 NM_UTILS_FLAGS2STR(NM_MPTCP_PM_ADDR_FLAG_BACKUP, "backup"),
+                                 NM_UTILS_FLAGS2STR(NM_MPTCP_PM_ADDR_FLAG_FULLMESH, "fullmesh"));
+
+const char *
+nm_platform_mptcp_addr_to_string(const NMPlatformMptcpAddr *mptcp_addr, char *buf, gsize len)
+{
+    char str_addr[30 + NM_UTILS_INET_ADDRSTRLEN];
+    char str_port[30];
+    char str_id[30];
+    char str_flags[200];
+    char str_flags2[30 + sizeof(str_flags)];
+    char str_ifindex[30];
+
+    if (!nm_utils_to_string_buffer_init_null(mptcp_addr, &buf, &len))
+        return buf;
+
+    if (mptcp_addr->addr_family == 0)
+        nm_sprintf_buf(str_addr, "no-addr");
+    else if (NM_IN_SET(mptcp_addr->addr_family, AF_INET, AF_INET6))
+        nm_utils_inet_ntop(mptcp_addr->addr_family, &mptcp_addr->addr, str_addr);
+    else
+        nm_sprintf_buf(str_addr, "af %d", mptcp_addr->addr_family);
+
+    if (mptcp_addr->flags != 0)
+        _mptcp_flags_to_string(mptcp_addr->flags, str_flags, sizeof(str_flags));
+    else
+        str_flags[0] = '\0';
+
+    g_snprintf(buf,
+               len,
+               "%s" /* address */
+               "%s" /* port */
+               "%s" /* id */
+               "%s" /* flags */
+               "%s" /* ifindex */
+               "",
+               str_addr,
+               mptcp_addr->port == 0 ? "" : nm_sprintf_buf(str_port, " port %u", mptcp_addr->port),
+               mptcp_addr->id == 0 ? "" : nm_sprintf_buf(str_id, " id %u", mptcp_addr->id),
+               str_flags[0] == '\0' ? "" : nm_sprintf_buf(str_flags2, " flags %s", str_flags),
+               mptcp_addr->ifindex == 0
+                   ? ""
+                   : nm_sprintf_buf(str_ifindex, " ifindex %d", mptcp_addr->ifindex));
+    return buf;
+}
+
+void
+nm_platform_mptcp_addr_hash_update(const NMPlatformMptcpAddr *obj, NMHashState *h)
+{
+    nm_assert(obj);
+    nm_assert_addr_family_or_unspec(obj->addr_family);
+
+    nm_hash_update_vals(h, obj->id, obj->flags, obj->port, obj->addr_family, obj->ifindex);
+    if (NM_IN_SET(obj->addr_family, AF_INET, AF_INET6))
+        nm_hash_update(h, &obj->addr, nm_utils_addr_family_to_size(obj->addr_family));
+}
+
+int
+nm_platform_mptcp_addr_cmp(const NMPlatformMptcpAddr *a, const NMPlatformMptcpAddr *b)
+{
+    NM_CMP_SELF(a, b);
+
+    nm_assert_addr_family_or_unspec(a->addr_family);
+    nm_assert_addr_family_or_unspec(b->addr_family);
+
+    NM_CMP_FIELD(a, b, ifindex);
+    NM_CMP_FIELD(a, b, id);
+    NM_CMP_FIELD(a, b, addr_family);
+    if (NM_IN_SET(a->addr_family, AF_INET, AF_INET6))
+        NM_CMP_FIELD_MEMCMP_LEN(a, b, addr, nm_utils_addr_family_to_size(a->addr_family));
+    NM_CMP_FIELD(a, b, port);
+
+    return 0;
+}
+
+guint
+nm_platform_mptcp_addr_index_addr_cmp(gconstpointer data)
+{
+    const NMPlatformMptcpAddr *mptcp_addr = data;
+    NMHashState                h;
+
+    nm_hash_init(&h, 1408914077u);
+    nm_hash_update_val(&h, mptcp_addr->addr_family);
+    nm_hash_update(&h, &mptcp_addr->addr, nm_utils_addr_family_to_size(mptcp_addr->addr_family));
+    return nm_hash_complete(&h);
+}
+
+gboolean
+nm_platform_mptcp_addr_index_addr_equal(gconstpointer data_a, gconstpointer data_b)
+{
+    const NMPlatformMptcpAddr *mptcp_addr_a = data_a;
+    const NMPlatformMptcpAddr *mptcp_addr_b = data_b;
+
+    return mptcp_addr_a->addr_family == mptcp_addr_b->addr_family
+           && nm_ip_addr_equal(mptcp_addr_a->addr_family, &mptcp_addr_a->addr, &mptcp_addr_b->addr);
+}
+
 const char *
 nm_platform_vf_to_string(const NMPlatformVF *vf, char *buf, gsize len)
 {
@@ -7668,6 +8014,95 @@ nm_platform_lnk_bridge_hash_update(const NMPlatformLnkBridge *obj, NMHashState *
                                               obj->vlan_stats_enabled));
 }
 
+void
+nm_platform_lnk_bond_hash_update(const NMPlatformLnkBond *obj, NMHashState *h)
+{
+    nm_hash_update_vals(h,
+                        obj->arp_all_targets,
+                        obj->arp_interval,
+                        obj->arp_validate,
+                        obj->downdelay,
+                        obj->lp_interval,
+                        obj->miimon,
+                        obj->min_links,
+                        obj->packets_per_port,
+                        obj->peer_notif_delay,
+                        obj->primary,
+                        obj->resend_igmp,
+                        obj->updelay,
+                        obj->ad_actor_sys_prio,
+                        obj->ad_user_port_key,
+                        obj->ad_actor_system,
+                        obj->ad_select,
+                        obj->all_ports_active,
+                        obj->arp_ip_targets_num,
+                        obj->fail_over_mac,
+                        obj->lacp_rate,
+                        obj->num_grat_arp,
+                        obj->mode,
+                        obj->primary_reselect,
+                        obj->xmit_hash_policy,
+                        NM_HASH_COMBINE_BOOLS(guint16,
+                                              obj->arp_all_targets_has,
+                                              obj->downdelay_has,
+                                              obj->lp_interval_has,
+                                              obj->miimon_has,
+                                              obj->peer_notif_delay_has,
+                                              obj->resend_igmp_has,
+                                              obj->tlb_dynamic_lb,
+                                              obj->tlb_dynamic_lb_has,
+                                              obj->updelay_has,
+                                              obj->use_carrier));
+
+    nm_hash_update(h, obj->arp_ip_target, obj->arp_ip_targets_num * sizeof(obj->arp_ip_target[0]));
+}
+
+int
+nm_platform_lnk_bond_cmp(const NMPlatformLnkBond *a, const NMPlatformLnkBond *b)
+{
+    NM_CMP_SELF(a, b);
+    NM_CMP_FIELD_MEMCMP_LEN(a,
+                            b,
+                            arp_ip_target,
+                            a->arp_ip_targets_num * sizeof(a->arp_ip_target[0]));
+    NM_CMP_FIELD(a, b, arp_all_targets);
+    NM_CMP_FIELD(a, b, arp_interval);
+    NM_CMP_FIELD(a, b, arp_validate);
+    NM_CMP_FIELD(a, b, downdelay);
+    NM_CMP_FIELD(a, b, lp_interval);
+    NM_CMP_FIELD(a, b, miimon);
+    NM_CMP_FIELD(a, b, min_links);
+    NM_CMP_FIELD(a, b, packets_per_port);
+    NM_CMP_FIELD(a, b, peer_notif_delay);
+    NM_CMP_FIELD(a, b, primary);
+    NM_CMP_FIELD(a, b, resend_igmp);
+    NM_CMP_FIELD(a, b, updelay);
+    NM_CMP_FIELD(a, b, ad_actor_sys_prio);
+    NM_CMP_FIELD(a, b, ad_user_port_key);
+    NM_CMP_FIELD_MEMCMP(a, b, ad_actor_system);
+    NM_CMP_FIELD(a, b, ad_select);
+    NM_CMP_FIELD(a, b, all_ports_active);
+    NM_CMP_FIELD(a, b, arp_ip_targets_num);
+    NM_CMP_FIELD(a, b, fail_over_mac);
+    NM_CMP_FIELD(a, b, lacp_rate);
+    NM_CMP_FIELD(a, b, num_grat_arp);
+    NM_CMP_FIELD(a, b, mode);
+    NM_CMP_FIELD(a, b, primary_reselect);
+    NM_CMP_FIELD(a, b, xmit_hash_policy);
+    NM_CMP_FIELD_BOOL(a, b, arp_all_targets_has);
+    NM_CMP_FIELD_BOOL(a, b, downdelay_has);
+    NM_CMP_FIELD_BOOL(a, b, lp_interval_has);
+    NM_CMP_FIELD_BOOL(a, b, miimon_has);
+    NM_CMP_FIELD_BOOL(a, b, peer_notif_delay_has);
+    NM_CMP_FIELD_BOOL(a, b, resend_igmp_has);
+    NM_CMP_FIELD_BOOL(a, b, tlb_dynamic_lb);
+    NM_CMP_FIELD_BOOL(a, b, tlb_dynamic_lb_has);
+    NM_CMP_FIELD_BOOL(a, b, updelay_has);
+    NM_CMP_FIELD_BOOL(a, b, use_carrier);
+
+    return 0;
+}
+
 int
 nm_platform_lnk_bridge_cmp(const NMPlatformLnkBridge *a, const NMPlatformLnkBridge *b)
 {
@@ -8182,7 +8617,8 @@ nm_platform_ip4_address_hash_update(const NMPlatformIP4Address *obj, NMHashState
                         NM_HASH_COMBINE_BOOLS(guint8,
                                               obj->use_ip4_broadcast_address,
                                               obj->a_acd_not_ready,
-                                              obj->a_force_commit));
+                                              obj->a_force_commit,
+                                              obj->a_no_auto_noprefixroute));
     nm_hash_update_strarr(h, obj->label);
 }
 
@@ -8231,6 +8667,7 @@ nm_platform_ip4_address_cmp(const NMPlatformIP4Address *a,
                 NM_CMP_FIELD(a, b, broadcast_address);
             NM_CMP_FIELD_UNSAFE(a, b, a_acd_not_ready);
             NM_CMP_FIELD_UNSAFE(a, b, a_force_commit);
+            NM_CMP_FIELD_UNSAFE(a, b, a_no_auto_noprefixroute);
         }
         return 0;
     }
@@ -8240,17 +8677,18 @@ nm_platform_ip4_address_cmp(const NMPlatformIP4Address *a,
 void
 nm_platform_ip6_address_hash_update(const NMPlatformIP6Address *obj, NMHashState *h)
 {
-    nm_hash_update_vals(h,
-                        obj->ifindex,
-                        obj->addr_source,
-                        obj->timestamp,
-                        obj->lifetime,
-                        obj->preferred,
-                        obj->n_ifa_flags,
-                        obj->plen,
-                        obj->address,
-                        obj->peer_address,
-                        NM_HASH_COMBINE_BOOLS(guint8, obj->a_force_commit));
+    nm_hash_update_vals(
+        h,
+        obj->ifindex,
+        obj->addr_source,
+        obj->timestamp,
+        obj->lifetime,
+        obj->preferred,
+        obj->n_ifa_flags,
+        obj->plen,
+        obj->address,
+        obj->peer_address,
+        NM_HASH_COMBINE_BOOLS(guint8, obj->a_force_commit, obj->a_no_auto_noprefixroute));
 }
 
 int
@@ -8294,6 +8732,7 @@ nm_platform_ip6_address_cmp(const NMPlatformIP6Address *a,
             NM_CMP_FIELD(a, b, n_ifa_flags);
             NM_CMP_FIELD(a, b, addr_source);
             NM_CMP_FIELD_UNSAFE(a, b, a_force_commit);
+            NM_CMP_FIELD_UNSAFE(a, b, a_no_auto_noprefixroute);
         }
         return 0;
     }
@@ -8337,15 +8776,18 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
             obj->initcwnd,
             obj->initrwnd,
             obj->mtu,
+            obj->rto_min,
             obj->r_rtm_flags & RTNH_F_ONLINK,
-            NM_HASH_COMBINE_BOOLS(guint8,
+            NM_HASH_COMBINE_BOOLS(guint16,
                                   obj->metric_any,
                                   obj->table_any,
+                                  obj->quickack,
                                   obj->lock_window,
                                   obj->lock_cwnd,
                                   obj->lock_initcwnd,
                                   obj->lock_initrwnd,
-                                  obj->lock_mtu));
+                                  obj->lock_mtu,
+                                  obj->lock_mss));
         break;
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY:
         nm_hash_update_vals(
@@ -8367,15 +8809,18 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
             obj->initcwnd,
             obj->initrwnd,
             obj->mtu,
+            obj->rto_min,
             obj->r_rtm_flags & (RTM_F_CLONED | RTNH_F_ONLINK),
-            NM_HASH_COMBINE_BOOLS(guint8,
+            NM_HASH_COMBINE_BOOLS(guint16,
                                   obj->metric_any,
                                   obj->table_any,
+                                  obj->quickack,
                                   obj->lock_window,
                                   obj->lock_cwnd,
                                   obj->lock_initcwnd,
                                   obj->lock_initrwnd,
-                                  obj->lock_mtu));
+                                  obj->lock_mtu,
+                                  obj->lock_mss));
         break;
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL:
         nm_hash_update_vals(h,
@@ -8396,15 +8841,18 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
                             obj->initcwnd,
                             obj->initrwnd,
                             obj->mtu,
+                            obj->rto_min,
                             obj->r_rtm_flags,
-                            NM_HASH_COMBINE_BOOLS(guint8,
+                            NM_HASH_COMBINE_BOOLS(guint16,
                                                   obj->metric_any,
                                                   obj->table_any,
+                                                  obj->quickack,
                                                   obj->lock_window,
                                                   obj->lock_cwnd,
                                                   obj->lock_initcwnd,
                                                   obj->lock_initrwnd,
                                                   obj->lock_mtu,
+                                                  obj->lock_mss,
                                                   obj->r_force_commit));
         break;
     }
@@ -8442,12 +8890,15 @@ nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a,
             NM_CMP_FIELD(a, b, initcwnd);
             NM_CMP_FIELD(a, b, initrwnd);
             NM_CMP_FIELD(a, b, mtu);
+            NM_CMP_FIELD(a, b, rto_min);
             NM_CMP_DIRECT(a->r_rtm_flags & RTNH_F_ONLINK, b->r_rtm_flags & RTNH_F_ONLINK);
+            NM_CMP_FIELD_UNSAFE(a, b, quickack);
             NM_CMP_FIELD_UNSAFE(a, b, lock_window);
             NM_CMP_FIELD_UNSAFE(a, b, lock_cwnd);
             NM_CMP_FIELD_UNSAFE(a, b, lock_initcwnd);
             NM_CMP_FIELD_UNSAFE(a, b, lock_initrwnd);
             NM_CMP_FIELD_UNSAFE(a, b, lock_mtu);
+            NM_CMP_FIELD_UNSAFE(a, b, lock_mss);
         }
         break;
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY:
@@ -8485,16 +8936,19 @@ nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a,
         } else
             NM_CMP_FIELD(a, b, r_rtm_flags);
         NM_CMP_FIELD(a, b, tos);
+        NM_CMP_FIELD_UNSAFE(a, b, quickack);
         NM_CMP_FIELD_UNSAFE(a, b, lock_window);
         NM_CMP_FIELD_UNSAFE(a, b, lock_cwnd);
         NM_CMP_FIELD_UNSAFE(a, b, lock_initcwnd);
         NM_CMP_FIELD_UNSAFE(a, b, lock_initrwnd);
         NM_CMP_FIELD_UNSAFE(a, b, lock_mtu);
+        NM_CMP_FIELD_UNSAFE(a, b, lock_mss);
         NM_CMP_FIELD(a, b, window);
         NM_CMP_FIELD(a, b, cwnd);
         NM_CMP_FIELD(a, b, initcwnd);
         NM_CMP_FIELD(a, b, initrwnd);
         NM_CMP_FIELD(a, b, mtu);
+        NM_CMP_FIELD(a, b, rto_min);
         if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL)
             NM_CMP_FIELD_UNSAFE(a, b, r_force_commit);
         break;
@@ -8552,19 +9006,22 @@ nm_platform_ip6_route_hash_update(const NMPlatformIP6Route *obj,
             nmp_utils_ip_config_source_round_trip_rtprot(obj->rt_source),
             obj->mss,
             obj->r_rtm_flags & RTM_F_CLONED,
-            NM_HASH_COMBINE_BOOLS(guint8,
+            NM_HASH_COMBINE_BOOLS(guint16,
                                   obj->metric_any,
                                   obj->table_any,
+                                  obj->quickack,
                                   obj->lock_window,
                                   obj->lock_cwnd,
                                   obj->lock_initcwnd,
                                   obj->lock_initrwnd,
-                                  obj->lock_mtu),
+                                  obj->lock_mtu,
+                                  obj->lock_mss),
             obj->window,
             obj->cwnd,
             obj->initcwnd,
             obj->initrwnd,
             obj->mtu,
+            obj->rto_min,
             _route_pref_normalize(obj->rt_pref));
         break;
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL:
@@ -8584,17 +9041,20 @@ nm_platform_ip6_route_hash_update(const NMPlatformIP6Route *obj,
                             NM_HASH_COMBINE_BOOLS(guint16,
                                                   obj->metric_any,
                                                   obj->table_any,
+                                                  obj->quickack,
                                                   obj->lock_window,
                                                   obj->lock_cwnd,
                                                   obj->lock_initcwnd,
                                                   obj->lock_initrwnd,
                                                   obj->lock_mtu,
+                                                  obj->lock_mss,
                                                   obj->r_force_commit),
                             obj->window,
                             obj->cwnd,
                             obj->initcwnd,
                             obj->initrwnd,
                             obj->mtu,
+                            obj->rto_min,
                             obj->rt_pref);
         break;
     }
@@ -8658,16 +9118,19 @@ nm_platform_ip6_route_cmp(const NMPlatformIP6Route *a,
             NM_CMP_DIRECT(a->r_rtm_flags & RTM_F_CLONED, b->r_rtm_flags & RTM_F_CLONED);
         } else
             NM_CMP_FIELD(a, b, r_rtm_flags);
+        NM_CMP_FIELD_UNSAFE(a, b, quickack);
         NM_CMP_FIELD_UNSAFE(a, b, lock_window);
         NM_CMP_FIELD_UNSAFE(a, b, lock_cwnd);
         NM_CMP_FIELD_UNSAFE(a, b, lock_initcwnd);
         NM_CMP_FIELD_UNSAFE(a, b, lock_initrwnd);
         NM_CMP_FIELD_UNSAFE(a, b, lock_mtu);
+        NM_CMP_FIELD_UNSAFE(a, b, lock_mss);
         NM_CMP_FIELD(a, b, window);
         NM_CMP_FIELD(a, b, cwnd);
         NM_CMP_FIELD(a, b, initcwnd);
         NM_CMP_FIELD(a, b, initrwnd);
         NM_CMP_FIELD(a, b, mtu);
+        NM_CMP_FIELD(a, b, rto_min);
         if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)
             NM_CMP_DIRECT(_route_pref_normalize(a->rt_pref), _route_pref_normalize(b->rt_pref));
         else
@@ -8927,6 +9390,37 @@ nm_platform_ip_address_cmp_expiry(const NMPlatformIPAddress *a, const NMPlatform
 
 /*****************************************************************************/
 
+guint16
+nm_platform_genl_get_family_id(NMPlatform *self, NMPGenlFamilyType family_type)
+{
+    _CHECK_SELF(self, klass, 0);
+
+    if (!_NM_INT_NOT_NEGATIVE(family_type) || family_type >= _NMP_GENL_FAMILY_TYPE_NUM)
+        g_return_val_if_reached(0);
+
+    return klass->genl_get_family_id(self, family_type);
+}
+
+/*****************************************************************************/
+
+int
+nm_platform_mptcp_addr_update(NMPlatform *self, NMOptionBool add, const NMPlatformMptcpAddr *addr)
+{
+    _CHECK_SELF(self, klass, -NME_BUG);
+
+    return klass->mptcp_addr_update(self, add, addr);
+}
+
+GPtrArray *
+nm_platform_mptcp_addrs_dump(NMPlatform *self)
+{
+    _CHECK_SELF(self, klass, NULL);
+
+    return klass->mptcp_addrs_dump(self);
+}
+
+/*****************************************************************************/
+
 GHashTable *
 nm_platform_ip4_address_addr_to_hash(NMPlatform *self, int ifindex)
 {
@@ -8939,7 +9433,7 @@ nm_platform_ip4_address_addr_to_hash(NMPlatform *self, int ifindex)
     g_return_val_if_fail(NM_IS_PLATFORM(self), NULL);
     g_return_val_if_fail(ifindex > 0, NULL);
 
-    nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP4_ADDRESS, ifindex);
+    nmp_lookup_init_object_by_ifindex(&lookup, NMP_OBJECT_TYPE_IP4_ADDRESS, ifindex);
 
     head_entry = nmp_cache_lookup(NM_PLATFORM_GET_PRIVATE(self)->cache, &lookup);
 
@@ -8975,8 +9469,6 @@ nm_platform_ip4_address_generate_device_route(const NMPlatformIP4Address *addr,
 
     nm_assert(addr);
     nm_assert(addr->plen <= 32);
-    if (addr->plen == 0)
-        return NULL;
 
     if (addr->plen == 0)
         return NULL;
@@ -9037,9 +9529,13 @@ log_link(NMPlatform                *self,
 {
     char sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
 
-    _LOG3D("signal: link %7s: %s",
-           nm_platform_signal_change_type_to_string(change_type),
-           nm_platform_link_to_string(device, sbuf, sizeof(sbuf)));
+    if (_LOGD_ENABLED()) {
+        NMLOG_COMMON(LOGL_DEBUG,
+                     device->name,
+                     "signal: link %7s: %s",
+                     nm_platform_signal_change_type_to_string(change_type),
+                     nm_platform_link_to_string(device, sbuf, sizeof(sbuf)));
+    }
 }
 
 static void
diff --git a/src/libnm-platform/nm-platform.h b/src/libnm-platform/nm-platform.h
index 190a91dc..90ffbed0 100644
--- a/src/libnm-platform/nm-platform.h
+++ b/src/libnm-platform/nm-platform.h
@@ -61,6 +61,12 @@ typedef gboolean (*NMPObjectPredicateFunc)(const NMPObject *obj, gpointer user_d
 
 #define NM_IFF_MULTI_QUEUE 0x0100 /* IFF_MULTI_QUEUE */
 
+#define NM_MPTCP_PM_ADDR_FLAG_SIGNAL   ((guint32) (1 << 0))
+#define NM_MPTCP_PM_ADDR_FLAG_SUBFLOW  ((guint32) (1 << 1))
+#define NM_MPTCP_PM_ADDR_FLAG_BACKUP   ((guint32) (1 << 2))
+#define NM_MPTCP_PM_ADDR_FLAG_FULLMESH ((guint32) (1 << 3))
+#define NM_MPTCP_PM_ADDR_FLAG_IMPLICIT ((guint32) (1 << 4))
+
 /* Redefine this in host's endianness */
 #define NM_GRE_KEY 0x2000
 
@@ -333,6 +339,12 @@ typedef enum {
      * should be configured. */             \
     bool a_force_commit : 1;                                                                 \
                                                                                              \
+    /* nm_platform_ip_address_sync() likes to add IFA_F_NOPREFIXROUTE flag for all
+     * addresses, regardless of a_ifi_flags property. By setting this boolean, that
+     * automatism can be suppressed, and the noprefixroute flag does not get added
+     * automatically. */           \
+    bool a_no_auto_noprefixroute : 1;                                                        \
+                                                                                             \
     /* Don't have a bitfield as last field in __NMPlatformIPAddress_COMMON. It would then
      * be unclear how the following fields get merged. We could also use a zero bitfield,
      * but instead we just have there the uint8 field. */    \
@@ -453,6 +465,9 @@ typedef union {
     /* RTA_METRICS.RTAX_INITRWND (iproute2: initrwnd) */                                  \
     guint32 initrwnd;                                                                     \
                                                                                           \
+    /* RTA_METRICS.RTAX_RTO_MIN (iproute2: rto_min) */                                    \
+    guint32 rto_min;                                                                      \
+                                                                                          \
     /* RTA_METRICS.RTAX_MTU (iproute2: mtu) */                                            \
     guint32 mtu;                                                                          \
                                                                                           \
@@ -499,6 +514,10 @@ typedef union {
     bool lock_initcwnd : 1;                                                               \
     bool lock_initrwnd : 1;                                                               \
     bool lock_mtu : 1;                                                                    \
+    bool lock_mss : 1;                                                                    \
+                                                                                          \
+    /* RTA_METRICS.RTAX_QUICKACK (iproute2: quickack) */                                  \
+    bool quickack : 1;                                                                    \
                                                                                           \
     /* if TRUE, the "metric" field is interpreted as an offset that is added to a default
      * metric. For example, form a DHCP lease we don't know the actually used metric, because
@@ -775,8 +794,6 @@ typedef struct {
     NMPlatformAction action;
 } NMPlatformTfilter;
 
-#undef __NMPlatformObjWithIfindex_COMMON
-
 typedef struct {
     bool          is_ip4;
     NMPObjectType obj_type;
@@ -826,10 +843,10 @@ typedef struct {
 } NMPlatformVFVlan;
 
 typedef struct {
+    guint             num_vlans;
     guint32           index;
     guint32           min_tx_rate;
     guint32           max_tx_rate;
-    guint             num_vlans;
     NMPlatformVFVlan *vlans;
     struct {
         guint8 data[20]; /* _NM_UTILS_HWADDR_LEN_MAX */
@@ -847,41 +864,82 @@ typedef struct {
 } NMPlatformBridgeVlan;
 
 typedef struct {
-    NMEtherAddr group_addr;
-    bool        mcast_querier : 1;
-    bool        mcast_query_use_ifaddr : 1;
-    bool        mcast_snooping : 1;
-    bool        stp_state : 1;
-    bool        vlan_stats_enabled : 1;
-    guint16     group_fwd_mask;
-    guint16     priority;
-    guint16     vlan_protocol;
-    guint32     ageing_time;
-    guint32     forward_delay;
-    guint32     hello_time;
-    guint32     max_age;
-    guint32     mcast_last_member_count;
-    guint32     mcast_startup_query_count;
-    guint32     mcast_hash_max;
     guint64     mcast_last_member_interval;
     guint64     mcast_membership_interval;
     guint64     mcast_querier_interval;
     guint64     mcast_query_interval;
     guint64     mcast_query_response_interval;
     guint64     mcast_startup_query_interval;
+    guint32     ageing_time;
+    guint32     forward_delay;
+    guint32     hello_time;
+    guint32     max_age;
+    guint32     mcast_hash_max;
+    guint32     mcast_last_member_count;
+    guint32     mcast_startup_query_count;
+    guint16     group_fwd_mask;
+    guint16     priority;
+    guint16     vlan_protocol;
+    NMEtherAddr group_addr;
     guint8      mcast_router;
+    bool        mcast_querier : 1;
+    bool        mcast_query_use_ifaddr : 1;
+    bool        mcast_snooping : 1;
+    bool        stp_state : 1;
+    bool        vlan_stats_enabled : 1;
 } NMPlatformLnkBridge;
 
 extern const NMPlatformLnkBridge nm_platform_lnk_bridge_default;
 
+/* Defined in net/bonding.h. */
+#define NM_BOND_MAX_ARP_TARGETS 16
+
 typedef struct {
+    in_addr_t   arp_ip_target[NM_BOND_MAX_ARP_TARGETS];
+    guint32     arp_all_targets;
+    guint32     arp_interval;
+    guint32     arp_validate;
+    guint32     downdelay;
+    guint32     lp_interval;
+    guint32     miimon;
+    guint32     min_links;
+    guint32     packets_per_port;
+    guint32     peer_notif_delay;
+    guint32     primary;
+    guint32     resend_igmp;
+    guint32     updelay;
+    guint16     ad_actor_sys_prio;
+    guint16     ad_user_port_key;
+    NMEtherAddr ad_actor_system;
+    guint8      ad_select;
+    guint8      all_ports_active;
+    guint8      arp_ip_targets_num;
+    guint8      fail_over_mac;
+    guint8      lacp_rate;
+    guint8      num_grat_arp;
+    guint8      mode;
+    guint8      primary_reselect;
+    guint8      xmit_hash_policy;
+    bool        arp_all_targets_has : 1;
+    bool        downdelay_has : 1;
+    bool        lp_interval_has : 1;
+    bool        miimon_has : 1;
+    bool        peer_notif_delay_has : 1;
+    bool        resend_igmp_has : 1;
+    bool        tlb_dynamic_lb : 1;
+    bool        tlb_dynamic_lb_has : 1;
+    bool        updelay_has : 1;
+    bool        use_carrier : 1;
+} NMPlatformLnkBond;
+
+typedef struct {
+    int       parent_ifindex;
     in_addr_t local;
     in_addr_t remote;
-    int       parent_ifindex;
-    guint16   input_flags;
-    guint16   output_flags;
     guint32   input_key;
     guint32   output_key;
+    guint16   input_flags;
+    guint16   output_flags;
     guint8    ttl;
     guint8    tos;
     bool      path_mtu_discovery : 1;
@@ -897,12 +955,12 @@ typedef struct {
     struct in6_addr local;
     struct in6_addr remote;
     int             parent_ifindex;
+    guint           flow_label;
+    guint32         flags;
     guint8          ttl;
     guint8          tclass;
     guint8          encap_limit;
     guint8          proto;
-    guint           flow_label;
-    guint32         flags;
 
     /* IP6GRE only */
     guint32 input_key;
@@ -914,9 +972,9 @@ typedef struct {
 } NMPlatformLnkIp6Tnl;
 
 typedef struct {
+    int       parent_ifindex;
     in_addr_t local;
     in_addr_t remote;
-    int       parent_ifindex;
     guint8    ttl;
     guint8    tos;
     bool      path_mtu_discovery : 1;
@@ -945,9 +1003,9 @@ typedef struct {
 } NMPlatformLnkMacvlan;
 
 typedef struct {
+    int       parent_ifindex;
     in_addr_t local;
     in_addr_t remote;
-    int       parent_ifindex;
     guint16   flags;
     guint8    ttl;
     guint8    tos;
@@ -983,9 +1041,9 @@ typedef struct {
 typedef struct {
     struct in6_addr group6;
     struct in6_addr local6;
+    int             parent_ifindex;
     in_addr_t       group;
     in_addr_t       local;
-    int             parent_ifindex;
     guint32         id;
     guint32         ageing;
     guint32         limit;
@@ -1038,6 +1096,17 @@ typedef enum {
 
 typedef void (*NMPlatformAsyncCallback)(GError *error, gpointer user_data);
 
+typedef struct {
+    __NMPlatformObjWithIfindex_COMMON;
+    guint32  id;
+    guint32  flags;
+    guint16  port;
+    NMIPAddr addr;
+    gint8    addr_family;
+} NMPlatformMptcpAddr;
+
+#undef __NMPlatformObjWithIfindex_COMMON
+
 /*****************************************************************************/
 
 typedef struct _NMPlatformCsmeConnInfo {
@@ -1096,6 +1165,25 @@ nm_platform_kernel_support_get(NMPlatformKernelSupportType type)
     return nm_platform_kernel_support_get_full(type, TRUE) != NM_OPTION_BOOL_FALSE;
 }
 
+typedef enum {
+    NMP_GENL_FAMILY_TYPE_ETHTOOL,
+    NMP_GENL_FAMILY_TYPE_MPTCP_PM,
+    NMP_GENL_FAMILY_TYPE_NL80211,
+    NMP_GENL_FAMILY_TYPE_NL802154,
+    NMP_GENL_FAMILY_TYPE_WIREGUARD,
+
+    _NMP_GENL_FAMILY_TYPE_NUM,
+    _NMP_GENL_FAMILY_TYPE_NONE = _NMP_GENL_FAMILY_TYPE_NUM,
+} NMPGenlFamilyType;
+
+typedef struct {
+    const char *name;
+} NMPGenlFamilyInfo;
+
+extern const NMPGenlFamilyInfo nmp_genl_family_infos[_NMP_GENL_FAMILY_TYPE_NUM];
+
+NMPGenlFamilyType nmp_genl_family_type_from_name(const char *name);
+
 /*****************************************************************************/
 
 struct _NMPlatformPrivate;
@@ -1205,6 +1293,7 @@ typedef struct {
                                  gboolean                egress_reset_all,
                                  const NMVlanQosMapping *egress_map,
                                  gsize                   n_egress_map);
+
     gboolean (*link_tun_add)(NMPlatform             *self,
                              const char             *name,
                              const NMPlatformLnkTun *props,
@@ -1302,6 +1391,13 @@ typedef struct {
 
     int (*tfilter_add)(NMPlatform *self, NMPNlmFlags flags, const NMPlatformTfilter *tfilter);
     int (*tfilter_delete)(NMPlatform *self, int ifindex, guint32 parent, gboolean log_error);
+
+    guint16 (*genl_get_family_id)(NMPlatform *platform, NMPGenlFamilyType family_type);
+
+    int (*mptcp_addr_update)(NMPlatform *self, NMOptionBool add, const NMPlatformMptcpAddr *addr);
+
+    GPtrArray *(*mptcp_addrs_dump)(NMPlatform *self);
+
 } NMPlatformClass;
 
 /* NMPlatform signals
@@ -1489,6 +1585,49 @@ nm_platform_route_type_uncoerce(guint8 type_coerced)
     return nm_platform_route_type_coerce(type_coerced);
 }
 
+static inline guint8
+nm_platform_ip4_address_get_scope(in_addr_t addr)
+{
+    /* For IPv4 addresses, we can set any scope we want (for any address).
+     * However, there are scopes that make sense based on the address,
+     * so choose those. */
+    return nm_utils_ip4_address_is_loopback(addr)     ? (254 /* RT_SCOPE_HOST */)
+           : nm_utils_ip4_address_is_link_local(addr) ? (253 /* RT_SCOPE_LINK */)
+                                                      : (0 /* RT_SCOPE_UNIVERSE */);
+}
+
+static inline guint8
+nm_platform_ip6_address_get_scope(const struct in6_addr *addr)
+{
+    /* For IPv6, kernel does not allow userspace to configure the address scope.
+     * Instead, it is calculated based on the address. See rt_scope() and
+     * ipv6_addr_scope(). We do the same here. */
+    return IN6_IS_ADDR_LOOPBACK(addr)    ? (254 /* RT_SCOPE_HOST */)
+           : IN6_IS_ADDR_LINKLOCAL(addr) ? (253 /* RT_SCOPE_LINK */)
+           : IN6_IS_ADDR_SITELOCAL(addr) ? (200 /* RT_SCOPE_SITE */)
+                                         : (0 /* RT_SCOPE_UNIVERSE */);
+}
+
+static inline guint8
+nm_platform_ip_address_get_scope(int addr_family, gconstpointer addr)
+{
+    /* Note that this function returns the scope as we configure
+     * it in kernel (for IPv4) or as kernel chooses it (for IPv6).
+     *
+     * That means, rfc1918 private addresses nm_utils_ip_is_site_local() are
+     * considered RT_SCOPE_UNIVERSE.
+     *
+     * Also, the deprecated IN6_IS_ADDR_SITELOCAL() addresses (fec0::/10)
+     * are considered RT_SCOPE_SITE, while unique local addresses (ULA, fc00::/7)
+     * are considered RT_SCOPE_UNIVERSE.
+     *
+     * You may not want to use this function when reasoning about
+     * site-local addresses (RFC1918, ULA). */
+    if (NM_IS_IPv4(addr_family))
+        return nm_platform_ip4_address_get_scope(*((in_addr_t *) addr));
+    return nm_platform_ip6_address_get_scope(addr);
+}
+
 gboolean nm_platform_get_use_udev(NMPlatform *self);
 gboolean nm_platform_get_log_with_ptr(NMPlatform *self);
 gboolean nm_platform_get_cache_tc(NMPlatform *self);
@@ -1657,9 +1796,18 @@ nm_platform_link_bridge_change(NMPlatform *self, int ifindex, const NMPlatformLn
 }
 
 static inline int
-nm_platform_link_bond_add(NMPlatform *self, const char *name, const NMPlatformLink **out_link)
+nm_platform_link_bond_change(NMPlatform *self, int ifindex, const NMPlatformLnkBond *props)
 {
-    return nm_platform_link_add(self, NM_LINK_TYPE_BOND, name, 0, NULL, 0, 0, NULL, out_link);
+    return nm_platform_link_change(self, NM_LINK_TYPE_BOND, ifindex, props);
+}
+
+static inline int
+nm_platform_link_bond_add(NMPlatform              *self,
+                          const char              *name,
+                          const NMPlatformLnkBond *props,
+                          const NMPlatformLink   **out_link)
+{
+    return nm_platform_link_add(self, NM_LINK_TYPE_BOND, name, 0, NULL, 0, 0, props, out_link);
 }
 
 static inline int
@@ -1993,6 +2141,8 @@ const NMPObject *nm_platform_link_get_lnk(NMPlatform            *self,
                                           int                    ifindex,
                                           NMLinkType             link_type,
                                           const NMPlatformLink **out_link);
+const NMPlatformLnkBond *
+nm_platform_link_get_lnk_bond(NMPlatform *self, int ifindex, const NMPlatformLink **out_link);
 const NMPlatformLnkBridge *
 nm_platform_link_get_lnk_bridge(NMPlatform *self, int ifindex, const NMPlatformLink **out_link);
 const NMPlatformLnkGre *
@@ -2287,6 +2437,7 @@ gboolean nm_platform_tc_sync(NMPlatform *self,
                              GPtrArray  *known_tfilters);
 
 const char *nm_platform_link_to_string(const NMPlatformLink *link, char *buf, gsize len);
+const char *nm_platform_lnk_bond_to_string(const NMPlatformLnkBond *lnk, char *buf, gsize len);
 const char *nm_platform_lnk_bridge_to_string(const NMPlatformLnkBridge *lnk, char *buf, gsize len);
 const char *nm_platform_lnk_gre_to_string(const NMPlatformLnkGre *lnk, char *buf, gsize len);
 const char *
@@ -2326,7 +2477,11 @@ const char *nm_platform_vlan_qos_mapping_to_string(const char             *name,
 const char *
 nm_platform_wireguard_peer_to_string(const struct _NMPWireGuardPeer *peer, char *buf, gsize len);
 
+const char *
+nm_platform_mptcp_addr_to_string(const NMPlatformMptcpAddr *mptcp_addr, char *buf, gsize len);
+
 int nm_platform_link_cmp(const NMPlatformLink *a, const NMPlatformLink *b);
+int nm_platform_lnk_bond_cmp(const NMPlatformLnkBond *a, const NMPlatformLnkBond *b);
 int nm_platform_lnk_bridge_cmp(const NMPlatformLnkBridge *a, const NMPlatformLnkBridge *b);
 int nm_platform_lnk_gre_cmp(const NMPlatformLnkGre *a, const NMPlatformLnkGre *b);
 int nm_platform_lnk_infiniband_cmp(const NMPlatformLnkInfiniband *a,
@@ -2404,6 +2559,8 @@ int nm_platform_qdisc_cmp_full(const NMPlatformQdisc *a,
                                gboolean               compare_handle);
 int nm_platform_tfilter_cmp(const NMPlatformTfilter *a, const NMPlatformTfilter *b);
 
+int nm_platform_mptcp_addr_cmp(const NMPlatformMptcpAddr *a, const NMPlatformMptcpAddr *b);
+
 void nm_platform_link_hash_update(const NMPlatformLink *obj, NMHashState *h);
 void nm_platform_ip4_address_hash_update(const NMPlatformIP4Address *obj, NMHashState *h);
 void nm_platform_ip6_address_hash_update(const NMPlatformIP6Address *obj, NMHashState *h);
@@ -2416,6 +2573,7 @@ void nm_platform_ip6_route_hash_update(const NMPlatformIP6Route *obj,
 void nm_platform_routing_rule_hash_update(const NMPlatformRoutingRule *obj,
                                           NMPlatformRoutingRuleCmpType cmp_type,
                                           NMHashState                 *h);
+void nm_platform_lnk_bond_hash_update(const NMPlatformLnkBond *obj, NMHashState *h);
 void nm_platform_lnk_bridge_hash_update(const NMPlatformLnkBridge *obj, NMHashState *h);
 void nm_platform_lnk_gre_hash_update(const NMPlatformLnkGre *obj, NMHashState *h);
 void nm_platform_lnk_infiniband_hash_update(const NMPlatformLnkInfiniband *obj, NMHashState *h);
@@ -2433,6 +2591,11 @@ void nm_platform_lnk_wireguard_hash_update(const NMPlatformLnkWireGuard *obj, NM
 void nm_platform_qdisc_hash_update(const NMPlatformQdisc *obj, NMHashState *h);
 void nm_platform_tfilter_hash_update(const NMPlatformTfilter *obj, NMHashState *h);
 
+void nm_platform_mptcp_addr_hash_update(const NMPlatformMptcpAddr *obj, NMHashState *h);
+
+guint    nm_platform_mptcp_addr_index_addr_cmp(gconstpointer data);
+gboolean nm_platform_mptcp_addr_index_addr_equal(gconstpointer data_a, gconstpointer data_b);
+
 #define NM_PLATFORM_LINK_FLAGS2STR_MAX_LEN ((gsize) 162)
 
 const char *nm_platform_link_flags2str(unsigned flags, char *buf, gsize len);
@@ -2505,4 +2668,13 @@ gboolean nm_platform_ip_address_match(int                        addr_family,
                                       const NMPlatformIPAddress *addr,
                                       NMPlatformMatchFlags       match_flag);
 
+/*****************************************************************************/
+
+guint16 nm_platform_genl_get_family_id(NMPlatform *self, NMPGenlFamilyType family_type);
+
+int
+nm_platform_mptcp_addr_update(NMPlatform *self, NMOptionBool add, const NMPlatformMptcpAddr *addr);
+
+GPtrArray *nm_platform_mptcp_addrs_dump(NMPlatform *self);
+
 #endif /* __NETWORKMANAGER_PLATFORM_H__ */
diff --git a/src/libnm-platform/nmp-base.h b/src/libnm-platform/nmp-base.h
index 48631688..a6ee3df1 100644
--- a/src/libnm-platform/nmp-base.h
+++ b/src/libnm-platform/nmp-base.h
@@ -147,6 +147,9 @@ typedef enum _nm_packed {
     NMP_OBJECT_TYPE_LNK_VRF,
     NMP_OBJECT_TYPE_LNK_VXLAN,
     NMP_OBJECT_TYPE_LNK_WIREGUARD,
+    NMP_OBJECT_TYPE_LNK_BOND,
+
+    NMP_OBJECT_TYPE_MPTCP_ADDR,
 
     __NMP_OBJECT_TYPE_LAST,
     NMP_OBJECT_TYPE_MAX = __NMP_OBJECT_TYPE_LAST - 1,
diff --git a/src/libnm-platform/nmp-global-tracker.c b/src/libnm-platform/nmp-global-tracker.c
new file mode 100644
index 00000000..ea4da284
--- /dev/null
+++ b/src/libnm-platform/nmp-global-tracker.c
@@ -0,0 +1,1261 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nmp-global-tracker.h"
+
+#include <linux/fib_rules.h>
+#include <linux/rtnetlink.h>
+
+#include "libnm-log-core/nm-logging.h"
+#include "libnm-std-aux/c-list-util.h"
+#include "nmp-object.h"
+
+/* This limit comes from kernel, and it limits the number of MPTCP addresses
+ * we can configure. */
+#define MPTCP_PM_ADDR_MAX 8
+
+/*****************************************************************************/
+
+/* NMPGlobalTracker tracks certain objects for the entire network namespace and can
+ * commit them.
+ *
+ * We tend to configure things per-interface and per-profile. In many cases,
+ * we thereby only need to care about the things for that interface. For example,
+ * we can configure IP addresses and (unicast) routes without having a system wide
+ * view. That is mainly, because such objects are themselves tied to an ifindex.
+ *
+ * However, for certain objects that's not the case. For example, policy routing
+ * rules, certain route types (blackhole, unavailable, prohibit, throw) and MPTCP
+ * endpoints require a holistic view of the system. That is, because rules and
+ * these route types have no ifindex. For MPTCP endpoints, they have an ifindex,
+ * however we can only configure a small number of them at a time, so we need a
+ * central (global) instance that can track which endpoints to configure.
+ *
+ * In general, the NMPGlobalTracker tracks objects for the entire namespace, and
+ * it's sync() method will figure out how to configure them.
+ *
+ * Since the users of NMPGloablTracker (NML3Cfg, NMDevice) themselves don't
+ * have this holistic view, the API of NMPGlobalTracker allows them to track
+ * individual objects independently (they register their objects for a private
+ * user-tag). If multiple such independent users track the same object, the tracking
+ * priority (track_priority_val) determines which one wins.
+ *
+ * NMPGlobalTracker can not only track whether an object should be present,
+ * it also can track whether it should be absent. See track_priority_present.
+ */
+
+/*****************************************************************************/
+
+struct _NMPGlobalTracker {
+    NMPlatform *platform;
+    GHashTable *by_obj;
+    GHashTable *by_user_tag;
+    GHashTable *by_data;
+    CList       by_obj_lst_heads[4];
+    guint       ref_count;
+};
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_PLATFORM
+#define _NMLOG_PREFIX_NAME "global-tracker"
+
+#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, LOGD_PLATFORM, _NMLOG_PREFIX_NAME, __VA_ARGS__)
+
+/*****************************************************************************/
+
+static gboolean
+NMP_IS_GLOBAL_TRACKER(gpointer self)
+{
+    return self && ((NMPGlobalTracker *) self)->ref_count > 0
+           && NM_IS_PLATFORM(((NMPGlobalTracker *) self)->platform);
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    const NMPObject *obj;
+    gconstpointer    user_tag;
+    CList            obj_lst;
+    CList            user_tag_lst;
+
+    /* @track_priority_val zero is special: those are weakly tracked objects.
+     * That means: NetworkManager will restore them only if it removed them earlier.
+     * But it will not remove or add them otherwise.
+     *
+     * Otherwise, @track_priority_val goes together with @track_priority_present.
+     * In case of one object being tracked multiple times (with different priorities),
+     * the one with higher priority wins. See _track_obj_data_get_best_data().
+     * Then, the winning present state either enforces that the rule is present
+     * or absent.
+     *
+     * If an object is not tracked at all, it is ignored by NetworkManager (except
+     * for MPTCP endpoints for the tracked interface). Assuming that it was added
+     * externally by the user. But unlike weakly tracked rules, NM will *not* restore
+     * such rules if NetworkManager themself removed them. */
+    guint32 track_priority_val;
+    bool    track_priority_present : 1;
+
+    /* Calling nmp_global_tracker_track() will ensure that the tracked entry is
+     * non-dirty. Together with nmp_global_tracker_set_dirty() and nmp_global_tracker_untrack_all()'s
+     * @all parameter, this can be used to remove stale entries. */
+    bool dirty : 1;
+} TrackData;
+
+typedef enum {
+    CONFIG_STATE_NONE          = 0,
+    CONFIG_STATE_ADDED_BY_US   = 1,
+    CONFIG_STATE_REMOVED_BY_US = 2,
+
+    /* ConfigState encodes whether the object was touched by us at all (CONFIG_STATE_NONE).
+     *
+     * Maybe we would only need to track whether we touched the object at all. But we
+     * track it more in detail what we did: did we add it (CONFIG_STATE_ADDED_BY_US)
+     * or did we remove it (CONFIG_STATE_REMOVED_BY_US)?
+     * Finally, we need CONFIG_STATE_OWNED_BY_US, which means that we didn't actively
+     * add/remove it, but whenever we are about to undo the add/remove, we need to do it.
+     * In that sense, CONFIG_STATE_OWNED_BY_US is really just a flag that we unconditionally
+     * force the state next time when necessary. */
+    CONFIG_STATE_OWNED_BY_US = 3,
+} ConfigState;
+
+typedef struct {
+    const NMPObject *obj;
+    CList            obj_lst_head;
+
+    CList by_obj_lst;
+
+    /* indicates whether we configured/removed the object (during sync()). We need that, so
+     * if the object gets untracked, that we know to remove/restore it.
+     *
+     * This makes NMPGlobalTracker stateful (beyond the configuration that indicates
+     * which objects are tracked).
+     * After a restart, NetworkManager would no longer remember which objects were added
+     * by us.
+     *
+     * That is partially fixed by NetworkManager taking over the objects that it
+     * actively configures (see %NMP_GLOBAL_TRACKER_EXTERN_WEAKLY_TRACKED_USER_TAG). */
+    ConfigState config_state;
+} TrackObjData;
+
+typedef struct {
+    gconstpointer user_tag;
+    CList         user_tag_lst_head;
+} TrackUserTagData;
+
+/*****************************************************************************/
+
+static void _track_data_untrack(NMPGlobalTracker *self,
+                                TrackData        *track_data,
+                                gboolean          remove_user_tag_data,
+                                gboolean          make_owned_by_us);
+
+/*****************************************************************************/
+
+static CList *
+_by_obj_lst_head(NMPGlobalTracker *self, NMPObjectType obj_type)
+{
+    G_STATIC_ASSERT(G_N_ELEMENTS(self->by_obj_lst_heads) == 4);
+
+    switch (obj_type) {
+    case NMP_OBJECT_TYPE_IP4_ROUTE:
+        return &self->by_obj_lst_heads[0];
+    case NMP_OBJECT_TYPE_IP6_ROUTE:
+        return &self->by_obj_lst_heads[1];
+    case NMP_OBJECT_TYPE_ROUTING_RULE:
+        return &self->by_obj_lst_heads[2];
+    case NMP_OBJECT_TYPE_MPTCP_ADDR:
+        return &self->by_obj_lst_heads[3];
+    default:
+        return nm_assert_unreachable_val(NULL);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+_track_data_assert(const TrackData *track_data, gboolean linked)
+{
+    nm_assert(track_data);
+    nm_assert(NM_IN_SET(NMP_OBJECT_GET_TYPE(track_data->obj),
+                        NMP_OBJECT_TYPE_IP4_ROUTE,
+                        NMP_OBJECT_TYPE_IP6_ROUTE,
+                        NMP_OBJECT_TYPE_ROUTING_RULE,
+                        NMP_OBJECT_TYPE_MPTCP_ADDR));
+    nm_assert(nmp_object_is_visible(track_data->obj));
+    nm_assert(track_data->user_tag);
+    nm_assert(!linked || !c_list_is_empty(&track_data->obj_lst));
+    nm_assert(!linked || !c_list_is_empty(&track_data->user_tag_lst));
+}
+
+static guint
+_track_data_hash(gconstpointer data)
+{
+    const TrackData *track_data = data;
+    NMHashState      h;
+
+    _track_data_assert(track_data, FALSE);
+
+    nm_hash_init(&h, 269297543u);
+    nmp_object_id_hash_update(track_data->obj, &h);
+    nm_hash_update_val(&h, track_data->user_tag);
+    return nm_hash_complete(&h);
+}
+
+static gboolean
+_track_data_equal(gconstpointer data_a, gconstpointer data_b)
+{
+    const TrackData *track_data_a = data_a;
+    const TrackData *track_data_b = data_b;
+
+    _track_data_assert(track_data_a, FALSE);
+    _track_data_assert(track_data_b, FALSE);
+
+    return track_data_a->user_tag == track_data_b->user_tag
+           && nmp_object_id_equal(track_data_a->obj, track_data_b->obj);
+}
+
+static void
+_track_data_destroy(gpointer data)
+{
+    TrackData *track_data = data;
+
+    _track_data_assert(track_data, FALSE);
+
+    c_list_unlink_stale(&track_data->obj_lst);
+    c_list_unlink_stale(&track_data->user_tag_lst);
+    nmp_object_unref(track_data->obj);
+    nm_g_slice_free(track_data);
+}
+
+static const TrackData *
+_track_obj_data_get_best_data(TrackObjData *obj_data)
+{
+    TrackData       *track_data;
+    const TrackData *td_best = NULL;
+
+    c_list_for_each_entry (track_data, &obj_data->obj_lst_head, obj_lst) {
+        _track_data_assert(track_data, TRUE);
+
+        if (td_best) {
+            if (td_best->track_priority_val > track_data->track_priority_val)
+                continue;
+            if (td_best->track_priority_val == track_data->track_priority_val) {
+                if (td_best->track_priority_present || !track_data->track_priority_present) {
+                    /* if the priorities are identical, then "present" wins over
+                     * "!present" (absent). */
+                    continue;
+                }
+            }
+        }
+
+        td_best = track_data;
+    }
+
+    return td_best;
+}
+
+static guint
+_track_obj_data_hash(gconstpointer data)
+{
+    const TrackObjData *obj_data = data;
+
+    return nmp_object_id_hash(obj_data->obj);
+}
+
+static gboolean
+_track_obj_data_equal(gconstpointer data_a, gconstpointer data_b)
+{
+    const TrackObjData *obj_data_a = data_a;
+    const TrackObjData *obj_data_b = data_b;
+
+    return nmp_object_id_equal(obj_data_a->obj, obj_data_b->obj);
+}
+
+static void
+_track_obj_data_destroy(gpointer data)
+{
+    TrackObjData *obj_data = data;
+
+    c_list_unlink_stale(&obj_data->obj_lst_head);
+    c_list_unlink_stale(&obj_data->by_obj_lst);
+    nmp_object_unref(obj_data->obj);
+    nm_g_slice_free(obj_data);
+}
+
+static void
+_track_user_tag_data_destroy(gpointer data)
+{
+    TrackUserTagData *user_tag_data = data;
+
+    c_list_unlink_stale(&user_tag_data->user_tag_lst_head);
+    nm_g_slice_free(user_tag_data);
+}
+
+static TrackData *
+_track_data_lookup(GHashTable *by_data, const NMPObject *obj, gconstpointer user_tag)
+{
+    TrackData track_data_needle = {
+        .obj      = obj,
+        .user_tag = user_tag,
+    };
+
+    return g_hash_table_lookup(by_data, &track_data_needle);
+}
+
+/*****************************************************************************/
+
+static const NMPObject *
+_obj_stackinit(NMPObject *obj_stack, NMPObjectType obj_type, gconstpointer obj)
+{
+    nmp_object_stackinit(obj_stack, obj_type, obj);
+
+    if (NM_MORE_ASSERTS > 10) {
+        if (obj_type == NMP_OBJECT_TYPE_MPTCP_ADDR) {
+            NMPlatformMptcpAddr *m = NMP_OBJECT_CAST_MPTCP_ADDR(obj_stack);
+            NMPlatformMptcpAddr  m_dummy;
+
+            /* Only certain MPTCP addresses can be added. */
+            nm_assert(m->ifindex > 0);
+            if (nm_platform_mptcp_addr_cmp(
+                    nmp_global_tracker_mptcp_addr_init_for_ifindex(&m_dummy, m->ifindex),
+                    m)
+                == 0) {
+                /* This is a dummy instance. We are good. */
+            } else {
+                nm_assert_addr_family(m->addr_family);
+                nm_assert(m->port == 0);
+                nm_assert(m->id == 0);
+            }
+        }
+    }
+
+    nm_assert(nmp_object_is_visible(obj_stack));
+    return obj_stack;
+}
+
+/**
+ * nmp_global_tracker_track:
+ * @self: the #NMPGlobalTracker instance
+ * @obj_type: the NMPObjectType of @obj that we are tracking.
+ * @obj: the NMPlatformObject (of type NMPObjectType) to track. Usually
+ *   a #NMPlatformRoutingRule, #NMPlatformIP4Route, #NMPlatformIP6Route
+ *   or #NMPlatformMptcpAddr pointer.
+ * @track_priority: the priority for tracking the rule. Note that
+ *   negative values indicate a forced absence of the rule. Priorities
+ *   are compared with their absolute values (with higher absolute
+ *   value being more important). For example, if you track the same
+ *   rule twice, once with priority -5 and +10, then the rule is
+ *   present (because the positive number is more important).
+ *   The special value 0 indicates weakly-tracked rules.
+ * @user_tag: the tag associated with tracking this rule. The same tag
+ *   must be used to untrack the rule later.
+ * @user_tag_untrack: if not %NULL, at the same time untrack this user-tag
+ *   for the same rule. Note that this is different from a plain nmp_global_tracker_untrack_rule(),
+ *   because it enforces ownership of the now tracked rule. On the other hand,
+ *   a plain nmp_global_tracker_untrack_rule() merely forgets about the tracking.
+ *   The purpose here is to set this to %NMP_GLOBAL_TRACKER_EXTERN_WEAKLY_TRACKED_USER_TAG.
+ *
+ * Returns: %TRUE, if something changed.
+ */
+gboolean
+nmp_global_tracker_track(NMPGlobalTracker *self,
+                         NMPObjectType     obj_type,
+                         gconstpointer     obj,
+                         gint32            track_priority,
+                         gconstpointer     user_tag,
+                         gconstpointer     user_tag_untrack)
+{
+    NMPObject         obj_stack;
+    const NMPObject  *p_obj_stack;
+    TrackData        *track_data;
+    TrackObjData     *obj_data;
+    TrackUserTagData *user_tag_data;
+    gboolean          changed         = FALSE;
+    gboolean          changed_untrack = FALSE;
+    guint32           track_priority_val;
+    gboolean          track_priority_present;
+
+    g_return_val_if_fail(NMP_IS_GLOBAL_TRACKER(self), FALSE);
+    g_return_val_if_fail(obj, FALSE);
+    g_return_val_if_fail(user_tag, FALSE);
+
+    /* The route must not be tied to an interface. We can only handle here
+     * blackhole/unreachable/prohibit route types. */
+    g_return_val_if_fail(
+        NM_IN_SET(obj_type, NMP_OBJECT_TYPE_ROUTING_RULE, NMP_OBJECT_TYPE_MPTCP_ADDR)
+            || (NM_IN_SET(obj_type, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE)
+                && ((const NMPlatformIPRoute *) obj)->ifindex == 0),
+        FALSE);
+
+    /* only positive track priorities are implemented for MPTCP addrs. */
+    nm_assert(obj_type != NMP_OBJECT_TYPE_MPTCP_ADDR || track_priority > 0);
+
+    p_obj_stack = _obj_stackinit(&obj_stack, obj_type, obj);
+
+    if (track_priority >= 0) {
+        track_priority_val     = track_priority;
+        track_priority_present = TRUE;
+    } else {
+        track_priority_val     = -track_priority;
+        track_priority_present = FALSE;
+    }
+
+    track_data = _track_data_lookup(self->by_data, p_obj_stack, user_tag);
+
+    if (!track_data) {
+        track_data  = g_slice_new(TrackData);
+        *track_data = (TrackData){
+            .obj      = nm_dedup_multi_index_obj_intern(nm_platform_get_multi_idx(self->platform),
+                                                   p_obj_stack),
+            .user_tag = user_tag,
+            .track_priority_val     = track_priority_val,
+            .track_priority_present = track_priority_present,
+            .dirty                  = FALSE,
+        };
+        g_hash_table_add(self->by_data, track_data);
+
+        obj_data = g_hash_table_lookup(self->by_obj, &track_data->obj);
+        if (!obj_data) {
+            obj_data  = g_slice_new(TrackObjData);
+            *obj_data = (TrackObjData){
+                .obj          = nmp_object_ref(track_data->obj),
+                .obj_lst_head = C_LIST_INIT(obj_data->obj_lst_head),
+                .config_state = CONFIG_STATE_NONE,
+            };
+            g_hash_table_add(self->by_obj, obj_data);
+            c_list_link_tail(_by_obj_lst_head(self, obj_type), &obj_data->by_obj_lst);
+        }
+        c_list_link_tail(&obj_data->obj_lst_head, &track_data->obj_lst);
+
+        user_tag_data = g_hash_table_lookup(self->by_user_tag, &track_data->user_tag);
+        if (!user_tag_data) {
+            user_tag_data  = g_slice_new(TrackUserTagData);
+            *user_tag_data = (TrackUserTagData){
+                .user_tag          = user_tag,
+                .user_tag_lst_head = C_LIST_INIT(user_tag_data->user_tag_lst_head),
+            };
+            g_hash_table_add(self->by_user_tag, user_tag_data);
+        }
+        c_list_link_tail(&user_tag_data->user_tag_lst_head, &track_data->user_tag_lst);
+        changed = TRUE;
+    } else {
+        track_data->dirty = FALSE;
+        if (track_data->track_priority_val != track_priority_val
+            || track_data->track_priority_present != track_priority_present) {
+            track_data->track_priority_val     = track_priority_val;
+            track_data->track_priority_present = track_priority_present;
+            changed                            = TRUE;
+        }
+    }
+
+    if (user_tag_untrack) {
+        if (user_tag != user_tag_untrack) {
+            TrackData *track_data_untrack;
+
+            track_data_untrack = _track_data_lookup(self->by_data, p_obj_stack, user_tag_untrack);
+            if (track_data_untrack) {
+                _track_data_untrack(self, track_data_untrack, FALSE, TRUE);
+                changed_untrack = TRUE;
+            }
+        } else
+            nm_assert_not_reached();
+    }
+
+    _track_data_assert(track_data, TRUE);
+
+    if (changed) {
+        char sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+
+        _LOGD(
+            "track [" NM_HASH_OBFUSCATE_PTR_FMT ",%s%u] %s \"%s\"",
+            NM_HASH_OBFUSCATE_PTR(track_data->user_tag),
+            (track_data->track_priority_val == 0
+                 ? ""
+                 : (track_data->track_priority_present ? "+" : "-")),
+            (guint) track_data->track_priority_val,
+            NMP_OBJECT_GET_CLASS(track_data->obj)->obj_type_name,
+            nmp_object_to_string(track_data->obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
+    }
+
+    return changed || changed_untrack;
+}
+
+static void
+_track_data_untrack(NMPGlobalTracker *self,
+                    TrackData        *track_data,
+                    gboolean          remove_user_tag_data,
+                    gboolean          make_owned_by_us)
+{
+    char          sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+    TrackObjData *obj_data;
+
+    nm_assert(NMP_IS_GLOBAL_TRACKER(self));
+    _track_data_assert(track_data, TRUE);
+    nm_assert(self->by_data);
+    nm_assert(g_hash_table_lookup(self->by_data, track_data) == track_data);
+
+    _LOGD("untrack [" NM_HASH_OBFUSCATE_PTR_FMT "] %s \"%s\"",
+          NM_HASH_OBFUSCATE_PTR(track_data->user_tag),
+          NMP_OBJECT_GET_CLASS(track_data->obj)->obj_type_name,
+          nmp_object_to_string(track_data->obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
+
+#if NM_MORE_ASSERTS
+    {
+        TrackUserTagData *user_tag_data;
+
+        user_tag_data = g_hash_table_lookup(self->by_user_tag, &track_data->user_tag);
+        nm_assert(user_tag_data);
+        nm_assert(c_list_contains(&user_tag_data->user_tag_lst_head, &track_data->user_tag_lst));
+    }
+#endif
+
+    nm_assert(!c_list_is_empty(&track_data->user_tag_lst));
+
+    obj_data = g_hash_table_lookup(self->by_obj, &track_data->obj);
+    nm_assert(obj_data);
+    nm_assert(c_list_contains(&obj_data->obj_lst_head, &track_data->obj_lst));
+    nm_assert(obj_data == g_hash_table_lookup(self->by_obj, &track_data->obj));
+
+    if (make_owned_by_us) {
+        if (obj_data->config_state == CONFIG_STATE_NONE) {
+            /* we need to mark this entry that it requires a touch on the next
+             * sync. */
+            obj_data->config_state = CONFIG_STATE_OWNED_BY_US;
+        }
+    } else if (remove_user_tag_data && c_list_length_is(&track_data->user_tag_lst, 1))
+        g_hash_table_remove(self->by_user_tag, &track_data->user_tag);
+
+    /* if obj_data is marked to be "added_by_us" or "removed_by_us", we need to keep this entry
+     * around for the next sync -- so that we can undo what we did earlier. */
+    if (obj_data->config_state == CONFIG_STATE_NONE && c_list_length_is(&track_data->obj_lst, 1))
+        g_hash_table_remove(self->by_obj, &track_data->obj);
+
+    g_hash_table_remove(self->by_data, track_data);
+}
+
+gboolean
+nmp_global_tracker_untrack(NMPGlobalTracker *self,
+                           NMPObjectType     obj_type,
+                           gconstpointer     obj,
+                           gconstpointer     user_tag)
+{
+    NMPObject        obj_stack;
+    const NMPObject *p_obj_stack;
+    TrackData       *track_data;
+    gboolean         changed = FALSE;
+
+    g_return_val_if_fail(NMP_IS_GLOBAL_TRACKER(self), FALSE);
+    nm_assert(NM_IN_SET(obj_type,
+                        NMP_OBJECT_TYPE_IP4_ROUTE,
+                        NMP_OBJECT_TYPE_IP6_ROUTE,
+                        NMP_OBJECT_TYPE_ROUTING_RULE,
+                        NMP_OBJECT_TYPE_MPTCP_ADDR));
+    g_return_val_if_fail(obj, FALSE);
+    g_return_val_if_fail(user_tag, FALSE);
+
+    p_obj_stack = _obj_stackinit(&obj_stack, obj_type, obj);
+
+    track_data = _track_data_lookup(self->by_data, p_obj_stack, user_tag);
+    if (track_data) {
+        _track_data_untrack(self, track_data, TRUE, FALSE);
+        changed = TRUE;
+    }
+
+    return changed;
+}
+
+void
+nmp_global_tracker_set_dirty(NMPGlobalTracker *self, gconstpointer user_tag)
+{
+    TrackData        *track_data;
+    TrackUserTagData *user_tag_data;
+
+    g_return_if_fail(NMP_IS_GLOBAL_TRACKER(self));
+    g_return_if_fail(user_tag);
+
+    user_tag_data = g_hash_table_lookup(self->by_user_tag, &user_tag);
+    if (!user_tag_data)
+        return;
+
+    c_list_for_each_entry (track_data, &user_tag_data->user_tag_lst_head, user_tag_lst)
+        track_data->dirty = TRUE;
+}
+
+gboolean
+nmp_global_tracker_untrack_all(NMPGlobalTracker *self,
+                               gconstpointer     user_tag,
+                               gboolean          all /* or only dirty */,
+                               gboolean          make_survivors_dirty)
+{
+    TrackData        *track_data;
+    TrackData        *track_data_safe;
+    TrackUserTagData *user_tag_data;
+    gboolean          changed = FALSE;
+
+    g_return_val_if_fail(NMP_IS_GLOBAL_TRACKER(self), FALSE);
+    g_return_val_if_fail(user_tag, FALSE);
+
+    user_tag_data = g_hash_table_lookup(self->by_user_tag, &user_tag);
+    if (!user_tag_data)
+        return FALSE;
+
+    c_list_for_each_entry_safe (track_data,
+                                track_data_safe,
+                                &user_tag_data->user_tag_lst_head,
+                                user_tag_lst) {
+        if (all || track_data->dirty) {
+            _track_data_untrack(self, track_data, FALSE, FALSE);
+            changed = TRUE;
+            continue;
+        }
+        if (make_survivors_dirty)
+            track_data->dirty = TRUE;
+    }
+    if (c_list_is_empty(&user_tag_data->user_tag_lst_head))
+        g_hash_table_remove(self->by_user_tag, user_tag_data);
+
+    return changed;
+}
+
+/*****************************************************************************/
+
+/* Usually, we track NMPlatformMptcpAddr instances with an ifindex set.
+ * If we have *any* such instance, we know that the ifindex is fully
+ * synched (meaning, we will delete all unknown endpoints for that interface).
+ * However, if we don't have an endpoint on the interface, we may still
+ * want to track that a certain ifindex is fully managed.
+ *
+ * This initializes a dummy instance for exactly that purpose. */
+const NMPlatformMptcpAddr *
+nmp_global_tracker_mptcp_addr_init_for_ifindex(NMPlatformMptcpAddr *addr, int ifindex)
+{
+    nm_assert(addr);
+    nm_assert(ifindex > 0);
+
+    *addr = (NMPlatformMptcpAddr){
+        .ifindex     = ifindex,
+        .addr_family = AF_UNSPEC,
+    };
+
+    return addr;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    TrackObjData    *obj_data;
+    const TrackData *td_best;
+} MptcpSyncData;
+
+static int
+_mptcp_entries_cmp(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+    const MptcpSyncData *d_a = a;
+    const MptcpSyncData *d_b = b;
+
+    /* 1) prefer addresses based on the priority (highest priority
+     * sorted first). */
+    NM_CMP_FIELD(d_b->td_best, d_a->td_best, track_priority_val);
+
+    /* Finally, we only care about the order in which they were tracked.
+     * Rely on the stable sort to get that right. */
+    return 0;
+}
+
+void
+nmp_global_tracker_sync_mptcp_addrs(NMPGlobalTracker *self, gboolean reapply)
+{
+    char                           sbuf[64 + NM_UTILS_TO_STRING_BUFFER_SIZE];
+    gs_unref_ptrarray GPtrArray   *kaddrs_arr = NULL;
+    gs_unref_hashtable GHashTable *kaddrs_idx = NULL;
+    TrackObjData                  *obj_data;
+    TrackObjData                  *obj_data_safe;
+    CList                         *by_obj_lst_head;
+    const TrackData               *td_best;
+    gs_unref_hashtable GHashTable *handled_ifindexes    = NULL;
+    gs_unref_array GArray         *entries              = NULL;
+    gs_unref_hashtable GHashTable *entries_hash_by_addr = NULL;
+    gs_unref_hashtable GHashTable *entries_to_delete    = NULL;
+    guint                          i;
+    guint                          j;
+
+    g_return_if_fail(NMP_IS_GLOBAL_TRACKER(self));
+
+    _LOGD("sync mptcp-addr%s", reapply ? " (reapply)" : "");
+
+    /* Iterate over the tracked objects and construct @handled_ifindexes, @entries
+     * and @entries_to_delete.
+     * - @handled_ifindexes is a hash with all managed interfaces (their ifindex).
+     * - @entries are the MptcpSyncData instances for the tracked objects.
+     * - @entries_to_delete are the NMPObject which we added earlier, but now not
+     *     anymore (and which we shall delete). */
+    by_obj_lst_head = _by_obj_lst_head(self, NMP_OBJECT_TYPE_MPTCP_ADDR);
+    c_list_for_each_entry_safe (obj_data, obj_data_safe, by_obj_lst_head, by_obj_lst) {
+        const NMPlatformMptcpAddr *mptcp_addr = NMP_OBJECT_CAST_MPTCP_ADDR(obj_data->obj);
+        NMPlatformMptcpAddr        xtst;
+
+        nm_assert(mptcp_addr->port == 0);
+        nm_assert(mptcp_addr->ifindex > 0);
+        nm_assert(mptcp_addr->id == 0);
+        nm_assert_addr_family_or_unspec(mptcp_addr->addr_family);
+
+        /* AF_UNSPEC means this is the dummy object. We only care about it to make the
+         * ifindex as managed via @handled_ifindexes. */
+        nm_assert(
+            (mptcp_addr->addr_family == AF_UNSPEC)
+            == (nm_platform_mptcp_addr_cmp(
+                    mptcp_addr,
+                    nmp_global_tracker_mptcp_addr_init_for_ifindex(&xtst, mptcp_addr->ifindex))
+                == 0));
+
+        /* We need to know which ifindexes are managed/handled by us. Build an index
+         * for that. */
+        if (!handled_ifindexes)
+            handled_ifindexes = g_hash_table_new(nm_direct_hash, NULL);
+        g_hash_table_add(handled_ifindexes, GINT_TO_POINTER(mptcp_addr->ifindex));
+
+        td_best = _track_obj_data_get_best_data(obj_data);
+
+        if (!td_best) {
+            nm_assert(obj_data->config_state == CONFIG_STATE_ADDED_BY_US);
+
+            /* This entry is a tombstone, that tells us that added the object earlier.
+             * We can delete the MPTCP address (if it's still configured).
+             *
+             * Then we can drop the tombstone. */
+
+            if (mptcp_addr->addr_family != AF_UNSPEC) {
+                if (!reapply) {
+                    if (!entries_to_delete) {
+                        entries_to_delete = g_hash_table_new_full((GHashFunc) nmp_object_id_hash,
+                                                                  (GEqualFunc) nmp_object_id_equal,
+                                                                  (GDestroyNotify) nmp_object_unref,
+                                                                  NULL);
+                    }
+                    g_hash_table_add(entries_to_delete, (gpointer) nmp_object_ref(obj_data->obj));
+                }
+            }
+
+            /* We can forget about this entry now. */
+            g_hash_table_remove(self->by_obj, obj_data);
+            continue;
+        }
+
+        /* negative and zero track priorities are not implemented (and make no sense?). */
+        nm_assert(td_best->track_priority_val > 0);
+        nm_assert(td_best->track_priority_present);
+
+        if (mptcp_addr->addr_family == AF_UNSPEC) {
+            /* This is a nmp_global_tracker_mptcp_addr_init_for_ifindex() dummy entry.
+             * It only exists so we can add the @handled_ifindexes entry above
+             * and handle addresses on this interface. */
+            obj_data->config_state = CONFIG_STATE_ADDED_BY_US;
+            continue;
+        }
+
+        if (!entries)
+            entries = g_array_new(FALSE, FALSE, sizeof(MptcpSyncData));
+
+        g_array_append_val(entries,
+                           ((const MptcpSyncData){
+                               .obj_data = obj_data,
+                               .td_best  = td_best,
+                           }));
+    }
+    /* We collected all the entires we want to configure. Now, sort them by
+     * priority, and drop all the duplicates (preferring the entries that
+     * appear first, where first means "older"). In kernel, we can only configure an IP
+     * address (without port) as endpoint once. If two interfaces provide the same IP
+     * address, we can only configure one. We need to select one and filter out duplicates.
+     * While there is no solution, the idea is to select the preferred address
+     * somewhat consistently.
+     *
+     * Also, create a lookup index @entries_hash_by_addr to lookup by address. */
+    if (entries) {
+        /* First we sort the entries by priority, to prefer the ones with higher
+         * priority. In case of equal priority, we rely on the stable sort to
+         * preserve the order in which things got tracked. */
+        g_array_sort_with_data(entries, _mptcp_entries_cmp, NULL);
+
+        entries_hash_by_addr = g_hash_table_new(nm_platform_mptcp_addr_index_addr_cmp,
+                                                nm_platform_mptcp_addr_index_addr_equal);
+
+        /* Now, drop all duplicates addresses. Only keep the first one. */
+        for (i = 0, j = 0; i < entries->len; i++) {
+            const MptcpSyncData       *d          = nm_g_array_index_p(entries, MptcpSyncData, i);
+            const NMPlatformMptcpAddr *mptcp_addr = NMP_OBJECT_CAST_MPTCP_ADDR(d->obj_data->obj);
+
+            obj_data = g_hash_table_lookup(entries_hash_by_addr, (gpointer) mptcp_addr);
+            if (obj_data) {
+                /* This object is shadowed. We ignore it.
+                 *
+                 * However, we first propagate the config_state. For MPTCP addrs, it can only be
+                 * NONE or ADDED_BY_US. */
+                nm_assert(NM_IN_SET(d->obj_data->config_state,
+                                    CONFIG_STATE_NONE,
+                                    CONFIG_STATE_ADDED_BY_US));
+                nm_assert(
+                    NM_IN_SET(obj_data->config_state, CONFIG_STATE_NONE, CONFIG_STATE_ADDED_BY_US));
+
+                if (d->obj_data->config_state == CONFIG_STATE_ADDED_BY_US) {
+                    obj_data->config_state    = CONFIG_STATE_ADDED_BY_US;
+                    d->obj_data->config_state = CONFIG_STATE_NONE;
+                }
+                continue;
+            }
+
+            if (!g_hash_table_insert(entries_hash_by_addr, (gpointer) mptcp_addr, d->obj_data))
+                nm_assert_not_reached();
+
+            if (i != j)
+                *(nm_g_array_index_p(entries, MptcpSyncData, j)) = *d;
+            j++;
+
+            if (j >= MPTCP_PM_ADDR_MAX) {
+                /* Kernel limits the number of addresses we can configure.
+                 * It's hard-coded here, taken from current kernel. Hopefully
+                 * it matches the running kernel.
+                 *
+                 * It's worse. There might be other addresses already configured
+                 * on other interfaces (or with a port). Our sync method will leave
+                 * them alone, as they were not added by us. So the actual limit
+                 * is possibly smaller, and kernel fails with EINVAL.
+                 *
+                 * Still, we definitely need to truncate the list here. Imagine
+                 * during an earlier sync we added MAX addresses on one interface.
+                 * Now, another interface activates, and wants to configure one
+                 * address. That address will get a higher priority (chosen by NML3Cfg),
+                 * so that part is good. However, it means we must drop the last from
+                 * the other MAX addresses. We achieve that by truncating the list
+                 * to MPTCP_PM_ADDR_MAX.
+                 */
+                break;
+            }
+        }
+        g_array_set_size(entries, j);
+    }
+
+    /* Get the list of currently (in kernel) configured MPTCP endpoints. */
+    kaddrs_arr = nm_platform_mptcp_addrs_dump(self->platform);
+
+    /* First, delete all kaddrs which we no longer want... */
+    if (kaddrs_arr) {
+        for (i = 0; i < kaddrs_arr->len; i++) {
+            const NMPObject           *obj              = kaddrs_arr->pdata[i];
+            const NMPlatformMptcpAddr *mptcp_addr       = NMP_OBJECT_CAST_MPTCP_ADDR(obj);
+            gboolean                   add_to_kaddr_idx = FALSE;
+
+            if (mptcp_addr->port != 0 || mptcp_addr->ifindex <= 0) {
+                /* We ignore all endpoints that have a port or no ifindex.
+                 * Those were never created by us, let the user who created
+                 * them handle them. */
+                goto keep_and_next;
+            }
+
+            if (!nm_g_hash_table_contains(handled_ifindexes,
+                                          GINT_TO_POINTER(mptcp_addr->ifindex))) {
+                /* This endpoint is on an interface we don't manage. Ignore (and keep) it. */
+                goto keep_and_next;
+            }
+
+            /* We have the object in the delete-list. However, we might still also want
+             * to add it back. Check for that too. */
+            obj_data = nm_g_hash_table_lookup(entries_hash_by_addr, mptcp_addr);
+            if (obj_data) {
+                const NMPlatformMptcpAddr *mptcp_addr2 = NMP_OBJECT_CAST_MPTCP_ADDR(obj_data->obj);
+
+                if (mptcp_addr->flags == mptcp_addr2->flags
+                    && mptcp_addr->ifindex == mptcp_addr2->ifindex) {
+                    /* We want to add this address and it's already configured. Keep it
+                     * and remember that we already have it. */
+                    add_to_kaddr_idx = TRUE;
+                    goto keep_and_next;
+                }
+
+                /* We want to configure a similar address mptcp_addr2) as the one that is already configured
+                 * (mptcp_addr). However, the ifindex or flag differs. Delete this one to add the
+                 * right one blow. */
+            } else {
+                /* We don't want to configure this address (anymore). */
+                if (reapply) {
+                    /* in reapply mode, we delete the extra address. */
+                } else {
+                    /* Otherwise, we only delete it, if we remember that we added this one
+                     * before. */
+                    if (!nm_g_hash_table_contains(entries_to_delete, obj)) {
+                        /* This address was not added by us. Keep it. */
+                        goto keep_and_next;
+                    }
+                }
+            }
+
+            if (!nm_platform_object_delete(self->platform, obj)) {
+                /* We failed to delete it. It's unclear what is the matter with this
+                 * object. Ignore the failure. */
+            }
+
+            continue;
+
+keep_and_next:
+            _LOGt("keep: %s \"%s\"%s",
+                  NMP_OBJECT_GET_CLASS(obj)->obj_type_name,
+                  nmp_object_to_string(obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)),
+                  add_to_kaddr_idx ? " (index)" : "");
+            if (add_to_kaddr_idx) {
+                if (!kaddrs_idx) {
+                    kaddrs_idx = g_hash_table_new((GHashFunc) nmp_object_id_hash,
+                                                  (GEqualFunc) nmp_object_id_equal);
+                }
+                g_hash_table_add(kaddrs_idx, (gpointer) obj);
+            }
+        }
+    }
+
+    if (entries) {
+        for (i = 0; i < entries->len; i++) {
+            const MptcpSyncData       *d          = nm_g_array_index_p(entries, MptcpSyncData, i);
+            const NMPlatformMptcpAddr *mptcp_addr = NMP_OBJECT_CAST_MPTCP_ADDR(d->obj_data->obj);
+            const NMPObject           *kobj;
+
+            nm_assert(mptcp_addr->port == 0);
+            nm_assert(mptcp_addr->ifindex > 0);
+
+            d->obj_data->config_state = CONFIG_STATE_ADDED_BY_US;
+
+            kobj = nm_g_hash_table_lookup(kaddrs_idx, d->obj_data->obj);
+            if (kobj && kobj->mptcp_addr.flags == mptcp_addr->flags) {
+                /* This address is already added with the right flags. We can
+                 * skip it. */
+                continue;
+            }
+
+            /* Kernel actually only allows us to add a small number of addresses.
+             * Also, if we have a conflicting address on another interface, the
+             * request will be rejected.
+             *
+             * Don't try to handle that. Just attempt to add the address, and if
+             * we fail, there is nothing we can do about it. */
+            nm_platform_mptcp_addr_update(self->platform, TRUE, mptcp_addr);
+        }
+    }
+}
+
+void
+nmp_global_tracker_sync(NMPGlobalTracker *self, NMPObjectType obj_type, gboolean keep_deleted)
+{
+    char                         sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+    const NMDedupMultiHeadEntry *pl_head_entry;
+    const NMPObject             *plobj;
+    gs_unref_ptrarray GPtrArray *objs_to_delete = NULL;
+    TrackObjData                *obj_data;
+    TrackObjData                *obj_data_safe;
+    CList                       *by_obj_lst_head;
+    guint                        i;
+    const TrackData             *td_best;
+
+    g_return_if_fail(NMP_IS_GLOBAL_TRACKER(self));
+    g_return_if_fail(NM_IN_SET(obj_type,
+                               NMP_OBJECT_TYPE_IP4_ROUTE,
+                               NMP_OBJECT_TYPE_IP6_ROUTE,
+                               NMP_OBJECT_TYPE_ROUTING_RULE));
+
+    _LOGD("sync %s%s",
+          nmp_class_from_type(obj_type)->obj_type_name,
+          keep_deleted ? " (don't remove any)" : "");
+
+    if (obj_type == NMP_OBJECT_TYPE_ROUTING_RULE)
+        pl_head_entry = nm_platform_lookup_obj_type(self->platform, obj_type);
+    else
+        pl_head_entry = nm_platform_lookup_object(self->platform, obj_type, 0);
+
+    if (pl_head_entry) {
+        NMDedupMultiIter pl_iter;
+
+        nmp_cache_iter_for_each (&pl_iter, pl_head_entry, &plobj) {
+            obj_data = g_hash_table_lookup(self->by_obj, &plobj);
+
+            if (!obj_data) {
+                /* this obj is not tracked. It was externally added, hence we
+                 * ignore it. */
+                continue;
+            }
+
+            td_best = _track_obj_data_get_best_data(obj_data);
+            if (td_best) {
+                if (td_best->track_priority_present) {
+                    if (obj_data->config_state == CONFIG_STATE_OWNED_BY_US)
+                        obj_data->config_state = CONFIG_STATE_ADDED_BY_US;
+                    continue;
+                }
+                if (td_best->track_priority_val == 0) {
+                    if (!NM_IN_SET(obj_data->config_state,
+                                   CONFIG_STATE_ADDED_BY_US,
+                                   CONFIG_STATE_OWNED_BY_US)) {
+                        obj_data->config_state = CONFIG_STATE_NONE;
+                        continue;
+                    }
+                    obj_data->config_state = CONFIG_STATE_NONE;
+                }
+            }
+
+            if (keep_deleted) {
+                _LOGD("forget/leak object added by us: %s \"%s\"",
+                      NMP_OBJECT_GET_CLASS(plobj)->obj_type_name,
+                      nmp_object_to_string(plobj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
+                continue;
+            }
+
+            if (!objs_to_delete)
+                objs_to_delete = g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
+
+            g_ptr_array_add(objs_to_delete, (gpointer) nmp_object_ref(plobj));
+
+            obj_data->config_state = CONFIG_STATE_REMOVED_BY_US;
+        }
+    }
+
+    if (objs_to_delete) {
+        for (i = 0; i < objs_to_delete->len; i++)
+            nm_platform_object_delete(self->platform, objs_to_delete->pdata[i]);
+    }
+
+    by_obj_lst_head = _by_obj_lst_head(self, obj_type);
+
+    c_list_for_each_entry_safe (obj_data, obj_data_safe, by_obj_lst_head, by_obj_lst) {
+        nm_assert(NMP_OBJECT_GET_TYPE(obj_data->obj) == obj_type);
+
+        td_best = _track_obj_data_get_best_data(obj_data);
+
+        if (!td_best) {
+            g_hash_table_remove(self->by_obj, obj_data);
+            continue;
+        }
+
+        if (!td_best->track_priority_present) {
+            if (obj_data->config_state == CONFIG_STATE_OWNED_BY_US)
+                obj_data->config_state = CONFIG_STATE_REMOVED_BY_US;
+            continue;
+        }
+        if (td_best->track_priority_val == 0) {
+            if (!NM_IN_SET(obj_data->config_state,
+                           CONFIG_STATE_REMOVED_BY_US,
+                           CONFIG_STATE_OWNED_BY_US)) {
+                obj_data->config_state = CONFIG_STATE_NONE;
+                continue;
+            }
+            obj_data->config_state = CONFIG_STATE_NONE;
+        }
+
+        plobj =
+            nm_platform_lookup_obj(self->platform, NMP_CACHE_ID_TYPE_OBJECT_TYPE, obj_data->obj);
+        if (plobj) {
+            int c;
+
+            switch (obj_type) {
+            case NMP_OBJECT_TYPE_ROUTING_RULE:
+                c = nm_platform_routing_rule_cmp(NMP_OBJECT_CAST_ROUTING_RULE(obj_data->obj),
+                                                 NMP_OBJECT_CAST_ROUTING_RULE(plobj),
+                                                 NM_PLATFORM_ROUTING_RULE_CMP_TYPE_SEMANTICALLY);
+                break;
+            case NMP_OBJECT_TYPE_IP4_ROUTE:
+                c = nm_platform_ip4_route_cmp(NMP_OBJECT_CAST_IP4_ROUTE(obj_data->obj),
+                                              NMP_OBJECT_CAST_IP4_ROUTE(plobj),
+                                              NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY);
+                break;
+            case NMP_OBJECT_TYPE_IP6_ROUTE:
+                c = nm_platform_ip6_route_cmp(NMP_OBJECT_CAST_IP6_ROUTE(obj_data->obj),
+                                              NMP_OBJECT_CAST_IP6_ROUTE(plobj),
+                                              NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY);
+                break;
+            default:
+                c = nm_assert_unreachable_val(0);
+                break;
+            }
+            if (c == 0)
+                continue;
+            nm_platform_object_delete(self->platform, plobj);
+        }
+
+        obj_data->config_state = CONFIG_STATE_ADDED_BY_US;
+
+        if (obj_type == NMP_OBJECT_TYPE_ROUTING_RULE) {
+            nm_platform_routing_rule_add(self->platform,
+                                         NMP_NLM_FLAG_ADD,
+                                         NMP_OBJECT_CAST_ROUTING_RULE(obj_data->obj));
+        } else
+            nm_platform_ip_route_add(self->platform, NMP_NLM_FLAG_APPEND, obj_data->obj);
+    }
+}
+
+/*****************************************************************************/
+
+void
+nmp_global_tracker_track_rule_from_platform(NMPGlobalTracker *self,
+                                            NMPlatform       *platform,
+                                            int               addr_family,
+                                            gint32            tracking_priority,
+                                            gconstpointer     user_tag)
+{
+    NMPLookup                    lookup;
+    const NMDedupMultiHeadEntry *head_entry;
+    NMDedupMultiIter             iter;
+    const NMPObject             *o;
+
+    g_return_if_fail(NMP_IS_GLOBAL_TRACKER(self));
+
+    if (!platform)
+        platform = self->platform;
+    else
+        g_return_if_fail(NM_IS_PLATFORM(platform));
+
+    nm_assert(NM_IN_SET(addr_family, AF_UNSPEC, AF_INET, AF_INET6));
+
+    nmp_lookup_init_obj_type(&lookup, NMP_OBJECT_TYPE_ROUTING_RULE);
+    head_entry = nm_platform_lookup(platform, &lookup);
+    nmp_cache_iter_for_each (&iter, head_entry, &o) {
+        const NMPlatformRoutingRule *rr = NMP_OBJECT_CAST_ROUTING_RULE(o);
+
+        if (addr_family != AF_UNSPEC && rr->addr_family != addr_family)
+            continue;
+
+        nmp_global_tracker_track_rule(self, rr, tracking_priority, user_tag, NULL);
+    }
+}
+
+/*****************************************************************************/
+
+void
+nmp_global_tracker_track_rule_default(NMPGlobalTracker *self,
+                                      int               addr_family,
+                                      gint32            track_priority,
+                                      gconstpointer     user_tag)
+{
+    g_return_if_fail(NMP_IS_GLOBAL_TRACKER(self));
+
+    nm_assert(NM_IN_SET(addr_family, AF_UNSPEC, AF_INET, AF_INET6));
+
+    /* track the default rules. See also `man ip-rule`. */
+
+    if (NM_IN_SET(addr_family, AF_UNSPEC, AF_INET)) {
+        nmp_global_tracker_track_rule(self,
+                                      &((NMPlatformRoutingRule){
+                                          .addr_family = AF_INET,
+                                          .priority    = 0,
+                                          .table       = RT_TABLE_LOCAL,
+                                          .action      = FR_ACT_TO_TBL,
+                                          .protocol    = RTPROT_KERNEL,
+                                      }),
+                                      track_priority,
+                                      user_tag,
+                                      NULL);
+        nmp_global_tracker_track_rule(self,
+                                      &((NMPlatformRoutingRule){
+                                          .addr_family = AF_INET,
+                                          .priority    = 32766,
+                                          .table       = RT_TABLE_MAIN,
+                                          .action      = FR_ACT_TO_TBL,
+                                          .protocol    = RTPROT_KERNEL,
+                                      }),
+                                      track_priority,
+                                      user_tag,
+                                      NULL);
+        nmp_global_tracker_track_rule(self,
+                                      &((NMPlatformRoutingRule){
+                                          .addr_family = AF_INET,
+                                          .priority    = 32767,
+                                          .table       = RT_TABLE_DEFAULT,
+                                          .action      = FR_ACT_TO_TBL,
+                                          .protocol    = RTPROT_KERNEL,
+                                      }),
+                                      track_priority,
+                                      user_tag,
+                                      NULL);
+    }
+    if (NM_IN_SET(addr_family, AF_UNSPEC, AF_INET6)) {
+        nmp_global_tracker_track_rule(self,
+                                      &((NMPlatformRoutingRule){
+                                          .addr_family = AF_INET6,
+                                          .priority    = 0,
+                                          .table       = RT_TABLE_LOCAL,
+                                          .action      = FR_ACT_TO_TBL,
+                                          .protocol    = RTPROT_KERNEL,
+                                      }),
+                                      track_priority,
+                                      user_tag,
+                                      NULL);
+        nmp_global_tracker_track_rule(self,
+                                      &((NMPlatformRoutingRule){
+                                          .addr_family = AF_INET6,
+                                          .priority    = 32766,
+                                          .table       = RT_TABLE_MAIN,
+                                          .action      = FR_ACT_TO_TBL,
+                                          .protocol    = RTPROT_KERNEL,
+                                      }),
+                                      track_priority,
+                                      user_tag,
+                                      NULL);
+    }
+}
+
+/*****************************************************************************/
+
+NMPGlobalTracker *
+nmp_global_tracker_new(NMPlatform *platform)
+{
+    NMPGlobalTracker *self;
+
+    g_return_val_if_fail(NM_IS_PLATFORM(platform), NULL);
+
+    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(TrackUserTagData, user_tag) == 0);
+
+    self  = g_slice_new(NMPGlobalTracker);
+    *self = (NMPGlobalTracker){
+        .ref_count = 1,
+        .platform  = g_object_ref(platform),
+        .by_data =
+            g_hash_table_new_full(_track_data_hash, _track_data_equal, NULL, _track_data_destroy),
+        .by_obj              = g_hash_table_new_full(_track_obj_data_hash,
+                                        _track_obj_data_equal,
+                                        NULL,
+                                        _track_obj_data_destroy),
+        .by_user_tag         = g_hash_table_new_full(nm_pdirect_hash,
+                                             nm_pdirect_equal,
+                                             NULL,
+                                             _track_user_tag_data_destroy),
+        .by_obj_lst_heads[0] = C_LIST_INIT(self->by_obj_lst_heads[0]),
+        .by_obj_lst_heads[1] = C_LIST_INIT(self->by_obj_lst_heads[1]),
+        .by_obj_lst_heads[2] = C_LIST_INIT(self->by_obj_lst_heads[2]),
+        .by_obj_lst_heads[3] = C_LIST_INIT(self->by_obj_lst_heads[3]),
+    };
+    return self;
+}
+
+NMPGlobalTracker *
+nmp_global_tracker_ref(NMPGlobalTracker *self)
+{
+    g_return_val_if_fail(NMP_IS_GLOBAL_TRACKER(self), NULL);
+
+    self->ref_count++;
+    return self;
+}
+
+void
+nmp_global_tracker_unref(NMPGlobalTracker *self)
+{
+    g_return_if_fail(NMP_IS_GLOBAL_TRACKER(self));
+
+    if (--self->ref_count > 0)
+        return;
+
+    g_hash_table_destroy(self->by_user_tag);
+    g_hash_table_destroy(self->by_obj);
+    g_hash_table_destroy(self->by_data);
+    nm_assert(c_list_is_empty(&self->by_obj_lst_heads[0]));
+    nm_assert(c_list_is_empty(&self->by_obj_lst_heads[1]));
+    nm_assert(c_list_is_empty(&self->by_obj_lst_heads[2]));
+    nm_assert(c_list_is_empty(&self->by_obj_lst_heads[3]));
+    g_object_unref(self->platform);
+    nm_g_slice_free(self);
+}
diff --git a/src/libnm-platform/nmp-global-tracker.h b/src/libnm-platform/nmp-global-tracker.h
new file mode 100644
index 00000000..08a4d85f
--- /dev/null
+++ b/src/libnm-platform/nmp-global-tracker.h
@@ -0,0 +1,84 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NMP_GLOBAL_TRACKER_H__
+#define __NMP_GLOBAL_TRACKER_H__
+
+#include "nm-platform.h"
+
+/*****************************************************************************/
+
+#define NMP_GLOBAL_TRACKER_EXTERN_WEAKLY_TRACKED_USER_TAG ((const void *) nmp_global_tracker_new)
+
+typedef struct _NMPGlobalTracker NMPGlobalTracker;
+
+NMPGlobalTracker *nmp_global_tracker_new(NMPlatform *platform);
+
+NMPGlobalTracker *nmp_global_tracker_ref(NMPGlobalTracker *self);
+void              nmp_global_tracker_unref(NMPGlobalTracker *self);
+
+#define nm_auto_unref_global_tracker nm_auto(_nmp_global_tracker_unref)
+NM_AUTO_DEFINE_FCN0(NMPGlobalTracker *, _nmp_global_tracker_unref, nmp_global_tracker_unref);
+
+gboolean nmp_global_tracker_track(NMPGlobalTracker *self,
+                                  NMPObjectType     obj_type,
+                                  gconstpointer     obj,
+                                  gint32            track_priority,
+                                  gconstpointer     user_tag,
+                                  gconstpointer     user_tag_untrack);
+
+static inline gboolean
+nmp_global_tracker_track_rule(NMPGlobalTracker            *self,
+                              const NMPlatformRoutingRule *routing_rule,
+                              gint32                       track_priority,
+                              gconstpointer                user_tag,
+                              gconstpointer                user_tag_untrack)
+{
+    return nmp_global_tracker_track(self,
+                                    NMP_OBJECT_TYPE_ROUTING_RULE,
+                                    routing_rule,
+                                    track_priority,
+                                    user_tag,
+                                    user_tag_untrack);
+}
+
+void nmp_global_tracker_track_rule_default(NMPGlobalTracker *self,
+                                           int               addr_family,
+                                           gint32            track_priority,
+                                           gconstpointer     user_tag);
+
+void nmp_global_tracker_track_rule_from_platform(NMPGlobalTracker *self,
+                                                 NMPlatform       *platform,
+                                                 int               addr_family,
+                                                 gint32            tracking_priority,
+                                                 gconstpointer     user_tag);
+
+gboolean nmp_global_tracker_untrack(NMPGlobalTracker *self,
+                                    NMPObjectType     obj_type,
+                                    gconstpointer     obj,
+                                    gconstpointer     user_tag);
+
+static inline gboolean
+nmp_global_tracker_untrack_rule(NMPGlobalTracker            *self,
+                                const NMPlatformRoutingRule *routing_rule,
+                                gconstpointer                user_tag)
+{
+    return nmp_global_tracker_untrack(self, NMP_OBJECT_TYPE_ROUTING_RULE, routing_rule, user_tag);
+}
+
+void nmp_global_tracker_set_dirty(NMPGlobalTracker *self, gconstpointer user_tag);
+
+gboolean nmp_global_tracker_untrack_all(NMPGlobalTracker *self,
+                                        gconstpointer     user_tag,
+                                        gboolean          all /* or only dirty */,
+                                        gboolean          make_survivors_dirty);
+
+void nmp_global_tracker_sync(NMPGlobalTracker *self, NMPObjectType obj_type, gboolean keep_deleted);
+
+void nmp_global_tracker_sync_mptcp_addrs(NMPGlobalTracker *self, gboolean reapply);
+
+/*****************************************************************************/
+
+const NMPlatformMptcpAddr *nmp_global_tracker_mptcp_addr_init_for_ifindex(NMPlatformMptcpAddr *addr,
+                                                                          int ifindex);
+
+#endif /* __NMP_GLOBAL_TRACKER_H__ */
diff --git a/src/libnm-platform/nmp-object.c b/src/libnm-platform/nmp-object.c
index 6c177b4c..d06aa9cd 100644
--- a/src/libnm-platform/nmp-object.c
+++ b/src/libnm-platform/nmp-object.c
@@ -20,25 +20,26 @@
 /*****************************************************************************/
 
 #define _NMLOG_DOMAIN LOGD_PLATFORM
-#define _NMLOG(level, obj, ...)                                               \
-    G_STMT_START                                                              \
-    {                                                                         \
-        const NMLogLevel __level = (level);                                   \
-                                                                              \
-        if (nm_logging_enabled(__level, _NMLOG_DOMAIN)) {                     \
-            const NMPObject *const __obj = (obj);                             \
-                                                                              \
-            _nm_log(__level,                                                  \
-                    _NMLOG_DOMAIN,                                            \
-                    0,                                                        \
-                    NULL,                                                     \
-                    NULL,                                                     \
-                    "nmp-object[%p/%s]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
-                    __obj,                                                    \
-                    (__obj ? NMP_OBJECT_GET_CLASS(__obj)->obj_type_name       \
-                           : "???") _NM_UTILS_MACRO_REST(__VA_ARGS__));       \
-        }                                                                     \
-    }                                                                         \
+#define _NMLOG(level, obj, ...)                                         \
+    G_STMT_START                                                        \
+    {                                                                   \
+        const NMLogLevel __level = (level);                             \
+                                                                        \
+        if (nm_logging_enabled(__level, _NMLOG_DOMAIN)) {               \
+            const NMPObject *const __obj = (obj);                       \
+                                                                        \
+            _nm_log(__level,                                            \
+                    _NMLOG_DOMAIN,                                      \
+                    0,                                                  \
+                    NULL,                                               \
+                    NULL,                                               \
+                    "nmp-object[" NM_HASH_OBFUSCATE_PTR_FMT ""          \
+                    "/%s]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),        \
+                    NM_HASH_OBFUSCATE_PTR(__obj),                       \
+                    (__obj ? NMP_OBJECT_GET_CLASS(__obj)->obj_type_name \
+                           : "???") _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+        }                                                               \
+    }                                                                   \
     G_STMT_END
 
 /*****************************************************************************/
@@ -380,7 +381,8 @@ _idx_obj_part(const DedupMultiIdxType *idx_type,
                        NMP_OBJECT_TYPE_IP4_ROUTE,
                        NMP_OBJECT_TYPE_IP6_ROUTE,
                        NMP_OBJECT_TYPE_QDISC,
-                       NMP_OBJECT_TYPE_TFILTER)
+                       NMP_OBJECT_TYPE_TFILTER,
+                       NMP_OBJECT_TYPE_MPTCP_ADDR)
             || !nmp_object_is_visible(obj_a)) {
             if (h)
                 nm_hash_update_val(h, obj_a);
@@ -805,13 +807,9 @@ _nmp_object_stackinit_from_class(NMPObject *obj, const NMPClass *klass)
     nm_assert(obj);
     nm_assert(klass);
 
-    *obj = (NMPObject){
-        .parent =
-            {
-                .klass      = (const NMDedupMultiObjClass *) klass,
-                ._ref_count = NM_OBJ_REF_COUNT_STACKINIT,
-            },
-    };
+    memset(obj, 0, sizeof(NMPObject));
+    obj->_class            = klass;
+    obj->parent._ref_count = NM_OBJ_REF_COUNT_STACKINIT;
     return obj;
 }
 
@@ -844,6 +842,17 @@ nmp_object_stackinit_id(NMPObject *obj, const NMPObject *src)
     _nmp_object_stackinit_from_class(obj, klass);
     if (klass->cmd_plobj_id_copy)
         klass->cmd_plobj_id_copy(&obj->object, &src->object);
+    else {
+        /* This object must not implement cmd_obj_copy().
+         * If it would, it would mean that we require a deep copy
+         * of the data. As @obj is stack-allocated, it cannot track
+         * ownership. The caller must not use nmp_object_stackinit_id()
+         * with an object of such a type. */
+        nm_assert(!klass->cmd_obj_copy);
+
+        /* plain memcpy of the public part suffices. */
+        memcpy(&obj->object, &src->object, klass->sizeof_data);
+    }
     return obj;
 }
 
@@ -904,7 +913,7 @@ nmp_object_to_string(const NMPObject      *obj,
     switch (to_string_mode) {
     case NMP_OBJECT_TO_STRING_ID:
         if (!klass->cmd_plobj_to_string_id) {
-            g_snprintf(buf, buf_size, "%p", obj);
+            g_snprintf(buf, buf_size, NM_HASH_OBFUSCATE_PTR_FMT, NM_HASH_OBFUSCATE_PTR(obj));
             return buf;
         }
         return klass->cmd_plobj_to_string_id(&obj->object, buf, buf_size);
@@ -912,9 +921,9 @@ nmp_object_to_string(const NMPObject      *obj,
         g_snprintf(
             buf,
             buf_size,
-            "[%s,%p,%u,%calive,%cvisible; %s]",
+            "[%s," NM_HASH_OBFUSCATE_PTR_FMT ",%u,%calive,%cvisible; %s]",
             klass->obj_type_name,
-            obj,
+            NM_HASH_OBFUSCATE_PTR(obj),
             obj->parent._ref_count,
             nmp_object_is_alive(obj) ? '+' : '-',
             nmp_object_is_visible(obj) ? '+' : '-',
@@ -943,14 +952,15 @@ _vt_cmd_obj_to_string_link(const NMPObject      *obj,
     case NMP_OBJECT_TO_STRING_ALL:
         nm_strbuf_append(&b,
                          &buf_size,
-                         "[%s,%p,%u,%calive,%cvisible,%cin-nl,%p; ",
+                         "[%s," NM_HASH_OBFUSCATE_PTR_FMT
+                         ",%u,%calive,%cvisible,%cin-nl," NM_HASH_OBFUSCATE_PTR_FMT "; ",
                          klass->obj_type_name,
-                         obj,
+                         NM_HASH_OBFUSCATE_PTR(obj),
                          obj->parent._ref_count,
                          nmp_object_is_alive(obj) ? '+' : '-',
                          nmp_object_is_visible(obj) ? '+' : '-',
                          obj->_link.netlink.is_in_netlink ? '+' : '-',
-                         obj->_link.udev.device);
+                         NM_HASH_OBFUSCATE_PTR(obj->_link.udev.device));
         NMP_OBJECT_GET_CLASS(obj)->cmd_plobj_to_string(&obj->object, b, buf_size);
         nm_strbuf_seek_end(&b, &buf_size);
         if (obj->_link.netlink.lnk) {
@@ -988,15 +998,15 @@ _vt_cmd_obj_to_string_lnk_vlan(const NMPObject      *obj,
 
     switch (to_string_mode) {
     case NMP_OBJECT_TO_STRING_ID:
-        g_snprintf(buf, buf_size, "%p", obj);
+        g_snprintf(buf, buf_size, NM_HASH_OBFUSCATE_PTR_FMT, NM_HASH_OBFUSCATE_PTR(obj));
         return buf;
     case NMP_OBJECT_TO_STRING_ALL:
 
         g_snprintf(buf,
                    buf_size,
-                   "[%s,%p,%u,%calive,%cvisible; %s]",
+                   "[%s," NM_HASH_OBFUSCATE_PTR_FMT ",%u,%calive,%cvisible; %s]",
                    klass->obj_type_name,
-                   obj,
+                   NM_HASH_OBFUSCATE_PTR(obj),
                    obj->parent._ref_count,
                    nmp_object_is_alive(obj) ? '+' : '-',
                    nmp_object_is_visible(obj) ? '+' : '-',
@@ -1052,17 +1062,17 @@ _vt_cmd_obj_to_string_lnk_wireguard(const NMPObject      *obj,
 
     switch (to_string_mode) {
     case NMP_OBJECT_TO_STRING_ID:
-        g_snprintf(buf, buf_size, "%p", obj);
+        g_snprintf(buf, buf_size, NM_HASH_OBFUSCATE_PTR_FMT, NM_HASH_OBFUSCATE_PTR(obj));
         return buf;
     case NMP_OBJECT_TO_STRING_ALL:
         b = buf;
 
         nm_strbuf_append(&b,
                          &buf_size,
-                         "[%s,%p,%u,%calive,%cvisible; %s"
+                         "[%s," NM_HASH_OBFUSCATE_PTR_FMT ",%u,%calive,%cvisible; %s"
                          "%s",
                          klass->obj_type_name,
-                         obj,
+                         NM_HASH_OBFUSCATE_PTR(obj),
                          obj->parent._ref_count,
                          nmp_object_is_alive(obj) ? '+' : '-',
                          nmp_object_is_visible(obj) ? '+' : '-',
@@ -1153,10 +1163,7 @@ _vt_cmd_obj_hash_update_link(const NMPObject *obj, NMHashState *h)
     nm_assert(NMP_OBJECT_GET_TYPE(obj) == NMP_OBJECT_TYPE_LINK);
 
     nm_platform_link_hash_update(&obj->link, h);
-    nm_hash_update_vals(h,
-                        obj->_link.netlink.is_in_netlink,
-                        obj->_link.wireguard_family_id,
-                        obj->_link.udev.device);
+    nm_hash_update_vals(h, obj->_link.netlink.is_in_netlink, obj->_link.udev.device);
     if (obj->_link.netlink.lnk)
         nmp_object_hash_update(obj->_link.netlink.lnk, h);
 }
@@ -1236,7 +1243,6 @@ _vt_cmd_obj_cmp_link(const NMPObject *obj1, const NMPObject *obj2)
     NM_CMP_RETURN(nm_platform_link_cmp(&obj1->link, &obj2->link));
     NM_CMP_DIRECT(obj1->_link.netlink.is_in_netlink, obj2->_link.netlink.is_in_netlink);
     NM_CMP_RETURN(nmp_object_cmp(obj1->_link.netlink.lnk, obj2->_link.netlink.lnk));
-    NM_CMP_DIRECT(obj1->_link.wireguard_family_id, obj2->_link.wireguard_family_id);
 
     if (obj1->_link.udev.device != obj2->_link.udev.device) {
         if (!obj1->_link.udev.device)
@@ -1428,21 +1434,6 @@ _vt_cmd_plobj_id_copy(ip6_address, NMPlatformIP6Address, {
     dst->address = src->address;
 });
 
-_vt_cmd_plobj_id_copy(ip4_route, NMPlatformIP4Route, {
-    *dst = *src;
-    nm_assert(nm_platform_ip4_route_cmp(dst, src, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) == 0);
-});
-
-_vt_cmd_plobj_id_copy(ip6_route, NMPlatformIP6Route, {
-    *dst = *src;
-    nm_assert(nm_platform_ip6_route_cmp(dst, src, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) == 0);
-});
-
-_vt_cmd_plobj_id_copy(routing_rule, NMPlatformRoutingRule, {
-    *dst = *src;
-    nm_assert(nm_platform_routing_rule_cmp(dst, src, NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID) == 0);
-});
-
 /* Uses internally nmp_object_copy(), hence it also violates the const
  * promise for @obj.
  * */
@@ -1561,6 +1552,37 @@ _vt_cmd_plobj_id_cmp_routing_rule(const NMPlatformObject *obj1, const NMPlatform
                                         NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID);
 }
 
+_vt_cmd_plobj_id_cmp(mptcp_addr, NMPlatformMptcpAddr, {
+    /* The primary key of an MPTCP endpoint is only the address:port@ifindex.
+     *
+     * Which does not fully correspond to kernel's view. Kernel's view is determined
+     * by the question whether you can add two objects that only differ by one
+     * attribute. If you can, the attribute is part of the ID otherwise it isn't.
+     *
+     * Note that for kernel, the "ifindex" is not part of the identity.
+     * That is, you cannot two add two endpoints that only differ by
+     * ifindex. However, for our purpose, it is very useful to make
+     * the "ifindex" part of the identity. For example, NMPGlobalTracker will use
+     * this to track MPTCP addresses from independent callers (NML3Cfg).
+     * It would be bad, if objects that differ by "ifindex" would be
+     * combined.
+     *
+     * The "id" is intentionally not part of the identity for us. Note however, that kernel
+     * does not allow configuring duplicates "id" -- so the "id" could be a primary key
+     * as far as kernel is concerned. However, when we track MPTCP endpoints that
+     * we want to configure, the "id" is left undefined (and we let kernel choose it).
+     * If the "id" would be part of the NMPObject's ID, we could not lookup
+     * an object unless we know the "id" -- which we often don't.
+     */
+    NM_CMP_FIELD(obj1, obj2, ifindex);
+    NM_CMP_FIELD(obj1, obj2, addr_family);
+    NM_CMP_FIELD_MEMCMP_LEN(obj1,
+                            obj2,
+                            addr,
+                            nm_utils_addr_family_to_size_untrusted(obj1->addr_family));
+    NM_CMP_FIELD(obj1, obj2, port);
+});
+
 void
 nmp_object_id_hash_update(const NMPObject *obj, NMHashState *h)
 {
@@ -1646,6 +1668,12 @@ _vt_cmd_plobj_id_hash_update(tfilter, NMPlatformTfilter, {
     nm_hash_update_vals(h, obj->ifindex, obj->handle);
 });
 
+_vt_cmd_plobj_id_hash_update(mptcp_addr, NMPlatformMptcpAddr, {
+    /* See the corresponding ID cmp function for details. */
+    nm_hash_update_vals(h, obj->addr_family, obj->port, obj->ifindex);
+    nm_hash_update(h, &obj->addr, nm_utils_addr_family_to_size_untrusted(obj->addr_family));
+});
+
 static void
 _vt_cmd_plobj_hash_update_ip4_route(const NMPlatformObject *obj, NMHashState *h)
 {
@@ -1754,6 +1782,12 @@ _vt_cmd_obj_is_alive_tfilter(const NMPObject *obj)
     return NMP_OBJECT_CAST_TFILTER(obj)->ifindex > 0;
 }
 
+static gboolean
+_vt_cmd_obj_is_alive_mptcp_addr(const NMPObject *obj)
+{
+    return NM_IN_SET(obj->mptcp_addr.addr_family, AF_INET, AF_INET6, AF_UNSPEC);
+}
+
 gboolean
 nmp_object_is_visible(const NMPObject *obj)
 {
@@ -2099,6 +2133,7 @@ nmp_lookup_init_obj_type(NMPLookup *lookup, NMPObjectType obj_type)
     case NMP_OBJECT_TYPE_ROUTING_RULE:
     case NMP_OBJECT_TYPE_QDISC:
     case NMP_OBJECT_TYPE_TFILTER:
+    case NMP_OBJECT_TYPE_MPTCP_ADDR:
         _nmp_object_stackinit_from_type(&lookup->selector_obj, obj_type);
         lookup->cache_id_type = NMP_CACHE_ID_TYPE_OBJECT_TYPE;
         return _L(lookup);
@@ -2122,7 +2157,7 @@ nmp_lookup_init_link_by_ifname(NMPLookup *lookup, const char *ifname)
 }
 
 const NMPLookup *
-nmp_lookup_init_object(NMPLookup *lookup, NMPObjectType obj_type, int ifindex)
+nmp_lookup_init_object_by_ifindex(NMPLookup *lookup, NMPObjectType obj_type, int ifindex)
 {
     NMPObject *o;
 
@@ -2133,19 +2168,11 @@ nmp_lookup_init_object(NMPLookup *lookup, NMPObjectType obj_type, int ifindex)
                         NMP_OBJECT_TYPE_IP4_ROUTE,
                         NMP_OBJECT_TYPE_IP6_ROUTE,
                         NMP_OBJECT_TYPE_QDISC,
-                        NMP_OBJECT_TYPE_TFILTER));
-
-    if (G_UNLIKELY(
-            (ifindex < 0)
-            || (ifindex == 0
-                && !NM_IN_SET(obj_type, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE)))) {
-        /* This function used to have a fallback that meant to lookup all objects, if
-         * ifindex is non-positive. As routes can have a zero ifindex, that fallback is
-         * confusing and no longer supported. Only have this code, to catch accidental bugs
-         * after the API change. */
-        nm_assert_not_reached();
-        return nmp_lookup_init_obj_type(lookup, obj_type);
-    }
+                        NMP_OBJECT_TYPE_TFILTER,
+                        NMP_OBJECT_TYPE_MPTCP_ADDR));
+    nm_assert(ifindex > 0
+              || (ifindex == 0
+                  && NM_IN_SET(obj_type, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE)));
 
     o                           = _nmp_object_stackinit_from_type(&lookup->selector_obj, obj_type);
     o->obj_with_ifindex.ifindex = ifindex;
@@ -3183,7 +3210,6 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
             .signal_type              = NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED,
             .supported_cache_ids      = _supported_cache_ids_ipx_route,
             .cmd_obj_is_alive         = _vt_cmd_obj_is_alive_ipx_route,
-            .cmd_plobj_id_copy        = _vt_cmd_plobj_id_copy_ip4_route,
             .cmd_plobj_id_cmp         = _vt_cmd_plobj_id_cmp_ip4_route,
             .cmd_plobj_id_hash_update = _vt_cmd_plobj_id_hash_update_ip4_route,
             .cmd_plobj_to_string_id   = (CmdPlobjToStringIdFunc) nm_platform_ip4_route_to_string,
@@ -3204,7 +3230,6 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
             .signal_type              = NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED,
             .supported_cache_ids      = _supported_cache_ids_ipx_route,
             .cmd_obj_is_alive         = _vt_cmd_obj_is_alive_ipx_route,
-            .cmd_plobj_id_copy        = _vt_cmd_plobj_id_copy_ip6_route,
             .cmd_plobj_id_cmp         = _vt_cmd_plobj_id_cmp_ip6_route,
             .cmd_plobj_id_hash_update = _vt_cmd_plobj_id_hash_update_ip6_route,
             .cmd_plobj_to_string_id   = (CmdPlobjToStringIdFunc) nm_platform_ip6_route_to_string,
@@ -3224,7 +3249,6 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
             .signal_type              = NM_PLATFORM_SIGNAL_ROUTING_RULE_CHANGED,
             .supported_cache_ids      = _supported_cache_ids_routing_rules,
             .cmd_obj_is_alive         = _vt_cmd_obj_is_alive_routing_rule,
-            .cmd_plobj_id_copy        = _vt_cmd_plobj_id_copy_routing_rule,
             .cmd_plobj_id_cmp         = _vt_cmd_plobj_id_cmp_routing_rule,
             .cmd_plobj_id_hash_update = _vt_cmd_plobj_id_hash_update_routing_rule,
             .cmd_plobj_to_string_id   = (CmdPlobjToStringIdFunc) nm_platform_routing_rule_to_string,
@@ -3485,4 +3509,32 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
             .cmd_plobj_hash_update = (CmdPlobjHashUpdateFunc) nm_platform_lnk_wireguard_hash_update,
             .cmd_plobj_cmp         = (CmdPlobjCmpFunc) nm_platform_lnk_wireguard_cmp,
         },
+    [NMP_OBJECT_TYPE_LNK_BOND - 1] =
+        {
+            .parent                = DEDUP_MULTI_OBJ_CLASS_INIT(),
+            .obj_type              = NMP_OBJECT_TYPE_LNK_BOND,
+            .sizeof_data           = sizeof(NMPObjectLnkBond),
+            .sizeof_public         = sizeof(NMPlatformLnkBond),
+            .obj_type_name         = "bond",
+            .lnk_link_type         = NM_LINK_TYPE_BOND,
+            .cmd_plobj_to_string   = (CmdPlobjToStringFunc) nm_platform_lnk_bond_to_string,
+            .cmd_plobj_hash_update = (CmdPlobjHashUpdateFunc) nm_platform_lnk_bond_hash_update,
+            .cmd_plobj_cmp         = (CmdPlobjCmpFunc) nm_platform_lnk_bond_cmp,
+        },
+    [NMP_OBJECT_TYPE_MPTCP_ADDR - 1] =
+        {
+            .parent                   = DEDUP_MULTI_OBJ_CLASS_INIT(),
+            .obj_type                 = NMP_OBJECT_TYPE_MPTCP_ADDR,
+            .sizeof_data              = sizeof(NMPObjectMptcpAddr),
+            .sizeof_public            = sizeof(NMPlatformMptcpAddr),
+            .obj_type_name            = "mptcp-addr",
+            .supported_cache_ids      = _supported_cache_ids_object,
+            .cmd_obj_is_alive         = _vt_cmd_obj_is_alive_mptcp_addr,
+            .cmd_plobj_id_cmp         = _vt_cmd_plobj_id_cmp_mptcp_addr,
+            .cmd_plobj_id_hash_update = _vt_cmd_plobj_id_hash_update_mptcp_addr,
+            .cmd_plobj_to_string_id   = (CmdPlobjToStringIdFunc) nm_platform_mptcp_addr_to_string,
+            .cmd_plobj_to_string      = (CmdPlobjToStringFunc) nm_platform_mptcp_addr_to_string,
+            .cmd_plobj_hash_update    = (CmdPlobjHashUpdateFunc) nm_platform_mptcp_addr_hash_update,
+            .cmd_plobj_cmp            = (CmdPlobjCmpFunc) nm_platform_mptcp_addr_cmp,
+        },
 };
diff --git a/src/libnm-platform/nmp-object.h b/src/libnm-platform/nmp-object.h
index d6b88ba2..4958404a 100644
--- a/src/libnm-platform/nmp-object.h
+++ b/src/libnm-platform/nmp-object.h
@@ -238,11 +238,6 @@ typedef struct {
 
     /* Auxiliary data object for Wi-Fi and WPAN */
     GObject *ext_data;
-
-    /* FIXME: not every NMPObjectLink should pay the price for tracking
-     * the wireguard family id. This should be tracked via ext_data, which
-     * would be exactly the right place. */
-    int wireguard_family_id;
 } NMPObjectLink;
 
 typedef struct {
@@ -250,6 +245,10 @@ typedef struct {
 } NMPObjectLnkBridge;
 
 typedef struct {
+    NMPlatformLnkBond _public;
+} NMPObjectLnkBond;
+
+typedef struct {
     NMPlatformLnkGre _public;
 } NMPObjectLnkGre;
 
@@ -336,6 +335,10 @@ typedef struct {
     NMPlatformTfilter _public;
 } NMPObjectTfilter;
 
+typedef struct {
+    NMPlatformMptcpAddr _public;
+} NMPObjectMptcpAddr;
+
 struct _NMPObject {
     union {
         NMDedupMultiObj parent;
@@ -352,6 +355,9 @@ struct _NMPObject {
         NMPlatformLnkBridge lnk_bridge;
         NMPObjectLnkBridge  _lnk_bridge;
 
+        NMPlatformLnkBond lnk_bond;
+        NMPObjectLnkBond  _lnk_bond;
+
         NMPlatformLnkGre lnk_gre;
         NMPObjectLnkGre  _lnk_gre;
 
@@ -409,6 +415,9 @@ struct _NMPObject {
         NMPObjectQdisc    _qdisc;
         NMPlatformTfilter tfilter;
         NMPObjectTfilter  _tfilter;
+
+        NMPlatformMptcpAddr mptcp_addr;
+        NMPObjectMptcpAddr  _mptcp_addr;
     };
 };
 
@@ -493,6 +502,7 @@ _NMP_OBJECT_TYPE_IS_OBJ_WITH_IFINDEX(NMPObjectType obj_type)
     case NMP_OBJECT_TYPE_TFILTER:
 
     case NMP_OBJECT_TYPE_LNK_BRIDGE:
+    case NMP_OBJECT_TYPE_LNK_BOND:
     case NMP_OBJECT_TYPE_LNK_GRE:
     case NMP_OBJECT_TYPE_LNK_GRETAP:
     case NMP_OBJECT_TYPE_LNK_INFINIBAND:
@@ -509,6 +519,8 @@ _NMP_OBJECT_TYPE_IS_OBJ_WITH_IFINDEX(NMPObjectType obj_type)
     case NMP_OBJECT_TYPE_LNK_VRF:
     case NMP_OBJECT_TYPE_LNK_VXLAN:
     case NMP_OBJECT_TYPE_LNK_WIREGUARD:
+
+    case NMP_OBJECT_TYPE_MPTCP_ADDR:
         return TRUE;
 
     case NMP_OBJECT_TYPE_ROUTING_RULE:
@@ -570,6 +582,8 @@ _NMP_OBJECT_TYPE_IS_OBJ_WITH_IFINDEX(NMPObjectType obj_type)
     _NMP_OBJECT_CAST(obj, lnk_wireguard, NMP_OBJECT_TYPE_LNK_WIREGUARD)
 #define NMP_OBJECT_CAST_LNK_BRIDGE(obj) \
     _NMP_OBJECT_CAST(obj, lnk_bridge, NMP_OBJECT_TYPE_LNK_BRIDGE)
+#define NMP_OBJECT_CAST_MPTCP_ADDR(obj) \
+    _NMP_OBJECT_CAST(obj, mptcp_addr, NMP_OBJECT_TYPE_MPTCP_ADDR)
 
 static inline int
 NMP_OBJECT_TYPE_TO_ADDR_FAMILY(NMPObjectType obj_type)
@@ -792,7 +806,8 @@ nmp_cache_lookup(const NMPCache *cache, const NMPLookup *lookup)
 
 const NMPLookup *nmp_lookup_init_obj_type(NMPLookup *lookup, NMPObjectType obj_type);
 const NMPLookup *nmp_lookup_init_link_by_ifname(NMPLookup *lookup, const char *ifname);
-const NMPLookup *nmp_lookup_init_object(NMPLookup *lookup, NMPObjectType obj_type, int ifindex);
+const NMPLookup *
+nmp_lookup_init_object_by_ifindex(NMPLookup *lookup, NMPObjectType obj_type, int ifindex);
 const NMPLookup *nmp_lookup_init_route_default(NMPLookup *lookup, NMPObjectType obj_type);
 const NMPLookup *nmp_lookup_init_route_by_weak_id(NMPLookup *lookup, const NMPObject *obj);
 const NMPLookup *nmp_lookup_init_ip4_route_by_weak_id(NMPLookup *lookup,
@@ -1019,7 +1034,7 @@ nm_platform_lookup_object(NMPlatform *platform, NMPObjectType obj_type, int ifin
 {
     NMPLookup lookup;
 
-    nmp_lookup_init_object(&lookup, obj_type, ifindex);
+    nmp_lookup_init_object_by_ifindex(&lookup, obj_type, ifindex);
     return nm_platform_lookup(platform, &lookup);
 }
 
@@ -1032,7 +1047,7 @@ nm_platform_lookup_object_clone(NMPlatform            *platform,
 {
     NMPLookup lookup;
 
-    nmp_lookup_init_object(&lookup, obj_type, ifindex);
+    nmp_lookup_init_object_by_ifindex(&lookup, obj_type, ifindex);
     return nm_platform_lookup_clone(platform, &lookup, predicate, user_data);
 }
 
diff --git a/src/libnm-platform/nmp-route-manager.c b/src/libnm-platform/nmp-route-manager.c
deleted file mode 100644
index 842301b1..00000000
--- a/src/libnm-platform/nmp-route-manager.c
+++ /dev/null
@@ -1,844 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-
-#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
-
-#include "nmp-route-manager.h"
-
-#include <linux/fib_rules.h>
-#include <linux/rtnetlink.h>
-
-#include "libnm-log-core/nm-logging.h"
-#include "libnm-std-aux/c-list-util.h"
-#include "nmp-object.h"
-
-/*****************************************************************************/
-
-struct _NMPRouteManager {
-    NMPlatform *platform;
-    GHashTable *by_obj;
-    GHashTable *by_user_tag;
-    GHashTable *by_data;
-    CList       by_obj_lst_heads[3];
-    guint       ref_count;
-};
-
-/*****************************************************************************/
-
-#define _NMLOG_DOMAIN      LOGD_PLATFORM
-#define _NMLOG_PREFIX_NAME "route-manager"
-
-#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, LOGD_PLATFORM, _NMLOG_PREFIX_NAME, __VA_ARGS__)
-
-/*****************************************************************************/
-
-static gboolean
-NMP_IS_ROUTE_MANAGER(gpointer self)
-{
-    return self && ((NMPRouteManager *) self)->ref_count > 0
-           && NM_IS_PLATFORM(((NMPRouteManager *) self)->platform);
-}
-
-/*****************************************************************************/
-
-typedef struct {
-    const NMPObject *obj;
-    gconstpointer    user_tag;
-    CList            obj_lst;
-    CList            user_tag_lst;
-
-    /* track_priority_val zero is special: those are weakly tracked rules.
-     * That means: NetworkManager will restore them only if it removed them earlier.
-     * But it will not remove or add them otherwise.
-     *
-     * Otherwise, the track_priority_val goes together with track_priority_present.
-     * In case of one rule being tracked multiple times (with different priorities),
-     * the one with higher priority wins. See _track_obj_data_get_best_data().
-     * Then, the winning present state either enforces that the rule is present
-     * or absent.
-     *
-     * If a rules is not tracked at all, it is ignored by NetworkManager. Assuming
-     * that it was added externally by the user. But unlike weakly tracked rules,
-     * NM will *not* restore such rules if NetworkManager themself removed them. */
-    guint32 track_priority_val;
-    bool    track_priority_present : 1;
-
-    bool dirty : 1;
-} TrackData;
-
-typedef enum {
-    CONFIG_STATE_NONE          = 0,
-    CONFIG_STATE_ADDED_BY_US   = 1,
-    CONFIG_STATE_REMOVED_BY_US = 2,
-
-    /* ConfigState encodes whether the rule was touched by us at all (CONFIG_STATE_NONE).
-     *
-     * Maybe we would only need to track whether we touched the rule at all. But we
-     * track it more in detail what we did: did we add it (CONFIG_STATE_ADDED_BY_US)
-     * or did we remove it (CONFIG_STATE_REMOVED_BY_US)?
-     * Finally, we need CONFIG_STATE_OWNED_BY_US, which means that we didn't actively
-     * add/remove it, but whenever we are about to undo the add/remove, we need to do it.
-     * In that sense, CONFIG_STATE_OWNED_BY_US is really just a flag that we unconditionally
-     * force the state next time when necessary. */
-    CONFIG_STATE_OWNED_BY_US = 3,
-} ConfigState;
-
-typedef struct {
-    const NMPObject *obj;
-    CList            obj_lst_head;
-
-    CList by_obj_lst;
-
-    /* indicates whether we configured/removed the rule (during sync()). We need that, so
-     * if the rule gets untracked, that we know to remove/restore it.
-     *
-     * This makes NMPRouteManager stateful (beyond the configuration that indicates
-     * which rules are tracked).
-     * After a restart, NetworkManager would no longer remember which rules were added
-     * by us.
-     *
-     * That is partially fixed by NetworkManager taking over the rules that it
-     * actively configures (see %NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG). */
-    ConfigState config_state;
-} TrackObjData;
-
-typedef struct {
-    gconstpointer user_tag;
-    CList         user_tag_lst_head;
-} TrackUserTagData;
-
-/*****************************************************************************/
-
-static void _track_data_untrack(NMPRouteManager *self,
-                                TrackData       *track_data,
-                                gboolean         remove_user_tag_data,
-                                gboolean         make_owned_by_us);
-
-/*****************************************************************************/
-
-static CList *
-_by_obj_lst_head(NMPRouteManager *self, NMPObjectType obj_type)
-{
-    G_STATIC_ASSERT(G_N_ELEMENTS(self->by_obj_lst_heads) == 3);
-
-    switch (obj_type) {
-    case NMP_OBJECT_TYPE_IP4_ROUTE:
-        return &self->by_obj_lst_heads[0];
-    case NMP_OBJECT_TYPE_IP6_ROUTE:
-        return &self->by_obj_lst_heads[1];
-    case NMP_OBJECT_TYPE_ROUTING_RULE:
-        return &self->by_obj_lst_heads[2];
-    default:
-        return nm_assert_unreachable_val(NULL);
-    }
-}
-
-/*****************************************************************************/
-
-static void
-_track_data_assert(const TrackData *track_data, gboolean linked)
-{
-    nm_assert(track_data);
-    nm_assert(NM_IN_SET(NMP_OBJECT_GET_TYPE(track_data->obj),
-                        NMP_OBJECT_TYPE_IP4_ROUTE,
-                        NMP_OBJECT_TYPE_IP6_ROUTE,
-                        NMP_OBJECT_TYPE_ROUTING_RULE));
-    nm_assert(nmp_object_is_visible(track_data->obj));
-    nm_assert(track_data->user_tag);
-    nm_assert(!linked || !c_list_is_empty(&track_data->obj_lst));
-    nm_assert(!linked || !c_list_is_empty(&track_data->user_tag_lst));
-}
-
-static guint
-_track_data_hash(gconstpointer data)
-{
-    const TrackData *track_data = data;
-    NMHashState      h;
-
-    _track_data_assert(track_data, FALSE);
-
-    nm_hash_init(&h, 269297543u);
-    nmp_object_id_hash_update(track_data->obj, &h);
-    nm_hash_update_val(&h, track_data->user_tag);
-    return nm_hash_complete(&h);
-}
-
-static gboolean
-_track_data_equal(gconstpointer data_a, gconstpointer data_b)
-{
-    const TrackData *track_data_a = data_a;
-    const TrackData *track_data_b = data_b;
-
-    _track_data_assert(track_data_a, FALSE);
-    _track_data_assert(track_data_b, FALSE);
-
-    return track_data_a->user_tag == track_data_b->user_tag
-           && nmp_object_id_equal(track_data_a->obj, track_data_b->obj);
-}
-
-static void
-_track_data_destroy(gpointer data)
-{
-    TrackData *track_data = data;
-
-    _track_data_assert(track_data, FALSE);
-
-    c_list_unlink_stale(&track_data->obj_lst);
-    c_list_unlink_stale(&track_data->user_tag_lst);
-    nmp_object_unref(track_data->obj);
-    nm_g_slice_free(track_data);
-}
-
-static const TrackData *
-_track_obj_data_get_best_data(TrackObjData *obj_data)
-{
-    TrackData       *track_data;
-    const TrackData *td_best = NULL;
-
-    c_list_for_each_entry (track_data, &obj_data->obj_lst_head, obj_lst) {
-        _track_data_assert(track_data, TRUE);
-
-        if (td_best) {
-            if (td_best->track_priority_val > track_data->track_priority_val)
-                continue;
-            if (td_best->track_priority_val == track_data->track_priority_val) {
-                if (td_best->track_priority_present || !track_data->track_priority_present) {
-                    /* if the priorities are identical, then "present" wins over
-                     * "!present" (absent). */
-                    continue;
-                }
-            }
-        }
-
-        td_best = track_data;
-    }
-
-    return td_best;
-}
-
-static guint
-_track_obj_data_hash(gconstpointer data)
-{
-    const TrackObjData *obj_data = data;
-
-    return nmp_object_id_hash(obj_data->obj);
-}
-
-static gboolean
-_track_obj_data_equal(gconstpointer data_a, gconstpointer data_b)
-{
-    const TrackObjData *obj_data_a = data_a;
-    const TrackObjData *obj_data_b = data_b;
-
-    return nmp_object_id_equal(obj_data_a->obj, obj_data_b->obj);
-}
-
-static void
-_track_obj_data_destroy(gpointer data)
-{
-    TrackObjData *obj_data = data;
-
-    c_list_unlink_stale(&obj_data->obj_lst_head);
-    c_list_unlink_stale(&obj_data->by_obj_lst);
-    nmp_object_unref(obj_data->obj);
-    nm_g_slice_free(obj_data);
-}
-
-static void
-_track_user_tag_data_destroy(gpointer data)
-{
-    TrackUserTagData *user_tag_data = data;
-
-    c_list_unlink_stale(&user_tag_data->user_tag_lst_head);
-    nm_g_slice_free(user_tag_data);
-}
-
-static TrackData *
-_track_data_lookup(GHashTable *by_data, const NMPObject *obj, gconstpointer user_tag)
-{
-    TrackData track_data_needle = {
-        .obj      = obj,
-        .user_tag = user_tag,
-    };
-
-    return g_hash_table_lookup(by_data, &track_data_needle);
-}
-
-/*****************************************************************************/
-
-/**
- * nmp_route_manager_track:
- * @self: the #NMPRouteManager instance
- * @obj_type: the NMPObjectType of @obj that we are tracking.
- * @obj: the NMPlatformObject (of type NMPObjectType) to track. Usually
- *   a #NMPlatformRoutingRule, #NMPlatformIP4Route or #NMPlatformIP6Route
- *   pointer.
- * @track_priority: the priority for tracking the rule. Note that
- *   negative values indicate a forced absence of the rule. Priorities
- *   are compared with their absolute values (with higher absolute
- *   value being more important). For example, if you track the same
- *   rule twice, once with priority -5 and +10, then the rule is
- *   present (because the positive number is more important).
- *   The special value 0 indicates weakly-tracked rules.
- * @user_tag: the tag associated with tracking this rule. The same tag
- *   must be used to untrack the rule later.
- * @user_tag_untrack: if not %NULL, at the same time untrack this user-tag
- *   for the same rule. Note that this is different from a plain nmp_route_manager_untrack_rule(),
- *   because it enforces ownership of the now tracked rule. On the other hand,
- *   a plain nmp_route_manager_untrack_rule() merely forgets about the tracking.
- *   The purpose here is to set this to %NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG.
- *
- * Returns: %TRUE, if something changed.
- */
-gboolean
-nmp_route_manager_track(NMPRouteManager *self,
-                        NMPObjectType    obj_type,
-                        gconstpointer    obj,
-                        gint32           track_priority,
-                        gconstpointer    user_tag,
-                        gconstpointer    user_tag_untrack)
-{
-    NMPObject         obj_stack;
-    const NMPObject  *p_obj_stack;
-    TrackData        *track_data;
-    TrackObjData     *obj_data;
-    TrackUserTagData *user_tag_data;
-    gboolean          changed         = FALSE;
-    gboolean          changed_untrack = FALSE;
-    guint32           track_priority_val;
-    gboolean          track_priority_present;
-
-    g_return_val_if_fail(NMP_IS_ROUTE_MANAGER(self), FALSE);
-    g_return_val_if_fail(obj, FALSE);
-    g_return_val_if_fail(user_tag, FALSE);
-
-    /* The route must not be tied to an interface. We can only handle here
-     * blackhole/unreachable/prohibit route types. */
-    g_return_val_if_fail(
-        obj_type == NMP_OBJECT_TYPE_ROUTING_RULE
-            || (NM_IN_SET(obj_type, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE)
-                && ((const NMPlatformIPRoute *) obj)->ifindex == 0),
-        FALSE);
-
-    nm_assert(track_priority != G_MININT32);
-
-    p_obj_stack = nmp_object_stackinit(&obj_stack, obj_type, obj);
-
-    nm_assert(nmp_object_is_visible(p_obj_stack));
-
-    if (track_priority >= 0) {
-        track_priority_val     = track_priority;
-        track_priority_present = TRUE;
-    } else {
-        track_priority_val     = -track_priority;
-        track_priority_present = FALSE;
-    }
-
-    track_data = _track_data_lookup(self->by_data, p_obj_stack, user_tag);
-
-    if (!track_data) {
-        track_data  = g_slice_new(TrackData);
-        *track_data = (TrackData){
-            .obj      = nm_dedup_multi_index_obj_intern(nm_platform_get_multi_idx(self->platform),
-                                                   p_obj_stack),
-            .user_tag = user_tag,
-            .track_priority_val     = track_priority_val,
-            .track_priority_present = track_priority_present,
-            .dirty                  = FALSE,
-        };
-        g_hash_table_add(self->by_data, track_data);
-
-        obj_data = g_hash_table_lookup(self->by_obj, &track_data->obj);
-        if (!obj_data) {
-            obj_data  = g_slice_new(TrackObjData);
-            *obj_data = (TrackObjData){
-                .obj          = nmp_object_ref(track_data->obj),
-                .obj_lst_head = C_LIST_INIT(obj_data->obj_lst_head),
-                .config_state = CONFIG_STATE_NONE,
-            };
-            g_hash_table_add(self->by_obj, obj_data);
-            c_list_link_tail(_by_obj_lst_head(self, obj_type), &obj_data->by_obj_lst);
-        }
-        c_list_link_tail(&obj_data->obj_lst_head, &track_data->obj_lst);
-
-        user_tag_data = g_hash_table_lookup(self->by_user_tag, &track_data->user_tag);
-        if (!user_tag_data) {
-            user_tag_data  = g_slice_new(TrackUserTagData);
-            *user_tag_data = (TrackUserTagData){
-                .user_tag          = user_tag,
-                .user_tag_lst_head = C_LIST_INIT(user_tag_data->user_tag_lst_head),
-            };
-            g_hash_table_add(self->by_user_tag, user_tag_data);
-        }
-        c_list_link_tail(&user_tag_data->user_tag_lst_head, &track_data->user_tag_lst);
-        changed = TRUE;
-    } else {
-        track_data->dirty = FALSE;
-        if (track_data->track_priority_val != track_priority_val
-            || track_data->track_priority_present != track_priority_present) {
-            track_data->track_priority_val     = track_priority_val;
-            track_data->track_priority_present = track_priority_present;
-            changed                            = TRUE;
-        }
-    }
-
-    if (user_tag_untrack) {
-        if (user_tag != user_tag_untrack) {
-            TrackData *track_data_untrack;
-
-            track_data_untrack = _track_data_lookup(self->by_data, p_obj_stack, user_tag_untrack);
-            if (track_data_untrack) {
-                _track_data_untrack(self, track_data_untrack, FALSE, TRUE);
-                changed_untrack = TRUE;
-            }
-        } else
-            nm_assert_not_reached();
-    }
-
-    _track_data_assert(track_data, TRUE);
-
-    if (changed) {
-        char sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
-
-        _LOGD(
-            "track [" NM_HASH_OBFUSCATE_PTR_FMT ",%s%u] %s \"%s\"",
-            NM_HASH_OBFUSCATE_PTR(track_data->user_tag),
-            (track_data->track_priority_val == 0
-                 ? ""
-                 : (track_data->track_priority_present ? "+" : "-")),
-            (guint) track_data->track_priority_val,
-            NMP_OBJECT_GET_CLASS(track_data->obj)->obj_type_name,
-            nmp_object_to_string(track_data->obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
-    }
-
-    return changed || changed_untrack;
-}
-
-static void
-_track_data_untrack(NMPRouteManager *self,
-                    TrackData       *track_data,
-                    gboolean         remove_user_tag_data,
-                    gboolean         make_owned_by_us)
-{
-    char          sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
-    TrackObjData *obj_data;
-
-    nm_assert(NMP_IS_ROUTE_MANAGER(self));
-    _track_data_assert(track_data, TRUE);
-    nm_assert(self->by_data);
-    nm_assert(g_hash_table_lookup(self->by_data, track_data) == track_data);
-
-    _LOGD("untrack [" NM_HASH_OBFUSCATE_PTR_FMT "] %s \"%s\"",
-          NM_HASH_OBFUSCATE_PTR(track_data->user_tag),
-          NMP_OBJECT_GET_CLASS(track_data->obj)->obj_type_name,
-          nmp_object_to_string(track_data->obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
-
-#if NM_MORE_ASSERTS
-    {
-        TrackUserTagData *user_tag_data;
-
-        user_tag_data = g_hash_table_lookup(self->by_user_tag, &track_data->user_tag);
-        nm_assert(user_tag_data);
-        nm_assert(c_list_contains(&user_tag_data->user_tag_lst_head, &track_data->user_tag_lst));
-    }
-#endif
-
-    nm_assert(!c_list_is_empty(&track_data->user_tag_lst));
-
-    obj_data = g_hash_table_lookup(self->by_obj, &track_data->obj);
-    nm_assert(obj_data);
-    nm_assert(c_list_contains(&obj_data->obj_lst_head, &track_data->obj_lst));
-    nm_assert(obj_data == g_hash_table_lookup(self->by_obj, &track_data->obj));
-
-    if (make_owned_by_us) {
-        if (obj_data->config_state == CONFIG_STATE_NONE) {
-            /* we need to mark this entry that it requires a touch on the next
-             * sync. */
-            obj_data->config_state = CONFIG_STATE_OWNED_BY_US;
-        }
-    } else if (remove_user_tag_data && c_list_length_is(&track_data->user_tag_lst, 1))
-        g_hash_table_remove(self->by_user_tag, &track_data->user_tag);
-
-    /* if obj_data is marked to be "added_by_us" or "removed_by_us", we need to keep this entry
-     * around for the next sync -- so that we can undo what we did earlier. */
-    if (obj_data->config_state == CONFIG_STATE_NONE && c_list_length_is(&track_data->obj_lst, 1))
-        g_hash_table_remove(self->by_obj, &track_data->obj);
-
-    g_hash_table_remove(self->by_data, track_data);
-}
-
-gboolean
-nmp_route_manager_untrack(NMPRouteManager *self,
-                          NMPObjectType    obj_type,
-                          gconstpointer    obj,
-                          gconstpointer    user_tag)
-{
-    NMPObject        obj_stack;
-    const NMPObject *p_obj_stack;
-    TrackData       *track_data;
-    gboolean         changed = FALSE;
-
-    g_return_val_if_fail(NMP_IS_ROUTE_MANAGER(self), FALSE);
-    nm_assert(NM_IN_SET(obj_type,
-                        NMP_OBJECT_TYPE_IP4_ROUTE,
-                        NMP_OBJECT_TYPE_IP6_ROUTE,
-                        NMP_OBJECT_TYPE_ROUTING_RULE));
-    g_return_val_if_fail(obj, FALSE);
-    g_return_val_if_fail(user_tag, FALSE);
-
-    p_obj_stack = nmp_object_stackinit(&obj_stack, obj_type, obj);
-
-    nm_assert(nmp_object_is_visible(p_obj_stack));
-
-    track_data = _track_data_lookup(self->by_data, p_obj_stack, user_tag);
-    if (track_data) {
-        _track_data_untrack(self, track_data, TRUE, FALSE);
-        changed = TRUE;
-    }
-
-    return changed;
-}
-
-void
-nmp_route_manager_set_dirty(NMPRouteManager *self, gconstpointer user_tag)
-{
-    TrackData        *track_data;
-    TrackUserTagData *user_tag_data;
-
-    g_return_if_fail(NMP_IS_ROUTE_MANAGER(self));
-    g_return_if_fail(user_tag);
-
-    user_tag_data = g_hash_table_lookup(self->by_user_tag, &user_tag);
-    if (!user_tag_data)
-        return;
-
-    c_list_for_each_entry (track_data, &user_tag_data->user_tag_lst_head, user_tag_lst)
-        track_data->dirty = TRUE;
-}
-
-gboolean
-nmp_route_manager_untrack_all(NMPRouteManager *self,
-                              gconstpointer    user_tag,
-                              gboolean         all /* or only dirty */,
-                              gboolean         make_survivors_dirty)
-{
-    TrackData        *track_data;
-    TrackData        *track_data_safe;
-    TrackUserTagData *user_tag_data;
-    gboolean          changed = FALSE;
-
-    g_return_val_if_fail(NMP_IS_ROUTE_MANAGER(self), FALSE);
-    g_return_val_if_fail(user_tag, FALSE);
-
-    user_tag_data = g_hash_table_lookup(self->by_user_tag, &user_tag);
-    if (!user_tag_data)
-        return FALSE;
-
-    c_list_for_each_entry_safe (track_data,
-                                track_data_safe,
-                                &user_tag_data->user_tag_lst_head,
-                                user_tag_lst) {
-        if (all || track_data->dirty) {
-            _track_data_untrack(self, track_data, FALSE, FALSE);
-            changed = TRUE;
-            continue;
-        }
-        if (make_survivors_dirty)
-            track_data->dirty = TRUE;
-    }
-    if (c_list_is_empty(&user_tag_data->user_tag_lst_head))
-        g_hash_table_remove(self->by_user_tag, user_tag_data);
-
-    return changed;
-}
-
-/*****************************************************************************/
-
-void
-nmp_route_manager_sync(NMPRouteManager *self, NMPObjectType obj_type, gboolean keep_deleted)
-{
-    char                         sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
-    const NMDedupMultiHeadEntry *pl_head_entry;
-    NMDedupMultiIter             pl_iter;
-    const NMPObject             *plobj;
-    gs_unref_ptrarray GPtrArray *objs_to_delete = NULL;
-    TrackObjData                *obj_data;
-    TrackObjData                *obj_data_safe;
-    CList                       *by_obj_lst_head;
-    guint                        i;
-    const TrackData             *td_best;
-
-    g_return_if_fail(NMP_IS_ROUTE_MANAGER(self));
-    g_return_if_fail(NM_IN_SET(obj_type,
-                               NMP_OBJECT_TYPE_IP4_ROUTE,
-                               NMP_OBJECT_TYPE_IP6_ROUTE,
-                               NMP_OBJECT_TYPE_ROUTING_RULE));
-
-    _LOGD("sync %s%s",
-          nmp_class_from_type(obj_type)->obj_type_name,
-          keep_deleted ? " (don't remove any)" : "");
-
-    if (obj_type == NMP_OBJECT_TYPE_ROUTING_RULE)
-        pl_head_entry = nm_platform_lookup_obj_type(self->platform, obj_type);
-    else
-        pl_head_entry = nm_platform_lookup_object(self->platform, obj_type, 0);
-
-    if (pl_head_entry) {
-        nmp_cache_iter_for_each (&pl_iter, pl_head_entry, &plobj) {
-            obj_data = g_hash_table_lookup(self->by_obj, &plobj);
-
-            if (!obj_data) {
-                /* this obj is not tracked. It was externally added, hence we
-                 * ignore it. */
-                continue;
-            }
-
-            td_best = _track_obj_data_get_best_data(obj_data);
-            if (td_best) {
-                if (td_best->track_priority_present) {
-                    if (obj_data->config_state == CONFIG_STATE_OWNED_BY_US)
-                        obj_data->config_state = CONFIG_STATE_ADDED_BY_US;
-                    continue;
-                }
-                if (td_best->track_priority_val == 0) {
-                    if (!NM_IN_SET(obj_data->config_state,
-                                   CONFIG_STATE_ADDED_BY_US,
-                                   CONFIG_STATE_OWNED_BY_US)) {
-                        obj_data->config_state = CONFIG_STATE_NONE;
-                        continue;
-                    }
-                    obj_data->config_state = CONFIG_STATE_NONE;
-                }
-            }
-
-            if (keep_deleted) {
-                _LOGD("forget/leak object added by us: %s \"%s\"",
-                      NMP_OBJECT_GET_CLASS(plobj)->obj_type_name,
-                      nmp_object_to_string(plobj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
-                continue;
-            }
-
-            if (!objs_to_delete)
-                objs_to_delete = g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
-
-            g_ptr_array_add(objs_to_delete, (gpointer) nmp_object_ref(plobj));
-
-            obj_data->config_state = CONFIG_STATE_REMOVED_BY_US;
-        }
-    }
-
-    if (objs_to_delete) {
-        for (i = 0; i < objs_to_delete->len; i++)
-            nm_platform_object_delete(self->platform, objs_to_delete->pdata[i]);
-    }
-
-    by_obj_lst_head = _by_obj_lst_head(self, obj_type);
-
-    c_list_for_each_entry_safe (obj_data, obj_data_safe, by_obj_lst_head, by_obj_lst) {
-        nm_assert(NMP_OBJECT_GET_TYPE(obj_data->obj) == obj_type);
-
-        td_best = _track_obj_data_get_best_data(obj_data);
-
-        if (!td_best) {
-            g_hash_table_remove(self->by_obj, obj_data);
-            continue;
-        }
-
-        if (!td_best->track_priority_present) {
-            if (obj_data->config_state == CONFIG_STATE_OWNED_BY_US)
-                obj_data->config_state = CONFIG_STATE_REMOVED_BY_US;
-            continue;
-        }
-        if (td_best->track_priority_val == 0) {
-            if (!NM_IN_SET(obj_data->config_state,
-                           CONFIG_STATE_REMOVED_BY_US,
-                           CONFIG_STATE_OWNED_BY_US)) {
-                obj_data->config_state = CONFIG_STATE_NONE;
-                continue;
-            }
-            obj_data->config_state = CONFIG_STATE_NONE;
-        }
-
-        plobj =
-            nm_platform_lookup_obj(self->platform, NMP_CACHE_ID_TYPE_OBJECT_TYPE, obj_data->obj);
-        if (plobj)
-            continue;
-
-        obj_data->config_state = CONFIG_STATE_ADDED_BY_US;
-
-        if (obj_type == NMP_OBJECT_TYPE_ROUTING_RULE) {
-            nm_platform_routing_rule_add(self->platform,
-                                         NMP_NLM_FLAG_ADD,
-                                         NMP_OBJECT_CAST_ROUTING_RULE(obj_data->obj));
-        } else
-            nm_platform_ip_route_add(self->platform, NMP_NLM_FLAG_APPEND, obj_data->obj);
-    }
-}
-
-/*****************************************************************************/
-
-void
-nmp_route_manager_track_rule_from_platform(NMPRouteManager *self,
-                                           NMPlatform      *platform,
-                                           int              addr_family,
-                                           gint32           tracking_priority,
-                                           gconstpointer    user_tag)
-{
-    NMPLookup                    lookup;
-    const NMDedupMultiHeadEntry *head_entry;
-    NMDedupMultiIter             iter;
-    const NMPObject             *o;
-
-    g_return_if_fail(NMP_IS_ROUTE_MANAGER(self));
-
-    if (!platform)
-        platform = self->platform;
-    else
-        g_return_if_fail(NM_IS_PLATFORM(platform));
-
-    nm_assert(NM_IN_SET(addr_family, AF_UNSPEC, AF_INET, AF_INET6));
-
-    nmp_lookup_init_obj_type(&lookup, NMP_OBJECT_TYPE_ROUTING_RULE);
-    head_entry = nm_platform_lookup(platform, &lookup);
-    nmp_cache_iter_for_each (&iter, head_entry, &o) {
-        const NMPlatformRoutingRule *rr = NMP_OBJECT_CAST_ROUTING_RULE(o);
-
-        if (addr_family != AF_UNSPEC && rr->addr_family != addr_family)
-            continue;
-
-        nmp_route_manager_track_rule(self, rr, tracking_priority, user_tag, NULL);
-    }
-}
-
-/*****************************************************************************/
-
-void
-nmp_route_manager_track_rule_default(NMPRouteManager *self,
-                                     int              addr_family,
-                                     gint32           track_priority,
-                                     gconstpointer    user_tag)
-{
-    g_return_if_fail(NMP_IS_ROUTE_MANAGER(self));
-
-    nm_assert(NM_IN_SET(addr_family, AF_UNSPEC, AF_INET, AF_INET6));
-
-    /* track the default rules. See also `man ip-rule`. */
-
-    if (NM_IN_SET(addr_family, AF_UNSPEC, AF_INET)) {
-        nmp_route_manager_track_rule(self,
-                                     &((NMPlatformRoutingRule){
-                                         .addr_family = AF_INET,
-                                         .priority    = 0,
-                                         .table       = RT_TABLE_LOCAL,
-                                         .action      = FR_ACT_TO_TBL,
-                                         .protocol    = RTPROT_KERNEL,
-                                     }),
-                                     track_priority,
-                                     user_tag,
-                                     NULL);
-        nmp_route_manager_track_rule(self,
-                                     &((NMPlatformRoutingRule){
-                                         .addr_family = AF_INET,
-                                         .priority    = 32766,
-                                         .table       = RT_TABLE_MAIN,
-                                         .action      = FR_ACT_TO_TBL,
-                                         .protocol    = RTPROT_KERNEL,
-                                     }),
-                                     track_priority,
-                                     user_tag,
-                                     NULL);
-        nmp_route_manager_track_rule(self,
-                                     &((NMPlatformRoutingRule){
-                                         .addr_family = AF_INET,
-                                         .priority    = 32767,
-                                         .table       = RT_TABLE_DEFAULT,
-                                         .action      = FR_ACT_TO_TBL,
-                                         .protocol    = RTPROT_KERNEL,
-                                     }),
-                                     track_priority,
-                                     user_tag,
-                                     NULL);
-    }
-    if (NM_IN_SET(addr_family, AF_UNSPEC, AF_INET6)) {
-        nmp_route_manager_track_rule(self,
-                                     &((NMPlatformRoutingRule){
-                                         .addr_family = AF_INET6,
-                                         .priority    = 0,
-                                         .table       = RT_TABLE_LOCAL,
-                                         .action      = FR_ACT_TO_TBL,
-                                         .protocol    = RTPROT_KERNEL,
-                                     }),
-                                     track_priority,
-                                     user_tag,
-                                     NULL);
-        nmp_route_manager_track_rule(self,
-                                     &((NMPlatformRoutingRule){
-                                         .addr_family = AF_INET6,
-                                         .priority    = 32766,
-                                         .table       = RT_TABLE_MAIN,
-                                         .action      = FR_ACT_TO_TBL,
-                                         .protocol    = RTPROT_KERNEL,
-                                     }),
-                                     track_priority,
-                                     user_tag,
-                                     NULL);
-    }
-}
-
-/*****************************************************************************/
-
-NMPRouteManager *
-nmp_route_manager_new(NMPlatform *platform)
-{
-    NMPRouteManager *self;
-
-    g_return_val_if_fail(NM_IS_PLATFORM(platform), NULL);
-
-    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(TrackUserTagData, user_tag) == 0);
-
-    self  = g_slice_new(NMPRouteManager);
-    *self = (NMPRouteManager){
-        .ref_count = 1,
-        .platform  = g_object_ref(platform),
-        .by_data =
-            g_hash_table_new_full(_track_data_hash, _track_data_equal, NULL, _track_data_destroy),
-        .by_obj              = g_hash_table_new_full(_track_obj_data_hash,
-                                        _track_obj_data_equal,
-                                        NULL,
-                                        _track_obj_data_destroy),
-        .by_user_tag         = g_hash_table_new_full(nm_pdirect_hash,
-                                             nm_pdirect_equal,
-                                             NULL,
-                                             _track_user_tag_data_destroy),
-        .by_obj_lst_heads[0] = C_LIST_INIT(self->by_obj_lst_heads[0]),
-        .by_obj_lst_heads[1] = C_LIST_INIT(self->by_obj_lst_heads[1]),
-        .by_obj_lst_heads[2] = C_LIST_INIT(self->by_obj_lst_heads[2]),
-    };
-    return self;
-}
-
-NMPRouteManager *
-nmp_route_manager_ref(NMPRouteManager *self)
-{
-    g_return_val_if_fail(NMP_IS_ROUTE_MANAGER(self), NULL);
-
-    self->ref_count++;
-    return self;
-}
-
-void
-nmp_route_manager_unref(NMPRouteManager *self)
-{
-    g_return_if_fail(NMP_IS_ROUTE_MANAGER(self));
-
-    if (--self->ref_count > 0)
-        return;
-
-    g_hash_table_destroy(self->by_user_tag);
-    g_hash_table_destroy(self->by_obj);
-    g_hash_table_destroy(self->by_data);
-    nm_assert(c_list_is_empty(&self->by_obj_lst_heads[0]));
-    nm_assert(c_list_is_empty(&self->by_obj_lst_heads[1]));
-    nm_assert(c_list_is_empty(&self->by_obj_lst_heads[2]));
-    g_object_unref(self->platform);
-    nm_g_slice_free(self);
-}
diff --git a/src/libnm-platform/nmp-route-manager.h b/src/libnm-platform/nmp-route-manager.h
deleted file mode 100644
index 97ec3840..00000000
--- a/src/libnm-platform/nmp-route-manager.h
+++ /dev/null
@@ -1,79 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-
-#ifndef __NMP_ROUTE_MANAGER_H__
-#define __NMP_ROUTE_MANAGER_H__
-
-#include "nm-platform.h"
-
-/*****************************************************************************/
-
-#define NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG ((const void *) nmp_route_manager_new)
-
-typedef struct _NMPRouteManager NMPRouteManager;
-
-NMPRouteManager *nmp_route_manager_new(NMPlatform *platform);
-
-NMPRouteManager *nmp_route_manager_ref(NMPRouteManager *self);
-void             nmp_route_manager_unref(NMPRouteManager *self);
-
-#define nm_auto_unref_route_manager nm_auto(_nmp_route_manager_unref)
-NM_AUTO_DEFINE_FCN0(NMPRouteManager *, _nmp_route_manager_unref, nmp_route_manager_unref);
-
-gboolean nmp_route_manager_track(NMPRouteManager *self,
-                                 NMPObjectType    obj_type,
-                                 gconstpointer    obj,
-                                 gint32           track_priority,
-                                 gconstpointer    user_tag,
-                                 gconstpointer    user_tag_untrack);
-
-static inline gboolean
-nmp_route_manager_track_rule(NMPRouteManager             *self,
-                             const NMPlatformRoutingRule *routing_rule,
-                             gint32                       track_priority,
-                             gconstpointer                user_tag,
-                             gconstpointer                user_tag_untrack)
-{
-    return nmp_route_manager_track(self,
-                                   NMP_OBJECT_TYPE_ROUTING_RULE,
-                                   routing_rule,
-                                   track_priority,
-                                   user_tag,
-                                   user_tag_untrack);
-}
-
-void nmp_route_manager_track_rule_default(NMPRouteManager *self,
-                                          int              addr_family,
-                                          gint32           track_priority,
-                                          gconstpointer    user_tag);
-
-void nmp_route_manager_track_rule_from_platform(NMPRouteManager *self,
-                                                NMPlatform      *platform,
-                                                int              addr_family,
-                                                gint32           tracking_priority,
-                                                gconstpointer    user_tag);
-
-gboolean nmp_route_manager_untrack(NMPRouteManager *self,
-                                   NMPObjectType    obj_type,
-                                   gconstpointer    obj,
-                                   gconstpointer    user_tag);
-
-static inline gboolean
-nmp_route_manager_untrack_rule(NMPRouteManager             *self,
-                               const NMPlatformRoutingRule *routing_rule,
-                               gconstpointer                user_tag)
-{
-    return nmp_route_manager_untrack(self, NMP_OBJECT_TYPE_ROUTING_RULE, routing_rule, user_tag);
-}
-
-void nmp_route_manager_set_dirty(NMPRouteManager *self, gconstpointer user_tag);
-
-gboolean nmp_route_manager_untrack_all(NMPRouteManager *self,
-                                       gconstpointer    user_tag,
-                                       gboolean         all /* or only dirty */,
-                                       gboolean         make_survivors_dirty);
-
-void nmp_route_manager_sync(NMPRouteManager *self, NMPObjectType obj_type, gboolean keep_deleted);
-
-/*****************************************************************************/
-
-#endif /* __NMP_ROUTE_MANAGER_H__ */
diff --git a/src/libnm-platform/tests/test-nm-platform.c b/src/libnm-platform/tests/test-nm-platform.c
index 9ac69bde..c351f014 100644
--- a/src/libnm-platform/tests/test-nm-platform.c
+++ b/src/libnm-platform/tests/test-nm-platform.c
@@ -59,7 +59,7 @@ test_use_symbols(void)
         (void (*)(void)) genlmsg_valid_hdr,
         (void (*)(void)) genlmsg_parse,
         (void (*)(void)) genl_ctrl_resolve,
-        (void (*)(void)) nl_socket_alloc,
+        (void (*)(void)) nl_socket_new,
         (void (*)(void)) nl_socket_free,
         (void (*)(void)) nl_socket_get_fd,
         (void (*)(void)) nl_socket_get_local_port,
@@ -70,9 +70,6 @@ test_use_symbols(void)
         (void (*)(void)) nl_socket_set_nonblocking,
         (void (*)(void)) nl_socket_set_buffer_size,
         (void (*)(void)) nl_socket_add_memberships,
-        (void (*)(void)) nl_socket_set_ext_ack,
-        (void (*)(void)) nl_socket_disable_msg_peek,
-        (void (*)(void)) nl_connect,
         (void (*)(void)) nl_wait_for_ack,
         (void (*)(void)) nl_recvmsgs,
         (void (*)(void)) nl_sendmsg,
diff --git a/src/libnm-platform/wifi/nm-wifi-utils-nl80211.c b/src/libnm-platform/wifi/nm-wifi-utils-nl80211.c
index 475b8a39..37edd928 100644
--- a/src/libnm-platform/wifi/nm-wifi-utils-nl80211.c
+++ b/src/libnm-platform/wifi/nm-wifi-utils-nl80211.c
@@ -9,14 +9,13 @@
 
 #include "nm-wifi-utils-nl80211.h"
 
+#include "libnm-std-aux/nm-linux-compat.h"
+
 #include <sys/ioctl.h>
-#include <net/ethernet.h>
 #include <unistd.h>
 #include <linux/nl80211.h>
 #include <linux/if.h>
 
-#include "linux-headers/nl80211-vnd-intel.h"
-
 #include "libnm-log-core/nm-logging.h"
 #include "libnm-platform/nm-netlink.h"
 #include "nm-wifi-utils-private.h"
@@ -46,9 +45,9 @@ typedef struct {
     NMWifiUtils     parent;
     struct nl_sock *nl_sock;
     guint32        *freqs;
-    int             id;
     int             num_freqs;
     int             phy;
+    guint16         genl_family_id;
     bool            can_wowlan : 1;
 } NMWifiUtilsNl80211;
 
@@ -59,7 +58,7 @@ typedef struct {
 G_DEFINE_TYPE(NMWifiUtilsNl80211, nm_wifi_utils_nl80211, NM_TYPE_WIFI_UTILS)
 
 static int
-ack_handler(struct nl_msg *msg, void *arg)
+ack_handler(const struct nl_msg *msg, void *arg)
 {
     int *done = arg;
     *done     = 1;
@@ -67,7 +66,7 @@ ack_handler(struct nl_msg *msg, void *arg)
 }
 
 static int
-finish_handler(struct nl_msg *msg, void *arg)
+finish_handler(const struct nl_msg *msg, void *arg)
 {
     int *done = arg;
     *done     = 1;
@@ -75,7 +74,7 @@ finish_handler(struct nl_msg *msg, void *arg)
 }
 
 static int
-error_handler(struct sockaddr_nl *nla, struct nlmsgerr *err, void *arg)
+error_handler(const struct sockaddr_nl *nla, const struct nlmsgerr *err, void *arg)
 {
     int *done = arg;
     *done     = err->error;
@@ -83,12 +82,12 @@ error_handler(struct sockaddr_nl *nla, struct nlmsgerr *err, void *arg)
 }
 
 static struct nl_msg *
-_nl80211_alloc_msg(int id, int ifindex, int phy, guint32 cmd, guint32 flags)
+_nl80211_alloc_msg(guint16 genl_family_id, int ifindex, int phy, uint8_t cmd, uint16_t flags)
 {
     nm_auto_nlmsg struct nl_msg *msg = NULL;
 
     msg = nlmsg_alloc();
-    genlmsg_put(msg, 0, 0, id, 0, flags, cmd, 0);
+    genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, genl_family_id, 0, flags, cmd, 0);
     NLA_PUT_U32(msg, NL80211_ATTR_IFINDEX, ifindex);
     if (phy != -1)
         NLA_PUT_U32(msg, NL80211_ATTR_WIPHY, phy);
@@ -99,15 +98,15 @@ nla_put_failure:
 }
 
 static struct nl_msg *
-nl80211_alloc_msg(NMWifiUtilsNl80211 *self, guint32 cmd, guint32 flags)
+nl80211_alloc_msg(NMWifiUtilsNl80211 *self, uint8_t cmd, uint16_t flags)
 {
-    return _nl80211_alloc_msg(self->id, self->parent.ifindex, self->phy, cmd, flags);
+    return _nl80211_alloc_msg(self->genl_family_id, self->parent.ifindex, self->phy, cmd, flags);
 }
 
 static int
 nl80211_send_and_recv(NMWifiUtilsNl80211 *self,
                       struct nl_msg      *msg,
-                      int (*valid_handler)(struct nl_msg *, void *),
+                      int (*valid_handler)(const struct nl_msg *, void *),
                       void *valid_data)
 {
     int                err;
@@ -168,7 +167,7 @@ struct nl80211_iface_info {
 };
 
 static int
-nl80211_iface_info_handler(struct nl_msg *msg, void *arg)
+nl80211_iface_info_handler(const struct nl_msg *msg, void *arg)
 {
     struct nl80211_iface_info *info = arg;
     struct genlmsghdr         *gnlh = nlmsg_data(nlmsg_hdr(msg));
@@ -270,7 +269,7 @@ nla_put_failure:
 }
 
 static int
-nl80211_get_wake_on_wlan_handler(struct nl_msg *msg, void *arg)
+nl80211_get_wake_on_wlan_handler(const struct nl_msg *msg, void *arg)
 {
     _NMSettingWirelessWakeOnWLan *wowl = arg;
     struct nlattr                *attrs[NL80211_ATTR_MAX + 1];
@@ -423,7 +422,7 @@ struct nl80211_station_info {
 };
 
 static int
-nl80211_station_dump_handler(struct nl_msg *msg, void *arg)
+nl80211_station_dump_handler(const struct nl_msg *msg, void *arg)
 {
     static const struct nla_policy stats_policy[] = {
         [NL80211_STA_INFO_INACTIVE_TIME]     = {.type = NLA_U32},
@@ -577,7 +576,7 @@ struct nl80211_device_info {
 #define WLAN_CIPHER_SUITE_SMS4      0x00147201
 
 static int
-nl80211_wiphy_info_handler(struct nl_msg *msg, void *arg)
+nl80211_wiphy_info_handler(const struct nl_msg *msg, void *arg)
 {
     static const struct nla_policy freq_policy[] = {
         [NL80211_FREQUENCY_ATTR_FREQ]     = {.type = NLA_U32},
@@ -817,7 +816,7 @@ struct nl80211_csme_conn_info {
 };
 
 static int
-nl80211_csme_conn_event_handler(struct nl_msg *msg, void *arg)
+nl80211_csme_conn_event_handler(const struct nl_msg *msg, void *arg)
 {
     struct nl80211_csme_conn_info *info          = arg;
     NMPlatformCsmeConnInfo        *out_conn_info = info->conn_info;
@@ -945,7 +944,7 @@ nm_wifi_utils_nl80211_class_init(NMWifiUtilsNl80211Class *klass)
 }
 
 NMWifiUtils *
-nm_wifi_utils_nl80211_new(int ifindex, struct nl_sock *genl)
+nm_wifi_utils_nl80211_new(struct nl_sock *genl, guint16 genl_family_id, int ifindex)
 {
     gs_unref_object NMWifiUtilsNl80211 *self        = NULL;
     nm_auto_nlmsg struct nl_msg        *msg         = NULL;
@@ -954,16 +953,14 @@ nm_wifi_utils_nl80211_new(int ifindex, struct nl_sock *genl)
     if (!genl)
         return NULL;
 
+    if (genl_family_id == 0)
+        return NULL;
+
     self = g_object_new(NM_TYPE_WIFI_UTILS_NL80211, NULL);
 
     self->parent.ifindex = ifindex;
     self->nl_sock        = genl;
-
-    self->id = genl_ctrl_resolve(self->nl_sock, "nl80211");
-    if (self->id < 0) {
-        _LOGD("genl_ctrl_resolve: failed to resolve \"nl80211\"");
-        return NULL;
-    }
+    self->genl_family_id = genl_family_id;
 
     self->phy = -1;
 
diff --git a/src/libnm-platform/wifi/nm-wifi-utils-nl80211.h b/src/libnm-platform/wifi/nm-wifi-utils-nl80211.h
index 4a633307..4783c572 100644
--- a/src/libnm-platform/wifi/nm-wifi-utils-nl80211.h
+++ b/src/libnm-platform/wifi/nm-wifi-utils-nl80211.h
@@ -24,6 +24,6 @@
 
 GType nm_wifi_utils_nl80211_get_type(void);
 
-NMWifiUtils *nm_wifi_utils_nl80211_new(int ifindex, struct nl_sock *genl);
+NMWifiUtils *nm_wifi_utils_nl80211_new(struct nl_sock *genl, guint16 genl_family_id, int ifindex);
 
 #endif /* __WIFI_UTILS_NL80211_H__ */
diff --git a/src/libnm-platform/wifi/nm-wifi-utils.c b/src/libnm-platform/wifi/nm-wifi-utils.c
index 08a8ec4f..0238b74a 100644
--- a/src/libnm-platform/wifi/nm-wifi-utils.c
+++ b/src/libnm-platform/wifi/nm-wifi-utils.c
@@ -32,16 +32,16 @@ nm_wifi_utils_class_init(NMWifiUtilsClass *klass)
 {}
 
 NMWifiUtils *
-nm_wifi_utils_new(int ifindex, struct nl_sock *genl, gboolean check_scan)
+nm_wifi_utils_new(struct nl_sock *genl, guint16 genl_family_id, int ifindex, gboolean check_scan)
 {
     NMWifiUtils *ret;
 
     g_return_val_if_fail(ifindex > 0, NULL);
 
-    ret = nm_wifi_utils_nl80211_new(ifindex, genl);
+    ret = nm_wifi_utils_nl80211_new(genl, genl_family_id, ifindex);
 
 #if HAVE_WEXT
-    if (ret == NULL)
+    if (!ret)
         ret = nm_wifi_utils_wext_new(ifindex, check_scan);
 #endif
 
diff --git a/src/libnm-platform/wifi/nm-wifi-utils.h b/src/libnm-platform/wifi/nm-wifi-utils.h
index 0d30c1a1..fd636418 100644
--- a/src/libnm-platform/wifi/nm-wifi-utils.h
+++ b/src/libnm-platform/wifi/nm-wifi-utils.h
@@ -7,8 +7,6 @@
 #ifndef __WIFI_UTILS_H__
 #define __WIFI_UTILS_H__
 
-#include <net/ethernet.h>
-
 #include "libnm-platform/nm-netlink.h"
 #include "libnm-base/nm-base.h"
 
@@ -27,7 +25,8 @@ GType nm_wifi_utils_get_type(void);
 
 gboolean nm_wifi_utils_is_wifi(int dirfd, const char *ifname);
 
-NMWifiUtils *nm_wifi_utils_new(int ifindex, struct nl_sock *genl, gboolean check_scan);
+NMWifiUtils *
+nm_wifi_utils_new(struct nl_sock *genl, guint16 genl_family_id, int ifindex, gboolean check_scan);
 
 _NMDeviceWifiCapabilities nm_wifi_utils_get_caps(NMWifiUtils *data);
 
diff --git a/src/libnm-platform/wpan/nm-wpan-utils.c b/src/libnm-platform/wpan/nm-wpan-utils.c
index d9bbd0f0..675efe5a 100644
--- a/src/libnm-platform/wpan/nm-wpan-utils.c
+++ b/src/libnm-platform/wpan/nm-wpan-utils.c
@@ -37,9 +37,9 @@
 
 struct NMWpanUtils {
     GObject         parent;
-    int             ifindex;
     struct nl_sock *nl_sock;
-    int             id;
+    int             ifindex;
+    guint16         genl_family_id;
 };
 
 typedef struct {
@@ -51,7 +51,7 @@ G_DEFINE_TYPE(NMWpanUtils, nm_wpan_utils, G_TYPE_OBJECT)
 /*****************************************************************************/
 
 static int
-ack_handler(struct nl_msg *msg, void *arg)
+ack_handler(const struct nl_msg *msg, void *arg)
 {
     int *done = arg;
     *done     = 1;
@@ -59,7 +59,7 @@ ack_handler(struct nl_msg *msg, void *arg)
 }
 
 static int
-finish_handler(struct nl_msg *msg, void *arg)
+finish_handler(const struct nl_msg *msg, void *arg)
 {
     int *done = arg;
     *done     = 1;
@@ -67,7 +67,7 @@ finish_handler(struct nl_msg *msg, void *arg)
 }
 
 static int
-error_handler(struct sockaddr_nl *nla, struct nlmsgerr *err, void *arg)
+error_handler(const struct sockaddr_nl *nla, const struct nlmsgerr *err, void *arg)
 {
     int *done = arg;
     *done     = err->error;
@@ -75,12 +75,13 @@ error_handler(struct sockaddr_nl *nla, struct nlmsgerr *err, void *arg)
 }
 
 static struct nl_msg *
-_nl802154_alloc_msg(int id, int ifindex, guint32 cmd, guint32 flags)
+_nl802154_alloc_msg(guint16 genl_family_id, int ifindex, uint8_t cmd, uint16_t flags)
 {
     nm_auto_nlmsg struct nl_msg *msg = NULL;
 
     msg = nlmsg_alloc();
-    genlmsg_put(msg, 0, 0, id, 0, flags, cmd, 0);
+    if (!genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, genl_family_id, 0, flags, cmd, 0))
+        goto nla_put_failure;
     NLA_PUT_U32(msg, NL802154_ATTR_IFINDEX, ifindex);
     return g_steal_pointer(&msg);
 
@@ -89,15 +90,15 @@ nla_put_failure:
 }
 
 static struct nl_msg *
-nl802154_alloc_msg(NMWpanUtils *self, guint32 cmd, guint32 flags)
+nl802154_alloc_msg(NMWpanUtils *self, uint8_t cmd, uint16_t flags)
 {
-    return _nl802154_alloc_msg(self->id, self->ifindex, cmd, flags);
+    return _nl802154_alloc_msg(self->genl_family_id, self->ifindex, cmd, flags);
 }
 
 static int
 nl802154_send_and_recv(NMWpanUtils   *self,
                        struct nl_msg *msg,
-                       int (*valid_handler)(struct nl_msg *, void *),
+                       int (*valid_handler)(const struct nl_msg *, void *),
                        void *valid_data)
 {
     int                err;
@@ -143,7 +144,7 @@ struct nl802154_interface {
 };
 
 static int
-nl802154_get_interface_handler(struct nl_msg *msg, void *arg)
+nl802154_get_interface_handler(const struct nl_msg *msg, void *arg)
 {
     static const struct nla_policy nl802154_policy[] = {
         [NL802154_ATTR_PAN_ID]     = {.type = NLA_U16},
@@ -264,7 +265,7 @@ nm_wpan_utils_class_init(NMWpanUtilsClass *klass)
 {}
 
 NMWpanUtils *
-nm_wpan_utils_new(int ifindex, struct nl_sock *genl, gboolean check_scan)
+nm_wpan_utils_new(struct nl_sock *genl, guint16 genl_family_id, int ifindex, gboolean check_scan)
 {
     NMWpanUtils *self;
 
@@ -273,16 +274,13 @@ nm_wpan_utils_new(int ifindex, struct nl_sock *genl, gboolean check_scan)
     if (!genl)
         return NULL;
 
-    self          = g_object_new(NM_TYPE_WPAN_UTILS, NULL);
-    self->ifindex = ifindex;
-    self->nl_sock = genl;
-    self->id      = genl_ctrl_resolve(genl, "nl802154");
-
-    if (self->id < 0) {
-        _LOGD(LOGD_PLATFORM, "genl_ctrl_resolve: failed to resolve \"nl802154\"");
-        g_object_unref(self);
+    if (!genl_family_id)
         return NULL;
-    }
+
+    self                 = g_object_new(NM_TYPE_WPAN_UTILS, NULL);
+    self->ifindex        = ifindex;
+    self->nl_sock        = genl;
+    self->genl_family_id = genl_family_id;
 
     return self;
 }
diff --git a/src/libnm-platform/wpan/nm-wpan-utils.h b/src/libnm-platform/wpan/nm-wpan-utils.h
index 6130c41e..eea5587d 100644
--- a/src/libnm-platform/wpan/nm-wpan-utils.h
+++ b/src/libnm-platform/wpan/nm-wpan-utils.h
@@ -6,8 +6,6 @@
 #ifndef __WPAN_UTILS_H__
 #define __WPAN_UTILS_H__
 
-#include <net/ethernet.h>
-
 #include "libnm-platform/nm-netlink.h"
 
 typedef struct NMWpanUtils NMWpanUtils;
@@ -23,7 +21,8 @@ typedef struct NMWpanUtils NMWpanUtils;
 
 GType nm_wpan_utils_get_type(void);
 
-NMWpanUtils *nm_wpan_utils_new(int ifindex, struct nl_sock *genl, gboolean check_scan);
+NMWpanUtils *
+nm_wpan_utils_new(struct nl_sock *genl, guint16 genl_family_id, int ifindex, gboolean check_scan);
 
 guint16  nm_wpan_utils_get_pan_id(NMWpanUtils *self);
 gboolean nm_wpan_utils_set_pan_id(NMWpanUtils *self, guint16 pan_id);