about summary refs log tree commit diff
path: root/src/core
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2023-02-10 11:50:34 +0100
committerMichael Biebl <biebl@debian.org>2023-02-10 11:50:34 +0100
commit1372848511cb896b80b51ed1a3e9606bd9816631 (patch)
tree674792b9385bdef935988894b45f06b2af39f88c /src/core
parent40ec077ea305994c1fc2130add6787ca0c73e2c6 (diff)
New upstream version 1.42.0 upstream/1.42.0
Diffstat (limited to 'src/core')
-rw-r--r--src/core/NetworkManagerUtils.c73
-rw-r--r--src/core/devices/adsl/nm-atm-manager.c3
-rw-r--r--src/core/devices/adsl/nm-device-adsl.c12
-rw-r--r--src/core/devices/adsl/nm-device-adsl.h3
-rw-r--r--src/core/devices/bluetooth/nm-bluez-manager.c5
-rw-r--r--src/core/devices/bluetooth/nm-bluez-manager.h2
-rw-r--r--src/core/devices/bluetooth/nm-device-bt.h2
-rw-r--r--src/core/devices/nm-device-6lowpan.c39
-rw-r--r--src/core/devices/nm-device-6lowpan.h2
-rw-r--r--src/core/devices/nm-device-bond.c140
-rw-r--r--src/core/devices/nm-device-bond.h3
-rw-r--r--src/core/devices/nm-device-bridge.c11
-rw-r--r--src/core/devices/nm-device-bridge.h2
-rw-r--r--src/core/devices/nm-device-dummy.c2
-rw-r--r--src/core/devices/nm-device-dummy.h2
-rw-r--r--src/core/devices/nm-device-ethernet.c15
-rw-r--r--src/core/devices/nm-device-ethernet.h2
-rw-r--r--src/core/devices/nm-device-factory.c1
-rw-r--r--src/core/devices/nm-device-factory.h5
-rw-r--r--src/core/devices/nm-device-generic.h2
-rw-r--r--src/core/devices/nm-device-infiniband.c8
-rw-r--r--src/core/devices/nm-device-infiniband.h2
-rw-r--r--src/core/devices/nm-device-ip-tunnel.c277
-rw-r--r--src/core/devices/nm-device-ip-tunnel.h3
-rw-r--r--src/core/devices/nm-device-logging.h13
-rw-r--r--src/core/devices/nm-device-loopback.c166
-rw-r--r--src/core/devices/nm-device-loopback.h27
-rw-r--r--src/core/devices/nm-device-macsec.c39
-rw-r--r--src/core/devices/nm-device-macsec.h2
-rw-r--r--src/core/devices/nm-device-macvlan.c39
-rw-r--r--src/core/devices/nm-device-macvlan.h2
-rw-r--r--src/core/devices/nm-device-ppp.c2
-rw-r--r--src/core/devices/nm-device-ppp.h2
-rw-r--r--src/core/devices/nm-device-tun.c2
-rw-r--r--src/core/devices/nm-device-tun.h2
-rw-r--r--src/core/devices/nm-device-utils.c42
-rw-r--r--src/core/devices/nm-device-veth.c2
-rw-r--r--src/core/devices/nm-device-veth.h3
-rw-r--r--src/core/devices/nm-device-vlan.c58
-rw-r--r--src/core/devices/nm-device-vlan.h3
-rw-r--r--src/core/devices/nm-device-vrf.c2
-rw-r--r--src/core/devices/nm-device-vrf.h2
-rw-r--r--src/core/devices/nm-device-vxlan.c30
-rw-r--r--src/core/devices/nm-device-vxlan.h2
-rw-r--r--src/core/devices/nm-device-wireguard.c25
-rw-r--r--src/core/devices/nm-device-wireguard.h2
-rw-r--r--src/core/devices/nm-device-wpan.c2
-rw-r--r--src/core/devices/nm-device-wpan.h3
-rw-r--r--src/core/devices/nm-device.c353
-rw-r--r--src/core/devices/nm-device.h28
-rw-r--r--src/core/devices/nm-lldp-listener.c332
-rw-r--r--src/core/devices/ovs/nm-device-ovs-bridge.c16
-rw-r--r--src/core/devices/ovs/nm-device-ovs-bridge.h2
-rw-r--r--src/core/devices/ovs/nm-device-ovs-interface.h2
-rw-r--r--src/core/devices/ovs/nm-device-ovs-port.h2
-rw-r--r--src/core/devices/ovs/nm-ovs-factory.c3
-rw-r--r--src/core/devices/ovs/nm-ovsdb.c581
-rw-r--r--src/core/devices/ovs/nm-ovsdb.h18
-rw-r--r--src/core/devices/team/nm-device-team.c62
-rw-r--r--src/core/devices/team/nm-device-team.h3
-rw-r--r--src/core/devices/team/nm-team-factory.c2
-rw-r--r--src/core/devices/tests/test-lldp.c16
-rw-r--r--src/core/devices/wifi/nm-device-iwd-p2p.h2
-rw-r--r--src/core/devices/wifi/nm-device-iwd.c466
-rw-r--r--src/core/devices/wifi/nm-device-iwd.h4
-rw-r--r--src/core/devices/wifi/nm-device-olpc-mesh.h2
-rw-r--r--src/core/devices/wifi/nm-device-wifi-p2p.c1
-rw-r--r--src/core/devices/wifi/nm-device-wifi-p2p.h2
-rw-r--r--src/core/devices/wifi/nm-device-wifi.c49
-rw-r--r--src/core/devices/wifi/nm-device-wifi.h3
-rw-r--r--src/core/devices/wifi/nm-iwd-manager.c261
-rw-r--r--src/core/devices/wifi/nm-iwd-manager.h7
-rw-r--r--src/core/devices/wifi/nm-wifi-ap.h2
-rw-r--r--src/core/devices/wifi/nm-wifi-factory.c2
-rw-r--r--src/core/devices/wifi/nm-wifi-p2p-peer.h2
-rw-r--r--src/core/devices/wifi/nm-wifi-utils.c175
-rw-r--r--src/core/devices/wwan/nm-device-modem.c24
-rw-r--r--src/core/devices/wwan/nm-device-modem.h2
-rw-r--r--src/core/devices/wwan/nm-modem-broadband.c13
-rw-r--r--src/core/devices/wwan/nm-modem-broadband.h2
-rw-r--r--src/core/devices/wwan/nm-modem-manager.h2
-rw-r--r--src/core/devices/wwan/nm-modem-ofono.c740
-rw-r--r--src/core/devices/wwan/nm-modem-ofono.h5
-rw-r--r--src/core/devices/wwan/nm-modem.c10
-rw-r--r--src/core/devices/wwan/nm-modem.h2
-rw-r--r--src/core/devices/wwan/nm-wwan-factory.c2
-rw-r--r--src/core/dhcp/nm-dhcp-client.c229
-rw-r--r--src/core/dhcp/nm-dhcp-client.h23
-rw-r--r--src/core/dhcp/nm-dhcp-dhclient.c2
-rw-r--r--src/core/dhcp/nm-dhcp-dhcpcanon.c2
-rw-r--r--src/core/dhcp/nm-dhcp-dhcpcd.c3
-rw-r--r--src/core/dhcp/nm-dhcp-listener.h2
-rw-r--r--src/core/dhcp/nm-dhcp-manager.h2
-rw-r--r--src/core/dhcp/nm-dhcp-nettools.c60
-rw-r--r--src/core/dhcp/nm-dhcp-options.c4
-rw-r--r--src/core/dhcp/nm-dhcp-systemd.c12
-rw-r--r--src/core/dhcp/nm-dhcp-utils.c30
-rw-r--r--src/core/dhcp/tests/test-dhcp-utils.c41
-rw-r--r--src/core/dns/nm-dns-dnsmasq.c34
-rw-r--r--src/core/dns/nm-dns-dnsmasq.h3
-rw-r--r--src/core/dns/nm-dns-manager.c128
-rw-r--r--src/core/dns/nm-dns-manager.h12
-rw-r--r--src/core/dns/nm-dns-plugin.h2
-rw-r--r--src/core/dns/nm-dns-systemd-resolved.c216
-rw-r--r--src/core/dns/nm-dns-systemd-resolved.h2
-rw-r--r--src/core/dnsmasq/nm-dnsmasq-manager.c12
-rw-r--r--src/core/dnsmasq/nm-dnsmasq-manager.h2
-rw-r--r--src/core/dnsmasq/nm-dnsmasq-utils.c6
-rw-r--r--src/core/main-utils.c31
-rw-r--r--src/core/meson.build5
-rw-r--r--src/core/ndisc/nm-fake-ndisc.c8
-rw-r--r--src/core/ndisc/nm-fake-ndisc.h2
-rw-r--r--src/core/ndisc/nm-lndp-ndisc.c16
-rw-r--r--src/core/ndisc/nm-lndp-ndisc.h2
-rw-r--r--src/core/ndisc/nm-ndisc.c189
-rw-r--r--src/core/ndisc/nm-ndisc.h10
-rw-r--r--src/core/nm-act-request.h3
-rw-r--r--src/core/nm-active-connection.c32
-rw-r--r--src/core/nm-active-connection.h2
-rw-r--r--src/core/nm-audit-manager.h2
-rw-r--r--src/core/nm-auth-manager.c2
-rw-r--r--src/core/nm-auth-manager.h2
-rw-r--r--src/core/nm-bond-manager.c967
-rw-r--r--src/core/nm-bond-manager.h32
-rw-r--r--src/core/nm-checkpoint.c2
-rw-r--r--src/core/nm-checkpoint.h2
-rw-r--r--src/core/nm-config-data.c19
-rw-r--r--src/core/nm-config-data.h3
-rw-r--r--src/core/nm-config.c30
-rw-r--r--src/core/nm-config.h2
-rw-r--r--src/core/nm-connectivity.c8
-rw-r--r--src/core/nm-connectivity.h2
-rw-r--r--src/core/nm-core-utils.c36
-rw-r--r--src/core/nm-core-utils.h2
-rw-r--r--src/core/nm-dbus-manager.c2
-rw-r--r--src/core/nm-dbus-manager.h3
-rw-r--r--src/core/nm-dbus-object.h3
-rw-r--r--src/core/nm-dbus-utils.c2
-rw-r--r--src/core/nm-default-daemon.h1
-rw-r--r--src/core/nm-dhcp-config.c4
-rw-r--r--src/core/nm-dhcp-config.h3
-rw-r--r--src/core/nm-dispatcher.c22
-rw-r--r--src/core/nm-dispatcher.h3
-rw-r--r--src/core/nm-firewall-utils.c252
-rw-r--r--src/core/nm-firewall-utils.h6
-rw-r--r--src/core/nm-firewalld-manager.h2
-rw-r--r--src/core/nm-hostname-manager.c83
-rw-r--r--src/core/nm-hostname-manager.h18
-rw-r--r--src/core/nm-ip-config.c178
-rw-r--r--src/core/nm-ip-config.h4
-rw-r--r--src/core/nm-keep-alive.h2
-rw-r--r--src/core/nm-l3-config-data.c282
-rw-r--r--src/core/nm-l3-config-data.h17
-rw-r--r--src/core/nm-l3-ipv4ll.c46
-rw-r--r--src/core/nm-l3-ipv4ll.h2
-rw-r--r--src/core/nm-l3-ipv6ll.c15
-rw-r--r--src/core/nm-l3cfg.c287
-rw-r--r--src/core/nm-l3cfg.h14
-rw-r--r--src/core/nm-manager.c109
-rw-r--r--src/core/nm-manager.h3
-rw-r--r--src/core/nm-netns.c595
-rw-r--r--src/core/nm-netns.h10
-rw-r--r--src/core/nm-pacrunner-manager.c6
-rw-r--r--src/core/nm-pacrunner-manager.h2
-rw-r--r--src/core/nm-policy.c75
-rw-r--r--src/core/nm-policy.h2
-rw-r--r--src/core/nm-rfkill-manager.h2
-rw-r--r--src/core/nm-session-monitor.h2
-rw-r--r--src/core/nm-sleep-monitor.c31
-rw-r--r--src/core/nm-sleep-monitor.h3
-rw-r--r--src/core/nm-test-utils-core.h24
-rw-r--r--src/core/platform/nm-fake-platform.c47
-rw-r--r--src/core/platform/nm-fake-platform.h2
-rw-r--r--src/core/platform/tests/meson.build2
-rw-r--r--src/core/platform/tests/monitor.c165
-rw-r--r--src/core/platform/tests/test-address.c6
-rw-r--r--src/core/platform/tests/test-cleanup.c4
-rw-r--r--src/core/platform/tests/test-common.c985
-rw-r--r--src/core/platform/tests/test-common.h190
-rw-r--r--src/core/platform/tests/test-link.c275
-rw-r--r--src/core/platform/tests/test-platform-general.c932
-rw-r--r--src/core/platform/tests/test-route.c397
-rw-r--r--src/core/platform/tests/test-tc.c8
-rw-r--r--src/core/ppp/nm-ppp-manager.c5
-rw-r--r--src/core/ppp/nm-ppp-mgr.c2
-rw-r--r--src/core/settings/nm-agent-manager.c6
-rw-r--r--src/core/settings/nm-agent-manager.h2
-rw-r--r--src/core/settings/nm-secret-agent.h2
-rw-r--r--src/core/settings/nm-settings-connection.h2
-rw-r--r--src/core/settings/nm-settings-plugin.h2
-rw-r--r--src/core/settings/nm-settings-storage.h2
-rw-r--r--src/core/settings/nm-settings.c4
-rw-r--r--src/core/settings/nm-settings.h2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-plugin.h2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c92
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-storage.h2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c15
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.h2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c22
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Vlan_test-vlan-interface.cexpected1
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-netmask-13
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-netmask-1.cexpected3
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-vlan-interface1
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wired-static-routes2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/route-test-wired-static-routes2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c41
-rw-r--r--src/core/settings/plugins/ifupdown/nms-ifupdown-parser.c6
-rw-r--r--src/core/settings/plugins/ifupdown/nms-ifupdown-plugin.h2
-rw-r--r--src/core/settings/plugins/keyfile/nms-keyfile-plugin.h2
-rw-r--r--src/core/settings/plugins/keyfile/nms-keyfile-storage.h2
-rw-r--r--src/core/settings/plugins/keyfile/tests/keyfiles/Test_Wired_Connection2
-rw-r--r--src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c4
-rw-r--r--src/core/supplicant/nm-supplicant-config.c32
-rw-r--r--src/core/supplicant/nm-supplicant-config.h2
-rw-r--r--src/core/supplicant/nm-supplicant-interface.c4
-rw-r--r--src/core/supplicant/nm-supplicant-interface.h2
-rw-r--r--src/core/supplicant/nm-supplicant-manager.c4
-rw-r--r--src/core/supplicant/nm-supplicant-manager.h2
-rw-r--r--src/core/supplicant/nm-supplicant-settings-verify.c18
-rw-r--r--src/core/tests/config/global-dns-good.conf13
-rw-r--r--src/core/tests/config/global-dns-options.conf5
-rw-r--r--src/core/tests/config/nm-test-device.h3
-rw-r--r--src/core/tests/config/test-config.c22
-rw-r--r--src/core/tests/test-core-with-expect.c28
-rw-r--r--src/core/tests/test-core.c31
-rw-r--r--src/core/tests/test-l3cfg.c37
-rw-r--r--src/core/tests/test-systemd.c16
-rw-r--r--src/core/vpn/nm-vpn-connection.c81
-rw-r--r--src/core/vpn/nm-vpn-connection.h2
-rw-r--r--src/core/vpn/nm-vpn-manager.h3
230 files changed, 8728 insertions, 3466 deletions
diff --git a/src/core/NetworkManagerUtils.c b/src/core/NetworkManagerUtils.c
index f5b7666b..6f4c60f8 100644
--- a/src/core/NetworkManagerUtils.c
+++ b/src/core/NetworkManagerUtils.c
@@ -419,8 +419,8 @@ route_compare(NMIPRoute *route1, NMIPRoute *route2, gint64 default_metric)
         nm_assert_not_reached();
     if (!inet_pton(family, nm_ip_route_get_dest(route2), &a2))
         nm_assert_not_reached();
-    nm_utils_ipx_address_clear_host_address(family, &a1, NULL, plen);
-    nm_utils_ipx_address_clear_host_address(family, &a2, NULL, plen);
+    nm_ip_addr_clear_host_address(family, &a1, NULL, plen);
+    nm_ip_addr_clear_host_address(family, &a2, NULL, plen);
     NM_CMP_DIRECT_MEMCMP(&a1, &a2, nm_utils_addr_family_to_size(family));
 
     return 0;
@@ -1382,11 +1382,16 @@ nm_utils_ip_route_attribute_to_platform(int                addr_family,
 
         GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_TOS, r4->tos, BYTE, byte, 0);
         GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_SCOPE, scope, BYTE, byte, RT_SCOPE_NOWHERE);
+        GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_WEIGHT, r4->weight, UINT32, uint32, 0);
         r4->scope_inv = nm_platform_route_scope_inv(scope);
     }
 
+    /* Note that for IPv4 routes in kernel, the onlink flag can be set for
+     * each next hop separately (rtnh_flags). Not for NetworkManager. We can
+     * only merge routes as ECMP routes (when setting a weight) if they all
+     * share the same onlink flag. See NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID.
+     * That simplifies the code. */
     GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_ONLINK, onlink, BOOLEAN, boolean, FALSE);
-
     r->r_rtm_flags = ((onlink) ? (unsigned) RTNH_F_ONLINK : 0u);
 
     GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_ADVMSS, r->mss, UINT32, uint32, 0);
@@ -1418,11 +1423,11 @@ nm_utils_ip_route_attribute_to_platform(int                addr_family,
         && g_variant_is_of_type(variant, G_VARIANT_TYPE_STRING)) {
         int prefix;
 
-        if (nm_utils_parse_inaddr_prefix_bin(addr_family,
-                                             g_variant_get_string(variant, NULL),
-                                             NULL,
-                                             &addr,
-                                             &prefix)) {
+        if (nm_inet_parse_with_prefix_bin(addr_family,
+                                          g_variant_get_string(variant, NULL),
+                                          NULL,
+                                          &addr,
+                                          &prefix)) {
             if (prefix < 0)
                 prefix = 128;
             r6->src      = addr.addr6;
@@ -1444,7 +1449,7 @@ nm_utils_ip_addresses_to_dbus(int                          addr_family,
     const int        IS_IPv4 = NM_IS_IPv4(addr_family);
     GVariantBuilder  builder_data;
     GVariantBuilder  builder_legacy;
-    char             addr_str[NM_UTILS_INET_ADDRSTRLEN];
+    char             addr_str[NM_INET_ADDRSTRLEN];
     NMDedupMultiIter iter;
     const NMPObject *obj;
     guint            i;
@@ -1479,8 +1484,7 @@ nm_utils_ip_addresses_to_dbus(int                          addr_family,
                 &addr_builder,
                 "{sv}",
                 "address",
-                g_variant_new_string(
-                    nm_utils_inet_ntop(addr_family, address->ax.address_ptr, addr_str)));
+                g_variant_new_string(nm_inet_ntop(addr_family, address->ax.address_ptr, addr_str)));
 
             g_variant_builder_add(&addr_builder,
                                   "{sv}",
@@ -1497,11 +1501,10 @@ nm_utils_ip_addresses_to_dbus(int                          addr_family,
                     p = &address->a6.peer_address;
             }
             if (p) {
-                g_variant_builder_add(
-                    &addr_builder,
-                    "{sv}",
-                    "peer",
-                    g_variant_new_string(nm_utils_inet_ntop(addr_family, p, addr_str)));
+                g_variant_builder_add(&addr_builder,
+                                      "{sv}",
+                                      "peer",
+                                      g_variant_new_string(nm_inet_ntop(addr_family, p, addr_str)));
             }
 
             if (IS_IPv4) {
@@ -1559,7 +1562,7 @@ nm_utils_ip_routes_to_dbus(int                          addr_family,
     const NMPObject *obj;
     GVariantBuilder  builder_data;
     GVariantBuilder  builder_legacy;
-    char             addr_str[NM_UTILS_INET_ADDRSTRLEN];
+    char             addr_str[NM_INET_ADDRSTRLEN];
 
     nm_assert_addr_family(addr_family);
 
@@ -1580,14 +1583,12 @@ nm_utils_ip_routes_to_dbus(int                          addr_family,
         nm_assert(r);
         nm_assert(r->rx.plen <= 8u * nm_utils_addr_family_to_size(addr_family));
         nm_assert(!IS_IPv4
-                  || r->r4.network
-                         == nm_utils_ip4_address_clear_host_address(r->r4.network, r->r4.plen));
-        nm_assert(
-            IS_IPv4
-            || (memcmp(&r->r6.network,
-                       nm_utils_ip6_address_clear_host_address(&n, &r->r6.network, r->r6.plen),
-                       sizeof(n))
-                == 0));
+                  || r->r4.network == nm_ip4_addr_clear_host_address(r->r4.network, r->r4.plen));
+        nm_assert(IS_IPv4
+                  || (memcmp(&r->r6.network,
+                             nm_ip6_addr_clear_host_address(&n, &r->r6.network, r->r6.plen),
+                             sizeof(n))
+                      == 0));
 
         if (r->rx.type_coerced != nm_platform_route_type_coerce(RTN_UNICAST))
             continue;
@@ -1602,7 +1603,7 @@ nm_utils_ip_routes_to_dbus(int                          addr_family,
                 &route_builder,
                 "{sv}",
                 "dest",
-                g_variant_new_string(nm_utils_inet_ntop(addr_family, r->rx.network_ptr, addr_str)));
+                g_variant_new_string(nm_inet_ntop(addr_family, r->rx.network_ptr, addr_str)));
 
             g_variant_builder_add(&route_builder,
                                   "{sv}",
@@ -1615,7 +1616,7 @@ nm_utils_ip_routes_to_dbus(int                          addr_family,
                     &route_builder,
                     "{sv}",
                     "next-hop",
-                    g_variant_new_string(nm_utils_inet_ntop(addr_family, gateway, addr_str)));
+                    g_variant_new_string(nm_inet_ntop(addr_family, gateway, addr_str)));
             }
 
             g_variant_builder_add(&route_builder,
@@ -1680,7 +1681,7 @@ nm_utils_platform_capture_ip_setting(NMPlatform *platform,
     NMDedupMultiIter                   iter;
     const NMPObject                   *obj;
     const char                        *method = NULL;
-    char                               sbuf[NM_UTILS_INET_ADDRSTRLEN];
+    char                               sbuf[NM_INET_ADDRSTRLEN];
     const NMPlatformIPXRoute          *best_default_route = NULL;
 
     s_ip =
@@ -1782,13 +1783,13 @@ nm_utils_platform_capture_ip_setting(NMPlatform *platform,
     }
 
     if (best_default_route && nm_setting_ip_config_get_num_addresses(s_ip) > 0) {
-        g_object_set(s_ip,
-                     NM_SETTING_IP_CONFIG_GATEWAY,
-                     nm_utils_inet_ntop(
-                         addr_family,
+        g_object_set(
+            s_ip,
+            NM_SETTING_IP_CONFIG_GATEWAY,
+            nm_inet_ntop(addr_family,
                          nm_platform_ip_route_get_gateway(addr_family, &best_default_route->rx),
                          sbuf),
-                     NULL);
+            NULL);
     }
 
     return NM_SETTING(g_steal_pointer(&s_ip));
@@ -1839,7 +1840,7 @@ nm_platform_setup(NMPlatform *instance)
  * Returns: (transfer none): The #NMPlatform singleton reference.
  */
 NMPlatform *
-nm_platform_get()
+nm_platform_get(void)
 {
     g_assert(singleton_instance);
 
@@ -1851,11 +1852,11 @@ nm_platform_get()
 void
 nm_linux_platform_setup(void)
 {
-    nm_platform_setup(nm_linux_platform_new(FALSE, FALSE, FALSE));
+    nm_platform_setup(nm_linux_platform_new(NULL, FALSE, FALSE, FALSE));
 }
 
 void
 nm_linux_platform_setup_with_tc_cache(void)
 {
-    nm_platform_setup(nm_linux_platform_new(FALSE, FALSE, TRUE));
+    nm_platform_setup(nm_linux_platform_new(NULL, FALSE, FALSE, TRUE));
 }
diff --git a/src/core/devices/adsl/nm-atm-manager.c b/src/core/devices/adsl/nm-atm-manager.c
index cc8d37e6..f3c0d367 100644
--- a/src/core/devices/adsl/nm-atm-manager.c
+++ b/src/core/devices/adsl/nm-atm-manager.c
@@ -17,7 +17,8 @@
 /*****************************************************************************/
 
 #define NM_TYPE_ATM_MANAGER (nm_atm_manager_get_type())
-#define NM_ATM_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_ATM_MANAGER, NMAtmManager))
+#define NM_ATM_MANAGER(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_ATM_MANAGER, NMAtmManager))
 #define NM_ATM_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_ATM_MANAGER, NMAtmManagerClass))
 #define NM_IS_ATM_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_ATM_MANAGER))
diff --git a/src/core/devices/adsl/nm-device-adsl.c b/src/core/devices/adsl/nm-device-adsl.c
index 79705c28..fcd16b1c 100644
--- a/src/core/devices/adsl/nm-device-adsl.c
+++ b/src/core/devices/adsl/nm-device-adsl.c
@@ -214,12 +214,12 @@ error:
 }
 
 static void
-link_changed_cb(NMPlatform     *platform,
-                int             obj_type_i,
-                int             ifindex,
-                NMPlatformLink *info,
-                int             change_type_i,
-                NMDeviceAdsl   *self)
+link_changed_cb(NMPlatform           *platform,
+                int                   obj_type_i,
+                int                   ifindex,
+                const NMPlatformLink *info,
+                int                   change_type_i,
+                NMDeviceAdsl         *self)
 {
     const NMPlatformSignalChangeType change_type = change_type_i;
 
diff --git a/src/core/devices/adsl/nm-device-adsl.h b/src/core/devices/adsl/nm-device-adsl.h
index c5c18901..287a1401 100644
--- a/src/core/devices/adsl/nm-device-adsl.h
+++ b/src/core/devices/adsl/nm-device-adsl.h
@@ -10,7 +10,8 @@
 #include "devices/nm-device.h"
 
 #define NM_TYPE_DEVICE_ADSL (nm_device_adsl_get_type())
-#define NM_DEVICE_ADSL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_ADSL, NMDeviceAdsl))
+#define NM_DEVICE_ADSL(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_ADSL, NMDeviceAdsl))
 #define NM_DEVICE_ADSL_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_ADSL, NMDeviceAdslClass))
 #define NM_IS_DEVICE_ADSL(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_ADSL))
diff --git a/src/core/devices/bluetooth/nm-bluez-manager.c b/src/core/devices/bluetooth/nm-bluez-manager.c
index 6bc00faf..ac26ede8 100644
--- a/src/core/devices/bluetooth/nm-bluez-manager.c
+++ b/src/core/devices/bluetooth/nm-bluez-manager.c
@@ -1010,8 +1010,9 @@ _network_server_get_bluez_manager(const NMBtVTableNetworkServer *vtable_network_
 {
     NMBluezManager *self;
 
-    self = (NMBluezManager *) (((char *) vtable_network_server)
-                               - G_STRUCT_OFFSET(NMBluezManager, _priv.vtable_network_server));
+    self = NM_CAST_ALIGN(NMBluezManager,
+                         (((char *) vtable_network_server)
+                          - G_STRUCT_OFFSET(NMBluezManager, _priv.vtable_network_server)));
 
     g_return_val_if_fail(NM_IS_BLUEZ_MANAGER(self), NULL);
 
diff --git a/src/core/devices/bluetooth/nm-bluez-manager.h b/src/core/devices/bluetooth/nm-bluez-manager.h
index 299f5c3b..7c823c64 100644
--- a/src/core/devices/bluetooth/nm-bluez-manager.h
+++ b/src/core/devices/bluetooth/nm-bluez-manager.h
@@ -8,7 +8,7 @@
 
 #define NM_TYPE_BLUEZ_MANAGER (nm_bluez_manager_get_type())
 #define NM_BLUEZ_MANAGER(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_BLUEZ_MANAGER, NMBluezManager))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_BLUEZ_MANAGER, NMBluezManager))
 #define NM_BLUEZ_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_BLUEZ_MANAGER, NMBluezManagerClass))
 #define NM_IS_BLUEZ_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_BLUEZ_MANAGER))
diff --git a/src/core/devices/bluetooth/nm-device-bt.h b/src/core/devices/bluetooth/nm-device-bt.h
index 59457fba..bf17e91f 100644
--- a/src/core/devices/bluetooth/nm-device-bt.h
+++ b/src/core/devices/bluetooth/nm-device-bt.h
@@ -9,7 +9,7 @@
 #include "devices/nm-device.h"
 
 #define NM_TYPE_DEVICE_BT (nm_device_bt_get_type())
-#define NM_DEVICE_BT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_BT, NMDeviceBt))
+#define NM_DEVICE_BT(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_BT, NMDeviceBt))
 #define NM_DEVICE_BT_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_BT, NMDeviceBtClass))
 #define NM_IS_DEVICE_BT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_BT))
diff --git a/src/core/devices/nm-device-6lowpan.c b/src/core/devices/nm-device-6lowpan.c
index d6543356..870a1c14 100644
--- a/src/core/devices/nm-device-6lowpan.c
+++ b/src/core/devices/nm-device-6lowpan.c
@@ -21,7 +21,6 @@
 /*****************************************************************************/
 
 typedef struct {
-    gulong parent_state_id;
 } NMDevice6LowpanPrivate;
 
 struct _NMDevice6Lowpan {
@@ -41,51 +40,15 @@ G_DEFINE_TYPE(NMDevice6Lowpan, nm_device_6lowpan, NM_TYPE_DEVICE)
 /*****************************************************************************/
 
 static void
-parent_state_changed(NMDevice           *parent,
-                     NMDeviceState       new_state,
-                     NMDeviceState       old_state,
-                     NMDeviceStateReason reason,
-                     gpointer            user_data)
-{
-    NMDevice6Lowpan *self = NM_DEVICE_6LOWPAN(user_data);
-
-    nm_device_set_unmanaged_by_flags(NM_DEVICE(self),
-                                     NM_UNMANAGED_PARENT,
-                                     !nm_device_get_managed(parent, FALSE),
-                                     reason);
-}
-
-static void
 parent_changed_notify(NMDevice *device,
                       int       old_ifindex,
                       NMDevice *old_parent,
                       int       new_ifindex,
                       NMDevice *new_parent)
 {
-    NMDevice6Lowpan        *self = NM_DEVICE_6LOWPAN(device);
-    NMDevice6LowpanPrivate *priv = NM_DEVICE_6LOWPAN_GET_PRIVATE(self);
-
     NM_DEVICE_CLASS(nm_device_6lowpan_parent_class)
         ->parent_changed_notify(device, old_ifindex, old_parent, new_ifindex, new_parent);
 
-    /*  note that @self doesn't have to clear @parent_state_id on dispose,
-     *  because NMDevice's dispose() will unset the parent, which in turn calls
-     *  parent_changed_notify(). */
-    nm_clear_g_signal_handler(old_parent, &priv->parent_state_id);
-
-    if (new_parent) {
-        priv->parent_state_id = g_signal_connect(new_parent,
-                                                 NM_DEVICE_STATE_CHANGED,
-                                                 G_CALLBACK(parent_state_changed),
-                                                 device);
-
-        /* Set parent-dependent unmanaged flag */
-        nm_device_set_unmanaged_by_flags(device,
-                                         NM_UNMANAGED_PARENT,
-                                         !nm_device_get_managed(new_parent, FALSE),
-                                         NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED);
-    }
-
     if (new_ifindex > 0) {
         /* Recheck availability now that the parent has changed */
         nm_device_queue_recheck_available(device,
@@ -272,7 +235,7 @@ nm_device_6lowpan_class_init(NMDevice6LowpanClass *klass)
 
 #define NM_TYPE_6LOWPAN_DEVICE_FACTORY (nm_6lowpan_device_factory_get_type())
 #define NM_6LOWPAN_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_6LOWPAN_DEVICE_FACTORY, NM6LowpanDeviceFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_6LOWPAN_DEVICE_FACTORY, NM6LowpanDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-6lowpan.h b/src/core/devices/nm-device-6lowpan.h
index 34ea3c8d..bc6f77c4 100644
--- a/src/core/devices/nm-device-6lowpan.h
+++ b/src/core/devices/nm-device-6lowpan.h
@@ -10,7 +10,7 @@
 
 #define NM_TYPE_DEVICE_6LOWPAN (nm_device_6lowpan_get_type())
 #define NM_DEVICE_6LOWPAN(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_6LOWPAN, NMDevice6Lowpan))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_6LOWPAN, NMDevice6Lowpan))
 #define NM_DEVICE_6LOWPAN_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_6LOWPAN, NMDevice6LowpanClass))
 #define NM_IS_DEVICE_6LOWPAN(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_6LOWPAN))
diff --git a/src/core/devices/nm-device-bond.c b/src/core/devices/nm-device-bond.c
index 10765b60..0b0697e3 100644
--- a/src/core/devices/nm-device-bond.c
+++ b/src/core/devices/nm-device-bond.c
@@ -20,6 +20,7 @@
 #include "libnm-core-intern/nm-core-internal.h"
 #include "nm-manager.h"
 #include "nm-setting-bond-port.h"
+#include "nm-bond-manager.h"
 
 #define _NMLOG_DEVICE_TYPE NMDeviceBond
 #include "nm-device-logging.h"
@@ -59,7 +60,8 @@
 /*****************************************************************************/
 
 struct _NMDeviceBond {
-    NMDevice parent;
+    NMDevice       parent;
+    NMBondManager *bond_manager;
 };
 
 struct _NMDeviceBondClass {
@@ -178,7 +180,9 @@ update_connection(NMDevice *device, NMConnection *connection)
         gs_free char *value  = NULL;
         char         *p;
 
-        if (NM_IN_STRSET(option, NM_SETTING_BOND_OPTION_ACTIVE_SLAVE))
+        if (NM_IN_STRSET(option,
+                         NM_SETTING_BOND_OPTION_ACTIVE_SLAVE,
+                         NM_SETTING_BOND_OPTION_BALANCE_SLB))
             continue;
 
         value =
@@ -224,9 +228,10 @@ controller_update_port_connection(NMDevice     *self,
                                   GError      **error)
 {
     NMSettingBondPort *s_port;
-    int                ifindex_port = nm_device_get_ifindex(port);
-    uint               queue_id     = NM_BOND_PORT_QUEUE_ID_DEF;
-    gs_free char      *queue_id_str = NULL;
+    int                ifindex_port       = nm_device_get_ifindex(port);
+    NMConnection      *applied_connection = nm_device_get_applied_connection(self);
+    uint               queue_id           = NM_BOND_PORT_QUEUE_ID_DEF;
+    gs_free char      *queue_id_str       = NULL;
 
     g_return_val_if_fail(ifindex_port > 0, FALSE);
 
@@ -243,7 +248,7 @@ controller_update_port_connection(NMDevice     *self,
 
     g_object_set(nm_connection_get_setting_connection(connection),
                  NM_SETTING_CONNECTION_MASTER,
-                 nm_device_get_iface(self),
+                 nm_connection_get_uuid(applied_connection),
                  NM_SETTING_CONNECTION_SLAVE_TYPE,
                  NM_SETTING_BOND_SETTING_NAME,
                  NULL);
@@ -274,10 +279,10 @@ set_arp_targets(NMDevice *device, const char *cur_arp_ip_target, const char *new
             in_addr_t   a4;
 
             s = new_strv[i];
-            if (nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &a4)) {
+            if (nm_inet_parse_bin(AF_INET, s, NULL, &a4)) {
                 char sbuf[INET_ADDRSTRLEN];
 
-                _nm_utils_inet4_ntop(a4, sbuf);
+                nm_inet4_ntop(a4, sbuf);
                 if (!nm_streq(s, sbuf)) {
                     if (!free_list)
                         free_list = g_ptr_array_new_with_free_func(g_free);
@@ -368,7 +373,7 @@ _bond_arp_ip_target_to_platform(const char *value, in_addr_t out[static NM_BOND_
     for (i = 0; ip[i]; i++) {
         if (added > NM_BOND_MAX_ARP_TARGETS - 1)
             break;
-        if (!nm_utils_parse_inaddr_bin(AF_INET, ip[i], NULL, &in_a))
+        if (!nm_inet_parse_bin(AF_INET, ip[i], NULL, &in_a))
             nm_assert_not_reached(); /* verify() already validated the IP addresses */
 
         out[added++] = in_a;
@@ -460,10 +465,97 @@ _platform_lnk_bond_init_from_setting(NMSettingBond *s_bond, NMPlatformLnkBond *p
     props->tlb_dynamic_lb_has   = NM_IN_SET(props->mode, NM_BOND_MODE_TLB, NM_BOND_MODE_ALB);
 }
 
+static void
+_balance_slb_cb(NMBondManager *bond_manager, NMBondManagerEventType event_type, gpointer user_data)
+{
+    NMDevice     *device = user_data;
+    NMDeviceBond *self   = NM_DEVICE_BOND(device);
+
+    nm_assert(NM_IS_DEVICE_BOND(self));
+    nm_assert(self->bond_manager == bond_manager);
+
+    switch (event_type) {
+    case NM_BOND_MANAGER_EVENT_TYPE_STATE:
+        switch (nm_bond_manager_get_state(bond_manager)) {
+        case NM_OPTION_BOOL_FALSE:
+            if (nm_device_get_state(device) <= NM_DEVICE_STATE_ACTIVATED) {
+                _LOGD(LOGD_BOND, "balance-slb: failed");
+                nm_device_state_changed(device,
+                                        NM_DEVICE_STATE_FAILED,
+                                        NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+            }
+            return;
+        case NM_OPTION_BOOL_TRUE:
+            if (nm_device_get_state(device) <= NM_DEVICE_STATE_ACTIVATED
+                && nm_device_devip_get_state(device, AF_UNSPEC) <= NM_DEVICE_IP_STATE_PENDING) {
+                nm_device_devip_set_state(device, AF_UNSPEC, NM_DEVICE_IP_STATE_READY, NULL);
+            }
+            return;
+        case NM_OPTION_BOOL_DEFAULT:
+            if (nm_device_get_state(device) <= NM_DEVICE_STATE_ACTIVATED
+                && nm_device_devip_get_state(device, AF_UNSPEC) == NM_DEVICE_IP_STATE_READY) {
+                /* We are again busy. We can also go back to "pending" from "ready".
+                 * If ip-config state is not yet complete, this will further delay it.
+                 * Otherwise, it should have no effect. */
+                nm_device_devip_set_state(device, AF_UNSPEC, NM_DEVICE_IP_STATE_PENDING, NULL);
+            }
+            return;
+        }
+        nm_assert_not_reached();
+        return;
+    }
+
+    nm_assert_not_reached();
+}
+
+static void
+_balance_slb_setup(NMDeviceBond *self, NMConnection *connection)
+{
+    int            ifindex     = nm_device_get_ifindex(NM_DEVICE(self));
+    gboolean       balance_slb = FALSE;
+    const char    *uuid;
+    NMSettingBond *s_bond;
+
+    if (ifindex > 0 && connection && (s_bond = nm_connection_get_setting_bond(connection)))
+        balance_slb = _v_intbool(s_bond, NM_SETTING_BOND_OPTION_BALANCE_SLB);
+
+    if (!balance_slb) {
+        if (nm_clear_pointer(&self->bond_manager, nm_bond_manager_destroy)) {
+            _LOGD(LOGD_BOND, "balance-slb: stopped");
+            nm_device_devip_set_state(NM_DEVICE(self), AF_UNSPEC, NM_DEVICE_IP_STATE_NONE, NULL);
+        }
+        return;
+    }
+
+    uuid = nm_connection_get_uuid(connection);
+
+    if (self->bond_manager) {
+        if (nm_bond_manager_get_ifindex(self->bond_manager) == ifindex
+            && nm_streq0(nm_bond_manager_get_connection_uuid(self->bond_manager), uuid)) {
+            _LOGD(LOGD_BOND, "balance-slb: reapply");
+            nm_bond_manager_reapply(self->bond_manager);
+            return;
+        }
+        nm_clear_pointer(&self->bond_manager, nm_bond_manager_destroy);
+        _LOGD(LOGD_BOND, "balance-slb: restart");
+    }
+
+    _LOGD(LOGD_BOND, "balance-slb: start");
+    if (nm_device_devip_get_state(NM_DEVICE(self), AF_UNSPEC) < NM_DEVICE_IP_STATE_PENDING)
+        nm_device_devip_set_state(NM_DEVICE(self), AF_UNSPEC, NM_DEVICE_IP_STATE_PENDING, NULL);
+    self->bond_manager = nm_bond_manager_new(nm_device_get_platform(NM_DEVICE(self)),
+                                             ifindex,
+                                             uuid,
+                                             _balance_slb_cb,
+                                             self);
+    nm_assert(nm_bond_manager_get_state(self->bond_manager) == NM_OPTION_BOOL_DEFAULT);
+}
+
 static NMActStageReturn
 act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
 {
-    NMActStageReturn  ret = NM_ACT_STAGE_RETURN_SUCCESS;
+    NMDeviceBond     *self = NM_DEVICE_BOND(device);
+    NMActStageReturn  ret  = NM_ACT_STAGE_RETURN_SUCCESS;
     NMConnection     *connection;
     NMSettingBond    *s_bond;
     NMPlatformLnkBond props;
@@ -476,6 +568,14 @@ act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
     s_bond = nm_connection_get_setting_bond(connection);
     g_return_val_if_fail(s_bond, NM_ACT_STAGE_RETURN_FAILURE);
 
+    if (nm_device_sys_iface_state_is_external(device))
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+
+    _balance_slb_setup(self, connection);
+
+    if (nm_device_sys_iface_state_is_external_or_assume(device))
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+
     _platform_lnk_bond_init_from_setting(s_bond, &props);
 
     /* Interface must be down to set bond options */
@@ -684,7 +784,7 @@ can_reapply_change(NMDevice   *device,
             const char *name = *option_list;
 
             /* We support changes to these */
-            if (NM_IN_STRSET(name, OPTIONS_REAPPLY_FULL))
+            if (NM_IN_STRSET(name, OPTIONS_REAPPLY_FULL, NM_SETTING_BOND_OPTION_BALANCE_SLB))
                 continue;
 
             /* Reject any other changes */
@@ -730,6 +830,16 @@ reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_ne
     set_bond_arp_ip_targets(device, s_bond);
 
     set_bond_attrs_or_default(device, s_bond, NM_MAKE_STRV(OPTIONS_REAPPLY_SUBSET));
+
+    _balance_slb_setup(self, con_new);
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    NMDeviceBond *self = NM_DEVICE_BOND(device);
+
+    _balance_slb_setup(self, NULL);
 }
 
 /*****************************************************************************/
@@ -768,20 +878,22 @@ nm_device_bond_class_init(NMDeviceBondClass *klass)
     device_class->update_connection              = update_connection;
     device_class->master_update_slave_connection = controller_update_port_connection;
 
-    device_class->create_and_realize = create_and_realize;
-    device_class->act_stage1_prepare = act_stage1_prepare;
+    device_class->create_and_realize                             = create_and_realize;
+    device_class->act_stage1_prepare                             = act_stage1_prepare;
+    device_class->act_stage1_prepare_also_for_external_or_assume = TRUE;
     device_class->get_configured_mtu = nm_device_get_configured_mtu_for_wired;
     device_class->attach_port        = attach_port;
     device_class->detach_port        = detach_port;
     device_class->can_reapply_change = can_reapply_change;
     device_class->reapply_connection = reapply_connection;
+    device_class->deactivate         = deactivate;
 }
 
 /*****************************************************************************/
 
 #define NM_TYPE_BOND_DEVICE_FACTORY (nm_bond_device_factory_get_type())
 #define NM_BOND_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_BOND_DEVICE_FACTORY, NMBondDeviceFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_BOND_DEVICE_FACTORY, NMBondDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-bond.h b/src/core/devices/nm-device-bond.h
index 19301793..083189bb 100644
--- a/src/core/devices/nm-device-bond.h
+++ b/src/core/devices/nm-device-bond.h
@@ -9,7 +9,8 @@
 #include "nm-device.h"
 
 #define NM_TYPE_DEVICE_BOND (nm_device_bond_get_type())
-#define NM_DEVICE_BOND(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_BOND, NMDeviceBond))
+#define NM_DEVICE_BOND(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_BOND, NMDeviceBond))
 #define NM_DEVICE_BOND_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_BOND, NMDeviceBondClass))
 #define NM_IS_DEVICE_BOND(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_BOND))
diff --git a/src/core/devices/nm-device-bridge.c b/src/core/devices/nm-device-bridge.c
index 31cf361e..c5ce34c2 100644
--- a/src/core/devices/nm-device-bridge.c
+++ b/src/core/devices/nm-device-bridge.c
@@ -679,9 +679,10 @@ master_update_slave_connection(NMDevice     *device,
     NMDeviceBridge      *self = NM_DEVICE_BRIDGE(device);
     NMSettingConnection *s_con;
     NMSettingBridgePort *s_port;
-    int                  ifindex_slave = nm_device_get_ifindex(slave);
-    const char          *iface         = nm_device_get_iface(device);
-    const Option        *option;
+    int                  ifindex_slave      = nm_device_get_ifindex(slave);
+    NMConnection        *applied_connection = nm_device_get_applied_connection(device);
+
+    const Option *option;
 
     g_return_val_if_fail(ifindex_slave > 0, FALSE);
 
@@ -717,7 +718,7 @@ master_update_slave_connection(NMDevice     *device,
 
     g_object_set(s_con,
                  NM_SETTING_CONNECTION_MASTER,
-                 iface,
+                 nm_connection_get_uuid(applied_connection),
                  NM_SETTING_CONNECTION_SLAVE_TYPE,
                  NM_SETTING_BRIDGE_SETTING_NAME,
                  NULL);
@@ -1270,7 +1271,7 @@ nm_device_bridge_class_init(NMDeviceBridgeClass *klass)
 
 #define NM_TYPE_BRIDGE_DEVICE_FACTORY (nm_bridge_device_factory_get_type())
 #define NM_BRIDGE_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_BRIDGE_DEVICE_FACTORY, NMBridgeDeviceFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_BRIDGE_DEVICE_FACTORY, NMBridgeDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-bridge.h b/src/core/devices/nm-device-bridge.h
index 32437947..6d9f1614 100644
--- a/src/core/devices/nm-device-bridge.h
+++ b/src/core/devices/nm-device-bridge.h
@@ -10,7 +10,7 @@
 
 #define NM_TYPE_DEVICE_BRIDGE (nm_device_bridge_get_type())
 #define NM_DEVICE_BRIDGE(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_BRIDGE, NMDeviceBridge))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_BRIDGE, NMDeviceBridge))
 #define NM_DEVICE_BRIDGE_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_BRIDGE, NMDeviceBridgeClass))
 #define NM_IS_DEVICE_BRIDGE(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_BRIDGE))
diff --git a/src/core/devices/nm-device-dummy.c b/src/core/devices/nm-device-dummy.c
index de701a0d..36a0ee6f 100644
--- a/src/core/devices/nm-device-dummy.c
+++ b/src/core/devices/nm-device-dummy.c
@@ -136,7 +136,7 @@ nm_device_dummy_class_init(NMDeviceDummyClass *klass)
 
 #define NM_TYPE_DUMMY_DEVICE_FACTORY (nm_dummy_device_factory_get_type())
 #define NM_DUMMY_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DUMMY_DEVICE_FACTORY, NMDummyDeviceFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DUMMY_DEVICE_FACTORY, NMDummyDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-dummy.h b/src/core/devices/nm-device-dummy.h
index 2845b4eb..9e550887 100644
--- a/src/core/devices/nm-device-dummy.h
+++ b/src/core/devices/nm-device-dummy.h
@@ -10,7 +10,7 @@
 
 #define NM_TYPE_DEVICE_DUMMY (nm_device_dummy_get_type())
 #define NM_DEVICE_DUMMY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_DUMMY, NMDeviceDummy))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_DUMMY, NMDeviceDummy))
 #define NM_DEVICE_DUMMY_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_DUMMY, NMDeviceDummyClass))
 #define NM_IS_DEVICE_DUMMY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_DUMMY))
diff --git a/src/core/devices/nm-device-ethernet.c b/src/core/devices/nm-device-ethernet.c
index 4788cdc6..97cf84a1 100644
--- a/src/core/devices/nm-device-ethernet.c
+++ b/src/core/devices/nm-device-ethernet.c
@@ -1735,11 +1735,10 @@ new_default_connection(NMDevice *self)
 
     /* Create a stable UUID. The UUID is also the Network_ID for stable-privacy addr-gen-mode,
      * thus when it changes we will also generate different IPv6 addresses. */
-    uuid = nm_uuid_generate_from_strings("default-wired",
-                                         nm_utils_machine_id_str(),
-                                         defname,
-                                         perm_hw_addr ?: iface,
-                                         NULL);
+    uuid = nm_uuid_generate_from_strings_old("default-wired",
+                                             nm_utils_machine_id_str(),
+                                             defname,
+                                             perm_hw_addr ?: iface);
 
     g_object_set(setting,
                  NM_SETTING_CONNECTION_ID,
@@ -2114,8 +2113,10 @@ nm_device_ethernet_class_init(NMDeviceEthernetClass *klass)
 /*****************************************************************************/
 
 #define NM_TYPE_ETHERNET_DEVICE_FACTORY (nm_ethernet_device_factory_get_type())
-#define NM_ETHERNET_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_ETHERNET_DEVICE_FACTORY, NMEthernetDeviceFactory))
+#define NM_ETHERNET_DEVICE_FACTORY(obj)                              \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj),                           \
+                                    NM_TYPE_ETHERNET_DEVICE_FACTORY, \
+                                    NMEthernetDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-ethernet.h b/src/core/devices/nm-device-ethernet.h
index 6e134d71..6c4acb82 100644
--- a/src/core/devices/nm-device-ethernet.h
+++ b/src/core/devices/nm-device-ethernet.h
@@ -11,7 +11,7 @@
 
 #define NM_TYPE_DEVICE_ETHERNET (nm_device_ethernet_get_type())
 #define NM_DEVICE_ETHERNET(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_ETHERNET, NMDeviceEthernet))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_ETHERNET, NMDeviceEthernet))
 #define NM_DEVICE_ETHERNET_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_ETHERNET, NMDeviceEthernetClass))
 #define NM_IS_DEVICE_ETHERNET(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_ETHERNET))
diff --git a/src/core/devices/nm-device-factory.c b/src/core/devices/nm-device-factory.c
index 41e9480f..2f0ae2e8 100644
--- a/src/core/devices/nm-device-factory.c
+++ b/src/core/devices/nm-device-factory.c
@@ -398,6 +398,7 @@ nm_device_factory_manager_load_factories(NMDeviceFactoryManagerFactoryFunc callb
     _ADD_INTERNAL(nm_ethernet_device_factory_get_type);
     _ADD_INTERNAL(nm_infiniband_device_factory_get_type);
     _ADD_INTERNAL(nm_ip_tunnel_device_factory_get_type);
+    _ADD_INTERNAL(nm_loopback_device_factory_get_type);
     _ADD_INTERNAL(nm_macsec_device_factory_get_type);
     _ADD_INTERNAL(nm_macvlan_device_factory_get_type);
     _ADD_INTERNAL(nm_ppp_device_factory_get_type);
diff --git a/src/core/devices/nm-device-factory.h b/src/core/devices/nm-device-factory.h
index 51d64767..ac5ae05f 100644
--- a/src/core/devices/nm-device-factory.h
+++ b/src/core/devices/nm-device-factory.h
@@ -17,7 +17,7 @@
 
 #define NM_TYPE_DEVICE_FACTORY (nm_device_factory_get_type())
 #define NM_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_FACTORY, NMDeviceFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_FACTORY, NMDeviceFactory))
 #define NM_DEVICE_FACTORY_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_FACTORY, NMDeviceFactoryClass))
 #define NM_IS_DEVICE_FACTORY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_FACTORY))
@@ -208,7 +208,8 @@ NMDevice *nm_device_factory_create_device(NMDeviceFactory      *factory,
                                                                                                  \
     NM_DEVICE_FACTORY_DECLARE_TYPES(st_code)                                                     \
                                                                                                  \
-    static void nm_##lower##_device_factory_init(NM##mixed##DeviceFactory *self) {}              \
+    static void nm_##lower##_device_factory_init(NM##mixed##DeviceFactory *self)                 \
+    {}                                                                                           \
                                                                                                  \
     static void nm_##lower##_device_factory_class_init(NM##mixed##DeviceFactoryClass *klass)     \
     {                                                                                            \
diff --git a/src/core/devices/nm-device-generic.h b/src/core/devices/nm-device-generic.h
index 48c43523..f06a5bdc 100644
--- a/src/core/devices/nm-device-generic.h
+++ b/src/core/devices/nm-device-generic.h
@@ -10,7 +10,7 @@
 
 #define NM_TYPE_DEVICE_GENERIC (nm_device_generic_get_type())
 #define NM_DEVICE_GENERIC(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_GENERIC, NMDeviceGeneric))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_GENERIC, NMDeviceGeneric))
 #define NM_DEVICE_GENERIC_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_GENERIC, NMDeviceGenericClass))
 #define NM_IS_DEVICE_GENERIC(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_GENERIC))
diff --git a/src/core/devices/nm-device-infiniband.c b/src/core/devices/nm-device-infiniband.c
index 6b6aef86..d025931e 100644
--- a/src/core/devices/nm-device-infiniband.c
+++ b/src/core/devices/nm-device-infiniband.c
@@ -403,10 +403,10 @@ nm_device_infiniband_class_init(NMDeviceInfinibandClass *klass)
 /*****************************************************************************/
 
 #define NM_TYPE_INFINIBAND_DEVICE_FACTORY (nm_infiniband_device_factory_get_type())
-#define NM_INFINIBAND_DEVICE_FACTORY(obj)                          \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj),                             \
-                                NM_TYPE_INFINIBAND_DEVICE_FACTORY, \
-                                NMInfinibandDeviceFactory))
+#define NM_INFINIBAND_DEVICE_FACTORY(obj)                              \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj),                             \
+                                    NM_TYPE_INFINIBAND_DEVICE_FACTORY, \
+                                    NMInfinibandDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-infiniband.h b/src/core/devices/nm-device-infiniband.h
index 69994a2d..5c915dd4 100644
--- a/src/core/devices/nm-device-infiniband.h
+++ b/src/core/devices/nm-device-infiniband.h
@@ -10,7 +10,7 @@
 
 #define NM_TYPE_DEVICE_INFINIBAND (nm_device_infiniband_get_type())
 #define NM_DEVICE_INFINIBAND(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_INFINIBAND, NMDeviceInfiniband))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_INFINIBAND, NMDeviceInfiniband))
 #define NM_DEVICE_INFINIBAND_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_INFINIBAND, NMDeviceInfinibandClass))
 #define NM_IS_DEVICE_INFINIBAND(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_INFINIBAND))
diff --git a/src/core/devices/nm-device-ip-tunnel.c b/src/core/devices/nm-device-ip-tunnel.c
index 02680dd7..5e5ba632 100644
--- a/src/core/devices/nm-device-ip-tunnel.c
+++ b/src/core/devices/nm-device-ip-tunnel.c
@@ -39,6 +39,7 @@ NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceIPTunnel,
                              PROP_OUTPUT_KEY,
                              PROP_ENCAPSULATION_LIMIT,
                              PROP_FLOW_LABEL,
+                             PROP_FWMARK,
                              PROP_FLAGS, );
 
 typedef struct {
@@ -53,6 +54,7 @@ typedef struct {
     char           *output_key;
     guint8          encap_limit;
     guint32         flow_label;
+    guint32         fwmark;
     NMIPTunnelFlags flags;
 } NMDeviceIPTunnelPrivate;
 
@@ -107,12 +109,12 @@ address_equal_pp(int addr_family, const char *a, const char *b)
     nm_assert_addr_family(addr_family);
 
     if (a) {
-        if (!nm_utils_parse_inaddr_bin(addr_family, a, NULL, &addr_a_val))
+        if (!nm_inet_parse_bin(addr_family, a, NULL, &addr_a_val))
             nm_assert_not_reached();
         addr_a = &addr_a_val;
     }
     if (b) {
-        if (!nm_utils_parse_inaddr_bin(addr_family, b, NULL, &addr_b_val))
+        if (!nm_inet_parse_bin(addr_family, b, NULL, &addr_b_val))
             nm_assert_not_reached();
         addr_b = &addr_b_val;
     }
@@ -125,7 +127,7 @@ address_set(int addr_family, char **p_addr, const NMIPAddr *addr_new)
 {
     nm_assert_addr_family(addr_family);
     nm_assert(p_addr);
-    nm_assert(!*p_addr || nm_utils_ipaddr_is_normalized(addr_family, *p_addr));
+    nm_assert(!*p_addr || nm_inet_is_normalized(addr_family, *p_addr));
 
     if (!addr_new || nm_ip_addr_is_null(addr_family, addr_new)) {
         if (nm_clear_g_free(p_addr))
@@ -136,7 +138,7 @@ address_set(int addr_family, char **p_addr, const NMIPAddr *addr_new)
     if (*p_addr) {
         NMIPAddr addr_val;
 
-        if (!nm_utils_parse_inaddr_bin(addr_family, *p_addr, NULL, &addr_val))
+        if (!nm_inet_parse_bin(addr_family, *p_addr, NULL, &addr_val))
             nm_assert_not_reached();
 
         if (nm_ip_addr_equal(addr_family, &addr_val, addr_new))
@@ -145,7 +147,7 @@ address_set(int addr_family, char **p_addr, const NMIPAddr *addr_new)
         g_free(*p_addr);
     }
 
-    *p_addr = nm_utils_inet_ntop_dup(addr_family, addr_new);
+    *p_addr = nm_inet_ntop_dup(addr_family, addr_new);
     return TRUE;
 }
 
@@ -162,8 +164,10 @@ update_properties_from_ifindex(NMDevice *device, int ifindex)
     guint8                   encap_limit    = 0;
     gboolean                 pmtud          = FALSE;
     guint32                  flow_label     = 0;
+    guint32                  fwmark         = 0;
     NMIPTunnelFlags          flags          = NM_IP_TUNNEL_FLAG_NONE;
-    char                    *key;
+    gs_free char            *input_key      = NULL;
+    gs_free char            *output_key     = NULL;
 
     if (ifindex <= 0) {
 clear:
@@ -207,35 +211,10 @@ clear:
         tos            = lnk->tos;
         pmtud          = lnk->path_mtu_discovery;
 
-        if (NM_FLAGS_HAS(lnk->input_flags, NM_GRE_KEY)) {
-            key = g_strdup_printf("%u", lnk->input_key);
-            if (g_strcmp0(priv->input_key, key)) {
-                g_free(priv->input_key);
-                priv->input_key = key;
-                _notify(self, PROP_INPUT_KEY);
-            } else
-                g_free(key);
-        } else {
-            if (priv->input_key) {
-                nm_clear_g_free(&priv->input_key);
-                _notify(self, PROP_INPUT_KEY);
-            }
-        }
-
-        if (NM_FLAGS_HAS(lnk->output_flags, NM_GRE_KEY)) {
-            key = g_strdup_printf("%u", lnk->output_key);
-            if (g_strcmp0(priv->output_key, key)) {
-                g_free(priv->output_key);
-                priv->output_key = key;
-                _notify(self, PROP_OUTPUT_KEY);
-            } else
-                g_free(key);
-        } else {
-            if (priv->output_key) {
-                nm_clear_g_free(&priv->output_key);
-                _notify(self, PROP_OUTPUT_KEY);
-            }
-        }
+        if (NM_FLAGS_HAS(lnk->input_flags, NM_GRE_KEY))
+            input_key = g_strdup_printf("%u", lnk->input_key);
+        if (NM_FLAGS_HAS(lnk->output_flags, NM_GRE_KEY))
+            output_key = g_strdup_printf("%u", lnk->output_key);
     } else if (priv->mode == NM_IP_TUNNEL_MODE_SIT) {
         const NMPlatformLnkSit *lnk;
 
@@ -296,36 +275,45 @@ clear:
         flags          = ip6tnl_flags_plat_to_setting(lnk->flags);
 
         if (NM_IN_SET(priv->mode, NM_IP_TUNNEL_MODE_IP6GRE, NM_IP_TUNNEL_MODE_IP6GRETAP)) {
-            if (NM_FLAGS_HAS(lnk->input_flags, NM_GRE_KEY)) {
-                key = g_strdup_printf("%u", lnk->input_key);
-                if (g_strcmp0(priv->input_key, key)) {
-                    g_free(priv->input_key);
-                    priv->input_key = key;
-                    _notify(self, PROP_INPUT_KEY);
-                } else
-                    g_free(key);
-            } else {
-                if (priv->input_key) {
-                    nm_clear_g_free(&priv->input_key);
-                    _notify(self, PROP_INPUT_KEY);
-                }
-            }
+            if (NM_FLAGS_HAS(lnk->input_flags, NM_GRE_KEY))
+                input_key = g_strdup_printf("%u", lnk->input_key);
+            if (NM_FLAGS_HAS(lnk->output_flags, NM_GRE_KEY))
+                output_key = g_strdup_printf("%u", lnk->output_key);
+        }
+    } else if (priv->mode == NM_IP_TUNNEL_MODE_VTI) {
+        const NMPlatformLnkVti *lnk;
 
-            if (NM_FLAGS_HAS(lnk->output_flags, NM_GRE_KEY)) {
-                key = g_strdup_printf("%u", lnk->output_key);
-                if (g_strcmp0(priv->output_key, key)) {
-                    g_free(priv->output_key);
-                    priv->output_key = key;
-                    _notify(self, PROP_OUTPUT_KEY);
-                } else
-                    g_free(key);
-            } else {
-                if (priv->output_key) {
-                    nm_clear_g_free(&priv->output_key);
-                    _notify(self, PROP_OUTPUT_KEY);
-                }
-            }
+        lnk = nm_platform_link_get_lnk_vti(nm_device_get_platform(device), ifindex, NULL);
+        if (!lnk) {
+            _LOGW(LOGD_PLATFORM, "could not read %s properties", "vti");
+            goto clear;
+        }
+
+        parent_ifindex = lnk->parent_ifindex;
+        local.addr4    = lnk->local;
+        remote.addr4   = lnk->remote;
+        fwmark         = lnk->fwmark;
+        if (lnk->ikey)
+            input_key = g_strdup_printf("%u", lnk->ikey);
+        if (lnk->okey)
+            input_key = g_strdup_printf("%u", lnk->okey);
+    } else if (priv->mode == NM_IP_TUNNEL_MODE_VTI6) {
+        const NMPlatformLnkVti6 *lnk;
+
+        lnk = nm_platform_link_get_lnk_vti6(nm_device_get_platform(device), ifindex, NULL);
+        if (!lnk) {
+            _LOGW(LOGD_PLATFORM, "could not read %s properties", "vti6");
+            goto clear;
         }
+
+        parent_ifindex = lnk->parent_ifindex;
+        local.addr6    = lnk->local;
+        remote.addr6   = lnk->remote;
+        fwmark         = lnk->fwmark;
+        if (lnk->ikey)
+            input_key = g_strdup_printf("%u", lnk->ikey);
+        if (lnk->okey)
+            input_key = g_strdup_printf("%u", lnk->okey);
     } else
         g_return_if_reached();
 
@@ -337,7 +325,6 @@ clear:
         _notify(self, PROP_REMOTE);
 
 out:
-
     if (priv->ttl != ttl) {
         priv->ttl = ttl;
         _notify(self, PROP_TTL);
@@ -363,6 +350,23 @@ out:
         _notify(self, PROP_FLOW_LABEL);
     }
 
+    if (!nm_streq0(priv->input_key, input_key)) {
+        g_free(priv->input_key);
+        priv->input_key = g_steal_pointer(&input_key);
+        _notify(self, PROP_INPUT_KEY);
+    }
+
+    if (!nm_streq0(priv->output_key, output_key)) {
+        g_free(priv->output_key);
+        priv->output_key = g_steal_pointer(&output_key);
+        _notify(self, PROP_OUTPUT_KEY);
+    }
+
+    if (priv->fwmark != fwmark) {
+        priv->fwmark = fwmark;
+        _notify(self, PROP_FWMARK);
+    }
+
     if (priv->flags != flags) {
         priv->flags = flags;
         _notify(self, PROP_FLAGS);
@@ -467,11 +471,17 @@ update_connection(NMDevice *device, NMConnection *connection)
                      NULL);
     }
 
+    if (nm_setting_ip_tunnel_get_fwmark(s_ip_tunnel) != priv->fwmark) {
+        g_object_set(G_OBJECT(s_ip_tunnel), NM_SETTING_IP_TUNNEL_FWMARK, priv->fwmark, NULL);
+    }
+
     if (NM_IN_SET(priv->mode,
                   NM_IP_TUNNEL_MODE_GRE,
                   NM_IP_TUNNEL_MODE_GRETAP,
                   NM_IP_TUNNEL_MODE_IP6GRE,
-                  NM_IP_TUNNEL_MODE_IP6GRETAP)) {
+                  NM_IP_TUNNEL_MODE_IP6GRETAP,
+                  NM_IP_TUNNEL_MODE_VTI,
+                  NM_IP_TUNNEL_MODE_VTI6)) {
         if (g_strcmp0(nm_setting_ip_tunnel_get_input_key(s_ip_tunnel), priv->input_key)) {
             g_object_set(G_OBJECT(s_ip_tunnel),
                          NM_SETTING_IP_TUNNEL_INPUT_KEY,
@@ -493,6 +503,7 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
     NMDeviceIPTunnel        *self = NM_DEVICE_IP_TUNNEL(device);
     NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(self);
     NMSettingIPTunnel       *s_ip_tunnel;
+    NMIPTunnelMode           mode;
     const char              *parent;
 
     if (!NM_DEVICE_CLASS(nm_device_ip_tunnel_parent_class)
@@ -500,8 +511,9 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
         return FALSE;
 
     s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection);
+    mode        = nm_setting_ip_tunnel_get_mode(s_ip_tunnel);
 
-    if (nm_setting_ip_tunnel_get_mode(s_ip_tunnel) != priv->mode) {
+    if (mode != priv->mode) {
         nm_utils_error_set_literal(error,
                                    NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
                                    "incompatible IP tunnel mode");
@@ -536,14 +548,16 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
             return FALSE;
         }
 
-        if (nm_setting_ip_tunnel_get_ttl(s_ip_tunnel) != priv->ttl) {
+        if (!NM_IN_SET(mode, NM_IP_TUNNEL_MODE_VTI, NM_IP_TUNNEL_MODE_VTI6)
+            && nm_setting_ip_tunnel_get_ttl(s_ip_tunnel) != priv->ttl) {
             nm_utils_error_set_literal(error,
                                        NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
                                        "TTL of IP tunnel mismatches");
             return FALSE;
         }
 
-        if (nm_setting_ip_tunnel_get_tos(s_ip_tunnel) != priv->tos) {
+        if (!NM_IN_SET(mode, NM_IP_TUNNEL_MODE_VTI, NM_IP_TUNNEL_MODE_VTI6)
+            && nm_setting_ip_tunnel_get_tos(s_ip_tunnel) != priv->tos) {
             nm_utils_error_set_literal(error,
                                        NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
                                        "TOS of IP tunnel mismatches");
@@ -551,11 +565,14 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
         }
 
         if (priv->addr_family == AF_INET) {
-            if (nm_setting_ip_tunnel_get_path_mtu_discovery(s_ip_tunnel)
-                != priv->path_mtu_discovery) {
-                nm_utils_error_set_literal(error,
-                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
-                                           "MTU discovery setting of IP tunnel mismatches");
+            if (!NM_IN_SET(mode, NM_IP_TUNNEL_MODE_VTI, NM_IP_TUNNEL_MODE_VTI6)
+                && nm_setting_ip_tunnel_get_path_mtu_discovery(s_ip_tunnel)
+                       != priv->path_mtu_discovery) {
+                nm_utils_error_set(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "MTU discovery setting of IP tunnel mismatches: %d vs %d",
+                                   nm_setting_ip_tunnel_get_path_mtu_discovery(s_ip_tunnel),
+                                   priv->path_mtu_discovery);
                 return FALSE;
             }
         } else {
@@ -605,6 +622,10 @@ platform_link_to_tunnel_mode(const NMPlatformLink *link)
         return NM_IP_TUNNEL_MODE_IPIP;
     case NM_LINK_TYPE_SIT:
         return NM_IP_TUNNEL_MODE_SIT;
+    case NM_LINK_TYPE_VTI:
+        return NM_IP_TUNNEL_MODE_VTI;
+    case NM_LINK_TYPE_VTI6:
+        return NM_IP_TUNNEL_MODE_VTI6;
     default:
         g_return_val_if_reached(NM_IP_TUNNEL_MODE_UNKNOWN);
     }
@@ -630,7 +651,9 @@ tunnel_mode_to_link_type(NMIPTunnelMode tunnel_mode)
     case NM_IP_TUNNEL_MODE_SIT:
         return NM_LINK_TYPE_SIT;
     case NM_IP_TUNNEL_MODE_VTI:
+        return NM_LINK_TYPE_VTI;
     case NM_IP_TUNNEL_MODE_VTI6:
+        return NM_LINK_TYPE_VTI6;
     case NM_IP_TUNNEL_MODE_ISATAP:
         return NM_LINK_TYPE_UNKNOWN;
     case NM_IP_TUNNEL_MODE_UNKNOWN:
@@ -654,6 +677,8 @@ create_and_realize(NMDevice              *device,
     NMPlatformLnkSit    lnk_sit    = {};
     NMPlatformLnkIpIp   lnk_ipip   = {};
     NMPlatformLnkIp6Tnl lnk_ip6tnl = {};
+    NMPlatformLnkVti    lnk_vti    = {};
+    NMPlatformLnkVti6   lnk_vti6   = {};
     const char         *str;
     gint64              val;
     NMIPTunnelMode      mode;
@@ -871,6 +896,81 @@ create_and_realize(NMDevice              *device,
             return FALSE;
         }
         break;
+    case NM_IP_TUNNEL_MODE_VTI:
+        if (parent)
+            lnk_vti.parent_ifindex = nm_device_get_ifindex(parent);
+
+        str = nm_setting_ip_tunnel_get_local(s_ip_tunnel);
+        if (str)
+            inet_pton(AF_INET, str, &lnk_vti.local);
+
+        str = nm_setting_ip_tunnel_get_remote(s_ip_tunnel);
+        nm_assert(str);
+        inet_pton(AF_INET, str, &lnk_vti.remote);
+
+        lnk_vti.ikey = _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_input_key(s_ip_tunnel),
+                                                    10,
+                                                    0,
+                                                    G_MAXUINT32,
+                                                    0);
+        lnk_vti.okey =
+            _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_output_key(s_ip_tunnel),
+                                         10,
+                                         0,
+                                         G_MAXUINT32,
+                                         0);
+        lnk_vti.fwmark = nm_setting_ip_tunnel_get_fwmark(s_ip_tunnel);
+
+        r = nm_platform_link_vti_add(nm_device_get_platform(device), iface, &lnk_vti, out_plink);
+        if (r < 0) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_CREATION_FAILED,
+                        "Failed to create VTI interface '%s' for '%s': %s",
+                        iface,
+                        nm_connection_get_id(connection),
+                        nm_strerror(r));
+            return FALSE;
+        }
+        break;
+    case NM_IP_TUNNEL_MODE_VTI6:
+        if (parent)
+            lnk_vti6.parent_ifindex = nm_device_get_ifindex(parent);
+
+        str = nm_setting_ip_tunnel_get_local(s_ip_tunnel);
+        if (str)
+            inet_pton(AF_INET6, str, &lnk_vti6.local);
+
+        str = nm_setting_ip_tunnel_get_remote(s_ip_tunnel);
+        nm_assert(str);
+        inet_pton(AF_INET6, str, &lnk_vti6.remote);
+
+        lnk_vti6.ikey =
+            _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_input_key(s_ip_tunnel),
+                                         10,
+                                         0,
+                                         G_MAXUINT32,
+                                         0);
+        lnk_vti6.okey =
+            _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_output_key(s_ip_tunnel),
+                                         10,
+                                         0,
+                                         G_MAXUINT32,
+                                         0);
+        lnk_vti6.fwmark = nm_setting_ip_tunnel_get_fwmark(s_ip_tunnel);
+
+        r = nm_platform_link_vti6_add(nm_device_get_platform(device), iface, &lnk_vti6, out_plink);
+        if (r < 0) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_CREATION_FAILED,
+                        "Failed to create VTI6 interface '%s' for '%s': %s",
+                        iface,
+                        nm_connection_get_id(connection),
+                        nm_strerror(r));
+            return FALSE;
+        }
+        break;
     default:
         g_set_error(error,
                     NM_DEVICE_ERROR,
@@ -986,6 +1086,9 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
     case PROP_FLAGS:
         g_value_set_uint(value, priv->flags);
         break;
+    case PROP_FWMARK:
+        g_value_set_uint(value, priv->fwmark);
+        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
         break;
@@ -1021,7 +1124,8 @@ constructed(GObject *object)
                   NM_IP_TUNNEL_MODE_IPIP6,
                   NM_IP_TUNNEL_MODE_IP6IP6,
                   NM_IP_TUNNEL_MODE_IP6GRE,
-                  NM_IP_TUNNEL_MODE_IP6GRETAP))
+                  NM_IP_TUNNEL_MODE_IP6GRETAP,
+                  NM_IP_TUNNEL_MODE_VTI6))
         priv->addr_family = AF_INET6;
     else
         priv->addr_family = AF_INET;
@@ -1070,6 +1174,9 @@ static const NMDBusInterfaceInfoExtended interface_info_device_ip_tunnel = {
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("FlowLabel",
                                                            "u",
                                                            NM_DEVICE_IP_TUNNEL_FLOW_LABEL),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("FwMark",
+                                                           "u",
+                                                           NM_DEVICE_IP_TUNNEL_FWMARK),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Flags",
                                                            "u",
                                                            NM_DEVICE_IP_TUNNEL_FLAGS), ), ),
@@ -1097,7 +1204,9 @@ nm_device_ip_tunnel_class_init(NMDeviceIPTunnelClass *klass)
                                                            NM_LINK_TYPE_IP6GRE,
                                                            NM_LINK_TYPE_IP6GRETAP,
                                                            NM_LINK_TYPE_IPIP,
-                                                           NM_LINK_TYPE_SIT);
+                                                           NM_LINK_TYPE_SIT,
+                                                           NM_LINK_TYPE_VTI,
+                                                           NM_LINK_TYPE_VTI6);
 
     device_class->act_stage1_prepare          = act_stage1_prepare;
     device_class->link_changed                = link_changed;
@@ -1192,14 +1301,24 @@ nm_device_ip_tunnel_class_init(NMDeviceIPTunnelClass *klass)
                                                    0,
                                                    G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
+    obj_properties[PROP_FWMARK] = g_param_spec_uint(NM_DEVICE_IP_TUNNEL_FWMARK,
+                                                    "",
+                                                    "",
+                                                    0,
+                                                    G_MAXUINT32,
+                                                    0,
+                                                    G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
     g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
 }
 
 /*****************************************************************************/
 
 #define NM_TYPE_IP_TUNNEL_DEVICE_FACTORY (nm_ip_tunnel_device_factory_get_type())
-#define NM_IP_TUNNEL_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_IP_TUNNEL_DEVICE_FACTORY, NMIPTunnelDeviceFactory))
+#define NM_IP_TUNNEL_DEVICE_FACTORY(obj)                              \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj),                            \
+                                    NM_TYPE_IP_TUNNEL_DEVICE_FACTORY, \
+                                    NMIPTunnelDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
@@ -1282,7 +1401,9 @@ NM_DEVICE_FACTORY_DEFINE_INTERNAL(
                                          NM_LINK_TYPE_IPIP,
                                          NM_LINK_TYPE_IP6TNL,
                                          NM_LINK_TYPE_IP6GRE,
-                                         NM_LINK_TYPE_IP6GRETAP)
+                                         NM_LINK_TYPE_IP6GRETAP,
+                                         NM_LINK_TYPE_VTI,
+                                         NM_LINK_TYPE_VTI6)
         NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_IP_TUNNEL_SETTING_NAME),
     factory_class->create_device         = create_device;
     factory_class->get_connection_parent = get_connection_parent;
diff --git a/src/core/devices/nm-device-ip-tunnel.h b/src/core/devices/nm-device-ip-tunnel.h
index 0a941cc2..1d7b7381 100644
--- a/src/core/devices/nm-device-ip-tunnel.h
+++ b/src/core/devices/nm-device-ip-tunnel.h
@@ -11,7 +11,7 @@
 
 #define NM_TYPE_DEVICE_IP_TUNNEL (nm_device_ip_tunnel_get_type())
 #define NM_DEVICE_IP_TUNNEL(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IP_TUNNEL, NMDeviceIPTunnel))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IP_TUNNEL, NMDeviceIPTunnel))
 #define NM_DEVICE_IP_TUNNEL_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_IP_TUNNEL, NMDeviceIPTunnelClass))
 #define NM_IS_DEVICE_IP_TUNNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_IP_TUNNEL))
@@ -31,6 +31,7 @@
 #define NM_DEVICE_IP_TUNNEL_ENCAPSULATION_LIMIT "encapsulation-limit"
 #define NM_DEVICE_IP_TUNNEL_FLOW_LABEL          "flow-label"
 #define NM_DEVICE_IP_TUNNEL_FLAGS               "flags"
+#define NM_DEVICE_IP_TUNNEL_FWMARK              "fwmark"
 
 typedef struct _NMDeviceIPTunnel      NMDeviceIPTunnel;
 typedef struct _NMDeviceIPTunnelClass NMDeviceIPTunnelClass;
diff --git a/src/core/devices/nm-device-logging.h b/src/core/devices/nm-device-logging.h
index c2602189..ffc90c2d 100644
--- a/src/core/devices/nm-device-logging.h
+++ b/src/core/devices/nm-device-logging.h
@@ -11,15 +11,12 @@
 #if !_NM_CC_SUPPORT_GENERIC
 #define _NM_DEVICE_CAST(self) ((NMDevice *) (self))
 #elif !defined(_NMLOG_DEVICE_TYPE)
-#define _NM_DEVICE_CAST(self) \
-    _Generic((self), NMDevice * : ((NMDevice *) (self)), NMDevice *const : ((NMDevice *) (self)))
+#define _NM_DEVICE_CAST(self) _Generic((self), NMDevice * : ((NMDevice *) (self)))
 #else
-#define _NM_DEVICE_CAST(self) \
-    _Generic((self), \
-                 _NMLOG_DEVICE_TYPE *      : ((NMDevice *) (self)), \
-                 _NMLOG_DEVICE_TYPE * const: ((NMDevice *) (self)), \
-                 NMDevice *                : ((NMDevice *) (self)), \
-                 NMDevice *           const: ((NMDevice *) (self)))
+#define _NM_DEVICE_CAST(self)                    \
+    _Generic((self), _NMLOG_DEVICE_TYPE *        \
+             : ((NMDevice *) (self)), NMDevice * \
+             : ((NMDevice *) (self)))
 #endif
 
 #undef _NMLOG_ENABLED
diff --git a/src/core/devices/nm-device-loopback.c b/src/core/devices/nm-device-loopback.c
new file mode 100644
index 00000000..ec72aa96
--- /dev/null
+++ b/src/core/devices/nm-device-loopback.c
@@ -0,0 +1,166 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2022 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+#include "nm-device-loopback.h"
+#include "nm-device-private.h"
+#include "libnm-platform/nm-platform.h"
+#include "nm-device-factory.h"
+#include "nm-setting-loopback.h"
+#include "libnm-core-aux-intern/nm-libnm-core-utils.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceLoopback
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+struct _NMDeviceLoopback {
+    NMDevice parent;
+};
+
+struct _NMDeviceLoopbackClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceLoopback, nm_device_loopback, NM_TYPE_DEVICE)
+
+/*****************************************************************************/
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *dev)
+{
+    /* In many aspects, loopback is a software device. Still, don't return
+     * NM_DEVICE_CAP_IS_SOFTWARE here, because we cannot delete nor create
+     * such an interface. */
+    return NM_DEVICE_CAP_NONE;
+}
+
+static guint32
+get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force)
+{
+    return nm_device_get_configured_mtu_from_connection(device,
+                                                        NM_TYPE_SETTING_LOOPBACK,
+                                                        out_source);
+}
+
+static const char *
+get_auto_ip_config_method(NMDevice *device, int addr_family)
+{
+    return NM_IS_IPv4(addr_family) ? NM_SETTING_IP4_CONFIG_METHOD_MANUAL
+                                   : NM_SETTING_IP6_CONFIG_METHOD_MANUAL;
+}
+
+static gboolean
+complete_connection(NMDevice            *device,
+                    NMConnection        *connection,
+                    const char          *specific_object,
+                    NMConnection *const *existing_connections,
+                    GError             **error)
+{
+    nm_utils_complete_generic_with_params(nm_device_get_platform(device),
+                                          connection,
+                                          NM_SETTING_LOOPBACK_SETTING_NAME,
+                                          existing_connections,
+                                          NULL,
+                                          _("Loopback connection"),
+                                          NULL,
+                                          nm_device_get_ip_iface(device));
+
+    _nm_connection_ensure_setting(connection, NM_TYPE_SETTING_LOOPBACK);
+
+    return TRUE;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    _nm_connection_ensure_setting(connection, NM_TYPE_SETTING_LOOPBACK);
+}
+
+static gboolean
+can_reapply_change(NMDevice   *device,
+                   const char *setting_name,
+                   NMSetting  *s_old,
+                   NMSetting  *s_new,
+                   GHashTable *diffs,
+                   GError    **error)
+{
+    if (!nm_streq(setting_name, NM_SETTING_LOOPBACK_SETTING_NAME)) {
+        return NM_DEVICE_CLASS(nm_device_loopback_parent_class)
+            ->can_reapply_change(device, setting_name, s_old, s_new, diffs, error);
+    }
+
+    return nm_device_hash_check_invalid_keys(diffs,
+                                             NM_SETTING_LOOPBACK_SETTING_NAME,
+                                             error,
+                                             NM_SETTING_LOOPBACK_MTU);
+}
+
+/*****************************************************************************/
+
+static const NMDBusInterfaceInfoExtended interface_info_device_loopback = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(NM_DBUS_INTERFACE_DEVICE_LOOPBACK, ),
+};
+
+static void
+nm_device_loopback_init(NMDeviceLoopback *self)
+{}
+
+static void
+nm_device_loopback_class_init(NMDeviceLoopbackClass *klass)
+{
+    NMDeviceClass     *device_class      = NM_DEVICE_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_loopback);
+
+    device_class->connection_type_supported        = NM_SETTING_LOOPBACK_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_LOOPBACK_SETTING_NAME;
+    device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_LOOPBACK);
+
+    device_class->complete_connection                    = complete_connection;
+    device_class->get_generic_capabilities               = get_generic_capabilities;
+    device_class->update_connection                      = update_connection;
+    device_class->act_stage1_prepare_set_hwaddr_ethernet = TRUE;
+    device_class->get_auto_ip_config_method              = get_auto_ip_config_method;
+    device_class->get_configured_mtu                     = get_configured_mtu;
+    device_class->allow_autoconnect_on_external          = TRUE;
+    device_class->can_reapply_change                     = can_reapply_change;
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_LOOPBACK_DEVICE_FACTORY (nm_loopback_device_factory_get_type())
+#define NM_LOOPBACK_DEVICE_FACTORY(obj)                              \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj),                           \
+                                    NM_TYPE_LOOPBACK_DEVICE_FACTORY, \
+                                    NMLoopbackDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory      *factory,
+              const char           *iface,
+              const NMPlatformLink *plink,
+              NMConnection         *connection,
+              gboolean             *out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_LOOPBACK,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "Loopback",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_LOOPBACK,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_LOOPBACK,
+                        NULL);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    LOOPBACK,
+    Loopback,
+    loopback,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_LOOPBACK)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_LOOPBACK_SETTING_NAME),
+    factory_class->create_device = create_device;);
diff --git a/src/core/devices/nm-device-loopback.h b/src/core/devices/nm-device-loopback.h
new file mode 100644
index 00000000..fc62a94b
--- /dev/null
+++ b/src/core/devices/nm-device-loopback.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2022 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_LOOPBACK_H__
+#define __NETWORKMANAGER_DEVICE_LOOPBACK_H__
+
+#include "nm-device-generic.h"
+
+#define NM_TYPE_DEVICE_LOOPBACK (nm_device_loopback_get_type())
+#define NM_DEVICE_LOOPBACK(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_LOOPBACK, NMDeviceLoopback))
+#define NM_DEVICE_LOOPBACK_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_LOOPBACK, NMDeviceLoopbackClass))
+#define NM_IS_DEVICE_LOOPBACK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_LOOPBACK))
+#define NM_IS_DEVICE_LOOPBACK_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_LOOPBACK))
+#define NM_DEVICE_LOOPBACK_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_LOOPBACK, NMDeviceLoopbackClass))
+
+typedef struct _NMDeviceLoopback      NMDeviceLoopback;
+typedef struct _NMDeviceLoopbackClass NMDeviceLoopbackClass;
+
+GType nm_device_loopback_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_LOOPBACK_H__ */
diff --git a/src/core/devices/nm-device-macsec.c b/src/core/devices/nm-device-macsec.c
index 5b1f5fdc..130708bb 100644
--- a/src/core/devices/nm-device-macsec.c
+++ b/src/core/devices/nm-device-macsec.c
@@ -45,7 +45,6 @@ NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceMacsec,
 
 typedef struct {
     NMPlatformLnkMacsec props;
-    gulong              parent_state_id;
     gulong              parent_mtu_id;
 
     struct {
@@ -92,25 +91,6 @@ static NM_UTILS_LOOKUP_STR_DEFINE(validation_mode_to_string,
                                   NM_UTILS_LOOKUP_STR_ITEM(2, "strict"), );
 
 static void
-parent_state_changed(NMDevice           *parent,
-                     NMDeviceState       new_state,
-                     NMDeviceState       old_state,
-                     NMDeviceStateReason reason,
-                     gpointer            user_data)
-{
-    NMDeviceMacsec *self = NM_DEVICE_MACSEC(user_data);
-
-    /* We'll react to our own carrier state notifications. Ignore the parent's. */
-    if (nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_CARRIER)
-        return;
-
-    nm_device_set_unmanaged_by_flags(NM_DEVICE(self),
-                                     NM_UNMANAGED_PARENT,
-                                     !nm_device_get_managed(parent, FALSE),
-                                     reason);
-}
-
-static void
 parent_mtu_maybe_changed(NMDevice *parent, GParamSpec *pspec, gpointer user_data)
 {
     /* the MTU of a MACsec device is limited by the parent's MTU.
@@ -132,27 +112,13 @@ parent_changed_notify(NMDevice *device,
     NM_DEVICE_CLASS(nm_device_macsec_parent_class)
         ->parent_changed_notify(device, old_ifindex, old_parent, new_ifindex, new_parent);
 
-    /*  note that @self doesn't have to clear @parent_state_id on dispose,
-     *  because NMDevice's dispose() will unset the parent, which in turn calls
-     *  parent_changed_notify(). */
-    nm_clear_g_signal_handler(old_parent, &priv->parent_state_id);
     nm_clear_g_signal_handler(old_parent, &priv->parent_mtu_id);
 
     if (new_parent) {
-        priv->parent_state_id = g_signal_connect(new_parent,
-                                                 NM_DEVICE_STATE_CHANGED,
-                                                 G_CALLBACK(parent_state_changed),
-                                                 device);
-        priv->parent_mtu_id   = g_signal_connect(new_parent,
+        priv->parent_mtu_id = g_signal_connect(new_parent,
                                                "notify::" NM_DEVICE_MTU,
                                                G_CALLBACK(parent_mtu_maybe_changed),
                                                device);
-
-        /* Set parent-dependent unmanaged flag */
-        nm_device_set_unmanaged_by_flags(device,
-                                         NM_UNMANAGED_PARENT,
-                                         !nm_device_get_managed(new_parent, FALSE),
-                                         NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED);
     }
 
     /* Recheck availability now that the parent has changed */
@@ -862,7 +828,6 @@ dispose(GObject *object)
 
     G_OBJECT_CLASS(nm_device_macsec_parent_class)->dispose(object);
 
-    nm_assert(NM_DEVICE_MACSEC_GET_PRIVATE(self)->parent_state_id == 0);
     nm_assert(NM_DEVICE_MACSEC_GET_PRIVATE(self)->parent_mtu_id == 0);
 }
 
@@ -1011,7 +976,7 @@ nm_device_macsec_class_init(NMDeviceMacsecClass *klass)
 
 #define NM_TYPE_MACSEC_DEVICE_FACTORY (nm_macsec_device_factory_get_type())
 #define NM_MACSEC_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MACSEC_DEVICE_FACTORY, NMMacsecDeviceFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MACSEC_DEVICE_FACTORY, NMMacsecDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-macsec.h b/src/core/devices/nm-device-macsec.h
index e91fe51c..fefb773c 100644
--- a/src/core/devices/nm-device-macsec.h
+++ b/src/core/devices/nm-device-macsec.h
@@ -10,7 +10,7 @@
 
 #define NM_TYPE_DEVICE_MACSEC (nm_device_macsec_get_type())
 #define NM_DEVICE_MACSEC(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_MACSEC, NMDeviceMacsec))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_MACSEC, NMDeviceMacsec))
 #define NM_DEVICE_MACSEC_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_MACSEC, NMDeviceMacsecClass))
 #define NM_IS_DEVICE_MACSEC(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_MACSEC))
diff --git a/src/core/devices/nm-device-macvlan.c b/src/core/devices/nm-device-macvlan.c
index 40ecc5fd..3f57bfb1 100644
--- a/src/core/devices/nm-device-macvlan.c
+++ b/src/core/devices/nm-device-macvlan.c
@@ -29,7 +29,6 @@
 NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceMacvlan, PROP_MODE, PROP_NO_PROMISC, PROP_TAP, );
 
 typedef struct {
-    gulong               parent_state_id;
     gulong               parent_mtu_id;
     NMPlatformLnkMacvlan props;
 } NMDeviceMacvlanPrivate;
@@ -103,25 +102,6 @@ platform_mode_to_string(guint mode)
 /*****************************************************************************/
 
 static void
-parent_state_changed(NMDevice           *parent,
-                     NMDeviceState       new_state,
-                     NMDeviceState       old_state,
-                     NMDeviceStateReason reason,
-                     gpointer            user_data)
-{
-    NMDeviceMacvlan *self = NM_DEVICE_MACVLAN(user_data);
-
-    /* We'll react to our own carrier state notifications. Ignore the parent's. */
-    if (nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_CARRIER)
-        return;
-
-    nm_device_set_unmanaged_by_flags(NM_DEVICE(self),
-                                     NM_UNMANAGED_PARENT,
-                                     !nm_device_get_managed(parent, FALSE),
-                                     reason);
-}
-
-static void
 parent_mtu_maybe_changed(NMDevice *parent, GParamSpec *pspec, gpointer user_data)
 {
     /* the MTU of a macvlan/macvtap device is limited by the parent's MTU.
@@ -143,27 +123,13 @@ parent_changed_notify(NMDevice *device,
     NM_DEVICE_CLASS(nm_device_macvlan_parent_class)
         ->parent_changed_notify(device, old_ifindex, old_parent, new_ifindex, new_parent);
 
-    /*  note that @self doesn't have to clear @parent_state_id on dispose,
-     *  because NMDevice's dispose() will unset the parent, which in turn calls
-     *  parent_changed_notify(). */
-    nm_clear_g_signal_handler(old_parent, &priv->parent_state_id);
     nm_clear_g_signal_handler(old_parent, &priv->parent_mtu_id);
 
     if (new_parent) {
-        priv->parent_state_id = g_signal_connect(new_parent,
-                                                 NM_DEVICE_STATE_CHANGED,
-                                                 G_CALLBACK(parent_state_changed),
-                                                 device);
-        priv->parent_mtu_id   = g_signal_connect(new_parent,
+        priv->parent_mtu_id = g_signal_connect(new_parent,
                                                "notify::" NM_DEVICE_MTU,
                                                G_CALLBACK(parent_mtu_maybe_changed),
                                                device);
-
-        /* Set parent-dependent unmanaged flag */
-        nm_device_set_unmanaged_by_flags(device,
-                                         NM_UNMANAGED_PARENT,
-                                         !nm_device_get_managed(new_parent, FALSE),
-                                         NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED);
     }
 
     if (new_ifindex > 0) {
@@ -490,7 +456,6 @@ dispose(GObject *object)
 {
     G_OBJECT_CLASS(nm_device_macvlan_parent_class)->dispose(object);
 
-    nm_assert(NM_DEVICE_MACVLAN_GET_PRIVATE(object)->parent_state_id == 0);
     nm_assert(NM_DEVICE_MACVLAN_GET_PRIVATE(object)->parent_mtu_id == 0);
 }
 #endif
@@ -566,7 +531,7 @@ nm_device_macvlan_class_init(NMDeviceMacvlanClass *klass)
 
 #define NM_TYPE_MACVLAN_DEVICE_FACTORY (nm_macvlan_device_factory_get_type())
 #define NM_MACVLAN_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MACVLAN_DEVICE_FACTORY, NMMacvlanDeviceFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MACVLAN_DEVICE_FACTORY, NMMacvlanDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-macvlan.h b/src/core/devices/nm-device-macvlan.h
index 109a2bcd..86ac79c7 100644
--- a/src/core/devices/nm-device-macvlan.h
+++ b/src/core/devices/nm-device-macvlan.h
@@ -10,7 +10,7 @@
 
 #define NM_TYPE_DEVICE_MACVLAN (nm_device_macvlan_get_type())
 #define NM_DEVICE_MACVLAN(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_MACVLAN, NMDeviceMacvlan))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_MACVLAN, NMDeviceMacvlan))
 #define NM_DEVICE_MACVLAN_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_MACVLAN, NMDeviceMacvlanClass))
 #define NM_IS_DEVICE_MACVLAN(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_MACVLAN))
diff --git a/src/core/devices/nm-device-ppp.c b/src/core/devices/nm-device-ppp.c
index 6615b65e..1860df3c 100644
--- a/src/core/devices/nm-device-ppp.c
+++ b/src/core/devices/nm-device-ppp.c
@@ -337,7 +337,7 @@ nm_device_ppp_class_init(NMDevicePppClass *klass)
 
 #define NM_TYPE_PPP_DEVICE_FACTORY (nm_ppp_device_factory_get_type())
 #define NM_PPP_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_PPP_DEVICE_FACTORY, NMPppDeviceFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_PPP_DEVICE_FACTORY, NMPppDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-ppp.h b/src/core/devices/nm-device-ppp.h
index 24c119ab..0e8ca54a 100644
--- a/src/core/devices/nm-device-ppp.h
+++ b/src/core/devices/nm-device-ppp.h
@@ -7,7 +7,7 @@
 #define __NETWORKMANAGER_DEVICE_PPP_H__
 
 #define NM_TYPE_DEVICE_PPP (nm_device_ppp_get_type())
-#define NM_DEVICE_PPP(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_PPP, NMDevicePpp))
+#define NM_DEVICE_PPP(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_PPP, NMDevicePpp))
 #define NM_DEVICE_PPP_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_PPP, NMDevicePppClass))
 #define NM_IS_DEVICE_PPP(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_PPP))
diff --git a/src/core/devices/nm-device-tun.c b/src/core/devices/nm-device-tun.c
index e623e9c0..cbea7d7d 100644
--- a/src/core/devices/nm-device-tun.c
+++ b/src/core/devices/nm-device-tun.c
@@ -527,7 +527,7 @@ nm_device_tun_class_init(NMDeviceTunClass *klass)
 
 #define NM_TYPE_TUN_DEVICE_FACTORY (nm_tun_device_factory_get_type())
 #define NM_TUN_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_TUN_DEVICE_FACTORY, NMTunDeviceFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_TUN_DEVICE_FACTORY, NMTunDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-tun.h b/src/core/devices/nm-device-tun.h
index 84497ea0..866d6c4f 100644
--- a/src/core/devices/nm-device-tun.h
+++ b/src/core/devices/nm-device-tun.h
@@ -9,7 +9,7 @@
 #include "nm-device-generic.h"
 
 #define NM_TYPE_DEVICE_TUN (nm_device_tun_get_type())
-#define NM_DEVICE_TUN(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_TUN, NMDeviceTun))
+#define NM_DEVICE_TUN(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_TUN, NMDeviceTun))
 #define NM_DEVICE_TUN_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_TUN, NMDeviceTunClass))
 #define NM_IS_DEVICE_TUN(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_TUN))
diff --git a/src/core/devices/nm-device-utils.c b/src/core/devices/nm-device-utils.c
index 170922eb..2bf24ae6 100644
--- a/src/core/devices/nm-device-utils.c
+++ b/src/core/devices/nm-device-utils.c
@@ -171,25 +171,25 @@ typedef struct {
 
 #define _NMLOG_PREFIX_NAME "resolve-addr"
 #define _NMLOG_DOMAIN      LOGD_CORE
-#define _NMLOG2(level, info, ...)                                                      \
-    G_STMT_START                                                                       \
-    {                                                                                  \
-        if (nm_logging_enabled((level), (_NMLOG_DOMAIN))) {                            \
-            ResolveAddrInfo *_info = (info);                                           \
-            char             _addr_str[NM_UTILS_INET_ADDRSTRLEN];                      \
-                                                                                       \
-            _nm_log((level),                                                           \
-                    (_NMLOG_DOMAIN),                                                   \
-                    0,                                                                 \
-                    NULL,                                                              \
-                    NULL,                                                              \
-                    _NMLOG_PREFIX_NAME "[" NM_HASH_OBFUSCATE_PTR_FMT                   \
-                                       ",%s]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),    \
-                    NM_HASH_OBFUSCATE_PTR(_info),                                      \
-                    nm_utils_inet_ntop(_info->addr_family, &_info->address, _addr_str) \
-                        _NM_UTILS_MACRO_REST(__VA_ARGS__));                            \
-        }                                                                              \
-    }                                                                                  \
+#define _NMLOG2(level, info, ...)                                                   \
+    G_STMT_START                                                                    \
+    {                                                                               \
+        if (nm_logging_enabled((level), (_NMLOG_DOMAIN))) {                         \
+            ResolveAddrInfo *_info = (info);                                        \
+            char             _addr_str[NM_INET_ADDRSTRLEN];                         \
+                                                                                    \
+            _nm_log((level),                                                        \
+                    (_NMLOG_DOMAIN),                                                \
+                    0,                                                              \
+                    NULL,                                                           \
+                    NULL,                                                           \
+                    _NMLOG_PREFIX_NAME "[" NM_HASH_OBFUSCATE_PTR_FMT                \
+                                       ",%s]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
+                    NM_HASH_OBFUSCATE_PTR(_info),                                   \
+                    nm_inet_ntop(_info->addr_family, &_info->address, _addr_str)    \
+                        _NM_UTILS_MACRO_REST(__VA_ARGS__));                         \
+        }                                                                           \
+    }                                                                               \
     G_STMT_END
 
 static void
@@ -234,9 +234,9 @@ resolve_addr_helper_cb(GObject *source, GAsyncResult *result, gpointer user_data
 static void
 resolve_addr_spawn_helper(ResolveAddrInfo *info)
 {
-    char addr_str[NM_UTILS_INET_ADDRSTRLEN];
+    char addr_str[NM_INET_ADDRSTRLEN];
 
-    nm_utils_inet_ntop(info->addr_family, &info->address, addr_str);
+    nm_inet_ntop(info->addr_family, &info->address, addr_str);
     _LOG2D(info, "start lookup via nm-daemon-helper");
     nm_utils_spawn_helper(NM_MAKE_STRV("resolve-address", addr_str),
                           g_task_get_cancellable(info->task),
diff --git a/src/core/devices/nm-device-veth.c b/src/core/devices/nm-device-veth.c
index 17115d33..c4b9e234 100644
--- a/src/core/devices/nm-device-veth.c
+++ b/src/core/devices/nm-device-veth.c
@@ -215,7 +215,7 @@ nm_device_veth_class_init(NMDeviceVethClass *klass)
 
 #define NM_TYPE_VETH_DEVICE_FACTORY (nm_veth_device_factory_get_type())
 #define NM_VETH_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VETH_DEVICE_FACTORY, NMVethDeviceFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VETH_DEVICE_FACTORY, NMVethDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-veth.h b/src/core/devices/nm-device-veth.h
index d43a0a4b..39182363 100644
--- a/src/core/devices/nm-device-veth.h
+++ b/src/core/devices/nm-device-veth.h
@@ -9,7 +9,8 @@
 #include "nm-device-ethernet.h"
 
 #define NM_TYPE_DEVICE_VETH (nm_device_veth_get_type())
-#define NM_DEVICE_VETH(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VETH, NMDeviceVeth))
+#define NM_DEVICE_VETH(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VETH, NMDeviceVeth))
 #define NM_DEVICE_VETH_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_VETH, NMDeviceVethClass))
 #define NM_IS_DEVICE_VETH(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_VETH))
diff --git a/src/core/devices/nm-device-vlan.c b/src/core/devices/nm-device-vlan.c
index 0c101c70..feb011db 100644
--- a/src/core/devices/nm-device-vlan.c
+++ b/src/core/devices/nm-device-vlan.c
@@ -8,6 +8,7 @@
 #include "nm-device-vlan.h"
 
 #include <sys/socket.h>
+#include <linux/if_ether.h>
 
 #include "nm-manager.h"
 #include "nm-utils.h"
@@ -31,7 +32,6 @@
 NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceVlan, PROP_VLAN_ID, );
 
 typedef struct {
-    gulong parent_state_id;
     gulong parent_hwaddr_id;
     gulong parent_mtu_id;
     guint  vlan_id;
@@ -54,25 +54,6 @@ G_DEFINE_TYPE(NMDeviceVlan, nm_device_vlan, NM_TYPE_DEVICE)
 /*****************************************************************************/
 
 static void
-parent_state_changed(NMDevice           *parent,
-                     NMDeviceState       new_state,
-                     NMDeviceState       old_state,
-                     NMDeviceStateReason reason,
-                     gpointer            user_data)
-{
-    NMDeviceVlan *self = NM_DEVICE_VLAN(user_data);
-
-    /* We'll react to our own carrier state notifications. Ignore the parent's. */
-    if (nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_CARRIER)
-        return;
-
-    nm_device_set_unmanaged_by_flags(NM_DEVICE(self),
-                                     NM_UNMANAGED_PARENT,
-                                     !nm_device_get_managed(parent, FALSE),
-                                     reason);
-}
-
-static void
 parent_mtu_maybe_changed(NMDevice *parent, GParamSpec *pspec, gpointer user_data)
 {
     /* the MTU of a VLAN device is limited by the parent's MTU.
@@ -132,19 +113,10 @@ parent_changed_notify(NMDevice *device,
     NM_DEVICE_CLASS(nm_device_vlan_parent_class)
         ->parent_changed_notify(device, old_ifindex, old_parent, new_ifindex, new_parent);
 
-    /*  note that @self doesn't have to clear @parent_state_id on dispose,
-     *  because NMDevice's dispose() will unset the parent, which in turn calls
-     *  parent_changed_notify(). */
-    nm_clear_g_signal_handler(old_parent, &priv->parent_state_id);
     nm_clear_g_signal_handler(old_parent, &priv->parent_hwaddr_id);
     nm_clear_g_signal_handler(old_parent, &priv->parent_mtu_id);
 
     if (new_parent) {
-        priv->parent_state_id = g_signal_connect(new_parent,
-                                                 NM_DEVICE_STATE_CHANGED,
-                                                 G_CALLBACK(parent_state_changed),
-                                                 device);
-
         priv->parent_hwaddr_id = g_signal_connect(new_parent,
                                                   "notify::" NM_DEVICE_HW_ADDRESS,
                                                   G_CALLBACK(parent_hwaddr_maybe_changed),
@@ -156,12 +128,6 @@ parent_changed_notify(NMDevice *device,
                                                G_CALLBACK(parent_mtu_maybe_changed),
                                                device);
         parent_mtu_maybe_changed(new_parent, NULL, self);
-
-        /* Set parent-dependent unmanaged flag */
-        nm_device_set_unmanaged_by_flags(device,
-                                         NM_UNMANAGED_PARENT,
-                                         !nm_device_get_managed(new_parent, FALSE),
-                                         NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED);
     }
 
     /* Recheck availability now that the parent has changed */
@@ -227,6 +193,8 @@ create_and_realize(NMDevice              *device,
     int                  parent_ifindex;
     guint                vlan_id;
     int                  r;
+    const char          *protocol_str;
+    guint16              protocol = ETH_P_8021Q;
 
     s_vlan = nm_connection_get_setting_vlan(connection);
     g_assert(s_vlan);
@@ -262,11 +230,22 @@ create_and_realize(NMDevice              *device,
 
     vlan_id = nm_setting_vlan_get_id(s_vlan);
 
+    protocol_str = nm_setting_vlan_get_protocol(s_vlan);
+    if (protocol_str) {
+        if (nm_streq(protocol_str, "802.1ad"))
+            protocol = ETH_P_8021AD;
+        else
+            nm_assert(nm_streq(protocol_str, "802.1Q"));
+    }
+
     r = nm_platform_link_vlan_add(nm_device_get_platform(device),
                                   iface,
                                   parent_ifindex,
-                                  vlan_id,
-                                  nm_setting_vlan_get_flags(s_vlan),
+                                  &((NMPlatformLnkVlan){
+                                      .id       = vlan_id,
+                                      .flags    = nm_setting_vlan_get_flags(s_vlan),
+                                      .protocol = protocol,
+                                  }),
                                   out_plink);
     if (r < 0) {
         g_set_error(error,
@@ -473,6 +452,9 @@ update_connection(NMDevice *device, NMConnection *connection)
         _nm_setting_vlan_set_priorities(s_vlan, NM_VLAN_INGRESS_MAP, NULL, 0);
         _nm_setting_vlan_set_priorities(s_vlan, NM_VLAN_EGRESS_MAP, NULL, 0);
     }
+
+    if (polnk && polnk->lnk_vlan.protocol == ETH_P_8021AD)
+        g_object_set(s_vlan, NM_SETTING_VLAN_PROTOCOL, "802.1ad", NULL);
 }
 
 static NMActStageReturn
@@ -592,7 +574,7 @@ nm_device_vlan_class_init(NMDeviceVlanClass *klass)
 
 #define NM_TYPE_VLAN_DEVICE_FACTORY (nm_vlan_device_factory_get_type())
 #define NM_VLAN_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VLAN_DEVICE_FACTORY, NMVlanDeviceFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VLAN_DEVICE_FACTORY, NMVlanDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-vlan.h b/src/core/devices/nm-device-vlan.h
index 5270706f..06fef5cf 100644
--- a/src/core/devices/nm-device-vlan.h
+++ b/src/core/devices/nm-device-vlan.h
@@ -9,7 +9,8 @@
 #include "nm-device.h"
 
 #define NM_TYPE_DEVICE_VLAN (nm_device_vlan_get_type())
-#define NM_DEVICE_VLAN(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VLAN, NMDeviceVlan))
+#define NM_DEVICE_VLAN(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VLAN, NMDeviceVlan))
 #define NM_DEVICE_VLAN_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_VLAN, NMDeviceVlanClass))
 #define NM_IS_DEVICE_VLAN(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_VLAN))
diff --git a/src/core/devices/nm-device-vrf.c b/src/core/devices/nm-device-vrf.c
index 91eb195a..ad31f3a2 100644
--- a/src/core/devices/nm-device-vrf.c
+++ b/src/core/devices/nm-device-vrf.c
@@ -347,7 +347,7 @@ nm_device_vrf_class_init(NMDeviceVrfClass *klass)
 
 #define NM_TYPE_VRF_DEVICE_FACTORY (nm_vrf_device_factory_get_type())
 #define NM_VRF_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VRF_DEVICE_FACTORY, NMVrfDeviceFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VRF_DEVICE_FACTORY, NMVrfDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-vrf.h b/src/core/devices/nm-device-vrf.h
index 5169041c..c6580fb8 100644
--- a/src/core/devices/nm-device-vrf.h
+++ b/src/core/devices/nm-device-vrf.h
@@ -6,7 +6,7 @@
 #include "nm-device-generic.h"
 
 #define NM_TYPE_DEVICE_VRF (nm_device_vrf_get_type())
-#define NM_DEVICE_VRF(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VRF, NMDeviceVrf))
+#define NM_DEVICE_VRF(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VRF, NMDeviceVrf))
 #define NM_DEVICE_VRF_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_VRF, NMDeviceVrfClass))
 #define NM_IS_DEVICE_VRF(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_VRF))
diff --git a/src/core/devices/nm-device-vxlan.c b/src/core/devices/nm-device-vxlan.c
index bf7c94c0..44a7be33 100644
--- a/src/core/devices/nm-device-vxlan.c
+++ b/src/core/devices/nm-device-vxlan.c
@@ -174,15 +174,15 @@ create_and_realize(NMDevice              *device,
 
     str = nm_setting_vxlan_get_local(s_vxlan);
     if (str) {
-        if (!nm_utils_parse_inaddr_bin(AF_INET, str, NULL, &props.local)
-            && !nm_utils_parse_inaddr_bin(AF_INET6, str, NULL, &props.local6))
+        if (!nm_inet_parse_bin(AF_INET, str, NULL, &props.local)
+            && !nm_inet_parse_bin(AF_INET6, str, NULL, &props.local6))
             return FALSE;
     }
 
     str = nm_setting_vxlan_get_remote(s_vxlan);
     if (str) {
-        if (!nm_utils_parse_inaddr_bin(AF_INET, str, NULL, &props.group)
-            && !nm_utils_parse_inaddr_bin(AF_INET6, str, NULL, &props.group6))
+        if (!nm_inet_parse_bin(AF_INET, str, NULL, &props.group)
+            && !nm_inet_parse_bin(AF_INET6, str, NULL, &props.group6))
             return FALSE;
     }
 
@@ -223,7 +223,7 @@ address_matches(const char *candidate, in_addr_t addr4, struct in6_addr *addr6)
     if (!candidate)
         return addr4 == 0u && IN6_IS_ADDR_UNSPECIFIED(addr6);
 
-    if (!nm_utils_parse_inaddr_bin(AF_UNSPEC, candidate, &addr_family, &candidate_addr))
+    if (!nm_inet_parse_bin(AF_UNSPEC, candidate, &addr_family, &candidate_addr))
         return FALSE;
 
     if (!nm_ip_addr_equal(addr_family,
@@ -401,7 +401,7 @@ update_connection(NMDevice *device, NMConnection *connection)
 {
     NMDeviceVxlanPrivate *priv = NM_DEVICE_VXLAN_GET_PRIVATE(device);
     NMSettingVxlan *s_vxlan    = _nm_connection_ensure_setting(connection, NM_TYPE_SETTING_VXLAN);
-    char            sbuf[NM_UTILS_INET_ADDRSTRLEN];
+    char            sbuf[NM_INET_ADDRSTRLEN];
 
     if (priv->props.id != nm_setting_vxlan_get_id(s_vxlan))
         g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_ID, priv->props.id, NULL);
@@ -417,12 +417,12 @@ update_connection(NMDevice *device, NMConnection *connection)
         if (priv->props.group) {
             g_object_set(s_vxlan,
                          NM_SETTING_VXLAN_REMOTE,
-                         _nm_utils_inet4_ntop(priv->props.group, sbuf),
+                         nm_inet4_ntop(priv->props.group, sbuf),
                          NULL);
         } else {
             g_object_set(s_vxlan,
                          NM_SETTING_VXLAN_REMOTE,
-                         _nm_utils_inet6_ntop(&priv->props.group6, sbuf),
+                         nm_inet6_ntop(&priv->props.group6, sbuf),
                          NULL);
         }
     }
@@ -433,12 +433,12 @@ update_connection(NMDevice *device, NMConnection *connection)
         if (priv->props.local) {
             g_object_set(s_vxlan,
                          NM_SETTING_VXLAN_LOCAL,
-                         _nm_utils_inet4_ntop(priv->props.local, sbuf),
+                         nm_inet4_ntop(priv->props.local, sbuf),
                          NULL);
         } else if (memcmp(&priv->props.local6, &in6addr_any, sizeof(in6addr_any))) {
             g_object_set(s_vxlan,
                          NM_SETTING_VXLAN_LOCAL,
-                         _nm_utils_inet6_ntop(&priv->props.local6, sbuf),
+                         nm_inet6_ntop(&priv->props.local6, sbuf),
                          NULL);
         }
     }
@@ -510,15 +510,15 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
         break;
     case PROP_GROUP:
         if (priv->props.group)
-            g_value_take_string(value, nm_utils_inet4_ntop_dup(priv->props.group));
+            g_value_take_string(value, nm_inet4_ntop_dup(priv->props.group));
         else if (!IN6_IS_ADDR_UNSPECIFIED(&priv->props.group6))
-            g_value_take_string(value, nm_utils_inet6_ntop_dup(&priv->props.group6));
+            g_value_take_string(value, nm_inet6_ntop_dup(&priv->props.group6));
         break;
     case PROP_LOCAL:
         if (priv->props.local)
-            g_value_take_string(value, nm_utils_inet4_ntop_dup(priv->props.local));
+            g_value_take_string(value, nm_inet4_ntop_dup(priv->props.local));
         else if (!IN6_IS_ADDR_UNSPECIFIED(&priv->props.local6))
-            g_value_take_string(value, nm_utils_inet6_ntop_dup(&priv->props.local6));
+            g_value_take_string(value, nm_inet6_ntop_dup(&priv->props.local6));
         break;
     case PROP_TOS:
         g_value_set_uchar(value, priv->props.tos);
@@ -741,7 +741,7 @@ nm_device_vxlan_class_init(NMDeviceVxlanClass *klass)
 
 #define NM_TYPE_VXLAN_DEVICE_FACTORY (nm_vxlan_device_factory_get_type())
 #define NM_VXLAN_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VXLAN_DEVICE_FACTORY, NMVxlanDeviceFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VXLAN_DEVICE_FACTORY, NMVxlanDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-vxlan.h b/src/core/devices/nm-device-vxlan.h
index 4c4165e5..e53ae82e 100644
--- a/src/core/devices/nm-device-vxlan.h
+++ b/src/core/devices/nm-device-vxlan.h
@@ -10,7 +10,7 @@
 
 #define NM_TYPE_DEVICE_VXLAN (nm_device_vxlan_get_type())
 #define NM_DEVICE_VXLAN(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VXLAN, NMDeviceVxlan))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VXLAN, NMDeviceVxlan))
 #define NM_DEVICE_VXLAN_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_VXLAN, NMDeviceVxlanClass))
 #define NM_IS_DEVICE_VXLAN(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_VXLAN))
diff --git a/src/core/devices/nm-device-wireguard.c b/src/core/devices/nm-device-wireguard.c
index 179c2a22..db02033d 100644
--- a/src/core/devices/nm-device-wireguard.c
+++ b/src/core/devices/nm-device-wireguard.c
@@ -219,7 +219,7 @@ _auto_default_route_get_enabled(NMSettingWireGuard *s_wg,
                 aip = nm_wireguard_peer_get_allowed_ip(peer, j, &valid);
                 if (!valid)
                     continue;
-                if (!nm_utils_parse_inaddr_prefix_bin(AF_UNSPEC, aip, &addr_family, NULL, &prefix))
+                if (!nm_inet_parse_with_prefix_bin(AF_UNSPEC, aip, &addr_family, NULL, &prefix))
                     continue;
                 if (prefix != 0)
                     continue;
@@ -1152,11 +1152,11 @@ _peers_get_platform_list(NMDeviceWireGuardPrivate            *priv,
 
                 aip = nm_wireguard_peer_get_allowed_ip(peer_data->peer, i_aip, &valid);
                 if (!valid
-                    || !nm_utils_parse_inaddr_prefix_bin(AF_UNSPEC,
-                                                         aip,
-                                                         &addr_family,
-                                                         &addrbin,
-                                                         &prefix)) {
+                    || !nm_inet_parse_with_prefix_bin(AF_UNSPEC,
+                                                      aip,
+                                                      &addr_family,
+                                                      &addrbin,
+                                                      &prefix)) {
                     /* the address is really not expected to be invalid, because then
                      * the connection would not verify. Anyway, silently skip it. */
                     continue;
@@ -1197,7 +1197,7 @@ skip:
             nm_assert(plp->_construct_idx_start < plp->_construct_idx_end);
             l = plp->_construct_idx_end - plp->_construct_idx_start;
             plp->allowed_ips =
-                &g_array_index(allowed_ips, NMPWireGuardAllowedIP, plp->_construct_idx_start);
+                &nm_g_array_index(allowed_ips, NMPWireGuardAllowedIP, plp->_construct_idx_start);
             plp->allowed_ips_len = l;
         }
     }
@@ -1701,8 +1701,7 @@ _get_dev2_ip_config(NMDeviceWireGuard *self, int addr_family)
 
             aip = nm_wireguard_peer_get_allowed_ip(peer, j, &valid);
 
-            if (!valid
-                || !nm_utils_parse_inaddr_prefix_bin(addr_family, aip, NULL, &addrbin, &prefix))
+            if (!valid || !nm_inet_parse_with_prefix_bin(addr_family, aip, NULL, &addrbin, &prefix))
                 continue;
 
             if (prefix < 0)
@@ -1722,7 +1721,7 @@ _get_dev2_ip_config(NMDeviceWireGuard *self, int addr_family)
                                             NM_L3_CONFIG_DAT_FLAGS_IGNORE_MERGE_NO_DEFAULT_ROUTES);
             }
 
-            nm_utils_ipx_address_clear_host_address(addr_family, &addrbin, NULL, prefix);
+            nm_ip_addr_clear_host_address(addr_family, &addrbin, NULL, prefix);
 
             rtable_coerced = route_table_coerced;
 
@@ -2062,8 +2061,10 @@ nm_device_wireguard_class_init(NMDeviceWireGuardClass *klass)
 /*************************************************************/
 
 #define NM_TYPE_WIREGUARD_DEVICE_FACTORY (nm_wireguard_device_factory_get_type())
-#define NM_WIREGUARD_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIREGUARD_DEVICE_FACTORY, NMWireGuardDeviceFactory))
+#define NM_WIREGUARD_DEVICE_FACTORY(obj)                              \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj),                            \
+                                    NM_TYPE_WIREGUARD_DEVICE_FACTORY, \
+                                    NMWireGuardDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-wireguard.h b/src/core/devices/nm-device-wireguard.h
index 7e18bdba..8fb8f8cf 100644
--- a/src/core/devices/nm-device-wireguard.h
+++ b/src/core/devices/nm-device-wireguard.h
@@ -10,7 +10,7 @@
 
 #define NM_TYPE_DEVICE_WIREGUARD (nm_device_wireguard_get_type())
 #define NM_DEVICE_WIREGUARD(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIREGUARD, NMDeviceWireGuard))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIREGUARD, NMDeviceWireGuard))
 #define NM_DEVICE_WIREGUARD_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_WIREGUARD, NMDeviceWireGuardClass))
 #define NM_IS_DEVICE_WIREGUARD(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_WIREGUARD))
diff --git a/src/core/devices/nm-device-wpan.c b/src/core/devices/nm-device-wpan.c
index ecb5ee23..98356ccf 100644
--- a/src/core/devices/nm-device-wpan.c
+++ b/src/core/devices/nm-device-wpan.c
@@ -221,7 +221,7 @@ nm_device_wpan_class_init(NMDeviceWpanClass *klass)
 
 #define NM_TYPE_WPAN_DEVICE_FACTORY (nm_wpan_device_factory_get_type())
 #define NM_WPAN_DEVICE_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WPAN_DEVICE_FACTORY, NMWpanDeviceFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WPAN_DEVICE_FACTORY, NMWpanDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-wpan.h b/src/core/devices/nm-device-wpan.h
index 969929ae..39cf4656 100644
--- a/src/core/devices/nm-device-wpan.h
+++ b/src/core/devices/nm-device-wpan.h
@@ -7,7 +7,8 @@
 #define __NETWORKMANAGER_DEVICE_WPAN_H__
 
 #define NM_TYPE_DEVICE_WPAN (nm_device_wpan_get_type())
-#define NM_DEVICE_WPAN(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WPAN, NMDeviceWpan))
+#define NM_DEVICE_WPAN(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WPAN, NMDeviceWpan))
 #define NM_DEVICE_WPAN_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_WPAN, NMDeviceWpanClass))
 #define NM_IS_DEVICE_WPAN(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_WPAN))
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index c4f0f9a2..04478614 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -18,7 +18,7 @@
 #include <netinet/in.h>
 #include <netinet/if_ether.h>
 #include <linux/if.h>
-#include <linux/if_addr.h>
+#include "nm-compat-headers/linux/if_addr.h"
 #include <linux/rtnetlink.h>
 #include <linux/if_ether.h>
 #include <linux/if_infiniband.h>
@@ -59,6 +59,7 @@
 #include "settings/nm-settings.h"
 #include "nm-setting-ethtool.h"
 #include "nm-setting-ovs-external-ids.h"
+#include "nm-setting-ovs-other-config.h"
 #include "nm-setting-user.h"
 #include "nm-auth-utils.h"
 #include "nm-keep-alive.h"
@@ -77,6 +78,7 @@
 
 #include "nm-device-generic.h"
 #include "nm-device-bridge.h"
+#include "nm-device-loopback.h"
 #include "nm-device-vlan.h"
 #include "nm-device-vrf.h"
 #include "nm-device-wireguard.h"
@@ -110,6 +112,7 @@ typedef enum {
     CLEANUP_TYPE_KEEP,
     CLEANUP_TYPE_REMOVED,
     CLEANUP_TYPE_DECONFIGURE,
+    CLEANUP_TYPE_KEEP_REAPPLY,
 } CleanupType;
 
 typedef enum _nm_packed {
@@ -848,7 +851,7 @@ static void _dev_ipshared4_spawn_dnsmasq(NMDevice *self);
 static void _dev_ipshared6_start(NMDevice *self);
 
 static void
-_cleanup_ip_pre(NMDevice *self, int addr_family, CleanupType cleanup_type, gboolean from_reapply);
+_cleanup_ip_pre(NMDevice *self, int addr_family, CleanupType cleanup_type, gboolean preserve_dhcp);
 
 static void concheck_update_state(NMDevice           *self,
                                   int                 addr_family,
@@ -1583,6 +1586,9 @@ _prop_get_ipv4_link_local(NMDevice *self)
     if (!s_ip4)
         return NM_SETTING_IP4_LL_DISABLED;
 
+    if (NM_IS_DEVICE_LOOPBACK(self))
+        return NM_SETTING_IP4_LL_DISABLED;
+
     link_local = nm_setting_ip4_config_get_link_local(s_ip4);
 
     if (link_local == NM_SETTING_IP4_LL_DEFAULT) {
@@ -2736,12 +2742,6 @@ _ethtool_state_set(NMDevice *self)
 
 /*****************************************************************************/
 
-static gboolean
-is_loopback(NMDevice *self)
-{
-    return NM_IS_DEVICE_GENERIC(self) && NM_DEVICE_GET_PRIVATE(self)->ifindex == 1;
-}
-
 gboolean
 nm_device_is_vpn(NMDevice *self)
 {
@@ -3578,7 +3578,7 @@ _dev_ip_state_check_async(NMDevice *self, int addr_family)
 }
 
 static void
-_dev_ip_state_cleanup(NMDevice *self, int addr_family, gboolean from_reapply)
+_dev_ip_state_cleanup(NMDevice *self, int addr_family, gboolean keep_reapply)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
     int              IS_IPv4;
@@ -3586,7 +3586,7 @@ _dev_ip_state_cleanup(NMDevice *self, int addr_family, gboolean from_reapply)
     if (addr_family == AF_UNSPEC) {
         _dev_ip_state_set_state(self,
                                 addr_family,
-                                from_reapply ? NM_DEVICE_IP_STATE_PENDING : NM_DEVICE_IP_STATE_NONE,
+                                keep_reapply ? NM_DEVICE_IP_STATE_PENDING : NM_DEVICE_IP_STATE_NONE,
                                 "ip-state-clear");
         return;
     }
@@ -3597,7 +3597,7 @@ _dev_ip_state_cleanup(NMDevice *self, int addr_family, gboolean from_reapply)
     nm_clear_g_source_inst(&priv->ip_data_x[IS_IPv4].req_timeout_source);
     _dev_ip_state_set_state(self,
                             addr_family,
-                            from_reapply ? NM_DEVICE_IP_STATE_PENDING : NM_DEVICE_IP_STATE_NONE,
+                            keep_reapply ? NM_DEVICE_IP_STATE_PENDING : NM_DEVICE_IP_STATE_NONE,
                             "ip-state-clear");
     priv->ip_data_x[IS_IPv4].wait_for_carrier = FALSE;
     priv->ip_data_x[IS_IPv4].wait_for_ports   = FALSE;
@@ -4129,18 +4129,30 @@ _dev_l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, N
         /* Check if AC6 addresses completed DAD */
         if (NM_FLAGS_ANY(notify_data->platform_change_on_idle.obj_type_flags,
                          nmp_object_type_to_flags(NMP_OBJECT_TYPE_IP6_ADDRESS))
-            && priv->ipac6_data.state == NM_DEVICE_IP_STATE_PENDING && priv->ipac6_data.l3cd
-            && nm_l3cfg_check_ready(l3cfg,
-                                    priv->ipac6_data.l3cd,
-                                    AF_INET6,
-                                    NM_L3CFG_CHECK_READY_FLAGS_IP6_DAD_READY,
-                                    NULL)) {
-            if (nm_l3cfg_has_temp_not_available_obj(priv->l3cfg, AF_INET6))
-                _dev_l3_cfg_commit(self, FALSE);
-
-            nm_clear_l3cd(&priv->ipac6_data.l3cd);
-            _dev_ipac6_set_state(self, NM_DEVICE_IP_STATE_READY);
-            _dev_ip_state_check_async(self, AF_INET6);
+            && priv->ipac6_data.state == NM_DEVICE_IP_STATE_PENDING && priv->ipac6_data.l3cd) {
+            gs_unref_array GArray *conflicts = NULL;
+            gboolean               ready;
+
+            ready = nm_l3cfg_check_ready(l3cfg,
+                                         priv->ipac6_data.l3cd,
+                                         AF_INET6,
+                                         NM_L3CFG_CHECK_READY_FLAGS_IP6_DAD_READY,
+                                         &conflicts);
+            if (conflicts) {
+                /* nm_ndisc_dad_failed() will emit a new "NDisc:config-received"
+                 * signal; _dev_ipac6_ndisc_config_changed() will be called
+                 * synchronously to update the current state and schedule a commit. */
+                nm_ndisc_dad_failed(priv->ipac6_data.ndisc, conflicts, TRUE);
+            } else if (ready) {
+                if (nm_l3cfg_has_temp_not_available_obj(priv->l3cfg, AF_INET6))
+                    _dev_l3_cfg_commit(self, FALSE);
+
+                nm_clear_l3cd(&priv->ipac6_data.l3cd);
+                _dev_ipac6_set_state(self, NM_DEVICE_IP_STATE_READY);
+                _dev_ip_state_check_async(self, AF_INET6);
+            } else {
+                /* wait */
+            }
         }
 
         _dev_ipmanual_check_ready(self);
@@ -5005,6 +5017,9 @@ nm_device_get_route_metric_default(NMDeviceType device_type)
      */
 
     switch (device_type) {
+    case NM_DEVICE_TYPE_LOOPBACK:
+        return 30;
+
     /* 50 is also used for VPN plugins (NM_VPN_ROUTE_METRIC_DEFAULT).
      *
      * Note that returning 50 from this function means that this device-type is
@@ -5425,7 +5440,7 @@ concheck_is_possible(NMDevice *self)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
-    if (!nm_device_is_real(self) || is_loopback(self))
+    if (!nm_device_is_real(self) || NM_IS_DEVICE_LOOPBACK(self))
         return FALSE;
 
     /* we enable periodic checks for every device state (except UNKNOWN). Especially with
@@ -6203,7 +6218,8 @@ nm_device_master_release_slave(NMDevice           *self,
     NMDevicePrivate          *priv;
     NMDevicePrivate          *slave_priv;
     SlaveInfo                *info;
-    gs_unref_object NMDevice *self_free = NULL;
+    gs_unref_object NMDevice *self_free  = NULL;
+    gs_unref_object NMDevice *slave_free = NULL;
 
     g_return_if_fail(NM_DEVICE(self));
     g_return_if_fail(NM_DEVICE(slave));
@@ -6246,14 +6262,15 @@ nm_device_master_release_slave(NMDevice           *self,
 
     /* keep both alive until the end of the function.
      * Transfers ownership from slave_priv->master.  */
-    self_free = self;
+    nm_assert(self == slave_priv->master);
+    self_free = g_steal_pointer(&slave_priv->master);
 
-    c_list_unlink(&info->lst_slave);
-    slave_priv->master = NULL;
+    nm_assert(slave == info->slave);
+    slave_free = g_steal_pointer(&info->slave);
 
+    c_list_unlink(&info->lst_slave);
     g_signal_handler_disconnect(slave, info->watch_id);
-    g_object_unref(slave);
-    g_slice_free(SlaveInfo, info);
+    nm_g_slice_free(info);
 
     if (c_list_is_empty(&priv->slaves)) {
         _active_connection_set_state_flags_full(self,
@@ -6577,27 +6594,30 @@ device_recheck_slave_status(NMDevice *self, const NMPlatformLink *plink)
                                        NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
     }
 
-    if (master && NM_DEVICE_GET_CLASS(master)->attach_port) {
-        nm_device_master_add_slave(master, self, FALSE);
+    if (master) {
+        if (NM_DEVICE_GET_CLASS(master)->attach_port) {
+            nm_device_master_add_slave(master, self, FALSE);
+        } else {
+            _LOGD(LOGD_DEVICE,
+                  "enslaved to non-master-type device %s; ignoring",
+                  nm_device_get_iface(master));
+        }
         goto out;
     }
 
-    if (master) {
-        _LOGD(LOGD_DEVICE,
-              "enslaved to non-master-type device %s; ignoring",
-              nm_device_get_iface(master));
-    } else {
+    if (plink->master) {
         _LOGD(LOGD_DEVICE,
               "enslaved to unknown device %d (%s%s%s)",
               plink->master,
               NM_PRINT_FMT_QUOTED(plink_master, "\"", plink_master->name, "\"", "??"));
+        if (!priv->ifindex_changed_id) {
+            priv->ifindex_changed_id = g_signal_connect(nm_device_get_manager(self),
+                                                        NM_MANAGER_DEVICE_IFINDEX_CHANGED,
+                                                        G_CALLBACK(device_ifindex_changed_cb),
+                                                        self);
+        }
     }
-    if (!priv->ifindex_changed_id) {
-        priv->ifindex_changed_id = g_signal_connect(nm_device_get_manager(self),
-                                                    NM_MANAGER_DEVICE_IFINDEX_CHANGED,
-                                                    G_CALLBACK(device_ifindex_changed_cb),
-                                                    self);
-    }
+
     return;
 
 out:
@@ -6609,6 +6629,8 @@ device_ifindex_changed_cb(NMManager *manager, NMDevice *device_changed, NMDevice
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
+    g_return_if_fail(priv->master_ifindex > 0);
+
     if (priv->master_ifindex != nm_device_get_ifindex(device_changed))
         return;
 
@@ -7465,11 +7487,6 @@ realize_start_setup(NMDevice             *self,
                                   NM_UNMANAGED_EXTERNAL_DOWN,
                                   _dev_unmanaged_is_external_down(self, TRUE));
 
-    /* Unmanaged the loopback device with an explicit NM_UNMANAGED_BY_TYPE flag.
-     * Later we might want to manage 'lo' too. Currently, that doesn't work because
-     * NetworkManager might down the interface or remove the 127.0.0.1 address. */
-    nm_device_set_unmanaged_flags(self, NM_UNMANAGED_BY_TYPE, is_loopback(self));
-
     nm_device_set_unmanaged_by_user_udev(self);
     nm_device_set_unmanaged_by_user_conf(self);
 
@@ -7659,8 +7676,7 @@ nm_device_unrealize(NMDevice *self, gboolean remove_resources, GError **error)
     nm_device_set_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT, TRUE);
 
     nm_device_set_unmanaged_flags(self,
-                                  NM_UNMANAGED_PARENT | NM_UNMANAGED_BY_TYPE
-                                      | NM_UNMANAGED_USER_UDEV | NM_UNMANAGED_USER_EXPLICIT
+                                  NM_UNMANAGED_USER_UDEV | NM_UNMANAGED_USER_EXPLICIT
                                       | NM_UNMANAGED_EXTERNAL_DOWN | NM_UNMANAGED_IS_SLAVE,
                                   NM_UNMAN_FLAG_OP_FORGET);
 
@@ -8585,7 +8601,7 @@ nm_device_generate_connection(NMDevice *self,
 
         pllink = nm_platform_link_get(nm_device_get_platform(self), priv->ifindex);
         if (pllink && pllink->inet6_token.id) {
-            char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+            char sbuf[NM_INET_ADDRSTRLEN];
 
             g_object_set(s_ip6,
                          NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE,
@@ -8776,6 +8792,14 @@ check_connection_compatible(NMDevice *self, NMConnection *connection, GError **e
         return FALSE;
     }
 
+    if (!nm_device_has_capability(self, NM_DEVICE_CAP_SRIOV)
+        && nm_connection_get_setting(connection, NM_TYPE_SETTING_SRIOV)) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "device does not support SR-IOV");
+        return FALSE;
+    }
+
     conn_iface = nm_manager_get_connection_iface(NM_MANAGER_GET, connection, NULL, NULL, &local);
 
     /* We always need a interface name for virtual devices, but for
@@ -9359,14 +9383,17 @@ activate_stage1_device_prepare(NMDevice *self)
     nm_device_state_changed(self, NM_DEVICE_STATE_PREPARE, NM_DEVICE_STATE_REASON_NONE);
 
     if (priv->stage1_sriov_state != NM_DEVICE_STAGE_STATE_COMPLETED) {
-        NMSettingSriov *s_sriov;
+        NMSettingSriov *s_sriov = NULL;
 
         if (nm_device_sys_iface_state_is_external_or_assume(self)) {
             /* pass */
-        } else if (priv->stage1_sriov_state == NM_DEVICE_STAGE_STATE_PENDING)
+        } else if (priv->stage1_sriov_state == NM_DEVICE_STAGE_STATE_PENDING) {
             return;
-        else if (priv->ifindex > 0 && nm_device_has_capability(self, NM_DEVICE_CAP_SRIOV)
-                 && (s_sriov = nm_device_get_applied_setting(self, NM_TYPE_SETTING_SRIOV))) {
+        } else if (priv->ifindex > 0) {
+            s_sriov = nm_device_get_applied_setting(self, NM_TYPE_SETTING_SRIOV);
+        }
+
+        if (s_sriov) {
             nm_auto_freev NMPlatformVF **plat_vfs = NULL;
             gs_free_error GError        *error    = NULL;
             NMSriovVF                   *vf;
@@ -9374,6 +9401,8 @@ activate_stage1_device_prepare(NMDevice *self)
             guint                        num;
             guint                        i;
 
+            nm_assert(nm_device_has_capability(self, NM_DEVICE_CAP_SRIOV));
+
             autoprobe = nm_setting_sriov_get_autoprobe_drivers(s_sriov);
             if (autoprobe == NM_TERNARY_DEFAULT) {
                 autoprobe = nm_config_data_get_connection_default_int64(
@@ -10058,7 +10087,7 @@ _dev_ipmanual_check_ready(NMDevice *self)
     gboolean               has_carrier;
     NML3CfgCheckReadyFlags flags;
     gboolean               ready;
-    gboolean               acd_used = FALSE;
+    gs_unref_array GArray *conflicts = NULL;
     int                    IS_IPv4;
 
     if (priv->ipmanual_data.state_4 != NM_DEVICE_IP_STATE_PENDING
@@ -10101,8 +10130,8 @@ _dev_ipmanual_check_ready(NMDevice *self)
                                      priv->l3cds[L3_CONFIG_DATA_TYPE_MANUALIP].d,
                                      addr_family,
                                      flags,
-                                     &acd_used);
-        if (acd_used) {
+                                     &conflicts);
+        if (conflicts) {
             _dev_ipmanual_set_state(self, addr_family, NM_DEVICE_IP_STATE_FAILED);
             _dev_ip_state_check_async(self, AF_UNSPEC);
         } else if (ready) {
@@ -10434,6 +10463,7 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
                     .request_broadcast = request_broadcast,
                     .acd_timeout_msec  = _prop_get_ipv4_dad_timeout(self),
                 },
+            .previous_lease = priv->l3cds[L3_CONFIG_DATA_TYPE_DHCP_X(IS_IPv4)].d,
         };
 
         priv->ipdhcp_data_4.client =
@@ -10484,16 +10514,16 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
                          G_CALLBACK(_dev_ipdhcpx_notify),
                          self);
 
-    /* FIXME(l3cfg:dhcp:previous-lease): take the NML3ConfigData from the previous lease (if any)
-     * and pass it on to NMDhcpClient. This is a fake lease that we use initially (until
-     * NMDhcpClient got a real lease). Note that NMDhcpClient needs to check whether the
-     * lease already expired. */
-
+    /* Take the NML3ConfigData from the previous lease (if any) that was passed to the NMDhcpClient.
+     * This may be the old lease only used during the duration of a reapply until we get the
+     * new lease. */
     previous_lease = nm_dhcp_client_get_lease(priv->ipdhcp_data_x[IS_IPv4].client);
+
     if (!priv->ipdhcp_data_x[IS_IPv4].config) {
         priv->ipdhcp_data_x[IS_IPv4].config = nm_dhcp_config_new(addr_family, previous_lease);
         _notify(self, PROP_DHCPX_CONFIG(IS_IPv4));
     }
+
     if (previous_lease) {
         nm_dhcp_config_set_lease(priv->ipdhcp_data_x[IS_IPv4].config, previous_lease);
         _dev_l3_register_l3cds_set_one_full(self,
@@ -10747,14 +10777,14 @@ nm_device_copy_ip6_dns_config(NMDevice *self, NMDevice *from_device)
         l3cd_src = priv_src->l3cds[L3_CONFIG_DATA_TYPE_AC_6].d;
     }
     if (l3cd_src) {
-        const char *const     *strvarr;
-        const struct in6_addr *addrs;
-        guint                  n;
-        guint                  i;
+        const char *const *strvarr;
+        const char *const *addrs;
+        guint              n;
+        guint              i;
 
         addrs = nm_l3_config_data_get_nameservers(l3cd_src, AF_INET6, &n);
         for (i = 0; i < n; i++)
-            nm_l3_config_data_add_nameserver(l3cd, AF_INET6, &addrs[i]);
+            nm_l3_config_data_add_nameserver(l3cd, AF_INET6, addrs[i]);
 
         strvarr = nm_l3_config_data_get_searches(l3cd_src, AF_INET6, &n);
         for (i = 0; i < n; i++)
@@ -10806,7 +10836,8 @@ _dev_ipll6_set_llstate(NMDevice *self, NML3IPv6LLState llstate, const struct in6
               || (!priv->ipll_data_6.v6.ipv6ll
                   && NM_IN_SET(priv->ipll_data_6.v6.llstate,
                                NM_L3_IPV6LL_STATE_NONE,
-                               NM_L3_IPV6LL_STATE_DEFUNCT)));
+                               NM_L3_IPV6LL_STATE_DEFUNCT,
+                               NM_L3_IPV6LL_STATE_READY)));
 
     switch (priv->ipll_data_6.v6.llstate) {
     case NM_L3_IPV6LL_STATE_NONE:
@@ -10844,7 +10875,7 @@ _dev_ipll6_set_llstate(NMDevice *self, NML3IPv6LLState llstate, const struct in6
     }
 
     if (changed) {
-        char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+        char sbuf[NM_INET_ADDRSTRLEN];
 
         _LOGT_ipll(AF_INET6,
                    "set state %s (was %s, llstate=%s, lladdr=%s)",
@@ -10853,7 +10884,7 @@ _dev_ipll6_set_llstate(NMDevice *self, NML3IPv6LLState llstate, const struct in6
                    nm_l3_ipv6ll_state_to_string(priv->ipll_data_6.v6.llstate),
                    nm_ip_addr_is_null(AF_INET6, &priv->ipll_data_6.v6.lladdr)
                        ? "(none)"
-                       : _nm_utils_inet6_ntop(&priv->ipll_data_6.v6.lladdr, sbuf));
+                       : nm_inet6_ntop(&priv->ipll_data_6.v6.lladdr, sbuf));
     }
 
     if (changed)
@@ -10889,6 +10920,11 @@ _dev_ipll6_start(NMDevice *self)
     if (priv->ipll_data_6.v6.ipv6ll)
         return;
 
+    if (NM_IS_DEVICE_LOOPBACK(self)) {
+        _dev_ipll6_set_llstate(self, NM_L3_IPV6LL_STATE_READY, NULL);
+        return;
+    }
+
     if (!priv->l3cfg) {
         _LOGD(LOGD_IP6, "linklocal6: no IP link for IPv6");
         goto out_fail;
@@ -11000,6 +11036,10 @@ nm_device_get_configured_mtu_from_connection(NMDevice          *self,
         if (setting)
             mtu = nm_setting_wireguard_get_mtu(NM_SETTING_WIREGUARD(setting));
         global_property_name = NM_CON_DEFAULT("wireguard.mtu");
+    } else if (setting_type == NM_TYPE_SETTING_LOOPBACK) {
+        if (setting)
+            mtu = nm_setting_loopback_get_mtu(NM_SETTING_LOOPBACK(setting));
+        global_property_name = NM_CON_DEFAULT("loopback.mtu");
     } else
         g_return_val_if_reached(0);
 
@@ -11411,10 +11451,14 @@ _dev_ipac6_ndisc_config_changed(NMNDisc              *ndisc,
                                 const NML3ConfigData *l3cd,
                                 NMDevice             *self)
 {
-    NMDevicePrivate *priv  = NM_DEVICE_GET_PRIVATE(self);
-    gboolean         ready = TRUE;
-    NMDedupMultiIter iter;
-    const NMPObject *obj;
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    gboolean         ready;
+
+    /* The ndisc configuration changes when we receive a new RA or
+     * when a lifetime expires; but also when DAD fails for a
+     * SLAAC address and we need to regenerate new stable-privacy
+     * addresses. In all these cases we update the AC6 configuration,
+     * schedule a commit and update the AC state. */
 
     _dev_ipac6_grace_period_start(self, 0, TRUE);
 
@@ -11425,22 +11469,11 @@ _dev_ipac6_ndisc_config_changed(NMNDisc              *ndisc,
                                         FALSE);
 
     nm_clear_l3cd(&priv->ipac6_data.l3cd);
-
-    /* wait that addresses are committed to platform and
-     * become non-tentative before declaring AC6 is ready.*/
-    nm_l3_config_data_iter_obj_for_each (&iter, l3cd, &obj, NMP_OBJECT_TYPE_IP6_ADDRESS) {
-        const NMPlatformIP6Address *addr = NMP_OBJECT_CAST_IP6_ADDRESS(obj);
-        const NMPlatformIP6Address *plat_addr;
-
-        plat_addr = nm_platform_ip6_address_get(nm_device_get_platform(self),
-                                                nm_device_get_ip_ifindex(self),
-                                                &addr->address);
-        if (!plat_addr || (plat_addr->n_ifa_flags & IFA_F_TENTATIVE)) {
-            ready = FALSE;
-            break;
-        }
-    }
-
+    ready = nm_l3cfg_check_ready(priv->l3cfg,
+                                 l3cd,
+                                 AF_INET6,
+                                 NM_L3CFG_CHECK_READY_FLAGS_IP6_DAD_READY,
+                                 NULL);
     if (ready) {
         _dev_ipac6_set_state(self, NM_DEVICE_IP_STATE_READY);
     } else {
@@ -12078,13 +12111,34 @@ activate_stage3_ip_config(NMDevice *self)
 
     ifindex = nm_device_get_ip_ifindex(self);
 
+    ipv4_method = nm_device_get_effective_ip_config_method(self, AF_INET);
+    if (nm_streq(ipv4_method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) {
+        /* "auto" usually means DHCPv4 or autoconf6, but it doesn't have to be. Subclasses
+         * can overwrite it. For example, you cannot run DHCPv4 on PPP/WireGuard links. */
+        ipv4_method = klass->get_ip_method_auto(self, AF_INET);
+    }
+
+    ipv6_method = nm_device_get_effective_ip_config_method(self, AF_INET6);
+
+    if (nm_streq(ipv6_method, NM_SETTING_IP6_CONFIG_METHOD_AUTO)) {
+        ipv6_method = klass->get_ip_method_auto(self, AF_INET6);
+    }
+
     if (priv->ip_data_4.do_reapply) {
         _LOGD_ip(AF_INET, "reapply...");
-        _cleanup_ip_pre(self, AF_INET, CLEANUP_TYPE_DECONFIGURE, TRUE);
+        priv->ip_data_4.do_reapply = FALSE;
+        _cleanup_ip_pre(self,
+                        AF_INET,
+                        CLEANUP_TYPE_KEEP_REAPPLY,
+                        nm_streq(ipv4_method, NM_SETTING_IP4_CONFIG_METHOD_AUTO));
     }
     if (priv->ip_data_6.do_reapply) {
         _LOGD_ip(AF_INET6, "reapply...");
-        _cleanup_ip_pre(self, AF_INET6, CLEANUP_TYPE_DECONFIGURE, TRUE);
+        priv->ip_data_6.do_reapply = FALSE;
+        _cleanup_ip_pre(self,
+                        AF_INET6,
+                        CLEANUP_TYPE_KEEP_REAPPLY,
+                        nm_streq(ipv6_method, NM_SETTING_IP6_CONFIG_METHOD_AUTO));
     }
 
     /* Add the interface to the specified firewall zone */
@@ -12136,18 +12190,6 @@ activate_stage3_ip_config(NMDevice *self)
      * let's do it! */
     _commit_mtu(self);
 
-    ipv4_method = nm_device_get_effective_ip_config_method(self, AF_INET);
-    if (nm_streq(ipv4_method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) {
-        /* "auto" usually means DHCPv4 or autoconf6, but it doesn't have to be. Subclasses
-         * can overwrite it. For example, you cannot run DHCPv4 on PPP/WireGuard links. */
-        ipv4_method = klass->get_ip_method_auto(self, AF_INET);
-    }
-
-    ipv6_method = nm_device_get_effective_ip_config_method(self, AF_INET6);
-    if (nm_streq(ipv6_method, NM_SETTING_IP6_CONFIG_METHOD_AUTO)) {
-        ipv6_method = klass->get_ip_method_auto(self, AF_INET6);
-    }
-
     if (!nm_device_sys_iface_state_is_external(self)
         && (!klass->ready_for_ip_config || klass->ready_for_ip_config(self, TRUE))) {
         if (priv->ipmanual_data.state_6 == NM_DEVICE_IP_STATE_NONE
@@ -12274,16 +12316,28 @@ _dev_ipshared4_new_l3cd(NMDevice *self, NMConnection *connection, NMPlatformIP4A
 static gboolean
 _dev_ipshared4_init(NMDevice *self)
 {
-    static const char *const modules[] = {"ip_tables",
-                                          "iptable_nat",
-                                          "nf_nat_ftp",
-                                          "nf_nat_irc",
-                                          "nf_nat_sip",
-                                          "nf_nat_tftp",
-                                          "nf_nat_pptp",
-                                          "nf_nat_h323"};
-    int                      errsv;
-    guint                    i;
+    static const char *const modules_iptables[] = {"ip_tables", "iptable_nat"};
+    static const char *const modules_nftables[] =
+        {"nf_nat_ftp", "nf_nat_irc", "nf_nat_sip", "nf_nat_tftp", "nf_nat_pptp", "nf_nat_h323"};
+    int   errsv;
+    guint i;
+
+    switch (nm_firewall_utils_get_backend()) {
+    case NM_FIREWALL_BACKEND_IPTABLES:
+        for (i = 0; i < G_N_ELEMENTS(modules_iptables); i++)
+            nmp_utils_modprobe(NULL, FALSE, modules_iptables[i], NULL);
+        break;
+    case NM_FIREWALL_BACKEND_NFTABLES:
+        for (i = 0; i < G_N_ELEMENTS(modules_nftables); i++)
+            nmp_utils_modprobe(NULL, FALSE, modules_nftables[i], NULL);
+        break;
+    case NM_FIREWALL_BACKEND_NONE:
+        /* do not modify network settings like ip forwarding */
+        return TRUE;
+    default:
+        nm_assert_not_reached();
+        break;
+    }
 
     if (nm_platform_sysctl_get_int32(nm_device_get_platform(self),
                                      NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/ipv4/ip_forward"),
@@ -12312,9 +12366,6 @@ _dev_ipshared4_init(NMDevice *self)
                        nm_strerror_native(errsv));
     }
 
-    for (i = 0; i < G_N_ELEMENTS(modules); i++)
-        nmp_utils_modprobe(NULL, FALSE, modules[i], NULL);
-
     return TRUE;
 }
 
@@ -12591,17 +12642,18 @@ delete_on_deactivate_check_and_schedule(NMDevice *self)
 }
 
 static void
-_cleanup_ip_pre(NMDevice *self, int addr_family, CleanupType cleanup_type, gboolean from_reapply)
+_cleanup_ip_pre(NMDevice *self, int addr_family, CleanupType cleanup_type, gboolean preserve_dhcp)
 {
-    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
-    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    const int        IS_IPv4      = NM_IS_IPv4(addr_family);
+    NMDevicePrivate *priv         = NM_DEVICE_GET_PRIVATE(self);
+    gboolean         keep_reapply = (cleanup_type == CLEANUP_TYPE_KEEP_REAPPLY);
 
     _dev_ipsharedx_cleanup(self, addr_family);
 
     _dev_ipdev_cleanup(self, AF_UNSPEC);
     _dev_ipdev_cleanup(self, addr_family);
 
-    _dev_ipdhcpx_cleanup(self, addr_family, TRUE, FALSE);
+    _dev_ipdhcpx_cleanup(self, addr_family, !preserve_dhcp || !keep_reapply, FALSE);
 
     if (!IS_IPv4)
         _dev_ipac6_cleanup(self);
@@ -12613,8 +12665,8 @@ _cleanup_ip_pre(NMDevice *self, int addr_family, CleanupType cleanup_type, gbool
     nm_clear_g_signal_handler(nm_manager_get_dns_manager(priv->manager),
                               &priv->ip_data.dnsmgr_update_pending_signal_id);
 
-    _dev_ip_state_cleanup(self, AF_UNSPEC, from_reapply);
-    _dev_ip_state_cleanup(self, addr_family, from_reapply);
+    _dev_ip_state_cleanup(self, AF_UNSPEC, keep_reapply);
+    _dev_ip_state_cleanup(self, addr_family, keep_reapply);
 }
 
 gboolean
@@ -12762,7 +12814,9 @@ can_reapply_change(NMDevice   *self,
         goto out_fail;
     }
 
-    if (nm_streq(setting_name, NM_SETTING_OVS_EXTERNAL_IDS_SETTING_NAME)
+    if (NM_IN_STRSET(setting_name,
+                     NM_SETTING_OVS_EXTERNAL_IDS_SETTING_NAME,
+                     NM_SETTING_OVS_OTHER_CONFIG_SETTING_NAME)
         && NM_DEVICE_GET_CLASS(self)->can_reapply_change_ovs_external_ids) {
         /* TODO: this means, you cannot reapply changes to the external-ids for
          * OVS system interfaces. */
@@ -12984,6 +13038,10 @@ check_and_reapply_connection(NMDevice            *self,
     if (priv->state >= NM_DEVICE_STATE_ACTIVATED)
         nm_device_update_metered(self);
 
+    /* Notify dispatcher when re-applied */
+    _LOGD(LOGD_DEVICE, "Notifying re-apply complete");
+    nm_dispatcher_call_device(NM_DISPATCHER_ACTION_REAPPLY, self, NULL, NULL, NULL, NULL);
+
     return TRUE;
 }
 
@@ -13889,7 +13947,7 @@ nm_device_start_ip_check(NMDevice *self)
     NMSettingConnection *s_con;
     guint                timeout     = 0;
     const char          *ping_binary = NULL;
-    char                 buf[NM_UTILS_INET_ADDRSTRLEN];
+    char                 buf[NM_INET_ADDRSTRLEN];
     NMLogDomain          log_domain = LOGD_IP4;
 
     /* Shouldn't be any active ping here, since IP_CHECK happens after the
@@ -13920,14 +13978,14 @@ nm_device_start_ip_check(NMDevice *self)
         } else if (priv->ip_data_4.state == NM_DEVICE_IP_STATE_READY) {
             gw = nm_l3_config_data_get_best_default_route(l3cd, AF_INET);
             if (gw) {
-                _nm_utils_inet4_ntop(NMP_OBJECT_CAST_IP4_ROUTE(gw)->gateway, buf);
+                nm_inet4_ntop(NMP_OBJECT_CAST_IP4_ROUTE(gw)->gateway, buf);
                 ping_binary = nm_utils_find_helper("ping", "/usr/bin/ping", NULL);
                 log_domain  = LOGD_IP4;
             }
         } else if (priv->ip_data_6.state == NM_DEVICE_IP_STATE_READY) {
             gw = nm_l3_config_data_get_best_default_route(l3cd, AF_INET6);
             if (gw) {
-                _nm_utils_inet6_ntop(&NMP_OBJECT_CAST_IP6_ROUTE(gw)->gateway, buf);
+                nm_inet6_ntop(&NMP_OBJECT_CAST_IP6_ROUTE(gw)->gateway, buf);
                 ping_binary = nm_utils_find_helper("ping6", "/usr/bin/ping6", NULL);
                 log_domain  = LOGD_IP6;
             }
@@ -14151,8 +14209,6 @@ NM_UTILS_FLAGS2STR_DEFINE(nm_unmanaged_flags2str,
                           NMUnmanagedFlags,
                           NM_UTILS_FLAGS2STR(NM_UNMANAGED_SLEEPING, "sleeping"),
                           NM_UTILS_FLAGS2STR(NM_UNMANAGED_QUITTING, "quitting"),
-                          NM_UTILS_FLAGS2STR(NM_UNMANAGED_PARENT, "parent"),
-                          NM_UTILS_FLAGS2STR(NM_UNMANAGED_BY_TYPE, "by-type"),
                           NM_UTILS_FLAGS2STR(NM_UNMANAGED_PLATFORM_INIT, "platform-init"),
                           NM_UTILS_FLAGS2STR(NM_UNMANAGED_USER_EXPLICIT, "user-explicit"),
                           NM_UTILS_FLAGS2STR(NM_UNMANAGED_BY_DEFAULT, "by-default"),
@@ -14847,9 +14903,10 @@ _nm_device_check_connection_available(NMDevice                      *self,
         } else {
             if (!nm_device_get_managed(self, TRUE)) {
                 /* device is strictly unmanaged by authoritative unmanaged reasons. */
-                nm_utils_error_set_literal(error,
-                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_UNMANAGED_DEVICE,
-                                           "device is strictly unmanaged");
+                nm_utils_error_set_literal(
+                    error,
+                    NM_UTILS_ERROR_CONNECTION_AVAILABLE_STRICTLY_UNMANAGED_DEVICE,
+                    "device is strictly unmanaged");
                 return FALSE;
             }
             if (!NM_FLAGS_HAS(flags,
@@ -16138,7 +16195,7 @@ nm_device_queue_state(NMDevice *self, NMDeviceState state, NMDeviceStateReason r
 
     /* We should only ever have one delayed state transition at a time */
     if (priv->queued_state.id) {
-        _LOGW(LOGD_DEVICE,
+        _LOGD(LOGD_DEVICE,
               "queue-state[%s, reason:%s, id:%u]: %s",
               nm_device_state_to_string(priv->queued_state.state),
               nm_device_state_reason_to_string_a(priv->queued_state.reason),
@@ -17127,6 +17184,12 @@ nm_device_clear_dns_lookup_data(NMDevice *self)
         nm_clear_pointer(&priv->hostname_resolver_x[i], _hostname_resolver_free);
 }
 
+gboolean
+nm_device_get_allow_autoconnect_on_external(NMDevice *self)
+{
+    return NM_DEVICE_GET_CLASS(self)->allow_autoconnect_on_external;
+}
+
 static GInetAddress *
 get_address_for_hostname_dns_lookup(NMDevice *self, int addr_family)
 {
@@ -17151,10 +17214,12 @@ get_address_for_hostname_dns_lookup(NMDevice *self, int addr_family)
 
     if (head_entry) {
         c_list_for_each_entry (iter, &head_entry->lst_entries_head, lst_entries) {
-            const NMPlatformIPAddress *addr = NMP_OBJECT_CAST_IP_ADDRESS(iter->obj);
+            const NMPlatformIPXAddress *addr = NMP_OBJECT_CAST_IPX_ADDRESS(iter->obj);
 
             if (IS_IPv4) {
-                return g_inet_address_new_from_bytes(addr->address_ptr, G_SOCKET_FAMILY_IPV4);
+                if (nm_ip4_addr_is_loopback(addr->a4.address))
+                    continue;
+                return g_inet_address_new_from_bytes(addr->ax.address_ptr, G_SOCKET_FAMILY_IPV4);
             }
 
             /* For IPv6 prefer, in order:
@@ -17163,15 +17228,19 @@ get_address_for_hostname_dns_lookup(NMDevice *self, int addr_family)
              * - link-local
              */
 
-            if (!IN6_IS_ADDR_LINKLOCAL(addr->address_ptr)) {
-                if (!(addr->n_ifa_flags & IFA_F_DEPRECATED)) {
-                    return g_inet_address_new_from_bytes(addr->address_ptr, G_SOCKET_FAMILY_IPV6);
+            if (IN6_ARE_ADDR_EQUAL(&addr->a6.address, &in6addr_loopback))
+                continue;
+
+            if (!IN6_IS_ADDR_LINKLOCAL(addr->ax.address_ptr)) {
+                if (!(addr->ax.n_ifa_flags & IFA_F_DEPRECATED)) {
+                    return g_inet_address_new_from_bytes(addr->ax.address_ptr,
+                                                         G_SOCKET_FAMILY_IPV6);
                 }
-                addr6_nonll = addr->address_ptr;
+                addr6_nonll = addr->ax.address_ptr;
                 continue;
             }
 
-            addr6_ll = addr->address_ptr;
+            addr6_ll = addr->ax.address_ptr;
         }
 
         if (addr6_nonll || addr6_ll)
@@ -17578,7 +17647,7 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps
         nm_assert(priv->type == NM_DEVICE_TYPE_UNKNOWN);
         priv->type = g_value_get_uint(value);
         nm_assert(priv->type > NM_DEVICE_TYPE_UNKNOWN);
-        nm_assert(priv->type <= NM_DEVICE_TYPE_VRF);
+        nm_assert(priv->type <= NM_DEVICE_TYPE_LOOPBACK);
         break;
     case PROP_LINK_TYPE:
         /* construct-only */
diff --git a/src/core/devices/nm-device.h b/src/core/devices/nm-device.h
index de850e68..f54457d1 100644
--- a/src/core/devices/nm-device.h
+++ b/src/core/devices/nm-device.h
@@ -91,7 +91,7 @@
 #define NM_DEVICE_INTERFACE_FLAGS  "interface-flags"
 
 #define NM_TYPE_DEVICE            (nm_device_get_type())
-#define NM_DEVICE(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE, NMDevice))
+#define NM_DEVICE(obj)            (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE, NMDevice))
 #define NM_DEVICE_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE, NMDeviceClass))
 #define NM_IS_DEVICE(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE))
 #define NM_IS_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE))
@@ -208,6 +208,8 @@ typedef struct _NMDeviceClass {
 
     bool can_reapply_change_ovs_external_ids : 1;
 
+    bool allow_autoconnect_on_external : 1;
+
     NMRfkillType rfkill_type : 4;
 
     void (*state_changed)(NMDevice           *device,
@@ -561,8 +563,6 @@ void nm_device_copy_ip6_dns_config(NMDevice *self, NMDevice *from_device);
  * @NM_UNMANAGED_NONE: placeholder value
  * @NM_UNMANAGED_SLEEPING: %TRUE when unmanaged because NM is sleeping.
  * @NM_UNMANAGED_QUITTING: %TRUE when unmanaged because NM is shutting down.
- * @NM_UNMANAGED_PARENT: %TRUE when unmanaged due to parent device being unmanaged
- * @NM_UNMANAGED_BY_TYPE: %TRUE for unmanaging device by type, like loopback.
  * @NM_UNMANAGED_PLATFORM_INIT: %TRUE when unmanaged because platform link not
  *   yet initialized. Unrealized device are also unmanaged for this reason.
  * @NM_UNMANAGED_USER_EXPLICIT: %TRUE when unmanaged by explicit user decision
@@ -592,21 +592,19 @@ typedef enum {
      * the device cannot be managed. */
     NM_UNMANAGED_SLEEPING      = (1LL << 0),
     NM_UNMANAGED_QUITTING      = (1LL << 1),
-    NM_UNMANAGED_PARENT        = (1LL << 2),
-    NM_UNMANAGED_BY_TYPE       = (1LL << 3),
-    NM_UNMANAGED_PLATFORM_INIT = (1LL << 4),
-    NM_UNMANAGED_USER_EXPLICIT = (1LL << 5),
-    NM_UNMANAGED_USER_SETTINGS = (1LL << 6),
+    NM_UNMANAGED_PLATFORM_INIT = (1LL << 2),
+    NM_UNMANAGED_USER_EXPLICIT = (1LL << 3),
+    NM_UNMANAGED_USER_SETTINGS = (1LL << 4),
 
     /* These flags can be non-effective and be overwritten
      * by other flags. */
-    NM_UNMANAGED_BY_DEFAULT    = (1LL << 7),
-    NM_UNMANAGED_USER_CONF     = (1LL << 8),
-    NM_UNMANAGED_USER_UDEV     = (1LL << 9),
-    NM_UNMANAGED_EXTERNAL_DOWN = (1LL << 10),
-    NM_UNMANAGED_IS_SLAVE      = (1LL << 11),
+    NM_UNMANAGED_BY_DEFAULT    = (1LL << 5),
+    NM_UNMANAGED_USER_CONF     = (1LL << 6),
+    NM_UNMANAGED_USER_UDEV     = (1LL << 7),
+    NM_UNMANAGED_EXTERNAL_DOWN = (1LL << 8),
+    NM_UNMANAGED_IS_SLAVE      = (1LL << 9),
 
-    NM_UNMANAGED_ALL = ((1LL << 12) - 1),
+    NM_UNMANAGED_ALL = ((1LL << 10) - 1),
 } NMUnmanagedFlags;
 
 typedef enum {
@@ -821,4 +819,6 @@ nm_device_get_hostname_from_dns_lookup(NMDevice *self, int addr_family, gboolean
 
 void nm_device_clear_dns_lookup_data(NMDevice *self);
 
+gboolean nm_device_get_allow_autoconnect_on_external(NMDevice *self);
+
 #endif /* __NETWORKMANAGER_DEVICE_H__ */
diff --git a/src/core/devices/nm-lldp-listener.c b/src/core/devices/nm-lldp-listener.c
index 137bf8fb..ac7e97f0 100644
--- a/src/core/devices/nm-lldp-listener.c
+++ b/src/core/devices/nm-lldp-listener.c
@@ -15,24 +15,20 @@
 #include "libnm-std-aux/unaligned.h"
 #include "libnm-platform/nm-platform.h"
 #include "libnm-glib-aux/nm-c-list.h"
+#include "libnm-lldp/nm-lldp-rx.h"
 #include "nm-utils.h"
 
-#include "libnm-systemd-core/nm-sd.h"
-
 #define MAX_NEIGHBORS            128
 #define MIN_UPDATE_INTERVAL_NSEC (2 * NM_UTILS_NSEC_PER_SEC)
 
-#define LLDP_MAC_NEAREST_BRIDGE \
-    (&((struct ether_addr){.ether_addr_octet = {0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e}}))
-#define LLDP_MAC_NEAREST_NON_TPMR_BRIDGE \
-    (&((struct ether_addr){.ether_addr_octet = {0x01, 0x80, 0xc2, 0x00, 0x00, 0x03}}))
-#define LLDP_MAC_NEAREST_CUSTOMER_BRIDGE \
-    (&((struct ether_addr){.ether_addr_octet = {0x01, 0x80, 0xc2, 0x00, 0x00, 0x00}}))
+#define LLDP_MAC_NEAREST_BRIDGE          (&NM_ETHER_ADDR_INIT(0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e))
+#define LLDP_MAC_NEAREST_NON_TPMR_BRIDGE (&NM_ETHER_ADDR_INIT(0x01, 0x80, 0xc2, 0x00, 0x00, 0x03))
+#define LLDP_MAC_NEAREST_CUSTOMER_BRIDGE (&NM_ETHER_ADDR_INIT(0x01, 0x80, 0xc2, 0x00, 0x00, 0x00))
 
 /*****************************************************************************/
 
 struct _NMLldpListener {
-    sd_lldp_rx *lldp_handle;
+    NMLldpRX   *lldp_rx;
     GHashTable *lldp_neighbors;
     GVariant   *variant;
 
@@ -49,12 +45,12 @@ struct _NMLldpListener {
 /*****************************************************************************/
 
 typedef struct {
-    GVariant         *variant;
-    sd_lldp_neighbor *neighbor_sd;
-    char             *chassis_id;
-    char             *port_id;
-    guint8            chassis_id_type;
-    guint8            port_id_type;
+    GVariant       *variant;
+    NMLldpNeighbor *neighbor_nm;
+    char           *chassis_id;
+    char           *port_id;
+    guint8          chassis_id_type;
+    guint8          port_id_type;
 } LldpNeighbor;
 
 /*****************************************************************************/
@@ -100,7 +96,7 @@ lldp_neighbor_get_raw(LldpNeighbor *neigh, const guint8 **out_raw_data, gsize *o
 
     nm_assert(neigh);
 
-    r = sd_lldp_neighbor_get_raw(neigh->neighbor_sd, &raw_data, &raw_len);
+    r = nm_lldp_neighbor_get_raw(neigh->neighbor_nm, &raw_data, &raw_len);
 
     nm_assert(r >= 0);
     nm_assert(raw_data);
@@ -111,24 +107,24 @@ lldp_neighbor_get_raw(LldpNeighbor *neigh, const guint8 **out_raw_data, gsize *o
 }
 
 static gboolean
-lldp_neighbor_id_get(struct sd_lldp_neighbor *neighbor_sd,
-                     guint8                  *out_chassis_id_type,
-                     const guint8           **out_chassis_id,
-                     gsize                   *out_chassis_id_len,
-                     guint8                  *out_port_id_type,
-                     const guint8           **out_port_id,
-                     gsize                   *out_port_id_len)
+lldp_neighbor_id_get(NMLldpNeighbor *neighbor_nm,
+                     guint8         *out_chassis_id_type,
+                     const guint8  **out_chassis_id,
+                     gsize          *out_chassis_id_len,
+                     guint8         *out_port_id_type,
+                     const guint8  **out_port_id,
+                     gsize          *out_port_id_len)
 {
     int r;
 
-    r = sd_lldp_neighbor_get_chassis_id(neighbor_sd,
+    r = nm_lldp_neighbor_get_chassis_id(neighbor_nm,
                                         out_chassis_id_type,
                                         (gconstpointer *) out_chassis_id,
                                         out_chassis_id_len);
     if (r < 0)
         return FALSE;
 
-    r = sd_lldp_neighbor_get_port_id(neighbor_sd,
+    r = nm_lldp_neighbor_get_port_id(neighbor_nm,
                                      out_port_id_type,
                                      (gconstpointer *) out_port_id,
                                      out_port_id_len);
@@ -139,82 +135,16 @@ lldp_neighbor_id_get(struct sd_lldp_neighbor *neighbor_sd,
 }
 
 static guint
-lldp_neighbor_id_hash(gconstpointer ptr)
+lldp_neighbor_id_hash(const LldpNeighbor *neigh)
 {
-    const LldpNeighbor *neigh = ptr;
-    guint8              chassis_id_type;
-    guint8              port_id_type;
-    const guint8       *chassis_id;
-    const guint8       *port_id;
-    gsize               chassis_id_len;
-    gsize               port_id_len;
-    NMHashState         h;
-
-    if (!lldp_neighbor_id_get(neigh->neighbor_sd,
-                              &chassis_id_type,
-                              &chassis_id,
-                              &chassis_id_len,
-                              &port_id_type,
-                              &port_id,
-                              &port_id_len)) {
-        nm_assert_not_reached();
-        return 0;
-    }
-
-    nm_hash_init(&h, 23423423u);
-    nm_hash_update_vals(&h, chassis_id_len, port_id_len, chassis_id_type, port_id_type);
-    nm_hash_update(&h, chassis_id, chassis_id_len);
-    nm_hash_update(&h, port_id, port_id_len);
-    return nm_hash_complete(&h);
+    return nm_lldp_neighbor_id_hash(nm_lldp_neighbor_get_id(neigh->neighbor_nm));
 }
 
 static int
 lldp_neighbor_id_cmp(const LldpNeighbor *a, const LldpNeighbor *b)
 {
-    guint8        a_chassis_id_type;
-    guint8        b_chassis_id_type;
-    guint8        a_port_id_type;
-    guint8        b_port_id_type;
-    const guint8 *a_chassis_id;
-    const guint8 *b_chassis_id;
-    const guint8 *a_port_id;
-    const guint8 *b_port_id;
-    gsize         a_chassis_id_len;
-    gsize         b_chassis_id_len;
-    gsize         a_port_id_len;
-    gsize         b_port_id_len;
-
-    NM_CMP_SELF(a, b);
-
-    if (!lldp_neighbor_id_get(a->neighbor_sd,
-                              &a_chassis_id_type,
-                              &a_chassis_id,
-                              &a_chassis_id_len,
-                              &a_port_id_type,
-                              &a_port_id,
-                              &a_port_id_len)) {
-        nm_assert_not_reached();
-        return FALSE;
-    }
-
-    if (!lldp_neighbor_id_get(b->neighbor_sd,
-                              &b_chassis_id_type,
-                              &b_chassis_id,
-                              &b_chassis_id_len,
-                              &b_port_id_type,
-                              &b_port_id,
-                              &b_port_id_len)) {
-        nm_assert_not_reached();
-        return FALSE;
-    }
-
-    NM_CMP_DIRECT(a_chassis_id_type, b_chassis_id_type);
-    NM_CMP_DIRECT(a_port_id_type, b_port_id_type);
-    NM_CMP_DIRECT(a_chassis_id_len, b_chassis_id_len);
-    NM_CMP_DIRECT(a_port_id_len, b_port_id_len);
-    NM_CMP_DIRECT_MEMCMP(a_chassis_id, b_chassis_id, a_chassis_id_len);
-    NM_CMP_DIRECT_MEMCMP(a_port_id, b_port_id, a_port_id_len);
-    return 0;
+    return nm_lldp_neighbor_id_cmp(nm_lldp_neighbor_get_id(a->neighbor_nm),
+                                   nm_lldp_neighbor_get_id(b->neighbor_nm));
 }
 
 static int
@@ -225,7 +155,7 @@ lldp_neighbor_id_cmp_p(gconstpointer a, gconstpointer b, gpointer user_data)
 }
 
 static gboolean
-lldp_neighbor_id_equal(gconstpointer a, gconstpointer b)
+lldp_neighbor_id_equal(const LldpNeighbor *a, const LldpNeighbor *b)
 {
     return lldp_neighbor_id_cmp(a, b) == 0;
 }
@@ -239,7 +169,7 @@ lldp_neighbor_free(LldpNeighbor *neighbor)
     g_free(neighbor->chassis_id);
     g_free(neighbor->port_id);
     nm_g_variant_unref(neighbor->variant);
-    sd_lldp_neighbor_unref(neighbor->neighbor_sd);
+    nm_lldp_neighbor_unref(neighbor->neighbor_nm);
     nm_g_slice_free(neighbor);
 }
 
@@ -257,7 +187,7 @@ lldp_neighbor_equal(LldpNeighbor *a, LldpNeighbor *b)
     gsize         raw_len_a;
     gsize         raw_len_b;
 
-    if (a->neighbor_sd == b->neighbor_sd)
+    if (a->neighbor_nm == b->neighbor_nm)
         return TRUE;
 
     lldp_neighbor_get_raw(a, &raw_data_a, &raw_len_a);
@@ -293,7 +223,7 @@ parse_management_address_tlv(const uint8_t *data, gsize len)
     if (len < 11)
         return NULL;
 
-    nm_assert((data[0] >> 1) == SD_LLDP_TYPE_MGMT_ADDRESS);
+    nm_assert((data[0] >> 1) == NM_LLDP_TYPE_MGMT_ADDRESS);
     nm_assert((((data[0] & 1) << 8) + data[1]) + 2 == len);
 
     data += 2;
@@ -360,7 +290,7 @@ format_network_address(const guint8 *data, gsize sz)
     } else
         return NULL;
 
-    return nm_utils_inet_ntop_dup(family, &a);
+    return nm_inet_ntop_dup(family, &a);
 }
 
 static const char *
@@ -402,7 +332,7 @@ format_string_cp(const guint8 *data, gsize len, gboolean allow_trim)
 }
 
 static LldpNeighbor *
-lldp_neighbor_new(sd_lldp_neighbor *neighbor_sd)
+lldp_neighbor_new(NMLldpNeighbor *neighbor_nm)
 {
     LldpNeighbor *neigh;
     guint8        chassis_id_type;
@@ -414,7 +344,7 @@ lldp_neighbor_new(sd_lldp_neighbor *neighbor_sd)
     gs_free char *s_chassis_id = NULL;
     gs_free char *s_port_id    = NULL;
 
-    if (!lldp_neighbor_id_get(neighbor_sd,
+    if (!lldp_neighbor_id_get(neighbor_nm,
                               &chassis_id_type,
                               &chassis_id,
                               &chassis_id_len,
@@ -424,17 +354,17 @@ lldp_neighbor_new(sd_lldp_neighbor *neighbor_sd)
         return NULL;
 
     switch (chassis_id_type) {
-    case SD_LLDP_CHASSIS_SUBTYPE_CHASSIS_COMPONENT:
-    case SD_LLDP_CHASSIS_SUBTYPE_INTERFACE_ALIAS:
-    case SD_LLDP_CHASSIS_SUBTYPE_PORT_COMPONENT:
-    case SD_LLDP_CHASSIS_SUBTYPE_INTERFACE_NAME:
-    case SD_LLDP_CHASSIS_SUBTYPE_LOCALLY_ASSIGNED:
+    case NM_LLDP_CHASSIS_SUBTYPE_CHASSIS_COMPONENT:
+    case NM_LLDP_CHASSIS_SUBTYPE_INTERFACE_ALIAS:
+    case NM_LLDP_CHASSIS_SUBTYPE_PORT_COMPONENT:
+    case NM_LLDP_CHASSIS_SUBTYPE_INTERFACE_NAME:
+    case NM_LLDP_CHASSIS_SUBTYPE_LOCALLY_ASSIGNED:
         s_chassis_id = format_string_cp(chassis_id, chassis_id_len, FALSE);
         break;
-    case SD_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS:
+    case NM_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS:
         s_chassis_id = nm_utils_hwaddr_ntoa(chassis_id, chassis_id_len);
         break;
-    case SD_LLDP_CHASSIS_SUBTYPE_NETWORK_ADDRESS:
+    case NM_LLDP_CHASSIS_SUBTYPE_NETWORK_ADDRESS:
         s_chassis_id = format_network_address(chassis_id, chassis_id_len);
         break;
     }
@@ -446,16 +376,16 @@ lldp_neighbor_new(sd_lldp_neighbor *neighbor_sd)
     }
 
     switch (port_id_type) {
-    case SD_LLDP_PORT_SUBTYPE_INTERFACE_ALIAS:
-    case SD_LLDP_PORT_SUBTYPE_PORT_COMPONENT:
-    case SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME:
-    case SD_LLDP_PORT_SUBTYPE_LOCALLY_ASSIGNED:
+    case NM_LLDP_PORT_SUBTYPE_INTERFACE_ALIAS:
+    case NM_LLDP_PORT_SUBTYPE_PORT_COMPONENT:
+    case NM_LLDP_PORT_SUBTYPE_INTERFACE_NAME:
+    case NM_LLDP_PORT_SUBTYPE_LOCALLY_ASSIGNED:
         s_port_id = format_string_cp(port_id, port_id_len, FALSE);
         break;
-    case SD_LLDP_PORT_SUBTYPE_MAC_ADDRESS:
+    case NM_LLDP_PORT_SUBTYPE_MAC_ADDRESS:
         s_port_id = nm_utils_hwaddr_ntoa(port_id, port_id_len);
         break;
-    case SD_LLDP_PORT_SUBTYPE_NETWORK_ADDRESS:
+    case NM_LLDP_PORT_SUBTYPE_NETWORK_ADDRESS:
         s_port_id = format_network_address(port_id, port_id_len);
         break;
     }
@@ -468,7 +398,7 @@ lldp_neighbor_new(sd_lldp_neighbor *neighbor_sd)
 
     neigh  = g_slice_new(LldpNeighbor);
     *neigh = (LldpNeighbor){
-        .neighbor_sd     = sd_lldp_neighbor_ref(neighbor_sd),
+        .neighbor_nm     = nm_lldp_neighbor_ref(neighbor_nm),
         .chassis_id_type = chassis_id_type,
         .chassis_id      = g_steal_pointer(&s_chassis_id),
         .port_id_type    = port_id_type,
@@ -480,15 +410,15 @@ lldp_neighbor_new(sd_lldp_neighbor *neighbor_sd)
 static GVariant *
 lldp_neighbor_to_variant(LldpNeighbor *neigh)
 {
-    struct ether_addr destination_address;
-    GVariantBuilder   builder;
-    const char       *str;
-    const guint8     *raw_data;
-    gsize             raw_len;
-    uint16_t          u16;
-    uint8_t          *data8;
-    gsize             len;
-    int               r;
+    NMEtherAddr     destination_address;
+    GVariantBuilder builder;
+    const char     *str;
+    const guint8   *raw_data;
+    gsize           raw_len;
+    uint16_t        u16;
+    uint8_t        *data8;
+    gsize           len;
+    int             r;
 
     if (neigh->variant)
         return neigh->variant;
@@ -505,33 +435,33 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
     nm_g_variant_builder_add_sv_uint32(&builder, NM_LLDP_ATTR_PORT_ID_TYPE, neigh->port_id_type);
     nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_PORT_ID, neigh->port_id);
 
-    r = sd_lldp_neighbor_get_destination_address(neigh->neighbor_sd, &destination_address);
+    r = nm_lldp_neighbor_get_destination_address(neigh->neighbor_nm, &destination_address);
     if (r < 0)
         str = NULL;
-    else if (nm_utils_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_BRIDGE))
+    else if (nm_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_BRIDGE))
         str = NM_LLDP_DEST_NEAREST_BRIDGE;
-    else if (nm_utils_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_NON_TPMR_BRIDGE))
+    else if (nm_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_NON_TPMR_BRIDGE))
         str = NM_LLDP_DEST_NEAREST_NON_TPMR_BRIDGE;
-    else if (nm_utils_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_CUSTOMER_BRIDGE))
+    else if (nm_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_CUSTOMER_BRIDGE))
         str = NM_LLDP_DEST_NEAREST_CUSTOMER_BRIDGE;
     else
         str = NULL;
     if (str)
         nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_DESTINATION, str);
 
-    if (sd_lldp_neighbor_get_port_description(neigh->neighbor_sd, &str) == 0)
+    if (nm_lldp_neighbor_get_port_description(neigh->neighbor_nm, &str) == 0)
         nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_PORT_DESCRIPTION, str);
 
-    if (sd_lldp_neighbor_get_system_name(neigh->neighbor_sd, &str) == 0)
+    if (nm_lldp_neighbor_get_system_name(neigh->neighbor_nm, &str) == 0)
         nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_SYSTEM_NAME, str);
 
-    if (sd_lldp_neighbor_get_system_description(neigh->neighbor_sd, &str) == 0)
+    if (nm_lldp_neighbor_get_system_description(neigh->neighbor_nm, &str) == 0)
         nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_SYSTEM_DESCRIPTION, str);
 
-    if (sd_lldp_neighbor_get_system_capabilities(neigh->neighbor_sd, &u16) == 0)
+    if (nm_lldp_neighbor_get_system_capabilities(neigh->neighbor_nm, &u16) == 0)
         nm_g_variant_builder_add_sv_uint32(&builder, NM_LLDP_ATTR_SYSTEM_CAPABILITIES, u16);
 
-    r = sd_lldp_neighbor_tlv_rewind(neigh->neighbor_sd);
+    r = nm_lldp_neighbor_tlv_rewind(neigh->neighbor_nm);
     if (r < 0)
         nm_assert_not_reached();
     else {
@@ -556,14 +486,14 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
             guint8 type;
             guint8 subtype;
 
-            if (sd_lldp_neighbor_tlv_get_type(neigh->neighbor_sd, &type) < 0)
+            if (nm_lldp_neighbor_tlv_get_type(neigh->neighbor_nm, &type) < 0)
                 continue;
 
-            if (sd_lldp_neighbor_tlv_get_raw(neigh->neighbor_sd, (void *) &data8, &len) < 0)
+            if (nm_lldp_neighbor_tlv_get_raw(neigh->neighbor_nm, (void *) &data8, &len) < 0)
                 continue;
 
             switch (type) {
-            case SD_LLDP_TYPE_MGMT_ADDRESS:
+            case NM_LLDP_TYPE_MGMT_ADDRESS:
                 tmp_variant = parse_management_address_tlv(data8, len);
                 if (tmp_variant) {
                     if (!v_management_addresses_has) {
@@ -573,13 +503,13 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
                     g_variant_builder_add_value(&v_management_addresses, tmp_variant);
                 }
                 continue;
-            case SD_LLDP_TYPE_PRIVATE:
+            case NM_LLDP_TYPE_PRIVATE:
                 break;
             default:
                 continue;
             }
 
-            r = sd_lldp_neighbor_tlv_get_oui(neigh->neighbor_sd, oui, &subtype);
+            r = nm_lldp_neighbor_tlv_get_oui(neigh->neighbor_nm, oui, &subtype);
             if (r < 0) {
                 if (r == -ENXIO)
                     continue;
@@ -609,15 +539,15 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
             data8 += 6;
             len -= 6;
 
-            if (memcmp(oui, SD_LLDP_OUI_802_1, sizeof(oui)) == 0) {
+            if (memcmp(oui, NM_LLDP_OUI_802_1, sizeof(oui)) == 0) {
                 switch (subtype) {
-                case SD_LLDP_OUI_802_1_SUBTYPE_PORT_VLAN_ID:
+                case NM_LLDP_OUI_802_1_SUBTYPE_PORT_VLAN_ID:
                     if (len != 2)
                         continue;
                     if (!v_ieee_802_1_pvid)
                         v_ieee_802_1_pvid = g_variant_new_uint32(unaligned_read_be16(data8));
                     break;
-                case SD_LLDP_OUI_802_1_SUBTYPE_PORT_PROTOCOL_VLAN_ID:
+                case NM_LLDP_OUI_802_1_SUBTYPE_PORT_PROTOCOL_VLAN_ID:
                     if (len != 3)
                         continue;
                     if (!v_ieee_802_1_ppvid) {
@@ -633,7 +563,7 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
                     g_variant_builder_add_value(&v_ieee_802_1_ppvids,
                                                 g_variant_builder_end(&tmp_builder));
                     break;
-                case SD_LLDP_OUI_802_1_SUBTYPE_VLAN_NAME:
+                case NM_LLDP_OUI_802_1_SUBTYPE_VLAN_NAME:
                 {
                     gs_free char *name_to_free = NULL;
                     const char   *name;
@@ -669,9 +599,9 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
                 default:
                     continue;
                 }
-            } else if (memcmp(oui, SD_LLDP_OUI_802_3, sizeof(oui)) == 0) {
+            } else if (memcmp(oui, NM_LLDP_OUI_802_3, sizeof(oui)) == 0) {
                 switch (subtype) {
-                case SD_LLDP_OUI_802_3_SUBTYPE_MAC_PHY_CONFIG_STATUS:
+                case NM_LLDP_OUI_802_3_SUBTYPE_MAC_PHY_CONFIG_STATUS:
                     if (len != 5)
                         continue;
 
@@ -687,7 +617,7 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
                         v_ieee_802_3_mac_phy_conf = g_variant_builder_end(&tmp_builder);
                     }
                     break;
-                case SD_LLDP_OUI_802_3_SUBTYPE_POWER_VIA_MDI:
+                case NM_LLDP_OUI_802_3_SUBTYPE_POWER_VIA_MDI:
                     if (len != 3)
                         continue;
 
@@ -703,7 +633,7 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
                         v_ieee_802_3_power_via_mdi = g_variant_builder_end(&tmp_builder);
                     }
                     break;
-                case SD_LLDP_OUI_802_3_SUBTYPE_MAXIMUM_FRAME_SIZE:
+                case NM_LLDP_OUI_802_3_SUBTYPE_MAXIMUM_FRAME_SIZE:
                     if (len != 2)
                         continue;
                     if (!v_ieee_802_3_max_frame_size)
@@ -711,9 +641,9 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
                             g_variant_new_uint32(unaligned_read_be16(data8));
                     break;
                 }
-            } else if (memcmp(oui, SD_LLDP_OUI_IANA, sizeof(oui)) == 0) {
+            } else if (memcmp(oui, NM_LLDP_OUI_IANA, sizeof(oui)) == 0) {
                 switch (subtype) {
-                case SD_LLDP_OUI_IANA_SUBTYPE_MUD:
+                case NM_LLDP_OUI_IANA_SUBTYPE_MUD:
                     if (!v_mud_url) {
                         gs_free char *s_free = NULL;
                         const char   *s;
@@ -725,7 +655,7 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
                     break;
                 }
             }
-        } while (sd_lldp_neighbor_tlv_next(neigh->neighbor_sd) > 0);
+        } while (nm_lldp_neighbor_tlv_next(neigh->neighbor_nm) > 0);
 
         if (v_management_addresses_has)
             nm_g_variant_builder_add_sv(&builder,
@@ -777,18 +707,17 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
 GVariant *
 nmtst_lldp_parse_from_raw(const guint8 *raw_data, gsize raw_len)
 {
-    nm_auto(sd_lldp_neighbor_unrefp) sd_lldp_neighbor *neighbor_sd = NULL;
-    nm_auto(lldp_neighbor_freep) LldpNeighbor         *neigh       = NULL;
-    GVariant                                          *variant;
-    int                                                r;
+    nm_auto(nm_lldp_neighbor_unrefp) NMLldpNeighbor *neighbor_nm = NULL;
+    nm_auto(lldp_neighbor_freep) LldpNeighbor       *neigh       = NULL;
+    GVariant                                        *variant;
 
     g_assert(raw_data);
     g_assert(raw_len > 0);
 
-    r = sd_lldp_neighbor_from_raw(&neighbor_sd, raw_data, raw_len);
-    g_assert(r >= 0);
+    neighbor_nm = nm_lldp_neighbor_new_from_raw(raw_data, raw_len);
+    g_assert(neighbor_nm);
 
-    neigh = lldp_neighbor_new(neighbor_sd);
+    neigh = lldp_neighbor_new(neighbor_nm);
     g_assert(neigh);
 
     variant = lldp_neighbor_to_variant(neigh);
@@ -843,18 +772,18 @@ data_changed_schedule(NMLldpListener *self)
 }
 
 static void
-process_lldp_neighbor(NMLldpListener *self, sd_lldp_neighbor *neighbor_sd, gboolean remove)
+process_lldp_neighbor(NMLldpListener *self, NMLldpNeighbor *neighbor_nm, gboolean remove)
 {
     nm_auto(lldp_neighbor_freep) LldpNeighbor *neigh = NULL;
     LldpNeighbor                              *neigh_old;
 
     nm_assert(self);
-    nm_assert(self->lldp_handle);
+    nm_assert(self->lldp_rx);
     nm_assert(self->lldp_neighbors);
 
-    g_return_if_fail(neighbor_sd);
+    g_return_if_fail(neighbor_nm);
 
-    neigh = lldp_neighbor_new(neighbor_sd);
+    neigh = lldp_neighbor_new(neighbor_nm);
     if (!neigh) {
         _LOGT("process: failed to parse neighbor");
         return;
@@ -884,14 +813,17 @@ handle_changed:
 }
 
 static void
-lldp_event_handler(sd_lldp_rx *lldp, sd_lldp_rx_event_t event, sd_lldp_neighbor *n, void *userdata)
+lldp_event_handler(NMLldpRX *lldp, NMLldpRXEvent event, NMLldpNeighbor *n, void *user_data)
 {
-    process_lldp_neighbor(userdata,
+    NMLldpListener *self = user_data;
+
+    _LOGD("event: %s", nm_lldp_rx_event_to_string(event));
+    process_lldp_neighbor(self,
                           n,
                           !NM_IN_SET(event,
-                                     SD_LLDP_RX_EVENT_ADDED,
-                                     SD_LLDP_RX_EVENT_UPDATED,
-                                     SD_LLDP_RX_EVENT_REFRESHED));
+                                     NM_LLDP_RX_EVENT_ADDED,
+                                     NM_LLDP_RX_EVENT_UPDATED,
+                                     NM_LLDP_RX_EVENT_REFRESHED));
 }
 
 /*****************************************************************************/
@@ -935,35 +867,14 @@ nm_lldp_listener_new(int                  ifindex,
                      gpointer             notify_user_data,
                      GError             **error)
 {
-    NMLldpListener *self = NULL;
-    sd_lldp_rx     *lldp_handle;
-    int             r;
+    NMLldpListener                      *self    = NULL;
+    nm_auto(nm_lldp_rx_unrefp) NMLldpRX *lldp_rx = NULL;
+    int                                  r;
 
     g_return_val_if_fail(ifindex > 0, FALSE);
     g_return_val_if_fail(!error || !*error, FALSE);
     g_return_val_if_fail(notify_callback, FALSE);
 
-    r = sd_lldp_rx_new(&lldp_handle);
-    if (r < 0) {
-        g_set_error_literal(error,
-                            NM_DEVICE_ERROR,
-                            NM_DEVICE_ERROR_FAILED,
-                            "initialization failed");
-        return FALSE;
-    }
-
-    r = sd_lldp_rx_set_ifindex(lldp_handle, ifindex);
-    if (r < 0) {
-        g_set_error_literal(error,
-                            NM_DEVICE_ERROR,
-                            NM_DEVICE_ERROR_FAILED,
-                            "failed setting ifindex");
-        goto fail_handle;
-    }
-
-    r = sd_lldp_rx_set_neighbors_max(lldp_handle, MAX_NEIGHBORS);
-    nm_assert(r == 0);
-
     self  = g_slice_new(NMLldpListener);
     *self = (NMLldpListener){
         .ifindex          = ifindex,
@@ -971,39 +882,33 @@ nm_lldp_listener_new(int                  ifindex,
         .notify_user_data = notify_user_data,
     };
 
-    r = sd_lldp_rx_set_callback(lldp_handle, lldp_event_handler, self);
-    if (r < 0) {
-        g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "set callback failed");
-        goto fail_handle;
-    }
+    nm_assert(nm_g_main_context_is_thread_default(g_main_context_default()));
 
-    r = sd_lldp_rx_attach_event(lldp_handle, NULL, 0);
-    if (r < 0) {
-        g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "attach event failed");
-        goto fail_attached;
-    }
+    lldp_rx = nm_lldp_rx_new(&((NMLldpRXConfig){
+        .ifindex       = ifindex,
+        .neighbors_max = MAX_NEIGHBORS,
+        .callback      = lldp_event_handler,
+        .userdata      = self,
+    }));
 
-    r = sd_lldp_rx_start(lldp_handle);
+    r = nm_lldp_rx_start(lldp_rx);
     if (r < 0) {
         g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "start failed");
-        goto fail_attached;
+        goto fail;
     }
 
-    self->lldp_neighbors = g_hash_table_new_full(lldp_neighbor_id_hash,
-                                                 lldp_neighbor_id_equal,
+    self->lldp_neighbors = g_hash_table_new_full((GHashFunc) lldp_neighbor_id_hash,
+                                                 (GEqualFunc) lldp_neighbor_id_equal,
                                                  (GDestroyNotify) lldp_neighbor_free,
                                                  NULL);
-    self->lldp_handle    = lldp_handle;
+
+    self->lldp_rx = g_steal_pointer(&lldp_rx);
 
     _LOGD("start lldp listener");
     return self;
 
-fail_attached:
-    sd_lldp_rx_detach_event(lldp_handle);
-fail_handle:
-    if (self)
-        nm_g_slice_free(self);
-    sd_lldp_rx_unref(lldp_handle);
+fail:
+    nm_g_slice_free(self);
     return NULL;
 }
 
@@ -1012,9 +917,8 @@ nm_lldp_listener_destroy(NMLldpListener *self)
 {
     g_return_if_fail(self);
 
-    sd_lldp_rx_stop(self->lldp_handle);
-    sd_lldp_rx_detach_event(self->lldp_handle);
-    sd_lldp_rx_unref(self->lldp_handle);
+    nm_lldp_rx_stop(self->lldp_rx);
+    nm_lldp_rx_unref(self->lldp_rx);
 
     nm_clear_g_source_inst(&self->ratelimit_source);
 
diff --git a/src/core/devices/ovs/nm-device-ovs-bridge.c b/src/core/devices/ovs/nm-device-ovs-bridge.c
index 048afb02..7b319af3 100644
--- a/src/core/devices/ovs/nm-device-ovs-bridge.c
+++ b/src/core/devices/ovs/nm-device-ovs-bridge.c
@@ -16,6 +16,7 @@
 #include "nm-setting-connection.h"
 #include "nm-setting-ovs-bridge.h"
 #include "nm-setting-ovs-external-ids.h"
+#include "nm-setting-ovs-other-config.h"
 #include "libnm-core-intern/nm-core-internal.h"
 
 #define _NMLOG_DEVICE_TYPE NMDeviceOvsBridge
@@ -124,13 +125,14 @@ nm_device_ovs_reapply_connection(NMDevice *self, NMConnection *con_old, NMConnec
         device_type = NM_DEVICE_TYPE_OVS_BRIDGE;
     }
 
-    nm_ovsdb_set_external_ids(
-        nm_ovsdb_get(),
-        device_type,
-        nm_device_get_ip_iface(self),
-        nm_connection_get_uuid(con_new),
-        _nm_connection_get_setting(con_old, NM_TYPE_SETTING_OVS_EXTERNAL_IDS),
-        _nm_connection_get_setting(con_new, NM_TYPE_SETTING_OVS_EXTERNAL_IDS));
+    nm_ovsdb_set_reapply(nm_ovsdb_get(),
+                         device_type,
+                         nm_device_get_ip_iface(self),
+                         nm_connection_get_uuid(con_new),
+                         _nm_connection_get_setting(con_old, NM_TYPE_SETTING_OVS_EXTERNAL_IDS),
+                         _nm_connection_get_setting(con_new, NM_TYPE_SETTING_OVS_EXTERNAL_IDS),
+                         _nm_connection_get_setting(con_old, NM_TYPE_SETTING_OVS_OTHER_CONFIG),
+                         _nm_connection_get_setting(con_new, NM_TYPE_SETTING_OVS_OTHER_CONFIG));
 }
 
 /*****************************************************************************/
diff --git a/src/core/devices/ovs/nm-device-ovs-bridge.h b/src/core/devices/ovs/nm-device-ovs-bridge.h
index 2b893343..51cc685d 100644
--- a/src/core/devices/ovs/nm-device-ovs-bridge.h
+++ b/src/core/devices/ovs/nm-device-ovs-bridge.h
@@ -8,7 +8,7 @@
 
 #define NM_TYPE_DEVICE_OVS_BRIDGE (nm_device_ovs_bridge_get_type())
 #define NM_DEVICE_OVS_BRIDGE(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OVS_BRIDGE, NMDeviceOvsBridge))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OVS_BRIDGE, NMDeviceOvsBridge))
 #define NM_DEVICE_OVS_BRIDGE_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_OVS_BRIDGE, NMDeviceOvsBridgeClass))
 #define NM_IS_DEVICE_OVS_BRIDGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_OVS_BRIDGE))
diff --git a/src/core/devices/ovs/nm-device-ovs-interface.h b/src/core/devices/ovs/nm-device-ovs-interface.h
index 03b0e9a4..b8c951d2 100644
--- a/src/core/devices/ovs/nm-device-ovs-interface.h
+++ b/src/core/devices/ovs/nm-device-ovs-interface.h
@@ -8,7 +8,7 @@
 
 #define NM_TYPE_DEVICE_OVS_INTERFACE (nm_device_ovs_interface_get_type())
 #define NM_DEVICE_OVS_INTERFACE(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OVS_INTERFACE, NMDeviceOvsInterface))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OVS_INTERFACE, NMDeviceOvsInterface))
 #define NM_DEVICE_OVS_INTERFACE_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_OVS_INTERFACE, NMDeviceOvsInterfaceClass))
 #define NM_IS_DEVICE_OVS_INTERFACE(obj) \
diff --git a/src/core/devices/ovs/nm-device-ovs-port.h b/src/core/devices/ovs/nm-device-ovs-port.h
index f4516c01..87446fa4 100644
--- a/src/core/devices/ovs/nm-device-ovs-port.h
+++ b/src/core/devices/ovs/nm-device-ovs-port.h
@@ -8,7 +8,7 @@
 
 #define NM_TYPE_DEVICE_OVS_PORT (nm_device_ovs_port_get_type())
 #define NM_DEVICE_OVS_PORT(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OVS_PORT, NMDeviceOvsPort))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OVS_PORT, NMDeviceOvsPort))
 #define NM_DEVICE_OVS_PORT_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_OVS_PORT, NMDeviceOvsPortClass))
 #define NM_IS_DEVICE_OVS_PORT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_OVS_PORT))
diff --git a/src/core/devices/ovs/nm-ovs-factory.c b/src/core/devices/ovs/nm-ovs-factory.c
index ff2c7858..50023778 100644
--- a/src/core/devices/ovs/nm-ovs-factory.c
+++ b/src/core/devices/ovs/nm-ovs-factory.c
@@ -27,7 +27,8 @@ typedef struct {
 } NMOvsFactoryClass;
 
 #define NM_TYPE_OVS_FACTORY (nm_ovs_factory_get_type())
-#define NM_OVS_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_OVS_FACTORY, NMOvsFactory))
+#define NM_OVS_FACTORY(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_OVS_FACTORY, NMOvsFactory))
 #define NM_OVS_FACTORY_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_OVS_FACTORY, NMOvsFactoryClass))
 #define NM_IS_OVS_FACTORY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_OVS_FACTORY))
diff --git a/src/core/devices/ovs/nm-ovsdb.c b/src/core/devices/ovs/nm-ovsdb.c
index 0d688396..85b7953f 100644
--- a/src/core/devices/ovs/nm-ovsdb.c
+++ b/src/core/devices/ovs/nm-ovsdb.c
@@ -17,6 +17,7 @@
 #include "devices/nm-device.h"
 #include "nm-manager.h"
 #include "nm-setting-ovs-external-ids.h"
+#include "nm-setting-ovs-other-config.h"
 #include "nm-priv-helper-call.h"
 #include "libnm-platform/nm-platform.h"
 
@@ -24,18 +25,26 @@
 
 #define OVSDB_MAX_FAILURES 3
 
+#define OTHER_CONFIG_HWADDR "hwaddr"
+
 /*****************************************************************************/
 
 #if JANSSON_VERSION_HEX < 0x020400
 #warning "requires at least libjansson 2.4"
 #endif
 
+typedef enum {
+    STRDICT_TYPE_EXTERNAL_IDS,
+    STRDICT_TYPE_OTHER_CONFIG,
+} StrdictType;
+
 typedef struct {
     char      *port_uuid;
     char      *name;
     char      *connection_uuid;
     GPtrArray *interfaces; /* interface uuids */
     GArray    *external_ids;
+    GArray    *other_config;
 } OpenvswitchPort;
 
 typedef struct {
@@ -44,6 +53,7 @@ typedef struct {
     char      *connection_uuid;
     GPtrArray *ports; /* port uuids */
     GArray    *external_ids;
+    GArray    *other_config;
 } OpenvswitchBridge;
 
 typedef struct {
@@ -52,6 +62,7 @@ typedef struct {
     char   *type;
     char   *connection_uuid;
     GArray *external_ids;
+    GArray *other_config;
 } OpenvswitchInterface;
 
 /*****************************************************************************/
@@ -66,7 +77,7 @@ typedef enum {
     OVSDB_ADD_INTERFACE,
     OVSDB_DEL_INTERFACE,
     OVSDB_SET_INTERFACE_MTU,
-    OVSDB_SET_EXTERNAL_IDS,
+    OVSDB_SET_REAPPLY,
 } OvsdbCommand;
 
 #define CALL_ID_UNSPEC G_MAXUINT64
@@ -92,9 +103,11 @@ typedef union {
         NMDeviceType device_type;
         char        *ifname;
         char        *connection_uuid;
-        GHashTable  *exid_old;
-        GHashTable  *exid_new;
-    } set_external_ids;
+        GHashTable  *external_ids_old;
+        GHashTable  *external_ids_new;
+        GHashTable  *other_config_old;
+        GHashTable  *other_config_new;
+    } set_reapply;
 } OvsdbMethodPayload;
 
 typedef struct {
@@ -223,20 +236,24 @@ static void cleanup_check_ready(NMOvsdb *self);
             },                                                    \
     }))
 
-#define OVSDB_METHOD_PAYLOAD_SET_EXTERNAL_IDS(xdevice_type,                         \
-                                              xifname,                              \
-                                              xconnection_uuid,                     \
-                                              xexid_old,                            \
-                                              xexid_new)                            \
-    (&((const OvsdbMethodPayload){                                                  \
-        .set_external_ids =                                                         \
-            {                                                                       \
-                .device_type     = xdevice_type,                                    \
-                .ifname          = (char *) NM_CONSTCAST(char, (xifname)),          \
-                .connection_uuid = (char *) NM_CONSTCAST(char, (xconnection_uuid)), \
-                .exid_old        = (xexid_old),                                     \
-                .exid_new        = (xexid_new),                                     \
-            },                                                                      \
+#define OVSDB_METHOD_PAYLOAD_SET_REAPPLY(xdevice_type,                               \
+                                         xifname,                                    \
+                                         xconnection_uuid,                           \
+                                         xexternal_ids_old,                          \
+                                         xexternal_ids_new,                          \
+                                         xother_config_old,                          \
+                                         xother_config_new)                          \
+    (&((const OvsdbMethodPayload){                                                   \
+        .set_reapply =                                                               \
+            {                                                                        \
+                .device_type      = xdevice_type,                                    \
+                .ifname           = (char *) NM_CONSTCAST(char, (xifname)),          \
+                .connection_uuid  = (char *) NM_CONSTCAST(char, (xconnection_uuid)), \
+                .external_ids_old = (xexternal_ids_old),                             \
+                .external_ids_new = (xexternal_ids_new),                             \
+                .other_config_old = (xother_config_old),                             \
+                .other_config_new = (xother_config_new),                             \
+            },                                                                       \
     }))
 
 /*****************************************************************************/
@@ -291,11 +308,13 @@ _call_complete(OvsdbMethodCall *call, json_t *response, GError *error)
     case OVSDB_SET_INTERFACE_MTU:
         nm_clear_g_free(&call->payload.set_interface_mtu.ifname);
         break;
-    case OVSDB_SET_EXTERNAL_IDS:
-        nm_clear_g_free(&call->payload.set_external_ids.ifname);
-        nm_clear_g_free(&call->payload.set_external_ids.connection_uuid);
-        nm_clear_pointer(&call->payload.set_external_ids.exid_old, g_hash_table_destroy);
-        nm_clear_pointer(&call->payload.set_external_ids.exid_new, g_hash_table_destroy);
+    case OVSDB_SET_REAPPLY:
+        nm_clear_g_free(&call->payload.set_reapply.ifname);
+        nm_clear_g_free(&call->payload.set_reapply.connection_uuid);
+        nm_clear_pointer(&call->payload.set_reapply.external_ids_old, g_hash_table_destroy);
+        nm_clear_pointer(&call->payload.set_reapply.external_ids_new, g_hash_table_destroy);
+        nm_clear_pointer(&call->payload.set_reapply.other_config_old, g_hash_table_destroy);
+        nm_clear_pointer(&call->payload.set_reapply.other_config_new, g_hash_table_destroy);
         break;
     }
 
@@ -312,6 +331,7 @@ _free_bridge(OpenvswitchBridge *ovs_bridge)
     g_free(ovs_bridge->connection_uuid);
     g_ptr_array_free(ovs_bridge->ports, TRUE);
     nm_g_array_unref(ovs_bridge->external_ids);
+    nm_g_array_unref(ovs_bridge->other_config);
     nm_g_slice_free(ovs_bridge);
 }
 
@@ -323,6 +343,7 @@ _free_port(OpenvswitchPort *ovs_port)
     g_free(ovs_port->connection_uuid);
     g_ptr_array_free(ovs_port->interfaces, TRUE);
     nm_g_array_unref(ovs_port->external_ids);
+    nm_g_array_unref(ovs_port->other_config);
     nm_g_slice_free(ovs_port);
 }
 
@@ -334,6 +355,7 @@ _free_interface(OpenvswitchInterface *ovs_interface)
     g_free(ovs_interface->connection_uuid);
     g_free(ovs_interface->type);
     nm_g_array_unref(ovs_interface->external_ids);
+    nm_g_array_unref(ovs_interface->other_config);
     nm_g_slice_free(ovs_interface);
 }
 
@@ -447,19 +469,22 @@ ovsdb_call_method(NMOvsdb                  *self,
                    call->payload.set_interface_mtu.ifname,
                    call->payload.set_interface_mtu.mtu);
         break;
-    case OVSDB_SET_EXTERNAL_IDS:
-        call->payload.set_external_ids.device_type = payload->set_external_ids.device_type;
-        call->payload.set_external_ids.ifname      = g_strdup(payload->set_external_ids.ifname);
-        call->payload.set_external_ids.connection_uuid =
-            g_strdup(payload->set_external_ids.connection_uuid);
-        call->payload.set_external_ids.exid_old =
-            nm_g_hash_table_ref(payload->set_external_ids.exid_old);
-        call->payload.set_external_ids.exid_new =
-            nm_g_hash_table_ref(payload->set_external_ids.exid_new);
+    case OVSDB_SET_REAPPLY:
+        call->payload.set_reapply.device_type     = payload->set_reapply.device_type;
+        call->payload.set_reapply.ifname          = g_strdup(payload->set_reapply.ifname);
+        call->payload.set_reapply.connection_uuid = g_strdup(payload->set_reapply.connection_uuid);
+        call->payload.set_reapply.external_ids_old =
+            nm_g_hash_table_ref(payload->set_reapply.external_ids_old);
+        call->payload.set_reapply.external_ids_new =
+            nm_g_hash_table_ref(payload->set_reapply.external_ids_new);
+        call->payload.set_reapply.other_config_old =
+            nm_g_hash_table_ref(payload->set_reapply.other_config_old);
+        call->payload.set_reapply.other_config_new =
+            nm_g_hash_table_ref(payload->set_reapply.other_config_new);
         _LOGT_call(call,
-                   "new: set-external-ids con-uuid=%s, interface=%s",
-                   call->payload.set_external_ids.connection_uuid,
-                   call->payload.set_external_ids.ifname);
+                   "new: set external-ids/other-config con-uuid=%s, interface=%s",
+                   call->payload.set_reapply.connection_uuid,
+                   call->payload.set_reapply.ifname);
         break;
     }
 
@@ -604,12 +629,12 @@ _set_bridge_mac(json_t *params, const char *ifname, const char *mac)
                   "other_config",
                   "delete",
                   "set",
-                  "hwaddr",
+                  OTHER_CONFIG_HWADDR,
 
                   "other_config",
                   "insert",
                   "map",
-                  "hwaddr",
+                  OTHER_CONFIG_HWADDR,
                   mac,
 
                   "where",
@@ -677,92 +702,155 @@ _set_port_interfaces(json_t *params, const char *ifname, json_t *new_interfaces)
 }
 
 static json_t *
-_j_create_external_ids_array_new(NMConnection *connection)
+_j_create_strdict_new(NMConnection *connection,
+                      StrdictType   strdict_type,
+                      const char   *other_config_hwaddr)
 {
+    NMSettingOvsOtherConfig *s_other_config = NULL;
+    NMSettingOvsExternalIDs *s_external_ids = NULL;
     json_t                  *array;
-    const char *const       *external_ids   = NULL;
-    guint                    n_external_ids = 0;
+    const char *const       *strv   = NULL;
+    guint                    n_strv = 0;
     guint                    i;
     const char              *uuid;
-    NMSettingOvsExternalIDs *s_exid;
 
     nm_assert(NM_IS_CONNECTION(connection));
+    nm_assert(NM_IN_SET(strdict_type, STRDICT_TYPE_EXTERNAL_IDS, STRDICT_TYPE_OTHER_CONFIG));
 
     array = json_array();
 
-    uuid = nm_connection_get_uuid(connection);
-    nm_assert(uuid);
-    json_array_append_new(array, json_pack("[s, s]", NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID, uuid));
+    if (strdict_type == STRDICT_TYPE_EXTERNAL_IDS) {
+        uuid = nm_connection_get_uuid(connection);
+        nm_assert(uuid);
+        json_array_append_new(array,
+                              json_pack("[s, s]", NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID, uuid));
+    } else {
+        if (other_config_hwaddr) {
+            json_array_append_new(array,
+                                  json_pack("[s, s]", OTHER_CONFIG_HWADDR, other_config_hwaddr));
+        }
+    }
+
+    if (strdict_type == STRDICT_TYPE_EXTERNAL_IDS) {
+        s_external_ids = _nm_connection_get_setting(connection, NM_TYPE_SETTING_OVS_EXTERNAL_IDS);
+        if (s_external_ids)
+            strv = nm_setting_ovs_external_ids_get_data_keys(s_external_ids, &n_strv);
+    } else {
+        s_other_config = _nm_connection_get_setting(connection, NM_TYPE_SETTING_OVS_OTHER_CONFIG);
+        if (s_other_config)
+            strv = nm_setting_ovs_other_config_get_data_keys(s_other_config, &n_strv);
+    }
+
+    for (i = 0; i < n_strv; i++) {
+        const char *k = strv[i];
 
-    s_exid = _nm_connection_get_setting(connection, NM_TYPE_SETTING_OVS_EXTERNAL_IDS);
-    if (s_exid)
-        external_ids = nm_setting_ovs_external_ids_get_data_keys(s_exid, &n_external_ids);
-    for (i = 0; i < n_external_ids; i++) {
-        const char *k = external_ids[i];
+        if (strdict_type == STRDICT_TYPE_OTHER_CONFIG && other_config_hwaddr
+            && nm_streq(k, OTHER_CONFIG_HWADDR)) {
+            /* "hwaddr" is explicitly overwritten. */
+            continue;
+        }
 
         json_array_append_new(
             array,
-            json_pack("[s, s]", k, nm_setting_ovs_external_ids_get_data(s_exid, k)));
+            json_pack("[s, s]",
+                      k,
+                      strdict_type == STRDICT_TYPE_EXTERNAL_IDS
+                          ? nm_setting_ovs_external_ids_get_data(s_external_ids, k)
+                          : nm_setting_ovs_other_config_get_data(s_other_config, k)));
     }
 
     return json_pack("[s, o]", "map", array);
 }
 
-static json_t *
-_j_create_external_ids_array_update(const char *connection_uuid,
-                                    GHashTable *exid_old,
-                                    GHashTable *exid_new)
+static void
+_j_create_strv_array_update(json_t     *mutations,
+                            StrdictType strdict_type,
+                            const char *connection_uuid,
+                            GHashTable *hash_old,
+                            GHashTable *hash_new)
 {
     GHashTableIter iter;
-    json_t        *mutations;
     json_t        *array;
     const char    *key;
     const char    *val;
 
-    nm_assert(connection_uuid);
+    /* This is called during reapply. We accept reapplying all settings,
+     * except other_config:hwaddr. That one cannot change and is specially
+     * handled below. The reason is that we knew the correct "hwaddr" during
+     * _j_create_strdict_new(), but we don't do now. At least not easily,
+     * and it's not clear that reapply of the MAC address is really useful. */
 
-    mutations = json_array();
+    nm_assert((!!connection_uuid) == (strdict_type == STRDICT_TYPE_EXTERNAL_IDS));
+    nm_assert(NM_IN_SET(strdict_type, STRDICT_TYPE_EXTERNAL_IDS, STRDICT_TYPE_OTHER_CONFIG));
 
-    array = json_array();
-    if (exid_old) {
-        g_hash_table_iter_init(&iter, exid_old);
+    array = NULL;
+    if (hash_old) {
+        g_hash_table_iter_init(&iter, hash_old);
         while (g_hash_table_iter_next(&iter, (gpointer *) &key, NULL)) {
+            if (strdict_type == STRDICT_TYPE_OTHER_CONFIG && nm_streq(key, OTHER_CONFIG_HWADDR))
+                continue;
+            if (!array)
+                array = json_array();
             json_array_append_new(array, json_string(key));
         }
     }
-    if (exid_new) {
-        g_hash_table_iter_init(&iter, exid_new);
+    if (hash_new) {
+        g_hash_table_iter_init(&iter, hash_new);
         while (g_hash_table_iter_next(&iter, (gpointer *) &key, NULL)) {
-            if (nm_g_hash_table_contains(exid_old, key))
+            if (strdict_type == STRDICT_TYPE_OTHER_CONFIG && nm_streq(key, OTHER_CONFIG_HWADDR))
                 continue;
+            if (nm_g_hash_table_contains(hash_old, key))
+                continue;
+            if (!array)
+                array = json_array();
             json_array_append_new(array, json_string(key));
         }
     }
-    if (!nm_g_hash_table_contains(exid_old, NM_OVS_EXTERNAL_ID_NM_PREFIX)
-        && !nm_g_hash_table_contains(exid_new, NM_OVS_EXTERNAL_ID_NM_PREFIX)) {
-        json_array_append_new(array, json_string(NM_OVS_EXTERNAL_ID_NM_PREFIX));
+    if (strdict_type == STRDICT_TYPE_EXTERNAL_IDS) {
+        if (!nm_g_hash_table_contains(hash_old, NM_OVS_EXTERNAL_ID_NM_PREFIX)
+            && !nm_g_hash_table_contains(hash_new, NM_OVS_EXTERNAL_ID_NM_PREFIX)) {
+            if (!array)
+                array = json_array();
+            json_array_append_new(array, json_string(NM_OVS_EXTERNAL_ID_NM_PREFIX));
+        }
+    }
+    if (array) {
+        json_array_append_new(
+            mutations,
+            json_pack("[s, s, [s, o]]",
+                      strdict_type == STRDICT_TYPE_EXTERNAL_IDS ? "external_ids" : "other_config",
+                      "delete",
+                      "set",
+                      array));
     }
-    json_array_append_new(mutations,
-                          json_pack("[s, s, [s, o]]", "external_ids", "delete", "set", array));
 
     array = json_array();
 
-    json_array_append_new(
-        array,
-        json_pack("[s, s]", NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID, connection_uuid));
-
-    if (exid_new) {
-        g_hash_table_iter_init(&iter, exid_new);
+    if (strdict_type == STRDICT_TYPE_EXTERNAL_IDS) {
+        json_array_append_new(
+            array,
+            json_pack("[s, s]", NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID, connection_uuid));
+    }
+    if (hash_new) {
+        g_hash_table_iter_init(&iter, hash_new);
         while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &val)) {
-            if (NM_STR_HAS_PREFIX(key, NM_OVS_EXTERNAL_ID_NM_PREFIX))
+            if (strdict_type == STRDICT_TYPE_EXTERNAL_IDS) {
+                if (NM_STR_HAS_PREFIX(key, NM_OVS_EXTERNAL_ID_NM_PREFIX))
+                    continue;
+            }
+            if (strdict_type == STRDICT_TYPE_OTHER_CONFIG && nm_streq(key, OTHER_CONFIG_HWADDR))
                 continue;
             json_array_append_new(array, json_pack("[s, s]", key, val));
         }
     }
 
-    json_array_append_new(mutations,
-                          json_pack("[s, s, [s, o]]", "external_ids", "insert", "map", array));
-    return mutations;
+    json_array_append_new(
+        mutations,
+        json_pack("[s, s, [s, o]]",
+                  strdict_type == STRDICT_TYPE_EXTERNAL_IDS ? "external_ids" : "other_config",
+                  "insert",
+                  "map",
+                  array));
 }
 
 /**
@@ -779,18 +867,19 @@ _insert_interface(json_t       *params,
     const char            *type = NULL;
     NMSettingOvsInterface *s_ovs_iface;
     NMSettingOvsDpdk      *s_ovs_dpdk;
-    const char            *devargs;
-    guint32                n_rxq;
     char                   sbuf[64];
     json_t                *dpdk_array;
     NMSettingOvsPatch     *s_ovs_patch;
     json_t                *options = json_array();
     json_t                *row;
-    guint32                mtu = 0;
+    guint32                mtu            = 0;
+    guint32                ofport_request = 0;
 
     s_ovs_iface = nm_connection_get_setting_ovs_interface(interface);
-    if (s_ovs_iface)
-        type = nm_setting_ovs_interface_get_interface_type(s_ovs_iface);
+    if (s_ovs_iface) {
+        type           = nm_setting_ovs_interface_get_interface_type(s_ovs_iface);
+        ofport_request = nm_setting_ovs_interface_get_ofport_request(s_ovs_iface);
+    }
 
     if (nm_streq0(type, "internal")) {
         NMSettingWired *s_wired;
@@ -808,8 +897,15 @@ _insert_interface(json_t       *params,
         s_ovs_patch = nm_connection_get_setting_ovs_patch(interface);
 
     if (s_ovs_dpdk) {
-        devargs = nm_setting_ovs_dpdk_get_devargs(s_ovs_dpdk);
-        n_rxq   = nm_setting_ovs_dpdk_get_n_rxq(s_ovs_dpdk);
+        const char *devargs;
+        guint32     n_rxq;
+        guint32     n_rxq_desc;
+        guint32     n_txq_desc;
+
+        devargs    = nm_setting_ovs_dpdk_get_devargs(s_ovs_dpdk);
+        n_rxq      = nm_setting_ovs_dpdk_get_n_rxq(s_ovs_dpdk);
+        n_rxq_desc = nm_setting_ovs_dpdk_get_n_rxq_desc(s_ovs_dpdk);
+        n_txq_desc = nm_setting_ovs_dpdk_get_n_txq_desc(s_ovs_dpdk);
 
         dpdk_array = json_array();
 
@@ -820,6 +916,16 @@ _insert_interface(json_t       *params,
             json_array_append_new(dpdk_array,
                                   json_pack("[s,s]", "n_rxq", nm_sprintf_buf(sbuf, "%u", n_rxq)));
         }
+        if (n_rxq_desc != 0) {
+            json_array_append_new(
+                dpdk_array,
+                json_pack("[s,s]", "n_rxq_desc", nm_sprintf_buf(sbuf, "%u", n_rxq_desc)));
+        }
+        if (n_txq_desc != 0) {
+            json_array_append_new(
+                dpdk_array,
+                json_pack("[s,s]", "n_txq_desc", nm_sprintf_buf(sbuf, "%u", n_txq_desc)));
+        }
 
         json_array_append_new(options, dpdk_array);
 
@@ -831,7 +937,7 @@ _insert_interface(json_t       *params,
         json_array_append_new(options, json_array());
     }
 
-    row = json_pack("{s:s, s:s, s:o, s:o}",
+    row = json_pack("{s:s, s:s, s:o, s:o, s:o}",
                     "name",
                     nm_connection_get_interface_name(interface),
                     "type",
@@ -839,7 +945,9 @@ _insert_interface(json_t       *params,
                     "options",
                     options,
                     "external_ids",
-                    _j_create_external_ids_array_new(interface));
+                    _j_create_strdict_new(interface, STRDICT_TYPE_EXTERNAL_IDS, NULL),
+                    "other_config",
+                    _j_create_strdict_new(interface, STRDICT_TYPE_OTHER_CONFIG, NULL));
 
     if (cloned_mac)
         json_object_set_new(row, "mac", json_string(cloned_mac));
@@ -847,6 +955,9 @@ _insert_interface(json_t       *params,
     if (mtu != 0)
         json_object_set_new(row, "mtu_request", json_integer(mtu));
 
+    if (ofport_request != 0)
+        json_object_set_new(row, "ofport_request", json_integer(ofport_request));
+
     json_array_append_new(params,
                           json_pack("{s:s, s:s, s:o, s:s}",
                                     "op",
@@ -869,6 +980,7 @@ _insert_port(json_t *params, NMConnection *port, json_t *new_interfaces)
 {
     NMSettingOvsPort *s_ovs_port;
     const char       *vlan_mode      = NULL;
+    json_t           *trunks         = NULL;
     guint             tag            = 0;
     const char       *lacp           = NULL;
     const char       *bond_mode      = NULL;
@@ -881,18 +993,34 @@ _insert_port(json_t *params, NMConnection *port, json_t *new_interfaces)
     row = json_object();
 
     if (s_ovs_port) {
+        const GPtrArray *ranges;
+        guint            i;
+        guint64          start;
+        guint64          end;
+
         vlan_mode      = nm_setting_ovs_port_get_vlan_mode(s_ovs_port);
         tag            = nm_setting_ovs_port_get_tag(s_ovs_port);
         lacp           = nm_setting_ovs_port_get_lacp(s_ovs_port);
         bond_mode      = nm_setting_ovs_port_get_bond_mode(s_ovs_port);
         bond_updelay   = nm_setting_ovs_port_get_bond_updelay(s_ovs_port);
         bond_downdelay = nm_setting_ovs_port_get_bond_downdelay(s_ovs_port);
+
+        ranges = _nm_setting_ovs_port_get_trunks_arr(s_ovs_port);
+        for (i = 0; i < ranges->len; i++) {
+            if (!trunks)
+                trunks = json_array();
+            nm_range_get_range(ranges->pdata[i], &start, &end);
+            for (; start <= end; start++)
+                json_array_append_new(trunks, json_integer(start));
+        }
     }
 
     if (vlan_mode)
         json_object_set_new(row, "vlan_mode", json_string(vlan_mode));
     if (tag)
         json_object_set_new(row, "tag", json_integer(tag));
+    if (trunks)
+        json_object_set_new(row, "trunks", json_pack("[s, o]", "set", trunks));
     if (lacp)
         json_object_set_new(row, "lacp", json_string(lacp));
     if (bond_mode)
@@ -904,7 +1032,12 @@ _insert_port(json_t *params, NMConnection *port, json_t *new_interfaces)
 
     json_object_set_new(row, "name", json_string(nm_connection_get_interface_name(port)));
     json_object_set_new(row, "interfaces", json_pack("[s, O]", "set", new_interfaces));
-    json_object_set_new(row, "external_ids", _j_create_external_ids_array_new(port));
+    json_object_set_new(row,
+                        "external_ids",
+                        _j_create_strdict_new(port, STRDICT_TYPE_EXTERNAL_IDS, NULL));
+    json_object_set_new(row,
+                        "other_config",
+                        _j_create_strdict_new(port, STRDICT_TYPE_OTHER_CONFIG, NULL));
 
     /* Create a new one. */
     json_array_append_new(params,
@@ -964,13 +1097,12 @@ _insert_bridge(json_t       *params,
 
     json_object_set_new(row, "name", json_string(nm_connection_get_interface_name(bridge)));
     json_object_set_new(row, "ports", json_pack("[s, O]", "set", new_ports));
-    json_object_set_new(row, "external_ids", _j_create_external_ids_array_new(bridge));
-
-    if (cloned_mac) {
-        json_object_set_new(row,
-                            "other_config",
-                            json_pack("[s, [[s, s]]]", "map", "hwaddr", cloned_mac));
-    }
+    json_object_set_new(row,
+                        "external_ids",
+                        _j_create_strdict_new(bridge, STRDICT_TYPE_EXTERNAL_IDS, NULL));
+    json_object_set_new(row,
+                        "other_config",
+                        _j_create_strdict_new(bridge, STRDICT_TYPE_OTHER_CONFIG, cloned_mac));
 
     /* Create a new one. */
     json_array_append_new(params,
@@ -1332,9 +1464,9 @@ ovsdb_next_command(NMOvsdb *self)
     switch (call->command) {
     case OVSDB_MONITOR:
         msg = json_pack("{s:I, s:s, s:[s, n, {"
-                        "  s:[{s:[s, s, s]}],"
-                        "  s:[{s:[s, s, s]}],"
                         "  s:[{s:[s, s, s, s]}],"
+                        "  s:[{s:[s, s, s, s]}],"
+                        "  s:[{s:[s, s, s, s, s]}],"
                         "  s:[{s:[]}]"
                         "}]}",
                         "id",
@@ -1348,16 +1480,19 @@ ovsdb_next_command(NMOvsdb *self)
                         "name",
                         "ports",
                         "external_ids",
+                        "other_config",
                         "Port",
                         "columns",
                         "name",
                         "interfaces",
                         "external_ids",
+                        "other_config",
                         "Interface",
                         "columns",
                         "name",
                         "type",
                         "external_ids",
+                        "other_config",
                         "error",
                         "Open_vSwitch",
                         "columns");
@@ -1398,24 +1533,38 @@ ovsdb_next_command(NMOvsdb *self)
                                             "==",
                                             call->payload.set_interface_mtu.ifname));
             break;
-        case OVSDB_SET_EXTERNAL_IDS:
+        case OVSDB_SET_REAPPLY:
+        {
+            json_t *mutations;
+
+            mutations = json_array();
+
+            _j_create_strv_array_update(mutations,
+                                        STRDICT_TYPE_EXTERNAL_IDS,
+                                        call->payload.set_reapply.connection_uuid,
+                                        call->payload.set_reapply.external_ids_old,
+                                        call->payload.set_reapply.external_ids_new);
+            _j_create_strv_array_update(mutations,
+                                        STRDICT_TYPE_OTHER_CONFIG,
+                                        NULL,
+                                        call->payload.set_reapply.other_config_old,
+                                        call->payload.set_reapply.other_config_new);
+
             json_array_append_new(
                 params,
                 json_pack("{s:s, s:s, s:o, s:[[s, s, s]]}",
                           "op",
                           "mutate",
                           "table",
-                          _device_type_to_table(call->payload.set_external_ids.device_type),
+                          _device_type_to_table(call->payload.set_reapply.device_type),
                           "mutations",
-                          _j_create_external_ids_array_update(
-                              call->payload.set_external_ids.connection_uuid,
-                              call->payload.set_external_ids.exid_old,
-                              call->payload.set_external_ids.exid_new),
+                          mutations,
                           "where",
                           "name",
                           "==",
-                          call->payload.set_external_ids.ifname));
+                          call->payload.set_reapply.ifname));
             break;
+        }
 
         default:
             nm_assert_not_reached();
@@ -1500,19 +1649,18 @@ _uuids_to_array(const json_t *items)
 }
 
 static void
-_external_ids_extract(json_t *external_ids, GArray **out_array, const char **out_connection_uuid)
+_strdict_extract(json_t *strdict, GArray **out_array)
 {
     json_t *array;
     json_t *value;
     gsize   index;
 
     nm_assert(out_array && !*out_array);
-    nm_assert(!out_connection_uuid || !*out_connection_uuid);
 
-    if (!nm_streq0("map", json_string_value(json_array_get(external_ids, 0))))
+    if (!nm_streq0("map", json_string_value(json_array_get(strdict, 0))))
         return;
 
-    array = json_array_get(external_ids, 1);
+    array = json_array_get(strdict, 1);
 
     json_array_foreach (array, index, value) {
         const char        *key = json_string_value(json_array_get(value, 0));
@@ -1533,16 +1681,26 @@ _external_ids_extract(json_t *external_ids, GArray **out_array, const char **out
             .name      = g_strdup(key),
             .value_str = g_strdup(val),
         };
-
-        if (out_connection_uuid && nm_streq(v->name, NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID)) {
-            *out_connection_uuid = v->value_str;
-            out_connection_uuid  = NULL;
-        }
     }
 }
 
+static const char *
+_strdict_find_key(GArray *array, const char *key)
+{
+    gssize idx;
+
+    idx = nm_utils_named_value_list_find(nm_g_array_first_p(array, NMUtilsNamedValue),
+                                         nm_g_array_len(array),
+                                         key,
+                                         FALSE);
+    if (idx < 0)
+        return NULL;
+
+    return nm_g_array_index(array, NMUtilsNamedValue, idx).value_str;
+}
+
 static gboolean
-_external_ids_equal(const GArray *arr1, const GArray *arr2)
+_strdict_equals(const GArray *arr1, const GArray *arr2)
 {
     guint n;
     guint i;
@@ -1552,19 +1710,19 @@ _external_ids_equal(const GArray *arr1, const GArray *arr2)
     if (n != nm_g_array_len(arr2))
         return FALSE;
     for (i = 0; i < n; i++) {
-        const NMUtilsNamedValue *n1 = &g_array_index(arr1, NMUtilsNamedValue, i);
-        const NMUtilsNamedValue *n2 = &g_array_index(arr2, NMUtilsNamedValue, i);
+        const NMUtilsNamedValue *n1 = &nm_g_array_index(arr1, NMUtilsNamedValue, i);
+        const NMUtilsNamedValue *n2 = &nm_g_array_index(arr2, NMUtilsNamedValue, i);
 
-        if (!nm_streq0(n1->name, n2->name))
+        if (!nm_streq(n1->name, n2->name))
             return FALSE;
-        if (!nm_streq0(n1->value_str, n2->value_str))
+        if (!nm_streq(n1->value_str, n2->value_str))
             return FALSE;
     }
     return TRUE;
 }
 
 static char *
-_external_ids_to_string(const GArray *arr)
+_strdict_to_string(const GArray *arr)
 {
     NMStrBuf strbuf;
     guint    i;
@@ -1575,7 +1733,7 @@ _external_ids_to_string(const GArray *arr)
     strbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_104, FALSE);
     nm_str_buf_append(&strbuf, "[");
     for (i = 0; i < arr->len; i++) {
-        const NMUtilsNamedValue *n = &g_array_index(arr, NMUtilsNamedValue, i);
+        const NMUtilsNamedValue *n = &nm_g_array_index(arr, NMUtilsNamedValue, i);
 
         if (i > 0)
             nm_str_buf_append_c(&strbuf, ',');
@@ -1605,8 +1763,9 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
     json_t         *interface = NULL;
     json_t         *items;
     json_t         *external_ids;
+    json_t         *other_config;
     json_error_t    json_error = {
-           0,
+        0,
     };
     void       *iter;
     const char *name;
@@ -1644,12 +1803,13 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
     json_object_foreach (interface, key, value) {
         OpenvswitchInterface  *ovs_interface;
         gs_unref_array GArray *external_ids_arr = NULL;
+        gs_unref_array GArray *other_config_arr = NULL;
         const char            *connection_uuid  = NULL;
         json_t                *error            = NULL;
         int                    r;
 
         r = json_unpack(value,
-                        "{s:{s:s, s:s, s?:o, s:o}}",
+                        "{s:{s:s, s:s, s?:o, s:o, s:o}}",
                         "new",
                         "name",
                         &name,
@@ -1658,7 +1818,9 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                         "error",
                         &error,
                         "external_ids",
-                        &external_ids);
+                        &external_ids,
+                        "other_config",
+                        &other_config);
         if (r != 0) {
             gpointer unused;
 
@@ -1701,7 +1863,10 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
             nm_clear_pointer(&ovs_interface, _free_interface);
         }
 
-        _external_ids_extract(external_ids, &external_ids_arr, &connection_uuid);
+        _strdict_extract(external_ids, &external_ids_arr);
+        connection_uuid =
+            _strdict_find_key(external_ids_arr, NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID);
+        _strdict_extract(other_config, &other_config_arr);
 
         if (ovs_interface) {
             gboolean changed = FALSE;
@@ -1710,14 +1875,20 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
 
             changed |= nm_strdup_reset(&ovs_interface->type, type);
             changed |= nm_strdup_reset(&ovs_interface->connection_uuid, connection_uuid);
-            if (!_external_ids_equal(ovs_interface->external_ids, external_ids_arr)) {
+            if (!_strdict_equals(ovs_interface->external_ids, external_ids_arr)) {
                 NM_SWAP(&ovs_interface->external_ids, &external_ids_arr);
                 changed = TRUE;
             }
+            if (!_strdict_equals(ovs_interface->other_config, other_config_arr)) {
+                NM_SWAP(&ovs_interface->other_config, &other_config_arr);
+                changed = TRUE;
+            }
             if (changed) {
-                gs_free char *strtmp = NULL;
+                gs_free char *strtmp1 = NULL;
+                gs_free char *strtmp2 = NULL;
 
-                _LOGT("obj[iface:%s]: changed an '%s' interface: %s%s%s, external-ids=%s",
+                _LOGT("obj[iface:%s]: changed an '%s' interface: %s%s%s, external-ids=%s, "
+                      "other-config=%s",
                       key,
                       type,
                       ovs_interface->name,
@@ -1725,10 +1896,12 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                                            ", ",
                                            ovs_interface->connection_uuid,
                                            ""),
-                      (strtmp = _external_ids_to_string(ovs_interface->external_ids)));
+                      (strtmp1 = _strdict_to_string(ovs_interface->external_ids)),
+                      (strtmp2 = _strdict_to_string(ovs_interface->other_config)));
             }
         } else {
-            gs_free char *strtmp = NULL;
+            gs_free char *strtmp1 = NULL;
+            gs_free char *strtmp2 = NULL;
 
             ovs_interface  = g_slice_new(OpenvswitchInterface);
             *ovs_interface = (OpenvswitchInterface){
@@ -1737,17 +1910,20 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 .type            = g_strdup(type),
                 .connection_uuid = g_strdup(connection_uuid),
                 .external_ids    = g_steal_pointer(&external_ids_arr),
+                .other_config    = g_steal_pointer(&other_config_arr),
             };
             g_hash_table_add(priv->interfaces, ovs_interface);
-            _LOGT("obj[iface:%s]: added an '%s' interface: %s%s%s, external-ids=%s",
-                  key,
-                  ovs_interface->type,
-                  ovs_interface->name,
-                  NM_PRINT_FMT_QUOTED2(ovs_interface->connection_uuid,
-                                       ", ",
-                                       ovs_interface->connection_uuid,
-                                       ""),
-                  (strtmp = _external_ids_to_string(ovs_interface->external_ids)));
+            _LOGT(
+                "obj[iface:%s]: added an '%s' interface: %s%s%s, external-ids=%s, other-config=%s",
+                key,
+                ovs_interface->type,
+                ovs_interface->name,
+                NM_PRINT_FMT_QUOTED2(ovs_interface->connection_uuid,
+                                     ", ",
+                                     ovs_interface->connection_uuid,
+                                     ""),
+                (strtmp1 = _strdict_to_string(ovs_interface->external_ids)),
+                (strtmp2 = _strdict_to_string(ovs_interface->other_config)));
             _signal_emit_device_added(self,
                                       ovs_interface->name,
                                       NM_DEVICE_TYPE_OVS_INTERFACE,
@@ -1768,16 +1944,19 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
         gs_unref_ptrarray GPtrArray *interfaces = NULL;
         OpenvswitchPort             *ovs_port;
         gs_unref_array GArray       *external_ids_arr = NULL;
+        gs_unref_array GArray       *other_config_arr = NULL;
         const char                  *connection_uuid  = NULL;
         int                          r;
 
         r = json_unpack(value,
-                        "{s:{s:s, s:o, s:o}}",
+                        "{s:{s:s, s:o, s:o, s:o}}",
                         "new",
                         "name",
                         &name,
                         "external_ids",
                         &external_ids,
+                        "other_config",
+                        &other_config,
                         "interfaces",
                         &items);
         if (r != 0) {
@@ -1811,7 +1990,11 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
             nm_clear_pointer(&ovs_port, _free_port);
         }
 
-        _external_ids_extract(external_ids, &external_ids_arr, &connection_uuid);
+        _strdict_extract(external_ids, &external_ids_arr);
+        connection_uuid =
+            _strdict_find_key(external_ids_arr, NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID);
+        _strdict_extract(other_config, &other_config_arr);
+
         interfaces = _uuids_to_array(items);
 
         if (ovs_port) {
@@ -1825,24 +2008,31 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 NM_SWAP(&ovs_port->interfaces, &interfaces);
                 changed = TRUE;
             }
-            if (!_external_ids_equal(ovs_port->external_ids, external_ids_arr)) {
+            if (!_strdict_equals(ovs_port->external_ids, external_ids_arr)) {
                 NM_SWAP(&ovs_port->external_ids, &external_ids_arr);
                 changed = TRUE;
             }
+            if (!_strdict_equals(ovs_port->other_config, other_config_arr)) {
+                NM_SWAP(&ovs_port->other_config, &other_config_arr);
+                changed = TRUE;
+            }
             if (changed) {
-                gs_free char *strtmp = NULL;
+                gs_free char *strtmp1 = NULL;
+                gs_free char *strtmp2 = NULL;
 
-                _LOGT("obj[port:%s]: changed a port: %s%s%s, external-ids=%s",
+                _LOGT("obj[port:%s]: changed a port: %s%s%s, external-ids=%s, other-config=%s",
                       key,
                       ovs_port->name,
                       NM_PRINT_FMT_QUOTED2(ovs_port->connection_uuid,
                                            ", ",
                                            ovs_port->connection_uuid,
                                            ""),
-                      (strtmp = _external_ids_to_string(ovs_port->external_ids)));
+                      (strtmp1 = _strdict_to_string(ovs_port->external_ids)),
+                      (strtmp2 = _strdict_to_string(ovs_port->other_config)));
             }
         } else {
-            gs_free char *strtmp = NULL;
+            gs_free char *strtmp1 = NULL;
+            gs_free char *strtmp2 = NULL;
 
             ovs_port  = g_slice_new(OpenvswitchPort);
             *ovs_port = (OpenvswitchPort){
@@ -1851,16 +2041,18 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 .connection_uuid = g_strdup(connection_uuid),
                 .interfaces      = g_steal_pointer(&interfaces),
                 .external_ids    = g_steal_pointer(&external_ids_arr),
+                .other_config    = g_steal_pointer(&other_config_arr),
             };
             g_hash_table_add(priv->ports, ovs_port);
-            _LOGT("obj[port:%s]: added a port: %s%s%s, external-ids=%s",
+            _LOGT("obj[port:%s]: added a port: %s%s%s, external-ids=%s, other-config=%s",
                   key,
                   ovs_port->name,
                   NM_PRINT_FMT_QUOTED2(ovs_port->connection_uuid,
                                        ", ",
                                        ovs_port->connection_uuid,
                                        ""),
-                  (strtmp = _external_ids_to_string(ovs_port->external_ids)));
+                  (strtmp1 = _strdict_to_string(ovs_port->external_ids)),
+                  (strtmp2 = _strdict_to_string(ovs_port->other_config)));
             _signal_emit_device_added(self, ovs_port->name, NM_DEVICE_TYPE_OVS_PORT, NULL);
         }
     }
@@ -1869,16 +2061,19 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
         gs_unref_ptrarray GPtrArray *ports = NULL;
         OpenvswitchBridge           *ovs_bridge;
         gs_unref_array GArray       *external_ids_arr = NULL;
+        gs_unref_array GArray       *other_config_arr = NULL;
         const char                  *connection_uuid  = NULL;
         int                          r;
 
         r = json_unpack(value,
-                        "{s:{s:s, s:o, s:o}}",
+                        "{s:{s:s, s:o, s:o, s:o}}",
                         "new",
                         "name",
                         &name,
                         "external_ids",
                         &external_ids,
+                        "other_config",
+                        &other_config,
                         "ports",
                         &items);
 
@@ -1916,7 +2111,11 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
             nm_clear_pointer(&ovs_bridge, _free_bridge);
         }
 
-        _external_ids_extract(external_ids, &external_ids_arr, &connection_uuid);
+        _strdict_extract(external_ids, &external_ids_arr);
+        connection_uuid =
+            _strdict_find_key(external_ids_arr, NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID);
+        _strdict_extract(other_config, &other_config_arr);
+
         ports = _uuids_to_array(items);
 
         if (ovs_bridge) {
@@ -1930,24 +2129,31 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 NM_SWAP(&ovs_bridge->ports, &ports);
                 changed = TRUE;
             }
-            if (!_external_ids_equal(ovs_bridge->external_ids, external_ids_arr)) {
+            if (!_strdict_equals(ovs_bridge->external_ids, external_ids_arr)) {
                 NM_SWAP(&ovs_bridge->external_ids, &external_ids_arr);
                 changed = TRUE;
             }
+            if (!_strdict_equals(ovs_bridge->other_config, other_config_arr)) {
+                NM_SWAP(&ovs_bridge->other_config, &other_config_arr);
+                changed = TRUE;
+            }
             if (changed) {
-                gs_free char *strtmp = NULL;
+                gs_free char *strtmp1 = NULL;
+                gs_free char *strtmp2 = NULL;
 
-                _LOGT("obj[bridge:%s]: changed a bridge: %s%s%s, external-ids=%s",
+                _LOGT("obj[bridge:%s]: changed a bridge: %s%s%s, external-ids=%s, other-config=%s",
                       key,
                       ovs_bridge->name,
                       NM_PRINT_FMT_QUOTED2(ovs_bridge->connection_uuid,
                                            ", ",
                                            ovs_bridge->connection_uuid,
                                            ""),
-                      (strtmp = _external_ids_to_string(ovs_bridge->external_ids)));
+                      (strtmp1 = _strdict_to_string(ovs_bridge->external_ids)),
+                      (strtmp2 = _strdict_to_string(ovs_bridge->external_ids)));
             }
         } else {
-            gs_free char *strtmp = NULL;
+            gs_free char *strtmp1 = NULL;
+            gs_free char *strtmp2 = NULL;
 
             ovs_bridge  = g_slice_new(OpenvswitchBridge);
             *ovs_bridge = (OpenvswitchBridge){
@@ -1956,16 +2162,18 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 .connection_uuid = g_strdup(connection_uuid),
                 .ports           = g_steal_pointer(&ports),
                 .external_ids    = g_steal_pointer(&external_ids_arr),
+                .other_config    = g_steal_pointer(&other_config_arr),
             };
             g_hash_table_add(priv->bridges, ovs_bridge);
-            _LOGT("obj[bridge:%s]: added a bridge: %s%s%s, external-ids=%s",
+            _LOGT("obj[bridge:%s]: added a bridge: %s%s%s, external-ids=%s, other-config=%s",
                   key,
                   ovs_bridge->name,
                   NM_PRINT_FMT_QUOTED2(ovs_bridge->connection_uuid,
                                        ", ",
                                        ovs_bridge->connection_uuid,
                                        ""),
-                  (strtmp = _external_ids_to_string(ovs_bridge->external_ids)));
+                  (strtmp1 = _strdict_to_string(ovs_bridge->external_ids)),
+                  (strtmp2 = _strdict_to_string(ovs_bridge->other_config)));
             _signal_emit_device_added(self, ovs_bridge->name, NM_DEVICE_TYPE_OVS_BRIDGE, NULL);
         }
     }
@@ -2007,7 +2215,7 @@ ovsdb_got_msg(NMOvsdb *self, json_t *msg)
 {
     NMOvsdbPrivate *priv       = NM_OVSDB_GET_PRIVATE(self);
     json_error_t    json_error = {
-           0,
+        0,
     };
     json_t     *json_id = NULL;
     json_int_t  id      = (json_int_t) -1;
@@ -2149,7 +2357,7 @@ ovsdb_read_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
     gssize          size;
     json_t         *msg;
     json_error_t    json_error = {
-           0,
+        0,
     };
 
     size = g_input_stream_read_finish(stream, res, &error);
@@ -2703,31 +2911,50 @@ nm_ovsdb_set_interface_mtu(NMOvsdb        *self,
 }
 
 void
-nm_ovsdb_set_external_ids(NMOvsdb                 *self,
-                          NMDeviceType             device_type,
-                          const char              *ifname,
-                          const char              *connection_uuid,
-                          NMSettingOvsExternalIDs *s_exid_old,
-                          NMSettingOvsExternalIDs *s_exid_new)
+nm_ovsdb_set_reapply(NMOvsdb                 *self,
+                     NMDeviceType             device_type,
+                     const char              *ifname,
+                     const char              *connection_uuid,
+                     NMSettingOvsExternalIDs *s_external_ids_old,
+                     NMSettingOvsExternalIDs *s_external_ids_new,
+                     NMSettingOvsOtherConfig *s_other_config_old,
+                     NMSettingOvsOtherConfig *s_other_config_new)
 {
-    gs_unref_hashtable GHashTable *exid_old = NULL;
-    gs_unref_hashtable GHashTable *exid_new = NULL;
-
-    exid_old =
-        s_exid_old ? nm_strdict_clone(_nm_setting_ovs_external_ids_get_data(s_exid_old)) : NULL;
-    exid_new =
-        s_exid_new ? nm_strdict_clone(_nm_setting_ovs_external_ids_get_data(s_exid_new)) : NULL;
+    gs_unref_hashtable GHashTable *external_ids_old = NULL;
+    gs_unref_hashtable GHashTable *external_ids_new = NULL;
+    gs_unref_hashtable GHashTable *other_config_old = NULL;
+    gs_unref_hashtable GHashTable *other_config_new = NULL;
+
+    external_ids_old =
+        s_external_ids_old
+            ? nm_strdict_clone(_nm_setting_ovs_external_ids_get_data(s_external_ids_old))
+            : NULL;
+    external_ids_new =
+        s_external_ids_new
+            ? nm_strdict_clone(_nm_setting_ovs_external_ids_get_data(s_external_ids_new))
+            : NULL;
+
+    other_config_old =
+        s_other_config_old
+            ? nm_strdict_clone(_nm_setting_ovs_other_config_get_data(s_other_config_old))
+            : NULL;
+    other_config_new =
+        s_other_config_new
+            ? nm_strdict_clone(_nm_setting_ovs_other_config_get_data(s_other_config_new))
+            : NULL;
 
     ovsdb_call_method(self,
                       NULL,
                       NULL,
                       FALSE,
-                      OVSDB_SET_EXTERNAL_IDS,
-                      OVSDB_METHOD_PAYLOAD_SET_EXTERNAL_IDS(device_type,
-                                                            ifname,
-                                                            connection_uuid,
-                                                            exid_old,
-                                                            exid_new));
+                      OVSDB_SET_REAPPLY,
+                      OVSDB_METHOD_PAYLOAD_SET_REAPPLY(device_type,
+                                                       ifname,
+                                                       connection_uuid,
+                                                       external_ids_old,
+                                                       external_ids_new,
+                                                       other_config_old,
+                                                       other_config_new));
 }
 
 /*****************************************************************************/
diff --git a/src/core/devices/ovs/nm-ovsdb.h b/src/core/devices/ovs/nm-ovsdb.h
index a50db45a..a022ff00 100644
--- a/src/core/devices/ovs/nm-ovsdb.h
+++ b/src/core/devices/ovs/nm-ovsdb.h
@@ -7,7 +7,7 @@
 #define __NETWORKMANAGER_OVSDB_H__
 
 #define NM_TYPE_OVSDB            (nm_ovsdb_get_type())
-#define NM_OVSDB(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_OVSDB, NMOvsdb))
+#define NM_OVSDB(obj)            (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_OVSDB, NMOvsdb))
 #define NM_OVSDB_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_OVSDB, NMOvsdbClass))
 #define NM_IS_OVSDB(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_OVSDB))
 #define NM_IS_OVSDB_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_OVSDB))
@@ -47,14 +47,14 @@ void nm_ovsdb_set_interface_mtu(NMOvsdb        *self,
                                 NMOvsdbCallback callback,
                                 gpointer        user_data);
 
-struct _NMSettingOvsExternalIDs;
-
-void nm_ovsdb_set_external_ids(NMOvsdb                         *self,
-                               NMDeviceType                     device_type,
-                               const char                      *ifname,
-                               const char                      *connection_uuid,
-                               struct _NMSettingOvsExternalIDs *s_exid_old,
-                               struct _NMSettingOvsExternalIDs *s_exid_new);
+void nm_ovsdb_set_reapply(NMOvsdb                 *self,
+                          NMDeviceType             device_type,
+                          const char              *ifname,
+                          const char              *connection_uuid,
+                          NMSettingOvsExternalIDs *s_external_ids_old,
+                          NMSettingOvsExternalIDs *s_external_ids_new,
+                          NMSettingOvsOtherConfig *s_other_config_old,
+                          NMSettingOvsOtherConfig *s_other_config_new);
 
 gboolean nm_ovsdb_is_ready(NMOvsdb *self);
 
diff --git a/src/core/devices/team/nm-device-team.c b/src/core/devices/team/nm-device-team.c
index 9eca008a..3e3fe52b 100644
--- a/src/core/devices/team/nm-device-team.c
+++ b/src/core/devices/team/nm-device-team.c
@@ -144,6 +144,7 @@ _update_port_config(NMDeviceTeam *self, const char *port_iface, const char *sani
     NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
     int                  err;
 
+    _LOGT(LOGD_TEAM, "setting port config: %s", sanitized_config);
     err = teamdctl_port_config_update_raw(priv->tdc, port_iface, sanitized_config);
     if (err != 0) {
         _LOGE(LOGD_TEAM, "failed to update config for port %s (err=%d)", port_iface, err);
@@ -154,23 +155,20 @@ _update_port_config(NMDeviceTeam *self, const char *port_iface, const char *sani
 }
 
 static gboolean
-ensure_teamd_connection(NMDevice *device)
+ensure_teamd_connection(NMDevice *device, GError **error)
 {
-    NMDeviceTeam         *self  = NM_DEVICE_TEAM(device);
-    NMDeviceTeamPrivate  *priv  = NM_DEVICE_TEAM_GET_PRIVATE(self);
-    gs_free_error GError *error = NULL;
-    const char           *port_iface;
-    const char           *port_config;
-    GHashTableIter        iter;
+    NMDeviceTeam        *self = NM_DEVICE_TEAM(device);
+    NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    const char          *port_iface;
+    const char          *port_config;
+    GHashTableIter       iter;
 
     if (priv->tdc)
         return TRUE;
 
-    priv->tdc = _tdc_connect_new(self, nm_device_get_iface(device), &error);
-    if (!priv->tdc) {
-        _LOGE(LOGD_TEAM, "failed to connect to teamd: %s", error->message);
+    priv->tdc = _tdc_connect_new(self, nm_device_get_iface(device), error);
+    if (!priv->tdc)
         return FALSE;
-    }
 
     g_hash_table_iter_init(&iter, priv->port_configs);
     while (g_hash_table_iter_next(&iter, (gpointer *) &port_iface, (gpointer *) &port_config))
@@ -229,10 +227,17 @@ update_connection(NMDevice *device, NMConnection *connection)
     NMSettingTeam       *s_team = _nm_connection_ensure_setting(connection, NM_TYPE_SETTING_TEAM);
     NMDeviceTeamPrivate *priv   = NM_DEVICE_TEAM_GET_PRIVATE(self);
     struct teamdctl     *tdc    = priv->tdc;
+    GError              *error  = NULL;
 
     /* Read the configuration only if not already set */
-    if (!priv->config && ensure_teamd_connection(device))
-        teamd_read_config(self);
+    if (!priv->config) {
+        if (ensure_teamd_connection(device, &error)) {
+            teamd_read_config(self);
+        } else {
+            _LOGD(LOGD_TEAM, "could not connect to teamd: %s", error->message);
+            g_clear_error(&error);
+        }
+    }
 
     /* Restore previous tdc state */
     if (priv->tdc && !tdc) {
@@ -258,9 +263,10 @@ master_update_slave_connection(NMDevice     *device,
     gs_free_error GError *connect_error = NULL;
     int                   err           = 0;
     struct teamdctl      *tdc;
-    const char           *team_port_config = NULL;
-    const char           *iface            = nm_device_get_iface(device);
-    const char           *iface_slave      = nm_device_get_iface(slave);
+    const char           *team_port_config   = NULL;
+    const char           *iface              = nm_device_get_iface(device);
+    const char           *iface_slave        = nm_device_get_iface(slave);
+    NMConnection         *applied_connection = nm_device_get_applied_connection(device);
 
     tdc = _tdc_connect_new(self, iface, &connect_error);
     if (!tdc) {
@@ -299,7 +305,7 @@ master_update_slave_connection(NMDevice     *device,
 
     g_object_set(nm_connection_get_setting_connection(connection),
                  NM_SETTING_CONNECTION_MASTER,
-                 iface,
+                 nm_connection_get_uuid(applied_connection),
                  NM_SETTING_CONNECTION_SLAVE_TYPE,
                  NM_SETTING_TEAM_SETTING_NAME,
                  NULL);
@@ -397,6 +403,7 @@ teamd_ready(NMDeviceTeam *self)
     NMDeviceTeamPrivate *priv   = NM_DEVICE_TEAM_GET_PRIVATE(self);
     NMDevice            *device = NM_DEVICE(self);
     gboolean             success;
+    GError              *error = NULL;
 
     if (priv->kill_in_progress) {
         /* If we are currently killing teamd, we are not
@@ -410,7 +417,11 @@ teamd_ready(NMDeviceTeam *self)
      * immediately.  But if we are, and grabbing it failed, fail the
      * device activation.
      */
-    success = ensure_teamd_connection(device);
+    success = ensure_teamd_connection(device, &error);
+    if (!success) {
+        _LOGW(LOGD_TEAM, "could not connect to teamd: %s", error->message);
+        g_clear_error(&error);
+    }
 
     if (nm_device_get_state(device) != NM_DEVICE_STATE_PREPARE
         || priv->stage1_state != NM_DEVICE_STAGE_STATE_PENDING)
@@ -759,18 +770,20 @@ teamd_start(NMDeviceTeam *self)
 static NMActStageReturn
 act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
 {
-    NMDeviceTeam         *self  = NM_DEVICE_TEAM(device);
-    NMDeviceTeamPrivate  *priv  = NM_DEVICE_TEAM_GET_PRIVATE(self);
-    gs_free_error GError *error = NULL;
-    NMSettingTeam        *s_team;
-    const char           *cfg;
+    NMDeviceTeam        *self  = NM_DEVICE_TEAM(device);
+    NMDeviceTeamPrivate *priv  = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    GError              *error = NULL;
+    NMSettingTeam       *s_team;
+    const char          *cfg;
 
     if (nm_device_sys_iface_state_is_external(device))
         return NM_ACT_STAGE_RETURN_SUCCESS;
 
     if (nm_device_sys_iface_state_is_external_or_assume(device)) {
-        if (ensure_teamd_connection(device))
+        if (ensure_teamd_connection(device, &error))
             return NM_ACT_STAGE_RETURN_SUCCESS;
+        _LOGD(LOGD_TEAM, "could not connect to teamd: %s", error->message);
+        g_clear_error(&error);
     }
 
     s_team = nm_device_get_applied_setting(device, NM_TYPE_SETTING_TEAM);
@@ -803,6 +816,7 @@ act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
                 _LOGW(LOGD_TEAM,
                       "existing teamd config mismatch; failed to kill existing teamd: %s",
                       error->message);
+                g_clear_error(&error);
                 NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED);
                 return NM_ACT_STAGE_RETURN_FAILURE;
             }
diff --git a/src/core/devices/team/nm-device-team.h b/src/core/devices/team/nm-device-team.h
index 6f5cff93..e2a54c6e 100644
--- a/src/core/devices/team/nm-device-team.h
+++ b/src/core/devices/team/nm-device-team.h
@@ -9,7 +9,8 @@
 #include "devices/nm-device.h"
 
 #define NM_TYPE_DEVICE_TEAM (nm_device_team_get_type())
-#define NM_DEVICE_TEAM(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_TEAM, NMDeviceTeam))
+#define NM_DEVICE_TEAM(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_TEAM, NMDeviceTeam))
 #define NM_DEVICE_TEAM_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_TEAM, NMDeviceTeamClass))
 #define NM_IS_DEVICE_TEAM(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_TEAM))
diff --git a/src/core/devices/team/nm-team-factory.c b/src/core/devices/team/nm-team-factory.c
index b446cbd0..68ea914e 100644
--- a/src/core/devices/team/nm-team-factory.c
+++ b/src/core/devices/team/nm-team-factory.c
@@ -17,7 +17,7 @@
 
 #define NM_TYPE_TEAM_FACTORY (nm_team_factory_get_type())
 #define NM_TEAM_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_TEAM_FACTORY, NMTeamFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_TEAM_FACTORY, NMTeamFactory))
 #define NM_TEAM_FACTORY_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_TEAM_FACTORY, NMTeamFactoryClass))
 #define NM_IS_TEAM_FACTORY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_TEAM_FACTORY))
diff --git a/src/core/devices/tests/test-lldp.c b/src/core/devices/tests/test-lldp.c
index 048410c1..4cd602f7 100644
--- a/src/core/devices/tests/test-lldp.c
+++ b/src/core/devices/tests/test-lldp.c
@@ -12,9 +12,8 @@
 #include <sys/stat.h>
 #include <sys/types.h>
 
+#include "libnm-lldp/nm-lldp.h"
 #include "devices/nm-lldp-listener.h"
-#include "libnm-systemd-core/nm-sd.h"
-
 #include "platform/tests/test-common.h"
 
 #include "nm-test-utils-core.h"
@@ -191,9 +190,9 @@ _test_recv_data0_check_do(GMainLoop *loop, NMLldpListener *listener, const TestR
     g_assert_cmpint(g_variant_n_children(neighbors), ==, 1);
 
     neighbor = get_lldp_neighbor(neighbors,
-                                 SD_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS,
+                                 NM_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS,
                                  "00:01:02:03:04:05",
-                                 SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME,
+                                 NM_LLDP_PORT_SUBTYPE_INTERFACE_NAME,
                                  "1/3");
     g_assert(neighbor);
     g_assert_cmpint(g_variant_n_children(neighbor), ==, 1 + 4 + 4);
@@ -547,9 +546,9 @@ _test_recv_data1_check(GMainLoop *loop, NMLldpListener *listener, TestRecvCallba
     g_assert_cmpint(g_variant_n_children(neighbors), ==, 1);
 
     neighbor = get_lldp_neighbor(neighbors,
-                                 SD_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS,
+                                 NM_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS,
                                  "00:01:30:F9:AD:A0",
-                                 SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME,
+                                 NM_LLDP_PORT_SUBTYPE_INTERFACE_NAME,
                                  "1/1");
     g_assert(neighbor);
     g_assert_cmpint(g_variant_n_children(neighbor), ==, 1 + 4 + 16);
@@ -874,7 +873,6 @@ test_recv(TestRecvFixture *fixture, gconstpointer user_data)
     TestRecvCallbackInfo info = {};
     gsize                i_frames;
     GError              *error = NULL;
-    guint                sd_id;
 
     if (fixture->ifindex == 0) {
         g_test_skip("Tun device not available");
@@ -884,8 +882,7 @@ test_recv(TestRecvFixture *fixture, gconstpointer user_data)
     listener = nm_lldp_listener_new(fixture->ifindex, lldp_neighbors_changed, &info, &error);
     nmtst_assert_success(listener, error);
 
-    loop  = g_main_loop_new(NULL, FALSE);
-    sd_id = nm_sd_event_attach_default();
+    loop = g_main_loop_new(NULL, FALSE);
 
     for (i_frames = 0; i_frames < data->frames_len; i_frames++) {
         const TestRecvFrame *f = data->frames[i_frames];
@@ -902,7 +899,6 @@ test_recv(TestRecvFixture *fixture, gconstpointer user_data)
 
     nm_clear_pointer(&listener, nm_lldp_listener_destroy);
 
-    nm_clear_g_source(&sd_id);
     nm_clear_pointer(&loop, g_main_loop_unref);
 }
 
diff --git a/src/core/devices/wifi/nm-device-iwd-p2p.h b/src/core/devices/wifi/nm-device-iwd-p2p.h
index 9ce353c0..98cf3762 100644
--- a/src/core/devices/wifi/nm-device-iwd-p2p.h
+++ b/src/core/devices/wifi/nm-device-iwd-p2p.h
@@ -11,7 +11,7 @@
 
 #define NM_TYPE_DEVICE_IWD_P2P (nm_device_iwd_p2p_get_type())
 #define NM_DEVICE_IWD_P2P(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IWD_P2P, NMDeviceIwdP2P))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IWD_P2P, NMDeviceIwdP2P))
 #define NM_DEVICE_IWD_P2P_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_IWD_P2P, NMDeviceIwdP2PClass))
 #define NM_IS_DEVICE_IWD_P2P(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_IWD_P2P))
diff --git a/src/core/devices/wifi/nm-device-iwd.c b/src/core/devices/wifi/nm-device-iwd.c
index ab37cbec..e03227cd 100644
--- a/src/core/devices/wifi/nm-device-iwd.c
+++ b/src/core/devices/wifi/nm-device-iwd.c
@@ -8,6 +8,7 @@
 #include "nm-device-iwd.h"
 
 #include <linux/if_ether.h>
+#include <linux/rtnetlink.h>
 
 #include "devices/nm-device-private.h"
 #include "devices/nm-device.h"
@@ -32,6 +33,7 @@
 #include "supplicant/nm-supplicant-types.h"
 #include "nm-auth-utils.h"
 #include "nm-manager.h"
+#include "nm-l3-config-data.h"
 
 #define _NMLOG_DEVICE_TYPE NMDeviceIwd
 #include "devices/nm-device-logging.h"
@@ -70,6 +72,7 @@ typedef struct {
     bool                          secrets_failed : 1;
     bool                          networks_requested : 1;
     bool                          networks_changed : 1;
+    bool                          assuming : 1;
     gint64                        last_scan;
     uint32_t                      ap_id;
     guint32                       rate;
@@ -77,6 +80,16 @@ typedef struct {
     GDBusMethodInvocation        *pending_agent_request;
     NMActiveConnection           *assumed_ac;
     guint                         assumed_ac_timeout;
+
+    union {
+        struct {
+            const NML3ConfigData *pending_l3cd_6;
+            const NML3ConfigData *pending_l3cd_4;
+        };
+        const NML3ConfigData *pending_l3cd_x[2];
+    };
+
+    NMIwdManager *manager;
 } NMDeviceIwdPrivate;
 
 struct _NMDeviceIwd {
@@ -289,6 +302,7 @@ insert_ap_from_network(NMDeviceIwd *self,
                        gint64       last_seen_msec,
                        int16_t      signal)
 {
+    NMDeviceIwdPrivate             *priv          = NM_DEVICE_IWD_GET_PRIVATE(self);
     gs_unref_object GDBusProxy     *network_proxy = NULL;
     nm_auto_ref_string NMRefString *bss_path      = nm_ref_string_new(path);
     NMWifiAP                       *ap;
@@ -299,7 +313,7 @@ insert_ap_from_network(NMDeviceIwd *self,
     }
 
     network_proxy =
-        nm_iwd_manager_get_dbus_interface(nm_iwd_manager_get(), path, NM_IWD_NETWORK_INTERFACE);
+        nm_iwd_manager_get_dbus_interface(priv->manager, path, NM_IWD_NETWORK_INTERFACE);
 
     ap = ap_from_network(self, network_proxy, bss_path, last_seen_msec, signal);
     if (!ap)
@@ -525,6 +539,9 @@ cleanup_association_attempt(NMDeviceIwd *self, gboolean disconnect)
 
     if (disconnect && priv->dbus_station_proxy)
         send_disconnect(self);
+
+    nm_clear_l3cd(&priv->pending_l3cd_6);
+    nm_clear_l3cd(&priv->pending_l3cd_4);
 }
 
 static void
@@ -581,6 +598,10 @@ deactivate(NMDevice *device)
     if (!priv->dbus_obj)
         return;
 
+    /* Don't cause IWD to break the connection being assumed */
+    if (priv->assuming)
+        return;
+
     if (priv->dbus_station_proxy) {
         gs_unref_variant GVariant *value =
             g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State");
@@ -673,7 +694,7 @@ deactivate_async(NMDevice                  *device,
 }
 
 static gboolean
-is_connection_known_network(NMConnection *connection)
+is_connection_known_network(NMIwdManager *manager, NMConnection *connection)
 {
     NMIwdNetworkSecurity security;
     gs_free char        *ssid = NULL;
@@ -681,17 +702,17 @@ is_connection_known_network(NMConnection *connection)
     if (!nm_wifi_connection_get_iwd_ssid_and_security(connection, &ssid, &security))
         return FALSE;
 
-    return nm_iwd_manager_is_known_network(nm_iwd_manager_get(), ssid, security);
+    return nm_iwd_manager_is_known_network(manager, ssid, security);
 }
 
 static gboolean
-is_ap_known_network(NMWifiAP *ap)
+is_ap_known_network(NMIwdManager *manager, NMWifiAP *ap)
 {
     gs_unref_object GDBusProxy *network_proxy = NULL;
     gs_unref_variant GVariant  *known_network = NULL;
 
     network_proxy =
-        nm_iwd_manager_get_dbus_interface(nm_iwd_manager_get(),
+        nm_iwd_manager_get_dbus_interface(manager,
                                           nm_ref_string_get_str(nm_wifi_ap_get_supplicant_path(ap)),
                                           NM_IWD_NETWORK_INTERFACE);
     if (!network_proxy)
@@ -794,7 +815,8 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
          * thus are Known Networks.
          */
         if (security == NM_IWD_NETWORK_SECURITY_8021X) {
-            if (!is_connection_known_network(connection)) {
+            if (!is_connection_known_network(priv->manager, connection)
+                && !nm_iwd_manager_is_recently_mirrored(priv->manager, ssid)) {
                 nm_utils_error_set_literal(error,
                                            NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
                                            "802.1x connections must have IWD provisioning files");
@@ -927,7 +949,9 @@ check_connection_available(NMDevice                      *device,
      */
     if (nm_wifi_connection_get_iwd_ssid_and_security(connection, NULL, &security)
         && security == NM_IWD_NETWORK_SECURITY_8021X) {
-        if (!is_ap_known_network(ap)) {
+        if (!is_ap_known_network(priv->manager, ap)
+            && !nm_iwd_manager_is_recently_mirrored(priv->manager,
+                                                    nm_setting_wireless_get_ssid(s_wifi))) {
             nm_utils_error_set_literal(
                 error,
                 NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
@@ -2044,7 +2068,7 @@ assume_connection(NMDeviceIwd *self, NMWifiAP *ap)
      * becomes "managed" only when ACTIVATED but for IWD it's really
      * managed when IP_CONFIG starts.
      */
-    sett_conn = nm_iwd_manager_get_ap_mirror_connection(nm_iwd_manager_get(), ap);
+    sett_conn = nm_iwd_manager_get_ap_mirror_connection(priv->manager, ap);
     if (!sett_conn)
         goto error;
 
@@ -2217,7 +2241,8 @@ act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
      * for a first-time connection to a hidden network.  If a hidden network is
      * a Known Network it should still have been in the AP list.
      */
-    if (!nm_setting_wireless_get_hidden(s_wireless) || is_connection_known_network(connection))
+    if (!nm_setting_wireless_get_hidden(s_wireless)
+        || is_connection_known_network(priv->manager, connection))
         return NM_ACT_STAGE_RETURN_FAILURE;
 
 add_new:
@@ -2270,6 +2295,18 @@ act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
             goto out_fail;
         }
 
+        /* With priv->iwd_autoconnect we have to let IWD handle retries for
+         * infrastructure networks.  IWD will not necessarily retry the same
+         * network after a failure but it will likely go into an autoconnect
+         * mode and we don't want to try to override the logic.  We don't need
+         * to reset the retry count so we set no timeout.
+         */
+        if (priv->iwd_autoconnect) {
+            NMSettingsConnection *sett_conn = nm_act_request_get_settings_connection(req);
+
+            nm_settings_connection_autoconnect_retries_set(sett_conn, 0);
+        }
+
         /* With priv->iwd_autoconnect, if we're assuming a connection because
          * of a state change to "connecting", signal stage 2 is still running.
          * If "connected" or "roaming", we can go right to the IP_CONFIG state
@@ -2310,7 +2347,9 @@ act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
          * fail, for other combinations we will let the Connect call fail
          * or ask us for any missing secrets through the Agent.
          */
-        if (nm_connection_get_setting_802_1x(connection) && !is_ap_known_network(ap)) {
+        if (nm_connection_get_setting_802_1x(connection) && !is_ap_known_network(priv->manager, ap)
+            && !nm_iwd_manager_is_recently_mirrored(priv->manager,
+                                                    nm_setting_wireless_get_ssid(s_wireless))) {
             _LOGI(LOGD_DEVICE | LOGD_WIFI,
                   "Activation: (wifi) access point '%s' has 802.1x security but is not configured "
                   "in IWD.",
@@ -2351,7 +2390,7 @@ act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
         }
 
         network_proxy = nm_iwd_manager_get_dbus_interface(
-            nm_iwd_manager_get(),
+            priv->manager,
             nm_ref_string_get_str(nm_wifi_ap_get_supplicant_path(ap)),
             NM_IWD_NETWORK_INTERFACE);
         if (!network_proxy) {
@@ -2421,6 +2460,38 @@ out_fail:
     return NM_ACT_STAGE_RETURN_FAILURE;
 }
 
+static void
+act_stage3_ip_config(NMDevice *device, int addr_family)
+{
+    NMDeviceIwd        *self    = NM_DEVICE_IWD(device);
+    NMDeviceIwdPrivate *priv    = NM_DEVICE_IWD_GET_PRIVATE(self);
+    const int           IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    if (!nm_iwd_manager_get_netconfig_enabled(nm_iwd_manager_get())) {
+        NMDeviceClass *device_class = NM_DEVICE_CLASS(nm_device_iwd_parent_class);
+
+        if (device_class->act_stage3_ip_config)
+            device_class->act_stage3_ip_config(device, addr_family);
+
+        return;
+    }
+
+    if (!priv->pending_l3cd_x[IS_IPv4])
+        return;
+
+    nm_device_devip_set_state(device,
+                              addr_family,
+                              NM_DEVICE_IP_STATE_READY,
+                              priv->pending_l3cd_x[IS_IPv4]);
+    nm_clear_l3cd(&priv->pending_l3cd_x[IS_IPv4]);
+}
+
+static gboolean
+ready_for_ip_config(NMDevice *device, gboolean is_manual)
+{
+    return !nm_iwd_manager_get_netconfig_enabled(nm_iwd_manager_get());
+}
+
 static guint32
 get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force)
 {
@@ -2719,12 +2790,20 @@ state_changed(NMDeviceIwd *self, const char *new_state)
               "IWD is connecting to the wrong AP, %s activation",
               switch_ap ? "replacing" : "aborting");
         cleanup_association_attempt(self, !switch_ap);
+
+        if (!switch_ap) {
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+            return;
+        }
+
+        priv->assuming = TRUE; /* Don't send Station.Disconnect() */
         nm_device_state_changed(device,
-                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_DISCONNECTED,
                                 NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
-
-        if (switch_ap)
-            assume_connection(self, ap);
+        priv->assuming = FALSE;
+        assume_connection(self, ap);
         return;
     }
 
@@ -3101,7 +3180,7 @@ nm_device_iwd_set_dbus_object(NMDeviceIwd *self, GDBusObject *object)
         goto error;
     }
 
-    adapter_proxy = nm_iwd_manager_get_dbus_interface(nm_iwd_manager_get(),
+    adapter_proxy = nm_iwd_manager_get_dbus_interface(priv->manager,
                                                       g_variant_get_string(value, NULL),
                                                       NM_IWD_WIPHY_INTERFACE);
     if (!adapter_proxy) {
@@ -3278,7 +3357,7 @@ nm_device_iwd_agent_query(NMDeviceIwd *self, GDBusMethodInvocation *invocation)
      * it.  IWD only queries us if it has not saved the PSK (e.g. by policy)
      * or a previous attempt has failed with current secrets so it wants a
      * fresh value.  It doesn't know about agent-owned secrets so whenever
-     * possible and the PSK is saved and not asked from NM.  However if this
+     * possible, the PSK is saved and not asked from NM.  However if this
      * is a new connection it may include all of the needed settings already
      * so allow using these, too.  Connection timestamp is set after
      * activation or after first activation failure (to 0).
@@ -3368,6 +3447,341 @@ nm_device_iwd_network_add_remove(NMDeviceIwd *self, GDBusProxy *network, bool ad
     }
 }
 
+static const NML3ConfigData *
+nm_device_iwd_parse_netconfig(NMDeviceIwd *self, int addr_family, GVariantIter *config_iter)
+{
+    NMDevice                               *device = NM_DEVICE(self);
+    nm_auto_unref_l3cd_init NML3ConfigData *l3cd   = NULL;
+    const char                             *key;
+    GVariant                               *value;
+    NMIPConfigSource                        source       = NM_IP_CONFIG_SOURCE_UNKNOWN;
+    nm_auto_free_variant_iter GVariantIter *address_iter = NULL;
+    nm_auto_free_variant_iter GVariantIter *route_iter   = NULL;
+    nm_auto_free_variant_iter GVariantIter *dns_iter     = NULL;
+    nm_auto_free_variant_iter GVariantIter *domain_iter  = NULL;
+    NMSettingConnectionMdns                 mdns         = NM_SETTING_CONNECTION_MDNS_DEFAULT;
+    const char                             *str_value;
+    GVariantIter                           *entry_iter;
+    uint32_t                                timestamp = nm_utils_get_monotonic_timestamp_sec();
+
+    while (g_variant_iter_next(config_iter, "{&sv}", &key, &value)) {
+        _nm_unused gs_unref_variant GVariant *value_free = value;
+
+        if (nm_streq(key, "Method")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)
+                || source != NM_IP_CONFIG_SOURCE_UNKNOWN)
+                goto param_error;
+
+            str_value = g_variant_get_string(value, NULL);
+            if (nm_streq(str_value, "static"))
+                source = NM_IP_CONFIG_SOURCE_USER;
+            else if (nm_streq(str_value, "auto"))
+                /* Use SOURCE_DHCP as shorthand for the various autoconfiguration protocols */
+                source = NM_IP_CONFIG_SOURCE_DHCP;
+            else
+                _LOGW(LOGD_WIFI, "iwd_parse_netconfig: Uknown Method value \"%s\"", str_value);
+        }
+
+        if (nm_streq(key, "Addresses")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE("aa{sv}")) || address_iter)
+                goto param_error;
+
+            g_variant_get(value, "aa{sv}", &address_iter);
+        }
+
+        if (nm_streq(key, "Routes")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE("aa{sv}")) || route_iter)
+                goto param_error;
+
+            g_variant_get(value, "aa{sv}", &route_iter);
+        }
+
+        if (nm_streq(key, "DomainNameServers")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING_ARRAY) || dns_iter)
+                goto param_error;
+
+            g_variant_get(value, "as", &dns_iter);
+        }
+
+        if (nm_streq(key, "DomainNames")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING_ARRAY) || domain_iter)
+                goto param_error;
+
+            g_variant_get(value, "as", &domain_iter);
+        }
+
+        if (nm_streq(key, "MDNS")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)
+                || mdns != NM_SETTING_CONNECTION_MDNS_DEFAULT)
+                goto param_error;
+
+            str_value = g_variant_get_string(value, NULL);
+            if (nm_streq(str_value, "yes"))
+                mdns = NM_SETTING_CONNECTION_MDNS_YES;
+            else if (nm_streq(str_value, "no"))
+                mdns = NM_SETTING_CONNECTION_MDNS_NO;
+            else if (nm_streq(str_value, "resolve"))
+                mdns = NM_SETTING_CONNECTION_MDNS_RESOLVE;
+            else
+                _LOGW(LOGD_WIFI, "iwd_parse_netconfig: Uknown MDNS value \"%s\"", str_value);
+        }
+    }
+
+    if (!address_iter || !route_iter)
+        goto param_error;
+
+    l3cd = nm_l3_config_data_new(nm_device_get_multi_index(device),
+                                 nm_device_get_ip_ifindex(device),
+                                 source);
+
+    while (g_variant_iter_next(address_iter, "a{sv}", &entry_iter)) {
+        _nm_unused nm_auto_free_variant_iter GVariantIter *entry_iter_free = entry_iter;
+        const char                                        *addr_str        = NULL;
+        NMIPAddr                                           addr_bin;
+        uint8_t                                            plen      = 0;
+        const char                                        *bcast_str = NULL;
+        NMIPAddr                                           bcast_bin = {};
+        uint32_t             valid_lifetime     = NM_PLATFORM_LIFETIME_PERMANENT;
+        uint32_t             preferred_lifetime = NM_PLATFORM_LIFETIME_PERMANENT;
+        NMPlatformIPXAddress a;
+
+        while (g_variant_iter_next(entry_iter, "{&sv}", &key, &value)) {
+            _nm_unused gs_unref_variant GVariant *value_free = value;
+
+            if (nm_streq(key, "Address")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING) || addr_str)
+                    goto param_error;
+
+                addr_str = g_variant_get_string(value, NULL);
+                if (inet_pton(addr_family, addr_str, &addr_bin) != 1)
+                    goto param_error;
+            }
+
+            if (nm_streq(key, "PrefixLength")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_BYTE))
+                    goto param_error;
+
+                plen = g_variant_get_byte(value);
+            }
+
+            if (nm_streq(key, "Broadcast")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING) || bcast_str)
+                    goto param_error;
+
+                bcast_str = g_variant_get_string(value, NULL);
+                if (inet_pton(addr_family, bcast_str, &bcast_bin) != 1)
+                    goto param_error;
+            }
+
+            if (nm_streq(key, "ValidLifetime")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32)
+                    || valid_lifetime != NM_PLATFORM_LIFETIME_PERMANENT)
+                    goto param_error;
+
+                valid_lifetime = g_variant_get_uint32(value);
+            }
+
+            if (nm_streq(key, "PreferredLifetime")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32)
+                    || preferred_lifetime != NM_PLATFORM_LIFETIME_PERMANENT)
+                    goto param_error;
+
+                preferred_lifetime = g_variant_get_uint32(value);
+            }
+        }
+
+        if (addr_str == NULL)
+            goto param_error;
+
+        if (valid_lifetime != NM_PLATFORM_LIFETIME_PERMANENT
+            && preferred_lifetime == NM_PLATFORM_LIFETIME_PERMANENT)
+            preferred_lifetime = valid_lifetime;
+
+        if (addr_family == AF_INET) {
+            a.a4 = (NMPlatformIP4Address){
+                .address      = addr_bin.addr4,
+                .peer_address = addr_bin.addr4,
+                .plen         = plen,
+                .timestamp    = (valid_lifetime != NM_PLATFORM_LIFETIME_PERMANENT) ? timestamp : 0,
+                .lifetime     = valid_lifetime,
+                .preferred    = preferred_lifetime,
+                .addr_source  = source,
+                .use_ip4_broadcast_address = (bcast_str != NULL),
+                .broadcast_address         = bcast_bin.addr4,
+            };
+        } else {
+            a.a6 = (NMPlatformIP6Address){
+                .address     = addr_bin.addr6,
+                .plen        = 128,
+                .timestamp   = (valid_lifetime != NM_PLATFORM_LIFETIME_PERMANENT) ? timestamp : 0,
+                .lifetime    = valid_lifetime,
+                .preferred   = preferred_lifetime,
+                .addr_source = source,
+            };
+        }
+
+        nm_l3_config_data_add_address(l3cd, addr_family, NULL, &a.ax);
+    }
+
+    while (g_variant_iter_next(route_iter, "a{sv}", &entry_iter)) {
+        _nm_unused nm_auto_free_variant_iter GVariantIter *entry_iter_free = entry_iter;
+        const char                                        *dst_addr_str    = NULL;
+        NMIPAddr                                           dst_addr_bin    = {};
+        uint8_t                                            dst_plen        = 0;
+        const char                                        *router_str      = NULL;
+        NMIPAddr                                           router_bin      = {};
+        const char                                        *pref_src_str    = NULL;
+        NMIPAddr                                           pref_src_bin    = {};
+        _nm_unused uint32_t lifetime   = NM_PLATFORM_LIFETIME_PERMANENT;
+        _nm_unused uint32_t priority   = 0;
+        uint8_t             preference = 0;
+        uint32_t            mtu        = 0;
+        NMPlatformIPXRoute  r;
+
+        while (g_variant_iter_next(entry_iter, "{&sv}", &key, &value)) {
+            _nm_unused gs_unref_variant GVariant *value_free = value;
+
+            if (nm_streq(key, "Destination")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE("(sy)")) || dst_addr_str)
+                    goto param_error;
+
+                g_variant_get(value, "(&sy)", &dst_addr_str, &dst_plen);
+                if (inet_pton(addr_family, dst_addr_str, &dst_addr_bin) != 1)
+                    goto param_error;
+            }
+
+            if (nm_streq(key, "Router")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING) || router_str)
+                    goto param_error;
+
+                router_str = g_variant_get_string(value, NULL);
+                if (inet_pton(addr_family, router_str, &router_bin) != 1)
+                    goto param_error;
+            }
+
+            if (nm_streq(key, "PreferredSource")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING) || pref_src_str)
+                    goto param_error;
+
+                pref_src_str = g_variant_get_string(value, NULL);
+                if (inet_pton(addr_family, pref_src_str, &pref_src_bin) != 1)
+                    goto param_error;
+            }
+
+            if (nm_streq(key, "Lifetime")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32)
+                    || lifetime != NM_PLATFORM_LIFETIME_PERMANENT)
+                    goto param_error;
+
+                lifetime = g_variant_get_uint32(value);
+            }
+
+            if (nm_streq(key, "Priority")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32))
+                    goto param_error;
+
+                priority = g_variant_get_uint32(value);
+            }
+
+            if (nm_streq(key, "Preference")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_BYTE))
+                    goto param_error;
+
+                preference = g_variant_get_byte(value);
+            }
+
+            if (nm_streq(key, "MTU")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32))
+                    goto param_error;
+
+                mtu = g_variant_get_uint32(value);
+            }
+        }
+
+        if (addr_family == AF_INET) {
+            r.r4 = (NMPlatformIP4Route){
+                .network  = dst_addr_str ? dst_addr_bin.addr4 : 0,
+                .plen     = dst_addr_str ? dst_plen : 0,
+                .gateway  = router_str ? router_bin.addr4 : 0,
+                .pref_src = pref_src_str ? pref_src_bin.addr4 : 0,
+                .scope_inv =
+                    nm_platform_route_scope_inv(router_str ? RT_SCOPE_UNIVERSE : RT_SCOPE_LINK),
+            };
+        } else {
+            r.r6 = (NMPlatformIP6Route){
+                .network     = dst_addr_str ? dst_addr_bin.addr6 : nm_ip_addr_zero.addr6,
+                .plen        = dst_addr_str ? dst_plen : 0,
+                .gateway     = router_str ? router_bin.addr6 : nm_ip_addr_zero.addr6,
+                .pref_src    = pref_src_str ? pref_src_bin.addr6 : nm_ip_addr_zero.addr6,
+                .rt_pref     = preference,
+                .r_rtm_flags = RTNH_F_ONLINK,
+            };
+        }
+
+        r.rx.metric_any   = TRUE;
+        r.rx.mtu          = mtu;
+        r.rx.rt_source    = source;
+        r.rx.type_coerced = nm_platform_route_type_coerce(RTN_UNICAST);
+        r.rx.table_any    = TRUE;
+
+        /* TODO: set the lifetime */
+        nm_l3_config_data_add_route(l3cd, addr_family, NULL, &r.rx);
+    }
+
+    if (dns_iter) {
+        while (g_variant_iter_next(dns_iter, "&s", &str_value)) {
+            NMIPAddr dns_bin;
+
+            if (inet_pton(addr_family, str_value, &dns_bin) != 1)
+                goto param_error;
+
+            nm_l3_config_data_add_nameserver_detail(l3cd, addr_family, &dns_bin, NULL);
+            nm_l3_config_data_set_dns_priority(l3cd, addr_family, NM_DNS_PRIORITY_DEFAULT_NORMAL);
+        }
+    }
+
+    if (domain_iter) {
+        while (g_variant_iter_next(domain_iter, "&s", &str_value))
+            nm_l3_config_data_add_search(l3cd, addr_family, str_value);
+    }
+
+    if (mdns != NM_SETTING_CONNECTION_MDNS_DEFAULT)
+        nm_l3_config_data_set_mdns(l3cd, mdns);
+
+    return nm_l3_config_data_ref_and_seal(l3cd);
+
+param_error:
+    return NULL;
+}
+
+bool
+nm_device_iwd_set_netconfig(NMDeviceIwd *self, int addr_family, GVariantIter *config_iter)
+{
+    NMDevice             *device = NM_DEVICE(self);
+    NMDeviceIwdPrivate   *priv   = NM_DEVICE_IWD_GET_PRIVATE(self);
+    const NML3ConfigData *l3cd;
+    NMDeviceState         state = nm_device_get_state(device);
+
+    if (state < NM_DEVICE_STATE_CONFIG || state > NM_DEVICE_STATE_ACTIVATED)
+        return FALSE;
+
+    l3cd = nm_device_iwd_parse_netconfig(self, addr_family, config_iter);
+    if (!l3cd) {
+        _LOGE(LOGD_WIFI, "Malformed netconfig DBus structure");
+        return FALSE;
+    }
+
+    if (state == NM_DEVICE_STATE_CONFIG) {
+        nm_l3_config_data_unref(priv->pending_l3cd_x[NM_IS_IPv4(addr_family)]);
+        priv->pending_l3cd_x[NM_IS_IPv4(addr_family)] = l3cd;
+    } else {
+        nm_device_devip_set_state(device, addr_family, NM_DEVICE_IP_STATE_READY, l3cd);
+        nm_l3_config_data_unref(l3cd);
+    }
+
+    return TRUE;
+}
+
 static void
 autoconnect_changed(NMDevice *device, GParamSpec *pspec, NMDeviceIwd *self)
 {
@@ -3411,7 +3825,7 @@ nm_device_iwd_init(NMDeviceIwd *self)
     g_signal_connect(self, "notify::" NM_DEVICE_AUTOCONNECT, G_CALLBACK(autoconnect_changed), self);
 
     /* Make sure the manager is running */
-    (void) nm_iwd_manager_get();
+    priv->manager = g_object_ref(nm_iwd_manager_get());
 }
 
 NMDevice *
@@ -3443,6 +3857,8 @@ dispose(GObject *object)
     G_OBJECT_CLASS(nm_device_iwd_parent_class)->dispose(object);
 
     nm_assert(c_list_is_empty(&priv->aps_lst_head));
+
+    g_clear_object(&priv->manager);
 }
 
 static void
@@ -3472,12 +3888,14 @@ nm_device_iwd_class_init(NMDeviceIwdClass *klass)
     device_class->set_enabled                 = set_enabled;
     device_class->get_type_description        = get_type_description;
 
-    device_class->act_stage1_prepare = act_stage1_prepare;
-    device_class->act_stage2_config  = act_stage2_config;
-    device_class->get_configured_mtu = get_configured_mtu;
-    device_class->deactivate         = deactivate;
-    device_class->deactivate_async   = deactivate_async;
-    device_class->can_reapply_change = can_reapply_change;
+    device_class->act_stage1_prepare   = act_stage1_prepare;
+    device_class->act_stage2_config    = act_stage2_config;
+    device_class->act_stage3_ip_config = act_stage3_ip_config;
+    device_class->ready_for_ip_config  = ready_for_ip_config;
+    device_class->get_configured_mtu   = get_configured_mtu;
+    device_class->deactivate           = deactivate;
+    device_class->deactivate_async     = deactivate_async;
+    device_class->can_reapply_change   = can_reapply_change;
 
     /* Stage 1 needed only for the set_current_ap() call.  Stage 2 is
      * needed if we're assuming a connection still in the "connecting"
diff --git a/src/core/devices/wifi/nm-device-iwd.h b/src/core/devices/wifi/nm-device-iwd.h
index bb136bf6..8449f7f6 100644
--- a/src/core/devices/wifi/nm-device-iwd.h
+++ b/src/core/devices/wifi/nm-device-iwd.h
@@ -11,7 +11,7 @@
 #include "nm-device-wifi.h"
 
 #define NM_TYPE_DEVICE_IWD (nm_device_iwd_get_type())
-#define NM_DEVICE_IWD(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IWD, NMDeviceIwd))
+#define NM_DEVICE_IWD(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IWD, NMDeviceIwd))
 #define NM_DEVICE_IWD_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_IWD, NMDeviceIwdClass))
 #define NM_IS_DEVICE_IWD(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_IWD))
@@ -46,4 +46,6 @@ void _nm_device_iwd_request_scan(NMDeviceIwd           *self,
 
 void nm_device_iwd_network_add_remove(NMDeviceIwd *device, GDBusProxy *network, bool add);
 
+bool nm_device_iwd_set_netconfig(NMDeviceIwd *device, int addr_family, GVariantIter *config_iter);
+
 #endif /* __NETWORKMANAGER_DEVICE_IWD_H__ */
diff --git a/src/core/devices/wifi/nm-device-olpc-mesh.h b/src/core/devices/wifi/nm-device-olpc-mesh.h
index 79b7fd5d..af3abbc0 100644
--- a/src/core/devices/wifi/nm-device-olpc-mesh.h
+++ b/src/core/devices/wifi/nm-device-olpc-mesh.h
@@ -15,7 +15,7 @@
 
 #define NM_TYPE_DEVICE_OLPC_MESH (nm_device_olpc_mesh_get_type())
 #define NM_DEVICE_OLPC_MESH(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OLPC_MESH, NMDeviceOlpcMesh))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OLPC_MESH, NMDeviceOlpcMesh))
 #define NM_DEVICE_OLPC_MESH_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_OLPC_MESH, NMDeviceOlpcMeshClass))
 #define NM_IS_DEVICE_OLPC_MESH(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_OLPC_MESH))
diff --git a/src/core/devices/wifi/nm-device-wifi-p2p.c b/src/core/devices/wifi/nm-device-wifi-p2p.c
index 4576af95..6c0a4d5e 100644
--- a/src/core/devices/wifi/nm-device-wifi-p2p.c
+++ b/src/core/devices/wifi/nm-device-wifi-p2p.c
@@ -22,7 +22,6 @@
 #include "nm-act-request.h"
 #include "nm-l3-config-data.h"
 #include "nm-manager.h"
-#include "nm-manager.h"
 #include "nm-setting-wifi-p2p.h"
 #include "nm-utils.h"
 #include "nm-wifi-p2p-peer.h"
diff --git a/src/core/devices/wifi/nm-device-wifi-p2p.h b/src/core/devices/wifi/nm-device-wifi-p2p.h
index d1aadd8e..08780464 100644
--- a/src/core/devices/wifi/nm-device-wifi-p2p.h
+++ b/src/core/devices/wifi/nm-device-wifi-p2p.h
@@ -11,7 +11,7 @@
 
 #define NM_TYPE_DEVICE_WIFI_P2P (nm_device_wifi_p2p_get_type())
 #define NM_DEVICE_WIFI_P2P(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIFI_P2P, NMDeviceWifiP2P))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIFI_P2P, NMDeviceWifiP2P))
 #define NM_DEVICE_WIFI_P2P_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_WIFI_P2P, NMDeviceWifiP2PClass))
 #define NM_IS_DEVICE_WIFI_P2P(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_WIFI_P2P))
diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
index 8af974d0..171743d0 100644
--- a/src/core/devices/wifi/nm-device-wifi.c
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -99,13 +99,13 @@ typedef struct {
     NMSupplMgrCreateIfaceHandle *sup_create_handle;
     NMSupplicantInterface       *sup_iface;
 
+    GSource *scan_kickoff_timeout_source;
+
     gint64 scan_last_complete_msec;
     gint64 scan_periodic_next_msec;
 
     gint64 scan_last_request_started_at_msec;
 
-    guint scan_kickoff_timeout_id;
-
     guint ap_dump_id;
 
     guint periodic_update_id;
@@ -508,7 +508,10 @@ _scan_notify_allowed(NMDeviceWifi *self, NMTernary do_kickoff)
         /* something prohibits scanning. */
     } else if (NM_IN_SET(priv->mode, _NM_802_11_MODE_ADHOC, _NM_802_11_MODE_AP)) {
         /* Don't scan when a an AP or Ad-Hoc connection is active as it will
-         * disrupt connected clients or peers. */
+         * disrupt connected clients or peers.
+         *
+         * Explicit scans are allowed however. */
+        explicit_allowed = TRUE;
     } else if (NM_IN_SET(state, NM_DEVICE_STATE_DISCONNECTED, NM_DEVICE_STATE_FAILED)) {
         /* Can always scan when disconnected */
         explicit_allowed = TRUE;
@@ -659,7 +662,7 @@ supplicant_interface_release(NMDeviceWifi *self)
                                         NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT,
                                         TRUE);
 
-    nm_clear_g_source(&priv->scan_kickoff_timeout_id);
+    nm_clear_g_source_inst(&priv->scan_kickoff_timeout_source);
     nm_clear_g_source_inst(&priv->scan_request_delay_source);
     nm_clear_g_cancellable(&priv->scan_request_cancellable);
 
@@ -1718,9 +1721,9 @@ _scan_kickoff_timeout_cb(gpointer user_data)
     NMDeviceWifi        *self = user_data;
     NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
 
-    priv->scan_kickoff_timeout_id = 0;
+    nm_clear_g_source_inst(&priv->scan_kickoff_timeout_source);
     _scan_kickoff(self);
-    return G_SOURCE_REMOVE;
+    return G_SOURCE_CONTINUE;
 }
 
 static void
@@ -1763,13 +1766,13 @@ _scan_kickoff(NMDeviceWifi *self)
                    / 1000),
             (int) ((priv->scan_last_request_started_at_msec + ratelimit_duration_msec - now_msec)
                    % 1000),
-            !priv->scan_kickoff_timeout_id ? ", schedule timeout" : "");
-        if (!priv->scan_kickoff_timeout_id
+            !priv->scan_kickoff_timeout_source ? ", schedule timeout" : "");
+        if (!priv->scan_kickoff_timeout_source
             && (priv->scan_explicit_allowed || priv->scan_periodic_allowed)) {
-            priv->scan_kickoff_timeout_id = g_timeout_add(priv->scan_last_request_started_at_msec
-                                                              + ratelimit_duration_msec - now_msec,
-                                                          _scan_kickoff_timeout_cb,
-                                                          self);
+            priv->scan_kickoff_timeout_source = nm_g_timeout_add_source(
+                priv->scan_last_request_started_at_msec + ratelimit_duration_msec - now_msec,
+                _scan_kickoff_timeout_cb,
+                self);
         }
         return;
     }
@@ -1786,14 +1789,16 @@ _scan_kickoff(NMDeviceWifi *self)
         _LOGT_scan("kickoff: don't scan (rate limited for another %d.%03d sec after previous scan)",
                    timeout_msec / 1000,
                    timeout_msec % 1000);
-        nm_clear_g_source(&priv->scan_kickoff_timeout_id);
-        priv->scan_kickoff_timeout_id = g_timeout_add(timeout_msec, _scan_kickoff_timeout_cb, self);
+        nm_clear_g_source_inst(&priv->scan_kickoff_timeout_source);
+        priv->scan_kickoff_timeout_source =
+            nm_g_timeout_add_source(timeout_msec, _scan_kickoff_timeout_cb, self);
         return;
     }
 
     if (priv->scan_explicit_requested) {
         if (!priv->scan_explicit_allowed) {
-            _LOGT_scan("kickoff: don't scan (explicit scan requested but not allowed)");
+            _LOGT_scan(
+                "kickoff: don't scan (explicit scan requested but not allowed at the moment)");
             return;
         }
         priv->scan_explicit_requested = FALSE;
@@ -1803,7 +1808,7 @@ _scan_kickoff(NMDeviceWifi *self)
             _LOGT_scan("kickoff: don't scan (periodic scan currently not allowed)");
             priv->scan_periodic_next_msec    = 0;
             priv->scan_periodic_interval_sec = 0;
-            nm_clear_g_source(&priv->scan_kickoff_timeout_id);
+            nm_clear_g_source_inst(&priv->scan_kickoff_timeout_source);
             return;
         }
 
@@ -1813,12 +1818,12 @@ _scan_kickoff(NMDeviceWifi *self)
             _LOGT_scan("kickoff: don't scan (periodic scan waiting for another %d.%03d sec%s)",
                        (int) ((priv->scan_periodic_next_msec - now_msec) / 1000),
                        (int) ((priv->scan_periodic_next_msec - now_msec) % 1000),
-                       !priv->scan_kickoff_timeout_id ? ", schedule timeout" : "");
-            if (!priv->scan_kickoff_timeout_id) {
-                priv->scan_kickoff_timeout_id =
-                    g_timeout_add_seconds((priv->scan_periodic_next_msec - now_msec + 999) / 1000,
-                                          _scan_kickoff_timeout_cb,
-                                          self);
+                       !priv->scan_kickoff_timeout_source ? ", schedule timeout" : "");
+            if (!priv->scan_kickoff_timeout_source) {
+                priv->scan_kickoff_timeout_source = nm_g_timeout_add_seconds_source(
+                    (priv->scan_periodic_next_msec - now_msec + 999) / 1000,
+                    _scan_kickoff_timeout_cb,
+                    self);
             }
             return;
         }
diff --git a/src/core/devices/wifi/nm-device-wifi.h b/src/core/devices/wifi/nm-device-wifi.h
index 3a85192e..17893a1b 100644
--- a/src/core/devices/wifi/nm-device-wifi.h
+++ b/src/core/devices/wifi/nm-device-wifi.h
@@ -10,7 +10,8 @@
 #include "devices/nm-device.h"
 
 #define NM_TYPE_DEVICE_WIFI (nm_device_wifi_get_type())
-#define NM_DEVICE_WIFI(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIFI, NMDeviceWifi))
+#define NM_DEVICE_WIFI(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIFI, NMDeviceWifi))
 #define NM_DEVICE_WIFI_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_WIFI, NMDeviceWifiClass))
 #define NM_IS_DEVICE_WIFI(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_WIFI))
diff --git a/src/core/devices/wifi/nm-iwd-manager.c b/src/core/devices/wifi/nm-iwd-manager.c
index 2e0d51e5..0d4daf21 100644
--- a/src/core/devices/wifi/nm-iwd-manager.c
+++ b/src/core/devices/wifi/nm-iwd-manager.c
@@ -47,13 +47,18 @@ typedef struct {
 } KnownNetworkData;
 
 typedef struct {
+    GBytes *ssid;
+    gint64  timestamp;
+} RecentlyMirroredData;
+
+typedef struct {
     NMManager          *manager;
     NMSettings         *settings;
     GCancellable       *cancellable;
     gboolean            running;
     GDBusObjectManager *object_manager;
     guint               agent_id;
-    char               *agent_path;
+    guint               netconfig_agent_id;
     GHashTable         *known_networks;
     NMDeviceIwd        *last_agent_call_device;
     char               *last_state_dir;
@@ -62,6 +67,7 @@ typedef struct {
     GHashTable         *p2p_devices;
     NMIwdWfdInfo        wfd_info;
     guint               wfd_use_count;
+    GSList             *recently_mirrored;
 } NMIwdManagerPrivate;
 
 struct _NMIwdManager {
@@ -271,6 +277,77 @@ return_error:
                                                   "Secrets not available for this connection");
 }
 
+static void
+netconfig_agent_dbus_method_cb(GDBusConnection       *connection,
+                               const char            *sender,
+                               const char            *object_path,
+                               const char            *interface_name,
+                               const char            *method_name,
+                               GVariant              *parameters,
+                               GDBusMethodInvocation *invocation,
+                               gpointer               user_data)
+{
+    NMIwdManager                           *self       = user_data;
+    NMIwdManagerPrivate                    *priv       = NM_IWD_MANAGER_GET_PRIVATE(self);
+    gs_free char                           *name_owner = NULL;
+    const char                             *device_path;
+    gs_unref_object GDBusInterface         *device_obj = NULL;
+    NMDevice                               *device;
+    nm_auto_free_variant_iter GVariantIter *config_iter = NULL;
+    const char                             *ifname;
+    int                                     addr_family;
+
+    /* Be paranoid and check the sender address */
+    name_owner = g_dbus_object_manager_client_get_name_owner(
+        G_DBUS_OBJECT_MANAGER_CLIENT(priv->object_manager));
+    if (!nm_streq0(name_owner, sender))
+        goto return_error;
+
+    if (nm_streq(method_name, "ConfigureIPv4"))
+        addr_family = AF_INET;
+    else if (nm_streq(method_name, "ConfigureIPv6"))
+        addr_family = AF_INET6;
+    else
+        goto return_error;
+
+    g_variant_get(parameters, "(&oa{sv})", &device_path, &config_iter);
+
+    device_obj = g_dbus_object_manager_get_interface(priv->object_manager,
+                                                     device_path,
+                                                     NM_IWD_DEVICE_INTERFACE);
+    if (!device_obj) {
+        _LOGE("netconfig-agent-request: unable to find the device object");
+        goto return_error;
+    }
+
+    ifname = get_property_string_or_null(G_DBUS_PROXY(device_obj), "Name");
+    if (!ifname) {
+        _LOGD("Name not cached for device at %s", device_path);
+        goto return_error;
+    }
+
+    device = nm_manager_get_device(priv->manager, ifname, NM_DEVICE_TYPE_WIFI);
+    if (!device || !NM_IS_DEVICE_IWD(device)) {
+        _LOGD("NM device %s is not an IWD-managed device", ifname);
+        goto return_error;
+    }
+
+    if (nm_device_iwd_set_netconfig(NM_DEVICE_IWD(device), addr_family, config_iter)) {
+        g_dbus_method_invocation_return_value(invocation, g_variant_new("()"));
+        return;
+    }
+
+    _LOGD("netconfig-agent-request: device %s did not handle the IWD Netconfig Agent request",
+          ifname);
+
+return_error:
+    /* IWD doesn't look at the specific error */
+    g_dbus_method_invocation_return_error_literal(invocation,
+                                                  NM_DEVICE_ERROR,
+                                                  NM_DEVICE_ERROR_INVALID_CONNECTION,
+                                                  "Couldn't set netconfig data");
+}
+
 static const GDBusInterfaceInfo iwd_agent_iface_info = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
     "net.connman.iwd.Agent",
     .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
@@ -296,36 +373,55 @@ static const GDBusInterfaceInfo iwd_agent_iface_info = NM_DEFINE_GDBUS_INTERFACE
                                     .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
                                         NM_DEFINE_GDBUS_ARG_INFO("reason", "s"), ), ), ), );
 
+static const GDBusInterfaceInfo iwd_netconfig_agent_iface_info =
+    NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        "net.connman.iwd.NetworkConfigurationAgent",
+        .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
+            NM_DEFINE_GDBUS_METHOD_INFO("ConfigureIPv4",
+                                        .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                                            NM_DEFINE_GDBUS_ARG_INFO("device", "o"),
+                                            NM_DEFINE_GDBUS_ARG_INFO("config", "a{sv}"), ), ),
+            NM_DEFINE_GDBUS_METHOD_INFO("ConfigureIPv6",
+                                        .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                                            NM_DEFINE_GDBUS_ARG_INFO("device", "o"),
+                                            NM_DEFINE_GDBUS_ARG_INFO("config", "a{sv}"), ), ), ), );
+
 static guint
-iwd_agent_export(GDBusConnection *connection, gpointer user_data, char **agent_path, GError **error)
+iwd_agent_export(GDBusConnection *connection, gpointer user_data, GError **error)
 {
     static const GDBusInterfaceVTable vtable = {
         .method_call = agent_dbus_method_cb,
     };
-    char         path[50];
-    unsigned int rnd;
-    guint        id;
 
-    nm_random_get_bytes(&rnd, sizeof(rnd));
-
-    nm_sprintf_buf(path, "/agent/%u", rnd);
+    return g_dbus_connection_register_object(
+        connection,
+        NM_IWD_AGENT_PATH,
+        NM_UNCONST_PTR(GDBusInterfaceInfo, &iwd_agent_iface_info),
+        &vtable,
+        user_data,
+        NULL,
+        error);
+}
 
-    id =
-        g_dbus_connection_register_object(connection,
-                                          path,
-                                          NM_UNCONST_PTR(GDBusInterfaceInfo, &iwd_agent_iface_info),
-                                          &vtable,
-                                          user_data,
-                                          NULL,
-                                          error);
+static guint
+iwd_netconfig_agent_export(GDBusConnection *connection, gpointer user_data, GError **error)
+{
+    static const GDBusInterfaceVTable vtable = {
+        .method_call = netconfig_agent_dbus_method_cb,
+    };
 
-    if (id)
-        *agent_path = g_strdup(path);
-    return id;
+    return g_dbus_connection_register_object(
+        connection,
+        NM_IWD_AGENT_PATH,
+        NM_UNCONST_PTR(GDBusInterfaceInfo, &iwd_netconfig_agent_iface_info),
+        &vtable,
+        user_data,
+        NULL,
+        error);
 }
 
 static void
-register_agent(NMIwdManager *self)
+register_agent(NMIwdManager *self, const char *method)
 {
     NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
     GDBusInterface      *agent_manager;
@@ -334,14 +430,14 @@ register_agent(NMIwdManager *self)
                                                         "/net/connman/iwd", /* IWD 1.0+ */
                                                         NM_IWD_AGENT_MANAGER_INTERFACE);
     if (!agent_manager) {
-        _LOGE("unable to register the IWD Agent: PSK/8021x Wi-Fi networks may not work");
+        _LOGE("unable to register the IWD Agent");
         return;
     }
 
     /* Register our agent */
     g_dbus_proxy_call(G_DBUS_PROXY(agent_manager),
-                      "RegisterAgent",
-                      g_variant_new("(o)", priv->agent_path),
+                      method,
+                      g_variant_new("(o)", NM_IWD_AGENT_PATH),
                       G_DBUS_CALL_FLAGS_NONE,
                       -1,
                       NULL,
@@ -353,6 +449,70 @@ register_agent(NMIwdManager *self)
 
 /*****************************************************************************/
 
+static void
+recently_mirrored_data_free(void *data)
+{
+    RecentlyMirroredData *rmd = data;
+
+    g_bytes_unref(rmd->ssid);
+    g_free(rmd);
+}
+
+/* When we mirror an 802.1x connection to an IWD config file, and there's an
+ * AP in range with matching SSID, that connection should become available
+ * for activation.  In IWD terms when an 802.1x network becomes a Known
+ * Network, it can be connected to using the .Connect D-Bus method.
+ *
+ * However there's a delay between writing the IWD config file and receiving
+ * the InterfaceAdded event for the Known Network so we don't immediately
+ * find out that the network can now be used.  If an NM client creates a
+ * new connection for an 802.1x AP and tries to activate it immediately,
+ * NMDeviceIWD will not allow it to because it doesn't know the network is
+ * known yet.  To work around this, we save the SSIDs of 802.1x connections
+ * we recently mirrored to IWD config files, for 2 seconds, and we treat
+ * them as Known Networks in that period since in theory activations should
+ * succeed.
+ */
+bool
+nm_iwd_manager_is_recently_mirrored(NMIwdManager *self, const GBytes *ssid)
+{
+    NMIwdManagerPrivate  *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    gint64                now  = nm_utils_get_monotonic_timestamp_nsec();
+    GSList               *iter;
+    RecentlyMirroredData *rmd;
+
+    /* Drop entries older than 2 seconds */
+    while (priv->recently_mirrored) {
+        rmd = priv->recently_mirrored->data;
+        if (now < rmd->timestamp + 2000000000)
+            break;
+
+        priv->recently_mirrored = g_slist_remove(priv->recently_mirrored, rmd);
+        recently_mirrored_data_free(rmd);
+    }
+
+    for (iter = priv->recently_mirrored; iter; iter = iter->next) {
+        rmd = iter->data;
+        if (g_bytes_equal(ssid, rmd->ssid))
+            return TRUE;
+    }
+
+    return FALSE;
+}
+
+static void
+save_mirrored(NMIwdManager *self, GBytes *ssid)
+{
+    NMIwdManagerPrivate  *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    RecentlyMirroredData *rmd  = g_malloc(sizeof(RecentlyMirroredData));
+
+    rmd->ssid               = g_bytes_ref(ssid);
+    rmd->timestamp          = nm_utils_get_monotonic_timestamp_nsec();
+    priv->recently_mirrored = g_slist_append(priv->recently_mirrored, rmd);
+}
+
+/*****************************************************************************/
+
 static KnownNetworkId *
 known_network_id_new(const char *name, NMIwdNetworkSecurity security)
 {
@@ -721,6 +881,9 @@ sett_conn_changed(NMSettingsConnection   *sett_conn,
                "iwd: changed Wi-Fi connection %s mirrored as IWD profile %s",
                nm_settings_connection_get_id(sett_conn),
                full_path);
+
+    if (security == NM_IWD_NETWORK_SECURITY_8021X)
+        save_mirrored(nm_iwd_manager_get(), ssid);
 }
 
 /* Look up an existing NMSettingsConnection for a network that has been
@@ -1283,6 +1446,7 @@ connection_added(NMSettings *settings, NMSettingsConnection *sett_conn, gpointer
     gs_free_error GError           *error      = NULL;
     nm_auto_unref_keyfile GKeyFile *iwd_config = NULL;
     NMSettingsConnectionIntFlags    flags;
+    NMIwdNetworkSecurity            security;
 
     if (!nm_streq(nm_settings_connection_get_connection_type(sett_conn), "802-11-wireless"))
         return;
@@ -1338,6 +1502,12 @@ connection_added(NMSettings *settings, NMSettingsConnection *sett_conn, gpointer
     _LOGD("New Wi-Fi connection %s mirrored as IWD profile %s",
           nm_settings_connection_get_id(sett_conn),
           full_path);
+
+    if (nm_wifi_connection_get_iwd_ssid_and_security(conn, NULL, &security)
+        && security == NM_IWD_NETWORK_SECURITY_8021X) {
+        NMSettingWireless *s_wifi = nm_connection_get_setting_wireless(conn);
+        save_mirrored(nm_iwd_manager_get(), nm_setting_wireless_get_ssid(s_wifi));
+    }
 }
 
 static gboolean
@@ -1355,26 +1525,33 @@ _om_has_name_owner(GDBusObjectManager *object_manager)
 static void
 release_object_manager(NMIwdManager *self)
 {
-    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    NMIwdManagerPrivate      *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    GDBusConnection          *agent_connection;
+    GDBusObjectManagerClient *omc;
 
     if (!priv->object_manager)
         return;
 
     g_signal_handlers_disconnect_by_data(priv->object_manager, self);
 
-    if (priv->agent_id) {
-        GDBusConnection          *agent_connection;
-        GDBusObjectManagerClient *omc = G_DBUS_OBJECT_MANAGER_CLIENT(priv->object_manager);
-
-        agent_connection = g_dbus_object_manager_client_get_connection(omc);
+    omc              = G_DBUS_OBJECT_MANAGER_CLIENT(priv->object_manager);
+    agent_connection = g_dbus_object_manager_client_get_connection(omc);
 
-        /* We're is called when we're shutting down (i.e. our DBus connection
-         * is being closed, and IWD will detect this) or IWD was stopped so
-         * in either case calling UnregisterAgent will not do anything.
-         */
+    /* We're called when we're shutting down (i.e. our DBus connection
+     * is being closed, and IWD will detect this) or IWD was stopped so
+     * in either case calling UnregisterAgent will not do anything.
+     * Just unregister the agent interfaces.  The agents are on the same
+     * object (same path) but it seems g_dbus_connection_unregister_object()
+     * should be called for each interface on the object separately.
+     */
+    if (priv->agent_id) {
         g_dbus_connection_unregister_object(agent_connection, priv->agent_id);
         priv->agent_id = 0;
-        nm_clear_g_free(&priv->agent_path);
+    }
+
+    if (priv->netconfig_agent_id) {
+        g_dbus_connection_unregister_object(agent_connection, priv->netconfig_agent_id);
+        priv->netconfig_agent_id = 0;
     }
 
     g_clear_object(&priv->object_manager);
@@ -1589,6 +1766,10 @@ next:
     }
 
     g_variant_iter_free(properties_iter);
+
+    /* Register the netconfig agent only once we know netconfig is enabled */
+    if (nm_iwd_manager_get_netconfig_enabled(self) && priv->netconfig_agent_id)
+        register_agent(self, "RegisterNetworkConfigurationAgent");
 }
 
 static void
@@ -1620,13 +1801,19 @@ got_object_manager(GObject *object, GAsyncResult *result, gpointer user_data)
     connection =
         g_dbus_object_manager_client_get_connection(G_DBUS_OBJECT_MANAGER_CLIENT(object_manager));
 
-    priv->agent_id = iwd_agent_export(connection, self, &priv->agent_path, &error);
+    priv->agent_id = iwd_agent_export(connection, self, &error);
     if (!priv->agent_id) {
         _LOGE("failed to export the IWD Agent: PSK/8021x Wi-Fi networks may not work: %s",
               error->message);
         g_clear_error(&error);
     }
 
+    priv->netconfig_agent_id = iwd_netconfig_agent_export(connection, self, &error);
+    if (!priv->netconfig_agent_id) {
+        _LOGE("failed to export the IWD Netconfig Agent: %s", error->message);
+        g_clear_error(&error);
+    }
+
     if (_om_has_name_owner(object_manager)) {
         GList                          *objects, *iter;
         gs_unref_object GDBusInterface *daemon = NULL;
@@ -1654,7 +1841,7 @@ got_object_manager(GObject *object, GAsyncResult *result, gpointer user_data)
         g_list_free_full(objects, g_object_unref);
 
         if (priv->agent_id)
-            register_agent(self);
+            register_agent(self, "RegisterAgent");
 
         priv->netconfig_enabled = false; /* Assume false until GetInfo() results come in */
 
@@ -1952,6 +2139,8 @@ dispose(GObject *object)
 
     g_hash_table_unref(nm_steal_pointer(&priv->p2p_devices));
 
+    g_slist_free_full(nm_steal_pointer(&priv->recently_mirrored), recently_mirrored_data_free);
+
     G_OBJECT_CLASS(nm_iwd_manager_parent_class)->dispose(object);
 }
 
diff --git a/src/core/devices/wifi/nm-iwd-manager.h b/src/core/devices/wifi/nm-iwd-manager.h
index 02cd6bba..80123c9a 100644
--- a/src/core/devices/wifi/nm-iwd-manager.h
+++ b/src/core/devices/wifi/nm-iwd-manager.h
@@ -30,8 +30,11 @@
 #define NM_IWD_P2P_SERVICE_MANAGER_INTERFACE "net.connman.iwd.p2p.ServiceManager"
 #define NM_IWD_P2P_WFD_INTERFACE             "net.connman.iwd.p2p.Display"
 
+#define NM_IWD_AGENT_PATH NM_DBUS_PATH "/iwd/agent"
+
 #define NM_TYPE_IWD_MANAGER (nm_iwd_manager_get_type())
-#define NM_IWD_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_IWD_MANAGER, NMIwdManager))
+#define NM_IWD_MANAGER(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_IWD_MANAGER, NMIwdManager))
 #define NM_IWD_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_IWD_MANAGER, NMIwdManagerClass))
 #define NM_IS_IWD_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_IWD_MANAGER))
@@ -63,4 +66,6 @@ gboolean nm_iwd_manager_check_wfd_info_compatible(NMIwdManager *self, const NMIw
 gboolean nm_iwd_manager_register_wfd(NMIwdManager *self, const NMIwdWfdInfo *wfd_info);
 void     nm_iwd_manager_unregister_wfd(NMIwdManager *self);
 
+bool nm_iwd_manager_is_recently_mirrored(NMIwdManager *self, const GBytes *ssid);
+
 #endif /* __NETWORKMANAGER_IWD_MANAGER_H__ */
diff --git a/src/core/devices/wifi/nm-wifi-ap.h b/src/core/devices/wifi/nm-wifi-ap.h
index c0422900..93dc8edf 100644
--- a/src/core/devices/wifi/nm-wifi-ap.h
+++ b/src/core/devices/wifi/nm-wifi-ap.h
@@ -13,7 +13,7 @@
 #include "libnm-base/nm-base.h"
 
 #define NM_TYPE_WIFI_AP            (nm_wifi_ap_get_type())
-#define NM_WIFI_AP(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_AP, NMWifiAP))
+#define NM_WIFI_AP(obj)            (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_AP, NMWifiAP))
 #define NM_WIFI_AP_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WIFI_AP, NMWifiAPClass))
 #define NM_IS_WIFI_AP(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WIFI_AP))
 #define NM_IS_WIFI_AP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_WIFI_AP))
diff --git a/src/core/devices/wifi/nm-wifi-factory.c b/src/core/devices/wifi/nm-wifi-factory.c
index 5e354c6b..19880a90 100644
--- a/src/core/devices/wifi/nm-wifi-factory.c
+++ b/src/core/devices/wifi/nm-wifi-factory.c
@@ -24,7 +24,7 @@
 
 #define NM_TYPE_WIFI_FACTORY (nm_wifi_factory_get_type())
 #define NM_WIFI_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_FACTORY, NMWifiFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_FACTORY, NMWifiFactory))
 #define NM_WIFI_FACTORY_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WIFI_FACTORY, NMWifiFactoryClass))
 #define NM_IS_WIFI_FACTORY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WIFI_FACTORY))
diff --git a/src/core/devices/wifi/nm-wifi-p2p-peer.h b/src/core/devices/wifi/nm-wifi-p2p-peer.h
index 5124d1de..b0abeb37 100644
--- a/src/core/devices/wifi/nm-wifi-p2p-peer.h
+++ b/src/core/devices/wifi/nm-wifi-p2p-peer.h
@@ -12,7 +12,7 @@
 
 #define NM_TYPE_WIFI_P2P_PEER (nm_wifi_p2p_peer_get_type())
 #define NM_WIFI_P2P_PEER(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_P2P_PEER, NMWifiP2PPeer))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_P2P_PEER, NMWifiP2PPeer))
 #define NM_WIFI_P2P_PEER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WIFI_P2P_PEER, NMWifiP2PPeerClass))
 #define NM_IS_WIFI_P2P_PEER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WIFI_P2P_PEER))
diff --git a/src/core/devices/wifi/nm-wifi-utils.c b/src/core/devices/wifi/nm-wifi-utils.c
index 1a8204b0..8a8d062f 100644
--- a/src/core/devices/wifi/nm-wifi-utils.c
+++ b/src/core/devices/wifi/nm-wifi-utils.c
@@ -11,6 +11,7 @@
 #include <netinet/if_ether.h>
 #include <stdlib.h>
 
+#include "libnm-glib-aux/nm-str-buf.h"
 #include "nm-utils.h"
 #include "libnm-core-intern/nm-core-internal.h"
 #include "libnm-core-aux-intern/nm-libnm-core-utils.h"
@@ -812,13 +813,12 @@ nm_wifi_utils_complete_connection(GBytes       *ap_ssid,
          * setting.  Since there's so much configuration required for it, there's
          * no way it can be automatically completed.
          */
-    } else if (nm_streq0(key_mgmt, "wpa-psk")
-               || (ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_SAE
-                   && (ap_wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK
-                       || ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK))) {
+    } else if (ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_SAE
+               && (ap_wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK
+                   || ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK)) {
         g_object_set(s_wsec,
                      NM_SETTING_WIRELESS_SECURITY_KEY_MGMT,
-                     "wpa-psk",
+                     nm_streq0(key_mgmt, "sae") ? "sae" : "wpa-psk",
                      NM_SETTING_WIRELESS_SECURITY_AUTH_ALG,
                      "open",
                      NULL);
@@ -828,7 +828,7 @@ nm_wifi_utils_complete_connection(GBytes       *ap_ssid,
                || NM_FLAGS_ANY(ap_rsn_flags,
                                NM_802_11_AP_SEC_KEY_MGMT_OWE | NM_802_11_AP_SEC_KEY_MGMT_OWE_TM)) {
         g_object_set(s_wsec, NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "owe", NULL);
-    } else if (ap_wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK
+    } else if (nm_streq0(key_mgmt, "wpa-psk") || ap_wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK
                || ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK) {
         g_object_set(s_wsec,
                      NM_SETTING_WIRELESS_SECURITY_KEY_MGMT,
@@ -1004,7 +1004,7 @@ psk_setting_to_iwd_config(GKeyFile *file, NMSettingWirelessSecurity *s_wsec, GEr
         if (NM_FLAGS_ANY(psk_flags, SECRETS_DONT_STORE_FLAGS)) {
             nm_log_info(
                 LOGD_WIFI,
-                "IWD network config is being created wihout the PSK but IWD will save the PSK on "
+                "IWD network config is being created without the PSK but IWD will save the PSK on "
                 "successful activation not honoring the psk-flags property");
         }
         return TRUE;
@@ -1549,10 +1549,16 @@ eap_setting_to_iwd_config(GKeyFile *file, NMSetting8021x *s_8021x, GError **erro
 }
 
 static gboolean
-ip4_config_to_iwd_config(GKeyFile *file, NMSettingIPConfig *s_ip, GError **error)
+ip_config_to_iwd_config(int addr_family, GKeyFile *file, NMSettingIPConfig *s_ip, GError **error)
 {
-    guint          num;
-    struct in_addr ip;
+    const int                IS_IPv4 = NM_IS_IPv4(addr_family);
+    nm_auto_str_buf NMStrBuf strbuf  = NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_488, FALSE);
+    NMIPAddress             *addr;
+    guint                    num;
+    guint                    i;
+    char                     buf[NM_INET_ADDRSTRLEN + 10];
+    const char              *kf_group = IS_IPv4 ? "IPv4" : "IPv6";
+    const char              *gw;
 
     /* These settings are not acutally used unless global
      * [General].EnableNetworkConfiguration is true, which we don't support.
@@ -1564,15 +1570,26 @@ ip4_config_to_iwd_config(GKeyFile *file, NMSettingIPConfig *s_ip, GError **error
     if (!s_ip)
         return TRUE;
 
+    nm_assert(NM_IS_IPv4(addr_family) ? NM_IS_SETTING_IP4_CONFIG(s_ip)
+                                      : NM_IS_SETTING_IP6_CONFIG(s_ip));
+
     num = nm_setting_ip_config_get_num_dns(s_ip);
     if (num) {
-        nm_auto_free_gstring GString *s = g_string_sized_new(128);
-        guint                         i;
-
+        nm_str_buf_reset(&strbuf);
         for (i = 0; i < num; i++) {
-            if (s->len)
-                g_string_append_c(s, ' ');
-            g_string_append(s, nm_setting_ip_config_get_dns(s_ip, i));
+            char     sbuf[NM_INET_ADDRSTRLEN];
+            NMIPAddr a;
+
+            if (!nm_utils_dnsname_parse_assert(addr_family,
+                                               nm_setting_ip_config_get_dns(s_ip, i),
+                                               NULL,
+                                               &a,
+                                               NULL))
+                continue;
+
+            if (strbuf.len > 0)
+                nm_str_buf_append_c(&strbuf, ' ');
+            nm_str_buf_append(&strbuf, nm_inet_ntop(addr_family, &a, sbuf));
         }
         /* It doesn't matter whether we add the DNS under [IPv4] or [IPv6]
          * except that with method=auto the list will override the
@@ -1581,97 +1598,61 @@ ip4_config_to_iwd_config(GKeyFile *file, NMSettingIPConfig *s_ip, GError **error
          * Note ignore-auto-dns=false isn't supported, this list always
          * overrides the DHCP DNSes.
          */
-        g_key_file_set_string(file, "IPv4", "DNS", s->str);
+        g_key_file_set_string(file, kf_group, "DNS", nm_str_buf_get_str(&strbuf));
     }
 
-    if (!nm_streq0(nm_setting_ip_config_get_method(s_ip), NM_SETTING_IP4_CONFIG_METHOD_MANUAL))
-        return TRUE;
-
-    num = nm_setting_ip_config_get_num_addresses(s_ip);
-    if (num) {
-        NMIPAddress *addr    = nm_setting_ip_config_get_address(s_ip, 0);
-        guint        prefix  = nm_ip_address_get_prefix(addr);
-        in_addr_t    netmask = _nm_utils_ip4_prefix_to_netmask(prefix);
-        char         buf[INET_ADDRSTRLEN];
-
-        nm_ip_address_get_address_binary(addr, &ip);
-        g_key_file_set_string(file, "IPv4", "Address", nm_ip_address_get_address(addr));
-        g_key_file_set_string(file, "IPv4", "Netmask", _nm_utils_inet4_ntop(netmask, buf));
-    } else {
-        inet_pton(AF_INET, "10.42.0.100", &ip);
-        g_key_file_set_string(file, "IPv4", "Address", "10.42.0.100");
+    if (!IS_IPv4) {
+        if (!NM_IN_STRSET(nm_setting_ip_config_get_method(s_ip),
+                          NM_SETTING_IP6_CONFIG_METHOD_AUTO,
+                          NM_SETTING_IP6_CONFIG_METHOD_DHCP,
+                          NM_SETTING_IP6_CONFIG_METHOD_MANUAL))
+            return TRUE;
+        g_key_file_set_boolean(file, kf_group, "Enabled", TRUE);
     }
 
-    if (nm_setting_ip_config_get_gateway(s_ip)) {
-        g_key_file_set_string(file, "IPv4", "Gateway", nm_setting_ip_config_get_gateway(s_ip));
-    } else {
-        uint32_t val;
-        char     buf[INET_ADDRSTRLEN];
-
-        /* IWD won't enable static IP unless both Address and Gateway are
-         * set so generate a gateway address if not known.
-         */
-        val = (ntohl(ip.s_addr) & 0xfffffff0) + 1;
-        if (val == ntohl(ip.s_addr))
-            val += 1;
-        g_key_file_set_string(file, "IPv4", "Gateway", _nm_utils_inet4_ntop(htonl(val), buf));
-    }
-
-    return TRUE;
-}
-
-static gboolean
-ip6_config_to_iwd_config(GKeyFile *file, NMSettingIPConfig *s_ip, GError **error)
-{
-    guint        num;
-    NMIPAddress *addr;
-    char         buf[INET6_ADDRSTRLEN + 10];
-
-    if (!s_ip)
+    num = nm_setting_ip_config_get_num_addresses(s_ip);
+    if (num == 0)
         return TRUE;
 
-    num = nm_setting_ip_config_get_num_dns(s_ip);
-    if (num) {
-        nm_auto_free_gstring GString *s = g_string_sized_new(128);
-        guint                         i;
+    addr = nm_setting_ip_config_get_address(s_ip, 0);
+    gw   = nm_setting_ip_config_get_gateway(s_ip);
 
-        for (i = 0; i < num; i++) {
-            if (s->len)
-                g_string_append_c(s, ' ');
-            g_string_append(s, nm_setting_ip_config_get_dns(s_ip, i));
-        }
-        g_key_file_set_string(file, "IPv6", "DNS", s->str);
-    }
+    if (IS_IPv4) {
+        in_addr_t ip;
 
-    if (!NM_IN_STRSET(nm_setting_ip_config_get_method(s_ip),
-                      NM_SETTING_IP6_CONFIG_METHOD_AUTO,
-                      NM_SETTING_IP6_CONFIG_METHOD_DHCP,
-                      NM_SETTING_IP6_CONFIG_METHOD_MANUAL))
-        return TRUE;
+        nm_ip_address_get_address_binary(addr, &ip);
 
-    g_key_file_set_boolean(file, "IPv6", "Enabled", TRUE);
+        g_key_file_set_string(file, kf_group, "Address", nm_ip_address_get_address(addr));
+        g_key_file_set_string(
+            file,
+            kf_group,
+            "Netmask",
+            nm_inet4_ntop(nm_ip4_addr_netmask_from_prefix(nm_ip_address_get_prefix(addr)), buf));
 
-    if (!nm_streq0(nm_setting_ip_config_get_method(s_ip), NM_SETTING_IP6_CONFIG_METHOD_MANUAL))
-        return TRUE;
+        if (!gw) {
+            guint32 val;
 
-    if (!nm_setting_ip_config_get_num_addresses(s_ip)) {
-        g_set_error_literal(error,
-                            NM_CONNECTION_ERROR,
-                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
-                            "IP address required for IPv6 manual config");
-        return FALSE;
+            /* IWD won't enable static IP unless both Address and Gateway are
+             * set so generate a gateway address if not known.
+             */
+            val = (ntohl(ip) & 0xfffffff0) + 1;
+            if (val == ntohl(ip))
+                val += 1;
+            gw = nm_inet4_ntop(htonl(val), buf);
+        }
+        g_key_file_set_string(file, kf_group, "Gateway", gw);
+    } else {
+        g_key_file_set_string(file,
+                              kf_group,
+                              "Address",
+                              nm_sprintf_buf(buf,
+                                             "%s/%u",
+                                             nm_ip_address_get_address(addr),
+                                             nm_ip_address_get_prefix(addr)));
+        if (gw)
+            g_key_file_set_string(file, kf_group, "Gateway", gw);
     }
 
-    addr = nm_setting_ip_config_get_address(s_ip, 0);
-    g_key_file_set_string(file,
-                          "IPv6",
-                          "Address",
-                          nm_sprintf_buf(buf,
-                                         "%s/%u",
-                                         nm_ip_address_get_address(addr),
-                                         nm_ip_address_get_prefix(addr)));
-    if (nm_setting_ip_config_get_gateway(s_ip))
-        g_key_file_set_string(file, "IPv6", "Gateway", nm_setting_ip_config_get_gateway(s_ip));
     return TRUE;
 }
 
@@ -1759,13 +1740,15 @@ nm_wifi_utils_connection_to_iwd_config(NMConnection *connection,
     else if (cloned_mac_addr && nm_utils_hwaddr_valid(cloned_mac_addr, ETH_ALEN))
         g_key_file_set_string(file, "Settings", "AddressOverride", cloned_mac_addr);
 
-    if (!ip4_config_to_iwd_config(
+    if (!ip_config_to_iwd_config(
+            AF_INET,
             file,
             NM_SETTING_IP_CONFIG(nm_connection_get_setting_ip4_config(connection)),
             error))
         return NULL;
 
-    if (!ip6_config_to_iwd_config(
+    if (!ip_config_to_iwd_config(
+            AF_INET6,
             file,
             NM_SETTING_IP_CONFIG(nm_connection_get_setting_ip6_config(connection)),
             error))
diff --git a/src/core/devices/wwan/nm-device-modem.c b/src/core/devices/wwan/nm-device-modem.c
index e0209aff..b83120f9 100644
--- a/src/core/devices/wwan/nm-device-modem.c
+++ b/src/core/devices/wwan/nm-device-modem.c
@@ -273,6 +273,15 @@ modem_state_cb(NMModem *modem, int new_state_i, int old_state_i, gpointer user_d
         /* Called when the ModemManager modem enabled state is changed externally
          * to NetworkManager (eg something using MM's D-Bus API directly).
          */
+
+        if (!NM_MODEM_GET_CLASS(priv->modem)->set_mm_enabled) {
+            /* We cannot re-enable this modem, thus device becomes unavailable. */
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_UNAVAILABLE,
+                                    NM_DEVICE_STATE_REASON_USER_REQUESTED);
+            return;
+        }
+
         if (nm_device_is_activating(device) || dev_state == NM_DEVICE_STATE_ACTIVATED) {
             /* user-initiated action, hence DISCONNECTED not FAILED */
             nm_device_state_changed(device,
@@ -435,6 +444,13 @@ check_connection_available(NMDevice                      *device,
         return FALSE;
     }
 
+    if (!NM_MODEM_GET_CLASS(priv->modem)->set_mm_enabled && state <= NM_MODEM_STATE_DISABLING) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "modem is disabled and NM cannot enable it");
+        return FALSE;
+    }
+
     if (state == NM_MODEM_STATE_LOCKED) {
         if (!nm_connection_get_setting_gsm(connection)) {
             nm_utils_error_set_literal(error,
@@ -599,6 +615,11 @@ set_enabled(NMDevice *device, gboolean enabled)
 
     if (enabled == FALSE) {
         nm_device_state_changed(device, NM_DEVICE_STATE_UNAVAILABLE, NM_DEVICE_STATE_REASON_NONE);
+    } else {
+        /* It's possible that the modem is enabled outside of NM. Need to recheck. */
+        nm_device_queue_recheck_available(device,
+                                          NM_DEVICE_STATE_REASON_MODEM_AVAILABLE,
+                                          NM_DEVICE_STATE_REASON_MODEM_FAILED);
     }
 }
 
@@ -617,6 +638,9 @@ is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
     if (modem_state <= NM_MODEM_STATE_INITIALIZING)
         return FALSE;
 
+    if (!NM_MODEM_GET_CLASS(priv->modem)->set_mm_enabled && modem_state <= NM_MODEM_STATE_DISABLING)
+        return FALSE;
+
     return TRUE;
 }
 
diff --git a/src/core/devices/wwan/nm-device-modem.h b/src/core/devices/wwan/nm-device-modem.h
index f171d76f..317a5bde 100644
--- a/src/core/devices/wwan/nm-device-modem.h
+++ b/src/core/devices/wwan/nm-device-modem.h
@@ -11,7 +11,7 @@
 
 #define NM_TYPE_DEVICE_MODEM (nm_device_modem_get_type())
 #define NM_DEVICE_MODEM(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_MODEM, NMDeviceModem))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_MODEM, NMDeviceModem))
 #define NM_DEVICE_MODEM_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_MODEM, NMDeviceModemClass))
 #define NM_IS_DEVICE_MODEM(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_MODEM))
diff --git a/src/core/devices/wwan/nm-modem-broadband.c b/src/core/devices/wwan/nm-modem-broadband.c
index db4fb29f..f0907c46 100644
--- a/src/core/devices/wwan/nm-modem-broadband.c
+++ b/src/core/devices/wwan/nm-modem-broadband.c
@@ -636,7 +636,7 @@ connect_context_step(NMModemBroadband *self)
         if (ctx->ip_types_i < ctx->ip_types->len) {
             NMModemIPType current;
 
-            current = g_array_index(ctx->ip_types, NMModemIPType, ctx->ip_types_i);
+            current = nm_g_array_index(ctx->ip_types, NMModemIPType, ctx->ip_types_i);
 
             if (current == NM_MODEM_IP_TYPE_IPV4)
                 mm_simple_connect_properties_set_ip_type(ctx->connect_properties,
@@ -993,7 +993,7 @@ stage3_ip_config_start(NMModem *modem, int addr_family, NMModemIPMethod ip_metho
         /* Fully fail if invalid IP address retrieved */
         address_string = mm_bearer_ip_config_get_address(self->_priv.ipv4_config);
         if (!address_string
-            || !nm_utils_parse_inaddr_bin(AF_INET, address_string, NULL, &address_network)) {
+            || !nm_inet_parse_bin(AF_INET, address_string, NULL, &address_network)) {
             g_set_error(&error,
                         NM_DEVICE_ERROR,
                         NM_DEVICE_ERROR_INVALID_CONNECTION,
@@ -1005,7 +1005,7 @@ stage3_ip_config_start(NMModem *modem, int addr_family, NMModemIPMethod ip_metho
 
         /* Missing gateway not a hard failure */
         gw_string = mm_bearer_ip_config_get_gateway(self->_priv.ipv4_config);
-        if (gw_string && !nm_utils_parse_inaddr_bin(AF_INET, gw_string, NULL, &gw)) {
+        if (gw_string && !nm_inet_parse_bin(AF_INET, gw_string, NULL, &gw)) {
             g_set_error(&error,
                         NM_DEVICE_ERROR,
                         NM_DEVICE_ERROR_INVALID_CONNECTION,
@@ -1057,9 +1057,8 @@ stage3_ip_config_start(NMModem *modem, int addr_family, NMModemIPMethod ip_metho
 
         dns = mm_bearer_ip_config_get_dns(self->_priv.ipv4_config);
         for (i = 0; dns && dns[i]; i++) {
-            if (nm_utils_parse_inaddr_bin(AF_INET, dns[i], NULL, &address_network)
-                && address_network > 0) {
-                nm_l3_config_data_add_nameserver(l3cd, AF_INET, &address_network);
+            if (nm_inet_parse_bin(AF_INET, dns[i], NULL, &address_network) && address_network > 0) {
+                nm_l3_config_data_add_nameserver_detail(l3cd, AF_INET, &address_network, NULL);
                 _LOGI("  DNS %s", dns[i]);
             }
         }
@@ -1174,7 +1173,7 @@ stage3_ip_config_start(NMModem *modem, int addr_family, NMModemIPMethod ip_metho
             struct in6_addr addr;
 
             if (inet_pton(AF_INET6, dns[i], &addr)) {
-                nm_l3_config_data_add_nameserver(l3cd, AF_INET6, &addr);
+                nm_l3_config_data_add_nameserver_detail(l3cd, AF_INET6, &addr, NULL);
                 _LOGI("  DNS %s", dns[i]);
             }
         }
diff --git a/src/core/devices/wwan/nm-modem-broadband.h b/src/core/devices/wwan/nm-modem-broadband.h
index 627fe25a..e4467310 100644
--- a/src/core/devices/wwan/nm-modem-broadband.h
+++ b/src/core/devices/wwan/nm-modem-broadband.h
@@ -10,7 +10,7 @@
 
 #define NM_TYPE_MODEM_BROADBAND (nm_modem_broadband_get_type())
 #define NM_MODEM_BROADBAND(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM_BROADBAND, NMModemBroadband))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM_BROADBAND, NMModemBroadband))
 #define NM_MODEM_BROADBAND_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM_BROADBAND, NMModemBroadbandClass))
 #define NM_IS_MODEM_BROADBAND(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM_BROADBAND))
diff --git a/src/core/devices/wwan/nm-modem-manager.h b/src/core/devices/wwan/nm-modem-manager.h
index e89a032b..a9339ffc 100644
--- a/src/core/devices/wwan/nm-modem-manager.h
+++ b/src/core/devices/wwan/nm-modem-manager.h
@@ -12,7 +12,7 @@
 
 #define NM_TYPE_MODEM_MANAGER (nm_modem_manager_get_type())
 #define NM_MODEM_MANAGER(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM_MANAGER, NMModemManager))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM_MANAGER, NMModemManager))
 #define NM_MODEM_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM_MANAGER, NMModemManagerClass))
 #define NM_IS_MODEM_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM_MANAGER))
diff --git a/src/core/devices/wwan/nm-modem-ofono.c b/src/core/devices/wwan/nm-modem-ofono.c
index fc27c3fd..b3745621 100644
--- a/src/core/devices/wwan/nm-modem-ofono.c
+++ b/src/core/devices/wwan/nm-modem-ofono.c
@@ -7,8 +7,12 @@
 
 #include "nm-modem-ofono.h"
 
+#include "libnm-glib-aux/nm-dbus-aux.h"
 #include "libnm-core-intern/nm-core-internal.h"
+#include "libnm-glib-aux/nm-uuid.h"
 #include "devices/nm-device-private.h"
+#include "nm-setting-gsm.h"
+#include "settings/nm-settings.h"
 #include "nm-modem.h"
 #include "libnm-platform/nm-platform.h"
 #include "nm-l3-config-data.h"
@@ -27,27 +31,44 @@
 /*****************************************************************************/
 
 typedef struct {
+    NMModemOfono *self;
+    char         *name;
+    char         *type;
+    gboolean      preferred;
+    GDBusProxy   *proxy;
+} OfonoContextData;
+
+typedef struct {
     GHashTable *connect_properties;
+    GHashTable *connections;
+    GHashTable *contexts;
 
     GDBusProxy *modem_proxy;
     GDBusProxy *connman_proxy;
-    GDBusProxy *context_proxy;
     GDBusProxy *sim_proxy;
 
     GCancellable *modem_proxy_cancellable;
     GCancellable *connman_proxy_cancellable;
-    GCancellable *context_proxy_cancellable;
+    GCancellable *connect_cancellable;
     GCancellable *sim_proxy_cancellable;
 
     GError *property_error;
 
-    char *context_path;
     char *imsi;
 
     gboolean modem_online;
+    gboolean connman_powered;
     gboolean gprs_attached;
 
     NML3ConfigData *l3cd_4;
+    NMSettings     *settings;
+
+    guint n_context_proxy_pending;
+
+    /* unowned; reference held by 'contexts' above. */
+    OfonoContextData *current_octx;
+
+    GSource *deferred_connection_timeout_source;
 } NMModemOfonoPrivate;
 
 struct _NMModemOfono {
@@ -96,6 +117,21 @@ G_DEFINE_TYPE(NMModemOfono, nm_modem_ofono, NM_TYPE_MODEM)
 
 /*****************************************************************************/
 
+/*
+ * Deterministic UUID is used to pair imsi+context with exported connection
+ * (via a pair of hash tables).
+ */
+
+static char *
+_generate_uuid(const char *imsi, const char *object_path)
+{
+    return nm_uuid_generate_from_strings(
+        NM_UUID_TYPE_VERSION5,
+        &NM_UUID_INIT(b5, 6b, ad, f5, ef, 9d, 4a, 21, a8, e7, 7d, db, 69, bb, 6b, ee),
+        imsi,
+        object_path);
+}
+
 static void
 get_capabilities(NMModem                   *_self,
                  NMDeviceModemCapabilities *modem_caps,
@@ -106,6 +142,8 @@ get_capabilities(NMModem                   *_self,
     *current_caps = NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS;
 }
 
+static void do_context_activate(NMModemOfono *self);
+
 static void
 update_modem_state(NMModemOfono *self)
 {
@@ -114,13 +152,16 @@ update_modem_state(NMModemOfono *self)
     NMModemState         new_state = NM_MODEM_STATE_DISABLED;
     const char          *reason    = NULL;
 
-    _LOGI("'Attached': %s 'Online': %s 'IMSI': %s",
+    _LOGI("'Attached': %s 'Online': %s 'Powered': %s 'IMSI': %s",
           priv->gprs_attached ? "true" : "false",
           priv->modem_online ? "true" : "false",
+          priv->connman_powered ? "true" : "false",
           priv->imsi);
 
     if (priv->modem_online == FALSE) {
         reason = "modem 'Online=false'";
+    } else if (priv->connman_powered == FALSE) {
+        reason = "ConnectionManager 'Powered=false'";
     } else if (priv->imsi == NULL && state != NM_MODEM_STATE_ENABLING) {
         reason = "modem not ready";
     } else if (priv->gprs_attached == FALSE) {
@@ -131,8 +172,27 @@ update_modem_state(NMModemOfono *self)
         reason    = "modem ready";
     }
 
-    if (state != new_state)
+    if (state != new_state) {
+        if (new_state == NM_MODEM_STATE_DISABLED && priv->deferred_connection_timeout_source) {
+            /*
+             * Do this before set_state(), because we want the final device state
+             * to be "unavailable", not "failed" or "disconnected".
+             */
+            _LOGI("canceling deferred context activation");
+
+            nm_clear_g_source_inst(&priv->deferred_connection_timeout_source);
+            nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_MODEM_BUSY);
+        }
+
         nm_modem_set_state(NM_MODEM(self), new_state, reason);
+
+        if (new_state == NM_MODEM_STATE_REGISTERED && priv->deferred_connection_timeout_source) {
+            _LOGI("resuming deferred context activation");
+
+            nm_clear_g_source_inst(&priv->deferred_connection_timeout_source);
+            do_context_activate(self);
+        }
+    }
 }
 
 /* Disconnect */
@@ -213,7 +273,8 @@ disconnect(NMModem                   *modem,
     ctx->callback           = callback;
     ctx->callback_user_data = user_data;
 
-    if (state != NM_MODEM_STATE_CONNECTED || g_cancellable_is_cancelled(cancellable)) {
+    if (state != NM_MODEM_STATE_CONNECTED || g_cancellable_is_cancelled(cancellable)
+        || priv->current_octx == NULL) {
         nm_utils_invoke_on_idle(cancellable, disconnect_context_complete_on_idle, ctx);
         return;
     }
@@ -222,7 +283,7 @@ disconnect(NMModem                   *modem,
                        NM_MODEM_STATE_DISCONNECTING,
                        nm_modem_state_to_string(NM_MODEM_STATE_DISCONNECTING));
 
-    g_dbus_proxy_call(priv->context_proxy,
+    g_dbus_proxy_call(priv->current_octx->proxy,
                       "SetProperty",
                       g_variant_new("(sv)", "Active", g_variant_new("b", warn)),
                       G_DBUS_CALL_FLAGS_NONE,
@@ -230,6 +291,8 @@ disconnect(NMModem                   *modem,
                       ctx->cancellable,
                       disconnect_done,
                       ctx);
+
+    priv->current_octx = NULL;
 }
 
 static void
@@ -251,7 +314,7 @@ check_connection_compatible_with_modem(NMModem *modem, NMConnection *connection,
 {
     NMModemOfono        *self = NM_MODEM_OFONO(modem);
     NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
-    const char          *id;
+    const char          *uuid;
 
     if (!_nm_connection_check_main_setting(connection, NM_SETTING_GSM_SETTING_NAME, NULL)) {
         nm_utils_error_set(error,
@@ -268,19 +331,12 @@ check_connection_compatible_with_modem(NMModem *modem, NMConnection *connection,
         return FALSE;
     }
 
-    id = nm_connection_get_id(connection);
-
-    if (!strstr(id, "/context")) {
-        nm_utils_error_set_literal(error,
-                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
-                                   "the connection ID has no context");
-        return FALSE;
-    }
+    uuid = nm_connection_get_uuid(connection);
 
-    if (!strstr(id, priv->imsi)) {
+    if (!g_hash_table_contains(priv->contexts, uuid)) {
         nm_utils_error_set_literal(error,
                                    NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
-                                   "the connection ID does not contain the IMSI");
+                                   "connection ID does not match known contexts");
         return FALSE;
     }
 
@@ -398,11 +454,11 @@ _sim_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
     priv->sim_proxy = proxy;
 
     /* Watch for custom ofono PropertyChanged signals */
-    _nm_dbus_signal_connect(priv->sim_proxy,
-                            "PropertyChanged",
-                            G_VARIANT_TYPE("(sv)"),
-                            G_CALLBACK(sim_property_changed),
-                            self);
+    _nm_dbus_proxy_signal_connect(priv->sim_proxy,
+                                  "PropertyChanged",
+                                  G_VARIANT_TYPE("(sv)"),
+                                  G_CALLBACK(sim_property_changed),
+                                  self);
 
     g_dbus_proxy_call(priv->sim_proxy,
                       "GetProperties",
@@ -469,6 +525,19 @@ handle_connman_property(GDBusProxy *proxy, const char *property, GVariant *v, gp
 
             update_modem_state(self);
         }
+    } else if (nm_streq(property, "Powered") && VARIANT_IS_OF_TYPE_BOOLEAN(v)) {
+        gboolean powered     = g_variant_get_boolean(v);
+        gboolean old_powered = priv->connman_powered;
+
+        _LOGD("Powered: %s", powered ? "True" : "False");
+
+        if (old_powered != powered) {
+            priv->connman_powered = powered;
+
+            _LOGI("Powered %s -> %s", old_powered ? "true" : "false", powered ? "true" : "false");
+
+            update_modem_state(self);
+        }
     }
 }
 
@@ -484,7 +553,6 @@ static void
 connman_get_properties_done(GObject *source, GAsyncResult *result, gpointer user_data)
 {
     NMModemOfono              *self;
-    NMModemOfonoPrivate       *priv;
     gs_free_error GError      *error        = NULL;
     gs_unref_variant GVariant *v_properties = NULL;
     gs_unref_variant GVariant *v_dict       = NULL;
@@ -498,9 +566,6 @@ connman_get_properties_done(GObject *source, GAsyncResult *result, gpointer user
         return;
 
     self = NM_MODEM_OFONO(user_data);
-    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
-
-    g_clear_object(&priv->connman_proxy_cancellable);
 
     if (!v_properties) {
         g_dbus_error_strip_remote_error(error);
@@ -526,6 +591,366 @@ connman_get_properties_done(GObject *source, GAsyncResult *result, gpointer user
 }
 
 static void
+ofono_context_data_free(OfonoContextData *octx)
+{
+    g_free(octx->name);
+    g_free(octx->type);
+
+    if (octx->proxy) {
+        g_signal_handlers_disconnect_by_data(octx->proxy, octx);
+        g_object_unref(octx->proxy);
+    }
+
+    g_slice_free(OfonoContextData, octx);
+}
+
+static void
+add_or_update_connection(NMModemOfono *self, const char *context_name, const char *uuid)
+{
+    NMModemOfonoPrivate          *priv       = NM_MODEM_OFONO_GET_PRIVATE(self);
+    gs_unref_object NMConnection *connection = NULL;
+    NMSetting                    *setting;
+    NMSettingsConnection         *sett_conn;
+    gs_free_error GError         *error = NULL;
+
+    /*
+     * See first if we have an existing connection (from previous or current
+     * run of NM) that we can update in-place.
+     */
+
+    sett_conn = nm_settings_get_connection_by_uuid(priv->settings, uuid);
+    if (sett_conn
+        && !NM_FLAGS_HAS(nm_settings_connection_get_flags(sett_conn),
+                         NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED)) {
+        /*
+         * Either we have a coliding connection, or our connection has been
+         * modified by user. For the latter case it's safe to leave it alone;
+         * the UUID will still let it connect. However, for the first case,
+         * we can't really do anything about it unless we re-write the way we
+         * track connection <-> context, which I (Ratchanan) don't want to do
+         * right now...
+         */
+        return;
+    }
+
+    connection = nm_simple_connection_new();
+    setting    = nm_setting_connection_new();
+    g_object_set(setting,
+                 NM_SETTING_CONNECTION_ID,
+                 context_name,
+                 NM_SETTING_CONNECTION_UUID,
+                 uuid,
+                 NM_SETTING_CONNECTION_AUTOCONNECT,
+                 TRUE,
+                 NM_SETTING_CONNECTION_TYPE,
+                 NM_SETTING_GSM_SETTING_NAME,
+                 NULL);
+    nm_connection_add_setting(connection, setting);
+
+    setting = nm_setting_gsm_new();
+
+    /*
+     * oFono should already know how to handle placing the call, but NM
+     * insists on having a number. Pass the usual *99#.
+     */
+    g_object_set(setting, NM_SETTING_GSM_NUMBER, "*99#", NULL);
+    nm_connection_add_setting(connection, setting);
+
+    if (!nm_connection_normalize(connection, NULL, NULL, &error)) {
+        nm_log_err(LOGD_MB,
+                   "ofono: could not not generate a connection for %s: %s",
+                   context_name,
+                   error->message);
+        return;
+    }
+
+    if (!sett_conn) {
+        nm_settings_add_connection(priv->settings,
+                                   NULL, /* plugin */
+                                   connection,
+                                   NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY,
+                                   NM_SETTINGS_CONNECTION_ADD_REASON_NONE,
+                                   NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED,
+                                   &sett_conn,
+                                   &error);
+    } else {
+        nm_settings_update_connection(priv->settings,
+                                      sett_conn,
+                                      NULL, /* plugin_name */
+                                      connection,
+                                      NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY,
+                                      NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED,
+                                      NM_SETTINGS_CONNECTION_INT_FLAGS_NONE,
+                                      NM_SETTINGS_CONNECTION_UPDATE_REASON_NONE,
+                                      /* log_context_name */ "ofono",
+                                      &error);
+    }
+
+    if (!sett_conn) {
+        nm_log_warn(LOGD_MB,
+                    "ofono: could not add or update new connection for '%s' (%s): %s",
+                    context_name,
+                    uuid,
+                    error->message);
+        return;
+    }
+
+    g_hash_table_insert(priv->connections, g_strdup(uuid), g_object_ref(sett_conn));
+}
+
+/*
+ * Used when we decide a connection is not needed. Rationale being that the
+ * connection could have been modified by user since it's created by us,
+ * after which we don't want to delete, but we don't want to track it either.
+ */
+
+static void
+untrack_connection_and_delete_if_generated(NMModemOfono *self, char const *uuid)
+{
+    NMModemOfonoPrivate  *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    NMSettingsConnection *sett_conn;
+
+    sett_conn = g_hash_table_lookup(priv->connections, uuid);
+    if (!sett_conn)
+        return;
+
+    if (NM_FLAGS_HAS(nm_settings_connection_get_flags(sett_conn),
+                     NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED))
+        nm_settings_connection_delete(sett_conn, FALSE);
+
+    g_hash_table_remove(priv->connections, uuid);
+}
+
+static void
+update_connection_list(NMModemOfono *self)
+{
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    GHashTableIter       iter;
+    char                *uuid;
+    OfonoContextData    *octx;
+    OfonoContextData    *octx_preferred = NULL;
+
+    _LOGI("(re-)checking which context needs a connection");
+
+    g_hash_table_iter_init(&iter, priv->contexts);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &uuid, (gpointer *) &octx)) {
+        if (octx->preferred) {
+            octx_preferred = octx;
+            break;
+        }
+    }
+
+    g_hash_table_iter_init(&iter, priv->contexts);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &uuid, (gpointer *) &octx)) {
+        gboolean connection_should_exist =
+            (!octx_preferred || octx_preferred == octx)
+            && (nm_streq(octx->type, "internet") || nm_streq(octx->type, "internet+mms"));
+        gboolean connection_exists = g_hash_table_contains(priv->connections, uuid);
+
+        if (connection_should_exist && !connection_exists) {
+            _LOGI("creating connection for %s%s",
+                  octx_preferred ? "preferred context " : "",
+                  g_dbus_proxy_get_object_path(octx->proxy));
+            add_or_update_connection(self, octx->name, uuid);
+        } else if (!connection_should_exist && connection_exists) {
+            _LOGI("removing connection for %s", g_dbus_proxy_get_object_path(octx->proxy));
+            untrack_connection_and_delete_if_generated(self, uuid);
+
+            /* priv->current_octx is deliberately not cleared here because the
+             * disconnection chain happens in another main loop iteration.
+             */
+        }
+    }
+}
+
+static void handle_settings(NMModemOfono *self, GVariant *v_dict);
+
+static void
+context_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    OfonoContextData          *octx    = user_data;
+    NMModemOfono              *self    = octx->self;
+    NMModemOfonoPrivate       *priv    = NM_MODEM_OFONO_GET_PRIVATE(self);
+    gs_unref_variant GVariant *v_inner = g_variant_get_child_value(v, 0);
+
+    if (!v_inner) {
+        _LOGW("ofono: (%s): error handling PropertyChanged signal",
+              nm_modem_get_uid(NM_MODEM(self)));
+        return;
+    }
+
+    if (nm_streq(property, "Name")) {
+        gs_free char *uuid = NULL;
+
+        g_return_if_fail(g_variant_is_of_type(v_inner, G_VARIANT_TYPE_STRING));
+        g_free(octx->name);
+        octx->name = g_variant_dup_string(v_inner, /* &length */ NULL);
+
+        uuid = _generate_uuid(priv->imsi, g_dbus_proxy_get_object_path(proxy));
+        if (g_hash_table_contains(priv->connections, uuid))
+            add_or_update_connection(self, octx->name, uuid);
+    } else if (nm_streq(property, "Type")) {
+        g_return_if_fail(g_variant_is_of_type(v_inner, G_VARIANT_TYPE_STRING));
+        g_free(octx->type);
+        octx->type = g_variant_dup_string(v_inner, /* &length */ NULL);
+
+        update_connection_list(self);
+    } else if (nm_streq(property, "Preferred")) {
+        g_return_if_fail(g_variant_is_of_type(v_inner, G_VARIANT_TYPE_BOOLEAN));
+        octx->preferred = g_variant_get_boolean(v_inner);
+
+        update_connection_list(self);
+    } else if (nm_streq(property, "Settings") && priv->current_octx == octx) {
+        g_return_if_fail(g_variant_is_of_type(v_inner, G_VARIANT_TYPE_VARDICT));
+        handle_settings(self, v_inner);
+    }
+}
+
+static void
+_context_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    OfonoContextData     *octx = user_data;
+    NMModemOfono         *self;
+    NMModemOfonoPrivate  *priv;
+    gs_free_error GError *error = NULL;
+    GDBusProxy           *proxy;
+    char                 *uuid;
+
+    proxy = g_dbus_proxy_new_for_bus_finish(result, &error);
+    if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+        ofono_context_data_free(octx);
+        return;
+    }
+
+    self = octx->self;
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if (!proxy) {
+        _LOGW("failed to create ConnectionContext proxy: %s", error->message);
+        ofono_context_data_free(octx);
+        return;
+    }
+
+    _LOGD("recieved proxy for %s", g_dbus_proxy_get_object_path(proxy));
+    octx->proxy = proxy;
+
+    _nm_dbus_proxy_signal_connect(proxy,
+                                  "PropertyChanged",
+                                  G_VARIANT_TYPE("(sv)"),
+                                  G_CALLBACK(context_property_changed),
+                                  octx);
+
+    uuid = _generate_uuid(priv->imsi, g_dbus_proxy_get_object_path(proxy));
+    g_hash_table_insert(priv->contexts, uuid, octx);
+    priv->n_context_proxy_pending--;
+
+    if (priv->n_context_proxy_pending == 0)
+        update_connection_list(self);
+}
+
+static void
+connman_context_removed(GDBusProxy *proxy, const char *object_path, gpointer user_data)
+{
+    NMModemOfono        *self = NM_MODEM_OFONO(user_data);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    gs_free char        *uuid = NULL;
+    OfonoContextData    *octx;
+
+    /* look up the connction, and if we have connection disconnect and remove it */
+    uuid = _generate_uuid(priv->imsi, object_path);
+    untrack_connection_and_delete_if_generated(self, uuid);
+
+    octx = g_hash_table_lookup(priv->contexts, uuid);
+    if (octx) {
+        gboolean preferred = octx->preferred;
+
+        if (octx == priv->current_octx)
+            priv->current_octx = NULL;
+
+        g_hash_table_remove(priv->contexts, uuid);
+
+        if (preferred)
+            update_connection_list(self);
+    }
+}
+
+static void
+connman_context_added(GDBusProxy *proxy, const char *object_path, GVariant *v, gpointer user_data)
+{
+    NMModemOfono              *self = user_data;
+    NMModemOfonoPrivate       *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    OfonoContextData          *octx;
+    gs_unref_variant GVariant *v_context_type      = NULL;
+    gs_unref_variant GVariant *v_context_name      = NULL;
+    gs_unref_variant GVariant *v_context_preferred = NULL;
+
+    nm_log_info(LOGD_MB, "ofono: processing context %s", object_path);
+
+    v_context_name = g_variant_lookup_value(v, "Name", G_VARIANT_TYPE_STRING);
+    v_context_type = g_variant_lookup_value(v, "Type", G_VARIANT_TYPE_STRING);
+    if (!v_context_name || !v_context_type) {
+        nm_log_err(LOGD_MB, "ofono: context dictionary is missing required key(s).");
+        return;
+    }
+
+    /* Preferred property exists in some oFono fork only (mostly Ubuntu Touch's).  */
+    v_context_preferred = g_variant_lookup_value(v, "Preferred", G_VARIANT_TYPE_BOOLEAN);
+
+    octx            = g_slice_new0(OfonoContextData);
+    octx->self      = self;
+    octx->name      = g_variant_dup_string(v_context_name, NULL);
+    octx->type      = g_variant_dup_string(v_context_type, NULL);
+    octx->preferred = v_context_preferred && g_variant_get_boolean(v_context_preferred);
+
+    priv->n_context_proxy_pending++;
+    g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
+                             G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES
+                                 | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+                             NULL, /* GDBusInterfaceInfo */
+                             OFONO_DBUS_SERVICE,
+                             object_path,
+                             OFONO_DBUS_INTERFACE_CONNECTION_CONTEXT,
+                             priv->connman_proxy_cancellable,
+                             _context_proxy_new_cb,
+                             octx);
+}
+
+static void
+connman_get_contexts_done(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono              *self;
+    gs_free_error GError      *error      = NULL;
+    gs_unref_variant GVariant *v_contexts = NULL;
+    gs_unref_variant GVariant *v_objects  = NULL;
+    gs_unref_variant GVariant *v          = NULL;
+    GVariantIter               i;
+    const char                *object_path;
+
+    v_contexts = _nm_dbus_proxy_call_finish(G_DBUS_PROXY(source),
+                                            result,
+                                            G_VARIANT_TYPE("(a(oa{sv}))"),
+                                            &error);
+    if (!v_contexts && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = user_data;
+
+    if (!v_contexts) {
+        g_dbus_error_strip_remote_error(error);
+        _LOGW("Error getting list of contexts: %s", error->message);
+        return;
+    }
+
+    nm_log_info(LOGD_MB, "ofono: printing %s", g_variant_get_type_string(v_contexts));
+
+    v_objects = g_variant_get_child_value(v_contexts, 0);
+
+    g_variant_iter_init(&i, v_objects);
+    while (g_variant_iter_loop(&i, "(&o@a{sv})", &object_path, &v))
+        connman_context_added(NULL, object_path, v, self);
+}
+
+static void
 _connman_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 {
     NMModemOfono         *self;
@@ -548,11 +973,23 @@ _connman_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 
     priv->connman_proxy = proxy;
 
-    _nm_dbus_signal_connect(priv->connman_proxy,
-                            "PropertyChanged",
-                            G_VARIANT_TYPE("(sv)"),
-                            G_CALLBACK(connman_property_changed),
-                            self);
+    _nm_dbus_proxy_signal_connect(priv->connman_proxy,
+                                  "PropertyChanged",
+                                  G_VARIANT_TYPE("(sv)"),
+                                  G_CALLBACK(connman_property_changed),
+                                  self);
+
+    _nm_dbus_proxy_signal_connect(priv->connman_proxy,
+                                  "ContextAdded",
+                                  G_VARIANT_TYPE("(oa{sv})"),
+                                  G_CALLBACK(connman_context_added),
+                                  self);
+
+    _nm_dbus_proxy_signal_connect(priv->connman_proxy,
+                                  "ContextRemoved",
+                                  G_VARIANT_TYPE("(o)"),
+                                  G_CALLBACK(connman_context_removed),
+                                  self);
 
     g_dbus_proxy_call(priv->connman_proxy,
                       "GetProperties",
@@ -562,6 +999,15 @@ _connman_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
                       priv->connman_proxy_cancellable,
                       connman_get_properties_done,
                       self);
+
+    g_dbus_proxy_call(priv->connman_proxy,
+                      "GetContexts",
+                      NULL,
+                      G_DBUS_CALL_FLAGS_NONE,
+                      20000,
+                      priv->connman_proxy_cancellable,
+                      connman_get_contexts_done,
+                      self);
 }
 
 static void
@@ -572,6 +1018,9 @@ handle_connman_iface(NMModemOfono *self, gboolean found)
     _LOGD("ConnectionManager interface %sfound", found ? "" : "not ");
 
     if (!found && (priv->connman_proxy || priv->connman_proxy_cancellable)) {
+        GHashTableIter        iter;
+        NMSettingsConnection *conn;
+
         _LOGI("ConnectionManager interface disappeared");
         nm_clear_g_cancellable(&priv->connman_proxy_cancellable);
         if (priv->connman_proxy) {
@@ -584,6 +1033,18 @@ handle_connman_iface(NMModemOfono *self, gboolean found)
          */
         priv->gprs_attached = FALSE;
 
+        g_hash_table_iter_init(&iter, priv->connections);
+        while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &conn)) {
+            if (NM_FLAGS_HAS(nm_settings_connection_get_flags(conn),
+                             NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED)) {
+                nm_settings_connection_delete(conn, FALSE);
+            }
+        }
+
+        priv->current_octx = NULL;
+        g_hash_table_remove_all(priv->connections);
+        g_hash_table_remove_all(priv->contexts);
+
         update_modem_state(self);
     } else if (found && (!priv->connman_proxy && !priv->connman_proxy_cancellable)) {
         _LOGI("found new ConnectionManager interface");
@@ -716,7 +1177,7 @@ stage1_prepare_done(GObject *source, GAsyncResult *result, gpointer user_data)
     self = NM_MODEM_OFONO(user_data);
     priv = NM_MODEM_OFONO_GET_PRIVATE(self);
 
-    g_clear_object(&priv->context_proxy_cancellable);
+    g_clear_object(&priv->connect_cancellable);
 
     nm_clear_pointer(&priv->connect_properties, g_hash_table_destroy);
 
@@ -730,9 +1191,8 @@ stage1_prepare_done(GObject *source, GAsyncResult *result, gpointer user_data)
 }
 
 static void
-handle_settings(GVariant *v_dict, gpointer user_data)
+handle_settings(NMModemOfono *self, GVariant *v_dict)
 {
-    NMModemOfono        *self = NM_MODEM_OFONO(user_data);
     NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
     char                 sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
     NMPlatformIP4Address address;
@@ -744,14 +1204,20 @@ handle_settings(GVariant *v_dict, gpointer user_data)
     int                  ifindex;
     GError              *error = NULL;
 
-    //_LOGD("PropertyChanged: %s", property);
-
     /*
      * TODO: might be a good idea and re-factor this to mimic bluez-device,
      * ie. have this function just check the key, and call a sub-func to
      * handle the action.
      */
 
+    if (nm_modem_get_state(NM_MODEM(self)) < NM_MODEM_STATE_REGISTERED) {
+        /*
+         * Connection definitely isn't happening. Avoid trigering bogus
+         * failure which would put device in a wrong state.
+         */
+        return;
+    }
+
     _LOGI("IPv4 static Settings:");
 
     if (!g_variant_lookup(v_dict, "Interface", "&s", &interface)) {
@@ -786,7 +1252,7 @@ handle_settings(GVariant *v_dict, gpointer user_data)
         _LOGW("Settings 'Address' missing");
         goto out;
     }
-    if (!s || !nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &address_network)) {
+    if (!s || !nm_inet_parse_bin(AF_INET, s, NULL, &address_network)) {
         _LOGW("can't convert 'Address' %s to addr", s ?: "");
         goto out;
     }
@@ -801,11 +1267,11 @@ handle_settings(GVariant *v_dict, gpointer user_data)
         _LOGW("Settings 'Netmask' missing");
         goto out;
     }
-    if (!s || !nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &address_network)) {
+    if (!s || !nm_inet_parse_bin(AF_INET, s, NULL, &address_network)) {
         _LOGW("invalid 'Netmask': %s", s ?: "");
         goto out;
     }
-    address.plen = _nm_utils_ip4_netmask_to_prefix(address_network);
+    address.plen = nm_ip4_addr_netmask_to_prefix(address_network);
 
     _LOGI("Address: %s", nm_platform_ip4_address_to_string(&address, sbuf, sizeof(sbuf)));
     nm_l3_config_data_add_address_4(priv->l3cd_4, &address);
@@ -814,7 +1280,7 @@ handle_settings(GVariant *v_dict, gpointer user_data)
         _LOGW("Settings 'Gateway' missing");
         goto out;
     }
-    if (!nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &gateway_network)) {
+    if (!nm_inet_parse_bin(AF_INET, s, NULL, &gateway_network)) {
         _LOGW("invalid 'Gateway': %s", s);
         goto out;
     }
@@ -841,14 +1307,13 @@ handle_settings(GVariant *v_dict, gpointer user_data)
         gsize    i;
 
         for (i = 0; array[i]; i++) {
-            if (!nm_utils_parse_inaddr_bin(AF_INET, array[i], NULL, &address_network)
-                || !address_network) {
+            if (!nm_inet_parse_bin(AF_INET, array[i], NULL, &address_network) || !address_network) {
                 _LOGW("invalid NameServer: %s", array[i]);
                 continue;
             }
             any_good = TRUE;
             _LOGI("DNS: %s", array[i]);
-            nm_l3_config_data_add_nameserver(priv->l3cd_4, AF_INET, &address_network);
+            nm_l3_config_data_add_nameserver_detail(priv->l3cd_4, AF_INET, &address_network, NULL);
         }
         if (!any_good) {
             _LOGW("Settings: 'DomainNameServers': none specified");
@@ -858,7 +1323,7 @@ handle_settings(GVariant *v_dict, gpointer user_data)
 
     if (g_variant_lookup(v_dict, "MessageProxy", "&s", &s)) {
         _LOGI("MessageProxy: %s", s);
-        if (s && nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &address_network)) {
+        if (s && nm_inet_parse_bin(AF_INET, s, NULL, &address_network)) {
             const NMPlatformIP4Route mms_route = {
                 .network       = address_network,
                 .plen          = 32,
@@ -893,28 +1358,6 @@ out:
 }
 
 static void
-context_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
-{
-    NMModemOfono              *self   = NM_MODEM_OFONO(user_data);
-    gs_unref_variant GVariant *v_dict = NULL;
-
-    _LOGD("PropertyChanged: %s", property);
-
-    if (g_strcmp0(property, "Settings") != 0)
-        return;
-
-    v_dict = g_variant_get_child_value(v, 0);
-    if (!v_dict) {
-        _LOGW("ofono: (%s): error getting IPv4 Settings", nm_modem_get_uid(NM_MODEM(self)));
-        return;
-    }
-
-    g_assert(g_variant_is_of_type(v_dict, G_VARIANT_TYPE_VARDICT));
-
-    handle_settings(v_dict, user_data);
-}
-
-static void
 stage3_ip_config_start(NMModem *modem, int addr_family, NMModemIPMethod ip_method)
 {
     NMModemOfono         *self  = NM_MODEM_OFONO(modem);
@@ -948,17 +1391,13 @@ out:
 static void
 context_properties_cb(GDBusProxy *proxy, GAsyncResult *result, gpointer user_data)
 {
-    NMModemOfono              *self;
-    NMModemOfonoPrivate       *priv;
+    NMModemOfono              *self       = user_data;
     gs_free_error GError      *error      = NULL;
     gs_unref_variant GVariant *properties = NULL;
     gs_unref_variant GVariant *settings   = NULL;
     gs_unref_variant GVariant *v_dict     = NULL;
     gboolean                   active;
 
-    self = NM_MODEM_OFONO(user_data);
-    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
-
     properties = g_dbus_proxy_call_finish(proxy, result, &error);
 
     if (!properties) {
@@ -978,13 +1417,6 @@ context_properties_cb(GDBusProxy *proxy, GAsyncResult *result, gpointer user_dat
         goto error;
     }
 
-    /* Watch for custom ofono PropertyChanged signals */
-    _nm_dbus_signal_connect(priv->context_proxy,
-                            "PropertyChanged",
-                            G_VARIANT_TYPE("(sv)"),
-                            G_CALLBACK(context_property_changed),
-                            self);
-
     if (active) {
         _LOGD("ofono: connection is already Active");
 
@@ -994,9 +1426,9 @@ context_properties_cb(GDBusProxy *proxy, GAsyncResult *result, gpointer user_dat
             goto error;
         }
 
-        handle_settings(settings, user_data);
+        handle_settings(self, settings);
     } else {
-        g_dbus_proxy_call(priv->context_proxy,
+        g_dbus_proxy_call(proxy,
                           "SetProperty",
                           g_variant_new("(sv)", "Active", g_variant_new("b", TRUE)),
                           G_DBUS_CALL_FLAGS_NONE,
@@ -1012,36 +1444,15 @@ error:
 }
 
 static void
-context_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+do_context_activate(NMModemOfono *self)
 {
-    NMModemOfono         *self;
-    NMModemOfonoPrivate  *priv;
-    gs_free_error GError *error = NULL;
-    GDBusProxy           *proxy;
-
-    proxy = g_dbus_proxy_new_for_bus_finish(result, &error);
-    if (!proxy || g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-        return;
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
 
-    self = NM_MODEM_OFONO(user_data);
-    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    g_return_if_fail(NM_IS_MODEM_OFONO(self));
 
-    if (!proxy) {
-        _LOGE("failed to create ofono ConnectionContext DBus proxy: %s", error->message);
-        g_clear_object(&priv->context_proxy_cancellable);
-        nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_MODEM_BUSY);
-        return;
-    }
+    nm_clear_g_cancellable(&priv->connect_cancellable);
 
-    priv->context_proxy = proxy;
-
-    if (!priv->gprs_attached) {
-        g_clear_object(&priv->context_proxy_cancellable);
-        nm_modem_emit_prepare_result(NM_MODEM(self),
-                                     FALSE,
-                                     NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER);
-        return;
-    }
+    priv->connect_cancellable = g_cancellable_new();
 
     /* We have an old copy of the settings from a previous activation,
      * clear it so that we can gate getting the IP config from oFono
@@ -1051,39 +1462,16 @@ context_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 
     /* We need to directly query ConnectionContextinteface to get the current
      * property values */
-    g_dbus_proxy_call(priv->context_proxy,
+    g_dbus_proxy_call(priv->current_octx->proxy,
                       "GetProperties",
                       NULL,
                       G_DBUS_CALL_FLAGS_NONE,
                       20000,
-                      NULL,
+                      priv->connect_cancellable,
                       (GAsyncReadyCallback) context_properties_cb,
                       self);
 }
 
-static void
-do_context_activate(NMModemOfono *self)
-{
-    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
-
-    g_return_if_fail(NM_IS_MODEM_OFONO(self));
-
-    nm_clear_g_cancellable(&priv->context_proxy_cancellable);
-    g_clear_object(&priv->context_proxy);
-
-    priv->context_proxy_cancellable = g_cancellable_new();
-
-    g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
-                             G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
-                             NULL,
-                             OFONO_DBUS_SERVICE,
-                             priv->context_path,
-                             OFONO_DBUS_INTERFACE_CONNECTION_CONTEXT,
-                             priv->context_proxy_cancellable,
-                             context_proxy_new_cb,
-                             self);
-}
-
 static GHashTable *
 create_connect_properties(NMConnection *connection)
 {
@@ -1109,6 +1497,20 @@ create_connect_properties(NMConnection *connection)
     return properties;
 }
 
+static gboolean
+wait_for_signal_timeout(gpointer user_data)
+{
+    NMModemOfono        *self = NM_MODEM_OFONO(user_data);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    nm_modem_emit_prepare_result(NM_MODEM(self),
+                                 FALSE,
+                                 NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT);
+
+    nm_clear_g_source_inst(&priv->deferred_connection_timeout_source);
+    return G_SOURCE_REMOVE;
+}
+
 static NMActStageReturn
 modem_act_stage1_prepare(NMModem             *modem,
                          NMConnection        *connection,
@@ -1116,20 +1518,10 @@ modem_act_stage1_prepare(NMModem             *modem,
 {
     NMModemOfono        *self = NM_MODEM_OFONO(modem);
     NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
-    const char          *context_id;
-    char               **id = NULL;
+    const char          *uuid = nm_connection_get_uuid(connection);
+    OfonoContextData    *octx = g_hash_table_lookup(priv->contexts, uuid);
 
-    context_id = nm_connection_get_id(connection);
-    id         = g_strsplit(context_id, "/", 0);
-    g_return_val_if_fail(id[2], NM_ACT_STAGE_RETURN_FAILURE);
-
-    _LOGD("trying %s %s", id[1], id[2]);
-
-    g_free(priv->context_path);
-    priv->context_path = g_strdup_printf("%s/%s", nm_modem_get_path(modem), id[2]);
-    g_strfreev(id);
-
-    if (!priv->context_path) {
+    if (!octx) {
         NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_GSM_APN_FAILED);
         return NM_ACT_STAGE_RETURN_FAILURE;
     }
@@ -1139,11 +1531,19 @@ modem_act_stage1_prepare(NMModem             *modem,
 
     priv->connect_properties = create_connect_properties(connection);
 
-    _LOGI("activating context %s", priv->context_path);
+    _LOGI("activating context %s", g_dbus_proxy_get_object_path(octx->proxy));
+    priv->current_octx = octx;
 
     update_modem_state(self);
     if (nm_modem_get_state(modem) == NM_MODEM_STATE_REGISTERED) {
         do_context_activate(self);
+    } else if (nm_modem_get_state(modem) == NM_MODEM_STATE_SEARCHING) {
+        _LOGI("activation deferred while modem is searching for signal.");
+
+        nm_clear_g_source_inst(&priv->deferred_connection_timeout_source);
+        priv->deferred_connection_timeout_source =
+            nm_g_timeout_add_seconds_source(60 /* seconds */, wait_for_signal_timeout, self);
+        /* update_modem_state() will advance the next step. */
     } else {
         _LOGW("could not activate context: modem is not registered.");
         NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER);
@@ -1176,11 +1576,11 @@ modem_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 
     priv->modem_proxy = proxy;
 
-    _nm_dbus_signal_connect(priv->modem_proxy,
-                            "PropertyChanged",
-                            G_VARIANT_TYPE("(sv)"),
-                            G_CALLBACK(modem_property_changed),
-                            self);
+    _nm_dbus_proxy_signal_connect(priv->modem_proxy,
+                                  "PropertyChanged",
+                                  G_VARIANT_TYPE("(sv)"),
+                                  G_CALLBACK(modem_property_changed),
+                                  self);
 
     g_dbus_proxy_call(priv->modem_proxy,
                       "GetProperties",
@@ -1196,7 +1596,17 @@ modem_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 
 static void
 nm_modem_ofono_init(NMModemOfono *self)
-{}
+{
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    priv->modem_proxy_cancellable = g_cancellable_new();
+    priv->connections = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_object_unref);
+    priv->contexts    = g_hash_table_new_full(nm_str_hash,
+                                           g_str_equal,
+                                           g_free,
+                                           (GDestroyNotify) ofono_context_data_free);
+    priv->settings    = g_object_ref(NM_SETTINGS_GET);
+}
 
 static void
 constructed(GObject *object)
@@ -1204,8 +1614,6 @@ constructed(GObject *object)
     NMModemOfono        *self = NM_MODEM_OFONO(object);
     NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
 
-    priv->modem_proxy_cancellable = g_cancellable_new();
-
     g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
                              G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
                              NULL,
@@ -1257,7 +1665,7 @@ dispose(GObject *object)
 
     nm_clear_g_cancellable(&priv->modem_proxy_cancellable);
     nm_clear_g_cancellable(&priv->connman_proxy_cancellable);
-    nm_clear_g_cancellable(&priv->context_proxy_cancellable);
+    nm_clear_g_cancellable(&priv->connect_cancellable);
     nm_clear_g_cancellable(&priv->sim_proxy_cancellable);
 
     if (priv->connect_properties) {
@@ -1265,6 +1673,16 @@ dispose(GObject *object)
         priv->connect_properties = NULL;
     }
 
+    if (priv->connections) {
+        g_hash_table_destroy(priv->connections);
+        priv->connections = NULL;
+    }
+
+    if (priv->contexts) {
+        g_hash_table_destroy(priv->contexts);
+        priv->contexts = NULL;
+    }
+
     nm_clear_l3cd(&priv->l3cd_4);
 
     if (priv->modem_proxy) {
@@ -1277,19 +1695,21 @@ dispose(GObject *object)
         g_clear_object(&priv->connman_proxy);
     }
 
-    if (priv->context_proxy) {
-        g_signal_handlers_disconnect_by_data(priv->context_proxy, self);
-        g_clear_object(&priv->context_proxy);
-    }
-
     if (priv->sim_proxy) {
         g_signal_handlers_disconnect_by_data(priv->sim_proxy, self);
         g_clear_object(&priv->sim_proxy);
     }
 
+    if (priv->settings) {
+        g_signal_handlers_disconnect_by_data(priv->settings, self);
+        g_clear_object(&priv->settings);
+    }
+
     g_free(priv->imsi);
     priv->imsi = NULL;
 
+    nm_clear_g_source_inst(&priv->deferred_connection_timeout_source);
+
     G_OBJECT_CLASS(nm_modem_ofono_parent_class)->dispose(object);
 }
 
diff --git a/src/core/devices/wwan/nm-modem-ofono.h b/src/core/devices/wwan/nm-modem-ofono.h
index ebe21023..02ba49ad 100644
--- a/src/core/devices/wwan/nm-modem-ofono.h
+++ b/src/core/devices/wwan/nm-modem-ofono.h
@@ -8,8 +8,9 @@
 
 #include "nm-modem.h"
 
-#define NM_TYPE_MODEM_OFONO    (nm_modem_ofono_get_type())
-#define NM_MODEM_OFONO(obj)    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM_OFONO, NMModemOfono))
+#define NM_TYPE_MODEM_OFONO (nm_modem_ofono_get_type())
+#define NM_MODEM_OFONO(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM_OFONO, NMModemOfono))
 #define NM_IS_MODEM_OFONO(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM_OFONO))
 #define NM_MODEM_OFONO_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM_OFONO, NMModemOfonoClass))
diff --git a/src/core/devices/wwan/nm-modem.c b/src/core/devices/wwan/nm-modem.c
index a5ee8ce8..0159d351 100644
--- a/src/core/devices/wwan/nm-modem.c
+++ b/src/core/devices/wwan/nm-modem.c
@@ -353,6 +353,12 @@ nm_modem_set_mm_enabled(NMModem *self, gboolean enabled)
     NMModemPrivate *priv       = NM_MODEM_GET_PRIVATE(self);
     NMModemState    prev_state = priv->state;
 
+    /* Not all modem classes support set_mm_enabled */
+    if (!NM_MODEM_GET_CLASS(self)->set_mm_enabled) {
+        _LOGD("cannot enable modem: not implemented");
+        return;
+    }
+
     if (enabled && priv->state >= NM_MODEM_STATE_ENABLING) {
         _LOGD("cannot enable modem: already enabled");
         return;
@@ -375,9 +381,7 @@ nm_modem_set_mm_enabled(NMModem *self, gboolean enabled)
         return;
     }
 
-    /* Not all modem classes support set_mm_enabled */
-    if (NM_MODEM_GET_CLASS(self)->set_mm_enabled)
-        NM_MODEM_GET_CLASS(self)->set_mm_enabled(self, enabled);
+    NM_MODEM_GET_CLASS(self)->set_mm_enabled(self, enabled);
 
     /* Pre-empt the state change signal */
     nm_modem_set_state(self,
diff --git a/src/core/devices/wwan/nm-modem.h b/src/core/devices/wwan/nm-modem.h
index ca541073..ec001102 100644
--- a/src/core/devices/wwan/nm-modem.h
+++ b/src/core/devices/wwan/nm-modem.h
@@ -11,7 +11,7 @@
 #include "devices/nm-device.h"
 
 #define NM_TYPE_MODEM            (nm_modem_get_type())
-#define NM_MODEM(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM, NMModem))
+#define NM_MODEM(obj)            (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM, NMModem))
 #define NM_MODEM_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM, NMModemClass))
 #define NM_IS_MODEM(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM))
 #define NM_IS_MODEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_MODEM))
diff --git a/src/core/devices/wwan/nm-wwan-factory.c b/src/core/devices/wwan/nm-wwan-factory.c
index 169407c7..1c6d351f 100644
--- a/src/core/devices/wwan/nm-wwan-factory.c
+++ b/src/core/devices/wwan/nm-wwan-factory.c
@@ -18,7 +18,7 @@
 
 #define NM_TYPE_WWAN_FACTORY (nm_wwan_factory_get_type())
 #define NM_WWAN_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WWAN_FACTORY, NMWwanFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WWAN_FACTORY, NMWwanFactory))
 #define NM_WWAN_FACTORY_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WWAN_FACTORY, NMWwanFactoryClass))
 #define NM_IS_WWAN_FACTORY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WWAN_FACTORY))
diff --git a/src/core/dhcp/nm-dhcp-client.c b/src/core/dhcp/nm-dhcp-client.c
index 805b42d2..600cb930 100644
--- a/src/core/dhcp/nm-dhcp-client.c
+++ b/src/core/dhcp/nm-dhcp-client.c
@@ -84,6 +84,7 @@ typedef struct _NMDhcpClientPrivate {
      * and is set from l3cd_next. */
     const NML3ConfigData *l3cd_curr;
 
+    GSource *previous_lease_timeout_source;
     GSource *no_lease_timeout_source;
     GSource *watch_source;
     GBytes  *effective_client_id;
@@ -269,6 +270,12 @@ nm_dhcp_client_create_options_dict(NMDhcpClient *self, gboolean static_keys)
     return options;
 }
 
+const NML3ConfigData *
+nm_dhcp_client_get_lease(NMDhcpClient *self)
+{
+    return NM_DHCP_CLIENT_GET_PRIVATE(self)->l3cd_curr;
+}
+
 /*****************************************************************************/
 
 gboolean
@@ -301,11 +308,16 @@ nm_dhcp_client_set_effective_client_id(NMDhcpClient *self, GBytes *client_id)
 /*****************************************************************************/
 
 static void
-_emit_notify(NMDhcpClient *self, const NMDhcpClientNotifyData *notify_data)
+_emit_notify_data(NMDhcpClient *self, const NMDhcpClientNotifyData *notify_data)
 {
     g_signal_emit(G_OBJECT(self), signals[SIGNAL_NOTIFY], 0, notify_data);
 }
 
+#define _emit_notify(self, _notify_type, ...) \
+    _emit_notify_data(                        \
+        (self),                               \
+        &((const NMDhcpClientNotifyData){.notify_type = (_notify_type), __VA_ARGS__}))
+
 /*****************************************************************************/
 
 static void
@@ -392,12 +404,7 @@ _no_lease_timeout(gpointer user_data)
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
     nm_clear_g_source_inst(&priv->no_lease_timeout_source);
-
-    _emit_notify(self,
-                 &((NMDhcpClientNotifyData){
-                     .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_NO_LEASE_TIMEOUT,
-                 }));
-
+    _emit_notify(self, NM_DHCP_CLIENT_NOTIFY_TYPE_NO_LEASE_TIMEOUT);
     return G_SOURCE_CONTINUE;
 }
 
@@ -473,7 +480,7 @@ _acd_complete_on_idle_cb(gpointer user_data)
 }
 
 #define _acd_reglist_data_get(priv, idx) \
-    nm_g_array_index_p((priv)->v4.acd.reglist, AcdRegListData, (idx))
+    (&nm_g_array_index((priv)->v4.acd.reglist, AcdRegListData, (idx)))
 
 static guint
 _acd_reglist_data_find(NMDhcpClientPrivate *priv, in_addr_t addr_needle)
@@ -503,10 +510,10 @@ _acd_reglist_data_remove(NMDhcpClient *self, guint idx, gboolean do_log)
     reglist_data = _acd_reglist_data_get(priv, idx);
 
     if (do_log) {
-        char sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+        char sbuf_addr[NM_INET_ADDRSTRLEN];
 
         _LOGD("acd: drop check for address %s (l3cd " NM_HASH_OBFUSCATE_PTR_FMT ")",
-              _nm_utils_inet4_ntop(reglist_data->addr, sbuf_addr),
+              nm_inet4_ntop(reglist_data->addr, sbuf_addr),
               NM_HASH_OBFUSCATE_PTR(reglist_data->l3cd));
     }
 
@@ -585,7 +592,7 @@ static void
 _acd_check_lease(NMDhcpClient *self, NMOptionBool *out_acd_state)
 {
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-    char                 sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char                 sbuf_addr[NM_INET_ADDRSTRLEN];
     in_addr_t            addr;
     gboolean             addr_changed = FALSE;
     guint                idx;
@@ -624,7 +631,7 @@ _acd_check_lease(NMDhcpClient *self, NMOptionBool *out_acd_state)
 
     _LOGD("acd: %s check for address %s (timeout %u msec, l3cd " NM_HASH_OBFUSCATE_PTR_FMT ")",
           addr_changed ? "add" : "update",
-          _nm_utils_inet4_ntop(addr, sbuf_addr),
+          nm_inet4_ntop(addr, sbuf_addr),
           priv->config.v4.acd_timeout_msec,
           NM_HASH_OBFUSCATE_PTR(priv->l3cd_next));
 
@@ -711,7 +718,7 @@ gboolean
 _nm_dhcp_client_accept_offer(NMDhcpClient *self, gconstpointer p_yiaddr)
 {
     NMDhcpClientPrivate   *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-    char                   sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char                   sbuf_addr[NM_INET_ADDRSTRLEN];
     NMIPAddr               yiaddr;
     const NML3AcdAddrInfo *acd_info;
 
@@ -744,7 +751,7 @@ _nm_dhcp_client_accept_offer(NMDhcpClient *self, gconstpointer p_yiaddr)
         return TRUE;
 
     _LOGD("offered lease rejected: address %s failed ACD check",
-          _nm_utils_inet4_ntop(yiaddr.addr4, sbuf_addr));
+          nm_inet4_ntop(yiaddr.addr4, sbuf_addr));
 
     return FALSE;
 }
@@ -863,6 +870,9 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
         return;
     }
 
+    if (priv->l3cd_next)
+        nm_clear_g_source_inst(&priv->previous_lease_timeout_source);
+
     nm_l3_config_data_reset(&priv->l3cd_curr, priv->l3cd_next);
 
     if (client_event_type == NM_DHCP_CLIENT_EVENT_TYPE_BOUND && priv->l3cd_curr
@@ -883,18 +893,12 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
 
     l3_cfg_notify_check_connected(self);
 
-    {
-        const NMDhcpClientNotifyData notify_data = {
-            .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
-            .lease_update =
-                {
-                    .l3cd     = priv->l3cd_curr,
-                    .accepted = !priv->l3cfg_notify.wait_dhcp_commit,
-                },
-        };
-
-        _emit_notify(self, &notify_data);
-    }
+    _emit_notify(self,
+                 NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
+                 .lease_update = {
+                     .l3cd     = priv->l3cd_curr,
+                     .accepted = !priv->l3cfg_notify.wait_dhcp_commit,
+                 });
 }
 
 static void
@@ -1010,12 +1014,10 @@ ipv6_lladdr_timeout(gpointer user_data)
 
     nm_clear_g_source_inst(&priv->v6.lladdr_timeout_source);
 
-    _emit_notify(
-        self,
-        &((NMDhcpClientNotifyData){
-            .notify_type         = NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
-            .it_looks_bad.reason = "timeout reached while waiting for an IPv6 link-local address",
-        }));
+    _emit_notify(self,
+                 NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
+                 .it_looks_bad.reason =
+                     "timeout reached while waiting for an IPv6 link-local address");
     return G_SOURCE_CONTINUE;
 }
 
@@ -1027,12 +1029,9 @@ ipv6_dad_timeout(gpointer user_data)
 
     nm_clear_g_source_inst(&priv->v6.dad_timeout_source);
 
-    _emit_notify(
-        self,
-        &((NMDhcpClientNotifyData){
-            .notify_type         = NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
-            .it_looks_bad.reason = "timeout reached while waiting for IPv6 DAD to complete",
-        }));
+    _emit_notify(self,
+                 NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
+                 .it_looks_bad.reason = "timeout reached while waiting for IPv6 DAD to complete");
     return G_SOURCE_CONTINUE;
 }
 
@@ -1068,35 +1067,33 @@ ipv6_lladdr_find(NMDhcpClient *self)
 static void
 ipv6_tentative_addr_check(NMDhcpClient                *self,
                           GPtrArray                  **tentative,
-                          GPtrArray                  **missing,
+                          GPtrArray                  **dadfailed,
                           const NMPlatformIP6Address **valid)
 {
     NMDhcpClientPrivate        *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
     NMDedupMultiIter            iter;
     const NMPlatformIP6Address *addr;
-    NML3Cfg                    *l3cfg = priv->config.l3cfg;
+    NML3Cfg                    *l3cfg    = priv->config.l3cfg;
+    NMPlatform                 *platform = nm_l3cfg_get_platform(l3cfg);
+    int                         ifindex  = nm_l3cfg_get_ifindex(l3cfg);
 
     /* For each address in the lease, check whether it's tentative
-     * in platform. */
+     * or dad-failed in platform. */
     nm_l3_config_data_iter_ip6_address_for_each (&iter, priv->l3cd_curr, &addr) {
         const NMPlatformIP6Address *pladdr;
-        NMPObject                   needle;
-
-        nmp_object_stackinit_id_ip6_address(&needle, nm_l3cfg_get_ifindex(l3cfg), &addr->address);
-        pladdr = NMP_OBJECT_CAST_IP6_ADDRESS(nm_platform_lookup_obj(nm_l3cfg_get_platform(l3cfg),
-                                                                    NMP_CACHE_ID_TYPE_OBJECT_TYPE,
-                                                                    &needle));
-        if (!pladdr) {
-            /* address removed: we assume that's because DAD failed */
-            if (missing) {
-                if (!*missing)
-                    *missing = g_ptr_array_new();
-                g_ptr_array_add(*missing, (gpointer) addr);
+
+        pladdr = nm_platform_ip6_address_get(platform, ifindex, &addr->address);
+        if ((pladdr && NM_FLAGS_HAS(pladdr->n_ifa_flags, IFA_F_DADFAILED))
+            || (!pladdr && nm_platform_ip6_dadfailed_check(platform, ifindex, &addr->address))) {
+            if (dadfailed) {
+                if (!*dadfailed)
+                    *dadfailed = g_ptr_array_new();
+                g_ptr_array_add(*dadfailed, (gpointer) addr);
             }
             continue;
         }
 
-        if (NM_FLAGS_HAS(pladdr->n_ifa_flags, IFA_F_TENTATIVE)
+        if (pladdr && NM_FLAGS_HAS(pladdr->n_ifa_flags, IFA_F_TENTATIVE)
             && !NM_FLAGS_HAS(pladdr->n_ifa_flags, IFA_F_OPTIMISTIC)) {
             if (tentative) {
                 if (!*tentative)
@@ -1105,6 +1102,9 @@ ipv6_tentative_addr_check(NMDhcpClient                *self,
             }
         }
 
+        /* Here the address is non-tentative or it was removed externally by the user.
+         * In both cases it has completed DAD.
+         */
         NM_SET_OUT(valid, addr);
     }
 }
@@ -1113,7 +1113,7 @@ static void
 l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcpClient *self)
 {
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-    char                 sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char                 sbuf_addr[NM_INET_ADDRSTRLEN];
 
     nm_assert(l3cfg == priv->config.l3cfg);
 
@@ -1133,10 +1133,8 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
 
             if (!NM_DHCP_CLIENT_GET_CLASS(self)->ip6_start(self, &addr->address, &error)) {
                 _emit_notify(self,
-                             &((NMDhcpClientNotifyData){
-                                 .notify_type         = NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
-                                 .it_looks_bad.reason = error->message,
-                             }));
+                             NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
+                             .it_looks_bad.reason = error->message);
             }
         }
     }
@@ -1144,13 +1142,13 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
     if (notify_data->notify_type == NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE_ON_IDLE
         && priv->l3cfg_notify.wait_ipv6_dad) {
         gs_unref_ptrarray GPtrArray *tentative = NULL;
-        gs_unref_ptrarray GPtrArray *missing   = NULL;
+        gs_unref_ptrarray GPtrArray *dadfailed = NULL;
         const NMPlatformIP6Address  *valid     = NULL;
         char                         str[NM_UTILS_TO_STRING_BUFFER_SIZE];
         guint                        i;
         gs_free_error GError        *error = NULL;
 
-        ipv6_tentative_addr_check(self, &tentative, &missing, &valid);
+        ipv6_tentative_addr_check(self, &tentative, &dadfailed, &valid);
         if (tentative) {
             for (i = 0; i < tentative->len; i++) {
                 _LOGD("still waiting DAD for address: %s",
@@ -1163,10 +1161,10 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
             nm_clear_g_source_inst(&priv->v6.dad_timeout_source);
             l3_cfg_notify_check_connected(self);
 
-            if (missing) {
-                for (i = 0; i < missing->len; i++) {
+            if (dadfailed) {
+                for (i = 0; i < dadfailed->len; i++) {
                     _LOGE("DAD failed for address: %s",
-                          nm_platform_ip6_address_to_string(missing->pdata[i], str, sizeof(str)));
+                          nm_platform_ip6_address_to_string(dadfailed->pdata[i], str, sizeof(str)));
                 }
             }
 
@@ -1176,22 +1174,19 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
 
                 if (_dhcp_client_accept(self, priv->l3cd_curr, &error)) {
                     _emit_notify(self,
-                                 &((NMDhcpClientNotifyData){
-                                     .notify_type  = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
-                                     .lease_update = {
-                                         .l3cd     = priv->l3cd_curr,
-                                         .accepted = TRUE,
-                                     }}));
+                                 NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
+                                 .lease_update = {
+                                     .l3cd     = priv->l3cd_curr,
+                                     .accepted = TRUE,
+                                 });
                 } else {
                     gs_free char *reason =
                         g_strdup_printf("error accepting lease: %s", error->message);
 
                     _LOGD("accept failed: %s", error->message);
                     _emit_notify(self,
-                                 &((NMDhcpClientNotifyData){
-                                     .notify_type         = NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
-                                     .it_looks_bad.reason = reason,
-                                 }));
+                                 NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
+                                 .it_looks_bad.reason = reason);
                 }
             } else {
                 _LOGD("decline the lease");
@@ -1266,20 +1261,17 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
                 _LOGD("accept failed: %s", error->message);
 
                 _emit_notify(self,
-                             &((NMDhcpClientNotifyData){
-                                 .notify_type         = NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
-                                 .it_looks_bad.reason = reason,
-                             }));
+                             NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
+                             .it_looks_bad.reason = reason, );
                 goto wait_dhcp_commit_done;
             }
 
-            _emit_notify(
-                self,
-                &((NMDhcpClientNotifyData){.notify_type  = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
-                                           .lease_update = {
-                                               .l3cd     = priv->l3cd_curr,
-                                               .accepted = TRUE,
-                                           }}));
+            _emit_notify(self,
+                         NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
+                         .lease_update = {
+                             .l3cd     = priv->l3cd_curr,
+                             .accepted = TRUE,
+                         });
         }
     }
 wait_dhcp_commit_done:
@@ -1318,7 +1310,7 @@ wait_dhcp_commit_done:
             if (acd_state != NM_OPTION_BOOL_DEFAULT) {
                 _LOGD("acd: acd %s for %s",
                       acd_state ? "ready" : "conflict",
-                      _nm_utils_inet4_ntop(priv->v4.acd.addr, sbuf_addr));
+                      nm_inet4_ntop(priv->v4.acd.addr, sbuf_addr));
                 nm_l3cfg_commit_type_clear(priv->config.l3cfg, &priv->v4.acd.l3cfg_commit_handle);
                 priv->v4.acd.state       = acd_state;
                 priv->v4.acd.done_source = nm_g_idle_add_source(_acd_complete_on_idle_cb, self);
@@ -1327,6 +1319,19 @@ wait_dhcp_commit_done:
     }
 }
 
+static gboolean
+_previous_lease_timeout_cb(gpointer user_data)
+{
+    NMDhcpClient        *self = user_data;
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    nm_clear_g_source_inst(&priv->previous_lease_timeout_source);
+
+    _nm_dhcp_client_notify(self, NM_DHCP_CLIENT_EVENT_TYPE_TIMEOUT, NULL);
+
+    return G_SOURCE_CONTINUE;
+}
+
 gboolean
 nm_dhcp_client_start(NMDhcpClient *self, GError **error)
 {
@@ -1358,6 +1363,23 @@ nm_dhcp_client_start(NMDhcpClient *self, GError **error)
 
     _no_lease_timeout_schedule(self);
 
+    if (priv->config.previous_lease) {
+        /* We got passed a previous lease (during a reapply). For a few seconds, we
+         * will pretend that this is current lease. */
+        priv->l3cd_curr = g_steal_pointer(&priv->config.previous_lease);
+
+        /* Schedule a timeout for when we give up using this lease. Note
+         * that then we will emit a NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE event
+         * and the lease is gone. Note that NMDevice ignores that and will
+         * keep using the lease.
+         *
+         * At the same time, we have _no_lease_timeout_schedule() ticking, when
+         * that expires, we will emit a NM_DHCP_CLIENT_NOTIFY_TYPE_NO_LEASE_TIMEOUT
+         * signal, which causes NMDevice to clear the lease. */
+        priv->previous_lease_timeout_source =
+            nm_g_timeout_add_seconds_source(15, _previous_lease_timeout_cb, self);
+    }
+
     if (IS_IPv4)
         return NM_DHCP_CLIENT_GET_CLASS(self)->ip4_start(self, error);
 
@@ -1438,6 +1460,8 @@ nm_dhcp_client_stop(NMDhcpClient *self, gboolean release)
     if (priv->is_stopped)
         return;
 
+    nm_clear_g_source_inst(&priv->previous_lease_timeout_source);
+
     priv->is_stopped = TRUE;
 
     if (priv->invocation) {
@@ -1597,15 +1621,11 @@ maybe_add_option(NMDhcpClient *self, GHashTable *hash, const char *key, GVariant
 void
 nm_dhcp_client_emit_ipv6_prefix_delegated(NMDhcpClient *self, const NMPlatformIP6Address *prefix)
 {
-    const NMDhcpClientNotifyData notify_data = {
-        .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_PREFIX_DELEGATED,
-        .prefix_delegated =
-            {
-                .prefix = prefix,
-            },
-    };
-
-    _emit_notify(self, &notify_data);
+    _emit_notify(self,
+                 NM_DHCP_CLIENT_NOTIFY_TYPE_PREFIX_DELEGATED,
+                 .prefix_delegated = {
+                     .prefix = prefix,
+                 });
 }
 
 gboolean
@@ -1621,7 +1641,7 @@ nm_dhcp_client_handle_event(gpointer               unused,
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
     NMDhcpClientEventType                   client_event_type;
     NMPlatformIP6Address                    prefix = {
-                           0,
+        0,
     };
 
     g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE);
@@ -1751,14 +1771,14 @@ nm_dhcp_client_server_id_is_rejected(NMDhcpClient *self, gconstpointer addr)
             in_addr_t mask;
             int       r_prefix;
 
-            if (!nm_utils_parse_inaddr_prefix_bin(AF_INET,
-                                                  priv->config.reject_servers[i],
-                                                  NULL,
-                                                  &r_addr,
-                                                  &r_prefix))
+            if (!nm_inet_parse_with_prefix_bin(AF_INET,
+                                               priv->config.reject_servers[i],
+                                               NULL,
+                                               &r_addr,
+                                               &r_prefix))
                 nm_assert_not_reached();
 
-            mask = _nm_utils_ip4_prefix_to_netmask(r_prefix < 0 ? 32 : r_prefix);
+            mask = nm_ip4_addr_netmask_from_prefix(r_prefix < 0 ? 32 : r_prefix);
             if ((addr4 & mask) == (r_addr & mask))
                 return TRUE;
         }
@@ -1783,6 +1803,8 @@ config_init(NMDhcpClientConfig *config, const NMDhcpClientConfig *src)
 
     g_object_ref(config->l3cfg);
 
+    nm_l3_config_data_ref_and_seal(config->previous_lease);
+
     nm_g_bytes_ref(config->hwaddr);
     nm_g_bytes_ref(config->bcast_hwaddr);
     nm_g_bytes_ref(config->vendor_class_identifier);
@@ -1843,6 +1865,8 @@ config_clear(NMDhcpClientConfig *config)
 {
     g_object_unref(config->l3cfg);
 
+    nm_clear_l3cd(&config->previous_lease);
+
     nm_clear_pointer(&config->hwaddr, g_bytes_unref);
     nm_clear_pointer(&config->bcast_hwaddr, g_bytes_unref);
     nm_clear_pointer(&config->vendor_class_identifier, g_bytes_unref);
@@ -1919,6 +1943,7 @@ dispose(GObject *object)
 
     watch_cleanup(self);
 
+    nm_clear_g_source_inst(&priv->previous_lease_timeout_source);
     nm_clear_g_source_inst(&priv->no_lease_timeout_source);
 
     if (!NM_IS_IPv4(priv->config.addr_family)) {
diff --git a/src/core/dhcp/nm-dhcp-client.h b/src/core/dhcp/nm-dhcp-client.h
index 6f403b61..f43770a0 100644
--- a/src/core/dhcp/nm-dhcp-client.h
+++ b/src/core/dhcp/nm-dhcp-client.h
@@ -14,7 +14,8 @@
 #define NM_DHCP_TIMEOUT_INFINITY ((guint32) G_MAXINT32)
 
 #define NM_TYPE_DHCP_CLIENT (nm_dhcp_client_get_type())
-#define NM_DHCP_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_CLIENT, NMDhcpClient))
+#define NM_DHCP_CLIENT(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_CLIENT, NMDhcpClient))
 #define NM_DHCP_CLIENT_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_CLIENT, NMDhcpClientClass))
 #define NM_IS_DHCP_CLIENT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_CLIENT))
@@ -91,9 +92,8 @@ typedef struct {
      * NMDhcpClient is supposed to run. */
     NML3Cfg *l3cfg;
 
-    /* FIXME(l3cfg:dhcp:previous-lease): most parameters of NMDhcpClient are immutable,
-     * so to change them (during reapply), we need to create and start
-     * a new NMDhcpClient instance.
+    /* Most parameters of NMDhcpClient are immutable, so to change them (during
+     * reapply), we need to create and start a new NMDhcpClient instance.
      *
      * However, while the restart happens, we want to stick to the previous
      * lease (if any). Allow the caller to provide such a previous lease,
@@ -120,7 +120,7 @@ typedef struct {
     const char *uuid;
 
     /* Set to reduce the number of broadcast packets when the
-	 * anycast hardware address of the DHCP service is known. */
+     * anycast hardware address of the DHCP service is known. */
     const char *anycast_address;
 
     /* The hostname or FQDN to send. */
@@ -224,18 +224,7 @@ const NMDhcpClientConfig *nm_dhcp_client_get_config(NMDhcpClient *self);
 
 pid_t nm_dhcp_client_get_pid(NMDhcpClient *self);
 
-static inline const NML3ConfigData *
-nm_dhcp_client_get_lease(NMDhcpClient *self)
-{
-    /* FIXME(l3cfg:dhcp:previous-lease): this function returns the currently
-     * valid, exposed lease.
-     *
-     * Note that NMDhcpClient should accept as construct argument a *previous* lease,
-     * and (if that lease is still valid), pretend that it's good to use. The point is
-     * so that during reapply we keep using the current address, until a new lease
-     * was received. */
-    return NULL;
-}
+const NML3ConfigData *nm_dhcp_client_get_lease(NMDhcpClient *self);
 
 void nm_dhcp_client_stop(NMDhcpClient *self, gboolean release);
 
diff --git a/src/core/dhcp/nm-dhcp-dhclient.c b/src/core/dhcp/nm-dhcp-dhclient.c
index e4f40d7c..35b2fb2e 100644
--- a/src/core/dhcp/nm-dhcp-dhclient.c
+++ b/src/core/dhcp/nm-dhcp-dhclient.c
@@ -43,7 +43,7 @@ _addr_family_to_path_part(int addr_family)
 
 #define NM_TYPE_DHCP_DHCLIENT (nm_dhcp_dhclient_get_type())
 #define NM_DHCP_DHCLIENT(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_DHCLIENT, NMDhcpDhclient))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_DHCLIENT, NMDhcpDhclient))
 #define NM_DHCP_DHCLIENT_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_DHCLIENT, NMDhcpDhclientClass))
 #define NM_IS_DHCP_DHCLIENT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_DHCLIENT))
diff --git a/src/core/dhcp/nm-dhcp-dhcpcanon.c b/src/core/dhcp/nm-dhcp-dhcpcanon.c
index 87e43fa8..cd42b692 100644
--- a/src/core/dhcp/nm-dhcp-dhcpcanon.c
+++ b/src/core/dhcp/nm-dhcp-dhcpcanon.c
@@ -18,7 +18,7 @@
 
 #define NM_TYPE_DHCP_DHCPCANON (nm_dhcp_dhcpcanon_get_type())
 #define NM_DHCP_DHCPCANON(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_DHCPCANON, NMDhcpDhcpcanon))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_DHCPCANON, NMDhcpDhcpcanon))
 #define NM_DHCP_DHCPCANON_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_DHCPCANON, NMDhcpDhcpcanonClass))
 #define NM_IS_DHCP_DHCPCANON(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_DHCPCANON))
diff --git a/src/core/dhcp/nm-dhcp-dhcpcd.c b/src/core/dhcp/nm-dhcp-dhcpcd.c
index 280d9ce6..8d2f928b 100644
--- a/src/core/dhcp/nm-dhcp-dhcpcd.c
+++ b/src/core/dhcp/nm-dhcp-dhcpcd.c
@@ -23,7 +23,8 @@
 /*****************************************************************************/
 
 #define NM_TYPE_DHCP_DHCPCD (nm_dhcp_dhcpcd_get_type())
-#define NM_DHCP_DHCPCD(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_DHCPCD, NMDhcpDhcpcd))
+#define NM_DHCP_DHCPCD(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_DHCPCD, NMDhcpDhcpcd))
 #define NM_DHCP_DHCPCD_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_DHCPCD, NMDhcpDhcpcdClass))
 #define NM_IS_DHCP_DHCPCD(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_DHCPCD))
diff --git a/src/core/dhcp/nm-dhcp-listener.h b/src/core/dhcp/nm-dhcp-listener.h
index 5f3c952c..0818b371 100644
--- a/src/core/dhcp/nm-dhcp-listener.h
+++ b/src/core/dhcp/nm-dhcp-listener.h
@@ -8,7 +8,7 @@
 
 #define NM_TYPE_DHCP_LISTENER (nm_dhcp_listener_get_type())
 #define NM_DHCP_LISTENER(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_LISTENER, NMDhcpListener))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_LISTENER, NMDhcpListener))
 #define NM_IS_DHCP_LISTENER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_LISTENER))
 #define NM_DHCP_LISTENER_GET_CLASS(obj) \
     (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DHCP_LISTENER, NMDhcpListenerClass))
diff --git a/src/core/dhcp/nm-dhcp-manager.h b/src/core/dhcp/nm-dhcp-manager.h
index bbd68236..980a26c9 100644
--- a/src/core/dhcp/nm-dhcp-manager.h
+++ b/src/core/dhcp/nm-dhcp-manager.h
@@ -12,7 +12,7 @@
 
 #define NM_TYPE_DHCP_MANAGER (nm_dhcp_manager_get_type())
 #define NM_DHCP_MANAGER(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_MANAGER, NMDhcpManager))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_MANAGER, NMDhcpManager))
 #define NM_DHCP_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_MANAGER, NMDhcpManagerClass))
 #define NM_IS_DHCP_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_MANAGER))
diff --git a/src/core/dhcp/nm-dhcp-nettools.c b/src/core/dhcp/nm-dhcp-nettools.c
index df88362e..9cdfd9aa 100644
--- a/src/core/dhcp/nm-dhcp-nettools.c
+++ b/src/core/dhcp/nm-dhcp-nettools.c
@@ -36,7 +36,7 @@
 
 #define NM_TYPE_DHCP_NETTOOLS (nm_dhcp_nettools_get_type())
 #define NM_DHCP_NETTOOLS(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_NETTOOLS, NMDhcpNettools))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_NETTOOLS, NMDhcpNettools))
 #define NM_DHCP_NETTOOLS_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_NETTOOLS, NMDhcpNettoolsClass))
 #define NM_IS_DHCP_NETTOOLS(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_NETTOOLS))
@@ -143,12 +143,12 @@ lease_option_consume_route(const uint8_t **datap,
         if (!nm_dhcp_lease_data_consume_in_addr(&data, &n_data, &dest))
             return FALSE;
 
-        plen = _nm_utils_ip4_get_default_prefix0(dest);
+        plen = nm_ip4_addr_get_default_prefix0(dest);
         if (plen == 0)
             return FALSE;
     }
 
-    dest = nm_utils_ip4_address_clear_host_address(dest, plen);
+    dest = nm_ip4_addr_clear_host_address(dest, plen);
 
     if (!nm_dhcp_lease_data_consume_in_addr(&data, &n_data, &gateway))
         return FALSE;
@@ -239,16 +239,16 @@ lease_parse_address(NMDhcpNettools    *self /* for logging context only */,
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_SUBNET_MASK, &l_data, &l_data_len);
     if (r == N_DHCP4_E_UNSET) {
-        char str1[NM_UTILS_INET_ADDRSTRLEN];
-        char str2[NM_UTILS_INET_ADDRSTRLEN];
+        char str1[NM_INET_ADDRSTRLEN];
+        char str2[NM_INET_ADDRSTRLEN];
 
         /* Some DHCP servers may not set the subnet-mask (issue#1037).
          * Do the same as the dhclient plugin and use a default. */
-        a_plen    = _nm_utils_ip4_get_default_prefix(a_address.s_addr);
-        a_netmask = _nm_utils_ip4_prefix_to_netmask(a_plen);
+        a_plen    = nm_ip4_addr_get_default_prefix(a_address.s_addr);
+        a_netmask = nm_ip4_addr_netmask_from_prefix(a_plen);
         _LOGT("missing subnet mask (option 1). Guess %s based on IP address %s",
-              _nm_utils_inet4_ntop(a_netmask, str1),
-              _nm_utils_inet4_ntop(a_address.s_addr, str2));
+              nm_inet4_ntop(a_netmask, str1),
+              nm_inet4_ntop(a_address.s_addr, str2));
     } else {
         if (r != 0
             || !nm_dhcp_lease_data_parse_in_addr(l_data,
@@ -261,8 +261,8 @@ lease_parse_address(NMDhcpNettools    *self /* for logging context only */,
                                        "could not get netmask from lease");
             return FALSE;
         }
-        a_plen    = _nm_utils_ip4_netmask_to_prefix(a_netmask);
-        a_netmask = _nm_utils_ip4_prefix_to_netmask(a_plen);
+        a_plen    = nm_ip4_addr_netmask_to_prefix(a_netmask);
+        a_netmask = nm_ip4_addr_netmask_from_prefix(a_plen);
     }
 
     nm_dhcp_option_add_option_in_addr(options,
@@ -335,25 +335,25 @@ lease_parse_address_list(NDhcp4ClientLease       *lease,
     nm_str_buf_reset(sbuf);
 
     for (; l_data_len > 0; l_data_len -= 4, l_data += 4) {
-        char            addr_str[NM_UTILS_INET_ADDRSTRLEN];
+        char            addr_str[NM_INET_ADDRSTRLEN];
         const in_addr_t addr = unaligned_read_ne32(l_data);
 
         nm_str_buf_append_required_delimiter(sbuf, ' ');
-        nm_str_buf_append(sbuf, _nm_utils_inet4_ntop(addr, addr_str));
+        nm_str_buf_append(sbuf, nm_inet4_ntop(addr, addr_str));
 
         switch (option) {
         case NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER:
-            if (addr == 0 || nm_utils_ip4_address_is_loopback(addr)) {
+            if (addr == 0 || nm_ip4_addr_is_loopback(addr)) {
                 /* Skip localhost addresses, like also networkd does.
                  * See https://github.com/systemd/systemd/issues/4524. */
                 nm_dhcp_lease_log_invalid_option(iface,
                                                  AF_INET,
                                                  option,
                                                  "address %s is ignored",
-                                                 _nm_utils_inet4_ntop(addr, addr_str));
+                                                 nm_inet4_ntop(addr, addr_str));
                 continue;
             }
-            nm_l3_config_data_add_nameserver(l3cd, AF_INET, &addr);
+            nm_l3_config_data_add_nameserver_detail(l3cd, AF_INET, &addr, NULL);
             break;
         case NM_DHCP_OPTION_DHCP4_NIS_SERVERS:
             nm_l3_config_data_add_nis_server(l3cd, addr);
@@ -378,8 +378,8 @@ lease_parse_routes(NDhcp4ClientLease *lease,
                    GHashTable        *options,
                    NMStrBuf          *sbuf)
 {
-    char          dest_str[NM_UTILS_INET_ADDRSTRLEN];
-    char          gateway_str[NM_UTILS_INET_ADDRSTRLEN];
+    char          dest_str[NM_INET_ADDRSTRLEN];
+    char          gateway_str[NM_INET_ADDRSTRLEN];
     in_addr_t     dest;
     in_addr_t     gateway;
     uint8_t       plen;
@@ -412,8 +412,8 @@ lease_parse_routes(NDhcp4ClientLease *lease,
 
         nm_str_buf_reset(sbuf);
         while (lease_option_consume_route(&l_data, &l_data_len, TRUE, &dest, &plen, &gateway)) {
-            _nm_utils_inet4_ntop(dest, dest_str);
-            _nm_utils_inet4_ntop(gateway, gateway_str);
+            nm_inet4_ntop(dest, dest_str);
+            nm_inet4_ntop(gateway, gateway_str);
 
             nm_str_buf_append_required_delimiter(sbuf, ' ');
             nm_str_buf_append_printf(sbuf, "%s/%d %s", dest_str, (int) plen, gateway_str);
@@ -454,8 +454,8 @@ lease_parse_routes(NDhcp4ClientLease *lease,
         nm_str_buf_reset(sbuf);
 
         while (lease_option_consume_route(&l_data, &l_data_len, FALSE, &dest, &plen, &gateway)) {
-            _nm_utils_inet4_ntop(dest, dest_str);
-            _nm_utils_inet4_ntop(gateway, gateway_str);
+            nm_inet4_ntop(dest, dest_str);
+            nm_inet4_ntop(gateway, gateway_str);
 
             nm_str_buf_append_required_delimiter(sbuf, ' ');
             nm_str_buf_append_printf(sbuf, "%s/%d %s", dest_str, (int) plen, gateway_str);
@@ -501,7 +501,7 @@ lease_parse_routes(NDhcp4ClientLease *lease,
 
         while (nm_dhcp_lease_data_consume_in_addr(&l_data, &l_data_len, &gateway)) {
             nm_str_buf_append_required_delimiter(sbuf, ' ');
-            nm_str_buf_append(sbuf, _nm_utils_inet4_ntop(gateway, gateway_str));
+            nm_str_buf_append(sbuf, nm_inet4_ntop(gateway, gateway_str));
 
             if (gateway == 0) {
                 /* silently skip 0.0.0.0 */
@@ -868,7 +868,7 @@ lease_save(NMDhcpNettools *self, NDhcp4ClientLease *lease, const char *lease_fil
 {
     struct in_addr           a_address;
     nm_auto_str_buf NMStrBuf sbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_104, FALSE);
-    char                     addr_str[NM_UTILS_INET_ADDRSTRLEN];
+    char                     addr_str[NM_INET_ADDRSTRLEN];
     gs_free_error GError    *error = NULL;
 
     nm_assert(lease);
@@ -879,9 +879,7 @@ lease_save(NMDhcpNettools *self, NDhcp4ClientLease *lease, const char *lease_fil
         return;
 
     nm_str_buf_append(&sbuf, "# This is private data. Do not parse.\n");
-    nm_str_buf_append_printf(&sbuf,
-                             "ADDRESS=%s\n",
-                             _nm_utils_inet4_ntop(a_address.s_addr, addr_str));
+    nm_str_buf_append_printf(&sbuf, "ADDRESS=%s\n", nm_inet4_ntop(a_address.s_addr, addr_str));
 
     if (!g_file_set_contents(lease_file, nm_str_buf_get_str_unsafe(&sbuf), sbuf.len, &error))
         _LOGW("error saving lease to %s: %s", lease_file, error->message);
@@ -966,7 +964,7 @@ dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
 
         if (nm_dhcp_client_server_id_is_rejected(NM_DHCP_CLIENT(self), &server_id)) {
             _LOGD("server-id %s is in the reject-list, ignoring",
-                  nm_utils_inet_ntop(AF_INET, &server_id, addr_str));
+                  nm_inet_ntop(AF_INET, &server_id, addr_str));
             return;
         }
 
@@ -976,8 +974,8 @@ dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
         }
 
         _LOGT("selecting offered lease from %s for %s",
-              _nm_utils_inet4_ntop(server_id.s_addr, addr_str),
-              _nm_utils_inet4_ntop(yiaddr.s_addr, addr_str2));
+              nm_inet4_ntop(server_id.s_addr, addr_str),
+              nm_inet4_ntop(yiaddr.s_addr, addr_str2));
 
         r = n_dhcp4_client_lease_select(event->offer.lease);
 
@@ -1340,7 +1338,7 @@ ip4_start(NMDhcpClient *client, GError **error)
                                    NULL);
         nm_parse_env_file(contents, "ADDRESS", &s_addr);
         if (s_addr)
-            nm_utils_parse_inaddr_bin(AF_INET, s_addr, NULL, &last_addr);
+            nm_inet_parse_bin(AF_INET, s_addr, NULL, &last_addr);
     }
 
     if (last_addr.s_addr) {
diff --git a/src/core/dhcp/nm-dhcp-options.c b/src/core/dhcp/nm-dhcp-options.c
index a5bb06d1..d95fe016 100644
--- a/src/core/dhcp/nm-dhcp-options.c
+++ b/src/core/dhcp/nm-dhcp-options.c
@@ -438,9 +438,9 @@ nm_dhcp_option_add_option_in_addr(GHashTable *options,
                                   guint       option,
                                   in_addr_t   value)
 {
-    char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+    char sbuf[NM_INET_ADDRSTRLEN];
 
-    nm_dhcp_option_add_option(options, addr_family, option, _nm_utils_inet4_ntop(value, sbuf));
+    nm_dhcp_option_add_option(options, addr_family, option, nm_inet4_ntop(value, sbuf));
 }
 
 void
diff --git a/src/core/dhcp/nm-dhcp-systemd.c b/src/core/dhcp/nm-dhcp-systemd.c
index 7ce15d30..10990822 100644
--- a/src/core/dhcp/nm-dhcp-systemd.c
+++ b/src/core/dhcp/nm-dhcp-systemd.c
@@ -30,7 +30,7 @@
 
 #define NM_TYPE_DHCP_SYSTEMD (nm_dhcp_systemd_get_type())
 #define NM_DHCP_SYSTEMD(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_SYSTEMD, NMDhcpSystemd))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_SYSTEMD, NMDhcpSystemd))
 #define NM_DHCP_SYSTEMD_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_SYSTEMD, NMDhcpSystemdClass))
 #define NM_IS_DHCP_SYSTEMD(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_SYSTEMD))
@@ -74,7 +74,7 @@ lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GErro
     gs_unref_hashtable GHashTable          *options = NULL;
     struct in6_addr                         tmp_addr;
     const struct in6_addr                  *dns;
-    char                                    addr_str[NM_UTILS_INET_ADDRSTRLEN];
+    char                                    addr_str[NM_INET_ADDRSTRLEN];
     char                                  **domains;
     char                                  **ntp_fqdns;
     const struct in6_addr                  *ntp_addrs;
@@ -107,7 +107,7 @@ lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GErro
 
             nm_l3_config_data_add_address_6(l3cd, &address);
 
-            _nm_utils_inet6_ntop(&tmp_addr, addr_str);
+            nm_inet6_ntop(&tmp_addr, addr_str);
             g_string_append(nm_gstring_add_space_delimiter(str), addr_str);
 
             has_any_addresses = TRUE;
@@ -133,9 +133,9 @@ lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GErro
     if (num > 0) {
         nm_gstring_prepare(&str);
         for (i = 0; i < num; i++) {
-            _nm_utils_inet6_ntop(&dns[i], addr_str);
+            nm_inet6_ntop(&dns[i], addr_str);
             g_string_append(nm_gstring_add_space_delimiter(str), addr_str);
-            nm_l3_config_data_add_nameserver(l3cd, AF_INET6, &dns[i]);
+            nm_l3_config_data_add_nameserver_detail(l3cd, AF_INET6, &dns[i], NULL);
         }
         nm_dhcp_option_add_option(options, AF_INET6, NM_DHCP_OPTION_DHCP6_DNS_SERVERS, str->str);
     }
@@ -170,7 +170,7 @@ lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GErro
     num = sd_dhcp6_lease_get_ntp_addrs(lease, &ntp_addrs);
     if (num > 0) {
         for (i = 0; i < num; i++) {
-            _nm_utils_inet6_ntop(&ntp_addrs[i], addr_str);
+            nm_inet6_ntop(&ntp_addrs[i], addr_str);
             g_string_append(nm_gstring_add_space_delimiter(str), addr_str);
         }
     }
diff --git a/src/core/dhcp/nm-dhcp-utils.c b/src/core/dhcp/nm-dhcp-utils.c
index 1bffb3c4..ca1c0482 100644
--- a/src/core/dhcp/nm-dhcp-utils.c
+++ b/src/core/dhcp/nm-dhcp-utils.c
@@ -94,7 +94,7 @@ ip4_process_dhcpcd_rfc3442_routes(const char     *iface,
                 l3cd,
                 &((const NMPlatformIP4Route){
                     .rt_source  = NM_IP_CONFIG_SOURCE_DHCP,
-                    .network    = nm_utils_ip4_address_clear_host_address(rt_addr, rt_cidr),
+                    .network    = nm_ip4_addr_clear_host_address(rt_addr, rt_cidr),
                     .plen       = rt_cidr,
                     .gateway    = rt_route,
                     .pref_src   = address,
@@ -139,7 +139,7 @@ process_dhclient_rfc3442_route(const char *const **p_octets, NMPlatformIP4Route
         str_addr = g_strjoin(".", addr[0], addr[1], addr[2], addr[3], NULL);
         if (inet_pton(AF_INET, str_addr, &tmp_addr) <= 0)
             return FALSE;
-        v_network = nm_utils_ip4_address_clear_host_address(tmp_addr, v_plen);
+        v_network = nm_ip4_addr_clear_host_address(tmp_addr, v_plen);
     }
 
     next_hop = g_strjoin(".", o[0], o[1], o[2], o[3], NULL);
@@ -203,9 +203,9 @@ ip4_process_dhclient_rfc3442_routes(const char     *iface,
             _LOG2I(LOGD_DHCP4,
                    iface,
                    "  classless static route %s/%d gw %s",
-                   _nm_utils_inet4_ntop(route.network, b1),
+                   nm_inet4_ntop(route.network, b1),
                    route.plen,
-                   _nm_utils_inet4_ntop(route.gateway, b2));
+                   nm_inet4_ntop(route.gateway, b2));
         }
     }
 
@@ -324,8 +324,8 @@ process_classful_routes(const char     *iface,
          * The Static Routes option (option 33) does not provide a subnet mask
          * for each route - it is assumed that the subnet mask is implicit in
          * whatever network number is specified in each route entry */
-        route.plen = _nm_utils_ip4_get_default_prefix(rt_addr);
-        if (rt_addr & ~_nm_utils_ip4_prefix_to_netmask(route.plen)) {
+        route.plen = nm_ip4_addr_get_default_prefix(rt_addr);
+        if (rt_addr & ~nm_ip4_addr_netmask_from_prefix(route.plen)) {
             /* RFC 943: target not "this network"; using host routing */
             route.plen = 32;
         }
@@ -337,7 +337,7 @@ process_classful_routes(const char     *iface,
         route.metric_any    = TRUE;
         route.metric        = 0;
 
-        route.network = nm_utils_ip4_address_clear_host_address(route.network, route.plen);
+        route.network = nm_ip4_addr_clear_host_address(route.network, route.plen);
 
         nm_l3_config_data_add_route_4(l3cd, &route);
 
@@ -400,7 +400,7 @@ nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx,
     gboolean                                gateway_has = FALSE;
     guint32                                 gateway     = 0;
     guint8                                  plen        = 0;
-    char                                    sbuf[NM_UTILS_INET_ADDRSTRLEN];
+    char                                    sbuf[NM_INET_ADDRSTRLEN];
     guint32                                 now;
 
     g_return_val_if_fail(options != NULL, NULL);
@@ -414,7 +414,7 @@ nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx,
     };
 
     str = g_hash_table_lookup(options, "ip_address");
-    if (!str || !nm_utils_parse_inaddr_bin(AF_INET, str, NULL, &addr))
+    if (!str || !nm_inet_parse_bin(AF_INET, str, NULL, &addr))
         return NULL;
     if (addr == INADDR_ANY)
         return NULL;
@@ -423,11 +423,11 @@ nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx,
 
     str = g_hash_table_lookup(options, "subnet_mask");
     if (str && (inet_pton(AF_INET, str, &tmp_addr) > 0)) {
-        plen = _nm_utils_ip4_netmask_to_prefix(tmp_addr);
+        plen = nm_ip4_addr_netmask_to_prefix(tmp_addr);
         _LOG2I(LOGD_DHCP4, iface, "  plen %d (%s)", plen, str);
     } else {
         /* Get default netmask for the IP according to appropriate class. */
-        plen = _nm_utils_ip4_get_default_prefix(addr);
+        plen = nm_ip4_addr_get_default_prefix(addr);
         _LOG2I(LOGD_DHCP4, iface, "  plen %d (default)", plen);
     }
 
@@ -440,7 +440,7 @@ nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx,
         process_classful_routes(iface, options, l3cd, address.address);
 
     if (gateway) {
-        _LOG2I(LOGD_DHCP4, iface, "  gateway %s", _nm_utils_inet4_ntop(gateway, sbuf));
+        _LOG2I(LOGD_DHCP4, iface, "  gateway %s", nm_inet4_ntop(gateway, sbuf));
         gateway_has = TRUE;
     } else {
         /* If the gateway wasn't provided as a classless static route with a
@@ -499,7 +499,7 @@ nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx,
         for (s = dns; dns && *s; s++) {
             if (inet_pton(AF_INET, *s, &tmp_addr) > 0) {
                 if (tmp_addr) {
-                    nm_l3_config_data_add_nameserver(l3cd, AF_INET, &tmp_addr);
+                    nm_l3_config_data_add_nameserver_detail(l3cd, AF_INET, &tmp_addr, NULL);
                     _LOG2I(LOGD_DHCP4, iface, "  nameserver '%s'", *s);
                 }
             } else
@@ -593,7 +593,7 @@ nm_dhcp_utils_ip6_prefix_from_options(GHashTable *options)
 {
     gs_strfreev char   **split_addr = NULL;
     NMPlatformIP6Address address    = {
-           0,
+        0,
     };
     struct in6_addr tmp_addr;
     char           *str = NULL;
@@ -704,7 +704,7 @@ nm_dhcp_utils_ip6_config_from_options(NMDedupMultiIndex *multi_idx,
         for (s = dns; dns && *s; s++) {
             if (inet_pton(AF_INET6, *s, &tmp_addr) > 0) {
                 if (!IN6_IS_ADDR_UNSPECIFIED(&tmp_addr)) {
-                    nm_l3_config_data_add_nameserver(l3cd, AF_INET6, &tmp_addr);
+                    nm_l3_config_data_add_nameserver_detail(l3cd, AF_INET6, &tmp_addr, NULL);
                     _LOG2I(LOGD_DHCP6, iface, "  nameserver '%s'", *s);
                 }
             } else
diff --git a/src/core/dhcp/tests/test-dhcp-utils.c b/src/core/dhcp/tests/test-dhcp-utils.c
index 7597f516..1c6d6302 100644
--- a/src/core/dhcp/tests/test-dhcp-utils.c
+++ b/src/core/dhcp/tests/test-dhcp-utils.c
@@ -88,7 +88,6 @@ test_generic_options(void)
     const char                              *expected_route2_dest = "100.99.88.56";
     const char                              *expected_route2_gw   = "10.1.1.1";
     const char *const                       *strarr;
-    const in_addr_t                         *ia_arr;
     guint                                    u;
 
     options = fill_table(generic_options, NULL);
@@ -115,10 +114,10 @@ test_generic_options(void)
     g_assert_cmpstr(strarr[0], ==, expected_search1);
     g_assert_cmpstr(strarr[1], ==, expected_search2);
 
-    ia_arr = nm_l3_config_data_get_nameservers(l3cd, AF_INET, &u);
+    strarr = nm_l3_config_data_get_nameservers(l3cd, AF_INET, &u);
     g_assert_cmpint(u, ==, 2);
-    nmtst_assert_ip4_address(ia_arr[0], expected_dns1);
-    nmtst_assert_ip4_address(ia_arr[1], expected_dns2);
+    g_assert_cmpstr(strarr[0], ==, expected_dns1);
+    g_assert_cmpstr(strarr[1], ==, expected_dns2);
 
     g_assert_cmpint(nm_l3_config_data_get_num_routes(l3cd, AF_INET), ==, 3);
 
@@ -285,7 +284,7 @@ test_classless_static_routes_1(void)
     const char                              *expected_route2_dest = "10.0.0.0";
     const char                              *expected_route2_gw   = "10.17.66.41";
     static const Option                      data[]               = {
-                                           /* dhclient custom format */
+        /* dhclient custom format */
         {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 8 10 10 17 66 41"},
         {NULL, NULL}};
 
@@ -310,7 +309,7 @@ test_classless_static_routes_2(void)
     const char                              *expected_route2_dest = "10.0.0.0";
     const char                              *expected_route2_gw   = "10.17.66.41";
     static const Option                      data[]               = {
-                                           /* dhcpcd format */
+        /* dhcpcd format */
         {"classless_static_routes", "192.168.10.0/24 192.168.1.1 10.0.0.0/8 10.17.66.41"},
         {NULL, NULL}};
 
@@ -336,7 +335,7 @@ test_fedora_dhclient_classless_static_routes(void)
     const char                              *expected_route2_gw   = "10.34.255.6";
     const char                              *expected_gateway     = "192.168.0.113";
     static const Option                      data[]               = {
-                                           /* Fedora dhclient format */
+        /* Fedora dhclient format */
         {"classless_static_routes",
                                             "0 192.168.0.113 25.129.210.177.132 192.168.0.113 7.2 10.34.255.6"},
         {NULL, NULL}};
@@ -362,7 +361,7 @@ test_dhclient_invalid_classless_routes_1(void)
     const char                              *expected_route1_dest = "192.168.10.0";
     const char                              *expected_route1_gw   = "192.168.1.1";
     static const Option                      data[]               = {
-                                           /* dhclient format */
+        /* dhclient format */
         {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 45 10 17 66 41"},
         {NULL, NULL}};
 
@@ -389,7 +388,7 @@ test_dhcpcd_invalid_classless_routes_1(void)
     const char                              *expected_route2_dest = "100.99.88.56";
     const char                              *expected_route2_gw   = "10.1.1.1";
     static const Option                      data[]               = {
-                                           /* dhcpcd format */
+        /* dhcpcd format */
         {"classless_static_routes", "192.168.10.0/24 192.168.1.1 10.0.adfadf/44 10.17.66.41"},
         {NULL, NULL}};
 
@@ -419,8 +418,8 @@ test_dhclient_invalid_classless_routes_2(void)
     const char                              *expected_route2_dest = "100.99.88.56";
     const char                              *expected_route2_gw   = "10.1.1.1";
     static const Option                      data[]               = {
-                                           {"rfc3442_classless_static_routes", "45 10 17 66 41 24 192 168 10 192 168 1 1"},
-                                           {NULL, NULL}};
+        {"rfc3442_classless_static_routes", "45 10 17 66 41 24 192 168 10 192 168 1 1"},
+        {NULL, NULL}};
 
     options = fill_table(generic_options, NULL);
     options = fill_table(data, options);
@@ -448,8 +447,8 @@ test_dhcpcd_invalid_classless_routes_2(void)
     const char                              *expected_route2_dest = "100.99.88.56";
     const char                              *expected_route2_gw   = "10.1.1.1";
     static const Option                      data[]               = {
-                                           {"classless_static_routes", "10.0.adfadf/44 10.17.66.41 192.168.10.0/24 192.168.1.1"},
-                                           {NULL, NULL}};
+        {"classless_static_routes", "10.0.adfadf/44 10.17.66.41 192.168.10.0/24 192.168.1.1"},
+        {NULL, NULL}};
 
     options = fill_table(generic_options, NULL);
     options = fill_table(data, options);
@@ -477,8 +476,8 @@ test_dhclient_invalid_classless_routes_3(void)
     const char                              *expected_route1_dest = "192.168.10.0";
     const char                              *expected_route1_gw   = "192.168.1.1";
     static const Option                      data[]               = {
-                                           {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 32 128 10 17 66 41"},
-                                           {NULL, NULL}};
+        {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 32 128 10 17 66 41"},
+        {NULL, NULL}};
 
     options = fill_table(generic_options, NULL);
     options = fill_table(data, options);
@@ -501,8 +500,8 @@ test_dhcpcd_invalid_classless_routes_3(void)
     const char                              *expected_route1_dest = "192.168.10.0";
     const char                              *expected_route1_gw   = "192.168.1.1";
     static Option                            data[]               = {
-                                                 {"classless_static_routes", "192.168.10.0/24 192.168.1.1 128/32 10.17.66.41"},
-                                                 {NULL, NULL}};
+        {"classless_static_routes", "192.168.10.0/24 192.168.1.1 128/32 10.17.66.41"},
+        {NULL, NULL}};
 
     options = fill_table(generic_options, NULL);
     options = fill_table(data, options);
@@ -526,8 +525,8 @@ test_dhclient_gw_in_classless_routes(void)
     const char                              *expected_route1_gw   = "192.168.1.1";
     const char                              *expected_gateway     = "192.2.3.4";
     static Option                            data[]               = {
-                                                 {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 0 192 2 3 4"},
-                                                 {NULL, NULL}};
+        {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 0 192 2 3 4"},
+        {NULL, NULL}};
 
     options = fill_table(generic_options, NULL);
     options = fill_table(data, options);
@@ -550,8 +549,8 @@ test_dhcpcd_gw_in_classless_routes(void)
     const char                              *expected_route1_gw   = "192.168.1.1";
     const char                              *expected_gateway     = "192.2.3.4";
     static Option                            data[]               = {
-                                                 {"classless_static_routes", "192.168.10.0/24 192.168.1.1 0.0.0.0/0 192.2.3.4"},
-                                                 {NULL, NULL}};
+        {"classless_static_routes", "192.168.10.0/24 192.168.1.1 0.0.0.0/0 192.2.3.4"},
+        {NULL, NULL}};
 
     options = fill_table(generic_options, NULL);
     options = fill_table(data, options);
diff --git a/src/core/dns/nm-dns-dnsmasq.c b/src/core/dns/nm-dns-dnsmasq.c
index cc7c03a2..8ae1bb96 100644
--- a/src/core/dns/nm-dns-dnsmasq.c
+++ b/src/core/dns/nm-dns-dnsmasq.c
@@ -801,13 +801,13 @@ add_dnsmasq_nameserver(NMDnsDnsmasq    *self,
     g_variant_builder_close(servers);
 }
 
-#define IP_ADDR_TO_STRING_BUFLEN (NM_UTILS_INET_ADDRSTRLEN + 1 + IFNAMSIZ)
+#define IP_ADDR_TO_STRING_BUFLEN (NM_INET_ADDRSTRLEN + 1 + IFNAMSIZ)
 
 static const char *
 ip_addr_to_string(int addr_family, gconstpointer addr, const char *iface, char *out_buf)
 {
     int         n_written;
-    char        buf2[NM_UTILS_INET_ADDRSTRLEN];
+    char        buf2[NM_INET_ADDRSTRLEN];
     const char *separator;
 
     nm_assert_addr_family(addr_family);
@@ -815,13 +815,13 @@ ip_addr_to_string(int addr_family, gconstpointer addr, const char *iface, char *
     nm_assert(out_buf);
 
     if (addr_family == AF_INET) {
-        nm_utils_inet_ntop(addr_family, addr, buf2);
+        nm_inet_ntop(addr_family, addr, buf2);
         separator = "@";
     } else {
         if (IN6_IS_ADDR_V4MAPPED(addr))
-            _nm_utils_inet4_ntop(((const struct in6_addr *) addr)->s6_addr32[3], buf2);
+            nm_inet4_ntop(((const struct in6_addr *) addr)->s6_addr32[3], buf2);
         else
-            _nm_utils_inet6_ntop(addr, buf2);
+            nm_inet6_ntop(addr, buf2);
         /* Need to scope link-local addresses with %<zone-id>. Before dnsmasq 2.58,
          * only '@' was supported as delimiter. Since 2.58, '@' and '%' are
          * supported. Due to a bug, since 2.73 only '%' works properly as "server"
@@ -868,22 +868,24 @@ add_global_config(NMDnsDnsmasq            *self,
 static void
 add_ip_config(NMDnsDnsmasq *self, GVariantBuilder *servers, const NMDnsConfigIPData *ip_data)
 {
-    const char   *iface;
-    const char   *domain;
-    char          ip_addr_to_string_buf[IP_ADDR_TO_STRING_BUFLEN];
-    gconstpointer nameservers;
-    guint         num;
-    guint         i;
-    guint         j;
+    const char        *iface;
+    const char        *domain;
+    char               ip_addr_to_string_buf[IP_ADDR_TO_STRING_BUFLEN];
+    const char *const *strarr;
+    guint              num;
+    guint              i;
+    guint              j;
 
     iface = nm_platform_link_get_name(NM_PLATFORM_GET, ip_data->data->ifindex);
 
-    nameservers = nm_l3_config_data_get_nameservers(ip_data->l3cd, ip_data->addr_family, &num);
+    strarr = nm_l3_config_data_get_nameservers(ip_data->l3cd, ip_data->addr_family, &num);
     for (i = 0; i < num; i++) {
-        gconstpointer addr;
+        NMIPAddr a;
 
-        addr = nm_ip_addr_from_packed_array(ip_data->addr_family, nameservers, i);
-        ip_addr_to_string(ip_data->addr_family, addr, iface, ip_addr_to_string_buf);
+        if (!nm_utils_dnsname_parse_assert(ip_data->addr_family, strarr[i], NULL, &a, NULL))
+            continue;
+
+        ip_addr_to_string(ip_data->addr_family, &a, iface, ip_addr_to_string_buf);
 
         if (!ip_data->domains.has_default_route_explicit && ip_data->domains.has_default_route)
             add_dnsmasq_nameserver(self, servers, ip_addr_to_string_buf, NULL);
diff --git a/src/core/dns/nm-dns-dnsmasq.h b/src/core/dns/nm-dns-dnsmasq.h
index 98761d3d..c2f2a115 100644
--- a/src/core/dns/nm-dns-dnsmasq.h
+++ b/src/core/dns/nm-dns-dnsmasq.h
@@ -10,7 +10,8 @@
 #include "nm-dns-manager.h"
 
 #define NM_TYPE_DNS_DNSMASQ (nm_dns_dnsmasq_get_type())
-#define NM_DNS_DNSMASQ(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DNS_DNSMASQ, NMDnsDnsmasq))
+#define NM_DNS_DNSMASQ(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DNS_DNSMASQ, NMDnsDnsmasq))
 #define NM_DNS_DNSMASQ_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DNS_DNSMASQ, NMDnsDnsmasqClass))
 #define NM_IS_DNS_DNSMASQ(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DNS_DNSMASQ))
diff --git a/src/core/dns/nm-dns-manager.c b/src/core/dns/nm-dns-manager.c
index edb8ce0f..6ee2e816 100644
--- a/src/core/dns/nm-dns-manager.c
+++ b/src/core/dns/nm-dns-manager.c
@@ -552,29 +552,29 @@ add_dns_domains(GPtrArray            *array,
 static void
 merge_one_l3cd(NMResolvConfData *rc, int addr_family, int ifindex, const NML3ConfigData *l3cd)
 {
-    char               buf[NM_UTILS_INET_ADDRSTRLEN + 50];
+    char               buf[NM_INET_ADDRSTRLEN + 50];
     gboolean           has_trust_ad;
     guint              num_nameservers;
     guint              num;
     guint              i;
-    gconstpointer      nameservers;
-    const char *const *strv;
+    const char *const *strarr;
 
     nm_assert(ifindex == nm_l3_config_data_get_ifindex(l3cd));
 
-    nameservers = nm_l3_config_data_get_nameservers(l3cd, addr_family, &num_nameservers);
+    strarr = nm_l3_config_data_get_nameservers(l3cd, addr_family, &num_nameservers);
     for (i = 0; i < num_nameservers; i++) {
-        const NMIPAddr *addr;
+        NMIPAddr a;
 
-        addr = nm_ip_addr_from_packed_array(addr_family, nameservers, i);
+        if (!nm_utils_dnsname_parse_assert(addr_family, strarr[i], NULL, &a, NULL))
+            continue;
 
         if (addr_family == AF_INET)
-            nm_utils_inet_ntop(addr_family, addr, buf);
-        else if (IN6_IS_ADDR_V4MAPPED(addr))
-            _nm_utils_inet4_ntop(addr->addr6.s6_addr32[3], buf);
+            nm_inet_ntop(addr_family, &a, buf);
+        else if (IN6_IS_ADDR_V4MAPPED(&a))
+            nm_inet4_ntop(a.addr6.s6_addr32[3], buf);
         else {
-            _nm_utils_inet6_ntop(&addr->addr6, buf);
-            if (IN6_IS_ADDR_LINKLOCAL(addr)) {
+            nm_inet6_ntop(&a.addr6, buf);
+            if (IN6_IS_ADDR_LINKLOCAL(&a)) {
                 const char *ifname;
 
                 ifname = nm_platform_link_get_name(NM_PLATFORM_GET, ifindex);
@@ -591,9 +591,9 @@ merge_one_l3cd(NMResolvConfData *rc, int addr_family, int ifindex, const NML3Con
     add_dns_domains(rc->searches, addr_family, l3cd, FALSE, TRUE);
 
     has_trust_ad = FALSE;
-    strv         = nm_l3_config_data_get_dns_options(l3cd, addr_family, &num);
+    strarr       = nm_l3_config_data_get_dns_options(l3cd, addr_family, &num);
     for (i = 0; i < num; i++) {
-        const char *option = strv[i];
+        const char *option = strarr[i];
 
         if (nm_streq(option, NM_SETTING_DNS_OPTION_TRUST_AD)) {
             has_trust_ad = TRUE;
@@ -619,7 +619,7 @@ merge_one_l3cd(NMResolvConfData *rc, int addr_family, int ifindex, const NML3Con
 
         nis_servers = nm_l3_config_data_get_nis_servers(l3cd, &num);
         for (i = 0; i < num; i++)
-            add_string_item(rc->nis_servers, _nm_utils_inet4_ntop(nis_servers[i], buf), TRUE);
+            add_string_item(rc->nis_servers, nm_inet4_ntop(nis_servers[i], buf), TRUE);
 
         if ((nis_domain = nm_l3_config_data_get_nis_domain(l3cd))) {
             /* FIXME: handle multiple domains */
@@ -1201,14 +1201,19 @@ compute_hash(NMDnsManager *self, const NMGlobalDnsConfig *global, guint8 buffer[
 
     if (global)
         nm_global_dns_config_update_checksum(global, sum);
-    else {
+
+    if (!global || !nm_global_dns_config_lookup_domain(global, "*")) {
         const CList *head;
 
         /* FIXME(ip-config-checksum): this relies on the fact that an IP
          * configuration without DNS parameters gives a zero checksum. */
         head = _mgr_get_ip_data_lst_head(self);
-        c_list_for_each_entry (ip_data, head, ip_data_lst)
-            nm_ip_config_dns_hash(ip_data->l3cd, sum, ip_data->addr_family);
+        c_list_for_each_entry (ip_data, head, ip_data_lst) {
+            nm_l3_config_data_hash_dns(ip_data->l3cd,
+                                       sum,
+                                       ip_data->addr_family,
+                                       ip_data->ip_config_type);
+        }
     }
 
     nm_utils_checksum_get_digest_len(sum, buffer, HASH_LEN);
@@ -1244,13 +1249,15 @@ merge_global_dns_config(NMResolvConfData *rc, NMGlobalDnsConfig *global_conf)
     }
 
     default_domain = nm_global_dns_config_lookup_domain(global_conf, "*");
-    nm_assert(default_domain);
+    if (!default_domain)
+        return TRUE;
 
     servers = nm_global_dns_domain_get_servers(default_domain);
-    if (servers) {
-        for (i = 0; servers[i]; i++)
-            add_string_item(rc->nameservers, servers[i], TRUE);
-    }
+    if (!servers)
+        return TRUE;
+
+    for (i = 0; servers[i]; i++)
+        add_string_item(rc->nameservers, servers[i], TRUE);
 
     return TRUE;
 }
@@ -1258,19 +1265,21 @@ merge_global_dns_config(NMResolvConfData *rc, NMGlobalDnsConfig *global_conf)
 static const char *
 get_nameserver_list(int addr_family, const NML3ConfigData *l3cd, NMStrBuf *tmp_strbuf)
 {
-    char          buf[NM_UTILS_INET_ADDRSTRLEN];
-    guint         num;
-    guint         i;
-    gconstpointer nameservers;
+    char               buf[NM_INET_ADDRSTRLEN];
+    guint              num;
+    guint              i;
+    const char *const *strarr;
 
     nm_str_buf_reset(tmp_strbuf);
 
-    nameservers = nm_l3_config_data_get_nameservers(l3cd, addr_family, &num);
+    strarr = nm_l3_config_data_get_nameservers(l3cd, addr_family, &num);
     for (i = 0; i < num; i++) {
-        const NMIPAddr *addr;
+        NMIPAddr a;
+
+        if (!nm_utils_dnsname_parse_assert(addr_family, strarr[i], NULL, &a, NULL))
+            continue;
 
-        addr = nm_ip_addr_from_packed_array(addr_family, nameservers, i);
-        nm_utils_inet_ntop(addr_family, addr->addr_ptr, buf);
+        nm_inet_ntop(addr_family, &a, buf);
         if (i > 0)
             nm_str_buf_append_c(tmp_strbuf, ' ');
         nm_str_buf_append(tmp_strbuf, buf);
@@ -1311,7 +1320,8 @@ _collect_resolv_conf_data(NMDnsManager      *self,
 
     if (global_config)
         merge_global_dns_config(&rc, global_config);
-    else {
+
+    if (!global_config || !nm_global_dns_config_lookup_domain(global_config, "*")) {
         nm_auto_str_buf NMStrBuf tmp_strbuf = NM_STR_BUF_INIT(0, FALSE);
         int                      first_prio = 0;
         const NMDnsConfigIPData *ip_data;
@@ -2090,8 +2100,7 @@ nm_dns_manager_set_hostname(NMDnsManager *self, const char *hostname, gboolean s
 
     /* Certain hostnames we don't want to include in resolv.conf 'searches' */
     if (hostname && nm_utils_is_specific_hostname(hostname)
-        && !g_str_has_suffix(hostname, ".in-addr.arpa")
-        && !nm_utils_ipaddr_is_valid(AF_UNSPEC, hostname)) {
+        && !g_str_has_suffix(hostname, ".in-addr.arpa") && !nm_inet_is_valid(AF_UNSPEC, hostname)) {
         domain = strchr(hostname, '.');
         if (domain) {
             domain++;
@@ -2555,14 +2564,12 @@ config_changed_cb(NMConfig           *config,
     }
 }
 
-static GVariant *
-_get_global_config_variant(NMGlobalDnsConfig *global)
+static void
+_get_global_config_variant(GVariantBuilder *builder, NMGlobalDnsConfig *global)
 {
     NMGlobalDnsDomain *domain;
-    GVariantBuilder    builder;
     guint              i, num;
 
-    g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}"));
     num = nm_global_dns_config_get_num_domains(global);
     for (i = 0; i < num; i++) {
         GVariantBuilder    conf_builder;
@@ -2598,10 +2605,8 @@ _get_global_config_variant(NMGlobalDnsConfig *global)
                               "priority",
                               g_variant_new_int32(NM_DNS_PRIORITY_DEFAULT_NORMAL));
 
-        g_variant_builder_add(&builder, "a{sv}", &conf_builder);
+        g_variant_builder_add(builder, "a{sv}", &conf_builder);
     }
-
-    return g_variant_ref_sink(g_variant_builder_end(&builder));
 }
 
 static GVariant *
@@ -2618,28 +2623,25 @@ _get_config_variant(NMDnsManager *self)
     if (priv->config_variant)
         return priv->config_variant;
 
-    global_config = nm_config_data_get_global_dns_config(nm_config_get_data(priv->config));
-    if (global_config) {
-        priv->config_variant = _get_global_config_variant(global_config);
-        _LOGT("current configuration: %s", (str = g_variant_print(priv->config_variant, TRUE)));
-        return priv->config_variant;
-    }
-
     g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}"));
 
+    global_config = nm_config_data_get_global_dns_config(nm_config_get_data(priv->config));
+    if (global_config)
+        _get_global_config_variant(&builder, global_config);
+
     head = _mgr_get_ip_data_lst_head(self);
     c_list_for_each_entry (ip_data, head, ip_data_lst) {
-        GVariantBuilder entry_builder;
-        GVariantBuilder strv_builder;
-        guint           num;
-        guint           num_domains;
-        guint           num_searches;
-        guint           i;
-        char            buf[NM_UTILS_INET_ADDRSTRLEN];
-        const char     *ifname;
-        gconstpointer   nameservers;
-
-        nameservers = nm_l3_config_data_get_nameservers(ip_data->l3cd, ip_data->addr_family, &num);
+        GVariantBuilder    entry_builder;
+        GVariantBuilder    strv_builder;
+        guint              num;
+        guint              num_domains;
+        guint              num_searches;
+        guint              i;
+        char               buf[NM_INET_ADDRSTRLEN];
+        const char        *ifname;
+        const char *const *strarr;
+
+        strarr = nm_l3_config_data_get_nameservers(ip_data->l3cd, ip_data->addr_family, &num);
         if (num == 0)
             continue;
 
@@ -2647,12 +2649,12 @@ _get_config_variant(NMDnsManager *self)
 
         g_variant_builder_init(&strv_builder, G_VARIANT_TYPE("as"));
         for (i = 0; i < num; i++) {
-            const NMIPAddr *addr;
+            NMIPAddr a;
+
+            if (!nm_utils_dnsname_parse_assert(ip_data->addr_family, strarr[i], NULL, &a, NULL))
+                continue;
 
-            addr = nm_ip_addr_from_packed_array(ip_data->addr_family, nameservers, i);
-            g_variant_builder_add(&strv_builder,
-                                  "s",
-                                  nm_utils_inet_ntop(ip_data->addr_family, addr, buf));
+            g_variant_builder_add(&strv_builder, "s", nm_inet_ntop(ip_data->addr_family, &a, buf));
         }
         g_variant_builder_add(&entry_builder,
                               "{sv}",
diff --git a/src/core/dns/nm-dns-manager.h b/src/core/dns/nm-dns-manager.h
index 210f9f6c..42f9dec5 100644
--- a/src/core/dns/nm-dns-manager.h
+++ b/src/core/dns/nm-dns-manager.h
@@ -12,16 +12,6 @@
 #include "nm-setting-connection.h"
 #include "nm-dns-plugin.h"
 
-typedef enum {
-    NM_DNS_IP_CONFIG_TYPE_REMOVED = -1,
-
-    NM_DNS_IP_CONFIG_TYPE_DEFAULT = 0,
-    NM_DNS_IP_CONFIG_TYPE_BEST_DEVICE,
-    NM_DNS_IP_CONFIG_TYPE_VPN,
-} NMDnsIPConfigType;
-
-/*****************************************************************************/
-
 struct _NMDnsConfigData;
 struct _NMDnsManager;
 
@@ -71,7 +61,7 @@ typedef struct _NMDnsConfigData {
 /*****************************************************************************/
 
 #define NM_TYPE_DNS_MANAGER (nm_dns_manager_get_type())
-#define NM_DNS_MANAGER(o)   (G_TYPE_CHECK_INSTANCE_CAST((o), NM_TYPE_DNS_MANAGER, NMDnsManager))
+#define NM_DNS_MANAGER(o)   (_NM_G_TYPE_CHECK_INSTANCE_CAST((o), NM_TYPE_DNS_MANAGER, NMDnsManager))
 #define NM_DNS_MANAGER_CLASS(k) \
     (G_TYPE_CHECK_CLASS_CAST((k), NM_TYPE_DNS_MANAGER, NMDnsManagerClass))
 #define NM_IS_DNS_MANAGER(o)       (G_TYPE_CHECK_INSTANCE_TYPE((o), NM_TYPE_DNS_MANAGER))
diff --git a/src/core/dns/nm-dns-plugin.h b/src/core/dns/nm-dns-plugin.h
index 24d6083b..364f15ce 100644
--- a/src/core/dns/nm-dns-plugin.h
+++ b/src/core/dns/nm-dns-plugin.h
@@ -11,7 +11,7 @@
 #include "nm-config-data.h"
 
 #define NM_TYPE_DNS_PLUGIN (nm_dns_plugin_get_type())
-#define NM_DNS_PLUGIN(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DNS_PLUGIN, NMDnsPlugin))
+#define NM_DNS_PLUGIN(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DNS_PLUGIN, NMDnsPlugin))
 #define NM_DNS_PLUGIN_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DNS_PLUGIN, NMDnsPluginClass))
 #define NM_IS_DNS_PLUGIN(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DNS_PLUGIN))
diff --git a/src/core/dns/nm-dns-systemd-resolved.c b/src/core/dns/nm-dns-systemd-resolved.c
index e0b94647..24c7c774 100644
--- a/src/core/dns/nm-dns-systemd-resolved.c
+++ b/src/core/dns/nm-dns-systemd-resolved.c
@@ -36,6 +36,7 @@
 /* define a variable, so that we can compare the operation with pointer equality. */
 static const char *const DBUS_OP_SET_LINK_DEFAULT_ROUTE = "SetLinkDefaultRoute";
 static const char *const DBUS_OP_SET_LINK_DNS_OVER_TLS  = "SetLinkDNSOverTLS";
+static const char *const DBUS_OP_SET_LINK_DNS_EX        = "SetLinkDNSEx";
 
 /*****************************************************************************/
 
@@ -90,12 +91,11 @@ typedef struct {
     bool             dbus_initied : 1;
     bool             send_updates_waiting : 1;
     bool             update_pending : 1;
-    /* These two variables ensure that the log is not spammed with
-     * API (not) supported messages.
-     * They can be removed when no distro uses systemd-resolved < v240 anymore
-     */
-    NMTernary has_link_default_route : 3;
-    NMTernary has_link_dns_over_tls : 3;
+
+    /* Detect support for the respective D-Bus API. */
+    NMTernary has_set_link_default_route : 3;
+    NMTernary has_set_link_dns_over_tls : 3;
+    NMTernary has_set_link_dns_ex : 3;
 } NMDnsSystemdResolvedPrivate;
 
 struct _NMDnsSystemdResolved {
@@ -148,6 +148,8 @@ static void _resolve_complete_error(NMDnsSystemdResolvedResolveHandle *handle, G
 
 static void _resolve_start(NMDnsSystemdResolved *self, NMDnsSystemdResolvedResolveHandle *handle);
 
+static void send_updates(NMDnsSystemdResolved *self);
+
 /*****************************************************************************/
 
 static gboolean
@@ -276,6 +278,7 @@ call_done(GObject *source, GAsyncResult *r, gpointer user_data)
     NMLogLevel                   log_level;
     const char                  *operation;
     int                          ifindex;
+    gboolean                     reconfigure = FALSE;
 
     request_item = user_data;
     self         = request_item->self;
@@ -290,30 +293,52 @@ call_done(GObject *source, GAsyncResult *r, gpointer user_data)
         goto out_dec_pending;
 
     if (v) {
-        if (operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE
-            && priv->has_link_default_route == NM_TERNARY_DEFAULT) {
-            priv->has_link_default_route = NM_TERNARY_TRUE;
-            _LOGD("systemd-resolved support for SetLinkDefaultRoute(): API supported");
-        }
-        if (operation == DBUS_OP_SET_LINK_DNS_OVER_TLS
-            && priv->has_link_dns_over_tls == NM_TERNARY_DEFAULT) {
-            priv->has_link_dns_over_tls = NM_TERNARY_TRUE;
-            _LOGD("systemd-resolved support for SetLinkDNSOverTLS(): API supported");
+        if (operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE) {
+            if (priv->has_set_link_default_route == NM_TERNARY_DEFAULT) {
+                priv->has_set_link_default_route = NM_TERNARY_TRUE;
+                _LOGD("systemd-resolved support for SetLinkDefaultRoute(): API supported");
+            }
+        } else if (operation == DBUS_OP_SET_LINK_DNS_OVER_TLS) {
+            if (priv->has_set_link_dns_over_tls == NM_TERNARY_DEFAULT) {
+                priv->has_set_link_dns_over_tls = NM_TERNARY_TRUE;
+                _LOGD("systemd-resolved support for SetLinkDNSOverTLS(): API supported");
+            }
+        } else if (operation == DBUS_OP_SET_LINK_DNS_EX) {
+            if (priv->has_set_link_dns_ex == NM_TERNARY_DEFAULT) {
+                priv->has_set_link_dns_ex = NM_TERNARY_TRUE;
+                _LOGD("systemd-resolved support for SetLinkDNSEx(): API supported");
+            }
         }
         priv->send_updates_warn_ratelimited = FALSE;
         goto out_dec_pending;
     }
 
     if (nm_g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD)) {
-        if (priv->has_link_default_route == NM_TERNARY_DEFAULT
-            && operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE) {
-            priv->has_link_default_route = NM_TERNARY_FALSE;
-            _LOGD("systemd-resolved support for SetLinkDefaultRoute(): API not supported");
-        }
-        if (priv->has_link_dns_over_tls == NM_TERNARY_DEFAULT
-            && operation == DBUS_OP_SET_LINK_DNS_OVER_TLS) {
-            priv->has_link_dns_over_tls = NM_TERNARY_FALSE;
-            _LOGD("systemd-resolved support for SetLinkDNSOverTLS(): API not supported");
+        if (operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE) {
+            if (priv->has_set_link_default_route == NM_TERNARY_DEFAULT) {
+                priv->has_set_link_default_route = NM_TERNARY_FALSE;
+                _LOGD("systemd-resolved support for SetLinkDefaultRoute(): API not supported");
+            }
+        } else if (operation == DBUS_OP_SET_LINK_DNS_OVER_TLS) {
+            if (priv->has_set_link_dns_over_tls == NM_TERNARY_DEFAULT) {
+                priv->has_set_link_dns_over_tls = NM_TERNARY_FALSE;
+                _LOGD("systemd-resolved support for SetLinkDNSOverTLS(): API not supported");
+            }
+        } else if (operation == DBUS_OP_SET_LINK_DNS_EX) {
+            if (priv->has_set_link_dns_ex == NM_TERNARY_DEFAULT) {
+                priv->has_set_link_dns_ex = NM_TERNARY_FALSE;
+                _LOGD("systemd-resolved support for SetLinkDNSEx(): API not supported");
+
+                _LOGW("systemd-resolved does not support SetLinkDNSEx API (v246). "
+                      "Cannot set DoT server name (SNI)");
+
+                /* We need to reconfigure with the SetLinkDNS fallback.
+                 *
+                 * In the other cases above, there is no need to reconfigure anything.
+                 * We won't retry SetLinkDefaultRoute/SetLinkDNSOverTLS anymore, but there
+                 * is nothing else we can do about that. */
+                reconfigure = TRUE;
+            }
         }
         goto out_dec_pending;
     }
@@ -335,21 +360,29 @@ out_dec_pending:
          * we must wrap up fast, and not hang an undefined amount time. */
         g_object_unref(self);
     }
+
+    if (reconfigure) {
+        priv->send_updates_waiting = TRUE;
+        send_updates(self);
+    }
 }
 
 static gboolean
 update_add_ip_config(NMDnsSystemdResolved    *self,
+                     const NMDnsConfigIPData *ip_data,
                      GVariantBuilder         *dns,
+                     GVariantBuilder         *dns_ex,
                      GVariantBuilder         *domains,
-                     const NMDnsConfigIPData *ip_data)
+                     gboolean                *out_require_dns_ex)
 {
-    gsize         addr_size;
-    guint         n;
-    guint         i;
-    gboolean      is_routing;
-    const char   *domain;
-    gboolean      has_config = FALSE;
-    gconstpointer nameservers;
+    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    gsize                        addr_size;
+    guint                        n;
+    guint                        i;
+    gboolean                     is_routing;
+    const char                  *domain;
+    gboolean                     has_config = FALSE;
+    const char *const           *strarr;
 
     addr_size = nm_utils_addr_family_to_size(ip_data->addr_family);
 
@@ -361,29 +394,52 @@ update_add_ip_config(NMDnsSystemdResolved    *self,
         return FALSE;
     }
 
-    nameservers = nm_l3_config_data_get_nameservers(ip_data->l3cd, ip_data->addr_family, &n);
+    strarr = nm_l3_config_data_get_nameservers(ip_data->l3cd, ip_data->addr_family, &n);
     for (i = 0; i < n; i++) {
-        g_variant_builder_open(dns, G_VARIANT_TYPE("(iay)"));
-        g_variant_builder_add(dns, "i", ip_data->addr_family);
-        g_variant_builder_add_value(
-            dns,
-            nm_g_variant_new_ay(nm_ip_addr_from_packed_array(ip_data->addr_family, nameservers, i),
-                                addr_size));
-        g_variant_builder_close(dns);
-        has_config = TRUE;
-    }
+        const char *server_name;
+        NMIPAddr    a;
+
+        if (!nm_utils_dnsname_parse_assert(ip_data->addr_family, strarr[i], NULL, &a, &server_name))
+            continue;
 
-    if (!ip_data->domains.has_default_route_explicit
-        && ip_data->domains.has_default_route_exclusive) {
-        g_variant_builder_add(domains, "(sb)", ".", TRUE);
+        if (server_name) {
+            NM_SET_OUT(out_require_dns_ex, TRUE);
+            if (priv->has_set_link_dns_ex == FALSE) {
+                /* The caller won't care about this result anymore. We can skip setting it. */
+                dns = NULL;
+            }
+        }
+
+        if (dns_ex) {
+            g_variant_builder_open(dns_ex, G_VARIANT_TYPE("(iayqs)"));
+            g_variant_builder_add(dns_ex, "i", ip_data->addr_family);
+            g_variant_builder_add_value(dns_ex, nm_g_variant_new_ay((gconstpointer) &a, addr_size));
+            g_variant_builder_add(dns_ex, "q", 0);
+            g_variant_builder_add(dns_ex, "s", server_name ?: "");
+            g_variant_builder_close(dns_ex);
+        }
+        if (dns) {
+            g_variant_builder_open(dns, G_VARIANT_TYPE("(iay)"));
+            g_variant_builder_add(dns, "i", ip_data->addr_family);
+            g_variant_builder_add_value(dns, nm_g_variant_new_ay((gconstpointer) &a, addr_size));
+            g_variant_builder_close(dns);
+        }
         has_config = TRUE;
     }
-    if (ip_data->domains.search) {
-        for (i = 0; ip_data->domains.search[i]; i++) {
-            domain = nm_utils_parse_dns_domain(ip_data->domains.search[i], &is_routing);
-            g_variant_builder_add(domains, "(sb)", domain[0] ? domain : ".", is_routing);
+
+    if (!has_config || domains) {
+        if (!ip_data->domains.has_default_route_explicit
+            && ip_data->domains.has_default_route_exclusive) {
+            g_variant_builder_add(domains, "(sb)", ".", TRUE);
             has_config = TRUE;
         }
+        if (ip_data->domains.search) {
+            for (i = 0; ip_data->domains.search[i]; i++) {
+                domain = nm_utils_parse_dns_domain(ip_data->domains.search[i], &is_routing);
+                g_variant_builder_add(domains, "(sb)", domain[0] ? domain : ".", is_routing);
+                has_config = TRUE;
+            }
+        }
     }
 
     return has_config;
@@ -406,7 +462,9 @@ free_pending_updates(NMDnsSystemdResolved *self)
 static gboolean
 prepare_one_interface(NMDnsSystemdResolved *self, const InterfaceConfig *ic)
 {
+    NMDnsSystemdResolvedPrivate  *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
     GVariantBuilder               dns;
+    GVariantBuilder               dns_ex;
     GVariantBuilder               domains;
     NMSettingConnectionMdns       mdns              = NM_SETTING_CONNECTION_MDNS_DEFAULT;
     NMSettingConnectionLlmnr      llmnr             = NM_SETTING_CONNECTION_LLMNR_DEFAULT;
@@ -417,6 +475,9 @@ prepare_one_interface(NMDnsSystemdResolved *self, const InterfaceConfig *ic)
     gboolean                      has_config        = FALSE;
     gboolean                      has_default_route = FALSE;
     guint                         i;
+    gboolean                      require_dns_ex = FALSE;
+
+    nm_assert(ic->ifindex != NM_LOOPBACK_IFINDEX);
 
     g_variant_builder_init(&dns, G_VARIANT_TYPE("(ia(iay))"));
     g_variant_builder_add(&dns, "i", ic->ifindex);
@@ -430,7 +491,7 @@ prepare_one_interface(NMDnsSystemdResolved *self, const InterfaceConfig *ic)
         for (i = 0; i < ic->ip_data_list->len; i++) {
             const NMDnsConfigIPData *ip_data = ic->ip_data_list->pdata[i];
 
-            if (update_add_ip_config(self, &dns, &domains, ip_data))
+            if (update_add_ip_config(self, ip_data, &dns, NULL, &domains, &require_dns_ex))
                 has_config = TRUE;
 
             if (ip_data->domains.has_default_route)
@@ -448,6 +509,23 @@ prepare_one_interface(NMDnsSystemdResolved *self, const InterfaceConfig *ic)
     g_variant_builder_close(&dns);
     g_variant_builder_close(&domains);
 
+    if (!require_dns_ex) {
+        /* No need to use the new API. SetLinkDNS() is sufficient. */
+    } else if (!priv->has_set_link_dns_ex) {
+        /* API to set server name is not supported. Nothing we can do. */
+        require_dns_ex = FALSE;
+    } else {
+        g_variant_builder_init(&dns_ex, G_VARIANT_TYPE("(ia(iayqs))"));
+        g_variant_builder_add(&dns_ex, "i", ic->ifindex);
+        g_variant_builder_open(&dns_ex, G_VARIANT_TYPE("a(iayqs)"));
+        for (i = 0; i < ic->ip_data_list->len; i++) {
+            const NMDnsConfigIPData *ip_data = ic->ip_data_list->pdata[i];
+
+            update_add_ip_config(self, ip_data, NULL, &dns_ex, NULL, NULL);
+        }
+        g_variant_builder_close(&dns_ex);
+    }
+
     switch (mdns) {
     case NM_SETTING_CONNECTION_MDNS_NO:
         mdns_arg = "no";
@@ -513,7 +591,14 @@ prepare_one_interface(NMDnsSystemdResolved *self, const InterfaceConfig *ic)
                          "SetLinkLLMNR",
                          ic->ifindex,
                          g_variant_new("(is)", ic->ifindex, llmnr_arg ?: ""));
-    _request_item_append(self, "SetLinkDNS", ic->ifindex, g_variant_builder_end(&dns));
+    if (require_dns_ex) {
+        _request_item_append(self,
+                             DBUS_OP_SET_LINK_DNS_EX,
+                             ic->ifindex,
+                             g_variant_builder_end(&dns_ex));
+        g_variant_builder_clear(&dns);
+    } else
+        _request_item_append(self, "SetLinkDNS", ic->ifindex, g_variant_builder_end(&dns));
     _request_item_append(self,
                          DBUS_OP_SET_LINK_DNS_OVER_TLS,
                          ic->ifindex,
@@ -631,9 +716,9 @@ send_updates(NMDnsSystemdResolved *self)
         gs_free char *ss = NULL;
 
         if ((request_item->operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE
-             && priv->has_link_default_route == NM_TERNARY_FALSE)
+             && priv->has_set_link_default_route == NM_TERNARY_FALSE)
             || (request_item->operation == DBUS_OP_SET_LINK_DNS_OVER_TLS
-                && priv->has_link_dns_over_tls == NM_TERNARY_FALSE)) {
+                && priv->has_set_link_dns_over_tls == NM_TERNARY_FALSE)) {
             /* The "SetLinkDefaultRoute" API is only supported since v240.
              * The "SetLinkDNSOverTLS" API is only supported since v239.
              * We detected whether they are supported, and skip the calls. There
@@ -708,6 +793,13 @@ update(NMDnsPlugin             *plugin,
 
         nm_assert(ifindex == nm_l3_config_data_get_ifindex(ip_data->l3cd));
 
+        if (ifindex == NM_LOOPBACK_IFINDEX) {
+            /* systemd-resolved API is per-link, and loopback is not supported.
+             * Unclear what to do about DNS configuration on loopback. Just skip
+             * it here. */
+            continue;
+        }
+
         ic = g_hash_table_lookup(interfaces, GINT_TO_POINTER(ifindex));
         if (!ic) {
             ic  = g_slice_new(InterfaceConfig);
@@ -759,7 +851,7 @@ update(NMDnsPlugin             *plugin,
     if (dirty_array) {
         g_array_sort_with_data(dirty_array, nm_cmp_int2ptr_p_with_data, NULL);
         for (i = 0; i < dirty_array->len; i++) {
-            int             ifindex = g_array_index(dirty_array, int, i);
+            int             ifindex = nm_g_array_index(dirty_array, int, i);
             InterfaceConfig ic;
 
             _LOGT("clear previously configured ifindex %d", ifindex);
@@ -798,8 +890,9 @@ name_owner_changed(NMDnsSystemdResolved *self, const char *owner)
         priv->try_start_blocked    = FALSE;
         priv->send_updates_waiting = TRUE;
     } else {
-        priv->has_link_default_route = NM_TERNARY_DEFAULT;
-        priv->has_link_dns_over_tls  = NM_TERNARY_DEFAULT;
+        priv->has_set_link_default_route = NM_TERNARY_DEFAULT;
+        priv->has_set_link_dns_over_tls  = NM_TERNARY_DEFAULT;
+        priv->has_set_link_dns_ex        = NM_TERNARY_DEFAULT;
     }
 
     send_updates(self);
@@ -968,7 +1061,7 @@ _resolve_handle_call_cb(GObject *source, GAsyncResult *result, gpointer user_dat
     g_variant_iter_free(v_names_iter);
 
     _resolve_complete(handle,
-                      &g_array_index(v_names, NMDnsSystemdResolvedAddressResult, 0),
+                      nm_g_array_first_p(v_names, NMDnsSystemdResolvedAddressResult),
                       v_names->len,
                       v_flags,
                       NULL);
@@ -1066,7 +1159,7 @@ nm_dns_systemd_resolved_resolve_address(NMDnsSystemdResolved
 {
     NMDnsSystemdResolvedPrivate       *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
     NMDnsSystemdResolvedResolveHandle *handle;
-    char                               addr_str[NM_UTILS_INET_ADDRSTRLEN];
+    char                               addr_str[NM_INET_ADDRSTRLEN];
 
     g_return_val_if_fail(NM_IS_DNS_SYSTEMD_RESOLVED(self), NULL);
     nm_assert_addr_family(addr_family);
@@ -1092,7 +1185,7 @@ nm_dns_systemd_resolved_resolve_address(NMDnsSystemdResolved
     _LOG2T(handle,
            "resolve-address(ifindex=%d, %s, flags=%" G_GINT64_MODIFIER "x): new request",
            handle->r_address.ifindex,
-           nm_utils_inet_ntop(handle->r_address.addr_family, &handle->r_address.addr, addr_str),
+           nm_inet_ntop(handle->r_address.addr_family, &handle->r_address.addr, addr_str),
            handle->r_address.flags);
 
     _resolve_start(self, handle);
@@ -1158,8 +1251,9 @@ nm_dns_systemd_resolved_init(NMDnsSystemdResolved *self)
 {
     NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
 
-    priv->has_link_default_route = NM_TERNARY_DEFAULT;
-    priv->has_link_dns_over_tls  = NM_TERNARY_DEFAULT;
+    priv->has_set_link_default_route = NM_TERNARY_DEFAULT;
+    priv->has_set_link_dns_over_tls  = NM_TERNARY_DEFAULT;
+    priv->has_set_link_dns_ex        = NM_TERNARY_DEFAULT;
 
     c_list_init(&priv->request_queue_lst_head);
     c_list_init(&priv->handle_lst_head);
diff --git a/src/core/dns/nm-dns-systemd-resolved.h b/src/core/dns/nm-dns-systemd-resolved.h
index 02bc524c..0563a70f 100644
--- a/src/core/dns/nm-dns-systemd-resolved.h
+++ b/src/core/dns/nm-dns-systemd-resolved.h
@@ -12,7 +12,7 @@
 
 #define NM_TYPE_DNS_SYSTEMD_RESOLVED (nm_dns_systemd_resolved_get_type())
 #define NM_DNS_SYSTEMD_RESOLVED(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DNS_SYSTEMD_RESOLVED, NMDnsSystemdResolved))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DNS_SYSTEMD_RESOLVED, NMDnsSystemdResolved))
 #define NM_DNS_SYSTEMD_RESOLVED_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DNS_SYSTEMD_RESOLVED, NMDnsSystemdResolvedClass))
 #define NM_IS_DNS_SYSTEMD_RESOLVED(obj) \
diff --git a/src/core/dnsmasq/nm-dnsmasq-manager.c b/src/core/dnsmasq/nm-dnsmasq-manager.c
index be463da3..d245d5d3 100644
--- a/src/core/dnsmasq/nm-dnsmasq-manager.c
+++ b/src/core/dnsmasq/nm-dnsmasq-manager.c
@@ -104,7 +104,6 @@ create_dm_cmd_line(const char           *iface,
     gs_free char                 *error_desc = NULL;
     const char                   *dm_binary;
     const NMPlatformIP4Address   *listen_address;
-    const in_addr_t              *ipv4arr;
     const char *const            *strarr;
     guint                         n;
     guint                         i;
@@ -147,7 +146,7 @@ create_dm_cmd_line(const char           *iface,
      */
     nm_strv_ptrarray_add_string_dup(cmd, "--strict-order");
 
-    _nm_utils_inet4_ntop(listen_address->address, listen_address_s);
+    nm_inet4_ntop(listen_address->address, listen_address_s);
 
     nm_strv_ptrarray_add_string_concat(cmd, "--listen-address=", listen_address_s);
 
@@ -163,13 +162,18 @@ create_dm_cmd_line(const char           *iface,
         nm_strv_ptrarray_add_string_concat(cmd, "--dhcp-option=option:router,", listen_address_s);
     }
 
-    ipv4arr = nm_l3_config_data_get_nameservers(l3cd, AF_INET, &n);
+    strarr = nm_l3_config_data_get_nameservers(l3cd, AF_INET, &n);
     if (n > 0) {
         nm_gstring_prepare(&s);
         g_string_append(s, "--dhcp-option=option:dns-server");
         for (i = 0; i < n; i++) {
+            in_addr_t a;
+
+            if (!nm_utils_dnsname_parse_assert(AF_INET, strarr[i], NULL, &a, NULL))
+                continue;
+
             g_string_append_c(s, ',');
-            g_string_append(s, _nm_utils_inet4_ntop(ipv4arr[i], sbuf_addr));
+            g_string_append(s, nm_inet4_ntop(a, sbuf_addr));
         }
         nm_strv_ptrarray_take_gstring(cmd, &s);
     }
diff --git a/src/core/dnsmasq/nm-dnsmasq-manager.h b/src/core/dnsmasq/nm-dnsmasq-manager.h
index 7fd4199e..2f10a9e8 100644
--- a/src/core/dnsmasq/nm-dnsmasq-manager.h
+++ b/src/core/dnsmasq/nm-dnsmasq-manager.h
@@ -8,7 +8,7 @@
 
 #define NM_TYPE_DNSMASQ_MANAGER (nm_dnsmasq_manager_get_type())
 #define NM_DNSMASQ_MANAGER(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DNSMASQ_MANAGER, NMDnsMasqManager))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DNSMASQ_MANAGER, NMDnsMasqManager))
 #define NM_DNSMASQ_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DNSMASQ_MANAGER, NMDnsMasqManagerClass))
 #define NM_IS_DNSMASQ_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DNSMASQ_MANAGER))
diff --git a/src/core/dnsmasq/nm-dnsmasq-utils.c b/src/core/dnsmasq/nm-dnsmasq-utils.c
index 7f799878..69424d74 100644
--- a/src/core/dnsmasq/nm-dnsmasq-utils.c
+++ b/src/core/dnsmasq/nm-dnsmasq-utils.c
@@ -50,7 +50,7 @@ nm_dnsmasq_utils_get_range(const NMPlatformIP4Address *addr,
         prefix = 24;
     }
 
-    netmask = _nm_utils_ip4_prefix_to_netmask(prefix);
+    netmask = nm_ip4_addr_netmask_from_prefix(prefix);
 
     /* treat addresses in host-order from here on. */
     netmask = ntohl(netmask);
@@ -96,8 +96,8 @@ nm_dnsmasq_utils_get_range(const NMPlatformIP4Address *addr,
     first = htonl(first);
     last  = htonl(last);
 
-    _nm_utils_inet4_ntop(first, out_first);
-    _nm_utils_inet4_ntop(last, out_last);
+    nm_inet4_ntop(first, out_first);
+    nm_inet4_ntop(last, out_last);
 
     return TRUE;
 }
diff --git a/src/core/main-utils.c b/src/core/main-utils.c
index 444e122e..c6fa05c0 100644
--- a/src/core/main-utils.c
+++ b/src/core/main-utils.c
@@ -18,6 +18,7 @@
 #include "main-utils.h"
 #include "NetworkManagerUtils.h"
 #include "nm-config.h"
+#include "libnm-glib-aux/nm-io-utils.h"
 
 static gboolean
 sighup_handler(gpointer user_data)
@@ -76,34 +77,20 @@ nm_main_utils_setup_signals(GMainLoop *main_loop)
 gboolean
 nm_main_utils_write_pidfile(const char *pidfile)
 {
-    char     pid[16];
-    int      fd;
-    int      errsv;
-    gboolean success = FALSE;
+    gs_free_error GError *error = NULL;
+    char                  pid[16];
 
-    if ((fd = open(pidfile, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 00644)) < 0) {
-        errsv = errno;
-        fprintf(stderr, _("Opening %s failed: %s\n"), pidfile, nm_strerror_native(errsv));
+    nm_sprintf_buf(pid, "%lld", (long long) getpid());
+    if (!nm_utils_file_set_contents(pidfile, pid, -1, 00644, NULL, NULL, &error)) {
+        fprintf(stderr, _("Writing to %s failed: %s\n"), pidfile, error->message);
         return FALSE;
     }
 
-    g_snprintf(pid, sizeof(pid), "%d", getpid());
-    if (write(fd, pid, strlen(pid)) < 0) {
-        errsv = errno;
-        fprintf(stderr, _("Writing to %s failed: %s\n"), pidfile, nm_strerror_native(errsv));
-    } else
-        success = TRUE;
-
-    if (nm_close(fd)) {
-        errsv = errno;
-        fprintf(stderr, _("Closing %s failed: %s\n"), pidfile, nm_strerror_native(errsv));
-    }
-
-    return success;
+    return TRUE;
 }
 
 void
-nm_main_utils_ensure_statedir()
+nm_main_utils_ensure_statedir(void)
 {
     gs_free char *parent = NULL;
     int           errsv;
@@ -129,7 +116,7 @@ nm_main_utils_ensure_statedir()
 }
 
 void
-nm_main_utils_ensure_rundir()
+nm_main_utils_ensure_rundir(void)
 {
     int errsv;
 
diff --git a/src/core/meson.build b/src/core/meson.build
index f3359ad0..6c1d463e 100644
--- a/src/core/meson.build
+++ b/src/core/meson.build
@@ -53,6 +53,7 @@ libNetworkManagerBase = static_library(
     'nm-l3-ipv4ll.c',
     'nm-l3-ipv6ll.c',
     'nm-l3cfg.c',
+    'nm-bond-manager.c',
     'nm-ip-config.c',
   ),
   dependencies: [
@@ -103,6 +104,7 @@ libNetworkManager = static_library(
     'devices/nm-device-generic.c',
     'devices/nm-device-infiniband.c',
     'devices/nm-device-ip-tunnel.c',
+    'devices/nm-device-loopback.c',
     'devices/nm-device-macsec.c',
     'devices/nm-device-macvlan.c',
     'devices/nm-device-ppp.c',
@@ -204,6 +206,7 @@ if enable_tests
       libnm_core_aux_intern,
       libnm_core_impl,
       libnm_crypto,
+      libnm_lldp,
       libnm_platform,
       libnm_base,
       libnm_systemd_shared,
@@ -255,6 +258,7 @@ NetworkManager_all_sym = executable(
     libnm_core_aux_intern,
     libnm_core_impl,
     libnm_crypto,
+    libnm_lldp,
     libnm_platform,
     libnm_base,
     libnm_log_core,
@@ -295,6 +299,7 @@ NetworkManager = executable(
     libnm_core_aux_intern,
     libnm_core_impl,
     libnm_crypto,
+    libnm_lldp,
     libnm_platform,
     libnm_base,
     libnm_udev_aux,
diff --git a/src/core/ndisc/nm-fake-ndisc.c b/src/core/ndisc/nm-fake-ndisc.c
index 59bf0c1b..f305e771 100644
--- a/src/core/ndisc/nm-fake-ndisc.c
+++ b/src/core/ndisc/nm-fake-ndisc.c
@@ -247,14 +247,14 @@ receive_ra(gpointer user_data)
     }
 
     for (i = 0; i < ra->gateways->len; i++) {
-        const NMNDiscGateway *item = &g_array_index(ra->gateways, NMNDiscGateway, i);
+        const NMNDiscGateway *item = &nm_g_array_index(ra->gateways, NMNDiscGateway, i);
 
         if (nm_ndisc_add_gateway(ndisc, item, now_msec))
             changed |= NM_NDISC_CONFIG_GATEWAYS;
     }
 
     for (i = 0; i < ra->prefixes->len; i++) {
-        FakePrefix        *item  = &g_array_index(ra->prefixes, FakePrefix, i);
+        FakePrefix        *item  = &nm_g_array_index(ra->prefixes, FakePrefix, i);
         const NMNDiscRoute route = {
             .network     = item->network,
             .plen        = item->plen,
@@ -282,14 +282,14 @@ receive_ra(gpointer user_data)
     }
 
     for (i = 0; i < ra->dns_servers->len; i++) {
-        const NMNDiscDNSServer *item = &g_array_index(ra->dns_servers, NMNDiscDNSServer, i);
+        const NMNDiscDNSServer *item = &nm_g_array_index(ra->dns_servers, NMNDiscDNSServer, i);
 
         if (nm_ndisc_add_dns_server(ndisc, item, now_msec))
             changed |= NM_NDISC_CONFIG_DNS_SERVERS;
     }
 
     for (i = 0; i < ra->dns_domains->len; i++) {
-        const NMNDiscDNSDomain *item = &g_array_index(ra->dns_domains, NMNDiscDNSDomain, i);
+        const NMNDiscDNSDomain *item = &nm_g_array_index(ra->dns_domains, NMNDiscDNSDomain, i);
 
         if (nm_ndisc_add_dns_domain(ndisc, item, now_msec))
             changed |= NM_NDISC_CONFIG_DNS_DOMAINS;
diff --git a/src/core/ndisc/nm-fake-ndisc.h b/src/core/ndisc/nm-fake-ndisc.h
index 91c2c21c..a4a29d7c 100644
--- a/src/core/ndisc/nm-fake-ndisc.h
+++ b/src/core/ndisc/nm-fake-ndisc.h
@@ -9,7 +9,7 @@
 #include "nm-ndisc.h"
 
 #define NM_TYPE_FAKE_NDISC (nm_fake_ndisc_get_type())
-#define NM_FAKE_NDISC(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_FAKE_NDISC, NMFakeNDisc))
+#define NM_FAKE_NDISC(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_FAKE_NDISC, NMFakeNDisc))
 #define NM_FAKE_NDISC_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_FAKE_NDISC, NMFakeNDiscClass))
 #define NM_IS_FAKE_NDISC(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_FAKE_NDISC))
diff --git a/src/core/ndisc/nm-lndp-ndisc.c b/src/core/ndisc/nm-lndp-ndisc.c
index 313756a6..63dde5a5 100644
--- a/src/core/ndisc/nm-lndp-ndisc.c
+++ b/src/core/ndisc/nm-lndp-ndisc.c
@@ -202,9 +202,7 @@ receive_ra(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
         r_plen = ndp_msg_opt_prefix_len(msg, offset);
         if (r_plen == 0 || r_plen > 128)
             continue;
-        nm_utils_ip6_address_clear_host_address(&r_network,
-                                                ndp_msg_opt_prefix(msg, offset),
-                                                r_plen);
+        nm_ip6_addr_clear_host_address(&r_network, ndp_msg_opt_prefix(msg, offset), r_plen);
 
         if (IN6_IS_ADDR_UNSPECIFIED(&r_network) || IN6_IS_ADDR_LINKLOCAL(&r_network))
             continue;
@@ -213,6 +211,7 @@ receive_ra(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
             const NMNDiscRoute route = {
                 .network = r_network,
                 .plen    = r_plen,
+                .on_link = TRUE,
                 .expiry_msec =
                     _nm_ndisc_lifetime_to_expiry(now_msec,
                                                  ndp_msg_opt_prefix_valid_time(msg, offset)),
@@ -244,15 +243,14 @@ receive_ra(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
         if (plen == 0 || plen > 128)
             continue;
 
-        nm_utils_ip6_address_clear_host_address(&network,
-                                                ndp_msg_opt_route_prefix(msg, offset),
-                                                plen);
+        nm_ip6_addr_clear_host_address(&network, ndp_msg_opt_route_prefix(msg, offset), plen);
 
         {
             const NMNDiscRoute route = {
                 .network = network,
                 .gateway = gateway_addr,
                 .plen    = plen,
+                .on_link = FALSE,
                 .expiry_msec =
                     _nm_ndisc_lifetime_to_expiry(now_msec, ndp_msg_opt_route_lifetime(msg, offset)),
                 .preference = _route_preference_coerce(ndp_msg_opt_route_preference(msg, offset)),
@@ -417,7 +415,7 @@ send_ra(NMNDisc *ndisc, GError **error)
     /* The device let us know about all addresses that the device got
      * whose prefixes are suitable for delegating. Let's announce them. */
     for (i = 0; i < rdata->addresses->len; i++) {
-        const NMNDiscAddress      *address = &g_array_index(rdata->addresses, NMNDiscAddress, i);
+        const NMNDiscAddress      *address = &nm_g_array_index(rdata->addresses, NMNDiscAddress, i);
         struct nd_opt_prefix_info *prefix;
 
         prefix = _ndp_msg_add_option(msg, sizeof(*prefix));
@@ -462,7 +460,7 @@ send_ra(NMNDisc *ndisc, GError **error)
 
         for (i = 0; i < rdata->dns_servers->len; i++) {
             const NMNDiscDNSServer *dns_server =
-                &g_array_index(rdata->dns_servers, NMNDiscDNSServer, i);
+                &nm_g_array_index(rdata->dns_servers, NMNDiscDNSServer, i);
 
             option->addrs[i] = dns_server->address;
         }
@@ -478,7 +476,7 @@ dns_servers_done:
 
         for (i = 0; i < rdata->dns_domains->len; i++) {
             const NMNDiscDNSDomain *dns_domain =
-                &g_array_index(rdata->dns_domains, NMNDiscDNSDomain, i);
+                &nm_g_array_index(rdata->dns_domains, NMNDiscDNSDomain, i);
             const char *domain = dns_domain->domain;
             gsize       domain_l;
             gsize       n_reserved;
diff --git a/src/core/ndisc/nm-lndp-ndisc.h b/src/core/ndisc/nm-lndp-ndisc.h
index 711a95e1..62b7b217 100644
--- a/src/core/ndisc/nm-lndp-ndisc.h
+++ b/src/core/ndisc/nm-lndp-ndisc.h
@@ -10,7 +10,7 @@
 #include "nm-core-utils.h"
 
 #define NM_TYPE_LNDP_NDISC (nm_lndp_ndisc_get_type())
-#define NM_LNDP_NDISC(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_LNDP_NDISC, NMLndpNDisc))
+#define NM_LNDP_NDISC(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_LNDP_NDISC, NMLndpNDisc))
 #define NM_LNDP_NDISC_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_LNDP_NDISC, NMLndpNDiscClass))
 #define NM_IS_LNDP_NDISC(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_LNDP_NDISC))
diff --git a/src/core/ndisc/nm-ndisc.c b/src/core/ndisc/nm-ndisc.c
index 9a6038d4..39a4df48 100644
--- a/src/core/ndisc/nm-ndisc.c
+++ b/src/core/ndisc/nm-ndisc.c
@@ -13,6 +13,7 @@
 #include "libnm-platform/nm-platform-utils.h"
 #include "libnm-platform/nm-platform.h"
 #include "libnm-platform/nmp-netns.h"
+#include "libnm-core-aux-intern/nm-libnm-core-utils.h"
 #include "nm-l3-config-data.h"
 #include "nm-l3cfg.h"
 #include "nm-ndisc-private.h"
@@ -160,8 +161,9 @@ nm_ndisc_data_to_l3cd(NMDedupMultiIndex        *multi_idx,
             .table_any     = TRUE,
             .table_coerced = 0,
             .metric_any    = TRUE,
-            .metric        = 0,
-            .rt_pref       = ndisc_route->preference,
+            /* Non-on_link routes get a small penalty */
+            .metric  = ndisc_route->duplicate && !ndisc_route->on_link ? 5 : 0,
+            .rt_pref = ndisc_route->preference,
         };
         nm_assert((NMIcmpv6RouterPref) r.rt_pref == ndisc_route->preference);
 
@@ -186,8 +188,12 @@ nm_ndisc_data_to_l3cd(NMDedupMultiIndex        *multi_idx,
         }
     }
 
-    for (i = 0; i < rdata->dns_servers_n; i++)
-        nm_l3_config_data_add_nameserver(l3cd, AF_INET6, &rdata->dns_servers[i].address);
+    for (i = 0; i < rdata->dns_servers_n; i++) {
+        nm_l3_config_data_add_nameserver_detail(l3cd,
+                                                AF_INET6,
+                                                &rdata->dns_servers[i].address,
+                                                NULL);
+    }
 
     for (i = 0; i < rdata->dns_domains_n; i++)
         nm_l3_config_data_add_search(l3cd, AF_INET6, rdata->dns_domains[i].domain);
@@ -324,11 +330,11 @@ _ASSERT_data_gateways(const NMNDiscDataInternal *data)
         return;
 
     for (i = 0; i < data->gateways->len; i++) {
-        const NMNDiscGateway *item = &g_array_index(data->gateways, NMNDiscGateway, i);
+        const NMNDiscGateway *item = &nm_g_array_index(data->gateways, NMNDiscGateway, i);
 
         nm_assert(!IN6_IS_ADDR_UNSPECIFIED(&item->address));
         for (j = 0; j < i; j++) {
-            const NMNDiscGateway *item2 = &g_array_index(data->gateways, NMNDiscGateway, j);
+            const NMNDiscGateway *item2 = &nm_g_array_index(data->gateways, NMNDiscGateway, j);
 
             nm_assert(!IN6_ARE_ADDR_EQUAL(&item->address, &item2->address));
         }
@@ -344,12 +350,47 @@ _ASSERT_data_gateways(const NMNDiscDataInternal *data)
 }
 
 /*****************************************************************************/
+static bool
+is_duplicate_route(const NMNDiscRoute *r0, const NMNDiscRoute *r1)
+{
+    return IN6_ARE_ADDR_EQUAL(&r0->network, &r1->network) && r0->plen == r1->plen;
+}
+
+static void
+_data_complete_prepare_routes(GArray *routes)
+{
+    guint i, j;
+
+    for (i = 0; i < routes->len; i++) {
+        NMNDiscRoute *r0 = &nm_g_array_index(routes, NMNDiscRoute, i);
+
+        r0->duplicate = FALSE;
+    }
+    for (i = 0; i < routes->len; i++) {
+        NMNDiscRoute *r0 = &nm_g_array_index(routes, NMNDiscRoute, i);
+
+        for (j = i + 1; j < routes->len; j++) {
+            NMNDiscRoute *r1 = &nm_g_array_index(routes, NMNDiscRoute, j);
+
+            if (!is_duplicate_route(r0, r1))
+                continue;
+
+            r0->duplicate = TRUE;
+            r1->duplicate = TRUE;
+
+            /* Maybe after index j, there is yet another duplicate. But we
+            * will find that later, when i becomes j. */
+            break;
+        }
+    }
+}
 
 static const NMNDiscData *
 _data_complete(NMNDiscDataInternal *data)
 {
     _ASSERT_data_gateways(data);
 
+    _data_complete_prepare_routes(data->routes);
 #define _SET(data, field)                                      \
     G_STMT_START                                               \
     {                                                          \
@@ -402,7 +443,7 @@ nm_ndisc_add_gateway(NMNDisc *ndisc, const NMNDiscGateway *new_item, gint64 now_
     guint                insert_idx = G_MAXUINT;
 
     for (i = 0; i < rdata->gateways->len;) {
-        NMNDiscGateway *item = &g_array_index(rdata->gateways, NMNDiscGateway, i);
+        NMNDiscGateway *item = &nm_g_array_index(rdata->gateways, NMNDiscGateway, i);
 
         if (IN6_ARE_ADDR_EQUAL(&item->address, &new_item->address)) {
             if (new_item->expiry_msec <= now_msec) {
@@ -476,12 +517,12 @@ complete_address(NMNDisc *ndisc, NMNDiscAddress *addr)
                                                             priv->config.network_id,
                                                             addr->dad_counter++,
                                                             &error)) {
-            _LOGW("complete-address: failed to generate an stable-privacy address: %s",
+            _LOGW("complete-address: failed to generate a stable-privacy address: %s",
                   error->message);
             g_clear_error(&error);
             return FALSE;
         }
-        _LOGD("complete-address: using an stable-privacy address");
+        _LOGD("complete-address: using a stable-privacy address");
         return TRUE;
     }
 
@@ -496,7 +537,7 @@ complete_address(NMNDisc *ndisc, NMNDiscAddress *addr)
         return TRUE;
     }
 
-    _LOGW("complete-address: can't generate a new_item EUI-64 address");
+    _LOGW("complete-address: can't generate a new EUI-64 address");
     return FALSE;
 }
 
@@ -519,7 +560,7 @@ nm_ndisc_add_address(NMNDisc              *ndisc,
     nm_assert((!!from_ra) == (now_msec > 0));
 
     for (i = 0; i < rdata->addresses->len; i++) {
-        NMNDiscAddress *item = &g_array_index(rdata->addresses, NMNDiscAddress, i);
+        NMNDiscAddress *item = &nm_g_array_index(rdata->addresses, NMNDiscAddress, i);
 
         if (from_ra) {
             /* RFC4862 5.5.3.d, we find an existing address with the same prefix.
@@ -651,10 +692,19 @@ nm_ndisc_add_route(NMNDisc *ndisc, const NMNDiscRoute *new_item, gint64 now_msec
     rdata = &priv->rdata;
 
     for (i = 0; i < rdata->routes->len;) {
-        NMNDiscRoute *item = &g_array_index(rdata->routes, NMNDiscRoute, i);
-
-        if (IN6_ARE_ADDR_EQUAL(&item->network, &new_item->network)
-            && item->plen == new_item->plen) {
+        NMNDiscRoute *item = &nm_g_array_index(rdata->routes, NMNDiscRoute, i);
+
+        /*
+         * It is possible that two entries in rdata->routes have
+         * the same prefix as well as the same prefix length.
+         * One of them, however, refers to the on-link prefix,
+         * and the other one to a route from the route information field.
+         * Moreover, they might have different route preferences.
+         * Hence, if both routes differ in the on-link flag,
+         * comparison is aborted, and both routes are added.
+         */
+        if (IN6_ARE_ADDR_EQUAL(&item->network, &new_item->network) && item->plen == new_item->plen
+            && item->on_link == new_item->on_link) {
             if (new_item->expiry_msec <= now_msec) {
                 g_array_remove_index(rdata->routes, i);
                 return TRUE;
@@ -707,7 +757,7 @@ nm_ndisc_add_dns_server(NMNDisc *ndisc, const NMNDiscDNSServer *new_item, gint64
     rdata = &priv->rdata;
 
     for (i = 0; i < rdata->dns_servers->len; i++) {
-        NMNDiscDNSServer *item = &g_array_index(rdata->dns_servers, NMNDiscDNSServer, i);
+        NMNDiscDNSServer *item = &nm_g_array_index(rdata->dns_servers, NMNDiscDNSServer, i);
 
         if (IN6_ARE_ADDR_EQUAL(&item->address, &new_item->address)) {
             if (new_item->expiry_msec <= now_msec) {
@@ -746,7 +796,7 @@ nm_ndisc_add_dns_domain(NMNDisc *ndisc, const NMNDiscDNSDomain *new_item, gint64
     rdata = &priv->rdata;
 
     for (i = 0; i < rdata->dns_domains->len; i++) {
-        item = &g_array_index(rdata->dns_domains, NMNDiscDNSDomain, i);
+        item = &nm_g_array_index(rdata->dns_domains, NMNDiscDNSDomain, i);
 
         if (nm_streq(item->domain, new_item->domain)) {
             if (new_item->expiry_msec <= now_msec) {
@@ -989,14 +1039,13 @@ announce_router_solicited(NMNDisc *ndisc)
 void
 nm_ndisc_set_config(NMNDisc *ndisc, const NML3ConfigData *l3cd)
 {
-    gboolean               changed = FALSE;
-    const struct in6_addr *in6arr;
-    const char *const     *strvarr;
-    NMDedupMultiIter       iter;
-    const NMPObject       *obj;
-    guint                  len;
-    guint                  i;
-    gint32                 fake_now = NM_NDISC_EXPIRY_BASE_TIMESTAMP / 1000;
+    gboolean           changed = FALSE;
+    const char *const *strvarr;
+    NMDedupMultiIter   iter;
+    const NMPObject   *obj;
+    guint              len;
+    guint              i;
+    gint32             fake_now = NM_NDISC_EXPIRY_BASE_TIMESTAMP / 1000;
 
     nm_assert(NM_IS_NDISC(ndisc));
     nm_assert(nm_ndisc_get_node_type(ndisc) == NM_NDISC_NODE_TYPE_ROUTER);
@@ -1035,15 +1084,19 @@ nm_ndisc_set_config(NMNDisc *ndisc, const NML3ConfigData *l3cd)
             changed = TRUE;
     }
 
-    in6arr = NULL;
-    len    = 0;
+    strvarr = NULL;
+    len     = 0;
     if (l3cd)
-        in6arr = nm_l3_config_data_get_nameservers(l3cd, AF_INET6, &len);
+        strvarr = nm_l3_config_data_get_nameservers(l3cd, AF_INET6, &len);
     for (i = 0; i < len; i++) {
+        struct in6_addr  a;
         NMNDiscDNSServer n;
 
+        if (!nm_utils_dnsname_parse_assert(AF_INET6, strvarr[i], NULL, &a, NULL))
+            continue;
+
         n = (NMNDiscDNSServer){
-            .address     = in6arr[i],
+            .address     = a,
             .expiry_msec = _nm_ndisc_lifetime_to_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP,
                                                         NM_NDISC_ROUTER_LIFETIME),
         };
@@ -1221,28 +1274,35 @@ nm_ndisc_stop(NMNDisc *ndisc)
 }
 
 NMNDiscConfigMap
-nm_ndisc_dad_failed(NMNDisc *ndisc, const struct in6_addr *address, gboolean emit_changed_signal)
+nm_ndisc_dad_failed(NMNDisc *ndisc, GArray *addresses, gboolean emit_changed_signal)
 {
     NMNDiscDataInternal *rdata;
     guint                i;
+    guint                j;
     gboolean             changed = FALSE;
 
+    g_return_val_if_fail(addresses, NM_NDISC_CONFIG_NONE);
+
     rdata = &NM_NDISC_GET_PRIVATE(ndisc)->rdata;
 
-    for (i = 0; i < rdata->addresses->len;) {
-        NMNDiscAddress *item = &g_array_index(rdata->addresses, NMNDiscAddress, i);
+    for (i = 0; i < addresses->len; i++) {
+        const struct in6_addr *addr = &nm_g_array_index(addresses, struct in6_addr, i);
 
-        if (IN6_ARE_ADDR_EQUAL(&item->address, address)) {
-            char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+        for (j = 0; j < rdata->addresses->len;) {
+            NMNDiscAddress *item = &nm_g_array_index(rdata->addresses, NMNDiscAddress, j);
 
-            _LOGD("DAD failed for discovered address %s", _nm_utils_inet6_ntop(address, sbuf));
-            changed = TRUE;
-            if (!complete_address(ndisc, item)) {
-                g_array_remove_index(rdata->addresses, i);
-                continue;
+            if (IN6_ARE_ADDR_EQUAL(&item->address, addr)) {
+                char sbuf[NM_INET_ADDRSTRLEN];
+
+                _LOGI("DAD failed for discovered address %s", nm_inet6_ntop(addr, sbuf));
+                changed = TRUE;
+                if (!complete_address(ndisc, item)) {
+                    g_array_remove_index(rdata->addresses, j);
+                    continue;
+                }
             }
+            j++;
         }
-        i++;
     }
 
     if (emit_changed_signal && changed)
@@ -1278,7 +1338,7 @@ _config_changed_log(NMNDisc *ndisc, NMNDiscConfigMap changed)
     NMNDiscDataInternal *rdata;
     guint                i;
     char                 changedstr[CONFIG_MAP_MAX_STR];
-    char                 addrstr[NM_UTILS_INET_ADDRSTRLEN];
+    char                 addrstr[NM_INET_ADDRSTRLEN];
     char                 str_pref[35];
     char                 str_exp[100];
     gint64               now_msec;
@@ -1303,42 +1363,42 @@ _config_changed_log(NMNDisc *ndisc, NMNDiscConfigMap changed)
         _LOGD("  retrans timer  : %u", (guint) rdata->public.retrans_timer_ms);
 
     for (i = 0; i < rdata->gateways->len; i++) {
-        const NMNDiscGateway *gateway = &g_array_index(rdata->gateways, NMNDiscGateway, i);
+        const NMNDiscGateway *gateway = &nm_g_array_index(rdata->gateways, NMNDiscGateway, i);
 
         _LOGD("  gateway %s pref %s exp %s",
-              _nm_utils_inet6_ntop(&gateway->address, addrstr),
+              nm_inet6_ntop(&gateway->address, addrstr),
               nm_icmpv6_router_pref_to_string(gateway->preference, str_pref, sizeof(str_pref)),
               get_exp(str_exp, now_msec, gateway));
     }
     for (i = 0; i < rdata->addresses->len; i++) {
-        const NMNDiscAddress *address = &g_array_index(rdata->addresses, NMNDiscAddress, i);
+        const NMNDiscAddress *address = &nm_g_array_index(rdata->addresses, NMNDiscAddress, i);
 
         _LOGD("  address %s exp %s",
-              _nm_utils_inet6_ntop(&address->address, addrstr),
+              nm_inet6_ntop(&address->address, addrstr),
               get_exp(str_exp, now_msec, address));
     }
     for (i = 0; i < rdata->routes->len; i++) {
-        const NMNDiscRoute *route = &g_array_index(rdata->routes, NMNDiscRoute, i);
-        char                sbuf[NM_UTILS_INET_ADDRSTRLEN];
+        const NMNDiscRoute *route = &nm_g_array_index(rdata->routes, NMNDiscRoute, i);
+        char                sbuf[NM_INET_ADDRSTRLEN];
 
         _LOGD("  route %s/%u via %s pref %s exp %s",
-              _nm_utils_inet6_ntop(&route->network, addrstr),
+              nm_inet6_ntop(&route->network, addrstr),
               (guint) route->plen,
-              _nm_utils_inet6_ntop(&route->gateway, sbuf),
+              nm_inet6_ntop(&route->gateway, sbuf),
               nm_icmpv6_router_pref_to_string(route->preference, str_pref, sizeof(str_pref)),
               get_exp(str_exp, now_msec, route));
     }
     for (i = 0; i < rdata->dns_servers->len; i++) {
         const NMNDiscDNSServer *dns_server =
-            &g_array_index(rdata->dns_servers, NMNDiscDNSServer, i);
+            &nm_g_array_index(rdata->dns_servers, NMNDiscDNSServer, i);
 
         _LOGD("  dns_server %s exp %s",
-              _nm_utils_inet6_ntop(&dns_server->address, addrstr),
+              nm_inet6_ntop(&dns_server->address, addrstr),
               get_exp(str_exp, now_msec, dns_server));
     }
     for (i = 0; i < rdata->dns_domains->len; i++) {
         const NMNDiscDNSDomain *dns_domain =
-            &g_array_index(rdata->dns_domains, NMNDiscDNSDomain, i);
+            &nm_g_array_index(rdata->dns_domains, NMNDiscDNSDomain, i);
 
         _LOGD("  dns_domain %s exp %s", dns_domain->domain, get_exp(str_exp, now_msec, dns_domain));
     }
@@ -1370,7 +1430,7 @@ clean_gateways(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gint6
     if (rdata->gateways->len == 0)
         return;
 
-    arr = &g_array_index(rdata->gateways, NMNDiscGateway, 0);
+    arr = &nm_g_array_first(rdata->gateways, NMNDiscGateway);
 
     for (i = 0, j = 0; i < rdata->gateways->len; i++) {
         if (!expiry_next(now_msec, arr[i].expiry_msec, next_msec))
@@ -1403,7 +1463,7 @@ clean_addresses(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gint
     if (rdata->addresses->len == 0)
         return;
 
-    arr = &g_array_index(rdata->addresses, NMNDiscAddress, 0);
+    arr = &nm_g_array_first(rdata->addresses, NMNDiscAddress);
 
     for (i = 0, j = 0; i < rdata->addresses->len; i++) {
         if (!expiry_next(now_msec, arr[i].expiry_msec, next_msec))
@@ -1433,7 +1493,7 @@ clean_routes(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gint64
     if (rdata->routes->len == 0)
         return;
 
-    arr = &g_array_index(rdata->routes, NMNDiscRoute, 0);
+    arr = &nm_g_array_first(rdata->routes, NMNDiscRoute);
 
     for (i = 0, j = 0; i < rdata->routes->len; i++) {
         if (!expiry_next(now_msec, arr[i].expiry_msec, next_msec))
@@ -1463,7 +1523,7 @@ clean_dns_servers(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gi
     if (rdata->dns_servers->len == 0)
         return;
 
-    arr = &g_array_index(rdata->dns_servers, NMNDiscDNSServer, 0);
+    arr = &nm_g_array_first(rdata->dns_servers, NMNDiscDNSServer);
 
     for (i = 0, j = 0; i < rdata->dns_servers->len; i++) {
         if (!expiry_next(now_msec, arr[i].expiry_msec, next_msec))
@@ -1493,7 +1553,7 @@ clean_dns_domains(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gi
     if (rdata->dns_domains->len == 0)
         return;
 
-    arr = &g_array_index(rdata->dns_domains, NMNDiscDNSDomain, 0);
+    arr = &nm_g_array_first(rdata->dns_domains, NMNDiscDNSDomain);
 
     for (i = 0, j = 0; i < rdata->dns_domains->len; i++) {
         if (!expiry_next(now_msec, arr[i].expiry_msec, next_msec))
@@ -1602,34 +1662,35 @@ calc_pre_expiry_rs_msec(NMNDisc *ndisc)
         _calc_pre_expiry_rs_msec_worker(
             &expiry_msec,
             priv->last_rs_msec,
-            g_array_index(rdata->gateways, NMNDiscGateway, i).expiry_msec);
+            nm_g_array_index(rdata->gateways, NMNDiscGateway, i).expiry_msec);
     }
 
     for (i = 0; i < rdata->addresses->len; i++) {
         _calc_pre_expiry_rs_msec_worker(
             &expiry_msec,
             priv->last_rs_msec,
-            g_array_index(rdata->addresses, NMNDiscAddress, i).expiry_msec);
+            nm_g_array_index(rdata->addresses, NMNDiscAddress, i).expiry_msec);
     }
 
     for (i = 0; i < rdata->routes->len; i++) {
-        _calc_pre_expiry_rs_msec_worker(&expiry_msec,
-                                        priv->last_rs_msec,
-                                        g_array_index(rdata->routes, NMNDiscRoute, i).expiry_msec);
+        _calc_pre_expiry_rs_msec_worker(
+            &expiry_msec,
+            priv->last_rs_msec,
+            nm_g_array_index(rdata->routes, NMNDiscRoute, i).expiry_msec);
     }
 
     for (i = 0; i < rdata->dns_servers->len; i++) {
         _calc_pre_expiry_rs_msec_worker(
             &expiry_msec,
             priv->last_rs_msec,
-            g_array_index(rdata->dns_servers, NMNDiscDNSServer, i).expiry_msec);
+            nm_g_array_index(rdata->dns_servers, NMNDiscDNSServer, i).expiry_msec);
     }
 
     for (i = 0; i < rdata->dns_domains->len; i++) {
         _calc_pre_expiry_rs_msec_worker(
             &expiry_msec,
             priv->last_rs_msec,
-            g_array_index(rdata->dns_domains, NMNDiscDNSDomain, i).expiry_msec);
+            nm_g_array_index(rdata->dns_domains, NMNDiscDNSDomain, i).expiry_msec);
     }
 
     return expiry_msec - solicit_retransmit_time_jitter(NM_NDISC_PRE_EXPIRY_TIME_MSEC);
diff --git a/src/core/ndisc/nm-ndisc.h b/src/core/ndisc/nm-ndisc.h
index 079ec037..8f1a12a2 100644
--- a/src/core/ndisc/nm-ndisc.h
+++ b/src/core/ndisc/nm-ndisc.h
@@ -8,7 +8,7 @@
 
 #include <stdlib.h>
 #include <netinet/in.h>
-#include <linux/if_addr.h>
+#include "nm-compat-headers/linux/if_addr.h"
 
 #include "nm-setting-ip6-config.h"
 #include "NetworkManagerUtils.h"
@@ -20,7 +20,7 @@
 #define NM_RA_TIMEOUT_INFINITY ((guint32) G_MAXINT32)
 
 #define NM_TYPE_NDISC            (nm_ndisc_get_type())
-#define NM_NDISC(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_NDISC, NMNDisc))
+#define NM_NDISC(obj)            (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_NDISC, NMNDisc))
 #define NM_NDISC_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_NDISC, NMNDiscClass))
 #define NM_IS_NDISC(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_NDISC))
 #define NM_IS_NDISC_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_NDISC))
@@ -115,6 +115,8 @@ typedef struct _NMNDiscRoute {
     gint64             expiry_msec;
     NMIcmpv6RouterPref preference;
     guint8             plen;
+    bool               on_link : 1;
+    bool               duplicate : 1;
 } NMNDiscRoute;
 
 typedef struct {
@@ -228,9 +230,9 @@ NMNDiscNodeType nm_ndisc_get_node_type(NMNDisc *self);
 gboolean nm_ndisc_set_iid(NMNDisc *ndisc, const NMUtilsIPv6IfaceId iid, gboolean is_token);
 void     nm_ndisc_start(NMNDisc *ndisc);
 void     nm_ndisc_stop(NMNDisc *ndisc);
+void     nm_ndisc_set_config(NMNDisc *ndisc, const NML3ConfigData *l3cd);
 NMNDiscConfigMap
-nm_ndisc_dad_failed(NMNDisc *ndisc, const struct in6_addr *address, gboolean emit_changed_signal);
-void nm_ndisc_set_config(NMNDisc *ndisc, const NML3ConfigData *l3cd);
+nm_ndisc_dad_failed(NMNDisc *ndisc, GArray *addresses, gboolean emit_changed_signal);
 
 NMPlatform *nm_ndisc_get_platform(NMNDisc *self);
 NMPNetns   *nm_ndisc_netns_get(NMNDisc *self);
diff --git a/src/core/nm-act-request.h b/src/core/nm-act-request.h
index 9075d5b7..4c85735f 100644
--- a/src/core/nm-act-request.h
+++ b/src/core/nm-act-request.h
@@ -10,7 +10,8 @@
 #include "nm-active-connection.h"
 
 #define NM_TYPE_ACT_REQUEST (nm_act_request_get_type())
-#define NM_ACT_REQUEST(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_ACT_REQUEST, NMActRequest))
+#define NM_ACT_REQUEST(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_ACT_REQUEST, NMActRequest))
 #define NM_ACT_REQUEST_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_ACT_REQUEST, NMActRequestClass))
 #define NM_IS_ACT_REQUEST(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_ACT_REQUEST))
diff --git a/src/core/nm-active-connection.c b/src/core/nm-active-connection.c
index 49568f7c..4488d909 100644
--- a/src/core/nm-active-connection.c
+++ b/src/core/nm-active-connection.c
@@ -116,22 +116,22 @@ static void auth_complete(NMActiveConnection *self, gboolean result, const char
 
 #define _NMLOG_DOMAIN      LOGD_DEVICE
 #define _NMLOG_PREFIX_NAME "active-connection"
-#define _NMLOG(level, ...)                                                                         \
-    G_STMT_START                                                                                   \
-    {                                                                                              \
-        char                       _sbuf[64];                                                      \
-        NMActiveConnectionPrivate *_priv = self ? NM_ACTIVE_CONNECTION_GET_PRIVATE(self) : NULL;   \
-                                                                                                   \
-        nm_log((level),                                                                            \
-               _NMLOG_DOMAIN,                                                                      \
-               (_priv && _priv->device) ? nm_device_get_iface(_priv->device) : NULL,               \
-               (_priv && _priv->applied_connection)                                                \
-                   ? nm_connection_get_uuid(_priv->applied_connection)                             \
-                   : NULL,                                                                         \
-               "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                                        \
-               _NMLOG_PREFIX_NAME,                                                                 \
-               self ? nm_sprintf_buf(_sbuf, "[%p]", self) : "" _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
-    }                                                                                              \
+#define _NMLOG(level, ...)                                                                       \
+    G_STMT_START                                                                                 \
+    {                                                                                            \
+        char                       _sbuf[64];                                                    \
+        NMActiveConnectionPrivate *_priv = self ? NM_ACTIVE_CONNECTION_GET_PRIVATE(self) : NULL; \
+                                                                                                 \
+        nm_log((level),                                                                          \
+               _NMLOG_DOMAIN,                                                                    \
+               (_priv && _priv->device) ? nm_device_get_iface(_priv->device) : NULL,             \
+               (_priv && _priv->applied_connection)                                              \
+                   ? nm_connection_get_uuid(_priv->applied_connection)                           \
+                   : NULL,                                                                       \
+               "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                                      \
+               _NMLOG_PREFIX_NAME,                                                               \
+               NM_HASH_OBFUSCATE_PTR_STR(self, _sbuf) _NM_UTILS_MACRO_REST(__VA_ARGS__));        \
+    }                                                                                            \
     G_STMT_END
 
 /*****************************************************************************/
diff --git a/src/core/nm-active-connection.h b/src/core/nm-active-connection.h
index 28d4246e..a2353a65 100644
--- a/src/core/nm-active-connection.h
+++ b/src/core/nm-active-connection.h
@@ -12,7 +12,7 @@
 
 #define NM_TYPE_ACTIVE_CONNECTION (nm_active_connection_get_type())
 #define NM_ACTIVE_CONNECTION(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_ACTIVE_CONNECTION, NMActiveConnection))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_ACTIVE_CONNECTION, NMActiveConnection))
 #define NM_ACTIVE_CONNECTION_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_ACTIVE_CONNECTION, NMActiveConnectionClass))
 #define NM_IS_ACTIVE_CONNECTION(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_ACTIVE_CONNECTION))
diff --git a/src/core/nm-audit-manager.h b/src/core/nm-audit-manager.h
index 3a7ea7c0..47ab8b76 100644
--- a/src/core/nm-audit-manager.h
+++ b/src/core/nm-audit-manager.h
@@ -12,7 +12,7 @@
 
 #define NM_TYPE_AUDIT_MANAGER (nm_audit_manager_get_type())
 #define NM_AUDIT_MANAGER(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_AUDIT_MANAGER, NMAuditManager))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_AUDIT_MANAGER, NMAuditManager))
 #define NM_AUDIT_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_AUDIT_MANAGER, NMAuditManagerClass))
 #define NM_IS_AUDIT_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_AUDIT_MANAGER))
diff --git a/src/core/nm-auth-manager.c b/src/core/nm-auth-manager.c
index 0f7e0c0d..242f0055 100644
--- a/src/core/nm-auth-manager.c
+++ b/src/core/nm-auth-manager.c
@@ -524,7 +524,7 @@ _name_owner_get_cb(const char *name_owner, GError *error, gpointer user_data)
 /*****************************************************************************/
 
 NMAuthManager *
-nm_auth_manager_get()
+nm_auth_manager_get(void)
 {
     g_return_val_if_fail(singleton_instance, NULL);
 
diff --git a/src/core/nm-auth-manager.h b/src/core/nm-auth-manager.h
index db6237d8..4ebad11b 100644
--- a/src/core/nm-auth-manager.h
+++ b/src/core/nm-auth-manager.h
@@ -40,7 +40,7 @@ nm_auth_call_result_eval(gboolean is_authorized, gboolean is_challenge, GError *
 
 #define NM_TYPE_AUTH_MANAGER (nm_auth_manager_get_type())
 #define NM_AUTH_MANAGER(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_AUTH_MANAGER, NMAuthManager))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_AUTH_MANAGER, NMAuthManager))
 #define NM_AUTH_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_AUTH_MANAGER, NMAuthManagerClass))
 #define NM_IS_AUTH_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_AUTH_MANAGER))
diff --git a/src/core/nm-bond-manager.c b/src/core/nm-bond-manager.c
new file mode 100644
index 00000000..2d15b0b5
--- /dev/null
+++ b/src/core/nm-bond-manager.c
@@ -0,0 +1,967 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-bond-manager.h"
+
+#include <linux/if.h>
+
+#include "NetworkManagerUtils.h"
+#include "libnm-core-aux-intern/nm-libnm-core-utils.h"
+#include "libnm-glib-aux/nm-str-buf.h"
+#include "libnm-platform/nm-platform.h"
+#include "libnm-platform/nmp-object.h"
+#include "nm-firewall-utils.h"
+
+/*****************************************************************************/
+
+typedef enum _nm_packed {
+    REGISTRATION_STATE_NONE,
+    REGISTRATION_STATE_UPPING,
+    REGISTRATION_STATE_UP,
+    REGISTRATION_STATE_DOWNING,
+} RegistrationState;
+
+struct _NMBondManager {
+    NMPlatform *platform;
+
+    NMBondManagerCallback callback;
+    gpointer              user_data;
+
+    /* This is only used for structured logging. */
+    char *connection_uuid;
+
+    GSource *reconfigure_on_idle_source;
+
+    /* During _reconfigure_check() we remember all ifindexes that are part
+     * of the current SLB bond. This is used during _link_changed_cb() to
+     * figure out whether a change on the interface might be relevant to
+     * trigger a _reconfigure_check() on idle. */
+    GHashTable *previous_ifindexes;
+
+    /* We need to keep track of active members that we configured in NFT.
+     * That is, because on update we use "add && flush" to reset the table,
+     * however that leaves empty chains around. If we previously had an active
+     * member, a chain for it was created that we need to clean up.
+     *
+     * Before every NFT call we use this to generate the list of members that
+     * are to be cleaned up. Thereby also adding the new active-memebers to
+     * the list. When the NFT calls returns with success, we can prune the
+     * now deleted member/chain. */
+    GHashTable *previous_members;
+
+    GCancellable *cancellable;
+
+    struct {
+        char        *bond_ifname_curr;
+        char        *bond_ifname_next;
+        const char **active_members_curr;
+        const char **active_members_next;
+    } dat;
+
+    gulong            link_changed_id;
+    int               ifindex;
+    RegistrationState reg_state;
+    bool              destroyed : 1;
+
+    /* Whether we noticed some changes that require us to _reconfigure_check().
+     * Note that while a NFT call is pending, we postpone the check. */
+    bool reconfigure_check : 1;
+
+    /* Whether a `nft` call is in progress. Usually this corresponds to
+     * having a cancellable, however, we may also cancel and clear the
+     * cancellable while the call is still in progress. */
+    bool nft_in_progress : 1;
+
+    /* Whether the last NFT invocation was good. If not, we may have
+     * an invalid state. Actually unused, so far because it's not
+     * clear what to do about failure to configure NFT (aside logging
+     * a warning). */
+    bool nft_good : 1;
+
+    /* The overall state. DEFAULT means that an update is pending.
+     * FALSE means that the last "nft" command failed.
+     * TRUE means that the last "nft" command was good. */
+    NMOptionBool state : 3;
+};
+
+#define NM_IS_BOND_MANAGER(self)                    \
+    ({                                              \
+        const NMBondManager *_self = (self);        \
+                                                    \
+        (_self && NM_IS_PLATFORM(_self->platform)); \
+    })
+
+/*****************************************************************************/
+
+static void _nft_call(NMBondManager     *self,
+                      gboolean           up,
+                      const char        *bond_ifname,
+                      const char *const *bond_ifnames_down,
+                      const char *const *active_members);
+
+static void _bond_manager_destroy(NMBondManager *self);
+
+static void _reconfigure_check(NMBondManager *self, gboolean reapply);
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_DEVICE
+#define _NMLOG_PREFIX_NAME "mlag"
+#define _NMLOG(level, ...)                                                                           \
+    G_STMT_START                                                                                     \
+    {                                                                                                \
+        const NMLogLevel _level = (level);                                                           \
+                                                                                                     \
+        if (nm_logging_enabled(_level, _NMLOG_DOMAIN)) {                                             \
+            NMBondManager *const _self = (self);                                                     \
+            const char *_ifname        = nm_platform_link_get_name(_self->platform, _self->ifindex); \
+            char        _sbuf[30];                                                                   \
+                                                                                                     \
+            _nm_log(_level,                                                                          \
+                    _NMLOG_DOMAIN,                                                                   \
+                    0,                                                                               \
+                    _ifname,                                                                         \
+                    _self->connection_uuid,                                                          \
+                    "%s[" NM_HASH_OBFUSCATE_PTR_FMT ", %s]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),    \
+                    _NMLOG_PREFIX_NAME,                                                              \
+                    NM_HASH_OBFUSCATE_PTR(_self),                                                    \
+                    (_ifname ?: nm_sprintf_buf(_sbuf, "(%d)", _self->ifindex))                       \
+                        _NM_UTILS_MACRO_REST(__VA_ARGS__));                                          \
+        }                                                                                            \
+    }                                                                                                \
+    G_STMT_END
+
+static const char *
+_log_info(NMStrBuf          *strbuf,
+          const char        *bond_ifname,
+          const char *const *active_members,
+          const char *const *previous_members)
+{
+    gsize i;
+
+    nm_str_buf_reset(strbuf);
+
+    if (!bond_ifname)
+        nm_str_buf_append(strbuf, "(disabled)");
+    else {
+        nm_str_buf_append_printf(strbuf, "(enabled, \"%s\"", bond_ifname);
+
+        for (i = 0; active_members && active_members[i]; i++) {
+            if (i == 0)
+                nm_str_buf_append(strbuf, ", active-members=[ \"");
+            else
+                nm_str_buf_append(strbuf, "\", \"");
+            nm_str_buf_append(strbuf, active_members[i]);
+        }
+        if (i > 0)
+            nm_str_buf_append(strbuf, "\" ]");
+
+        for (i = 0; previous_members && previous_members[i]; i++) {
+            nm_assert(!nm_strv_contains(active_members, -1, previous_members[i]));
+            if (i == 0)
+                nm_str_buf_append(strbuf, ", previous-members=[ \"");
+            else
+                nm_str_buf_append(strbuf, "\", \"");
+            nm_str_buf_append(strbuf, previous_members[i]);
+        }
+        if (i > 0)
+            nm_str_buf_append(strbuf, "\" ]");
+
+        nm_str_buf_append(strbuf, ")");
+    }
+
+    return nm_str_buf_get_str(strbuf);
+}
+
+/*****************************************************************************/
+
+static gboolean
+_nm_assert_self_(NMBondManager *self)
+{
+    nm_assert(self);
+    nm_assert(NM_IS_PLATFORM(self->platform));
+    nm_assert(!self->cancellable || G_IS_CANCELLABLE(self->cancellable));
+    nm_assert(!self->cancellable || !g_cancellable_is_cancelled(self->cancellable));
+    nm_assert(!self->dat.active_members_curr || self->dat.bond_ifname_curr);
+    nm_assert(!self->dat.active_members_next || self->dat.bond_ifname_next);
+    nm_assert(!self->cancellable || self->nft_in_progress);
+    nm_assert(!self->reconfigure_on_idle_source || self->reconfigure_check);
+    nm_assert(!self->nft_in_progress || !self->reconfigure_on_idle_source);
+
+    nm_assert(!self->dat.active_members_curr || self->dat.bond_ifname_curr[0]);
+    nm_assert(!self->dat.active_members_next || self->dat.bond_ifname_next[0]);
+
+    nm_assert(!self->destroyed || !self->dat.bond_ifname_next);
+    nm_assert(!self->destroyed
+              || NM_IN_SET((RegistrationState) self->reg_state,
+                           REGISTRATION_STATE_UPPING,
+                           REGISTRATION_STATE_DOWNING));
+
+    switch (self->reg_state) {
+    case REGISTRATION_STATE_NONE:
+        nm_assert(!self->nft_in_progress);
+        nm_assert(!self->cancellable);
+        nm_assert(!self->dat.bond_ifname_curr);
+        nm_assert(!self->dat.bond_ifname_next);
+        break;
+    case REGISTRATION_STATE_UPPING:
+        nm_assert(self->nft_in_progress);
+        nm_assert(self->dat.bond_ifname_curr);
+        break;
+    case REGISTRATION_STATE_UP:
+        nm_assert(!self->nft_in_progress);
+        nm_assert(!self->cancellable);
+        nm_assert(self->dat.bond_ifname_curr);
+        nm_assert(!self->dat.bond_ifname_next);
+        break;
+    case REGISTRATION_STATE_DOWNING:
+        nm_assert(self->nft_in_progress);
+        nm_assert(self->dat.bond_ifname_curr);
+        break;
+    default:
+        nm_assert_not_reached();
+        break;
+    }
+
+    return TRUE;
+}
+
+#define _nm_assert_self(self) nm_assert(_nm_assert_self_(self))
+
+/*****************************************************************************/
+
+static void
+_callback_invoke(NMBondManager *self, NMBondManagerEventType event_type)
+{
+    if (!self->callback)
+        return;
+
+    self->callback(self, event_type, self->user_data);
+}
+
+static void
+_notify_state_change(NMBondManager *self)
+{
+    NMOptionBool state;
+
+    if (self->nft_in_progress)
+        state = NM_OPTION_BOOL_DEFAULT;
+    else
+        state = !!self->nft_good;
+
+    if (state == self->state)
+        return;
+
+    self->state = state;
+    _callback_invoke(self, NM_BOND_MANAGER_EVENT_TYPE_STATE);
+}
+
+/*****************************************************************************/
+
+static void
+_nft_call_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    nm_auto_str_buf NMStrBuf strbuf = NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_232, FALSE);
+    NMBondManager           *self;
+    gpointer                 ptr_up;
+    gs_free const char     **previous_members = NULL;
+    gs_free_error GError    *error            = NULL;
+
+    nm_utils_user_data_unpack(user_data, &self, &ptr_up, &previous_members);
+
+    _nm_assert_self(self);
+
+    self->nft_in_progress = FALSE;
+
+    nm_firewall_nft_call_finish(result, &error);
+
+    if (!error) {
+        gsize i;
+
+        /* On success, we can forget about our previous members that we successfully
+         * deleted. */
+        if (!GPOINTER_TO_INT(ptr_up)) {
+            /* We successfully deleted the NFT table. Forget all previous members. */
+            g_hash_table_remove_all(self->previous_members);
+        } else if (previous_members) {
+            /* These previous members are now forgotten for good. */
+            for (i = 0; previous_members[i]; i++)
+                g_hash_table_remove(self->previous_members, previous_members[i]);
+        }
+    } else {
+        /* If all our NFT calls keep failing, we never actually prune entries from
+         * self->previous_members. That is a problem, however, under normal operation
+         * NFT calls should not continuously fail, and we would have a small fixed
+         * number of active-members. */
+    }
+
+    nm_clear_g_cancellable(&self->cancellable);
+
+    if (nm_utils_error_is_cancelled(error)) {
+        switch (self->reg_state) {
+        case REGISTRATION_STATE_NONE:
+        case REGISTRATION_STATE_UP:
+        case REGISTRATION_STATE_DOWNING:
+            /* It is not expected that we cancel anything in this state. */
+            nm_assert_not_reached();
+            goto out;
+        case REGISTRATION_STATE_UPPING:
+            nm_assert(self->dat.bond_ifname_curr);
+            /* We cancelled while upping. We need to issue another down,
+             * to make sure the data is gone. */
+            if (!self->dat.bond_ifname_next) {
+                /* There is no other name to configure. We just need to down
+                 * the current one. */
+                _LOGT("reconfigure: configuration cancelled, deconfigure %s",
+                      self->dat.bond_ifname_curr);
+                _nft_call(self, FALSE, self->dat.bond_ifname_curr, NULL, NULL);
+                self->reg_state = REGISTRATION_STATE_DOWNING;
+                goto out;
+            }
+            /* There is already another configuration. UPPING again. */
+            _LOGT("reconfigure: configuration cancelled, configure %s",
+                  _log_info(&strbuf,
+                            self->dat.bond_ifname_next,
+                            self->dat.active_members_next,
+                            NULL));
+            _nft_call(self,
+                      TRUE,
+                      self->dat.bond_ifname_next,
+                      NM_MAKE_STRV(self->dat.bond_ifname_curr),
+                      self->dat.active_members_next);
+            self->reg_state = REGISTRATION_STATE_UPPING;
+            nm_clear_g_free(&self->dat.bond_ifname_curr);
+            nm_clear_g_free(&self->dat.active_members_curr);
+            self->dat.bond_ifname_curr    = g_steal_pointer(&self->dat.bond_ifname_next);
+            self->dat.active_members_curr = g_steal_pointer(&self->dat.active_members_next);
+            goto out;
+        }
+        nm_assert_not_reached();
+        goto out;
+    }
+
+    if (error) {
+        self->nft_good = FALSE;
+    } else {
+        /* Technically, if a previous downing failed, we cannot know that
+         * we were able to fix this bug a successful run now. That is, because
+         * if the interface got renamed, and the downing for the previous
+         * interface name failed, we leak that table and the success now doesn't
+         * fix that.
+         *
+         * That is a bug, but probably not severe because:
+         * - interfaces are not supposed to be renamed.
+         * - if this NFT command succeed, we expect that also the previous downings worked.
+         *
+         * The problem here is only that nft_good might lie and indicate
+         * no problem. However, when a downing fails, we anyway leak the table already
+         * and the bad thing happend. We cannot fix if `nft` command fails.
+         */
+        self->nft_good = TRUE;
+    }
+
+    switch (self->reg_state) {
+    case REGISTRATION_STATE_NONE:
+    case REGISTRATION_STATE_UP:
+        /* Unexpected to get a callback completion in these states. */
+        nm_assert_not_reached();
+        goto out;
+    case REGISTRATION_STATE_UPPING:
+        nm_assert(!self->dat.bond_ifname_next);
+        if (error) {
+            /* Unclear what to do about this error. Just log about it, nothing else. */
+            _LOGW("reconfigure: nft configuration for balance-slb failed: %s", error->message);
+        } else
+            _LOGT("reconfigure: configuration completed");
+        self->reg_state = REGISTRATION_STATE_UP;
+        goto out;
+    case REGISTRATION_STATE_DOWNING:
+        nm_assert(self->dat.bond_ifname_curr);
+        if (!self->dat.bond_ifname_next) {
+            if (error) {
+                /* Unclear what to do about this error. Just log about it, nothing else. */
+                _LOGW("reconfigure: nft deconfiguration for balance-slb failed: %s",
+                      error->message);
+            } else
+                _LOGT("reconfigure: deconfiguration completed");
+            nm_clear_g_free(&self->dat.bond_ifname_curr);
+            nm_clear_g_free(&self->dat.active_members_curr);
+            self->reg_state = REGISTRATION_STATE_NONE;
+
+            if (self->destroyed) {
+                _bond_manager_destroy(self);
+                return;
+            }
+
+            goto out;
+        }
+        if (error) {
+            /* Unclear what to do about this error. Just log about it, nothing else. */
+            _LOGW("reconfigure: nft deconfiguration failed before restart: %s", error->message);
+        } else
+            _LOGT("reconfigure: deconfiguration completed before restart");
+        _nft_call(self,
+                  TRUE,
+                  self->dat.bond_ifname_next,
+                  NM_MAKE_STRV(self->dat.bond_ifname_curr),
+                  self->dat.active_members_next);
+        nm_clear_g_free(&self->dat.bond_ifname_curr);
+        nm_clear_g_free(&self->dat.active_members_curr);
+        self->dat.bond_ifname_curr    = g_steal_pointer(&self->dat.bond_ifname_next);
+        self->dat.active_members_curr = g_steal_pointer(&self->dat.active_members_next);
+        self->reg_state               = REGISTRATION_STATE_UPPING;
+        goto out;
+    }
+
+    nm_assert_not_reached();
+
+out:
+    if (self->reconfigure_check) {
+        if (self->destroyed)
+            nm_assert_not_reached();
+        else if (!self->nft_in_progress) {
+            nm_assert(!self->reconfigure_on_idle_source);
+            _reconfigure_check(self, FALSE);
+        }
+    }
+
+    _notify_state_change(self);
+}
+
+static void
+_nft_call(NMBondManager     *self,
+          gboolean           up,
+          const char        *bond_ifname,
+          const char *const *bond_ifnames_down,
+          const char *const *active_members)
+{
+    gs_unref_bytes GBytes     *stdin_buf             = NULL;
+    gs_free const char *const *previous_members_strv = NULL;
+
+    if (up) {
+        gs_unref_ptrarray GPtrArray *arr = NULL;
+        GHashTableIter               iter;
+        const char                  *n;
+        gsize                        i;
+
+        /* We need to track the active-members that we add, because, when we update the
+         * NFT table without the member from previously, we use "add && flush", which
+         * leaves empty chains for the previous members around. We need to cleanup those
+         * chains, hence the need to track which members we ever added.
+         *
+         * Before making an UP call, we add the newly configured active_members to the list
+         * of previous_members. All the while, passing a list of previous_members_strv
+         * which we currently no longer configure.
+         *
+         * Only when the call succeeds (in _nft_call_cb()), we will forget about previously added
+         * members. This is done by passing the list of members that we are forgetting now
+         * on to the callback below. */
+
+        /* Get the list of previous members that are no longer in the current
+         * active list. */
+        g_hash_table_iter_init(&iter, self->previous_members);
+        while (g_hash_table_iter_next(&iter, (gpointer *) &n, NULL)) {
+            if (nm_strv_contains(active_members, -1, n))
+                continue;
+            if (!arr)
+                arr = g_ptr_array_new();
+            g_ptr_array_add(arr, (gpointer) n);
+        }
+        if (arr) {
+            nm_strv_sort((const char **) arr->pdata, arr->len);
+            previous_members_strv = nm_strv_dup_packed((const char *const *) arr->pdata, arr->len);
+        }
+
+        /* The now active member also get tracked as previous members for the future. */
+        if (active_members) {
+            for (i = 0; active_members[i]; i++)
+                g_hash_table_add(self->previous_members, g_strdup(active_members[i]));
+        }
+    }
+
+    stdin_buf = nm_firewall_nft_stdio_mlag(up,
+                                           bond_ifname,
+                                           bond_ifnames_down,
+                                           active_members,
+                                           previous_members_strv);
+
+    nm_clear_g_cancellable(&self->cancellable);
+    self->cancellable = g_cancellable_new();
+
+    nm_shutdown_wait_obj_register_cancellable(self->cancellable, "nft-mlag");
+
+    if (_LOGT_ENABLED()) {
+        if (up) {
+            nm_auto_str_buf NMStrBuf strbuf =
+                NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_232, FALSE);
+
+            _LOGT("reconfigure: call nft: %s",
+                  _log_info(&strbuf, bond_ifname, active_members, previous_members_strv));
+        } else
+            _LOGT("reconfigure: call nft: disable on \"%s\"", bond_ifname);
+    }
+
+    self->nft_in_progress = TRUE;
+
+    if (self->reconfigure_check)
+        nm_clear_g_source_inst(&self->reconfigure_on_idle_source);
+
+    nm_firewall_nft_call(stdin_buf,
+                         self->cancellable,
+                         _nft_call_cb,
+                         nm_utils_user_data_pack(self,
+                                                 GINT_TO_POINTER(up),
+                                                 g_steal_pointer(&previous_members_strv)));
+}
+
+/*****************************************************************************/
+
+static void
+_reconfigure_do(NMBondManager *self,
+                gboolean       reapply,
+                const char    *bond_ifname,
+                const char   **active_members_take)
+{
+    nm_auto_str_buf NMStrBuf strbuf = NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_232, FALSE);
+    gs_free const char     **active_members = g_steal_pointer(&active_members_take);
+
+    _nm_assert_self(self);
+    nm_assert(!active_members || bond_ifname);
+    nm_assert(!active_members || active_members[0]);
+
+    /* The difficulty of all of this is "state". In particular, since we make the nft call
+     * async, we need to handle all the possible cases, how an update event can invalidate
+     * a currently pending call. */
+
+    switch (self->reg_state) {
+    case REGISTRATION_STATE_NONE:
+        nm_assert(!self->dat.bond_ifname_curr);
+        nm_assert(!self->dat.active_members_curr);
+        nm_assert(!self->dat.bond_ifname_next);
+        nm_assert(!self->dat.active_members_next);
+        nm_assert(!self->cancellable);
+        nm_assert(!self->nft_in_progress);
+
+        if (!bond_ifname) {
+            /* No configuration done. Nothing to do. */
+            goto out;
+        }
+
+        _LOGT("reconfigure: start configuring (%s)",
+              _log_info(&strbuf, bond_ifname, active_members, NULL));
+        self->dat.bond_ifname_curr    = g_strdup(bond_ifname);
+        self->dat.active_members_curr = nm_strv_dup_packed(active_members, -1);
+        _nft_call(self, TRUE, self->dat.bond_ifname_curr, NULL, self->dat.active_members_curr);
+        self->reg_state = REGISTRATION_STATE_UPPING;
+        goto out;
+    case REGISTRATION_STATE_UPPING:
+        nm_assert(self->dat.bond_ifname_curr);
+        nm_assert(self->nft_in_progress);
+
+        /* We are UPPING, we cancel the pending operation and will
+         * handle the rest when the callback completes. */
+        if (!bond_ifname) {
+            if (self->cancellable || self->dat.bond_ifname_next)
+                _LOGT("reconfigure: aborting configuring");
+            nm_clear_g_free(&self->dat.bond_ifname_next);
+            nm_clear_g_free(&self->dat.active_members_next);
+            nm_clear_g_cancellable(&self->cancellable);
+            goto out;
+        }
+        if (!reapply && self->cancellable && nm_streq0(bond_ifname, self->dat.bond_ifname_curr)
+            && nm_strv_equal(active_members, self->dat.active_members_curr)) {
+            /* Nothing to do. We are already upping this setup. */
+            nm_assert(!self->dat.bond_ifname_next);
+            nm_assert(!self->dat.active_members_next);
+            goto out;
+        }
+        if (!reapply && !self->cancellable && nm_streq0(bond_ifname, self->dat.bond_ifname_next)
+            && nm_strv_equal(active_members, self->dat.active_members_next)) {
+            /* We already cancelled the current upping, and have scheduled another
+             * (identical) run. Nothing to do. */
+            goto out;
+        }
+        _LOGT("reconfigure: abort configuring to configure %s",
+              _log_info(&strbuf, bond_ifname, active_members, NULL));
+        nm_clear_g_free(&self->dat.bond_ifname_next);
+        nm_clear_g_free(&self->dat.active_members_next);
+        self->dat.bond_ifname_next    = g_strdup(bond_ifname);
+        self->dat.active_members_next = nm_strv_dup_packed(active_members, -1);
+        nm_clear_g_cancellable(&self->cancellable);
+        goto out;
+    case REGISTRATION_STATE_UP:
+        nm_assert(self->dat.bond_ifname_curr);
+        nm_assert(!self->dat.bond_ifname_next);
+        nm_assert(!self->dat.active_members_next);
+        nm_assert(!self->cancellable);
+        nm_assert(!self->nft_in_progress);
+
+        if (!bond_ifname) {
+            _LOGT("reconfigure: deconfigure to disable");
+            _nft_call(self, FALSE, self->dat.bond_ifname_curr, NULL, NULL);
+            self->reg_state = REGISTRATION_STATE_DOWNING;
+            goto out;
+        }
+        if (!reapply && nm_streq0(bond_ifname, self->dat.bond_ifname_curr)
+            && nm_strv_equal(active_members, self->dat.active_members_curr)) {
+            /* Nothing to do. The current configuration is already active. */
+            goto out;
+        }
+        _LOGT("reconfigure: configure, update to %s",
+              _log_info(&strbuf, bond_ifname, active_members, NULL));
+        _nft_call(self,
+                  TRUE,
+                  bond_ifname,
+                  NM_MAKE_STRV(self->dat.bond_ifname_curr),
+                  active_members);
+        self->reg_state = REGISTRATION_STATE_UPPING;
+        nm_clear_g_free(&self->dat.bond_ifname_curr);
+        nm_clear_g_free(&self->dat.active_members_curr);
+        self->dat.bond_ifname_curr    = g_strdup(bond_ifname);
+        self->dat.active_members_curr = nm_strv_dup_packed(active_members, -1);
+        goto out;
+    case REGISTRATION_STATE_DOWNING:
+        nm_assert(self->dat.bond_ifname_curr);
+        nm_assert(self->nft_in_progress);
+
+        /* we are already DOWNING. It suffices to clear the scheduled "next"
+         * config and wait, and reset the "next" configuration. */
+        if (nm_streq0(bond_ifname, self->dat.bond_ifname_next)
+            && nm_strv_equal(active_members, self->dat.active_members_next)) {
+            /* Nothing to do. */
+            goto out;
+        }
+        _LOGT("reconfigure: deconfiguring and waiting for %s",
+              _log_info(&strbuf, bond_ifname, active_members, NULL));
+        nm_clear_g_free(&self->dat.bond_ifname_next);
+        nm_clear_g_free(&self->dat.active_members_next);
+        if (bond_ifname) {
+            self->dat.bond_ifname_next    = g_strdup(bond_ifname);
+            self->dat.active_members_next = nm_strv_dup_packed(active_members, -1);
+        }
+        goto out;
+    }
+    nm_assert_not_reached();
+
+out:
+    _notify_state_change(self);
+}
+
+static void
+_reconfigure_check(NMBondManager *self, gboolean reapply)
+{
+    const NMPlatformLink        *plink_ctrl;
+    const NMPlatformLink        *plink_port;
+    const NMPlatformLnkBond     *plnkbond_ctrl;
+    NMDedupMultiIter             pliter;
+    const NMDedupMultiHeadEntry *pl_links_head_entry;
+    const char                  *active_members_lst_stack[16];
+    gs_free const char         **active_members_lst_heap = NULL;
+    const char                 **active_members_lst      = active_members_lst_stack;
+    gsize                        active_members_alloc    = G_N_ELEMENTS(active_members_lst_stack);
+    gsize                        active_members_n        = 0;
+    gs_free const char         **active_members_result   = NULL;
+    const char                  *bond_ifname             = NULL;
+
+    _nm_assert_self(self);
+    nm_assert(!self->destroyed);
+
+    self->reconfigure_check = FALSE;
+    nm_clear_g_source_inst(&self->reconfigure_on_idle_source);
+
+    g_hash_table_remove_all(self->previous_ifindexes);
+
+    plnkbond_ctrl = nm_platform_link_get_lnk_bond(self->platform, self->ifindex, &plink_ctrl);
+
+    /* We only do bonding-slb MLAG handling if our ifindex is a bond with
+     * mode=balance-xor && xmit_hash_policy=vlan+srcmac. */
+    if (!plnkbond_ctrl)
+        goto out;
+    if (!plink_ctrl)
+        goto out;
+    if (plink_ctrl->type != NM_LINK_TYPE_BOND)
+        goto out;
+    if (plnkbond_ctrl->mode != NM_BOND_MODE_XOR)
+        goto out;
+    if (plnkbond_ctrl->xmit_hash_policy != NM_BOND_XMIT_HASH_POLICY_VLAN_SRCMAC)
+        goto out;
+
+    /* Find all the connected ports that are IFF_RUNNING. */
+    pl_links_head_entry = nm_platform_lookup_obj_type(self->platform, NMP_OBJECT_TYPE_LINK);
+    nmp_cache_iter_for_each_link (&pliter, pl_links_head_entry, &plink_port) {
+        if (plink_port->master != self->ifindex)
+            continue;
+        if (!NM_FLAGS_HAS(plink_port->n_ifi_flags, IFF_RUNNING))
+            continue;
+
+        g_hash_table_add(self->previous_ifindexes, GINT_TO_POINTER(plink_port->ifindex));
+
+        if (active_members_n == active_members_alloc) {
+            active_members_alloc *= 2;
+            active_members_lst_heap =
+                g_renew(const char *, active_members_lst_heap, active_members_alloc);
+            if (active_members_lst == active_members_lst_stack) {
+                memcpy(active_members_lst_heap,
+                       active_members_lst_stack,
+                       sizeof(const char *) * active_members_n);
+            }
+            active_members_lst = active_members_lst_heap;
+        }
+
+        active_members_lst[active_members_n++] = plink_port->name;
+    }
+
+    if (active_members_n > 0) {
+        gsize i;
+        gsize j;
+
+        /* We sort the active members by name */
+        g_qsort_with_data(active_members_lst,
+                          active_members_n,
+                          sizeof(const char *),
+                          nm_strcmp_p_with_data,
+                          NULL);
+
+        /* There really shouldn't be any duplicates. Nonetheless, check
+         * and drop them. They must be unique, because nm_firewall_nft_stdio_mlag()
+         * relies on that. */
+        for (j = 1, i = 1; i < active_members_n; i++) {
+            if (nm_streq(active_members_lst[j - 1], active_members_lst[i])) {
+                /* Repeated. Skip. */
+                continue;
+            }
+            if (j != i)
+                active_members_lst[j] = active_members_lst[i];
+            j++;
+        }
+        active_members_n = j;
+
+        active_members_result = g_new(const char *, active_members_n + 1u);
+        j                     = 0;
+
+        if (self->dat.active_members_curr) {
+            /* We configured a list earlier. We want to preserve the sort order
+             * from before. Prefer entries that we already had, in their previous
+             * order. */
+            for (i = 0; self->dat.active_members_curr[i]; i++) {
+                gssize idx;
+
+                /* We cannot use binary search, because we steal the elements we found
+                 * already. Hence this is O(n^2). We could use binary search if we would
+                 * not modify active_members_lst, but then we would need to remember
+                 * somehow which elements are already consumed. */
+                idx = nm_strv_find_first(active_members_lst,
+                                         active_members_n,
+                                         self->dat.active_members_curr[i]);
+                if (idx >= 0)
+                    active_members_result[j++] = g_steal_pointer(&active_members_lst[idx]);
+            }
+        }
+
+        /* append the remaining entries, which are sorted by name. */
+        for (i = 0; i < active_members_n; i++) {
+            if (active_members_lst[i])
+                active_members_result[j++] = active_members_lst[i];
+        }
+
+        nm_assert(j == active_members_n);
+        active_members_result[j] = NULL;
+    }
+
+    bond_ifname = plink_ctrl->name;
+
+out:
+    _reconfigure_do(self, reapply, bond_ifname, g_steal_pointer(&active_members_result));
+}
+
+static gboolean
+_reconfigure_check_on_idle_cb(gpointer user_data)
+{
+    NMBondManager *self = user_data;
+
+    nm_assert(!self->nft_in_progress);
+    _reconfigure_check(self, FALSE);
+    return G_SOURCE_CONTINUE;
+}
+
+/*****************************************************************************/
+
+static void
+_link_changed_cb(NMPlatform           *platform,
+                 int                   obj_type_i,
+                 int                   ifindex,
+                 const NMPlatformLink *plink,
+                 int                   change_type_i,
+                 NMBondManager        *self)
+{
+    if (self->reconfigure_check) {
+        /* Recheck already scheduled. */
+        return;
+    }
+
+    if (self->destroyed) {
+        /* We should not get another event at this point. Anyway, ignore. */
+        return;
+    }
+
+    if (ifindex == self->ifindex)
+        goto schedule;
+
+    if (plink->master == self->ifindex)
+        goto schedule;
+
+    if (g_hash_table_contains(self->previous_ifindexes, GINT_TO_POINTER(ifindex)))
+        goto schedule;
+
+    /* This event is not relevant. Skip. */
+    return;
+
+schedule:
+    self->reconfigure_check = TRUE;
+    if (!self->nft_in_progress) {
+        self->reconfigure_on_idle_source =
+            nm_g_idle_add_source(_reconfigure_check_on_idle_cb, self);
+    }
+}
+
+/*****************************************************************************/
+
+void
+nm_bond_manager_reapply(NMBondManager *self)
+{
+    _reconfigure_check(self, TRUE);
+}
+
+/*****************************************************************************/
+
+int
+nm_bond_manager_get_ifindex(NMBondManager *self)
+{
+    nm_assert(NM_IS_BOND_MANAGER(self));
+
+    return self->ifindex;
+}
+
+const char *
+nm_bond_manager_get_connection_uuid(NMBondManager *self)
+{
+    nm_assert(NM_IS_BOND_MANAGER(self));
+
+    return self->connection_uuid;
+}
+
+NMOptionBool
+nm_bond_manager_get_state(NMBondManager *self)
+{
+    nm_assert(NM_IS_BOND_MANAGER(self));
+
+    return self->state;
+}
+
+/*****************************************************************************/
+
+NMBondManager *
+nm_bond_manager_new(struct _NMPlatform   *platform,
+                    int                   ifindex,
+                    const char           *connection_uuid,
+                    NMBondManagerCallback callback,
+                    gpointer              user_data)
+{
+    NMBondManager *self;
+
+    nm_assert(NM_IS_PLATFORM(platform));
+    nm_assert(ifindex > 0);
+
+    self  = g_slice_new(NMBondManager);
+    *self = (NMBondManager){
+        .platform           = g_object_ref(platform),
+        .ifindex            = ifindex,
+        .reg_state          = REGISTRATION_STATE_NONE,
+        .destroyed          = FALSE,
+        .nft_good           = TRUE,
+        .callback           = callback,
+        .user_data          = user_data,
+        .previous_ifindexes = g_hash_table_new(nm_direct_hash, NULL),
+        .previous_members   = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, NULL),
+        .connection_uuid    = g_strdup(connection_uuid),
+        .state              = NM_OPTION_BOOL_DEFAULT,
+    };
+
+    self->link_changed_id = g_signal_connect(self->platform,
+                                             NM_PLATFORM_SIGNAL_LINK_CHANGED,
+                                             G_CALLBACK(_link_changed_cb),
+                                             self);
+
+    _LOGT("new balance-slb (MLAG) manager for interface %d", self->ifindex);
+
+    _reconfigure_check(self, TRUE);
+
+    return self;
+}
+
+void
+nm_bond_manager_destroy(NMBondManager *self)
+{
+    g_return_if_fail(self);
+    g_return_if_fail(!self->destroyed);
+
+    self->destroyed = TRUE;
+
+    self->callback  = NULL;
+    self->user_data = NULL;
+
+    nm_clear_g_signal_handler(self->platform, &self->link_changed_id);
+
+    nm_clear_g_source_inst(&self->reconfigure_on_idle_source);
+    self->reconfigure_check = FALSE;
+
+    nm_clear_g_free(&self->dat.bond_ifname_next);
+    nm_clear_g_free(&self->dat.active_members_next);
+
+    switch (self->reg_state) {
+    case REGISTRATION_STATE_NONE:
+        break;
+    case REGISTRATION_STATE_UPPING:
+        /* We still have some nfts registered. We need to wrap them up. */
+        _LOGT("destroying but deconfigure pending configuration first");
+        nm_clear_g_free(&self->dat.bond_ifname_next);
+        nm_clear_g_free(&self->dat.active_members_next);
+        nm_clear_g_cancellable(&self->cancellable);
+        return;
+    case REGISTRATION_STATE_UP:
+        _LOGT("destroying but deconfigure first");
+        _nft_call(self, FALSE, self->dat.bond_ifname_curr, NULL, NULL);
+        self->reg_state = REGISTRATION_STATE_DOWNING;
+        return;
+    case REGISTRATION_STATE_DOWNING:
+        _LOGT("destroying but wait for deconfiguring");
+        return;
+    }
+
+    _bond_manager_destroy(self);
+}
+
+static void
+_bond_manager_destroy(NMBondManager *self)
+{
+    _LOGT("destroyed");
+
+    nm_assert(self);
+    nm_assert(self->destroyed);
+    nm_assert(self->reg_state == REGISTRATION_STATE_NONE);
+    nm_assert(self->link_changed_id == 0);
+    nm_assert(!self->cancellable);
+    nm_assert(!self->dat.bond_ifname_curr);
+    nm_assert(!self->dat.active_members_curr);
+    nm_assert(!self->reconfigure_on_idle_source);
+
+    nm_clear_g_free(&self->dat.bond_ifname_next);
+    nm_clear_g_free(&self->dat.active_members_next);
+
+    g_object_unref(self->platform);
+    g_hash_table_unref(self->previous_ifindexes);
+    g_hash_table_unref(self->previous_members);
+    g_free(self->connection_uuid);
+    nm_g_slice_free(self);
+}
diff --git a/src/core/nm-bond-manager.h b/src/core/nm-bond-manager.h
new file mode 100644
index 00000000..92a89f0b
--- /dev/null
+++ b/src/core/nm-bond-manager.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NM_BOND_MANAGER_H__
+#define __NM_BOND_MANAGER_H__
+
+typedef struct _NMBondManager NMBondManager;
+
+struct _NMPlatform;
+
+typedef enum {
+    NM_BOND_MANAGER_EVENT_TYPE_STATE,
+} NMBondManagerEventType;
+
+typedef void (*NMBondManagerCallback)(NMBondManager         *self,
+                                      NMBondManagerEventType event_type,
+                                      gpointer               user_data);
+
+NMBondManager *nm_bond_manager_new(struct _NMPlatform   *platform,
+                                   int                   ifindex,
+                                   const char           *connection_uuid,
+                                   NMBondManagerCallback callback,
+                                   gpointer              user_data);
+
+void nm_bond_manager_reapply(NMBondManager *self);
+
+void nm_bond_manager_destroy(NMBondManager *self);
+
+int          nm_bond_manager_get_ifindex(NMBondManager *self);
+const char  *nm_bond_manager_get_connection_uuid(NMBondManager *self);
+NMOptionBool nm_bond_manager_get_state(NMBondManager *self);
+
+#endif /* __NM_BOND_MANAGER_H__ */
diff --git a/src/core/nm-checkpoint.c b/src/core/nm-checkpoint.c
index 15667332..cd0e17fa 100644
--- a/src/core/nm-checkpoint.c
+++ b/src/core/nm-checkpoint.c
@@ -416,10 +416,10 @@ activate:
             }
         } else {
             /* The device was initially disconnected, deactivate any existing connection */
-            _LOGD("rollback: disconnecting device");
 
             if (nm_device_get_state(device) > NM_DEVICE_STATE_DISCONNECTED
                 && nm_device_get_state(device) < NM_DEVICE_STATE_DEACTIVATING) {
+                _LOGD("rollback: disconnecting device");
                 nm_device_state_changed(device,
                                         NM_DEVICE_STATE_DEACTIVATING,
                                         NM_DEVICE_STATE_REASON_USER_REQUESTED);
diff --git a/src/core/nm-checkpoint.h b/src/core/nm-checkpoint.h
index db33b6e7..b396acfe 100644
--- a/src/core/nm-checkpoint.h
+++ b/src/core/nm-checkpoint.h
@@ -10,7 +10,7 @@
 #include "nm-dbus-interface.h"
 
 #define NM_TYPE_CHECKPOINT (nm_checkpoint_get_type())
-#define NM_CHECKPOINT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_CHECKPOINT, NMCheckpoint))
+#define NM_CHECKPOINT(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_CHECKPOINT, NMCheckpoint))
 #define NM_CHECKPOINT_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_CHECKPOINT, NMCheckpointClass))
 #define NM_IS_CHECKPOINT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_CHECKPOINT))
diff --git a/src/core/nm-config-data.c b/src/core/nm-config-data.c
index 753e3fb5..c6ab998f 100644
--- a/src/core/nm-config-data.c
+++ b/src/core/nm-config-data.c
@@ -1125,10 +1125,13 @@ load_global_dns(GKeyFile *keyfile, gboolean internal)
     gboolean           default_found = FALSE;
     char             **strv;
 
-    group =
-        internal ? NM_CONFIG_KEYFILE_GROUP_INTERN_GLOBAL_DNS : NM_CONFIG_KEYFILE_GROUP_GLOBAL_DNS;
-    domain_prefix     = internal ? NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_GLOBAL_DNS_DOMAIN
-                                 : NM_CONFIG_KEYFILE_GROUPPREFIX_GLOBAL_DNS_DOMAIN;
+    if (internal) {
+        group         = NM_CONFIG_KEYFILE_GROUP_INTERN_GLOBAL_DNS;
+        domain_prefix = NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_GLOBAL_DNS_DOMAIN;
+    } else {
+        group         = NM_CONFIG_KEYFILE_GROUP_GLOBAL_DNS;
+        domain_prefix = NM_CONFIG_KEYFILE_GROUPPREFIX_GLOBAL_DNS_DOMAIN;
+    }
     domain_prefix_len = strlen(domain_prefix);
 
     if (!nm_config_keyfile_has_global_dns_config(keyfile, internal))
@@ -1191,8 +1194,7 @@ load_global_dns(GKeyFile *keyfile, gboolean internal)
         if (strv) {
             nm_strv_cleanup(strv, TRUE, TRUE, TRUE);
             for (i = 0, j = 0; strv[i]; i++) {
-                if (nm_utils_ipaddr_is_valid(AF_INET, strv[i])
-                    || nm_utils_ipaddr_is_valid(AF_INET6, strv[i]))
+                if (nm_inet_is_valid(AF_INET, strv[i]) || nm_inet_is_valid(AF_INET6, strv[i]))
                     strv[j++] = strv[i];
                 else
                     g_free(strv[i]);
@@ -1231,7 +1233,7 @@ load_global_dns(GKeyFile *keyfile, gboolean internal)
             default_found = TRUE;
     }
 
-    if (!default_found) {
+    if (!default_found && g_hash_table_size(dns_config->domains)) {
         nm_log_dbg(LOGD_CORE,
                    "%s global DNS configuration is missing default domain, ignore it",
                    internal ? "internal" : "user");
@@ -1328,8 +1330,7 @@ global_dns_domain_from_dbus(char *name, GVariant *variant)
             strv = g_variant_dup_strv(val, NULL);
             nm_strv_cleanup(strv, TRUE, TRUE, TRUE);
             for (i = 0, j = 0; strv && strv[i]; i++) {
-                if (nm_utils_ipaddr_is_valid(AF_INET, strv[i])
-                    || nm_utils_ipaddr_is_valid(AF_INET6, strv[i]))
+                if (nm_inet_is_valid(AF_INET, strv[i]) || nm_inet_is_valid(AF_INET6, strv[i]))
                     strv[j++] = strv[i];
                 else
                     g_free(strv[i]);
diff --git a/src/core/nm-config-data.h b/src/core/nm-config-data.h
index fdc7e1fc..e3dc90dd 100644
--- a/src/core/nm-config-data.h
+++ b/src/core/nm-config-data.h
@@ -31,7 +31,8 @@ typedef enum {
 /*****************************************************************************/
 
 #define NM_TYPE_CONFIG_DATA (nm_config_data_get_type())
-#define NM_CONFIG_DATA(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_CONFIG_DATA, NMConfigData))
+#define NM_CONFIG_DATA(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_CONFIG_DATA, NMConfigData))
 #define NM_CONFIG_DATA_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_CONFIG_DATA, NMConfigDataClass))
 #define NM_IS_CONFIG_DATA(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_CONFIG_DATA))
diff --git a/src/core/nm-config.c b/src/core/nm-config.c
index 62e4e7ea..b7445362 100644
--- a/src/core/nm-config.c
+++ b/src/core/nm-config.c
@@ -575,63 +575,63 @@ nm_config_cmd_line_options_add_to_entries(NMConfigCmdLineOptions *cli, GOptionCo
 {
     GOptionGroup *group;
     GOptionEntry  config_options[] = {
-         {"config",
+        {"config",
           0,
           0,
           G_OPTION_ARG_FILENAME,
           &cli->config_main_file,
           N_("Config file location"),
           DEFAULT_CONFIG_MAIN_FILE},
-         {"config-dir",
+        {"config-dir",
           0,
           0,
           G_OPTION_ARG_FILENAME,
           &cli->config_dir,
           N_("Config directory location"),
           DEFAULT_CONFIG_DIR},
-         {"system-config-dir",
+        {"system-config-dir",
           0,
           0,
           G_OPTION_ARG_FILENAME,
           &cli->system_config_dir,
           N_("System config directory location"),
           DEFAULT_SYSTEM_CONFIG_DIR},
-         {"intern-config",
+        {"intern-config",
           0,
           0,
           G_OPTION_ARG_FILENAME,
           &cli->intern_config_file,
           N_("Internal config file location"),
           DEFAULT_INTERN_CONFIG_FILE},
-         {"state-file",
+        {"state-file",
           0,
           0,
           G_OPTION_ARG_FILENAME,
           &cli->state_file,
           N_("State file location"),
           DEFAULT_STATE_FILE},
-         {"no-auto-default",
+        {"no-auto-default",
           0,
           G_OPTION_FLAG_HIDDEN,
           G_OPTION_ARG_FILENAME,
           &cli->no_auto_default_file,
           N_("State file for no-auto-default devices"),
           DEFAULT_NO_AUTO_DEFAULT_FILE},
-         {"plugins",
+        {"plugins",
           0,
           0,
           G_OPTION_ARG_STRING,
           &cli->plugins,
           N_("List of plugins separated by ','"),
           NM_CONFIG_DEFAULT_MAIN_PLUGINS},
-         {"configure-and-quit",
+        {"configure-and-quit",
           0,
           G_OPTION_FLAG_OPTIONAL_ARG,
           G_OPTION_ARG_CALLBACK,
           parse_configure_and_quit,
           N_("Quit after initial configuration"),
           NULL},
-         {"debug",
+        {"debug",
           'd',
           0,
           G_OPTION_ARG_NONE,
@@ -639,29 +639,29 @@ nm_config_cmd_line_options_add_to_entries(NMConfigCmdLineOptions *cli, GOptionCo
           N_("Don't become a daemon, and log to stderr"),
           NULL},
 
-         /* These three are hidden for now, and should eventually just go away. */
-         {"connectivity-uri",
+        /* These three are hidden for now, and should eventually just go away. */
+        {"connectivity-uri",
           0,
           G_OPTION_FLAG_HIDDEN,
           G_OPTION_ARG_STRING,
           &cli->connectivity_uri,
           N_("An http(s) address for checking internet connectivity"),
           "http://example.com"},
-         {"connectivity-interval",
+        {"connectivity-interval",
           0,
           G_OPTION_FLAG_HIDDEN,
           G_OPTION_ARG_INT,
           &cli->connectivity_interval,
           N_("The interval between connectivity checks (in seconds)"),
           G_STRINGIFY(NM_CONFIG_DEFAULT_CONNECTIVITY_INTERVAL)},
-         {"connectivity-response",
+        {"connectivity-response",
           0,
           G_OPTION_FLAG_HIDDEN,
           G_OPTION_ARG_STRING,
           &cli->connectivity_response,
           N_("The expected start of the response"),
           NM_CONFIG_DEFAULT_CONNECTIVITY_RESPONSE},
-         {0},
+        {0},
     };
 
     g_return_if_fail(opt_ctx);
@@ -680,7 +680,7 @@ nm_config_cmd_line_options_add_to_entries(NMConfigCmdLineOptions *cli, GOptionCo
 /*****************************************************************************/
 
 GKeyFile *
-nm_config_create_keyfile()
+nm_config_create_keyfile(void)
 {
     GKeyFile *keyfile;
 
diff --git a/src/core/nm-config.h b/src/core/nm-config.h
index 0dd159f4..d56770d0 100644
--- a/src/core/nm-config.h
+++ b/src/core/nm-config.h
@@ -12,7 +12,7 @@
 #include "libnm-base/nm-config-base.h"
 
 #define NM_TYPE_CONFIG            (nm_config_get_type())
-#define NM_CONFIG(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_CONFIG, NMConfig))
+#define NM_CONFIG(obj)            (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_CONFIG, NMConfig))
 #define NM_CONFIG_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_CONFIG, NMConfigClass))
 #define NM_IS_CONFIG(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_CONFIG))
 #define NM_IS_CONFIG_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_CONFIG))
diff --git a/src/core/nm-connectivity.c b/src/core/nm-connectivity.c
index c0e77e35..aaf9d810 100644
--- a/src/core/nm-connectivity.c
+++ b/src/core/nm-connectivity.c
@@ -810,7 +810,7 @@ system_resolver_resolve_cb(GObject *source_object, GAsyncResult *res, gpointer u
 
     for (iter = list; iter; iter = iter->next) {
         GInetAddress *a = iter->data;
-        char          str_addr[NM_UTILS_INET_ADDRSTRLEN];
+        char          str_addr[NM_INET_ADDRSTRLEN];
         int           addr_family;
 
         switch (g_inet_address_get_family(a)) {
@@ -835,7 +835,7 @@ system_resolver_resolve_cb(GObject *source_object, GAsyncResult *res, gpointer u
             nm_str_buf_append_c(&strbuf_hosts, ',');
 
         nm_str_buf_append(&strbuf_hosts,
-                          nm_utils_inet_ntop(addr_family, g_inet_address_to_bytes(a), str_addr));
+                          nm_inet_ntop(addr_family, g_inet_address_to_bytes(a), str_addr));
 next:;
     }
 
@@ -910,7 +910,7 @@ systemd_resolved_resolve_cb(GObject *object, GAsyncResult *res, gpointer user_da
 
     for (i = 0; i < no_addresses; i++) {
         gs_unref_variant GVariant *address = NULL;
-        char                       str_addr[NM_UTILS_INET_ADDRSTRLEN];
+        char                       str_addr[NM_INET_ADDRSTRLEN];
         const guchar              *address_buf;
 
         g_variant_get_child(addresses, i, "(ii@ay)", &ifindex, &addr_family, &address);
@@ -933,7 +933,7 @@ systemd_resolved_resolve_cb(GObject *object, GAsyncResult *res, gpointer user_da
         } else
             nm_str_buf_append_c(&strbuf_hosts, ',');
 
-        nm_str_buf_append(&strbuf_hosts, nm_utils_inet_ntop(addr_family, address_buf, str_addr));
+        nm_str_buf_append(&strbuf_hosts, nm_inet_ntop(addr_family, address_buf, str_addr));
     }
     if (strbuf_hosts.len == 0) {
         _LOG2D("systemd-resolve returned no usable IPv%c addresses",
diff --git a/src/core/nm-connectivity.h b/src/core/nm-connectivity.h
index 7b57a112..d9ea95c2 100644
--- a/src/core/nm-connectivity.h
+++ b/src/core/nm-connectivity.h
@@ -32,7 +32,7 @@ nm_connectivity_state_cmp(NMConnectivityState a, NMConnectivityState b)
 
 #define NM_TYPE_CONNECTIVITY (nm_connectivity_get_type())
 #define NM_CONNECTIVITY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_CONNECTIVITY, NMConnectivity))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_CONNECTIVITY, NMConnectivity))
 #define NM_CONNECTIVITY_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_CONNECTIVITY, NMConnectivityClass))
 #define NM_IS_CONNECTIVITY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_CONNECTIVITY))
diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c
index 4aad9414..9448ba7b 100644
--- a/src/core/nm-core-utils.c
+++ b/src/core/nm-core-utils.c
@@ -58,7 +58,7 @@ G_STATIC_ASSERT(sizeof(NMUtilsTestFlags) <= sizeof(int));
 static int _nm_utils_testing = 0;
 
 gboolean
-nm_utils_get_testing_initialized()
+nm_utils_get_testing_initialized(void)
 {
     NMUtilsTestFlags flags;
 
@@ -69,7 +69,7 @@ nm_utils_get_testing_initialized()
 }
 
 NMUtilsTestFlags
-nm_utils_get_testing()
+nm_utils_get_testing(void)
 {
     NMUtilsTestFlags flags;
 
@@ -2333,14 +2333,14 @@ nm_utils_log_connection_diff(NMConnection *connection,
 
     for (i = 0; i < sorted_hashes->len; i++) {
         LogConnectionSettingData *setting_data =
-            &g_array_index(sorted_hashes, LogConnectionSettingData, i);
+            &nm_g_array_index(sorted_hashes, LogConnectionSettingData, i);
 
         _log_connection_sort_names(setting_data, sorted_names);
         print_setting_header = TRUE;
         for (j = 0; j < sorted_names->len; j++) {
             char                     *str_conn, *str_diff;
             LogConnectionSettingItem *item =
-                &g_array_index(sorted_names, LogConnectionSettingItem, j);
+                &nm_g_array_index(sorted_names, LogConnectionSettingItem, j);
 
             str_conn = (item->diff_result & NM_SETTING_DIFF_RESULT_IN_A)
                            ? _log_connection_get_property(setting_data->setting, item->item_name)
@@ -2717,11 +2717,17 @@ _host_id_hash_v2(const guint8 *seed_arr,
     const UuidData                  *machine_id_data;
     char                             slen[100];
 
-    /*
-        (stat -c '%s' /var/lib/NetworkManager/secret_key;
-         echo -n ' ';
-         cat /var/lib/NetworkManager/secret_key;
-         cat /etc/machine-id | tr -d '\n' | sed -n 's/[a-f0-9-]/\0/pg') | sha256sum
+    /* The following snippet generates the same (binary) host-id:
+
+        (
+          stat -c '%s' /var/lib/NetworkManager/secret_key | tr -d '\n';
+          echo -n ' ';
+          cat /var/lib/NetworkManager/secret_key;
+          cat /etc/machine-id | tr -d '\n' | sed -n 's/[a-f0-9-]/\0/pg'
+        ) \
+        | sha256sum \
+        | awk '{print $1}' \
+        | xxd -r -p
     */
 
     nm_sprintf_buf(slen, "%" G_GSIZE_FORMAT " ", seed_len);
@@ -3013,7 +3019,7 @@ nmtst_utils_host_id_pop(void)
 
     nm_log_dbg(LOGD_CORE, "nmtst: host-id pop");
 
-    h = &g_array_index(nmtst_host_id_stack, HostIdData, nmtst_host_id_stack->len - 1);
+    h = &nm_g_array_index(nmtst_host_id_stack, HostIdData, nmtst_host_id_stack->len - 1);
 
     g_free((char *) h->host_id);
     g_array_set_size(nmtst_host_id_stack, nmtst_host_id_stack->len - 1u);
@@ -3022,7 +3028,7 @@ nmtst_utils_host_id_pop(void)
             &host_id_static,
             h,
             nmtst_host_id_stack->len == 0u ? nmtst_host_id_static_0
-                                           : nm_g_array_last(nmtst_host_id_stack, HostIdData)))
+                                           : &nm_g_array_last(nmtst_host_id_stack, HostIdData)))
         g_assert_not_reached();
 }
 
@@ -3864,7 +3870,7 @@ nm_utils_dhcp_client_id_duid(guint32 iaid, const guint8 *duid, gsize duid_len)
         guint8  type;
         guint32 iaid;
         guint8  duid[];
-    } * client_id;
+    }    *client_id;
     gsize total_size;
 
     /* the @duid must include the 16 bit duid-type and the data (of max 128 bytes). */
@@ -3914,7 +3920,7 @@ nm_utils_dhcp_client_id_systemd_node_specific_full(guint32       iaid,
                 } en;
             };
         } duid;
-    } * client_id;
+    }      *client_id;
     guint64 u64;
 
     g_return_val_if_fail(machine_id, NULL);
@@ -4193,7 +4199,7 @@ nm_utils_get_reverse_dns_domains_ip_6(const struct in6_addr *ip, guint8 plen, GP
         return;
 
     memcpy(&addr, ip, sizeof(struct in6_addr));
-    nm_utils_ip6_address_clear_host_address(&addr, NULL, plen);
+    nm_ip6_addr_clear_host_address(&addr, NULL, plen);
 
     /* Number of nibbles to include in domains */
     nibbles = (plen - 1) / 4 + 1;
@@ -4338,7 +4344,7 @@ skip:
 
     result = g_new(char *, paths->len + 1);
     for (i = 0; i < paths->len; i++)
-        result[i] = g_array_index(paths, struct plugin_info, i).path;
+        result[i] = nm_g_array_index(paths, struct plugin_info, i).path;
     result[i] = NULL;
 
     g_array_free(paths, TRUE);
diff --git a/src/core/nm-core-utils.h b/src/core/nm-core-utils.h
index 483af077..bc936496 100644
--- a/src/core/nm-core-utils.h
+++ b/src/core/nm-core-utils.h
@@ -112,7 +112,7 @@ nm_hash_update_in6addr_prefix(NMHashState *h, const struct in6_addr *addr, guint
 
     nm_assert(addr);
 
-    nm_utils_ip6_address_clear_host_address(&a, addr, plen);
+    nm_ip6_addr_clear_host_address(&a, addr, plen);
     /* we don't hash plen itself. The caller may want to do that.*/
     nm_hash_update_in6addr(h, &a);
 }
diff --git a/src/core/nm-dbus-manager.c b/src/core/nm-dbus-manager.c
index 7fcbf6ca..af7de8c4 100644
--- a/src/core/nm-dbus-manager.c
+++ b/src/core/nm-dbus-manager.c
@@ -1077,7 +1077,7 @@ nm_dbus_manager_lookup_object(NMDBusManager *self, const char *path)
     if (!ptr)
         return NULL;
 
-    obj = (NMDBusObject *) (((char *) ptr) - G_STRUCT_OFFSET(NMDBusObject, internal));
+    obj = NM_CAST_ALIGN(NMDBusObject, (((char *) ptr) - G_STRUCT_OFFSET(NMDBusObject, internal)));
     nm_assert(NM_IS_DBUS_OBJECT(obj));
     return obj;
 }
diff --git a/src/core/nm-dbus-manager.h b/src/core/nm-dbus-manager.h
index d977f99f..b68161db 100644
--- a/src/core/nm-dbus-manager.h
+++ b/src/core/nm-dbus-manager.h
@@ -10,7 +10,8 @@
 #include "nm-dbus-utils.h"
 
 #define NM_TYPE_DBUS_MANAGER (nm_dbus_manager_get_type())
-#define NM_DBUS_MANAGER(o)   (G_TYPE_CHECK_INSTANCE_CAST((o), NM_TYPE_DBUS_MANAGER, NMDBusManager))
+#define NM_DBUS_MANAGER(o) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((o), NM_TYPE_DBUS_MANAGER, NMDBusManager))
 #define NM_DBUS_MANAGER_CLASS(k) \
     (G_TYPE_CHECK_CLASS_CAST((k), NM_TYPE_DBUS_MANAGER, NMDBusManagerClass))
 #define NM_IS_DBUS_MANAGER(o)       (G_TYPE_CHECK_INSTANCE_TYPE((o), NM_TYPE_DBUS_MANAGER))
diff --git a/src/core/nm-dbus-object.h b/src/core/nm-dbus-object.h
index df22bd92..2a0ff0ee 100644
--- a/src/core/nm-dbus-object.h
+++ b/src/core/nm-dbus-object.h
@@ -46,7 +46,8 @@ typedef struct {
 /*****************************************************************************/
 
 #define NM_TYPE_DBUS_OBJECT (nm_dbus_object_get_type())
-#define NM_DBUS_OBJECT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DBUS_OBJECT, NMDBusObject))
+#define NM_DBUS_OBJECT(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DBUS_OBJECT, NMDBusObject))
 #define NM_DBUS_OBJECT_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DBUS_OBJECT, NMDBusObjectClass))
 #define NM_IS_DBUS_OBJECT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DBUS_OBJECT))
diff --git a/src/core/nm-dbus-utils.c b/src/core/nm-dbus-utils.c
index 096b647b..05eca84f 100644
--- a/src/core/nm-dbus-utils.c
+++ b/src/core/nm-dbus-utils.c
@@ -166,7 +166,7 @@ nm_dbus_utils_get_paths_for_clist(const CList *lst_head,
     i    = 0;
     strv = g_new(const char *, n + 1);
     c_list_for_each (iter, lst_head) {
-        NMDBusObject *obj = (NMDBusObject *) (((const char *) iter) - member_offset);
+        NMDBusObject *obj = NM_CAST_ALIGN(NMDBusObject, (((char *) iter) - member_offset));
 
         path = nm_dbus_object_get_path(obj);
         if (!path) {
diff --git a/src/core/nm-default-daemon.h b/src/core/nm-default-daemon.h
index 57d4c6af..f8270ef4 100644
--- a/src/core/nm-default-daemon.h
+++ b/src/core/nm-default-daemon.h
@@ -18,6 +18,7 @@
 #include "nm-version.h"
 
 #include "nm-core-types.h"
+#include "nm-errors.h"
 #include "nm-types.h"
 #include "libnm-log-core/nm-logging.h"
 
diff --git a/src/core/nm-dhcp-config.c b/src/core/nm-dhcp-config.c
index 394a3a49..fca1dcdd 100644
--- a/src/core/nm-dhcp-config.c
+++ b/src/core/nm-dhcp-config.c
@@ -18,7 +18,7 @@
 
 #define NM_TYPE_DHCP4_CONFIG (nm_dhcp4_config_get_type())
 #define NM_DHCP4_CONFIG(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP4_CONFIG, NMDhcp4Config))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP4_CONFIG, NMDhcp4Config))
 #define NM_DHCP4_CONFIG_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP4_CONFIG, NMDhcp4ConfigClass))
 #define NM_IS_DHCP4_CONFIG(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP4_CONFIG))
@@ -33,7 +33,7 @@ static GType nm_dhcp4_config_get_type(void);
 
 #define NM_TYPE_DHCP6_CONFIG (nm_dhcp6_config_get_type())
 #define NM_DHCP6_CONFIG(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP6_CONFIG, NMDhcp6Config))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP6_CONFIG, NMDhcp6Config))
 #define NM_DHCP6_CONFIG_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP6_CONFIG, NMDhcp6ConfigClass))
 #define NM_IS_DHCP6_CONFIG(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP6_CONFIG))
diff --git a/src/core/nm-dhcp-config.h b/src/core/nm-dhcp-config.h
index cdca4ef1..e372ec81 100644
--- a/src/core/nm-dhcp-config.h
+++ b/src/core/nm-dhcp-config.h
@@ -9,7 +9,8 @@
 /*****************************************************************************/
 
 #define NM_TYPE_DHCP_CONFIG (nm_dhcp_config_get_type())
-#define NM_DHCP_CONFIG(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_CONFIG, NMDhcpConfig))
+#define NM_DHCP_CONFIG(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_CONFIG, NMDhcpConfig))
 #define NM_DHCP_CONFIG_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_CONFIG, NMDhcpConfigClass))
 #define NM_IS_DHCP_CONFIG(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_CONFIG))
diff --git a/src/core/nm-dispatcher.c b/src/core/nm-dispatcher.c
index d3529ee4..01a63826 100644
--- a/src/core/nm-dispatcher.c
+++ b/src/core/nm-dispatcher.c
@@ -8,6 +8,7 @@
 
 #include "nm-dispatcher.h"
 
+#include "libnm-glib-aux/nm-dbus-aux.h"
 #include "libnm-core-aux-extern/nm-dispatcher-api.h"
 #include "NetworkManagerUtils.h"
 #include "nm-utils.h"
@@ -179,7 +180,6 @@ dump_ip_to_props(const NML3ConfigData *l3cd, int addr_family, GVariantBuilder *b
     const NMPObject   *default_route;
     const char *const *strarr;
     const in_addr_t   *ip4arr;
-    gconstpointer      iparr;
 
     if (IS_IPv4)
         g_variant_builder_init(&int_builder, G_VARIANT_TYPE("aau"));
@@ -223,14 +223,17 @@ dump_ip_to_props(const NML3ConfigData *l3cd, int addr_family, GVariantBuilder *b
         g_variant_builder_init(&int_builder, G_VARIANT_TYPE("au"));
     else
         g_variant_builder_init(&int_builder, G_VARIANT_TYPE("aay"));
-    iparr = nm_l3_config_data_get_nameservers(l3cd, addr_family, &n);
+    strarr = nm_l3_config_data_get_nameservers(l3cd, addr_family, &n);
     for (i = 0; i < n; i++) {
+        NMIPAddr a;
+
+        if (!nm_utils_dnsname_parse_assert(addr_family, strarr[i], NULL, &a, NULL))
+            continue;
+
         if (IS_IPv4)
-            g_variant_builder_add(&int_builder, "u", ((const in_addr_t *) iparr)[i]);
-        else {
-            var1 = nm_g_variant_new_ay_in6addr(&(((const struct in6_addr *) iparr)[i]));
-            g_variant_builder_add(&int_builder, "@ay", var1);
-        }
+            g_variant_builder_add(&int_builder, "u", &a);
+        else
+            g_variant_builder_add(&int_builder, "@ay", nm_g_variant_new_ay_in6addr(&a.addr6));
     }
     g_variant_builder_add(builder, "{sv}", "nameservers", g_variant_builder_end(&int_builder));
 
@@ -426,7 +429,7 @@ dispatcher_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
     if (!ret) {
         NMLogLevel log_level = LOGL_DEBUG;
 
-        if (_nm_dbus_error_has_name(error, "org.freedesktop.systemd1.LoadFailed")) {
+        if (nm_dbus_error_is(error, "org.freedesktop.systemd1.LoadFailed")) {
             g_dbus_error_strip_remote_error(error);
             log_level = LOGL_WARN;
         }
@@ -465,7 +468,8 @@ static const char *action_table[] = {[NM_DISPATCHER_ACTION_HOSTNAME]      = NMD_
                                      [NM_DISPATCHER_ACTION_DHCP_CHANGE_4] = NMD_ACTION_DHCP4_CHANGE,
                                      [NM_DISPATCHER_ACTION_DHCP_CHANGE_6] = NMD_ACTION_DHCP6_CHANGE,
                                      [NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE] =
-                                         NMD_ACTION_CONNECTIVITY_CHANGE};
+                                         NMD_ACTION_CONNECTIVITY_CHANGE,
+                                     [NM_DISPATCHER_ACTION_REAPPLY] = NMD_ACTION_REAPPLY};
 
 static const char *
 action_to_string(NMDispatcherAction action)
diff --git a/src/core/nm-dispatcher.h b/src/core/nm-dispatcher.h
index 73e0599a..30716399 100644
--- a/src/core/nm-dispatcher.h
+++ b/src/core/nm-dispatcher.h
@@ -21,7 +21,8 @@ typedef enum {
     NM_DISPATCHER_ACTION_VPN_DOWN,
     NM_DISPATCHER_ACTION_DHCP_CHANGE_4,
     NM_DISPATCHER_ACTION_DHCP_CHANGE_6,
-    NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE
+    NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE,
+    NM_DISPATCHER_ACTION_REAPPLY,
 } NMDispatcherAction;
 
 #define NM_DISPATCHER_ACTION_DHCP_CHANGE_X(IS_IPv4) \
diff --git a/src/core/nm-firewall-utils.c b/src/core/nm-firewall-utils.c
index 4bcde85f..f231583a 100644
--- a/src/core/nm-firewall-utils.c
+++ b/src/core/nm-firewall-utils.c
@@ -39,6 +39,71 @@ static const struct {
 
 /*****************************************************************************/
 
+static const char *
+_nft_ifname_valid(const char *str)
+{
+    gsize i;
+
+    /* `nft -f -` takes certain strings, like "device $IFNAME", but
+     * those strings are from a limited character set. Check that
+     * @str is valid according to those rules.
+     *
+     * src/scanner.l:
+     *   digit   [0-9]
+     *   letter  [a-zA-Z]
+     *   string  ({letter}|[_.])({letter}|{digit}|[/\-_\.])*
+     **/
+
+    if (!str || !str[0])
+        return NULL;
+
+    for (i = 0; str[i]; i++) {
+        switch (str[i]) {
+        case 'a' ... 'z':
+        case 'A' ... 'Z':
+        case '_':
+        case '.':
+            continue;
+        case '0' ... '9':
+        case '/':
+        case '-':
+            if (i == 0)
+                return NULL;
+            continue;
+        default:
+            return NULL;
+        }
+    }
+    if (i >= NMP_IFNAMSIZ)
+        return NULL;
+
+    return str;
+}
+
+static const char *
+_strbuf_set_sanitized(NMStrBuf *strbuf, const char *prefix, const char *str_to_sanitize)
+{
+    nm_str_buf_reset(strbuf);
+
+    if (prefix)
+        nm_str_buf_append(strbuf, prefix);
+
+    for (; str_to_sanitize[0] != '\0'; str_to_sanitize++) {
+        const char ch = str_to_sanitize[0];
+
+        if (g_ascii_isalpha(ch) || g_ascii_isdigit(ch)) {
+            nm_str_buf_append_c(strbuf, ch);
+            continue;
+        }
+        nm_str_buf_append_c(strbuf, '_');
+        nm_str_buf_append_c_hex(strbuf, ch, FALSE);
+    }
+
+    return nm_str_buf_get_str(strbuf);
+}
+
+/*****************************************************************************/
+
 #define _SHARE_IPTABLES_SUBNET_TO_STR_LEN (INET_ADDRSTRLEN + 1 + 2 + 1)
 
 static const char *
@@ -50,12 +115,12 @@ _share_iptables_subnet_to_str(char      buf[static _SHARE_IPTABLES_SUBNET_TO_STR
     in_addr_t netmask;
     int       l;
 
-    netmask = _nm_utils_ip4_prefix_to_netmask(plen);
+    netmask = nm_ip4_addr_netmask_from_prefix(plen);
 
     l = g_snprintf(buf,
                    _SHARE_IPTABLES_SUBNET_TO_STR_LEN,
                    "%s/%u",
-                   _nm_utils_inet4_ntop(addr & netmask, buf_addr),
+                   nm_inet4_ntop(addr & netmask, buf_addr),
                    plen);
     nm_assert(l < _SHARE_IPTABLES_SUBNET_TO_STR_LEN);
     return buf;
@@ -693,6 +758,189 @@ _fw_nft_set_shared_construct(gboolean up, const char *ip_iface, in_addr_t addr,
 
 /*****************************************************************************/
 
+GBytes *
+nm_firewall_nft_stdio_mlag(gboolean           up,
+                           const char        *bond_ifname,
+                           const char *const *bond_ifnames_down,
+                           const char *const *active_members,
+                           const char *const *previous_members)
+{
+    nm_auto_str_buf NMStrBuf strbuf_table_name =
+        NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_32, FALSE);
+    nm_auto_str_buf NMStrBuf strbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, FALSE);
+    const char              *table_name;
+    gsize                    i;
+
+    if (NM_MORE_ASSERTS > 10 && active_members) {
+        /* No duplicates. We make certain assumptions here, and we don't
+         * want to check that there are no duplicates. The caller must take
+         * care of this. */
+        for (i = 0; active_members[i]; i++)
+            nm_assert(!nm_strv_contains(&active_members[i + 1], -1, active_members[i]));
+    }
+
+    /* If an interface gets renamed, we need to update the nft tables. Since one nft
+     * invocation is atomic, it is reasonable to drop the previous tables(s) at the
+     * same time when creating the new one. */
+    for (; bond_ifnames_down && bond_ifnames_down[0]; bond_ifnames_down++) {
+        if (nm_streq(bond_ifname, bond_ifnames_down[0]))
+            continue;
+        table_name = _strbuf_set_sanitized(&strbuf_table_name, "nm-mlag-", bond_ifnames_down[0]);
+        _fw_nft_append_cmd_table(&strbuf, "netdev", table_name, FALSE);
+    }
+
+    table_name = _strbuf_set_sanitized(&strbuf_table_name, "nm-mlag-", bond_ifname);
+
+    _fw_nft_append_cmd_table(&strbuf, "netdev", table_name, up);
+
+    if (up) {
+        nm_auto_str_buf NMStrBuf strbuf_1 =
+            NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_232, FALSE);
+        const gsize n_active_members = NM_PTRARRAY_LEN(active_members);
+
+        if (!_nft_ifname_valid(bond_ifname)) {
+            /* We cannot meaningfully express this interface name. Ignore all chains
+             * and only create an empty table. */
+            goto out;
+        }
+
+        for (; previous_members && previous_members[0]; previous_members++) {
+            const char *previous_member = previous_members[0];
+            const char *chain_name;
+
+            /* The caller already ensures that the previous member is not part of the new
+             * active members. Avoid the overhead of checking, and assert against that. */
+            nm_assert(!nm_strv_contains(active_members, n_active_members, previous_member));
+
+            if (!_nft_ifname_valid(previous_member))
+                continue;
+
+            chain_name = _strbuf_set_sanitized(&strbuf_1, "rx-drop-bc-mc-", previous_member);
+
+            /* We want atomically update our table, however, we don't want to delete
+             * and recreate it, because then the sets get lost (which we don't want).
+             *
+             * Instead, we only "add && flush" the table, which removes all rules from
+             * the chain. However, as our active-members change, we want to delete
+             * the obsolete chains too.
+             *
+             * nft has no way to delete all chains in a table, we have to name
+             * them one by one. So we keep track of active members that we had
+             * in the past, and which are now no longer in use. For those previous
+             * members we delete the chains (again, with the "add && delete" dance
+             * to avoid failure deleting a non-existing chain (in case our tracking
+             * is wrong or somebody else modified the table in the meantime).
+             *
+             * We need to track the previous members, because we don't want to first
+             * ask nft which chains exist. Doing that would be cumbersome as we would
+             * have to do one async program invocation and parse stdout. */
+            _append(&strbuf,
+                    "add chain netdev %s %s {"
+                    " type filter hook ingress device %s priority filter; "
+                    "}",
+                    table_name,
+                    chain_name,
+                    previous_member);
+            _append(&strbuf, "delete chain netdev %s %s", table_name, chain_name);
+        }
+
+        /* OVS SLB rule 1
+         *
+         * "Open vSwitch avoids packet duplication by accepting multicast and broadcast
+         *  packets on only the active member, and dropping multicast and broadcast
+         *  packets on all other members."
+         *
+         * primary is first member, we drop on all others */
+        for (i = 0; i < n_active_members; i++) {
+            const char *active_member = active_members[i];
+            const char *chain_name;
+
+            if (!_nft_ifname_valid(active_member))
+                continue;
+
+            chain_name = _strbuf_set_sanitized(&strbuf_1, "rx-drop-bc-mc-", active_member);
+
+            _append(&strbuf,
+                    "add chain netdev %s %s {"
+                    " type filter hook ingress device %s priority filter; "
+                    "}",
+                    table_name,
+                    chain_name,
+                    active_member);
+
+            if (i == 0) {
+                _append(&strbuf, "delete chain netdev %s %s", table_name, chain_name);
+                continue;
+            }
+
+            _append(&strbuf,
+                    "add rule netdev %s %s pkttype {"
+                    " broadcast, multicast "
+                    "} counter drop",
+                    table_name,
+                    chain_name);
+        }
+
+        /* OVS SLB rule 2
+         *
+         * "Open vSwitch deals with this case by dropping packets received on any SLB
+         * bonded link that have a source MAC+VLAN that has been learned on any other
+         * port."
+         */
+        _append(&strbuf,
+                "add set netdev %s macset-tagged {"
+                " typeof ether saddr . vlan id; flags timeout; "
+                "}",
+                table_name);
+        _append(&strbuf,
+                "add set netdev %s macset-untagged {"
+                " typeof ether saddr; flags timeout;"
+                "}",
+                table_name);
+
+        _append(&strbuf,
+                "add chain netdev %s tx-snoop-source-mac {"
+                " type filter hook egress device %s priority filter; "
+                "}",
+                table_name,
+                bond_ifname);
+        _append(&strbuf,
+                "add rule netdev %s tx-snoop-source-mac set update ether saddr . vlan id"
+                " timeout 5s @macset-tagged counter return"
+                "", /* tagged */
+                table_name);
+        _append(&strbuf,
+                "add rule netdev %s tx-snoop-source-mac set update ether saddr"
+                " timeout 5s @macset-untagged counter"
+                "", /* untagged*/
+                table_name);
+
+        _append(&strbuf,
+                "add chain netdev %s rx-drop-looped-packets {"
+                " type filter hook ingress device %s priority filter; "
+                "}",
+                table_name,
+                bond_ifname);
+        _append(&strbuf,
+                "add rule netdev %s rx-drop-looped-packets ether saddr . vlan id"
+                " @macset-tagged counter drop",
+                table_name);
+        _append(&strbuf,
+                "add rule netdev %s rx-drop-looped-packets ether type vlan counter return"
+                "", /* avoid looking up tagged packets in untagged table */
+                table_name);
+        _append(&strbuf,
+                "add rule netdev %s rx-drop-looped-packets ether saddr @macset-untagged"
+                " counter drop",
+                table_name);
+    }
+
+out:
+    return nm_str_buf_finalize_to_gbytes(&strbuf);
+}
+
+/*****************************************************************************/
+
 struct _NMFirewallConfig {
     char     *ip_iface;
     in_addr_t addr;
diff --git a/src/core/nm-firewall-utils.h b/src/core/nm-firewall-utils.h
index 9d883fea..ca138ccf 100644
--- a/src/core/nm-firewall-utils.h
+++ b/src/core/nm-firewall-utils.h
@@ -35,4 +35,10 @@ void nm_firewall_nft_call(GBytes             *stdin_buf,
 
 gboolean nm_firewall_nft_call_finish(GAsyncResult *result, GError **error);
 
+GBytes *nm_firewall_nft_stdio_mlag(gboolean           up,
+                                   const char        *bond_ifname,
+                                   const char *const *bond_ifnames_down,
+                                   const char *const *active_members,
+                                   const char *const *previous_members);
+
 #endif /* __NM_FIREWALL_UTILS_H__ */
diff --git a/src/core/nm-firewalld-manager.h b/src/core/nm-firewalld-manager.h
index a7e71486..1e8f9a42 100644
--- a/src/core/nm-firewalld-manager.h
+++ b/src/core/nm-firewalld-manager.h
@@ -8,7 +8,7 @@
 
 #define NM_TYPE_FIREWALLD_MANAGER (nm_firewalld_manager_get_type())
 #define NM_FIREWALLD_MANAGER(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_FIREWALLD_MANAGER, NMFirewalldManager))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_FIREWALLD_MANAGER, NMFirewalldManager))
 #define NM_FIREWALLD_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_FIREWALLD_MANAGER, NMFirewalldManagerClass))
 #define NM_IS_FIREWALLD_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_FIREWALLD_MANAGER))
diff --git a/src/core/nm-hostname-manager.c b/src/core/nm-hostname-manager.c
index ba0c6c9d..b036590f 100644
--- a/src/core/nm-hostname-manager.c
+++ b/src/core/nm-hostname-manager.c
@@ -99,53 +99,32 @@ _file_monitor_new(const char *path)
 
 /*****************************************************************************/
 
-#if defined(HOSTNAME_PERSIST_GENTOO)
 static char *
-read_hostname_gentoo(const char *path)
+read_hostname(const char *path, gboolean is_gentoo)
 {
-    gs_free char      *contents  = NULL;
-    gs_strfreev char **all_lines = NULL;
-    const char        *tmp;
-    guint              i;
+    gs_free char        *contents  = NULL;
+    gs_free const char **all_lines = NULL;
+    const char          *tmp;
+    gsize                i;
 
     if (!g_file_get_contents(path, &contents, NULL, NULL))
         return NULL;
 
-    all_lines = g_strsplit(contents, "\n", 0);
-    for (i = 0; all_lines[i]; i++) {
-        g_strstrip(all_lines[i]);
-        if (all_lines[i][0] == '#' || all_lines[i][0] == '\0')
-            continue;
-        if (g_str_has_prefix(all_lines[i], "hostname=")) {
-            tmp = &all_lines[i][NM_STRLEN("hostname=")];
-            return g_shell_unquote(tmp, NULL);
+    all_lines = nm_strsplit_set_full(contents, "\n", NM_STRSPLIT_SET_FLAGS_STRSTRIP);
+    for (i = 0; (tmp = all_lines[i]); i++) {
+        if (is_gentoo) {
+            if (!NM_STR_HAS_PREFIX(tmp, "hostname="))
+                continue;
+            tmp = &tmp[NM_STRLEN("hostname=")];
+        } else {
+            if (tmp[0] == '#')
+                continue;
         }
+        nm_assert(tmp && tmp[0] != '\0');
+        return g_shell_unquote(tmp, NULL);
     }
     return NULL;
 }
-#endif
-
-#if defined(HOSTNAME_PERSIST_SLACKWARE)
-static char *
-read_hostname_slackware(const char *path)
-{
-    gs_free char      *contents  = NULL;
-    gs_strfreev char **all_lines = NULL;
-    guint              i         = 0;
-
-    if (!g_file_get_contents(path, &contents, NULL, NULL))
-        return NULL;
-
-    all_lines = g_strsplit(contents, "\n", 0);
-    for (i = 0; all_lines[i]; i++) {
-        g_strstrip(all_lines[i]);
-        if (all_lines[i][0] == '#' || all_lines[i][0] == '\0')
-            continue;
-        return g_shell_unquote(&all_lines[i][0], NULL);
-    }
-    return NULL;
-}
-#endif
 
 #if defined(HOSTNAME_PERSIST_SUSE)
 static gboolean
@@ -237,10 +216,11 @@ _set_hostname_read_file(NMHostnameManager *self)
 #endif
 
 #if defined(HOSTNAME_PERSIST_GENTOO)
-    hostname = read_hostname_gentoo(HOSTNAME_FILE);
+    hostname = read_hostname(HOSTNAME_FILE, TRUE);
 #elif defined(HOSTNAME_PERSIST_SLACKWARE)
-    hostname = read_hostname_slackware(HOSTNAME_FILE);
+    hostname = read_hostname(HOSTNAME_FILE, FALSE);
 #else
+    (void) read_hostname;
     if (g_file_get_contents(HOSTNAME_FILE, &hostname, NULL, NULL))
         g_strchomp(hostname);
 #endif
@@ -433,11 +413,11 @@ _write_hostname_on_idle_cb(gpointer user_data, GCancellable *cancellable)
 }
 
 void
-nm_hostname_manager_write_hostname(NMHostnameManager  *self,
-                                   const char         *hostname,
-                                   GCancellable       *cancellable,
-                                   GAsyncReadyCallback callback,
-                                   gpointer            user_data)
+nm_hostname_manager_set_static_hostname(NMHostnameManager  *self,
+                                        const char         *hostname,
+                                        GCancellable       *cancellable,
+                                        GAsyncReadyCallback callback,
+                                        gpointer            user_data)
 {
     NMHostnameManagerPrivate *priv;
     GTask                    *task;
@@ -446,8 +426,11 @@ nm_hostname_manager_write_hostname(NMHostnameManager  *self,
 
     priv = NM_HOSTNAME_MANAGER_GET_PRIVATE(self);
 
-    task =
-        nm_g_task_new(self, cancellable, nm_hostname_manager_write_hostname, callback, user_data);
+    task = nm_g_task_new(self,
+                         cancellable,
+                         nm_hostname_manager_set_static_hostname,
+                         callback,
+                         user_data);
 
     g_task_set_task_data(task, g_strdup(hostname), g_free);
 
@@ -467,12 +450,12 @@ nm_hostname_manager_write_hostname(NMHostnameManager  *self,
 }
 
 gboolean
-nm_hostname_manager_write_hostname_finish(NMHostnameManager *self,
-                                          GAsyncResult      *result,
-                                          GError           **error)
+nm_hostname_manager_set_static_hostname_finish(NMHostnameManager *self,
+                                               GAsyncResult      *result,
+                                               GError           **error)
 {
     g_return_val_if_fail(NM_IS_HOSTNAME_MANAGER(self), FALSE);
-    g_return_val_if_fail(nm_g_task_is_valid(result, self, nm_hostname_manager_write_hostname),
+    g_return_val_if_fail(nm_g_task_is_valid(result, self, nm_hostname_manager_set_static_hostname),
                          FALSE);
 
     return g_task_propagate_boolean(G_TASK(result), error);
diff --git a/src/core/nm-hostname-manager.h b/src/core/nm-hostname-manager.h
index 31596819..54818da3 100644
--- a/src/core/nm-hostname-manager.h
+++ b/src/core/nm-hostname-manager.h
@@ -12,7 +12,7 @@
 
 #define NM_TYPE_HOSTNAME_MANAGER (nm_hostname_manager_get_type())
 #define NM_HOSTNAME_MANAGER(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_HOSTNAME_MANAGER, NMHostnameManager))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_HOSTNAME_MANAGER, NMHostnameManager))
 #define NM_HOSTNAME_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_HOSTNAME_MANAGER, NMHostnameManagerClass))
 #define NM_IS_HOSTNAME_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_HOSTNAME_MANAGER))
@@ -36,15 +36,15 @@ NMHostnameManager *nm_hostname_manager_get(void);
 
 const char *nm_hostname_manager_get_static_hostname(NMHostnameManager *self);
 
-void nm_hostname_manager_write_hostname(NMHostnameManager  *self,
-                                        const char         *hostname,
-                                        GCancellable       *cancellable,
-                                        GAsyncReadyCallback callback,
-                                        gpointer            user_data);
+void nm_hostname_manager_set_static_hostname(NMHostnameManager  *self,
+                                             const char         *hostname,
+                                             GCancellable       *cancellable,
+                                             GAsyncReadyCallback callback,
+                                             gpointer            user_data);
 
-gboolean nm_hostname_manager_write_hostname_finish(NMHostnameManager *self,
-                                                   GAsyncResult      *result,
-                                                   GError           **error);
+gboolean nm_hostname_manager_set_static_hostname_finish(NMHostnameManager *self,
+                                                        GAsyncResult      *result,
+                                                        GError           **error);
 
 void nm_hostname_manager_set_transient_hostname(NMHostnameManager             *self,
                                                 const char                    *hostname,
diff --git a/src/core/nm-ip-config.c b/src/core/nm-ip-config.c
index b191b54b..f87840cc 100644
--- a/src/core/nm-ip-config.c
+++ b/src/core/nm-ip-config.c
@@ -10,6 +10,7 @@
 
 #include <linux/rtnetlink.h>
 
+#include "libnm-core-aux-intern/nm-libnm-core-utils.h"
 #include "nm-l3cfg.h"
 #include "NetworkManagerUtils.h"
 
@@ -90,7 +91,7 @@ get_property_ip(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec
     NMIPConfig        *self        = NM_IP_CONFIG(object);
     NMIPConfigPrivate *priv        = NM_IP_CONFIG_GET_PRIVATE(self);
     const int          addr_family = nm_ip_config_get_addr_family(self);
-    char               sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char               sbuf_addr[NM_INET_ADDRSTRLEN];
     const char *const *strv;
     guint              len;
     int                v_i;
@@ -108,7 +109,7 @@ get_property_ip(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec
                 NMP_OBJECT_CAST_IP_ROUTE(priv->v_gateway.best_default_route));
             g_value_set_variant(
                 value,
-                g_variant_new_string(nm_utils_inet_ntop(addr_family, gateway, sbuf_addr)));
+                g_variant_new_string(nm_inet_ntop(addr_family, gateway, sbuf_addr)));
         } else
             g_value_set_variant(value, nm_g_variant_singleton_s_empty());
         break;
@@ -333,9 +334,10 @@ get_property_ip4(GObject *object, guint prop_id, GValue *value, GParamSpec *pspe
 {
     NMIPConfig        *self = NM_IP_CONFIG(object);
     NMIPConfigPrivate *priv = NM_IP_CONFIG_GET_PRIVATE(self);
-    char               addr_str[NM_UTILS_INET_ADDRSTRLEN];
+    char               addr_str[NM_INET_ADDRSTRLEN];
     GVariantBuilder    builder;
     const in_addr_t   *addrs;
+    const char *const *strarr;
     guint              len;
     guint              i;
 
@@ -347,28 +349,37 @@ get_property_ip4(GObject *object, guint prop_id, GValue *value, GParamSpec *pspe
         g_value_set_variant(value, priv->v_routes);
         break;
     case PROP_IP4_NAMESERVERS:
-        addrs = nm_l3_config_data_get_nameservers(priv->l3cd, AF_INET, &len);
-        g_value_set_variant(value,
-                            (len == 0) ? nm_g_variant_singleton_au()
-                                       : nm_g_variant_new_au(addrs, len));
-        break;
     case PROP_IP4_NAMESERVER_DATA:
-        addrs = nm_l3_config_data_get_nameservers(priv->l3cd, AF_INET, &len);
-        if (len == 0)
-            g_value_set_variant(value, nm_g_variant_singleton_aaLsvI());
-        else {
-            g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}"));
+        strarr = nm_l3_config_data_get_nameservers(priv->l3cd, AF_INET, &len);
+        if (len == 0) {
+            g_value_set_variant(value,
+                                (prop_id == PROP_IP4_NAMESERVERS)
+                                    ? nm_g_variant_singleton_au()
+                                    : nm_g_variant_singleton_aaLsvI());
+        } else {
+            if (prop_id == PROP_IP4_NAMESERVERS)
+                g_variant_builder_init(&builder, G_VARIANT_TYPE("au"));
+            else
+                g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}"));
             for (i = 0; i < len; i++) {
-                GVariantBuilder nested_builder;
+                in_addr_t a;
 
-                _nm_utils_inet4_ntop(addrs[i], addr_str);
+                if (!nm_utils_dnsname_parse_assert(AF_INET, strarr[i], NULL, &a, NULL))
+                    continue;
 
-                g_variant_builder_init(&nested_builder, G_VARIANT_TYPE("a{sv}"));
-                g_variant_builder_add(&nested_builder,
-                                      "{sv}",
-                                      "address",
-                                      g_variant_new_string(addr_str));
-                g_variant_builder_add(&builder, "a{sv}", &nested_builder);
+                if (prop_id == PROP_IP4_NAMESERVERS)
+                    g_variant_builder_add(&builder, "u", &a);
+                else {
+                    GVariantBuilder nested_builder;
+
+                    nm_inet4_ntop(a, addr_str);
+                    g_variant_builder_init(&nested_builder, G_VARIANT_TYPE("a{sv}"));
+                    g_variant_builder_add(&nested_builder,
+                                          "{sv}",
+                                          "address",
+                                          g_variant_new_string(addr_str));
+                    g_variant_builder_add(&builder, "a{sv}", &nested_builder);
+                }
             }
 
             g_value_take_variant(value, g_variant_builder_end(&builder));
@@ -387,7 +398,7 @@ get_property_ip4(GObject *object, guint prop_id, GValue *value, GParamSpec *pspe
         else {
             g_variant_builder_init(&builder, G_VARIANT_TYPE("as"));
             for (i = 0; i < len; i++)
-                g_variant_builder_add(&builder, "s", _nm_utils_inet4_ntop(addrs[i], addr_str));
+                g_variant_builder_add(&builder, "s", nm_inet4_ntop(addrs[i], addr_str));
             g_value_take_variant(value, g_variant_builder_end(&builder));
         }
         break;
@@ -498,77 +509,6 @@ nm_ip4_config_class_init(NMIP4ConfigClass *klass)
     g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST_ip4, obj_properties_ip4);
 }
 
-void
-nm_ip_config_dns_hash(const NML3ConfigData *l3cd, GChecksum *sum, int addr_family)
-{
-    guint              i;
-    int                val;
-    const char *const *nameservers;
-    const in_addr_t   *wins;
-    const char *const *domains;
-    const char *const *searches;
-    const char *const *options;
-    guint              num_nameservers;
-    guint              num_wins;
-    guint              num_domains;
-    guint              num_searches;
-    guint              num_options;
-
-    g_return_if_fail(l3cd);
-    g_return_if_fail(sum);
-
-    nameservers = nm_l3_config_data_get_nameservers(l3cd, addr_family, &num_nameservers);
-    for (i = 0; i < num_nameservers; i++) {
-        g_checksum_update(sum,
-                          nm_ip_addr_from_packed_array(addr_family, nameservers, i),
-                          nm_utils_addr_family_to_size(addr_family));
-    }
-
-    if (addr_family == AF_INET) {
-        wins = nm_l3_config_data_get_wins(l3cd, &num_wins);
-        for (i = 0; i < num_wins; i++)
-            g_checksum_update(sum, (guint8 *) &wins[i], 4);
-    }
-
-    domains = nm_l3_config_data_get_domains(l3cd, addr_family, &num_domains);
-    for (i = 0; i < num_domains; i++) {
-        g_checksum_update(sum, (const guint8 *) domains[i], strlen(domains[i]));
-    }
-
-    searches = nm_l3_config_data_get_searches(l3cd, addr_family, &num_searches);
-    for (i = 0; i < num_searches; i++) {
-        g_checksum_update(sum, (const guint8 *) searches[i], strlen(searches[i]));
-    }
-
-    options = nm_l3_config_data_get_dns_options(l3cd, addr_family, &num_options);
-    for (i = 0; i < num_options; i++) {
-        g_checksum_update(sum, (const guint8 *) options[i], strlen(options[i]));
-    }
-
-    val = nm_l3_config_data_get_mdns(l3cd);
-    if (val != NM_SETTING_CONNECTION_MDNS_DEFAULT)
-        g_checksum_update(sum, (const guint8 *) &val, sizeof(val));
-
-    val = nm_l3_config_data_get_llmnr(l3cd);
-    if (val != NM_SETTING_CONNECTION_LLMNR_DEFAULT)
-        g_checksum_update(sum, (const guint8 *) &val, sizeof(val));
-
-    val = nm_l3_config_data_get_dns_over_tls(l3cd);
-    if (val != NM_SETTING_CONNECTION_DNS_OVER_TLS_DEFAULT)
-        g_checksum_update(sum, (const guint8 *) &val, sizeof(val));
-
-    /* FIXME(ip-config-checksum): the DNS priority should be considered relevant
-     * and added into the checksum as well, but this can't be done right now
-     * because in the DNS manager we rely on the fact that an empty
-     * configuration (i.e. just created) has a zero checksum. This is needed to
-     * avoid rewriting resolv.conf when there is no change.
-     *
-     * The DNS priority initial value depends on the connection type (VPN or
-     * not), so it's a bit difficult to add it to checksum maintaining the
-     * assumption of checksum(empty)=0
-     */
-}
-
 /*****************************************************************************/
 
 /* public */
@@ -630,12 +570,12 @@ static const NMDBusInterfaceInfoExtended interface_info_ip6_config = {
 static void
 get_property_ip6(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
 {
-    NMIPConfig            *self = NM_IP_CONFIG(object);
-    NMIPConfigPrivate     *priv = NM_IP_CONFIG_GET_PRIVATE(self);
-    GVariantBuilder        builder;
-    guint                  len;
-    guint                  i;
-    const struct in6_addr *addrs;
+    NMIPConfig        *self = NM_IP_CONFIG(object);
+    NMIPConfigPrivate *priv = NM_IP_CONFIG_GET_PRIVATE(self);
+    GVariantBuilder    builder;
+    guint              len;
+    guint              i;
+    const char *const *strarr;
 
     switch (prop_id) {
     case PROP_IP6_ADDRESSES:
@@ -645,13 +585,19 @@ get_property_ip6(GObject *object, guint prop_id, GValue *value, GParamSpec *pspe
         g_value_set_variant(value, priv->v_routes);
         break;
     case PROP_IP6_NAMESERVERS:
-        addrs = nm_l3_config_data_get_nameservers(priv->l3cd, AF_INET6, &len);
+        strarr = nm_l3_config_data_get_nameservers(priv->l3cd, AF_INET6, &len);
         if (len == 0)
             g_value_set_variant(value, nm_g_variant_singleton_aay());
         else {
             g_variant_builder_init(&builder, G_VARIANT_TYPE("aay"));
-            for (i = 0; i < len; i++)
-                g_variant_builder_add(&builder, "@ay", nm_g_variant_new_ay_in6addr(&addrs[i]));
+            for (i = 0; i < len; i++) {
+                struct in6_addr a;
+
+                if (!nm_utils_dnsname_parse_assert(AF_INET6, strarr[i], NULL, &a, NULL))
+                    continue;
+
+                g_variant_builder_add(&builder, "@ay", nm_g_variant_new_ay_in6addr(&a));
+            }
             g_value_take_variant(value, g_variant_builder_end(&builder));
         }
         break;
@@ -736,8 +682,8 @@ _handle_l3cd_changed(NMIPConfig *self, const NML3ConfigData *l3cd)
     nm_auto_unref_l3cd const NML3ConfigData *l3cd_old    = NULL;
     GParamSpec                              *changed_params[8];
     guint                                    n_changed_params = 0;
-    const char *const                       *strv;
-    const char *const                       *strv_old;
+    const char *const                       *strarr;
+    const char *const                       *strarr_old;
     gconstpointer                            addrs;
     gconstpointer                            addrs_old;
     guint                                    len;
@@ -748,9 +694,9 @@ _handle_l3cd_changed(NMIPConfig *self, const NML3ConfigData *l3cd)
     l3cd_old   = g_steal_pointer(&priv->l3cd);
     priv->l3cd = nm_l3_config_data_ref(l3cd);
 
-    addrs_old = nm_l3_config_data_get_nameservers(l3cd_old, addr_family, &len_old);
-    addrs     = nm_l3_config_data_get_nameservers(priv->l3cd, addr_family, &len);
-    if (!nm_memeq_n(addrs_old, len_old, addrs, len, nm_utils_addr_family_to_size(addr_family))) {
+    strarr_old = nm_l3_config_data_get_nameservers(l3cd_old, addr_family, &len_old);
+    strarr     = nm_l3_config_data_get_nameservers(priv->l3cd, addr_family, &len);
+    if (!nm_strv_equal_n(strarr_old, len_old, strarr, len)) {
         if (IS_IPv4) {
             changed_params[n_changed_params++] = obj_properties_ip4[PROP_IP4_NAMESERVER_DATA];
             changed_params[n_changed_params++] = obj_properties_ip4[PROP_IP4_NAMESERVERS];
@@ -758,14 +704,14 @@ _handle_l3cd_changed(NMIPConfig *self, const NML3ConfigData *l3cd)
             changed_params[n_changed_params++] = obj_properties_ip6[PROP_IP6_NAMESERVERS];
     }
 
-    strv_old = nm_l3_config_data_get_domains(l3cd_old, addr_family, &len_old);
-    strv     = nm_l3_config_data_get_domains(priv->l3cd, addr_family, &len);
-    if (!nm_strv_equal_n(strv, len, strv_old, len_old))
+    strarr_old = nm_l3_config_data_get_domains(l3cd_old, addr_family, &len_old);
+    strarr     = nm_l3_config_data_get_domains(priv->l3cd, addr_family, &len);
+    if (!nm_strv_equal_n(strarr, len, strarr_old, len_old))
         changed_params[n_changed_params++] = obj_properties_ip[PROP_IP_DOMAINS];
 
-    strv_old = nm_l3_config_data_get_searches(l3cd_old, addr_family, &len_old);
-    strv     = nm_l3_config_data_get_searches(priv->l3cd, addr_family, &len);
-    if (!nm_strv_equal_n(strv, len, strv_old, len_old))
+    strarr_old = nm_l3_config_data_get_searches(l3cd_old, addr_family, &len_old);
+    strarr     = nm_l3_config_data_get_searches(priv->l3cd, addr_family, &len);
+    if (!nm_strv_equal_n(strarr, len, strarr_old, len_old))
         changed_params[n_changed_params++] = obj_properties_ip[PROP_IP_SEARCHES];
 
     v_i_old = nm_l3_config_data_get_dns_priority_or_default(l3cd_old, addr_family);
@@ -773,9 +719,9 @@ _handle_l3cd_changed(NMIPConfig *self, const NML3ConfigData *l3cd)
     if (v_i != v_i_old)
         changed_params[n_changed_params++] = obj_properties_ip[PROP_IP_DNS_PRIORITY];
 
-    strv_old = nm_l3_config_data_get_dns_options(l3cd_old, addr_family, &len);
-    strv     = nm_l3_config_data_get_dns_options(priv->l3cd, addr_family, &len);
-    if (!nm_strv_equal_n(strv, len, strv_old, len_old))
+    strarr_old = nm_l3_config_data_get_dns_options(l3cd_old, addr_family, &len);
+    strarr     = nm_l3_config_data_get_dns_options(priv->l3cd, addr_family, &len);
+    if (!nm_strv_equal_n(strarr, len, strarr_old, len_old))
         changed_params[n_changed_params++] = obj_properties_ip[PROP_IP_DNS_OPTIONS];
 
     if (IS_IPv4) {
diff --git a/src/core/nm-ip-config.h b/src/core/nm-ip-config.h
index 68103cf7..0dcea83b 100644
--- a/src/core/nm-ip-config.h
+++ b/src/core/nm-ip-config.h
@@ -12,7 +12,7 @@
 /*****************************************************************************/
 
 #define NM_TYPE_IP_CONFIG (nm_ip_config_get_type())
-#define NM_IP_CONFIG(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_IP_CONFIG, NMIPConfig))
+#define NM_IP_CONFIG(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_IP_CONFIG, NMIPConfig))
 #define NM_IP_CONFIG_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_IP_CONFIG, NMIPConfigClass))
 #define NM_IS_IP_CONFIG(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_IP_CONFIG))
@@ -51,8 +51,6 @@ NMIPConfig *nm_ip_config_new(int addr_family, NML3Cfg *l3cfg);
 
 void nm_ip_config_take_and_unexport_on_idle(NMIPConfig *self_take);
 
-void nm_ip_config_dns_hash(const NML3ConfigData *l3cd, GChecksum *sum, int addr_family);
-
 /*****************************************************************************/
 
 static inline NML3Cfg *
diff --git a/src/core/nm-keep-alive.h b/src/core/nm-keep-alive.h
index 7737c17a..14378d25 100644
--- a/src/core/nm-keep-alive.h
+++ b/src/core/nm-keep-alive.h
@@ -7,7 +7,7 @@
 #define __NETWORKMANAGER_KEEP_ALIVE_H__
 
 #define NM_TYPE_KEEP_ALIVE     (nm_keep_alive_get_type())
-#define NM_KEEP_ALIVE(o)       (G_TYPE_CHECK_INSTANCE_CAST((o), NM_TYPE_KEEP_ALIVE, NMKeepAlive))
+#define NM_KEEP_ALIVE(o)       (_NM_G_TYPE_CHECK_INSTANCE_CAST((o), NM_TYPE_KEEP_ALIVE, NMKeepAlive))
 #define NM_KEEP_ALIVE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), NM_TYPE_KEEP_ALIVE, NMKeepAliveClass))
 #define NM_KEEP_ALIVE_GET_CLASS(o) \
     (G_TYPE_INSTANCE_GET_CLASS((o), NM_TYPE_KEEP_ALIVE, NMKeepAliveClass))
diff --git a/src/core/nm-l3-config-data.c b/src/core/nm-l3-config-data.c
index bcd0b62a..17bb9db8 100644
--- a/src/core/nm-l3-config-data.c
+++ b/src/core/nm-l3-config-data.c
@@ -5,7 +5,7 @@
 #include "nm-l3-config-data.h"
 
 #include <linux/if.h>
-#include <linux/if_addr.h>
+#include "nm-compat-headers/linux/if_addr.h"
 #include <linux/rtnetlink.h>
 
 #include "libnm-core-intern/nm-core-internal.h"
@@ -67,10 +67,10 @@ struct _NML3ConfigData {
 
     union {
         struct {
-            GArray *nameservers_6;
-            GArray *nameservers_4;
+            GPtrArray *nameservers_6;
+            GPtrArray *nameservers_4;
         };
-        GArray *nameservers_x[2];
+        GPtrArray *nameservers_x[2];
     };
 
     union {
@@ -269,17 +269,6 @@ _garray_inaddr_get(GArray *arr, guint *out_len)
     return arr->data;
 }
 
-static gconstpointer
-_garray_inaddr_at(GArray *arr, gboolean IS_IPv4, guint idx)
-{
-    nm_assert(arr);
-    nm_assert(idx < arr->len);
-
-    if (IS_IPv4)
-        return &g_array_index(arr, in_addr_t, idx);
-    return &g_array_index(arr, struct in6_addr, idx);
-}
-
 static gboolean
 _garray_inaddr_add(GArray **p_arr, int addr_family, gconstpointer addr)
 {
@@ -352,7 +341,7 @@ nm_l3_config_data_log(const NML3ConfigData *self,
                       NMLogDomain           log_domain)
 {
     char  sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
-    char  sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char  sbuf_addr[NM_INET_ADDRSTRLEN];
     int   IS_IPv4;
     guint i;
 
@@ -480,13 +469,11 @@ nm_l3_config_data_log(const NML3ConfigData *self,
                self->dns_priority_x[IS_IPv4]);
         }
 
-        for (i = 0; i < nm_g_array_len(self->nameservers_x[IS_IPv4]); i++) {
+        for (i = 0; i < nm_g_ptr_array_len(self->nameservers_x[IS_IPv4]); i++) {
             _L("nameserver%c[%u]: %s",
                nm_utils_addr_family_to_char(addr_family),
                i,
-               nm_utils_inet_ntop(addr_family,
-                                  _garray_inaddr_at(self->nameservers_x[IS_IPv4], IS_IPv4, i),
-                                  sbuf_addr));
+               (char *) self->nameservers_x[IS_IPv4]->pdata[i]);
         }
 
         for (i = 0; i < nm_g_ptr_array_len(self->domains_x[IS_IPv4]); i++) {
@@ -514,12 +501,12 @@ nm_l3_config_data_log(const NML3ConfigData *self,
             for (i = 0; i < nm_g_array_len(self->wins); i++) {
                 _L("wins[%u]: %s",
                    i,
-                   _nm_utils_inet4_ntop(g_array_index(self->wins, in_addr_t, i), sbuf_addr));
+                   nm_inet4_ntop(nm_g_array_index(self->wins, in_addr_t, i), sbuf_addr));
             }
             for (i = 0; i < nm_g_array_len(self->nis_servers); i++) {
                 _L("nis-server[%u]: %s",
                    i,
-                   _nm_utils_inet4_ntop(g_array_index(self->nis_servers, in_addr_t, i), sbuf_addr));
+                   nm_inet4_ntop(nm_g_array_index(self->nis_servers, in_addr_t, i), sbuf_addr));
             }
             if (self->nis_domain)
                 _L("nis-domain: %s", self->nis_domain->str);
@@ -621,8 +608,7 @@ nm_l3_config_data_log(const NML3ConfigData *self,
 static gboolean
 _route_valid_4(const NMPlatformIP4Route *r)
 {
-    return r && r->plen <= 32
-           && r->network == nm_utils_ip4_address_clear_host_address(r->network, r->plen);
+    return r && r->plen <= 32 && r->network == nm_ip4_addr_clear_host_address(r->network, r->plen);
 }
 
 static gboolean
@@ -632,7 +618,7 @@ _route_valid_6(const NMPlatformIP6Route *r)
 
     return r && r->plen <= 128
            && (memcmp(&r->network,
-                      nm_utils_ip6_address_clear_host_address(&n, &r->network, r->plen),
+                      nm_ip6_addr_clear_host_address(&n, &r->network, r->plen),
                       sizeof(n))
                == 0);
 }
@@ -793,8 +779,8 @@ nm_l3_config_data_unref(const NML3ConfigData *self)
     nm_clear_pointer(&mutable->dhcp_lease_4, nm_dhcp_lease_unref);
     nm_clear_pointer(&mutable->dhcp_lease_6, nm_dhcp_lease_unref);
 
-    nm_clear_pointer(&mutable->nameservers_4, g_array_unref);
-    nm_clear_pointer(&mutable->nameservers_6, g_array_unref);
+    nm_clear_pointer(&mutable->nameservers_4, g_ptr_array_unref);
+    nm_clear_pointer(&mutable->nameservers_6, g_ptr_array_unref);
 
     nm_clear_pointer(&mutable->domains_4, g_ptr_array_unref);
     nm_clear_pointer(&mutable->domains_6, g_ptr_array_unref);
@@ -1422,7 +1408,7 @@ _check_and_add_domain(GPtrArray **p_arr, const char *domain)
     return TRUE;
 }
 
-gconstpointer
+const char *const *
 nm_l3_config_data_get_nameservers(const NML3ConfigData *self, int addr_family, guint *out_len)
 {
     nm_assert(!self || _NM_IS_L3_CONFIG_DATA(self, TRUE));
@@ -1434,33 +1420,68 @@ nm_l3_config_data_get_nameservers(const NML3ConfigData *self, int addr_family, g
         return NULL;
     }
 
-    return _garray_inaddr_get(self->nameservers_x[NM_IS_IPv4(addr_family)], out_len);
+    return nm_strv_ptrarray_get_unsafe(self->nameservers_x[NM_IS_IPv4(addr_family)], out_len);
 }
 
 gboolean
-nm_l3_config_data_add_nameserver(NML3ConfigData                        *self,
-                                 int                                    addr_family,
-                                 gconstpointer /* (const NMIPAddr *) */ nameserver)
+nm_l3_config_data_add_nameserver(NML3ConfigData *self, int addr_family, const char *nameserver)
 {
+    GPtrArray **p_arr;
+
     nm_assert(_NM_IS_L3_CONFIG_DATA(self, FALSE));
     nm_assert_addr_family(addr_family);
     nm_assert(nameserver);
 
-    return _garray_inaddr_add(&self->nameservers_x[NM_IS_IPv4(addr_family)],
-                              addr_family,
-                              nameserver);
+    if (NM_MORE_ASSERTS > 5) {
+        gs_free char *s_free = NULL;
+
+        nm_assert(
+            nm_streq0(nm_utils_dnsname_normalize(addr_family, nameserver, &s_free), nameserver));
+    }
+
+    p_arr = &self->nameservers_x[NM_IS_IPv4(addr_family)];
+
+    if (nm_strv_ptrarray_contains(*p_arr, nameserver))
+        return FALSE;
+
+    nm_strv_ptrarray_add_string_dup(nm_strv_ptrarray_ensure(p_arr), nameserver);
+    return TRUE;
+}
+
+gboolean
+nm_l3_config_data_add_nameserver_detail(NML3ConfigData *self,
+                                        int             addr_family,
+                                        gconstpointer   addr_bin,
+                                        const char     *server_name)
+{
+    gs_free char *s_free = NULL;
+    char         *s;
+    gsize         l;
+
+    nm_assert(_NM_IS_L3_CONFIG_DATA(self, FALSE));
+    nm_assert_addr_family(addr_family);
+    nm_assert(addr_bin);
+
+    l = (NM_INET_ADDRSTRLEN + 2u) + (server_name ? strlen(server_name) : 0u);
+
+    s = nm_malloc_maybe_a(300, l, &s_free);
+
+    if (!nm_utils_dnsname_construct(addr_family, addr_bin, server_name, s, l))
+        nm_assert_not_reached();
+
+    return nm_l3_config_data_add_nameserver(self, addr_family, s);
 }
 
 gboolean
 nm_l3_config_data_clear_nameservers(NML3ConfigData *self, int addr_family)
 {
-    gs_unref_array GArray *old = NULL;
+    gs_unref_ptrarray GPtrArray *old = NULL;
 
     nm_assert(_NM_IS_L3_CONFIG_DATA(self, FALSE));
     nm_assert_addr_family(addr_family);
 
     old = g_steal_pointer(&self->nameservers_x[NM_IS_IPv4(addr_family)]);
-    return (nm_g_array_len(old) > 0);
+    return (nm_g_ptr_array_len(old) > 0);
 }
 
 const in_addr_t *
@@ -2185,8 +2206,7 @@ _dedup_multi_index_cmp(const NML3ConfigData *a,
 
                 NM_CMP_DIRECT(ra.metric, rb.metric);
                 NM_CMP_DIRECT(ra.plen, rb.plen);
-                NM_CMP_RETURN_DIRECT(
-                    nm_utils_ip4_address_same_prefix_cmp(ra.network, rb.network, ra.plen));
+                NM_CMP_RETURN_DIRECT(nm_ip4_addr_same_prefix_cmp(ra.network, rb.network, ra.plen));
                 break;
             }
             case NMP_OBJECT_TYPE_IP6_ROUTE:
@@ -2197,7 +2217,7 @@ _dedup_multi_index_cmp(const NML3ConfigData *a,
                 NM_CMP_DIRECT(ra.metric, rb.metric);
                 NM_CMP_DIRECT(ra.plen, rb.plen);
                 NM_CMP_RETURN_DIRECT(
-                    nm_utils_ip6_address_same_prefix_cmp(&ra.network, &rb.network, ra.plen));
+                    nm_ip6_addr_same_prefix_cmp(&ra.network, &rb.network, ra.plen));
                 break;
             }
             default:
@@ -2274,7 +2294,6 @@ nm_l3_config_data_cmp_full(const NML3ConfigData *a,
     }
 
     for (IS_IPv4 = 1; IS_IPv4 >= 0; IS_IPv4--) {
-        const int        addr_family = IS_IPv4 ? AF_INET : AF_INET6;
         const NMPObject *def_route_a = a->best_default_route_x[IS_IPv4];
         const NMPObject *def_route_b = b->best_default_route_x[IS_IPv4];
 
@@ -2298,8 +2317,7 @@ nm_l3_config_data_cmp_full(const NML3ConfigData *a,
 
                 NM_CMP_DIRECT(ra.metric, rb.metric);
                 NM_CMP_DIRECT(ra.plen, rb.plen);
-                NM_CMP_RETURN_DIRECT(
-                    nm_utils_ip4_address_same_prefix_cmp(ra.network, rb.network, ra.plen));
+                NM_CMP_RETURN_DIRECT(nm_ip4_addr_same_prefix_cmp(ra.network, rb.network, ra.plen));
             } else {
                 NMPlatformIP6Route ra = def_route_a->ip6_route;
                 NMPlatformIP6Route rb = def_route_b->ip6_route;
@@ -2307,16 +2325,15 @@ nm_l3_config_data_cmp_full(const NML3ConfigData *a,
                 NM_CMP_DIRECT(ra.metric, rb.metric);
                 NM_CMP_DIRECT(ra.plen, rb.plen);
                 NM_CMP_RETURN_DIRECT(
-                    nm_utils_ip6_address_same_prefix_cmp(&ra.network, &rb.network, ra.plen));
+                    nm_ip6_addr_same_prefix_cmp(&ra.network, &rb.network, ra.plen));
             }
         }
 
         if (NM_FLAGS_HAS(flags, NM_L3_CONFIG_CMP_FLAGS_DNS)) {
             const NML3ConfigDatFlags FLAG = NM_L3_CONFIG_DAT_FLAGS_HAS_DNS_PRIORITY(IS_IPv4);
 
-            NM_CMP_RETURN(_garray_inaddr_cmp(a->nameservers_x[IS_IPv4],
-                                             b->nameservers_x[IS_IPv4],
-                                             addr_family));
+            NM_CMP_RETURN(
+                nm_strv_ptrarray_cmp(a->nameservers_x[IS_IPv4], b->nameservers_x[IS_IPv4]));
             NM_CMP_RETURN(nm_strv_ptrarray_cmp(a->domains_x[IS_IPv4], b->domains_x[IS_IPv4]));
             NM_CMP_RETURN(nm_strv_ptrarray_cmp(a->searches_x[IS_IPv4], b->searches_x[IS_IPv4]));
             NM_CMP_RETURN(
@@ -2391,10 +2408,7 @@ nm_l3_config_data_cmp_full(const NML3ConfigData *a,
 /*****************************************************************************/
 
 static const NMPObject *
-_data_get_direct_route_for_host(const NML3ConfigData *self,
-                                int                   addr_family,
-                                gconstpointer         host,
-                                guint32               route_table)
+_data_get_direct_route_for_host(const NML3ConfigData *self, int addr_family, gconstpointer host)
 {
     const int                 IS_IPv4        = NM_IS_IPv4(addr_family);
     const NMPObject          *best_route_obj = NULL;
@@ -2422,13 +2436,7 @@ _data_get_direct_route_for_host(const NML3ConfigData *self,
         if (best_route && best_route->rx.plen > item->rx.plen)
             continue;
 
-        if (nm_platform_route_table_uncoerce(item->rx.table_coerced, TRUE) != route_table)
-            continue;
-
-        if (!nm_utils_ip_address_same_prefix(addr_family,
-                                             host,
-                                             item->rx.network_ptr,
-                                             item->rx.plen))
+        if (!nm_ip_addr_same_prefix(addr_family, host, item->rx.network_ptr, item->rx.plen))
             continue;
 
         if (best_route && best_route->rx.metric <= item->rx.metric)
@@ -2478,9 +2486,9 @@ nm_l3_config_data_get_blacklisted_ip4_routes(const NML3ConfigData *self, gboolea
         if (my_addr->plen == 0)
             continue;
 
-        network_4 = nm_utils_ip4_address_clear_host_address(my_addr->peer_address, my_addr->plen);
+        network_4 = nm_ip4_addr_clear_host_address(my_addr->peer_address, my_addr->plen);
 
-        if (nm_utils_ip4_address_is_zeronet(network_4)) {
+        if (nm_ip4_addr_is_zeronet(network_4)) {
             /* Kernel doesn't add device-routes for destinations that
              * start with 0.x.y.z. Skip them. */
             continue;
@@ -2555,12 +2563,10 @@ nm_l3_config_data_add_dependent_onlink_routes(NML3ConfigData *self, int addr_fam
         if (nm_ip_addr_is_null(addr_family, p_gateway))
             continue;
 
-        if (!NM_PLATFORM_IP_ROUTE_IS_DEFAULT(route_src)
-            || _data_get_direct_route_for_host(
-                self,
-                addr_family,
-                p_gateway,
-                nm_platform_route_table_uncoerce(route_src->rx.table_coerced, TRUE)))
+        if (NM_FLAGS_HAS(route_src->rx.r_rtm_flags, (unsigned) RTNH_F_ONLINK))
+            continue;
+
+        if (_data_get_direct_route_for_host(self, addr_family, p_gateway))
             continue;
 
         new_route = nmp_object_clone(obj_src, FALSE);
@@ -2672,9 +2678,9 @@ nm_l3_config_data_add_dependent_device_routes(NML3ConfigData       *self,
                         a6 = &addr_src->a6.peer_address;
                     plen = 128;
                 } else {
-                    a6   = nm_utils_ip6_address_clear_host_address(&a6_stack,
-                                                                 &addr_src->a6.address,
-                                                                 addr_src->a6.plen);
+                    a6   = nm_ip6_addr_clear_host_address(&a6_stack,
+                                                        &addr_src->a6.address,
+                                                        addr_src->a6.plen);
                     plen = addr_src->a6.plen;
                 }
 
@@ -2819,12 +2825,12 @@ _init_from_connection_ip(NML3ConfigData *self, int addr_family, NMConnection *co
 
         plen = nm_ip_route_get_prefix(s_route);
 
-        nm_utils_ipx_address_clear_host_address(addr_family, &network_bin, &network_bin, plen);
+        nm_ip_addr_clear_host_address(addr_family, &network_bin, &network_bin, plen);
 
         if (IS_IPv4) {
             r.r4 = (NMPlatformIP4Route){
                 .network    = network_bin.addr4,
-                .plen       = nm_ip_route_get_prefix(s_route),
+                .plen       = plen,
                 .gateway    = next_hop_bin.addr4,
                 .metric_any = metric_any,
                 .metric     = metric,
@@ -2834,7 +2840,7 @@ _init_from_connection_ip(NML3ConfigData *self, int addr_family, NMConnection *co
         } else {
             r.r6 = (NMPlatformIP6Route){
                 .network    = network_bin.addr6,
-                .plen       = nm_ip_route_get_prefix(s_route),
+                .plen       = plen,
                 .gateway    = next_hop_bin.addr6,
                 .metric_any = metric_any,
                 .metric     = metric,
@@ -2845,19 +2851,28 @@ _init_from_connection_ip(NML3ConfigData *self, int addr_family, NMConnection *co
 
         nm_utils_ip_route_attribute_to_platform(addr_family, s_route, &r.rx, -1);
 
+        if (IS_IPv4 && r.r4.gateway == 0) {
+            /* As far as kernel is concerned, an IPv4 without gateway cannot have
+             * the onlink flag set, we need to clear it. If we wouldn't clear it,
+             * then the address we would add in kernel would be entirely different than
+             * the one we create here (because the "onlink" flag is part of the
+             * identifier of a route, see nm_platform_ip4_route_cmp()).
+             *
+             * Note however that for ECMP routes we currently can only merge routes
+             * that agree in their onlink flag. So a route without gateway cannot
+             * merge with an onlink route that has a gateway. That needs fixing,
+             * by not treating the onlink flag as for the entire route, but allowing
+             * to merge ECMP routes with different onlink flag. And first, we need
+             * to track the onlink flag for the nexthop (NMPlatformIP4RtNextHop). */
+            r.r4.r_rtm_flags &= ~((unsigned) RTNH_F_ONLINK);
+        }
+
         nm_l3_config_data_add_route(self, addr_family, NULL, &r.rx);
     }
 
     nnameservers = nm_setting_ip_config_get_num_dns(s_ip);
-    for (i = 0; i < nnameservers; i++) {
-        const char *s;
-        NMIPAddr    ip;
-
-        s = nm_setting_ip_config_get_dns(s_ip, i);
-        if (!nm_utils_parse_inaddr_bin(addr_family, s, NULL, &ip))
-            continue;
-        nm_l3_config_data_add_nameserver(self, addr_family, &ip);
-    }
+    for (i = 0; i < nnameservers; i++)
+        nm_l3_config_data_add_nameserver(self, addr_family, nm_setting_ip_config_get_dns(s_ip, i));
 
     nsearches = nm_setting_ip_config_get_num_dns_searches(s_ip);
     for (i = 0; i < nsearches; i++) {
@@ -3023,6 +3038,107 @@ nm_l3_config_data_new_from_platform(NMDedupMultiIndex        *multi_idx,
 /*****************************************************************************/
 
 void
+nm_l3_config_data_hash_dns(const NML3ConfigData *l3cd,
+                           GChecksum            *sum,
+                           int                   addr_family,
+                           NMDnsIPConfigType     dns_ip_config_type)
+{
+    guint              i;
+    int                val;
+    const char *const *strarr;
+    const in_addr_t   *wins;
+    const char *const *domains;
+    const char *const *searches;
+    const char *const *options;
+    guint              num_nameservers;
+    guint              num_wins;
+    guint              num_domains;
+    guint              num_searches;
+    guint              num_options;
+    gboolean           empty = TRUE;
+
+    g_return_if_fail(l3cd);
+    g_return_if_fail(sum);
+
+    strarr = nm_l3_config_data_get_nameservers(l3cd, addr_family, &num_nameservers);
+    for (i = 0; i < num_nameservers; i++) {
+        g_checksum_update(sum, (gpointer) strarr[i], strlen(strarr[i]));
+        empty = FALSE;
+    }
+
+    if (addr_family == AF_INET) {
+        wins = nm_l3_config_data_get_wins(l3cd, &num_wins);
+        for (i = 0; i < num_wins; i++) {
+            g_checksum_update(sum, (guint8 *) &wins[i], 4);
+            empty = FALSE;
+        }
+    }
+
+    domains = nm_l3_config_data_get_domains(l3cd, addr_family, &num_domains);
+    for (i = 0; i < num_domains; i++) {
+        g_checksum_update(sum, (const guint8 *) domains[i], strlen(domains[i]));
+        empty = FALSE;
+    }
+
+    searches = nm_l3_config_data_get_searches(l3cd, addr_family, &num_searches);
+    for (i = 0; i < num_searches; i++) {
+        g_checksum_update(sum, (const guint8 *) searches[i], strlen(searches[i]));
+        empty = FALSE;
+    }
+
+    options = nm_l3_config_data_get_dns_options(l3cd, addr_family, &num_options);
+    for (i = 0; i < num_options; i++) {
+        g_checksum_update(sum, (const guint8 *) options[i], strlen(options[i]));
+        empty = FALSE;
+    }
+
+    val = nm_l3_config_data_get_mdns(l3cd);
+    if (val != NM_SETTING_CONNECTION_MDNS_DEFAULT) {
+        g_checksum_update(sum, (const guint8 *) &val, sizeof(val));
+        empty = FALSE;
+    }
+
+    val = nm_l3_config_data_get_llmnr(l3cd);
+    if (val != NM_SETTING_CONNECTION_LLMNR_DEFAULT) {
+        g_checksum_update(sum, (const guint8 *) &val, sizeof(val));
+        empty = FALSE;
+    }
+
+    val = nm_l3_config_data_get_dns_over_tls(l3cd);
+    if (val != NM_SETTING_CONNECTION_DNS_OVER_TLS_DEFAULT) {
+        g_checksum_update(sum, (const guint8 *) &val, sizeof(val));
+        empty = FALSE;
+    }
+
+    if (!empty) {
+        int prio = 0;
+
+        /* In the DNS manager we rely on the fact that an empty (i.e. without
+         * any name server, domain, option, etc. parameters) configuration
+         * has a zero checksum. This is needed to avoid rewriting resolv.conf
+         * when not needed. Since the dns-type and the priority are always
+         * present, hash them only when the rest of configuration is not empty.
+         */
+        g_checksum_update(sum, (const guint8 *) &dns_ip_config_type, sizeof(dns_ip_config_type));
+        nm_l3_config_data_get_dns_priority(l3cd, addr_family, &prio);
+        g_checksum_update(sum, (const guint8 *) &prio, sizeof(prio));
+    }
+
+    /* FIXME(ip-config-checksum): the DNS priority should be considered relevant
+     * and added into the checksum as well, but this can't be done right now
+     * because in the DNS manager we rely on the fact that an empty
+     * configuration (i.e. just created) has a zero checksum. This is needed to
+     * avoid rewriting resolv.conf when there is no change.
+     *
+     * The DNS priority initial value depends on the connection type (VPN or
+     * not), so it's a bit difficult to add it to checksum maintaining the
+     * assumption of checksum(empty)=0
+     */
+}
+
+/*****************************************************************************/
+
+void
 nm_l3_config_data_merge(NML3ConfigData       *self,
                         const NML3ConfigData *src,
                         NML3ConfigMergeFlags  merge_flags,
@@ -3212,9 +3328,7 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
         }
 
         if (!NM_FLAGS_HAS(merge_flags, NM_L3_CONFIG_MERGE_FLAGS_NO_DNS))
-            _garray_inaddr_merge(&self->nameservers_x[IS_IPv4],
-                                 src->nameservers_x[IS_IPv4],
-                                 addr_family);
+            _strv_ptrarray_merge(&self->nameservers_x[IS_IPv4], src->nameservers_x[IS_IPv4]);
 
         if (!NM_FLAGS_HAS(merge_flags, NM_L3_CONFIG_MERGE_FLAGS_NO_DNS))
             _strv_ptrarray_merge(&self->domains_x[IS_IPv4], src->domains_x[IS_IPv4]);
diff --git a/src/core/nm-l3-config-data.h b/src/core/nm-l3-config-data.h
index ca42d4a1..bfab04d9 100644
--- a/src/core/nm-l3-config-data.h
+++ b/src/core/nm-l3-config-data.h
@@ -495,12 +495,16 @@ const in_addr_t *nm_l3_config_data_get_wins(const NML3ConfigData *self, guint *o
 
 gboolean nm_l3_config_data_add_wins(NML3ConfigData *self, in_addr_t wins);
 
-gconstpointer
+const char *const *
 nm_l3_config_data_get_nameservers(const NML3ConfigData *self, int addr_family, guint *out_len);
 
-gboolean nm_l3_config_data_add_nameserver(NML3ConfigData                        *self,
-                                          int                                    addr_family,
-                                          gconstpointer /* (const NMIPAddr *) */ nameserver);
+gboolean
+nm_l3_config_data_add_nameserver(NML3ConfigData *self, int addr_family, const char *nameserver);
+
+gboolean nm_l3_config_data_add_nameserver_detail(NML3ConfigData *self,
+                                                 int             addr_family,
+                                                 gconstpointer   addr_bin,
+                                                 const char     *server_name);
 
 gboolean nm_l3_config_data_clear_nameservers(NML3ConfigData *self, int addr_family);
 
@@ -600,4 +604,9 @@ nmtst_l3_config_data_get_best_gateway(const NML3ConfigData *self, int addr_famil
     return nm_platform_ip_route_get_gateway(addr_family, NMP_OBJECT_CAST_IP_ROUTE(rt));
 }
 
+void nm_l3_config_data_hash_dns(const NML3ConfigData *l3cd,
+                                GChecksum            *sum,
+                                int                   addr_family,
+                                NMDnsIPConfigType     dns_ip_config_type);
+
 #endif /* __NM_L3_CONFIG_DATA_H__ */
diff --git a/src/core/nm-l3-ipv4ll.c b/src/core/nm-l3-ipv4ll.c
index 3fc1e165..ec871bef 100644
--- a/src/core/nm-l3-ipv4ll.c
+++ b/src/core/nm-l3-ipv4ll.c
@@ -104,17 +104,17 @@ NM_UTILS_ENUM2STR_DEFINE(nm_l3_ipv4ll_state_to_string,
 
 /*****************************************************************************/
 
-#define _ASSERT(self)                                                                        \
-    G_STMT_START                                                                             \
-    {                                                                                        \
-        NML3IPv4LL *const _self = (self);                                                    \
-                                                                                             \
-        nm_assert(NM_IS_L3_IPV4LL(_self));                                                   \
-        if (NM_MORE_ASSERTS > 5) {                                                           \
-            nm_assert(_self->addr == 0u || nm_utils_ip4_address_is_link_local(_self->addr)); \
-            nm_assert(!_self->l3cd || NM_IS_L3_CONFIG_DATA(_self->l3cd));                    \
-        }                                                                                    \
-    }                                                                                        \
+#define _ASSERT(self)                                                               \
+    G_STMT_START                                                                    \
+    {                                                                               \
+        NML3IPv4LL *const _self = (self);                                           \
+                                                                                    \
+        nm_assert(NM_IS_L3_IPV4LL(_self));                                          \
+        if (NM_MORE_ASSERTS > 5) {                                                  \
+            nm_assert(_self->addr == 0u || nm_ip4_addr_is_link_local(_self->addr)); \
+            nm_assert(!_self->l3cd || NM_IS_L3_CONFIG_DATA(_self->l3cd));           \
+        }                                                                           \
+    }                                                                               \
     G_STMT_END
 
 /*****************************************************************************/
@@ -290,7 +290,7 @@ _ip4_address_is_link_local(const NMPlatformIP4Address *a)
 {
     nm_assert(a);
 
-    return nm_utils_ip4_address_is_link_local(a->address) && a->plen == ADDR_IPV4LL_PREFIX_LEN
+    return nm_ip4_addr_is_link_local(a->address) && a->plen == ADDR_IPV4LL_PREFIX_LEN
            && a->address == a->peer_address;
 }
 
@@ -322,7 +322,7 @@ _l3cd_config_create(int ifindex, in_addr_t addr, NMDedupMultiIndex *multi_idx)
 {
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
 
-    nm_assert(nm_utils_ip4_address_is_link_local(addr));
+    nm_assert(nm_ip4_addr_is_link_local(addr));
     nm_assert(ifindex > 0);
     nm_assert(multi_idx);
 
@@ -382,7 +382,7 @@ static void
 _ipv4ll_addrgen(NML3IPv4LL *self, gboolean generate_new_addr)
 {
     CSipHash  state;
-    char      sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char      sbuf_addr[NM_INET_ADDRSTRLEN];
     gboolean  seed_changed = FALSE;
     in_addr_t addr_new;
     guint64   h;
@@ -459,7 +459,7 @@ _ipv4ll_addrgen(NML3IPv4LL *self, gboolean generate_new_addr)
     if (!seed_changed && !generate_new_addr) {
         /* neither did the caller request a new address, nor was the seed changed. The current
          * address is still to be used. */
-        nm_assert(nm_utils_ip4_address_is_link_local(self->addr));
+        nm_assert(nm_ip4_addr_is_link_local(self->addr));
         return;
     }
 
@@ -492,9 +492,9 @@ gen_addr:
     if (self->addr == addr_new || NM_IN_SET(ntohl(addr_new) & 0x0000FF00u, 0x0000u, 0xFF00u))
         goto gen_addr;
 
-    nm_assert(nm_utils_ip4_address_is_link_local(addr_new));
+    nm_assert(nm_ip4_addr_is_link_local(addr_new));
 
-    _LOGT("addr-gen: set address %s", _nm_utils_inet4_ntop(addr_new, sbuf_addr));
+    _LOGT("addr-gen: set address %s", nm_inet4_ntop(addr_new, sbuf_addr));
     self->addr = addr_new;
 }
 
@@ -549,7 +549,7 @@ static void
 _l3cd_config_add(NML3IPv4LL *self)
 {
     nm_auto_unref_l3cd const NML3ConfigData *l3cd = NULL;
-    char                                     sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char                                     sbuf_addr[NM_INET_ADDRSTRLEN];
     gboolean                                 changed;
 
     _ASSERT(self);
@@ -571,7 +571,7 @@ _l3cd_config_add(NML3IPv4LL *self)
     self->l3cd_timeout_msec_changed = FALSE;
 
     _LOGT("add l3cd config with %s (acd-timeout %u msec%s)",
-          _nm_utils_inet4_ntop(self->addr, sbuf_addr),
+          nm_inet4_ntop(self->addr, sbuf_addr),
           self->reg_timeout_msec,
           changed ? "" : " changed");
 
@@ -639,7 +639,7 @@ _ipv4ll_platform_ip4_address_lookup(NML3IPv4LL *self, in_addr_t addr)
     if (addr == 0u)
         return NULL;
 
-    nm_assert(nm_utils_ip4_address_is_link_local(addr));
+    nm_assert(nm_ip4_addr_is_link_local(addr));
 
     pladdr = nm_platform_ip4_address_get(nm_l3_ipv4ll_get_platform(self),
                                          nm_l3_ipv4ll_get_ifindex(self),
@@ -658,7 +658,7 @@ _ipv4ll_l3cfg_get_acd_addr_info(NML3IPv4LL *self, in_addr_t addr)
     if (addr == 0u)
         return NULL;
 
-    nm_assert(nm_utils_ip4_address_is_link_local(addr));
+    nm_assert(nm_ip4_addr_is_link_local(addr));
     return nm_l3cfg_get_acd_addr_info(self->l3cfg, addr);
 }
 
@@ -766,7 +766,7 @@ _ipv4ll_set_timed_out_update(NML3IPv4LL *self, TimedOutState new_state)
 static gboolean
 _ipv4ll_set_state(NML3IPv4LL *self, NML3IPv4LLState state)
 {
-    char sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char sbuf_addr[NM_INET_ADDRSTRLEN];
     char sbuf100[100];
 
     if (self->state == state)
@@ -775,7 +775,7 @@ _ipv4ll_set_state(NML3IPv4LL *self, NML3IPv4LLState state)
     self->notify_on_idle = TRUE;
     _LOGT("state: set state %s (addr=%s)",
           nm_l3_ipv4ll_state_to_string(state, sbuf100, sizeof(sbuf100)),
-          _nm_utils_inet4_ntop(self->addr, sbuf_addr));
+          nm_inet4_ntop(self->addr, sbuf_addr));
     return TRUE;
 }
 
diff --git a/src/core/nm-l3-ipv4ll.h b/src/core/nm-l3-ipv4ll.h
index b39762a8..af74ce08 100644
--- a/src/core/nm-l3-ipv4ll.h
+++ b/src/core/nm-l3-ipv4ll.h
@@ -45,7 +45,7 @@ NM_IS_L3_IPV4LL(const NML3IPv4LL *self)
 {
     nm_assert(!self
               || (NM_IS_L3CFG(*((NML3Cfg **) self))
-                  && (*((int *) (((char *) self) + sizeof(gpointer)))) > 0));
+                  && *(NM_CAST_ALIGN(int, (((char *) self) + sizeof(gpointer)))) > 0));
     return !!self;
 }
 
diff --git a/src/core/nm-l3-ipv6ll.c b/src/core/nm-l3-ipv6ll.c
index 2640c075..2e2a6a0a 100644
--- a/src/core/nm-l3-ipv6ll.c
+++ b/src/core/nm-l3-ipv6ll.c
@@ -4,7 +4,7 @@
 
 #include "nm-l3-ipv6ll.h"
 
-#include <linux/if_addr.h>
+#include "nm-compat-headers/linux/if_addr.h"
 
 #include "nm-core-utils.h"
 
@@ -216,7 +216,7 @@ _emit_changed_on_idle_cb(gpointer user_data)
     _LOGT("emit changed signal (state=%s%s%s)",
           nm_l3_ipv6ll_state_to_string(state),
           lladdr ? ", " : "",
-          lladdr ? _nm_utils_inet6_ntop(lladdr, sbuf) : "");
+          lladdr ? nm_inet6_ntop(lladdr, sbuf) : "");
 
     self->notify_fcn(self, state, lladdr, self->user_data);
 
@@ -525,14 +525,14 @@ _check(NML3IPv6LL *self)
         if (_pladdr_is_ll_tentative(pladdr)) {
             if (_set_cur_lladdr_obj(self, NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS, pladdr)) {
                 _LOGT("changed: waiting for address %s to complete DAD",
-                      _nm_utils_inet6_ntop(&self->cur_lladdr, sbuf));
+                      nm_inet6_ntop(&self->cur_lladdr, sbuf));
                 _lladdr_handle_changed(self);
             }
             return;
         }
 
         if (_set_cur_lladdr_obj(self, NM_L3_IPV6LL_STATE_READY, pladdr)) {
-            _LOGT("changed: address %s is ready", _nm_utils_inet6_ntop(&self->cur_lladdr, sbuf));
+            _LOGT("changed: address %s is ready", nm_inet6_ntop(&self->cur_lladdr, sbuf));
             _lladdr_handle_changed(self);
         }
         return;
@@ -545,9 +545,9 @@ _check(NML3IPv6LL *self)
             NM_IN_SET(self->state, NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS, NM_L3_IPV6LL_STATE_READY));
         if (self->state == NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS)
             _LOGT("changed: address %s did not complete DAD",
-                  _nm_utils_inet6_ntop(&self->cur_lladdr, sbuf));
+                  nm_inet6_ntop(&self->cur_lladdr, sbuf));
         else {
-            _LOGT("changed: address %s is gone", _nm_utils_inet6_ntop(&self->cur_lladdr, sbuf));
+            _LOGT("changed: address %s is gone", nm_inet6_ntop(&self->cur_lladdr, sbuf));
         }
 
         /* reset the state here, so that we are sure that the following
@@ -579,8 +579,7 @@ _check(NML3IPv6LL *self)
      * If that does not happen within timeout, we assume that this address failed DAD. */
     self->wait_for_addr_source = nm_g_timeout_add_source(2000, _wait_for_addr_timeout_cb, self);
     if (_set_cur_lladdr_bin(self, NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS, &lladdr)) {
-        _LOGT("changed: starting DAD for address %s",
-              _nm_utils_inet6_ntop(&self->cur_lladdr, sbuf));
+        _LOGT("changed: starting DAD for address %s", nm_inet6_ntop(&self->cur_lladdr, sbuf));
         _lladdr_handle_changed(self);
     }
     return;
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index 8fa1e07d..a49654fe 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -7,7 +7,7 @@
 #include "libnm-std-aux/nm-linux-compat.h"
 
 #include <net/if.h>
-#include <linux/if_addr.h>
+#include "nm-compat-headers/linux/if_addr.h"
 #include <linux/if_ether.h>
 #include <linux/rtnetlink.h>
 
@@ -358,16 +358,16 @@ G_DEFINE_TYPE(NML3Cfg, nm_l3cfg, G_TYPE_OBJECT)
     }                                                                         \
     G_STMT_END
 
-#define _LOGT_acd(acd_data, ...)                                      \
-    G_STMT_START                                                      \
-    {                                                                 \
-        char _sbuf_acd[NM_UTILS_INET_ADDRSTRLEN];                     \
-                                                                      \
-        _LOGT("acd[%s, %s]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),     \
-              _nm_utils_inet4_ntop((acd_data)->info.addr, _sbuf_acd), \
-              _l3_acd_addr_state_to_string((acd_data)->info.state)    \
-                  _NM_UTILS_MACRO_REST(__VA_ARGS__));                 \
-    }                                                                 \
+#define _LOGT_acd(acd_data, ...)                                   \
+    G_STMT_START                                                   \
+    {                                                              \
+        char _sbuf_acd[NM_INET_ADDRSTRLEN];                        \
+                                                                   \
+        _LOGT("acd[%s, %s]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),  \
+              nm_inet4_ntop((acd_data)->info.addr, _sbuf_acd),     \
+              _l3_acd_addr_state_to_string((acd_data)->info.state) \
+                  _NM_UTILS_MACRO_REST(__VA_ARGS__));              \
+    }                                                              \
     G_STMT_END
 
 /*****************************************************************************/
@@ -548,7 +548,7 @@ _l3_config_notify_data_to_string(const NML3ConfigNotifyData *notify_data,
                                  char                       *sbuf,
                                  gsize                       sbuf_size)
 {
-    char      sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char      sbuf_addr[NM_INET_ADDRSTRLEN];
     char      sbuf100[100];
     char      sbufobf[NM_HASH_OBFUSCATE_PTR_STR_BUF_SIZE];
     char     *s = sbuf;
@@ -577,7 +577,7 @@ _l3_config_notify_data_to_string(const NML3ConfigNotifyData *notify_data,
         nm_strbuf_append(&s,
                          &l,
                          ", addr=%s, state=%s",
-                         _nm_utils_inet4_ntop(notify_data->acd_event.info.addr, sbuf_addr),
+                         nm_inet4_ntop(notify_data->acd_event.info.addr, sbuf_addr),
                          _l3_acd_addr_state_to_string(notify_data->acd_event.info.state));
         break;
     case NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE:
@@ -603,7 +603,7 @@ _l3_config_notify_data_to_string(const NML3ConfigNotifyData *notify_data,
             &l,
             ", ipv4ll=" NM_HASH_OBFUSCATE_PTR_FMT "%s%s, state=%s",
             NM_HASH_OBFUSCATE_PTR(notify_data->ipv4ll_event.ipv4ll),
-            NM_PRINT_FMT_QUOTED2(addr4 != 0, ", addr=", _nm_utils_inet4_ntop(addr4, sbuf_addr), ""),
+            NM_PRINT_FMT_QUOTED2(addr4 != 0, ", addr=", nm_inet4_ntop(addr4, sbuf_addr), ""),
             nm_l3_ipv4ll_state_to_string(nm_l3_ipv4ll_get_state(notify_data->ipv4ll_event.ipv4ll),
                                          sbuf100,
                                          sizeof(sbuf100)));
@@ -1143,7 +1143,7 @@ _commit_collect_routes(NML3Cfg          *self,
                                                NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4));
 
     if (!head_entry)
-        return;
+        goto loop_done;
 
     c_list_for_each_entry (entry, &head_entry->lst_entries_head, lst_entries) {
         const NMPObject *obj = entry->obj;
@@ -1152,6 +1152,14 @@ _commit_collect_routes(NML3Cfg          *self,
         if (_obj_is_route_nodev(obj))
             r = routes_nodev;
         else {
+            nm_assert(NMP_OBJECT_CAST_IP_ROUTE(obj)->ifindex == self->priv.ifindex);
+
+            if (IS_IPv4 && NMP_OBJECT_CAST_IP4_ROUTE(obj)->weight > 0) {
+                /* This route needs to be registered as ECMP route. */
+                nm_netns_ip_route_ecmp_register(self->priv.netns, self, obj);
+                continue;
+            }
+
             if (!_obj_states_sync_filter(self, obj, commit_type))
                 continue;
             r = routes;
@@ -1162,6 +1170,36 @@ _commit_collect_routes(NML3Cfg          *self,
 
         g_ptr_array_add(*r, (gpointer) nmp_object_ref(obj));
     }
+
+loop_done:
+
+    if (IS_IPv4) {
+        gs_unref_ptrarray GPtrArray *singlehop_routes = NULL;
+        guint                        i;
+
+        /* NMNetns will merge the routes. The ones that found a merge partner are true multihop
+         * routes, with potentially a next hop on different interfaces. The routes
+         * that didn't find a merge partner are returned in "singlehop_routes". */
+        nm_netns_ip_route_ecmp_commit(self->priv.netns,
+                                      self,
+                                      &singlehop_routes,
+                                      NM_IN_SET(commit_type, NM_L3_CFG_COMMIT_TYPE_REAPPLY));
+
+        if (singlehop_routes) {
+            for (i = 0; i < singlehop_routes->len; i++) {
+                const NMPObject *obj = singlehop_routes->pdata[i];
+
+                if (!_obj_states_sync_filter(self, obj, commit_type))
+                    continue;
+
+                if (!*routes)
+                    *routes =
+                        g_ptr_array_new_with_free_func((GDestroyNotify) nm_dedup_multi_obj_unref);
+
+                g_ptr_array_add(*routes, (gpointer) nmp_object_ref(obj));
+            }
+        }
+    }
 }
 
 static void
@@ -1430,7 +1468,8 @@ _acd_data_free(AcdData *acd_data)
 }
 
 static guint
-_acd_data_collect_tracks_data(const AcdData     *acd_data,
+_acd_data_collect_tracks_data(NML3Cfg           *self,
+                              const AcdData     *acd_data,
                               NMTernary          dirty_selector,
                               guint32           *out_best_acd_timeout_msec,
                               NML3AcdDefendType *out_best_acd_defend_type)
@@ -1466,6 +1505,12 @@ _acd_data_collect_tracks_data(const AcdData     *acd_data,
     nm_assert(n == 0 || best_acd_defend_type > _NM_L3_ACD_DEFEND_TYPE_NONE);
     nm_assert(best_acd_defend_type <= NM_L3_ACD_DEFEND_TYPE_ALWAYS);
 
+    if (self->priv.ifindex == NM_LOOPBACK_IFINDEX) {
+        /* On loopback interface, ACD makes no sense. We always force the
+         * timeout to zero, which means no ACD. */
+        best_acd_timeout_msec = 0;
+    }
+
     NM_SET_OUT(out_best_acd_timeout_msec, n > 0 ? best_acd_timeout_msec : 0u);
     NM_SET_OUT(out_best_acd_defend_type, best_acd_defend_type);
     return n;
@@ -2230,7 +2275,7 @@ _l3_acd_data_state_change(NML3Cfg           *self,
     gint64            now_msec;
     const char       *log_reason;
     char              sbuf256[256];
-    char              sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char              sbuf_addr[NM_INET_ADDRSTRLEN];
 
     if (!p_now_msec) {
         now_msec   = 0;
@@ -2313,7 +2358,8 @@ _l3_acd_data_state_change(NML3Cfg           *self,
         return;
 
 handle_init:
-        if (_acd_data_collect_tracks_data(acd_data,
+        if (_acd_data_collect_tracks_data(self,
+                                          acd_data,
                                           NM_TERNARY_FALSE,
                                           &acd_timeout_msec,
                                           &acd_defend_type)
@@ -2362,7 +2408,8 @@ handle_init:
 
         /* we just did a commit of the IP configuration and now visit all ACD states
          * and kick off the necessary actions... */
-        if (_acd_data_collect_tracks_data(acd_data,
+        if (_acd_data_collect_tracks_data(self,
+                                          acd_data,
                                           NM_TERNARY_TRUE,
                                           &acd_timeout_msec,
                                           &acd_defend_type)
@@ -2465,7 +2512,8 @@ handle_init:
             /* after a timeout, re-probe the address. This only happens if the caller
              * does not deconfigure the address after USED/CONFLICT. But in that case,
              * we eventually want to retry. */
-            if (_acd_data_collect_tracks_data(acd_data,
+            if (_acd_data_collect_tracks_data(self,
+                                              acd_data,
                                               NM_TERNARY_TRUE,
                                               &acd_timeout_msec,
                                               &acd_defend_type)
@@ -2550,7 +2598,7 @@ handle_init:
         if (!_l3_acd_data_defendconflict_warning_ratelimited(acd_data, p_now_msec)) {
             _LOGI("IPv4 address %s is used on network connected to interface %d%s%s%s from "
                   "host %s",
-                  _nm_utils_inet4_ntop(acd_data->info.addr, sbuf_addr),
+                  nm_inet4_ntop(acd_data->info.addr, sbuf_addr),
                   self->priv.ifindex,
                   NM_PRINT_FMT_QUOTED(self->priv.plobj_next,
                                       " (",
@@ -2565,7 +2613,7 @@ handle_init:
         nm_assert(acd_data->info.state == NM_L3_ACD_ADDR_STATE_DEFENDING);
         _LOGT_acd(acd_data,
                   "address %s defended from %s",
-                  _nm_utils_inet4_ntop(acd_data->info.addr, sbuf_addr),
+                  nm_inet4_ntop(acd_data->info.addr, sbuf_addr),
                   nm_ether_addr_to_string_a(sender_addr));
         /* we just log an info message. Nothing else to do. */
         return;
@@ -2575,7 +2623,7 @@ handle_init:
 
         _LOGT_acd(acd_data,
                   "address conflict for %s detected with %s",
-                  _nm_utils_inet4_ntop(acd_data->info.addr, sbuf_addr),
+                  nm_inet4_ntop(acd_data->info.addr, sbuf_addr),
                   nm_ether_addr_to_string_a(sender_addr));
 
         if (!_l3_acd_data_defendconflict_warning_ratelimited(acd_data, p_now_msec)) {
@@ -2587,7 +2635,7 @@ handle_init:
                                       NMP_OBJECT_CAST_LINK(self->priv.plobj_next)->name,
                                       ")",
                                       ""),
-                  _nm_utils_inet4_ntop(acd_data->info.addr, sbuf_addr),
+                  nm_inet4_ntop(acd_data->info.addr, sbuf_addr),
                   nm_ether_addr_to_string_a(sender_addr));
         }
 
@@ -2990,68 +3038,103 @@ nm_l3cfg_check_ready(NML3Cfg               *self,
                      const NML3ConfigData  *l3cd,
                      int                    addr_family,
                      NML3CfgCheckReadyFlags flags,
-                     gboolean              *acd_used)
+                     GArray               **conflicts)
 {
     NMDedupMultiIter iter;
     const NMPObject *obj;
+    gboolean         ready = TRUE;
 
     nm_assert(NM_IS_L3CFG(self));
-    nm_assert_addr_family_or_unspec(addr_family);
-
-    NM_SET_OUT(acd_used, FALSE);
+    nm_assert_addr_family(addr_family);
+    nm_assert(!conflicts || !*conflicts);
 
     if (!l3cd)
         return TRUE;
 
-    if (NM_IN_SET(addr_family, AF_UNSPEC, AF_INET)
-        && NM_FLAGS_HAS(flags, NM_L3CFG_CHECK_READY_FLAGS_IP4_ACD_READY)) {
-        gboolean pending = FALSE;
-
-        nm_l3_config_data_iter_obj_for_each (&iter, l3cd, &obj, NMP_OBJECT_TYPE_IP4_ADDRESS) {
-            const NML3AcdAddrInfo *addr_info;
-
-            addr_info = nm_l3cfg_get_acd_addr_info(self, NMP_OBJECT_CAST_IP4_ADDRESS(obj)->address);
-            if (!addr_info) {
-                /* We don't track the this address? That's odd. Not ready. */
-                pending = TRUE;
-            } else {
-                if (addr_info->state <= NM_L3_ACD_ADDR_STATE_PROBING) {
-                    /* Still probing. Not ready. */
-                    pending = TRUE;
-                } else if (addr_info->state == NM_L3_ACD_ADDR_STATE_USED) {
-                    NM_SET_OUT(acd_used, TRUE);
+    if (addr_family == AF_INET) {
+        if (NM_FLAGS_HAS(flags, NM_L3CFG_CHECK_READY_FLAGS_IP4_ACD_READY)) {
+            nm_l3_config_data_iter_obj_for_each (&iter, l3cd, &obj, NMP_OBJECT_TYPE_IP4_ADDRESS) {
+                const NML3AcdAddrInfo *addr_info;
+
+                addr_info =
+                    nm_l3cfg_get_acd_addr_info(self, NMP_OBJECT_CAST_IP4_ADDRESS(obj)->address);
+                if (!addr_info) {
+                    /* We don't track the this address? That's odd. Not ready. */
+                    ready = FALSE;
+                } else {
+                    if (addr_info->state <= NM_L3_ACD_ADDR_STATE_PROBING) {
+                        /* Still probing. Not ready. */
+                        ready = FALSE;
+                    } else if (addr_info->state == NM_L3_ACD_ADDR_STATE_USED) {
+                        if (conflicts) {
+                            if (!*conflicts)
+                                *conflicts = g_array_new(FALSE,
+                                                         FALSE,
+                                                         nm_utils_addr_family_to_size(addr_family));
+                            g_array_append_val(*conflicts, addr_info->addr);
+                        }
+                    }
                 }
+                /* we only care that we don't have ACD still pending. Otherwise we are ready,
+                 * including if we have no addr_info about this address or the address is in use. */
             }
-            /* we only care that we don't have ACD still pending. Otherwise we are ready,
-             * including if we have no addr_info about this address or the address is in use. */
         }
-        if (pending)
-            return FALSE;
+        return ready;
     }
 
-    if (NM_IN_SET(addr_family, AF_UNSPEC, AF_INET6)
-        && NM_FLAGS_HAS(flags, NM_L3CFG_CHECK_READY_FLAGS_IP6_DAD_READY)) {
+    /* AF_INET6 */
+    if (NM_FLAGS_HAS(flags, NM_L3CFG_CHECK_READY_FLAGS_IP6_DAD_READY)) {
         nm_l3_config_data_iter_obj_for_each (&iter, l3cd, &obj, NMP_OBJECT_TYPE_IP6_ADDRESS) {
             ObjStateData *obj_state;
+            gboolean      dadfailed = FALSE;
 
             obj_state = g_hash_table_lookup(self->priv.p->obj_state_hash, &obj);
 
             if (!obj_state) {
                 /* Hm, we don't track this object? That is odd. Not ready. */
-                return FALSE;
+                ready = FALSE;
+                continue;
             }
 
             if (!obj_state->os_nm_configured && !obj_state->os_plobj) {
                 /* We didn't (yet) configure this address and it also is not in platform.
                  * Not ready. */
-                return FALSE;
+                ready = FALSE;
+                continue;
+            }
+
+            if (obj_state->os_plobj
+                && NM_FLAGS_HAS(NMP_OBJECT_CAST_IP6_ADDRESS(obj_state->os_plobj)->n_ifa_flags,
+                                IFA_F_DADFAILED)) {
+                /* The address is still present with DADFAILED flag. */
+                dadfailed = TRUE;
+            } else if (obj_state->os_nm_configured && !obj_state->os_plobj
+                       && nm_platform_ip6_dadfailed_check(
+                           self->priv.platform,
+                           self->priv.ifindex,
+                           &NMP_OBJECT_CAST_IP6_ADDRESS(obj_state->obj)->address)) {
+                /* We configured the address and kernel removed it with DADFAILED flag. */
+                dadfailed = TRUE;
+            }
+
+            if (dadfailed) {
+                if (conflicts) {
+                    if (!*conflicts) {
+                        *conflicts =
+                            g_array_new(FALSE, FALSE, nm_utils_addr_family_to_size(addr_family));
+                    }
+                    g_array_append_val(*conflicts,
+                                       NMP_OBJECT_CAST_IP6_ADDRESS(obj_state->obj)->address);
+                }
+                continue;
             }
 
             if (obj_state->os_plobj
                 && NM_FLAGS_HAS(NMP_OBJECT_CAST_IP6_ADDRESS(obj_state->os_plobj)->n_ifa_flags,
                                 IFA_F_TENTATIVE)) {
                 /* The address is configured in kernel, but still tentative. Not ready. */
-                return FALSE;
+                ready = FALSE;
+                continue;
             }
 
             /* This address is ready. Even if it is not (not anymore) configured in kernel (as
@@ -3060,7 +3143,7 @@ nm_l3cfg_check_ready(NML3Cfg               *self,
         }
     }
 
-    return TRUE;
+    return ready;
 }
 
 /*****************************************************************************/
@@ -3144,7 +3227,7 @@ nm_l3cfg_commit_on_idle_is_scheduled(NML3Cfg *self)
 /*****************************************************************************/
 
 #define _l3_config_datas_at(l3_config_datas, idx) \
-    (&g_array_index((l3_config_datas), L3ConfigData, (idx)))
+    (&nm_g_array_index((l3_config_datas), L3ConfigData, (idx)))
 
 static gssize
 _l3_config_datas_find_next(GArray               *l3_config_datas,
@@ -3395,8 +3478,10 @@ nm_l3cfg_add_config(NML3Cfg              *self,
 
     nm_assert(l3_config_data->acd_defend_type_confdata == acd_defend_type);
 
-    if (changed)
+    if (changed) {
         _l3_changed_configs_set_dirty(self);
+        nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
+    }
 
     return changed;
 }
@@ -3440,6 +3525,9 @@ _l3cfg_remove_config(NML3Cfg              *self,
         }
     }
 
+    if (changed)
+        nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
+
     if (self->priv.p->l3_config_datas->len == 0) {
         nm_assert(changed);
         nm_clear_pointer(&self->priv.p->l3_config_datas, g_array_unref);
@@ -3713,6 +3801,38 @@ _l3cfg_update_combined_config(NML3Cfg               *self,
                                     &hook_data);
         }
 
+        if (self->priv.ifindex == NM_LOOPBACK_IFINDEX) {
+            NMPlatformIPXAddress ax;
+            NMPlatformIPXRoute   rx;
+
+            if (!nm_l3_config_data_lookup_address_4(l3cd,
+                                                    NM_IPV4LO_ADDR1,
+                                                    NM_IPV4LO_PREFIXLEN,
+                                                    NM_IPV4LO_ADDR1)) {
+                nm_l3_config_data_add_address_4(
+                    l3cd,
+                    nm_platform_ip4_address_init_loopback_addr1(&ax.a4));
+            }
+            if (!nm_l3_config_data_lookup_address_6(l3cd, &in6addr_loopback)) {
+                nm_l3_config_data_add_address_6(l3cd,
+                                                nm_platform_ip6_address_init_loopback(&ax.a6));
+            }
+
+            rx.r4 = (NMPlatformIP4Route){
+                .ifindex       = NM_LOOPBACK_IFINDEX,
+                .rt_source     = NM_IP_CONFIG_SOURCE_KERNEL,
+                .network       = NM_IPV4LO_ADDR1,
+                .plen          = NM_IPV4LO_PREFIXLEN,
+                .table_coerced = nm_platform_route_table_coerce(RT_TABLE_LOCAL),
+                .scope_inv     = nm_platform_route_scope_inv(RT_SCOPE_HOST),
+                .type_coerced  = nm_platform_route_type_coerce(RTN_LOCAL),
+                .pref_src      = NM_IPV4LO_ADDR1,
+            };
+            nm_platform_ip_route_normalize(AF_INET, &rx.rx);
+            if (!nm_l3_config_data_lookup_route(l3cd, AF_INET, &rx.rx)) {
+                nm_l3_config_data_add_route_4(l3cd, &rx.r4);
+            }
+        }
         for (i = 0; i < l3_config_datas_len; i++) {
             const L3ConfigData *l3cd_data = l3_config_datas_arr[i];
             int                 IS_IPv4;
@@ -4371,9 +4491,9 @@ _l3_commit_mptcp_af(NML3Cfg          *self,
                 default:
                     if (IS_IPv4) {
                         /* We take all addresses, including rfc1918 private addresses
-                         * (nm_utils_ip_is_site_local()). */
+                         * (nm_ip_addr_is_site_local()). */
                     } else {
-                        if (nm_utils_ip6_is_ula(&addr->a6.address)) {
+                        if (nm_ip6_addr_is_ula(&addr->a6.address)) {
                             /* Exclude unique local IPv6 addresses fc00::/7. */
                             goto skip_addr;
                         } else {
@@ -4470,7 +4590,7 @@ _l3_commit_one(NML3Cfg              *self,
     gs_unref_ptrarray GPtrArray *addresses_prune                    = NULL;
     gs_unref_ptrarray GPtrArray *routes_prune                       = NULL;
     gs_unref_ptrarray GPtrArray *routes_temporary_not_available_arr = NULL;
-    NMIPRouteTableSyncMode       route_table_sync = NM_IP_ROUTE_TABLE_SYNC_MODE_NONE;
+    NMIPRouteTableSyncMode       route_table_sync;
     gboolean                     final_failure_for_temporary_not_available = FALSE;
     char                         sbuf_commit_type[50];
     gboolean                     success = TRUE;
@@ -4487,15 +4607,15 @@ _l3_commit_one(NML3Cfg              *self,
           nm_utils_addr_family_to_char(addr_family),
           _l3_cfg_commit_type_to_string(commit_type, sbuf_commit_type, sizeof(sbuf_commit_type)));
 
-    if (self->priv.p->combined_l3cd_commited) {
-        addresses = _commit_collect_addresses(self, addr_family, commit_type);
+    addresses = _commit_collect_addresses(self, addr_family, commit_type);
 
-        _commit_collect_routes(self, addr_family, commit_type, &routes, &routes_nodev);
+    _commit_collect_routes(self, addr_family, commit_type, &routes, &routes_nodev);
 
-        route_table_sync =
-            nm_l3_config_data_get_route_table_sync(self->priv.p->combined_l3cd_commited,
-                                                   addr_family);
-    }
+    route_table_sync =
+        self->priv.p->combined_l3cd_commited
+            ? nm_l3_config_data_get_route_table_sync(self->priv.p->combined_l3cd_commited,
+                                                     addr_family)
+            : NM_IP_ROUTE_TABLE_SYNC_MODE_NONE;
 
     if (!IS_IPv4) {
         _l3_commit_ip6_privacy(self, commit_type);
@@ -4552,6 +4672,23 @@ _l3_commit_one(NML3Cfg              *self,
         }
     }
 
+    if (self->priv.ifindex == NM_LOOPBACK_IFINDEX) {
+        if (!addresses) {
+            NMPlatformIPXAddress ax;
+
+            addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
+            if (IS_IPv4) {
+                g_ptr_array_add(
+                    addresses,
+                    nmp_object_new(NMP_OBJECT_TYPE_IP4_ADDRESS,
+                                   nm_platform_ip4_address_init_loopback_addr1(&ax.a4)));
+            } else {
+                g_ptr_array_add(addresses,
+                                nmp_object_new(NMP_OBJECT_TYPE_IP6_ADDRESS,
+                                               nm_platform_ip6_address_init_loopback(&ax.a6)));
+            }
+        }
+    }
     /* FIXME(l3cfg): need to honor and set nm_l3_config_data_get_ndisc_*(). */
     /* FIXME(l3cfg): need to honor and set nm_l3_config_data_get_mtu(). */
 
@@ -4559,7 +4696,10 @@ _l3_commit_one(NML3Cfg              *self,
                                 addr_family,
                                 self->priv.ifindex,
                                 addresses,
-                                addresses_prune);
+                                addresses_prune,
+                                self->priv.ifindex == NM_LOOPBACK_IFINDEX
+                                    ? NMP_IP_ADDRESS_SYNC_FLAGS_NONE
+                                    : NMP_IP_ADDRESS_SYNC_FLAGS_WITH_NOPREFIXROUTE);
 
     _nodev_routes_sync(self, addr_family, commit_type, routes_nodev);
 
@@ -4810,6 +4950,9 @@ nm_l3cfg_commit_type_register(NML3Cfg                 *self,
         c_list_link_tail(&self->priv.p->commit_type_lst_head, &handle->commit_type_lst);
 
     ret = handle;
+
+    nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
+
 out:
     _LOGT("commit type register (type \"%s\", source \"%s\", existing " NM_HASH_OBFUSCATE_PTR_FMT
           ") -> " NM_HASH_OBFUSCATE_PTR_FMT "",
@@ -4832,6 +4975,8 @@ nm_l3cfg_commit_type_unregister(NML3Cfg *self, NML3CfgCommitTypeHandle *handle)
 
     _LOGT("commit type unregister " NM_HASH_OBFUSCATE_PTR_FMT "", NM_HASH_OBFUSCATE_PTR(handle));
 
+    nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
+
     c_list_unlink_stale(&handle->commit_type_lst);
     if (c_list_is_empty(&self->priv.p->commit_type_lst_head))
         g_object_unref(self);
@@ -5024,6 +5169,9 @@ nm_l3cfg_init(NML3Cfg *self)
     c_list_init(&self->priv.p->blocked_lst_head_4);
     c_list_init(&self->priv.p->blocked_lst_head_6);
 
+    c_list_init(&self->internal_netns.signal_pending_lst);
+    c_list_init(&self->internal_netns.ecmp_track_ifindex_lst_head);
+
     self->priv.p->obj_state_hash = g_hash_table_new_full(nmp_object_indirect_id_hash,
                                                          nmp_object_indirect_id_equal,
                                                          _obj_state_data_free,
@@ -5066,6 +5214,9 @@ finalize(GObject *object)
     NML3Cfg *self = NM_L3CFG(object);
     gboolean changed;
 
+    nm_assert(c_list_is_empty(&self->internal_netns.signal_pending_lst));
+    nm_assert(c_list_is_empty(&self->internal_netns.ecmp_track_ifindex_lst_head));
+
     nm_assert(!self->priv.p->ipconfig_4);
     nm_assert(!self->priv.p->ipconfig_6);
 
diff --git a/src/core/nm-l3cfg.h b/src/core/nm-l3cfg.h
index e0257623..9d622b4a 100644
--- a/src/core/nm-l3cfg.h
+++ b/src/core/nm-l3cfg.h
@@ -13,7 +13,7 @@
 #define NM_ACD_TIMEOUT_MAX_MSEC         30000u
 
 #define NM_TYPE_L3CFG            (nm_l3cfg_get_type())
-#define NM_L3CFG(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_L3CFG, NML3Cfg))
+#define NM_L3CFG(obj)            (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_L3CFG, NML3Cfg))
 #define NM_L3CFG_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_L3CFG, NML3CfgClass))
 #define NM_IS_L3CFG(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_L3CFG))
 #define NM_IS_L3CFG_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_L3CFG))
@@ -209,6 +209,16 @@ struct _NML3Cfg {
         const NMPObject          *plobj_next;
         int                       ifindex;
     } priv;
+
+    /* NML3Cfg strongly cooperates with NMNetns. The latter is
+     * the one that creates and manages (also) the lifetime of the
+     * NML3Cfg instance. We track some per-l3cfg-data that is only
+     * relevant to NMNetns here. */
+    struct {
+        guint32 signal_pending_obj_type_flags;
+        CList   signal_pending_lst;
+        CList   ecmp_track_ifindex_lst_head;
+    } internal_netns;
 };
 
 typedef struct _NML3CfgClass NML3CfgClass;
@@ -400,7 +410,7 @@ gboolean nm_l3cfg_check_ready(NML3Cfg               *self,
                               const NML3ConfigData  *l3cd,
                               int                    addr_family,
                               NML3CfgCheckReadyFlags flags,
-                              gboolean              *acd_used);
+                              GArray               **conflicts);
 
 gboolean nm_l3cfg_has_temp_not_available_obj(NML3Cfg *self, int addr_family);
 
diff --git a/src/core/nm-manager.c b/src/core/nm-manager.c
index 822df7ad..66547294 100644
--- a/src/core/nm-manager.c
+++ b/src/core/nm-manager.c
@@ -19,6 +19,7 @@
 #include "NetworkManagerUtils.h"
 #include "devices/nm-device-factory.h"
 #include "devices/nm-device-generic.h"
+#include "devices/nm-device-loopback.h"
 #include "devices/nm-device.h"
 #include "dns/nm-dns-manager.h"
 #include "dhcp/nm-dhcp-manager.h"
@@ -114,6 +115,7 @@ static guint signals[LAST_SIGNAL] = {0};
 
 NM_GOBJECT_PROPERTIES_DEFINE(NMManager,
                              PROP_VERSION,
+                             PROP_VERSION_INFO,
                              PROP_CAPABILITIES,
                              PROP_STATE,
                              PROP_STARTUP,
@@ -419,6 +421,31 @@ static NM_CACHED_QUARK_FCN("autoconnect-root", autoconnect_root_quark);
 
 /*****************************************************************************/
 
+static GVariant *
+_version_info_get(void)
+{
+    const guint32 arr[] = {
+        NM_VERSION,
+    };
+
+    /* The array contains as first element NM_VERSION, which can be
+     * used to numerically compare the version (see also NM_ENCODE_VERSION,
+     * nm_utils_version(), nm_encode_version() and nm_decode_version().
+     *
+     * The following elements of the array are a bitfield of capabilities.
+     * These capabilities should only depend on compile-time abilities
+     * (unlike NM_MANAGER_CAPABILITIES, NMCapability). The supported values
+     * are from NMVersionInfoCapability enum. This way to expose capabilities
+     * is more cumbersome but more efficient compared to NM_MANAGER_CAPABILITIES.
+     * As such, it is cheap to add capabilities for something, where you would
+     * avoid it as NM_MANAGER_CAPABILITIES due to the overhead.
+     */
+
+    return nm_g_variant_new_au(arr, G_N_ELEMENTS(arr));
+}
+
+/*****************************************************************************/
+
 static gboolean
 _connection_is_vpn(NMConnection *connection)
 {
@@ -632,11 +659,8 @@ static guint
 _device_route_metric_data_by_ifindex_hash(gconstpointer p)
 {
     const DeviceRouteMetricData *data = p;
-    NMHashState                  h;
 
-    nm_hash_init(&h, 1030338191);
-    nm_hash_update_vals(&h, data->ifindex);
-    return nm_hash_complete(&h);
+    return nm_hash_val(1030338191, data->ifindex);
 }
 
 static gboolean
@@ -1464,8 +1488,11 @@ find_best_device_state(NMManager *manager)
     NMActiveConnection *ac;
 
     c_list_for_each_entry (ac, &priv->active_connections_lst_head, active_connections_lst) {
-        NMActiveConnectionState ac_state = nm_active_connection_get_state(ac);
+        NMActiveConnectionState ac_state;
 
+        if (NM_IS_DEVICE_LOOPBACK(nm_active_connection_get_device(ac)))
+            continue;
+        ac_state = nm_active_connection_get_state(ac);
         switch (ac_state) {
         case NM_ACTIVE_CONNECTION_STATE_ACTIVATED:
             if (nm_active_connection_get_default(ac, AF_UNSPEC)) {
@@ -1890,8 +1917,11 @@ find_parent_device_for_connection(NMManager       *self,
      * with some known device.
      */
     c_list_for_each_entry (candidate, &priv->devices_lst_head, devices_lst) {
-        /* Unmanaged devices are not compatible with any connection */
-        if (!nm_device_get_managed(candidate, FALSE))
+        /* For a realized device, check that it's managed; otherwise it's not
+         * compatible with any connection. If the device is unrealized then
+         * the managed state is meaningless.
+         */
+        if (nm_device_is_real(candidate) && !nm_device_get_managed(candidate, FALSE))
             continue;
 
         if (nm_device_get_settings_connection(candidate) == parent_connection)
@@ -1949,7 +1979,7 @@ nm_manager_get_connection_iface(NMManager    *self,
 
         g_set_error(error,
                     NM_MANAGER_ERROR,
-                    NM_MANAGER_ERROR_FAILED,
+                    NM_MANAGER_ERROR_MISSING_PLUGIN,
                     "NetworkManager plugin for '%s' unavailable",
                     nm_connection_get_connection_type(connection));
         return NULL;
@@ -3083,12 +3113,11 @@ recheck_assume_connection(NMManager *self, NMDevice *device)
     g_return_val_if_fail(NM_IS_DEVICE(device), FALSE);
 
     if (!nm_device_get_managed(device, FALSE)) {
-        /* If the device is unmanaged by NM_UNMANAGED_PLATFORM_INIT or NM_UNMANAGED_PARENT,
+        /* If the device is unmanaged by NM_UNMANAGED_PLATFORM_INIT,
          * don't reset the state now but wait until it becomes managed. */
-        if (nm_device_get_unmanaged_flags(device, NM_UNMANAGED_ALL)
-            & ~(NM_UNMANAGED_PLATFORM_INIT | NM_UNMANAGED_PARENT))
+        if (nm_device_get_unmanaged_flags(device, NM_UNMANAGED_ALL) & ~NM_UNMANAGED_PLATFORM_INIT)
             nm_device_assume_state_reset(device);
-        _LOG2D(LOGD_DEVICE, device, "assume: don't assume because %s", "not managed");
+        _LOG2D(LOGD_DEVICE, device, "assume: don't assume because device is not managed");
         return FALSE;
     }
 
@@ -3341,6 +3370,9 @@ _get_best_connectivity(NMManager *self, int addr_family)
         NMConnectivityState state;
         gint64              metric;
 
+        if (NM_IS_DEVICE_LOOPBACK(dev))
+            continue;
+
         r = nm_device_get_best_default_route(dev, addr_family);
         if (r)
             metric = NMP_OBJECT_CAST_IP_ROUTE(r)->metric;
@@ -3410,10 +3442,9 @@ _device_realize_finish(NMManager *self, NMDevice *device, const NMPlatformLink *
     nm_device_realize_finish(device, plink);
 
     if (!nm_device_get_managed(device, FALSE)) {
-        /* If the device is unmanaged by NM_UNMANAGED_PLATFORM_INIT or NM_UNMANAGED_PARENT,
+        /* If the device is unmanaged by NM_UNMANAGED_PLATFORM_INIT,
          * don't reset the state now but wait until it becomes managed. */
-        if (nm_device_get_unmanaged_flags(device, NM_UNMANAGED_ALL)
-            & ~(NM_UNMANAGED_PLATFORM_INIT | NM_UNMANAGED_PARENT))
+        if (nm_device_get_unmanaged_flags(device, NM_UNMANAGED_ALL) & ~NM_UNMANAGED_PLATFORM_INIT)
             nm_device_assume_state_reset(device);
         return;
     }
@@ -4608,11 +4639,9 @@ ensure_master_active_connection(NMManager            *self,
                     NULL))
                 continue;
 
-            if (!nm_device_is_software(candidate)) {
-                master_state = nm_device_get_state(candidate);
-                if (nm_device_is_real(candidate) && master_state != NM_DEVICE_STATE_DISCONNECTED)
-                    continue;
-            }
+            if (nm_device_is_real(candidate)
+                && nm_device_get_state(candidate) != NM_DEVICE_STATE_DISCONNECTED)
+                continue;
 
             master_ac = nm_manager_activate_connection(
                 self,
@@ -4715,8 +4744,10 @@ find_slaves(NMManager            *manager,
             }
 
             nm_assert(n_slaves < n_all_connections);
-            slaves[n_slaves].connection = candidate, slaves[n_slaves].device = slave_device,
-            n_slaves++;
+            slaves[n_slaves++] = (SlaveConnectionInfo){
+                .connection = candidate,
+                .device     = slave_device,
+            };
 
             if (slave_device)
                 g_hash_table_add(devices, slave_device);
@@ -6612,7 +6643,7 @@ do_sleep_wake(NMManager *self, gboolean sleeping_changed)
         }
 
         /* Give the connections a chance to recreate the virtual devices.
-	 * We've torn them down on sleep. */
+         * We've torn them down on sleep. */
         connections_changed(self);
     }
 
@@ -7089,10 +7120,6 @@ nm_manager_write_device_state(NMManager *self, NMDevice *device, int *out_ifinde
     ifindex = nm_device_get_ip_ifindex(device);
     if (ifindex <= 0)
         return FALSE;
-    if (ifindex == 1) {
-        /* ignore loopback */
-        return FALSE;
-    }
 
     if (!nm_platform_link_get(priv->platform, ifindex))
         return FALSE;
@@ -7904,12 +7931,12 @@ nm_manager_set_capability(NMManager *self, NMCapability cap)
 
     priv = NM_MANAGER_GET_PRIVATE(self);
 
-    idx = nm_utils_array_find_binary_search(&g_array_index(priv->capabilities, guint32, 0),
-                                            sizeof(guint32),
-                                            priv->capabilities->len,
-                                            &cap_i,
-                                            nm_cmp_uint32_p_with_data,
-                                            NULL);
+    idx = nm_array_find_bsearch(nm_g_array_first_p(priv->capabilities, guint32),
+                                priv->capabilities->len,
+                                sizeof(guint32),
+                                &cap_i,
+                                nm_cmp_uint32_p_with_data,
+                                NULL);
     if (idx >= 0)
         return;
 
@@ -8132,9 +8159,12 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
     case PROP_VERSION:
         g_value_set_string(value, VERSION);
         break;
+    case PROP_VERSION_INFO:
+        g_value_set_variant(value, _version_info_get());
+        break;
     case PROP_CAPABILITIES:
         g_value_set_variant(value,
-                            nm_g_variant_new_au((const guint32 *) priv->capabilities->data,
+                            nm_g_variant_new_au(nm_g_array_first_p(priv->capabilities, guint32),
                                                 priv->capabilities->len));
         break;
     case PROP_STATE:
@@ -8645,6 +8675,9 @@ static const NMDBusInterfaceInfoExtended interface_info_manager = {
                                                            NM_MANAGER_ACTIVATING_CONNECTION),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Startup", "b", NM_MANAGER_STARTUP),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Version", "s", NM_MANAGER_VERSION),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("VersionInfo",
+                                                           "au",
+                                                           NM_MANAGER_VERSION_INFO),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Capabilities",
                                                            "au",
                                                            NM_MANAGER_CAPABILITIES),
@@ -8703,6 +8736,14 @@ nm_manager_class_init(NMManagerClass *manager_class)
                                                        NULL,
                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
+    obj_properties[PROP_VERSION_INFO] =
+        g_param_spec_variant(NM_MANAGER_VERSION_INFO,
+                             "",
+                             "",
+                             G_VARIANT_TYPE("au"),
+                             NULL,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
     obj_properties[PROP_CAPABILITIES] =
         g_param_spec_variant(NM_MANAGER_CAPABILITIES,
                              "",
diff --git a/src/core/nm-manager.h b/src/core/nm-manager.h
index fcb00227..0cbbcbf0 100644
--- a/src/core/nm-manager.h
+++ b/src/core/nm-manager.h
@@ -12,7 +12,7 @@
 #include "nm-dbus-manager.h"
 
 #define NM_TYPE_MANAGER            (nm_manager_get_type())
-#define NM_MANAGER(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MANAGER, NMManager))
+#define NM_MANAGER(obj)            (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MANAGER, NMManager))
 #define NM_MANAGER_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MANAGER, NMManagerClass))
 #define NM_IS_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MANAGER))
 #define NM_IS_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_MANAGER))
@@ -20,6 +20,7 @@
     (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_MANAGER, NMManagerClass))
 
 #define NM_MANAGER_VERSION                      "version"
+#define NM_MANAGER_VERSION_INFO                 "version-info"
 #define NM_MANAGER_CAPABILITIES                 "capabilities"
 #define NM_MANAGER_STATE                        "state"
 #define NM_MANAGER_STARTUP                      "startup"
diff --git a/src/core/nm-netns.c b/src/core/nm-netns.c
index 859dbb21..12ca8508 100644
--- a/src/core/nm-netns.c
+++ b/src/core/nm-netns.c
@@ -7,6 +7,8 @@
 
 #include "nm-netns.h"
 
+#include <linux/rtnetlink.h>
+
 #include "libnm-glib-aux/nm-dedup-multi.h"
 #include "libnm-glib-aux/nm-c-list.h"
 
@@ -16,6 +18,7 @@
 #include "libnm-platform/nm-platform.h"
 #include "libnm-platform/nmp-netns.h"
 #include "libnm-platform/nmp-global-tracker.h"
+#include "libnm-std-aux/c-list-util.h"
 
 /*****************************************************************************/
 
@@ -28,6 +31,8 @@ typedef struct {
     NMPGlobalTracker *global_tracker;
     GHashTable       *l3cfgs;
     GHashTable       *shared_ips;
+    GHashTable       *ecmp_track_by_obj;
+    GHashTable       *ecmp_track_by_ecmpid;
     CList             l3cfg_signal_pending_lst_head;
     GSource          *signal_pending_idle_source;
 } NMNetnsPrivate;
@@ -67,6 +72,165 @@ NM_DEFINE_SINGLETON_GETTER(NMNetns, nm_netns_get, NM_TYPE_NETNS);
 
 /*****************************************************************************/
 
+#define nm_assert_l3cfg(self, l3cfg)                                                      \
+    G_STMT_START                                                                          \
+    {                                                                                     \
+        NMNetns *_self  = (self);                                                         \
+        NML3Cfg *_l3cfg = (l3cfg);                                                        \
+                                                                                          \
+        nm_assert(NM_IS_NETNS(self));                                                     \
+        nm_assert(NM_IS_L3CFG(_l3cfg));                                                   \
+        if (NM_MORE_ASSERTS > 5)                                                          \
+            nm_assert(_l3cfg == nm_netns_l3cfg_get(_self, nm_l3cfg_get_ifindex(_l3cfg))); \
+    }                                                                                     \
+    G_STMT_END
+
+/*****************************************************************************/
+
+typedef struct {
+    const NMPObject *representative_obj;
+    const NMPObject *merged_obj;
+    CList            ecmpid_lst_head;
+    bool             needs_update : 1;
+    bool             already_visited : 1;
+} EcmpTrackEcmpid;
+
+typedef struct {
+    const NMPObject *obj;
+
+    NML3Cfg         *l3cfg;
+    EcmpTrackEcmpid *parent_track_ecmpid;
+
+    CList ifindex_lst;
+    CList ecmpid_lst;
+
+    /* Calling nm_netns_ip_route_ecmp_register() will ensure that the tracked
+     * entry is non-dirty. This can be used to remove stale entries. */
+    bool dirty : 1;
+
+    /* This flag is set during nm_netns_ip_route_ecmp_register(), when first tracking the
+     * route. It is cleared on the next nm_netns_ip_route_ecmp_commit(). It thus only
+     * exists for a short time, to know during a commit that the route is new and
+     * we need to do something special. */
+    bool is_new : 1;
+
+    /* The entry is ready to be configured. This exists, because the nexthop of
+     * a route must be reachable directly (being onlink). That is, we may need
+     * to add a direct, single-hop route to the gateway, which is done by
+     * the NML3Cfg of that interface. Since the NML3Cfg calls nm_netns_ip_route_ecmp_commit()
+     * and only adds the direct route afterwards, the ECMP route may not be ready
+     * right away, but only upon seeing the entry a second time. */
+    bool is_ready : 1;
+} EcmpTrackObj;
+
+static int
+_ecmp_track_sort_lst_cmp(const CList *a, const CList *b, const void *user_data)
+{
+    EcmpTrackObj             *track_obj_a = c_list_entry(a, EcmpTrackObj, ecmpid_lst);
+    EcmpTrackObj             *track_obj_b = c_list_entry(b, EcmpTrackObj, ecmpid_lst);
+    const NMPlatformIP4Route *route_a     = NMP_OBJECT_CAST_IP4_ROUTE(track_obj_a->obj);
+    const NMPlatformIP4Route *route_b     = NMP_OBJECT_CAST_IP4_ROUTE(track_obj_b->obj);
+
+    nm_assert(route_a->ifindex > 0);
+    nm_assert(route_a->n_nexthops <= 1);
+    nm_assert(route_b->ifindex > 0);
+    nm_assert(route_b->n_nexthops <= 1);
+
+    NM_CMP_FIELD(route_a, route_b, ifindex);
+    NM_CMP_FIELD(route_b, route_a, weight);
+    NM_CMP_DIRECT(htonl(route_a->gateway), htonl(route_b->gateway));
+
+    return nm_assert_unreachable_val(
+        nm_platform_ip4_route_cmp(route_a, route_b, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID));
+}
+
+static gboolean
+_ecmp_track_init_merged_obj(EcmpTrackEcmpid *track_ecmpid, const NMPObject **out_obj_del)
+{
+    EcmpTrackObj                   *track_obj;
+    nm_auto_nmpobj const NMPObject *obj_new = NULL;
+    gsize                           n_nexthops;
+    gsize                           i;
+
+    nm_assert(track_ecmpid);
+    nm_assert(!c_list_is_empty(&track_ecmpid->ecmpid_lst_head));
+    nm_assert(track_ecmpid->representative_obj
+              == c_list_first_entry(&track_ecmpid->ecmpid_lst_head, EcmpTrackObj, ecmpid_lst)->obj);
+    nm_assert(out_obj_del && !*out_obj_del);
+
+    if (!track_ecmpid->needs_update) {
+        /* Already up to date. Nothing to do. */
+        return FALSE;
+    }
+
+    track_ecmpid->needs_update = FALSE;
+
+    n_nexthops = c_list_length(&track_ecmpid->ecmpid_lst_head);
+
+    if (n_nexthops == 1) {
+        /* There is only a single entry. There is nothing to merge, just set
+         * the first entry. */
+        obj_new = nmp_object_ref(track_ecmpid->representative_obj);
+        goto out;
+    }
+
+    /* We want that the nexthop list is deterministic. We thus sort the list and update
+     * the representative_obj. */
+    c_list_sort(&track_ecmpid->ecmpid_lst_head, _ecmp_track_sort_lst_cmp, NULL);
+    nmp_object_ref_set(
+        &track_ecmpid->representative_obj,
+        c_list_first_entry(&track_ecmpid->ecmpid_lst_head, EcmpTrackObj, ecmpid_lst)->obj);
+
+    obj_new = nmp_object_clone(track_ecmpid->representative_obj, FALSE);
+
+    nm_assert(obj_new->ip4_route.n_nexthops <= 1);
+    nm_assert(!obj_new->_ip4_route.extra_nexthops);
+
+    /* Note that there actually cannot be duplicate (ifindex,gateway,weight) tuples, because
+     * NML3Cfg uses NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID to track the routes, and track_ecmpid
+     * groups them further by NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID. The comparison for
+     * ECMP_ID is a strict superset of ID, hence there are no dupliated.
+     *
+     * Also, kernel wouldn't care if there were duplicate nexthops anyway.
+     *
+     * This means, it's gonna be simple. We sorted the single-hop routes by next-hop,
+     * now just create a plain list of the nexthops (no check for duplciates, etc). */
+
+    ((NMPObject *) obj_new)->ip4_route.n_nexthops = n_nexthops;
+    ((NMPObject *) obj_new)->_ip4_route.extra_nexthops =
+        g_new(NMPlatformIP4RtNextHop, n_nexthops - 1u);
+
+    i = 0;
+    c_list_for_each_entry (track_obj, &track_ecmpid->ecmpid_lst_head, ecmpid_lst) {
+        if (i > 0) {
+            const NMPlatformIP4Route *r  = NMP_OBJECT_CAST_IP4_ROUTE(track_obj->obj);
+            NMPlatformIP4RtNextHop   *nh = (gpointer) &obj_new->_ip4_route.extra_nexthops[i - 1];
+
+            *nh = (NMPlatformIP4RtNextHop){
+                .ifindex = r->ifindex,
+                .gateway = r->gateway,
+                .weight  = r->weight,
+            };
+        }
+        i++;
+    }
+
+out:
+    nm_assert(obj_new);
+    if (nmp_object_equal(track_ecmpid->merged_obj, obj_new))
+        /* the objects are equal but the update was needed, for example if the
+         * routes were removed from kernel but not from our tracking
+         * dictionaries and therefore we tried to register them again. */
+        return TRUE;
+
+    if (track_ecmpid->merged_obj)
+        *out_obj_del = g_steal_pointer(&track_ecmpid->merged_obj);
+    track_ecmpid->merged_obj = g_steal_pointer(&obj_new);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
 NMPNetns *
 nm_netns_get_platform_netns(NMNetns *self)
 {
@@ -93,21 +257,67 @@ nm_netns_get_multi_idx(NMNetns *self)
 
 /*****************************************************************************/
 
-typedef struct {
-    int      ifindex;
-    guint32  signal_pending_obj_type_flags;
-    NML3Cfg *l3cfg;
-    CList    signal_pending_lst;
-} L3CfgData;
+static guint
+_ecmp_routes_by_ecmpid_hash(gconstpointer ptr)
+{
+    const NMPObject *const *p_obj = ptr;
+
+    return nm_platform_ip4_route_hash(NMP_OBJECT_CAST_IP4_ROUTE(*p_obj),
+                                      NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID);
+}
+
+static int
+_ecmp_routes_by_ecmpid_equal(gconstpointer ptr_a, gconstpointer ptr_b)
+{
+    const NMPObject *const *p_obj_a = ptr_a;
+    const NMPObject *const *p_obj_b = ptr_b;
+
+    return nm_platform_ip4_route_cmp(NMP_OBJECT_CAST_IP4_ROUTE(*p_obj_a),
+                                     NMP_OBJECT_CAST_IP4_ROUTE(*p_obj_b),
+                                     NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID)
+           == 0;
+}
 
 static void
-_l3cfg_data_free(gpointer ptr)
+_ecmp_routes_by_ecmpid_free(gpointer ptr)
 {
-    L3CfgData *l3cfg_data = ptr;
+    EcmpTrackEcmpid *track_ecmpid = ptr;
+
+    c_list_unlink_stale(&track_ecmpid->ecmpid_lst_head);
+    nmp_object_unref(track_ecmpid->representative_obj);
+    nmp_object_unref(track_ecmpid->merged_obj);
+    nm_g_slice_free(track_ecmpid);
+}
 
-    c_list_unlink_stale(&l3cfg_data->signal_pending_lst);
+static void
+_ecmp_routes_by_obj_free(gpointer ptr)
+{
+    EcmpTrackObj *track_obj = ptr;
 
-    nm_g_slice_free(l3cfg_data);
+    c_list_unlink_stale(&track_obj->ifindex_lst);
+    c_list_unlink_stale(&track_obj->ecmpid_lst);
+    nmp_object_unref(track_obj->obj);
+    nm_g_slice_free(track_obj);
+}
+
+/*****************************************************************************/
+
+static NML3Cfg *
+_l3cfg_hashed_to_l3cfg(gpointer ptr)
+{
+    gpointer l3cfg;
+
+    l3cfg = &(((char *) ptr)[-G_STRUCT_OFFSET(NML3Cfg, priv.ifindex)]);
+    nm_assert(NM_IS_L3CFG(l3cfg));
+    return l3cfg;
+}
+
+static void
+_l3cfg_hashed_free(gpointer ptr)
+{
+    NML3Cfg *l3cfg = _l3cfg_hashed_to_l3cfg(ptr);
+
+    c_list_unlink(&l3cfg->internal_netns.signal_pending_lst);
 }
 
 static void
@@ -128,58 +338,48 @@ _l3cfg_weak_notify(gpointer data, GObject *where_the_object_was)
 NML3Cfg *
 nm_netns_l3cfg_get(NMNetns *self, int ifindex)
 {
-    NMNetnsPrivate *priv;
-    L3CfgData      *l3cfg_data;
-
-    g_return_val_if_fail(NM_IS_NETNS(self), NULL);
-    g_return_val_if_fail(ifindex > 0, NULL);
-
-    priv = NM_NETNS_GET_PRIVATE(self);
+    NMNetnsPrivate *priv = NM_NETNS_GET_PRIVATE(self);
+    gpointer        ptr;
 
-    l3cfg_data = g_hash_table_lookup(priv->l3cfgs, &ifindex);
+    nm_assert(ifindex > 0);
 
-    return l3cfg_data ? l3cfg_data->l3cfg : NULL;
+    ptr = g_hash_table_lookup(priv->l3cfgs, &ifindex);
+    return ptr ? _l3cfg_hashed_to_l3cfg(ptr) : NULL;
 }
 
 NML3Cfg *
 nm_netns_l3cfg_acquire(NMNetns *self, int ifindex)
 {
     NMNetnsPrivate *priv;
-    L3CfgData      *l3cfg_data;
+    NML3Cfg        *l3cfg;
 
     g_return_val_if_fail(NM_IS_NETNS(self), NULL);
     g_return_val_if_fail(ifindex > 0, NULL);
 
     priv = NM_NETNS_GET_PRIVATE(self);
 
-    l3cfg_data = g_hash_table_lookup(priv->l3cfgs, &ifindex);
-
-    if (l3cfg_data) {
+    l3cfg = nm_netns_l3cfg_get(self, ifindex);
+    if (l3cfg) {
         nm_log_trace(LOGD_CORE,
                      "l3cfg[" NM_HASH_OBFUSCATE_PTR_FMT ",ifindex=%d] %s",
-                     NM_HASH_OBFUSCATE_PTR(l3cfg_data->l3cfg),
+                     NM_HASH_OBFUSCATE_PTR(l3cfg),
                      ifindex,
                      "referenced");
-        return g_object_ref(l3cfg_data->l3cfg);
+        return g_object_ref(l3cfg);
     }
 
-    l3cfg_data  = g_slice_new(L3CfgData);
-    *l3cfg_data = (L3CfgData){
-        .ifindex            = ifindex,
-        .l3cfg              = nm_l3cfg_new(self, ifindex),
-        .signal_pending_lst = C_LIST_INIT(l3cfg_data->signal_pending_lst),
-    };
+    l3cfg = nm_l3cfg_new(self, ifindex);
 
-    if (!g_hash_table_add(priv->l3cfgs, l3cfg_data))
+    if (!g_hash_table_add(priv->l3cfgs, &l3cfg->priv.ifindex))
         nm_assert_not_reached();
 
     if (NM_UNLIKELY(g_hash_table_size(priv->l3cfgs) == 1))
         g_object_ref(self);
 
-    g_object_weak_ref(G_OBJECT(l3cfg_data->l3cfg), _l3cfg_weak_notify, self);
+    g_object_weak_ref(G_OBJECT(l3cfg), _l3cfg_weak_notify, self);
 
     /* Transfer ownership! We keep only a weak ref. */
-    return l3cfg_data->l3cfg;
+    return l3cfg;
 }
 
 /*****************************************************************************/
@@ -189,7 +389,7 @@ _platform_signal_on_idle_cb(gpointer user_data)
 {
     gs_unref_object NMNetns *self = g_object_ref(NM_NETNS(user_data));
     NMNetnsPrivate          *priv = NM_NETNS_GET_PRIVATE(self);
-    L3CfgData               *l3cfg_data;
+    NML3Cfg                 *l3cfg;
     CList                    work_list;
 
     nm_clear_g_source_inst(&priv->signal_pending_idle_source);
@@ -205,12 +405,12 @@ _platform_signal_on_idle_cb(gpointer user_data)
     c_list_init(&work_list);
     c_list_splice(&work_list, &priv->l3cfg_signal_pending_lst_head);
 
-    while ((l3cfg_data = c_list_first_entry(&work_list, L3CfgData, signal_pending_lst))) {
-        nm_assert(NM_IS_L3CFG(l3cfg_data->l3cfg));
-        c_list_unlink(&l3cfg_data->signal_pending_lst);
+    while ((l3cfg = c_list_first_entry(&work_list, NML3Cfg, internal_netns.signal_pending_lst))) {
+        nm_assert(NM_IS_L3CFG(l3cfg));
+        c_list_unlink(&l3cfg->internal_netns.signal_pending_lst);
         _nm_l3cfg_notify_platform_change_on_idle(
-            l3cfg_data->l3cfg,
-            nm_steal_int(&l3cfg_data->signal_pending_obj_type_flags));
+            l3cfg,
+            nm_steal_int(&l3cfg->internal_netns.signal_pending_obj_type_flags));
     }
 
     return G_SOURCE_CONTINUE;
@@ -228,24 +428,28 @@ _platform_signal_cb(NMPlatform   *platform,
     NMNetnsPrivate                  *priv        = NM_NETNS_GET_PRIVATE(self);
     const NMPObjectType              obj_type    = obj_type_i;
     const NMPlatformSignalChangeType change_type = change_type_i;
-    L3CfgData                       *l3cfg_data;
+    NML3Cfg                         *l3cfg;
 
-    l3cfg_data = g_hash_table_lookup(priv->l3cfgs, &ifindex);
-    if (!l3cfg_data)
+    if (ifindex <= 0) {
+        /* platform signal callback could be triggered by nodev routes, skip them */
+        return;
+    }
+
+    l3cfg = nm_netns_l3cfg_get(self, ifindex);
+    if (!l3cfg)
         return;
 
-    l3cfg_data->signal_pending_obj_type_flags |= nmp_object_type_to_flags(obj_type);
+    l3cfg->internal_netns.signal_pending_obj_type_flags |= nmp_object_type_to_flags(obj_type);
 
-    if (c_list_is_empty(&l3cfg_data->signal_pending_lst)) {
-        c_list_link_tail(&priv->l3cfg_signal_pending_lst_head, &l3cfg_data->signal_pending_lst);
+    if (c_list_is_empty(&l3cfg->internal_netns.signal_pending_lst)) {
+        c_list_link_tail(&priv->l3cfg_signal_pending_lst_head,
+                         &l3cfg->internal_netns.signal_pending_lst);
         if (!priv->signal_pending_idle_source)
             priv->signal_pending_idle_source =
                 nm_g_idle_add_source(_platform_signal_on_idle_cb, self);
     }
 
-    _nm_l3cfg_notify_platform_change(l3cfg_data->l3cfg,
-                                     change_type,
-                                     NMP_OBJECT_UP_CAST(platform_object));
+    _nm_l3cfg_notify_platform_change(l3cfg, change_type, NMP_OBJECT_UP_CAST(platform_object));
 }
 
 /*****************************************************************************/
@@ -257,7 +461,7 @@ nm_netns_shared_ip_reserve(NMNetns *self)
     NMNetnsSharedIPHandle *handle;
     const in_addr_t        addr_start = ntohl(0x0a2a0001u); /* 10.42.0.1 */
     in_addr_t              addr;
-    char                   sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char                   sbuf_addr[NM_INET_ADDRSTRLEN];
 
     /* Find an unused address in the 10.42.x.x range */
 
@@ -287,10 +491,10 @@ nm_netns_shared_ip_reserve(NMNetns *self)
             if (count > 0xFFu) {
                 if (handle->_ref_count == 1) {
                     _LOGE("shared-ip4: ran out of shared IP addresses. Reuse %s/24",
-                          _nm_utils_inet4_ntop(handle->addr, sbuf_addr));
+                          nm_inet4_ntop(handle->addr, sbuf_addr));
                 } else {
                     _LOGD("shared-ip4: reserved IP address range %s/24 (duplicate)",
-                          _nm_utils_inet4_ntop(handle->addr, sbuf_addr));
+                          nm_inet4_ntop(handle->addr, sbuf_addr));
                 }
                 handle->_ref_count++;
                 return handle;
@@ -307,8 +511,7 @@ nm_netns_shared_ip_reserve(NMNetns *self)
 
     g_hash_table_add(priv->shared_ips, handle);
 
-    _LOGD("shared-ip4: reserved IP address range %s/24",
-          _nm_utils_inet4_ntop(handle->addr, sbuf_addr));
+    _LOGD("shared-ip4: reserved IP address range %s/24", nm_inet4_ntop(handle->addr, sbuf_addr));
     return handle;
 }
 
@@ -317,7 +520,7 @@ nm_netns_shared_ip_release(NMNetnsSharedIPHandle *handle)
 {
     NMNetns        *self;
     NMNetnsPrivate *priv;
-    char            sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char            sbuf_addr[NM_INET_ADDRSTRLEN];
 
     g_return_if_fail(handle);
 
@@ -334,7 +537,7 @@ nm_netns_shared_ip_release(NMNetnsSharedIPHandle *handle)
         nm_assert(handle->addr == ntohl(0x0A2AFF01u)); /* 10.42.255.1 */
         handle->_ref_count--;
         _LOGD("shared-ip4: release IP address range %s/24 (%d more references held)",
-              _nm_utils_inet4_ntop(handle->addr, sbuf_addr),
+              nm_inet4_ntop(handle->addr, sbuf_addr),
               handle->_ref_count);
         return;
     }
@@ -347,8 +550,7 @@ nm_netns_shared_ip_release(NMNetnsSharedIPHandle *handle)
         g_object_unref(self);
     }
 
-    _LOGD("shared-ip4: release IP address range %s/24",
-          _nm_utils_inet4_ntop(handle->addr, sbuf_addr));
+    _LOGD("shared-ip4: release IP address range %s/24", nm_inet4_ntop(handle->addr, sbuf_addr));
 
     handle->_self = NULL;
     nm_g_slice_free(handle);
@@ -356,6 +558,270 @@ nm_netns_shared_ip_release(NMNetnsSharedIPHandle *handle)
 
 /*****************************************************************************/
 
+void
+nm_netns_ip_route_ecmp_register(NMNetns *self, NML3Cfg *l3cfg, const NMPObject *obj)
+{
+    NMNetnsPrivate           *priv;
+    EcmpTrackObj             *track_obj;
+    const NMPlatformIP4Route *route;
+    char                      sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+
+    nm_assert_l3cfg(self, l3cfg);
+
+    route = NMP_OBJECT_CAST_IP4_ROUTE(obj);
+
+    nm_assert(route->ifindex > 0);
+    nm_assert(route->ifindex == nm_l3cfg_get_ifindex(l3cfg));
+    nm_assert(route->n_nexthops <= 1);
+
+    priv = NM_NETNS_GET_PRIVATE(self);
+
+    track_obj = g_hash_table_lookup(priv->ecmp_track_by_obj, &obj);
+
+    if (NM_MORE_ASSERTS > 10) {
+        EcmpTrackObj *track_obj2;
+        gboolean      found = FALSE;
+
+        c_list_for_each_entry (track_obj2,
+                               &l3cfg->internal_netns.ecmp_track_ifindex_lst_head,
+                               ifindex_lst) {
+            if (track_obj2->obj == obj) {
+                found = TRUE;
+                break;
+            }
+        }
+
+        nm_assert((!!track_obj) == found);
+    }
+
+    if (!track_obj) {
+        EcmpTrackEcmpid *track_ecmpid;
+
+        track_ecmpid = g_hash_table_lookup(priv->ecmp_track_by_ecmpid, &obj);
+        if (!track_ecmpid) {
+            track_ecmpid  = g_slice_new(EcmpTrackEcmpid);
+            *track_ecmpid = (EcmpTrackEcmpid){
+                .representative_obj = nmp_object_ref(obj),
+                .merged_obj         = NULL,
+                .ecmpid_lst_head    = C_LIST_INIT(track_ecmpid->ecmpid_lst_head),
+                .needs_update       = TRUE,
+            };
+            g_hash_table_add(priv->ecmp_track_by_ecmpid, track_ecmpid);
+        } else
+            track_ecmpid->needs_update = TRUE;
+
+        track_obj  = g_slice_new(EcmpTrackObj);
+        *track_obj = (EcmpTrackObj){
+            .obj                 = nmp_object_ref(obj),
+            .l3cfg               = l3cfg,
+            .parent_track_ecmpid = track_ecmpid,
+            .dirty               = FALSE,
+            .is_new              = TRUE,
+            .is_ready            = FALSE,
+        };
+
+        g_hash_table_add(priv->ecmp_track_by_obj, track_obj);
+        c_list_link_tail(&l3cfg->internal_netns.ecmp_track_ifindex_lst_head,
+                         &track_obj->ifindex_lst);
+        c_list_link_tail(&track_ecmpid->ecmpid_lst_head, &track_obj->ecmpid_lst);
+
+        _LOGT(
+            "ecmp-route: track %s",
+            nmp_object_to_string(track_obj->obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
+    } else {
+        track_obj->dirty                             = FALSE;
+        track_obj->parent_track_ecmpid->needs_update = TRUE;
+    }
+}
+
+void
+nm_netns_ip_route_ecmp_commit(NMNetns    *self,
+                              NML3Cfg    *l3cfg,
+                              GPtrArray **out_singlehop_routes,
+                              gboolean    is_reapply)
+{
+    NMNetnsPrivate  *priv = NM_NETNS_GET_PRIVATE(self);
+    EcmpTrackObj    *track_obj;
+    EcmpTrackObj    *track_obj_safe;
+    EcmpTrackEcmpid *track_ecmpid;
+    const NMPObject *route_obj;
+    char             sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+    gboolean         already_notified = FALSE;
+
+    nm_assert_l3cfg(self, l3cfg);
+
+    _LOGT("ecmp-route: committing IPv4 ECMP routes");
+
+    /* First, delete all dirty entries, and mark the survivors as dirty, so that on the
+     * next update they must be touched again. */
+    c_list_for_each_entry_safe (track_obj,
+                                track_obj_safe,
+                                &l3cfg->internal_netns.ecmp_track_ifindex_lst_head,
+                                ifindex_lst) {
+        track_ecmpid                  = track_obj->parent_track_ecmpid;
+        track_ecmpid->already_visited = FALSE;
+
+        nm_assert(g_hash_table_lookup(priv->ecmp_track_by_ecmpid, track_ecmpid) == track_ecmpid);
+        nm_assert(g_hash_table_lookup(priv->ecmp_track_by_obj, track_obj) == track_obj);
+        nm_assert(c_list_contains(&track_ecmpid->ecmpid_lst_head, &track_obj->ecmpid_lst));
+        nm_assert(track_obj->l3cfg == l3cfg);
+
+        if (!track_obj->dirty) {
+            /* This one is still in used. Keep it, but mark dirty, so that on the
+             * next update cycle, it needs to be touched again or will be deleted. */
+            track_obj->dirty = TRUE;
+            if (is_reapply) {
+                track_obj->is_new   = TRUE;
+                track_obj->is_ready = FALSE;
+            }
+            if (track_obj->is_new) {
+                const NMPlatformIP4Route *route =
+                    NMP_OBJECT_CAST_IP4_ROUTE(track_ecmpid->merged_obj);
+
+                /* This is a new route entry that was just added. Upon first
+                 * addition, the route is not yet ready for configuration,
+                 * because we need to make sure that the gateway is reachable
+                 * via an onlink route. The calling l3cfg will configure that
+                 * route, but only after returning from this function.  So we
+                 * need to go through one more commit.
+                 *
+                 * We also need to make sure that we are called back right
+                 * after l3cfg configured that route. We achieve that by
+                 * scheduling another idle commit on "l3cfg". */
+                track_obj->is_new = FALSE;
+                if (route
+                    && (route->gateway == 0
+                        || NM_FLAGS_HAS(route->r_rtm_flags, (unsigned) RTNH_F_ONLINK))) {
+                    /* This route is onlink. We don't need to configure an onlink route
+                     * to the gateway, and the route is immediately ready for configuration. */
+                    track_obj->is_ready = TRUE;
+                } else if (c_list_length_is(&track_ecmpid->ecmpid_lst_head, 1)) {
+                    /* This route has no merge partner and ends up being a
+                     * single hop route. It will be returned and configured by
+                     * the calling "l3cfg".
+                     *
+                     * Unlike for multi-hop routes, we don't need to be called
+                     * again after the onlink route was added. We are done, and
+                     * don't need to schedule an idle commit. */
+                    track_obj->is_ready = TRUE;
+                } else {
+                    /* This is a new route which has a gateway. We need for the "l3cfg"
+                     * to first configure the onlink route. It's not yet ready for configuration.
+                     *
+                     * Instead, schedule an idle commit to make sure we get called back
+                     * again, and then (upon seeing the entry the second time) the onlink
+                     * route is already configured and we will be ready. */
+                    if (!already_notified) {
+                        /* Some micro optimization with already_notified to avoid calling
+                         * schedule unnecessarily. */
+                        already_notified = TRUE;
+                        nm_l3cfg_commit_on_idle_schedule(l3cfg, NM_L3_CFG_COMMIT_TYPE_AUTO);
+                    }
+                }
+            } else {
+                /* We see this entry the second time (or more) so it's ready. */
+                track_obj->is_ready = TRUE;
+            }
+            continue;
+        }
+
+        /* This entry can be dropped. */
+        if (!g_hash_table_remove(priv->ecmp_track_by_obj, track_obj))
+            nm_assert_not_reached();
+
+        if (c_list_is_empty(&track_ecmpid->ecmpid_lst_head)) {
+            if (track_ecmpid->merged_obj) {
+                if (NMP_OBJECT_CAST_IP4_ROUTE(track_ecmpid->merged_obj)->n_nexthops > 1)
+                    nm_platform_object_delete(priv->platform, track_ecmpid->merged_obj);
+            }
+            g_hash_table_remove(priv->ecmp_track_by_ecmpid, track_ecmpid);
+
+            continue;
+        }
+
+        /* We need to update the representative obj. */
+        nmp_object_ref_set(
+            &track_ecmpid->representative_obj,
+            c_list_first_entry(&track_ecmpid->ecmpid_lst_head, EcmpTrackObj, ecmpid_lst)->obj);
+        track_ecmpid->needs_update = TRUE;
+    }
+
+    /* Now, we need to iterate again over all objects, and regenerate the merged_obj. */
+    c_list_for_each_entry (track_obj,
+                           &l3cfg->internal_netns.ecmp_track_ifindex_lst_head,
+                           ifindex_lst) {
+        const NMPlatformIP4Route       *route;
+        EcmpTrackObj                   *track_obj2;
+        nm_auto_nmpobj const NMPObject *obj_del = NULL;
+        gboolean                        changed;
+        gboolean                        all_is_ready;
+
+        track_ecmpid = track_obj->parent_track_ecmpid;
+        if (track_ecmpid->already_visited) {
+            /* We already visited this ecmpid in the same loop. We can skip, otherwise
+             * we might add the same route twice. */
+            continue;
+        }
+        track_ecmpid->already_visited = TRUE;
+
+        all_is_ready = TRUE;
+        c_list_for_each_entry (track_obj2, &track_ecmpid->ecmpid_lst_head, ecmpid_lst) {
+            if (!track_obj2->is_ready) {
+                all_is_ready = FALSE;
+                break;
+            }
+        }
+        if (!all_is_ready) {
+            /* Here we might have a merged_obj already which can have the wrong
+             * setting e.g the wrong nexthops. We leave them for the moment and
+             * then we reconfigure it when this entry is ready. */
+            continue;
+        }
+
+        changed = _ecmp_track_init_merged_obj(track_obj->parent_track_ecmpid, &obj_del);
+
+        nm_assert(!obj_del || changed);
+
+        route_obj = track_ecmpid->merged_obj;
+        route     = NMP_OBJECT_CAST_IP4_ROUTE(route_obj);
+
+        if (obj_del) {
+            if (NMP_OBJECT_CAST_IP4_ROUTE(obj_del)->n_nexthops > 1)
+                nm_platform_object_delete(priv->platform, obj_del);
+            else if (track_obj->l3cfg != l3cfg)
+                nm_l3cfg_commit_on_idle_schedule(track_obj->l3cfg, NM_L3_CFG_COMMIT_TYPE_AUTO);
+        }
+
+        if (route->n_nexthops <= 1) {
+            /* This is a single hop route. Return it to the caller. */
+            if (!*out_singlehop_routes) {
+                /* Note that the returned array does not own a reference. This
+                 * function has only one caller, and for that caller, it's just
+                 * fine that the result is not additionally kept alive. */
+                *out_singlehop_routes =
+                    g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
+            }
+            g_ptr_array_add(*out_singlehop_routes, (gpointer) nmp_object_ref(route_obj));
+            if (changed) {
+                _LOGT("ecmp-route: single-hop %s",
+                      nmp_object_to_string(route_obj,
+                                           NMP_OBJECT_TO_STRING_PUBLIC,
+                                           sbuf,
+                                           sizeof(sbuf)));
+            }
+            continue;
+        }
+
+        if (changed || is_reapply) {
+            _LOGT("ecmp-route: multi-hop %s",
+                  nmp_object_to_string(route_obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
+            nm_platform_ip_route_add(priv->platform, NMP_NLM_FLAG_APPEND, route_obj);
+        }
+    }
+}
+
+/*****************************************************************************/
+
 static void
 set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
 {
@@ -385,6 +851,14 @@ nm_netns_init(NMNetns *self)
 
     priv->_self_signal_user_data = self;
     c_list_init(&priv->l3cfg_signal_pending_lst_head);
+
+    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(EcmpTrackObj, obj) == 0);
+    priv->ecmp_track_by_obj =
+        g_hash_table_new_full(nm_pdirect_hash, nm_pdirect_equal, _ecmp_routes_by_obj_free, NULL);
+    priv->ecmp_track_by_ecmpid = g_hash_table_new_full(_ecmp_routes_by_ecmpid_hash,
+                                                       _ecmp_routes_by_ecmpid_equal,
+                                                       _ecmp_routes_by_ecmpid_free,
+                                                       NULL);
 }
 
 static void
@@ -396,7 +870,7 @@ constructed(GObject *object)
     if (!priv->platform)
         g_return_if_reached();
 
-    priv->l3cfgs = g_hash_table_new_full(nm_pint_hash, nm_pint_equal, _l3cfg_data_free, NULL);
+    priv->l3cfgs = g_hash_table_new_full(nm_pint_hash, nm_pint_equal, _l3cfg_hashed_free, NULL);
 
     priv->platform_netns = nm_platform_netns_get(priv->platform);
 
@@ -464,6 +938,9 @@ dispose(GObject *object)
     nm_assert(c_list_is_empty(&priv->l3cfg_signal_pending_lst_head));
     nm_assert(!priv->shared_ips);
 
+    nm_clear_pointer(&priv->ecmp_track_by_obj, g_hash_table_destroy);
+    nm_clear_pointer(&priv->ecmp_track_by_ecmpid, g_hash_table_destroy);
+
     nm_clear_g_source_inst(&priv->signal_pending_idle_source);
 
     if (priv->platform)
diff --git a/src/core/nm-netns.h b/src/core/nm-netns.h
index 17c1c637..84a78f83 100644
--- a/src/core/nm-netns.h
+++ b/src/core/nm-netns.h
@@ -9,7 +9,7 @@
 #include "libnm-platform/nmp-base.h"
 
 #define NM_TYPE_NETNS            (nm_netns_get_type())
-#define NM_NETNS(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_NETNS, NMNetns))
+#define NM_NETNS(obj)            (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_NETNS, NMNetns))
 #define NM_NETNS_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_NETNS, NMNetnsClass))
 #define NM_IS_NETNS(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_NETNS))
 #define NM_IS_NETNS_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_NETNS))
@@ -51,4 +51,12 @@ NMNetnsSharedIPHandle *nm_netns_shared_ip_reserve(NMNetns *self);
 
 void nm_netns_shared_ip_release(NMNetnsSharedIPHandle *handle);
 
+/*****************************************************************************/
+
+void nm_netns_ip_route_ecmp_register(NMNetns *self, NML3Cfg *l3cfg, const NMPObject *obj);
+void nm_netns_ip_route_ecmp_commit(NMNetns    *self,
+                                   NML3Cfg    *l3cfg,
+                                   GPtrArray **routes,
+                                   gboolean    is_reapply);
+
 #endif /* __NM_NETNS_H__ */
diff --git a/src/core/nm-pacrunner-manager.c b/src/core/nm-pacrunner-manager.c
index 86a40632..80db755f 100644
--- a/src/core/nm-pacrunner-manager.c
+++ b/src/core/nm-pacrunner-manager.c
@@ -129,7 +129,7 @@ get_ip_domains(GPtrArray *domains, const NML3ConfigData *l3cd, int addr_family)
     char                      *cidr;
     guint                      num;
     guint                      i;
-    char                       sbuf[NM_UTILS_INET_ADDRSTRLEN];
+    char                       sbuf[NM_INET_ADDRSTRLEN];
     const NMPlatformIPAddress *address;
     const NMPlatformIPRoute   *route;
     const char *const         *strv;
@@ -144,7 +144,7 @@ get_ip_domains(GPtrArray *domains, const NML3ConfigData *l3cd, int addr_family)
 
     nm_l3_config_data_iter_ip_address_for_each (&ipconf_iter, l3cd, addr_family, &address) {
         cidr = g_strdup_printf("%s/%u",
-                               nm_utils_inet_ntop(addr_family, address->address_ptr, sbuf),
+                               nm_inet_ntop(addr_family, address->address_ptr, sbuf),
                                address->plen);
         g_ptr_array_add(domains, cidr);
     }
@@ -153,7 +153,7 @@ get_ip_domains(GPtrArray *domains, const NML3ConfigData *l3cd, int addr_family)
         if (NM_PLATFORM_IP_ROUTE_IS_DEFAULT(route))
             continue;
         cidr = g_strdup_printf("%s/%u",
-                               nm_utils_inet_ntop(addr_family, route->network_ptr, sbuf),
+                               nm_inet_ntop(addr_family, route->network_ptr, sbuf),
                                route->plen);
         g_ptr_array_add(domains, cidr);
     }
diff --git a/src/core/nm-pacrunner-manager.h b/src/core/nm-pacrunner-manager.h
index 3f16908f..b434349a 100644
--- a/src/core/nm-pacrunner-manager.h
+++ b/src/core/nm-pacrunner-manager.h
@@ -9,7 +9,7 @@
 
 #define NM_TYPE_PACRUNNER_MANAGER (nm_pacrunner_manager_get_type())
 #define NM_PACRUNNER_MANAGER(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_PACRUNNER_MANAGER, NMPacrunnerManager))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_PACRUNNER_MANAGER, NMPacrunnerManager))
 #define NM_PACRUNNER_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_PACRUNNER_MANAGER, NMPacrunnerManagerClass))
 #define NM_IS_PACRUNNER_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_PACRUNNER_MANAGER))
diff --git a/src/core/nm-policy.c b/src/core/nm-policy.c
index 0c07c29e..4192808a 100644
--- a/src/core/nm-policy.c
+++ b/src/core/nm-policy.c
@@ -108,9 +108,10 @@ _PRIV_TO_SELF(NMPolicyPrivate *priv)
 
     nm_assert(priv);
 
-    self = (NMPolicy *) (((char *) priv) - G_STRUCT_OFFSET(NMPolicy, _priv));
+    self = NM_CAST_ALIGN(NMPolicy, (((char *) priv) - G_STRUCT_OFFSET(NMPolicy, _priv)));
 
     nm_assert(NM_IS_POLICY(self));
+
     return self;
 }
 
@@ -167,11 +168,11 @@ static void
 clear_ip6_prefix_delegation(gpointer data)
 {
     IP6PrefixDelegation *delegation = data;
-    char                 sbuf[NM_UTILS_INET_ADDRSTRLEN];
+    char                 sbuf[NM_INET_ADDRSTRLEN];
 
     _LOGD(LOGD_IP6,
           "ipv6-pd: undelegating prefix %s/%d",
-          _nm_utils_inet6_ntop(&delegation->prefix.address, sbuf),
+          nm_inet6_ntop(&delegation->prefix.address, sbuf),
           delegation->prefix.plen);
 
     g_hash_table_foreach(delegation->subnets, _clear_ip6_subnet, NULL);
@@ -187,7 +188,7 @@ expire_ip6_delegations(NMPolicy *self)
     guint                i;
 
     for (i = 0; i < priv->ip6_prefix_delegations->len; i++) {
-        delegation = &g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i);
+        delegation = &nm_g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i);
         if (delegation->prefix.timestamp + delegation->prefix.lifetime < now)
             g_array_remove_index_fast(priv->ip6_prefix_delegations, i);
     }
@@ -204,7 +205,7 @@ ip6_subnet_from_delegation(IP6PrefixDelegation *delegation, NMDevice *device)
 {
     NMPlatformIP6Address *subnet;
     int                   ifindex = nm_device_get_ifindex(device);
-    char                  sbuf[NM_UTILS_INET_ADDRSTRLEN];
+    char                  sbuf[NM_INET_ADDRSTRLEN];
 
     subnet = g_hash_table_lookup(delegation->subnets, GINT_TO_POINTER(ifindex));
     if (!subnet) {
@@ -212,7 +213,7 @@ ip6_subnet_from_delegation(IP6PrefixDelegation *delegation, NMDevice *device)
         if (delegation->next_subnet >= (1 << (64 - delegation->prefix.plen))) {
             _LOGD(LOGD_IP6,
                   "ipv6-pd: no more prefixes in %s/%d",
-                  _nm_utils_inet6_ntop(&delegation->prefix.address, sbuf),
+                  nm_inet6_ntop(&delegation->prefix.address, sbuf),
                   delegation->prefix.plen);
             return FALSE;
         }
@@ -241,7 +242,7 @@ ip6_subnet_from_delegation(IP6PrefixDelegation *delegation, NMDevice *device)
 
     _LOGD(LOGD_IP6,
           "ipv6-pd: %s allocated from a /%d prefix on %s",
-          _nm_utils_inet6_ntop(&subnet->address, sbuf),
+          nm_inet6_ntop(&subnet->address, sbuf),
           delegation->prefix.plen,
           nm_device_get_iface(device));
 
@@ -267,7 +268,7 @@ ip6_subnet_from_device(NMPolicy *self, NMDevice *from_device, NMDevice *device)
     expire_ip6_delegations(self);
 
     for (i = 0; i < priv->ip6_prefix_delegations->len; i++) {
-        delegation = &g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i);
+        delegation = &nm_g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i);
 
         if (delegation->device != from_device)
             continue;
@@ -295,7 +296,7 @@ ip6_remove_device_prefix_delegations(NMPolicy *self, NMDevice *device)
     guint                i;
 
     for (i = 0; i < priv->ip6_prefix_delegations->len; i++) {
-        delegation = &g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i);
+        delegation = &nm_g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i);
         if (delegation->device == device)
             g_array_remove_index_fast(priv->ip6_prefix_delegations, i);
     }
@@ -312,11 +313,11 @@ device_ip6_prefix_delegated(NMDevice                   *device,
     guint                i;
     const CList         *tmp_list;
     NMActiveConnection  *ac;
-    char                 sbuf[NM_UTILS_INET_ADDRSTRLEN];
+    char                 sbuf[NM_INET_ADDRSTRLEN];
 
     _LOGI(LOGD_IP6,
           "ipv6-pd: received a prefix %s/%d from %s",
-          _nm_utils_inet6_ntop(&prefix->address, sbuf),
+          nm_inet6_ntop(&prefix->address, sbuf),
           prefix->plen,
           nm_device_get_iface(device));
 
@@ -324,16 +325,15 @@ device_ip6_prefix_delegated(NMDevice                   *device,
 
     for (i = 0; i < priv->ip6_prefix_delegations->len; i++) {
         /* Look for an already known prefix to update. */
-        delegation = &g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i);
+        delegation = &nm_g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i);
         if (IN6_ARE_ADDR_EQUAL(&delegation->prefix.address, &prefix->address))
             break;
     }
 
     if (i == priv->ip6_prefix_delegations->len) {
         /* Allocate a delegation for new prefix. */
-        g_array_set_size(priv->ip6_prefix_delegations, i + 1);
-        delegation          = &g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i);
-        delegation->subnets = g_hash_table_new(nm_direct_hash, NULL);
+        delegation = nm_g_array_append_new(priv->ip6_prefix_delegations, IP6PrefixDelegation);
+        delegation->subnets     = g_hash_table_new(nm_direct_hash, NULL);
         delegation->next_subnet = 0;
     }
 
@@ -458,7 +458,7 @@ get_best_active_connection(NMPolicy *self, int addr_family, gboolean fully_activ
 }
 
 static gboolean
-all_devices_not_active(NMPolicy *self)
+any_devices_active(NMPolicy *self)
 {
     NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
     const CList     *tmp_lst;
@@ -468,12 +468,13 @@ all_devices_not_active(NMPolicy *self)
         NMDeviceState state;
 
         state = nm_device_get_state(device);
-        if (state <= NM_DEVICE_STATE_DISCONNECTED || state >= NM_DEVICE_STATE_DEACTIVATING) {
+        if (state <= NM_DEVICE_STATE_DISCONNECTED || state >= NM_DEVICE_STATE_DEACTIVATING)
             continue;
-        }
-        return FALSE;
+        if (nm_device_sys_iface_state_is_external(device))
+            continue;
+        return TRUE;
     }
-    return TRUE;
+    return FALSE;
 }
 
 #define FALLBACK_HOSTNAME4 "localhost.localdomain"
@@ -607,7 +608,7 @@ _set_hostname(NMPolicy *self, const char *new_hostname, const char *msg)
         priv->updating_dns = TRUE;
         nm_dns_manager_set_hostname(priv->dns_manager,
                                     priv->cur_hostname_full,
-                                    all_devices_not_active(self));
+                                    !any_devices_active(self));
         priv->updating_dns = FALSE;
     }
 
@@ -738,6 +739,9 @@ build_device_hostname_infos(NMPolicy *self)
         if (!device)
             continue;
 
+        if (nm_device_sys_iface_state_is_external(device))
+            continue;
+
         only_from_default =
             device_get_hostname_property_boolean(device, NM_SETTING_HOSTNAME_ONLY_FROM_DEFAULT);
 
@@ -770,10 +774,10 @@ build_device_hostname_infos(NMPolicy *self)
 
         g_array_sort(array, device_hostname_info_compare);
 
-        info0 = &g_array_index(array, DeviceHostnameInfo, 0);
+        info0 = &nm_g_array_first(array, DeviceHostnameInfo);
         if (info0->priority < 0) {
             for (i = 1; i < array->len; i++) {
-                const DeviceHostnameInfo *info = &g_array_index(array, DeviceHostnameInfo, i);
+                const DeviceHostnameInfo *info = &nm_g_array_index(array, DeviceHostnameInfo, i);
 
                 if (info->priority > info0->priority) {
                     g_array_set_size(array, i);
@@ -878,7 +882,7 @@ update_system_hostname(NMPolicy *self, const char *msg)
     if (infos && _LOGT_ENABLED(LOGD_DNS)) {
         _LOGT(LOGD_DNS, "device hostname info:");
         for (i = 0; i < infos->len; i++) {
-            info = &g_array_index(infos, DeviceHostnameInfo, i);
+            info = &nm_g_array_index(infos, DeviceHostnameInfo, i);
             _LOGT(LOGD_DNS,
                   "  - prio:%5d ipv%c%s %s %s dev:%s",
                   info->priority,
@@ -891,7 +895,7 @@ update_system_hostname(NMPolicy *self, const char *msg)
     }
 
     for (i = 0; infos && i < infos->len; i++) {
-        info        = &g_array_index(infos, DeviceHostnameInfo, i);
+        info        = &nm_g_array_index(infos, DeviceHostnameInfo, i);
         addr_family = info->IS_IPv4 ? AF_INET : AF_INET6;
         g_signal_handlers_disconnect_by_func(info->device, device_dns_lookup_done, self);
         g_signal_handlers_disconnect_by_func(info->device, device_carrier_changed, priv);
@@ -1361,8 +1365,16 @@ auto_activate_device(NMPolicy *self, NMDevice *device)
     // but another connection now overrides the current one for that device,
     // deactivate the device and activate the new connection instead of just
     // bailing if the device is already active
-    if (nm_device_get_act_request(device))
-        return;
+    if (nm_device_get_act_request(device)) {
+        if (nm_device_sys_iface_state_is_external(device)
+            && nm_device_get_allow_autoconnect_on_external(device)) {
+            /* this is an external activation, and we allow autoconnecting on
+             * top of that.
+             *
+             * pass. */
+        } else
+            return;
+    }
 
     if (!nm_device_autoconnect_allowed(device))
         return;
@@ -1689,8 +1701,13 @@ schedule_activate_check(NMPolicy *self, NMDevice *device)
         return;
 
     nm_manager_for_each_active_connection (priv->manager, ac, tmp_list) {
-        if (nm_active_connection_get_device(ac) == device)
-            return;
+        if (nm_active_connection_get_device(ac) == device) {
+            if (nm_device_sys_iface_state_is_external(device)
+                && nm_device_get_allow_autoconnect_on_external(device)) {
+                /* pass */
+            } else
+                return;
+        }
     }
 
     nm_device_add_pending_action(device, NM_PENDING_ACTION_AUTOACTIVATE, TRUE);
diff --git a/src/core/nm-policy.h b/src/core/nm-policy.h
index fa216db3..f2b98dbd 100644
--- a/src/core/nm-policy.h
+++ b/src/core/nm-policy.h
@@ -8,7 +8,7 @@
 #define __NETWORKMANAGER_POLICY_H__
 
 #define NM_TYPE_POLICY            (nm_policy_get_type())
-#define NM_POLICY(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_POLICY, NMPolicy))
+#define NM_POLICY(obj)            (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_POLICY, NMPolicy))
 #define NM_POLICY_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_POLICY, NMPolicyClass))
 #define NM_IS_POLICY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_POLICY))
 #define NM_IS_POLICY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_POLICY))
diff --git a/src/core/nm-rfkill-manager.h b/src/core/nm-rfkill-manager.h
index 88d0421a..818ab4b2 100644
--- a/src/core/nm-rfkill-manager.h
+++ b/src/core/nm-rfkill-manager.h
@@ -37,7 +37,7 @@ const char *nm_rfkill_type_to_string(NMRfkillType rtype);
 
 #define NM_TYPE_RFKILL_MANAGER (nm_rfkill_manager_get_type())
 #define NM_RFKILL_MANAGER(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_RFKILL_MANAGER, NMRfkillManager))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_RFKILL_MANAGER, NMRfkillManager))
 #define NM_RFKILL_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_RFKILL_MANAGER, NMRfkillManagerClass))
 #define NM_IS_RFKILL_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_RFKILL_MANAGER))
diff --git a/src/core/nm-session-monitor.h b/src/core/nm-session-monitor.h
index 63a3136b..5b7c3f91 100644
--- a/src/core/nm-session-monitor.h
+++ b/src/core/nm-session-monitor.h
@@ -10,7 +10,7 @@
 
 #define NM_TYPE_SESSION_MONITOR (nm_session_monitor_get_type())
 #define NM_SESSION_MONITOR(o) \
-    (G_TYPE_CHECK_INSTANCE_CAST((o), NM_TYPE_SESSION_MONITOR, NMSessionMonitor))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((o), NM_TYPE_SESSION_MONITOR, NMSessionMonitor))
 #define NM_SESSION_MONITOR_CLASS(k) \
     (G_TYPE_CHECK_CLASS_CAST((k), NM_TYPE_SESSION_MONITOR, NMSessionMonitorClass))
 #define NM_SESSION_MONITOR_GET_CLASS(o) \
diff --git a/src/core/nm-sleep-monitor.c b/src/core/nm-sleep-monitor.c
index a7ea496d..66ea2f6c 100644
--- a/src/core/nm-sleep-monitor.c
+++ b/src/core/nm-sleep-monitor.c
@@ -11,6 +11,7 @@
 #include <sys/stat.h>
 #include <gio/gunixfdlist.h>
 
+#include "libnm-glib-aux/nm-dbus-aux.h"
 #include "libnm-core-intern/nm-core-internal.h"
 #include "NetworkManagerUtils.h"
 
@@ -294,24 +295,24 @@ on_proxy_acquired(GObject *object, GAsyncResult *res, NMSleepMonitor *self)
     g_clear_object(&self->cancellable);
 
 #if USE_UPOWER
-    self->sig_id_1 = _nm_dbus_signal_connect(self->proxy,
-                                             "Sleeping",
-                                             NULL,
-                                             G_CALLBACK(upower_sleeping_cb),
-                                             self);
-    self->sig_id_2 = _nm_dbus_signal_connect(self->proxy,
-                                             "Resuming",
-                                             NULL,
-                                             G_CALLBACK(upower_resuming_cb),
-                                             self);
+    self->sig_id_1 = _nm_dbus_proxy_signal_connect(self->proxy,
+                                                   "Sleeping",
+                                                   NULL,
+                                                   G_CALLBACK(upower_sleeping_cb),
+                                                   self);
+    self->sig_id_2 = _nm_dbus_proxy_signal_connect(self->proxy,
+                                                   "Resuming",
+                                                   NULL,
+                                                   G_CALLBACK(upower_resuming_cb),
+                                                   self);
 #else
     self->sig_id_1 =
         g_signal_connect(self->proxy, "notify::g-name-owner", G_CALLBACK(name_owner_cb), self);
-    self->sig_id_2 = _nm_dbus_signal_connect(self->proxy,
-                                             "PrepareForSleep",
-                                             G_VARIANT_TYPE("(b)"),
-                                             G_CALLBACK(prepare_for_sleep_cb),
-                                             self);
+    self->sig_id_2 = _nm_dbus_proxy_signal_connect(self->proxy,
+                                                   "PrepareForSleep",
+                                                   G_VARIANT_TYPE("(b)"),
+                                                   G_CALLBACK(prepare_for_sleep_cb),
+                                                   self);
     {
         gs_free char *owner = NULL;
 
diff --git a/src/core/nm-sleep-monitor.h b/src/core/nm-sleep-monitor.h
index 7d45065b..0b7708db 100644
--- a/src/core/nm-sleep-monitor.h
+++ b/src/core/nm-sleep-monitor.h
@@ -8,7 +8,8 @@
 #define __NETWORKMANAGER_SLEEP_MONITOR_H__
 
 #define NM_TYPE_SLEEP_MONITOR (nm_sleep_monitor_get_type())
-#define NM_SLEEP_MONITOR(o)   (G_TYPE_CHECK_INSTANCE_CAST((o), NM_TYPE_SLEEP_MONITOR, NMSleepMonitor))
+#define NM_SLEEP_MONITOR(o) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((o), NM_TYPE_SLEEP_MONITOR, NMSleepMonitor))
 #define NM_SLEEP_MONITOR_CLASS(k) \
     (G_TYPE_CHECK_CLASS_CAST((k), NM_TYPE_SLEEP_MONITOR, NMSleepMonitorClass))
 #define NM_SLEEP_MONITOR_GET_CLASS(o) \
diff --git a/src/core/nm-test-utils-core.h b/src/core/nm-test-utils-core.h
index 467f9bb2..e341d62f 100644
--- a/src/core/nm-test-utils-core.h
+++ b/src/core/nm-test-utils-core.h
@@ -72,8 +72,8 @@ nmtst_platform_ip6_address(const char *address, const char *peer_address, guint
     g_assert(plen <= 128);
 
     memset(&addr, 0, sizeof(addr));
-    addr.address      = *nmtst_inet6_from_string(address);
-    addr.peer_address = *nmtst_inet6_from_string(peer_address);
+    addr.address      = nmtst_inet6_from_string(address);
+    addr.peer_address = nmtst_inet6_from_string(peer_address);
     addr.plen         = plen;
 
     return &addr;
@@ -148,10 +148,10 @@ nmtst_platform_ip6_route(const char *network, guint plen, const char *gateway, c
     nm_assert(plen <= 128);
 
     memset(&route, 0, sizeof(route));
-    route.network  = *nmtst_inet6_from_string(network);
+    route.network  = nmtst_inet6_from_string(network);
     route.plen     = plen;
-    route.gateway  = *nmtst_inet6_from_string(gateway);
-    route.pref_src = *nmtst_inet6_from_string(pref_src);
+    route.gateway  = nmtst_inet6_from_string(gateway);
+    route.pref_src = nmtst_inet6_from_string(pref_src);
 
     return &route;
 }
@@ -178,8 +178,9 @@ nmtst_platform_ip6_route_full(const char      *network,
 static inline int
 _nmtst_platform_ip4_routes_equal_sort(gconstpointer a, gconstpointer b, gpointer user_data)
 {
-    return nm_platform_ip4_route_cmp_full((const NMPlatformIP4Route *) a,
-                                          (const NMPlatformIP4Route *) b);
+    return nm_platform_ip4_route_cmp((const NMPlatformIP4Route *) a,
+                                     (const NMPlatformIP4Route *) b,
+                                     NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL);
 }
 
 static inline void
@@ -210,7 +211,7 @@ nmtst_platform_ip4_routes_equal(const NMPlatformIP4Route *a,
     }
 
     for (i = 0; i < len; i++) {
-        if (nm_platform_ip4_route_cmp_full(&a[i], &b[i]) != 0) {
+        if (nm_platform_ip4_route_cmp(&a[i], &b[i], NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL) != 0) {
             char buf1[NM_UTILS_TO_STRING_BUFFER_SIZE];
             char buf2[NM_UTILS_TO_STRING_BUFFER_SIZE];
 
@@ -248,8 +249,9 @@ nmtst_platform_ip4_routes_equal_aptr(const NMPObject *const   *a,
 static inline int
 _nmtst_platform_ip6_routes_equal_sort(gconstpointer a, gconstpointer b, gpointer user_data)
 {
-    return nm_platform_ip6_route_cmp_full((const NMPlatformIP6Route *) a,
-                                          (const NMPlatformIP6Route *) b);
+    return nm_platform_ip6_route_cmp((const NMPlatformIP6Route *) a,
+                                     (const NMPlatformIP6Route *) b,
+                                     NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL);
 }
 
 static inline void
@@ -280,7 +282,7 @@ nmtst_platform_ip6_routes_equal(const NMPlatformIP6Route *a,
     }
 
     for (i = 0; i < len; i++) {
-        if (nm_platform_ip6_route_cmp_full(&a[i], &b[i]) != 0) {
+        if (nm_platform_ip6_route_cmp(&a[i], &b[i], NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL) != 0) {
             char buf1[NM_UTILS_TO_STRING_BUFFER_SIZE];
             char buf2[NM_UTILS_TO_STRING_BUFFER_SIZE];
 
diff --git a/src/core/platform/nm-fake-platform.c b/src/core/platform/nm-fake-platform.c
index a1ca5434..c92d9aef 100644
--- a/src/core/platform/nm-fake-platform.c
+++ b/src/core/platform/nm-fake-platform.c
@@ -170,7 +170,7 @@ link_get(NMPlatform *platform, int ifindex)
     if (idx >= priv->links->len)
         goto not_found;
 
-    device = &g_array_index(priv->links, NMFakePlatformLink, idx);
+    device = &nm_g_array_index(priv->links, NMFakePlatformLink, idx);
     if (!device->obj)
         goto not_found;
 
@@ -234,8 +234,7 @@ link_add_pre(NMPlatform *platform,
 
     g_assert(!name || strlen(name) < IFNAMSIZ);
 
-    g_array_set_size(priv->links, priv->links->len + 1);
-    device  = &g_array_index(priv->links, NMFakePlatformLink, priv->links->len - 1);
+    device  = nm_g_array_append_new(priv->links, NMFakePlatformLink);
     ifindex = priv->links->len;
 
     memset(device, 0, sizeof(*device));
@@ -272,7 +271,7 @@ link_add_pre(NMPlatform *platform,
         g_assert(address_len == 0);
 
     device->obj        = o;
-    device->ip6_lladdr = *nmtst_inet6_from_string(ip6_lladdr);
+    device->ip6_lladdr = nmtst_inet6_from_string(ip6_lladdr);
 
     return device;
 }
@@ -980,7 +979,7 @@ ipx_address_delete(NMPlatform   *platform,
                 || (plen && address->plen != *plen)
                 || (peer_addr
                     && (((peer_addr_i ^ address->peer_address)
-                         & _nm_utils_ip4_prefix_to_netmask(address->plen))
+                         & nm_ip4_addr_netmask_from_prefix(address->plen))
                         != 0)))
                 continue;
         } else {
@@ -1093,10 +1092,7 @@ object_delete(NMPlatform *platform, const NMPObject *obj)
 }
 
 static int
-ip_route_add(NMPlatform              *platform,
-             NMPNlmFlags              flags,
-             int                      addr_family,
-             const NMPlatformIPRoute *route)
+ip_route_add(NMPlatform *platform, NMPNlmFlags flags, NMPObject *obj_stack)
 {
     NMDedupMultiIter                iter;
     nm_auto_nmpobj NMPObject       *obj = NULL;
@@ -1110,23 +1106,29 @@ ip_route_add(NMPlatform              *platform,
     NMPlatformIPRoute              *r           = NULL;
     NMPlatformIP4Route             *r4          = NULL;
     NMPlatformIP6Route             *r6          = NULL;
+    int                             addr_family;
     gboolean                        has_same_weak_id;
     gboolean                        only_dirty;
     guint16                         nlmsgflags;
 
-    g_assert(NM_IN_SET(addr_family, AF_INET, AF_INET6));
+    g_assert(NM_IN_SET(NMP_OBJECT_GET_TYPE(obj_stack),
+                       NMP_OBJECT_TYPE_IP4_ROUTE,
+                       NMP_OBJECT_TYPE_IP6_ROUTE));
+
+    addr_family = NMP_OBJECT_GET_ADDR_FAMILY(obj_stack);
 
     flags = NM_FLAGS_UNSET(flags, NMP_NLM_FLAG_SUPPRESS_NETLINK_FAILURE);
 
     /* currently, only replace is implemented. */
     g_assert(flags == NMP_NLM_FLAG_REPLACE);
 
-    obj = nmp_object_new(addr_family == AF_INET ? NMP_OBJECT_TYPE_IP4_ROUTE
-                                                : NMP_OBJECT_TYPE_IP6_ROUTE,
-                         (const NMPlatformObject *) route);
-    r = NMP_OBJECT_CAST_IP_ROUTE(obj);
+    if (NMP_OBJECT_GET_TYPE(obj_stack) == NMP_OBJECT_TYPE_IP4_ROUTE
+        && obj_stack->ip4_route.n_nexthops == 0 && obj_stack->ip4_route.ifindex > 0)
+        obj_stack->ip4_route.n_nexthops = 1;
 
-    nm_platform_ip_route_normalize(addr_family, r);
+    obj = nmp_object_clone(obj_stack, FALSE);
+
+    r = NMP_OBJECT_CAST_IP_ROUTE(obj);
 
     switch (addr_family) {
     case AF_INET:
@@ -1151,8 +1153,8 @@ ip_route_add(NMPlatform              *platform,
                                  &o) {
             if (addr_family == AF_INET) {
                 const NMPlatformIP4Route *item = NMP_OBJECT_CAST_IP4_ROUTE(o);
-                guint32 n = nm_utils_ip4_address_clear_host_address(item->network, item->plen);
-                guint32 g = nm_utils_ip4_address_clear_host_address(r4->gateway, item->plen);
+                guint32 n = nm_ip4_addr_clear_host_address(item->network, item->plen);
+                guint32 g = nm_ip4_addr_clear_host_address(r4->gateway, item->plen);
 
                 if (r->ifindex == item->ifindex && n == g) {
                     has_route_to_gw = TRUE;
@@ -1162,21 +1164,21 @@ ip_route_add(NMPlatform              *platform,
                 const NMPlatformIP6Route *item = NMP_OBJECT_CAST_IP6_ROUTE(o);
 
                 if (r->ifindex == item->ifindex
-                    && nm_utils_ip6_address_same_prefix(&r6->gateway, &item->network, item->plen)) {
+                    && nm_ip6_addr_same_prefix(&r6->gateway, &item->network, item->plen)) {
                     has_route_to_gw = TRUE;
                     break;
                 }
             }
         }
         if (!has_route_to_gw) {
-            char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+            char sbuf[NM_INET_ADDRSTRLEN];
 
             if (addr_family == AF_INET) {
                 nm_log_warn(
                     LOGD_PLATFORM,
                     "Fake platform: failure adding ip4-route '%d: %s/%d %d': Network Unreachable",
                     r->ifindex,
-                    _nm_utils_inet4_ntop(r4->network, sbuf),
+                    nm_inet4_ntop(r4->network, sbuf),
                     r->plen,
                     r->metric);
             } else {
@@ -1184,7 +1186,7 @@ ip_route_add(NMPlatform              *platform,
                     LOGD_PLATFORM,
                     "Fake platform: failure adding ip6-route '%d: %s/%d %d': Network Unreachable",
                     r->ifindex,
-                    _nm_utils_inet6_ntop(&r6->network, sbuf),
+                    nm_inet6_ntop(&r6->network, sbuf),
                     r->plen,
                     r->metric);
             }
@@ -1230,6 +1232,7 @@ ip_route_add(NMPlatform              *platform,
                                               obj,
                                               FALSE,
                                               nlmsgflags,
+                                              TRUE,
                                               &obj_old,
                                               &obj_new,
                                               &obj_replace,
@@ -1292,7 +1295,7 @@ finalize(GObject *object)
 
     g_hash_table_unref(priv->options);
     for (i = 0; i < priv->links->len; i++) {
-        NMFakePlatformLink *device = &g_array_index(priv->links, NMFakePlatformLink, i);
+        NMFakePlatformLink *device = &nm_g_array_index(priv->links, NMFakePlatformLink, i);
 
         nm_clear_pointer(&device->obj, nmp_object_unref);
     }
diff --git a/src/core/platform/nm-fake-platform.h b/src/core/platform/nm-fake-platform.h
index 8f8571fa..88fd3243 100644
--- a/src/core/platform/nm-fake-platform.h
+++ b/src/core/platform/nm-fake-platform.h
@@ -10,7 +10,7 @@
 
 #define NM_TYPE_FAKE_PLATFORM (nm_fake_platform_get_type())
 #define NM_FAKE_PLATFORM(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_FAKE_PLATFORM, NMFakePlatform))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_FAKE_PLATFORM, NMFakePlatform))
 #define NM_FAKE_PLATFORM_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_FAKE_PLATFORM, NMFakePlatformClass))
 #define NM_IS_FAKE_PLATFORM(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_FAKE_PLATFORM))
diff --git a/src/core/platform/tests/meson.build b/src/core/platform/tests/meson.build
index 8824ed2f..252c6840 100644
--- a/src/core/platform/tests/meson.build
+++ b/src/core/platform/tests/meson.build
@@ -13,7 +13,7 @@ test_units = [
   ['test-nmp-object', 'test-nmp-object.c', test_c_flags, default_test_timeout],
   ['test-platform-general', 'test-platform-general.c', test_c_flags, default_test_timeout],
   ['test-route-fake', 'test-route.c', test_fake_c_flags, default_test_timeout],
-  ['test-route-linux', 'test-route.c', test_linux_c_flags, default_test_timeout],
+  ['test-route-linux', 'test-route.c', test_linux_c_flags, 900],
   ['test-tc-linux', 'test-tc.c', test_linux_c_flags, default_test_timeout],
 ]
 
diff --git a/src/core/platform/tests/monitor.c b/src/core/platform/tests/monitor.c
index 8d0e0107..c83192bb 100644
--- a/src/core/platform/tests/monitor.c
+++ b/src/core/platform/tests/monitor.c
@@ -8,6 +8,8 @@
 #include <stdlib.h>
 #include <syslog.h>
 
+#include "libnm-glib-aux/nm-str-buf.h"
+#include "libnm-glib-aux/nm-io-utils.h"
 #include "libnm-platform/nm-linux-platform.h"
 #include "libnm-platform/nmp-object.h"
 
@@ -17,23 +19,45 @@ NMTST_DEFINE();
 
 static struct {
     gboolean persist;
+    char    *state_file;
 } global_opt = {
     .persist = TRUE,
 };
 
+const char *const *const ip_argv_rule6 = NM_MAKE_STRV("ip", "-d", "-6", "rule");
+
+static const struct {
+    NMPObjectType      obj_type;
+    const char *const *ip_argv;
+} dump_obj_types[] = {
+    {NMP_OBJECT_TYPE_LINK, NM_MAKE_STRV("ip", "-d", "link")},
+    {NMP_OBJECT_TYPE_IP4_ADDRESS, NM_MAKE_STRV("ip", "-d", "-4", "address")},
+    {NMP_OBJECT_TYPE_IP6_ADDRESS, NM_MAKE_STRV("ip", "-d", "-6", "address")},
+    {NMP_OBJECT_TYPE_IP4_ROUTE, NM_MAKE_STRV("ip", "-d", "-4", "route")},
+    {NMP_OBJECT_TYPE_IP6_ROUTE, NM_MAKE_STRV("ip", "-d", "-6", "route")},
+    {NMP_OBJECT_TYPE_ROUTING_RULE, NM_MAKE_STRV("ip", "-d", "-4", "rule")},
+};
+
 static gboolean
 read_argv(int *argc, char ***argv)
 {
     GOptionContext *context;
     GOptionEntry    options[] = {
-           {"no-persist",
+        {"no-persist",
             'P',
             G_OPTION_FLAG_REVERSE,
             G_OPTION_ARG_NONE,
             &global_opt.persist,
             "Exit after processing netlink messages",
             NULL},
-           {0},
+        {"state-file",
+            'S',
+            0,
+            G_OPTION_ARG_FILENAME,
+            &global_opt.state_file,
+            "Dump the platform cache to this file",
+            "FILE"},
+        {0},
     };
     gs_free_error GError *error = NULL;
 
@@ -63,6 +87,123 @@ mptcp_addr_dump(NMPlatform *platform)
 
 /*****************************************************************************/
 
+static void
+_dump_state(NMPlatform *platform, const char *state_file)
+{
+    nm_auto_str_buf NMStrBuf sbuf = NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_488, FALSE);
+    nm_auto_unref_gdatetime GDateTime *time_datetime = NULL;
+    gs_free char                      *time_str      = NULL;
+    int                                i_obj_type;
+
+    if (!state_file)
+        return;
+
+    time_datetime = g_date_time_new_now_local();
+    time_str      = g_date_time_format(time_datetime, "%C%y-%m-%dT%H:%M:%S.%f");
+
+    nm_log_dbg(LOGD_PLATFORM, "dump to file \"%s\", at %s", state_file, time_str);
+
+    nm_str_buf_append_printf(&sbuf, "time: %s\n", time_str);
+    nm_str_buf_append_printf(&sbuf, "pid: %lld\n", (long long) getpid());
+
+    for (i_obj_type = 0; i_obj_type < (int) G_N_ELEMENTS(dump_obj_types); i_obj_type++) {
+        NMPObjectType                obj_type = dump_obj_types[i_obj_type].obj_type;
+        const char *const           *ip_argv  = dump_obj_types[i_obj_type].ip_argv;
+        const NMDedupMultiHeadEntry *pl_head_entry;
+        NMDedupMultiIter             pl_iter;
+        const NMPObject             *obj;
+        guint                        i;
+        char                         buf1[1000];
+
+        pl_head_entry = nm_platform_lookup_obj_type(platform, obj_type);
+
+        nm_str_buf_append_printf(&sbuf,
+                                 "\n%s: %u\n",
+                                 nmp_class_from_type(obj_type)->obj_type_name,
+                                 pl_head_entry ? pl_head_entry->len : 0u);
+
+        i = 0;
+        nmp_cache_iter_for_each (&pl_iter, pl_head_entry, &obj) {
+            nmp_object_to_string(obj, NMP_OBJECT_TO_STRING_PUBLIC, buf1, sizeof(buf1));
+            nm_str_buf_append_printf(&sbuf,
+                                     "%s[%u]: %s\n",
+                                     nmp_class_from_type(obj_type)->obj_type_name,
+                                     i,
+                                     buf1);
+            g_assert(strlen(buf1) < sizeof(buf1) - 1u);
+            i++;
+        }
+
+ip_again:
+        if (ip_argv) {
+            gs_free_error GError *error      = NULL;
+            gs_free char         *ip_argv_ss = NULL;
+            gs_free char         *s_stdout   = NULL;
+            gs_free char         *s_stderr   = NULL;
+            int                   exit_code;
+
+            g_spawn_sync(NULL,
+                         (char **) ip_argv,
+                         NULL,
+                         G_SPAWN_SEARCH_PATH,
+                         NULL,
+                         NULL,
+                         &s_stdout,
+                         &s_stderr,
+                         &exit_code,
+                         &error);
+
+            nm_str_buf_append_printf(&sbuf,
+                                     "%s: call %s: ",
+                                     nmp_class_from_type(obj_type)->obj_type_name,
+                                     ip_argv_ss = g_strjoinv(" ", (char **) ip_argv));
+            if (error) {
+                nm_str_buf_append_printf(&sbuf, "FAILED: %s\n", error->message);
+            } else if (WIFEXITED(exit_code) && WEXITSTATUS(exit_code) == 0)
+                nm_str_buf_append_printf(&sbuf, "SUCCESS\n");
+            else {
+                nm_str_buf_append_printf(
+                    &sbuf,
+                    "ERROR: %s\n",
+                    nm_utils_get_process_exit_status_desc_buf(exit_code, buf1, sizeof(buf1)));
+            }
+            if (!nm_str_is_empty(s_stdout))
+                nm_str_buf_append_printf(&sbuf, "STDOUT>\n%s<\n", s_stdout);
+            if (!nm_str_is_empty(s_stderr))
+                nm_str_buf_append_printf(&sbuf, "STDERR>\n%s<\n", s_stderr);
+
+            if (obj_type == NMP_OBJECT_TYPE_ROUTING_RULE
+                && ip_argv == dump_obj_types[i_obj_type].ip_argv) {
+                ip_argv = ip_argv_rule6;
+                goto ip_again;
+            }
+        }
+    }
+
+    nm_utils_file_set_contents(state_file,
+                               nm_str_buf_get_str_unsafe(&sbuf),
+                               sbuf.len,
+                               00644,
+                               NULL,
+                               NULL,
+                               NULL);
+
+    nm_log_dbg(LOGD_PLATFORM, "dump to file complete");
+}
+
+static void
+_dump_state_platform_cb(NMPlatform   *platform,
+                        int           obj_type_i,
+                        int           ifindex,
+                        gconstpointer platform_object,
+                        int           change_type_i,
+                        gpointer      unused_user_data)
+{
+    _dump_state(platform, global_opt.state_file);
+}
+
+/*****************************************************************************/
+
 int
 main(int argc, char **argv)
 {
@@ -82,12 +223,32 @@ main(int argc, char **argv)
 
     nm_linux_platform_setup();
 
+    if (global_opt.state_file) {
+        int i_obj_type;
+
+        for (i_obj_type = 0; i_obj_type < (int) G_N_ELEMENTS(dump_obj_types); i_obj_type++) {
+            NMPObjectType obj_type = dump_obj_types[i_obj_type].obj_type;
+
+            g_signal_connect(NM_PLATFORM_GET,
+                             nmp_class_from_type(obj_type)->signal_type,
+                             G_CALLBACK(_dump_state_platform_cb),
+                             NULL);
+        }
+    }
+
     mptcp_addr_dump(NM_PLATFORM_GET);
 
+    _dump_state(NM_PLATFORM_GET, global_opt.state_file);
+
     if (global_opt.persist)
         g_main_loop_run(loop);
 
     g_main_loop_unref(loop);
 
+    g_signal_handlers_disconnect_by_func(NM_PLATFORM_GET,
+                                         G_CALLBACK(_dump_state_platform_cb),
+                                         NULL);
+    g_object_unref(NM_PLATFORM_GET);
+
     return EXIT_SUCCESS;
 }
diff --git a/src/core/platform/tests/test-address.c b/src/core/platform/tests/test-address.c
index 700891e8..0bce00c4 100644
--- a/src/core/platform/tests/test-address.c
+++ b/src/core/platform/tests/test-address.c
@@ -118,7 +118,7 @@ test_ip4_address_general(void)
     addresses = nmtstp_platform_ip4_address_get_all(NM_PLATFORM_GET, ifindex);
     g_assert(addresses);
     g_assert_cmpint(addresses->len, ==, 1);
-    address = &g_array_index(addresses, NMPlatformIP4Address, 0);
+    address = &nm_g_array_first(addresses, NMPlatformIP4Address);
     g_assert_cmpint(address->ifindex, ==, ifindex);
     g_assert_cmphex(address->address, ==, addr);
     g_assert_cmphex(address->peer_address, ==, addr);
@@ -193,7 +193,7 @@ test_ip6_address_general(void)
     addresses = nmtstp_platform_ip6_address_get_all(NM_PLATFORM_GET, ifindex);
     g_assert(addresses);
     g_assert_cmpint(addresses->len, ==, 1);
-    address = &g_array_index(addresses, NMPlatformIP6Address, 0);
+    address = &nm_g_array_first(addresses, NMPlatformIP6Address);
     g_assert_cmpint(address->ifindex, ==, ifindex);
     g_assert(!memcmp(&address->address, &addr, sizeof(addr)));
     g_assert_cmpint(address->plen, ==, IP6_PLEN);
@@ -453,7 +453,7 @@ _nmtstp_init_tests(int *argc, char ***argv)
 void
 _nmtstp_setup_tests(void)
 {
-#define add_test_func(testpath, test_func) nmtstp_env1_add_test_func(testpath, test_func, FALSE)
+#define add_test_func(testpath, test_func) nmtstp_env1_add_test_func(testpath, test_func, 1, FALSE)
     add_test_func("/address/ipv4/general", test_ip4_address_general);
     add_test_func("/address/ipv6/general", test_ip6_address_general);
 
diff --git a/src/core/platform/tests/test-cleanup.c b/src/core/platform/tests/test-cleanup.c
index 2198e921..139a0280 100644
--- a/src/core/platform/tests/test-cleanup.c
+++ b/src/core/platform/tests/test-cleanup.c
@@ -63,7 +63,7 @@ test_cleanup_internal(void)
             nm_platform_process_events(NM_PLATFORM_GET);
         }
         addrs = nmtstp_platform_ip6_address_get_all(NM_PLATFORM_GET, ifindex);
-        if (addrs->len == 1 && (a = &g_array_index(addrs, NMPlatformIP6Address, 0))
+        if (addrs->len == 1 && (a = &nm_g_array_first(addrs, NMPlatformIP6Address))
             && IN6_IS_ADDR_LINKLOCAL(&a->address))
             break;
     });
@@ -73,7 +73,7 @@ test_cleanup_internal(void)
                                          addr4,
                                          plen4,
                                          addr4,
-                                         nm_platform_ip4_broadcast_address_create(addr4, plen4),
+                                         nm_ip4_addr_get_broadcast_address(addr4, plen4),
                                          lifetime,
                                          preferred,
                                          0,
diff --git a/src/core/platform/tests/test-common.c b/src/core/platform/tests/test-common.c
index b5bb1232..a37982fc 100644
--- a/src/core/platform/tests/test-common.c
+++ b/src/core/platform/tests/test-common.c
@@ -15,6 +15,7 @@
 #include <linux/rtnetlink.h>
 
 #include "n-acd/src/n-acd.h"
+#include "libnm-platform/nm-platform-utils.h"
 
 #define SIGNAL_DATA_FMT "'%s-%s' ifindex %d%s%s%s (%d times received)"
 #define SIGNAL_DATA_ARG(data)                                                                     \
@@ -22,8 +23,60 @@
         (data)->ifname ? " ifname '" : "", (data)->ifname ?: "", (data)->ifname ? "'" : "",       \
         (data)->received_count
 
-int NMTSTP_ENV1_IFINDEX = -1;
-int NMTSTP_ENV1_EX      = -1;
+int NMTSTP_ENV1_IFINDEXES[];
+
+const char *const NMTSTP_ENV1_DEVICE_NAME[] = {
+    "nm-test-device0",
+    "nm-test-device1",
+};
+
+int NMTSTP_ENV1_EX = -1;
+
+/*****************************************************************************/
+
+typedef struct {
+    const char *module_name;
+
+    NMLinkType iftype;
+
+    /* These modules create additional interfaces, like
+     * "gre0" for "ip_gre" module.
+     *
+     * - actually some modules create multiple interfaces, like "ip_gre"
+     *   creating "gre0", "gretap0", "erspan0".
+     * - if already an interface with such name exist, kernel would create
+     *   names like "gre1" or "gretap1". We don't care for that, because
+     *   we run in our own namespace and we control which interfaces are there.
+     *   We wouldn't create an interface with such a conflicting name.
+     *
+     * Anyway. This is the name of *one* of the interfaces that the module would
+     * create. */
+    const char *ifname;
+
+    /* This only gets set, if iftype is NM_LINK_TYPE_UNKNOWN. It corresponds to
+     * NMPlatformLink.kind. */
+    const char *ifkind;
+
+} IPTunnelModInfo;
+
+#define INF(_module_name, _iftype, _ifname, ...)                                           \
+    {                                                                                      \
+        .module_name = ""_module_name, .iftype = _iftype, .ifname = ""_ifname, __VA_ARGS__ \
+    }
+
+static const IPTunnelModInfo ip_tunnel_mod_infos[] = {
+    INF("ip_gre", NM_LINK_TYPE_GRE, "gre0"),
+    INF("ip_gre", NM_LINK_TYPE_GRETAP, "gretap0"),
+    INF("ip_gre", NM_LINK_TYPE_UNKNOWN, "erspan0", .ifkind = "erspan"),
+    INF("ipip", NM_LINK_TYPE_IPIP, "tunl0"),
+    INF("ip6_tunnel", NM_LINK_TYPE_IP6TNL, "ip6tnl0"),
+    INF("ip6_gre", NM_LINK_TYPE_IP6GRE, "ip6gre0"),
+    INF("sit", NM_LINK_TYPE_SIT, "sit0"),
+    INF("ip_vti", NM_LINK_TYPE_VTI, "ip_vti0"),
+    INF("ip6_vti", NM_LINK_TYPE_VTI6, "ip6_vti0"),
+};
+
+#undef INF
 
 /*****************************************************************************/
 
@@ -218,13 +271,13 @@ _nmtstp_platform_ip_addresses_assert(const char        *filename,
             }
 
             if (!addr_bin->found) {
-                char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+                char sbuf[NM_INET_ADDRSTRLEN];
 
                 g_error("%s:%d: IPv%c address %s was not found on ifindex %d",
                         filename,
                         lineno,
                         nm_utils_addr_family_to_char(addr_bin->addr_family),
-                        nm_utils_inet_ntop(addr_bin->addr_family, &addr_bin->addr, sbuf),
+                        nm_inet_ntop(addr_bin->addr_family, &addr_bin->addr, sbuf),
                         ifindex);
             }
         }
@@ -583,7 +636,7 @@ _nmtstp_assert_ip4_route_exists(const char *file,
     r = _ip4_route_get(platform, ifindex, network, plen, metric, tos, &c);
 
     if (c != c_exists && c_exists != -1) {
-        char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+        char sbuf[NM_INET_ADDRSTRLEN];
 
         NM_PRAGMA_WARNING_DISABLE_DANGLING_POINTER
         g_error("[%s:%u] %s(): The ip4 route %s/%d metric %u tos %u shall exist %u times, but "
@@ -591,7 +644,7 @@ _nmtstp_assert_ip4_route_exists(const char *file,
                 file,
                 line,
                 func,
-                _nm_utils_inet4_ntop(network, sbuf),
+                nm_inet4_ntop(network, sbuf),
                 plen,
                 metric,
                 tos,
@@ -683,8 +736,8 @@ _nmtstp_assert_ip6_route_exists(const char            *file,
     r = _ip6_route_get(platform, ifindex, network, plen, metric, src, src_plen, &c);
 
     if (c != c_exists && c_exists != -1) {
-        char s_src[NM_UTILS_INET_ADDRSTRLEN];
-        char s_network[NM_UTILS_INET_ADDRSTRLEN];
+        char s_src[NM_INET_ADDRSTRLEN];
+        char s_network[NM_INET_ADDRSTRLEN];
 
         NM_PRAGMA_WARNING_DISABLE_DANGLING_POINTER
         g_error("[%s:%u] %s(): The ip6 route %s/%d metric %u src %s/%d shall exist %u times, but "
@@ -692,10 +745,10 @@ _nmtstp_assert_ip6_route_exists(const char            *file,
                 file,
                 line,
                 func,
-                _nm_utils_inet6_ntop(network, s_network),
+                nm_inet6_ntop(network, s_network),
                 plen,
                 metric,
-                _nm_utils_inet6_ntop(src, s_src),
+                nm_inet6_ntop(src, s_src),
                 src_plen,
                 c_exists,
                 c);
@@ -739,6 +792,642 @@ nmtstp_run_command(const char *format, ...)
 
 /*****************************************************************************/
 
+static int
+_assert_platform_sort_objs(gconstpointer ptr_a, gconstpointer ptr_b)
+{
+    const NMPObject *a = *((const NMPObject *const *) ptr_a);
+    const NMPObject *b = *((const NMPObject *const *) ptr_b);
+
+    g_assert(NMP_OBJECT_IS_VALID(a));
+    g_assert(NMP_OBJECT_IS_VALID(b));
+    g_assert(NMP_OBJECT_GET_TYPE(a) == NMP_OBJECT_GET_TYPE(b));
+
+    NM_CMP_RETURN(nmp_object_id_cmp(a, b));
+    g_assert_not_reached();
+    return 0;
+}
+
+static void
+_assert_platform_printarr(NMPObjectType obj_type, GPtrArray *arr1, GPtrArray *arr2)
+{
+    char  sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+    guint i;
+
+    _LOGT("compare arrays of %s. In cache %u entries, fetched %u entries",
+          NMP_OBJECT_TYPE_NAME(obj_type),
+          nm_g_ptr_array_len(arr1),
+          nm_g_ptr_array_len(arr2));
+
+    for (i = 0; i < nm_g_ptr_array_len(arr1); i++) {
+        _LOGT("cache[%u] %s",
+              i,
+              nmp_object_to_string(arr1->pdata[i], NMP_OBJECT_TO_STRING_ALL, sbuf, sizeof(sbuf)));
+    }
+    for (i = 0; i < nm_g_ptr_array_len(arr2); i++) {
+        _LOGT("fetch[%u] %s",
+              i,
+              nmp_object_to_string(arr2->pdata[i], NMP_OBJECT_TO_STRING_ALL, sbuf, sizeof(sbuf)));
+    }
+
+    switch (obj_type) {
+    case NMP_OBJECT_TYPE_LINK:
+        nmtstp_run_command("ip -d link");
+        break;
+    case NMP_OBJECT_TYPE_IP4_ADDRESS:
+        nmtstp_run_command("ip -d -4 address");
+        break;
+    case NMP_OBJECT_TYPE_IP6_ADDRESS:
+        nmtstp_run_command("ip -d -6 address");
+        break;
+    case NMP_OBJECT_TYPE_IP4_ROUTE:
+        nmtstp_run_command("ip -d -4 route show table all");
+        break;
+    case NMP_OBJECT_TYPE_IP6_ROUTE:
+        nmtstp_run_command("ip -d -6 route show table all");
+        break;
+    default:
+        g_assert_not_reached();
+        break;
+    }
+}
+
+static gboolean
+_assert_platform_normalize_all(GPtrArray *arr)
+{
+    guint    i;
+    gboolean normalized = FALSE;
+
+    for (i = 0; i < nm_g_ptr_array_len(arr); i++) {
+        const NMPObject **ptr         = (gpointer) &arr->pdata[i];
+        nm_auto_nmpobj NMPObject *new = NULL;
+        gboolean skip                 = FALSE;
+
+        switch (NMP_OBJECT_GET_TYPE(*ptr)) {
+        case NMP_OBJECT_TYPE_LINK:
+        {
+            const NMPlatformLink *link = NMP_OBJECT_CAST_LINK(*ptr);
+
+            if (nmtstp_link_is_iptunnel_special(link)) {
+                /* These are special interfaces for the ip tunnel modules, like
+                 * "gre0" created by the "ip_gre" module.
+                 *
+                 * These interfaces can appear at any moment, when the module
+                 * gets loaded (by anybody on the host). We might want to avoid
+                 * that by calling nmtstp_ensure_module(), but it's worse.
+                 * Kernel does not send correct RTM_NEWLINK events when those
+                 * interfaces get created. So the cache content based on the
+                 * events will differ from a new load from a dump.
+                 *
+                 * We need to ignore those interfaces. */
+                skip = TRUE;
+            } else if (link->type == NM_LINK_TYPE_UNKNOWN) {
+                /* The link type is not detected. This might be a generated
+                 * interface like nmtstp_link_is_iptunnel_special(), but for
+                 * kernel modules that we don't know about. Ignore them too. */
+                skip = TRUE;
+            }
+
+            if (!skip) {
+                new                  = nmp_object_clone(*ptr, FALSE);
+                new->link.rx_packets = 0;
+                new->link.rx_bytes   = 0;
+                new->link.tx_packets = 0;
+                new->link.tx_bytes   = 0;
+            }
+            if (nmp_object_ref_set(ptr, new))
+                normalized = TRUE;
+            break;
+        }
+        default:
+            break;
+        }
+    }
+
+    while (g_ptr_array_remove(arr, NULL)) {
+        /* Remove NULL values. */
+        normalized = TRUE;
+    }
+
+    return normalized;
+}
+
+static gboolean
+_assert_platform_compare_arr(NMPObjectType obj_type,
+                             const char   *detail_type,
+                             GPtrArray    *arr1,
+                             GPtrArray    *arr2,
+                             gboolean      normalized,
+                             gboolean      share_multi_idx,
+                             gboolean      do_assert)
+{
+    const NMPClass *obj_class = nmp_class_from_type(obj_type);
+    char            sbuf1[NM_UTILS_TO_STRING_BUFFER_SIZE];
+    char            sbuf2[NM_UTILS_TO_STRING_BUFFER_SIZE];
+    int             idx;
+    int             idx_pointer_comp = -1;
+
+#define _fail_msg(do_assert, ...) \
+    G_STMT_START                  \
+    {                             \
+        if (do_assert) {          \
+            g_error(__VA_ARGS__); \
+        } else {                  \
+            _LOGW(__VA_ARGS__);   \
+            return FALSE;         \
+        }                         \
+    }                             \
+    G_STMT_END
+
+    for (idx = 0; TRUE; idx++) {
+        if (nm_g_ptr_array_len(arr1) == idx && nm_g_ptr_array_len(arr2) == idx)
+            break;
+        if (idx >= nm_g_ptr_array_len(arr1)) {
+            _assert_platform_printarr(obj_type, arr1, arr2);
+            _fail_msg(do_assert,
+                      "Comparing %s (%s) for platform fails. Platform now shows entry #%u which is "
+                      "not in the cache but expected %s",
+                      obj_class->obj_type_name,
+                      detail_type,
+                      idx,
+                      nmp_object_to_string(arr2->pdata[idx],
+                                           NMP_OBJECT_TO_STRING_ALL,
+                                           sbuf1,
+                                           sizeof(sbuf1)));
+        }
+        if (idx >= nm_g_ptr_array_len(arr2)) {
+            _assert_platform_printarr(obj_type, arr1, arr2);
+            _fail_msg(
+                do_assert,
+                "Comparing %s (%s) for platform fails. Platform has no more entry #%u which is "
+                "still in the cache as %s",
+                obj_class->obj_type_name,
+                detail_type,
+                idx,
+                nmp_object_to_string(arr1->pdata[idx],
+                                     NMP_OBJECT_TO_STRING_ALL,
+                                     sbuf1,
+                                     sizeof(sbuf1)));
+        }
+        if (!nmp_object_equal(arr1->pdata[idx], arr2->pdata[idx])) {
+            _assert_platform_printarr(obj_type, arr1, arr2);
+            _fail_msg(do_assert,
+                      "Comparing %s (%s) for platform fails. Platform entry #%u is now %s but in "
+                      "cache is %s",
+                      obj_class->obj_type_name,
+                      detail_type,
+                      idx,
+                      nmp_object_to_string(arr2->pdata[idx],
+                                           NMP_OBJECT_TO_STRING_ALL,
+                                           sbuf1,
+                                           sizeof(sbuf1)),
+                      nmp_object_to_string(arr1->pdata[idx],
+                                           NMP_OBJECT_TO_STRING_ALL,
+                                           sbuf2,
+                                           sizeof(sbuf2)));
+        }
+
+        if (!normalized && (share_multi_idx != (arr1->pdata[idx] == arr2->pdata[idx]))
+            && idx_pointer_comp == -1)
+            idx_pointer_comp = idx;
+    }
+
+    if (idx_pointer_comp != -1) {
+        _assert_platform_printarr(obj_type, arr1, arr2);
+        _fail_msg(do_assert,
+                  "Comparing %s (%s) for platform fails for pointer comparison. Platform entry "
+                  "#%u is now %s but in cache is %s",
+                  obj_class->obj_type_name,
+                  detail_type,
+                  idx_pointer_comp,
+                  nmp_object_to_string(arr2->pdata[idx_pointer_comp],
+                                       NMP_OBJECT_TO_STRING_ALL,
+                                       sbuf1,
+                                       sizeof(sbuf1)),
+                  nmp_object_to_string(arr1->pdata[idx_pointer_comp],
+                                       NMP_OBJECT_TO_STRING_ALL,
+                                       sbuf2,
+                                       sizeof(sbuf2)));
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+gboolean
+nmtstp_link_is_iptunnel_special(const NMPlatformLink *link)
+{
+    int i;
+
+    g_assert(link);
+
+    /* These interfaces are autogenerated when loading the ip tunnel
+     * modules. For example, loading "ip_gre" results in interfaces
+     * "gre0", "gretap0", "erspan0".
+     *
+     * Actually, if the interface names are already taken ("gre0" already
+     * exists), it will create "gre1" and so on. We don't care about that,
+     * because in our test's netns that is not happening. */
+
+    for (i = 0; i < (int) G_N_ELEMENTS(ip_tunnel_mod_infos); i++) {
+        const IPTunnelModInfo *module_info = &ip_tunnel_mod_infos[i];
+
+        if (module_info->iftype != link->type)
+            continue;
+        if (!nm_streq(module_info->ifname, link->name))
+            continue;
+        if (module_info->ifkind && !nm_streq(module_info->ifkind, link->kind))
+            continue;
+
+        return TRUE;
+    }
+
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+gboolean
+nmtstp_ensure_module(const char *module_name)
+{
+    /* using iproute2 seems to fail sometimes? Force use of platform code. */
+    const int              EX          = 0;
+    gs_free char          *test_ifname = NULL;
+    const NMPlatformLink  *link;
+    static int             module_state[G_N_ELEMENTS(ip_tunnel_mod_infos)] = {0};
+    int                    i_module_info;
+    const IPTunnelModInfo *module_info = NULL;
+    int                    i;
+    int                    ifindex;
+    gboolean               result;
+
+    if (!module_name) {
+        result = TRUE;
+        for (i = 0; i < (int) G_N_ELEMENTS(ip_tunnel_mod_infos); i++) {
+            if (!nmtstp_ensure_module(ip_tunnel_mod_infos[i].module_name))
+                result = FALSE;
+        }
+        return result;
+    }
+
+    for (i_module_info = 0; i_module_info < (int) G_N_ELEMENTS(ip_tunnel_mod_infos);
+         i_module_info++) {
+        if (nm_streq(module_name, ip_tunnel_mod_infos[i_module_info].module_name)) {
+            module_info = &ip_tunnel_mod_infos[i_module_info];
+            break;
+        }
+    }
+    if (!module_info)
+        g_error("%s:%d: Module name \"%s\" not implemented!", __FILE__, __LINE__, module_name);
+
+    test_ifname = g_strdup_printf("nm-mod-%s", module_info->ifname);
+    g_assert(nm_utils_ifname_valid_kernel(test_ifname, NULL));
+
+again:
+    i = g_atomic_int_get(&module_state[i_module_info]);
+    if (i != 0)
+        return i > 0;
+
+    /* When tunnel modules get loaded, then interfaces like "gre0", "gretap0"
+     * and "erspan0" (for "ip_gre" module) appear. For other modules, the interfaces
+     * are named differently. Of course, unless those interface name are already taken,
+     * in which case it will create "gre1", etc). So ugly.
+     *
+     * Anyway. as we run unit tests in parallel (`make check -j`), another
+     * test might just load the module just now, which results in the creation of
+     * those interfaces in our current namespace. That can break the test.
+     */
+
+    link = nmtstp_link_get_typed(NM_PLATFORM_GET, 0, test_ifname, module_info->iftype);
+    g_assert(!link);
+
+    link = nmtstp_link_get_typed(NM_PLATFORM_GET, 0, module_info->ifname, module_info->iftype);
+    if (link) {
+        g_assert(nmtstp_link_is_iptunnel_special(link));
+        /* An interface with this name exists. While technically this could not be the interface
+         * generated by the kernel module, in our test netns we can assume that it is. This is
+         * good enough. */
+        result = TRUE;
+        goto out;
+    }
+
+    /* Try to load the module. It probably won't work, because we don't have permissions.
+     * Ignore any failure. */
+    nmp_utils_modprobe(NULL, TRUE, module_info->module_name, NULL);
+
+    if (nm_streq(module_name, "ip_gre")) {
+        link = nmtstp_link_gre_add(NULL,
+                                   EX,
+                                   test_ifname,
+                                   &((const NMPlatformLnkGre){
+                                       .local          = nmtst_inet4_from_string("192.168.233.204"),
+                                       .remote         = nmtst_inet4_from_string("172.168.10.25"),
+                                       .parent_ifindex = 0,
+                                       .ttl            = 174,
+                                       .tos            = 37,
+                                       .path_mtu_discovery = TRUE,
+                                   }));
+    } else if (nm_streq(module_name, "ipip")) {
+        link = nmtstp_link_ipip_add(NULL,
+                                    EX,
+                                    test_ifname,
+                                    &((const NMPlatformLnkIpIp){
+                                        .local              = nmtst_inet4_from_string("1.2.3.4"),
+                                        .remote             = nmtst_inet4_from_string("5.6.7.8"),
+                                        .parent_ifindex     = 0,
+                                        .tos                = 32,
+                                        .path_mtu_discovery = FALSE,
+                                    }));
+    } else if (nm_streq(module_name, "ip6_tunnel")) {
+        link = nmtstp_link_ip6tnl_add(NULL,
+                                      EX,
+                                      test_ifname,
+                                      &((const NMPlatformLnkIp6Tnl){
+                                          .local       = nmtst_inet6_from_string("fd01::15"),
+                                          .remote      = nmtst_inet6_from_string("fd01::16"),
+                                          .tclass      = 20,
+                                          .encap_limit = 6,
+                                          .flow_label  = 1337,
+                                          .proto       = IPPROTO_IPV6,
+                                      }));
+    } else if (nm_streq(module_name, "ip6_gre")) {
+        link = nmtstp_link_ip6gre_add(NULL,
+                                      EX,
+                                      test_ifname,
+                                      &((const NMPlatformLnkIp6Tnl){
+                                          .local      = nmtst_inet6_from_string("fd01::42"),
+                                          .remote     = nmtst_inet6_from_string("fd01::aaaa"),
+                                          .tclass     = 21,
+                                          .flow_label = 1338,
+                                          .is_gre     = TRUE,
+                                      }));
+    } else if (nm_streq(module_name, "sit")) {
+        link = nmtstp_link_sit_add(NULL,
+                                   EX,
+                                   test_ifname,
+                                   &((const NMPlatformLnkSit){
+                                       .local  = nmtst_inet4_from_string("192.168.200.1"),
+                                       .remote = nmtst_inet4_from_string("172.25.100.14"),
+                                       .ttl    = 0,
+                                       .tos    = 31,
+                                       .path_mtu_discovery = FALSE,
+                                   }));
+    } else if (nm_streq(module_name, "ip_vti")) {
+        link = nmtstp_link_vti_add(NULL,
+                                   EX,
+                                   test_ifname,
+                                   &((const NMPlatformLnkVti){
+                                       .local  = nmtst_inet4_from_string("192.168.212.204"),
+                                       .remote = nmtst_inet4_from_string("172.168.11.25"),
+                                       .ikey   = 12,
+                                       .okey   = 13,
+                                   }));
+    } else if (nm_streq(module_name, "ip6_vti")) {
+        link = nmtstp_link_vti6_add(NULL,
+                                    EX,
+                                    test_ifname,
+                                    &((const NMPlatformLnkVti6){
+                                        .local  = nmtst_inet6_from_string("fd01::1"),
+                                        .remote = nmtst_inet6_from_string("fd02::2"),
+                                        .ikey   = 13,
+                                        .okey   = 14,
+                                    }));
+    } else
+        g_error("%s:%d: Module name \"%s\" not implemented!", __FILE__, __LINE__, module_name);
+
+    if (!link) {
+        /* We might be unable to add the interface, if the kernel module does not exist.
+         * Be graceful about that. */
+        ifindex = 0;
+        g_assert(!nmtstp_link_get_typed(NM_PLATFORM_GET, 0, test_ifname, module_info->iftype));
+        g_assert(
+            !nmtstp_link_get_typed(NM_PLATFORM_GET, 0, module_info->ifname, module_info->iftype));
+    } else {
+        ifindex = link->ifindex;
+
+        g_assert(link);
+        g_assert(
+            link
+            == nmtstp_link_get_typed(NM_PLATFORM_GET, ifindex, test_ifname, module_info->iftype));
+
+        nmtstp_link_delete(NULL, -1, link->ifindex, test_ifname, TRUE);
+
+        link = nmtstp_link_get_typed(NM_PLATFORM_GET, 0, module_info->ifname, module_info->iftype);
+        g_assert(nmtstp_link_is_iptunnel_special(link));
+    }
+
+    result = ifindex > 0 ? 1 : -1;
+
+out:
+    if (!g_atomic_int_compare_and_exchange(&module_state[i_module_info], 0, result))
+        goto again;
+
+    if (!result) {
+        /* The function aims to be graceful about missing kernel modules. */
+
+        /* g_error("Failure to ensure module \"%s\"", module_name); */
+    }
+
+    return result;
+}
+
+gboolean
+nmtstp_check_platform_full(NMPlatform *platform, guint32 obj_type_flags, gboolean do_assert)
+{
+    static const NMPObjectType obj_types[] = {
+        NMP_OBJECT_TYPE_IP4_ADDRESS,
+        NMP_OBJECT_TYPE_IP6_ADDRESS,
+        NMP_OBJECT_TYPE_IP4_ROUTE,
+        NMP_OBJECT_TYPE_IP6_ROUTE,
+        NMP_OBJECT_TYPE_LINK,
+    };
+    gboolean                    obj_type_flags_all = (obj_type_flags == 0u);
+    gs_unref_object NMPlatform *platform2          = NULL;
+    int                         i_obj_types;
+    gboolean                    share_multi_idx = nmtst_get_rand_bool();
+
+    /* This test creates a new NMLinuxPlatform instance. This will fill
+     * the cache with a new dump.
+     *
+     * Then it compares the content with @platform and checks that they
+     * agree. This tests that @platform cache is consistent, as it was
+     * updated based on netlink events. */
+
+    g_assert(NM_IS_LINUX_PLATFORM(platform));
+
+    _LOGD("assert-platform: start");
+
+    nm_platform_process_events(platform);
+
+    platform2 = nm_linux_platform_new(share_multi_idx ? nm_platform_get_multi_idx(platform) : NULL,
+                                      TRUE,
+                                      nmtst_get_rand_bool(),
+                                      nmtst_get_rand_bool());
+    g_assert(NM_IS_LINUX_PLATFORM(platform2));
+
+    for (i_obj_types = 0; i_obj_types < (int) G_N_ELEMENTS(obj_types); i_obj_types++) {
+        const NMPObjectType          obj_type         = obj_types[i_obj_types];
+        const guint32                i_obj_type_flags = nmp_object_type_to_flags(obj_type);
+        gs_unref_ptrarray GPtrArray *arr1             = NULL;
+        gs_unref_ptrarray GPtrArray *arr2             = NULL;
+        NMPLookup                    lookup;
+        gboolean                     check_unordered = TRUE;
+        guint                        idx;
+        gboolean                     normalized;
+
+        if (!obj_type_flags_all) {
+            if (!NM_FLAGS_ANY(obj_type_flags, i_obj_type_flags))
+                continue;
+            obj_type_flags = NM_FLAGS_UNSET(obj_type_flags, i_obj_type_flags);
+        }
+
+        nmp_lookup_init_obj_type(&lookup, obj_type);
+
+        arr1 = nm_platform_lookup_clone(platform, &lookup, NULL, NULL) ?: g_ptr_array_new();
+        arr2 = nm_platform_lookup_clone(platform2, &lookup, NULL, NULL) ?: g_ptr_array_new();
+
+        normalized = _assert_platform_normalize_all(arr1);
+        normalized = _assert_platform_normalize_all(arr2);
+
+        if (check_unordered) {
+            /* We need to sort the two lists. */
+            g_ptr_array_sort(arr1, _assert_platform_sort_objs);
+            g_ptr_array_sort(arr2, _assert_platform_sort_objs);
+        }
+
+        if (!_assert_platform_compare_arr(obj_type,
+                                          "main",
+                                          arr1,
+                                          arr2,
+                                          normalized,
+                                          share_multi_idx,
+                                          do_assert))
+            return FALSE;
+
+        if (NM_IN_SET(obj_type, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE)) {
+            /* For routes, the WEAK_ID needs to be sorted and match the expected order. Check that. */
+            g_assert(!normalized);
+            for (idx = 0; idx < nm_g_ptr_array_len(arr1); idx++) {
+                const NMPObject             *obj1         = arr1->pdata[idx];
+                const NMPObject             *obj2         = arr2->pdata[idx];
+                gs_unref_ptrarray GPtrArray *arr1b        = NULL;
+                gs_unref_ptrarray GPtrArray *arr2b        = NULL;
+                gs_unref_ptrarray GPtrArray *arr1b_sorted = NULL;
+                gs_unref_ptrarray GPtrArray *arr2b_sorted = NULL;
+                guint                        found_obj1   = 0;
+                guint                        found_obj2   = 0;
+                guint                        i;
+
+                nmp_lookup_init_route_by_weak_id(&lookup, obj1);
+                arr1b =
+                    nm_platform_lookup_clone(platform, &lookup, NULL, NULL) ?: g_ptr_array_new();
+                g_assert_cmpint(arr1b->len, >, 0u);
+
+                nmp_lookup_init_route_by_weak_id(&lookup, obj2);
+                arr2b =
+                    nm_platform_lookup_clone(platform2, &lookup, NULL, NULL) ?: g_ptr_array_new();
+                g_assert_cmpint(arr2b->len, ==, arr1b->len);
+
+                /* First check that the lists agree, if we sort them. The list of
+                 * weak-ids was supposed to honor the sort order from `ip route show`,
+                 * but as that is not the case (see blow), first check whether at
+                 * least the same routes are in the list (with wrong sort order). */
+                arr1b_sorted = nm_g_ptr_array_new_clone(arr1b, NULL, NULL, NULL);
+                arr2b_sorted = nm_g_ptr_array_new_clone(arr2b, NULL, NULL, NULL);
+                g_ptr_array_sort(arr1b_sorted, _assert_platform_sort_objs);
+                g_ptr_array_sort(arr2b_sorted, _assert_platform_sort_objs);
+                if (!_assert_platform_compare_arr(obj_type,
+                                                  "weak-id-sorted",
+                                                  arr1b_sorted,
+                                                  arr2b_sorted,
+                                                  normalized,
+                                                  share_multi_idx,
+                                                  do_assert))
+                    return FALSE;
+
+                if (obj_type == NMP_OBJECT_TYPE_IP6_ROUTE) {
+                    /* For IPv6, the weak-ids are actually not sorted correctly.
+                     * This is because IPv6 multihop/ECMP routes get split into
+                     * multiple objects, and we don't get this right.
+                     *
+                     * This may be a bug. But we probably don't rely on this
+                     * anymore, because the weak-id were used to find which
+                     * route got replaced with `NLM_F_REPLACE`, but that anyway
+                     * doesn't work. We now always request a new dump. */
+                } else if (obj_type == NMP_OBJECT_TYPE_IP4_ROUTE) {
+                    /* For IPv4, it also does not reliably always work. This may
+                     * be a bug we want to fix. For now, ignore the check.
+                     *
+                     * a) Kernel can wrongly allow to configure the same route twice.
+                     * That means, the same route is visible in `ip route` output,
+                     * meaning, it would be added twice to the platform cache.
+                     * At least due to that problem, may the weak-id not be properly sorted.
+                     * See https://bugzilla.redhat.com/show_bug.cgi?id=2165720 which is
+                     * a bug of kernel allowing to configure the exact same route twice.
+                     *
+                     * b) See https://bugzilla.redhat.com/show_bug.cgi?id=2162315 which is
+                     * a bug where kernel does allow to configure single-hop routes that differ by
+                     * their next-hop weight, but on the netlink API those routes look the same.
+                     *
+                     * Due to a) and b), the platform cache may contain only one instance
+                     * of a route, which is visible more than once in `ip route` output.
+                     * This merging of different routes causes problems, and it also means
+                     * that the RTM_NEWROUTE events are wrongly interpreted and the weak-id
+                     * is not properly sorted.
+                     */
+                } else {
+                    /* Assert that also the original, not-sorted lists agree. */
+                    if (!_assert_platform_compare_arr(obj_type,
+                                                      "weak-id",
+                                                      arr1b,
+                                                      arr2b,
+                                                      normalized,
+                                                      share_multi_idx,
+                                                      do_assert))
+                        return FALSE;
+                }
+
+                for (i = 0; i < arr1b->len; i++) {
+                    if (arr1b->pdata[i] == obj1)
+                        found_obj1++;
+                    if (arr2b->pdata[i] == obj2)
+                        found_obj2++;
+                }
+
+                g_assert_cmpint(found_obj1, ==, 1u);
+                g_assert_cmpint(found_obj2, ==, 1u);
+            }
+        }
+    }
+
+    g_clear_object(&platform2);
+
+    _LOGD("assert-platform: done");
+
+    g_assert_cmpint(obj_type_flags, ==, 0u);
+
+    return TRUE;
+}
+
+void
+nmtstp_check_platform(NMPlatform *platform, guint32 obj_type_flags)
+{
+    if (!nmtstp_check_platform_full(platform, obj_type_flags, FALSE)) {
+        /* It's unclear why this failure sometimes happens. It happens
+         * on gitlab-ci on Ubuntu/Debian(??).
+         *
+         * Retrying shortly after seems to avoid it. */
+        g_usleep(20 * 1000);
+        nm_platform_process_events(platform);
+        nmtstp_run_command("ip route");
+        nm_platform_process_events(platform);
+
+        nmtstp_check_platform_full(platform, obj_type_flags, TRUE);
+    }
+}
+
+/*****************************************************************************/
+
 typedef struct {
     GMainLoop *loop;
     guint      signal_counts;
@@ -1036,7 +1725,7 @@ nmtstp_ip_address_assert_lifetime(const NMPlatformIPAddress *addr,
 
 static void
 _ip_address_add(NMPlatform     *platform,
-                gboolean        external_command,
+                int             external_command,
                 gboolean        is_v4,
                 int             ifindex,
                 const NMIPAddr *address,
@@ -1058,8 +1747,8 @@ _ip_address_add(NMPlatform     *platform,
         gs_free char *s_valid     = NULL;
         gs_free char *s_preferred = NULL;
         gs_free char *s_label     = NULL;
-        char          b1[NM_UTILS_INET_ADDRSTRLEN];
-        char          b2[NM_UTILS_INET_ADDRSTRLEN];
+        char          b1[NM_INET_ADDRSTRLEN];
+        char          b2[NM_INET_ADDRSTRLEN];
 
         ifname = nm_platform_link_get_name(platform, ifindex);
         g_assert(ifname);
@@ -1072,18 +1761,18 @@ _ip_address_add(NMPlatform     *platform,
             s_label = g_strdup_printf("%s:%s", ifname, label);
 
         if (is_v4) {
-            char s_peer[NM_UTILS_INET_ADDRSTRLEN + 50];
+            char s_peer[NM_INET_ADDRSTRLEN + 50];
 
             g_assert(flags == 0);
 
             if (peer_address->addr4 != address->addr4 || nmtst_get_rand_uint32() % 2) {
                 /* If the peer is the same as the local address, we can omit it. The result should be identical */
-                nm_sprintf_buf(s_peer, " peer %s", _nm_utils_inet4_ntop(peer_address->addr4, b2));
+                nm_sprintf_buf(s_peer, " peer %s", nm_inet4_ntop(peer_address->addr4, b2));
             } else
                 s_peer[0] = '\0';
 
             nmtstp_run_command_check("ip address change %s%s/%d dev %s%s%s%s",
-                                     _nm_utils_inet4_ntop(address->addr4, b1),
+                                     nm_inet4_ntop(address->addr4, b1),
                                      s_peer,
                                      plen,
                                      ifname,
@@ -1096,10 +1785,10 @@ _ip_address_add(NMPlatform     *platform,
             /* flags not implemented (yet) */
             g_assert(flags == 0);
             nmtstp_run_command_check("ip address change %s%s%s/%d dev %s%s%s%s",
-                                     _nm_utils_inet6_ntop(&address->addr6, b1),
+                                     nm_inet6_ntop(&address->addr6, b1),
                                      !IN6_IS_ADDR_UNSPECIFIED(&peer_address->addr6) ? " peer " : "",
                                      !IN6_IS_ADDR_UNSPECIFIED(&peer_address->addr6)
-                                         ? _nm_utils_inet6_ntop(&peer_address->addr6, b2)
+                                         ? nm_inet6_ntop(&peer_address->addr6, b2)
                                          : "",
                                      plen,
                                      ifname,
@@ -1188,7 +1877,7 @@ _ip_address_add(NMPlatform     *platform,
 
 void
 nmtstp_ip4_address_add(NMPlatform *platform,
-                       gboolean    external_command,
+                       int         external_command,
                        int         ifindex,
                        in_addr_t   address,
                        int         plen,
@@ -1213,7 +1902,7 @@ nmtstp_ip4_address_add(NMPlatform *platform,
 
 void
 nmtstp_ip6_address_add(NMPlatform     *platform,
-                       gboolean        external_command,
+                       int             external_command,
                        int             ifindex,
                        struct in6_addr address,
                        int             plen,
@@ -1257,8 +1946,8 @@ nmtstp_ip4_route_add(NMPlatform      *platform,
     route.metric    = metric;
     route.mss       = mss;
 
-    g_assert(
-        NMTST_NM_ERR_SUCCESS(nm_platform_ip4_route_add(platform, NMP_NLM_FLAG_REPLACE, &route)));
+    g_assert(NMTST_NM_ERR_SUCCESS(
+        nm_platform_ip4_route_add(platform, NMP_NLM_FLAG_REPLACE, &route, NULL)));
 }
 
 void
@@ -1291,7 +1980,7 @@ nmtstp_ip6_route_add(NMPlatform      *platform,
 
 static void
 _ip_address_del(NMPlatform     *platform,
-                gboolean        external_command,
+                int             external_command,
                 gboolean        is_v4,
                 int             ifindex,
                 const NMIPAddr *address,
@@ -1306,8 +1995,8 @@ _ip_address_del(NMPlatform     *platform,
 
     if (external_command) {
         const char *ifname;
-        char        b1[NM_UTILS_INET_ADDRSTRLEN];
-        char        b2[NM_UTILS_INET_ADDRSTRLEN];
+        char        b1[NM_INET_ADDRSTRLEN];
+        char        b2[NM_INET_ADDRSTRLEN];
         int         success;
         gboolean    had_address;
 
@@ -1325,18 +2014,17 @@ _ip_address_del(NMPlatform     *platform,
             had_address = !!nm_platform_ip6_address_get(platform, ifindex, &address->addr6);
 
         if (is_v4) {
-            success = nmtstp_run_command("ip address delete %s%s%s/%d dev %s",
-                                         _nm_utils_inet4_ntop(address->addr4, b1),
-                                         peer_address->addr4 != address->addr4 ? " peer " : "",
-                                         peer_address->addr4 != address->addr4
-                                             ? _nm_utils_inet4_ntop(peer_address->addr4, b2)
-                                             : "",
-                                         plen,
-                                         ifname);
+            success = nmtstp_run_command(
+                "ip address delete %s%s%s/%d dev %s",
+                nm_inet4_ntop(address->addr4, b1),
+                peer_address->addr4 != address->addr4 ? " peer " : "",
+                peer_address->addr4 != address->addr4 ? nm_inet4_ntop(peer_address->addr4, b2) : "",
+                plen,
+                ifname);
         } else {
             g_assert(!peer_address);
             success = nmtstp_run_command("ip address delete %s/%d dev %s",
-                                         _nm_utils_inet6_ntop(&address->addr6, b1),
+                                         nm_inet6_ntop(&address->addr6, b1),
                                          plen,
                                          ifname);
         }
@@ -1391,7 +2079,7 @@ _ip_address_del(NMPlatform     *platform,
 
 void
 nmtstp_ip4_address_del(NMPlatform *platform,
-                       gboolean    external_command,
+                       int         external_command,
                        int         ifindex,
                        in_addr_t   address,
                        int         plen,
@@ -1408,7 +2096,7 @@ nmtstp_ip4_address_del(NMPlatform *platform,
 
 void
 nmtstp_ip6_address_del(NMPlatform     *platform,
-                       gboolean        external_command,
+                       int             external_command,
                        int             ifindex,
                        struct in6_addr address,
                        int             plen)
@@ -1478,7 +2166,7 @@ nmtstp_link_bridge_normalize_jiffies_time(const NMPlatformLnkBridge *requested,
 
 const NMPlatformLink *
 nmtstp_link_bridge_add(NMPlatform                *platform,
-                       gboolean                   external_command,
+                       int                        external_command,
                        const char                *name,
                        const NMPlatformLnkBridge *lnk)
 {
@@ -1633,10 +2321,7 @@ nmtstp_link_bridge_add(NMPlatform                *platform,
     return pllink;
 }
 const NMPlatformLink *
-nmtstp_link_veth_add(NMPlatform *platform,
-                     gboolean    external_command,
-                     const char *name,
-                     const char *peer)
+nmtstp_link_veth_add(NMPlatform *platform, int external_command, const char *name, const char *peer)
 {
     const NMPlatformLink *pllink  = NULL;
     gboolean              success = FALSE;
@@ -1681,7 +2366,7 @@ again:
 }
 
 const NMPlatformLink *
-nmtstp_link_dummy_add(NMPlatform *platform, gboolean external_command, const char *name)
+nmtstp_link_dummy_add(NMPlatform *platform, int external_command, const char *name)
 {
     const NMPlatformLink *pllink = NULL;
     gboolean              success;
@@ -1706,7 +2391,7 @@ nmtstp_link_dummy_add(NMPlatform *platform, gboolean external_command, const cha
 
 const NMPlatformLink *
 nmtstp_link_gre_add(NMPlatform             *platform,
-                    gboolean                external_command,
+                    int                     external_command,
                     const char             *name,
                     const NMPlatformLnkGre *lnk)
 {
@@ -1734,13 +2419,13 @@ nmtstp_link_gre_add(NMPlatform             *platform,
         obj  = lnk->is_tap ? "link" : "tunnel";
         type = lnk->is_tap ? "type gretap" : "mode gre";
 
-        success = !nmtstp_run_command("ip %s add %s %s %s local %s remote %s ttl %u tos %02x %s",
+        success = !nmtstp_run_command("ip %s add %s %s%s%s local %s remote %s ttl %u tos %02x %s",
                                       obj,
                                       name,
                                       type,
-                                      dev ?: "",
-                                      _nm_utils_inet4_ntop(lnk->local, b1),
-                                      _nm_utils_inet4_ntop(lnk->remote, b2),
+                                      NM_PRINT_FMT_QUOTED2(dev, " ", dev, ""),
+                                      nm_inet4_ntop(lnk->local, b1),
+                                      nm_inet4_ntop(lnk->remote, b2),
                                       lnk->ttl,
                                       lnk->tos,
                                       lnk->path_mtu_discovery ? "pmtudisc" : "nopmtudisc");
@@ -1757,14 +2442,14 @@ nmtstp_link_gre_add(NMPlatform             *platform,
 
 const NMPlatformLink *
 nmtstp_link_ip6tnl_add(NMPlatform                *platform,
-                       gboolean                   external_command,
+                       int                        external_command,
                        const char                *name,
                        const NMPlatformLnkIp6Tnl *lnk)
 {
     const NMPlatformLink *pllink = NULL;
     gboolean              success;
-    char                  b1[NM_UTILS_INET_ADDRSTRLEN];
-    char                  b2[NM_UTILS_INET_ADDRSTRLEN];
+    char                  b1[NM_INET_ADDRSTRLEN];
+    char                  b2[NM_INET_ADDRSTRLEN];
     char                  encap[20];
     char                  tclass[20];
     gboolean              encap_ignore;
@@ -1800,13 +2485,13 @@ nmtstp_link_ip6tnl_add(NMPlatform                *platform,
         tclass_inherit = NM_FLAGS_HAS(lnk->flags, IP6_TNL_F_USE_ORIG_TCLASS);
 
         success = !nmtstp_run_command(
-            "ip -6 tunnel add %s mode %s %s local %s remote %s ttl %u tclass %s encaplimit %s "
+            "ip -6 tunnel add %s mode %s%s%s local %s remote %s ttl %u tclass %s encaplimit %s "
             "flowlabel %x",
             name,
             mode,
-            dev,
-            _nm_utils_inet6_ntop(&lnk->local, b1),
-            _nm_utils_inet6_ntop(&lnk->remote, b2),
+            NM_PRINT_FMT_QUOTED2(dev, " ", dev, ""),
+            nm_inet6_ntop(&lnk->local, b1),
+            nm_inet6_ntop(&lnk->remote, b2),
             lnk->ttl,
             tclass_inherit ? "inherit" : nm_sprintf_buf(tclass, "%02x", lnk->tclass),
             encap_ignore ? "none" : nm_sprintf_buf(encap, "%u", lnk->encap_limit),
@@ -1823,14 +2508,14 @@ nmtstp_link_ip6tnl_add(NMPlatform                *platform,
 
 const NMPlatformLink *
 nmtstp_link_ip6gre_add(NMPlatform                *platform,
-                       gboolean                   external_command,
+                       int                        external_command,
                        const char                *name,
                        const NMPlatformLnkIp6Tnl *lnk)
 {
     const NMPlatformLink *pllink = NULL;
     gboolean              success;
-    char                  b1[NM_UTILS_INET_ADDRSTRLEN];
-    char                  b2[NM_UTILS_INET_ADDRSTRLEN];
+    char                  b1[NM_INET_ADDRSTRLEN];
+    char                  b2[NM_INET_ADDRSTRLEN];
     char                  tclass[20];
     gboolean              tclass_inherit;
 
@@ -1851,12 +2536,12 @@ nmtstp_link_ip6gre_add(NMPlatform                *platform,
         tclass_inherit = NM_FLAGS_HAS(lnk->flags, IP6_TNL_F_USE_ORIG_TCLASS);
 
         success = !nmtstp_run_command(
-            "ip link add %s type %s %s local %s remote %s ttl %u tclass %s flowlabel %x",
+            "ip link add %s type %s%s%s local %s remote %s ttl %u tclass %s flowlabel %x",
             name,
             lnk->is_tap ? "ip6gretap" : "ip6gre",
-            dev,
-            _nm_utils_inet6_ntop(&lnk->local, b1),
-            _nm_utils_inet6_ntop(&lnk->remote, b2),
+            NM_PRINT_FMT_QUOTED2(dev, " ", dev, ""),
+            nm_inet6_ntop(&lnk->local, b1),
+            nm_inet6_ntop(&lnk->remote, b2),
             lnk->ttl,
             tclass_inherit ? "inherit" : nm_sprintf_buf(tclass, "%02x", lnk->tclass),
             lnk->flow_label);
@@ -1882,7 +2567,7 @@ nmtstp_link_ip6gre_add(NMPlatform                *platform,
 
 const NMPlatformLink *
 nmtstp_link_ipip_add(NMPlatform              *platform,
-                     gboolean                 external_command,
+                     int                      external_command,
                      const char              *name,
                      const NMPlatformLnkIpIp *lnk)
 {
@@ -1905,11 +2590,11 @@ nmtstp_link_ipip_add(NMPlatform              *platform,
                 g_strdup_printf("dev %s", nm_platform_link_get_name(platform, lnk->parent_ifindex));
 
         success = !nmtstp_run_command(
-            "ip tunnel add %s mode ipip %s local %s remote %s ttl %u tos %02x %s",
+            "ip tunnel add %s mode ipip%s%s local %s remote %s ttl %u tos %02x %s",
             name,
-            dev,
-            _nm_utils_inet4_ntop(lnk->local, b1),
-            _nm_utils_inet4_ntop(lnk->remote, b2),
+            NM_PRINT_FMT_QUOTED2(dev, " ", dev, ""),
+            nm_inet4_ntop(lnk->local, b1),
+            nm_inet4_ntop(lnk->remote, b2),
             lnk->ttl,
             lnk->tos,
             lnk->path_mtu_discovery ? "pmtudisc" : "nopmtudisc");
@@ -1925,7 +2610,7 @@ nmtstp_link_ipip_add(NMPlatform              *platform,
 
 const NMPlatformLink *
 nmtstp_link_macvlan_add(NMPlatform                 *platform,
-                        gboolean                    external_command,
+                        int                         external_command,
                         const char                 *name,
                         int                         parent,
                         const NMPlatformLnkMacvlan *lnk)
@@ -1945,10 +2630,10 @@ nmtstp_link_macvlan_add(NMPlatform                 *platform,
     if (external_command) {
         const char *dev;
         char       *modes[] = {
-                  [MACVLAN_MODE_BRIDGE]   = "bridge",
-                  [MACVLAN_MODE_VEPA]     = "vepa",
-                  [MACVLAN_MODE_PRIVATE]  = "private",
-                  [MACVLAN_MODE_PASSTHRU] = "passthru",
+            [MACVLAN_MODE_BRIDGE]   = "bridge",
+            [MACVLAN_MODE_VEPA]     = "vepa",
+            [MACVLAN_MODE_PRIVATE]  = "private",
+            [MACVLAN_MODE_PASSTHRU] = "passthru",
         };
 
         dev = nm_platform_link_get_name(platform, parent);
@@ -1974,7 +2659,7 @@ nmtstp_link_macvlan_add(NMPlatform                 *platform,
 
 const NMPlatformLink *
 nmtstp_link_sit_add(NMPlatform             *platform,
-                    gboolean                external_command,
+                    int                     external_command,
                     const char             *name,
                     const NMPlatformLnkSit *lnk)
 {
@@ -2004,8 +2689,8 @@ nmtstp_link_sit_add(NMPlatform             *platform,
             !nmtstp_run_command("ip tunnel add %s mode sit%s local %s remote %s ttl %u tos %02x %s",
                                 name,
                                 dev,
-                                _nm_utils_inet4_ntop(lnk->local, b1),
-                                _nm_utils_inet4_ntop(lnk->remote, b2),
+                                nm_inet4_ntop(lnk->local, b1),
+                                nm_inet4_ntop(lnk->remote, b2),
                                 lnk->ttl,
                                 lnk->tos,
                                 lnk->path_mtu_discovery ? "pmtudisc" : "nopmtudisc");
@@ -2021,7 +2706,7 @@ nmtstp_link_sit_add(NMPlatform             *platform,
 
 const NMPlatformLink *
 nmtstp_link_tun_add(NMPlatform             *platform,
-                    gboolean                external_command,
+                    int                     external_command,
                     const char             *name,
                     const NMPlatformLnkTun *lnk,
                     int                    *out_fd)
@@ -2085,9 +2770,135 @@ nmtstp_link_tun_add(NMPlatform             *platform,
 }
 
 const NMPlatformLink *
-nmtstp_link_vrf_add(NMPlatform             *platform,
+nmtstp_link_vlan_add(NMPlatform              *platform,
+                     int                      external_command,
+                     const char              *name,
+                     int                      parent,
+                     const NMPlatformLnkVlan *lnk)
+{
+    const NMPlatformLink *pllink = NULL;
+    gboolean              success;
+
+    g_assert(nm_utils_ifname_valid_kernel(name, NULL));
+
+    external_command = nmtstp_run_command_check_external(external_command);
+
+    _init_platform(&platform, external_command);
+
+    if (external_command) {
+        const char *dev;
+
+        dev = nm_platform_link_get_name(platform, parent);
+        g_assert(dev);
+        g_assert(NM_IN_SET(lnk->protocol, ETH_P_8021Q, ETH_P_8021AD));
+
+        success = !nmtstp_run_command(
+            "ip link add name %s link %s type vlan id %hu protocol %s%s%s%s%s",
+            name,
+            dev,
+            lnk->id,
+            lnk->protocol == ETH_P_8021Q ? "802.1Q" : "802.1ad",
+            !(lnk->flags & _NM_VLAN_FLAG_REORDER_HEADERS) ? " reorder_hdr off" : "",
+            (lnk->flags & _NM_VLAN_FLAG_GVRP) ? " gvrp on" : "",
+            (lnk->flags & _NM_VLAN_FLAG_MVRP) ? " mvrp on" : "",
+            (lnk->flags & _NM_VLAN_FLAG_LOOSE_BINDING) ? " loose_binding on" : "");
+        if (success)
+            pllink = nmtstp_assert_wait_for_link(platform, name, NM_LINK_TYPE_VLAN, 100);
+    } else
+        success =
+            NMTST_NM_ERR_SUCCESS(nm_platform_link_vlan_add(platform, name, parent, lnk, &pllink));
+
+    _assert_pllink(platform, success, pllink, name, NM_LINK_TYPE_VLAN);
+
+    return pllink;
+}
+
+const NMPlatformLink *
+nmtstp_link_vti_add(NMPlatform             *platform,
                     gboolean                external_command,
                     const char             *name,
+                    const NMPlatformLnkVti *lnk)
+{
+    const NMPlatformLink *pllink = NULL;
+    gboolean              success;
+    char                  b1[INET_ADDRSTRLEN];
+    char                  b2[INET_ADDRSTRLEN];
+
+    g_assert(nm_utils_ifname_valid_kernel(name, NULL));
+    external_command = nmtstp_run_command_check_external(external_command);
+    _init_platform(&platform, external_command);
+    g_assert(lnk->fwmark == 0);
+
+    if (external_command) {
+        gs_free char *dev = NULL;
+
+        if (lnk->parent_ifindex)
+            dev =
+                g_strdup_printf("dev %s", nm_platform_link_get_name(platform, lnk->parent_ifindex));
+
+        success =
+            !nmtstp_run_command("ip link add %s type vti %s local %s remote %s ikey %u okey %u",
+                                name,
+                                dev ?: "",
+                                nm_inet4_ntop(lnk->local, b1),
+                                nm_inet4_ntop(lnk->remote, b2),
+                                lnk->ikey,
+                                lnk->okey);
+        if (success)
+            pllink = nmtstp_assert_wait_for_link(platform, name, NM_LINK_TYPE_VTI, 100);
+    } else
+        success = NMTST_NM_ERR_SUCCESS(nm_platform_link_vti_add(platform, name, lnk, &pllink));
+
+    _assert_pllink(platform, success, pllink, name, NM_LINK_TYPE_VTI);
+
+    return pllink;
+}
+
+const NMPlatformLink *
+nmtstp_link_vti6_add(NMPlatform              *platform,
+                     gboolean                 external_command,
+                     const char              *name,
+                     const NMPlatformLnkVti6 *lnk)
+{
+    const NMPlatformLink *pllink = NULL;
+    gboolean              success;
+    char                  b1[INET6_ADDRSTRLEN];
+    char                  b2[INET6_ADDRSTRLEN];
+
+    g_assert(nm_utils_ifname_valid_kernel(name, NULL));
+    external_command = nmtstp_run_command_check_external(external_command);
+    _init_platform(&platform, external_command);
+    g_assert(lnk->fwmark == 0);
+
+    if (external_command) {
+        gs_free char *dev = NULL;
+
+        if (lnk->parent_ifindex)
+            dev =
+                g_strdup_printf("dev %s", nm_platform_link_get_name(platform, lnk->parent_ifindex));
+
+        success =
+            !nmtstp_run_command("ip link add %s type vti6 %s local %s remote %s ikey %u okey %u",
+                                name,
+                                dev ?: "",
+                                nm_inet6_ntop(&lnk->local, b1),
+                                nm_inet6_ntop(&lnk->remote, b2),
+                                lnk->ikey,
+                                lnk->okey);
+        if (success)
+            pllink = nmtstp_assert_wait_for_link(platform, name, NM_LINK_TYPE_VTI6, 100);
+    } else
+        success = NMTST_NM_ERR_SUCCESS(nm_platform_link_vti6_add(platform, name, lnk, &pllink));
+
+    _assert_pllink(platform, success, pllink, name, NM_LINK_TYPE_VTI6);
+
+    return pllink;
+}
+
+const NMPlatformLink *
+nmtstp_link_vrf_add(NMPlatform             *platform,
+                    int                     external_command,
+                    const char             *name,
                     const NMPlatformLnkVrf *lnk,
                     gboolean               *out_not_supported)
 {
@@ -2123,7 +2934,7 @@ nmtstp_link_vrf_add(NMPlatform             *platform,
 
 const NMPlatformLink *
 nmtstp_link_vxlan_add(NMPlatform               *platform,
-                      gboolean                  external_command,
+                      int                       external_command,
                       const char               *name,
                       const NMPlatformLnkVxlan *lnk)
 {
@@ -2139,24 +2950,24 @@ nmtstp_link_vxlan_add(NMPlatform               *platform,
 
     if (external_command) {
         gs_free char *dev = NULL;
-        char          local[NM_UTILS_INET_ADDRSTRLEN];
-        char          group[NM_UTILS_INET_ADDRSTRLEN];
+        char          local[NM_INET_ADDRSTRLEN];
+        char          group[NM_INET_ADDRSTRLEN];
 
         if (lnk->parent_ifindex)
             dev =
                 g_strdup_printf("dev %s", nm_platform_link_get_name(platform, lnk->parent_ifindex));
 
         if (lnk->local)
-            _nm_utils_inet4_ntop(lnk->local, local);
+            nm_inet4_ntop(lnk->local, local);
         else if (memcmp(&lnk->local6, &in6addr_any, sizeof(in6addr_any)))
-            _nm_utils_inet6_ntop(&lnk->local6, local);
+            nm_inet6_ntop(&lnk->local6, local);
         else
             local[0] = '\0';
 
         if (lnk->group)
-            _nm_utils_inet4_ntop(lnk->group, group);
+            nm_inet4_ntop(lnk->group, group);
         else if (memcmp(&lnk->group6, &in6addr_any, sizeof(in6addr_any)))
-            _nm_utils_inet6_ntop(&lnk->group6, group);
+            nm_inet6_ntop(&lnk->group6, group);
         else
             group[0] = '\0';
 
@@ -2239,7 +3050,7 @@ nmtstp_link_get(NMPlatform *platform, int ifindex, const char *name)
 
 void
 nmtstp_link_delete(NMPlatform *platform,
-                   gboolean    external_command,
+                   int         external_command,
                    int         ifindex,
                    const char *name,
                    gboolean    require_exist)
@@ -2291,7 +3102,7 @@ nmtstp_link_delete(NMPlatform *platform,
 /*****************************************************************************/
 
 void
-nmtstp_link_set_updown(NMPlatform *platform, gboolean external_command, int ifindex, gboolean up)
+nmtstp_link_set_updown(NMPlatform *platform, int external_command, int ifindex, gboolean up)
 {
     const NMPlatformLink *plink;
     gint64                end_time;
@@ -2739,7 +3550,7 @@ nmtstp_acd_defender_new(int ifindex, in_addr_t ip_addr, const NMEtherAddr *mac_a
     NAcdProbe                                         *probe        = NULL;
     int                                                fd;
     int                                                r;
-    char                                               sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char                                               sbuf_addr[NM_INET_ADDRSTRLEN];
 
     g_assert_cmpint(ifindex, >, 0);
     g_assert(mac_addr);
@@ -2780,7 +3591,7 @@ nmtstp_acd_defender_new(int ifindex, in_addr_t ip_addr, const NMEtherAddr *mac_a
           NM_HASH_OBFUSCATE_PTR(defender),
           ifindex,
           NM_ETHER_ADDR_FORMAT_VAL(mac_addr),
-          _nm_utils_inet4_ntop(ip_addr, sbuf_addr));
+          nm_inet4_ntop(ip_addr, sbuf_addr));
 
     n_acd_probe_set_userdata(defender->probe, defender);
 
diff --git a/src/core/platform/tests/test-common.h b/src/core/platform/tests/test-common.h
index da30b0ed..2802d8fd 100644
--- a/src/core/platform/tests/test-common.h
+++ b/src/core/platform/tests/test-common.h
@@ -18,8 +18,6 @@
 
 #include "nm-test-utils-core.h"
 
-#define DEVICE_NAME "nm-test-device"
-
 /*****************************************************************************/
 
 #define nmtstp_normalize_jiffies_time(requested_value, kernel_value)                             \
@@ -141,6 +139,13 @@ int nmtstp_run_command(const char *format, ...) _nm_printf(1, 2);
 
 /*****************************************************************************/
 
+gboolean
+nmtstp_check_platform_full(NMPlatform *platform, guint32 obj_type_flags, gboolean do_assert);
+
+void nmtstp_check_platform(NMPlatform *platform, guint32 obj_type_flags);
+
+/*****************************************************************************/
+
 guint                 nmtstp_wait_for_signal(NMPlatform *platform, gint64 timeout_msec);
 guint                 nmtstp_wait_for_signal_until(NMPlatform *platform, gint64 until_ms);
 const NMPlatformLink *nmtstp_wait_for_link(NMPlatform *platform,
@@ -261,7 +266,7 @@ void     nmtstp_ip_address_assert_lifetime(const NMPlatformIPAddress *addr,
                                            guint32                    expected_preferred);
 
 void nmtstp_ip4_address_add(NMPlatform *platform,
-                            gboolean    external_command,
+                            int         external_command,
                             int         ifindex,
                             in_addr_t   address,
                             int         plen,
@@ -271,7 +276,7 @@ void nmtstp_ip4_address_add(NMPlatform *platform,
                             guint32     flags,
                             const char *label);
 void nmtstp_ip6_address_add(NMPlatform     *platform,
-                            gboolean        external_command,
+                            int             external_command,
                             int             ifindex,
                             struct in6_addr address,
                             int             plen,
@@ -280,13 +285,13 @@ void nmtstp_ip6_address_add(NMPlatform     *platform,
                             guint32         preferred,
                             guint32         flags);
 void nmtstp_ip4_address_del(NMPlatform *platform,
-                            gboolean    external_command,
+                            int         external_command,
                             int         ifindex,
                             in_addr_t   address,
                             int         plen,
                             in_addr_t   peer_address);
 void nmtstp_ip6_address_del(NMPlatform     *platform,
-                            gboolean        external_command,
+                            int             external_command,
                             int             ifindex,
                             struct in6_addr address,
                             int             plen);
@@ -452,8 +457,7 @@ const NMPlatformLink *nmtstp_link_get(NMPlatform *platform, int ifindex, const c
 
 gboolean nmtstp_kernel_support_get(NMPlatformKernelSupportType type);
 
-void
-nmtstp_link_set_updown(NMPlatform *platform, gboolean external_command, int ifindex, gboolean up);
+void nmtstp_link_set_updown(NMPlatform *platform, int external_command, int ifindex, gboolean up);
 
 const NMPlatformLnkBridge *
 nmtstp_link_bridge_normalize_jiffies_time(const NMPlatformLnkBridge *requested,
@@ -461,61 +465,78 @@ nmtstp_link_bridge_normalize_jiffies_time(const NMPlatformLnkBridge *requested,
                                           NMPlatformLnkBridge       *dst);
 
 const NMPlatformLink *nmtstp_link_bridge_add(NMPlatform                *platform,
-                                             gboolean                   external_command,
+                                             int                        external_command,
                                              const char                *name,
                                              const NMPlatformLnkBridge *lnk);
 const NMPlatformLink *nmtstp_link_veth_add(NMPlatform *platform,
-                                           gboolean    external_command,
+                                           int         external_command,
                                            const char *name,
                                            const char *peer);
 const NMPlatformLink *
-nmtstp_link_dummy_add(NMPlatform *platform, gboolean external_command, const char *name);
+nmtstp_link_dummy_add(NMPlatform *platform, int external_command, const char *name);
 const NMPlatformLink *nmtstp_link_gre_add(NMPlatform             *platform,
-                                          gboolean                external_command,
+                                          int                     external_command,
                                           const char             *name,
                                           const NMPlatformLnkGre *lnk);
 const NMPlatformLink *nmtstp_link_ip6tnl_add(NMPlatform                *platform,
-                                             gboolean                   external_command,
+                                             int                        external_command,
                                              const char                *name,
                                              const NMPlatformLnkIp6Tnl *lnk);
 const NMPlatformLink *nmtstp_link_ip6gre_add(NMPlatform                *platform,
-                                             gboolean                   external_command,
+                                             int                        external_command,
                                              const char                *name,
                                              const NMPlatformLnkIp6Tnl *lnk);
 const NMPlatformLink *nmtstp_link_ipip_add(NMPlatform              *platform,
-                                           gboolean                 external_command,
+                                           int                      external_command,
                                            const char              *name,
                                            const NMPlatformLnkIpIp *lnk);
 const NMPlatformLink *nmtstp_link_macvlan_add(NMPlatform                 *platform,
-                                              gboolean                    external_command,
+                                              int                         external_command,
                                               const char                 *name,
                                               int                         parent,
                                               const NMPlatformLnkMacvlan *lnk);
 const NMPlatformLink *nmtstp_link_sit_add(NMPlatform             *platform,
-                                          gboolean                external_command,
+                                          int                     external_command,
                                           const char             *name,
                                           const NMPlatformLnkSit *lnk);
 const NMPlatformLink *nmtstp_link_tun_add(NMPlatform             *platform,
-                                          gboolean                external_command,
+                                          int                     external_command,
                                           const char             *name,
                                           const NMPlatformLnkTun *lnk,
                                           int                    *out_fd);
-const NMPlatformLink *nmtstp_link_vrf_add(NMPlatform             *platform,
+const NMPlatformLink *nmtstp_link_vlan_add(NMPlatform              *platform,
+                                           int                      external_command,
+                                           const char              *name,
+                                           int                      parent,
+                                           const NMPlatformLnkVlan *lnk);
+const NMPlatformLink *nmtstp_link_vti_add(NMPlatform             *platform,
                                           gboolean                external_command,
                                           const char             *name,
+                                          const NMPlatformLnkVti *lnk);
+const NMPlatformLink *nmtstp_link_vti6_add(NMPlatform              *platform,
+                                           gboolean                 external_command,
+                                           const char              *name,
+                                           const NMPlatformLnkVti6 *lnk);
+const NMPlatformLink *nmtstp_link_vrf_add(NMPlatform             *platform,
+                                          int                     external_command,
+                                          const char             *name,
                                           const NMPlatformLnkVrf *lnk,
                                           gboolean               *out_not_supported);
 const NMPlatformLink *nmtstp_link_vxlan_add(NMPlatform               *platform,
-                                            gboolean                  external_command,
+                                            int                       external_command,
                                             const char               *name,
                                             const NMPlatformLnkVxlan *lnk);
 
 void nmtstp_link_delete(NMPlatform *platform,
-                        gboolean    external_command,
+                        int         external_command,
                         int         ifindex,
                         const char *name,
                         gboolean    require_exist);
 
+gboolean nmtstp_link_is_iptunnel_special(const NMPlatformLink *link);
+
+gboolean nmtstp_ensure_module(const char *module_name);
+
 /*****************************************************************************/
 
 #define nmtst_object_new_mptcp_addr(...) \
@@ -523,36 +544,47 @@ void nmtstp_link_delete(NMPlatform *platform,
 
 /*****************************************************************************/
 
-extern int NMTSTP_ENV1_IFINDEX;
+extern int               NMTSTP_ENV1_IFINDEXES[2];
+extern const char *const NMTSTP_ENV1_DEVICE_NAME[G_N_ELEMENTS(NMTSTP_ENV1_IFINDEXES)];
+
+#define DEVICE_NAME         "nm-test-device0"
+#define NMTSTP_ENV1_IFINDEX (NMTSTP_ENV1_IFINDEXES[0])
+
 extern int NMTSTP_ENV1_EX;
 
 static inline void
 _nmtstp_env1_wrapper_setup(const NmtstTestData *test_data)
 {
-    int     *p_ifindex;
+    int     *p_ifindexes;
+    gpointer p_n_ifaces;
     gpointer p_ifup;
-
-    nmtst_test_data_unpack(test_data, &p_ifindex, NULL, NULL, NULL, &p_ifup);
-
-    g_assert(p_ifindex && *p_ifindex == -1);
+    int      n_ifaces;
+    int      i;
 
     _LOGT("TEST[%s]: setup", test_data->testpath);
 
-    nmtstp_link_delete(NM_PLATFORM_GET, -1, -1, DEVICE_NAME, FALSE);
+    nmtst_test_data_unpack(test_data, &p_ifindexes, &p_n_ifaces, NULL, NULL, NULL, &p_ifup);
 
-    g_assert(NMTST_NM_ERR_SUCCESS(nm_platform_link_dummy_add(NM_PLATFORM_GET, DEVICE_NAME, NULL)));
+    n_ifaces = GPOINTER_TO_UINT(p_n_ifaces);
+    g_assert_cmpint(n_ifaces, >=, 1);
+    g_assert_cmpint(n_ifaces, <=, (int) G_N_ELEMENTS(NMTSTP_ENV1_IFINDEXES));
 
-    *p_ifindex = nm_platform_link_get_ifindex(NM_PLATFORM_GET, DEVICE_NAME);
-    g_assert_cmpint(*p_ifindex, >, 0);
-    g_assert_cmpint(NMTSTP_ENV1_IFINDEX, ==, -1);
+    for (i = 0; i < (int) G_N_ELEMENTS(NMTSTP_ENV1_IFINDEXES); i++) {
+        g_assert_cmpint(NMTSTP_ENV1_IFINDEXES[i], ==, 0);
+        g_assert_cmpint(p_ifindexes[i], ==, 0);
+    }
 
-    if (GPOINTER_TO_INT(p_ifup))
-        g_assert(nm_platform_link_change_flags(NM_PLATFORM_GET, *p_ifindex, IFF_UP, TRUE) >= 0);
+    for (i = 0; i < n_ifaces; i++) {
+        p_ifindexes[i] = nmtstp_link_dummy_add(NULL, -1, NMTSTP_ENV1_DEVICE_NAME[i])->ifindex;
+        if (GPOINTER_TO_INT(p_ifup))
+            nmtstp_link_set_updown(NULL, -1, p_ifindexes[i], TRUE);
+    }
 
     nm_platform_process_events(NM_PLATFORM_GET);
 
-    NMTSTP_ENV1_IFINDEX = *p_ifindex;
-    NMTSTP_ENV1_EX      = nmtstp_run_command_check_external_global();
+    for (i = 0; i < n_ifaces; i++)
+        NMTSTP_ENV1_IFINDEXES[i] = p_ifindexes[i];
+    NMTSTP_ENV1_EX = nmtstp_run_command_check_external_global();
 }
 
 static inline void
@@ -563,7 +595,7 @@ _nmtstp_env1_wrapper_run(gconstpointer user_data)
     GTestFunc            test_func;
     gconstpointer        d;
 
-    nmtst_test_data_unpack(test_data, NULL, &test_func, &test_func_data, &d, NULL);
+    nmtst_test_data_unpack(test_data, NULL, NULL, &test_func, &test_func_data, &d, NULL);
 
     _LOGT("TEST[%s]: run", test_data->testpath);
     if (test_func)
@@ -575,55 +607,73 @@ _nmtstp_env1_wrapper_run(gconstpointer user_data)
 static inline void
 _nmtstp_env1_wrapper_teardown(const NmtstTestData *test_data)
 {
-    int *p_ifindex;
+    int     *p_ifindexes;
+    gpointer p_n_ifaces;
+    int      n_ifaces;
+    int      i;
 
-    nmtst_test_data_unpack(test_data, &p_ifindex, NULL, NULL, NULL, NULL);
+    _LOGT("TEST[%s]: teardown", test_data->testpath);
 
-    g_assert_cmpint(NMTSTP_ENV1_IFINDEX, ==, *p_ifindex);
-    NMTSTP_ENV1_IFINDEX = -1;
+    nmtst_test_data_unpack(test_data, &p_ifindexes, &p_n_ifaces, NULL, NULL, NULL, NULL);
 
-    _LOGT("TEST[%s]: teardown", test_data->testpath);
+    n_ifaces = GPOINTER_TO_UINT(p_n_ifaces);
+    g_assert_cmpint(n_ifaces, >=, 1);
+    g_assert_cmpint(n_ifaces, <=, (int) G_N_ELEMENTS(NMTSTP_ENV1_IFINDEXES));
+
+    for (i = 0; i < (int) G_N_ELEMENTS(NMTSTP_ENV1_IFINDEXES); i++) {
+        if (i < n_ifaces)
+            g_assert_cmpint(p_ifindexes[i], >, 0);
+        else
+            g_assert_cmpint(p_ifindexes[i], ==, 0);
+        g_assert_cmpint(NMTSTP_ENV1_IFINDEXES[i], ==, p_ifindexes[i]);
+        NMTSTP_ENV1_IFINDEXES[i] = 0;
+    }
 
-    g_assert_cmpint(*p_ifindex, ==, nm_platform_link_get_ifindex(NM_PLATFORM_GET, DEVICE_NAME));
-    g_assert(nm_platform_link_delete(NM_PLATFORM_GET, *p_ifindex));
+    for (i = 0; i < n_ifaces; i++)
+        nmtstp_link_delete(NULL, -1, p_ifindexes[i], NMTSTP_ENV1_DEVICE_NAME[i], TRUE);
 
     nm_platform_process_events(NM_PLATFORM_GET);
 
     _LOGT("TEST[%s]: finished", test_data->testpath);
 
-    *p_ifindex = -1;
+    for (i = 0; i < (int) G_N_ELEMENTS(NMTSTP_ENV1_IFINDEXES); i++)
+        p_ifindexes[i] = 0;
 }
 
 /* add test function, that set's up a particular environment, consisting
  * of a dummy device with ifindex NMTSTP_ENV1_IFINDEX. */
-#define _nmtstp_env1_add_test_func_full(testpath, test_func, test_data_func, arg, ifup) \
-    nmtst_add_test_func_full(testpath,                                                  \
-                             _nmtstp_env1_wrapper_run,                                  \
-                             _nmtstp_env1_wrapper_setup,                                \
-                             _nmtstp_env1_wrapper_teardown,                             \
-                             ({                                                         \
-                                 static int _ifindex = -1;                              \
-                                 &_ifindex;                                             \
-                             }),                                                        \
-                             ({                                                         \
-                                 GTestFunc _test_func = (test_func);                    \
-                                 _test_func;                                            \
-                             }),                                                        \
-                             ({                                                         \
-                                 GTestDataFunc _test_func = (test_data_func);           \
-                                 _test_func;                                            \
-                             }),                                                        \
-                             (arg),                                                     \
-                             ({                                                         \
-                                 gboolean _ifup = (ifup);                               \
-                                 GINT_TO_POINTER(_ifup);                                \
+#define _nmtstp_env1_add_test_func_full(testpath, test_func, test_data_func, arg, n_ifaces, ifup)  \
+    nmtst_add_test_func_full(testpath,                                                             \
+                             _nmtstp_env1_wrapper_run,                                             \
+                             _nmtstp_env1_wrapper_setup,                                           \
+                             _nmtstp_env1_wrapper_teardown,                                        \
+                             ({                                                                    \
+                                 static int _ifindexes[G_N_ELEMENTS(NMTSTP_ENV1_IFINDEXES)] = {0}; \
+                                 _ifindexes;                                                       \
+                             }),                                                                   \
+                             ({                                                                    \
+                                 guint _n_ifaces = (n_ifaces);                                     \
+                                 GUINT_TO_POINTER(_n_ifaces);                                      \
+                             }),                                                                   \
+                             ({                                                                    \
+                                 GTestFunc _test_func = (test_func);                               \
+                                 _test_func;                                                       \
+                             }),                                                                   \
+                             ({                                                                    \
+                                 GTestDataFunc _test_func = (test_data_func);                      \
+                                 _test_func;                                                       \
+                             }),                                                                   \
+                             (arg),                                                                \
+                             ({                                                                    \
+                                 gboolean _ifup = (ifup);                                          \
+                                 GINT_TO_POINTER(!!_ifup);                                         \
                              }))
 
-#define nmtstp_env1_add_test_func_data(testpath, test_func, arg, ifup) \
-    _nmtstp_env1_add_test_func_full(testpath, NULL, test_func, arg, ifup)
+#define nmtstp_env1_add_test_func_data(testpath, test_func, arg, n_ifaces, ifup) \
+    _nmtstp_env1_add_test_func_full(testpath, NULL, test_func, arg, n_ifaces, ifup)
 
-#define nmtstp_env1_add_test_func(testpath, test_func, ifup) \
-    _nmtstp_env1_add_test_func_full(testpath, test_func, NULL, NULL, ifup)
+#define nmtstp_env1_add_test_func(testpath, test_func, n_ifaces, ifup) \
+    _nmtstp_env1_add_test_func_full(testpath, test_func, NULL, NULL, n_ifaces, ifup)
 
 /*****************************************************************************/
 
diff --git a/src/core/platform/tests/test-link.c b/src/core/platform/tests/test-link.c
index b72bcb65..cc1ec6d1 100644
--- a/src/core/platform/tests/test-link.c
+++ b/src/core/platform/tests/test-link.c
@@ -161,8 +161,14 @@ software_add(NMLinkType link_type, const char *name)
                 accept_signals(parent_changed, 1, 2);
             free_signal(parent_changed);
 
-            return NMTST_NM_ERR_SUCCESS(
-                nm_platform_link_vlan_add(NM_PLATFORM_GET, name, parent_ifindex, VLAN_ID, 0, NULL));
+            return NMTST_NM_ERR_SUCCESS(nm_platform_link_vlan_add(NM_PLATFORM_GET,
+                                                                  name,
+                                                                  parent_ifindex,
+                                                                  &((NMPlatformLnkVlan){
+                                                                      .id       = VLAN_ID,
+                                                                      .protocol = ETH_P_8021Q,
+                                                                  }),
+                                                                  NULL));
         }
     }
     default:
@@ -1183,7 +1189,7 @@ _test_wireguard_change(NMPlatform *platform, int ifindex, int test_mode)
 
         for (i = 0; i < num_peers; i++) {
             NMPWireGuardPeer       peer;
-            char                   s_addr[NM_UTILS_INET_ADDRSTRLEN];
+            char                   s_addr[NM_INET_ADDRSTRLEN];
             NMSockAddrUnion        endpoint;
             guint                  i_allowed_ips, n_allowed_ips;
             NMPWireGuardAllowedIP *allowed_ips;
@@ -1203,8 +1209,8 @@ _test_wireguard_change(NMPlatform *platform, int ifindex, int test_mode)
                     .in6 =
                         {
                             .sin6_family = AF_INET6,
-                            .sin6_addr   = *nmtst_inet6_from_string(
-                                nm_sprintf_buf(s_addr, "a:b:c:e::1:%d", i)),
+                            .sin6_addr =
+                                nmtst_inet6_from_string(nm_sprintf_buf(s_addr, "a:b:c:e::1:%d", i)),
                             .sin6_port = htons(16000 + i),
                         },
                 };
@@ -1225,7 +1231,7 @@ _test_wireguard_change(NMPlatform *platform, int ifindex, int test_mode)
                         nm_sprintf_buf(s_addr, "10.%u.%u.0", i, i_allowed_ips));
                     aip->mask = 32 - (i_allowed_ips % 8);
                 } else {
-                    aip->addr.addr6 = *nmtst_inet6_from_string(
+                    aip->addr.addr6 = nmtst_inet6_from_string(
                         nm_sprintf_buf(s_addr, "a:d:f:%02x:%02x::", i, i_allowed_ips));
                     aip->mask = 128 - (i_allowed_ips % 10);
                 }
@@ -1250,7 +1256,7 @@ _test_wireguard_change(NMPlatform *platform, int ifindex, int test_mode)
     r = nm_platform_link_wireguard_change(platform,
                                           ifindex,
                                           &lnk_wireguard,
-                                          (const NMPWireGuardPeer *) peers->data,
+                                          nm_g_array_first_p(peers, const NMPWireGuardPeer),
                                           NULL,
                                           peers->len,
                                           NM_PLATFORM_WIREGUARD_CHANGE_FLAG_HAS_PRIVATE_KEY
@@ -1280,8 +1286,11 @@ test_software_detect(gconstpointer user_data)
     const gboolean                   ext        = test_data->external_command;
     NMPlatformLnkBridge              lnk_bridge = {};
     NMPlatformLnkTun                 lnk_tun;
-    NMPlatformLnkGre                 lnk_gre = {};
-    nm_auto_close int                tun_fd  = -1;
+    NMPlatformLnkGre                 lnk_gre  = {};
+    NMPlatformLnkVti                 lnk_vti  = {};
+    NMPlatformLnkVti6                lnk_vti6 = {};
+    nm_auto_close int                tun_fd   = -1;
+    gboolean                         module_loaded;
 
     nmtstp_run_command_check("ip link add %s type dummy", PARENT_NAME);
     ifindex_parent =
@@ -1296,24 +1305,24 @@ test_software_detect(gconstpointer user_data)
         lnk_bridge.ageing_time   = 2200;
         lnk_bridge.stp_state     = TRUE;
         lnk_bridge.priority      = 22;
-        lnk_bridge.vlan_protocol = 0x8100;
+        lnk_bridge.vlan_protocol = ETH_P_8021Q;
         lnk_bridge.vlan_stats_enabled =
             nmtstp_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_IFLA_BR_VLAN_STATS_ENABLED)
                 ? TRUE
                 : FALSE;
-        lnk_bridge.group_fwd_mask             = 8;
-        lnk_bridge.group_addr                 = (NMEtherAddr){{0x01, 0x80, 0xC2, 0x00, 0x00, 0x08}};
-        lnk_bridge.mcast_snooping             = TRUE;
-        lnk_bridge.mcast_router               = 1;
-        lnk_bridge.mcast_query_use_ifaddr     = TRUE;
-        lnk_bridge.mcast_querier              = TRUE;
-        lnk_bridge.mcast_hash_max             = 1024;
-        lnk_bridge.mcast_last_member_count    = 2;
-        lnk_bridge.mcast_startup_query_count  = 3;
-        lnk_bridge.mcast_last_member_interval = 5000;
-        lnk_bridge.mcast_membership_interval  = 25000;
-        lnk_bridge.mcast_querier_interval     = 26000;
-        lnk_bridge.mcast_query_interval       = 12000;
+        lnk_bridge.group_fwd_mask          = 8;
+        lnk_bridge.group_addr              = NM_ETHER_ADDR_INIT(0x01, 0x80, 0xC2, 0x00, 0x00, 0x08);
+        lnk_bridge.mcast_snooping          = TRUE;
+        lnk_bridge.mcast_router            = 1;
+        lnk_bridge.mcast_query_use_ifaddr  = TRUE;
+        lnk_bridge.mcast_querier           = TRUE;
+        lnk_bridge.mcast_hash_max          = 1024;
+        lnk_bridge.mcast_last_member_count = 2;
+        lnk_bridge.mcast_startup_query_count     = 3;
+        lnk_bridge.mcast_last_member_interval    = 5000;
+        lnk_bridge.mcast_membership_interval     = 25000;
+        lnk_bridge.mcast_querier_interval        = 26000;
+        lnk_bridge.mcast_query_interval          = 12000;
         lnk_bridge.mcast_query_response_interval = 5200;
         lnk_bridge.mcast_startup_query_interval  = 3000;
 
@@ -1322,8 +1331,7 @@ test_software_detect(gconstpointer user_data)
         break;
 
     case NM_LINK_TYPE_GRE:
-    {
-        gboolean gracefully_skip = FALSE;
+        module_loaded = nmtstp_ensure_module("ip_gre");
 
         lnk_gre.local              = nmtst_inet4_from_string("192.168.233.204");
         lnk_gre.remote             = nmtst_inet4_from_string("172.168.10.25");
@@ -1332,13 +1340,8 @@ test_software_detect(gconstpointer user_data)
         lnk_gre.tos                = 37;
         lnk_gre.path_mtu_discovery = TRUE;
 
-        if (!nm_platform_link_get_by_ifname(NM_PLATFORM_GET, "gre0")) {
-            /* Seems that the ip_gre module is not loaded... try to load it. */
-            gracefully_skip = nmp_utils_modprobe(NULL, TRUE, "ip_gre", NULL) != 0;
-        }
-
         if (!nmtstp_link_gre_add(NULL, ext, DEVICE_NAME, &lnk_gre)) {
-            if (gracefully_skip) {
+            if (!module_loaded) {
                 g_test_skip(
                     "Cannot create gre tunnel because of missing ip_gre module (modprobe ip_gre)");
                 goto out_delete_parent;
@@ -1346,10 +1349,9 @@ test_software_detect(gconstpointer user_data)
             g_error("Failed adding GRE tunnel");
         }
         break;
-    }
+
     case NM_LINK_TYPE_GRETAP:
-    {
-        gboolean gracefully_skip = FALSE;
+        module_loaded = nmtstp_ensure_module("ip_gre");
 
         lnk_gre.local              = nmtst_inet4_from_string("192.168.1.133");
         lnk_gre.remote             = nmtst_inet4_from_string("172.168.101.2");
@@ -1359,13 +1361,8 @@ test_software_detect(gconstpointer user_data)
         lnk_gre.path_mtu_discovery = FALSE;
         lnk_gre.is_tap             = TRUE;
 
-        if (!nm_platform_link_get_by_ifname(NM_PLATFORM_GET, "gretap0")) {
-            /* Seems that the ip_gre module is not loaded... try to load it. */
-            gracefully_skip = nmp_utils_modprobe(NULL, TRUE, "ip_gre", NULL) != 0;
-        }
-
         if (!nmtstp_link_gre_add(NULL, ext, DEVICE_NAME, &lnk_gre)) {
-            if (gracefully_skip) {
+            if (!module_loaded) {
                 g_test_skip("Cannot create gretap tunnel because of missing ip_gre module "
                             "(modprobe ip_gre)");
                 goto out_delete_parent;
@@ -1373,16 +1370,12 @@ test_software_detect(gconstpointer user_data)
             g_error("Failed adding GRETAP tunnel");
         }
         break;
-    }
+
     case NM_LINK_TYPE_IPIP:
     {
-        NMPlatformLnkIpIp lnk_ipip        = {};
-        gboolean          gracefully_skip = FALSE;
+        NMPlatformLnkIpIp lnk_ipip = {};
 
-        if (!nm_platform_link_get_by_ifname(NM_PLATFORM_GET, "tunl0")) {
-            /* Seems that the ipip module is not loaded... try to load it. */
-            gracefully_skip = nmp_utils_modprobe(NULL, TRUE, "ipip", NULL) != 0;
-        }
+        module_loaded = nmtstp_ensure_module("ipip");
 
         lnk_ipip.local              = nmtst_inet4_from_string("1.2.3.4");
         lnk_ipip.remote             = nmtst_inet4_from_string("5.6.7.8");
@@ -1391,7 +1384,7 @@ test_software_detect(gconstpointer user_data)
         lnk_ipip.path_mtu_discovery = FALSE;
 
         if (!nmtstp_link_ipip_add(NULL, ext, DEVICE_NAME, &lnk_ipip)) {
-            if (gracefully_skip) {
+            if (!module_loaded) {
                 g_test_skip(
                     "Cannot create ipip tunnel because of missing ipip module (modprobe ipip)");
                 goto out_delete_parent;
@@ -1400,20 +1393,17 @@ test_software_detect(gconstpointer user_data)
         }
         break;
     }
+
     case NM_LINK_TYPE_IP6TNL:
     {
-        NMPlatformLnkIp6Tnl lnk_ip6tnl      = {};
-        gboolean            gracefully_skip = FALSE;
+        NMPlatformLnkIp6Tnl lnk_ip6tnl = {};
 
-        if (!nm_platform_link_get_by_ifname(NM_PLATFORM_GET, "ip6tnl0")) {
-            /* Seems that the ip6_tunnel module is not loaded... try to load it. */
-            gracefully_skip = nmp_utils_modprobe(NULL, TRUE, "ip6_tunnel", NULL) != 0;
-        }
+        module_loaded = nmtstp_ensure_module("ip6_tunnel");
 
         switch (test_data->test_mode) {
         case 0:
-            lnk_ip6tnl.local          = *nmtst_inet6_from_string("fd01::15");
-            lnk_ip6tnl.remote         = *nmtst_inet6_from_string("fd01::16");
+            lnk_ip6tnl.local          = nmtst_inet6_from_string("fd01::15");
+            lnk_ip6tnl.remote         = nmtst_inet6_from_string("fd01::16");
             lnk_ip6tnl.parent_ifindex = ifindex_parent;
             lnk_ip6tnl.tclass         = 20;
             lnk_ip6tnl.encap_limit    = 6;
@@ -1421,8 +1411,8 @@ test_software_detect(gconstpointer user_data)
             lnk_ip6tnl.proto          = IPPROTO_IPV6;
             break;
         case 1:
-            lnk_ip6tnl.local          = *nmtst_inet6_from_string("fd01::17");
-            lnk_ip6tnl.remote         = *nmtst_inet6_from_string("fd01::18");
+            lnk_ip6tnl.local          = nmtst_inet6_from_string("fd01::17");
+            lnk_ip6tnl.remote         = nmtst_inet6_from_string("fd01::18");
             lnk_ip6tnl.parent_ifindex = ifindex_parent;
             lnk_ip6tnl.tclass         = 0;
             lnk_ip6tnl.encap_limit    = 0;
@@ -1433,7 +1423,7 @@ test_software_detect(gconstpointer user_data)
         }
 
         if (!nmtstp_link_ip6tnl_add(NULL, ext, DEVICE_NAME, &lnk_ip6tnl)) {
-            if (gracefully_skip) {
+            if (!module_loaded) {
                 g_test_skip("Cannot create ip6tnl tunnel because of missing ip6_tunnel module "
                             "(modprobe ip6_tunnel)");
                 goto out_delete_parent;
@@ -1442,25 +1432,22 @@ test_software_detect(gconstpointer user_data)
         }
         break;
     }
+
     case NM_LINK_TYPE_IP6GRE:
     {
-        NMPlatformLnkIp6Tnl lnk_ip6tnl      = {};
-        gboolean            gracefully_skip = FALSE;
+        NMPlatformLnkIp6Tnl lnk_ip6tnl = {};
 
-        if (!nm_platform_link_get_by_ifname(NM_PLATFORM_GET, "ip6gre0")) {
-            /* Seems that the ip6_tunnel module is not loaded... try to load it. */
-            gracefully_skip = nmp_utils_modprobe(NULL, TRUE, "ip6_gre", NULL) != 0;
-        }
+        module_loaded = nmtstp_ensure_module("ip6_gre");
 
-        lnk_ip6tnl.local          = *nmtst_inet6_from_string("fd01::42");
-        lnk_ip6tnl.remote         = *nmtst_inet6_from_string("fd01::aaaa");
+        lnk_ip6tnl.local          = nmtst_inet6_from_string("fd01::42");
+        lnk_ip6tnl.remote         = nmtst_inet6_from_string("fd01::aaaa");
         lnk_ip6tnl.parent_ifindex = ifindex_parent;
         lnk_ip6tnl.tclass         = 21;
         lnk_ip6tnl.flow_label     = 1338;
         lnk_ip6tnl.is_gre         = TRUE;
 
         if (!nmtstp_link_ip6gre_add(NULL, ext, DEVICE_NAME, &lnk_ip6tnl)) {
-            if (gracefully_skip) {
+            if (!module_loaded) {
                 g_test_skip("Cannot create ip6gre tunnel because of missing ip6_gre module "
                             "(modprobe ip6_gre)");
                 goto out_delete_parent;
@@ -1469,18 +1456,15 @@ test_software_detect(gconstpointer user_data)
         }
         break;
     }
+
     case NM_LINK_TYPE_IP6GRETAP:
     {
-        NMPlatformLnkIp6Tnl lnk_ip6tnl      = {};
-        gboolean            gracefully_skip = FALSE;
+        NMPlatformLnkIp6Tnl lnk_ip6tnl = {};
 
-        if (!nm_platform_link_get_by_ifname(NM_PLATFORM_GET, "ip6gre0")) {
-            /* Seems that the ip6_tunnel module is not loaded... try to load it. */
-            gracefully_skip = nmp_utils_modprobe(NULL, TRUE, "ip6_gre", NULL) != 0;
-        }
+        module_loaded = nmtstp_ensure_module("ip6_gre");
 
-        lnk_ip6tnl.local          = *nmtst_inet6_from_string("fe80::abcd");
-        lnk_ip6tnl.remote         = *nmtst_inet6_from_string("fc01::bbbb");
+        lnk_ip6tnl.local          = nmtst_inet6_from_string("fe80::abcd");
+        lnk_ip6tnl.remote         = nmtst_inet6_from_string("fc01::bbbb");
         lnk_ip6tnl.parent_ifindex = ifindex_parent;
         lnk_ip6tnl.ttl            = 10;
         lnk_ip6tnl.tclass         = 22;
@@ -1489,7 +1473,7 @@ test_software_detect(gconstpointer user_data)
         lnk_ip6tnl.is_tap         = TRUE;
 
         if (!nmtstp_link_ip6gre_add(NULL, ext, DEVICE_NAME, &lnk_ip6tnl)) {
-            if (gracefully_skip) {
+            if (!module_loaded) {
                 g_test_skip("Cannot create ip6gretap tunnel because of missing ip6_gre module "
                             "(modprobe ip6_gre)");
                 goto out_delete_parent;
@@ -1498,6 +1482,7 @@ test_software_detect(gconstpointer user_data)
         }
         break;
     }
+
     case NM_LINK_TYPE_MACVLAN:
     {
         NMPlatformLnkMacvlan  lnk_macvlan = {};
@@ -1531,6 +1516,7 @@ test_software_detect(gconstpointer user_data)
             g_error("Failed adding MACVLAN interface");
         break;
     }
+
     case NM_LINK_TYPE_MACVTAP:
     {
         NMPlatformLnkMacvlan lnk_macvtap = {};
@@ -1543,10 +1529,12 @@ test_software_detect(gconstpointer user_data)
             g_error("Failed adding MACVTAP interface");
         break;
     }
+
     case NM_LINK_TYPE_SIT:
     {
-        NMPlatformLnkSit lnk_sit         = {};
-        gboolean         gracefully_skip = FALSE;
+        NMPlatformLnkSit lnk_sit = {};
+
+        module_loaded = nmtstp_ensure_module("sit");
 
         lnk_sit.local              = nmtst_inet4_from_string("192.168.200.1");
         lnk_sit.remote             = nmtst_inet4_from_string("172.25.100.14");
@@ -1555,13 +1543,8 @@ test_software_detect(gconstpointer user_data)
         lnk_sit.tos                = 31;
         lnk_sit.path_mtu_discovery = FALSE;
 
-        if (!nm_platform_link_get_by_ifname(NM_PLATFORM_GET, "sit0")) {
-            /* Seems that the sit module is not loaded... try to load it. */
-            gracefully_skip = nmp_utils_modprobe(NULL, TRUE, "sit", NULL) != 0;
-        }
-
         if (!nmtstp_link_sit_add(NULL, ext, DEVICE_NAME, &lnk_sit)) {
-            if (gracefully_skip) {
+            if (!module_loaded) {
                 g_test_skip(
                     "Cannot create sit tunnel because of missing sit module (modprobe sit)");
                 goto out_delete_parent;
@@ -1570,11 +1553,31 @@ test_software_detect(gconstpointer user_data)
         }
         break;
     }
+
     case NM_LINK_TYPE_VLAN:
-        nmtstp_run_command_check("ip link add name %s link %s type vlan id 1242",
-                                 DEVICE_NAME,
-                                 PARENT_NAME);
+    {
+        NMPlatformLnkVlan lnk_vlan = {};
+
+        switch (test_data->test_mode) {
+        case 0:
+            lnk_vlan.id       = 1242;
+            lnk_vlan.protocol = ETH_P_8021Q;
+            lnk_vlan.flags    = _NM_VLAN_FLAG_REORDER_HEADERS;
+            break;
+        case 1:
+            lnk_vlan.id       = 4094;
+            lnk_vlan.protocol = ETH_P_8021AD;
+            lnk_vlan.flags    = _NM_VLAN_FLAG_GVRP | _NM_VLAN_FLAG_MVRP;
+            break;
+        default:
+            nm_assert_not_reached();
+        }
+
+        if (!nmtstp_link_vlan_add(NULL, ext, DEVICE_NAME, ifindex_parent, &lnk_vlan))
+            g_error("Failed adding VLAN interface");
         break;
+    }
+
     case NM_LINK_TYPE_VRF:
     {
         NMPlatformLnkVrf lnk_vrf = {};
@@ -1591,6 +1594,45 @@ test_software_detect(gconstpointer user_data)
         }
         break;
     }
+
+    case NM_LINK_TYPE_VTI:
+        module_loaded = nmtstp_ensure_module("ip_vti");
+
+        lnk_vti.local          = nmtst_inet4_from_string("192.168.212.204");
+        lnk_vti.remote         = nmtst_inet4_from_string("172.168.11.25");
+        lnk_vti.parent_ifindex = ifindex_parent;
+        lnk_vti.ikey           = 12;
+        lnk_vti.okey           = 13;
+
+        if (!nmtstp_link_vti_add(NULL, ext, DEVICE_NAME, &lnk_vti)) {
+            if (!module_loaded) {
+                g_test_skip(
+                    "Cannot create vti tunnel because of missing vti module (modprobe ip_vti)");
+                goto out_delete_parent;
+            }
+            g_error("Failed adding VTI tunnel");
+        }
+        break;
+
+    case NM_LINK_TYPE_VTI6:
+        module_loaded = nmtstp_ensure_module("ip6_vti");
+
+        lnk_vti6.local          = nmtst_inet6_from_string("fd01::1");
+        lnk_vti6.remote         = nmtst_inet6_from_string("fd02::2");
+        lnk_vti6.parent_ifindex = ifindex_parent;
+        lnk_vti6.ikey           = 13;
+        lnk_vti6.okey           = 14;
+
+        if (!nmtstp_link_vti6_add(NULL, ext, DEVICE_NAME, &lnk_vti6)) {
+            if (!module_loaded) {
+                g_test_skip(
+                    "Cannot create vti6 tunnel because of missing vti module (modprobe ip_vti)");
+                goto out_delete_parent;
+            }
+            g_error("Failed adding VTI6 tunnel");
+        }
+        break;
+
     case NM_LINK_TYPE_VXLAN:
     {
         NMPlatformLnkVxlan lnk_vxlan = {};
@@ -1608,8 +1650,8 @@ test_software_detect(gconstpointer user_data)
         case 1:
             lnk_vxlan.parent_ifindex = nm_platform_link_get_ifindex(NM_PLATFORM_GET, PARENT_NAME);
             lnk_vxlan.id             = 11214423;
-            lnk_vxlan.local6         = *nmtst_inet6_from_string("1:2:3:4:334:23::23");
-            lnk_vxlan.group6         = *nmtst_inet6_from_string("ff0e::115");
+            lnk_vxlan.local6         = nmtst_inet6_from_string("1:2:3:4:334:23::23");
+            lnk_vxlan.group6         = nmtst_inet6_from_string("ff0e::115");
             lnk_vxlan.ttl            = 32;
             lnk_vxlan.dst_port       = 57412;
             lnk_vxlan.src_port_min   = 1000;
@@ -1622,6 +1664,7 @@ test_software_detect(gconstpointer user_data)
         g_assert(nmtstp_link_vxlan_add(NULL, ext, DEVICE_NAME, &lnk_vxlan));
         break;
     }
+
     case NM_LINK_TYPE_TUN:
     {
         gboolean owner_valid = nmtst_get_rand_bool();
@@ -1656,6 +1699,7 @@ test_software_detect(gconstpointer user_data)
                                      (!lnk_tun.persist || nmtst_get_rand_bool()) ? &tun_fd : NULL));
         break;
     }
+
     case NM_LINK_TYPE_WIREGUARD:
     {
         const NMPlatformLink *link;
@@ -1727,7 +1771,7 @@ test_software_detect(gconstpointer user_data)
             g_assert_cmpint(plnk->ageing_time, ==, lnk_bridge_norm->ageing_time);
             g_assert_cmpint(plnk->stp_state, ==, TRUE);
             g_assert_cmpint(plnk->priority, ==, 22);
-            g_assert_cmpint(plnk->vlan_protocol, ==, 0x8100);
+            g_assert_cmpint(plnk->vlan_protocol, ==, ETH_P_8021Q);
             g_assert_cmpint(plnk->vlan_stats_enabled, ==, lnk_bridge_norm->vlan_stats_enabled);
             g_assert_cmpint(plnk->group_fwd_mask, ==, 8);
             g_assert_cmpint(plnk->mcast_snooping, ==, TRUE);
@@ -1910,7 +1954,19 @@ test_software_detect(gconstpointer user_data)
             const NMPlatformLnkVlan *plnk = &lnk->lnk_vlan;
 
             g_assert(plnk == nm_platform_link_get_lnk_vlan(NM_PLATFORM_GET, ifindex, NULL));
-            g_assert_cmpint(plnk->id, ==, 1242);
+
+            switch (test_data->test_mode) {
+            case 0:
+                g_assert_cmpint(plnk->id, ==, 1242);
+                g_assert_cmpint(plnk->protocol, ==, ETH_P_8021Q);
+                g_assert_cmpint(plnk->flags, ==, _NM_VLAN_FLAG_REORDER_HEADERS);
+                break;
+            case 1:
+                g_assert_cmpint(plnk->id, ==, 4094);
+                g_assert_cmpint(plnk->protocol, ==, ETH_P_8021AD);
+                g_assert_cmpint(plnk->flags, ==, _NM_VLAN_FLAG_GVRP | _NM_VLAN_FLAG_MVRP);
+                break;
+            }
             break;
         }
         case NM_LINK_TYPE_VRF:
@@ -1921,6 +1977,22 @@ test_software_detect(gconstpointer user_data)
             g_assert_cmpint(plnk->table, ==, 9876);
             break;
         }
+        case NM_LINK_TYPE_VTI:
+        {
+            const NMPlatformLnkVti *plnk = &lnk->lnk_vti;
+
+            g_assert(plnk == nm_platform_link_get_lnk_vti(NM_PLATFORM_GET, ifindex, NULL));
+            g_assert(nm_platform_lnk_vti_cmp(plnk, &lnk_vti) == 0);
+            break;
+        }
+        case NM_LINK_TYPE_VTI6:
+        {
+            const NMPlatformLnkVti6 *plnk = &lnk->lnk_vti6;
+
+            g_assert(plnk == nm_platform_link_get_lnk_vti6(NM_PLATFORM_GET, ifindex, NULL));
+            g_assert(nm_platform_lnk_vti6_cmp(plnk, &lnk_vti6) == 0);
+            break;
+        }
         case NM_LINK_TYPE_VXLAN:
         {
             const NMPlatformLnkVxlan *plnk = &lnk->lnk_vxlan;
@@ -2511,7 +2583,7 @@ test_create_many_links_do(guint n_devices)
         if (EX == 2)
             nmtstp_run_command_check("ip link delete %s", name);
         else
-            nmtstp_link_delete(NULL, EX, g_array_index(ifindexes, int, i), name, TRUE);
+            nmtstp_link_delete(NULL, EX, nm_g_array_index(ifindexes, int, i), name, TRUE);
     }
 
     _LOGI(">>> process events after deleting devices...");
@@ -2754,7 +2826,7 @@ _test_netns_create_platform(void)
     netns = nmp_netns_new();
     g_assert(NMP_IS_NETNS(netns));
 
-    platform = nm_linux_platform_new(TRUE, TRUE, TRUE);
+    platform = nm_linux_platform_new(NULL, TRUE, TRUE, TRUE);
     g_assert(NM_IS_LINUX_PLATFORM(platform));
 
     nmp_netns_pop(netns);
@@ -2843,7 +2915,7 @@ test_netns_general(gpointer fixture, gconstpointer test_data)
     if (_check_sysctl_skip())
         return;
 
-    platform_1 = nm_linux_platform_new(TRUE, TRUE, TRUE);
+    platform_1 = nm_linux_platform_new(NULL, TRUE, TRUE, TRUE);
     platform_2 = _test_netns_create_platform();
 
     /* add some dummy devices. The "other-*" devices are there to bump the ifindex */
@@ -2971,7 +3043,7 @@ test_netns_set_netns(gpointer fixture, gconstpointer test_data)
     if (_test_netns_check_skip())
         return;
 
-    platforms[0] = platform_0 = nm_linux_platform_new(TRUE, TRUE, TRUE);
+    platforms[0] = platform_0 = nm_linux_platform_new(NULL, TRUE, TRUE, TRUE);
     platforms[1] = platform_1 = _test_netns_create_platform();
     platforms[2] = platform_2 = _test_netns_create_platform();
 
@@ -3070,7 +3142,7 @@ test_netns_push(gpointer fixture, gconstpointer test_data)
     if (_check_sysctl_skip())
         return;
 
-    pl[0].platform = platform_0 = nm_linux_platform_new(TRUE, TRUE, TRUE);
+    pl[0].platform = platform_0 = nm_linux_platform_new(NULL, TRUE, TRUE, TRUE);
     pl[1].platform = platform_1 = _test_netns_create_platform();
     pl[2].platform = platform_2 = _test_netns_create_platform();
 
@@ -3217,7 +3289,7 @@ test_netns_bind_to_path(gpointer fixture, gconstpointer test_data)
     if (_test_netns_check_skip())
         return;
 
-    platforms[0] = platform_0 = nm_linux_platform_new(TRUE, TRUE, TRUE);
+    platforms[0] = platform_0 = nm_linux_platform_new(NULL, TRUE, TRUE, TRUE);
     platforms[1] = platform_1 = _test_netns_create_platform();
     platforms[2] = platform_2 = _test_netns_create_platform();
 
@@ -3382,7 +3454,7 @@ test_sysctl_netns_switch(void)
     if (_test_netns_check_skip())
         return;
 
-    platforms[0] = platform_0 = nm_linux_platform_new(TRUE, TRUE, TRUE);
+    platforms[0] = platform_0 = nm_linux_platform_new(NULL, TRUE, TRUE, TRUE);
     platforms[1] = platform_1 = _test_netns_create_platform();
     platforms[2] = platform_2 = _test_netns_create_platform();
     PL                        = platforms[nmtst_get_rand_uint32() % 3];
@@ -3823,7 +3895,10 @@ _nmtstp_setup_tests(void)
         test_software_detect_add("/link/software/detect/macvtap", NM_LINK_TYPE_MACVTAP, 0);
         test_software_detect_add("/link/software/detect/sit", NM_LINK_TYPE_SIT, 0);
         test_software_detect_add("/link/software/detect/tun", NM_LINK_TYPE_TUN, 0);
-        test_software_detect_add("/link/software/detect/vlan", NM_LINK_TYPE_VLAN, 0);
+        test_software_detect_add("/link/software/detect/vlan/0", NM_LINK_TYPE_VLAN, 0);
+        test_software_detect_add("/link/software/detect/vlan/1", NM_LINK_TYPE_VLAN, 1);
+        test_software_detect_add("/link/software/detect/vti", NM_LINK_TYPE_VTI, 0);
+        test_software_detect_add("/link/software/detect/vti6", NM_LINK_TYPE_VTI6, 0);
         test_software_detect_add("/link/software/detect/vrf", NM_LINK_TYPE_VRF, 0);
         test_software_detect_add("/link/software/detect/vxlan/0", NM_LINK_TYPE_VXLAN, 0);
         test_software_detect_add("/link/software/detect/vxlan/1", NM_LINK_TYPE_VXLAN, 1);
diff --git a/src/core/platform/tests/test-platform-general.c b/src/core/platform/tests/test-platform-general.c
index c18886c9..cebd44f6 100644
--- a/src/core/platform/tests/test-platform-general.c
+++ b/src/core/platform/tests/test-platform-general.c
@@ -31,7 +31,7 @@ test_init_linux_platform(void)
 {
     gs_unref_object NMPlatform *platform = NULL;
 
-    platform = nm_linux_platform_new(TRUE, NM_PLATFORM_NETNS_SUPPORT_DEFAULT, TRUE);
+    platform = nm_linux_platform_new(NULL, TRUE, NM_PLATFORM_NETNS_SUPPORT_DEFAULT, TRUE);
 }
 
 /*****************************************************************************/
@@ -42,7 +42,7 @@ test_link_get_all(void)
     gs_unref_object NMPlatform  *platform = NULL;
     gs_unref_ptrarray GPtrArray *links    = NULL;
 
-    platform = nm_linux_platform_new(TRUE, NM_PLATFORM_NETNS_SUPPORT_DEFAULT, TRUE);
+    platform = nm_linux_platform_new(NULL, TRUE, NM_PLATFORM_NETNS_SUPPORT_DEFAULT, TRUE);
 
     links = nm_platform_link_get_all(platform, TRUE);
 }
@@ -101,456 +101,486 @@ static void
 test_platform_ip_address_pretty_sort_cmp(gconstpointer test_data)
 {
     static const char *const EXPECTED_BUFFER[3] = {
-        [0] = ("5b1aea34648cabfec7c3523f76cf1ce34ca17a9a32f3f0f218074e48000000cb504e03d53e1124c5"
-               "0065aeb2e6fbf952902383028e3b47f2111d891d17a47631c169f11ff9119c40b468b66331bca67a"
-               "034f43340d02683c7545097aea4a849f060ddf57004a5be9b10123e6e4f15ecd1dff045298cfee06"
-               "b7f0318a03000000061db3dd7fde25654a4059d565dbc8a91e014457000000db56189572f32f4af5"
-               "00d785d5c1b5aad487c192f08bd7a09bdd2144439c88b02a0300000004b0595daf37deb499996bfb"
-               "667f072dec035d9c000000f4409bcfb3c167090e00d90a8543961b2ccd47724a3c460ba85183f4c5"
-               "a7afe28303000000d05ff720b16c73761a3fcb675219061ab402c7940000006a883c146972decb8b"
-               "00dac8f5e6d66df46ea139107754dee62c36d2fc6aae660b03000000fc1fb23d24438387def0f4c6"
-               "544e4b275d037146000000d349adc17647176b7c00d856e3b883efebc09dd9d61966b7ae7412041d"
-               "e5fe7d360300000066a3ae3939762df33a2d55060c78d551cb0310fd00000043b9aca07b6e4925dd"
-               "00943a272c171ed15abbbe1cd911db7b56e8f3fe6f0e9e6903000000ec2e2924348352d715cf9d41"
-               "1ea012e5300294b6000000c528ab20075efee38d006f8ee4ccd2f50c30706cae3fdcc2f0ee3d5e26"
-               "dac12ebd03000000667224dae775e5ccd141aca2ff0f57676702648100000086b62f22ca33478e1f"
-               "001715df7fc6a902edae579e2e10c7f7a22ba0340436242c03000000ee3fbc164632d4f28aa28c08"
-               "f80dd50c6702dfb400000023a28fa772a19cdf6d00c173f6078a806d329c9b008bbfc9723107f3f5"
-               "bcfe97e1030000007ad742d3d7320a4f880cf47f5dd0cf69cb0d840f0000007749509cb6752b2cee"
-               "00b7e7736a0afc9879ea40e69710fc9f6e8e99cdabf85f0c0300000008d33a2ea67776d7f88d69c8"
-               "c7e3b2d3c900f05400000021abb42316c533d9c90088189c471a1ff2f0ce3ff66e782110125ae2ed"
-               "922b6cb8030000001d5c302df59dd32a677e5a3af4fe297f6002f061d45d864d09f4ecf17625f82d"
-               "00c748288ae21e371051852c972ea7954079884a6c0fc09c03000000809829af4a9df58dbfef186d"
-               "416f3a1ef102d10f000000675203d82a3c84c4eb274ed18687780281d47a32f2235376239daeaacb"
-               "3cae864b0300000091921681c2162b9bfa40546cdb544e44df037c270000009a051bb2ee8192ac0c"
-               "fa3abc1ea0ab3e7d75a2f42b50c6a363401323780422ae3f03000000c43bd794c697895e5bfe47c6"
-               "f53a54e01b021d89000000d3e02d2ec587fc330003781b0f294a2b73c4421398f4de67e9cee64b38"
-               "ae8cb60b0300000038d856a40c67c3d4afa9c8649da90bd2ff02f2560000000bd7d5f14870c6d18d"
-               "6242b356955e1a13d1111b067acbce3e3bcdf382bb0ec36f0300000064a2827b7cdd38d0314c178e"
-               "925b568ae512c4e9000000e5264fd3ebe9cf9f6df9615189f78ee815b2781ea55c9555876b6a3f13"
-               "36674cde0300000054cfe8f42bf0e9381ba2c4396de60f032b00f34de2959871c0d4c0d4eb720c4a"
-               "e3c9472e5f2da5d9c5cfd9f2343b21362c19a09269121aec030000000fcc00ac2daa755e5292cd32"
-               "dc096cd5d802c5cf3351ee1cfcc2056cc1ff8e51bbce426cfa4b861cc78592be7b14e7ba9c15acb8"
-               "ba8110fb03000000904f288262e6ea77a22fa5f863eaeefa70031205000000f39b098b43cd68be6e"
-               "3f81268193fd637e585287dc6ca972a2cfb18b2bf6989d2d030000006ff7add5df2c93e05459507b"
-               "5d8c6a0f460554d2000000a8c67b2e48b47003c81785d3ffdbd9a617857ae8126b028c0e3e295354"
-               "bc51e540030000006c3d048a92d3adc69a84eb47622400207700416a00000087fbd7e2f7dea0077a"
-               "e9c0c0d9c386e9c6e6a2cbfa10ee58bdc75183607c52a52d030000004e8e8bc17c01ac63eca0c1cf"
-               "bb204132fb0213e4000000c2303dd1d65c7b2cffeca6d1adaccae11f8045a80f855ebf3b772eb2b1"
-               "bae87d60030000005d31e5b2de252167c1c91b3a36ba0c700e118477000000b913a353edc66efc06"
-               "8829c0e66ad0143a0554efb46b382ab0404cdf027a899f2d030000009bf5a5d06776e66a9d80ab13"
-               "2e1ac921eb01adbb0000002f561fa80a40e55b8b1dd92e18c0cfb2b4386bec092fa5757ecde9348b"
-               "daae7d6303000000c0982b283aa9986700b2a2b3ed257b8b041df48f000000cac72105b327335a25"
-               "a364d1015ffac03089f45539e1b0178462156cd6c4e1966803000000b3121cd5ef51e696c816290d"
-               "baee0e77260a82e1530ff5397c9125f59577d71c4b258a005116d11354edff62ceaa458fc75a91c4"
-               "d03bc90503000000eb760e3f004a2efc8ffc96b89b1bc8f415034e77be46bae5a1b3cae76665a268"
-               "abfe8a41d149f074a378b881153ece68ade15cec7625ca4a03000000e682f80f94315c45ef817264"
-               "c89a736ed51dd637b077ddad3108be69f9b97d05c917ad6b10e693bb6e26f2ba90c8e909e85e20e5"
-               "2af145480300000004265f6c72923e22594f3f79e208ed2e2e020771db46a54cadb245eafde6b025"
-               "67f814de39dd8f88152a58454d9ca9d20f45dfa700154d12020000009edfa7a74c1c83c18e47935e"
-               "8354846e3403386a000000d94064f6b021281e230094ae20ac26c7d41fd050bf8d85055bcfe3756c"
-               "98c39e7c02000000efc57ff447036b9551adcb22fd50793f79103c7d0000006e759d0c90bc512e13"
-               "00cef5e4a27985bd8d1e69c83dc21056b973972c6855676c02000000017c9148da5892bb4951c3a7"
-               "ed55689d3305ce7d3fd48469845f0c233dbdd2a90035df1f1782dee88487fd0db5971b46b7a350f9"
-               "8c3b371502000000710c030690f5f18ea125dbf7d7e93bd65c01a56dfdcfc1155f236c8b9c79a620"
-               "00660bbf024b03ff0a8e27c405e6424457393c6f74993ade02000000e62112898dab2dade2ab2fc9"
-               "c56a7c86be0296228ec3f2f439d949b22b9c161c00b745cebdd06352cb2c354434819f4b248eb2b8"
-               "6297dfa702000000fd0d53bb6215de4bb6f6f3d031a790287e06e9c18bb836c0a25ea71f5daaed4d"
-               "00eb2ebebe3c432fec7d3abc4ccf30d3aaaf02a46e91b91502000000be5b534e0a6527f67485ab35"
-               "aca0c7ee4102338500000063e66e3a23d6732578acd14320aefd4503189f7630248e501c9052c22b"
-               "e3ff1a5f02000000b4425486c619147eb0216050ed7afd741000e83600000011f9c797c0c0ec468e"
-               "9de5a9caf493472201e48d163106cc397446a33fa349e1cc02000000b879a45176740744fcb6a035"
-               "339d504c9719d80d000000df765b4d9a130257dc5ff7f03a9518862bc9a89192c1c8be55281a59d1"
-               "bcd7be0702000000e45aaf61814f703b40125e6baf4648cbaa1f7b86000000dafe7d6ec4ac1ea051"
-               "010911915ddb05f2a84e27c18a06e2c9cb29efdfb56e03e0010000002f70249eefc67bce716c856e"
-               "3973dc42a1033be9000000efb1f14d985d5bbe6c00085f3d6176f78a19bb8df1804c122fce5078c1"
-               "57d6249b010000007dc7f17f666969e45c3baf7a119a65ee0b01fa9e000000f3de6dc8dfcf2a3ad3"
-               "00cc0fa3b314f695bd82e9ea8891ed0a3d1b91ca4f1b134801000000449a774929e86716ec852d59"
-               "f3d17232ed026ad600000068843a9aef34983882004f24a17dba1ee7e143fd52599364e13468f80f"
-               "fc1519de010000002545666937c22a3b6e9686dcc405cbee5000c2520000001f7a0165fe20744edb"
-               "00a04f146e4c699341b95f41d9c1e67743efb54e54270e46010000006a894e387625da376feecb80"
-               "ac245409c003cc2700000067179bff0644399ae70064ed0432d599eaa660ac824b7fae389861419c"
-               "9f3c2c8801000000f51f44e644d7cf1d06b2b115d882549e62002fba5b1cdec48d10bbb13b8313d8"
-               "002a24d34fc812bd2f8a59d96d064126666befbb81ae55f0010000000aa20624fbab244735d67c61"
-               "283031667b0274a103e0b1b153222aa590a2312e00f1a35900459582f4ef43c780908872746e39ef"
-               "536c75bb01000000720da9e07ac998b5690807d52617369ee1004ca500000081af5fd7ceacdca10f"
-               "00d7b351a36b178aabc78a4a0d80684ee5dbf45bc7a4c65601000000cd625009a40aeb62a42b6a62"
-               "548e3c38b9002002000000fbfaf1809becd2506300a6da29b2e94d3ab51fe0e9af98b180a36035fb"
-               "f05d7308010000009d82c856ba942455050f63f8282f34644402acf5000000a54b7892f487afa98c"
-               "0021c19c69d9942f460bcea75481f25e307d0de959747cf201000000ab2dadc5a39411fd4ff1116d"
-               "478987316a0e3fc2000000cfc6ebe434a7ae8ff040483e310819e3b10db116431ec6f7691539ce41"
-               "d514573f01000000f6932f6932d1d5f5d0ce2cf6bf7a70ba5100a1620000001b11d9172cf84a0347"
-               "8b82cdd8883b0f3fd6ed84d6c959e553b887edcdf90e54b501000000a779f37c30d2c5053c40ecae"
-               "210aeb0934012a247000b96d06992b6a58acd3995b9663d2df48f333ef467092818aa77d6732b678"
-               "cf844ef501000000cd743f59bd14c89b5cfd5ce6e82e785415032b7000000043e0f79ab6902dde53"
-               "36b3ec7127e19f254ac9588319558041fc62ed564153e174010000004d89c0bb5408c1b7d17fb084"
-               "d9e1825fa602e1af0000004b7f9e85ff73e050a2cd89fd276e08cb887464b9344ec3127c750fabdd"
-               "9985d62701000000e491cbbbf9443fd6a77eb3c5bf64b671d6068dbf0000008306c83bc026977911"
-               "4bdade862c224f6f36506ebd455e679cef369bb8fc2b445701000000496ad01bf007eacb0a28aec8"
-               "68282510b61e91ea6480e356def85b75827b26e8a0bef8675b29a197b7b3cceaf5f1bb12c535256a"
-               "61c2f5d200000000b809d259e499db7dc8f1853bdcb0e4bc0e1700b60000006dae02dfa0c26e7c3a"
-               "00603bd35e7fa0f02f2f3313d8469d09a92409c0f257b044000000006a4c619f9037d7a754655354"
-               "c47cf0f5510167700000001bef820fe02931f2d30075049150b8d10ab700cc3a7cf51be0403b654b"
-               "10613fc90000000016a31dc2e46a234558817151b38fc9c909038d71000000d761fdf274fc5dca8c"
-               "00bd1871ae17bb4515856092b4d9e89b676761b2eecc89f600000000d635edea8ffe423d01883918"
-               "039249f39800b37e00000053b2a16e643722d9b700c0d8a41fb7c8307f06b4b29088f20d9ac676d5"
-               "4d4647f5000000000f88e3759a08e7a56663861eea1bf42ccd0572f200000057ca82ac5180101be4"
-               "005cef468ea086ea9aaafdc388811ec787bc45db50da49bf00000000d94beb259f9f0251becc9879"
-               "07879cca681ec7bb000000df248b49953d136cac00054b019402a4c2167721673f2b02b3256ead1f"
-               "02e9b69000000000eacce745ea213cb1a84035b632ef3be6071fafa202187470e9da55709427c226"
-               "0024d9a086ed27184b443181a5edc511c1507a142fea9bc10000000062fb81e733cff620ca7feefe"
-               "1933631e8e03f6d90000002f6e6904f57239c09c47150b9b14010469823acb72bb89182f93112196"
-               "cdb3455c00000000c66af76e80f062ea1a7e0b7be218d06753001b04000000bb9f2b8a1a8982c42f"
-               "53700a3437c5156e072b2f2fb337c9cfe07c34ddb83579b80000000072437f9b6245de39ec9e71ee"
-               "4b951507be002b340000000f895d5f24037d233302ad3b82d639272e4a46eadfbd2146bf8cfdb205"
-               "ebfdcec30000000067a982be8a934f852cc3d4d82bc0ec7303129f8f00000087965a316af2abb982"
-               "b001ec16901f55f960c55c22f32099a8020bea2a25a5927300000000c4458d70911714f1819aa46f"
-               "be143934c900ae6dec0fb124f264af67eed7c9a8c38b6b8ee2614344bea73223e59bf0193432e9fa"
-               "b4d54acb00000000a417f3da01d99dae5f190096cc582cabd51fd15a0000003edc5aa115165eb914"
-               "d8fbe9a4fdb3d117d76b0de5416f95e7bb75d72f4cc43c5100000000c2aac1d67a909b297c703915"
-               "312e9c3c5a03c40b20ea99d5ab90135fa636af4464d210a256be75e0509c4477d19ac64b2e69220d"
-               "288cd9bf00000000225a0c71b2ce78bb16104c8eaf18c565ec17fb112087b97cfcebbaeb0c565359"
-               "23f14874042a8aff1d1f8e5ec3cc13cc36bbe3c9b2cd3dbd00000000e958e48d2ab8b0313e7f8933"
-               "e315130fa9034c8288f83f6e70b1646d8026e9f1704f1b16286ba2dabc7ef0820c91ed33ef60a8b5"
-               "70ccc567df7a49b8a4c73cf5ffa4ffc8a4bab14874afdf54e60fe816a9fe07ca0675e09818e9bbec"
-               "0018c59068baf76ab08940f6fc3bbdc7f6090f7512ea3b9503000000fb95e8332fdfff658483a2d0"
-               "39a7bf148e01481ea9fec460e89f7abdb682380a00eae374835b4a49a2b980b6aba92da6409969aa"
-               "fd338f35030000006e8cc34573654ea6624f138ef9531cd9360002e4a9fe989477a363d55ad47b8a"
-               "11090df8da40d88d40a6d75bc404156225b7eedeb95c593103000000dbe9962069e66142e2275334"
-               "4fda8a3e0b001cf7a9fedd68269b53eccb44a831714acfb58c54dda9ac3bb1b1d43e6505621b9a7f"
-               "1e0f515603000000f57609453df7803357ae79989870ccec3d000ad9a9fee016663fe6ee0228c1da"
-               "5f5b481b9f05a16768cb3bc873b04d937a6ed8f7d7ac5f9903000000822a272bec4501a1e27acfee"
-               "7a8588ffd5016caea9fee59560fad65d67c624f285d7174177a47579dda0b6ead1e1b34901eaedd5"
-               "dc335c8b030000000a3c8e7c91bf821c4b09cee3c37ff4283a011480a9fec42cac4ef3ad378ffec7"
-               "085a62475588456e4ead7cbda620a3abae816e345d68265d020000009b7a559be3acf4fc36fa6b51"
-               "3cf09e8b99009462a9fe10acfce165e332a623840004e5bab49185ad9b7d0070a36dd51323b2c542"
-               "dedea2030200000002a1f8e6f5657f3f9e3eb807cbad76451002edb8a9fe628a70456d79f25d5895"
-               "0057fa60d9279fb2b8fcbac6abea74f7a6df7979b3980deb020000006c1de913df1424ce3ee1d76f"
-               "c1ea76e98c00fc78a9feb53e27c21963f5b4cb2454830c68e44ea74b4f2b83f9b3bee14d861a4c9e"
-               "bc74179402000000438a72e12ae0436a2495a609675344f7e200a5eba9fec85f0a1742f70abe95c5"
-               "0345132a61eda239d9d083c3bf085387046ef8a3b0916e5d020000004a503a712350acbd48411f0d"
-               "c15d2f0f4903d345a9fe06954d2471f00d3c723b91adb0da7c4aa7e7eb5a15bcde015fb98b841fd8"
-               "9a6ce5d902000000a6a5ee19d6f63fe9cbdd01f7246f13b2050524a2a9fe5ef7a029c896b4132bd8"
-               "95072cfeffe9d6812231bb02f922259b173c333c1dce2f83010000003a3efc240b9977e166bee737"
-               "fe73670c4301644ca9fe59b4cd20eb7dd3d38785008ffad45e0dfe491d1ba96742c7b5e02b227122"
-               "a9a89f8701000000fcdf6d9be94030b34774d1d7dddedd989901f627a9fe965a87041331b2834bcf"
-               "00b4e3ce848518c4e3f6cbf25e5e1b990fc46767e36f90d401000000fd41fe47684b370b6ec6584d"
-               "64496089570368eaa9feae91c819bb068196d59900de3246e43f5e7945aaf95e2ffa3a11b9ef1b21"
-               "74e7860401000000cd7db68b9ae586dacdb4070c50320427c4033d03a9fef22c636360d696be085a"
-               "007c499dd538d34574c038404923e75d0e09e2fcaefd545b010000007ced4f8b362956cd46646a0e"
-               "222160e5f700bb29a9feed37a2123c5b6a813387e99bb834a19da86c64fefab2548759d313a5b92d"
-               "c896c9a7010000000a5f6abd850b06e3d9662795ce2cc1cee01702aba9feaaa218dc091e9aa1477f"
-               "6fdc9830d7fbc95829a8838314dff34d24c332219a1b163a000000000e2f4f911a0dd8c052ed6b13"
-               "e2815a1a37002bdda9fef1018b587eed72bb58f000201f17bd99fcf72909ac9e7a55299e9bb4fd53"
-               "7666c05300000000ffa8e89871fb9510b900b2061d0c334b81022dd1a9fe4b0f9a60e0e289f08db3"
-               "00bafc4e095a3230fde69eec3af2e6d0bd68ab722cb749e2000000000eb4f73e75039fcf251b32fc"
-               "79685b05dd02aa9ba9fed2e40edb4d758fb554158ae5c7c00d58fbcc58fc4c2fd0d44ff1e51c3515"
-               "40b26f6600000000badfad0fe8fa8f3c1aad2a82530b7447d902f401a9fe2f4c329f997184c9d257"
-               "8e4cf9c48147507e1cd59ea1ae6da48b1eba6d16672d2fa500000000e276c5661a64192db44ce7af"
-               "5eb888e9eb03bb04a9fe943b101e1f553215abf8fbdf17c3641e447514ca565fb9b68c08a5e4351e"
-               "a2f5680800000000faa100fdfdd68d34be184524a6bdc8789c035178a9fe5bcc8da98489c55b8c12"
-               "87e6f7bb020ffdb0e612098610e52bb2a16a40089f3c39f200000000de745cb25851429b3bbfb501"
-               "68dfd04edf033542a9fe5f574c5cf8107ba282f4f983821918f93e742d08f0550b55726f2292362e"),
-        [1] = ("54270e46020000006a894e387625da376feecb80ac245409c058cc271d9c2f67179bff0644399ae7"
-               "9c64ed0432d599eaa660ac824b7fae389861419c7a899f2d010000009bf5a5d06776e66a9d80ab13"
-               "2e1ac921eb04adbb229df32e561fa80a40e55b8b1dd92e18c0cfb2b4386bec092fa5757ecde9348b"
-               "c4e1966800000000b3121cd5ef51e696c816290dbaee0e77264c82e1530ff5397c9125f59577d71c"
-               "4b258a005116d11354edff62ceaa458fc75a91c425a5927300000000c4458d70911714f1819aa46f"
-               "be143934c915ae6dec0fb124f264af67eed7c9a8c38b6b8ee2614344bea73223e59bf0193432e9fa"
-               "4cc43c5103000000c2aac1d67a909b297c703915312e9c3c5a64c40b20ea99d5ab90135fa636af44"
-               "64d210a256be75e0509c4477d19ac64b2e69220d12ea3b9501000000fb95e8332fdfff658483a2d0"
-               "39a7bf148e4c481e0bccc460e89f7abdb682380a62eae374835b4a49a2b980b6aba92da6409969aa"
-               "288cd9bf00000000225a0c71b2ce78bb16104c8eaf18c565ec5ffb112087b97cfcebbaeb0c565359"
-               "23f14874042a8aff1d1f8e5ec3cc13cc36bbe3c9bb0ec36f0000000064a2827b7cdd38d0314c178e"
-               "925b568ae566c4e9fefb24e5264fd3ebe9cf9f6df9615189f78ee815b2781ea55c9555876b6a3f13"
-               "02e9b69000000000eacce745ea213cb1a84035b632ef3be60755afa2fed87470e9da55709427c226"
-               "d324d9a086ed27184b443181a5edc511c1507a142fea9bc10300000062fb81e733cff620ca7feefe"
-               "1933631e8e18f6d9fed62d2c6e6904f57239c09c47150b9b14010469823acb72bb89182f93112196"
-               "9985d62700000000e491cbbbf9443fd6a77eb3c5bf64b671d6098dbffeff9b8306c83bc026977911"
-               "4bdade862c224f6f36506ebd455e679cef369bb8d514573f03000000f6932f6932d1d5f5d0ce2cf6"
-               "bf7a70ba5111a162fe92ec1b11d9172cf84a03478b82cdd8883b0f3fd6ed84d6c959e553b887edcd"
-               "6297dfa700000000fd0d53bb6215de4bb6f6f3d031a790287e7be9c1feb836c0a25ea71f5daaed4d"
-               "99eb2ebebe3c432fec7d3abc4ccf30d3aaaf02a41e0f515601000000f57609453df7803357ae7998"
-               "9870ccec3d4f0ad9000000000000000000000000000000019f05a16768cb3bc873b04d937a6ed8f7"
-               "ae8cb60b0300000038d856a40c67c3d4afa9c8649da90bd2ff4cf256000000000000000000000000"
-               "00000001955e1a13d1111b067acbce3e3bcdf38200154d12010000009edfa7a74c1c83c18e47935e"
-               "8354846e346e386a00000000000000000000000000000001ac26c7d41fd050bf8d85055bcfe3756c"
-               "a2f56808092069affaa100fdfdd68d34be184524a6bdc8789c575178000000000000000000000000"
-               "00000000020ffdb0e612098610e52bb2a16a40086f0e9e6903000000ec2e2924348352d715cf9d41"
-               "1ea012e5305a94b600000000000000000000000000000000ccd2f50c30706cae3fdcc2f0ee3d5e26"
-               "81ae55f0030000000aa20624fbab244735d67c61283031667b2274a1000000000000000000000000"
-               "0000000000459582f4ef43c780908872746e39efaefd545b020000007ced4f8b362956cd46646a0e"
-               "222160e5f749bb2900000000000000000000000000000000a19da86c64fefab2548759d313a5b92d"
-               "f05d7308020000009d82c856ba942455050f63f8282f34644466acf5000000000000000000000000"
-               "0000000069d9942f460bcea75481f25e307d0de96e91b91501000000be5b534e0a6527f67485ab35"
-               "aca0c7ee4167338500000000000000000000000000000000aefd4503189f7630248e501c9052c22b"
-               "5d68265d010000009b7a559be3acf4fc36fa6b513cf09e8b99729462000000000000000000000000"
-               "00000000b49185ad9b7d0070a36dd51323b2c542e5fe7d360100000066a3ae3939762df33a2d5506"
-               "0c78d551cb7410fd000000000000000000000000000000002c171ed15abbbe1cd911db7b56e8f3fe"
-               "9a6ce5d900000000a6a5ee19d6f63fe9cbdd01f7246f13b2055624a2000000000000000000000000"
-               "00000000ffe9d6812231bb02f922259b173c333cbae87d60000000005d31e5b2de252167c1c91b3a"
-               "36ba0c700e538477000000000000000000000000000000006ad0143a0554efb46b382ab0404cdf02"
-               "7c52a52d030000004e8e8bc17c01ac63eca0c1cfbb204132fb0813e4000000000000000000000000"
-               "00000000accae11f8045a80f855ebf3b772eb2b1d03bc90503000000eb760e3f004a2efc8ffc96b8"
-               "9b1bc8f415174e7700000000000000000000000000000000d149f074a378b881153ece68ade15cec"
-               "7666c05303000000ffa8e89871fb9510b900b2061d0c334b816a2dd1000000000000000000000000"
-               "00000000095a3230fde69eec3af2e6d0bd68ab7210613fc90200000016a31dc2e46a234558817151"
-               "b38fc9c9093c8d7100000000000000000000000000000000ae17bb4515856092b4d9e89b676761b2"
-               "57d6249b020000007dc7f17f666969e45c3baf7a119a65ee0b08fa9e000000000000000000000000"
-               "00000000b314f695bd82e9ea8891ed0a3d1b91cab56e03e0010000002f70249eefc67bce716c856e"
-               "3973dc42a1003be9000000000000000000000000000000006176f78a19bb8df1804c122fce5078c1"
-               "eecc89f601000000d635edea8ffe423d01883918039249f3984ab37e000000000000000000000000"
-               "000000001fb7c8307f06b4b29088f20d9ac676d5f90e54b501000000a779f37c30d2c5053c40ecae"
-               "210aeb0934392a2400000000000000000000000000000000df48f333ef467092818aa77d6732b678"
-               "bcd7be0700000000e45aaf61814f703b40125e6baf4648cbaa557b86000000000000000000000000"
-               "000000005ddb05f2a84e27c18a06e2c9cb29efdf50da49bf00000000d94beb259f9f0251becc9879"
-               "07879cca6857c7bb000000000000000000000000000000009402a4c2167721673f2b02b3256ead1f"
-               "a349e1cc00000000b879a45176740744fcb6a035339d504c972bd80d000000000000000000000000"
-               "000000009518862bc9a89192c1c8be55281a59d1ebfdcec30000000067a982be8a934f852cc3d4d8"
-               "2bc0ec7303579f8f00000000000000000000000000000000901f55f960c55c22f32099a8020bea2a"
-               "5b1aea3401000000c7c3523f76cf1ce34ca17a9a32f3f0f2187c4e48000000000000000000000000"
-               "00000002e6fbf952902383028e3b47f2111d891d536c75bb01000000720da9e07ac998b5690807d5"
-               "2617369ee16d4ca500000000000000000000ffff47d7b351a36b178aabc78a4a0d80684ee5dbf45b"
-               "17a4763102000000f9119c40b468b66331bca67a034f43340d2c683c00000000000000000000ffff"
-               "b24a5be9b10123e6e4f15ecd1dff045298cfee069c88b02a0300000004b0595daf37deb499996bfb"
-               "667f072dec755d9cdc8a11f4409bcfb3c167090e99d90a8543961b2ccd47724a3c460ba85183f4c5"
-               "a7afe28302000000d05ff720b16c73761a3fcb675219061ab466c79489b2cc6a883c146972decb8b"
-               "5adac8f5e6d66df46ea139107754dee62c36d2fcc7a4c65601000000cd625009a40aeb62a42b6a62"
-               "548e3c38b96620022753c5fbfaf1809becd2506315a6da29b2e94d3ab51fe0e9af98b180a36035fb"
-               "e36f90d401000000fd41fe47684b370b6ec6584d64496089574568ead4d1ae91c819bb068196d599"
-               "efde3246e43f5e7945aaf95e2ffa3a11b9ef1b21cdb3455c00000000c66af76e80f062ea1a7e0b7b"
-               "e218d067532d1b0487b8c3b99f2b8a1a8982c42f53700a3437c5156e072b2f2fb337c9cfe07c34dd"
-               "c896c9a7000000000a5f6abd850b06e3d9662795ce2cc1cee03202aba042aaa218dc091e9aa1477f"
-               "6fdc9830d7fbc95829a8838314dff34d24c332214d4647f5000000000f88e3759a08e7a56663861e"
-               "ea1bf42ccd1172f2ad063857ca82ac5180101be4f85cef468ea086ea9aaafdc388811ec787bc45db"
-               "7625ca4a00000000e682f80f94315c45ef817264c89a736ed580d637b077ddad3108be69f9b97d05"
-               "c917ad6b10e693bb6e26f2ba90c8e909e85e20e5b0916e5d030000004a503a712350acbd48411f0d"
-               "c15d2f0f4952d345599706954d2471f00d3c723b91adb0da7c4aa7e7eb5a15bcde015fb98b841fd8"
-               "ba8110fb03000000904f288262e6ea77a22fa5f863eaeefa700b120523bd98f29b098b43cd68be6e"
-               "3f81268193fd637e585287dc6ca972a2cfb18b2b3cae864b0200000091921681c2162b9bfa40546c"
-               "db544e44df587c2790d3119a051bb2ee8192ac0cfa3abc1ea0ab3e7d75a2f42b50c6a36340132378"
+        [0] = ("8d1e69c83dc21056b973972c17a47631c169f11ff9119c40b407b6630000007a034f43340d01683c"
+               "0045097aea4a849f060ddf57b24a5be9b10123e6e4f15ecd9c88b02a06d0dc5504b0595daf37deb4"
+               "99996bfb667f072dec025d9cdc8a11f4409bcfb3c167090e00d90a8543961b2ccd47724a3c460ba8"
+               "5183f4c5daae7d63ecc8fb1103000000fcebbaeb0c56535923f14874042a8aff1d018e5e000000cd"
+               "36bbe3c9bb0ec36f00e007bb64a2827b7cdd38d0314c178e925b568ae56bc4e92931f2d303000000"
+               "50b8d10ab700cc3a7cf51be0403b654ba2026808092069affaa100fdfdd68d3400184524a6bdc878"
+               "9cf851782d895bcc8da98489c55b8c1230d2c50503000000210aeb0934cb2a247000b96d06992b6a"
+               "5803d399000000d2df48f333ef467092008aa77d6732b678cf844ef5a943825fcd743f59bd14c89b"
+               "df6f628a03000000f25d5895fb57fa60d9279fb2b8fcbac6ab0274f70000007bf6989d2d6a909e98"
+               "00f7add5df2c93e05459507b5d8c6a0f466554d2ae4d52a89427c2260300000086ed27184b443181"
+               "a5edc511c1507a141e03515600000033f57609453df7803300ae79989870ccec3def0ad92749e016"
+               "663fe6ee0228c1da5f5b481b0300000068cb3bc873b04d937a6ed8f7bc51e5406603d0980000008b"
+               "92d3adc69a84eb47002400207799416af1f0a086fbd7e2f7dea0077ae9c0c0d9f264af6703000000"
+               "c38b6b8ee2614344bea73223e59bf0193402e9fa0000002d8ec7e4b89bf5a5d00076e66a9d80ab13"
+               "2e1ac921eb76adbb229df32e561fa80aabf85f0c0300000008d33a2ea67776d7f88d69c8c7e3b2d3"
+               "c902f05400000020abb42316c533d9c90088189c471a1ff2f0ce3ff66e782110125ae2ed74993ade"
+               "507e308d030000008dab2dade2ab2fc9c56a7c86be5f96228e02f2f4000000b32b9c161c36b745ce"
+               "00d06352cb2c354434819f4b248eb2b8e3ff1a5ffc386da337c27c15030000007e5c53e755c90fa0"
+               "9e9653ddcb1b14d2750d8a130000001734b49e955cbcf46d006a1818053da43ec5085d1df74d1e28"
+               "7271092897e26251de485b1303000000957c22721583c453d15ade4386ffd00b3600858900000013"
+               "f09e601d88d4cb0c004d15c69e89f94ef7b30486591030639eb0e99f7f53702f3c84c4eb03000000"
+               "87780281d47a32f2235376239daeaacb3c02864b000000ae91921681c2162b9bfa40546cdb544e44"
+               "df2d7c2790d3119a051bb2ee8192ac0cfa3abc1e0300000075a2f42b50c6a36340132378b95c5931"
+               "3b03ba640000002069e66142e22753344fda8a3e0b841cf76391dd68269b53eccb44a831714acfb5"
+               "c442139803000000cee64b38b56e03e0f121014a2f70249eef027bce0000006f3973dc42a1003be9"
+               "4f89d8eeb1f14d985d5bbe6c10085f3d6176f78a19bb8df1449a774903000000ec852d59f3d17232"
+               "edd86ad6c3103a6884029aef00000082594f24a17dba1ee7e143fd52599364e13468f80fd514573f"
+               "b572671cf6932f6909f4ecf103000000a1c748288ae21e371051852c972ea7954012884a00000045"
+               "fd13a6826a4c619f9037d7a754655354c47cf0f551e96770a754ac19ef820fe0eafb1ac403000000"
+               "3aa9986700b2a2b3ed257b8b0489f48f0500c8cac72105b327335a25a364d1015ffac03089f45539"
+               "e1b0178462156cd6b83579b8da27345a4d9ca9d20300000074e786047d8683facd7db68b9ae586da"
+               "cd03070c00000027c4dd3d031e33f22c636360d696be085aad7c499dd538d34574c038404923e75d"
+               "36506ebd03000000ef369bb8ebfdcec3f8c8fa6867a982be8a054f85000000d82bc0ec7303f99f8f"
+               "e8d07c84965a316af2abb982b001ec16901f55f960c55c22c896c9a7030000000a5f6abd850b06e3"
+               "d9662795ce2cc1cee00002ab000000a318dc091e9aa1477f6fdc9830d7fbc95829a8838314dff34d"
+               "24c332219a1b163a8483a2d0030000008ed3481e0bccc460e89f7abdb682380a6202e3740000004b"
+               "a2b980b6aba92da6409969aabae87d60e00473755d31e5b2de252167c1c91b3a891715df03000000"
+               "edae579e2e10c7f7a22ba0340436242cbe11248c000000174632d4f28aa28c08f80dd50c6712dfb4"
+               "d184e820a28fa772a19cdf6dd1c173f6bc7ef08203000000ef60a8b540b26f66761d1f13badfad0f"
+               "e8038f3c1aad2a82530b7447d9c2f401012f2f4c329f997184c9d2578e4cf9c48147507e1cd59ea1"
+               "6e8e99cd03000000644f0e8ab3121cd5ef51e696c816290dba010e77000000e1530ff5397c9125f5"
+               "9577d71c4b258a005116d11354edff62ceaa458fc75a91c45cfd5ce603000000152f2b70eaef7443"
+               "e0f79ab6902dde53361dec71000000254ac9588319558041fc62ed564153e17478f8c3344d89c0bb"
+               "5408c1b7d17fb084b16c7376030000005219061ab4c5c79489b2cc6a883c1469721dcb8b5adac8f5"
+               "e6d66df46ea139107754dee62c36d2fc70ccc567df7a49b8a4c73cf5ffa4ffc8acdca10f03000000"
+               "a36b178aabc78a4a0d80684ee5dbf45bba0210fb9733cc24904f288262e6ea77a22fa5f863eaeefa"
+               "701d120523bd98f29b098b43cd68be6e37686b5e030000006215de4bb6f6f3d031a790287e45e9c1"
+               "8b0236c0a25ea71f5daaed4d99eb2ebebe3c432fec7d3abc4ccf30d3aaaf02a4f05d7308b67f91bf"
+               "0408423e03000000886dc91e2e5b0f0b65843fc961f7ad18010a3c67d9714f19f1e41c073164d93b"
+               "1171719c7f81773302d007c983566139c695e0f4b661ee6b3cf09e8b02000000e79c10acfce165e3"
+               "32a62384ec04e5bab40285ad00000071a36dd51323b2c54200154d12f86c260a9edfa7a74c1c83c1"
+               "8e47935e8354846e27c219630200000054830c68e44ea74b4f2b83f9b3bee14d86034c9e000000f6"
+               "abba4a32d635edea00fe423d01883918039249f398f9b37ea0914651b2a16e642c19a09202000000"
+               "92defa453a3efc240b9977e166bee737fe10670c0000004c323b59b4cd20eb7d00d38785c88ffad4"
+               "5e0dfe491d1ba96742c7b5e02b227122fcd32a7f0200000023a5223a2fb2c0764e08db62a3db07d0"
+               "900260b922a7b4a1565e63d3b610c9df00e32703b0ed3a7d0f8b13cd736aaa72021a381fb50e2c42"
+               "54270e46020000006a894e387625da376feecb80ac245409c005cc271d9c2f67179bff0644399ae7"
+               "0064ed0432d599eaa660ac824b7fae389861419c50da49bf8c3b371502000000710c030690f5f18e"
+               "a125dbf7d7e93bd65c01a56dfdcfc1155f236c8b9c79a62000660bbf024b03ff0a8e27c405e64244"
+               "57393c6fd7ac5f99544e4b2702000000a7f810d249adc17647176b7c07d856e3b806efebc09dd9d6"
+               "1966b7ae7412041d00fe7d361f2d5dfd66a3ae3939762df33a2d55060c78d551fcb6a03502000000"
+               "9726d80d9c2d89df765b4d9a130257dc5f19f03a0000002bc9a89192c1c8be55281a59d1fd338f35"
+               "7e75f8ce6e8cc34573654ea6624f138e8c45eb5002000000562dcd24a27c8212127da516b1091d1d"
+               "4f0008170000000d32d823b0617718eb0f3195e2fe917423d314ae73572ff808b373f5b193d661ed"
+               "e45aaf610200000040125e6baf4648cbaaa37b861e6d46dafe1f6ec400000053010911915ddb05f2"
+               "a84e27c18a06e2c9cb29efdf672d2fa57fc85ebbe276c566804c122f0100000059747cf23a1349c8"
+               "ab2dadc5a39411fd4f03116d000000316a553fc28cff0bcd00ebe434a7ae8ff040483e310819e3b1"
+               "0db116431ec6f76949ba9be3010000000c67c3d4afa9c8649da90bd2fff2f2568101870b0000004b"
+               "70c6d18d6242b356005e1a13d1111b067acbce3e3bcdf3824f1b134847ff26ba32d1d5f501000000"
+               "bf7a70ba5193a162bc92ec1b11d9172cf8020347000000d8883b0f3fd6ed84d60059e553b887edcd"
+               "61c2f5d2f381f843b809d259e499db7d2b092583010000009f9f0251becc987907879cca68fec7bb"
+               "bb008edf000000973d136caca5054b010002a4c2167721673f2b02b3256ead1f2fea9bc12f0cfca1"
+               "62fb81e701000000ca7feefe1933631e8e69f6d9d6962d2c6e0304f50000009e47150b9b14010469"
+               "003acb72bb89182f931121966e91b9153b36bc60be5b534e1dff04520100000036674cdefb57b9ae"
+               "54cfe8f42bf0e9381b00c4396de60f032bc7f34de295987100d4c0d4eb720c4ae3c9472e5f2da5d9"
+               "c5cfd9f2343b21363f81268101000000585287dc6ca972a2cfb18b2b2cb749e2e002544d0eb4f73e"
+               "75039fcf251b32fc00685b05ddd3aa9ba511d2e40edb4d758fb554158ae5c7c0fde69eec01000000"
+               "bd68ab722af14548b29572e504265f6c72003e2200000079e208ed2e2ebb07710046a54cadb245ea"
+               "fde6b02567f814de39dd8f88152a5845a4bab14801000000e6aae816430607ca0675e09818e9bbec"
+               "5900c5900000006ab08940f6fc3bbdc700090f756aae660b3b02239efc1fb23d24438387def0f4c6"
+               "cbb110fd01000000b9aca07b6e4925dd27943a272c171ed15a02be1c0000007b56e8f3fededea203"
+               "00a8042602a1f8e6f5657f3f9e3eb807cbad76451055edb81539ce410100000018238487a417f3da"
+               "01d99dae5f190096cc0e2cab0000005b2363d13edc5aa115165eb914d8fbe9a4fdb3d117d76b0de5"
+               "416f95e7bb75d72fc8f1853b010000000e2b00b667b7b16dae02dfa0c26e7c3add003bd3000000f1"
+               "2f2f3313d8469d09a92409c0b7f0318a575a4f8e061db3dd7fde25654a4059d5d8a4c5cf01000000"
+               "fcc2056cc1ff8e51bbce426cfa4b861cc70192be7b14e7ba9c15acb881ae55f0f512dc1c0aa20624"
+               "fbab244735d67c61283031667b2d74a103e0b1b10100000090a2312e17f1a35900459582f4ef43c7"
+               "80038872000000efa9a89f87189b1d0afcdf6d9be94030b34774d1d7dddedd9899f0f627be16965a"
+               "8704133101000000d9b4e3ce848518c4e3f6cbf25e5e1b990f026767000000bba23eb66e720da9e0"
+               "7ac998b5690807d52617369ee1af4ca5a6a95581af5fd7ce732d803e010000001a0dd8c052ed6b13"
+               "e2815a1a37a52bddd106f101000000ed72bb58f0e9201f17bd99fcf72909ac9e7a55299e9bb4fd53"
+               "bc7417940fcffe3fb60291ea01000000def85b75827b26e8a0bef8675b29a197b71ecceaf5f1bb12"
+               "c535256ab0916e5d58099ffc4a503a712350acbd48411f0dc15d2f0f49dad34565dbc8a900000000"
+               "b550c5db56189572f32f4af595d785d5c117aad4000000f08bd7a09bdd214443008cd9bf66ec305a"
+               "225a0c71b2ce78bb16104c8eaf18c56587e6f7bb00000000e612098610e52bb2a16a4008aefd545b"
+               "7a019db50000008b362956cd46646a0e002160e5f769bb295766ed37a2123c5b6a813387e99bb834"
+               "b7a350f900000000fa7db4532545666937c22a3b6e9686dcc403cbee000000529ed8901d7a0165fe"
+               "00744edb6ba04f146e4c699341b95f41d9c1e67743efb54e4fc812bd000000006d064126666befbb"
+               "98c39e7cc282208fef007ff40000009751adcb22fd50793f007a3c7dadd1c86e759d0c90bc512e13"
+               "7dcef5e4a27985bd348352d7000000001ea012e5307294b6e346dac428ab20075e05e38d000000e6"
+               "ccd2f50c30706cae00dcc2f0ee3d5e2612ea3b95e1d2a5d0fb95e8332fdfff650a6527f600000000"
+               "aca0c7ee4197338581cafd63e66e3a23d61e257800000020aefd4503189f7630008e501c9052c22b"
+               "9f3c39f24b0572b1de745cb25851429bf32099a8000000004d4647f53b1921af0f88e3759a08e7a5"
+               "661f861eea1bf42ccd3c72f2ad0638570082ac5180101be4f85cef468ea086ea9aaafdc388811ec7"
+               "3bbfb50100000000df6b3542f4bb5f574c5cf8107ba282f4f9038219000000772d08f0550b55726f"
+               "2292362e5d68265d9f98c82d9b7a559be3acf4fc36fa6b513722d9b7000000001fb7c8307f06b4b2"
+               "9088f20d9ac676d55b00ea34000000fec7c3523f76cf1ce34ca17a9a32f3f0f218424e48836dd1cb"
+               "504e03d53e1124c5d9e1825f00000000284a024b7f9e85ff73e050a2cd89fd276e00cb8800000037"
+               "4ec3127c750fabdd69121aec129cdc690fcc00ac2daa755e5292cd32dc096cd5438a72e100000000"
+               "2495a609675344f7e2e3a5ebaec3c85f0a1242f7000000c70345132a61eda239d9d083c3bf085387"
+               "046ef8a36f0e9e6982d15956ec2e2924ae6da48b00000000bcfe97e1761e12457ad742d3d7320a4f"
+               "8800f47f5dd0cf69cb22840f09b3fe7749509cb6752b2cee30b7e7736a0afc9879ea40e69710fc9f"
+               "57d6249b000000007dc7f17f666969e45c3baf7a119a65ee0b1ffa9e000000f2de6dc8dfcf2a3ad3"
+               "bbcc0fa3b314f695bd82e9ea8891ed0a3d1b91caae8cb60bab90135f0000000064d210a256be75e0"
+               "509c4477d19ac64b2e03220d6c0fc09c25d6534c809829af4a9df58dbfef186d416f3a1ef170d10f"
+               "2c932e655203d82a87bb24e500000000e9cf9f6df9615189f78ee815b2781ea55c1755876b6a3f13"
+               "922b6cb8360feaac1d5c302df59dd32a677e5a3af4fe297f608df061d45d864db442548600000000"
+               "b0216050ed7afd741024e8365ea1aa11f90397c0c0ec468e9de5a9caf493472201e48d163106cc39"
+               "7446a33fdc335c8b152c507a0a3c8e7cc67b2e48b47003c81785d3ffdbd9a617857ae8126b028c0e"
+               "3e0f5354a9feb690c628d048eacce74500213cb1a84035b632ef3be6070dafa202187470e9da5570"
+               "40e55b8b03000000c0cfb2b4386bec092fa5757ecde9348bda012ebda9fea6aa667224dae775e5cc"
+               "0041aca2ff0f576767d3648102b61886b62f22ca33478e1ff9531cd903000000d241989477a363d5"
+               "5ad47b8a11090df8da00d88da9fed75bc404156225b7eedefc2b44574b15e2ae496ad01bf007eacb"
+               "0a28aec8682825108c54dda903000000d43e6505621b9a7f0422ae3fc8979ee0c400d794a9fe895e"
+               "5bfe47c6f53a54e01b0c1d89414d94d0e02d2ec587fc330003781b0f294a2b7387bc45db03000000"
+               "36bd39a44e8e8bc17c01ac63eca0c1cfbb004132a9fe13e4862573c2303dd1d65c7b2cffeca6d1ad"
+               "accae11f8045a80f855ebf3b772eb2b1182c028703000000ec4501a1e27acfee7a8588ffd5a06cae"
+               "4201e595a9fed65d67c624f285d7174177a47579dda0b6ead1e1b34901eaedd5bcd7be07d78e6222"
+               "d611d7b103000000f9f23b5f1570a4648a0ff80dd9a37a82e10120aaa9fe258532449deb7afb217d"
+               "58c0bb20de6f36d664873c5be82c5361358730bcde43782d87b8c3b9020000008982c42f53700a34"
+               "37c5156e072b2f2fb300c9cfa9fe34ddb3980deb14ab7b5a001de913df1424ce3ee1d76fc1ea76e9"
+               "8c9dfc78997ab53ec386e9c60200000010ee58bdc75183609985d627b6a4b1cbe402cbbba9fe3fd6"
+               "a77eb3c5bf64b67100f18dbf463f9b8306c83bc0269779114bdade862c224f6fdd65aeb202000000"
+               "902383028e3b47f2111d891d9f3c2c88cd007619a9fe44e644d7cf1d06b2b115d882549e62c12fba"
+               "5b1cdec48d10bbb13b8313d8cd2a24d3be46bae5020000006665a268abfe8a41d149f074a378b881"
+               "1500ce68a9fe5cec25a59273e7519f34c4458d70911714f1819aa46fbe143934c933ae6dec0fb124"
+               "59970695020000000d3c723b91adb0da7c4aa7e7eb5a15bcde035fb9a9fe1fd84cc43c510385b9a4"
+               "c2aac1d67a909b297c703915312e9c3c5a06c40b20ea99d5078a806d020000008bbfc9723107f3f5"
+               "b2cd3dbddc74e26de905e48da9feb0313e7f8933e315130fa9a84c8288f83f6e70b1646d8026e9f1"
+               "704f1b16286ba2da72437f9b01000000ec9e71ee4b951507beb42b3473cef00e89015f24a9fe2333"
+               "02ad3b82d639272e0046eadfbd2146bf8cfdb205f90e54b58a6ee136a779f37c0d58fbcc01000000"
+               "d0d44ff1e51c35157666c05348e6f507ff01e898a9fe9510b900b2061d0c334b009f2dd1cdf64b0f"
+               "9a60e0e289f08db3e4bafc4e095a3230b4d9e89b010000006855676c277cf1bd017c9148da5892bb"
+               "4903c3a7a9fe689d3391ce7d3fd48469005f0c233dbdd2a97835df1f1782dee88487fd0db5971b46"
+               "9d82c85601000000050f63f8282f3464443cacf59c6379a44b0392f4a9fea98cb121c19c69d9942f"
+               "000bcea75481f25e307d0de9a7afe283bd2f22ead05ff72045aaf95e01000000b9ef1b2110613fc9"
+               "f90adac616a31dc2e4002345a9fe7151b38fc9c909cf8d71e64b0bd761fdf274fc5dca8ce1bd1871"
+               "ae17bb451585609236ba0c700100000040318db913a353edc66efc068829c0e66a17143aa9feefb4"
+               "6b382ab0404cdf029a6ce5d906837dbca6a5ee19d6f63fe9cbdd01f7246f13b2050424a200000000"
+               "a029c896b4132bd895072cfeffe9d6812200bb02a9fe259b173c333cd03bc90500b899afeb760e3f"
+               "004a2efc8ffc96b89b1bc8f415bd4e77c7a4c65600000000cd625009a40aeb62a42b6a62548e3c38"
+               "b9022002a9fec5fbfaf1809becd2506300a6da29b2e94d3ab51fe0e9af98b180a36035fb6297dfa7"
+               "0e09e2fc00000000cc807cdce682f80f94315c45ef817264c802736ea9fed637b077ddad3108be69"
+               "f9b97d05c917ad6b10e693bb6e26f2ba90c8e909e85e20e591bf821c00000000c37ff4283a631480"
+               "91acc42cac4ef3ad3702fec7a9fe62475588456e4ead7cbda620a3abae816e34cba1d74f18d1c4ed"
+               "b99c0e49bcb946f71a64192d000000005eb888e9eb37bb046686943b101e1f553203abf8a9fe17c3"
+               "641e447514ca565fb9b68c08a5e4351ea349e1ccf662e058b879a45176740744a19da86c00000000"
+               "548759d313a5b92de36f90d4bc8b496afd03fe47a9fe370b6ec6584d64496089570968ead4d1ae91"
+               "c819bb068196d599efde3246e43f5e7934f0386a000000004064f6b021281e235f94ae20ac26c7d4"
+               "1f0350bfa9fe055bcfe3756ccdb3455c60ca7b11c66af76e80f062ea1a7e0b7be218d067530e1b04"),
+        [1] = ("4d9ca9d20300000074e786047d8683facd7db68b9ae586dacd0b070c50320427c4dd3d031e33f22c"
+               "636360d696be085aad7c499dd538d34574c038404923e75d30d2c50503000000210aeb0934cb2a24"
+               "7000b96d06992b6a5875d3995b9663d2df48f333ef467092818aa77d6732b678cf844ef5a943825f"
+               "cd743f59bd14c89bdf6f628a02000000f25d5895fb57fa60d9279fb2b8fcbac6ab6674f7a6df7979"
+               "f6989d2d6a909e986ff7add5df2c93e05459507b5d8c6a0f466554d2ae4d52a8de485b1301000000"
+               "957c22721583c453d15ade4386ffd00b36518589be395312f09e601d88d4cb0c2f4d15c69e89f94e"
+               "f7b30486591030639eb0e99f7f53702fa4bab14801000000e6aae816430607ca0675e09818e9bbec"
+               "5966c59068baf76ab08940f6fc3bbdc7f6090f756aae660b3b02239efc1fb23d24438387def0f4c6"
+               "36ba0c700000000040318db913a353edc66efc068829c0e66a32143a0554efb46b382ab0404cdf02"
+               "9a6ce5d906837dbca6a5ee19d6f63fe9cbdd01f7246f13b254270e46000000006a894e387625da37"
+               "6feecb80ac245409c06bcc271d9c2f67179bff0644399ae79c64ed0432d599eaa660ac824b7fae38"
+               "9861419c50da49bf5cfd5ce600000000152f2b70eaef7443e0f79ab6902dde53362fec7127e19f25"
+               "4ac9588319558041fc62ed564153e17478f8c3344d89c0bb5408c1b7d17fb084b16c737600000000"
+               "5219061ab4c5c79489b2cc6a883c14697280cb8b5adac8f5e6d66df46ea139107754dee62c36d2fc"
+               "70ccc567df7a49b8a4c73cf5ffa4ffc83722d9b7000000001fb7c8307f06b4b29088f20d9ac676d5"
+               "5b2dea34648cabfec7c3523f76cf1ce34ca17a9a32f3f0f218424e48836dd1cb504e03d53e1124c5"
+               "87bb24e500000000e9cf9f6df9615189f78ee815b2781ea55c5f55876b6a3f13922b6cb8360feaac"
+               "1d5c302df59dd32a677e5a3af4fe297f608df061d45d864d348352d7000000001ea012e5307294b6"
+               "e346dac428ab20075e11e38dd16f8ee4ccd2f50c30706cae3fdcc2f0ee3d5e2612ea3b95e1d2a5d0"
+               "fb95e8332fdfff65fa3abc1e0200000075a2f42b50c6a36340132378b95c59313b58ba64dbe99620"
+               "69e66142e22753344fda8a3e0b841cf76391dd68269b53eccb44a831714acfb5f32099a800000000"
+               "4d4647f53b1921af0f88e3759a08e7a56655861efedbf42ccd3c72f2ad063857ca82ac5180101be4"
+               "f85cef468ea086ea9aaafdc388811ec7c442139803000000cee64b38b56e03e0f121014a2f70249e"
+               "ef217bcefeec856e3973dc42a1003be94f89d8eeb1f14d985d5bbe6c10085f3d6176f78a19bb8df1"
+               "09f4ecf100000000a1c748288ae21e371051852c972ea7954066884afed7b044fd13a6826a4c619f"
+               "9037d7a754655354c47cf0f551e96770a754ac19ef820fe0732d803e000000001a0dd8c052ed6b13"
+               "e2815a1a37a52bddd109f101fed87eed72bb58f0e9201f17bd99fcf72909ac9e7a55299e9bb4fd53"
+               "bc7417940fcffe3feafb1ac4020000003aa9986700b2a2b3ed257b8b0489f48f055ac8cafea105b3"
+               "27335a25a364d1015ffac03089f45539e1b0178462156cd6b83579b8da27345a544e4b2700000000"
+               "a7f810d249adc17647176b7c07d856e3b87befebfe9dd9d61966b7ae7412041de5fe7d361f2d5dfd"
+               "66a3ae3939762df33a2d55060c78d551dd65aeb202000000902383028e3b47f2111d891d9f3c2c88"
+               "cd1676190000000000000000000000000000000162c12fba5b1cdec48d10bbb13b8313d8cd2a24d3"
+               "27c219630100000054830c68e44ea74b4f2b83f9b3bee14d866e4c9e000000000000000000000000"
+               "0000000101883918039249f398f9b37ea0914651b2a16e64a19da86c64fefab2548759d313a5b92d"
+               "e36f90d4bc8b496afd57fe4700000000000000000000000000000000d4d1ae91c819bb068196d599"
+               "efde3246e43f5e79507e308d030000008dab2dade2ab2fc9c56a7c86be5f96228e05f2f400000000"
+               "000000000000000000000000cb2c354434819f4b248eb2b8e3ff1a5ffc386da38483a2d003000000"
+               "8ed3481e0bccc460e89f7abdb682380a6208e37400000000000000000000000000000000bae87d60"
+               "e00473755d31e5b2de252167c1c91b3a87e6f7bb03000000e612098610e52bb2a16a4008aefd545b"
+               "7a219db500000000000000000000000000000000f769bb295766ed37a2123c5b6a813387e99bb834"
+               "bc7ef08203000000ef60a8b540b26f66761d1f13badfad0fe8178f3c000000000000000000000000"
+               "00000000329f997184c9d2578e4cf9c48147507e1cd59ea13f81268103000000585287dc6ca972a2"
+               "cfb18b2b2cb749e2e022544d00000000000000000000000000000000ddd3aa9ba511d2e40edb4d75"
+               "8fb554158ae5c7c09d82c85602000000050f63f8282f3464443cacf59c6379a44b8092f400000000"
+               "0000000000000000000000005481f25e307d0de9a7afe283bd2f22ead05ff72045aaf95e02000000"
+               "b9ef1b2110613fc9f90adac616a31dc2e44923450000000000000000000000000000000061fdf274"
+               "fc5dca8ce1bd1871ae17bb4515856092cbb110fd02000000b9aca07b6e4925dd27943a272c171ed1"
+               "5a66be1c0000000000000000000000000000000002a1f8e6f5657f3f9e3eb807cbad76451055edb8"
+               "5f5b481b0100000068cb3bc873b04d937a6ed8f7bc51e5406674d098000000000000000000000000"
+               "000000007799416af1f0a086fbd7e2f7dea0077ae9c0c0d9ecc8fb1101000000fcebbaeb0c565359"
+               "23f14874042a8aff1d008e5e0000000000000000000000000000000064a2827b7cdd38d0314c178e"
+               "925b568ae56bc4e9438a72e1000000002495a609675344f7e2e3a5ebaec3c85f0a5742f700000000"
+               "000000000000000000000000bf085387046ef8a36f0e9e6982d15956ec2e2924891715df00000000"
+               "edae579e2e10c7f7a22ba0340436242cbe53248c000000000000000000000000000000006712dfb4"
+               "d184e820a28fa772a19cdf6dd1c173f6e45aaf610000000040125e6baf4648cbaaa37b861e6d46da"
+               "fe556ec4000000000000000000000000000000008a06e2c9cb29efdf672d2fa57fc85ebbe276c566"
+               "8c45eb5003000000562dcd24a27c8212127da516b1091d1d4f5f0817000000000000000000000000"
+               "00000000fe917423d314ae73572ff808b373f5b193d661edbe46bae5030000006665a268abfe8a41"
+               "d149f074a378b8811526ce6800000000000000000000000000000000911714f1819aa46fbe143934"
+               "c933ae6dec0fb124fcd32a7f0300000023a5223a2fb2c0764e08db62a3db07d0907360b900000000"
+               "000000000000000000000000b0ed3a7d0f8b13cd736aaa72021a381fb50e2c421dff045203000000"
+               "36674cdefb57b9ae54cfe8f42bf0e9381b04c43900000000000000000000000000000000eb720c4a"
+               "e3c9472e5f2da5d9c5cfd9f2343b21363c84c4eb0200000087780281d47a32f2235376239daeaacb"
+               "3c54864b00000000000000000000000000000000db544e44df2d7c2790d3119a051bb2ee8192ac0c"
+               "804c122f0100000059747cf23a1349c8ab2dadc5a39411fd4f60116d000000000000000000000000"
+               "00000000a7ae8ff040483e310819e3b10db116431ec6f76987b8c3b9010000008982c42f53700a34"
+               "37c5156e072b2f2fb372c9cf00000000000000000000000000000000df1424ce3ee1d76fc1ea76e9"
+               "8c9dfc78997ab53ef9531cd901000000d241989477a363d55ad47b8a11090df8da18d88d00000000"
+               "0000000000000000000000004b15e2ae496ad01bf007eacb0a28aec86828251065dbc8a900000000"
+               "b550c5db56189572f32f4af595d785d5c10eaad40000000000000000000000000000000066ec305a"
+               "225a0c71b2ce78bb16104c8eaf18c56557d6249b000000007dc7f17f666969e45c3baf7a119a65ee"
+               "0b34fa9e00000000000000000000000000000000b314f695bd82e9ea8891ed0a3d1b91caae8cb60b"
+               "fcb6a035000000009726d80d9c2d89df765b4d9a130257dc5f2bf03a000000000000000000000000"
+               "00000000fd338f357e75f8ce6e8cc34573654ea6624f138e91bf821c03000000c37ff4283a631480"
+               "91acc42cac4ef3ad373bfec700000000000000000000000000000002ae816e34cba1d74f18d1c4ed"
+               "b99c0e49bcb946f734f0386a010000004064f6b021281e235f94ae20ac26c7d41f2a50bf00000000"
+               "000000000000000000000002c66af76e80f062ea1a7e0b7be218d067530e1b04c386e9c601000000"
+               "10ee58bdc75183609985d627b6a4b1cbe413cbbb00000000000000000000000000000002463f9b83"
+               "06c83bc0269779114bdade862c224f6f1a64192d010000005eb888e9eb37bb046686943b101e1f55"
+               "3273abf800000000000000000000000000000002a5e4351ea349e1ccf662e058b879a45176740744"
+               "32d1d5f503000000bf7a70ba5193a162bc92ec1b11d9172cf83a0347000000000000000000000000"
+               "00000002b887edcd61c2f5d2f381f843b809d259e499db7dfde69eec01000000bd68ab722af14548"
+               "b29572e504265f6c726d3e2200000000000000000000ffffdb46a54cadb245eafde6b02567f814de"
+               "39dd8f88152a584562fb81e702000000ca7feefe1933631e8e69f6d9d6962d2c6e5804f57239c09c"
+               "47150b9b14010469823acb72bb89182f931121966e91b9153b36bc60be5b534e6e8e99cd01000000"
+               "644f0e8ab3121cd5ef51e696c816290dba040e7726d082e1530ff5397c9125f59577d71c4b258a00"
+               "5116d11354edff62ceaa458fc75a91c4b4d9e89b010000006855676c277cf1bd017c9148da5892bb"
+               "4945c3a7ed55689d3391ce7d3fd48469845f0c233dbdd2a97835df1f1782dee88487fd0db5971b46"
+               "2c19a0920000000092defa453a3efc240b9977e166bee737fe1c670c439a644c323b59b4cd20eb7d"
+               "d3d38785c88ffad45e0dfe491d1ba96742c7b5e02b227122ab90135f0300000064d210a256be75e0"
+               "509c4477d19ac64b2e64220d6c0fc09c25d6534c809829af4a9df58dbfef186d416f3a1ef170d10f"
+               "2c932e655203d82a59970695030000000d3c723b91adb0da7c4aa7e7eb5a15bcde525fb98b841fd8"
+               "4cc43c510385b9a4c2aac1d67a909b297c703915312e9c3c5a06c40b20ea99d58c3b371502000000"
+               "710c030690f5f18ea125dbf7d7e93bd65c03a56dfdcfc1155f236c8b9c79a6205f660bbf024b03ff"
+               "0a8e27c405e6424457393c6fd7ac5f9940e55b8b01000000c0cfb2b4386bec092fa5757ecde9348b"
+               "da4c2ebd4f78a6aa667224dae775e5ccd141aca2ff0f576767d3648102b61886b62f22ca33478e1f"
+               "abf85f0c0100000008d33a2ea67776d7f88d69c8c7e3b2d3c954f054e93f8120abb42316c533d9c9"
+               "4d88189c471a1ff2f0ce3ff66e782110125ae2ed74993adeae6da48b00000000bcfe97e1761e1245"
+               "7ad742d3d7320a4f8815f47f5dd0cf69cb22840f09b3fe7749509cb6752b2cee30b7e7736a0afc98"
+               "79ea40e69710fc9f0408423e00000000886dc91e2e5b0f0b65843fc961f7ad18014c3c67d9714f19"
+               "f1e41c073164d93b1171719c7f81773302d007c983566139c695e0f4b661ee6bc896c9a703000000"
+               "0a5f6abd850b06e3d9662795ce2cc1cee00802abfec2aaa218dc091e9aa1477f6fdc9830d7fbc958"
+               "29a8838314dff34d24c332219a1b163a3bbfb50103000000df6b3542f4bb5f574c5cf8107ba282f4"
+               "f9188219fef93e742d08f0550b55726f2292362e5d68265d9f98c82d9b7a559be3acf4fc36fa6b51"
+               "c8f1853b030000000e2b00b667b7b16dae02dfa0c26e7c3add113bd3febfa0f02f2f3313d8469d09"
+               "a92409c0b7f0318a575a4f8e061db3dd7fde25654a4059d5b442548601000000b0216050ed7afd74"
+               "1024e8365ea1aa11f92797c0feac468e9de5a9caf493472201e48d163106cc397446a33fdc335c8b"
+               "152c507a0a3c8e7cacdca10f01000000a36b178aabc78a4a0d80684ee5dbf45bba3010fbfeb3cc24"
+               "904f288262e6ea77a22fa5f863eaeefa701d120523bd98f29b098b43cd68be6e37c27c1500000000"
+               "7e5c53e755c90fa09e9653ddcb1b14d2757f8a13feac3c1734b49e955cbcf46d256a1818053da43e"
+               "c5085d1df74d1e287271092897e2625103e0b1b10100000090a2312e17f1a35900459582f4ef43c7"
+               "807b887200000000000000000000000000000001e94030b34774d1d7dddedd9899f0f627be16965a"
+               "87bc45db0100000036bd39a44e8e8bc17c01ac63eca0c1cfbb4f4132000000000000000000000000"
+               "00000001eca6d1adaccae11f8045a80f855ebf3b772eb2b1449a774903000000ec852d59f3d17232"
+               "edd86ad6c3103a68844c9aef00000000000000000000000000000001599364e13468f80fd514573f"
+               "b572671cf6932f69d9e1825f03000000284a024b7f9e85ff73e050a2cd89fd276e0acb8800000000"
+               "000000000000000000000001129cdc690fcc00ac2daa755e5292cd32dc096cd572437f9b02000000"
+               "ec9e71ee4b951507beb42b3473cef00e896b5f2400000000000000000000000000000001bd2146bf"
+               "8cfdb205f90e54b58a6ee136a779f37c2931f2d30300000050b8d10ab700cc3a7cf51be0403b654b"
+               "a242680800000000000000000000000000000000a6bdc8789cf851782d895bcc8da98489c55b8c12"
+               "37686b5e030000006215de4bb6f6f3d031a790287e45e9c18b7136c0000000000000000000000000"
+               "00000000ec7d3abc4ccf30d3aaaf02a4f05d7308b67f91bfc7a4c65603000000cd625009a40aeb62"
+               "a42b6a62548e3c38b96a200200000000000000000000000000000000b2e94d3ab51fe0e9af98b180"
+               "a36035fb6297dfa78704133102000000d9b4e3ce848518c4e3f6cbf25e5e1b990f13676700000000"
+               "000000000000000000000000690807d52617369ee1af4ca5a6a95581af5fd7ceb7a350f902000000"
+               "fa7db4532545666937c22a3b6e9686dcc43ccbee000000000000000000000000000000006ba04f14"
+               "6e4c699341b95f41d9c1e67743efb54e3cf09e8b01000000e79c10acfce165e332a62384ec04e5ba"
+               "b46785ad00000000000000000000000000000000f86c260a9edfa7a74c1c83c18e47935e8354846e"
+               "4fc812bd010000006d064126666befbb98c39e7cc282208fef4a7ff4000000000000000000000000"
+               "00000000add1c86e759d0c90bc512e137dcef5e4a27985bdd8a4c5cf01000000fcc2056cc1ff8e51"
+               "bbce426cfa4b861cc73992be000000000000000000000000000000000aa20624fbab244735d67c61"
+               "283031667b2d74a10d58fbcc01000000d0d44ff1e51c35157666c05348e6f507ff72e89800000000"
+               "000000000000000000000000cdf64b0f9a60e0e289f08db3e4bafc4e095a32302b09258301000000"
+               "9f9f0251becc987907879cca68fec7bbbb4d8edf0000000000000000000000000000000016772167"
+               "3f2b02b3256ead1f2fea9bc12f0cfca1078a806d000000008bbfc9723107f3f5b2cd3dbddc74e26d"
+               "e956e48d0000000000000000000000000000000088f83f6e70b1646d8026e9f1704f1b16286ba2da"
+               "0a6527f600000000aca0c7ee4197338581cafd63e66e3a23d6572578000000000000000000000000"
+               "000000009052c22b9f3c39f24b0572b1de745cb25851429b8c54dda900000000d43e6505621b9a7f"
+               "0422ae3fc8979ee0c400d79400000000000000000000000000000000414d94d0e02d2ec587fc3300"
+               "03781b0f294a2b73050424a203000000a029c896b4132bd895072cfeffe9d6812232bb0200000000"
+               "000000000000000000000000eb760e3f004a2efc8ffc96b89b1bc8f415bd4e77d611d7b103000000"
+               "f9f23b5f1570a4648a0ff80dd9a37a82e10c20aa00000000000000000000000000000000de6f36d6"
+               "64873c5be82c5361358730bcde43782df264af6703000000c38b6b8ee2614344bea73223e59bf019"
+               "345ae9fa000000000000000000000000000000009d80ab132e1ac921eb76adbb229df32e561fa80a"
+               "49ba9be3020000000c67c3d4afa9c8649da90bd2fff2f2568108870b000000000000000000000000"
+               "00000000d1111b067acbce3e3bcdf3824f1b134847ff26ba0e09e2fc01000000cc807cdce682f80f"
+               "94315c45ef817264c83c736e00000000000000000000000000000000c917ad6b10e693bb6e26f2ba"
+               "90c8e909e85e20e5182c028701000000ec4501a1e27acfee7a8588ffd5a06cae4228e59500000000"
+               "000000000000000000000000dda0b6ead1e1b34901eaedd5bcd7be07d78e62229427c22601000000"
+               "86ed27184b443181a5edc511c1507a141e4a5156000000000000000000000000000000009870ccec"
+               "3def0ad92749e016663fe6ee0228c1da1539ce410000000018238487a417f3da01d99dae5f190096"
+               "cc292cab00000000000000000000000000000000d8fbe9a4fdb3d117d76b0de5416f95e7bb75d72f"
+               "b60291ea00000000def85b75827b26e8a0bef8675b29a197b71bccea000000000000000000000000"
+               "000000004a503a712350acbd48411f0dc15d2f0f49dad3458d1e69c801000000b973972c17a47631"
+               "c169f11ff9119c40b47cb66300000000000000000000000000000002ea4a849f060ddf57b24a5be9"
+               "b10123e6e4f15ecdc67b2e48000000001785d3ffdbd9a617857ae8126b028c0e3e4b535400000000"
+               "000000000000000000000002a84035b632ef3be6070dafa202187470e9da55709c88b02a02000000"
+               "04b0595daf37deb499996bfb667f072dec2c5d9c00000000000000000000ffff99d90a8543961b2c"
+               "cd47724a3c460ba85183f4c5daae7d6336506ebd00000000ef369bb8ebfdcec3f8c8fa6867a982be"
+               "8a534f8500000000000000000000ffffe8d07c84965a316af2abb982b001ec16901f55f960c55c22"),
+        [2] = ("df6f628a02000000f25d5895fb57fa60d9279fb2b8fcbac6ab6674f7a6df7979f6989d2d6a909e98"
+               "6ff7add5df2c93e05459507b5d8c6a0f466554d2ae4d52a8fa3abc1e0200000075a2f42b50c6a363"
+               "40132378b95c59313b58ba64dbe9962069e66142e22753344fda8a3e0b841cf76391dd68269b53ec"
+               "cb44a831714acfb554270e46000000006a894e387625da376feecb80ac245409c06bcc271d9c2f67"
+               "179bff0644399ae79c64ed0432d599eaa660ac824b7fae389861419c50da49bfb16c737600000000"
+               "5219061ab4c5c79489b2cc6a883c14697280cb8b5adac8f5e6d66df46ea139107754dee62c36d2fc"
+               "70ccc567df7a49b8a4c73cf5ffa4ffc83722d9b7000000001fb7c8307f06b4b29088f20d9ac676d5"
+               "5b2dea34648cabfec7c3523f76cf1ce34ca17a9a32f3f0f218424e48836dd1cb504e03d53e1124c5"
+               "87bb24e500000000e9cf9f6df9615189f78ee815b2781ea55c5f55876b6a3f13922b6cb8360feaac"
+               "1d5c302df59dd32a677e5a3af4fe297f608df061d45d864d4d9ca9d20300000074e786047d8683fa"
+               "cd7db68b9ae586dacd0b070c50320427c4dd3d031e33f22c636360d696be085aad7c499dd538d345"
+               "74c038404923e75d30d2c50503000000210aeb0934cb2a247000b96d06992b6a5875d3995b9663d2"
+               "df48f333ef467092818aa77d6732b678cf844ef5a943825fcd743f59bd14c89bde485b1301000000"
+               "957c22721583c453d15ade4386ffd00b36518589be395312f09e601d88d4cb0c2f4d15c69e89f94e"
+               "f7b30486591030639eb0e99f7f53702fa4bab14801000000e6aae816430607ca0675e09818e9bbec"
+               "5966c59068baf76ab08940f6fc3bbdc7f6090f756aae660b3b02239efc1fb23d24438387def0f4c6"
+               "36ba0c700000000040318db913a353edc66efc068829c0e66a32143a0554efb46b382ab0404cdf02"
+               "9a6ce5d906837dbca6a5ee19d6f63fe9cbdd01f7246f13b25cfd5ce600000000152f2b70eaef7443"
+               "e0f79ab6902dde53362fec7127e19f254ac9588319558041fc62ed564153e17478f8c3344d89c0bb"
+               "5408c1b7d17fb084348352d7000000001ea012e5307294b6e346dac428ab20075e11e38dd16f8ee4"
+               "ccd2f50c30706cae3fdcc2f0ee3d5e2612ea3b95e1d2a5d0fb95e8332fdfff65c442139803000000"
+               "cee64b38b56e03e0f121014a2f70249eef217bcefeec856e3973dc42a1003be94f89d8eeb1f14d98"
+               "5d5bbe6c10085f3d6176f78a19bb8df109f4ecf100000000a1c748288ae21e371051852c972ea795"
+               "4066884afed7b044fd13a6826a4c619f9037d7a754655354c47cf0f551e96770a754ac19ef820fe0"
+               "f32099a8000000004d4647f53b1921af0f88e3759a08e7a56655861efedbf42ccd3c72f2ad063857"
+               "ca82ac5180101be4f85cef468ea086ea9aaafdc388811ec7732d803e000000001a0dd8c052ed6b13"
+               "e2815a1a37a52bddd109f101fed87eed72bb58f0e9201f17bd99fcf72909ac9e7a55299e9bb4fd53"
+               "bc7417940fcffe3f544e4b2700000000a7f810d249adc17647176b7c07d856e3b87befebfe9dd9d6"
+               "1966b7ae7412041de5fe7d361f2d5dfd66a3ae3939762df33a2d55060c78d551eafb1ac402000000"
+               "3aa9986700b2a2b3ed257b8b0489f48f055ac8cafea105b327335a25a364d1015ffac03089f45539"
+               "e1b0178462156cd6b83579b8da27345add65aeb202000000902383028e3b47f2111d891d9f3c2c88"
+               "cd1676190000000000000000000000000000000162c12fba5b1cdec48d10bbb13b8313d8cd2a24d3"
+               "27c219630100000054830c68e44ea74b4f2b83f9b3bee14d866e4c9e000000000000000000000000"
+               "0000000101883918039249f398f9b37ea0914651b2a16e648483a2d0030000008ed3481e0bccc460"
+               "e89f7abdb682380a6208e37400000000000000000000000000000000bae87d60e00473755d31e5b2"
+               "de252167c1c91b3a8c45eb5003000000562dcd24a27c8212127da516b1091d1d4f5f081700000000"
+               "000000000000000000000000fe917423d314ae73572ff808b373f5b193d661edbe46bae503000000"
+               "6665a268abfe8a41d149f074a378b8811526ce6800000000000000000000000000000000911714f1"
+               "819aa46fbe143934c933ae6dec0fb124fcd32a7f0300000023a5223a2fb2c0764e08db62a3db07d0"
+               "907360b900000000000000000000000000000000b0ed3a7d0f8b13cd736aaa72021a381fb50e2c42"
+               "1dff04520300000036674cdefb57b9ae54cfe8f42bf0e9381b04c439000000000000000000000000"
+               "00000000eb720c4ae3c9472e5f2da5d9c5cfd9f2343b21363c84c4eb0200000087780281d47a32f2"
+               "235376239daeaacb3c54864b00000000000000000000000000000000db544e44df2d7c2790d3119a"
+               "051bb2ee8192ac0c804c122f0100000059747cf23a1349c8ab2dadc5a39411fd4f60116d00000000"
+               "000000000000000000000000a7ae8ff040483e310819e3b10db116431ec6f76987b8c3b901000000"
+               "8982c42f53700a3437c5156e072b2f2fb372c9cf00000000000000000000000000000000df1424ce"
+               "3ee1d76fc1ea76e98c9dfc78997ab53eecc8fb1101000000fcebbaeb0c56535923f14874042a8aff"
+               "1d008e5e0000000000000000000000000000000064a2827b7cdd38d0314c178e925b568ae56bc4e9"
+               "f9531cd901000000d241989477a363d55ad47b8a11090df8da18d88d000000000000000000000000"
+               "000000004b15e2ae496ad01bf007eacb0a28aec86828251065dbc8a900000000b550c5db56189572"
+               "f32f4af595d785d5c10eaad40000000000000000000000000000000066ec305a225a0c71b2ce78bb"
+               "16104c8eaf18c56557d6249b000000007dc7f17f666969e45c3baf7a119a65ee0b34fa9e00000000"
+               "000000000000000000000000b314f695bd82e9ea8891ed0a3d1b91caae8cb60b891715df00000000"
+               "edae579e2e10c7f7a22ba0340436242cbe53248c000000000000000000000000000000006712dfb4"
+               "d184e820a28fa772a19cdf6dd1c173f6fcb6a035000000009726d80d9c2d89df765b4d9a130257dc"
+               "5f2bf03a00000000000000000000000000000000fd338f357e75f8ce6e8cc34573654ea6624f138e"
+               "a19da86c64fefab2548759d313a5b92de36f90d4bc8b496afd57fe47000000000000000000000000"
+               "00000000d4d1ae91c819bb068196d599efde3246e43f5e79507e308d030000008dab2dade2ab2fc9"
+               "c56a7c86be5f96228e05f2f400000000000000000000000000000000cb2c354434819f4b248eb2b8"
+               "e3ff1a5ffc386da387e6f7bb03000000e612098610e52bb2a16a4008aefd545b7a219db500000000"
+               "000000000000000000000000f769bb295766ed37a2123c5b6a813387e99bb834bc7ef08203000000"
+               "ef60a8b540b26f66761d1f13badfad0fe8178f3c00000000000000000000000000000000329f9971"
+               "84c9d2578e4cf9c48147507e1cd59ea13f81268103000000585287dc6ca972a2cfb18b2b2cb749e2"
+               "e022544d00000000000000000000000000000000ddd3aa9ba511d2e40edb4d758fb554158ae5c7c0"
+               "9d82c85602000000050f63f8282f3464443cacf59c6379a44b8092f4000000000000000000000000"
+               "000000005481f25e307d0de9a7afe283bd2f22ead05ff72045aaf95e02000000b9ef1b2110613fc9"
+               "f90adac616a31dc2e44923450000000000000000000000000000000061fdf274fc5dca8ce1bd1871"
+               "ae17bb4515856092cbb110fd02000000b9aca07b6e4925dd27943a272c171ed15a66be1c00000000"
+               "00000000000000000000000002a1f8e6f5657f3f9e3eb807cbad76451055edb85f5b481b01000000"
+               "68cb3bc873b04d937a6ed8f7bc51e5406674d098000000000000000000000000000000007799416a"
+               "f1f0a086fbd7e2f7dea0077ae9c0c0d9438a72e1000000002495a609675344f7e2e3a5ebaec3c85f"
+               "0a5742f700000000000000000000000000000000bf085387046ef8a36f0e9e6982d15956ec2e2924"
+               "e45aaf610000000040125e6baf4648cbaaa37b861e6d46dafe556ec4000000000000000000000000"
+               "000000008a06e2c9cb29efdf672d2fa57fc85ebbe276c56691bf821c03000000c37ff4283a631480"
+               "91acc42cac4ef3ad373bfec700000000000000000000000000000002ae816e34cba1d74f18d1c4ed"
+               "b99c0e49bcb946f732d1d5f503000000bf7a70ba5193a162bc92ec1b11d9172cf83a034700000000"
+               "000000000000000000000002b887edcd61c2f5d2f381f843b809d259e499db7d34f0386a01000000"
+               "4064f6b021281e235f94ae20ac26c7d41f2a50bf00000000000000000000000000000002c66af76e"
+               "80f062ea1a7e0b7be218d067530e1b04c386e9c60100000010ee58bdc75183609985d627b6a4b1cb"
+               "e413cbbb00000000000000000000000000000002463f9b8306c83bc0269779114bdade862c224f6f"
+               "1a64192d010000005eb888e9eb37bb046686943b101e1f553273abf8000000000000000000000000"
+               "00000002a5e4351ea349e1ccf662e058b879a45176740744fde69eec01000000bd68ab722af14548"
+               "b29572e504265f6c726d3e2200000000000000000000ffffdb46a54cadb245eafde6b02567f814de"
+               "39dd8f88152a5845ab90135f0300000064d210a256be75e0509c4477d19ac64b2e64220d6c0fc09c"
+               "25d6534c809829af4a9df58dbfef186d416f3a1ef170d10f2c932e655203d82a5997069503000000"
+               "0d3c723b91adb0da7c4aa7e7eb5a15bcde525fb98b841fd84cc43c510385b9a4c2aac1d67a909b29"
+               "7c703915312e9c3c5a06c40b20ea99d562fb81e702000000ca7feefe1933631e8e69f6d9d6962d2c"
+               "6e5804f57239c09c47150b9b14010469823acb72bb89182f931121966e91b9153b36bc60be5b534e"
                "8c3b371502000000710c030690f5f18ea125dbf7d7e93bd65c03a56dfdcfc1155f236c8b9c79a620"
-               "5f660bbf024b03ff0a8e27c405e6424457393c6fdac12ebd01000000667224dae775e5ccd141aca2"
-               "ff0f57676754648102b61886b62f22ca33478e1f891715df7fc6a902edae579e2e10c7f7a22ba034"
-               "abf85f0c0100000008d33a2ea67776d7f88d69c8c7e3b2d3c951f054e93f8120abb42316c533d9c9"
-               "4d88189c471a1ff2f0ce3ff66e782110125ae2eddaae7d6300000000c0982b283aa9986700b2a2b3"
-               "ed257b8b042ff48f053ec8cac72105b327335a25a364d1015ffac03089f45539e1b0178462156cd6"
-               "6855676c00000000017c9148da5892bb4951c3a7ed55689d336bce7d3fd48469845f0c233dbdd2a9"
-               "7835df1f1782dee88487fd0db5971b46b7a350f998c39e7c00000000efc57ff447036b9551adcb22"
-               "fd50793f791c3c7dadd1c86e759d0c90bc512e137dcef5e4a27985bd8d1e69c83dc21056b973972c"
-               "bc51e540030000006c3d048a92d3adc69a84eb47622400207708416afef0a086fbd7e2f7dea0077a"
-               "e9c0c0d9c386e9c6e6a2cbfa10ee58bdc75183600422ae3f03000000c43bd794c697895e5bfe47c6"
-               "f53a54e01b211d89fecd94d0e02d2ec587fc330003781b0f294a2b73c4421398f4de67e9cee64b38"
-               "69121aec010000000fcc00ac2daa755e5292cd32dc096cd5d830c5cffe91ee1cfcc2056cc1ff8e51"
-               "bbce426cfa4b861cc78592be7b14e7ba9c15acb836674cde0200000054cfe8f42bf0e9381ba2c439"
-               "6de60f032b5af34dfe959871c0d4c0d4eb720c4ae3c9472e5f2da5d9c5cfd9f2343b21362c19a092"
-               "b2cd3dbd01000000e958e48d2ab8b0313e7f8933e315130fa9274c82feb83f6e70b1646d8026e9f1"
-               "704f1b16286ba2dabc7ef0820c91ed33ef60a8b5bcfe97e1000000007ad742d3d7320a4f880cf47f"
-               "5dd0cf69cb7f840ffeb3fe7749509cb6752b2cee30b7e7736a0afc9879ea40e69710fc9f6e8e99cd"
-               "1dce2f83020000003a3efc240b9977e166bee737fe73670c436b644c000000000000000000000000"
-               "000000015e0dfe491d1ba96742c7b5e02b227122cf844ef501000000cd743f59bd14c89b5cfd5ce6"
-               "e82e7854157b2b700000000000000000000000000000000127e19f254ac9588319558041fc62ed56"
-               "b83579b80300000072437f9b6245de39ec9e71ee4b951507be0a2b34000000000000000000000000"
-               "00000001d639272e4a46eadfbd2146bf8cfdb205b3980deb020000006c1de913df1424ce3ee1d76f"
-               "c1ea76e98c16fc7800000000000000000000000000000001e44ea74b4f2b83f9b3bee14d861a4c9e"
-               "0436242c03000000ee3fbc164632d4f28aa28c08f80dd50c6705dfb4000000000000000000000000"
-               "00000000078a806d329c9b008bbfc9723107f3f52af145480300000004265f6c72923e22594f3f79"
-               "e208ed2e2e7107710000000000000000000000000000000039dd8f88152a58454d9ca9d20f45dfa7"
-               "922b6cb8030000001d5c302df59dd32a677e5a3af4fe297f6042f061000000000000000000000000"
-               "000000008ae21e371051852c972ea7954079884a9a1b163a030000000e2f4f911a0dd8c052ed6b13"
-               "e2815a1a37322bdd00000000000000000000000000000000bd99fcf72909ac9e7a55299e9bb4fd53"
-               "9f3c2c8803000000f51f44e644d7cf1d06b2b115d882549e62042fba000000000000000000000000"
-               "000000004fc812bd2f8a59d96d064126666befbbf257b044030000006a4c619f9037d7a754655354"
-               "c47cf0f5512167700000000000000000000000000000000050b8d10ab700cc3a7cf51be0403b654b"
-               "74e7860402000000cd7db68b9ae586dacdb4070c50320427c4803d03000000000000000000000000"
-               "00000000d538d34574c038404923e75d0e09e2fcfc1519de010000002545666937c22a3b6e9686dc"
-               "c405cbee504dc252000000000000000000000000000000006e4c699341b95f41d9c1e67743efb54e"
-               "6aae660b01000000fc1fb23d24438387def0f4c6544e4b275d4a7146000000000000000000000000"
-               "00000000b883efebc09dd9d61966b7ae7412041dd7ac5f9901000000822a272bec4501a1e27acfee"
-               "7a8588ffd5286cae0000000000000000000000000000000077a47579dda0b6ead1e1b34901eaedd5"
-               "61c2f5d200000000b809d259e499db7dc8f1853bdcb0e4bc0e0e00b6000000000000000000000000"
-               "000000005e7fa0f02f2f3313d8469d09a92409c0b4d54acb00000000a417f3da01d99dae5f190096"
-               "cc582cabd534d15a00000000000000000000000000000000fdb3d117d76b0de5416f95e7bb75d72f"
-               "fc2b445700000000496ad01bf007eacb0a28aec868282510b61b91ea000000000000000000000000"
-               "000000005b29a197b7b3cceaf5f1bb12c535256a74993ade03000000e62112898dab2dade2ab2fc9"
-               "c56a7c86be73962200000000000000000000000000000000bdd06352cb2c354434819f4b248eb2b8"
-               "dc335c8b030000000a3c8e7c91bf821c4b09cee3c37ff4283a0c1480000000000000000000000000"
-               "000000005588456e4ead7cbda620a3abae816e34e3ff1a5f03000000b4425486c619147eb0216050"
-               "ed7afd74105fe83600000000000000000000000000000000f493472201e48d163106cc397446a33f"
-               "bc74179403000000438a72e12ae0436a2495a609675344f7e226a5eb000000000000000000000000"
-               "0000000061eda239d9d083c3bf085387046ef8a34153e174020000004d89c0bb5408c1b7d17fb084"
-               "d9e1825fa613e1af000000000000000000000000000000006e08cb887464b9344ec3127c750fabdd"
-               "6c0fc09c02000000809829af4a9df58dbfef186d416f3a1ef154d10f000000000000000000000000"
-               "0000000087780281d47a32f2235376239daeaacba9a89f8701000000fcdf6d9be94030b34774d1d7"
-               "dddedd989972f62700000000000000000000000000000000848518c4e3f6cbf25e5e1b990fc46767"
-               "fd338f35010000006e8cc34573654ea6624f138ef9531cd9361802e4000000000000000000000000"
-               "00000000da40d88d40a6d75bc404156225b7eede2cb749e2010000000eb4f73e75039fcf251b32fc"
-               "79685b05dd3caa9b000000000000000000000000000000000d58fbcc58fc4c2fd0d44ff1e51c3515"
-               "b7f0318a01000000061db3dd7fde25654a4059d565dbc8a91e004457000000000000000000000000"
-               "00000000c1b5aad487c192f08bd7a09bdd21444359747cf200000000ab2dadc5a39411fd4ff1116d"
-               "478987316a293fc2000000000000000000000000000000000819e3b10db116431ec6f7691539ce41"
-               "b95c593100000000dbe9962069e66142e22753344fda8a3e0b001cf7000000000000000000000000"
-               "000000008c54dda9ac3bb1b1d43e6505621b9a7f672d2fa501000000e276c5661a64192db44ce7af"
-               "5eb888e9eb73bb0400000000000000000000000000000002641e447514ca565fb9b68c08a5e4351e"
-               "9f3c39f201000000de745cb25851429b3bbfb50168dfd04edf2a3542000000000000000000000000"
-               "0000000218f93e742d08f0550b55726f2292362e70ccc56700000000a4c73cf5ffa4ffc8a4bab148"
-               "74afdf54e64be8160000000000000000000000000000000268baf76ab08940f6fc3bbdc7f6090f75"
-               "4f1b134803000000449a774929e86716ec852d59f3d17232ed3a6ad6000000000000000000000000"
-               "000000027dba1ee7e143fd52599364e13468f80f40b26f6603000000badfad0fe8fa8f3c1aad2a82"
-               "530b7447d93bf401000000000000000000000000000000028147507e1cd59ea1ae6da48b1eba6d16"
-               "dedea2030100000002a1f8e6f5657f3f9e3eb807cbad76451013edb8000000000000000000000000"
-               "00000002d9279fb2b8fcbac6abea74f7a6df7979f6989d2d000000006ff7add5df2c93e05459507b"
-               "5d8c6a0f465354d200000000000000000000ffff1785d3ffdbd9a617857ae8126b028c0e3e295354"),
-        [2] = ("4cc43c5103000000c2aac1d67a909b297c703915312e9c3c5a64c40b20ea99d5ab90135fa636af44"
-               "64d210a256be75e0509c4477d19ac64b2e69220d54270e46020000006a894e387625da376feecb80"
-               "ac245409c058cc271d9c2f67179bff0644399ae79c64ed0432d599eaa660ac824b7fae389861419c"
-               "12ea3b9501000000fb95e8332fdfff658483a2d039a7bf148e4c481e0bccc460e89f7abdb682380a"
-               "62eae374835b4a49a2b980b6aba92da6409969aa7a899f2d010000009bf5a5d06776e66a9d80ab13"
-               "2e1ac921eb04adbb229df32e561fa80a40e55b8b1dd92e18c0cfb2b4386bec092fa5757ecde9348b"
-               "288cd9bf00000000225a0c71b2ce78bb16104c8eaf18c565ec5ffb112087b97cfcebbaeb0c565359"
-               "23f14874042a8aff1d1f8e5ec3cc13cc36bbe3c9c4e1966800000000b3121cd5ef51e696c816290d"
-               "baee0e77264c82e1530ff5397c9125f59577d71c4b258a005116d11354edff62ceaa458fc75a91c4"
-               "25a5927300000000c4458d70911714f1819aa46fbe143934c915ae6dec0fb124f264af67eed7c9a8"
-               "c38b6b8ee2614344bea73223e59bf0193432e9fa2fea9bc10300000062fb81e733cff620ca7feefe"
-               "1933631e8e18f6d9fed62d2c6e6904f57239c09c47150b9b14010469823acb72bb89182f93112196"
-               "9985d62700000000e491cbbbf9443fd6a77eb3c5bf64b671d6098dbffeff9b8306c83bc026977911"
-               "4bdade862c224f6f36506ebd455e679cef369bb8bb0ec36f0000000064a2827b7cdd38d0314c178e"
-               "925b568ae566c4e9fefb24e5264fd3ebe9cf9f6df9615189f78ee815b2781ea55c9555876b6a3f13"
-               "02e9b69000000000eacce745ea213cb1a84035b632ef3be60755afa2fed87470e9da55709427c226"
-               "d324d9a086ed27184b443181a5edc511c1507a14d514573f03000000f6932f6932d1d5f5d0ce2cf6"
-               "bf7a70ba5111a162fe92ec1b11d9172cf84a03478b82cdd8883b0f3fd6ed84d6c959e553b887edcd"
-               "6297dfa700000000fd0d53bb6215de4bb6f6f3d031a790287e7be9c1feb836c0a25ea71f5daaed4d"
-               "99eb2ebebe3c432fec7d3abc4ccf30d3aaaf02a4ae8cb60b0300000038d856a40c67c3d4afa9c864"
-               "9da90bd2ff4cf25600000000000000000000000000000001955e1a13d1111b067acbce3e3bcdf382"
-               "00154d12010000009edfa7a74c1c83c18e47935e8354846e346e386a000000000000000000000000"
-               "00000001ac26c7d41fd050bf8d85055bcfe3756c1e0f515601000000f57609453df7803357ae7998"
-               "9870ccec3d4f0ad9000000000000000000000000000000019f05a16768cb3bc873b04d937a6ed8f7"
-               "7c52a52d030000004e8e8bc17c01ac63eca0c1cfbb204132fb0813e4000000000000000000000000"
-               "00000000accae11f8045a80f855ebf3b772eb2b1d03bc90503000000eb760e3f004a2efc8ffc96b8"
-               "9b1bc8f415174e7700000000000000000000000000000000d149f074a378b881153ece68ade15cec"
-               "6f0e9e6903000000ec2e2924348352d715cf9d411ea012e5305a94b6000000000000000000000000"
-               "00000000ccd2f50c30706cae3fdcc2f0ee3d5e267666c05303000000ffa8e89871fb9510b900b206"
-               "1d0c334b816a2dd100000000000000000000000000000000095a3230fde69eec3af2e6d0bd68ab72"
-               "81ae55f0030000000aa20624fbab244735d67c61283031667b2274a1000000000000000000000000"
-               "0000000000459582f4ef43c780908872746e39ef10613fc90200000016a31dc2e46a234558817151"
-               "b38fc9c9093c8d7100000000000000000000000000000000ae17bb4515856092b4d9e89b676761b2"
-               "57d6249b020000007dc7f17f666969e45c3baf7a119a65ee0b08fa9e000000000000000000000000"
-               "00000000b314f695bd82e9ea8891ed0a3d1b91ca6e91b91501000000be5b534e0a6527f67485ab35"
-               "aca0c7ee4167338500000000000000000000000000000000aefd4503189f7630248e501c9052c22b"
-               "5d68265d010000009b7a559be3acf4fc36fa6b513cf09e8b99729462000000000000000000000000"
-               "00000000b49185ad9b7d0070a36dd51323b2c542b56e03e0010000002f70249eefc67bce716c856e"
-               "3973dc42a1003be9000000000000000000000000000000006176f78a19bb8df1804c122fce5078c1"
-               "e5fe7d360100000066a3ae3939762df33a2d55060c78d551cb7410fd000000000000000000000000"
-               "000000002c171ed15abbbe1cd911db7b56e8f3feeecc89f601000000d635edea8ffe423d01883918"
-               "039249f3984ab37e000000000000000000000000000000001fb7c8307f06b4b29088f20d9ac676d5"
-               "f90e54b501000000a779f37c30d2c5053c40ecae210aeb0934392a24000000000000000000000000"
-               "00000000df48f333ef467092818aa77d6732b678bcd7be0700000000e45aaf61814f703b40125e6b"
-               "af4648cbaa557b86000000000000000000000000000000005ddb05f2a84e27c18a06e2c9cb29efdf"
-               "50da49bf00000000d94beb259f9f0251becc987907879cca6857c7bb000000000000000000000000"
-               "000000009402a4c2167721673f2b02b3256ead1fa349e1cc00000000b879a45176740744fcb6a035"
-               "339d504c972bd80d000000000000000000000000000000009518862bc9a89192c1c8be55281a59d1"
-               "bae87d60000000005d31e5b2de252167c1c91b3a36ba0c700e538477000000000000000000000000"
-               "000000006ad0143a0554efb46b382ab0404cdf02ebfdcec30000000067a982be8a934f852cc3d4d8"
-               "2bc0ec7303579f8f00000000000000000000000000000000901f55f960c55c22f32099a8020bea2a"
-               "a2f56808092069affaa100fdfdd68d34be184524a6bdc8789c575178000000000000000000000000"
-               "00000000020ffdb0e612098610e52bb2a16a4008aefd545b020000007ced4f8b362956cd46646a0e"
-               "222160e5f749bb2900000000000000000000000000000000a19da86c64fefab2548759d313a5b92d"
-               "f05d7308020000009d82c856ba942455050f63f8282f34644466acf5000000000000000000000000"
-               "0000000069d9942f460bcea75481f25e307d0de99a6ce5d900000000a6a5ee19d6f63fe9cbdd01f7"
-               "246f13b2055624a200000000000000000000000000000000ffe9d6812231bb02f922259b173c333c"
-               "5b1aea3401000000c7c3523f76cf1ce34ca17a9a32f3f0f2187c4e48000000000000000000000000"
-               "00000002e6fbf952902383028e3b47f2111d891d17a4763102000000f9119c40b468b66331bca67a"
-               "034f43340d2c683c00000000000000000000ffffb24a5be9b10123e6e4f15ecd1dff045298cfee06"
-               "536c75bb01000000720da9e07ac998b5690807d52617369ee16d4ca500000000000000000000ffff"
-               "47d7b351a36b178aabc78a4a0d80684ee5dbf45bb0916e5d030000004a503a712350acbd48411f0d"
-               "c15d2f0f4952d345599706954d2471f00d3c723b91adb0da7c4aa7e7eb5a15bcde015fb98b841fd8"
-               "ba8110fb03000000904f288262e6ea77a22fa5f863eaeefa700b120523bd98f29b098b43cd68be6e"
-               "3f81268193fd637e585287dc6ca972a2cfb18b2b3cae864b0200000091921681c2162b9bfa40546c"
-               "db544e44df587c2790d3119a051bb2ee8192ac0cfa3abc1ea0ab3e7d75a2f42b50c6a36340132378"
-               "8c3b371502000000710c030690f5f18ea125dbf7d7e93bd65c03a56dfdcfc1155f236c8b9c79a620"
-               "5f660bbf024b03ff0a8e27c405e6424457393c6fdac12ebd01000000667224dae775e5ccd141aca2"
-               "ff0f57676754648102b61886b62f22ca33478e1f891715df7fc6a902edae579e2e10c7f7a22ba034"
-               "abf85f0c0100000008d33a2ea67776d7f88d69c8c7e3b2d3c951f054e93f8120abb42316c533d9c9"
-               "4d88189c471a1ff2f0ce3ff66e782110125ae2edc7a4c65601000000cd625009a40aeb62a42b6a62"
-               "548e3c38b96620022753c5fbfaf1809becd2506315a6da29b2e94d3ab51fe0e9af98b180a36035fb"
-               "daae7d6300000000c0982b283aa9986700b2a2b3ed257b8b042ff48f053ec8cac72105b327335a25"
-               "a364d1015ffac03089f45539e1b0178462156cd66855676c00000000017c9148da5892bb4951c3a7"
-               "ed55689d336bce7d3fd48469845f0c233dbdd2a97835df1f1782dee88487fd0db5971b46b7a350f9"
-               "cdb3455c00000000c66af76e80f062ea1a7e0b7be218d067532d1b0487b8c3b99f2b8a1a8982c42f"
-               "53700a3437c5156e072b2f2fb337c9cfe07c34ddc896c9a7000000000a5f6abd850b06e3d9662795"
-               "ce2cc1cee03202aba042aaa218dc091e9aa1477f6fdc9830d7fbc95829a8838314dff34d24c33221"
-               "98c39e7c00000000efc57ff447036b9551adcb22fd50793f791c3c7dadd1c86e759d0c90bc512e13"
-               "7dcef5e4a27985bd8d1e69c83dc21056b973972c7625ca4a00000000e682f80f94315c45ef817264"
-               "c89a736ed580d637b077ddad3108be69f9b97d05c917ad6b10e693bb6e26f2ba90c8e909e85e20e5"
-               "9c88b02a0300000004b0595daf37deb499996bfb667f072dec755d9cdc8a11f4409bcfb3c167090e"
-               "99d90a8543961b2ccd47724a3c460ba85183f4c5a7afe28302000000d05ff720b16c73761a3fcb67"
-               "5219061ab466c79489b2cc6a883c146972decb8b5adac8f5e6d66df46ea139107754dee62c36d2fc"
-               "e36f90d401000000fd41fe47684b370b6ec6584d64496089574568ead4d1ae91c819bb068196d599"
-               "efde3246e43f5e7945aaf95e2ffa3a11b9ef1b214d4647f5000000000f88e3759a08e7a56663861e"
-               "ea1bf42ccd1172f2ad063857ca82ac5180101be4f85cef468ea086ea9aaafdc388811ec787bc45db"
-               "0422ae3f03000000c43bd794c697895e5bfe47c6f53a54e01b211d89fecd94d0e02d2ec587fc3300"
-               "03781b0f294a2b73c4421398f4de67e9cee64b38bc51e540030000006c3d048a92d3adc69a84eb47"
-               "622400207708416afef0a086fbd7e2f7dea0077ae9c0c0d9c386e9c6e6a2cbfa10ee58bdc7518360"
-               "36674cde0200000054cfe8f42bf0e9381ba2c4396de60f032b5af34dfe959871c0d4c0d4eb720c4a"
-               "e3c9472e5f2da5d9c5cfd9f2343b21362c19a092b2cd3dbd01000000e958e48d2ab8b0313e7f8933"
-               "e315130fa9274c82feb83f6e70b1646d8026e9f1704f1b16286ba2dabc7ef0820c91ed33ef60a8b5"
-               "bcfe97e1000000007ad742d3d7320a4f880cf47f5dd0cf69cb7f840ffeb3fe7749509cb6752b2cee"
-               "30b7e7736a0afc9879ea40e69710fc9f6e8e99cd69121aec010000000fcc00ac2daa755e5292cd32"
-               "dc096cd5d830c5cffe91ee1cfcc2056cc1ff8e51bbce426cfa4b861cc78592be7b14e7ba9c15acb8"
-               "b83579b80300000072437f9b6245de39ec9e71ee4b951507be0a2b34000000000000000000000000"
-               "00000001d639272e4a46eadfbd2146bf8cfdb205b3980deb020000006c1de913df1424ce3ee1d76f"
-               "c1ea76e98c16fc7800000000000000000000000000000001e44ea74b4f2b83f9b3bee14d861a4c9e"
-               "cf844ef501000000cd743f59bd14c89b5cfd5ce6e82e7854157b2b70000000000000000000000000"
-               "0000000127e19f254ac9588319558041fc62ed561dce2f83020000003a3efc240b9977e166bee737"
-               "fe73670c436b644c000000000000000000000000000000015e0dfe491d1ba96742c7b5e02b227122"
-               "2af145480300000004265f6c72923e22594f3f79e208ed2e2e710771000000000000000000000000"
-               "0000000039dd8f88152a58454d9ca9d20f45dfa774993ade03000000e62112898dab2dade2ab2fc9"
-               "c56a7c86be73962200000000000000000000000000000000bdd06352cb2c354434819f4b248eb2b8"
-               "922b6cb8030000001d5c302df59dd32a677e5a3af4fe297f6042f061000000000000000000000000"
-               "000000008ae21e371051852c972ea7954079884a9f3c2c8803000000f51f44e644d7cf1d06b2b115"
-               "d882549e62042fba000000000000000000000000000000004fc812bd2f8a59d96d064126666befbb"
-               "dc335c8b030000000a3c8e7c91bf821c4b09cee3c37ff4283a0c1480000000000000000000000000"
-               "000000005588456e4ead7cbda620a3abae816e34e3ff1a5f03000000b4425486c619147eb0216050"
-               "ed7afd74105fe83600000000000000000000000000000000f493472201e48d163106cc397446a33f"
-               "f257b044030000006a4c619f9037d7a754655354c47cf0f551216770000000000000000000000000"
-               "0000000050b8d10ab700cc3a7cf51be0403b654bbc74179403000000438a72e12ae0436a2495a609"
-               "675344f7e226a5eb0000000000000000000000000000000061eda239d9d083c3bf085387046ef8a3"
-               "4153e174020000004d89c0bb5408c1b7d17fb084d9e1825fa613e1af000000000000000000000000"
-               "000000006e08cb887464b9344ec3127c750fabdd6c0fc09c02000000809829af4a9df58dbfef186d"
-               "416f3a1ef154d10f0000000000000000000000000000000087780281d47a32f2235376239daeaacb"
-               "74e7860402000000cd7db68b9ae586dacdb4070c50320427c4803d03000000000000000000000000"
-               "00000000d538d34574c038404923e75d0e09e2fca9a89f8701000000fcdf6d9be94030b34774d1d7"
-               "dddedd989972f62700000000000000000000000000000000848518c4e3f6cbf25e5e1b990fc46767"
-               "fd338f35010000006e8cc34573654ea6624f138ef9531cd9361802e4000000000000000000000000"
-               "00000000da40d88d40a6d75bc404156225b7eede2cb749e2010000000eb4f73e75039fcf251b32fc"
-               "79685b05dd3caa9b000000000000000000000000000000000d58fbcc58fc4c2fd0d44ff1e51c3515"
-               "6aae660b01000000fc1fb23d24438387def0f4c6544e4b275d4a7146000000000000000000000000"
-               "00000000b883efebc09dd9d61966b7ae7412041db7f0318a01000000061db3dd7fde25654a4059d5"
-               "65dbc8a91e00445700000000000000000000000000000000c1b5aad487c192f08bd7a09bdd214443"
-               "61c2f5d200000000b809d259e499db7dc8f1853bdcb0e4bc0e0e00b6000000000000000000000000"
-               "000000005e7fa0f02f2f3313d8469d09a92409c0b4d54acb00000000a417f3da01d99dae5f190096"
-               "cc582cabd534d15a00000000000000000000000000000000fdb3d117d76b0de5416f95e7bb75d72f"
-               "59747cf200000000ab2dadc5a39411fd4ff1116d478987316a293fc2000000000000000000000000"
-               "000000000819e3b10db116431ec6f7691539ce41b95c593100000000dbe9962069e66142e2275334"
-               "4fda8a3e0b001cf7000000000000000000000000000000008c54dda9ac3bb1b1d43e6505621b9a7f"
-               "fc2b445700000000496ad01bf007eacb0a28aec868282510b61b91ea000000000000000000000000"
-               "000000005b29a197b7b3cceaf5f1bb12c535256a0436242c03000000ee3fbc164632d4f28aa28c08"
-               "f80dd50c6705dfb400000000000000000000000000000000078a806d329c9b008bbfc9723107f3f5"
-               "9a1b163a030000000e2f4f911a0dd8c052ed6b13e2815a1a37322bdd000000000000000000000000"
-               "00000000bd99fcf72909ac9e7a55299e9bb4fd53fc1519de010000002545666937c22a3b6e9686dc"
-               "c405cbee504dc252000000000000000000000000000000006e4c699341b95f41d9c1e67743efb54e"
-               "d7ac5f9901000000822a272bec4501a1e27acfee7a8588ffd5286cae000000000000000000000000"
-               "0000000077a47579dda0b6ead1e1b34901eaedd54f1b134803000000449a774929e86716ec852d59"
-               "f3d17232ed3a6ad6000000000000000000000000000000027dba1ee7e143fd52599364e13468f80f"
-               "40b26f6603000000badfad0fe8fa8f3c1aad2a82530b7447d93bf401000000000000000000000000"
-               "000000028147507e1cd59ea1ae6da48b1eba6d16dedea2030100000002a1f8e6f5657f3f9e3eb807"
-               "cbad76451013edb800000000000000000000000000000002d9279fb2b8fcbac6abea74f7a6df7979"
-               "672d2fa501000000e276c5661a64192db44ce7af5eb888e9eb73bb04000000000000000000000000"
-               "00000002641e447514ca565fb9b68c08a5e4351e9f3c39f201000000de745cb25851429b3bbfb501"
-               "68dfd04edf2a35420000000000000000000000000000000218f93e742d08f0550b55726f2292362e"
-               "70ccc56700000000a4c73cf5ffa4ffc8a4bab14874afdf54e64be816000000000000000000000000"
-               "0000000268baf76ab08940f6fc3bbdc7f6090f75f6989d2d000000006ff7add5df2c93e05459507b"
-               "5d8c6a0f465354d200000000000000000000ffff1785d3ffdbd9a617857ae8126b028c0e3e295354"),
+               "5f660bbf024b03ff0a8e27c405e6424457393c6fd7ac5f996e8e99cd01000000644f0e8ab3121cd5"
+               "ef51e696c816290dba040e7726d082e1530ff5397c9125f59577d71c4b258a005116d11354edff62"
+               "ceaa458fc75a91c440e55b8b01000000c0cfb2b4386bec092fa5757ecde9348bda4c2ebd4f78a6aa"
+               "667224dae775e5ccd141aca2ff0f576767d3648102b61886b62f22ca33478e1fabf85f0c01000000"
+               "08d33a2ea67776d7f88d69c8c7e3b2d3c954f054e93f8120abb42316c533d9c94d88189c471a1ff2"
+               "f0ce3ff66e782110125ae2ed74993adeb4d9e89b010000006855676c277cf1bd017c9148da5892bb"
+               "4945c3a7ed55689d3391ce7d3fd48469845f0c233dbdd2a97835df1f1782dee88487fd0db5971b46"
+               "ae6da48b00000000bcfe97e1761e12457ad742d3d7320a4f8815f47f5dd0cf69cb22840f09b3fe77"
+               "49509cb6752b2cee30b7e7736a0afc9879ea40e69710fc9f2c19a0920000000092defa453a3efc24"
+               "0b9977e166bee737fe1c670c439a644c323b59b4cd20eb7dd3d38785c88ffad45e0dfe491d1ba967"
+               "42c7b5e02b2271220408423e00000000886dc91e2e5b0f0b65843fc961f7ad18014c3c67d9714f19"
+               "f1e41c073164d93b1171719c7f81773302d007c983566139c695e0f4b661ee6bc896c9a703000000"
+               "0a5f6abd850b06e3d9662795ce2cc1cee00802abfec2aaa218dc091e9aa1477f6fdc9830d7fbc958"
+               "29a8838314dff34d24c332219a1b163a3bbfb50103000000df6b3542f4bb5f574c5cf8107ba282f4"
+               "f9188219fef93e742d08f0550b55726f2292362e5d68265d9f98c82d9b7a559be3acf4fc36fa6b51"
+               "c8f1853b030000000e2b00b667b7b16dae02dfa0c26e7c3add113bd3febfa0f02f2f3313d8469d09"
+               "a92409c0b7f0318a575a4f8e061db3dd7fde25654a4059d5acdca10f01000000a36b178aabc78a4a"
+               "0d80684ee5dbf45bba3010fbfeb3cc24904f288262e6ea77a22fa5f863eaeefa701d120523bd98f2"
+               "9b098b43cd68be6e37c27c15000000007e5c53e755c90fa09e9653ddcb1b14d2757f8a13feac3c17"
+               "34b49e955cbcf46d256a1818053da43ec5085d1df74d1e287271092897e26251b442548601000000"
+               "b0216050ed7afd741024e8365ea1aa11f92797c0feac468e9de5a9caf493472201e48d163106cc39"
+               "7446a33fdc335c8b152c507a0a3c8e7c449a774903000000ec852d59f3d17232edd86ad6c3103a68"
+               "844c9aef00000000000000000000000000000001599364e13468f80fd514573fb572671cf6932f69"
+               "d9e1825f03000000284a024b7f9e85ff73e050a2cd89fd276e0acb88000000000000000000000000"
+               "00000001129cdc690fcc00ac2daa755e5292cd32dc096cd572437f9b02000000ec9e71ee4b951507"
+               "beb42b3473cef00e896b5f2400000000000000000000000000000001bd2146bf8cfdb205f90e54b5"
+               "8a6ee136a779f37c87bc45db0100000036bd39a44e8e8bc17c01ac63eca0c1cfbb4f413200000000"
+               "000000000000000000000001eca6d1adaccae11f8045a80f855ebf3b772eb2b103e0b1b101000000"
+               "90a2312e17f1a35900459582f4ef43c7807b887200000000000000000000000000000001e94030b3"
+               "4774d1d7dddedd9899f0f627be16965a050424a203000000a029c896b4132bd895072cfeffe9d681"
+               "2232bb0200000000000000000000000000000000eb760e3f004a2efc8ffc96b89b1bc8f415bd4e77"
+               "d611d7b103000000f9f23b5f1570a4648a0ff80dd9a37a82e10c20aa000000000000000000000000"
+               "00000000de6f36d664873c5be82c5361358730bcde43782df264af6703000000c38b6b8ee2614344"
+               "bea73223e59bf019345ae9fa000000000000000000000000000000009d80ab132e1ac921eb76adbb"
+               "229df32e561fa80ac7a4c65603000000cd625009a40aeb62a42b6a62548e3c38b96a200200000000"
+               "000000000000000000000000b2e94d3ab51fe0e9af98b180a36035fb6297dfa749ba9be302000000"
+               "0c67c3d4afa9c8649da90bd2fff2f2568108870b00000000000000000000000000000000d1111b06"
+               "7acbce3e3bcdf3824f1b134847ff26ba8704133102000000d9b4e3ce848518c4e3f6cbf25e5e1b99"
+               "0f13676700000000000000000000000000000000690807d52617369ee1af4ca5a6a95581af5fd7ce"
+               "b7a350f902000000fa7db4532545666937c22a3b6e9686dcc43ccbee000000000000000000000000"
+               "000000006ba04f146e4c699341b95f41d9c1e67743efb54e0e09e2fc01000000cc807cdce682f80f"
+               "94315c45ef817264c83c736e00000000000000000000000000000000c917ad6b10e693bb6e26f2ba"
+               "90c8e909e85e20e5182c028701000000ec4501a1e27acfee7a8588ffd5a06cae4228e59500000000"
+               "000000000000000000000000dda0b6ead1e1b34901eaedd5bcd7be07d78e62223cf09e8b01000000"
+               "e79c10acfce165e332a62384ec04e5bab46785ad00000000000000000000000000000000f86c260a"
+               "9edfa7a74c1c83c18e47935e8354846e4fc812bd010000006d064126666befbb98c39e7cc282208f"
+               "ef4a7ff400000000000000000000000000000000add1c86e759d0c90bc512e137dcef5e4a27985bd"
+               "9427c2260100000086ed27184b443181a5edc511c1507a141e4a5156000000000000000000000000"
+               "000000009870ccec3def0ad92749e016663fe6ee0228c1da0d58fbcc01000000d0d44ff1e51c3515"
+               "7666c05348e6f507ff72e89800000000000000000000000000000000cdf64b0f9a60e0e289f08db3"
+               "e4bafc4e095a32302b092583010000009f9f0251becc987907879cca68fec7bbbb4d8edf00000000"
+               "000000000000000000000000167721673f2b02b3256ead1f2fea9bc12f0cfca1078a806d00000000"
+               "8bbfc9723107f3f5b2cd3dbddc74e26de956e48d0000000000000000000000000000000088f83f6e"
+               "70b1646d8026e9f1704f1b16286ba2da0a6527f600000000aca0c7ee4197338581cafd63e66e3a23"
+               "d6572578000000000000000000000000000000009052c22b9f3c39f24b0572b1de745cb25851429b"
+               "1539ce410000000018238487a417f3da01d99dae5f190096cc292cab000000000000000000000000"
+               "00000000d8fbe9a4fdb3d117d76b0de5416f95e7bb75d72fb60291ea00000000def85b75827b26e8"
+               "a0bef8675b29a197b71bccea000000000000000000000000000000004a503a712350acbd48411f0d"
+               "c15d2f0f49dad3452931f2d30300000050b8d10ab700cc3a7cf51be0403b654ba242680800000000"
+               "000000000000000000000000a6bdc8789cf851782d895bcc8da98489c55b8c1237686b5e03000000"
+               "6215de4bb6f6f3d031a790287e45e9c18b7136c000000000000000000000000000000000ec7d3abc"
+               "4ccf30d3aaaf02a4f05d7308b67f91bfd8a4c5cf01000000fcc2056cc1ff8e51bbce426cfa4b861c"
+               "c73992be000000000000000000000000000000000aa20624fbab244735d67c61283031667b2d74a1"
+               "8c54dda900000000d43e6505621b9a7f0422ae3fc8979ee0c400d794000000000000000000000000"
+               "00000000414d94d0e02d2ec587fc330003781b0f294a2b738d1e69c801000000b973972c17a47631"
+               "c169f11ff9119c40b47cb66300000000000000000000000000000002ea4a849f060ddf57b24a5be9"
+               "b10123e6e4f15ecdc67b2e48000000001785d3ffdbd9a617857ae8126b028c0e3e4b535400000000"
+               "000000000000000000000002a84035b632ef3be6070dafa202187470e9da557036506ebd00000000"
+               "ef369bb8ebfdcec3f8c8fa6867a982be8a534f8500000000000000000000ffffe8d07c84965a316a"
+               "f2abb982b001ec16901f55f960c55c229c88b02a0200000004b0595daf37deb499996bfb667f072d"
+               "ec2c5d9c00000000000000000000ffff99d90a8543961b2ccd47724a3c460ba85183f4c5daae7d63"),
     };
     const int   TEST_DATA_I      = GPOINTER_TO_INT(test_data);
     const int   addr_family      = (TEST_DATA_I == 0 ? AF_INET : AF_INET6);
diff --git a/src/core/platform/tests/test-route.c b/src/core/platform/tests/test-route.c
index 148ed792..bd8fdc27 100644
--- a/src/core/platform/tests/test-route.c
+++ b/src/core/platform/tests/test-route.c
@@ -27,7 +27,7 @@ _wait_for_ipv4_addr_device_route(NMPlatform *platform,
     /* Wait that the addresses gets a device-route. After adding a address,
      * the device route is not added immediately. It takes a moment... */
 
-    addr = nm_utils_ip4_address_clear_host_address(addr, plen);
+    addr = nm_ip4_addr_clear_host_address(addr, plen);
     NMTST_WAIT_ASSERT(400, {
         NMDedupMultiIter iter;
         NMPLookup        lookup;
@@ -41,7 +41,7 @@ _wait_for_ipv4_addr_device_route(NMPlatform *platform,
             &o) {
             const NMPlatformIP4Route *r = NMP_OBJECT_CAST_IP4_ROUTE(o);
 
-            if (r->plen == plen && addr == nm_utils_ip4_address_clear_host_address(r->network, plen)
+            if (r->plen == plen && addr == nm_ip4_addr_clear_host_address(r->network, plen)
                 && r->metric == 0 && r->scope_inv == nm_platform_route_scope_inv(RT_SCOPE_LINK)
                 && r->rt_source == NM_IP_CONFIG_SOURCE_RTPROT_KERNEL)
                 return;
@@ -333,30 +333,33 @@ test_ip4_route(void)
     /* Test route listing */
     routes = nmtstp_ip4_route_get_all(NM_PLATFORM_GET, ifindex);
     memset(rts, 0, sizeof(rts));
-    rts[0].rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
-    rts[0].network   = gateway;
-    rts[0].plen      = 32;
-    rts[0].ifindex   = ifindex;
-    rts[0].gateway   = INADDR_ANY;
-    rts[0].metric    = metric;
-    rts[0].mss       = mss;
-    rts[0].scope_inv = nm_platform_route_scope_inv(RT_SCOPE_LINK);
-    rts[1].rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
-    rts[1].network   = network;
-    rts[1].plen      = plen;
-    rts[1].ifindex   = ifindex;
-    rts[1].gateway   = gateway;
-    rts[1].metric    = metric;
-    rts[1].mss       = mss;
-    rts[1].scope_inv = nm_platform_route_scope_inv(RT_SCOPE_UNIVERSE);
-    rts[2].rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
-    rts[2].network   = 0;
-    rts[2].plen      = 0;
-    rts[2].ifindex   = ifindex;
-    rts[2].gateway   = gateway;
-    rts[2].metric    = metric;
-    rts[2].mss       = mss;
-    rts[2].scope_inv = nm_platform_route_scope_inv(RT_SCOPE_UNIVERSE);
+    rts[0].rt_source  = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
+    rts[0].network    = gateway;
+    rts[0].plen       = 32;
+    rts[0].ifindex    = ifindex;
+    rts[0].gateway    = INADDR_ANY;
+    rts[0].metric     = metric;
+    rts[0].mss        = mss;
+    rts[0].scope_inv  = nm_platform_route_scope_inv(RT_SCOPE_LINK);
+    rts[0].n_nexthops = 1;
+    rts[1].rt_source  = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
+    rts[1].network    = network;
+    rts[1].plen       = plen;
+    rts[1].ifindex    = ifindex;
+    rts[1].gateway    = gateway;
+    rts[1].metric     = metric;
+    rts[1].mss        = mss;
+    rts[1].scope_inv  = nm_platform_route_scope_inv(RT_SCOPE_UNIVERSE);
+    rts[1].n_nexthops = 1;
+    rts[2].rt_source  = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
+    rts[2].network    = 0;
+    rts[2].plen       = 0;
+    rts[2].ifindex    = ifindex;
+    rts[2].gateway    = gateway;
+    rts[2].metric     = metric;
+    rts[2].mss        = mss;
+    rts[2].scope_inv  = nm_platform_route_scope_inv(RT_SCOPE_UNIVERSE);
+    rts[2].n_nexthops = 1;
     g_assert_cmpint(routes->len, ==, 3);
     nmtst_platform_ip4_routes_equal_aptr((const NMPObject *const *) routes->pdata,
                                          rts,
@@ -635,21 +638,22 @@ test_ip4_route_options(gconstpointer test_data)
     switch (TEST_IDX) {
     case 1:
         rts_add[rts_n++] = ((NMPlatformIP4Route){
-            .ifindex   = IFINDEX,
-            .rt_source = NM_IP_CONFIG_SOURCE_USER,
-            .network   = nmtst_inet4_from_string("172.16.1.0"),
-            .plen      = 24,
-            .metric    = 20,
-            .tos       = 0x28,
-            .window    = 10000,
-            .cwnd      = 16,
-            .initcwnd  = 30,
-            .initrwnd  = 50,
-            .mtu       = 1350,
-            .lock_cwnd = TRUE,
-            .mss       = 1300,
-            .quickack  = TRUE,
-            .rto_min   = 1000,
+            .ifindex    = IFINDEX,
+            .rt_source  = NM_IP_CONFIG_SOURCE_USER,
+            .network    = nmtst_inet4_from_string("172.16.1.0"),
+            .plen       = 24,
+            .metric     = 20,
+            .tos        = 0x28,
+            .window     = 10000,
+            .cwnd       = 16,
+            .initcwnd   = 30,
+            .initrwnd   = 50,
+            .mtu        = 1350,
+            .lock_cwnd  = TRUE,
+            .mss        = 1300,
+            .quickack   = TRUE,
+            .rto_min    = 1000,
+            .n_nexthops = 1,
         });
         break;
     case 2:
@@ -663,12 +667,13 @@ test_ip4_route_options(gconstpointer test_data)
               .n_ifa_flags  = 0,
         });
         rts_add[rts_n++] = ((NMPlatformIP4Route){
-            .ifindex   = IFINDEX,
-            .rt_source = NM_IP_CONFIG_SOURCE_USER,
-            .network   = nmtst_inet4_from_string("172.17.1.0"),
-            .gateway   = nmtst_inet4_from_string("172.16.1.1"),
-            .plen      = 24,
-            .metric    = 20,
+            .ifindex    = IFINDEX,
+            .rt_source  = NM_IP_CONFIG_SOURCE_USER,
+            .network    = nmtst_inet4_from_string("172.17.1.0"),
+            .gateway    = nmtst_inet4_from_string("172.16.1.1"),
+            .plen       = 24,
+            .metric     = 20,
+            .n_nexthops = 1,
         });
         rts_add[rts_n++] = ((NMPlatformIP4Route){
             .ifindex     = IFINDEX,
@@ -678,6 +683,7 @@ test_ip4_route_options(gconstpointer test_data)
             .r_rtm_flags = RTNH_F_ONLINK,
             .plen        = 24,
             .metric      = 20,
+            .n_nexthops  = 1,
         });
         break;
     default:
@@ -691,24 +697,23 @@ test_ip4_route_options(gconstpointer test_data)
         const NMPlatformIP4Address *a = &addr[i];
 
         g_assert(a->ifindex == IFINDEX);
-        g_assert(nm_platform_ip4_address_add(
-            NM_PLATFORM_GET,
-            a->ifindex,
-            a->address,
-            a->plen,
-            a->peer_address,
-            nm_platform_ip4_broadcast_address_create(a->address, a->plen),
-            a->lifetime,
-            a->preferred,
-            a->n_ifa_flags,
-            a->label));
+        g_assert(nm_platform_ip4_address_add(NM_PLATFORM_GET,
+                                             a->ifindex,
+                                             a->address,
+                                             a->plen,
+                                             a->peer_address,
+                                             nm_ip4_addr_get_broadcast_address(a->address, a->plen),
+                                             a->lifetime,
+                                             a->preferred,
+                                             a->n_ifa_flags,
+                                             a->label));
         if (a->peer_address == a->address)
             _wait_for_ipv4_addr_device_route(NM_PLATFORM_GET, 200, a->ifindex, a->address, a->plen);
     }
 
     for (i = 0; i < rts_n; i++)
         g_assert(NMTST_NM_ERR_SUCCESS(
-            nm_platform_ip4_route_add(NM_PLATFORM_GET, NMP_NLM_FLAG_REPLACE, &rts_add[i])));
+            nm_platform_ip4_route_add(NM_PLATFORM_GET, NMP_NLM_FLAG_REPLACE, &rts_add[i], NULL)));
 
     for (i = 0; i < rts_n; i++) {
         rts_cmp[i] = rts_add[i];
@@ -747,7 +752,7 @@ test_ip6_route_get(void)
         nmtstp_wait_for_signal(NM_PLATFORM_GET, 10);
         if (nmtstp_ip6_route_get(NM_PLATFORM_GET,
                                  ifindex,
-                                 nmtst_inet6_from_string("fd01:abcd::"),
+                                 nmtst_inet6_from_string_p("fd01:abcd::"),
                                  64,
                                  NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP6,
                                  NULL,
@@ -755,7 +760,7 @@ test_ip6_route_get(void)
             break;
     });
 
-    a      = nmtst_inet6_from_string("fd01:abcd::42");
+    a      = nmtst_inet6_from_string_p("fd01:abcd::42");
     result = nm_platform_ip_route_get(NM_PLATFORM_GET,
                                       AF_INET6,
                                       a,
@@ -797,7 +802,7 @@ test_ip6_route_options(gconstpointer test_data)
         rts_add[rts_n++] = ((NMPlatformIP6Route){
             .ifindex   = IFINDEX,
             .rt_source = NM_IP_CONFIG_SOURCE_USER,
-            .network   = *nmtst_inet6_from_string("2001:db8:a:b:0:0:0:0"),
+            .network   = nmtst_inet6_from_string("2001:db8:a:b:0:0:0:0"),
             .plen      = 64,
             .gateway   = in6addr_any,
             .metric    = 1024,
@@ -812,7 +817,7 @@ test_ip6_route_options(gconstpointer test_data)
     case 2:
         addr[addr_n++]   = ((NMPlatformIP6Address){
               .ifindex      = IFINDEX,
-              .address      = *nmtst_inet6_from_string("2000::2"),
+              .address      = nmtst_inet6_from_string("2000::2"),
               .plen         = 128,
               .peer_address = in6addr_any,
               .lifetime     = NM_PLATFORM_LIFETIME_PERMANENT,
@@ -822,17 +827,17 @@ test_ip6_route_options(gconstpointer test_data)
         rts_add[rts_n++] = ((NMPlatformIP6Route){
             .ifindex   = IFINDEX,
             .rt_source = NM_IP_CONFIG_SOURCE_USER,
-            .network   = *nmtst_inet6_from_string("1010::1"),
+            .network   = nmtst_inet6_from_string("1010::1"),
             .plen      = 128,
             .gateway   = in6addr_any,
             .metric    = 256,
-            .pref_src  = *nmtst_inet6_from_string("2000::2"),
+            .pref_src  = nmtst_inet6_from_string("2000::2"),
         });
         break;
     case 3:
         addr[addr_n++]   = ((NMPlatformIP6Address){
               .ifindex      = IFINDEX,
-              .address      = *nmtst_inet6_from_string("2001:db8:8086::5"),
+              .address      = nmtst_inet6_from_string("2001:db8:8086::5"),
               .plen         = 128,
               .peer_address = in6addr_any,
               .lifetime     = NM_PLATFORM_LIFETIME_PERMANENT,
@@ -842,7 +847,7 @@ test_ip6_route_options(gconstpointer test_data)
         rts_add[rts_n++] = ((NMPlatformIP6Route){
             .ifindex   = IFINDEX,
             .rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER),
-            .network   = *nmtst_inet6_from_string("2001:db8:8086::"),
+            .network   = nmtst_inet6_from_string("2001:db8:8086::"),
             .plen      = 110,
             .metric    = 10021,
             .mss       = 0,
@@ -850,9 +855,9 @@ test_ip6_route_options(gconstpointer test_data)
         rts_add[rts_n++] = ((NMPlatformIP6Route){
             .ifindex   = IFINDEX,
             .rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER),
-            .network   = *nmtst_inet6_from_string("2001:db8:abad:c0de::"),
+            .network   = nmtst_inet6_from_string("2001:db8:abad:c0de::"),
             .plen      = 64,
-            .gateway   = *nmtst_inet6_from_string("2001:db8:8086::1"),
+            .gateway   = nmtst_inet6_from_string("2001:db8:8086::1"),
             .metric    = 21,
             .mss       = 0,
         });
@@ -918,7 +923,7 @@ test_ip(gconstpointer test_data)
     const NMPlatformLink *l;
     char                  ifname[IFNAMSIZ];
     char                  ifname2[IFNAMSIZ];
-    char                  s1[NM_UTILS_INET_ADDRSTRLEN];
+    char                  s1[NM_INET_ADDRSTRLEN];
     NMPlatform           *platform = NM_PLATFORM_GET;
     const int             EX_      = -1;
     struct {
@@ -983,8 +988,8 @@ again_find_idx:
             order_idx[order_len++] = idx;
 
             r->ifindex = iface_data[idx].ifindex;
-            g_assert(
-                NMTST_NM_ERR_SUCCESS(nm_platform_ip4_route_add(platform, NMP_NLM_FLAG_APPEND, r)));
+            g_assert(NMTST_NM_ERR_SUCCESS(
+                nm_platform_ip4_route_add(platform, NMP_NLM_FLAG_APPEND, r, NULL)));
         } else {
             i   = nmtst_get_rand_uint32() % order_len;
             idx = order_idx[i];
@@ -1171,7 +1176,7 @@ _rule_check_kernel_support(NMPlatform *platform, int attribute)
                  .addr_family = AF_INET,
                  .priority    = PROBE_PRORITY,
                  .uid_range =
-                     {
+                {
                          .start = 0,
                          .end   = 0,
                 },
@@ -1278,7 +1283,7 @@ _rule_create_random(NMPlatform *platform)
     guint32                p;
     int                    addr_size;
     guint                  i;
-    char                   saddr[NM_UTILS_INET_ADDRSTRLEN];
+    char                   saddr[NM_INET_ADDRSTRLEN];
     static struct {
         guint32 uid;
         guint32 euid;
@@ -1331,7 +1336,7 @@ _rule_create_random(NMPlatform *platform)
                     p_addr->addr4 =
                         nmtst_inet4_from_string(nm_sprintf_buf(saddr, "192.192.5.%u", (~p) % 256u));
                 else
-                    p_addr->addr6 = *nmtst_inet6_from_string(
+                    p_addr->addr6 = nmtst_inet6_from_string(
                         nm_sprintf_buf(saddr, "1:2:3:4::f:%02x", (~p) % 256u));
             } else if ((p % 3u) == 1)
                 nmtst_rand_buf(NULL, p_addr, addr_size);
@@ -1931,7 +1936,7 @@ test_blackhole(gconstpointer test_data)
     nm_platform_ip_route_normalize(addr_family, &rr.rx);
 
     if (IS_IPv4)
-        r = nm_platform_ip4_route_add(NM_PLATFORM_GET, NMP_NLM_FLAG_APPEND, &rr.r4);
+        r = nm_platform_ip4_route_add(NM_PLATFORM_GET, NMP_NLM_FLAG_APPEND, &rr.r4, NULL);
     else
         r = nm_platform_ip6_route_add(NM_PLATFORM_GET, NMP_NLM_FLAG_APPEND, &rr.r6);
 
@@ -2155,6 +2160,229 @@ test_mptcp(gconstpointer test_data)
 
 /*****************************************************************************/
 
+static void
+_ensure_onlink_routes(void)
+{
+    int i;
+
+    for (i = 0; i < G_N_ELEMENTS(NMTSTP_ENV1_DEVICE_NAME) && NMTSTP_ENV1_DEVICE_NAME[i]; i++) {
+        nmtstp_run_command("ip route append 7.7.7.0/24 dev %s%s",
+                           NMTSTP_ENV1_DEVICE_NAME[i],
+                           nmtst_is_debug() ? "" : " &>/dev/null");
+        nmtstp_run_command("ip route append 7:7:7::/64 dev %s%s",
+                           NMTSTP_ENV1_DEVICE_NAME[i],
+                           nmtst_is_debug() ? "" : " &>/dev/null");
+    }
+}
+
+static void
+test_cache_consistency_routes(gconstpointer test_data)
+{
+    const int                    TEST_IDX      = GPOINTER_TO_INT(test_data);
+    NMPlatform                  *platform      = NM_PLATFORM_GET;
+    gboolean                     is_test_quick = nmtst_test_quick();
+    const int                    N_RUN         = is_test_quick ? 50 : 500;
+    int                          i_run;
+    gs_unref_ptrarray GPtrArray *keeper = g_ptr_array_new_with_free_func(g_free);
+
+    _ensure_onlink_routes();
+
+    for (i_run = 0; i_run < N_RUN; i_run++) {
+        const char   *extra_options[100];
+        gsize         n_extra_options   = 0;
+        gs_free char *extra_options_str = NULL;
+        int           i_if;
+        int           ifindex;
+        const char   *ifname;
+        int           IS_IPv4;
+        const char   *op;
+        const char   *prefix;
+        const char   *s;
+        const char   *route_type;
+        int           i;
+        int           n;
+        char          addr_family_char[2] = {'6', '4'};
+
+        g_ptr_array_set_size(keeper, 0);
+
+        switch (TEST_IDX) {
+        case 1:
+            IS_IPv4 = TRUE;
+            break;
+        case 2:
+            IS_IPv4 = FALSE;
+            break;
+        default:
+            IS_IPv4 = nmtst_get_rand_bool();
+            break;
+        }
+
+        i_if = nmtst_get_rand_uint32() % 2;
+        op   = nmtst_rand_select_str("flush", "add", "change", "append", "prepend", "replace");
+
+        ifindex = NMTSTP_ENV1_IFINDEXES[i_if];
+        ifname  = NMTSTP_ENV1_DEVICE_NAME[i_if];
+
+        g_assert_cmpint(ifindex, ==, nm_platform_link_get_ifindex(platform, ifname));
+
+        if (nm_streq(op, "flush")) {
+            if (!nmtst_get_rand_one_case_in(10)) {
+                /* flush more seldom. */
+                continue;
+            }
+            nmtstp_run_command("ip -%c route flush dev %s"
+                               " table %s" /* table */
+                               "%s"        /* redirect */
+                               "",
+                               addr_family_char[IS_IPv4],
+                               ifname,
+                               nmtst_rand_select_str("main", "10222", "10223", "all"),
+                               nmtst_is_debug() ? "" : " &>/dev/null");
+            _ensure_onlink_routes();
+            goto done;
+        }
+
+        route_type = nmtst_get_rand_one_case_in(4)
+                         ? nmtst_rand_select_str("unicast", "blackhole", "local", "broadcast")
+                         : NULL;
+
+        if (NM_IN_STRSET(route_type, "blackhole")) {
+            ifindex = 0;
+            ifname  = NULL;
+        }
+
+        if (IS_IPv4) {
+            prefix = nmtst_rand_select_str("192.168.4.0/24",
+                                           "192.168.5.0/24",
+                                           "192.168.5.5/32",
+                                           "default");
+        } else {
+            prefix =
+                nmtst_rand_select_str("a:b:c:d::/64", "a:b:c:e::/64", "a:b:c:f::/64", "default");
+        }
+
+        s = nmtst_rand_select_str(NULL, "kernel", "bird");
+        if (s) {
+            if (nmtst_get_rand_bool()) {
+                s = nm_streq(s, "kernel") ? nmtst_rand_select_str("boot", "static", "ra")
+                                          : nmtst_rand_select_str("zebra", "bird");
+            }
+            extra_options[n_extra_options++] = "proto";
+            extra_options[n_extra_options++] = s;
+        }
+
+        s = nmtst_rand_select_str(NULL, "10", "20");
+        if (s) {
+            extra_options[n_extra_options++] = "metric";
+            extra_options[n_extra_options++] = s;
+        }
+
+        s = nmtst_rand_select_str(NULL, "10222", "10223");
+        if (s) {
+            extra_options[n_extra_options++] = "table";
+            extra_options[n_extra_options++] = s;
+        }
+
+        if (!IS_IPv4 && NM_IN_STRSET(op, "add", "change", "append", "prepend", "replace")) {
+            /* kernel has a bug with append/prepend of IPv6 routes with next-hops.
+             * This leads to wrong notification messages, wrong merging of multi-hop
+             * routes and cache inconsistency in NMPlatform.
+             *
+             * https://bugzilla.redhat.com/show_bug.cgi?id=2161994
+             *
+             * For now, disable the test case to make the unit test not fail.
+             *
+             * While being a kernel bug, it leads to cache inconsistency in NMPlatform,
+             * which is a problem for NetworkManager. I don't see how we can detect
+             * this problem to trigger a refresh. */
+        } else if (ifname && nmtst_get_rand_one_case_in(3)) {
+            n = (nmtst_get_rand_uint32() % 4) + 1;
+            for (i = 0; i < n; i++) {
+                extra_options[n_extra_options++] = "nexthop";
+                extra_options[n_extra_options++] = "via";
+                if (IS_IPv4) {
+                    extra_options[n_extra_options++] =
+                        nmtst_keeper_printf(&keeper, "7.7.7.%d", i + 1);
+                } else {
+                    extra_options[n_extra_options++] =
+                        nmtst_keeper_printf(&keeper, "7:7:7:7::%d", i + 1);
+                }
+                extra_options[n_extra_options++] = "dev";
+                extra_options[n_extra_options++] = NMTSTP_ENV1_DEVICE_NAME[nmtst_get_rand_bool()];
+                if (IS_IPv4 && i == 0) {
+                    /* For IPv4, there is a problem if we configure a route with
+                     * only one next-hop and a weight. In that case, kernel allows
+                     * to add duplicates (that only differ by weight), but on netlink
+                     * the weight is not exposed, so the routes look identical and
+                     * are deduplicated by the hash.
+                     * See https://bugzilla.redhat.com/show_bug.cgi?id=2162315
+                     *
+                     * This needs a kernel fix. Workaround that issue here, otherwise the test
+                     * will randomly fail. */
+                } else if (nmtst_get_rand_one_case_in(3)) {
+                    extra_options[n_extra_options++] = "weight";
+                    extra_options[n_extra_options++] = "5";
+                }
+            }
+
+            ifname  = NULL;
+            ifindex = 0;
+        }
+
+        g_assert_cmpint(n_extra_options, <, G_N_ELEMENTS(extra_options));
+        extra_options[n_extra_options] = NULL;
+
+        if (nmtst_is_debug())
+            nmtstp_run_command("ip -%c -d route show table all", addr_family_char[IS_IPv4]);
+
+        /* We ignore errors. The reason is that operations like "change" might fail if
+         * the route doesn't exist. That's fine for our test. We just do randomly things
+         * and some of them will stick. */
+        nmtstp_run_command(
+            "ip -%c route "
+            "%s"   /* op */
+            "%s%s" /* route_type */
+            " %s"  /* prefix */
+            "%s%s" /* ifname */
+            "%s%s" /* extra_options */
+            "%s"   /* redirect */
+            "",
+            addr_family_char[IS_IPv4],
+            op,
+            NM_PRINT_FMT_QUOTED2(route_type, " ", route_type, ""),
+            prefix,
+            NM_PRINT_FMT_QUOTED2(ifname, " dev ", ifname, ""),
+            NM_PRINT_FMT_QUOTED2(extra_options[0],
+                                 " ",
+                                 (extra_options_str = g_strjoinv(" ", (char **) extra_options)),
+                                 ""),
+            nmtst_is_debug() ? "" : " &>/dev/null");
+
+        if (nmtst_is_debug())
+            nmtstp_run_command("ip -%c -d route show table all", addr_family_char[IS_IPv4]);
+done:
+        nm_platform_process_events(platform);
+
+        if (!is_test_quick || (i_run + 1 == N_RUN) || nmtst_get_rand_one_case_in(5)) {
+            nmtstp_check_platform(
+                platform,
+                nmtst_get_rand_one_case_in(5)
+                    ? 0u
+                    : nmp_object_type_to_flags(NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4)));
+        }
+    }
+
+    if (is_test_quick) {
+        gs_free char *msg = NULL;
+
+        msg = g_strdup_printf("Ran a quick version of test %s (try NMTST_DEBUG=slow)",
+                              nmtst_test_get_path());
+        g_test_skip(msg);
+    }
+}
+
+/*****************************************************************************/
+
 NMTstpSetupFunc const _nmtstp_setup_platform_func = SETUP;
 
 void
@@ -2166,9 +2394,11 @@ _nmtstp_init_tests(int *argc, char ***argv)
 void
 _nmtstp_setup_tests(void)
 {
-#define add_test_func(testpath, test_func) nmtstp_env1_add_test_func(testpath, test_func, TRUE)
+#define add_test_func(testpath, test_func) nmtstp_env1_add_test_func(testpath, test_func, 1, TRUE)
 #define add_test_func_data(testpath, test_func, arg) \
-    nmtstp_env1_add_test_func_data(testpath, test_func, arg, TRUE)
+    nmtstp_env1_add_test_func_data(testpath, test_func, arg, 1, TRUE)
+#define add_test_func_data_with_if2(testpath, test_func, arg) \
+    nmtstp_env1_add_test_func_data(testpath, test_func, arg, 2, TRUE)
 
     add_test_func("/route/ip4", test_ip4_route);
     add_test_func("/route/ip6", test_ip6_route);
@@ -2201,4 +2431,15 @@ _nmtstp_setup_tests(void)
         add_test_func_data("/route/mptcp/1", test_mptcp, GINT_TO_POINTER(1));
         add_test_func_data("/route/mptcp/2", test_mptcp, GINT_TO_POINTER(2));
     }
+    if (nmtstp_is_root_test()) {
+        add_test_func_data_with_if2("/route/test_cache_consistency_routes/1",
+                                    test_cache_consistency_routes,
+                                    GINT_TO_POINTER(1));
+        add_test_func_data_with_if2("/route/test_cache_consistency_routes/2",
+                                    test_cache_consistency_routes,
+                                    GINT_TO_POINTER(2));
+        add_test_func_data_with_if2("/route/test_cache_consistency_routes/3",
+                                    test_cache_consistency_routes,
+                                    GINT_TO_POINTER(3));
+    }
 }
diff --git a/src/core/platform/tests/test-tc.c b/src/core/platform/tests/test-tc.c
index 6a2019c8..832fbea6 100644
--- a/src/core/platform/tests/test-tc.c
+++ b/src/core/platform/tests/test-tc.c
@@ -214,8 +214,8 @@ _nmtstp_init_tests(int *argc, char ***argv)
 void
 _nmtstp_setup_tests(void)
 {
-    nmtstp_env1_add_test_func("/link/qdisc/1", test_qdisc1, TRUE);
-    nmtstp_env1_add_test_func("/link/qdisc/fq_codel", test_qdisc_fq_codel, TRUE);
-    nmtstp_env1_add_test_func("/link/qdisc/sfq", test_qdisc_sfq, TRUE);
-    nmtstp_env1_add_test_func("/link/qdisc/tbf", test_qdisc_tbf, TRUE);
+    nmtstp_env1_add_test_func("/link/qdisc/1", test_qdisc1, 1, TRUE);
+    nmtstp_env1_add_test_func("/link/qdisc/fq_codel", test_qdisc_fq_codel, 1, TRUE);
+    nmtstp_env1_add_test_func("/link/qdisc/sfq", test_qdisc_sfq, 1, TRUE);
+    nmtstp_env1_add_test_func("/link/qdisc/tbf", test_qdisc_tbf, 1, TRUE);
 }
diff --git a/src/core/ppp/nm-ppp-manager.c b/src/core/ppp/nm-ppp-manager.c
index 003a9806..14cd5731 100644
--- a/src/core/ppp/nm-ppp-manager.c
+++ b/src/core/ppp/nm-ppp-manager.c
@@ -66,7 +66,8 @@ static NM_CACHED_QUARK_FCN("ppp-manager-secret-tries", ppp_manager_secret_tries_
 /*****************************************************************************/
 
 #define NM_TYPE_PPP_MANAGER (nm_ppp_manager_get_type())
-#define NM_PPP_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_PPP_MANAGER, NMPPPManager))
+#define NM_PPP_MANAGER(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_PPP_MANAGER, NMPPPManager))
 #define NM_PPP_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_PPP_MANAGER, NMPPPManagerClass))
 #define NM_IS_PPP_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_PPP_MANAGER))
@@ -582,7 +583,7 @@ impl_ppp_manager_set_ip4_config(NMDBusObject                      *obj,
 
     if (g_variant_lookup(config_dict, NM_PPP_IP4_CONFIG_DNS, "au", &iter)) {
         while (g_variant_iter_next(iter, "u", &u32))
-            nm_l3_config_data_add_nameserver(l3cd, AF_INET, &u32);
+            nm_l3_config_data_add_nameserver_detail(l3cd, AF_INET, &u32, NULL);
         g_variant_iter_free(iter);
     }
 
diff --git a/src/core/ppp/nm-ppp-mgr.c b/src/core/ppp/nm-ppp-mgr.c
index 31c173a3..bebffaf3 100644
--- a/src/core/ppp/nm-ppp-mgr.c
+++ b/src/core/ppp/nm-ppp-mgr.c
@@ -68,7 +68,7 @@ NM_UTILS_LOOKUP_STR_DEFINE(nm_ppp_mgr_callback_type_to_string,
 #define SELF_TO_USERDATA(self) (&(self)->idle_start)
 
 #define SELF_FROM_USERDATA(user_data) \
-    ((NMPppMgr *) (((const char *) (user_data)) - G_STRUCT_OFFSET(NMPppMgr, idle_start)))
+    NM_CAST_ALIGN(NMPppMgr, (((char *) (user_data)) - G_STRUCT_OFFSET(NMPppMgr, idle_start)))
 
 /*****************************************************************************/
 
diff --git a/src/core/settings/nm-agent-manager.c b/src/core/settings/nm-agent-manager.c
index 295378bc..0a120649 100644
--- a/src/core/settings/nm-agent-manager.c
+++ b/src/core/settings/nm-agent-manager.c
@@ -1172,8 +1172,10 @@ _con_get_try_complete_early(Request *req)
     }
     /* Do we have everything we need? */
     if (NM_FLAGS_HAS(req->con.get.flags, NM_SECRET_AGENT_GET_SECRETS_FLAG_ONLY_SYSTEM)
-        || ((nm_connection_need_secrets(tmp, NULL) == NULL)
-            && !NM_FLAGS_HAS(req->con.get.flags, NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW))) {
+        || (NM_FLAGS_HAS(req->con.get.flags, NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW)
+            && !nm_connection_need_secrets_for_rerequest(tmp))
+        || (!NM_FLAGS_HAS(req->con.get.flags, NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW)
+            && !nm_connection_need_secrets(tmp, NULL))) {
         _LOGD(NULL, "(" LOG_REQ_FMT ") system settings secrets sufficient", LOG_REQ_ARG(req));
 
         /* Got everything, we're done */
diff --git a/src/core/settings/nm-agent-manager.h b/src/core/settings/nm-agent-manager.h
index cae8e8e0..e5c6e31c 100644
--- a/src/core/settings/nm-agent-manager.h
+++ b/src/core/settings/nm-agent-manager.h
@@ -13,7 +13,7 @@
 
 #define NM_TYPE_AGENT_MANAGER (nm_agent_manager_get_type())
 #define NM_AGENT_MANAGER(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_AGENT_MANAGER, NMAgentManager))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_AGENT_MANAGER, NMAgentManager))
 #define NM_AGENT_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_AGENT_MANAGER, NMAgentManagerClass))
 #define NM_IS_AGENT_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_AGENT_MANAGER))
diff --git a/src/core/settings/nm-secret-agent.h b/src/core/settings/nm-secret-agent.h
index 14458b38..eb8dd382 100644
--- a/src/core/settings/nm-secret-agent.h
+++ b/src/core/settings/nm-secret-agent.h
@@ -12,7 +12,7 @@
 
 #define NM_TYPE_SECRET_AGENT (nm_secret_agent_get_type())
 #define NM_SECRET_AGENT(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SECRET_AGENT, NMSecretAgent))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SECRET_AGENT, NMSecretAgent))
 #define NM_SECRET_AGENT_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_SECRET_AGENT, NMSecretAgentClass))
 #define NM_IS_SECRET_AGENT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_SECRET_AGENT))
diff --git a/src/core/settings/nm-settings-connection.h b/src/core/settings/nm-settings-connection.h
index 893b0d7b..47599c3d 100644
--- a/src/core/settings/nm-settings-connection.h
+++ b/src/core/settings/nm-settings-connection.h
@@ -122,7 +122,7 @@ typedef enum {
 
 #define NM_TYPE_SETTINGS_CONNECTION (nm_settings_connection_get_type())
 #define NM_SETTINGS_CONNECTION(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SETTINGS_CONNECTION, NMSettingsConnection))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SETTINGS_CONNECTION, NMSettingsConnection))
 #define NM_SETTINGS_CONNECTION_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_SETTINGS_CONNECTION, NMSettingsConnectionClass))
 #define NM_IS_SETTINGS_CONNECTION(obj) \
diff --git a/src/core/settings/nm-settings-plugin.h b/src/core/settings/nm-settings-plugin.h
index 198447c0..c9d1265f 100644
--- a/src/core/settings/nm-settings-plugin.h
+++ b/src/core/settings/nm-settings-plugin.h
@@ -26,7 +26,7 @@ typedef struct {
 
 #define NM_TYPE_SETTINGS_PLUGIN (nm_settings_plugin_get_type())
 #define NM_SETTINGS_PLUGIN(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SETTINGS_PLUGIN, NMSettingsPlugin))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SETTINGS_PLUGIN, NMSettingsPlugin))
 #define NM_SETTINGS_PLUGIN_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_SETTINGS_PLUGIN, NMSettingsPluginClass))
 #define NM_IS_SETTINGS_PLUGIN(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_SETTINGS_PLUGIN))
diff --git a/src/core/settings/nm-settings-storage.h b/src/core/settings/nm-settings-storage.h
index 198b0e8b..65750152 100644
--- a/src/core/settings/nm-settings-storage.h
+++ b/src/core/settings/nm-settings-storage.h
@@ -12,7 +12,7 @@
 
 #define NM_TYPE_SETTINGS_STORAGE (nm_settings_storage_get_type())
 #define NM_SETTINGS_STORAGE(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SETTINGS_STORAGE, NMSettingsStorage))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SETTINGS_STORAGE, NMSettingsStorage))
 #define NM_SETTINGS_STORAGE_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_SETTINGS_STORAGE, NMSettingsStorageClass))
 #define NM_IS_SETTINGS_STORAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_SETTINGS_STORAGE))
diff --git a/src/core/settings/nm-settings.c b/src/core/settings/nm-settings.c
index b7d846c6..63476c3c 100644
--- a/src/core/settings/nm-settings.c
+++ b/src/core/settings/nm-settings.c
@@ -3479,7 +3479,7 @@ _save_hostname_write_cb(GObject *source, GAsyncResult *result, gpointer user_dat
 
     nm_utils_user_data_unpack(user_data, &self, &context, &auth_subject, &hostname, &cancellable);
 
-    nm_hostname_manager_write_hostname_finish(NM_HOSTNAME_MANAGER(source), result, &error);
+    nm_hostname_manager_set_static_hostname_finish(NM_HOSTNAME_MANAGER(source), result, &error);
 
     nm_audit_log_control_op(NM_AUDIT_OP_HOSTNAME_SAVE,
                             hostname ?: "",
@@ -3544,7 +3544,7 @@ _save_hostname_pk_cb(NMAuthChain *chain, GDBusMethodInvocation *context, gpointe
                                   (gpointer *) &priv->shutdown_cancellable);
     }
 
-    nm_hostname_manager_write_hostname(
+    nm_hostname_manager_set_static_hostname(
         priv->hostname_manager,
         hostname,
         priv->shutdown_cancellable,
diff --git a/src/core/settings/nm-settings.h b/src/core/settings/nm-settings.h
index 8747d1dc..aba3c565 100644
--- a/src/core/settings/nm-settings.h
+++ b/src/core/settings/nm-settings.h
@@ -15,7 +15,7 @@
 #include "nm-settings-connection.h"
 
 #define NM_TYPE_SETTINGS (nm_settings_get_type())
-#define NM_SETTINGS(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SETTINGS, NMSettings))
+#define NM_SETTINGS(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SETTINGS, NMSettings))
 #define NM_SETTINGS_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_SETTINGS, NMSettingsClass))
 #define NM_IS_SETTINGS(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_SETTINGS))
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-plugin.h b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-plugin.h
index dc1a01c8..49e04291 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-plugin.h
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-plugin.h
@@ -10,7 +10,7 @@
 
 #define NMS_TYPE_IFCFG_RH_PLUGIN (nms_ifcfg_rh_plugin_get_type())
 #define NMS_IFCFG_RH_PLUGIN(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NMS_TYPE_IFCFG_RH_PLUGIN, NMSIfcfgRHPlugin))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NMS_TYPE_IFCFG_RH_PLUGIN, NMSIfcfgRHPlugin))
 #define NMS_IFCFG_RH_PLUGIN_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NMS_TYPE_IFCFG_RH_PLUGIN, NMSIfcfgRHPluginClass))
 #define NMS_IS_IFCFG_RH_PLUGIN(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMS_TYPE_IFCFG_RH_PLUGIN))
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c
index 4d8e7bd6..0eeead8a 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c
@@ -739,7 +739,7 @@ read_full_ip4_address(shvarFile    *ifcfg,
     int           prefix = 0;
     gboolean      has_key;
     guint32       a;
-    char          inet_buf[NM_UTILS_INET_ADDRSTRLEN];
+    char          inet_buf[NM_INET_ADDRSTRLEN];
 
     g_return_val_if_fail(which >= -1, FALSE);
     g_return_val_if_fail(ifcfg != NULL, FALSE);
@@ -761,7 +761,7 @@ read_full_ip4_address(shvarFile    *ifcfg,
         if (!read_ip4_address(ifcfg, numbered_tag(tag, "GATEWAY", which), &has_key, &a, error))
             return FALSE;
         if (has_key)
-            *out_gateway = nm_utils_inet4_ntop_dup(a);
+            *out_gateway = nm_inet4_ntop_dup(a);
     }
 
     /* Prefix */
@@ -782,16 +782,16 @@ read_full_ip4_address(shvarFile    *ifcfg,
         if (!read_ip4_address(ifcfg, numbered_tag(tag, "NETMASK", which), &has_key, &a, error))
             return FALSE;
         if (has_key)
-            prefix = _nm_utils_ip4_netmask_to_prefix(a);
+            prefix = nm_ip4_addr_netmask_to_prefix(a);
         else {
             if (base_addr)
                 prefix = nm_ip_address_get_prefix(base_addr);
             else {
                 /* Try to autodetermine the prefix for the address' class */
-                prefix = _nm_utils_ip4_get_default_prefix(ipaddr);
+                prefix = nm_ip4_addr_get_default_prefix(ipaddr);
                 PARSE_WARNING("missing %s, assuming %s/%d",
                               prefix_tag,
-                              _nm_utils_inet4_ntop(ipaddr, inet_buf),
+                              nm_inet4_ntop(ipaddr, inet_buf),
                               prefix);
             }
         }
@@ -887,6 +887,7 @@ enum {
     PARSE_LINE_ATTR_ROUTE_ADVMSS,
     PARSE_LINE_ATTR_ROUTE_RTO_MIN,
     PARSE_LINE_ATTR_ROUTE_QUICKACK,
+    PARSE_LINE_ATTR_ROUTE_WEIGHT,
 
     /* iproute2 arguments that only matter when parsing the file. */
     PARSE_LINE_ATTR_ROUTE_TO,
@@ -966,6 +967,12 @@ parse_route_line(const char *line,
                 .int_base_16 = TRUE,
                 .ignore      = PARSE_LINE_AF_FLAG_FOR_IPV6,
             },
+        [PARSE_LINE_ATTR_ROUTE_WEIGHT] =
+            {
+                .key      = NM_IP_ROUTE_ATTRIBUTE_WEIGHT,
+                .type     = PARSE_LINE_TYPE_UINT32,
+                .disabled = PARSE_LINE_AF_FLAG_FOR_IPV6,
+            },
         [PARSE_LINE_ATTR_ROUTE_SCOPE] =
             {
                 .key    = NM_IP_ROUTE_ATTRIBUTE_SCOPE,
@@ -1282,7 +1289,7 @@ parse_line_type_addr_with_prefix:
             int prefix = -1;
 
             if (p_info->type == PARSE_LINE_TYPE_ADDR) {
-                if (!nm_utils_parse_inaddr_bin(addr_family, s, NULL, &p_data->v.addr.addr)) {
+                if (!nm_inet_parse_bin(addr_family, s, NULL, &p_data->v.addr.addr)) {
                     if (p_info == &parse_infos[PARSE_LINE_ATTR_ROUTE_VIA]
                         && nm_streq(s, "(null)")) {
                         /* Due to a bug, would older versions of NM write "via (null)"
@@ -1313,11 +1320,11 @@ parse_line_type_addr_with_prefix:
                 if (p_info == &parse_infos[PARSE_LINE_ATTR_ROUTE_TO] && nm_streq(s, "default")) {
                     memset(&p_data->v.addr.addr, 0, sizeof(p_data->v.addr.addr));
                     prefix = 0;
-                } else if (!nm_utils_parse_inaddr_prefix_bin(addr_family,
-                                                             s,
-                                                             NULL,
-                                                             &p_data->v.addr.addr,
-                                                             &prefix)) {
+                } else if (!nm_inet_parse_with_prefix_bin(addr_family,
+                                                          s,
+                                                          NULL,
+                                                          &p_data->v.addr.addr,
+                                                          &prefix)) {
                     g_set_error(error,
                                 NM_SETTINGS_ERROR,
                                 NM_SETTINGS_ERROR_INVALID_CONNECTION,
@@ -1468,7 +1475,7 @@ read_one_ip4_route(shvarFile *ifcfg, guint32 which, NMIPRoute **out_route, GErro
     const char   *v;
     gs_free char *value = NULL;
     gint64        prefix, metric;
-    char          inet_buf[NM_UTILS_INET_ADDRSTRLEN];
+    char          inet_buf[NM_INET_ADDRSTRLEN];
 
     g_return_val_if_fail(ifcfg != NULL, FALSE);
     g_return_val_if_fail(out_route && !*out_route, FALSE);
@@ -1496,14 +1503,14 @@ read_one_ip4_route(shvarFile *ifcfg, guint32 which, NMIPRoute **out_route, GErro
                           error))
         return FALSE;
     if (has_key) {
-        prefix = _nm_utils_ip4_netmask_to_prefix(netmask);
-        if (netmask != _nm_utils_ip4_prefix_to_netmask(prefix)) {
+        prefix = nm_ip4_addr_netmask_to_prefix(netmask);
+        if (netmask != nm_ip4_addr_netmask_from_prefix(prefix)) {
             g_set_error(error,
                         NM_SETTINGS_ERROR,
                         NM_SETTINGS_ERROR_INVALID_CONNECTION,
                         "Invalid IP4 netmask '%s' \"%s\"",
                         netmask_tag,
-                        _nm_utils_inet4_ntop(netmask, inet_buf));
+                        nm_inet4_ntop(netmask, inet_buf));
             return FALSE;
         }
     } else {
@@ -1667,7 +1674,7 @@ parse_full_ip6_address(shvarFile    *ifcfg,
     nm_assert(out_address && !*out_address);
     nm_assert(!error || !*error);
 
-    if (!nm_utils_parse_inaddr_prefix_bin(AF_INET6, addr_str, NULL, &addr_bin, &prefix)) {
+    if (!nm_inet_parse_with_prefix_bin(AF_INET6, addr_str, NULL, &addr_bin, &prefix)) {
         g_set_error(error,
                     NM_SETTINGS_ERROR,
                     NM_SETTINGS_ERROR_INVALID_CONNECTION,
@@ -1935,6 +1942,8 @@ make_ip4_setting(shvarFile *ifcfg,
                  (guint) route_table,
                  NM_SETTING_IP4_CONFIG_LINK_LOCAL,
                  ipv4_link_local,
+                 NM_SETTING_IP_CONFIG_AUTO_ROUTE_EXT_GW,
+                 svGetValueTernary(ifcfg, "IPV4_AUTO_ROUTE_EXT_GW"),
                  NULL);
 
     if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED))
@@ -2027,7 +2036,7 @@ make_ip4_setting(shvarFile *ifcfg,
                                   "because the connection has no static addresses",
                                   f);
                 } else
-                    gateway = nm_utils_inet4_ntop_dup(a);
+                    gateway = nm_inet4_ntop_dup(a);
             }
         }
     }
@@ -2047,7 +2056,9 @@ make_ip4_setting(shvarFile *ifcfg,
          * Pick up just IPv4 addresses (IPv6 addresses are taken by make_ip6_setting())
          */
         for (i = 1; i < 10000; i++) {
-            char tag[256];
+            int      af;
+            NMIPAddr ip;
+            char     tag[256];
 
             numbered_tag(tag, "DNS", i);
             nm_clear_g_free(&value);
@@ -2055,18 +2066,18 @@ make_ip4_setting(shvarFile *ifcfg,
             if (!v)
                 break;
 
-            if (nm_utils_ipaddr_is_valid(AF_INET, v)) {
-                if (!nm_setting_ip_config_add_dns(s_ip4, v))
-                    PARSE_WARNING("duplicate DNS server %s", tag);
-            } else if (nm_utils_ipaddr_is_valid(AF_INET6, v)) {
-                /* Ignore IPv6 addresses */
-            } else {
+            if (!nm_utils_dnsname_parse(AF_UNSPEC, v, &af, &ip, NULL)) {
                 g_set_error(error,
                             NM_SETTINGS_ERROR,
                             NM_SETTINGS_ERROR_INVALID_CONNECTION,
                             "Invalid DNS server address '%s'",
                             v);
                 return NULL;
+            } else if (af == AF_INET) {
+                if (!nm_setting_ip_config_add_dns(s_ip4, v))
+                    PARSE_WARNING("duplicate DNS server %s", tag);
+            } else {
+                /* Ignore IPv6 addresses */
             }
         }
 
@@ -2446,6 +2457,8 @@ make_ip6_setting(shvarFile *ifcfg, shvarFile *network_ifcfg, gboolean routes_rea
                  (guint) route_table,
                  NM_SETTING_IP6_CONFIG_IP6_PRIVACY,
                  ip6_privacy_val,
+                 NM_SETTING_IP_CONFIG_AUTO_ROUTE_EXT_GW,
+                 svGetValueTernary(ifcfg, "IPV6_AUTO_ROUTE_EXT_GW"),
                  NULL);
 
     /* Don't bother to read IP, DNS and routes when IPv6 is disabled */
@@ -2546,7 +2559,7 @@ make_ip6_setting(shvarFile *ifcfg, shvarFile *network_ifcfg, gboolean routes_rea
 
             if ((ptr = strchr(v, '%')) != NULL)
                 *ptr = '\0'; /* remove %interface prefix if present */
-            if (!nm_utils_ipaddr_is_valid(AF_INET6, v)) {
+            if (!nm_inet_is_valid(AF_INET6, v)) {
                 if (!is_disabled) {
                     g_set_error(error,
                                 NM_SETTINGS_ERROR,
@@ -2586,7 +2599,9 @@ make_ip6_setting(shvarFile *ifcfg, shvarFile *network_ifcfg, gboolean routes_rea
      * Pick up just IPv6 addresses (IPv4 addresses are taken by make_ip4_setting())
      */
     for (i = 1; i < 10000; i++) {
-        char tag[256];
+        int      af;
+        NMIPAddr ip;
+        char     tag[256];
 
         numbered_tag(tag, "DNS", i);
         nm_clear_g_free(&value);
@@ -2594,16 +2609,7 @@ make_ip6_setting(shvarFile *ifcfg, shvarFile *network_ifcfg, gboolean routes_rea
         if (!v)
             break;
 
-        if (nm_utils_ipaddr_is_valid(AF_INET6, v)) {
-            if (is_disabled) {
-                PARSE_WARNING("ignore DNS server addresses with method disabled/ignore");
-                break;
-            }
-            if (!nm_setting_ip_config_add_dns(s_ip6, v))
-                PARSE_WARNING("duplicate DNS server %s", tag);
-        } else if (nm_utils_ipaddr_is_valid(AF_INET, v)) {
-            /* Ignore IPv4 addresses */
-        } else {
+        if (!nm_utils_dnsname_parse(AF_UNSPEC, v, &af, &ip, NULL)) {
             if (is_disabled)
                 continue;
             g_set_error(error,
@@ -2612,6 +2618,15 @@ make_ip6_setting(shvarFile *ifcfg, shvarFile *network_ifcfg, gboolean routes_rea
                         "Invalid DNS server address '%s'",
                         v);
             return NULL;
+        } else if (af == AF_INET6) {
+            if (is_disabled) {
+                PARSE_WARNING("ignore DNS server addresses with method disabled/ignore");
+                break;
+            }
+            if (!nm_setting_ip_config_add_dns(s_ip6, v))
+                PARSE_WARNING("duplicate DNS server %s", tag);
+        } else {
+            /* Ignore IPv4 addresses */
         }
     }
 
@@ -6227,6 +6242,11 @@ make_vlan_setting(shvarFile *ifcfg, const char *file, GError **error)
     }
     g_object_set(s_vlan, NM_SETTING_VLAN_PARENT, parent, NULL);
 
+    v = svGetValueStr(ifcfg, "VLAN_PROTOCOL", &value);
+    if (v) {
+        g_object_set(s_vlan, NM_SETTING_VLAN_PROTOCOL, v, NULL);
+    }
+
     vlan_flags |= NM_VLAN_FLAG_REORDER_HEADERS;
 
     gvrp = svGetValueBoolean(ifcfg, "GVRP", -1);
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-storage.h b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-storage.h
index 2ab6beb6..737c9614 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-storage.h
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-storage.h
@@ -13,7 +13,7 @@
 
 #define NMS_TYPE_IFCFG_RH_STORAGE (nms_ifcfg_rh_storage_get_type())
 #define NMS_IFCFG_RH_STORAGE(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NMS_TYPE_IFCFG_RH_STORAGE, NMSIfcfgRHStorage))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NMS_TYPE_IFCFG_RH_STORAGE, NMSIfcfgRHStorage))
 #define NMS_IFCFG_RH_STORAGE_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NMS_TYPE_IFCFG_RH_STORAGE, NMSIfcfgRHStorageClass))
 #define NMS_IS_IFCFG_RH_STORAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMS_TYPE_IFCFG_RH_STORAGE))
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c
index e1ef8174..85a0eb26 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c
@@ -957,6 +957,7 @@ const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[] = {
     _KEY_TYPE("IEEE_8021X_SUBJECT_MATCH", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IEEE_8021X_SYSTEM_CA_CERTS", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPADDR", NMS_IFCFG_KEY_TYPE_IS_NUMBERED),
+    _KEY_TYPE("IPV4_AUTO_ROUTE_EXT_GW", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV4_DHCP_TIMEOUT", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV4_DNS_PRIORITY", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV4_FAILURE_FATAL", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
@@ -971,6 +972,7 @@ const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[] = {
     _KEY_TYPE("IPV6TUNNELIPV4", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV6_ADDR_GEN_MODE", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV6_AUTOCONF", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
+    _KEY_TYPE("IPV6_AUTO_ROUTE_EXT_GW", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV6_DEFAULTDEV", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV6_DEFAULTGW", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV6_DEFROUTE", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
@@ -1061,6 +1063,7 @@ const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[] = {
     _KEY_TYPE("VLAN_FLAGS", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("VLAN_ID", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("VLAN_INGRESS_PRIORITY_MAP", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
+    _KEY_TYPE("VLAN_PROTOCOL", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("VRF", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("VRF_UUID", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("WAIT_ACTIVATION_DELAY", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
@@ -1080,12 +1083,12 @@ nms_ifcfg_well_known_key_find_info(const char *key, gssize *out_idx)
 
     G_STATIC_ASSERT(G_STRUCT_OFFSET(NMSIfcfgKeyTypeInfo, key_name) == 0);
 
-    idx = nm_utils_array_find_binary_search(nms_ifcfg_well_known_keys,
-                                            sizeof(nms_ifcfg_well_known_keys[0]),
-                                            G_N_ELEMENTS(nms_ifcfg_well_known_keys),
-                                            &key,
-                                            nm_strcmp_p_with_data,
-                                            NULL);
+    idx = nm_array_find_bsearch(nms_ifcfg_well_known_keys,
+                                G_N_ELEMENTS(nms_ifcfg_well_known_keys),
+                                sizeof(nms_ifcfg_well_known_keys[0]),
+                                &key,
+                                nm_strcmp_p_with_data,
+                                NULL);
     NM_SET_OUT(out_idx, idx);
     if (idx < 0)
         return NULL;
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.h b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.h
index d1f8dbad..31064274 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.h
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.h
@@ -33,7 +33,7 @@ typedef struct {
     NMSIfcfgKeyTypeFlags key_flags;
 } NMSIfcfgKeyTypeInfo;
 
-extern const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[256];
+extern const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[259];
 
 const NMSIfcfgKeyTypeInfo *nms_ifcfg_well_known_key_find_info(const char *key, gssize *out_idx);
 
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c
index e8948c3d..2ea097fd 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c
@@ -1453,6 +1453,7 @@ write_vlan_setting(NMConnection *connection, shvarFile *ifcfg, gboolean *wired,
     svSetValueStr(ifcfg, "TYPE", TYPE_VLAN);
     svSetValueStr(ifcfg, "PHYSDEV", nm_setting_vlan_get_parent(s_vlan));
     svSetValueInt64(ifcfg, "VLAN_ID", nm_setting_vlan_get_id(s_vlan));
+    svSetValueStr(ifcfg, "VLAN_PROTOCOL", nm_setting_vlan_get_protocol(s_vlan));
 
     vlan_flags = nm_setting_vlan_get_flags(s_vlan);
     svSetValueBoolean(ifcfg, "REORDER_HDR", NM_FLAGS_HAS(vlan_flags, NM_VLAN_FLAG_REORDER_HEADERS));
@@ -2359,13 +2360,14 @@ get_route_attributes_string(NMIPRoute *route, int family)
                 /* we also have a corresponding attribute with the numeric value. The
                  * lock setting is handled above. */
             }
-        } else if (nm_streq(names[i], NM_IP_ROUTE_ATTRIBUTE_SCOPE)) {
+        } else if (NM_IN_STRSET(names[i], NM_IP_ROUTE_ATTRIBUTE_SCOPE)) {
             g_string_append_printf(str, "%s %u", names[i], (unsigned) g_variant_get_byte(attr));
         } else if (nm_streq(names[i], NM_IP_ROUTE_ATTRIBUTE_TOS)) {
             g_string_append_printf(str, "%s 0x%02x", names[i], (unsigned) g_variant_get_byte(attr));
         } else if (NM_IN_STRSET(names[i],
+                                NM_IP_ROUTE_ATTRIBUTE_RTO_MIN,
                                 NM_IP_ROUTE_ATTRIBUTE_TABLE,
-                                NM_IP_ROUTE_ATTRIBUTE_RTO_MIN)) {
+                                NM_IP_ROUTE_ATTRIBUTE_WEIGHT)) {
             g_string_append_printf(str, "%s %u", names[i], (unsigned) g_variant_get_uint32(attr));
         } else if (nm_streq(names[i], NM_IP_ROUTE_ATTRIBUTE_QUICKACK)) {
             g_string_append_printf(str, "%s %u", names[i], (unsigned) g_variant_get_boolean(attr));
@@ -2416,8 +2418,8 @@ write_route_file_svformat(const char *filename, NMSettingIPConfig *s_ip4)
 
         svSetValueStr(routefile, addr_key, nm_ip_route_get_dest(route));
 
-        netmask = _nm_utils_ip4_prefix_to_netmask(nm_ip_route_get_prefix(route));
-        svSetValueStr(routefile, netmask_key, _nm_utils_inet4_ntop(netmask, buf));
+        netmask = nm_ip4_addr_netmask_from_prefix(nm_ip_route_get_prefix(route));
+        svSetValueStr(routefile, netmask_key, nm_inet4_ntop(netmask, buf));
 
         svSetValueStr(routefile, gw_key, nm_ip_route_get_next_hop(route));
 
@@ -2826,9 +2828,7 @@ write_ip4_setting(NMConnection *connection,
         if (has_netmask) {
             char buf[INET_ADDRSTRLEN];
 
-            svSetValueStr(ifcfg,
-                          tag,
-                          _nm_utils_inet4_ntop(_nm_utils_ip4_prefix_to_netmask(prefix), buf));
+            svSetValueStr(ifcfg, tag, nm_inet4_ntop(nm_ip4_addr_netmask_from_prefix(prefix), buf));
         }
 
         n++;
@@ -2937,6 +2937,10 @@ write_ip4_setting(NMConnection *connection,
         }
         svSetValueStr(ifcfg, "DHCP_REJECT_SERVERS", str->str);
     }
+
+    svSetValueTernary(ifcfg,
+                      "IPV4_AUTO_ROUTE_EXT_GW",
+                      nm_setting_ip_config_get_auto_route_ext_gw(s_ip4));
 }
 
 static void
@@ -3197,6 +3201,10 @@ write_ip6_setting(NMConnection *connection, shvarFile *ifcfg, GString **out_rout
     write_res_options(ifcfg, s_ip6, "IPV6_RES_OPTIONS");
 
     NM_SET_OUT(out_route6_content, write_route_file(s_ip6));
+
+    svSetValueTernary(ifcfg,
+                      "IPV6_AUTO_ROUTE_EXT_GW",
+                      nm_setting_ip_config_get_auto_route_ext_gw(s_ip6));
 }
 
 static void
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Vlan_test-vlan-interface.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Vlan_test-vlan-interface.cexpected
index 6ad151aa..6373da90 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Vlan_test-vlan-interface.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Vlan_test-vlan-interface.cexpected
@@ -2,6 +2,7 @@ VLAN=yes
 TYPE=Vlan
 PHYSDEV=eth9
 VLAN_ID=43
+VLAN_PROTOCOL=802.1ad
 REORDER_HDR=yes
 GVRP=yes
 VLAN_FLAGS=LOOSE_BINDING
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-netmask-1 b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-netmask-1
index ecb36c37..5e40362e 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-netmask-1
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-netmask-1
@@ -1,4 +1,7 @@
 DNS1="192.0.2.1"
+DNS2="192.0.2.2#adfs.afddsaf"
+DNS3="1::2"
+DNS4="1::3#dfdf.er"
 IPADDR="102.0.2.2"
 GATEWAY="192.0.2.1"
 NETMASK="255.254.0.0"
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-netmask-1.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-netmask-1.cexpected
index 87493ac2..d0eeeb99 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-netmask-1.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-netmask-1.cexpected
@@ -1,4 +1,7 @@
 DNS1=192.0.2.1
+DNS2=192.0.2.2#adfs.afddsaf
+DNS3=1::2
+DNS4=1::3#dfdf.er
 IPADDR=102.0.2.2
 GATEWAY=192.0.2.1
 NETMASK=255.254.0.0
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-vlan-interface b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-vlan-interface
index d8d9193f..38fe4069 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-vlan-interface
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-vlan-interface
@@ -5,6 +5,7 @@ PHYSDEV=eth9
 VLAN_FLAGS=GVRP,LOOSE_BINDING
 VLAN_INGRESS_PRIORITY_MAP=0:1,2:5
 VLAN_EGRESS_PRIORITY_MAP=12:3,14:7,3:1
+VLAN_PROTOCOL=802.1ad
 ONBOOT=yes
 BOOTPROTO=static
 IPADDR=192.168.43.149
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wired-static-routes b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wired-static-routes
index 7faf49bd..fac9a5a2 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wired-static-routes
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wired-static-routes
@@ -9,7 +9,7 @@ IPV6INIT=no
 MTU=1492
 NM_CONTROLLED=yes
 DNS1=4.2.2.1
-DNS2=4.2.2.2
+DNS2=4.2.2.2#dns.name
 IPADDR=192.168.1.5
 NETMASK=255.255.255.0
 GATEWAY=192.168.1.1
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/route-test-wired-static-routes b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/route-test-wired-static-routes
index 717ad1ac..728e8e96 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/route-test-wired-static-routes
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/route-test-wired-static-routes
@@ -6,7 +6,7 @@ ADDRESS1=44.55.66.77
 NETMASK1=255.255.255.255
 GATEWAY1=192.168.1.7
 METRIC1=3
-OPTIONS1="mtu lock 9000 cwnd 12 src 1.1.1.1 tos 0x28 window 30000 scope 10 initcwnd lock 13 initrwnd 14 rto_min 300 advmss 1300 quickack 1"
+OPTIONS1="mtu lock 9000 cwnd 12 src 1.1.1.1 tos 0x28 window 30000 scope 10 initcwnd lock 13 initrwnd 14 rto_min 300 advmss 1300 quickack 1 weight 5"
 
 ADDRESS2=44.55.66.78
 NETMASK2=255.255.255.255
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c b/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c
index 886a605f..40ff7c67 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c
+++ b/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c
@@ -461,6 +461,7 @@ test_read_netmask_1(void)
     gs_free char                 *content    = NULL;
     NMSettingConnection          *s_con;
     NMSettingIPConfig            *s_ip4;
+    NMSettingIPConfig            *s_ip6;
     NMIPAddress                  *ip4_addr;
     const char                   *FILENAME = TEST_IFCFG_DIR "/ifcfg-netmask-1";
 
@@ -470,12 +471,20 @@ test_read_netmask_1(void)
     g_assert_cmpstr(nm_setting_connection_get_id(s_con), ==, "System netmask-1");
 
     s_ip4 = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_IP4_CONFIG);
-    g_assert_cmpuint(nm_setting_ip_config_get_num_dns(s_ip4), ==, 1);
+    g_assert_cmpuint(nm_setting_ip_config_get_num_dns(s_ip4), ==, 2);
+    g_assert_cmpstr(nm_setting_ip_config_get_dns(s_ip4, 0), ==, "192.0.2.1");
+    g_assert_cmpstr(nm_setting_ip_config_get_dns(s_ip4, 1), ==, "192.0.2.2#adfs.afddsaf");
+
     ip4_addr = nm_setting_ip_config_get_address(s_ip4, 0);
     g_assert(ip4_addr);
     g_assert_cmpstr(nm_ip_address_get_address(ip4_addr), ==, "102.0.2.2");
     g_assert_cmpint(nm_ip_address_get_prefix(ip4_addr), ==, 15);
 
+    s_ip6 = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_IP6_CONFIG);
+    g_assert_cmpuint(nm_setting_ip_config_get_num_dns(s_ip6), ==, 2);
+    g_assert_cmpstr(nm_setting_ip_config_get_dns(s_ip6, 0), ==, "1::2");
+    g_assert_cmpstr(nm_setting_ip_config_get_dns(s_ip6, 1), ==, "1::3#dfdf.er");
+
     nmtst_assert_connection_verifies_without_normalization(connection);
 
     content = nmtst_file_get_contents(FILENAME);
@@ -1356,6 +1365,7 @@ test_read_wired_static_routes(void)
     nmtst_assert_route_attribute_byte(ip4_route, NM_IP_ROUTE_ATTRIBUTE_TOS, 0x28);
     nmtst_assert_route_attribute_uint32(ip4_route, NM_IP_ROUTE_ATTRIBUTE_WINDOW, 30000);
     nmtst_assert_route_attribute_uint32(ip4_route, NM_IP_ROUTE_ATTRIBUTE_CWND, 12);
+    nmtst_assert_route_attribute_uint32(ip4_route, NM_IP_ROUTE_ATTRIBUTE_WEIGHT, 5);
     nmtst_assert_route_attribute_uint32(ip4_route, NM_IP_ROUTE_ATTRIBUTE_INITCWND, 13);
     nmtst_assert_route_attribute_uint32(ip4_route, NM_IP_ROUTE_ATTRIBUTE_INITRWND, 14);
     nmtst_assert_route_attribute_uint32(ip4_route, NM_IP_ROUTE_ATTRIBUTE_MTU, 9000);
@@ -1390,6 +1400,10 @@ test_read_wired_static_routes(void)
     g_assert_cmpint(nm_ip_route_get_prefix(ip4_route), ==, 32);
     nmtst_assert_route_attribute_string(ip4_route, NM_IP_ROUTE_ATTRIBUTE_TYPE, "local");
     nmtst_assert_route_attribute_byte(ip4_route, NM_IP_ROUTE_ATTRIBUTE_SCOPE, 254);
+
+    g_assert_cmpint(nm_setting_ip_config_get_num_dns(s_ip4), ==, 2);
+    g_assert_cmpstr(nm_setting_ip_config_get_dns(s_ip4, 0), ==, "4.2.2.1");
+    g_assert_cmpstr(nm_setting_ip_config_get_dns(s_ip4, 1), ==, "4.2.2.2#dns.name");
 }
 
 static void
@@ -2036,11 +2050,11 @@ test_read_wired_aliases_good(gconstpointer test_data)
                                                           NULL};
     const char                   *expected_address_3[] = {"192.168.1.5", "192.168.1.6", NULL};
     const char                   *expected_label_0[]   = {
-                            NULL,
-                            "aliasem0:1",
-                            "aliasem0:2",
-                            "aliasem0:99",
-                            NULL,
+        NULL,
+        "aliasem0:1",
+        "aliasem0:2",
+        "aliasem0:99",
+        NULL,
     };
     const char *expected_label_3[] = {
         NULL,
@@ -2082,7 +2096,7 @@ test_read_wired_aliases_good(gconstpointer test_data)
         g_assert(ip4_addr != NULL);
 
         addr = nm_ip_address_get_address(ip4_addr);
-        g_assert(nm_utils_ipaddr_is_valid(AF_INET, addr));
+        g_assert(nm_inet_is_valid(AF_INET, addr));
 
         for (j = 0; j < expected_num_addresses; j++) {
             if (!g_strcmp0(addr, expected_address[j]))
@@ -4266,6 +4280,7 @@ static void
 test_write_wired_static(void)
 {
     nmtst_auto_unlinkfile char   *testfile   = NULL;
+    nmtst_auto_unlinkfile char   *route4file = NULL;
     nmtst_auto_unlinkfile char   *route6file = NULL;
     gs_unref_object NMConnection *connection = NULL;
     gs_unref_object NMConnection *reread     = NULL;
@@ -4275,6 +4290,7 @@ test_write_wired_static(void)
     NMSettingIPConfig            *s_ip6, *reread_s_ip6;
     NMIPAddress                  *addr;
     NMIPAddress                  *addr6;
+    NMIPRoute                    *route4;
     NMIPRoute                    *route6;
     GError                       *error = NULL;
 
@@ -4379,6 +4395,13 @@ test_write_wired_static(void)
     nm_setting_ip_config_add_route(s_ip6, route6);
     nm_ip_route_unref(route6);
 
+    route4 = nm_ip_route_new(AF_INET, "1.1.1.1", 24, "1.2.3.4", 99, &error);
+    g_assert_no_error(error);
+    nm_ip_route_set_attribute(route4, NM_IP_ROUTE_ATTRIBUTE_CWND, g_variant_new_uint32(100));
+    nm_ip_route_set_attribute(route4, NM_IP_ROUTE_ATTRIBUTE_WEIGHT, g_variant_new_uint32(5));
+    nm_setting_ip_config_add_route(s_ip4, route4);
+    nm_ip_route_unref(route4);
+
     /* DNS servers */
     nm_setting_ip_config_add_dns(s_ip6, "fade:0102:0103::face");
     nm_setting_ip_config_add_dns(s_ip6, "cafe:ffff:eeee:dddd:cccc:bbbb:aaaa:feed");
@@ -4390,7 +4413,9 @@ test_write_wired_static(void)
     nmtst_assert_connection_verifies(connection);
 
     _writer_new_connection(connection, TEST_SCRATCH_DIR, &testfile);
+
     route6file = utils_get_route6_path(testfile);
+    route4file = utils_get_route_path(testfile);
 
     reread = _connection_from_file(testfile, NULL, TYPE_ETHERNET, NULL);
 
@@ -7754,6 +7779,7 @@ test_read_vlan_interface(void)
 
     g_assert_cmpstr(nm_setting_vlan_get_parent(s_vlan), ==, "eth9");
     g_assert_cmpint(nm_setting_vlan_get_id(s_vlan), ==, 43);
+    g_assert_cmpstr(nm_setting_vlan_get_protocol(s_vlan), ==, "802.1ad");
     g_assert_cmpint(nm_setting_vlan_get_flags(s_vlan),
                     ==,
                     NM_VLAN_FLAG_GVRP | NM_VLAN_FLAG_LOOSE_BINDING | NM_VLAN_FLAG_REORDER_HEADERS);
@@ -7930,6 +7956,7 @@ test_read_vlan_flags_1(void)
 
     g_assert_cmpstr(nm_setting_vlan_get_parent(s_vlan), ==, "eth9");
     g_assert_cmpint(nm_setting_vlan_get_id(s_vlan), ==, 44);
+    g_assert_cmpstr(nm_setting_vlan_get_protocol(s_vlan), ==, NULL);
     g_assert_cmpint(nm_setting_vlan_get_flags(s_vlan),
                     ==,
                     NM_VLAN_FLAG_LOOSE_BINDING | NM_VLAN_FLAG_REORDER_HEADERS);
diff --git a/src/core/settings/plugins/ifupdown/nms-ifupdown-parser.c b/src/core/settings/plugins/ifupdown/nms-ifupdown-parser.c
index a1d6781b..3bdd78c9 100644
--- a/src/core/settings/plugins/ifupdown/nms-ifupdown-parser.c
+++ b/src/core/settings/plugins/ifupdown/nms-ifupdown-parser.c
@@ -451,7 +451,7 @@ update_ip4_setting_from_if_block(NMConnection *connection, if_block *block, GErr
                             netmask_v);
                 return FALSE;
             } else {
-                netmask_int = _nm_utils_ip4_netmask_to_prefix(tmp_mask);
+                netmask_int = nm_ip4_addr_netmask_to_prefix(tmp_mask);
             }
         }
 
@@ -470,7 +470,7 @@ update_ip4_setting_from_if_block(NMConnection *connection, if_block *block, GErr
         /* gateway */
         gateway_v = ifparser_getkey(block, "gateway");
         if (gateway_v) {
-            if (!nm_utils_ipaddr_is_valid(AF_INET, gateway_v)) {
+            if (!nm_inet_is_valid(AF_INET, gateway_v)) {
                 g_set_error(error,
                             NM_SETTINGS_ERROR,
                             NM_SETTINGS_ERROR_INVALID_CONNECTION,
@@ -579,7 +579,7 @@ update_ip6_setting_from_if_block(NMConnection *connection, if_block *block, GErr
 
         gateway_v = ifparser_getkey(block, "gateway");
         if (gateway_v) {
-            if (!nm_utils_ipaddr_is_valid(AF_INET6, gateway_v)) {
+            if (!nm_inet_is_valid(AF_INET6, gateway_v)) {
                 g_set_error(error,
                             NM_SETTINGS_ERROR,
                             NM_SETTINGS_ERROR_INVALID_CONNECTION,
diff --git a/src/core/settings/plugins/ifupdown/nms-ifupdown-plugin.h b/src/core/settings/plugins/ifupdown/nms-ifupdown-plugin.h
index edf1b317..570bf2a3 100644
--- a/src/core/settings/plugins/ifupdown/nms-ifupdown-plugin.h
+++ b/src/core/settings/plugins/ifupdown/nms-ifupdown-plugin.h
@@ -11,7 +11,7 @@
 
 #define NMS_TYPE_IFUPDOWN_PLUGIN (nms_ifupdown_plugin_get_type())
 #define NMS_IFUPDOWN_PLUGIN(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NMS_TYPE_IFUPDOWN_PLUGIN, NMSIfupdownPlugin))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NMS_TYPE_IFUPDOWN_PLUGIN, NMSIfupdownPlugin))
 #define NMS_IFUPDOWN_PLUGIN_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NMS_TYPE_IFUPDOWN_PLUGIN, NMSIfupdownPluginClass))
 #define NMS_IS_IFUPDOWN_PLUGIN(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMS_TYPE_IFUPDOWN_PLUGIN))
diff --git a/src/core/settings/plugins/keyfile/nms-keyfile-plugin.h b/src/core/settings/plugins/keyfile/nms-keyfile-plugin.h
index 19e4122d..03a47526 100644
--- a/src/core/settings/plugins/keyfile/nms-keyfile-plugin.h
+++ b/src/core/settings/plugins/keyfile/nms-keyfile-plugin.h
@@ -14,7 +14,7 @@
 
 #define NMS_TYPE_KEYFILE_PLUGIN (nms_keyfile_plugin_get_type())
 #define NMS_KEYFILE_PLUGIN(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NMS_TYPE_KEYFILE_PLUGIN, NMSKeyfilePlugin))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NMS_TYPE_KEYFILE_PLUGIN, NMSKeyfilePlugin))
 #define NMS_KEYFILE_PLUGIN_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NMS_TYPE_KEYFILE_PLUGIN, NMSKeyfilePluginClass))
 #define NMS_IS_KEYFILE_PLUGIN(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMS_TYPE_KEYFILE_PLUGIN))
diff --git a/src/core/settings/plugins/keyfile/nms-keyfile-storage.h b/src/core/settings/plugins/keyfile/nms-keyfile-storage.h
index 8343cb2b..66521618 100644
--- a/src/core/settings/plugins/keyfile/nms-keyfile-storage.h
+++ b/src/core/settings/plugins/keyfile/nms-keyfile-storage.h
@@ -14,7 +14,7 @@
 
 #define NMS_TYPE_KEYFILE_STORAGE (nms_keyfile_storage_get_type())
 #define NMS_KEYFILE_STORAGE(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NMS_TYPE_KEYFILE_STORAGE, NMSKeyfileStorage))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NMS_TYPE_KEYFILE_STORAGE, NMSKeyfileStorage))
 #define NMS_KEYFILE_STORAGE_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NMS_TYPE_KEYFILE_STORAGE, NMSKeyfileStorageClass))
 #define NMS_IS_KEYFILE_STORAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMS_TYPE_KEYFILE_STORAGE))
diff --git a/src/core/settings/plugins/keyfile/tests/keyfiles/Test_Wired_Connection b/src/core/settings/plugins/keyfile/tests/keyfiles/Test_Wired_Connection
index f9ccc003..eb3754a2 100644
--- a/src/core/settings/plugins/keyfile/tests/keyfiles/Test_Wired_Connection
+++ b/src/core/settings/plugins/keyfile/tests/keyfiles/Test_Wired_Connection
@@ -36,7 +36,7 @@ routes9=1.1.1.9/19,0.0.0.0,0
 route10=1.1.1.10/21,,0
 routes10=1.1.1.10/20,,0
 routes11=1.1.1.11/21,,21
-routes11_options=cwnd=10,lock-cwnd=true,mtu=1430,src=7.7.7.7,type=unicast
+routes11_options=cwnd=10,lock-cwnd=true,mtu=1430,src=7.7.7.7,type=unicast,weight=5
 routes12=1.2.3.4/32
 routes12_options=type=local
 address30=1.2.3.30/24
diff --git a/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c b/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c
index 47be3a5e..83019bab 100644
--- a/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c
+++ b/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c
@@ -311,6 +311,7 @@ test_read_valid_wired_connection(void)
 
     nmtst_assert_route_attribute_uint32(route, NM_IP_ROUTE_ATTRIBUTE_CWND, 10);
     nmtst_assert_route_attribute_uint32(route, NM_IP_ROUTE_ATTRIBUTE_MTU, 1430);
+    nmtst_assert_route_attribute_uint32(route, NM_IP_ROUTE_ATTRIBUTE_WEIGHT, 5);
     nmtst_assert_route_attribute_boolean(route, NM_IP_ROUTE_ATTRIBUTE_LOCK_CWND, TRUE);
     nmtst_assert_route_attribute_string(route, NM_IP_ROUTE_ATTRIBUTE_SRC, "7.7.7.7");
     nmtst_assert_route_attribute_string(route, NM_IP_ROUTE_ATTRIBUTE_TYPE, "unicast");
@@ -495,6 +496,7 @@ test_write_wired_connection(void)
     g_assert_no_error(error);
     nm_ip_route_set_attribute(rt, NM_IP_ROUTE_ATTRIBUTE_CWND, g_variant_new_uint32(10));
     nm_ip_route_set_attribute(rt, NM_IP_ROUTE_ATTRIBUTE_MTU, g_variant_new_uint32(1492));
+    nm_ip_route_set_attribute(rt, NM_IP_ROUTE_ATTRIBUTE_WEIGHT, g_variant_new_uint32(5));
     nm_ip_route_set_attribute(rt, NM_IP_ROUTE_ATTRIBUTE_SRC, g_variant_new_string("1.2.3.4"));
     g_assert(nm_setting_ip_config_add_route(s_ip4, rt));
     nm_ip_route_unref(rt);
@@ -2298,7 +2300,7 @@ test_read_missing_id_uuid(void)
     const char                   *FILENAME       = TEST_KEYFILES_DIR "/Test_Missing_ID_UUID";
     const char                    F[] = "keyfile\0" TEST_KEYFILES_DIR "/Test_Missing_ID_UUID";
 
-    expected_uuid = nm_uuid_generate_from_strings("keyfile", FILENAME, NULL);
+    expected_uuid = nm_uuid_generate_from_strings_old("keyfile", FILENAME);
 
     expected_uuid2 =
         nm_uuid_generate_from_string_str(F, sizeof(F), NM_UUID_TYPE_VERSION3, &nm_uuid_ns_1);
diff --git a/src/core/supplicant/nm-supplicant-config.c b/src/core/supplicant/nm-supplicant-config.c
index bd48ed92..c63a0058 100644
--- a/src/core/supplicant/nm-supplicant-config.c
+++ b/src/core/supplicant/nm-supplicant-config.c
@@ -153,16 +153,16 @@ nm_supplicant_config_add_option_with_type(NMSupplicantConfig *self,
         return FALSE;
     }
 
-    opt        = g_slice_new0(ConfigOption);
-    opt->value = g_malloc(len + 1);
-    memcpy(opt->value, value, len);
-    opt->value[len] = '\0';
-
-    opt->len  = len;
-    opt->type = type;
+    opt  = g_slice_new(ConfigOption);
+    *opt = (ConfigOption){
+        .value = nm_memdup_nul(value, len),
+        .len   = len,
+        .type  = type,
+    };
 
     {
         char buf[255];
+
         memset(&buf[0], 0, sizeof(buf));
         memcpy(&buf[0], opt->value, opt->len > 254 ? 254 : opt->len);
         nm_log_info(LOGD_SUPPLICANT,
@@ -1379,12 +1379,24 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
     }
 
     phase1_auth_flags = nm_setting_802_1x_get_phase1_auth_flags(setting);
-    if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_0_DISABLE))
+    if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_0_ENABLE))
+        g_string_append_printf(phase1, "%stls_disable_tlsv1_0=0", (phase1->len ? " " : ""));
+    else if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_0_DISABLE))
         g_string_append_printf(phase1, "%stls_disable_tlsv1_0=1", (phase1->len ? " " : ""));
-    if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_1_DISABLE))
+    if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_1_ENABLE))
+        g_string_append_printf(phase1, "%stls_disable_tlsv1_1=0", (phase1->len ? " " : ""));
+    else if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_1_DISABLE))
         g_string_append_printf(phase1, "%stls_disable_tlsv1_1=1", (phase1->len ? " " : ""));
-    if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_2_DISABLE))
+    if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_2_ENABLE))
+        g_string_append_printf(phase1, "%stls_disable_tlsv1_2=0", (phase1->len ? " " : ""));
+    else if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_2_DISABLE))
         g_string_append_printf(phase1, "%stls_disable_tlsv1_2=1", (phase1->len ? " " : ""));
+    if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_3_ENABLE))
+        g_string_append_printf(phase1, "%stls_disable_tlsv1_3=0", (phase1->len ? " " : ""));
+    else if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_3_DISABLE))
+        g_string_append_printf(phase1, "%stls_disable_tlsv1_3=1", (phase1->len ? " " : ""));
+    if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_DISABLE_TIME_CHECKS))
+        g_string_append_printf(phase1, "%stls_disable_time_checks=1", (phase1->len ? " " : ""));
 
     if (phase1->len) {
         if (!add_string_val(self, phase1->str, "phase1", FALSE, NULL, error)) {
diff --git a/src/core/supplicant/nm-supplicant-config.h b/src/core/supplicant/nm-supplicant-config.h
index ee7f4dc8..063f7f55 100644
--- a/src/core/supplicant/nm-supplicant-config.h
+++ b/src/core/supplicant/nm-supplicant-config.h
@@ -16,7 +16,7 @@
 
 #define NM_TYPE_SUPPLICANT_CONFIG (nm_supplicant_config_get_type())
 #define NM_SUPPLICANT_CONFIG(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SUPPLICANT_CONFIG, NMSupplicantConfig))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SUPPLICANT_CONFIG, NMSupplicantConfig))
 #define NM_SUPPLICANT_CONFIG_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_SUPPLICANT_CONFIG, NMSupplicantConfigClass))
 #define NM_IS_SUPPLICANT_CONFIG(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_SUPPLICANT_CONFIG))
diff --git a/src/core/supplicant/nm-supplicant-interface.c b/src/core/supplicant/nm-supplicant-interface.c
index fa8b4e37..18525724 100644
--- a/src/core/supplicant/nm-supplicant-interface.c
+++ b/src/core/supplicant/nm-supplicant-interface.c
@@ -2647,7 +2647,7 @@ scan_request_cb(GObject *source, GAsyncResult *result, gpointer user_data)
         _LOGD("request-scan: request cancelled");
     else {
         if (error) {
-            if (_nm_dbus_error_has_name(error, "fi.w1.wpa_supplicant1.Interface.ScanError"))
+            if (nm_dbus_error_is(error, "fi.w1.wpa_supplicant1.Interface.ScanError"))
                 _LOGD("request-scan: could not get scan request result: %s", error->message);
             else {
                 g_dbus_error_strip_remote_error(error);
@@ -3174,7 +3174,7 @@ _signal_handle(NMSupplicantInterface *self,
 
                         _set_p2p_assigned_addr(iface,
                                                addr_data,
-                                               _nm_utils_ip4_netmask_to_prefix(netmask));
+                                               nm_ip4_addr_netmask_to_prefix(netmask));
                     } else {
                         _LOGW("P2P: GroupStarted signaled invalid IP Address information");
                     }
diff --git a/src/core/supplicant/nm-supplicant-interface.h b/src/core/supplicant/nm-supplicant-interface.h
index 5ccb455c..b8d9b013 100644
--- a/src/core/supplicant/nm-supplicant-interface.h
+++ b/src/core/supplicant/nm-supplicant-interface.h
@@ -58,7 +58,7 @@ typedef enum {
 
 #define NM_TYPE_SUPPLICANT_INTERFACE (nm_supplicant_interface_get_type())
 #define NM_SUPPLICANT_INTERFACE(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SUPPLICANT_INTERFACE, NMSupplicantInterface))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SUPPLICANT_INTERFACE, NMSupplicantInterface))
 #define NM_SUPPLICANT_INTERFACE_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_SUPPLICANT_INTERFACE, NMSupplicantInterfaceClass))
 #define NM_IS_SUPPLICANT_INTERFACE(obj) \
diff --git a/src/core/supplicant/nm-supplicant-manager.c b/src/core/supplicant/nm-supplicant-manager.c
index 2ec7db23..f6927500 100644
--- a/src/core/supplicant/nm-supplicant-manager.c
+++ b/src/core/supplicant/nm-supplicant-manager.c
@@ -450,7 +450,7 @@ _create_iface_dbus_call_get_interface_cb(GObject *source, GAsyncResult *result,
         char ifname[NMP_IFNAMSIZ];
 
         if (handle->create_iface_try_count < CREATE_IFACE_TRY_COUNT_MAX
-            && _nm_dbus_error_has_name(error, NM_WPAS_ERROR_UNKNOWN_IFACE)
+            && nm_dbus_error_is(error, NM_WPAS_ERROR_UNKNOWN_IFACE)
             && nm_platform_if_indextoname(NM_PLATFORM_GET, handle->ifindex, ifname)) {
             /* Before, supplicant told us the interface existed. Was there a race?
              * Try again. */
@@ -500,7 +500,7 @@ _create_iface_dbus_call_create_interface_cb(GObject      *source,
                 nm_assert(handle->self);
                 TRUE;
             })
-            && _nm_dbus_error_has_name(error, NM_WPAS_ERROR_EXISTS_ERROR)
+            && nm_dbus_error_is(error, NM_WPAS_ERROR_EXISTS_ERROR)
             && nm_platform_if_indextoname(NM_PLATFORM_GET, handle->ifindex, ifname)) {
             self = handle->self;
             _LOGT("create-iface[" NM_HASH_OBFUSCATE_PTR_FMT
diff --git a/src/core/supplicant/nm-supplicant-manager.h b/src/core/supplicant/nm-supplicant-manager.h
index ab163b0d..b3284b70 100644
--- a/src/core/supplicant/nm-supplicant-manager.h
+++ b/src/core/supplicant/nm-supplicant-manager.h
@@ -12,7 +12,7 @@
 
 #define NM_TYPE_SUPPLICANT_MANAGER (nm_supplicant_manager_get_type())
 #define NM_SUPPLICANT_MANAGER(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SUPPLICANT_MANAGER, NMSupplicantManager))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SUPPLICANT_MANAGER, NMSupplicantManager))
 #define NM_SUPPLICANT_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_SUPPLICANT_MANAGER, NMSupplicantManagerClass))
 #define NM_IS_SUPPLICANT_MANAGER(obj) \
diff --git a/src/core/supplicant/nm-supplicant-settings-verify.c b/src/core/supplicant/nm-supplicant-settings-verify.c
index 9881c552..fc40bade 100644
--- a/src/core/supplicant/nm-supplicant-settings-verify.c
+++ b/src/core/supplicant/nm-supplicant-settings-verify.c
@@ -113,7 +113,11 @@ static const struct Opt opt_table[] = {
                              "tls_disable_tlsv1_1=0",
                              "tls_disable_tlsv1_1=1",
                              "tls_disable_tlsv1_2=0",
-                             "tls_disable_tlsv1_2=1", )),
+                             "tls_disable_tlsv1_2=1",
+                             "tls_disable_tlsv1_3=0",
+                             "tls_disable_tlsv1_3=1",
+                             "tls_disable_time_checks=0",
+                             "tls_disable_time_checks=1", )),
     OPT_KEYWORD("phase2",
                 NM_MAKE_STRV("auth=PAP",
                              "auth=CHAP",
@@ -277,12 +281,12 @@ nm_supplicant_settings_verify_setting(const char *key, const char *value, const
         }
     }
 
-    opt_idx = nm_utils_array_find_binary_search(opt_table,
-                                                sizeof(opt_table[0]),
-                                                G_N_ELEMENTS(opt_table),
-                                                &key,
-                                                nm_strcmp_p_with_data,
-                                                NULL);
+    opt_idx = nm_array_find_bsearch(opt_table,
+                                    G_N_ELEMENTS(opt_table),
+                                    sizeof(opt_table[0]),
+                                    &key,
+                                    nm_strcmp_p_with_data,
+                                    NULL);
     if (opt_idx < 0) {
         if (nm_streq(key, "mode")) {
             if (len != 1)
diff --git a/src/core/tests/config/global-dns-good.conf b/src/core/tests/config/global-dns-good.conf
new file mode 100644
index 00000000..6265a611
--- /dev/null
+++ b/src/core/tests/config/global-dns-good.conf
@@ -0,0 +1,13 @@
+# Good configuration, since there is a default domain section
+
+[global-dns]
+searches=foo.com
+options=timeout:5
+
+[global-dns-domain-*]
+servers=4.5.6.7
+options=myoption1
+
+[global-dns-domain-test.com]
+servers=1.2.3.4
+options=myoption2
diff --git a/src/core/tests/config/global-dns-options.conf b/src/core/tests/config/global-dns-options.conf
new file mode 100644
index 00000000..0be17735
--- /dev/null
+++ b/src/core/tests/config/global-dns-options.conf
@@ -0,0 +1,5 @@
+# Good configuration, since there is no domain section
+
+[global-dns]
+searches=foo.com
+options=timeout:5
diff --git a/src/core/tests/config/nm-test-device.h b/src/core/tests/config/nm-test-device.h
index 638177a0..48a53bf4 100644
--- a/src/core/tests/config/nm-test-device.h
+++ b/src/core/tests/config/nm-test-device.h
@@ -9,7 +9,8 @@
 #include "devices/nm-device.h"
 
 #define NM_TYPE_TEST_DEVICE (nm_test_device_get_type())
-#define NM_TEST_DEVICE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_TEST_DEVICE, NMTestDevice))
+#define NM_TEST_DEVICE(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_TEST_DEVICE, NMTestDevice))
 #define NM_TEST_DEVICE_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_TEST_DEVICE, NMTestDeviceClass))
 #define NM_IS_TEST_DEVICE(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_TEST_DEVICE))
diff --git a/src/core/tests/config/test-config.c b/src/core/tests/config/test-config.c
index 17a66b06..054b9003 100644
--- a/src/core/tests/config/test-config.c
+++ b/src/core/tests/config/test-config.c
@@ -370,7 +370,21 @@ test_config_global_dns(void)
 
     g_object_unref(config);
 
-    /* Check that a file without a default domain section gives a NULL configuration */
+    /* Check that a file with a default domain section gives a good configuration */
+    config =
+        setup_config(NULL, TEST_DIR "/global-dns-good.conf", "", NULL, "/no/such/dir", "", NULL);
+    dns = nm_config_data_get_global_dns_config(nm_config_get_data_orig(config));
+    g_assert(dns);
+    g_object_unref(config);
+
+    /* Check that a file with options but no domains gives a good configuration */
+    config =
+        setup_config(NULL, TEST_DIR "/global-dns-options.conf", "", NULL, "/no/such/dir", "", NULL);
+    dns = nm_config_data_get_global_dns_config(nm_config_get_data_orig(config));
+    g_assert(dns);
+    g_object_unref(config);
+
+    /* Check that a file with a domain domain, but without a default one gives a NULL configuration */
     config =
         setup_config(NULL, TEST_DIR "/global-dns-invalid.conf", "", NULL, "/no/such/dir", "", NULL);
     dns = nm_config_data_get_global_dns_config(nm_config_get_data_orig(config));
@@ -1103,9 +1117,9 @@ test_config_set_values(void)
     const char               *CONFIG_USER               = BUILD_DIR "/test-set-values-user.conf";
     const char               *CONFIG_INTERN             = BUILD_DIR "/test-set-values-intern.conf";
     const char               *atomic_section_prefixes[] = {
-                      "atomic-prefix-1.",
-                      "atomic-prefix-2.",
-                      NULL,
+        "atomic-prefix-1.",
+        "atomic-prefix-2.",
+        NULL,
     };
 
     g_assert(g_file_set_contents(CONFIG_USER, "", 0, NULL));
diff --git a/src/core/tests/test-core-with-expect.c b/src/core/tests/test-core-with-expect.c
index de510c76..b05144ac 100644
--- a/src/core/tests/test-core-with-expect.c
+++ b/src/core/tests/test-core-with-expect.c
@@ -227,11 +227,11 @@ do_test_nm_utils_kill_child(void)
 {
     GLogLevelFlags fatal_mask;
     char          *argv_watchdog[] = {
-                 "bash",
-                 "-c",
-                 "sleep 4; "
-                          "kill -KILL 0; #watchdog for #" TEST_TOKEN,
-                 NULL,
+        "bash",
+        "-c",
+        "sleep 4; "
+                 "kill -KILL 0; #watchdog for #" TEST_TOKEN,
+        NULL,
     };
     char *argv1[] = {
         "bash",
@@ -282,7 +282,7 @@ do_test_nm_utils_kill_child(void)
     pid4a   = test_nm_utils_kill_child_spawn(argv4, TRUE);
 
     /* give processes time to start (and potentially block signals) ... */
-    g_usleep(G_USEC_PER_SEC / 10);
+    g_usleep(G_USEC_PER_SEC / 5);
 
     fatal_mask = g_log_set_always_fatal(G_LOG_FATAL_MASK);
 
@@ -338,12 +338,12 @@ do_test_nm_utils_kill_child(void)
         "(No child process*, 10) after sending no signal (0)");
     test_nm_utils_kill_child_sync_do("test-s-3-2", pid3s, 0, 0, FALSE, NULL);
 
-    NMTST_EXPECT_NM_DEBUG("kill child process 'test-s-4' (*): waiting up to 1 milliseconds for "
+    NMTST_EXPECT_NM_DEBUG("kill child process 'test-s-4' (*): waiting up to 50 milliseconds for "
                           "process to terminate normally after sending SIGTERM (15)...");
     NMTST_EXPECT_NM_DEBUG("kill child process 'test-s-4' (*): sending SIGKILL...");
     NMTST_EXPECT_NM_DEBUG("kill child process 'test-s-4' (*): after sending SIGTERM (15) and "
                           "SIGKILL, process * exited by signal 9 (* usec elapsed)");
-    test_nm_utils_kill_child_sync_do("test-s-4", pid4s, SIGTERM, 1, TRUE, &expected_signal_KILL);
+    test_nm_utils_kill_child_sync_do("test-s-4", pid4s, SIGTERM, 50, TRUE, &expected_signal_KILL);
 
     NMTST_EXPECT_NM_DEBUG("kill child process 'test-a-1-1' (*): wait for process to terminate "
                           "after sending SIGTERM (15) (send SIGKILL in 3000 milliseconds)...");
@@ -528,27 +528,27 @@ test_nm_utils_array_remove_at_indexes(void)
                 _remove_at_indexes_init_random_idx(idx, i_len, i_idx_len);
                 g_array_set_size(array, i_len);
                 for (i = 0; i < i_len; i++)
-                    g_array_index(array, gssize, i) = i;
+                    nm_g_array_index(array, gssize, i) = i;
 
-                nm_utils_array_remove_at_indexes(array, &g_array_index(idx, guint, 0), i_idx_len);
+                nm_utils_array_remove_at_indexes(array, nm_g_array_first_p(idx, guint), i_idx_len);
 
                 g_hash_table_remove_all(unique);
                 /* ensure that all the indexes are still unique */
                 for (i = 0; i < array->len; i++)
-                    g_hash_table_add(unique, GUINT_TO_POINTER(g_array_index(array, gssize, i)));
+                    g_hash_table_add(unique, GUINT_TO_POINTER(nm_g_array_index(array, gssize, i)));
                 g_assert_cmpint(g_hash_table_size(unique), ==, array->len);
 
                 for (i = 0; i < idx->len; i++)
-                    g_hash_table_add(unique, GUINT_TO_POINTER(g_array_index(idx, guint, i)));
+                    g_hash_table_add(unique, GUINT_TO_POINTER(nm_g_array_index(idx, guint, i)));
                 g_assert_cmpint(g_hash_table_size(unique), ==, i_len);
 
                 /* ensure proper sort order in array */
                 for (i = 0; i < array->len; i++) {
-                    gssize i1 = g_array_index(array, gssize, i);
+                    gssize i1 = nm_g_array_index(array, gssize, i);
 
                     g_assert(i1 >= 0 && i1 < i_len);
                     if (i > 0) {
-                        gsize i0 = g_array_index(array, gssize, i - 1);
+                        gsize i0 = nm_g_array_index(array, gssize, i - 1);
                         g_assert_cmpint(i0, <, i1);
                     }
                 }
diff --git a/src/core/tests/test-core.c b/src/core/tests/test-core.c
index c260788e..887803bf 100644
--- a/src/core/tests/test-core.c
+++ b/src/core/tests/test-core.c
@@ -40,7 +40,7 @@ test_config_h(void)
 
 /*****************************************************************************/
 
-/* Reference implementation for nm_utils_ip6_address_clear_host_address.
+/* Reference implementation for nm_ip6_addr_clear_host_address.
  * Taken originally from set_address_masked(), src/ndisc/nm-lndp-ndisc.c
  **/
 static void
@@ -95,14 +95,14 @@ test_nm_utils_ip6_address_clear_host_address(void)
             _randomize_in6_addr(&addr1, r);
             _randomize_in6_addr(&addr2, r);
             addr1 = addr_src;
-            nm_utils_ip6_address_clear_host_address(&addr2, &addr1, plen);
+            nm_ip6_addr_clear_host_address(&addr2, &addr1, plen);
             g_assert_cmpint(memcmp(&addr1, &addr_src, sizeof(struct in6_addr)), ==, 0);
             g_assert_cmpint(memcmp(&addr2, &addr_ref, sizeof(struct in6_addr)), ==, 0);
 
             /* test for self assignment/inplace update. */
             _randomize_in6_addr(&addr1, r);
             addr1 = addr_src;
-            nm_utils_ip6_address_clear_host_address(&addr1, &addr1, plen);
+            nm_ip6_addr_clear_host_address(&addr1, &addr1, plen);
             g_assert_cmpint(memcmp(&addr1, &addr_ref, sizeof(struct in6_addr)), ==, 0);
         }
     }
@@ -152,14 +152,14 @@ test_logging_error(void)
 static void
 _test_same_prefix(const char *a1, const char *a2, guint8 plen)
 {
-    struct in6_addr a = *nmtst_inet6_from_string(a1);
-    struct in6_addr b = *nmtst_inet6_from_string(a2);
+    struct in6_addr a = nmtst_inet6_from_string(a1);
+    struct in6_addr b = nmtst_inet6_from_string(a2);
 
-    g_assert(nm_utils_ip6_address_same_prefix(&a, &b, plen));
+    g_assert(nm_ip6_addr_same_prefix(&a, &b, plen));
 }
 
 static void
-test_nm_utils_ip6_address_same_prefix(void)
+test_nm_ip_addr_same_prefix(void)
 {
     guint       n, i;
     const guint N = 100;
@@ -202,7 +202,7 @@ again_plen:
         } else
             b = a;
 
-        result = nm_utils_ip6_address_same_prefix(&a.val, &b.val, plen);
+        result = nm_ip6_addr_same_prefix(&a.val, &b.val, plen);
         g_assert(result == is_same);
         g_assert(NM_IN_SET(result, TRUE, FALSE));
     }
@@ -214,12 +214,12 @@ again_plen:
         plen = nmtst_get_rand_uint32() % 129;
 
         memset(addrmask.ptr, 0xFF, sizeof(addrmask));
-        nm_utils_ip6_address_clear_host_address(&addrmask.val, &addrmask.val, plen);
+        nm_ip6_addr_clear_host_address(&addrmask.val, &addrmask.val, plen);
 
         for (i = 0; i < sizeof(a); i++)
             b.ptr[i] = (a.ptr[i] & addrmask.ptr[i]) | (b.ptr[i] & ~addrmask.ptr[i]);
 
-        g_assert(nm_utils_ip6_address_same_prefix(&a.val, &b.val, plen) == TRUE);
+        g_assert(nm_ip6_addr_same_prefix(&a.val, &b.val, plen) == TRUE);
     }
 
     /* test#3 */
@@ -234,10 +234,10 @@ again_plen:
             continue;
 
         memset(addrmask.ptr, 0xFF, sizeof(addrmask));
-        nm_utils_ip6_address_clear_host_address(&addrmask.val, &addrmask.val, plen);
+        nm_ip6_addr_clear_host_address(&addrmask.val, &addrmask.val, plen);
 
         memset(addrmask_bit.ptr, 0xFF, sizeof(addrmask_bit));
-        nm_utils_ip6_address_clear_host_address(&addrmask_bit.val, &addrmask_bit.val, plen - 1);
+        nm_ip6_addr_clear_host_address(&addrmask_bit.val, &addrmask_bit.val, plen - 1);
 
         for (i = 0; i < sizeof(a); i++)
             b.ptr[i] = (a.ptr[i] & addrmask.ptr[i]) | (b.ptr[i] & ~addrmask.ptr[i]);
@@ -254,7 +254,7 @@ again_plen:
         }
         g_assert(reached);
 
-        g_assert(nm_utils_ip6_address_same_prefix(&a.val, &b.val, plen) == FALSE);
+        g_assert(nm_ip6_addr_same_prefix(&a.val, &b.val, plen) == FALSE);
     }
 
     /* test#4 */
@@ -2594,10 +2594,9 @@ main(int argc, char **argv)
 
     g_test_add_func("/general/nm_strbuf_append", test_nm_utils_strbuf_append);
 
-    g_test_add_func("/general/nm_utils_ip6_address_clear_host_address",
+    g_test_add_func("/general/nm_ip6_addr_clear_host_address",
                     test_nm_utils_ip6_address_clear_host_address);
-    g_test_add_func("/general/nm_utils_ip6_address_same_prefix",
-                    test_nm_utils_ip6_address_same_prefix);
+    g_test_add_func("/general/nm_ip6_addr_same_prefix", test_nm_ip_addr_same_prefix);
     g_test_add_func("/general/nm_utils_log_connection_diff", test_nm_utils_log_connection_diff);
 
     g_test_add_func("/general/nm_utils_sysctl_ip_conf_path", test_nm_utils_sysctl_ip_conf_path);
diff --git a/src/core/tests/test-l3cfg.c b/src/core/tests/test-l3cfg.c
index a9ac4004..962b2eec 100644
--- a/src/core/tests/test-l3cfg.c
+++ b/src/core/tests/test-l3cfg.c
@@ -2,7 +2,7 @@
 
 #include "src/core/nm-default-daemon.h"
 
-#include <linux/if_addr.h>
+#include "nm-compat-headers/linux/if_addr.h"
 
 #include "nm-l3cfg.h"
 #include "nm-l3-ipv4ll.h"
@@ -339,6 +339,10 @@ test_l3cfg(gconstpointer test_data)
 
     _LOGD("test start (/l3cfg/%d)", TEST_IDX);
 
+    /* FIXME: https://bugzilla.redhat.com/show_bug.cgi?id=2165141 */
+    g_test_skip("Skip: this test is currently known to fail (rhbz#2165141)");
+    return;
+
     if (nmtst_test_quick()) {
         gs_free char *msg =
             g_strdup_printf("Skipping test: don't run long running test %s (NMTST_DEBUG=slow)\n",
@@ -418,7 +422,7 @@ test_l3cfg(gconstpointer test_data)
 
         nm_l3_config_data_add_address_6(
             l3cd,
-            NM_PLATFORM_IP6_ADDRESS_INIT(.address = *nmtst_inet6_from_string("1:2:3:4::45"),
+            NM_PLATFORM_IP6_ADDRESS_INIT(.address = nmtst_inet6_from_string("1:2:3:4::45"),
                                          .plen    = 64, ));
 
         if (nmtst_get_rand_one_case_in(2))
@@ -572,7 +576,7 @@ _test_l3_ipv4ll_signal_notify(NML3Cfg                    *l3cfg,
                               const NML3ConfigNotifyData *notify_data,
                               TestL3IPv4LLData           *tdata)
 {
-    char sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char sbuf_addr[NM_INET_ADDRSTRLEN];
 
     g_assert(NM_IS_L3CFG(l3cfg));
     g_assert(tdata);
@@ -593,9 +597,9 @@ _test_l3_ipv4ll_signal_notify(NML3Cfg                    *l3cfg,
             if (tdata->f->test_idx == 2 && nmtst_get_rand_bool()) {
                 tdata->addr_commit++;
                 tdata->addr_commit_addr = nm_l3_ipv4ll_get_addr(tdata->l3ipv4ll);
-                g_assert(nm_utils_ip4_address_is_link_local(tdata->addr_commit_addr));
+                g_assert(nm_ip4_addr_is_link_local(tdata->addr_commit_addr));
                 _LOGT("add address %s that passed ACD",
-                      _nm_utils_inet4_ntop(tdata->addr_commit_addr, sbuf_addr));
+                      nm_inet4_ntop(tdata->addr_commit_addr, sbuf_addr));
                 if (!nm_l3cfg_add_config(nm_l3_ipv4ll_get_l3cfg(tdata->l3ipv4ll),
                                          TEST_L3_IPV4LL_TAG(tdata, 1),
                                          nmtst_get_rand_bool(),
@@ -630,9 +634,9 @@ _test_l3_ipv4ll_signal_notify(NML3Cfg                    *l3cfg,
             if (tdata->addr_commit > 0) {
                 g_assert_cmpint(tdata->addr_commit, ==, 1);
                 tdata->addr_commit--;
-                g_assert(nm_utils_ip4_address_is_link_local(tdata->addr_commit_addr));
+                g_assert(nm_ip4_addr_is_link_local(tdata->addr_commit_addr));
                 _LOGT("remove address %s that previously passed ACD",
-                      _nm_utils_inet4_ntop(tdata->addr_commit_addr, sbuf_addr));
+                      nm_inet4_ntop(tdata->addr_commit_addr, sbuf_addr));
                 if (!nm_l3cfg_remove_config_all(nm_l3_ipv4ll_get_l3cfg(tdata->l3ipv4ll),
                                                 TEST_L3_IPV4LL_TAG(tdata, 1)))
                     g_assert_not_reached();
@@ -663,7 +667,7 @@ test_l3_ipv4ll(gconstpointer test_data)
     gint64                                                       start_time_msec;
     gint64                                                       total_poll_time_msec;
     nm_auto_remove_l3ipv4ll_registration NML3IPv4LLRegistration *l3ipv4ll_reg = NULL;
-    char sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char                                                         sbuf_addr[NM_INET_ADDRSTRLEN];
 
     _LOGD("test start (/l3-ipv4ll/%d)", TEST_IDX);
 
@@ -733,12 +737,12 @@ test_l3_ipv4ll(gconstpointer test_data)
     if (tdata->f->test_idx == 2 && nmtst_get_rand_one_case_in(3)) {
         in_addr_t a = nm_l3_ipv4ll_get_addr(l3ipv4ll);
 
-        g_assert(nm_utils_ip4_address_is_link_local(a));
+        g_assert(nm_ip4_addr_is_link_local(a));
         _test_l3_ipv4ll_maybe_add_addr_4(tdata,
                                          tdata->f->ifindex1,
                                          2,
                                          &tdata->add_conflict_done,
-                                         _nm_utils_inet4_ntop(a, sbuf_addr));
+                                         nm_inet4_ntop(a, sbuf_addr));
         g_assert_cmpint(tdata->f->hwaddr1.len, ==, sizeof(NMEtherAddr));
         acd_defender_2 =
             nmtstp_acd_defender_new(tdata->f->ifindex1, a, &tdata->f->hwaddr1.ether_addr);
@@ -768,12 +772,11 @@ test_l3_ipv4ll(gconstpointer test_data)
 
         if (tdata->addr_commit == 1 && !tdata->add_conflict_checked) {
             tdata->add_conflict_checked = TRUE;
-            _test_l3_ipv4ll_maybe_add_addr_4(
-                tdata,
-                tdata->f->ifindex1,
-                2,
-                &tdata->add_conflict_done,
-                _nm_utils_inet4_ntop(tdata->addr_commit_addr, sbuf_addr));
+            _test_l3_ipv4ll_maybe_add_addr_4(tdata,
+                                             tdata->f->ifindex1,
+                                             2,
+                                             &tdata->add_conflict_done,
+                                             nm_inet4_ntop(tdata->addr_commit_addr, sbuf_addr));
             if (tdata->add_conflict_done)
                 total_poll_time_msec += L3IPV4LL_ACD_TIMEOUT_MSEC / 2;
             g_assert_cmpint(tdata->f->hwaddr1.len, ==, sizeof(NMEtherAddr));
@@ -986,7 +989,7 @@ test_l3_ipv6ll(gconstpointer test_data)
         nmtstp_ip6_address_add(tdata->f->platform,
                                -1,
                                tdata->f->ifindex1,
-                               *nmtst_inet6_from_string(_LLADDR_TEST1),
+                               nmtst_inet6_from_string(_LLADDR_TEST1),
                                64,
                                in6addr_any,
                                NM_PLATFORM_LIFETIME_PERMANENT,
diff --git a/src/core/tests/test-systemd.c b/src/core/tests/test-systemd.c
index cf31ec33..1b0b7f65 100644
--- a/src/core/tests/test-systemd.c
+++ b/src/core/tests/test-systemd.c
@@ -12,21 +12,6 @@
 
 /*****************************************************************************/
 
-static void
-test_lldp_create(void)
-{
-    sd_lldp_rx *lldp = NULL;
-    int         r;
-
-    r = sd_lldp_rx_new(&lldp);
-    g_assert(r == 0);
-    g_assert(lldp);
-
-    sd_lldp_rx_unref(lldp);
-}
-
-/*****************************************************************************/
-
 typedef struct {
     GMainLoop       *mainloop;
     sd_event_source *event_source;
@@ -105,7 +90,6 @@ main(int argc, char **argv)
 {
     nmtst_init(&argc, &argv, TRUE);
 
-    g_test_add_func("/systemd/lldp/create", test_lldp_create);
     g_test_add_func("/systemd/sd-event", test_sd_event);
 
     return g_test_run();
diff --git a/src/core/vpn/nm-vpn-connection.c b/src/core/vpn/nm-vpn-connection.c
index bbb73550..4fb926a5 100644
--- a/src/core/vpn/nm-vpn-connection.c
+++ b/src/core/vpn/nm-vpn-connection.c
@@ -107,14 +107,15 @@ typedef struct {
     NMIPAddr gw_internal;
     NMIPAddr gw_external;
 
-    /* Whether this address family is enabled. If not, then we won't have a l3cd instance,
-     * but the activation for this address family is still complete. */
+    /* Whether VPN auto-configuration is enabled in the connection profile for
+     * this address family. */
+    bool method_auto : 1;
+
+    /* Whether VPN auto-configuration is enabled, in the connection profile AND
+     * in the configuration reported by the VPN. If not, then we won't have a
+     * l3cd instance, but the activation for this address family is still
+     * complete. */
     bool enabled : 1;
-
-    /* Whether this address family is ready. This means we received the IP configuration.
-     * Usually this implies we also have a corresponding l3cd, but that might not be the
-     * case if this address family is disabled. */
-    bool conf_ready : 1;
 } IPData;
 
 typedef struct {
@@ -1217,7 +1218,7 @@ _parent_device_l3cd_add_gateway_route(NML3ConfigData *l3cd,
             if (!nm_ip_addr_is_null(addr_family, gw)) {
                 nm_ip_addr_set(addr_family, &parent_gw, gw);
                 has_parent_gw = TRUE;
-            } else if (nm_utils_ip_is_site_local(addr_family, vpn_gw))
+            } else if (nm_ip_addr_is_site_local(addr_family, vpn_gw))
                 has_parent_gw = TRUE;
             else if ((obj = nm_device_get_best_default_route(parent_device, addr_family))
                      && nm_ip_addr_is_null(
@@ -1296,6 +1297,17 @@ _l3cfg_l3cd_gw_extern_update(NMVpnConnection *self)
 
     changed = FALSE;
     for (IS_IPv4 = 1; IS_IPv4 >= 0; IS_IPv4--) {
+        const int          addr_family = IS_IPv4 ? AF_INET : AF_INET6;
+        NMSettingIPConfig *s_ip;
+
+        s_ip = nm_connection_get_setting_ip_config(_get_applied_connection(self), addr_family);
+        if (s_ip && nm_setting_ip_config_get_auto_route_ext_gw(s_ip) == NM_TERNARY_FALSE) {
+            _LOGD("IPv%c route to the external gateway have been deactivated via auto-route-ext-gw "
+                  "setting",
+                  nm_utils_addr_family_to_char(addr_family));
+            continue;
+        }
+
         if (_parent_device_l3cd_add_gateway_route(
                 l3cd,
                 IS_IPv4 ? AF_INET : AF_INET6,
@@ -1870,9 +1882,16 @@ _dbus_signal_config_cb(NMVpnConnection *self, GVariant *dict)
     else
         priv->ip_data_6.enabled = FALSE;
 
-    _LOGD("config: reply received (IPv4:%s, IPv6:%s)",
+    _LOGD("config: reply received (IPv4:%s(%s), IPv6:%s(%s))",
           priv->ip_data_4.enabled ? "on" : "off",
-          priv->ip_data_6.enabled ? "on" : "off");
+          priv->ip_data_4.method_auto ? "auto" : "disabled",
+          priv->ip_data_4.enabled ? "on" : "off",
+          priv->ip_data_6.method_auto ? "auto" : "disabled");
+
+    if (!priv->ip_data_4.method_auto)
+        priv->ip_data_4.enabled = FALSE;
+    if (!priv->ip_data_6.method_auto)
+        priv->ip_data_6.enabled = FALSE;
 
     if (priv->vpn_state == STATE_CONNECT)
         _set_vpn_state(self, STATE_IP_CONFIG_GET, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);
@@ -1936,7 +1955,8 @@ _dbus_signal_ip_config_cb(NMVpnConnection *self, int addr_family, GVariant *dict
                 return;
             }
 
-            priv->ip_data_4.enabled = TRUE;
+            if (priv->ip_data_4.method_auto)
+                priv->ip_data_4.enabled = TRUE;
             priv->ip_data_6.enabled = FALSE;
         }
     } else {
@@ -1953,6 +1973,11 @@ _dbus_signal_ip_config_cb(NMVpnConnection *self, int addr_family, GVariant *dict
         _set_vpn_state(self, STATE_IP_CONFIG_GET, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);
     }
 
+    if (!priv->ip_data_x[IS_IPv4].enabled) {
+        _check_complete(self, TRUE);
+        return;
+    }
+
     ip_ifindex = nm_vpn_connection_get_ip_ifindex(self, TRUE);
     if (ip_ifindex <= 0)
         g_return_if_reached();
@@ -2011,14 +2036,14 @@ _dbus_signal_ip_config_cb(NMVpnConnection *self, int addr_family, GVariant *dict
     if (IS_IPv4) {
         if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_DNS, "au", &var_iter)) {
             while (g_variant_iter_next(var_iter, "u", &u32))
-                nm_l3_config_data_add_nameserver(l3cd, addr_family, &u32);
+                nm_l3_config_data_add_nameserver_detail(l3cd, addr_family, &u32, NULL);
             g_variant_iter_free(var_iter);
         }
     } else {
         if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_DNS, "aay", &var_iter)) {
             while (g_variant_iter_next(var_iter, "@ay", &v)) {
                 if (nm_ip_addr_set_from_variant(AF_INET6, &v_addr, v, NULL))
-                    nm_l3_config_data_add_nameserver(l3cd, addr_family, &v_addr);
+                    nm_l3_config_data_add_nameserver_detail(l3cd, addr_family, &v_addr, NULL);
                 g_variant_unref(v);
             }
             g_variant_iter_free(var_iter);
@@ -2104,9 +2129,8 @@ _dbus_signal_ip_config_cb(NMVpnConnection *self, int addr_family, GVariant *dict
 
                     if (plen > 32)
                         break;
-                    route.r4.plen = plen;
-                    route.r4.network =
-                        nm_utils_ip4_address_clear_host_address(route.r4.network, plen);
+                    route.r4.plen    = plen;
+                    route.r4.network = nm_ip4_addr_clear_host_address(route.r4.network, plen);
 
                     if (priv->ip_data_4.gw_external.addr4
                         && route.r4.network == priv->ip_data_4.gw_external.addr4
@@ -2146,9 +2170,7 @@ _dbus_signal_ip_config_cb(NMVpnConnection *self, int addr_family, GVariant *dict
 
                 nm_ip_addr_set_from_variant(AF_INET6, &route.r6.gateway, next_hop, NULL);
 
-                nm_utils_ip6_address_clear_host_address(&route.r6.network,
-                                                        &route.r6.network,
-                                                        route.r6.plen);
+                nm_ip6_addr_clear_host_address(&route.r6.network, &route.r6.network, route.r6.plen);
 
                 if (!IN6_IS_ADDR_UNSPECIFIED(&priv->ip_data_6.gw_external.addr6)
                     && IN6_ARE_ADDR_EQUAL(&route.r6.network, &priv->ip_data_6.gw_external.addr6)
@@ -2761,6 +2783,11 @@ nm_vpn_connection_activate(NMVpnConnection *self, NMVpnPluginInfo *plugin_info)
 
     _LOGI("starting %s", nm_vpn_plugin_info_get_name(plugin_info));
 
+    priv->ip_data_4.method_auto = nm_streq0(nm_utils_get_ip_config_method(connection, AF_INET),
+                                            NM_SETTING_IP4_CONFIG_METHOD_AUTO);
+    priv->ip_data_6.method_auto = nm_streq0(nm_utils_get_ip_config_method(connection, AF_INET6),
+                                            NM_SETTING_IP6_CONFIG_METHOD_AUTO);
+
     priv->connection_can_persist = nm_setting_vpn_get_persistent(s_vpn);
     priv->plugin_info            = g_object_ref(plugin_info);
 
@@ -2841,17 +2868,11 @@ device_changed(NMActiveConnection *active, NMDevice *new_device, NMDevice *old_d
     _l3cfg_clear(self, l3cfg_old);
 
     priv->ifindex_dev = ifindex;
-    if (ifindex > 0) {
-        priv->l3cfg_dev = nm_netns_l3cfg_acquire(priv->netns, ifindex);
-        g_signal_connect(priv->l3cfg_dev,
-                         NM_L3CFG_SIGNAL_NOTIFY,
-                         G_CALLBACK(_l3cfg_notify_cb),
-                         self);
-        priv->l3cfg_commit_type_dev = nm_l3cfg_commit_type_register(priv->l3cfg_dev,
-                                                                    NM_L3_CFG_COMMIT_TYPE_UPDATE,
-                                                                    NULL,
-                                                                    "vpn");
-    }
+
+    priv->l3cfg_dev = nm_netns_l3cfg_acquire(priv->netns, ifindex);
+    g_signal_connect(priv->l3cfg_dev, NM_L3CFG_SIGNAL_NOTIFY, G_CALLBACK(_l3cfg_notify_cb), self);
+    priv->l3cfg_commit_type_dev =
+        nm_l3cfg_commit_type_register(priv->l3cfg_dev, NM_L3_CFG_COMMIT_TYPE_UPDATE, NULL, "vpn");
 
     if (_l3cfg_l3cd_gw_extern_update(self))
         nm_l3cfg_commit_on_idle_schedule(priv->l3cfg_dev, NM_L3_CFG_COMMIT_TYPE_AUTO);
diff --git a/src/core/vpn/nm-vpn-connection.h b/src/core/vpn/nm-vpn-connection.h
index 51e2e500..f9c38322 100644
--- a/src/core/vpn/nm-vpn-connection.h
+++ b/src/core/vpn/nm-vpn-connection.h
@@ -15,7 +15,7 @@
 
 #define NM_TYPE_VPN_CONNECTION (nm_vpn_connection_get_type())
 #define NM_VPN_CONNECTION(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VPN_CONNECTION, NMVpnConnection))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VPN_CONNECTION, NMVpnConnection))
 #define NM_VPN_CONNECTION_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_VPN_CONNECTION, NMVpnConnectionClass))
 #define NM_IS_VPN_CONNECTION(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_VPN_CONNECTION))
diff --git a/src/core/vpn/nm-vpn-manager.h b/src/core/vpn/nm-vpn-manager.h
index 41ecb8d2..cd691dd1 100644
--- a/src/core/vpn/nm-vpn-manager.h
+++ b/src/core/vpn/nm-vpn-manager.h
@@ -10,7 +10,8 @@
 #include "nm-vpn-connection.h"
 
 #define NM_TYPE_VPN_MANAGER (nm_vpn_manager_get_type())
-#define NM_VPN_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VPN_MANAGER, NMVpnManager))
+#define NM_VPN_MANAGER(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VPN_MANAGER, NMVpnManager))
 #define NM_VPN_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_VPN_MANAGER, NMVpnManagerClass))
 #define NM_IS_VPN_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_VPN_MANAGER))