about summary refs log tree commit diff
path: root/src/core
diff options
context:
space:
mode:
Diffstat (limited to 'src/core')
-rw-r--r--src/core/NetworkManagerUtils.c11
-rw-r--r--src/core/devices/bluetooth/nm-device-bt.c25
-rw-r--r--src/core/devices/nm-device-bond.c209
-rw-r--r--src/core/devices/nm-device-bridge.c148
-rw-r--r--src/core/devices/nm-device-ethernet.c2
-rw-r--r--src/core/devices/nm-device-veth.c15
-rw-r--r--src/core/devices/nm-device-vrf.c54
-rw-r--r--src/core/devices/nm-device-wireguard.c2
-rw-r--r--src/core/devices/nm-device.c610
-rw-r--r--src/core/devices/nm-device.h21
-rw-r--r--src/core/devices/ovs/nm-device-ovs-bridge.c18
-rw-r--r--src/core/devices/ovs/nm-device-ovs-port.c120
-rw-r--r--src/core/devices/ovs/nm-ovs-factory.c3
-rw-r--r--src/core/devices/ovs/nm-ovsdb.c11
-rw-r--r--src/core/devices/team/nm-device-team.c170
-rw-r--r--src/core/devices/wifi/nm-device-wifi.c34
-rw-r--r--src/core/devices/wifi/tests/test-devices-wifi.c34
-rw-r--r--src/core/devices/wwan/nm-modem-ofono.c2
-rw-r--r--src/core/dhcp/nm-dhcp-client.c745
-rw-r--r--src/core/dhcp/nm-dhcp-client.h8
-rw-r--r--src/core/dhcp/nm-dhcp-helper.c125
-rw-r--r--src/core/dhcp/nm-dhcp-manager.c59
-rw-r--r--src/core/dhcp/nm-dhcp-nettools.c315
-rw-r--r--src/core/dhcp/nm-dhcp-systemd.c725
-rw-r--r--src/core/dhcp/nm-dhcp-utils.c197
-rw-r--r--src/core/dhcp/nm-dhcp-utils.h51
-rw-r--r--src/core/dhcp/tests/test-dhcp-utils.c48
-rw-r--r--src/core/dns/nm-dns-dnsmasq.c123
-rw-r--r--src/core/dns/nm-dns-manager.c202
-rw-r--r--src/core/dns/nm-dns-manager.h9
-rw-r--r--src/core/dns/nm-dns-plugin.c161
-rw-r--r--src/core/dns/nm-dns-plugin.h13
-rw-r--r--src/core/dns/nm-dns-systemd-resolved.c425
-rw-r--r--src/core/dns/nm-dns-unbound.c84
-rw-r--r--src/core/dns/nm-dns-unbound.h27
-rw-r--r--src/core/main.c22
-rw-r--r--src/core/meson.build1
-rw-r--r--src/core/nm-config-data.c9
-rw-r--r--src/core/nm-config.c229
-rw-r--r--src/core/nm-config.h11
-rw-r--r--src/core/nm-connectivity.c4
-rw-r--r--src/core/nm-core-utils.c18
-rw-r--r--src/core/nm-dhcp-config.c23
-rw-r--r--src/core/nm-dhcp-config.h3
-rw-r--r--src/core/nm-dispatcher.c78
-rw-r--r--src/core/nm-firewall-utils.c3
-rw-r--r--src/core/nm-hostname-manager.c132
-rw-r--r--src/core/nm-hostname-manager.h10
-rw-r--r--src/core/nm-l3-config-data.c48
-rw-r--r--src/core/nm-l3-config-data.h4
-rw-r--r--src/core/nm-l3-ipv4ll.c4
-rw-r--r--src/core/nm-l3-ipv6ll.c4
-rw-r--r--src/core/nm-l3cfg.c338
-rw-r--r--src/core/nm-l3cfg.h17
-rw-r--r--src/core/nm-manager.c152
-rw-r--r--src/core/nm-manager.h4
-rw-r--r--src/core/nm-netns.c48
-rw-r--r--src/core/nm-netns.h2
-rw-r--r--src/core/nm-policy.c2
-rw-r--r--src/core/platform/nm-fake-platform.c9
-rw-r--r--src/core/platform/tests/monitor.c31
-rw-r--r--src/core/platform/tests/test-common.c16
-rw-r--r--src/core/platform/tests/test-common.h5
-rw-r--r--src/core/platform/tests/test-link.c9
-rw-r--r--src/core/platform/tests/test-nmp-object.c4
-rw-r--r--src/core/platform/tests/test-platform-general.c60
-rw-r--r--src/core/platform/tests/test-route.c275
-rw-r--r--src/core/platform/tests/test-tc.c9
-rw-r--r--src/core/settings/nm-settings.c162
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c93
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c3
-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.c37
-rw-r--r--src/core/settings/plugins/ifcfg-rh/shvar.c3
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-System_test-bridge-component-b.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_User_1.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Bond_Main.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Permissions.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Proxy_Basic.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Routing_Rules.cexpected3
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_VLAN_reorder_hdr.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_AP_Mode.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Band_A.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Hidden.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_always.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_default.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_missing.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_never.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wifi_LEAP.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wifi_WEP_104_ASCII.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_Auto-Negotiate.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_Static_Routes.cexpected1
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_Wake-on-LAN.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_match.cexpected1
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-dcb-test.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-random_wifi_connection.cexpected1
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-random_wifi_connection_2.cexpected1
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-ip6-disabled.cexpected1
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-link_local3
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-sriov-write.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-tc-write-empty.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-tc-write.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-1.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-2.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-3.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-4.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-5.cexpected2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_write_wired_auto_negotiate_on.cexpected2
-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.c65
-rw-r--r--src/core/settings/plugins/ifupdown/nms-ifupdown-parser.c2
-rw-r--r--src/core/settings/plugins/keyfile/nms-keyfile-plugin.c10
-rw-r--r--src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c10
-rw-r--r--src/core/supplicant/nm-supplicant-interface.c2
-rw-r--r--src/core/tests/config/test-config.c6
-rw-r--r--src/core/tests/test-core-with-expect.c10
-rw-r--r--src/core/tests/test-core.c30
-rw-r--r--src/core/tests/test-l3cfg.c2
-rw-r--r--src/core/tests/test-systemd.c159
-rw-r--r--src/core/tests/test-utils.c4
120 files changed, 4583 insertions, 2474 deletions
diff --git a/src/core/NetworkManagerUtils.c b/src/core/NetworkManagerUtils.c
index 5727aac8..8b0b4845 100644
--- a/src/core/NetworkManagerUtils.c
+++ b/src/core/NetworkManagerUtils.c
@@ -30,7 +30,6 @@
 #include "libnm-platform/nm-linux-platform.h"
 #include "libnm-platform/nm-platform-utils.h"
 #include "nm-auth-utils.h"
-#include "libnm-systemd-shared/nm-sd-utils-shared.h"
 
 /*****************************************************************************/
 
@@ -1155,7 +1154,7 @@ nm_utils_file_is_in_path(const char *abs_filename, const char *abs_path)
     g_return_val_if_fail(abs_filename && abs_filename[0] == '/', NULL);
     g_return_val_if_fail(abs_path && abs_path[0] == '/', NULL);
 
-    path = nm_sd_utils_path_startswith(abs_filename, abs_path);
+    path = nm_path_startswith(abs_filename, abs_path);
     if (!path)
         return NULL;
 
@@ -1389,16 +1388,20 @@ nm_utils_ip_route_attribute_to_platform(int                addr_family,
 
     r->r_rtm_flags = ((onlink) ? (unsigned) RTNH_F_ONLINK : 0u);
 
+    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_ADVMSS, r->mss, UINT32, uint32, 0);
     GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_WINDOW, r->window, UINT32, uint32, 0);
     GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_CWND, r->cwnd, UINT32, uint32, 0);
     GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_INITCWND, r->initcwnd, UINT32, uint32, 0);
     GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_INITRWND, r->initrwnd, UINT32, uint32, 0);
     GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_MTU, r->mtu, UINT32, uint32, 0);
+    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_RTO_MIN, r->rto_min, UINT32, uint32, 0);
+    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_QUICKACK, r->quickack, BOOLEAN, boolean, FALSE);
     GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_LOCK_WINDOW, r->lock_window, BOOLEAN, boolean, FALSE);
     GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_LOCK_CWND, r->lock_cwnd, BOOLEAN, boolean, FALSE);
     GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_LOCK_INITCWND, r->lock_initcwnd, BOOLEAN, boolean, FALSE);
     GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_LOCK_INITRWND, r->lock_initrwnd, BOOLEAN, boolean, FALSE);
     GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_LOCK_MTU, r->lock_mtu, BOOLEAN, boolean, FALSE);
+    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_LOCK_ADVMSS, r->lock_mss, BOOLEAN, boolean, FALSE);
 
     if ((variant = nm_ip_route_get_attribute(s_route, NM_IP_ROUTE_ATTRIBUTE_SRC))
         && g_variant_is_of_type(variant, G_VARIANT_TYPE_STRING)) {
@@ -1691,7 +1694,7 @@ nm_utils_platform_capture_ip_setting(NMPlatform *platform,
         return NM_SETTING(g_steal_pointer(&s_ip));
     }
 
-    nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4), ifindex);
+    nmp_lookup_init_object_by_ifindex(&lookup, NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4), ifindex);
     nm_platform_iter_obj_for_each (&iter, platform, &lookup, &obj) {
         const NMPlatformIPXAddress           *address = NMP_OBJECT_CAST_IPX_ADDRESS(obj);
         nm_auto_unref_ip_address NMIPAddress *s_addr  = NULL;
@@ -1745,7 +1748,7 @@ nm_utils_platform_capture_ip_setting(NMPlatform *platform,
     }
     g_object_set(s_ip, NM_SETTING_IP_CONFIG_METHOD, method, NULL);
 
-    nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4), ifindex);
+    nmp_lookup_init_object_by_ifindex(&lookup, NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4), ifindex);
     nm_platform_iter_obj_for_each (&iter, platform, &lookup, &obj) {
         const NMPlatformIPXRoute         *route   = NMP_OBJECT_CAST_IPX_ROUTE(obj);
         nm_auto_unref_ip_route NMIPRoute *s_route = NULL;
diff --git a/src/core/devices/bluetooth/nm-device-bt.c b/src/core/devices/bluetooth/nm-device-bt.c
index 39b5c4ec..a8258123 100644
--- a/src/core/devices/bluetooth/nm-device-bt.c
+++ b/src/core/devices/bluetooth/nm-device-bt.c
@@ -262,6 +262,7 @@ complete_connection(NMDevice            *device,
     const char         *ctype;
     gboolean            is_dun = FALSE;
     gboolean            is_pan = FALSE;
+    gboolean            is_nap = FALSE;
     NMSettingGsm       *s_gsm;
     NMSettingCdma      *s_cdma;
     NMSettingSerial    *s_serial;
@@ -281,6 +282,8 @@ complete_connection(NMDevice            *device,
             is_dun = TRUE;
         else if (!strcmp(ctype, NM_SETTING_BLUETOOTH_TYPE_PANU))
             is_pan = TRUE;
+        else if (!strcmp(ctype, NM_SETTING_BLUETOOTH_TYPE_NAP))
+            is_nap = TRUE;
     } else {
         if (s_gsm || s_cdma)
             is_dun = TRUE;
@@ -288,7 +291,27 @@ complete_connection(NMDevice            *device,
             is_pan = TRUE;
     }
 
-    if (is_pan) {
+    if (is_nap) {
+        /* Make sure the device supports NAP */
+        if (!(priv->capabilities & NM_BT_CAPABILITY_NAP)) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                _("NAP requested, but Bluetooth device does not support NAP"));
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_BLUETOOTH_SETTING_NAME,
+                           NM_SETTING_BLUETOOTH_TYPE);
+            return FALSE;
+        }
+
+        g_object_set(G_OBJECT(s_bt),
+                     NM_SETTING_BLUETOOTH_TYPE,
+                     NM_SETTING_BLUETOOTH_TYPE_NAP,
+                     NULL);
+
+        fallback_prefix = _("NAP connection");
+    } else if (is_pan) {
         /* Make sure the device supports PAN */
         if (!(priv->capabilities & NM_BT_CAPABILITY_NAP)) {
             g_set_error_literal(error,
diff --git a/src/core/devices/nm-device-bond.c b/src/core/devices/nm-device-bond.c
index 16896d57..44bb316d 100644
--- a/src/core/devices/nm-device-bond.c
+++ b/src/core/devices/nm-device-bond.c
@@ -9,6 +9,8 @@
 
 #include <stdlib.h>
 #include <net/if.h>
+#include <linux/if_ether.h>
+#include <linux/if_link.h>
 
 #include "NetworkManagerUtils.h"
 #include "nm-device-private.h"
@@ -16,6 +18,7 @@
 #include "nm-device-factory.h"
 #include "libnm-core-aux-intern/nm-libnm-core-utils.h"
 #include "libnm-core-intern/nm-core-internal.h"
+#include "nm-manager.h"
 #include "nm-setting-bond-port.h"
 
 #define _NMLOG_DEVICE_TYPE NMDeviceBond
@@ -312,7 +315,7 @@ set_bond_attr_or_default(NMDevice *device, NMSettingBond *s_bond, const char *op
     NMDeviceBond *self = NM_DEVICE_BOND(device);
     const char   *value;
 
-    value = nm_setting_bond_get_option_or_default(s_bond, opt);
+    value = nm_setting_bond_get_option_normalized(s_bond, opt);
     if (!value) {
         if (_LOGT_ENABLED(LOGD_BOND) && nm_setting_bond_get_option_by_name(s_bond, opt))
             _LOGT(LOGD_BOND, "bond option '%s' not set as it conflicts with other options", opt);
@@ -346,54 +349,147 @@ set_bond_arp_ip_targets(NMDevice *device, NMSettingBond *s_bond)
     set_arp_targets(
         device,
         cur_arp_ip_target,
-        nm_setting_bond_get_option_or_default(s_bond, NM_SETTING_BOND_OPTION_ARP_IP_TARGET));
+        nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_ARP_IP_TARGET));
 }
 
-static gboolean
-apply_bonding_config(NMDeviceBond *self)
+static guint8
+_bond_arp_ip_target_to_platform(const char *value, in_addr_t out[static NM_BOND_MAX_ARP_TARGETS])
 {
-    NMDevice      *device = NM_DEVICE(self);
-    NMSettingBond *s_bond;
-    NMBondMode     mode;
-    const char    *mode_str;
-    gs_free char  *device_bond_mode = NULL;
+    gs_free const char **ip = NULL;
+    in_addr_t            in_a;
+    int                  i;
+    int                  added = 0;
 
-    s_bond = nm_device_get_applied_setting(device, NM_TYPE_SETTING_BOND);
-    g_return_val_if_fail(s_bond, FALSE);
+    ip = nm_utils_bond_option_arp_ip_targets_split(value);
 
-    mode_str = nm_setting_bond_get_option_or_default(s_bond, NM_SETTING_BOND_OPTION_MODE);
-    mode     = _nm_setting_bond_mode_from_string(mode_str);
-    g_return_val_if_fail(mode != NM_BOND_MODE_UNKNOWN, FALSE);
+    if (!ip)
+        return added;
 
-    /* Set mode first, as some other options (e.g. arp_interval) are valid
-     * only for certain modes.
-     */
-    device_bond_mode = nm_platform_sysctl_master_get_option(nm_device_get_platform(device),
-                                                            nm_device_get_ifindex(device),
-                                                            NM_SETTING_BOND_OPTION_MODE);
-    /* Need to release all slaves before we can change bond mode */
-    if (!nm_streq0(device_bond_mode, mode_str))
-        nm_device_master_release_slaves_all(device);
+    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))
+            nm_assert_not_reached(); /* verify() already validated the IP addresses */
+
+        out[added++] = in_a;
+    }
+    return added;
+}
 
-    set_bond_attr_or_default(device, s_bond, NM_SETTING_BOND_OPTION_MODE);
+static int
+_setting_bond_primary_opt_as_ifindex(NMSettingBond *s_bond)
+{
+    const char *primary_str;
+    int         ifindex = 0;
 
-    set_bond_arp_ip_targets(device, s_bond);
+    primary_str = nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_PRIMARY);
 
-    set_bond_attrs_or_default(device, s_bond, NM_MAKE_STRV(OPTIONS_APPLY_SUBSET));
-    return TRUE;
+    if (primary_str != NULL)
+        ifindex = nm_platform_link_get_ifindex(NM_PLATFORM_GET, primary_str);
+
+    return ifindex;
+}
+
+static void
+_platform_lnk_bond_init_from_setting(NMSettingBond *s_bond, NMPlatformLnkBond *props)
+{
+    const char *opt_value;
+
+    *props = (NMPlatformLnkBond){
+        .mode = _nm_setting_bond_mode_from_string(
+            nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_MODE)),
+        .primary   = _setting_bond_primary_opt_as_ifindex(s_bond),
+        .miimon    = _nm_setting_bond_opt_value_as_u32(s_bond, NM_SETTING_BOND_OPTION_MIIMON),
+        .updelay   = _nm_setting_bond_opt_value_as_u32(s_bond, NM_SETTING_BOND_OPTION_UPDELAY),
+        .downdelay = _nm_setting_bond_opt_value_as_u32(s_bond, NM_SETTING_BOND_OPTION_DOWNDELAY),
+        .arp_interval =
+            _nm_setting_bond_opt_value_as_u32(s_bond, NM_SETTING_BOND_OPTION_ARP_INTERVAL),
+        .resend_igmp =
+            _nm_setting_bond_opt_value_as_u32(s_bond, NM_SETTING_BOND_OPTION_RESEND_IGMP),
+        .min_links = _nm_setting_bond_opt_value_as_u32(s_bond, NM_SETTING_BOND_OPTION_MIN_LINKS),
+        .lp_interval =
+            _nm_setting_bond_opt_value_as_u32(s_bond, NM_SETTING_BOND_OPTION_LP_INTERVAL),
+        .packets_per_port =
+            _nm_setting_bond_opt_value_as_u32(s_bond, NM_SETTING_BOND_OPTION_PACKETS_PER_SLAVE),
+        .peer_notif_delay =
+            _nm_setting_bond_opt_value_as_u32(s_bond, NM_SETTING_BOND_OPTION_PEER_NOTIF_DELAY),
+        .arp_all_targets = _nm_setting_bond_arp_all_targets_from_string(
+            nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_ARP_ALL_TARGETS)),
+        .arp_validate = _nm_setting_bond_arp_validate_from_string(
+            nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_ARP_VALIDATE)),
+        .ad_actor_sys_prio =
+            _nm_setting_bond_opt_value_as_u16(s_bond, NM_SETTING_BOND_OPTION_AD_ACTOR_SYS_PRIO),
+        .ad_user_port_key =
+            _nm_setting_bond_opt_value_as_u16(s_bond, NM_SETTING_BOND_OPTION_AD_USER_PORT_KEY),
+        .primary_reselect = _nm_setting_bond_primary_reselect_from_string(
+            nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_PRIMARY_RESELECT)),
+        .fail_over_mac = _nm_setting_bond_fail_over_mac_from_string(
+            nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_FAIL_OVER_MAC)),
+        .xmit_hash_policy = _nm_setting_bond_xmit_hash_policy_from_string(
+            nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_XMIT_HASH_POLICY)),
+        .num_grat_arp =
+            _nm_setting_bond_opt_value_as_u8(s_bond, NM_SETTING_BOND_OPTION_NUM_GRAT_ARP),
+        .all_ports_active =
+            _nm_setting_bond_opt_value_as_u8(s_bond, NM_SETTING_BOND_OPTION_ALL_SLAVES_ACTIVE),
+        .lacp_rate = _nm_setting_bond_lacp_rate_from_string(
+            nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_LACP_RATE)),
+        .ad_select = _nm_setting_bond_ad_select_from_string(
+            nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_AD_SELECT)),
+    };
+
+    nm_ether_addr_from_string(
+        &props->ad_actor_system,
+        nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_AD_ACTOR_SYSTEM));
+
+    opt_value = nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_USE_CARRIER);
+    if (opt_value != NULL)
+        props->use_carrier = _nm_utils_ascii_str_to_bool(opt_value, FALSE);
+
+    opt_value =
+        nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_TLB_DYNAMIC_LB);
+    if (opt_value != NULL)
+        props->tlb_dynamic_lb = _nm_utils_ascii_str_to_bool(opt_value, FALSE);
+
+    opt_value = nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_ARP_IP_TARGET);
+    if (opt_value != NULL)
+        props->arp_ip_targets_num =
+            _bond_arp_ip_target_to_platform(opt_value, props->arp_ip_target);
+
+    props->miimon_has           = !props->arp_interval && !props->arp_validate;
+    props->updelay_has          = props->miimon_has && props->miimon;
+    props->downdelay_has        = props->miimon_has && props->miimon;
+    props->peer_notif_delay_has = (props->miimon || props->arp_interval) && props->peer_notif_delay;
+    props->arp_all_targets_has  = props->arp_interval && props->arp_all_targets;
+    props->resend_igmp_has      = props->resend_igmp != 1;
+    props->lp_interval_has      = props->lp_interval != 1;
+    props->tlb_dynamic_lb_has   = NM_IN_SET(props->mode, NM_BOND_MODE_TLB, NM_BOND_MODE_ALB);
 }
 
 static NMActStageReturn
 act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
 {
-    NMDeviceBond    *self = NM_DEVICE_BOND(device);
-    NMActStageReturn ret  = NM_ACT_STAGE_RETURN_SUCCESS;
+    NMActStageReturn  ret = NM_ACT_STAGE_RETURN_SUCCESS;
+    NMConnection     *connection;
+    NMSettingBond    *s_bond;
+    NMPlatformLnkBond props;
+    int               r;
+    int               ifindex = nm_device_get_ifindex(device);
+
+    connection = nm_device_get_applied_connection(device);
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    s_bond = nm_connection_get_setting_bond(connection);
+    g_return_val_if_fail(s_bond, NM_ACT_STAGE_RETURN_FAILURE);
+
+    _platform_lnk_bond_init_from_setting(s_bond, &props);
 
     /* Interface must be down to set bond options */
     nm_device_take_down(device, TRUE);
-    if (!apply_bonding_config(self))
+    r = nm_platform_link_bond_change(nm_device_get_platform(device), ifindex, &props);
+    if (r < 0) {
         ret = NM_ACT_STAGE_RETURN_FAILURE;
-    else {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+    } else {
         if (!nm_device_hw_addr_set_cloned(device, nm_device_get_applied_connection(device), FALSE))
             ret = NM_ACT_STAGE_RETURN_FAILURE;
     }
@@ -424,8 +520,14 @@ commit_port_options(NMDevice *bond_device, NMDevice *port, NMSettingBondPort *s_
                                          queue_id_str);
 }
 
-static gboolean
-enslave_slave(NMDevice *device, NMDevice *port, NMConnection *connection, gboolean configure)
+static NMTernary
+attach_port(NMDevice                  *device,
+            NMDevice                  *port,
+            NMConnection              *connection,
+            gboolean                   configure,
+            GCancellable              *cancellable,
+            NMDeviceAttachPortCallback callback,
+            gpointer                   user_data)
 {
     NMDeviceBond      *self = NM_DEVICE_BOND(device);
     NMSettingBondPort *s_port;
@@ -442,7 +544,7 @@ enslave_slave(NMDevice *device, NMDevice *port, NMConnection *connection, gboole
         nm_device_bring_up(port, TRUE, NULL);
 
         if (!success) {
-            _LOGI(LOGD_BOND, "assigning bond port %s: failed", nm_device_get_ip_iface(port));
+            _LOGI(LOGD_BOND, "attaching bond port %s: failed", nm_device_get_ip_iface(port));
             return FALSE;
         }
 
@@ -450,15 +552,15 @@ enslave_slave(NMDevice *device, NMDevice *port, NMConnection *connection, gboole
 
         commit_port_options(device, port, s_port);
 
-        _LOGI(LOGD_BOND, "assigned bond port %s", nm_device_get_ip_iface(port));
+        _LOGI(LOGD_BOND, "attached bond port %s", nm_device_get_ip_iface(port));
     } else
-        _LOGI(LOGD_BOND, "bond port %s was assigned", nm_device_get_ip_iface(port));
+        _LOGI(LOGD_BOND, "bond port %s was attached", nm_device_get_ip_iface(port));
 
     return TRUE;
 }
 
 static void
-release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
+detach_port(NMDevice *device, NMDevice *port, gboolean configure)
 {
     NMDeviceBond *self = NM_DEVICE_BOND(device);
     gboolean      success;
@@ -472,10 +574,10 @@ release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
             configure = FALSE;
     }
 
-    ifindex_slave = nm_device_get_ip_ifindex(slave);
+    ifindex_slave = nm_device_get_ip_ifindex(port);
 
     if (ifindex_slave <= 0)
-        _LOGD(LOGD_BOND, "bond slave %s is already released", nm_device_get_ip_iface(slave));
+        _LOGD(LOGD_BOND, "bond port %s is already detached", nm_device_get_ip_iface(port));
 
     if (configure) {
         NMConnection   *applied;
@@ -490,9 +592,9 @@ release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
                                                ifindex_slave);
 
             if (success) {
-                _LOGI(LOGD_BOND, "released bond slave %s", nm_device_get_ip_iface(slave));
+                _LOGI(LOGD_BOND, "detached bond port %s", nm_device_get_ip_iface(port));
             } else {
-                _LOGW(LOGD_BOND, "failed to release bond slave %s", nm_device_get_ip_iface(slave));
+                _LOGW(LOGD_BOND, "failed to detach bond port %s", nm_device_get_ip_iface(port));
             }
         }
 
@@ -512,12 +614,12 @@ release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
          * other state is noticed by the now-released slave.
          */
         if (ifindex_slave > 0) {
-            if (!nm_device_bring_up(slave, TRUE, NULL))
-                _LOGW(LOGD_BOND, "released bond slave could not be brought up.");
+            if (!nm_device_bring_up(port, TRUE, NULL))
+                _LOGW(LOGD_BOND, "detached bond port could not be brought up.");
         }
     } else {
         if (ifindex_slave > 0) {
-            _LOGI(LOGD_BOND, "bond slave %s was released", nm_device_get_ip_iface(slave));
+            _LOGI(LOGD_BOND, "bond port %s was detached", nm_device_get_ip_iface(port));
         }
     }
 }
@@ -529,12 +631,19 @@ create_and_realize(NMDevice              *device,
                    const NMPlatformLink **out_plink,
                    GError               **error)
 {
-    const char *iface = nm_device_get_iface(device);
-    int         r;
+    const char       *iface = nm_device_get_iface(device);
+    NMSettingBond    *s_bond;
+    NMPlatformLnkBond props;
+    int               r;
 
     g_assert(iface);
 
-    r = nm_platform_link_bond_add(nm_device_get_platform(device), iface, out_plink);
+    s_bond = nm_connection_get_setting_bond(connection);
+    nm_assert(s_bond);
+
+    _platform_lnk_bond_init_from_setting(s_bond, &props);
+
+    r = nm_platform_link_bond_add(nm_device_get_platform(device), iface, &props, out_plink);
     if (r < 0) {
         g_set_error(error,
                     NM_DEVICE_ERROR,
@@ -612,7 +721,7 @@ reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_ne
     s_bond = nm_connection_get_setting_bond(con_new);
     g_return_if_fail(s_bond);
 
-    value = nm_setting_bond_get_option_or_default(s_bond, NM_SETTING_BOND_OPTION_MODE);
+    value = nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_MODE);
     mode  = _nm_setting_bond_mode_from_string(value);
     g_return_if_fail(mode != NM_BOND_MODE_UNKNOWN);
 
@@ -663,8 +772,8 @@ nm_device_bond_class_init(NMDeviceBondClass *klass)
     device_class->create_and_realize = create_and_realize;
     device_class->act_stage1_prepare = act_stage1_prepare;
     device_class->get_configured_mtu = nm_device_get_configured_mtu_for_wired;
-    device_class->enslave_slave      = enslave_slave;
-    device_class->release_slave      = release_slave;
+    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;
 }
diff --git a/src/core/devices/nm-device-bridge.c b/src/core/devices/nm-device-bridge.c
index 9bf7ac01..31cf361e 100644
--- a/src/core/devices/nm-device-bridge.c
+++ b/src/core/devices/nm-device-bridge.c
@@ -836,30 +836,33 @@ _platform_lnk_bridge_init_from_setting(NMSettingBridge *s_bridge, NMPlatformLnkB
     to_sysfs_group_address_sys(nm_setting_bridge_get_group_address(s_bridge), &props->group_addr);
 }
 
-static NMActStageReturn
-act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+static gboolean
+link_config(NMDevice *device, NMConnection *connection)
 {
-    NMConnection       *connection;
+    int                 ifindex = nm_device_get_ifindex(device);
     NMSettingBridge    *s_bridge;
     NMPlatformLnkBridge props;
-    int                 r;
-    int                 ifindex = nm_device_get_ifindex(device);
-
-    connection = nm_device_get_applied_connection(device);
-    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
 
     s_bridge = nm_connection_get_setting_bridge(connection);
-    g_return_val_if_fail(s_bridge, NM_ACT_STAGE_RETURN_FAILURE);
+    g_return_val_if_fail(s_bridge, FALSE);
 
     _platform_lnk_bridge_init_from_setting(s_bridge, &props);
 
-    r = nm_platform_link_bridge_change(nm_device_get_platform(device), ifindex, &props);
-    if (r < 0) {
-        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
-        return NM_ACT_STAGE_RETURN_FAILURE;
-    }
+    if (nm_platform_link_bridge_change(nm_device_get_platform(device), ifindex, &props) < 0)
+        return FALSE;
+
+    return bridge_set_vlan_options(device, s_bridge);
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMConnection *connection;
+
+    connection = nm_device_get_applied_connection(device);
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
 
-    if (!bridge_set_vlan_options(device, s_bridge)) {
+    if (!link_config(device, connection)) {
         NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
         return NM_ACT_STAGE_RETURN_FAILURE;
     }
@@ -974,8 +977,14 @@ deactivate(NMDevice *device)
     }
 }
 
-static gboolean
-enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure)
+static NMTernary
+attach_port(NMDevice                  *device,
+            NMDevice                  *port,
+            NMConnection              *connection,
+            gboolean                   configure,
+            GCancellable              *cancellable,
+            NMDeviceAttachPortCallback callback,
+            gpointer                   user_data)
 {
     NMDeviceBridge      *self = NM_DEVICE_BRIDGE(device);
     NMConnection        *master_connection;
@@ -985,7 +994,7 @@ enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gbool
     if (configure) {
         if (!nm_platform_link_enslave(nm_device_get_platform(device),
                                       nm_device_get_ip_ifindex(device),
-                                      nm_device_get_ip_ifindex(slave)))
+                                      nm_device_get_ip_ifindex(port)))
             return FALSE;
 
         master_connection = nm_device_get_applied_connection(device);
@@ -1010,25 +1019,25 @@ enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gbool
              * (except for the default one) and so there's no need to flush. */
 
             if (plat_vlans
-                && !nm_platform_link_set_bridge_vlans(nm_device_get_platform(slave),
-                                                      nm_device_get_ifindex(slave),
+                && !nm_platform_link_set_bridge_vlans(nm_device_get_platform(port),
+                                                      nm_device_get_ifindex(port),
                                                       TRUE,
                                                       plat_vlans))
                 return FALSE;
         }
 
-        commit_slave_options(slave, s_port);
+        commit_slave_options(port, s_port);
 
-        _LOGI(LOGD_BRIDGE, "attached bridge port %s", nm_device_get_ip_iface(slave));
+        _LOGI(LOGD_BRIDGE, "attached bridge port %s", nm_device_get_ip_iface(port));
     } else {
-        _LOGI(LOGD_BRIDGE, "bridge port %s was attached", nm_device_get_ip_iface(slave));
+        _LOGI(LOGD_BRIDGE, "bridge port %s was attached", nm_device_get_ip_iface(port));
     }
 
     return TRUE;
 }
 
 static void
-release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
+detach_port(NMDevice *device, NMDevice *port, gboolean configure)
 {
     NMDeviceBridge *self = NM_DEVICE_BRIDGE(device);
     gboolean        success;
@@ -1041,10 +1050,10 @@ release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
             configure = FALSE;
     }
 
-    ifindex_slave = nm_device_get_ip_ifindex(slave);
+    ifindex_slave = nm_device_get_ip_ifindex(port);
 
     if (ifindex_slave <= 0) {
-        _LOGD(LOGD_TEAM, "bond slave %s is already released", nm_device_get_ip_iface(slave));
+        _LOGD(LOGD_TEAM, "bridge port %s is already detached", nm_device_get_ip_iface(port));
         return;
     }
 
@@ -1054,12 +1063,12 @@ release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
                                            ifindex_slave);
 
         if (success) {
-            _LOGI(LOGD_BRIDGE, "detached bridge port %s", nm_device_get_ip_iface(slave));
+            _LOGI(LOGD_BRIDGE, "detached bridge port %s", nm_device_get_ip_iface(port));
         } else {
-            _LOGW(LOGD_BRIDGE, "failed to detach bridge port %s", nm_device_get_ip_iface(slave));
+            _LOGW(LOGD_BRIDGE, "failed to detach bridge port %s", nm_device_get_ip_iface(port));
         }
     } else {
-        _LOGI(LOGD_BRIDGE, "bridge port %s was detached", nm_device_get_ip_iface(slave));
+        _LOGI(LOGD_BRIDGE, "bridge port %s was detached", nm_device_get_ip_iface(port));
     }
 }
 
@@ -1085,10 +1094,6 @@ create_and_realize(NMDevice              *device,
     s_bridge = nm_connection_get_setting_bridge(connection);
     nm_assert(s_bridge);
 
-    s_wired = nm_connection_get_setting_wired(connection);
-    if (s_wired)
-        mtu = nm_setting_wired_get_mtu(s_wired);
-
     hwaddr = nm_setting_bridge_get_mac_address(s_bridge);
     if (!hwaddr
         && nm_device_hw_addr_get_cloned(device, connection, FALSE, &hwaddr_cloned, NULL, NULL)) {
@@ -1113,6 +1118,11 @@ create_and_realize(NMDevice              *device,
 
     _platform_lnk_bridge_init_from_setting(s_bridge, &props);
 
+    s_wired = nm_connection_get_setting_wired(connection);
+    nm_assert(s_wired);
+
+    mtu = nm_setting_wired_get_mtu(s_wired);
+
     /* If mtu != 0, we set the MTU of the new bridge at creation time. However, kernel will still
      * automatically adjust the MTU of the bridge based on the minimum of the slave's MTU.
      * We don't want this automatism as the user asked for a fixed MTU.
@@ -1142,6 +1152,72 @@ create_and_realize(NMDevice              *device,
 
 /*****************************************************************************/
 
+static gboolean
+can_reapply_change(NMDevice   *device,
+                   const char *setting_name,
+                   NMSetting  *s_old,
+                   NMSetting  *s_new,
+                   GHashTable *diffs,
+                   GError    **error)
+{
+    /* Delegate changes to other settings to parent class */
+    if (!nm_streq(setting_name, NM_SETTING_BRIDGE_SETTING_NAME)) {
+        return NM_DEVICE_CLASS(nm_device_bridge_parent_class)
+            ->can_reapply_change(device, setting_name, s_old, s_new, diffs, error);
+    }
+
+    return nm_device_hash_check_invalid_keys(diffs,
+                                             NM_SETTING_BRIDGE_SETTING_NAME,
+                                             error,
+                                             NM_SETTING_BRIDGE_STP,
+                                             NM_SETTING_BRIDGE_PRIORITY,
+                                             NM_SETTING_BRIDGE_FORWARD_DELAY,
+                                             NM_SETTING_BRIDGE_HELLO_TIME,
+                                             NM_SETTING_BRIDGE_MAX_AGE,
+                                             NM_SETTING_BRIDGE_AGEING_TIME,
+                                             NM_SETTING_BRIDGE_GROUP_FORWARD_MASK,
+                                             NM_SETTING_BRIDGE_MULTICAST_HASH_MAX,
+                                             NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_COUNT,
+                                             NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL,
+                                             NM_SETTING_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL,
+                                             NM_SETTING_BRIDGE_MULTICAST_SNOOPING,
+                                             NM_SETTING_BRIDGE_MULTICAST_ROUTER,
+                                             NM_SETTING_BRIDGE_MULTICAST_QUERIER,
+                                             NM_SETTING_BRIDGE_MULTICAST_QUERIER_INTERVAL,
+                                             NM_SETTING_BRIDGE_MULTICAST_QUERY_INTERVAL,
+                                             NM_SETTING_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL,
+                                             NM_SETTING_BRIDGE_MULTICAST_QUERY_USE_IFADDR,
+                                             NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT,
+                                             NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL,
+                                             NM_SETTING_BRIDGE_GROUP_ADDRESS,
+                                             NM_SETTING_BRIDGE_VLAN_PROTOCOL,
+                                             NM_SETTING_BRIDGE_VLAN_STATS_ENABLED,
+                                             NM_SETTING_BRIDGE_VLAN_FILTERING,
+                                             NM_SETTING_BRIDGE_VLAN_DEFAULT_PVID,
+                                             NM_SETTING_BRIDGE_VLANS);
+}
+
+static void
+reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_new)
+{
+    NMDeviceBridge  *self = NM_DEVICE_BRIDGE(device);
+    NMSettingBridge *s_bridge;
+
+    NM_DEVICE_CLASS(nm_device_bridge_parent_class)->reapply_connection(device, con_old, con_new);
+
+    _LOGD(LOGD_BRIDGE, "reapplying bridge settings");
+    s_bridge = nm_connection_get_setting_bridge(con_new);
+    g_return_if_fail(s_bridge);
+
+    /* Make sure bridge_set_vlan_options() called by link_config()
+     * sets vlan_filtering and default_pvid anew. */
+    self->vlan_configured = FALSE;
+
+    link_config(device, con_new);
+}
+
+/*****************************************************************************/
+
 static void
 nm_device_bridge_init(NMDeviceBridge *self)
 {
@@ -1183,9 +1259,11 @@ nm_device_bridge_class_init(NMDeviceBridgeClass *klass)
     device_class->act_stage1_prepare                     = act_stage1_prepare;
     device_class->act_stage2_config                      = act_stage2_config;
     device_class->deactivate                             = deactivate;
-    device_class->enslave_slave                          = enslave_slave;
-    device_class->release_slave                          = release_slave;
+    device_class->attach_port                            = attach_port;
+    device_class->detach_port                            = detach_port;
     device_class->get_configured_mtu                     = nm_device_get_configured_mtu_for_wired;
+    device_class->can_reapply_change                     = can_reapply_change;
+    device_class->reapply_connection                     = reapply_connection;
 }
 
 /*****************************************************************************/
diff --git a/src/core/devices/nm-device-ethernet.c b/src/core/devices/nm-device-ethernet.c
index 32f2fbf9..40c6d208 100644
--- a/src/core/devices/nm-device-ethernet.c
+++ b/src/core/devices/nm-device-ethernet.c
@@ -451,7 +451,7 @@ supplicant_auth_state_changed(NMSupplicantInterface *iface,
 
     if (state == NM_SUPPLICANT_AUTH_STATE_SUCCESS) {
         nm_clear_g_signal_handler(priv->supplicant.iface, &priv->supplicant.iface_state_id);
-        nm_device_update_dynamic_ip_setup(NM_DEVICE(self));
+        nm_device_update_dynamic_ip_setup(NM_DEVICE(self), "supplicant auth state changed");
     }
 }
 
diff --git a/src/core/devices/nm-device-veth.c b/src/core/devices/nm-device-veth.c
index 8c95a293..63dfd8bb 100644
--- a/src/core/devices/nm-device-veth.c
+++ b/src/core/devices/nm-device-veth.c
@@ -82,6 +82,8 @@ create_and_realize(NMDevice              *device,
                    GError               **error)
 {
     const char    *iface = nm_device_get_iface(device);
+    const char    *peer;
+    NMDevice      *peer_device;
     NMSettingVeth *s_veth;
     int            r;
 
@@ -96,10 +98,15 @@ create_and_realize(NMDevice              *device,
         return FALSE;
     }
 
-    r = nm_platform_link_veth_add(nm_device_get_platform(device),
-                                  iface,
-                                  nm_setting_veth_get_peer(s_veth),
-                                  out_plink);
+    peer        = nm_setting_veth_get_peer(s_veth);
+    peer_device = nm_manager_get_device(NM_MANAGER_GET, peer, NM_DEVICE_TYPE_VETH);
+    if (peer_device) {
+        /* The veth device and its peer already exist. No need to create it again. */
+        if (nm_streq0(nm_device_get_iface(nm_device_parent_get_device(peer_device)), iface))
+            return TRUE;
+    }
+
+    r = nm_platform_link_veth_add(nm_device_get_platform(device), iface, peer, out_plink);
     if (r < 0) {
         g_set_error(error,
                     NM_DEVICE_ERROR,
diff --git a/src/core/devices/nm-device-vrf.c b/src/core/devices/nm-device-vrf.c
index ae80e1d4..2aef0e3d 100644
--- a/src/core/devices/nm-device-vrf.c
+++ b/src/core/devices/nm-device-vrf.c
@@ -206,38 +206,44 @@ update_connection(NMDevice *device, NMConnection *connection)
         g_object_set(G_OBJECT(s_vrf), NM_SETTING_VRF_TABLE, priv->props.table, NULL);
 }
 
-static gboolean
-enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure)
+static NMTernary
+attach_port(NMDevice                  *device,
+            NMDevice                  *port,
+            NMConnection              *connection,
+            gboolean                   configure,
+            GCancellable              *cancellable,
+            NMDeviceAttachPortCallback callback,
+            gpointer                   user_data)
 {
-    NMDeviceVrf *self        = NM_DEVICE_VRF(device);
-    gboolean     success     = TRUE;
-    const char  *slave_iface = nm_device_get_ip_iface(slave);
+    NMDeviceVrf *self       = NM_DEVICE_VRF(device);
+    gboolean     success    = TRUE;
+    const char  *port_iface = nm_device_get_ip_iface(port);
 
-    nm_device_master_check_slave_physical_port(device, slave, LOGD_DEVICE);
+    nm_device_master_check_slave_physical_port(device, port, LOGD_DEVICE);
 
     if (configure) {
-        nm_device_take_down(slave, TRUE);
+        nm_device_take_down(port, TRUE);
         success = nm_platform_link_enslave(nm_device_get_platform(device),
                                            nm_device_get_ip_ifindex(device),
-                                           nm_device_get_ip_ifindex(slave));
-        nm_device_bring_up(slave, TRUE, NULL);
+                                           nm_device_get_ip_ifindex(port));
+        nm_device_bring_up(port, TRUE, NULL);
 
         if (!success)
             return FALSE;
 
-        _LOGI(LOGD_DEVICE, "enslaved VRF slave %s", slave_iface);
+        _LOGI(LOGD_DEVICE, "attached VRF port %s", port_iface);
     } else
-        _LOGI(LOGD_BOND, "VRF slave %s was enslaved", slave_iface);
+        _LOGI(LOGD_BOND, "VRF port %s was attached", port_iface);
 
     return TRUE;
 }
 
 static void
-release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
+detach_port(NMDevice *device, NMDevice *port, gboolean configure)
 {
     NMDeviceVrf *self = NM_DEVICE_VRF(device);
     gboolean     success;
-    int          ifindex_slave;
+    int          ifindex_port;
     int          ifindex;
 
     if (configure) {
@@ -246,26 +252,26 @@ release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
             configure = FALSE;
     }
 
-    ifindex_slave = nm_device_get_ip_ifindex(slave);
+    ifindex_port = nm_device_get_ip_ifindex(port);
 
-    if (ifindex_slave <= 0)
-        _LOGD(LOGD_DEVICE, "VRF slave %s is already released", nm_device_get_ip_iface(slave));
+    if (ifindex_port <= 0)
+        _LOGD(LOGD_DEVICE, "VRF port %s is already detached", nm_device_get_ip_iface(port));
 
     if (configure) {
-        if (ifindex_slave > 0) {
+        if (ifindex_port > 0) {
             success = nm_platform_link_release(nm_device_get_platform(device),
                                                nm_device_get_ip_ifindex(device),
-                                               ifindex_slave);
+                                               ifindex_port);
 
             if (success) {
-                _LOGI(LOGD_DEVICE, "released VRF slave %s", nm_device_get_ip_iface(slave));
+                _LOGI(LOGD_DEVICE, "detached VRF port %s", nm_device_get_ip_iface(port));
             } else {
-                _LOGW(LOGD_DEVICE, "failed to release VRF slave %s", nm_device_get_ip_iface(slave));
+                _LOGW(LOGD_DEVICE, "failed to detach VRF port %s", nm_device_get_ip_iface(port));
             }
         }
     } else {
-        if (ifindex_slave > 0) {
-            _LOGI(LOGD_DEVICE, "VRF slave %s was released", nm_device_get_ip_iface(slave));
+        if (ifindex_port > 0) {
+            _LOGI(LOGD_DEVICE, "VRF port %s was detached", nm_device_get_ip_iface(port));
         }
     }
 }
@@ -316,8 +322,8 @@ nm_device_vrf_class_init(NMDeviceVrfClass *klass)
     device_class->is_master                        = TRUE;
     device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_VRF);
 
-    device_class->enslave_slave               = enslave_slave;
-    device_class->release_slave               = release_slave;
+    device_class->attach_port                 = attach_port;
+    device_class->detach_port                 = detach_port;
     device_class->link_changed                = link_changed;
     device_class->unrealize_notify            = unrealize_notify;
     device_class->create_and_realize          = create_and_realize;
diff --git a/src/core/devices/nm-device-wireguard.c b/src/core/devices/nm-device-wireguard.c
index bdb96cb2..179c2a22 100644
--- a/src/core/devices/nm-device-wireguard.c
+++ b/src/core/devices/nm-device-wireguard.c
@@ -18,7 +18,7 @@
 #include "nm-device-private.h"
 #include "libnm-platform/nm-platform.h"
 #include "libnm-platform/nmp-object.h"
-#include "libnm-platform/nmp-route-manager.h"
+#include "libnm-platform/nmp-global-tracker.h"
 #include "nm-device-factory.h"
 #include "nm-active-connection.h"
 #include "nm-act-request.h"
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index 6b854517..d63b902b 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -41,7 +41,7 @@
 #include "libnm-platform/nm-platform.h"
 #include "libnm-platform/nm-platform-utils.h"
 #include "libnm-platform/nmp-object.h"
-#include "libnm-platform/nmp-route-manager.h"
+#include "libnm-platform/nmp-global-tracker.h"
 #include "ndisc/nm-ndisc.h"
 #include "ndisc/nm-lndp-ndisc.h"
 
@@ -120,11 +120,12 @@ typedef enum _nm_packed {
 } AddrMethodState;
 
 typedef struct {
-    CList     lst_slave;
-    NMDevice *slave;
-    gulong    watch_id;
-    bool      slave_is_enslaved;
-    bool      configure;
+    CList         lst_slave;
+    NMDevice     *slave;
+    GCancellable *cancellable;
+    gulong        watch_id;
+    bool          slave_is_enslaved;
+    bool          configure;
 } SlaveInfo;
 
 typedef struct {
@@ -392,6 +393,8 @@ typedef struct _NMDevicePrivate {
     guint device_link_changed_id;
     guint device_ip_link_changed_id;
 
+    GSource *delay_activation_source;
+
     NMDeviceState       state;
     NMDeviceStateReason state_reason;
     struct {
@@ -604,6 +607,7 @@ typedef struct _NMDevicePrivate {
             const NMDeviceIPState state;
             NMDeviceIPState       state_;
         };
+        gulong dnsmgr_update_pending_signal_id;
     } ip_data;
 
     union {
@@ -1394,6 +1398,65 @@ _prop_get_connection_dns_over_tls(NMDevice *self)
                                                        NM_SETTING_CONNECTION_DNS_OVER_TLS_DEFAULT);
 }
 
+static NMMptcpFlags
+_prop_get_connection_mptcp_flags(NMDevice *self)
+{
+    NMConnection *connection;
+    NMMptcpFlags  mptcp_flags = NM_MPTCP_FLAGS_NONE;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), NM_MPTCP_FLAGS_DISABLED);
+
+    connection = nm_device_get_applied_connection(self);
+    if (connection) {
+        mptcp_flags =
+            nm_setting_connection_get_mptcp_flags(nm_connection_get_setting_connection(connection));
+    }
+
+    if (mptcp_flags == NM_MPTCP_FLAGS_NONE) {
+        guint64 v;
+
+        v = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                        NM_CON_DEFAULT("connection.mptcp-flags"),
+                                                        self,
+                                                        0,
+                                                        G_MAXINT64,
+                                                        NM_MPTCP_FLAGS_NONE);
+        if (v != NM_MPTCP_FLAGS_NONE) {
+            /* We silently ignore all invalid flags (and will normalize them away below). */
+            mptcp_flags = (NMMptcpFlags) v;
+            if (mptcp_flags == NM_MPTCP_FLAGS_NONE)
+                mptcp_flags = NM_MPTCP_FLAGS_ENABLED;
+        }
+    }
+
+    if (mptcp_flags == NM_MPTCP_FLAGS_NONE)
+        mptcp_flags = _NM_MPTCP_FLAGS_DEFAULT;
+
+    mptcp_flags = nm_mptcp_flags_normalize(mptcp_flags);
+
+    if (!NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_DISABLED)) {
+        if (!NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_ALSO_WITHOUT_SYSCTL)) {
+            guint32 v;
+
+            /* If enabled, but without "also-without-sysctl", then MPTCP is still
+             * disabled, if the sysctl says so...
+             *
+             * We evaluate this here. The point is that the decision is then cached
+             * until deactivation/reapply. The user can toggle the sysctl any time,
+             * but we only pick it up at certain moments (now). */
+            v = nm_platform_sysctl_get_int32(
+                nm_device_get_platform(self),
+                NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/mptcp/enabled"),
+                -1);
+            if (v <= 0)
+                mptcp_flags = NM_MPTCP_FLAGS_DISABLED;
+        } else
+            mptcp_flags = NM_FLAGS_UNSET(mptcp_flags, NM_MPTCP_FLAGS_ALSO_WITHOUT_SYSCTL);
+    }
+
+    return mptcp_flags;
+}
+
 static guint32
 _prop_get_ipvx_route_table(NMDevice *self, int addr_family)
 {
@@ -1508,6 +1571,53 @@ _prop_get_connection_lldp(NMDevice *self)
     return lldp == NM_SETTING_CONNECTION_LLDP_ENABLE_RX;
 }
 
+static NMSettingIP4LinkLocal
+_prop_get_ipv4_link_local(NMDevice *self)
+{
+    NMSettingIP4Config   *s_ip4;
+    NMSettingIP4LinkLocal link_local;
+
+    s_ip4 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP4_CONFIG);
+    if (!s_ip4)
+        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) {
+        /* For connections without a ipv4.link-local property configured the global configuration
+           might defines the default value for ipv4.link-local. */
+        link_local = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                                 NM_CON_DEFAULT("ipv4.link-local"),
+                                                                 self,
+                                                                 NM_SETTING_IP4_LL_AUTO,
+                                                                 NM_SETTING_IP4_LL_ENABLED,
+                                                                 NM_SETTING_IP4_LL_DEFAULT);
+        if (link_local == NM_SETTING_IP4_LL_DEFAULT) {
+            /* If there is no global configuration for ipv4.link-local assume auto */
+            link_local = NM_SETTING_IP4_LL_AUTO;
+        } else if (link_local == NM_SETTING_IP4_LL_ENABLED
+                   && nm_streq(nm_setting_ip_config_get_method((NMSettingIPConfig *) s_ip4),
+                               NM_SETTING_IP4_CONFIG_METHOD_DISABLED)) {
+            /* ipv4.method=disabled has higher priority than the global ipv4.link-local=enabled */
+            link_local = NM_SETTING_IP4_LL_DISABLED;
+        } else if (link_local == NM_SETTING_IP4_LL_DISABLED
+                   && nm_streq(nm_setting_ip_config_get_method((NMSettingIPConfig *) s_ip4),
+                               NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL)) {
+            /* ipv4.method=link-local has higher priority than the global ipv4.link-local=disabled */
+            link_local = NM_SETTING_IP4_LL_ENABLED;
+        }
+    }
+
+    if (link_local == NM_SETTING_IP4_LL_AUTO) {
+        link_local = nm_streq(nm_setting_ip_config_get_method((NMSettingIPConfig *) s_ip4),
+                              NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL)
+                         ? NM_SETTING_IP4_LL_ENABLED
+                         : NM_SETTING_IP4_LL_DISABLED;
+    }
+
+    return link_local;
+}
+
 static guint32
 _prop_get_ipv4_dad_timeout(NMDevice *self)
 {
@@ -2118,6 +2228,50 @@ _prop_get_ipv6_ip6_privacy(NMDevice *self)
     return _ip6_privacy_clamp(ip6_privacy);
 }
 
+static NMSettingIP6ConfigAddrGenMode
+_prop_get_ipv6_addr_gen_mode(NMDevice *self)
+{
+    NMSettingIP6ConfigAddrGenMode addr_gen_mode;
+    NMSettingIP6Config           *s_ip6;
+    gint64                        c;
+
+    g_return_val_if_fail(self, NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_STABLE_PRIVACY);
+
+    s_ip6 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP6_CONFIG);
+    if (s_ip6) {
+        addr_gen_mode = nm_setting_ip6_config_get_addr_gen_mode(s_ip6);
+        if (NM_IN_SET(addr_gen_mode,
+                      NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_EUI64,
+                      NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_STABLE_PRIVACY))
+            return addr_gen_mode;
+    } else
+        addr_gen_mode = NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_DEFAULT;
+
+    nm_assert(NM_IN_SET(addr_gen_mode,
+                        NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_DEFAULT_OR_EUI64,
+                        NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_DEFAULT));
+
+    c = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                    NM_CON_DEFAULT("ipv6.addr-gen-mode"),
+                                                    self,
+                                                    NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_EUI64,
+                                                    NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_DEFAULT,
+                                                    -1);
+    if (c != -1)
+        addr_gen_mode = c;
+
+    if (addr_gen_mode == NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_DEFAULT)
+        addr_gen_mode = NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_STABLE_PRIVACY;
+    else if (addr_gen_mode == NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_DEFAULT_OR_EUI64)
+        addr_gen_mode = NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_EUI64;
+
+    nm_assert(NM_IN_SET(addr_gen_mode,
+                        NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_EUI64,
+                        NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_STABLE_PRIVACY));
+
+    return addr_gen_mode;
+}
+
 static const char *
 _prop_get_x_cloned_mac_address(NMDevice *self, NMConnection *connection, gboolean is_wifi)
 {
@@ -2678,6 +2832,7 @@ nm_device_create_l3_config_data_from_connection(NMDevice *self, NMConnection *co
     nm_l3_config_data_set_llmnr(l3cd, _prop_get_connection_llmnr(self));
     nm_l3_config_data_set_dns_over_tls(l3cd, _prop_get_connection_dns_over_tls(self));
     nm_l3_config_data_set_ip6_privacy(l3cd, _prop_get_ipv6_ip6_privacy(self));
+    nm_l3_config_data_set_mptcp_flags(l3cd, _prop_get_connection_mptcp_flags(self));
     return l3cd;
 }
 
@@ -2930,6 +3085,13 @@ _add_capabilities(NMDevice *self, NMDeviceCapabilities capabilities)
 /*****************************************************************************/
 
 static void
+_dev_ip_state_dnsmgr_update_pending_changed(NMDnsManager *dnsmgr, GParamSpec *pspec, NMDevice *self)
+{
+    _dev_ip_state_check(self, AF_INET);
+    _dev_ip_state_check(self, AF_INET6);
+}
+
+static void
 _dev_ip_state_req_timeout_cancel(NMDevice *self, int addr_family)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
@@ -3321,6 +3483,27 @@ got_ip_state:
             combinedip_state = priv->ip_data.state;
     }
 
+    if (combinedip_state == NM_DEVICE_IP_STATE_READY
+        && priv->ip_data.state <= NM_DEVICE_IP_STATE_PENDING
+        && nm_dns_manager_get_update_pending(nm_manager_get_dns_manager(priv->manager))) {
+        /* We would be ready, but a DNS update is pending. That prevents us from getting fully ready. */
+        if (priv->ip_data.dnsmgr_update_pending_signal_id == 0) {
+            priv->ip_data.dnsmgr_update_pending_signal_id =
+                g_signal_connect(nm_manager_get_dns_manager(priv->manager),
+                                 "notify::" NM_DNS_MANAGER_UPDATE_PENDING,
+                                 G_CALLBACK(_dev_ip_state_dnsmgr_update_pending_changed),
+                                 self);
+            _LOGT_ip(AF_UNSPEC,
+                     "check-state: (combined) state: wait for DNS before becoming ready");
+        }
+        combinedip_state = NM_DEVICE_IP_STATE_PENDING;
+    }
+    if (combinedip_state != NM_DEVICE_IP_STATE_PENDING
+        && priv->ip_data.dnsmgr_update_pending_signal_id != 0) {
+        nm_clear_g_signal_handler(nm_manager_get_dns_manager(priv->manager),
+                                  &priv->ip_data.dnsmgr_update_pending_signal_id);
+    }
+
     _LOGT_ip(AF_UNSPEC,
              "check-state: (combined) state %s => %s",
              nm_device_ip_state_to_string(priv->ip_data.state),
@@ -5896,43 +6079,16 @@ find_slave_info(NMDevice *self, NMDevice *slave)
     return NULL;
 }
 
-/**
- * nm_device_master_enslave_slave:
- * @self: the master device
- * @slave: the slave device to enslave
- * @connection: (allow-none): the slave device's connection
- *
- * If @self is capable of enslaving other devices (ie it's a bridge, bond, team,
- * etc) then this function enslaves @slave.
- *
- * Returns: %TRUE on success, %FALSE on failure or if this device cannot enslave
- *  other devices.
- */
-static gboolean
-nm_device_master_enslave_slave(NMDevice *self, NMDevice *slave, NMConnection *connection)
+static void
+attach_port_done(NMDevice *self, NMDevice *slave, gboolean success)
 {
     SlaveInfo *info;
-    gboolean   success = FALSE;
-    gboolean   configure;
-
-    g_return_val_if_fail(self != NULL, FALSE);
-    g_return_val_if_fail(slave != NULL, FALSE);
-    g_return_val_if_fail(NM_DEVICE_GET_CLASS(self)->enslave_slave != NULL, FALSE);
 
     info = find_slave_info(self, slave);
     if (!info)
-        return FALSE;
-
-    if (info->slave_is_enslaved)
-        success = TRUE;
-    else {
-        configure = (info->configure && connection != NULL);
-        if (configure)
-            g_return_val_if_fail(nm_device_get_state(slave) >= NM_DEVICE_STATE_DISCONNECTED, FALSE);
+        return;
 
-        success = NM_DEVICE_GET_CLASS(self)->enslave_slave(self, slave, connection, configure);
-        info->slave_is_enslaved = success;
-    }
+    info->slave_is_enslaved = success;
 
     nm_device_slave_notify_enslave(info->slave, success);
 
@@ -5952,8 +6108,71 @@ nm_device_master_enslave_slave(NMDevice *self, NMDevice *slave, NMConnection *co
      */
     if (success)
         nm_device_activate_schedule_stage3_ip_config(self, FALSE);
+}
 
-    return success;
+static void
+attach_port_cb(NMDevice *self, GError *error, gpointer user_data)
+{
+    NMDevice  *slave = user_data;
+    SlaveInfo *info;
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    info = find_slave_info(self, slave);
+    if (!info)
+        return;
+
+    nm_clear_g_cancellable(&info->cancellable);
+    attach_port_done(self, slave, !error);
+}
+
+/**
+ * nm_device_master_enslave_slave:
+ * @self: the master device
+ * @slave: the slave device to enslave
+ * @connection: (allow-none): the slave device's connection
+ *
+ * If @self is capable of enslaving other devices (ie it's a bridge, bond, team,
+ * etc) then this function enslaves @slave.
+ */
+static void
+nm_device_master_enslave_slave(NMDevice *self, NMDevice *slave, NMConnection *connection)
+{
+    SlaveInfo *info;
+    NMTernary  success;
+    gboolean   configure;
+
+    g_return_if_fail(self);
+    g_return_if_fail(slave);
+    g_return_if_fail(NM_DEVICE_GET_CLASS(self)->attach_port);
+
+    info = find_slave_info(self, slave);
+    if (!info)
+        return;
+
+    if (info->slave_is_enslaved)
+        success = TRUE;
+    else {
+        configure = (info->configure && connection != NULL);
+        if (configure)
+            g_return_if_fail(nm_device_get_state(slave) >= NM_DEVICE_STATE_DISCONNECTED);
+
+        nm_clear_g_cancellable(&info->cancellable);
+        info->cancellable = g_cancellable_new();
+        success           = NM_DEVICE_GET_CLASS(self)->attach_port(self,
+                                                         slave,
+                                                         connection,
+                                                         configure,
+                                                         info->cancellable,
+                                                         attach_port_cb,
+                                                         slave);
+
+        if (success == NM_TERNARY_DEFAULT)
+            return;
+    }
+
+    attach_port_done(self, slave, success);
 }
 
 /**
@@ -5986,7 +6205,7 @@ nm_device_master_release_slave(NMDevice           *self,
                         RELEASE_SLAVE_TYPE_NO_CONFIG,
                         RELEASE_SLAVE_TYPE_CONFIG,
                         RELEASE_SLAVE_TYPE_CONFIG_FORCE));
-    g_return_if_fail(NM_DEVICE_GET_CLASS(self)->release_slave != NULL);
+    g_return_if_fail(NM_DEVICE_GET_CLASS(self)->detach_port != NULL);
 
     info = find_slave_info(self, slave);
 
@@ -6007,13 +6226,14 @@ nm_device_master_release_slave(NMDevice           *self,
 
     g_return_if_fail(self == slave_priv->master);
     nm_assert(slave == info->slave);
+    nm_clear_g_cancellable(&info->cancellable);
 
     /* first, let subclasses handle the release ... */
     if (info->slave_is_enslaved || nm_device_sys_iface_state_is_external(slave)
         || release_type >= RELEASE_SLAVE_TYPE_CONFIG_FORCE)
-        NM_DEVICE_GET_CLASS(self)->release_slave(self,
-                                                 slave,
-                                                 release_type >= RELEASE_SLAVE_TYPE_CONFIG);
+        NM_DEVICE_GET_CLASS(self)->detach_port(self,
+                                               slave,
+                                               release_type >= RELEASE_SLAVE_TYPE_CONFIG);
 
     /* raise notifications about the release, including clearing is_enslaved. */
     nm_device_slave_notify_release(slave, reason);
@@ -6112,7 +6332,7 @@ _dev_unmanaged_check_external_down(NMDevice *self, gboolean only_if_unmanaged, g
 }
 
 void
-nm_device_update_dynamic_ip_setup(NMDevice *self)
+nm_device_update_dynamic_ip_setup(NMDevice *self, const char *reason)
 {
     NMDevicePrivate *priv;
 
@@ -6123,6 +6343,8 @@ nm_device_update_dynamic_ip_setup(NMDevice *self)
     if (priv->state < NM_DEVICE_STATE_IP_CONFIG || priv->state > NM_DEVICE_STATE_ACTIVATED)
         return;
 
+    _LOGD(LOGD_DEVICE, "restarting dynamic IP configuration (%s)", reason);
+
     g_hash_table_remove_all(priv->ip6_saved_properties);
 
     if (priv->ipdhcp_data_4.state != NM_DEVICE_IP_STATE_NONE)
@@ -6344,7 +6566,7 @@ device_recheck_slave_status(NMDevice *self, const NMPlatformLink *plink)
                                        NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
     }
 
-    if (master && NM_DEVICE_GET_CLASS(master)->enslave_slave) {
+    if (master && NM_DEVICE_GET_CLASS(master)->attach_port) {
         nm_device_master_add_slave(master, self, FALSE);
         goto out;
     }
@@ -6424,6 +6646,7 @@ device_link_changed(gpointer user_data)
     NMDeviceClass                  *klass             = NM_DEVICE_GET_CLASS(self);
     NMDevicePrivate                *priv              = NM_DEVICE_GET_PRIVATE(self);
     gboolean                        ip_ifname_changed = FALSE;
+    gboolean                        hw_addr_changed;
     nm_auto_nmpobj const NMPObject *pllink_keep_alive = NULL;
     const NMPlatformLink           *pllink;
     const char                     *str;
@@ -6470,9 +6693,9 @@ device_link_changed(gpointer user_data)
     if (ifindex == nm_device_get_ip_ifindex(self))
         _stats_update_counters_from_pllink(self, pllink);
 
-    had_hw_addr = (priv->hw_addr != NULL);
-    nm_device_update_hw_address(self);
-    got_hw_addr = (!had_hw_addr && priv->hw_addr);
+    had_hw_addr     = (priv->hw_addr != NULL);
+    hw_addr_changed = nm_device_update_hw_address(self);
+    got_hw_addr     = (!had_hw_addr && priv->hw_addr);
     nm_device_update_permanent_hw_address(self, FALSE);
 
     if (pllink->name[0] && !nm_streq(priv->iface, pllink->name)) {
@@ -6522,7 +6745,9 @@ device_link_changed(gpointer user_data)
 
     /* Update DHCP, etc, if needed */
     if (ip_ifname_changed)
-        nm_device_update_dynamic_ip_setup(self);
+        nm_device_update_dynamic_ip_setup(self, "IP interface changed");
+    else if (hw_addr_changed)
+        nm_device_update_dynamic_ip_setup(self, "hw-address changed");
 
     was_up   = priv->up;
     priv->up = NM_FLAGS_HAS(pllink->n_ifi_flags, IFF_UP);
@@ -6582,7 +6807,7 @@ device_link_changed(gpointer user_data)
          * renew DHCP leases and such.
          */
         if (priv->state == NM_DEVICE_STATE_ACTIVATED) {
-            nm_device_update_dynamic_ip_setup(self);
+            nm_device_update_dynamic_ip_setup(self, "interface got carrier");
         }
     }
 
@@ -6644,7 +6869,7 @@ device_ip_link_changed(gpointer user_data)
         priv->ip_iface_ = g_strdup(ip_iface);
         update_prop_ip_iface(self);
 
-        nm_device_update_dynamic_ip_setup(self);
+        nm_device_update_dynamic_ip_setup(self, "interface renamed");
     }
 
     return G_SOURCE_REMOVE;
@@ -7540,8 +7765,9 @@ slave_state_changed(NMDevice           *slave,
     }
 
     if (release) {
-        configure = priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_MANAGED
-                    && nm_device_sys_iface_state_get(slave) != NM_DEVICE_SYS_IFACE_STATE_EXTERNAL;
+        configure = (priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_MANAGED
+                     && nm_device_sys_iface_state_get(slave) != NM_DEVICE_SYS_IFACE_STATE_EXTERNAL)
+                    || nm_device_sys_iface_state_get(slave) == NM_DEVICE_SYS_IFACE_STATE_MANAGED;
 
         nm_device_master_release_slave(self,
                                        slave,
@@ -7577,7 +7803,7 @@ nm_device_master_add_slave(NMDevice *self, NMDevice *slave, gboolean configure)
 
     g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
     g_return_val_if_fail(NM_IS_DEVICE(slave), FALSE);
-    g_return_val_if_fail(NM_DEVICE_GET_CLASS(self)->enslave_slave != NULL, FALSE);
+    g_return_val_if_fail(NM_DEVICE_GET_CLASS(self)->attach_port, FALSE);
 
     priv       = NM_DEVICE_GET_PRIVATE(self);
     slave_priv = NM_DEVICE_GET_PRIVATE(slave);
@@ -7806,6 +8032,9 @@ nm_device_slave_notify_release(NMDevice *self, NMDeviceStateReason reason)
 
     g_return_if_fail(priv->master);
 
+    if (!priv->is_enslaved)
+        return;
+
     if (priv->state > NM_DEVICE_STATE_DISCONNECTED && priv->state <= NM_DEVICE_STATE_ACTIVATED) {
         switch (nm_device_state_reason_check(reason)) {
         case NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED:
@@ -7835,14 +8064,12 @@ nm_device_slave_notify_release(NMDevice *self, NMDeviceStateReason reason)
     } else
         _LOGI(LOGD_DEVICE, "released from master device %s", nm_device_get_iface(priv->master));
 
-    if (priv->is_enslaved) {
-        priv->is_enslaved = FALSE;
+    priv->is_enslaved = FALSE;
 
-        _notify(self, PROP_MASTER);
+    _notify(self, PROP_MASTER);
 
-        nm_clear_pointer(&NM_DEVICE_GET_PRIVATE(priv->master)->ports_variant, g_variant_unref);
-        nm_gobject_notify_together(priv->master, PROP_PORTS, PROP_SLAVES);
-    }
+    nm_clear_pointer(&NM_DEVICE_GET_PRIVATE(priv->master)->ports_variant, g_variant_unref);
+    nm_gobject_notify_together(priv->master, PROP_PORTS, PROP_SLAVES);
 }
 
 /**
@@ -8161,13 +8388,13 @@ device_has_config(NMDevice *self)
 
     head_entry = nm_platform_lookup(
         nm_device_get_platform(self),
-        nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP4_ADDRESS, pllink->ifindex));
+        nmp_lookup_init_object_by_ifindex(&lookup, NMP_OBJECT_TYPE_IP4_ADDRESS, pllink->ifindex));
     if (head_entry)
         return TRUE;
 
     head_entry = nm_platform_lookup(
         nm_device_get_platform(self),
-        nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP6_ADDRESS, pllink->ifindex));
+        nmp_lookup_init_object_by_ifindex(&lookup, NMP_OBJECT_TYPE_IP6_ADDRESS, pllink->ifindex));
     if (head_entry)
         return TRUE;
 
@@ -9329,13 +9556,13 @@ lldp_setup(NMDevice *self, NMTernary enabled)
 static void
 _routing_rules_sync(NMDevice *self, NMTernary set_mode)
 {
-    NMDevicePrivate *priv               = NM_DEVICE_GET_PRIVATE(self);
-    NMPRouteManager *route_manager      = nm_netns_get_route_manager(nm_device_get_netns(self));
-    NMDeviceClass   *klass              = NM_DEVICE_GET_CLASS(self);
-    gboolean         untrack_only_dirty = FALSE;
-    gboolean         keep_deleted_rules;
-    gpointer         user_tag_1;
-    gpointer         user_tag_2;
+    NMDevicePrivate  *priv               = NM_DEVICE_GET_PRIVATE(self);
+    NMPGlobalTracker *global_tracker     = nm_netns_get_global_tracker(nm_device_get_netns(self));
+    NMDeviceClass    *klass              = NM_DEVICE_GET_CLASS(self);
+    gboolean          untrack_only_dirty = FALSE;
+    gboolean          keep_deleted_rules;
+    gpointer          user_tag_1;
+    gpointer          user_tag_2;
 
     /* take two arbitrary user-tag pointers that belong to @self. */
     user_tag_1 = &priv->v4_route_table;
@@ -9367,13 +9594,13 @@ _routing_rules_sync(NMDevice *self, NMTernary set_mode)
                 nm_ip_routing_rule_to_platform(rule, &plrule);
 
                 /* We track this rule, but we also make it explicitly not weakly-tracked
-                 * (meaning to untrack NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG at
+                 * (meaning to untrack NMP_GLOBAL_TRACKER_EXTERN_WEAKLY_TRACKED_USER_TAG at
                  * the same time). */
-                nmp_route_manager_track_rule(route_manager,
-                                             &plrule,
-                                             10,
-                                             user_tag_1,
-                                             NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG);
+                nmp_global_tracker_track_rule(global_tracker,
+                                              &plrule,
+                                              10,
+                                              user_tag_1,
+                                              NMP_GLOBAL_TRACKER_EXTERN_WEAKLY_TRACKED_USER_TAG);
             }
         }
 
@@ -9383,25 +9610,25 @@ _routing_rules_sync(NMDevice *self, NMTernary set_mode)
             extra_rules = klass->get_extra_rules(self);
             if (extra_rules) {
                 for (i = 0; i < extra_rules->len; i++) {
-                    nmp_route_manager_track_rule(
-                        route_manager,
+                    nmp_global_tracker_track_rule(
+                        global_tracker,
                         NMP_OBJECT_CAST_ROUTING_RULE(extra_rules->pdata[i]),
                         10,
                         user_tag_2,
-                        NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG);
+                        NMP_GLOBAL_TRACKER_EXTERN_WEAKLY_TRACKED_USER_TAG);
                 }
             }
         }
     }
 
-    nmp_route_manager_untrack_all(route_manager, user_tag_1, !untrack_only_dirty, TRUE);
+    nmp_global_tracker_untrack_all(global_tracker, user_tag_1, !untrack_only_dirty, TRUE);
     if (klass->get_extra_rules)
-        nmp_route_manager_untrack_all(route_manager, user_tag_2, !untrack_only_dirty, TRUE);
+        nmp_global_tracker_untrack_all(global_tracker, user_tag_2, !untrack_only_dirty, TRUE);
 
     keep_deleted_rules = FALSE;
     if (set_mode == NM_TERNARY_DEFAULT) {
         /* when exiting NM, we leave the device up and the rules configured.
-         * We just call nmp_route_manager_sync() to forget about the synced rules,
+         * We just call nmp_global_tracker_sync() to forget about the synced rules,
          * but we don't actually delete them.
          *
          * FIXME: that is a problem after restart of NetworkManager, because these
@@ -9415,7 +9642,7 @@ _routing_rules_sync(NMDevice *self, NMTernary set_mode)
          * file and track them after restart again. */
         keep_deleted_rules = TRUE;
     }
-    nmp_route_manager_sync(route_manager, NMP_OBJECT_TYPE_ROUTING_RULE, keep_deleted_rules);
+    nmp_global_tracker_sync(global_tracker, NMP_OBJECT_TYPE_ROUTING_RULE, keep_deleted_rules);
 }
 
 static gboolean
@@ -10045,6 +10272,7 @@ _dev_ipdhcpx_notify(NMDhcpClient *client, const NMDhcpClientNotifyData *notify_d
                                             FALSE);
 
         if (notify_data->lease_update.accepted) {
+            nm_manager_write_device_state(priv->manager, self, NULL);
             if (priv->ipdhcp_data_x[IS_IPv4].state != NM_DEVICE_IP_STATE_READY) {
                 _dev_ipdhcpx_set_state(self, addr_family, NM_DEVICE_IP_STATE_READY);
                 nm_dispatcher_call_device(NM_DISPATCHER_ACTION_DHCP_CHANGE_X(IS_IPv4),
@@ -10190,7 +10418,11 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
             .vendor_class_identifier = vendor_class_identifier,
             .use_fqdn                = hostname_is_fqdn,
             .reject_servers          = reject_servers,
-            .v4.request_broadcast    = request_broadcast,
+            .v4 =
+                {
+                    .request_broadcast = request_broadcast,
+                    .acd_timeout_msec  = _prop_get_ipv4_dad_timeout(self),
+                },
         };
 
         priv->ipdhcp_data_4.client =
@@ -10205,22 +10437,25 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
         duid = _prop_get_ipv6_dhcp_duid(self, connection, hwaddr, &enforce_duid);
 
         config = (NMDhcpClientConfig){
-            .addr_family        = AF_INET6,
-            .l3cfg              = nm_device_get_l3cfg(self),
-            .iface              = nm_device_get_ip_iface(self),
-            .uuid               = nm_connection_get_uuid(connection),
-            .send_hostname      = nm_setting_ip_config_get_dhcp_send_hostname(s_ip),
-            .hostname           = nm_setting_ip_config_get_dhcp_hostname(s_ip),
-            .hostname_flags     = _prop_get_ipvx_dhcp_hostname_flags(self, AF_INET6),
-            .client_id          = duid,
-            .mud_url            = _prop_get_connection_mud_url(self, s_con),
-            .timeout            = no_lease_timeout_sec,
-            .anycast_address    = _device_get_dhcp_anycast_address(self),
-            .v6.enforce_duid    = enforce_duid,
-            .v6.iaid            = iaid,
-            .v6.iaid_explicit   = iaid_explicit,
-            .v6.info_only       = (priv->ipdhcp_data_6.v6.mode == NM_NDISC_DHCP_LEVEL_OTHERCONF),
-            .v6.needed_prefixes = priv->ipdhcp_data_6.v6.needed_prefixes,
+            .addr_family     = AF_INET6,
+            .l3cfg           = nm_device_get_l3cfg(self),
+            .iface           = nm_device_get_ip_iface(self),
+            .uuid            = nm_connection_get_uuid(connection),
+            .send_hostname   = nm_setting_ip_config_get_dhcp_send_hostname(s_ip),
+            .hostname        = nm_setting_ip_config_get_dhcp_hostname(s_ip),
+            .hostname_flags  = _prop_get_ipvx_dhcp_hostname_flags(self, AF_INET6),
+            .client_id       = duid,
+            .mud_url         = _prop_get_connection_mud_url(self, s_con),
+            .timeout         = no_lease_timeout_sec,
+            .anycast_address = _device_get_dhcp_anycast_address(self),
+            .v6 =
+                {
+                    .enforce_duid  = enforce_duid,
+                    .iaid          = iaid,
+                    .iaid_explicit = iaid_explicit,
+                    .info_only     = (priv->ipdhcp_data_6.v6.mode == NM_NDISC_DHCP_LEVEL_OTHERCONF),
+                    .needed_prefixes = priv->ipdhcp_data_6.v6.needed_prefixes,
+                },
         };
 
         priv->ipdhcp_data_6.client =
@@ -10635,7 +10870,6 @@ _dev_ipll6_start(NMDevice *self)
 {
     NMDevicePrivate       *priv = NM_DEVICE_GET_PRIVATE(self);
     NMConnection          *connection;
-    NMSettingIP6Config    *s_ip6 = NULL;
     gboolean               assume;
     const char            *ifname;
     NML3IPv6LLState        llstate;
@@ -10656,14 +10890,10 @@ _dev_ipll6_start(NMDevice *self)
     }
 
     connection = nm_device_get_applied_connection(self);
-    if (connection)
-        s_ip6 = NM_SETTING_IP6_CONFIG(nm_connection_get_setting_ip6_config(connection));
 
     assume = nm_device_sys_iface_state_is_external_or_assume(self);
 
-    if (s_ip6
-        && nm_setting_ip6_config_get_addr_gen_mode(s_ip6)
-               == NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_STABLE_PRIVACY) {
+    if (_prop_get_ipv6_addr_gen_mode(self) == NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_STABLE_PRIVACY) {
         NMUtilsStableType stable_type;
         const char       *stable_id;
 
@@ -10866,9 +11096,10 @@ _commit_mtu(NMDevice *self)
 {
     NMDevicePrivate      *priv   = NM_DEVICE_GET_PRIVATE(self);
     NMDeviceMtuSource     source = NM_DEVICE_MTU_SOURCE_NONE;
+    NMSettingIPConfig    *s_ip6;
     const NML3ConfigData *l3cd;
     guint32               ip6_mtu_orig;
-    guint32               ip6_mtu;
+    guint32               ip6_mtu = 0;
     guint32               mtu_desired_orig;
     guint32               mtu_desired;
     guint32               mtu_plat;
@@ -10953,10 +11184,9 @@ _commit_mtu(NMDevice *self)
         }
     }
 
-    if (mtu_desired && mtu_desired < 1280) {
-        NMSettingIPConfig *s_ip6;
+    s_ip6 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP6_CONFIG);
 
-        s_ip6 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP6_CONFIG);
+    if (mtu_desired && mtu_desired < 1280) {
         if (s_ip6
             && !NM_IN_STRSET(nm_setting_ip_config_get_method(s_ip6),
                              NM_SETTING_IP6_CONFIG_METHOD_IGNORE,
@@ -10971,7 +11201,12 @@ _commit_mtu(NMDevice *self)
         }
     }
 
-    ip6_mtu = priv->ip6_mtu;
+    if (s_ip6)
+        ip6_mtu = nm_setting_ip6_config_get_mtu(NM_SETTING_IP6_CONFIG(s_ip6));
+
+    if (!ip6_mtu)
+        ip6_mtu = priv->ip6_mtu;
+
     if (!ip6_mtu && priv->mtu_source == NM_DEVICE_MTU_SOURCE_NONE) {
         /* initially, if the IPv6 MTU is not specified, grow it as large as the
          * link MTU @mtu_desired. Only exception is, if @mtu_desired is so small
@@ -11353,7 +11588,7 @@ _dev_ipac6_start(NMDevice *self)
             .ifname                       = nm_device_get_ip_iface(self),
             .stable_type                  = stable_type,
             .network_id                   = stable_id,
-            .addr_gen_mode                = nm_setting_ip6_config_get_addr_gen_mode(s_ip),
+            .addr_gen_mode                = _prop_get_ipv6_addr_gen_mode(self),
             .node_type                    = node_type,
             .max_addresses                = max_addresses,
             .router_solicitations         = router_solicitations,
@@ -11640,11 +11875,14 @@ activate_stage3_ip_config_for_addr_family(NMDevice *self, int addr_family, const
         goto out_devip;
 
     if (IS_IPv4) {
+        if (_prop_get_ipv4_link_local(self) == NM_SETTING_IP4_LL_ENABLED)
+            _dev_ipll4_start(self);
+
         if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO))
             _dev_ipdhcpx_start(self, AF_INET);
-        else if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL))
-            _dev_ipll4_start(self);
-        else if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_SHARED))
+        else if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL)) {
+            /* pass */
+        } else if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_SHARED))
             _dev_ipshared4_start(self);
         else if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED))
             priv->ip_data_x[IS_IPv4].is_disabled = TRUE;
@@ -12344,7 +12582,8 @@ delete_on_deactivate_check_and_schedule(NMDevice *self)
 static void
 _cleanup_ip_pre(NMDevice *self, int addr_family, CleanupType cleanup_type, gboolean from_reapply)
 {
-    const int IS_IPv4 = NM_IS_IPv4(addr_family);
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
 
     _dev_ipsharedx_cleanup(self, addr_family);
 
@@ -12360,6 +12599,9 @@ _cleanup_ip_pre(NMDevice *self, int addr_family, CleanupType cleanup_type, gbool
 
     _dev_ipmanual_cleanup(self);
 
+    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);
 }
@@ -12479,12 +12721,15 @@ can_reapply_change(NMDevice   *self,
                                                  NM_SETTING_CONNECTION_STABLE_ID,
                                                  NM_SETTING_CONNECTION_AUTOCONNECT,
                                                  NM_SETTING_CONNECTION_AUTOCONNECT_SLAVES,
+                                                 NM_SETTING_CONNECTION_AUTOCONNECT_PRIORITY,
                                                  NM_SETTING_CONNECTION_ZONE,
                                                  NM_SETTING_CONNECTION_METERED,
                                                  NM_SETTING_CONNECTION_LLDP,
                                                  NM_SETTING_CONNECTION_MDNS,
                                                  NM_SETTING_CONNECTION_LLMNR,
-                                                 NM_SETTING_CONNECTION_DNS_OVER_TLS);
+                                                 NM_SETTING_CONNECTION_DNS_OVER_TLS,
+                                                 NM_SETTING_CONNECTION_MPTCP_FLAGS,
+                                                 NM_SETTING_CONNECTION_WAIT_ACTIVATION_DELAY);
     }
 
     if (NM_IN_STRSET(setting_name,
@@ -12700,7 +12945,8 @@ check_and_reapply_connection(NMDevice     *self,
                 NM_SETTING_CONNECTION_LLDP,
                 NM_SETTING_CONNECTION_MDNS,
                 NM_SETTING_CONNECTION_LLMNR,
-                NM_SETTING_CONNECTION_DNS_OVER_TLS)) {
+                NM_SETTING_CONNECTION_DNS_OVER_TLS,
+                NM_SETTING_CONNECTION_MPTCP_FLAGS)) {
             priv->ip_data_4.do_reapply = TRUE;
             priv->ip_data_6.do_reapply = TRUE;
         }
@@ -13361,29 +13607,71 @@ _dispatcher_cleanup(NMDevice *self)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
+    nm_clear_g_source_inst(&priv->delay_activation_source);
+
     if (!priv->dispatcher.call_id)
         return FALSE;
 
     nm_dispatcher_call_cancel(g_steal_pointer(&priv->dispatcher.call_id));
     priv->dispatcher.post_state        = NM_DEVICE_STATE_UNKNOWN;
     priv->dispatcher.post_state_reason = NM_DEVICE_STATE_REASON_NONE;
+
     return TRUE;
 }
 
 static void
-_dispatcher_complete_proceed_state(NMDispatcherCallId *call_id, gpointer user_data)
+_queue_dispatcher_post_state(NMDevice *self)
 {
-    NMDevice        *self = NM_DEVICE(user_data);
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
-    g_return_if_fail(call_id == priv->dispatcher.call_id);
-
-    priv->dispatcher.call_id = NULL;
     nm_device_queue_state(self, priv->dispatcher.post_state, priv->dispatcher.post_state_reason);
     priv->dispatcher.post_state        = NM_DEVICE_STATE_UNKNOWN;
     priv->dispatcher.post_state_reason = NM_DEVICE_STATE_REASON_NONE;
 }
 
+static gboolean
+_wait_activation_delay_timeout(gpointer user_data)
+{
+    NMDevice        *self = user_data;
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_clear_g_source_inst(&priv->delay_activation_source);
+
+    _LOGD(LOGD_DEVICE, "finished waiting on activation delay");
+    _queue_dispatcher_post_state(self);
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+_dispatcher_complete_proceed_state(NMDispatcherCallId *call_id, gpointer user_data)
+{
+    NMDevice            *self = NM_DEVICE(user_data);
+    NMDevicePrivate     *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMConnection        *conn;
+    NMSettingConnection *s_conn;
+    gint32               delay_timeout;
+
+    g_return_if_fail(call_id == priv->dispatcher.call_id);
+    nm_assert(!priv->delay_activation_source);
+
+    priv->dispatcher.call_id = NULL;
+    conn                     = nm_device_get_applied_connection(self);
+    if (conn) {
+        s_conn = nm_connection_get_setting_connection(conn);
+        if (s_conn) {
+            delay_timeout = nm_setting_connection_get_wait_activation_delay(s_conn);
+            if (delay_timeout > 0) {
+                priv->delay_activation_source =
+                    nm_g_timeout_add_source(delay_timeout, _wait_activation_delay_timeout, self);
+                return;
+            }
+        }
+    }
+
+    _queue_dispatcher_post_state(self);
+}
+
 /*****************************************************************************/
 
 static void
@@ -13437,14 +13725,14 @@ spawn_ping(NMDevice *self)
     gs_free char         *str_timeout = NULL;
     gs_free char         *tmp_str     = NULL;
     const char           *args[]      = {priv->gw_ping.binary,
-                          "-I",
-                          nm_device_get_ip_iface(self),
-                          "-c",
-                          "1",
-                          "-w",
-                          NULL,
-                          priv->gw_ping.address,
-                          NULL};
+                                         "-I",
+                                         nm_device_get_ip_iface(self),
+                                         "-c",
+                                         "1",
+                                         "-w",
+                                         NULL,
+                                         priv->gw_ping.address,
+                                         NULL};
     gs_free_error GError *error       = NULL;
     gboolean              ret;
 
@@ -13665,10 +13953,38 @@ _get_carrier_wait_ms(NMDevice *self)
                                                   CARRIER_WAIT_TIME_MS);
 }
 
+/*
+ * Devices that support carrier detect must be IFF_UP to report carrier
+ * changes; so after setting the device IFF_UP we must suppress startup
+ * complete (via a pending action) until either the carrier turns on, or
+ * a timeout is reached.
+ */
+static void
+carrier_detect_wait(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    gint64           now_ms, until_ms;
+
+    if (!nm_device_has_capability(self, NM_DEVICE_CAP_CARRIER_DETECT))
+        return;
+
+    /* we start a grace period of 5 seconds during which we will schedule
+     * a pending action whenever we have no carrier.
+     *
+     * If during that time carrier goes away, we declare the interface
+     * as not ready. */
+    nm_clear_g_source(&priv->carrier_wait_id);
+    if (!priv->carrier)
+        nm_device_add_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE);
+
+    now_ms   = nm_utils_get_monotonic_timestamp_msec();
+    until_ms = NM_MAX(now_ms + _get_carrier_wait_ms(self), priv->carrier_wait_until_ms);
+    priv->carrier_wait_id = g_timeout_add(until_ms - now_ms, carrier_wait_timeout, self);
+}
+
 gboolean
 nm_device_bring_up(NMDevice *self, gboolean block, gboolean *no_firmware)
 {
-    NMDevicePrivate     *priv         = NM_DEVICE_GET_PRIVATE(self);
     gboolean             device_is_up = FALSE;
     NMDeviceCapabilities capabilities;
     int                  ifindex;
@@ -13724,27 +14040,7 @@ nm_device_bring_up(NMDevice *self, gboolean block, gboolean *no_firmware)
         capabilities |= NM_DEVICE_GET_CLASS(self)->get_generic_capabilities(self);
     _add_capabilities(self, capabilities);
 
-    /* Devices that support carrier detect must be IFF_UP to report carrier
-     * changes; so after setting the device IFF_UP we must suppress startup
-     * complete (via a pending action) until either the carrier turns on, or
-     * a timeout is reached.
-     */
-    if (nm_device_has_capability(self, NM_DEVICE_CAP_CARRIER_DETECT)) {
-        gint64 now_ms, until_ms;
-
-        /* we start a grace period of 5 seconds during which we will schedule
-         * a pending action whenever we have no carrier.
-         *
-         * If during that time carrier goes away, we declare the interface
-         * as not ready. */
-        nm_clear_g_source(&priv->carrier_wait_id);
-        if (!priv->carrier)
-            nm_device_add_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE);
-
-        now_ms   = nm_utils_get_monotonic_timestamp_msec();
-        until_ms = NM_MAX(now_ms + _get_carrier_wait_ms(self), priv->carrier_wait_until_ms);
-        priv->carrier_wait_id = g_timeout_add(until_ms - now_ms, carrier_wait_timeout, self);
-    }
+    carrier_detect_wait(self);
 
     /* Can only get HW address of some devices when they are up */
     nm_device_update_hw_address(self);
@@ -14997,6 +15293,9 @@ _cancel_activation(NMDevice *self)
     _dispatcher_cleanup(self);
     ip_check_gw_ping_cleanup(self);
 
+    _dev_ip_state_cleanup(self, AF_INET, FALSE);
+    _dev_ip_state_cleanup(self, AF_INET6, FALSE);
+
     /* Break the activation chain */
     activation_source_clear(self);
 }
@@ -15493,6 +15792,13 @@ _set_state_full(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason,
             if (priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_MANAGED)
                 ip6_managed_setup(self);
             device_init_static_sriov_num_vfs(self);
+
+            /* We didn't bring the device up and we have little idea
+             * when was it brought up. Play it safe and assume it could
+             * have been brought up very recently and it might one of
+             * those who take time to detect carrier.
+             */
+            carrier_detect_wait(self);
         }
 
         if (priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_MANAGED) {
@@ -16804,7 +17110,7 @@ get_address_for_hostname_dns_lookup(NMDevice *self, int addr_family)
      *   opposed to an address that is configured? */
     head_entry = nm_platform_lookup(
         nm_device_get_platform(self),
-        nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4), ifindex));
+        nmp_lookup_init_object_by_ifindex(&lookup, NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4), ifindex));
 
     if (head_entry) {
         c_list_for_each_entry (iter, &head_entry->lst_entries_head, lst_entries) {
diff --git a/src/core/devices/nm-device.h b/src/core/devices/nm-device.h
index d59aecb2..de850e68 100644
--- a/src/core/devices/nm-device.h
+++ b/src/core/devices/nm-device.h
@@ -163,6 +163,7 @@ typedef enum {
 } NMDeviceCheckDevAvailableFlags;
 
 typedef void (*NMDeviceDeactivateCallback)(NMDevice *self, GError *error, gpointer user_data);
+typedef void (*NMDeviceAttachPortCallback)(NMDevice *self, GError *error, gpointer user_data);
 
 typedef struct _NMDeviceClass {
     NMDBusObjectClass parent;
@@ -373,12 +374,18 @@ typedef struct _NMDeviceClass {
                                                NMConnection *connection,
                                                GError      **error);
 
-    gboolean (*enslave_slave)(NMDevice     *self,
-                              NMDevice     *slave,
-                              NMConnection *connection,
-                              gboolean      configure);
-
-    void (*release_slave)(NMDevice *self, NMDevice *slave, gboolean configure);
+    /* Attachs a port asynchronously. Returns TRUE/FALSE on immediate
+     * success/error; in such cases, the callback is not invoked. If the
+     * action couldn't be completed immediately, DEFAULT is returned and
+     * the callback will always be invoked asynchronously. */
+    NMTernary (*attach_port)(NMDevice                  *self,
+                             NMDevice                  *port,
+                             NMConnection              *connection,
+                             gboolean                   configure,
+                             GCancellable              *cancellable,
+                             NMDeviceAttachPortCallback callback,
+                             gpointer                   user_data);
+    void (*detach_port)(NMDevice *self, NMDevice *port, gboolean configure);
 
     void (*parent_changed_notify)(NMDevice *self,
                                   int       old_ifindex,
@@ -757,7 +764,7 @@ void nm_device_update_metered(NMDevice *self);
 gboolean nm_device_update_hw_address(NMDevice *self);
 void     nm_device_update_initial_hw_address(NMDevice *self);
 void     nm_device_update_permanent_hw_address(NMDevice *self, gboolean force_freeze);
-void     nm_device_update_dynamic_ip_setup(NMDevice *self);
+void     nm_device_update_dynamic_ip_setup(NMDevice *self, const char *reason);
 guint    nm_device_get_supplicant_timeout(NMDevice *self);
 
 gboolean nm_device_auth_retries_try_next(NMDevice *self);
diff --git a/src/core/devices/ovs/nm-device-ovs-bridge.c b/src/core/devices/ovs/nm-device-ovs-bridge.c
index ea77dd18..048afb02 100644
--- a/src/core/devices/ovs/nm-device-ovs-bridge.c
+++ b/src/core/devices/ovs/nm-device-ovs-bridge.c
@@ -78,20 +78,26 @@ act_stage3_ip_config(NMDevice *device, int addr_family)
     nm_device_devip_set_state(device, addr_family, NM_DEVICE_IP_STATE_READY, NULL);
 }
 
-static gboolean
-enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure)
+static NMTernary
+attach_port(NMDevice                  *device,
+            NMDevice                  *port,
+            NMConnection              *connection,
+            gboolean                   configure,
+            GCancellable              *cancellable,
+            NMDeviceAttachPortCallback callback,
+            gpointer                   user_data)
 {
     if (!configure)
         return TRUE;
 
-    if (!NM_IS_DEVICE_OVS_PORT(slave))
+    if (!NM_IS_DEVICE_OVS_PORT(port))
         return FALSE;
 
     return TRUE;
 }
 
 static void
-release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
+detach_port(NMDevice *device, NMDevice *port, gboolean configure)
 {}
 
 void
@@ -159,8 +165,8 @@ nm_device_ovs_bridge_class_init(NMDeviceOvsBridgeClass *klass)
     device_class->get_generic_capabilities            = get_generic_capabilities;
     device_class->act_stage3_ip_config                = act_stage3_ip_config;
     device_class->ready_for_ip_config                 = ready_for_ip_config;
-    device_class->enslave_slave                       = enslave_slave;
-    device_class->release_slave                       = release_slave;
+    device_class->attach_port                         = attach_port;
+    device_class->detach_port                         = detach_port;
     device_class->can_reapply_change_ovs_external_ids = TRUE;
     device_class->reapply_connection                  = nm_device_ovs_reapply_connection;
 }
diff --git a/src/core/devices/ovs/nm-device-ovs-port.c b/src/core/devices/ovs/nm-device-ovs-port.c
index 6ba52f40..5510e39f 100644
--- a/src/core/devices/ovs/nm-device-ovs-port.c
+++ b/src/core/devices/ovs/nm-device-ovs-port.c
@@ -72,20 +72,42 @@ act_stage3_ip_config(NMDevice *device, int addr_family)
     nm_device_devip_set_state(device, addr_family, NM_DEVICE_IP_STATE_READY, NULL);
 }
 
+typedef struct {
+    NMDevice                  *device;
+    NMDevice                  *port;
+    GCancellable              *cancellable;
+    NMDeviceAttachPortCallback callback;
+    gpointer                   callback_user_data;
+} AttachPortData;
+
 static void
 add_iface_cb(GError *error, gpointer user_data)
 {
-    NMDevice *slave = user_data;
-
-    if (error && !g_error_matches(error, NM_UTILS_ERROR, NM_UTILS_ERROR_CANCELLED_DISPOSING)) {
-        nm_log_warn(LOGD_DEVICE,
-                    "device %s could not be added to a ovs port: %s",
-                    nm_device_get_iface(slave),
-                    error->message);
-        nm_device_state_changed(slave, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_OVSDB_FAILED);
+    AttachPortData       *data = user_data;
+    NMDeviceOvsPort      *self;
+    gs_free_error GError *local = NULL;
+
+    if (g_cancellable_is_cancelled(data->cancellable)) {
+        local = nm_utils_error_new_cancelled(FALSE, NULL);
+        error = local;
+    } else if (error && !nm_utils_error_is_cancelled_or_disposing(error)) {
+        self = NM_DEVICE_OVS_PORT(data->device);
+        _LOGW(LOGD_DEVICE,
+              "device %s could not be added to a ovs port: %s",
+              nm_device_get_iface(data->port),
+              error->message);
+        nm_device_state_changed(data->port,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_OVSDB_FAILED);
     }
 
-    g_object_unref(slave);
+    data->callback(data->device, error, data->callback_user_data);
+
+    g_object_unref(data->device);
+    g_object_unref(data->port);
+    nm_clear_g_cancellable(&data->cancellable);
+
+    nm_g_slice_free(data);
 }
 
 static gboolean
@@ -115,14 +137,21 @@ set_mtu_cb(GError *error, gpointer user_data)
     g_object_unref(self);
 }
 
-static gboolean
-enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure)
+static NMTernary
+attach_port(NMDevice                  *device,
+            NMDevice                  *port,
+            NMConnection              *connection,
+            gboolean                   configure,
+            GCancellable              *cancellable,
+            NMDeviceAttachPortCallback callback,
+            gpointer                   user_data)
 {
     NMDeviceOvsPort    *self      = NM_DEVICE_OVS_PORT(device);
     NMActiveConnection *ac_port   = NULL;
     NMActiveConnection *ac_bridge = NULL;
     NMDevice           *bridge_device;
     NMSettingWired     *s_wired;
+    AttachPortData     *data;
 
     if (!configure)
         return TRUE;
@@ -131,42 +160,49 @@ enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gbool
     ac_bridge = nm_active_connection_get_master(ac_port);
     if (!ac_bridge) {
         _LOGW(LOGD_DEVICE,
-              "can't enslave %s: bridge active-connection not found",
-              nm_device_get_iface(slave));
+              "can't attach %s: bridge active-connection not found",
+              nm_device_get_iface(port));
         return FALSE;
     }
 
     bridge_device = nm_active_connection_get_device(ac_bridge);
     if (!bridge_device) {
-        _LOGW(LOGD_DEVICE, "can't enslave %s: bridge device not found", nm_device_get_iface(slave));
+        _LOGW(LOGD_DEVICE, "can't attach %s: bridge device not found", nm_device_get_iface(port));
         return FALSE;
     }
 
+    data  = g_slice_new(AttachPortData);
+    *data = (AttachPortData){
+        .device             = g_object_ref(device),
+        .port               = g_object_ref(port),
+        .cancellable        = g_object_ref(cancellable),
+        .callback           = callback,
+        .callback_user_data = user_data,
+    };
+
     nm_ovsdb_add_interface(nm_ovsdb_get(),
                            nm_active_connection_get_applied_connection(ac_bridge),
                            nm_device_get_applied_connection(device),
-                           nm_device_get_applied_connection(slave),
+                           nm_device_get_applied_connection(port),
                            bridge_device,
-                           slave,
+                           port,
                            add_iface_cb,
-                           g_object_ref(slave));
+                           data);
 
     /* DPDK ports does not have a link after the devbind, so the MTU must be
      * set on ovsdb after adding the interface. */
-    if (NM_IS_DEVICE_OVS_INTERFACE(slave) && _ovs_interface_is_dpdk(slave)) {
-        s_wired = nm_device_get_applied_setting(slave, NM_TYPE_SETTING_WIRED);
-
-        if (!s_wired || !nm_setting_wired_get_mtu(s_wired))
-            return TRUE;
-
-        nm_ovsdb_set_interface_mtu(nm_ovsdb_get(),
-                                   nm_device_get_ip_iface(slave),
-                                   nm_setting_wired_get_mtu(s_wired),
-                                   set_mtu_cb,
-                                   g_object_ref(slave));
+    if (NM_IS_DEVICE_OVS_INTERFACE(port) && _ovs_interface_is_dpdk(port)) {
+        s_wired = nm_device_get_applied_setting(port, NM_TYPE_SETTING_WIRED);
+        if (s_wired && nm_setting_wired_get_mtu(s_wired)) {
+            nm_ovsdb_set_interface_mtu(nm_ovsdb_get(),
+                                       nm_device_get_ip_iface(port),
+                                       nm_setting_wired_get_mtu(s_wired),
+                                       set_mtu_cb,
+                                       g_object_ref(port));
+        }
     }
 
-    return TRUE;
+    return NM_TERNARY_DEFAULT;
 }
 
 static void
@@ -186,31 +222,31 @@ del_iface_cb(GError *error, gpointer user_data)
 }
 
 static void
-release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
+detach_port(NMDevice *device, NMDevice *port, gboolean configure)
 {
-    NMDeviceOvsPort *self              = NM_DEVICE_OVS_PORT(device);
-    bool             slave_not_managed = !NM_IN_SET(nm_device_sys_iface_state_get(slave),
-                                        NM_DEVICE_SYS_IFACE_STATE_MANAGED,
-                                        NM_DEVICE_SYS_IFACE_STATE_ASSUME);
+    NMDeviceOvsPort *self             = NM_DEVICE_OVS_PORT(device);
+    bool             port_not_managed = !NM_IN_SET(nm_device_sys_iface_state_get(port),
+                                       NM_DEVICE_SYS_IFACE_STATE_MANAGED,
+                                       NM_DEVICE_SYS_IFACE_STATE_ASSUME);
 
-    _LOGI(LOGD_DEVICE, "releasing ovs interface %s", nm_device_get_ip_iface(slave));
+    _LOGI(LOGD_DEVICE, "detaching ovs interface %s", nm_device_get_ip_iface(port));
 
     /* Even if the an interface's device has gone away (e.g. externally
      * removed and thus we're called with configure=FALSE), we still need
      * to make sure its OVSDB entry is gone.
      */
-    if (configure || slave_not_managed) {
+    if (configure || port_not_managed) {
         nm_ovsdb_del_interface(nm_ovsdb_get(),
-                               nm_device_get_iface(slave),
+                               nm_device_get_iface(port),
                                del_iface_cb,
-                               g_object_ref(slave));
+                               g_object_ref(port));
     }
 
     if (configure) {
         /* Open VSwitch is going to delete this one. We must ignore what happens
          * next with the interface. */
-        if (NM_IS_DEVICE_OVS_INTERFACE(slave))
-            nm_device_update_from_platform_link(slave, NULL);
+        if (NM_IS_DEVICE_OVS_INTERFACE(port))
+            nm_device_update_from_platform_link(port, NULL);
     }
 }
 
@@ -245,8 +281,8 @@ nm_device_ovs_port_class_init(NMDeviceOvsPortClass *klass)
     device_class->get_generic_capabilities            = get_generic_capabilities;
     device_class->act_stage3_ip_config                = act_stage3_ip_config;
     device_class->ready_for_ip_config                 = ready_for_ip_config;
-    device_class->enslave_slave                       = enslave_slave;
-    device_class->release_slave                       = release_slave;
+    device_class->attach_port                         = attach_port;
+    device_class->detach_port                         = detach_port;
     device_class->can_reapply_change_ovs_external_ids = TRUE;
     device_class->reapply_connection                  = nm_device_ovs_reapply_connection;
 }
diff --git a/src/core/devices/ovs/nm-ovs-factory.c b/src/core/devices/ovs/nm-ovs-factory.c
index 5aaa5c01..ff2c7858 100644
--- a/src/core/devices/ovs/nm-ovs-factory.c
+++ b/src/core/devices/ovs/nm-ovs-factory.c
@@ -178,7 +178,8 @@ ovsdb_device_removed(NMOvsdb         *ovsdb,
     device_state = nm_device_get_state(device);
 
     if (device_type == NM_DEVICE_TYPE_OVS_INTERFACE && nm_device_get_act_request(device)
-        && device_state < NM_DEVICE_STATE_DEACTIVATING) {
+        && (device_state > NM_DEVICE_STATE_DISCONNECTED
+            && device_state < NM_DEVICE_STATE_DEACTIVATING)) {
         nm_device_state_changed(device,
                                 NM_DEVICE_STATE_DEACTIVATING,
                                 NM_DEVICE_STATE_REASON_REMOVED);
diff --git a/src/core/devices/ovs/nm-ovsdb.c b/src/core/devices/ovs/nm-ovsdb.c
index 44e16cb7..e7c96852 100644
--- a/src/core/devices/ovs/nm-ovsdb.c
+++ b/src/core/devices/ovs/nm-ovsdb.c
@@ -376,6 +376,9 @@ ovsdb_call_method(NMOvsdb                  *self,
     NMOvsdbPrivate  *priv = NM_OVSDB_GET_PRIVATE(self);
     OvsdbMethodCall *call;
 
+    /* FIXME(shutdown): this function should accept a cancellable to
+     * interrupt the operation. */
+
     /* Ensure we're not unsynchronized before we queue the method call. */
     ovsdb_try_connect(self);
 
@@ -1550,7 +1553,7 @@ _external_ids_to_string(const GArray *arr)
     if (!arr)
         return g_strdup("empty");
 
-    nm_str_buf_init(&strbuf, NM_UTILS_GET_NEXT_REALLOC_SIZE_104, FALSE);
+    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);
@@ -1584,7 +1587,7 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
     json_t         *items;
     json_t         *external_ids;
     json_error_t    json_error = {
-        0,
+           0,
     };
     void       *iter;
     const char *name;
@@ -1985,7 +1988,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;
@@ -2127,7 +2130,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);
diff --git a/src/core/devices/team/nm-device-team.c b/src/core/devices/team/nm-device-team.c
index b67c7100..e6d34266 100644
--- a/src/core/devices/team/nm-device-team.c
+++ b/src/core/devices/team/nm-device-team.c
@@ -65,6 +65,50 @@ static gboolean teamd_start(NMDeviceTeam *self);
 
 /*****************************************************************************/
 
+static struct teamdctl *
+_tdc_connect_new(NMDeviceTeam *self, const char *iface, GError **error)
+{
+    NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    struct teamdctl     *tdc;
+    const char          *cli_type;
+    int                  r;
+
+    tdc = teamdctl_alloc();
+    if (!tdc) {
+        nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "failure to allocate teamdctl structure");
+        g_return_val_if_reached(NULL);
+    }
+
+    if (priv->teamd_dbus_watch)
+        cli_type = "dbus";
+    else if (priv->usock_monitor)
+        cli_type = "usock";
+    else
+        cli_type = NULL;
+
+again:
+    r = teamdctl_connect(tdc, iface, NULL, cli_type);
+    if (r != 0) {
+        _LOGD(LOGD_TEAM,
+              "failure to connect to teamdctl%s%s, err=%d",
+              NM_PRINT_FMT_QUOTED2(cli_type, " with cli_type=", cli_type, ""),
+              r);
+        if (cli_type) {
+            /* How odd. Let's retry with any CLI type. */
+            cli_type = NULL;
+            goto again;
+        }
+        teamdctl_free(tdc);
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           "failure to connect to teamd (err=%d)",
+                           r);
+        return NULL;
+    }
+
+    return tdc;
+}
+
 static NMDeviceCapabilities
 get_generic_capabilities(NMDevice *device)
 {
@@ -96,21 +140,16 @@ complete_connection(NMDevice            *device,
 static gboolean
 ensure_teamd_connection(NMDevice *device)
 {
-    NMDeviceTeam        *self = NM_DEVICE_TEAM(device);
-    NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
-    int                  err;
+    NMDeviceTeam         *self  = NM_DEVICE_TEAM(device);
+    NMDeviceTeamPrivate  *priv  = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    gs_free_error GError *error = NULL;
 
     if (priv->tdc)
         return TRUE;
 
-    priv->tdc = teamdctl_alloc();
-    g_assert(priv->tdc);
-    err = teamdctl_connect(priv->tdc, nm_device_get_iface(device), NULL, NULL);
-    if (err != 0) {
-        _LOGE(LOGD_TEAM, "failed to connect to teamd (err=%d)", err);
-        teamdctl_free(priv->tdc);
-        priv->tdc = NULL;
-    }
+    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);
 
     return !!priv->tdc;
 }
@@ -183,42 +222,31 @@ update_connection(NMDevice *device, NMConnection *connection)
 /*****************************************************************************/
 
 static gboolean
-master_update_slave_connection(NMDevice     *self,
+master_update_slave_connection(NMDevice     *device,
                                NMDevice     *slave,
                                NMConnection *connection,
                                GError      **error)
 {
-    NMSettingTeamPort *s_port;
-    char              *port_config = NULL;
-    int                err         = 0;
-    struct teamdctl   *tdc;
-    const char        *team_port_config = NULL;
-    const char        *iface            = nm_device_get_iface(self);
-    const char        *iface_slave      = nm_device_get_iface(slave);
-
-    tdc = teamdctl_alloc();
+    NMDeviceTeam         *self = NM_DEVICE_TEAM(device);
+    NMSettingTeamPort    *s_port;
+    char                 *port_config   = NULL;
+    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);
+
+    tdc = _tdc_connect_new(self, iface, &connect_error);
     if (!tdc) {
         g_set_error(error,
                     NM_DEVICE_ERROR,
                     NM_DEVICE_ERROR_FAILED,
                     "update slave connection for slave '%s' failed to connect to teamd for master "
-                    "%s (out of memory?)",
-                    iface_slave,
-                    iface);
-        g_return_val_if_reached(FALSE);
-    }
-
-    err = teamdctl_connect(tdc, iface, NULL, NULL);
-    if (err) {
-        teamdctl_free(tdc);
-        g_set_error(error,
-                    NM_DEVICE_ERROR,
-                    NM_DEVICE_ERROR_FAILED,
-                    "update slave connection for slave '%s' failed to connect to teamd for master "
-                    "%s (err=%d)",
+                    "%s (%s)",
                     iface_slave,
                     iface,
-                    err);
+                    connect_error->message);
         return FALSE;
     }
 
@@ -790,19 +818,25 @@ deactivate(NMDevice *device)
     teamd_cleanup(self, TRUE);
 }
 
-static gboolean
-enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure)
+static NMTernary
+attach_port(NMDevice                  *device,
+            NMDevice                  *port,
+            NMConnection              *connection,
+            gboolean                   configure,
+            GCancellable              *cancellable,
+            NMDeviceAttachPortCallback callback,
+            gpointer                   user_data)
 {
-    NMDeviceTeam        *self        = NM_DEVICE_TEAM(device);
-    NMDeviceTeamPrivate *priv        = NM_DEVICE_TEAM_GET_PRIVATE(self);
-    gboolean             success     = TRUE;
-    const char          *slave_iface = nm_device_get_ip_iface(slave);
+    NMDeviceTeam        *self       = NM_DEVICE_TEAM(device);
+    NMDeviceTeamPrivate *priv       = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    gboolean             success    = TRUE;
+    const char          *port_iface = nm_device_get_ip_iface(port);
     NMSettingTeamPort   *s_team_port;
 
-    nm_device_master_check_slave_physical_port(device, slave, LOGD_TEAM);
+    nm_device_master_check_slave_physical_port(device, port, LOGD_TEAM);
 
     if (configure) {
-        nm_device_take_down(slave, TRUE);
+        nm_device_take_down(port, TRUE);
 
         s_team_port = nm_connection_get_setting_team_port(connection);
         if (s_team_port) {
@@ -811,19 +845,19 @@ enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gbool
             if (config) {
                 if (!priv->tdc) {
                     _LOGW(LOGD_TEAM,
-                          "enslaved team port %s config not changed, not connected to teamd",
-                          slave_iface);
+                          "attached team port %s config not changed, not connected to teamd",
+                          port_iface);
                 } else {
                     gs_free char *sanitized_config = NULL;
                     int           err;
 
                     sanitized_config = g_strdup(config);
                     g_strdelimit(sanitized_config, "\r\n", ' ');
-                    err = teamdctl_port_config_update_raw(priv->tdc, slave_iface, 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)",
-                              slave_iface,
+                              port_iface,
                               err);
                         return FALSE;
                     }
@@ -832,8 +866,8 @@ enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gbool
         }
         success = nm_platform_link_enslave(nm_device_get_platform(device),
                                            nm_device_get_ip_ifindex(device),
-                                           nm_device_get_ip_ifindex(slave));
-        nm_device_bring_up(slave, TRUE, NULL);
+                                           nm_device_get_ip_ifindex(port));
+        nm_device_bring_up(port, TRUE, NULL);
 
         if (!success)
             return FALSE;
@@ -841,21 +875,21 @@ enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gbool
         nm_clear_g_source(&priv->teamd_read_timeout);
         priv->teamd_read_timeout = g_timeout_add_seconds(5, teamd_read_timeout_cb, self);
 
-        _LOGI(LOGD_TEAM, "enslaved team port %s", slave_iface);
+        _LOGI(LOGD_TEAM, "attached team port %s", port_iface);
     } else
-        _LOGI(LOGD_TEAM, "team port %s was enslaved", slave_iface);
+        _LOGI(LOGD_TEAM, "team port %s was attached", port_iface);
 
     return TRUE;
 }
 
 static void
-release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
+detach_port(NMDevice *device, NMDevice *port, gboolean configure)
 {
     NMDeviceTeam        *self = NM_DEVICE_TEAM(device);
     NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
     gboolean             do_release, success;
     NMSettingTeamPort   *s_port;
-    int                  ifindex_slave;
+    int                  ifindex_port;
     int                  ifindex;
 
     do_release = configure;
@@ -865,39 +899,39 @@ release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
             do_release = FALSE;
     }
 
-    ifindex_slave = nm_device_get_ip_ifindex(slave);
+    ifindex_port = nm_device_get_ip_ifindex(port);
 
-    if (ifindex_slave <= 0) {
-        _LOGD(LOGD_TEAM, "team port %s is already released", nm_device_get_ip_iface(slave));
+    if (ifindex_port <= 0) {
+        _LOGD(LOGD_TEAM, "team port %s is already detached", nm_device_get_ip_iface(port));
     } else if (do_release) {
         success = nm_platform_link_release(nm_device_get_platform(device),
                                            nm_device_get_ip_ifindex(device),
-                                           ifindex_slave);
+                                           ifindex_port);
         if (success)
-            _LOGI(LOGD_TEAM, "released team port %s", nm_device_get_ip_iface(slave));
+            _LOGI(LOGD_TEAM, "detached team port %s", nm_device_get_ip_iface(port));
         else
-            _LOGW(LOGD_TEAM, "failed to release team port %s", nm_device_get_ip_iface(slave));
+            _LOGW(LOGD_TEAM, "failed to detach team port %s", nm_device_get_ip_iface(port));
 
         /* Kernel team code "closes" the port when releasing it, (which clears
          * IFF_UP), so we must bring it back up here to ensure carrier changes and
          * other state is noticed by the now-released port.
          */
-        if (!nm_device_bring_up(slave, TRUE, NULL)) {
+        if (!nm_device_bring_up(port, TRUE, NULL)) {
             _LOGW(LOGD_TEAM,
-                  "released team port %s could not be brought up",
-                  nm_device_get_ip_iface(slave));
+                  "detached team port %s could not be brought up",
+                  nm_device_get_ip_iface(port));
         }
 
         nm_clear_g_source(&priv->teamd_read_timeout);
         priv->teamd_read_timeout = g_timeout_add_seconds(5, teamd_read_timeout_cb, self);
     } else
-        _LOGI(LOGD_TEAM, "team port %s was released", nm_device_get_ip_iface(slave));
+        _LOGI(LOGD_TEAM, "team port %s was detached", nm_device_get_ip_iface(port));
 
     /* Delete any port configuration we previously set */
     if (configure && priv->tdc
-        && (s_port = nm_device_get_applied_setting(slave, NM_TYPE_SETTING_TEAM_PORT))
+        && (s_port = nm_device_get_applied_setting(port, NM_TYPE_SETTING_TEAM_PORT))
         && (nm_setting_team_port_get_config(s_port)))
-        teamdctl_port_config_update_raw(priv->tdc, nm_device_get_ip_iface(slave), "{}");
+        teamdctl_port_config_update_raw(priv->tdc, nm_device_get_ip_iface(port), "{}");
 }
 
 static gboolean
@@ -1064,8 +1098,8 @@ nm_device_team_class_init(NMDeviceTeamClass *klass)
     device_class->act_stage1_prepare                             = act_stage1_prepare;
     device_class->get_configured_mtu = nm_device_get_configured_mtu_for_wired;
     device_class->deactivate         = deactivate;
-    device_class->enslave_slave      = enslave_slave;
-    device_class->release_slave      = release_slave;
+    device_class->attach_port        = attach_port;
+    device_class->detach_port        = detach_port;
 
     obj_properties[PROP_CONFIG] = g_param_spec_string(NM_DEVICE_TEAM_CONFIG,
                                                       "",
diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
index 04608d14..43798b85 100644
--- a/src/core/devices/wifi/nm-device-wifi.c
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -2520,7 +2520,7 @@ supplicant_iface_state(NMDeviceWifi              *self,
                 _LOGD(LOGD_WIFI,
                       "supplicant state settled after roaming, renew dynamic IP configuration");
                 nm_clear_g_source_inst(&priv->roam_supplicant_wait_source);
-                nm_device_update_dynamic_ip_setup(device);
+                nm_device_update_dynamic_ip_setup(device, "roamed to a different AP");
             }
         }
         break;
@@ -2663,7 +2663,7 @@ supplicant_iface_notify_current_bss(NMSupplicantInterface *iface,
 
             if (nm_supplicant_interface_get_state(priv->sup_iface)
                 == NM_SUPPLICANT_INTERFACE_STATE_COMPLETED) {
-                nm_device_update_dynamic_ip_setup(NM_DEVICE(self));
+                nm_device_update_dynamic_ip_setup(NM_DEVICE(self), "roamed to a different AP");
             } else {
                 /* Wait that the authentication to new the AP completes before
                  * trying to renew, otherwise the DHCP REQUEST could be lost
@@ -3180,21 +3180,21 @@ ensure_hotspot_frequency(NMDeviceWifi *self, NMSettingWireless *s_wifi, NMWifiAP
         gsize         ssid_len;
         const guint8 *ssid_data;
         const guint8  random_seed[16] = {0x9a,
-                                        0xdc,
-                                        0x86,
-                                        0x9a,
-                                        0xa8,
-                                        0xa2,
-                                        0x07,
-                                        0x97,
-                                        0xbe,
-                                        0x6d,
-                                        0xe6,
-                                        0x99,
-                                        0x9f,
-                                        0xa8,
-                                        0x09,
-                                        0x2b};
+                                         0xdc,
+                                         0x86,
+                                         0x9a,
+                                         0xa8,
+                                         0xa2,
+                                         0x07,
+                                         0x97,
+                                         0xbe,
+                                         0x6d,
+                                         0xe6,
+                                         0x99,
+                                         0x9f,
+                                         0xa8,
+                                         0x09,
+                                         0x2b};
 
         /* Calculate a stable "random" number based on the SSID. */
         ssid      = nm_setting_wireless_get_ssid(s_wifi);
diff --git a/src/core/devices/wifi/tests/test-devices-wifi.c b/src/core/devices/wifi/tests/test-devices-wifi.c
index 69d365e3..a52696ea 100644
--- a/src/core/devices/wifi/tests/test-devices-wifi.c
+++ b/src/core/devices/wifi/tests/test-devices-wifi.c
@@ -412,8 +412,8 @@ test_ap_wpa_psk_connection_base(const char   *key_mgmt,
     const char   *ssid        = "blahblah";
     const char   *bssid       = "01:02:03:04:05:06";
     const KeyData exp_wifi[]  = {{NM_SETTING_WIRELESS_SSID, ssid, 0},
-                                {NM_SETTING_WIRELESS_MODE, "infrastructure", 0},
-                                {NULL}};
+                                 {NM_SETTING_WIRELESS_MODE, "infrastructure", 0},
+                                 {NULL}};
     const KeyData both_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, key_mgmt, 0},
                                  {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, auth_alg, 0},
                                  {NM_SETTING_WIRELESS_SECURITY_PSK, "asdfasdfasdfasdfasdfafs", 0},
@@ -541,8 +541,8 @@ test_ap_wpa_eap_connection_base(const char *key_mgmt,
     const char   *bssid       = "01:02:03:04:05:06";
     const KeyData src_empty[] = {{NULL}};
     const KeyData src_wsec[]  = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, key_mgmt, 0},
-                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, auth_alg, 0},
-                                {NULL}};
+                                 {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, auth_alg, 0},
+                                 {NULL}};
     gboolean      success;
     GError       *error = NULL;
 
@@ -778,12 +778,12 @@ test_priv_ap_leap_connection_1(gconstpointer add_wifi)
     const char   *bssid         = "01:02:03:04:05:06";
     const char   *leap_username = "Bill Smith";
     const KeyData src_wsec[]    = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0},
-                                {NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME, leap_username, 0},
-                                {NULL}};
+                                   {NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME, leap_username, 0},
+                                   {NULL}};
     const KeyData exp_wsec[]    = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0},
-                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "leap", 0},
-                                {NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME, leap_username, 0},
-                                {NULL}};
+                                   {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "leap", 0},
+                                   {NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME, leap_username, 0},
+                                   {NULL}};
     gboolean      success;
     GError       *error = NULL;
 
@@ -862,15 +862,15 @@ test_priv_ap_dynamic_wep_1(void)
     const char   *ssid         = "blahblah";
     const char   *bssid        = "01:02:03:04:05:06";
     const KeyData src_wsec[]   = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0},
-                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0},
-                                {NULL}};
+                                  {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0},
+                                  {NULL}};
     const KeyData both_8021x[] = {{NM_SETTING_802_1X_EAP, "peap", 0},
                                   {NM_SETTING_802_1X_IDENTITY, "Bill Smith", 0},
                                   {NM_SETTING_802_1X_PHASE2_AUTH, "mschapv2", 0},
                                   {NULL}};
     const KeyData exp_wsec[]   = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0},
-                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0},
-                                {NULL}};
+                                  {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0},
+                                  {NULL}};
     gboolean      success;
     GError       *error = NULL;
 
@@ -917,8 +917,8 @@ test_priv_ap_dynamic_wep_2(void)
                                   {NM_SETTING_802_1X_PHASE2_AUTH, "mschapv2", 0},
                                   {NULL}};
     const KeyData exp_wsec[]   = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0},
-                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0},
-                                {NULL}};
+                                  {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0},
+                                  {NULL}};
     gboolean      success;
     GError       *error = NULL;
 
@@ -1130,8 +1130,8 @@ test_wpa_ap_leap_connection_1(gconstpointer data)
     const char   *bssid         = "01:02:03:04:05:06";
     const char   *leap_username = "Bill Smith";
     const KeyData src_wsec[]    = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0},
-                                {NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME, leap_username, 0},
-                                {NULL}};
+                                   {NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME, leap_username, 0},
+                                   {NULL}};
     gboolean      success;
     GError       *error = NULL;
 
diff --git a/src/core/devices/wwan/nm-modem-ofono.c b/src/core/devices/wwan/nm-modem-ofono.c
index 0db30046..c003880e 100644
--- a/src/core/devices/wwan/nm-modem-ofono.c
+++ b/src/core/devices/wwan/nm-modem-ofono.c
@@ -805,7 +805,7 @@ handle_settings(GVariant *v_dict, gpointer user_data)
         _LOGW("invalid 'Netmask': %s", s ?: "");
         goto out;
     }
-    address.plen = nm_utils_ip4_netmask_to_prefix(address_network);
+    address.plen = _nm_utils_ip4_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);
diff --git a/src/core/dhcp/nm-dhcp-client.c b/src/core/dhcp/nm-dhcp-client.c
index 00a2d207..77cfeecf 100644
--- a/src/core/dhcp/nm-dhcp-client.c
+++ b/src/core/dhcp/nm-dhcp-client.c
@@ -32,6 +32,38 @@
 
 /*****************************************************************************/
 
+/* This is how long we do ACD for each entry and reject new offers for
+ * the same address. Note that the maximum ACD timeout is limited to 30 seconds
+ * (NM_ACD_TIMEOUT_MAX_MSEC).
+ **/
+#define ACD_REGLIST_GRACE_PERIOD_MSEC 300000u
+
+G_STATIC_ASSERT(ACD_REGLIST_GRACE_PERIOD_MSEC > (NM_ACD_TIMEOUT_MAX_MSEC + 1000));
+
+#define ACD_REGLIST_MAX_ENTRIES 30
+
+/* To do ACD for an address (new lease), we will register a NML3ConfigData
+ * with l3cfg. After ACD completes, we still continue having NML3Cfg
+ * watch that address, for ACD_REGLIST_GRACE_PERIOD_MSEC. The reasons are:
+ *
+ * - the caller is supposed to actually configure the address right after
+ *   ACD passed. We would not want to drop the ACD state before the caller
+ *   got a chance to do that.
+ * - when ACD fails, we decline the address and expect the DHCP client
+ *   to present a new lease. We may want to outright reject the address,
+ *   if ACD is bad. Thus, we want to keep running ACD for the address a bit
+ *   longer, so that future requests for the same address can be rejected.
+ *
+ * This data structure is used for tracking the registered ACD address.
+ */
+typedef struct {
+    const NML3ConfigData *l3cd;
+    gint64                expiry_msec;
+    in_addr_t             addr;
+} AcdRegListData;
+
+/*****************************************************************************/
+
 enum {
     SIGNAL_NOTIFY,
     LAST_SIGNAL,
@@ -42,15 +74,48 @@ static guint signals[LAST_SIGNAL] = {0};
 NM_GOBJECT_PROPERTIES_DEFINE(NMDhcpClient, PROP_CONFIG, );
 
 typedef struct _NMDhcpClientPrivate {
-    NMDhcpClientConfig    config;
-    const NML3ConfigData *l3cd;
-    GSource              *no_lease_timeout_source;
-    GSource              *watch_source;
-    GBytes               *effective_client_id;
+    NMDhcpClientConfig config;
+
+    /* This is the "next" data. That is, the one what was received last via
+     * _nm_dhcp_client_notify(), but which is currently pending on ACD. */
+    const NML3ConfigData *l3cd_next;
+
+    /* This is the currently exposed data. It passed ACD (or no ACD was performed),
+     * and is set from l3cd_next. */
+    const NML3ConfigData *l3cd_curr;
+
+    GSource *no_lease_timeout_source;
+    GSource *watch_source;
+    GBytes  *effective_client_id;
 
     union {
         struct {
             struct {
+                NML3CfgCommitTypeHandle *l3cfg_commit_handle;
+                GSource                 *done_source;
+
+                /* When we do ACD for a l3cd lease, we will keep running ACD for
+                 * the grace period ACD_REGLIST_GRACE_PERIOD_MSEC, even if we already
+                 * determined the state. There are two reasons for that:
+                 *
+                 * - after ACD completes we notify the lease to the user, who is supposed
+                 *   to configure the address in NML3Cfg. If we were already removing the
+                 *   ACD state from NML3Cfg, ACD might need to start over. Instead, when
+                 *   the caller tries to configure the address, ACD state is already good.
+                 *
+                 * - if we decline on ACD offer, we may want to keep running and
+                 *   select other offers. Offers for which we just failed ACD (within
+                 *   ACD_REGLIST_GRACE_PERIOD_MSEC) are rejected. See _nm_dhcp_client_accept_offer().
+                 *   For that, we keep monitoring the ACD state for up to ACD_REGLIST_MAX_ENTRIES
+                 *   addresses, to not restart and select the same lease twice in a row.
+                 */
+                GArray  *reglist;
+                GSource *reglist_timeout_source;
+
+                in_addr_t    addr;
+                NMOptionBool state;
+            } acd;
+            struct {
                 GDBusMethodInvocation *invocation;
             } bound;
         } v4;
@@ -77,16 +142,22 @@ G_DEFINE_ABSTRACT_TYPE(NMDhcpClient, nm_dhcp_client, G_TYPE_OBJECT)
 
 /*****************************************************************************/
 
+#define L3CD_ACD_TAG(priv) (&(priv)->v4.acd.addr)
+
 static gboolean _dhcp_client_accept(NMDhcpClient *self, const NML3ConfigData *l3cd, GError **error);
 
-_nm_unused static gboolean _dhcp_client_decline(NMDhcpClient         *self,
-                                                const NML3ConfigData *l3cd,
-                                                const char           *error_message,
-                                                GError              **error);
+static gboolean _dhcp_client_decline(NMDhcpClient         *self,
+                                     const NML3ConfigData *l3cd,
+                                     const char           *error_message,
+                                     GError              **error);
 
 static void
 l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcpClient *self);
 
+static void _acd_reglist_timeout_reschedule(NMDhcpClient *self, gint64 now_msec);
+
+static void _acd_reglist_data_remove(NMDhcpClient *self, guint idx, gboolean do_log);
+
 /*****************************************************************************/
 
 /* we use pid=-1 for invalid PIDs. Ensure that pid_t can hold negative values. */
@@ -157,6 +228,16 @@ nm_dhcp_client_get_effective_client_id(NMDhcpClient *self)
     return priv->effective_client_id;
 }
 
+NML3ConfigData *
+nm_dhcp_client_create_l3cd(NMDhcpClient *self)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    return nm_l3_config_data_new(nm_l3cfg_get_multi_idx(priv->config.l3cfg),
+                                 nm_l3cfg_get_ifindex(priv->config.l3cfg),
+                                 NM_IP_CONFIG_SOURCE_DHCP);
+}
+
 /*****************************************************************************/
 
 void
@@ -200,7 +281,8 @@ l3_cfg_notify_check_connected(NMDhcpClient *self)
     gboolean             do_connect;
 
     do_connect = priv->l3cfg_notify.wait_dhcp_commit | priv->l3cfg_notify.wait_ll_address
-                 | priv->l3cfg_notify.wait_ipv6_dad;
+                 | priv->l3cfg_notify.wait_ipv6_dad
+                 | (NM_IS_IPv4(priv->config.addr_family) && priv->v4.acd.l3cfg_commit_handle);
 
     if (!do_connect) {
         nm_clear_g_signal_handler(priv->config.l3cfg, &priv->l3cfg_notify.id);
@@ -306,6 +388,333 @@ _no_lease_timeout_schedule(NMDhcpClient *self)
 
 /*****************************************************************************/
 
+static void
+_acd_state_reset(NMDhcpClient *self, gboolean forget_addr, gboolean forget_reglist)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    if (!NM_IS_IPv4(priv->config.addr_family))
+        return;
+
+    if (priv->v4.acd.addr != INADDR_ANY) {
+        nm_l3cfg_commit_type_clear(priv->config.l3cfg, &priv->v4.acd.l3cfg_commit_handle);
+        l3_cfg_notify_check_connected(self);
+        nm_clear_g_source_inst(&priv->v4.acd.done_source);
+        if (forget_addr) {
+            priv->v4.acd.addr  = INADDR_ANY;
+            priv->v4.acd.state = NM_OPTION_BOOL_DEFAULT;
+        }
+    } else
+        nm_assert(priv->v4.acd.state == NM_OPTION_BOOL_DEFAULT);
+
+    if (forget_reglist) {
+        guint n;
+
+        while ((n = nm_g_array_len(priv->v4.acd.reglist)) > 0)
+            _acd_reglist_data_remove(self, n - 1, TRUE);
+    }
+
+    nm_assert(!priv->v4.acd.l3cfg_commit_handle);
+    nm_assert(!priv->v4.acd.done_source);
+    nm_assert(!forget_reglist
+              || !nm_l3cfg_remove_config_all(priv->config.l3cfg, L3CD_ACD_TAG(priv)));
+}
+
+static gboolean
+_acd_complete_on_idle_cb(gpointer user_data)
+{
+    NMDhcpClient        *self = user_data;
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    nm_assert(NM_IS_IPv4(priv->config.addr_family));
+    nm_assert(priv->v4.acd.addr != INADDR_ANY);
+    nm_assert(!priv->v4.acd.l3cfg_commit_handle);
+    nm_assert(priv->l3cd_next);
+
+    _acd_state_reset(self, FALSE, FALSE);
+
+    _nm_dhcp_client_notify(self, NM_DHCP_CLIENT_EVENT_TYPE_BOUND, priv->l3cd_next);
+
+    return G_SOURCE_CONTINUE;
+}
+
+#define _acd_reglist_data_get(priv, idx) \
+    nm_g_array_index_p((priv)->v4.acd.reglist, AcdRegListData, (idx))
+
+static guint
+_acd_reglist_data_find(NMDhcpClientPrivate *priv, in_addr_t addr_needle)
+{
+    const guint n = nm_g_array_len(priv->v4.acd.reglist);
+    guint       i;
+
+    nm_assert(addr_needle != INADDR_ANY);
+
+    for (i = 0; i < n; i++) {
+        AcdRegListData *reglist_data = _acd_reglist_data_get(priv, i);
+
+        if (reglist_data->addr == addr_needle)
+            return i;
+    }
+    return G_MAXUINT;
+}
+
+static void
+_acd_reglist_data_remove(NMDhcpClient *self, guint idx, gboolean do_log)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    AcdRegListData      *reglist_data;
+
+    nm_assert(idx < nm_g_array_len(priv->v4.acd.reglist));
+
+    reglist_data = _acd_reglist_data_get(priv, idx);
+
+    if (do_log) {
+        char sbuf_addr[NM_UTILS_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_HASH_OBFUSCATE_PTR(reglist_data->l3cd));
+    }
+
+    if (!nm_l3cfg_remove_config(priv->config.l3cfg, L3CD_ACD_TAG(priv), reglist_data->l3cd))
+        nm_assert_not_reached();
+
+    nm_clear_l3cd(&reglist_data->l3cd);
+
+    nm_l3cfg_commit_on_idle_schedule(priv->config.l3cfg, NM_L3_CFG_COMMIT_TYPE_UPDATE);
+
+    g_array_remove_index(priv->v4.acd.reglist, idx);
+
+    if (priv->v4.acd.reglist->len == 0) {
+        nm_clear_pointer(&priv->v4.acd.reglist, g_array_unref);
+        nm_clear_g_source_inst(&priv->v4.acd.reglist_timeout_source);
+    }
+}
+
+static gboolean
+_acd_reglist_timeout_cb(gpointer user_data)
+{
+    NMDhcpClient        *self = user_data;
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    gint64               now_msec;
+
+    nm_clear_g_source_inst(&priv->v4.acd.reglist_timeout_source);
+
+    now_msec = nm_utils_get_monotonic_timestamp_msec();
+
+    while (nm_g_array_len(priv->v4.acd.reglist) > 0) {
+        AcdRegListData *reglist_data = _acd_reglist_data_get(priv, 0);
+
+        if (reglist_data->expiry_msec > now_msec)
+            break;
+
+        _acd_reglist_data_remove(self, 0, TRUE);
+    }
+
+    _acd_reglist_timeout_reschedule(self, now_msec);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+_acd_reglist_timeout_reschedule(NMDhcpClient *self, gint64 now_msec)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    AcdRegListData      *reglist_data;
+
+    if (nm_g_array_len(priv->v4.acd.reglist) == 0) {
+        nm_assert(!priv->v4.acd.reglist_timeout_source);
+        return;
+    }
+
+    if (priv->v4.acd.reglist_timeout_source) {
+        /* already pending. As we only add new elements with a *later*
+          * expiry, we don't need to ever cancel a pending timer. Worst
+          * case, the timer fires, and there is nothing to do and we
+          * reschedule. */
+        return;
+    }
+
+    now_msec = nm_utils_get_monotonic_timestamp_msec();
+
+    reglist_data = _acd_reglist_data_get(priv, 0);
+
+    nm_assert(reglist_data->expiry_msec > now_msec);
+
+    priv->v4.acd.reglist_timeout_source =
+        nm_g_timeout_add_source(reglist_data->expiry_msec - now_msec,
+                                _acd_reglist_timeout_cb,
+                                self);
+}
+
+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];
+    in_addr_t            addr;
+    gboolean             addr_changed = FALSE;
+    guint                idx;
+    gint64               now_msec;
+
+    if (!NM_IS_IPv4(priv->config.addr_family))
+        goto handle_no_acd;
+
+    if (!priv->l3cd_next)
+        goto handle_no_acd;
+
+    /* an IPv4 lease is always expected to have exactly one address. */
+    nm_assert(nm_l3_config_data_get_num_addresses(priv->l3cd_next, AF_INET) == 1);
+
+    if (priv->config.v4.acd_timeout_msec == 0)
+        goto handle_no_acd;
+
+    addr = NMP_OBJECT_CAST_IP4_ADDRESS(
+               nm_l3_config_data_get_first_obj(priv->l3cd_next, NMP_OBJECT_TYPE_IP4_ADDRESS, NULL))
+               ->address;
+    nm_assert(addr != INADDR_ANY);
+
+    nm_clear_g_source_inst(&priv->v4.acd.done_source);
+
+    if (priv->v4.acd.state != NM_OPTION_BOOL_DEFAULT && priv->v4.acd.addr == addr) {
+        /* the ACD state is already determined. Return right away. */
+        nm_assert(!priv->v4.acd.l3cfg_commit_handle);
+        *out_acd_state = !!priv->v4.acd.state;
+        return;
+    }
+
+    if (priv->v4.acd.addr != addr) {
+        addr_changed      = TRUE;
+        priv->v4.acd.addr = addr;
+    }
+
+    _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),
+          priv->config.v4.acd_timeout_msec,
+          NM_HASH_OBFUSCATE_PTR(priv->l3cd_next));
+
+    priv->v4.acd.state = NM_OPTION_BOOL_DEFAULT;
+
+    if (nm_l3cfg_add_config(priv->config.l3cfg,
+                            L3CD_ACD_TAG(priv),
+                            FALSE,
+                            priv->l3cd_next,
+                            NM_L3CFG_CONFIG_PRIORITY_IPV4LL,
+                            0,
+                            0,
+                            NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP4,
+                            NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP6,
+                            0,
+                            0,
+                            NM_DNS_PRIORITY_DEFAULT_NORMAL,
+                            NM_DNS_PRIORITY_DEFAULT_NORMAL,
+                            NM_L3_ACD_DEFEND_TYPE_ONCE,
+                            NM_MIN(priv->config.v4.acd_timeout_msec, NM_ACD_TIMEOUT_MAX_MSEC),
+                            NM_L3CFG_CONFIG_FLAGS_ONLY_FOR_ACD,
+                            NM_L3_CONFIG_MERGE_FLAGS_NONE))
+        addr_changed = TRUE;
+
+    if (!priv->v4.acd.reglist)
+        priv->v4.acd.reglist = g_array_new(FALSE, FALSE, sizeof(AcdRegListData));
+
+    idx = _acd_reglist_data_find(priv, addr);
+
+    now_msec = nm_utils_get_monotonic_timestamp_msec();
+
+    g_array_append_val(priv->v4.acd.reglist,
+                       ((AcdRegListData){
+                           .l3cd        = nm_l3_config_data_ref(priv->l3cd_next),
+                           .addr        = addr,
+                           .expiry_msec = now_msec + ACD_REGLIST_GRACE_PERIOD_MSEC,
+                       }));
+
+    if (idx != G_MAXUINT) {
+        /* we already tracked this "addr". We don't need to track it twice,
+         * forget about this one. This also has the effect, that we will
+         * always append the new entry to the list (so the list
+         * stays sorted by the increasing timestamp). */
+        _acd_reglist_data_remove(self, idx, FALSE);
+    }
+
+    if (priv->v4.acd.reglist->len > ACD_REGLIST_MAX_ENTRIES) {
+        /* rate limit how many addresses we track for ACD. */
+        _acd_reglist_data_remove(self, 0, TRUE);
+    }
+
+    _acd_reglist_timeout_reschedule(self, now_msec);
+
+    if (!priv->v4.acd.l3cfg_commit_handle) {
+        priv->v4.acd.l3cfg_commit_handle =
+            nm_l3cfg_commit_type_register(priv->config.l3cfg,
+                                          NM_L3_CFG_COMMIT_TYPE_UPDATE,
+                                          NULL,
+                                          "dhcp4-acd");
+        l3_cfg_notify_check_connected(self);
+    }
+
+    if (addr_changed)
+        nm_l3cfg_commit_on_idle_schedule(priv->config.l3cfg, NM_L3_CFG_COMMIT_TYPE_AUTO);
+
+    /* ACD is started/pending... */
+    nm_assert(priv->v4.acd.addr != INADDR_ANY);
+    nm_assert(priv->v4.acd.state == NM_OPTION_BOOL_DEFAULT);
+    nm_assert(priv->v4.acd.l3cfg_commit_handle);
+    nm_assert(priv->l3cfg_notify.id);
+    *out_acd_state = NM_OPTION_BOOL_DEFAULT;
+    return;
+
+handle_no_acd:
+    /* Indicate that ACD is good (or disabled) by returning TRUE. */
+    _acd_state_reset(self, TRUE, FALSE);
+    *out_acd_state = NM_OPTION_BOOL_TRUE;
+    return;
+}
+
+/*****************************************************************************/
+
+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];
+    NMIPAddr               yiaddr;
+    const NML3AcdAddrInfo *acd_info;
+
+    if (!NM_IS_IPv4(priv->config.addr_family))
+        return nm_assert_unreachable_val(FALSE);
+
+    if (priv->config.v4.acd_timeout_msec == 0) {
+        /* ACD is disabled. Note that we might track the address for other
+         * reasons and have information about the ACD state below. But
+         * with ACD disabled, we always ignore that information. */
+        return TRUE;
+    }
+
+    nm_ip_addr_set(priv->config.addr_family, &yiaddr, p_yiaddr);
+
+    /* Note that once we do ACD for a certain address, even after completing
+     * it, we keep the l3cd registered in NML3Cfg for ACD_REGLIST_GRACE_PERIOD_MSEC
+     * The idea is, that we don't yet turn off ACD for a grace period, so that
+     * we can avoid selecting the same lease again.
+     *
+     * Note that we even check whether we have an ACD state if priv->v4.acd.reglist
+     * is empty. Maybe for odd reasons, we track ACD for the address already. */
+
+    acd_info = nm_l3cfg_get_acd_addr_info(priv->config.l3cfg, yiaddr.addr4);
+
+    if (!acd_info)
+        return TRUE;
+
+    if (!NM_IN_SET(acd_info->state, NM_L3_ACD_ADDR_STATE_USED, NM_L3_ACD_ADDR_STATE_CONFLICT))
+        return TRUE;
+
+    _LOGD("offered lease rejected: address %s failed ACD check",
+          _nm_utils_inet4_ntop(yiaddr.addr4, sbuf_addr));
+
+    return FALSE;
+}
+
 void
 _nm_dhcp_client_notify(NMDhcpClient         *self,
                        NMDhcpClientEventType client_event_type,
@@ -313,6 +722,8 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
 {
     NMDhcpClientPrivate                     *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
     GHashTable                              *options;
+    gboolean                                 l3cd_changed;
+    NMOptionBool                             acd_state;
     const int                                IS_IPv4     = NM_IS_IPv4(priv->config.addr_family);
     nm_auto_unref_l3cd const NML3ConfigData *l3cd_merged = NULL;
     char                                     sbuf1[NM_HASH_OBFUSCATE_PTR_STR_BUF_SIZE];
@@ -343,8 +754,7 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
           nm_dhcp_client_event_type_to_string(client_event_type),
           NM_PRINT_FMT_QUOTED2(l3cd, ", l3cd=", NM_HASH_OBFUSCATE_PTR_STR(l3cd, sbuf1), ""));
 
-    if (l3cd)
-        nm_l3_config_data_seal(l3cd);
+    nm_l3_config_data_seal(l3cd);
 
     if (client_event_type >= NM_DHCP_CLIENT_EVENT_TYPE_TIMEOUT)
         watch_cleanup(self);
@@ -353,33 +763,40 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
         /* nm_dhcp_utils_merge_new_dhcp6_lease() relies on "life_starts" option
          * for merging, which is only set by dhclient. Internal client never sets that,
          * but it supports multiple IP addresses per lease. */
-        if (nm_dhcp_utils_merge_new_dhcp6_lease(priv->l3cd, l3cd, &l3cd_merged)) {
+        if (nm_dhcp_utils_merge_new_dhcp6_lease(priv->l3cd_next, l3cd, &l3cd_merged)) {
+            _LOGD("lease merged with existing one");
             l3cd = nm_l3_config_data_seal(l3cd_merged);
         }
     }
 
-    if (priv->l3cd == l3cd)
-        return;
-
     if (l3cd) {
         nm_clear_g_source_inst(&priv->no_lease_timeout_source);
-    } else {
-        if (priv->l3cd)
-            _no_lease_timeout_schedule(self);
-    }
+    } else
+        _no_lease_timeout_schedule(self);
 
-    /* FIXME(l3cfg:dhcp): the API of NMDhcpClient is changing to expose a simpler API.
-     * The internals like the state should not be exposed (or possibly dropped in large
-     * parts). */
+    l3cd_changed = nm_l3_config_data_reset(&priv->l3cd_next, l3cd);
 
-    nm_l3_config_data_reset(&priv->l3cd, l3cd);
+    _acd_check_lease(self, &acd_state);
 
-    options = l3cd ? nm_dhcp_lease_get_options(
-                  nm_l3_config_data_get_dhcp_lease(l3cd, priv->config.addr_family))
-                   : NULL;
+    options = priv->l3cd_next ? nm_dhcp_lease_get_options(
+                  nm_l3_config_data_get_dhcp_lease(priv->l3cd_next, priv->config.addr_family))
+                              : NULL;
+
+    if (_LOGI_ENABLED()) {
+        const char *req_str =
+            IS_IPv4 ? nm_dhcp_option_request_string(AF_INET, NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS)
+                    : nm_dhcp_option_request_string(AF_INET6, NM_DHCP_OPTION_DHCP6_NM_IP_ADDRESS);
+        const char *addr = nm_g_hash_table_lookup(options, req_str);
+
+        _LOGI("state changed %s%s%s%s",
+              priv->l3cd_next ? "new lease" : "no lease",
+              NM_PRINT_FMT_QUOTED2(addr, ", address=", addr, ""),
+              acd_state == NM_OPTION_BOOL_DEFAULT ? ", acd pending"
+                                                  : (acd_state ? "" : ", acd conflict"));
+    }
 
     if (_LOGD_ENABLED()) {
-        if (options) {
+        if (l3cd_changed && options) {
             gs_free const char **keys = NULL;
             guint                nkeys;
             guint                i;
@@ -390,58 +807,41 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
                       keys[i],
                       (char *) g_hash_table_lookup(options, keys[i]));
             }
-
-            if (priv->config.addr_family == AF_INET6) {
-                gs_free char *event_id = NULL;
-
-                event_id = nm_dhcp_utils_get_dhcp6_event_id(options);
-                if (event_id)
-                    _LOGT("event-id: \"%s\"", event_id);
-            }
         }
     }
 
-    if (_LOGI_ENABLED()) {
-        const char *req_str =
-            IS_IPv4 ? nm_dhcp_option_request_string(AF_INET, NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS)
-                    : nm_dhcp_option_request_string(AF_INET6, NM_DHCP_OPTION_DHCP6_NM_IP_ADDRESS);
-        const char *addr = nm_g_hash_table_lookup(options, req_str);
+    if (acd_state == NM_OPTION_BOOL_DEFAULT) {
+        /* ACD is in progress... */
+        return;
+    }
 
-        _LOGI("state changed %s%s%s%s",
-              priv->l3cd ? "new lease" : "no lease",
-              NM_PRINT_FMT_QUOTED(addr, ", address=", addr, "", ""));
+    if (!acd_state) {
+        gs_free_error GError *error = NULL;
+
+        /* We only decline. We don't actually emit to the caller that
+         * something is wrong (like NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD).
+         * If we would, NMDevice might decide to tear down the device, when
+         * we actually should continue trying to get a better lease. There
+         * is already "ipv4.dhcp-timeout" which will handle the failure if
+         * we don't get a good lease. */
+        if (!_dhcp_client_decline(self, priv->l3cd_next, "acd failed", &error))
+            _LOGD("decline failed: %s", error->message);
+        return;
     }
 
-    /* FIXME(l3cfg:dhcp:acd): NMDhcpClient must also do ACD. It needs acd_timeout_msec
-     * as a configuration parameter (in NMDhcpClientConfig). When ACD is enabled,
-     * when a new lease gets announced, it must first use NML3Cfg to run ACD on the
-     * interface (the previous lease -- if any -- will still be used at that point).
-     * If ACD fails, we call _dhcp_client_decline() and try to get a different
-     * lease.
-     * If ACD passes, we need to notify the new lease, and the user (NMDevice) may
-     * then configure the address. We need to watch the configured addresses (in NML3Cfg),
-     * and if the address appears there, we need to accept the lease. That is complicated
-     * but necessary, because we can only accept the lease after we configured the
-     * address.
-     *
-     * As a whole, ACD is transparent for the user (NMDevice). It's entirely managed
-     * by NMDhcpClient. Note that we do ACD through NML3Cfg, which centralizes IP handling
-     * for one interface, so for example if the same address happens to be configured
-     * as a static address (bypassing ACD), then NML3Cfg is aware of that and signals
-     * immediate success. */
-
-    if (client_event_type == NM_DHCP_CLIENT_EVENT_TYPE_BOUND && priv->l3cd
-        && nm_l3_config_data_get_num_addresses(priv->l3cd, priv->config.addr_family) > 0) {
+    nm_l3_config_data_reset(&priv->l3cd_curr, priv->l3cd_next);
+
+    if (client_event_type == NM_DHCP_CLIENT_EVENT_TYPE_BOUND && priv->l3cd_curr
+        && nm_l3_config_data_get_num_addresses(priv->l3cd_curr, priv->config.addr_family) > 0)
         priv->l3cfg_notify.wait_dhcp_commit = TRUE;
-    } else {
+    else
         priv->l3cfg_notify.wait_dhcp_commit = FALSE;
-    }
 
-    if (!priv->l3cfg_notify.wait_dhcp_commit && priv->l3cd) {
+    if (!priv->l3cfg_notify.wait_dhcp_commit && priv->l3cd_curr) {
         gs_free_error GError *error = NULL;
 
         _LOGD("accept lease right away");
-        if (!_dhcp_client_accept(self, priv->l3cd, &error)) {
+        if (!_dhcp_client_accept(self, priv->l3cd_curr, &error)) {
             _LOGD("accept failed: %s", error->message);
             /* Unclear why this happened, or what to do about it. Just proceed. */
         }
@@ -454,7 +854,7 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
             .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
             .lease_update =
                 {
-                    .l3cd     = priv->l3cd,
+                    .l3cd     = priv->l3cd_curr,
                     .accepted = !priv->l3cfg_notify.wait_dhcp_commit,
                 },
         };
@@ -529,7 +929,7 @@ _dhcp_client_accept(NMDhcpClient *self, const NML3ConfigData *l3cd, GError **err
 
     klass = NM_DHCP_CLIENT_GET_CLASS(self);
 
-    g_return_val_if_fail(NM_DHCP_CLIENT_GET_PRIVATE(self)->l3cd, FALSE);
+    g_return_val_if_fail(NM_DHCP_CLIENT_GET_PRIVATE(self)->l3cd_curr, FALSE);
 
     return klass->accept(self, l3cd, error);
 }
@@ -569,7 +969,7 @@ _dhcp_client_decline(NMDhcpClient         *self,
 
     klass = NM_DHCP_CLIENT_GET_CLASS(self);
 
-    g_return_val_if_fail(NM_DHCP_CLIENT_GET_PRIVATE(self)->l3cd, FALSE);
+    g_return_val_if_fail(NM_DHCP_CLIENT_GET_PRIVATE(self)->l3cd_next, FALSE);
 
     return klass->decline(self, l3cd, error_message, error);
 }
@@ -626,7 +1026,9 @@ ipv6_lladdr_find(NMDhcpClient *self)
     nm_assert(!NM_IS_IPv4(priv->config.addr_family));
 
     l3cfg = priv->config.l3cfg;
-    nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP6_ADDRESS, nm_l3cfg_get_ifindex(l3cfg));
+    nmp_lookup_init_object_by_ifindex(&lookup,
+                                      NMP_OBJECT_TYPE_IP6_ADDRESS,
+                                      nm_l3cfg_get_ifindex(l3cfg));
 
     nm_platform_iter_obj_for_each (&iter, nm_l3cfg_get_platform(l3cfg), &lookup, &obj) {
         const NMPlatformIP6Address *pladdr = NMP_OBJECT_CAST_IP6_ADDRESS(obj);
@@ -651,7 +1053,7 @@ ipv6_tentative_addr_find(NMDhcpClient *self)
 
     /* For each address in the lease, check whether it's tentative
      * in platform. */
-    nm_l3_config_data_iter_ip6_address_for_each (&iter, priv->l3cd, &addr) {
+    nm_l3_config_data_iter_ip6_address_for_each (&iter, priv->l3cd_curr, &addr) {
         const NMPlatformIP6Address *pladdr;
         NMPObject                   needle;
 
@@ -676,6 +1078,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];
 
     nm_assert(l3cfg == priv->config.l3cfg);
 
@@ -717,7 +1120,7 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
                 self,
                 &((NMDhcpClientNotifyData){.notify_type  = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
                                            .lease_update = {
-                                               .l3cd     = priv->l3cd,
+                                               .l3cd     = priv->l3cd_curr,
                                                .accepted = TRUE,
                                            }}));
         }
@@ -736,7 +1139,7 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
          * lease and notifying NMDevice. */
 
         nm_l3_config_data_iter_ip_address_for_each (&ipconf_iter,
-                                                    priv->l3cd,
+                                                    priv->l3cd_curr,
                                                     priv->config.addr_family,
                                                     &lease_address)
             break;
@@ -778,9 +1181,11 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
 
         _LOGD("accept lease");
 
-        if (!_dhcp_client_accept(self, priv->l3cd, &error)) {
+        if (!_dhcp_client_accept(self, priv->l3cd_curr, &error)) {
             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,
@@ -794,12 +1199,54 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
                 self,
                 &((NMDhcpClientNotifyData){.notify_type  = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
                                            .lease_update = {
-                                               .l3cd     = priv->l3cd,
+                                               .l3cd     = priv->l3cd_curr,
                                                .accepted = TRUE,
                                            }}));
         }
     }
-wait_dhcp_commit_done:;
+wait_dhcp_commit_done:
+
+    if (notify_data->notify_type == NM_L3_CONFIG_NOTIFY_TYPE_ACD_EVENT
+        && priv->v4.acd.l3cfg_commit_handle) {
+        nm_assert(priv->v4.acd.addr != INADDR_ANY);
+        nm_assert(priv->v4.acd.state == NM_OPTION_BOOL_DEFAULT);
+        nm_assert(!priv->v4.acd.done_source);
+
+        if (priv->v4.acd.addr == notify_data->acd_event.info.addr
+            && nm_l3_acd_addr_info_find_track_info(&notify_data->acd_event.info,
+                                                   L3CD_ACD_TAG(priv),
+                                                   NULL,
+                                                   NULL)) {
+            NMOptionBool acd_state;
+
+            switch (notify_data->acd_event.info.state) {
+            default:
+                nm_assert_not_reached();
+                /* fall-through */
+            case NM_L3_ACD_ADDR_STATE_INIT:
+            case NM_L3_ACD_ADDR_STATE_PROBING:
+                acd_state = NM_OPTION_BOOL_DEFAULT;
+                break;
+            case NM_L3_ACD_ADDR_STATE_USED:
+            case NM_L3_ACD_ADDR_STATE_CONFLICT:
+            case NM_L3_ACD_ADDR_STATE_EXTERNAL_REMOVED:
+                acd_state = NM_OPTION_BOOL_FALSE;
+                break;
+            case NM_L3_ACD_ADDR_STATE_READY:
+            case NM_L3_ACD_ADDR_STATE_DEFENDING:
+                acd_state = NM_OPTION_BOOL_TRUE;
+                break;
+            }
+            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_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);
+            }
+        }
+    }
 }
 
 gboolean
@@ -928,6 +1375,8 @@ nm_dhcp_client_stop(NMDhcpClient *self, gboolean release)
                                               "dhcp stopping");
     }
 
+    _acd_state_reset(self, TRUE, TRUE);
+
     priv->l3cfg_notify.wait_dhcp_commit = FALSE;
     priv->l3cfg_notify.wait_ll_address  = FALSE;
     priv->l3cfg_notify.wait_ipv6_dad    = FALSE;
@@ -942,6 +1391,9 @@ nm_dhcp_client_stop(NMDhcpClient *self, gboolean release)
         _LOGI("canceled DHCP transaction");
     nm_assert(priv->pid == -1);
 
+    nm_clear_l3cd(&priv->l3cd_next);
+    nm_clear_l3cd(&priv->l3cd_curr);
+
     _nm_dhcp_client_notify(self, NM_DHCP_CLIENT_EVENT_TYPE_TERMINATED, NULL);
 }
 
@@ -951,37 +1403,31 @@ static char *
 bytearray_variant_to_string(NMDhcpClient *self, GVariant *value, const char *key)
 {
     const guint8 *array;
+    char         *str;
     gsize         length;
-    GString      *str;
-    int           i;
-    unsigned char c;
-    char         *converted = NULL;
+    gsize         i;
 
-    g_return_val_if_fail(value != NULL, NULL);
+    nm_assert(value);
 
     array = g_variant_get_fixed_array(value, &length, 1);
 
-    /* Since the DHCP options come through environment variables, they should
-     * already be UTF-8 safe, but just make sure.
+    /* Since the DHCP options come originally came as environment variables, they
+     * have not guaranteed encoding. Let's only accept ASCII here.
      */
-    str = g_string_sized_new(length);
+    str = g_malloc(length + 1);
     for (i = 0; i < length; i++) {
-        c = array[i];
+        guint8 c = array[i];
 
-        /* Convert NULLs to spaces and non-ASCII characters to ? */
         if (c == '\0')
-            c = ' ';
+            str[i] = ' ';
         else if (c > 127)
-            c = '?';
-        str = g_string_append_c(str, c);
+            str[i] = '?';
+        else
+            str[i] = (char) c;
     }
-    str = g_string_append_c(str, '\0');
+    str[i] = '\0';
 
-    converted = str->str;
-    if (!g_utf8_validate(converted, -1, NULL))
-        _LOGW("option '%s' couldn't be converted to UTF-8", key);
-    g_string_free(str, FALSE);
-    return converted;
+    return str;
 }
 
 static int
@@ -1004,11 +1450,13 @@ label_is_unknown_xyz(const char *label)
 static void
 maybe_add_option(NMDhcpClient *self, GHashTable *hash, const char *key, GVariant *value)
 {
-    char *str_value = NULL;
+    char *str_value;
+    int   priv_opt_num;
 
-    g_return_if_fail(g_variant_is_of_type(value, G_VARIANT_TYPE_BYTESTRING));
+    if (!g_variant_is_of_type(value, G_VARIANT_TYPE_BYTESTRING))
+        return;
 
-    if (g_str_has_prefix(key, OLD_TAG))
+    if (NM_STR_HAS_PREFIX(key, OLD_TAG))
         return;
 
     /* Filter out stuff that's not actually new DHCP options */
@@ -1021,34 +1469,33 @@ maybe_add_option(NMDhcpClient *self, GHashTable *hash, const char *key, GVariant
         return;
 
     str_value = bytearray_variant_to_string(self, value, key);
-    if (str_value) {
-        int priv_opt_num;
+    if (!str_value)
+        return;
 
-        g_hash_table_insert(hash, g_strdup(key), str_value);
+    g_hash_table_insert(hash, g_strdup(key), str_value);
 
-        /* dhclient has no special labels for private dhcp options: it uses "unknown_xyz"
+    /* dhclient has no special labels for private dhcp options: it uses "unknown_xyz"
          * labels for that. We need to identify those to alias them to our "private_xyz"
          * format unused in the internal dchp plugins.
          */
-        if ((priv_opt_num = label_is_unknown_xyz(key)) > 0) {
-            gs_free guint8 *check_val = NULL;
-            char           *hex_str   = NULL;
-            gsize           len;
+    if ((priv_opt_num = label_is_unknown_xyz(key)) > 0) {
+        gs_free guint8 *check_val = NULL;
+        char           *hex_str   = NULL;
+        gsize           len;
 
-            /* dhclient passes values from dhcp private options in its own "string" format:
+        /* dhclient passes values from dhcp private options in its own "string" format:
              * if the raw values are printable as ascii strings, it will pass the string
              * representation; if the values are not printable as an ascii string, it will
              * pass a string displaying the hex values (hex string). Try to enforce passing
              * always an hex string, converting string representation if needed.
              */
-            check_val = nm_utils_hexstr2bin_alloc(str_value, FALSE, TRUE, ":", 0, &len);
-            hex_str   = nm_utils_bin2hexstr_full(check_val ?: (guint8 *) str_value,
-                                               check_val ? len : strlen(str_value),
-                                               ':',
-                                               FALSE,
-                                               NULL);
-            g_hash_table_insert(hash, g_strdup_printf("private_%d", priv_opt_num), hex_str);
-        }
+        check_val = nm_utils_hexstr2bin_alloc(str_value, FALSE, TRUE, ":", 0, &len);
+        hex_str   = nm_utils_bin2hexstr_full(check_val ?: (guint8 *) str_value,
+                                           check_val ? len : strlen(str_value),
+                                           ':',
+                                           FALSE,
+                                           NULL);
+        g_hash_table_insert(hash, g_strdup_printf("private_%d", priv_opt_num), hex_str);
     }
 }
 
@@ -1079,8 +1526,9 @@ nm_dhcp_client_handle_event(gpointer               unused,
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
     NMDhcpClientEventType                   client_event_type;
     NMPlatformIP6Address                    prefix = {
-        0,
+                           0,
     };
+    int IS_IPv4;
 
     g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE);
     g_return_val_if_fail(iface != NULL, FALSE);
@@ -1091,6 +1539,8 @@ nm_dhcp_client_handle_event(gpointer               unused,
 
     priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
+    g_return_val_if_fail(!priv->is_stopped, FALSE);
+
     if (!nm_streq0(priv->config.iface, iface))
         return FALSE;
     if (priv->pid != pid)
@@ -1175,10 +1625,12 @@ nm_dhcp_client_handle_event(gpointer               unused,
         client_event_type = NM_DHCP_CLIENT_EVENT_TYPE_FAIL;
     }
 
-    if (priv->v4.bound.invocation)
+    IS_IPv4 = NM_IS_IPv4(priv->config.addr_family);
+
+    if (IS_IPv4 && priv->v4.bound.invocation)
         g_dbus_method_invocation_return_value(g_steal_pointer(&priv->v4.bound.invocation), NULL);
 
-    if (NM_IS_IPv4(priv->config.addr_family)
+    if (IS_IPv4
         && NM_IN_SET(client_event_type,
                      NM_DHCP_CLIENT_EVENT_TYPE_BOUND,
                      NM_DHCP_CLIENT_EVENT_TYPE_EXTENDED))
@@ -1202,23 +1654,23 @@ nm_dhcp_client_server_id_is_rejected(NMDhcpClient *self, gconstpointer addr)
     /* IPv6 not implemented yet */
     nm_assert(priv->config.addr_family == AF_INET);
 
-    if (!priv->config.reject_servers || !priv->config.reject_servers[0])
-        return FALSE;
-
-    for (i = 0; priv->config.reject_servers[i]; i++) {
-        in_addr_t r_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))
-            nm_assert_not_reached();
-        mask = _nm_utils_ip4_prefix_to_netmask(r_prefix < 0 ? 32 : r_prefix);
-        if ((addr4 & mask) == (r_addr & mask))
-            return TRUE;
+    if (priv->config.reject_servers) {
+        for (i = 0; priv->config.reject_servers[i]; i++) {
+            in_addr_t r_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))
+                nm_assert_not_reached();
+
+            mask = _nm_utils_ip4_prefix_to_netmask(r_prefix < 0 ? 32 : r_prefix);
+            if ((addr4 & mask) == (r_addr & mask))
+                return TRUE;
+        }
     }
 
     return FALSE;
@@ -1251,7 +1703,7 @@ config_init(NMDhcpClientConfig *config, const NMDhcpClientConfig *src)
     config->hostname        = g_strdup(config->hostname);
     config->mud_url         = g_strdup(config->mud_url);
 
-    config->reject_servers = (const char *const *) nm_strv_dup(config->reject_servers, -1, TRUE);
+    config->reject_servers = nm_strv_dup_packed(config->reject_servers, -1);
 
     if (NM_IS_IPv4(config->addr_family))
         config->v4.last_address = g_strdup(config->v4.last_address);
@@ -1284,7 +1736,7 @@ config_init(NMDhcpClientConfig *config, const NMDhcpClientConfig *src)
         if (!config->send_hostname) {
             nm_clear_g_free((gpointer *) &config->hostname);
         } else if ((config->use_fqdn && !nm_sd_dns_name_is_valid(config->hostname))
-                   || (!config->use_fqdn && !nm_sd_hostname_is_valid(config->hostname, FALSE))) {
+                   || (!config->use_fqdn && !nm_hostname_is_valid(config->hostname, FALSE))) {
             nm_log_warn(LOGD_DHCP,
                         "dhcp%c: %s '%s' is invalid, will be ignored",
                         nm_utils_addr_family_to_char(config->addr_family),
@@ -1310,8 +1762,7 @@ config_clear(NMDhcpClientConfig *config)
     nm_clear_g_free((gpointer *) &config->anycast_address);
     nm_clear_g_free((gpointer *) &config->hostname);
     nm_clear_g_free((gpointer *) &config->mud_url);
-
-    nm_clear_pointer((gpointer *) &config->reject_servers, g_strfreev);
+    nm_clear_g_free((gpointer *) &config->reject_servers);
 
     if (config->addr_family == AF_INET) {
         nm_clear_g_free((gpointer *) &config->v4.last_address);
@@ -1338,6 +1789,13 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps
                     {
                         .invocation = NULL,
                     },
+                .acd =
+                    {
+                        .addr                = INADDR_ANY,
+                        .state               = NM_OPTION_BOOL_DEFAULT,
+                        .l3cfg_commit_handle = NULL,
+                        .done_source         = NULL,
+                    },
             };
         } else {
             priv->v6 = (typeof(priv->v6)){
@@ -1383,6 +1841,11 @@ dispose(GObject *object)
 
     nm_clear_pointer(&priv->effective_client_id, g_bytes_unref);
 
+    nm_assert(!priv->watch_source);
+    nm_assert(!priv->l3cd_next);
+    nm_assert(!priv->l3cd_curr);
+    nm_assert(priv->l3cfg_notify.id == 0);
+
     G_OBJECT_CLASS(nm_dhcp_client_parent_class)->dispose(object);
 }
 
diff --git a/src/core/dhcp/nm-dhcp-client.h b/src/core/dhcp/nm-dhcp-client.h
index e4b99929..51c6bc04 100644
--- a/src/core/dhcp/nm-dhcp-client.h
+++ b/src/core/dhcp/nm-dhcp-client.h
@@ -150,6 +150,10 @@ typedef struct {
             /* The address from the previous lease */
             const char *last_address;
 
+            /* Whether to do ACD for the DHCPv4 address. With timeout zero, ACD
+             * is disabled. */
+            guint acd_timeout_msec;
+
             /* Set BOOTP broadcast flag in request packets, so that servers
              * will always broadcast replies. */
             bool request_broadcast : 1;
@@ -261,6 +265,8 @@ void _nm_dhcp_client_notify(NMDhcpClient         *self,
                             NMDhcpClientEventType client_event_type,
                             const NML3ConfigData *l3cd);
 
+gboolean _nm_dhcp_client_accept_offer(NMDhcpClient *self, gconstpointer p_yiaddr);
+
 gboolean nm_dhcp_client_handle_event(gpointer               unused,
                                      const char            *iface,
                                      int                    pid,
@@ -282,6 +288,8 @@ int                nm_dhcp_client_get_ifindex(NMDhcpClient *self);
 void    nm_dhcp_client_set_effective_client_id(NMDhcpClient *self, GBytes *client_id);
 GBytes *nm_dhcp_client_get_effective_client_id(NMDhcpClient *self);
 
+NML3ConfigData *nm_dhcp_client_create_l3cd(NMDhcpClient *self);
+
 /*****************************************************************************
  * Client data
  *****************************************************************************/
diff --git a/src/core/dhcp/nm-dhcp-helper.c b/src/core/dhcp/nm-dhcp-helper.c
index aab658a2..5a17f4e8 100644
--- a/src/core/dhcp/nm-dhcp-helper.c
+++ b/src/core/dhcp/nm-dhcp-helper.c
@@ -103,14 +103,17 @@ next:;
 int
 main(int argc, char *argv[])
 {
-    gs_unref_object GDBusConnection *connection = NULL;
-    gs_free_error GError            *error      = NULL;
-    gs_unref_variant GVariant       *parameters = NULL;
-    gs_unref_variant GVariant       *result     = NULL;
-    gboolean                         success    = FALSE;
+    gs_unref_object GDBusConnection *connection  = NULL;
+    gs_free_error GError            *error       = NULL;
+    gs_free_error GError            *error_flush = NULL;
+    gs_unref_variant GVariant       *parameters  = NULL;
+    gs_unref_variant GVariant       *result      = NULL;
+    gs_free char                    *s_err       = NULL;
+    gboolean                         success;
     guint                            try_count;
     gint64                           time_start;
     gint64                           time_end;
+    gint64                           remaining_time;
 
     /* Connecting to the unix socket can fail with EAGAIN if there are too
      * many pending connections and the server can't accept them in time
@@ -121,6 +124,8 @@ main(int argc, char *argv[])
     time_end   = time_start + (5000 * 1000L);
     try_count  = 0;
 
+    _LOGi("nm-dhcp-helper: event called");
+
 do_connect:
     try_count++;
     connection =
@@ -131,16 +136,16 @@ do_connect:
                                                &error);
     if (!connection) {
         if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
-            gint64 time_remaining = time_end - g_get_monotonic_time();
-            gint64 interval;
+            remaining_time = time_end - g_get_monotonic_time();
+            if (remaining_time > 0) {
+                gint64 interval;
 
-            if (time_remaining > 0) {
                 _LOGi("failure to connect: %s (retry %u, waited %lld ms)",
                       error->message,
                       try_count,
-                      (long long) (time_end - time_remaining - time_start) / 1000);
+                      (long long) (time_end - remaining_time - time_start) / 1000);
                 interval = NM_CLAMP((gint64) (100L * (1L << NM_MIN(try_count, 31))), 5000, 100000);
-                g_usleep(NM_MIN(interval, time_remaining));
+                g_usleep(NM_MIN(interval, remaining_time));
                 g_clear_error(&error);
                 goto do_connect;
             }
@@ -148,6 +153,7 @@ do_connect:
 
         g_dbus_error_strip_remote_error(error);
         _LOGE("could not connect to NetworkManager D-Bus socket: %s", error->message);
+        success = FALSE;
         goto out;
     }
 
@@ -169,57 +175,74 @@ do_notify:
                                          NULL,
                                          &error);
 
-    if (!result) {
-        gs_free char *s_err = NULL;
+    if (result) {
+        success = TRUE;
+        goto out;
+    }
 
-        s_err = g_dbus_error_get_remote_error(error);
-        if (NM_IN_STRSET(s_err, "org.freedesktop.DBus.Error.UnknownMethod")) {
-            gint64 remaining_time = time_end - g_get_monotonic_time();
-            gint64 interval;
+    s_err = g_dbus_error_get_remote_error(error);
 
-            /* I am not sure that a race can actually happen, as we register the object
-             * on the server side during GDBusServer:new-connection signal.
-             *
-             * However, there was also a race for subscribing to an event, so let's just
-             * do some retry. */
-            if (remaining_time > 0) {
-                _LOGi("failure to call notify: %s (retry %u)", error->message, try_count);
-                interval = NM_CLAMP((gint64) (100L * (1L << NM_MIN(try_count, 31))), 5000, 25000);
-                g_usleep(NM_MIN(interval, remaining_time));
-                g_clear_error(&error);
-                goto do_notify;
-            }
-        }
+    if (NM_IN_STRSET(s_err, "org.freedesktop.NetworkManager.Device.Failed")) {
+        _LOGi("notify failed with reason: %s", error->message);
+        success = FALSE;
+        goto out;
+    }
+
+    if (!NM_IN_STRSET(s_err, "org.freedesktop.DBus.Error.UnknownMethod")) {
+        /* Some unexpected error. We treat that as a failure. In particular,
+         * the daemon will fail the request if ACD fails. This causes nm-dhcp-helper
+         * to fail, which in turn causes dhclient to send a DECLINE. */
         _LOGW("failure to call notify: %s (try signal via Event)", error->message);
+        success = FALSE;
+        goto out;
+    }
+
+    /* I am not sure that a race can actually happen, as we register the object
+     * on the server side during GDBusServer:new-connection signal.
+     *
+     * However, there was also a race for subscribing to an event, so let's just
+     * do some retry. */
+    remaining_time = time_end - g_get_monotonic_time();
+    if (remaining_time > 0) {
+        gint64 interval;
+
+        _LOGi("failure to call notify: %s (retry %u)", error->message, try_count);
+        interval = NM_CLAMP((gint64) (100L * (1L << NM_MIN(try_count, 31))), 5000, 25000);
+        g_usleep(NM_MIN(interval, remaining_time));
         g_clear_error(&error);
+        goto do_notify;
+    }
 
-        /* for backward compatibility, try to emit the signal. There is no stable
-         * API between the dhcp-helper and NetworkManager. However, while upgrading
-         * the NetworkManager package, a newer helper might want to notify an
-         * older server, which still uses the "Event". */
-        if (!g_dbus_connection_emit_signal(connection,
-                                           NULL,
-                                           "/",
-                                           NM_DHCP_CLIENT_DBUS_IFACE,
-                                           "Event",
-                                           parameters,
-                                           &error)) {
-            g_dbus_error_strip_remote_error(error);
-            _LOGE("could not send DHCP Event signal: %s", error->message);
-            goto out;
-        }
+    /* for backward compatibility, try to emit the signal. There is no stable
+     * API between the dhcp-helper and NetworkManager. However, while upgrading
+     * the NetworkManager package, a newer helper might want to notify an
+     * older server, which still uses the "Event". */
+
+    _LOGW("failure to call notify: %s (try signal via Event)", error->message);
+    g_clear_error(&error);
+
+    if (g_dbus_connection_emit_signal(connection,
+                                      NULL,
+                                      "/",
+                                      NM_DHCP_CLIENT_DBUS_IFACE,
+                                      "Event",
+                                      parameters,
+                                      &error)) {
         /* We were able to send the asynchronous Event. Consider that a success. */
         success = TRUE;
-    } else
-        success = TRUE;
-
-    if (!g_dbus_connection_flush_sync(connection, NULL, &error)) {
-        g_dbus_error_strip_remote_error(error);
-        _LOGE("could not flush D-Bus connection: %s", error->message);
-        success = FALSE;
         goto out;
     }
 
+    g_dbus_error_strip_remote_error(error);
+    _LOGE("could not send DHCP Event signal: %s", error->message);
+    success = FALSE;
+
 out:
+    if (!g_dbus_connection_flush_sync(connection, NULL, &error_flush)) {
+        _LOGE("could not flush D-Bus connection: %s", error_flush->message);
+        /* if we considered this a success so far, don't fail because of this. */
+    }
+
+    _LOGi("success: %s", success ? "YES" : "NO");
     return success ? EXIT_SUCCESS : EXIT_FAILURE;
 }
diff --git a/src/core/dhcp/nm-dhcp-manager.c b/src/core/dhcp/nm-dhcp-manager.c
index 9fea1666..cfff23f8 100644
--- a/src/core/dhcp/nm-dhcp-manager.c
+++ b/src/core/dhcp/nm-dhcp-manager.c
@@ -42,6 +42,30 @@ G_DEFINE_TYPE(NMDhcpManager, nm_dhcp_manager, G_TYPE_OBJECT)
 
 /*****************************************************************************/
 
+#undef _NMLOG_ENABLED
+#define _NMLOG_ENABLED(level, addr_family) nm_logging_enabled((level), _LOGD_DHCP(addr_family))
+
+#define _NMLOG(level, addr_family, ...)                                                           \
+    G_STMT_START                                                                                  \
+    {                                                                                             \
+        const int         _addr_family = (addr_family);                                           \
+        const NMLogLevel  _log_level   = (level);                                                 \
+        const NMLogDomain _log_domain  = LOGD_DHCP_af(_addr_family);                              \
+                                                                                                  \
+        if (nm_logging_enabled(_log_level, _log_domain)) {                                        \
+            _nm_log(_log_level,                                                                   \
+                    _log_domain,                                                                  \
+                    0,                                                                            \
+                    NULL,                                                                         \
+                    NULL,                                                                         \
+                    "dhcp%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                                \
+                    nm_utils_addr_family_to_str(_addr_family) _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+        }                                                                                         \
+    }                                                                                             \
+    G_STMT_END
+
+/*****************************************************************************/
+
 /* default to installed helper, but can be modified for testing */
 const char *nm_dhcp_helper_path = LIBEXECDIR "/nm-dhcp-helper";
 
@@ -167,11 +191,10 @@ nm_dhcp_manager_start_client(NMDhcpManager *self, NMDhcpClientConfig *config, GE
 
     gtype = _client_factory_get_gtype(priv->client_factory, config->addr_family);
 
-    nm_log_trace(LOGD_DHCP,
-                 "dhcp%c: creating IPv%c DHCP client of type %s",
-                 nm_utils_addr_family_to_char(config->addr_family),
-                 nm_utils_addr_family_to_char(config->addr_family),
-                 g_type_name(gtype));
+    _LOGT(config->addr_family,
+          "creating IPv%c DHCP client of type %s",
+          nm_utils_addr_family_to_char(config->addr_family),
+          g_type_name(gtype));
 
     client = g_object_new(gtype, NM_DHCP_CLIENT_CONFIG, config, NULL);
 
@@ -244,11 +267,11 @@ nm_dhcp_manager_init(NMDhcpManager *self)
         if (!f)
             continue;
 
-        nm_log_dbg(LOGD_DHCP,
-                   "dhcp-init: enabled DHCP client '%s'%s%s",
-                   f->name,
-                   _client_factory_available(f) ? "" : " (not available)",
-                   f->undocumented ? " (undocumented internal plugin)" : "");
+        _LOGD(AF_UNSPEC,
+              "init: enabled DHCP client '%s'%s%s",
+              f->name,
+              _client_factory_available(f) ? "" : " (not available)",
+              f->undocumented ? " (undocumented internal plugin)" : "");
     }
 
     /* Client-specific setup */
@@ -261,20 +284,20 @@ nm_dhcp_manager_init(NMDhcpManager *self)
     if (client) {
         client_factory = _client_factory_available(_client_factory_find_by_name(client));
         if (!client_factory)
-            nm_log_warn(LOGD_DHCP, "dhcp-init: DHCP client '%s' not available", client);
+            _LOGW(AF_UNSPEC, "init: DHCP client '%s' not available", client);
     }
     if (!client_factory) {
         client_factory = _client_factory_find_by_name("" NM_CONFIG_DEFAULT_MAIN_DHCP);
         if (!client_factory)
-            nm_log_err(LOGD_DHCP,
-                       "dhcp-init: default DHCP client '%s' is not installed",
-                       NM_CONFIG_DEFAULT_MAIN_DHCP);
+            _LOGE(AF_UNSPEC,
+                  "init: default DHCP client '%s' is not installed",
+                  NM_CONFIG_DEFAULT_MAIN_DHCP);
         else {
             client_factory = _client_factory_available(client_factory);
             if (!client_factory)
-                nm_log_info(LOGD_DHCP,
-                            "dhcp-init: default DHCP client '%s' is not available",
-                            NM_CONFIG_DEFAULT_MAIN_DHCP);
+                _LOGI(AF_UNSPEC,
+                      "init: default DHCP client '%s' is not available",
+                      NM_CONFIG_DEFAULT_MAIN_DHCP);
         }
     }
     if (!client_factory) {
@@ -287,7 +310,7 @@ nm_dhcp_manager_init(NMDhcpManager *self)
 
     g_return_if_fail(client_factory);
 
-    nm_log_info(LOGD_DHCP, "dhcp-init: Using DHCP client '%s'", client_factory->name);
+    _LOGI(AF_UNSPEC, "init: Using DHCP client '%s'", client_factory->name);
 
     /* NOTE: currently the DHCP plugin is chosen once at start. It's not
      * possible to reload that configuration. If that ever becomes possible,
diff --git a/src/core/dhcp/nm-dhcp-nettools.c b/src/core/dhcp/nm-dhcp-nettools.c
index 2e41cf15..05b7b52e 100644
--- a/src/core/dhcp/nm-dhcp-nettools.c
+++ b/src/core/dhcp/nm-dhcp-nettools.c
@@ -13,22 +13,24 @@
 #include <ctype.h>
 #include <net/if_arp.h>
 
+#include "n-dhcp4/src/n-dhcp4.h"
+
 #include "libnm-glib-aux/nm-dedup-multi.h"
-#include "libnm-std-aux/unaligned.h"
+#include "libnm-glib-aux/nm-io-utils.h"
 #include "libnm-glib-aux/nm-str-buf.h"
+#include "libnm-std-aux/unaligned.h"
 
-#include "nm-l3-config-data.h"
-#include "nm-utils.h"
-#include "nm-config.h"
-#include "nm-dhcp-utils.h"
-#include "nm-dhcp-options.h"
-#include "nm-core-utils.h"
 #include "NetworkManagerUtils.h"
 #include "libnm-platform/nm-platform.h"
+#include "nm-config.h"
+#include "nm-core-utils.h"
 #include "nm-dhcp-client-logging.h"
-#include "n-dhcp4/src/n-dhcp4.h"
+#include "nm-dhcp-options.h"
+#include "nm-dhcp-utils.h"
+#include "nm-l3-config-data.h"
+#include "nm-utils.h"
+
 #include "libnm-systemd-shared/nm-sd-utils-shared.h"
-#include "libnm-systemd-core/nm-sd-utils-dhcp.h"
 
 /*****************************************************************************/
 
@@ -56,6 +58,8 @@ typedef struct {
         const NML3ConfigData *lease_l3cd;
     } granted;
 
+    GSource *pop_all_events_on_idle_source;
+
     GSource *event_source;
     char    *lease_file;
 } NMDhcpNettoolsPrivate;
@@ -76,6 +80,10 @@ G_DEFINE_TYPE(NMDhcpNettools, nm_dhcp_nettools, NM_TYPE_DHCP_CLIENT)
 
 /*****************************************************************************/
 
+static void dhcp4_event_pop_all_events_on_idle(NMDhcpNettools *self);
+
+/*****************************************************************************/
+
 static void
 set_error_nettools(GError **error, int r, const char *message)
 {
@@ -156,8 +164,10 @@ lease_option_consume_route(const uint8_t **datap,
 /*****************************************************************************/
 
 static gboolean
-lease_parse_address(NDhcp4ClientLease *lease,
+lease_parse_address(NMDhcpNettools    *self /* for logging context only */,
+                    NDhcp4ClientLease *lease,
                     NML3ConfigData    *l3cd,
+                    const char        *iface,
                     GHashTable        *options,
                     in_addr_t         *out_address,
                     GError           **error)
@@ -228,15 +238,33 @@ lease_parse_address(NDhcp4ClientLease *lease,
     }
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_SUBNET_MASK, &l_data, &l_data_len);
-    if (r != 0 || !nm_dhcp_lease_data_parse_in_addr(l_data, l_data_len, &a_netmask)) {
-        nm_utils_error_set_literal(error,
-                                   NM_UTILS_ERROR_UNKNOWN,
-                                   "could not get netmask from lease");
-        return FALSE;
+    if (r == N_DHCP4_E_UNSET) {
+        char str1[NM_UTILS_INET_ADDRSTRLEN];
+        char str2[NM_UTILS_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);
+        _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));
+    } else {
+        if (r != 0
+            || !nm_dhcp_lease_data_parse_in_addr(l_data,
+                                                 l_data_len,
+                                                 &a_netmask,
+                                                 iface,
+                                                 NM_DHCP_OPTION_DHCP4_SUBNET_MASK)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_UNKNOWN,
+                                       "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_utils_ip4_netmask_to_prefix(a_netmask);
-
     nm_dhcp_option_add_option_in_addr(options,
                                       AF_INET,
                                       NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS,
@@ -282,6 +310,7 @@ lease_parse_address(NDhcp4ClientLease *lease,
 static void
 lease_parse_address_list(NDhcp4ClientLease       *lease,
                          NML3ConfigData          *l3cd,
+                         const char              *iface,
                          NMDhcpOptionDhcp4Options option,
                          GHashTable              *options,
                          NMStrBuf                *sbuf)
@@ -294,8 +323,14 @@ lease_parse_address_list(NDhcp4ClientLease       *lease,
     if (r != 0)
         return;
 
-    if (l_data_len == 0 || l_data_len % 4 != 0)
+    if (l_data_len == 0 || l_data_len % 4 != 0) {
+        nm_dhcp_lease_log_invalid_option(iface,
+                                         AF_INET,
+                                         option,
+                                         "wrong option length %lu",
+                                         (unsigned long) l_data_len);
         return;
+    }
 
     nm_str_buf_reset(sbuf);
 
@@ -308,9 +343,14 @@ lease_parse_address_list(NDhcp4ClientLease       *lease,
 
         switch (option) {
         case NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER:
-            if (addr == 0 || nm_ip4_addr_is_localhost(addr)) {
+            if (addr == 0 || nm_utils_ip4_address_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));
                 continue;
             }
             nm_l3_config_data_add_nameserver(l3cd, AF_INET, &addr);
@@ -502,7 +542,10 @@ lease_parse_routes(NDhcp4ClientLease *lease,
 }
 
 static void
-lease_parse_search_domains(NDhcp4ClientLease *lease, NML3ConfigData *l3cd, GHashTable *options)
+lease_parse_search_domains(NDhcp4ClientLease *lease,
+                           NML3ConfigData    *l3cd,
+                           const char        *iface,
+                           GHashTable        *options)
 {
     gs_strfreev char **domains = NULL;
     const guint8      *l_data;
@@ -514,7 +557,11 @@ lease_parse_search_domains(NDhcp4ClientLease *lease, NML3ConfigData *l3cd, GHash
     if (r != 0)
         return;
 
-    domains = nm_dhcp_lease_data_parse_search_list(l_data, l_data_len);
+    domains = nm_dhcp_lease_data_parse_search_list(l_data,
+                                                   l_data_len,
+                                                   iface,
+                                                   AF_INET,
+                                                   NM_DHCP_OPTION_DHCP4_DOMAIN_SEARCH_LIST);
 
     if (!domains || !domains[0])
         return;
@@ -556,12 +603,9 @@ lease_parse_private_options(NDhcp4ClientLease *lease, GHashTable *options)
 }
 
 static NML3ConfigData *
-lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
-                    const char        *iface,
-                    int                ifindex,
-                    NDhcp4ClientLease *lease,
-                    GError           **error)
+lease_to_ip4_config(NMDhcpNettools *self, NDhcp4ClientLease *lease, GError **error)
 {
+    const char                             *iface;
     nm_auto_str_buf NMStrBuf                sbuf    = NM_STR_BUF_INIT(0, FALSE);
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd    = NULL;
     gs_unref_hashtable GHashTable          *options = NULL;
@@ -574,13 +618,15 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
     struct in_addr                          v_inaddr_s;
     int                                     r;
 
-    g_return_val_if_fail(lease != NULL, NULL);
+    nm_assert(lease);
+
+    iface = nm_dhcp_client_get_iface(NM_DHCP_CLIENT(self));
 
-    l3cd = nm_l3_config_data_new(multi_idx, ifindex, NM_IP_CONFIG_SOURCE_DHCP);
+    l3cd = nm_dhcp_client_create_l3cd(NM_DHCP_CLIENT(self));
 
     options = nm_dhcp_option_create_options_dict();
 
-    if (!lease_parse_address(lease, l3cd, options, &lease_address, error))
+    if (!lease_parse_address(self, lease, l3cd, iface, options, &lease_address, error))
         return NULL;
 
     r = n_dhcp4_client_lease_get_server_identifier(lease, &v_inaddr_s);
@@ -592,7 +638,12 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
     }
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_BROADCAST, &l_data, &l_data_len);
-    if (r == 0 && nm_dhcp_lease_data_parse_in_addr(l_data, l_data_len, &v_inaddr)) {
+    if (r == 0
+        && nm_dhcp_lease_data_parse_in_addr(l_data,
+                                            l_data_len,
+                                            &v_inaddr,
+                                            iface,
+                                            NM_DHCP_OPTION_DHCP4_BROADCAST)) {
         nm_dhcp_option_add_option_in_addr(options,
                                           AF_INET,
                                           NM_DHCP_OPTION_DHCP4_BROADCAST,
@@ -601,10 +652,21 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
 
     lease_parse_routes(lease, l3cd, lease_address, options, &sbuf);
 
-    lease_parse_address_list(lease, l3cd, NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER, options, &sbuf);
+    lease_parse_address_list(lease,
+                             l3cd,
+                             iface,
+                             NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER,
+                             options,
+                             &sbuf);
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_DOMAIN_NAME, &l_data, &l_data_len);
-    if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) {
+    if (r == 0
+        && nm_dhcp_lease_data_parse_cstr(l_data,
+                                         l_data_len,
+                                         &l_data_len,
+                                         iface,
+                                         AF_INET,
+                                         NM_DHCP_OPTION_DHCP4_DOMAIN_NAME)) {
         gs_free const char **domains = NULL;
 
         nm_str_buf_reset(&sbuf);
@@ -620,7 +682,10 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
             for (i = 0; domains[i]; i++) {
                 gs_free char *s = NULL;
 
-                s = nm_dhcp_lease_data_parse_domain_validate(domains[i]);
+                s = nm_dhcp_lease_data_parse_domain_validate(domains[i],
+                                                             iface,
+                                                             AF_INET,
+                                                             NM_DHCP_OPTION_DHCP4_DOMAIN_NAME);
                 if (!s)
                     continue;
 
@@ -638,10 +703,16 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
         }
     }
 
-    lease_parse_search_domains(lease, l3cd, options);
+    lease_parse_search_domains(lease, l3cd, iface, options);
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_INTERFACE_MTU, &l_data, &l_data_len);
-    if (r == 0 && nm_dhcp_lease_data_parse_mtu(l_data, l_data_len, &v_u16)) {
+    if (r == 0
+        && nm_dhcp_lease_data_parse_mtu(l_data,
+                                        l_data_len,
+                                        &v_u16,
+                                        iface,
+                                        AF_INET,
+                                        NM_DHCP_OPTION_DHCP4_INTERFACE_MTU)) {
         nm_dhcp_option_add_option_u64(options, AF_INET, NM_DHCP_OPTION_DHCP4_INTERFACE_MTU, v_u16);
         nm_l3_config_data_set_mtu(l3cd, v_u16);
     }
@@ -654,15 +725,26 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
     if (r == 0) {
         gs_free char *s = NULL;
 
-        if (nm_dhcp_lease_data_parse_domain(l_data, l_data_len, &s)) {
+        if (nm_dhcp_lease_data_parse_domain(l_data,
+                                            l_data_len,
+                                            &s,
+                                            iface,
+                                            AF_INET,
+                                            NM_DHCP_OPTION_DHCP4_HOST_NAME)) {
             nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_HOST_NAME, s);
         }
     }
 
-    lease_parse_address_list(lease, l3cd, NM_DHCP_OPTION_DHCP4_NTP_SERVER, options, &sbuf);
+    lease_parse_address_list(lease, l3cd, iface, NM_DHCP_OPTION_DHCP4_NTP_SERVER, options, &sbuf);
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_ROOT_PATH, &l_data, &l_data_len);
-    if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) {
+    if (r == 0
+        && nm_dhcp_lease_data_parse_cstr(l_data,
+                                         l_data_len,
+                                         &l_data_len,
+                                         iface,
+                                         AF_INET,
+                                         NM_DHCP_OPTION_DHCP4_ROOT_PATH)) {
         /* https://tools.ietf.org/html/rfc2132#section-3.19
          *
          *   The path is formatted as a character string consisting of
@@ -684,7 +766,13 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
                             NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY,
                             &l_data,
                             &l_data_len);
-    if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) {
+    if (r == 0
+        && nm_dhcp_lease_data_parse_cstr(l_data,
+                                         l_data_len,
+                                         &l_data_len,
+                                         iface,
+                                         AF_INET,
+                                         NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY)) {
         /* https://tools.ietf.org/html/draft-ietf-wrec-wpad-01#section-4.4.1
          *
          * We reject NUL characters inside the string (except trailing NULs).
@@ -704,7 +792,13 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
     }
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_NIS_DOMAIN, &l_data, &l_data_len);
-    if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) {
+    if (r == 0
+        && nm_dhcp_lease_data_parse_cstr(l_data,
+                                         l_data_len,
+                                         &l_data_len,
+                                         iface,
+                                         AF_INET,
+                                         NM_DHCP_OPTION_DHCP4_NIS_DOMAIN)) {
         gs_free char *to_free = NULL;
 
         /* https://tools.ietf.org/html/rfc2132#section-8.1 */
@@ -730,7 +824,13 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
     }
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_BOOTFILE_NAME, &l_data, &l_data_len);
-    if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) {
+    if (r == 0
+        && nm_dhcp_lease_data_parse_cstr(l_data,
+                                         l_data_len,
+                                         &l_data_len,
+                                         iface,
+                                         AF_INET,
+                                         NM_DHCP_OPTION_DHCP4_BOOTFILE_NAME)) {
         gs_free char *to_free = NULL;
 
         v_str = nm_utils_buf_utf8safe_escape((char *) l_data,
@@ -743,9 +843,14 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
                                   v_str ?: "");
     }
 
-    lease_parse_address_list(lease, l3cd, NM_DHCP_OPTION_DHCP4_NIS_SERVERS, options, &sbuf);
+    lease_parse_address_list(lease, l3cd, iface, NM_DHCP_OPTION_DHCP4_NIS_SERVERS, options, &sbuf);
 
-    lease_parse_address_list(lease, l3cd, NM_DHCP_OPTION_DHCP4_NETBIOS_NAMESERVER, options, &sbuf);
+    lease_parse_address_list(lease,
+                             l3cd,
+                             iface,
+                             NM_DHCP_OPTION_DHCP4_NETBIOS_NAMESERVER,
+                             options,
+                             &sbuf);
 
     lease_parse_private_options(lease, options);
 
@@ -785,9 +890,7 @@ lease_save(NMDhcpNettools *self, NDhcp4ClientLease *lease, const char *lease_fil
 static void
 bound4_handle(NMDhcpNettools *self, guint event, NDhcp4ClientLease *lease)
 {
-    NMDhcpNettoolsPrivate                  *priv   = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
-    NMDhcpClient                           *client = NM_DHCP_CLIENT(self);
-    const NMDhcpClientConfig               *client_config;
+    NMDhcpNettoolsPrivate                  *priv  = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd  = NULL;
     gs_free_error GError                   *error = NULL;
 
@@ -796,17 +899,14 @@ bound4_handle(NMDhcpNettools *self, guint event, NDhcp4ClientLease *lease)
 
     _LOGT("lease available (%s)", (event == N_DHCP4_CLIENT_EVENT_GRANTED) ? "granted" : "extended");
 
-    client_config = nm_dhcp_client_get_config(client);
-    l3cd          = lease_to_ip4_config(nm_dhcp_client_get_multi_idx(client),
-                               client_config->iface,
-                               nm_dhcp_client_get_ifindex(client),
-                               lease,
-                               &error);
+    l3cd = lease_to_ip4_config(self, lease, &error);
     if (!l3cd) {
         _LOGW("failure to parse lease: %s", error->message);
 
-        if (event == N_DHCP4_CLIENT_EVENT_GRANTED)
+        if (event == N_DHCP4_CLIENT_EVENT_GRANTED) {
             n_dhcp4_client_lease_decline(lease, "invalid lease");
+            dhcp4_event_pop_all_events_on_idle(self);
+        }
 
         _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
         return;
@@ -854,7 +954,7 @@ dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
     case N_DHCP4_CLIENT_EVENT_OFFER:
         r = n_dhcp4_client_lease_get_server_identifier(event->offer.lease, &server_id);
         if (r) {
-            _LOGW("selecting lease failed: %d", r);
+            _LOGW("selecting lease failed: could not get DHCP server identifier (%d)", r);
             return;
         }
 
@@ -870,11 +970,19 @@ dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
             return;
         }
 
+        if (!_nm_dhcp_client_accept_offer(NM_DHCP_CLIENT(self), &yiaddr.s_addr)) {
+            /* We don't log about this, the parent class is expected to notify about the reasons. */
+            return;
+        }
+
         _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));
 
         r = n_dhcp4_client_lease_select(event->offer.lease);
+
+        dhcp4_event_pop_all_events_on_idle(self);
+
         if (r) {
             _LOGW("selecting lease failed: %d", r);
             return;
@@ -906,22 +1014,79 @@ dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
     }
 }
 
+static void
+dhcp4_event_pop_all_events(NMDhcpNettools *self)
+{
+    NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
+    NDhcp4ClientEvent     *event;
+
+    while (!n_dhcp4_client_pop_event(priv->client, &event) && event)
+        dhcp4_event_handle(self, event);
+
+    nm_clear_g_source_inst(&priv->pop_all_events_on_idle_source);
+}
+
+static gboolean
+dhcp4_event_pop_all_events_on_idle_cb(gpointer user_data)
+{
+    NMDhcpNettools        *self = user_data;
+    NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
+
+    nm_clear_g_source_inst(&priv->pop_all_events_on_idle_source);
+    dhcp4_event_pop_all_events(self);
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+dhcp4_event_pop_all_events_on_idle(NMDhcpNettools *self)
+{
+    NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
+
+    /* For the most part, NDhcp4Client gets driven from internal, that is
+     * by having events ready on the socket or the timerfd. For those
+     * events, we will poll on the (epoll) FD, then let it be processed
+     * by n_dhcp4_client_dispatch(), and pop the queued events.
+     *
+     * But certain commands (n_dhcp4_client_lease_select(), n_dhcp4_client_lease_accept(),
+     * n_dhcp4_client_lease_decline()) are initiated by the user. And they tend
+     * to log events. Logging is done by queuing a message, but that won't be processed,
+     * unless we pop the event.
+     *
+     * To ensure that those logging events get popped, schedule an idle handler to do that.
+     *
+     * Yes, this means, that the messages only get logged later, when the idle handler
+     * runs. The alternative seems even more problematic, because we don't know
+     * the current call-state, and it seems dangerous to pop unexpected events.
+     * E.g. we call n_dhcp4_client_lease_select() from inside the event-handler,
+     * it seems wrong to call dhcp4_event_pop_all_events() in that context again.
+     *
+     * See-also: https://github.com/nettools/n-dhcp4/issues/34
+     */
+
+    if (!priv->pop_all_events_on_idle_source) {
+        priv->pop_all_events_on_idle_source =
+            nm_g_idle_add_source(dhcp4_event_pop_all_events_on_idle_cb, self);
+    }
+}
+
 static gboolean
 dhcp4_event_cb(int fd, GIOCondition condition, gpointer user_data)
 {
     NMDhcpNettools        *self = user_data;
     NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
-    NDhcp4ClientEvent     *event;
     int                    r;
 
     r = n_dhcp4_client_dispatch(priv->client);
     if (r < 0) {
-        /* FIXME: if any operation (e.g. send()) fails during the
+        /* If any operation (e.g. send()) fails during the
          * dispatch, n-dhcp4 returns an error without arming timers
          * or progressing state, so the only reasonable thing to do
          * is to move to failed state so that the client will be
-         * restarted. Ideally n-dhcp4 should retry failed operations
-         * a predefined number of times (possibly infinite).
+         * restarted.
+         *
+         * That means, n_dhcp4_client_dispatch() must not fail if it can
+         * somehow workaround the problem. A failure is really fatal
+         * and the client needs to be restarted.
          */
         _LOGE("error %d dispatching events", r);
         nm_clear_g_source_inst(&priv->event_source);
@@ -929,8 +1094,7 @@ dhcp4_event_cb(int fd, GIOCondition condition, gpointer user_data)
         return G_SOURCE_REMOVE;
     }
 
-    while (!n_dhcp4_client_pop_event(priv->client, &event) && event)
-        dhcp4_event_handle(self, event);
+    dhcp4_event_pop_all_events(self);
 
     return G_SOURCE_CONTINUE;
 }
@@ -1056,6 +1220,8 @@ _accept(NMDhcpClient *client, const NML3ConfigData *l3cd, GError **error)
     if (!r)
         lease_save(self, priv->granted.lease, priv->lease_file);
 
+    dhcp4_event_pop_all_events_on_idle(self);
+
     nm_clear_pointer(&priv->granted.lease, n_dhcp4_client_lease_unref);
     nm_clear_l3cd(&priv->granted.lease_l3cd);
 
@@ -1091,6 +1257,8 @@ decline(NMDhcpClient *client, const NML3ConfigData *l3cd, const char *error_mess
 
     r = n_dhcp4_client_lease_decline(lease, error_message);
 
+    dhcp4_event_pop_all_events_on_idle(self);
+
     if (r) {
         set_error_nettools(error, r, "failed to decline lease");
         return FALSE;
@@ -1155,18 +1323,20 @@ ip4_start(NMDhcpClient *client, GError **error)
     if (client_config->v4.last_address)
         inet_pton(AF_INET, client_config->v4.last_address, &last_addr);
     else {
-        /*
-         * TODO: we stick to the systemd-networkd lease file format. Quite easy for now to
-         * just use the functions in systemd code. Anyway, as in the end we just use the
-         * ip address from all the options found in the lease, write a function that parses
-         * the lease file just for the assigned address and returns it in &last_address.
-         * Then drop reference to systemd-networkd structures and functions.
-         */
-        nm_auto(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL;
-
-        dhcp_lease_load(&lease, lease_file);
-        if (lease)
-            sd_dhcp_lease_get_address(lease, &last_addr);
+        gs_free char *contents = NULL;
+        gs_free char *s_addr   = NULL;
+
+        nm_utils_file_get_contents(-1,
+                                   lease_file,
+                                   64 * 1024,
+                                   NM_UTILS_FILE_GET_CONTENTS_FLAG_NONE,
+                                   &contents,
+                                   NULL,
+                                   NULL,
+                                   NULL);
+        nm_parse_env_file(contents, "ADDRESS", &s_addr);
+        if (s_addr)
+            nm_utils_parse_inaddr_bin(AF_INET, s_addr, NULL, &last_addr);
     }
 
     if (last_addr.s_addr) {
@@ -1304,6 +1474,7 @@ dispose(GObject *object)
 
     nm_clear_g_free(&priv->lease_file);
     nm_clear_g_source_inst(&priv->event_source);
+    nm_clear_g_source_inst(&priv->pop_all_events_on_idle_source);
     nm_clear_pointer(&priv->granted.lease, n_dhcp4_client_lease_unref);
     nm_clear_l3cd(&priv->granted.lease_l3cd);
     nm_clear_pointer(&priv->probe, n_dhcp4_client_probe_free);
diff --git a/src/core/dhcp/nm-dhcp-systemd.c b/src/core/dhcp/nm-dhcp-systemd.c
index f2dd1823..49e21d97 100644
--- a/src/core/dhcp/nm-dhcp-systemd.c
+++ b/src/core/dhcp/nm-dhcp-systemd.c
@@ -25,7 +25,6 @@
 #include "libnm-platform/nm-platform.h"
 #include "nm-dhcp-client-logging.h"
 #include "libnm-systemd-core/nm-sd.h"
-#include "libnm-systemd-core/nm-sd-utils-dhcp.h"
 
 /*****************************************************************************/
 
@@ -47,7 +46,6 @@ static GType nm_dhcp_systemd_get_type(void);
 /*****************************************************************************/
 
 typedef struct {
-    sd_dhcp_client  *client4;
     sd_dhcp6_client *client6;
     char            *lease_file;
 
@@ -70,683 +68,7 @@ G_DEFINE_TYPE(NMDhcpSystemd, nm_dhcp_systemd, NM_TYPE_DHCP_CLIENT)
 /*****************************************************************************/
 
 static NML3ConfigData *
-lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
-                    const char        *iface,
-                    int                ifindex,
-                    sd_dhcp_lease     *lease,
-                    GError           **error)
-{
-    nm_auto_unref_l3cd_init NML3ConfigData *l3cd    = NULL;
-    gs_unref_hashtable GHashTable          *options = NULL;
-    const struct in_addr                   *addr_list;
-    char                                    addr_str[NM_UTILS_INET_ADDRSTRLEN];
-    const char                             *s;
-    nm_auto_free_gstring GString           *str              = NULL;
-    nm_auto_free sd_dhcp_route            **routes_static    = NULL;
-    nm_auto_free sd_dhcp_route            **routes_classless = NULL;
-    const char *const                      *search_domains   = NULL;
-    guint32                                 default_route_metric_offset;
-    guint16                                 mtu;
-    int                                     i;
-    int                                     num;
-    int                                     is_classless;
-    int                                     n_routes_static;
-    int                                     n_routes_classless;
-    const void                             *data;
-    gsize                                   data_len;
-    gboolean                                has_router_from_classless = FALSE;
-    const gint32                            ts      = nm_utils_get_monotonic_timestamp_sec();
-    gint64                                  ts_time = time(NULL);
-    struct in_addr                          a_address;
-    struct in_addr                          a_netmask;
-    struct in_addr                          a_next_server;
-    struct in_addr                          server_id;
-    struct in_addr                          broadcast;
-    const struct in_addr                   *a_router;
-    guint32                                 a_plen;
-    guint32                                 a_lifetime;
-    guint32                                 renewal;
-    guint32                                 rebinding;
-    gs_free nm_sd_dhcp_option              *private_options = NULL;
-
-    nm_assert(lease != NULL);
-
-    if (sd_dhcp_lease_get_address(lease, &a_address) < 0) {
-        nm_utils_error_set_literal(error,
-                                   NM_UTILS_ERROR_UNKNOWN,
-                                   "could not get address from lease");
-        return NULL;
-    }
-
-    if (sd_dhcp_lease_get_netmask(lease, &a_netmask) < 0) {
-        nm_utils_error_set_literal(error,
-                                   NM_UTILS_ERROR_UNKNOWN,
-                                   "could not get netmask from lease");
-        return NULL;
-    }
-
-    if (sd_dhcp_lease_get_lifetime(lease, &a_lifetime) < 0) {
-        nm_utils_error_set_literal(error,
-                                   NM_UTILS_ERROR_UNKNOWN,
-                                   "could not get lifetime from lease");
-        return NULL;
-    }
-
-    l3cd = nm_l3_config_data_new(multi_idx, ifindex, NM_IP_CONFIG_SOURCE_DHCP);
-
-    options = nm_dhcp_option_create_options_dict();
-
-    _nm_utils_inet4_ntop(a_address.s_addr, addr_str);
-    nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS, addr_str);
-
-    a_plen = nm_utils_ip4_netmask_to_prefix(a_netmask.s_addr);
-    nm_dhcp_option_add_option(options,
-                              AF_INET,
-                              NM_DHCP_OPTION_DHCP4_SUBNET_MASK,
-                              _nm_utils_inet4_ntop(a_netmask.s_addr, addr_str));
-
-    nm_dhcp_option_add_option_u64(options,
-                                  AF_INET,
-                                  NM_DHCP_OPTION_DHCP4_IP_ADDRESS_LEASE_TIME,
-                                  a_lifetime);
-    nm_dhcp_option_add_option_u64(options,
-                                  AF_INET,
-                                  NM_DHCP_OPTION_DHCP4_NM_EXPIRY,
-                                  (guint64) (ts_time + a_lifetime));
-
-    if (sd_dhcp_lease_get_next_server(lease, &a_next_server) == 0) {
-        _nm_utils_inet4_ntop(a_next_server.s_addr, addr_str);
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NM_NEXT_SERVER, addr_str);
-    }
-
-    nm_l3_config_data_add_address_4(l3cd,
-                                    &((const NMPlatformIP4Address){
-                                        .address      = a_address.s_addr,
-                                        .peer_address = a_address.s_addr,
-                                        .plen         = a_plen,
-                                        .addr_source  = NM_IP_CONFIG_SOURCE_DHCP,
-                                        .timestamp    = ts,
-                                        .lifetime     = a_lifetime,
-                                        .preferred    = a_lifetime,
-                                    }));
-
-    if (sd_dhcp_lease_get_server_identifier(lease, &server_id) >= 0) {
-        _nm_utils_inet4_ntop(server_id.s_addr, addr_str);
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_SERVER_ID, addr_str);
-    }
-
-    if (sd_dhcp_lease_get_broadcast(lease, &broadcast) >= 0) {
-        _nm_utils_inet4_ntop(broadcast.s_addr, addr_str);
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_BROADCAST, addr_str);
-    }
-
-    num = sd_dhcp_lease_get_dns(lease, &addr_list);
-    if (num > 0) {
-        nm_gstring_prepare(&str);
-        for (i = 0; i < num; i++) {
-            _nm_utils_inet4_ntop(addr_list[i].s_addr, addr_str);
-            g_string_append(nm_gstring_add_space_delimiter(str), addr_str);
-
-            if (addr_list[i].s_addr == 0 || nm_ip4_addr_is_localhost(addr_list[i].s_addr)) {
-                /* Skip localhost addresses, like also networkd does.
-                 * See https://github.com/systemd/systemd/issues/4524. */
-                continue;
-            }
-            nm_l3_config_data_add_nameserver(l3cd, AF_INET, &addr_list[i].s_addr);
-        }
-        nm_dhcp_option_add_option(options,
-                                  AF_INET,
-                                  NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER,
-                                  str->str);
-    }
-
-    num = sd_dhcp_lease_get_search_domains(lease, (char ***) &search_domains);
-    if (num > 0) {
-        nm_gstring_prepare(&str);
-        for (i = 0; i < num; i++) {
-            g_string_append(nm_gstring_add_space_delimiter(str), search_domains[i]);
-            nm_l3_config_data_add_search(l3cd, AF_INET, search_domains[i]);
-        }
-        nm_dhcp_option_add_option(options,
-                                  AF_INET,
-                                  NM_DHCP_OPTION_DHCP4_DOMAIN_SEARCH_LIST,
-                                  str->str);
-    }
-
-    if (sd_dhcp_lease_get_domainname(lease, &s) >= 0) {
-        gs_strfreev char **domains = NULL;
-        char             **d;
-
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_DOMAIN_NAME, s);
-
-        /* Multiple domains sometimes stuffed into option 15 "Domain Name".
-         * As systemd escapes such characters, split them at \\032. */
-        domains = g_strsplit(s, "\\032", 0);
-        for (d = domains; *d; d++)
-            nm_l3_config_data_add_domain(l3cd, AF_INET, *d);
-    }
-
-    if (sd_dhcp_lease_get_hostname(lease, &s) >= 0) {
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_HOST_NAME, s);
-    }
-
-    default_route_metric_offset = 0;
-    n_routes_static             = sd_dhcp_lease_get_static_routes(lease, &routes_static);
-    n_routes_classless          = sd_dhcp_lease_get_classless_routes(lease, &routes_classless);
-    for (is_classless = 1; is_classless >= 0; is_classless--) {
-        int                   n_routes = (is_classless ? n_routes_classless : n_routes_static);
-        sd_dhcp_route *const *routes   = (is_classless ? routes_classless : routes_static);
-
-        if (n_routes <= 0)
-            continue;
-
-        nm_gstring_prepare(&str);
-
-        for (i = 0; i < n_routes; i++) {
-            char           network_net_str[NM_UTILS_INET_ADDRSTRLEN];
-            char           gateway_str[NM_UTILS_INET_ADDRSTRLEN];
-            guint8         r_plen;
-            struct in_addr r_network;
-            struct in_addr r_gateway;
-            in_addr_t      network_net;
-            guint32        m;
-
-            if (sd_dhcp_route_get_destination(routes[i], &r_network) < 0)
-                continue;
-            if (sd_dhcp_route_get_destination_prefix_length(routes[i], &r_plen) < 0 || r_plen > 32)
-                continue;
-            if (sd_dhcp_route_get_gateway(routes[i], &r_gateway) < 0)
-                continue;
-
-            network_net = nm_utils_ip4_address_clear_host_address(r_network.s_addr, r_plen);
-            _nm_utils_inet4_ntop(network_net, network_net_str);
-            _nm_utils_inet4_ntop(r_gateway.s_addr, gateway_str);
-
-            g_string_append_printf(nm_gstring_add_space_delimiter(str),
-                                   "%s/%d %s",
-                                   network_net_str,
-                                   (int) r_plen,
-                                   gateway_str);
-
-            if (!is_classless && n_routes_classless > 0) {
-                /* RFC 3443: if the DHCP server returns both a Classless Static Routes
-                 * option and a Static Routes option, the DHCP client MUST ignore the
-                 * Static Routes option. */
-                continue;
-            }
-
-            if (r_plen == 0) {
-                if (!is_classless) {
-                    /* for option 33 (static route), RFC 2132 says:
-                     *
-                     * The default route (0.0.0.0) is an illegal destination for a static
-                     * route. */
-                    continue;
-                }
-
-                /* if there are multiple default routes, we add them with differing
-                 * metrics. */
-                m                         = default_route_metric_offset++;
-                has_router_from_classless = TRUE;
-            } else
-                m = 0;
-
-            nm_l3_config_data_add_route_4(l3cd,
-                                          &((const NMPlatformIP4Route){
-                                              .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
-                                              .network       = network_net,
-                                              .plen          = r_plen,
-                                              .gateway       = r_gateway.s_addr,
-                                              .pref_src      = a_address.s_addr,
-                                              .metric_any    = TRUE,
-                                              .metric        = m,
-                                              .table_any     = TRUE,
-                                              .table_coerced = 0,
-                                          }));
-        }
-
-        if (str->len > 0) {
-            nm_dhcp_option_add_option(options,
-                                      AF_INET,
-                                      is_classless ? NM_DHCP_OPTION_DHCP4_CLASSLESS_STATIC_ROUTE
-                                                   : NM_DHCP_OPTION_DHCP4_STATIC_ROUTE,
-                                      str->str);
-        }
-    }
-
-    num = sd_dhcp_lease_get_router(lease, &a_router);
-    if (num > 0) {
-        default_route_metric_offset = 0;
-
-        nm_gstring_prepare(&str);
-        for (i = 0; i < num; i++) {
-            guint32 m;
-
-            s = _nm_utils_inet4_ntop(a_router[i].s_addr, addr_str);
-            g_string_append(nm_gstring_add_space_delimiter(str), s);
-
-            if (a_router[i].s_addr == 0) {
-                /* silently skip 0.0.0.0 */
-                continue;
-            }
-
-            if (has_router_from_classless) {
-                /* If the DHCP server returns both a Classless Static Routes option and a
-                 * Router option, the DHCP client MUST ignore the Router option [RFC 3442].
-                 *
-                 * Be more lenient and ignore the Router option only if Classless Static
-                 * Routes contain a default gateway (as other DHCP backends do).
-                 */
-                continue;
-            }
-
-            /* if there are multiple default routes, we add them with differing
-             * metrics. */
-            m = default_route_metric_offset++;
-
-            nm_l3_config_data_add_route_4(l3cd,
-                                          &((const NMPlatformIP4Route){
-                                              .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
-                                              .gateway       = a_router[i].s_addr,
-                                              .pref_src      = a_address.s_addr,
-                                              .table_any     = TRUE,
-                                              .table_coerced = 0,
-                                              .metric_any    = TRUE,
-                                              .metric        = m,
-                                          }));
-        }
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_ROUTER, str->str);
-    }
-
-    if (sd_dhcp_lease_get_mtu(lease, &mtu) >= 0 && mtu) {
-        nm_dhcp_option_add_option_u64(options, AF_INET, NM_DHCP_OPTION_DHCP4_INTERFACE_MTU, mtu);
-        nm_l3_config_data_set_mtu(l3cd, mtu);
-    }
-
-    num = sd_dhcp_lease_get_ntp(lease, &addr_list);
-    if (num > 0) {
-        nm_gstring_prepare(&str);
-        for (i = 0; i < num; i++) {
-            _nm_utils_inet4_ntop(addr_list[i].s_addr, addr_str);
-            g_string_append(nm_gstring_add_space_delimiter(str), addr_str);
-        }
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NTP_SERVER, str->str);
-    }
-
-    if (sd_dhcp_lease_get_root_path(lease, &s) >= 0) {
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_ROOT_PATH, s);
-    }
-
-    if (sd_dhcp_lease_get_t1(lease, &renewal) >= 0) {
-        nm_dhcp_option_add_option_u64(options,
-                                      AF_INET,
-                                      NM_DHCP_OPTION_DHCP4_RENEWAL_T1_TIME,
-                                      renewal);
-    }
-
-    if (sd_dhcp_lease_get_t2(lease, &rebinding) >= 0) {
-        nm_dhcp_option_add_option_u64(options,
-                                      AF_INET,
-                                      NM_DHCP_OPTION_DHCP4_REBINDING_T2_TIME,
-                                      rebinding);
-    }
-
-    if (sd_dhcp_lease_get_timezone(lease, &s) >= 0) {
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NEW_TZDB_TIMEZONE, s);
-    }
-
-    if (sd_dhcp_lease_get_vendor_specific(lease, &data, &data_len) >= 0) {
-        if (!!memmem(data, data_len, "ANDROID_METERED", NM_STRLEN("ANDROID_METERED")))
-            nm_l3_config_data_set_metered(l3cd, TRUE);
-    }
-
-    num = nm_sd_dhcp_lease_get_private_options(lease, &private_options);
-    if (num > 0) {
-        for (i = 0; i < num; i++) {
-            guint8        code       = private_options[i].code;
-            const guint8 *l_data     = private_options[i].data;
-            gsize         l_data_len = private_options[i].data_len;
-            char         *option_string;
-
-            if (code == NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY) {
-                if (nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) {
-                    gs_free char *to_free = NULL;
-                    const char   *escaped;
-
-                    escaped =
-                        nm_utils_buf_utf8safe_escape((char *) l_data, l_data_len, 0, &to_free);
-                    nm_dhcp_option_add_option(options,
-                                              AF_INET,
-                                              NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY,
-                                              escaped ?: "");
-
-                    nm_l3_config_data_set_proxy_method(l3cd, NM_PROXY_CONFIG_METHOD_AUTO);
-                    nm_l3_config_data_set_proxy_pac_url(l3cd, escaped ?: "");
-                }
-                continue;
-            }
-            if (code == NM_DHCP_OPTION_DHCP4_PRIVATE_CLASSLESS_STATIC_ROUTE) {
-                /* nettools and dhclient parse option 249 (Microsoft Classless Static Route)
-                 * as fallback for routes and ignores them from private options.
-                 *
-                 * The systemd plugin does not, and for consistency with nettools we
-                 * also don't expose it as private option either. */
-                continue;
-            }
-
-            option_string = nm_utils_bin2hexstr_full(l_data, l_data_len, ':', FALSE, NULL);
-            nm_dhcp_option_take_option(options, AF_INET, code, option_string);
-        }
-    }
-
-    nm_dhcp_option_add_requests_to_options(options, AF_INET);
-
-    nm_l3_config_data_set_dhcp_lease_from_options(l3cd, AF_INET, g_steal_pointer(&options));
-
-    return g_steal_pointer(&l3cd);
-}
-
-/*****************************************************************************/
-
-static void
-bound4_handle(NMDhcpSystemd *self, gboolean extended)
-{
-    NMDhcpSystemdPrivate                   *priv  = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
-    const char                             *iface = nm_dhcp_client_get_iface(NM_DHCP_CLIENT(self));
-    nm_auto_unref_l3cd_init NML3ConfigData *l3cd  = NULL;
-    sd_dhcp_lease                          *lease = NULL;
-    GError                                 *error = NULL;
-
-    if (sd_dhcp_client_get_lease(priv->client4, &lease) < 0 || !lease) {
-        _LOGW("no lease!");
-        _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
-        return;
-    }
-
-    _LOGD("lease available");
-
-    l3cd = lease_to_ip4_config(nm_dhcp_client_get_multi_idx(NM_DHCP_CLIENT(self)),
-                               iface,
-                               nm_dhcp_client_get_ifindex(NM_DHCP_CLIENT(self)),
-                               lease,
-                               &error);
-    if (!l3cd) {
-        _LOGW("%s", error->message);
-        g_clear_error(&error);
-        _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
-        return;
-    }
-
-    dhcp_lease_save(lease, priv->lease_file);
-
-    _nm_dhcp_client_notify(NM_DHCP_CLIENT(self),
-                           extended ? NM_DHCP_CLIENT_EVENT_TYPE_EXTENDED
-                                    : NM_DHCP_CLIENT_EVENT_TYPE_BOUND,
-                           l3cd);
-}
-
-static int
-dhcp_event_cb(sd_dhcp_client *client, int event, gpointer user_data)
-{
-    NMDhcpSystemd        *self = NM_DHCP_SYSTEMD(user_data);
-    NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
-    char                  addr_str[INET_ADDRSTRLEN];
-    sd_dhcp_lease        *lease = NULL;
-    struct in_addr        addr;
-    int                   r;
-
-    nm_assert(priv->client4 == client);
-
-    _LOGD("client event %d", event);
-
-    switch (event) {
-    case SD_DHCP_CLIENT_EVENT_EXPIRED:
-        _nm_dhcp_client_notify(NM_DHCP_CLIENT(user_data), NM_DHCP_CLIENT_EVENT_TYPE_EXPIRE, NULL);
-        break;
-    case SD_DHCP_CLIENT_EVENT_STOP:
-        _nm_dhcp_client_notify(NM_DHCP_CLIENT(user_data), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
-        break;
-    case SD_DHCP_CLIENT_EVENT_RENEW:
-    case SD_DHCP_CLIENT_EVENT_IP_CHANGE:
-        bound4_handle(self, TRUE);
-        break;
-    case SD_DHCP_CLIENT_EVENT_IP_ACQUIRE:
-        bound4_handle(self, FALSE);
-        break;
-    case SD_DHCP_CLIENT_EVENT_SELECTING:
-        r = sd_dhcp_client_get_lease(priv->client4, &lease);
-        if (r < 0)
-            return r;
-        r = sd_dhcp_lease_get_server_identifier(lease, &addr);
-        if (r < 0)
-            return r;
-        if (nm_dhcp_client_server_id_is_rejected(NM_DHCP_CLIENT(user_data), &addr)) {
-            _LOGD("server-id %s is in the reject-list, ignoring",
-                  nm_utils_inet_ntop(AF_INET, &addr, addr_str));
-            return -ENOMSG;
-        }
-        break;
-    case SD_DHCP_CLIENT_EVENT_TRANSIENT_FAILURE:
-        break;
-    default:
-        _LOGW("unhandled DHCP event %d", event);
-        break;
-    }
-
-    return 0;
-}
-
-static gboolean
-ip4_start(NMDhcpClient *client, GError **error)
-{
-    nm_auto(sd_dhcp_client_unrefp) sd_dhcp_client *sd_client = NULL;
-    NMDhcpSystemd                                 *self      = NM_DHCP_SYSTEMD(client);
-    NMDhcpSystemdPrivate                          *priv      = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
-    const NMDhcpClientConfig                      *client_config;
-    gs_free char                                  *lease_file = NULL;
-    GBytes                                        *hwaddr;
-    const uint8_t                                 *hwaddr_arr;
-    gsize                                          hwaddr_len;
-    int                                            arp_type;
-    GBytes                                        *client_id;
-    gs_unref_bytes GBytes                         *client_id_new = NULL;
-    GBytes                                        *vendor_class_identifier;
-    const uint8_t                                 *client_id_arr;
-    size_t                                         client_id_len;
-    struct in_addr                                 last_addr = {0};
-    const char                                    *hostname;
-    const char                                    *mud_url;
-    int                                            r, i;
-    GBytes                                        *bcast_hwaddr;
-    const uint8_t                                 *bcast_hwaddr_arr;
-    gsize                                          bcast_hwaddr_len;
-
-    g_return_val_if_fail(!priv->client4, FALSE);
-    g_return_val_if_fail(!priv->client6, FALSE);
-
-    client_config = nm_dhcp_client_get_config(client);
-
-    /* TODO: honor nm_dhcp_client_get_anycast_address() */
-
-    r = sd_dhcp_client_new(&sd_client, FALSE);
-    if (r < 0) {
-        nm_utils_error_set_errno(error, r, "failed to create dhcp-client: %s");
-        return FALSE;
-    }
-
-    _LOGT("dhcp-client4: set " NM_HASH_OBFUSCATE_PTR_FMT, NM_HASH_OBFUSCATE_PTR(sd_client));
-
-    r = sd_dhcp_client_attach_event(sd_client, NULL, 0);
-    if (r < 0) {
-        nm_utils_error_set_errno(error, r, "failed to attach event: %s");
-        return FALSE;
-    }
-
-    hwaddr = client_config->hwaddr;
-    if (!hwaddr || !(hwaddr_arr = g_bytes_get_data(hwaddr, &hwaddr_len))
-        || (arp_type = nm_utils_arp_type_detect_from_hwaddrlen(hwaddr_len)) < 0) {
-        nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "invalid MAC address");
-        return FALSE;
-    }
-
-    bcast_hwaddr_arr = NULL;
-    bcast_hwaddr     = client_config->bcast_hwaddr;
-    if (bcast_hwaddr) {
-        bcast_hwaddr_arr = g_bytes_get_data(bcast_hwaddr, &bcast_hwaddr_len);
-        if (bcast_hwaddr_len != hwaddr_len)
-            bcast_hwaddr_arr = NULL;
-    }
-
-    r = sd_dhcp_client_set_mac(sd_client,
-                               hwaddr_arr,
-                               bcast_hwaddr_arr,
-                               hwaddr_len,
-                               (guint16) arp_type);
-    if (r < 0) {
-        nm_utils_error_set_errno(error, r, "failed to set MAC address: %s");
-        return FALSE;
-    }
-
-    r = sd_dhcp_client_set_ifindex(sd_client, nm_dhcp_client_get_ifindex(client));
-    if (r < 0) {
-        nm_utils_error_set_errno(error, r, "failed to set ifindex: %s");
-        return FALSE;
-    }
-
-    nm_dhcp_utils_get_leasefile_path(AF_INET,
-                                     "internal",
-                                     client_config->iface,
-                                     client_config->uuid,
-                                     &lease_file);
-
-    if (client_config->v4.last_address)
-        inet_pton(AF_INET, client_config->v4.last_address, &last_addr);
-    else {
-        nm_auto(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL;
-
-        dhcp_lease_load(&lease, lease_file);
-        if (lease)
-            sd_dhcp_lease_get_address(lease, &last_addr);
-    }
-
-    r = sd_dhcp_client_set_request_broadcast(sd_client, client_config->v4.request_broadcast);
-    nm_assert(r >= 0);
-
-    if (last_addr.s_addr) {
-        r = sd_dhcp_client_set_request_address(sd_client, &last_addr);
-        if (r < 0) {
-            nm_utils_error_set_errno(error, r, "failed to set last IPv4 address: %s");
-            return FALSE;
-        }
-    }
-
-    client_id = client_config->client_id;
-    if (!client_id) {
-        client_id_new = nm_utils_dhcp_client_id_mac(arp_type, hwaddr_arr, hwaddr_len);
-        client_id     = client_id_new;
-    }
-
-    if (!(client_id_arr = g_bytes_get_data(client_id, &client_id_len)) || client_id_len < 2) {
-        /* invalid client-ids are not expected. */
-        nm_assert_not_reached();
-
-        nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "no valid IPv4 client-id");
-        return FALSE;
-    }
-
-    /* Note that we always set a client-id. In particular for infiniband that is necessary,
-     * see https://tools.ietf.org/html/rfc4390#section-2.1 . */
-    r = sd_dhcp_client_set_client_id(sd_client,
-                                     client_id_arr[0],
-                                     client_id_arr + 1,
-                                     NM_MIN(client_id_len - 1, _NM_MAX_CLIENT_ID_LEN));
-    if (r < 0) {
-        nm_utils_error_set_errno(error, r, "failed to set IPv4 client-id: %s");
-        return FALSE;
-    }
-
-    /* Add requested options */
-    for (i = 0; i < (int) G_N_ELEMENTS(_nm_dhcp_option_dhcp4_options); i++) {
-        if (_nm_dhcp_option_dhcp4_options[i].include) {
-            nm_assert(_nm_dhcp_option_dhcp4_options[i].option_num <= 255);
-            r = sd_dhcp_client_set_request_option(sd_client,
-                                                  _nm_dhcp_option_dhcp4_options[i].option_num);
-            nm_assert(r >= 0 || r == -EEXIST);
-        }
-    }
-
-    hostname = client_config->hostname;
-    if (hostname) {
-        /* FIXME: sd-dhcp decides which hostname/FQDN option to send (12 or 81)
-         * only based on whether the hostname has a domain part or not. At the
-         * moment there is no way to force one or another.
-         */
-        r = sd_dhcp_client_set_hostname(sd_client, hostname);
-        if (r < 0) {
-            nm_utils_error_set_errno(error, r, "failed to set DHCP hostname: %s");
-            return FALSE;
-        }
-    }
-
-    mud_url = client_config->mud_url;
-    if (mud_url) {
-        r = sd_dhcp_client_set_mud_url(sd_client, mud_url);
-        if (r < 0) {
-            nm_utils_error_set_errno(error, r, "failed to set DHCP MUDURL: %s");
-            return FALSE;
-        }
-    }
-
-    vendor_class_identifier = client_config->vendor_class_identifier;
-    if (vendor_class_identifier) {
-        const char *option_data;
-        gsize       len;
-
-        option_data = g_bytes_get_data(vendor_class_identifier, &len);
-        nm_assert(option_data);
-        nm_assert(len <= 255);
-
-        option_data = nm_strndup_a(300, option_data, len, NULL);
-
-        r = sd_dhcp_client_set_vendor_class_identifier(sd_client, option_data);
-        if (r < 0) {
-            nm_utils_error_set_errno(error, r, "failed to set DHCP vendor class identifier: %s");
-            return FALSE;
-        }
-    }
-
-    r = sd_dhcp_client_set_callback(sd_client, dhcp_event_cb, client);
-    if (r < 0) {
-        nm_utils_error_set_errno(error, r, "failed to set callback: %s");
-        return FALSE;
-    }
-
-    priv->client4 = g_steal_pointer(&sd_client);
-
-    g_free(priv->lease_file);
-    priv->lease_file = g_steal_pointer(&lease_file);
-
-    nm_dhcp_client_set_effective_client_id(client, client_id);
-
-    r = sd_dhcp_client_start(priv->client4);
-    if (r < 0) {
-        sd_dhcp_client_set_callback(priv->client4, NULL, NULL);
-        nm_clear_pointer(&priv->client4, sd_dhcp_client_unref);
-        nm_utils_error_set_errno(error, r, "failed to start DHCP client: %s");
-        return FALSE;
-    }
-
-    return TRUE;
-}
-
-static NML3ConfigData *
-lease_to_ip6_config(NMDedupMultiIndex *multi_idx,
-                    const char        *iface,
-                    int                ifindex,
-                    sd_dhcp6_lease    *lease,
-                    gboolean           info_only,
-                    gint32             ts,
-                    GError           **error)
+lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GError **error)
 {
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd    = NULL;
     gs_unref_hashtable GHashTable          *options = NULL;
@@ -762,11 +84,11 @@ lease_to_ip6_config(NMDedupMultiIndex *multi_idx,
 
     nm_assert(lease);
 
-    l3cd = nm_l3_config_data_new(multi_idx, ifindex, NM_IP_CONFIG_SOURCE_DHCP);
+    l3cd = nm_dhcp_client_create_l3cd(NM_DHCP_CLIENT(self));
 
     options = nm_dhcp_option_create_options_dict();
 
-    if (!info_only) {
+    if (!nm_dhcp_client_get_config(NM_DHCP_CLIENT(self))->v6.info_only) {
         gboolean has_any_addresses = FALSE;
         uint32_t lft_pref;
         uint32_t lft_valid;
@@ -864,17 +186,13 @@ lease_to_ip6_config(NMDedupMultiIndex *multi_idx,
 static void
 bound6_handle(NMDhcpSystemd *self)
 {
-    NMDhcpSystemdPrivate                   *priv  = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
-    const gint32                            ts    = nm_utils_get_monotonic_timestamp_sec();
-    const char                             *iface = nm_dhcp_client_get_iface(NM_DHCP_CLIENT(self));
-    const NMDhcpClientConfig               *client_config;
+    NMDhcpSystemdPrivate                   *priv   = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
+    const gint32                            ts     = nm_utils_get_monotonic_timestamp_sec();
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd   = NULL;
     gs_free_error GError                   *error  = NULL;
     NMPlatformIP6Address                    prefix = {0};
     sd_dhcp6_lease                         *lease  = NULL;
 
-    client_config = nm_dhcp_client_get_config(NM_DHCP_CLIENT(self));
-
     if (sd_dhcp6_client_get_lease(priv->client6, &lease) < 0 || !lease) {
         _LOGW(" no lease!");
         _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
@@ -883,13 +201,7 @@ bound6_handle(NMDhcpSystemd *self)
 
     _LOGD("lease available");
 
-    l3cd = lease_to_ip6_config(nm_dhcp_client_get_multi_idx(NM_DHCP_CLIENT(self)),
-                               iface,
-                               nm_dhcp_client_get_ifindex(NM_DHCP_CLIENT(self)),
-                               lease,
-                               client_config->v6.info_only,
-                               ts,
-                               &error);
+    l3cd = lease_to_ip6_config(self, lease, ts, &error);
 
     if (!l3cd) {
         _LOGW("%s", error->message);
@@ -953,7 +265,6 @@ ip6_start(NMDhcpClient *client, const struct in6_addr *ll_addr, GError **error)
     GBytes                                          *duid;
     gboolean                                         prefix_delegation;
 
-    g_return_val_if_fail(!priv->client4, FALSE);
     g_return_val_if_fail(!priv->client6, FALSE);
 
     client_config = nm_dhcp_client_get_config(client);
@@ -1080,18 +391,13 @@ stop(NMDhcpClient *client, gboolean release)
 
     NM_DHCP_CLIENT_CLASS(nm_dhcp_systemd_parent_class)->stop(client, release);
 
-    _LOGT("dhcp-client%d: stop %p",
-          priv->client4 ? '4' : '6',
-          priv->client4 ? (gpointer) priv->client4 : (gpointer) priv->client6);
+    _LOGT("dhcp-client6: stop");
 
-    if (priv->client4) {
-        sd_dhcp_client_set_callback(priv->client4, NULL, NULL);
-        r = sd_dhcp_client_stop(priv->client4);
-    } else if (priv->client6) {
-        sd_dhcp6_client_set_callback(priv->client6, NULL, NULL);
-        r = sd_dhcp6_client_stop(priv->client6);
-    }
+    if (!priv->client6)
+        return;
 
+    sd_dhcp6_client_set_callback(priv->client6, NULL, NULL);
+    r = sd_dhcp6_client_stop(priv->client6);
     if (r)
         _LOGW("failed to stop client (%d)", r);
 }
@@ -1109,12 +415,6 @@ dispose(GObject *object)
 
     nm_clear_g_free(&priv->lease_file);
 
-    if (priv->client4) {
-        sd_dhcp_client_stop(priv->client4);
-        sd_dhcp_client_unref(priv->client4);
-        priv->client4 = NULL;
-    }
-
     if (priv->client6) {
         sd_dhcp6_client_stop(priv->client6);
         sd_dhcp6_client_unref(priv->client6);
@@ -1132,14 +432,13 @@ nm_dhcp_systemd_class_init(NMDhcpSystemdClass *sdhcp_class)
 
     object_class->dispose = dispose;
 
-    client_class->ip4_start = ip4_start;
     client_class->ip6_start = ip6_start;
     client_class->stop      = stop;
 }
 
 const NMDhcpClientFactory _nm_dhcp_client_factory_systemd = {
     .name         = "systemd",
-    .get_type_4   = nm_dhcp_systemd_get_type,
+    .get_type_4   = nm_dhcp_nettools_get_type,
     .get_type_6   = nm_dhcp_systemd_get_type,
     .undocumented = TRUE,
 };
diff --git a/src/core/dhcp/nm-dhcp-utils.c b/src/core/dhcp/nm-dhcp-utils.c
index 88fe83f3..1bffb3c4 100644
--- a/src/core/dhcp/nm-dhcp-utils.c
+++ b/src/core/dhcp/nm-dhcp-utils.c
@@ -14,6 +14,7 @@
 #include "libnm-systemd-shared/nm-sd-utils-shared.h"
 
 #include "nm-dhcp-utils.h"
+#include "nm-dhcp-options.h"
 #include "nm-l3-config-data.h"
 #include "nm-utils.h"
 #include "nm-config.h"
@@ -295,7 +296,7 @@ process_classful_routes(const char     *iface,
         return;
 
     if ((NM_PTRARRAY_LEN(searches) % 2) != 0) {
-        _LOG2I(LOGD_DHCP, iface, "  static routes provided, but invalid");
+        _LOG2I(LOGD_DHCP4, iface, "  static routes provided, but invalid");
         return;
     }
 
@@ -305,11 +306,11 @@ process_classful_routes(const char     *iface,
         guint32            rt_addr, rt_route;
 
         if (inet_pton(AF_INET, *s, &rt_addr) <= 0) {
-            _LOG2W(LOGD_DHCP, iface, "DHCP provided invalid static route address: '%s'", *s);
+            _LOG2W(LOGD_DHCP4, iface, "DHCP provided invalid static route address: '%s'", *s);
             continue;
         }
         if (inet_pton(AF_INET, *(s + 1), &rt_route) <= 0) {
-            _LOG2W(LOGD_DHCP, iface, "DHCP provided invalid static route gateway: '%s'", *(s + 1));
+            _LOG2W(LOGD_DHCP4, iface, "DHCP provided invalid static route gateway: '%s'", *(s + 1));
             continue;
         }
 
@@ -340,7 +341,7 @@ process_classful_routes(const char     *iface,
 
         nm_l3_config_data_add_route_4(l3cd, &route);
 
-        _LOG2I(LOGD_DHCP,
+        _LOG2I(LOGD_DHCP4,
                iface,
                "  static route %s",
                nm_platform_ip4_route_to_string(&route, sbuf, sizeof(sbuf)));
@@ -352,6 +353,7 @@ process_domain_search(int addr_family, const char *iface, const char *str, NML3C
 {
     gs_free const char **searches  = NULL;
     gs_free char        *unescaped = NULL;
+    NMLogDomain          logd      = NM_IS_IPv4(addr_family) ? LOGD_DHCP4 : LOGD_DHCP6;
     const char         **s;
     char                *p;
     int                  i;
@@ -373,13 +375,13 @@ process_domain_search(int addr_family, const char *iface, const char *str, NML3C
     } while (*p++);
 
     if (strchr(unescaped, '\\')) {
-        _LOG2W(LOGD_DHCP, iface, "  invalid domain search: '%s'", unescaped);
+        _LOG2W(logd, iface, "  invalid domain search: '%s'", unescaped);
         return;
     }
 
     searches = nm_strsplit_set(unescaped, " ");
     for (s = searches; searches && *s; s++) {
-        _LOG2I(LOGD_DHCP, iface, "  domain search '%s'", *s);
+        _LOG2I(logd, iface, "  domain search '%s'", *s);
         nm_l3_config_data_add_search(l3cd, addr_family, *s);
     }
 }
@@ -414,18 +416,21 @@ 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))
         return NULL;
+    if (addr == INADDR_ANY)
+        return NULL;
 
     _LOG2I(LOGD_DHCP4, iface, "  address %s", str);
 
     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_utils_ip4_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);
         _LOG2I(LOGD_DHCP4, iface, "  plen %d (default)", plen);
     }
+
     nm_platform_ip4_address_set_addr(&address, addr, plen);
 
     /* Routes: if the server returns classless static routes, we MUST ignore
@@ -588,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;
@@ -822,26 +827,6 @@ nm_dhcp_utils_get_leasefile_path(int         addr_family,
     return FALSE;
 }
 
-char *
-nm_dhcp_utils_get_dhcp6_event_id(GHashTable *lease)
-{
-    const char *start;
-    const char *iaid;
-
-    if (!lease)
-        return NULL;
-
-    iaid = g_hash_table_lookup(lease, "iaid");
-    if (!iaid)
-        return NULL;
-
-    start = g_hash_table_lookup(lease, "life_starts");
-    if (!start)
-        return NULL;
-
-    return g_strdup_printf("%s|%s", iaid, start);
-}
-
 gboolean
 nm_dhcp_utils_merge_new_dhcp6_lease(const NML3ConfigData  *l3cd_old,
                                     const NML3ConfigData  *l3cd_new,
@@ -902,28 +887,75 @@ nm_dhcp_utils_merge_new_dhcp6_lease(const NML3ConfigData  *l3cd_old,
 
 /*****************************************************************************/
 
+void
+nm_dhcp_lease_log_invalid_option(const char *iface,
+                                 int         addr_family,
+                                 guint       option,
+                                 const char *fmt,
+                                 ...)
+{
+    const char   *option_name;
+    gs_free char *msg = NULL;
+    va_list       ap;
+
+    option_name = nm_dhcp_option_request_string(addr_family, option);
+
+    va_start(ap, fmt);
+    msg = g_strdup_vprintf(fmt, ap);
+    va_end(ap);
+
+    _LOG2I(NM_IS_IPv4(addr_family) ? LOGD_DHCP4 : LOGD_DHCP6,
+           iface,
+           "error parsing DHCP option %d (%s)%s%s",
+           option,
+           option_name,
+           msg ? ": " : "",
+           msg ?: "");
+}
+
 gboolean
-nm_dhcp_lease_data_parse_u16(const guint8 *data, gsize n_data, uint16_t *out_val)
+nm_dhcp_lease_data_parse_u16(const guint8 *data,
+                             gsize         n_data,
+                             uint16_t     *out_val,
+                             const char   *iface,
+                             int           addr_family,
+                             guint         option)
 {
-    if (n_data != 2)
+    if (n_data != 2) {
+        nm_dhcp_lease_log_invalid_option(iface,
+                                         addr_family,
+                                         option,
+                                         "invalid option length %lu",
+                                         (unsigned long) n_data);
         return FALSE;
+    }
 
     *out_val = unaligned_read_be16(data);
     return TRUE;
 }
 
 gboolean
-nm_dhcp_lease_data_parse_mtu(const guint8 *data, gsize n_data, uint16_t *out_val)
+nm_dhcp_lease_data_parse_mtu(const guint8 *data,
+                             gsize         n_data,
+                             uint16_t     *out_val,
+                             const char   *iface,
+                             int           addr_family,
+                             guint         option)
 {
     uint16_t mtu;
 
-    if (!nm_dhcp_lease_data_parse_u16(data, n_data, &mtu))
+    if (!nm_dhcp_lease_data_parse_u16(data, n_data, &mtu, iface, addr_family, option))
         return FALSE;
 
     if (mtu < 68) {
         /* https://tools.ietf.org/html/rfc2132#section-5.1:
          *
          * The minimum legal value for the MTU is 68. */
+        nm_dhcp_lease_log_invalid_option(iface,
+                                         addr_family,
+                                         option,
+                                         "value %u is smaller than 68",
+                                         mtu);
         return FALSE;
     }
 
@@ -932,7 +964,12 @@ nm_dhcp_lease_data_parse_mtu(const guint8 *data, gsize n_data, uint16_t *out_val
 }
 
 gboolean
-nm_dhcp_lease_data_parse_cstr(const guint8 *data, gsize n_data, gsize *out_new_len)
+nm_dhcp_lease_data_parse_cstr(const guint8 *data,
+                              gsize         n_data,
+                              gsize        *out_new_len,
+                              const char   *iface,
+                              int           addr_family,
+                              guint         option)
 {
     /* WARNING: this function only validates that the string does not contain
      * NUL characters (and ignores trailing NULs). It does not check character
@@ -947,6 +984,10 @@ nm_dhcp_lease_data_parse_cstr(const guint8 *data, gsize n_data, gsize *out_new_l
              *
              * https://tools.ietf.org/html/rfc2132#section-2
              * https://github.com/systemd/systemd/issues/1337 */
+            nm_dhcp_lease_log_invalid_option(iface,
+                                             addr_family,
+                                             option,
+                                             "string contains embedded NUL");
             return FALSE;
         }
     }
@@ -956,32 +997,47 @@ nm_dhcp_lease_data_parse_cstr(const guint8 *data, gsize n_data, gsize *out_new_l
 }
 
 char *
-nm_dhcp_lease_data_parse_domain_validate(const char *str)
+nm_dhcp_lease_data_parse_domain_validate(const char *str,
+                                         const char *iface,
+                                         int         addr_family,
+                                         guint       option)
 {
     gs_free char *s = NULL;
 
     s = nm_sd_dns_name_normalize(str);
     if (!s)
-        return NULL;
+        goto err;
 
     if (nm_str_is_empty(s) || (s[0] == '.' && s[1] == '\0')) {
         /* root domains are not allowed. */
-        return NULL;
+        goto err;
     }
 
     if (nm_utils_is_localhost(s))
-        return NULL;
+        goto err;
 
     if (!g_utf8_validate(s, -1, NULL)) {
         /* the result must be valid UTF-8. */
-        return NULL;
+        goto err;
     }
 
     return g_steal_pointer(&s);
+err:
+    nm_dhcp_lease_log_invalid_option(iface,
+                                     addr_family,
+                                     option,
+                                     "'%s' is not a valid DNS domain",
+                                     str);
+    return NULL;
 }
 
 gboolean
-nm_dhcp_lease_data_parse_domain(const guint8 *data, gsize n_data, char **out_val)
+nm_dhcp_lease_data_parse_domain(const guint8 *data,
+                                gsize         n_data,
+                                char        **out_val,
+                                const char   *iface,
+                                int           addr_family,
+                                guint         option)
 {
     gs_free char *str1_free = NULL;
     const char   *str1;
@@ -989,7 +1045,7 @@ nm_dhcp_lease_data_parse_domain(const guint8 *data, gsize n_data, char **out_val
 
     /* this is mostly the same as systemd's lease_parse_domain(). */
 
-    if (!nm_dhcp_lease_data_parse_cstr(data, n_data, &n_data))
+    if (!nm_dhcp_lease_data_parse_cstr(data, n_data, &n_data, iface, addr_family, option))
         return FALSE;
 
     if (n_data == 0) {
@@ -1001,12 +1057,13 @@ nm_dhcp_lease_data_parse_domain(const guint8 *data, gsize n_data, char **out_val
          *
          * Note that this is *after* we potentially stripped trailing NULs.
          */
+        nm_dhcp_lease_log_invalid_option(iface, addr_family, option, "empty value");
         return FALSE;
     }
 
     str1 = nm_strndup_a(300, (char *) data, n_data, &str1_free);
 
-    s = nm_dhcp_lease_data_parse_domain_validate(str1);
+    s = nm_dhcp_lease_data_parse_domain_validate(str1, iface, addr_family, option);
     if (!s)
         return FALSE;
 
@@ -1015,7 +1072,11 @@ nm_dhcp_lease_data_parse_domain(const guint8 *data, gsize n_data, char **out_val
 }
 
 gboolean
-nm_dhcp_lease_data_parse_in_addr(const guint8 *data, gsize n_data, in_addr_t *out_val)
+nm_dhcp_lease_data_parse_in_addr(const guint8 *data,
+                                 gsize         n_data,
+                                 in_addr_t    *out_val,
+                                 const char   *iface,
+                                 guint         option)
 {
     /* - option 1, https://tools.ietf.org/html/rfc2132#section-3.3
      * - option 28, https://tools.ietf.org/html/rfc2132#section-5.3
@@ -1025,8 +1086,14 @@ nm_dhcp_lease_data_parse_in_addr(const guint8 *data, gsize n_data, in_addr_t *ou
      * according to RFC 3396 section 7. Therefore, it's possible that a
      * option carrying a IPv4 address has a length > 4.
      */
-    if (n_data < 4)
+    if (n_data < 4) {
+        nm_dhcp_lease_log_invalid_option(iface,
+                                         AF_INET,
+                                         option,
+                                         "invalid address length %lu",
+                                         (unsigned long) n_data);
         return FALSE;
+    }
 
     *out_val = unaligned_read_ne32(data);
     return TRUE;
@@ -1069,7 +1136,8 @@ static char *
 lease_option_print_domain_name(const uint8_t  *cache,
                                size_t         *n_cachep,
                                const uint8_t **datap,
-                               size_t         *n_datap)
+                               size_t         *n_datap,
+                               gboolean       *invalid)
 {
     nm_auto_str_buf NMStrBuf sbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_40, FALSE);
     const uint8_t           *domain;
@@ -1080,6 +1148,8 @@ lease_option_print_domain_name(const uint8_t  *cache,
     gboolean                 first     = TRUE;
     uint8_t                  c;
 
+    NM_SET_OUT(invalid, FALSE);
+
     /*
      * We are given two adjacent memory regions. The @cache contains alreday parsed
      * domain names, and the @datap contains the remaining data to parse.
@@ -1096,8 +1166,10 @@ lease_option_print_domain_name(const uint8_t  *cache,
      * Note, that each time a jump to an offset is performed, the size of the
      * cache shrinks, so this is guaranteed to terminate.
      */
-    if (cache + n_cache != *datap)
+    if (cache + n_cache != *datap) {
+        NM_SET_OUT(invalid, TRUE);
         return NULL;
+    }
 
     for (;;) {
         if (!nm_dhcp_lease_data_consume(domainp, n_domainp, &c, sizeof(c)))
@@ -1122,8 +1194,10 @@ lease_option_print_domain_name(const uint8_t  *cache,
             else
                 first = FALSE;
 
-            if (!lease_option_print_label(&sbuf, n_label, domainp, n_domainp))
+            if (!lease_option_print_label(&sbuf, n_label, domainp, n_domainp)) {
+                NM_SET_OUT(invalid, TRUE);
                 return NULL;
+            }
 
             break;
         }
@@ -1136,13 +1210,17 @@ lease_option_print_domain_name(const uint8_t  *cache,
              * two high bits are masked out.
              */
 
-            if (!nm_dhcp_lease_data_consume(domainp, n_domainp, &c, sizeof(c)))
+            if (!nm_dhcp_lease_data_consume(domainp, n_domainp, &c, sizeof(c))) {
+                NM_SET_OUT(invalid, TRUE);
                 return NULL;
+            }
 
             offset += c;
 
-            if (offset >= n_cache)
+            if (offset >= n_cache) {
+                NM_SET_OUT(invalid, TRUE);
                 return NULL;
+            }
 
             domain   = cache + offset;
             n_domain = n_cache - offset;
@@ -1154,29 +1232,44 @@ lease_option_print_domain_name(const uint8_t  *cache,
             break;
         }
         default:
+            NM_SET_OUT(invalid, TRUE);
             return NULL;
         }
     }
 }
 
 char **
-nm_dhcp_lease_data_parse_search_list(const guint8 *data, gsize n_data)
+nm_dhcp_lease_data_parse_search_list(const guint8 *data,
+                                     gsize         n_data,
+                                     const char   *iface,
+                                     int           addr_family,
+                                     guint         option)
 {
     GPtrArray    *array   = NULL;
     const guint8 *cache   = data;
     gsize         n_cache = 0;
+    guint         i       = 0;
 
     for (;;) {
         gs_free char *s = NULL;
-
-        s = lease_option_print_domain_name(cache, &n_cache, &data, &n_data);
-        if (!s)
+        gboolean      invalid;
+
+        s = lease_option_print_domain_name(cache, &n_cache, &data, &n_data, &invalid);
+        if (!s) {
+            if (iface && invalid)
+                nm_dhcp_lease_log_invalid_option(iface,
+                                                 addr_family,
+                                                 option,
+                                                 "search domain #%u is invalid",
+                                                 i);
             break;
+        }
 
         if (!array)
             array = g_ptr_array_new();
 
         g_ptr_array_add(array, g_steal_pointer(&s));
+        i++;
     }
 
     if (!array)
diff --git a/src/core/dhcp/nm-dhcp-utils.h b/src/core/dhcp/nm-dhcp-utils.h
index bef50c52..00199b02 100644
--- a/src/core/dhcp/nm-dhcp-utils.h
+++ b/src/core/dhcp/nm-dhcp-utils.h
@@ -59,13 +59,48 @@ nm_dhcp_lease_data_consume_in_addr(const uint8_t **datap, size_t *n_datap, in_ad
     return nm_dhcp_lease_data_consume(datap, n_datap, addrp, sizeof(struct in_addr));
 }
 
-char *nm_dhcp_lease_data_parse_domain_validate(const char *str);
-
-gboolean nm_dhcp_lease_data_parse_u16(const guint8 *data, gsize n_data, guint16 *out_val);
-gboolean nm_dhcp_lease_data_parse_mtu(const guint8 *data, gsize n_data, guint16 *out_val);
-gboolean nm_dhcp_lease_data_parse_cstr(const guint8 *data, gsize n_data, gsize *out_new_len);
-gboolean nm_dhcp_lease_data_parse_domain(const guint8 *data, gsize n_data, char **out_val);
-gboolean nm_dhcp_lease_data_parse_in_addr(const guint8 *data, gsize n_data, in_addr_t *out_val);
-char   **nm_dhcp_lease_data_parse_search_list(const guint8 *data, gsize n_data);
+void     nm_dhcp_lease_log_invalid_option(const char *iface,
+                                          int         addr_family,
+                                          guint       option,
+                                          const char *fmt,
+                                          ...) G_GNUC_PRINTF(4, 5);
+char    *nm_dhcp_lease_data_parse_domain_validate(const char *str,
+                                                  const char *iface,
+                                                  int         addr_family,
+                                                  guint       option);
+gboolean nm_dhcp_lease_data_parse_u16(const guint8 *data,
+                                      gsize         n_data,
+                                      guint16      *out_val,
+                                      const char   *iface,
+                                      int           addr_family,
+                                      guint         option);
+gboolean nm_dhcp_lease_data_parse_mtu(const guint8 *data,
+                                      gsize         n_data,
+                                      guint16      *out_val,
+                                      const char   *iface,
+                                      int           addr_family,
+                                      guint         option);
+gboolean nm_dhcp_lease_data_parse_cstr(const guint8 *data,
+                                       gsize         n_data,
+                                       gsize        *out_new_len,
+                                       const char   *iface,
+                                       int           addr_family,
+                                       guint         option);
+gboolean nm_dhcp_lease_data_parse_domain(const guint8 *data,
+                                         gsize         n_data,
+                                         char        **out_val,
+                                         const char   *iface,
+                                         int           addr_family,
+                                         guint         option);
+gboolean nm_dhcp_lease_data_parse_in_addr(const guint8 *data,
+                                          gsize         n_data,
+                                          in_addr_t    *out_val,
+                                          const char   *iface,
+                                          guint         option);
+char   **nm_dhcp_lease_data_parse_search_list(const guint8 *data,
+                                              gsize         n_data,
+                                              const char   *iface,
+                                              int           addr_family,
+                                              guint         option);
 
 #endif /* __NETWORKMANAGER_DHCP_UTILS_H__ */
diff --git a/src/core/dhcp/tests/test-dhcp-utils.c b/src/core/dhcp/tests/test-dhcp-utils.c
index f021b642..7597f516 100644
--- a/src/core/dhcp/tests/test-dhcp-utils.c
+++ b/src/core/dhcp/tests/test-dhcp-utils.c
@@ -196,16 +196,16 @@ test_parse_search_list(void)
     char  **domains;
 
     data    = (guint8[]){0x05, 'l', 'o', 'c', 'a', 'l', 0x00};
-    domains = nm_dhcp_lease_data_parse_search_list(data, 7);
+    domains = nm_dhcp_lease_data_parse_search_list(data, 7, NULL, 0, 0);
     g_assert(domains);
     g_assert_cmpint(g_strv_length(domains), ==, 1);
     g_assert_cmpstr(domains[0], ==, "local");
     g_strfreev(domains);
 
     data    = (guint8[]){0x04, 't',  'e',  's', 't', 0x07, 'e',  'x',  'a',  'm', 'p', 'l',
-                      'e',  0x03, 'c',  'o', 'm', 0x00, 0xc0, 0x05, 0x03, 'a', 'b', 'c',
-                      0xc0, 0x0d, 0x06, 'f', 'o', 'o',  'b',  'a',  'r',  0x00};
-    domains = nm_dhcp_lease_data_parse_search_list(data, 34);
+                         'e',  0x03, 'c',  'o', 'm', 0x00, 0xc0, 0x05, 0x03, 'a', 'b', 'c',
+                         0xc0, 0x0d, 0x06, 'f', 'o', 'o',  'b',  'a',  'r',  0x00};
+    domains = nm_dhcp_lease_data_parse_search_list(data, 34, NULL, 0, 0);
     g_assert(domains);
     g_assert_cmpint(g_strv_length(domains), ==, 4);
     g_assert_cmpstr(domains[0], ==, "test.example.com");
@@ -220,7 +220,7 @@ test_parse_search_list(void)
         'a',
         'd',
     };
-    domains = nm_dhcp_lease_data_parse_search_list(data, 4);
+    domains = nm_dhcp_lease_data_parse_search_list(data, 4, NULL, 0, 0);
     g_assert(!domains);
 
     data = (guint8[]){
@@ -235,7 +235,7 @@ test_parse_search_list(void)
         'a',
         'd',
     };
-    domains = nm_dhcp_lease_data_parse_search_list(data, 10);
+    domains = nm_dhcp_lease_data_parse_search_list(data, 10, NULL, 0, 0);
     g_assert(domains);
     g_assert_cmpint(g_strv_length(domains), ==, 1);
     g_assert_cmpstr(domains[0], ==, "okay");
@@ -285,7 +285,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 +310,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,9 +336,9 @@ 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"},
+                                            "0 192.168.0.113 25.129.210.177.132 192.168.0.113 7.2 10.34.255.6"},
         {NULL, NULL}};
 
     options = fill_table(generic_options, NULL);
@@ -362,7 +362,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 +389,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 +419,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 +448,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 +477,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 +501,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 +526,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 +550,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 43426882..7d0f0490 100644
--- a/src/core/dns/nm-dns-dnsmasq.c
+++ b/src/core/dns/nm-dns-dnsmasq.c
@@ -678,19 +678,23 @@ typedef struct {
 
     char *name_owner;
 
+    GSource *main_timeout_source;
+    GSource *burst_retry_timeout_source;
+
     gint64 burst_start_at;
 
     GPid process_pid;
 
     guint name_owner_changed_id;
-    guint main_timeout_id;
-
-    guint burst_retry_timeout_id;
 
     guint8 burst_count;
 
     bool is_stopped : 1;
 
+    bool set_server_ex_args_dirty : 1;
+
+    bool update_pending : 1;
+
 } NMDnsDnsmasqPrivate;
 
 struct _NMDnsDnsmasq {
@@ -704,7 +708,8 @@ struct _NMDnsDnsmasqClass {
 
 G_DEFINE_TYPE(NMDnsDnsmasq, nm_dns_dnsmasq, NM_TYPE_DNS_PLUGIN)
 
-#define NM_DNS_DNSMASQ_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMDnsDnsmasq, NM_IS_DNS_DNSMASQ)
+#define NM_DNS_DNSMASQ_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDnsDnsmasq, NM_IS_DNS_DNSMASQ, NMDnsPlugin)
 
 /*****************************************************************************/
 
@@ -717,6 +722,55 @@ static gboolean start_dnsmasq(NMDnsDnsmasq *self, gboolean force_start, GError *
 
 /*****************************************************************************/
 
+static gboolean
+_update_pending_detect(NMDnsDnsmasq *self)
+{
+    NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self);
+
+    if (priv->is_stopped)
+        return FALSE;
+    if (priv->main_timeout_source) {
+        /* we are waiting for dnsmasq to start. */
+        return TRUE;
+    }
+    if (priv->update_cancellable) {
+        /* An update is in progress. Busy. */
+        return TRUE;
+    }
+    if (priv->set_server_ex_args_dirty) {
+        /* the args just changed and were not yet sent. Busy. */
+        return TRUE;
+    }
+
+    return FALSE;
+}
+
+static void
+_update_pending_maybe_changed(NMDnsDnsmasq *self)
+{
+    NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self);
+    gboolean             update_pending;
+
+    update_pending = _update_pending_detect(self);
+    if (priv->update_pending == update_pending)
+        return;
+
+    priv->update_pending = update_pending;
+    _nm_dns_plugin_update_pending_maybe_changed(NM_DNS_PLUGIN(self));
+}
+
+static gboolean
+get_update_pending(NMDnsPlugin *plugin)
+{
+    NMDnsDnsmasq        *self = NM_DNS_DNSMASQ(plugin);
+    NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self);
+
+    nm_assert(priv->update_pending == _update_pending_detect(self));
+    return priv->update_pending;
+}
+
+/*****************************************************************************/
+
 static void
 add_dnsmasq_nameserver(NMDnsDnsmasq    *self,
                        GVariantBuilder *servers,
@@ -871,6 +925,7 @@ static void
 dnsmasq_update_done(GObject *source_object, GAsyncResult *res, gpointer user_data)
 {
     NMDnsDnsmasq              *self;
+    NMDnsDnsmasqPrivate       *priv;
     gs_free_error GError      *error    = NULL;
     gs_unref_variant GVariant *response = NULL;
 
@@ -880,10 +935,16 @@ dnsmasq_update_done(GObject *source_object, GAsyncResult *res, gpointer user_dat
         return;
 
     self = user_data;
+    priv = NM_DNS_DNSMASQ_GET_PRIVATE(self);
+
+    nm_clear_g_cancellable(&priv->update_cancellable);
+
     if (!response)
         _LOGW("dnsmasq update failed: %s", error->message);
     else
         _LOGD("dnsmasq update successful");
+
+    _update_pending_maybe_changed(self);
 }
 
 static void
@@ -899,6 +960,8 @@ send_dnsmasq_update(NMDnsDnsmasq *self)
     nm_clear_g_cancellable(&priv->update_cancellable);
     priv->update_cancellable = g_cancellable_new();
 
+    priv->set_server_ex_args_dirty = FALSE;
+
     g_dbus_connection_call(priv->dbus_connection,
                            priv->name_owner,
                            DNSMASQ_DBUS_PATH,
@@ -911,6 +974,8 @@ send_dnsmasq_update(NMDnsDnsmasq *self)
                            priv->update_cancellable,
                            dnsmasq_update_done,
                            self);
+
+    _update_pending_maybe_changed(self);
 }
 
 /*****************************************************************************/
@@ -928,17 +993,19 @@ _main_cleanup(NMDnsDnsmasq *self, gboolean emit_failed)
 
     nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->name_owner_changed_id);
 
-    nm_clear_g_source(&priv->main_timeout_id);
+    nm_clear_g_source_inst(&priv->main_timeout_source);
     nm_clear_g_cancellable(&priv->update_cancellable);
 
     /* cancelling the main_cancellable will also cause _gl_pid_spawn*() to terminate the
      * process in the background. */
     nm_clear_g_cancellable(&priv->main_cancellable);
 
-    if (!priv->is_stopped && priv->burst_retry_timeout_id == 0) {
+    if (!priv->is_stopped && !priv->burst_retry_timeout_source) {
         start_dnsmasq(self, FALSE, NULL);
         send_dnsmasq_update(self);
     }
+
+    _update_pending_maybe_changed(self);
 }
 
 static void
@@ -961,8 +1028,10 @@ name_owner_changed(NMDnsDnsmasq *self, const char *name_owner)
     }
 
     _LOGT("D-Bus name for dnsmasq got owner %s", name_owner);
-    nm_clear_g_source(&priv->main_timeout_id);
+    nm_clear_g_source_inst(&priv->main_timeout_source);
     send_dnsmasq_update(self);
+
+    _update_pending_maybe_changed(self);
 }
 
 static void
@@ -1047,11 +1116,11 @@ _burst_retry_timeout_cb(gpointer user_data)
     NMDnsDnsmasq        *self = user_data;
     NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self);
 
-    priv->burst_retry_timeout_id = 0;
+    nm_clear_g_source_inst(&priv->burst_retry_timeout_source);
 
     start_dnsmasq(self, TRUE, NULL);
     send_dnsmasq_update(self);
-    return G_SOURCE_REMOVE;
+    return G_SOURCE_CONTINUE;
 }
 
 static gboolean
@@ -1090,33 +1159,35 @@ start_dnsmasq(NMDnsDnsmasq *self, gboolean force_start, GError **error)
         || priv->burst_start_at + RATELIMIT_INTERVAL_MSEC <= now) {
         priv->burst_start_at = now;
         priv->burst_count    = 1;
-        nm_clear_g_source(&priv->burst_retry_timeout_id);
+        nm_clear_g_source_inst(&priv->burst_retry_timeout_source);
         _LOGT("rate-limit: start burst interval of %d seconds %s",
               RATELIMIT_INTERVAL_MSEC / 1000,
               force_start ? " (force)" : "");
     } else if (priv->burst_count < RATELIMIT_BURST) {
-        nm_assert(priv->burst_retry_timeout_id == 0);
+        nm_assert(!priv->burst_retry_timeout_source);
         priv->burst_count++;
         _LOGT("rate-limit: %u try within burst interval of %d seconds",
               (guint) priv->burst_count,
               RATELIMIT_INTERVAL_MSEC / 1000);
     } else {
-        if (priv->burst_retry_timeout_id == 0) {
+        if (!priv->burst_retry_timeout_source) {
             _LOGW("dnsmasq dies and gets respawned too quickly. Back off. Something is very wrong");
-            priv->burst_retry_timeout_id =
-                g_timeout_add_seconds((2 * RATELIMIT_INTERVAL_MSEC) / 1000,
-                                      _burst_retry_timeout_cb,
-                                      self);
+            priv->burst_retry_timeout_source =
+                nm_g_timeout_add_seconds_source((2 * RATELIMIT_INTERVAL_MSEC) / 1000,
+                                                _burst_retry_timeout_cb,
+                                                self);
         } else
             _LOGT("rate-limit: currently rate-limited from restart");
         return TRUE;
     }
 
-    priv->main_timeout_id = g_timeout_add(10000, spawn_timeout_cb, self);
+    priv->main_timeout_source = nm_g_timeout_add_source(10000, spawn_timeout_cb, self);
 
     priv->main_cancellable = g_cancellable_new();
 
     _gl_pid_spawn(dm_binary, priv->main_cancellable, spawn_notify, self);
+
+    _update_pending_maybe_changed(self);
     return TRUE;
 }
 
@@ -1136,8 +1207,11 @@ update(NMDnsPlugin             *plugin,
     nm_clear_pointer(&priv->set_server_ex_args, g_variant_unref);
     priv->set_server_ex_args =
         g_variant_ref_sink(create_update_args(self, global_config, ip_data_lst_head, hostdomain));
+    priv->set_server_ex_args_dirty = TRUE;
 
     send_dnsmasq_update(self);
+
+    _update_pending_maybe_changed(self);
     return TRUE;
 }
 
@@ -1151,11 +1225,13 @@ stop(NMDnsPlugin *plugin)
 
     priv->is_stopped     = TRUE;
     priv->burst_start_at = 0;
-    nm_clear_g_source(&priv->burst_retry_timeout_id);
+    nm_clear_g_source_inst(&priv->burst_retry_timeout_source);
 
     /* Cancelling the cancellable will also terminate the
      * process (in the background). */
     _main_cleanup(self, FALSE);
+
+    _update_pending_maybe_changed(self);
 }
 
 /*****************************************************************************/
@@ -1178,7 +1254,7 @@ dispose(GObject *object)
 
     priv->is_stopped = TRUE;
 
-    nm_clear_g_source(&priv->burst_retry_timeout_id);
+    nm_clear_g_source_inst(&priv->burst_retry_timeout_source);
 
     _main_cleanup(self, FALSE);
 
@@ -1197,8 +1273,9 @@ nm_dns_dnsmasq_class_init(NMDnsDnsmasqClass *dns_class)
 
     object_class->dispose = dispose;
 
-    plugin_class->plugin_name = "dnsmasq";
-    plugin_class->is_caching  = TRUE;
-    plugin_class->stop        = stop;
-    plugin_class->update      = update;
+    plugin_class->plugin_name        = "dnsmasq";
+    plugin_class->is_caching         = TRUE;
+    plugin_class->stop               = stop;
+    plugin_class->update             = update;
+    plugin_class->get_update_pending = get_update_pending;
 }
diff --git a/src/core/dns/nm-dns-manager.c b/src/core/dns/nm-dns-manager.c
index 566f3d66..1e54452a 100644
--- a/src/core/dns/nm-dns-manager.c
+++ b/src/core/dns/nm-dns-manager.c
@@ -26,7 +26,6 @@
 
 #include "libnm-core-intern/nm-core-internal.h"
 #include "libnm-glib-aux/nm-str-buf.h"
-#include "libnm-systemd-shared/nm-sd-utils-shared.h"
 
 #include "NetworkManagerUtils.h"
 #include "devices/nm-device.h"
@@ -35,7 +34,6 @@
 #include "nm-dns-dnsmasq.h"
 #include "nm-dns-plugin.h"
 #include "nm-dns-systemd-resolved.h"
-#include "nm-dns-unbound.h"
 #include "nm-ip-config.h"
 #include "nm-l3-config-data.h"
 #include "nm-manager.h"
@@ -57,6 +55,8 @@
 #define HAS_NETCONFIG 1
 #endif
 
+#define UPDATE_PENDING_UNBLOCK_TIMEOUT_MSEC 5000
+
 /*****************************************************************************/
 
 typedef enum { SR_SUCCESS, SR_NOTFOUND, SR_ERROR } SpawnResult;
@@ -78,7 +78,11 @@ enum {
     LAST_SIGNAL
 };
 
-NM_GOBJECT_PROPERTIES_DEFINE(NMDnsManager, PROP_MODE, PROP_RC_MANAGER, PROP_CONFIGURATION, );
+NM_GOBJECT_PROPERTIES_DEFINE(NMDnsManager,
+                             PROP_MODE,
+                             PROP_RC_MANAGER,
+                             PROP_CONFIGURATION,
+                             PROP_UPDATE_PENDING, );
 
 static guint signals[LAST_SIGNAL] = {0};
 
@@ -89,6 +93,11 @@ typedef struct {
     CList     ip_data_lst_head;
     GVariant *config_variant;
 
+    /* A DNS plugin should not be marked as pending indefinitely.
+     * We are only blocked if "update_pending" is TRUE and we have
+     * "update_pending_unblock" timer ticking. */
+    GSource *update_pending_unblock;
+
     bool ip_data_lst_need_sort : 1;
 
     bool configs_lst_need_sort : 1;
@@ -98,6 +107,8 @@ typedef struct {
 
     bool config_changed : 1;
 
+    bool update_pending : 1;
+
     char *hostdomain;
     guint updates_queue;
 
@@ -109,6 +120,9 @@ typedef struct {
     NMDnsPlugin                  *sd_resolve_plugin;
     NMDnsPlugin                  *plugin;
 
+    gulong update_changed_signal_id_sd;
+    gulong update_changed_signal_id;
+
     NMConfig *config;
 
     struct {
@@ -137,28 +151,23 @@ NM_DEFINE_SINGLETON_GETTER(NMDnsManager, nm_dns_manager_get, NM_TYPE_DNS_MANAGER
 
 #define _NMLOG_PREFIX_NAME "dns-mgr"
 #define _NMLOG_DOMAIN      LOGD_DNS
-#define _NMLOG(level, ...)                                           \
-    G_STMT_START                                                     \
-    {                                                                \
-        const NMLogLevel __level = (level);                          \
-                                                                     \
-        if (nm_logging_enabled(__level, _NMLOG_DOMAIN)) {            \
-            char                      __prefix[20];                  \
-            const NMDnsManager *const __self = (self);               \
-                                                                     \
-            _nm_log(__level,                                         \
-                    _NMLOG_DOMAIN,                                   \
-                    0,                                               \
-                    NULL,                                            \
-                    NULL,                                            \
-                    "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),     \
-                    _NMLOG_PREFIX_NAME,                              \
-                    ((!__self || __self == singleton_instance)       \
-                         ? ""                                        \
-                         : nm_sprintf_buf(__prefix, "[%p]", __self)) \
-                        _NM_UTILS_MACRO_REST(__VA_ARGS__));          \
-        }                                                            \
-    }                                                                \
+#define _NMLOG(level, ...)                                                 \
+    G_STMT_START                                                           \
+    {                                                                      \
+        const NMLogLevel __level = (level);                                \
+                                                                           \
+        if (nm_logging_enabled(__level, _NMLOG_DOMAIN)) {                  \
+            _nm_unused const NMDnsManager *const __self = (self);          \
+                                                                           \
+            _nm_log(__level,                                               \
+                    _NMLOG_DOMAIN,                                         \
+                    0,                                                     \
+                    NULL,                                                  \
+                    NULL,                                                  \
+                    "%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),             \
+                    _NMLOG_PREFIX_NAME _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+        }                                                                  \
+    }                                                                      \
     G_STMT_END
 
 /*****************************************************************************/
@@ -207,6 +216,85 @@ static NM_UTILS_LOOKUP_STR_DEFINE(
 
 /*****************************************************************************/
 
+static gboolean
+_update_pending_detect(NMDnsManager *self)
+{
+    NMDnsManagerPrivate *priv = NM_DNS_MANAGER_GET_PRIVATE(self);
+
+    if (priv->plugin && nm_dns_plugin_get_update_pending(priv->plugin))
+        return TRUE;
+    if (priv->sd_resolve_plugin && nm_dns_plugin_get_update_pending(priv->sd_resolve_plugin))
+        return TRUE;
+    return FALSE;
+}
+
+static gboolean
+_update_pending_unblock_cb(gpointer user_data)
+{
+    NMDnsManager        *self = user_data;
+    NMDnsManagerPrivate *priv = NM_DNS_MANAGER_GET_PRIVATE(self);
+
+    nm_assert(priv->update_pending);
+    nm_assert(priv->update_pending_unblock);
+    nm_assert(_update_pending_detect(self));
+
+    nm_clear_g_source_inst(&priv->update_pending_unblock);
+
+    _LOGW(
+        "update-pending changed: DNS plugin did not become ready again. Assume something is wrong");
+
+    _notify(self, PROP_UPDATE_PENDING);
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+_update_pending_maybe_changed(NMDnsManager *self)
+{
+    NMDnsManagerPrivate *priv = NM_DNS_MANAGER_GET_PRIVATE(self);
+    gboolean             update_pending;
+
+    update_pending = _update_pending_detect(self);
+    if (priv->update_pending == update_pending)
+        return;
+
+    if (update_pending) {
+        nm_assert(!priv->update_pending_unblock);
+        priv->update_pending_unblock = nm_g_timeout_add_source(UPDATE_PENDING_UNBLOCK_TIMEOUT_MSEC,
+                                                               _update_pending_unblock_cb,
+                                                               self);
+    } else
+        nm_clear_g_source_inst(&priv->update_pending_unblock);
+
+    priv->update_pending = update_pending;
+    _LOGD("update-pending changed: %spending", update_pending ? "" : "not ");
+    _notify(self, PROP_UPDATE_PENDING);
+}
+
+static void
+_update_pending_changed_cb(NMDnsPlugin *plugin, gboolean update_pending, NMDnsManager *self)
+{
+    _update_pending_maybe_changed(self);
+}
+
+gboolean
+nm_dns_manager_get_update_pending(NMDnsManager *self)
+{
+    NMDnsManagerPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DNS_MANAGER(self), FALSE);
+
+    priv = NM_DNS_MANAGER_GET_PRIVATE(self);
+    nm_assert(priv->update_pending == _update_pending_detect(self));
+    nm_assert(priv->update_pending || !priv->update_pending_unblock);
+
+    /* update-pending can only be TRUE for a certain time (before we assume
+     * something is really wrong with the plugin). That is, as long as
+     * update_pending_unblock is ticking. */
+    return !!priv->update_pending_unblock;
+}
+
+/*****************************************************************************/
+
 static int
 _dns_config_ip_data_get_dns_priority1(const NML3ConfigData *l3cd, int addr_family)
 {
@@ -2015,7 +2103,7 @@ nm_dns_manager_set_hostname(NMDnsManager *self, const char *hostname, gboolean s
                 domain = hostname;
             }
 
-            if (!nm_sd_hostname_is_valid(domain, FALSE))
+            if (!nm_hostname_is_valid(domain, FALSE))
                 domain = NULL;
         }
     }
@@ -2120,6 +2208,7 @@ _clear_plugin(NMDnsManager *self)
     nm_clear_g_source(&priv->plugin_ratelimit.timer);
 
     if (priv->plugin) {
+        nm_clear_g_signal_handler(priv->plugin, &priv->update_changed_signal_id);
         nm_dns_plugin_stop(priv->plugin);
         g_clear_object(&priv->plugin);
         return TRUE;
@@ -2127,6 +2216,20 @@ _clear_plugin(NMDnsManager *self)
     return FALSE;
 }
 
+static gboolean
+_clear_sd_resolved_plugin(NMDnsManager *self)
+{
+    NMDnsManagerPrivate *priv = NM_DNS_MANAGER_GET_PRIVATE(self);
+
+    if (priv->sd_resolve_plugin) {
+        nm_clear_g_signal_handler(priv->sd_resolve_plugin, &priv->update_changed_signal_id_sd);
+        nm_dns_plugin_stop(priv->sd_resolve_plugin);
+        g_clear_object(&priv->sd_resolve_plugin);
+        return TRUE;
+    }
+    return FALSE;
+}
+
 static NMDnsManagerResolvConfManager
 _check_resconf_immutable(NMDnsManagerResolvConfManager rc_manager)
 {
@@ -2313,16 +2416,14 @@ again:
             priv->plugin   = nm_dns_dnsmasq_new();
             plugin_changed = TRUE;
         }
-    } else if (nm_streq0(mode, "unbound")) {
-        if (force_reload_plugin || !NM_IS_DNS_UNBOUND(priv->plugin)) {
-            _clear_plugin(self);
-            priv->plugin   = nm_dns_unbound_new();
-            plugin_changed = TRUE;
-        }
     } else {
         if (!NM_IN_STRSET(mode, "none", "default")) {
-            if (mode)
-                _LOGW("init: unknown dns mode '%s'", mode);
+            if (mode) {
+                if (nm_streq(mode, "unbound"))
+                    _LOGW("init: ns mode 'unbound' was removed. Update your configuration");
+                else
+                    _LOGW("init: unknown dns mode '%s'", mode);
+            }
             mode = "default";
         }
         if (_clear_plugin(self))
@@ -2359,7 +2460,7 @@ again:
             priv->sd_resolve_plugin  = nm_dns_systemd_resolved_new();
             systemd_resolved_changed = TRUE;
         }
-    } else if (nm_clear_g_object(&priv->sd_resolve_plugin))
+    } else if (_clear_sd_resolved_plugin(self))
         systemd_resolved_changed = TRUE;
 
     g_object_freeze_notify(G_OBJECT(self));
@@ -2390,6 +2491,23 @@ again:
                                   ""));
     }
 
+    if (plugin_changed && priv->plugin && priv->update_changed_signal_id == 0) {
+        priv->update_changed_signal_id = g_signal_connect(priv->plugin,
+                                                          NM_DNS_PLUGIN_UPDATE_PENDING_CHANGED,
+                                                          G_CALLBACK(_update_pending_changed_cb),
+                                                          self);
+    }
+
+    if (systemd_resolved_changed && priv->sd_resolve_plugin
+        && priv->update_changed_signal_id_sd == 0) {
+        priv->update_changed_signal_id_sd = g_signal_connect(priv->sd_resolve_plugin,
+                                                             NM_DNS_PLUGIN_UPDATE_PENDING_CHANGED,
+                                                             G_CALLBACK(_update_pending_changed_cb),
+                                                             self);
+    }
+
+    _update_pending_maybe_changed(self);
+
     g_object_thaw_notify(G_OBJECT(self));
 }
 
@@ -2594,6 +2712,9 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
     case PROP_CONFIGURATION:
         g_value_set_variant(value, _get_config_variant(self));
         break;
+    case PROP_UPDATE_PENDING:
+        g_value_set_boolean(value, nm_dns_manager_get_update_pending(self));
+        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
         break;
@@ -2641,9 +2762,11 @@ dispose(GObject *object)
     if (priv->config)
         g_signal_handlers_disconnect_by_func(priv->config, config_changed_cb, self);
 
-    g_clear_object(&priv->sd_resolve_plugin);
+    _clear_sd_resolved_plugin(self);
     _clear_plugin(self);
 
+    nm_clear_g_source_inst(&priv->update_pending_unblock);
+
     c_list_for_each_entry_safe (ip_data, ip_data_safe, &priv->ip_data_lst_head, ip_data_lst)
         _dns_config_ip_data_free(ip_data);
 
@@ -2719,6 +2842,13 @@ nm_dns_manager_class_init(NMDnsManagerClass *klass)
                              NULL,
                              G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
+    obj_properties[PROP_UPDATE_PENDING] =
+        g_param_spec_boolean(NM_DNS_MANAGER_UPDATE_PENDING,
+                             "",
+                             "",
+                             FALSE,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
     g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
 
     signals[CONFIG_CHANGED] = g_signal_new(NM_DNS_MANAGER_CONFIG_CHANGED,
diff --git a/src/core/dns/nm-dns-manager.h b/src/core/dns/nm-dns-manager.h
index c30d4b3a..210f9f6c 100644
--- a/src/core/dns/nm-dns-manager.h
+++ b/src/core/dns/nm-dns-manager.h
@@ -80,9 +80,10 @@ typedef struct _NMDnsConfigData {
     (G_TYPE_INSTANCE_GET_CLASS((o), NM_TYPE_DNS_MANAGER, NMDnsManagerClass))
 
 /* properties */
-#define NM_DNS_MANAGER_MODE          "mode"
-#define NM_DNS_MANAGER_RC_MANAGER    "rc-manager"
-#define NM_DNS_MANAGER_CONFIGURATION "configuration"
+#define NM_DNS_MANAGER_MODE           "mode"
+#define NM_DNS_MANAGER_RC_MANAGER     "rc-manager"
+#define NM_DNS_MANAGER_CONFIGURATION  "configuration"
+#define NM_DNS_MANAGER_UPDATE_PENDING "update-pending"
 
 /* internal signals */
 #define NM_DNS_MANAGER_CONFIG_CHANGED "config-changed"
@@ -149,6 +150,8 @@ void nm_dns_manager_stop(NMDnsManager *self);
 
 NMDnsPlugin *nm_dns_manager_get_systemd_resolved(NMDnsManager *self);
 
+gboolean nm_dns_manager_get_update_pending(NMDnsManager *self);
+
 /*****************************************************************************/
 
 char *nmtst_dns_create_resolv_conf(const char *const *searches,
diff --git a/src/core/dns/nm-dns-plugin.c b/src/core/dns/nm-dns-plugin.c
index 847d7839..41a0dbc1 100644
--- a/src/core/dns/nm-dns-plugin.c
+++ b/src/core/dns/nm-dns-plugin.c
@@ -17,11 +17,16 @@
 
 /*****************************************************************************/
 
+enum {
+    UPDATE_PENDING_CHANGED,
+    LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL] = {0};
+
 typedef struct _NMDnsPluginPrivate {
-    GPid  pid;
-    guint watch_id;
-    char *progname;
-    char *pidfile;
+    bool update_pending_inited : 1;
+    bool update_pending : 1;
 } NMDnsPluginPrivate;
 
 G_DEFINE_ABSTRACT_TYPE(NMDnsPlugin, nm_dns_plugin, G_TYPE_OBJECT)
@@ -32,26 +37,29 @@ G_DEFINE_ABSTRACT_TYPE(NMDnsPlugin, nm_dns_plugin, G_TYPE_OBJECT)
 
 #define _NMLOG_PREFIX_NAME "dns-plugin"
 #define _NMLOG_DOMAIN      LOGD_DNS
-#define _NMLOG(level, ...)                                                    \
-    G_STMT_START                                                              \
-    {                                                                         \
-        const NMLogLevel __level = (level);                                   \
-                                                                              \
-        if (nm_logging_enabled(__level, _NMLOG_DOMAIN)) {                     \
-            char                     __prefix[20];                            \
-            const NMDnsPlugin *const __self = (self);                         \
-                                                                              \
-            _nm_log(__level,                                                  \
-                    _NMLOG_DOMAIN,                                            \
-                    0,                                                        \
-                    NULL,                                                     \
-                    NULL,                                                     \
-                    "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),              \
-                    _NMLOG_PREFIX_NAME,                                       \
-                    (!__self ? "" : nm_sprintf_buf(__prefix, "[%p]", __self)) \
-                        _NM_UTILS_MACRO_REST(__VA_ARGS__));                   \
-        }                                                                     \
-    }                                                                         \
+#define _NMLOG(level, ...)                                                                      \
+    G_STMT_START                                                                                \
+    {                                                                                           \
+        const NMLogLevel __level = (level);                                                     \
+                                                                                                \
+        if (nm_logging_enabled(__level, _NMLOG_DOMAIN)) {                                       \
+            char                     __prefix[20];                                              \
+            const NMDnsPlugin *const __self = (self);                                           \
+                                                                                                \
+            _nm_log(__level,                                                                    \
+                    _NMLOG_DOMAIN,                                                              \
+                    0,                                                                          \
+                    NULL,                                                                       \
+                    NULL,                                                                       \
+                    "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                                \
+                    _NMLOG_PREFIX_NAME,                                                         \
+                    (!__self ? ""                                                               \
+                             : nm_sprintf_buf(__prefix,                                         \
+                                              "[" NM_HASH_OBFUSCATE_PTR_FMT "]",                \
+                                              NM_HASH_OBFUSCATE_PTR(                            \
+                                                  __self))) _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+        }                                                                                       \
+    }                                                                                           \
     G_STMT_END
 
 /*****************************************************************************/
@@ -104,10 +112,109 @@ nm_dns_plugin_stop(NMDnsPlugin *self)
 
 /*****************************************************************************/
 
+static gboolean
+_get_update_pending(NMDnsPlugin *self)
+{
+    NMDnsPluginClass *klass;
+
+    nm_assert(NM_IS_DNS_PLUGIN(self));
+
+    klass = NM_DNS_PLUGIN_GET_CLASS(self);
+    if (klass->get_update_pending) {
+        if (klass->get_update_pending(self))
+            return TRUE;
+    }
+    return FALSE;
+}
+
+gboolean
+nm_dns_plugin_get_update_pending(NMDnsPlugin *self)
+{
+    NMDnsPluginPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DNS_PLUGIN(self), FALSE);
+
+    priv = NM_DNS_PLUGIN_GET_PRIVATE(self);
+
+    /* We cache the boolean and rely on the subclass to call
+     * _nm_dns_plugin_update_pending_maybe_changed(). The subclass
+     * anyway must get it right to notify us when the value (maybe)
+     * changes. By caching the value, the subclass is free to notify
+     * even if the value did not actually change.
+     *
+     * Also, this allows the base implementation to combine multiple
+     * sources/reasons (if we need that in the future). */
+
+    if (!priv->update_pending_inited) {
+        priv->update_pending_inited = TRUE;
+        priv->update_pending        = _get_update_pending(self);
+        _LOGD("[%s] update-pending changed (%spending)",
+              nm_dns_plugin_get_name(self),
+              priv->update_pending ? "" : "not ");
+    } else
+        nm_assert(priv->update_pending == _get_update_pending(self));
+
+    return priv->update_pending;
+}
+
+void
+_nm_dns_plugin_update_pending_maybe_changed(NMDnsPlugin *self)
+{
+    NMDnsPluginPrivate *priv;
+    gboolean            v;
+
+    g_return_if_fail(NM_IS_DNS_PLUGIN(self));
+
+    priv = NM_DNS_PLUGIN_GET_PRIVATE(self);
+
+    v = _get_update_pending(self);
+
+    if (!priv->update_pending_inited)
+        priv->update_pending_inited = TRUE;
+    else if (priv->update_pending == v)
+        return;
+
+    priv->update_pending = v;
+
+    _LOGD("[%s] update-pending changed (%spending)",
+          nm_dns_plugin_get_name(self),
+          priv->update_pending ? "" : "not ");
+
+    g_signal_emit(self, signals[UPDATE_PENDING_CHANGED], 0, (gboolean) priv->update_pending);
+}
+
+/*****************************************************************************/
+
 static void
 nm_dns_plugin_init(NMDnsPlugin *self)
-{}
+{
+    NMDnsPluginPrivate *priv;
+
+    priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_DNS_PLUGIN, NMDnsPluginPrivate);
+
+    self->_priv = priv;
+
+    nm_assert(priv->update_pending_inited == FALSE);
+    nm_assert(priv->update_pending == FALSE);
+
+    nm_shutdown_wait_obj_register_object(self, "dns-plugin");
+}
 
 static void
-nm_dns_plugin_class_init(NMDnsPluginClass *plugin_class)
-{}
+nm_dns_plugin_class_init(NMDnsPluginClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    g_type_class_add_private(object_class, sizeof(NMDnsPluginPrivate));
+
+    signals[UPDATE_PENDING_CHANGED] = g_signal_new(NM_DNS_PLUGIN_UPDATE_PENDING_CHANGED,
+                                                   G_OBJECT_CLASS_TYPE(klass),
+                                                   G_SIGNAL_RUN_FIRST,
+                                                   0,
+                                                   NULL,
+                                                   NULL,
+                                                   NULL,
+                                                   G_TYPE_NONE,
+                                                   1,
+                                                   G_TYPE_BOOLEAN);
+}
diff --git a/src/core/dns/nm-dns-plugin.h b/src/core/dns/nm-dns-plugin.h
index f9c424ab..24d6083b 100644
--- a/src/core/dns/nm-dns-plugin.h
+++ b/src/core/dns/nm-dns-plugin.h
@@ -19,8 +19,13 @@
 #define NM_DNS_PLUGIN_GET_CLASS(obj) \
     (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DNS_PLUGIN, NMDnsPluginClass))
 
+#define NM_DNS_PLUGIN_UPDATE_PENDING_CHANGED "update-pending-changed"
+
+struct _NMDnsPluginPrivate;
+
 typedef struct {
-    GObject parent;
+    GObject                     parent;
+    struct _NMDnsPluginPrivate *_priv;
 } NMDnsPlugin;
 
 typedef struct {
@@ -39,6 +44,8 @@ typedef struct {
 
     void (*stop)(NMDnsPlugin *self);
 
+    gboolean (*get_update_pending)(NMDnsPlugin *self);
+
     const char *plugin_name;
 
     /* Types should set to TRUE if they start a local caching nameserver
@@ -63,4 +70,8 @@ gboolean nm_dns_plugin_update(NMDnsPlugin             *self,
 
 void nm_dns_plugin_stop(NMDnsPlugin *self);
 
+gboolean nm_dns_plugin_get_update_pending(NMDnsPlugin *self);
+
+void _nm_dns_plugin_update_pending_maybe_changed(NMDnsPlugin *self);
+
 #endif /* __NM_DNS_PLUGIN_H__ */
diff --git a/src/core/dns/nm-dns-systemd-resolved.c b/src/core/dns/nm-dns-systemd-resolved.c
index c4993884..e0b94647 100644
--- a/src/core/dns/nm-dns-systemd-resolved.c
+++ b/src/core/dns/nm-dns-systemd-resolved.c
@@ -40,8 +40,8 @@ static const char *const DBUS_OP_SET_LINK_DNS_OVER_TLS  = "SetLinkDNSOverTLS";
 /*****************************************************************************/
 
 typedef struct {
-    int   ifindex;
-    CList configs_lst_head;
+    int        ifindex;
+    GPtrArray *ip_data_list;
 } InterfaceConfig;
 
 typedef struct {
@@ -50,6 +50,7 @@ typedef struct {
     GVariant             *argument;
     NMDnsSystemdResolved *self;
     int                   ifindex;
+    int                   ref_count;
 } RequestItem;
 
 struct _NMDnsSystemdResolvedResolveHandle {
@@ -77,15 +78,18 @@ typedef struct {
     GDBusConnection *dbus_connection;
     GHashTable      *dirty_interfaces;
     GCancellable    *cancellable;
-    GSource         *try_start_timeout_source;
+    GCancellable    *service_start_cancellable;
     CList            request_queue_lst_head;
     char            *dbus_owner;
     CList            handle_lst_head;
     guint            name_owner_changed_id;
+    guint            n_pending;
     bool             send_updates_warn_ratelimited : 1;
     bool             try_start_blocked : 1;
+    bool             stopped : 1;
     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
@@ -106,7 +110,7 @@ struct _NMDnsSystemdResolvedClass {
 G_DEFINE_TYPE(NMDnsSystemdResolved, nm_dns_systemd_resolved, NM_TYPE_DNS_PLUGIN)
 
 #define NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self) \
-    _NM_GET_PRIVATE(self, NMDnsSystemdResolved, NM_IS_DNS_SYSTEMD_RESOLVED)
+    _NM_GET_PRIVATE(self, NMDnsSystemdResolved, NM_IS_DNS_SYSTEMD_RESOLVED, NMDnsPlugin)
 
 /*****************************************************************************/
 
@@ -146,10 +150,88 @@ static void _resolve_start(NMDnsSystemdResolved *self, NMDnsSystemdResolvedResol
 
 /*****************************************************************************/
 
+static gboolean
+_update_pending_detect(NMDnsSystemdResolved *self)
+{
+    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+
+    if (priv->n_pending > 0) {
+        /* we have pending calls. We definitely want to wait for them to complete. */
+        return TRUE;
+    }
+    if (!priv->dbus_initied) {
+        if (!priv->dbus_connection)
+            return FALSE;
+        /* D-Bus not yet initialized (and we don't know the name owner yet). Pending. */
+        return TRUE;
+    }
+    if (priv->service_start_cancellable) {
+        /* We are waiting to D-Bus activate resolved. Pending. */
+        return TRUE;
+    }
+    if (priv->try_start_blocked) {
+        /* We earlier tried to start resolved, but are rate limited. We are not pending an update
+         * (that we expect to complete any time soon). */
+        return FALSE;
+    }
+    if (priv->send_updates_waiting) {
+        /* we wait to send updates. We are pending. */
+        return TRUE;
+    }
+    return FALSE;
+}
+
+static void
+_update_pending_maybe_changed(NMDnsSystemdResolved *self)
+{
+    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    gboolean                     update_pending;
+
+    /* Important: we need to make sure that we call _update_pending_maybe_changed(), when
+     * the state changes. */
+
+    update_pending = _update_pending_detect(self);
+    if (priv->update_pending != update_pending) {
+        priv->update_pending = update_pending;
+        _nm_dns_plugin_update_pending_maybe_changed(NM_DNS_PLUGIN(self));
+    }
+}
+
+static gboolean
+get_update_pending(NMDnsPlugin *plugin)
+{
+    NMDnsSystemdResolved        *self = NM_DNS_SYSTEMD_RESOLVED(plugin);
+    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+
+    nm_assert(priv->update_pending == _update_pending_detect(self));
+    return priv->update_pending;
+}
+
+/*****************************************************************************/
+
+static RequestItem *
+_request_item_ref(RequestItem *request_item)
+{
+    nm_assert(request_item);
+    nm_assert(request_item->ref_count > 0);
+    nm_assert(request_item->ref_count < G_MAXINT);
+    nm_assert(!c_list_is_empty(&request_item->request_queue_lst));
+
+    request_item->ref_count++;
+    return request_item;
+}
+
 static void
-_request_item_free(RequestItem *request_item)
+_request_item_unref(RequestItem *request_item)
 {
-    c_list_unlink_stale(&request_item->request_queue_lst);
+    nm_assert(request_item);
+    nm_assert(request_item->ref_count > 0);
+
+    if (--request_item->ref_count > 0)
+        return;
+
+    nm_assert(c_list_is_empty(&request_item->request_queue_lst));
+
     g_variant_unref(request_item->argument);
     nm_g_slice_free(request_item);
 }
@@ -165,6 +247,7 @@ _request_item_append(NMDnsSystemdResolved *self,
 
     request_item  = g_slice_new(RequestItem);
     *request_item = (RequestItem){
+        .ref_count = 1,
         .operation = operation,
         .argument  = g_variant_ref_sink(argument),
         .self      = self,
@@ -178,8 +261,8 @@ _request_item_append(NMDnsSystemdResolved *self,
 static void
 _interface_config_free(InterfaceConfig *config)
 {
-    nm_c_list_elem_free_all(&config->configs_lst_head, NULL);
-    g_slice_free(InterfaceConfig, config);
+    nm_g_ptr_array_unref(config->ip_data_list);
+    nm_g_slice_free(config);
 }
 
 static void
@@ -191,42 +274,48 @@ call_done(GObject *source, GAsyncResult *r, gpointer user_data)
     NMDnsSystemdResolvedPrivate *priv;
     RequestItem                 *request_item;
     NMLogLevel                   log_level;
-
-    v = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), r, &error);
-    if (nm_utils_error_is_cancelled(error))
-        return;
+    const char                  *operation;
+    int                          ifindex;
 
     request_item = user_data;
     self         = request_item->self;
-    priv         = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    operation    = request_item->operation;
+    ifindex      = request_item->ifindex;
+    _request_item_unref(request_item);
+
+    priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+
+    v = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), r, &error);
+    if (nm_utils_error_is_cancelled(error))
+        goto out_dec_pending;
 
     if (v) {
-        if (request_item->operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE
+        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 (request_item->operation == DBUS_OP_SET_LINK_DNS_OVER_TLS
+        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");
         }
         priv->send_updates_warn_ratelimited = FALSE;
-        return;
+        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
-            && request_item->operation == DBUS_OP_SET_LINK_DEFAULT_ROUTE) {
+            && 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
-            && request_item->operation == DBUS_OP_SET_LINK_DNS_OVER_TLS) {
+            && 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");
         }
-        return;
+        goto out_dec_pending;
     }
 
     log_level = LOGL_DEBUG;
@@ -234,18 +323,25 @@ call_done(GObject *source, GAsyncResult *r, gpointer user_data)
         priv->send_updates_warn_ratelimited = TRUE;
         log_level                           = LOGL_WARN;
     }
-    _NMLOG(log_level,
-           "send-updates %s@%d failed: %s",
-           request_item->operation,
-           request_item->ifindex,
-           error->message);
+    _NMLOG(log_level, "send-updates %s@%d failed: %s", operation, ifindex, error->message);
+
+out_dec_pending:
+    nm_assert(priv->n_pending > 0);
+    if (--priv->n_pending <= 0) {
+        _update_pending_maybe_changed(self);
+        /* We keep @self alive while pending operations are in progress. It's simpler
+         * to implement. But this requires that we implement "stop()" signal to cancel
+         * all pending requests. Cancelling is necessary, because during shutdown,
+         * we must wrap up fast, and not hang an undefined amount time. */
+        g_object_unref(self);
+    }
 }
 
 static gboolean
-update_add_ip_config(NMDnsSystemdResolved *self,
-                     GVariantBuilder      *dns,
-                     GVariantBuilder      *domains,
-                     NMDnsConfigIPData    *ip_data)
+update_add_ip_config(NMDnsSystemdResolved    *self,
+                     GVariantBuilder         *dns,
+                     GVariantBuilder         *domains,
+                     const NMDnsConfigIPData *ip_data)
 {
     gsize         addr_size;
     guint         n;
@@ -258,8 +354,12 @@ update_add_ip_config(NMDnsSystemdResolved *self,
     addr_size = nm_utils_addr_family_to_size(ip_data->addr_family);
 
     if ((!ip_data->domains.search || !ip_data->domains.search[0])
-        && !ip_data->domains.has_default_route_exclusive && !ip_data->domains.has_default_route)
+        && !ip_data->domains.has_default_route_exclusive && !ip_data->domains.has_default_route) {
+        /* we have no search domain (which systemd-resolved uses to routing the request), but
+         * also the "DefaultRoute" is not set on the interface. This setting has no effect and
+         * gets ignored. */
         return FALSE;
+    }
 
     nameservers = nm_l3_config_data_get_nameservers(ip_data->l3cd, ip_data->addr_family, &n);
     for (i = 0; i < n; i++) {
@@ -295,23 +395,28 @@ free_pending_updates(NMDnsSystemdResolved *self)
     NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
     RequestItem                 *request_item;
 
-    while ((request_item =
-                c_list_first_entry(&priv->request_queue_lst_head, RequestItem, request_queue_lst)))
-        _request_item_free(request_item);
+    while (
+        (request_item =
+             c_list_first_entry(&priv->request_queue_lst_head, RequestItem, request_queue_lst))) {
+        c_list_unlink(&request_item->request_queue_lst);
+        _request_item_unref(request_item);
+    }
 }
 
 static gboolean
-prepare_one_interface(NMDnsSystemdResolved *self, InterfaceConfig *ic)
+prepare_one_interface(NMDnsSystemdResolved *self, const InterfaceConfig *ic)
 {
     GVariantBuilder               dns;
     GVariantBuilder               domains;
-    NMCListElem                  *elem;
-    NMSettingConnectionMdns       mdns         = NM_SETTING_CONNECTION_MDNS_DEFAULT;
-    NMSettingConnectionLlmnr      llmnr        = NM_SETTING_CONNECTION_LLMNR_DEFAULT;
-    NMSettingConnectionDnsOverTls dns_over_tls = NM_SETTING_CONNECTION_DNS_OVER_TLS_DEFAULT;
-    const char                   *mdns_arg = NULL, *llmnr_arg = NULL, *dns_over_tls_arg = NULL;
+    NMSettingConnectionMdns       mdns              = NM_SETTING_CONNECTION_MDNS_DEFAULT;
+    NMSettingConnectionLlmnr      llmnr             = NM_SETTING_CONNECTION_LLMNR_DEFAULT;
+    NMSettingConnectionDnsOverTls dns_over_tls      = NM_SETTING_CONNECTION_DNS_OVER_TLS_DEFAULT;
+    const char                   *mdns_arg          = NULL;
+    const char                   *llmnr_arg         = NULL;
+    const char                   *dns_over_tls_arg  = NULL;
     gboolean                      has_config        = FALSE;
     gboolean                      has_default_route = FALSE;
+    guint                         i;
 
     g_variant_builder_init(&dns, G_VARIANT_TYPE("(ia(iay))"));
     g_variant_builder_add(&dns, "i", ic->ifindex);
@@ -321,18 +426,22 @@ prepare_one_interface(NMDnsSystemdResolved *self, InterfaceConfig *ic)
     g_variant_builder_add(&domains, "i", ic->ifindex);
     g_variant_builder_open(&domains, G_VARIANT_TYPE("a(sb)"));
 
-    c_list_for_each_entry (elem, &ic->configs_lst_head, lst) {
-        NMDnsConfigIPData *ip_data = elem->data;
+    if (ic->ip_data_list) {
+        for (i = 0; i < ic->ip_data_list->len; i++) {
+            const NMDnsConfigIPData *ip_data = ic->ip_data_list->pdata[i];
 
-        has_config |= update_add_ip_config(self, &dns, &domains, ip_data);
+            if (update_add_ip_config(self, &dns, &domains, ip_data))
+                has_config = TRUE;
 
-        if (ip_data->domains.has_default_route)
-            has_default_route = TRUE;
+            if (ip_data->domains.has_default_route)
+                has_default_route = TRUE;
 
-        if (NM_IS_IPv4(ip_data->addr_family)) {
-            mdns         = NM_MAX(mdns, nm_l3_config_data_get_mdns(ip_data->l3cd));
-            llmnr        = NM_MAX(llmnr, nm_l3_config_data_get_llmnr(ip_data->l3cd));
-            dns_over_tls = NM_MAX(dns_over_tls, nm_l3_config_data_get_dns_over_tls(ip_data->l3cd));
+            if (NM_IS_IPv4(ip_data->addr_family)) {
+                mdns  = NM_MAX(mdns, nm_l3_config_data_get_mdns(ip_data->l3cd));
+                llmnr = NM_MAX(llmnr, nm_l3_config_data_get_llmnr(ip_data->l3cd));
+                dns_over_tls =
+                    NM_MAX(dns_over_tls, nm_l3_config_data_get_dns_over_tls(ip_data->l3cd));
+            }
         }
     }
 
@@ -413,33 +522,45 @@ prepare_one_interface(NMDnsSystemdResolved *self, InterfaceConfig *ic)
     return has_config;
 }
 
-static gboolean
-_ensure_resolved_running_timeout(gpointer user_data)
+static void
+start_resolved_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 {
-    NMDnsSystemdResolved              *self = user_data;
-    NMDnsSystemdResolvedPrivate       *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    gs_unref_variant GVariant         *res   = NULL;
+    gs_free_error GError              *error = NULL;
+    NMDnsSystemdResolved              *self;
+    NMDnsSystemdResolvedPrivate       *priv;
     NMDnsSystemdResolvedResolveHandle *handle;
 
-    nm_clear_g_source_inst(&priv->try_start_timeout_source);
+    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    self = user_data;
+    priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    nm_clear_g_cancellable(&priv->service_start_cancellable);
 
-    _LOGT("timeout waiting to D-Bus activate systemd-resolved. Systemd-resolved won't be "
-          "used until it appears on the bus");
+    if (!res) {
+        g_dbus_error_strip_remote_error(error);
+        _LOGD("error activating systemd-resolved: %s", error->message);
 
 again:
-    c_list_for_each_entry (handle, &priv->handle_lst_head, handle_lst) {
-        gs_free_error GError *error = NULL;
+        c_list_for_each_entry (handle, &priv->handle_lst_head, handle_lst) {
+            gs_free_error GError *local = NULL;
 
-        if (handle->is_failing_on_idle)
-            continue;
+            if (handle->is_failing_on_idle)
+                continue;
 
-        nm_utils_error_set_literal(&error,
-                                   NM_UTILS_ERROR_NOT_READY,
-                                   "timeout waiting for systemd-resolved to start");
-        _resolve_complete_error(handle, error);
-        goto again;
-    }
+            nm_utils_error_set(&local,
+                               NM_UTILS_ERROR_NOT_READY,
+                               "error activating systemd-resolved: %s",
+                               error->message);
+            _resolve_complete_error(handle, local);
+            goto again;
+        }
+    } else
+        _LOGD("systemd-resolved successfully started");
 
-    return G_SOURCE_CONTINUE;
+    _update_pending_maybe_changed(self);
 }
 
 static NMTernary
@@ -447,6 +568,9 @@ ensure_resolved_running(NMDnsSystemdResolved *self)
 {
     NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
 
+    if (priv->stopped)
+        return NM_TERNARY_FALSE;
+
     if (!priv->dbus_initied)
         return NM_TERNARY_DEFAULT;
 
@@ -459,16 +583,15 @@ ensure_resolved_running(NMDnsSystemdResolved *self)
 
         _LOGT("try D-Bus activating systemd-resolved...");
         priv->try_start_blocked = TRUE;
-
-        priv->try_start_timeout_source =
-            nm_g_timeout_add_source(4000, _ensure_resolved_running_timeout, self);
-
+        nm_clear_g_cancellable(&priv->service_start_cancellable);
+        priv->service_start_cancellable = g_cancellable_new();
         nm_dbus_connection_call_start_service_by_name(priv->dbus_connection,
                                                       SYSTEMD_RESOLVED_DBUS_SERVICE,
-                                                      -1,
-                                                      NULL,
-                                                      NULL,
-                                                      NULL);
+                                                      4000,
+                                                      priv->service_start_cancellable,
+                                                      start_resolved_cb,
+                                                      self);
+        _update_pending_maybe_changed(self);
         return NM_TERNARY_DEFAULT;
     }
 
@@ -523,6 +646,12 @@ send_updates(NMDnsSystemdResolved *self)
               request_item->operation,
               (ss = g_variant_print(request_item->argument, FALSE)));
 
+        if (priv->n_pending++ == 0) {
+            /* We are inside send_updates(). All callers are already calling
+             * _update_pending_maybe_changed() afterwards. */
+            g_object_ref(self);
+        }
+
         g_dbus_connection_call(priv->dbus_connection,
                                priv->dbus_owner,
                                SYSTEMD_RESOLVED_DBUS_PATH,
@@ -534,7 +663,7 @@ send_updates(NMDnsSystemdResolved *self)
                                -1,
                                priv->cancellable,
                                call_done,
-                               request_item);
+                               _request_item_ref(request_item));
     }
 
 start_resolve:
@@ -554,43 +683,54 @@ update(NMDnsPlugin             *plugin,
        const char              *hostdomain,
        GError                 **error)
 {
-    NMDnsSystemdResolved          *self            = NM_DNS_SYSTEMD_RESOLVED(plugin);
-    NMDnsSystemdResolvedPrivate   *priv            = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
-    gs_unref_hashtable GHashTable *interfaces      = NULL;
-    gs_free gpointer              *interfaces_keys = NULL;
+    NMDnsSystemdResolved          *self       = NM_DNS_SYSTEMD_RESOLVED(plugin);
+    NMDnsSystemdResolvedPrivate   *priv       = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    gs_unref_hashtable GHashTable *interfaces = NULL;
+    const NMUtilsNamedValue       *interfaces_arr;
+    NMUtilsNamedValue              interfaces_arr_stack[50];
+    gs_free NMUtilsNamedValue     *interfaces_arr_heap = NULL;
     guint                          interfaces_len;
-    int                            ifindex;
     gpointer                       pointer;
     NMDnsConfigIPData             *ip_data;
     GHashTableIter                 iter;
+    gs_unref_array GArray         *dirty_array = NULL;
     guint                          i;
 
+    nm_assert(!priv->stopped);
+
+    /* Group configs by ifindex/interfaces. */
     interfaces =
         g_hash_table_new_full(nm_direct_hash, NULL, NULL, (GDestroyNotify) _interface_config_free);
 
     c_list_for_each_entry (ip_data, ip_data_lst_head, ip_data_lst) {
-        InterfaceConfig *ic = NULL;
+        InterfaceConfig *ic      = NULL;
+        int              ifindex = ip_data->data->ifindex;
 
-        ifindex = ip_data->data->ifindex;
         nm_assert(ifindex == nm_l3_config_data_get_ifindex(ip_data->l3cd));
 
         ic = g_hash_table_lookup(interfaces, GINT_TO_POINTER(ifindex));
         if (!ic) {
-            ic          = g_slice_new(InterfaceConfig);
-            ic->ifindex = ifindex;
-            c_list_init(&ic->configs_lst_head);
+            ic  = g_slice_new(InterfaceConfig);
+            *ic = (InterfaceConfig){
+                .ifindex      = ifindex,
+                .ip_data_list = g_ptr_array_sized_new(4),
+            };
             g_hash_table_insert(interfaces, GINT_TO_POINTER(ifindex), ic);
         }
 
-        c_list_link_tail(&ic->configs_lst_head, &nm_c_list_elem_new_stale(ip_data)->lst);
+        g_ptr_array_add(ic->ip_data_list, ip_data);
     }
 
     free_pending_updates(self);
 
-    interfaces_keys =
-        nm_utils_hash_keys_to_array(interfaces, nm_cmp_int2ptr_p_with_data, NULL, &interfaces_len);
+    interfaces_arr = nm_utils_hash_to_array_with_buffer(interfaces,
+                                                        &interfaces_len,
+                                                        nm_cmp_int2ptr_p_with_data,
+                                                        NULL,
+                                                        interfaces_arr_stack,
+                                                        &interfaces_arr_heap);
     for (i = 0; i < interfaces_len; i++) {
-        InterfaceConfig *ic = g_hash_table_lookup(interfaces, GINT_TO_POINTER(interfaces_keys[i]));
+        const InterfaceConfig *ic = interfaces_arr[i].value_ptr;
 
         if (prepare_one_interface(self, ic))
             g_hash_table_add(priv->dirty_interfaces, GINT_TO_POINTER(ic->ifindex));
@@ -602,23 +742,38 @@ update(NMDnsPlugin             *plugin,
      * resolved, and the current update doesn't contain that interface,
      * reset the resolved configuration for that ifindex. */
     g_hash_table_iter_init(&iter, priv->dirty_interfaces);
-    while (g_hash_table_iter_next(&iter, (gpointer *) &pointer, NULL)) {
-        ifindex = GPOINTER_TO_INT(pointer);
-        if (!g_hash_table_contains(interfaces, GINT_TO_POINTER(ifindex))) {
+    while (g_hash_table_iter_next(&iter, &pointer, NULL)) {
+        int ifindex = GPOINTER_TO_INT(pointer);
+
+        if (g_hash_table_contains(interfaces, GINT_TO_POINTER(ifindex))) {
+            /* the interface is still tracked and still dirty. Keep. */
+            continue;
+        }
+
+        if (!dirty_array)
+            dirty_array = g_array_new(FALSE, FALSE, sizeof(int));
+        g_array_append_val(dirty_array, ifindex);
+
+        g_hash_table_iter_remove(&iter);
+    }
+    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);
             InterfaceConfig ic;
 
             _LOGT("clear previously configured ifindex %d", ifindex);
             ic = (InterfaceConfig){
-                .ifindex          = ifindex,
-                .configs_lst_head = C_LIST_INIT(ic.configs_lst_head),
+                .ifindex      = ifindex,
+                .ip_data_list = NULL,
             };
             prepare_one_interface(self, &ic);
-            g_hash_table_iter_remove(&iter);
         }
     }
 
     priv->send_updates_waiting = TRUE;
     send_updates(self);
+    _update_pending_maybe_changed(self);
     return TRUE;
 }
 
@@ -636,8 +791,7 @@ name_owner_changed(NMDnsSystemdResolved *self, const char *owner)
     else
         _LOGT("D-Bus name for systemd-resolved has owner %s", owner);
 
-    nm_clear_g_source_inst(&priv->try_start_timeout_source);
-
+    nm_clear_g_cancellable(&priv->service_start_cancellable);
     nm_strdup_reset(&priv->dbus_owner, owner);
 
     if (owner) {
@@ -649,6 +803,7 @@ name_owner_changed(NMDnsSystemdResolved *self, const char *owner)
     }
 
     send_updates(self);
+    _update_pending_maybe_changed(self);
 }
 
 static void
@@ -957,6 +1112,48 @@ nm_dns_systemd_resolved_resolve_cancel(NMDnsSystemdResolvedResolveHandle *handle
 /*****************************************************************************/
 
 static void
+stop(NMDnsPlugin *plugin)
+{
+    NMDnsSystemdResolved              *self = NM_DNS_SYSTEMD_RESOLVED(plugin);
+    NMDnsSystemdResolvedPrivate       *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
+    NMDnsSystemdResolvedResolveHandle *handle;
+
+    /* This function must be re-entrant!!
+     *
+     * Currently there is no concept of unregistering/shutting down. It's not
+     * clear whether we should de-configure anything in systemd-resolved, we
+     * don't.
+     *
+     * Implementing stop() is important because pending operations take a
+     * reference on @self. We can only cancel (fast shutdown) the instance
+     * by cancelling those requests. */
+
+    priv->stopped           = TRUE;
+    priv->try_start_blocked = TRUE;
+
+    nm_clear_g_cancellable(&priv->cancellable);
+
+    nm_clear_g_free(&priv->dbus_owner);
+
+    while ((handle = c_list_first_entry(&priv->handle_lst_head,
+                                        NMDnsSystemdResolvedResolveHandle,
+                                        handle_lst))) {
+        gs_free_error GError *error = NULL;
+
+        nm_utils_error_set_cancelled(&error, TRUE, "NMDnsSystemdResolved");
+        _resolve_complete_error(handle, error);
+    }
+
+    free_pending_updates(self);
+
+    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->name_owner_changed_id);
+
+    nm_clear_g_cancellable(&priv->service_start_cancellable);
+}
+
+/*****************************************************************************/
+
+static void
 nm_dns_systemd_resolved_init(NMDnsSystemdResolved *self)
 {
     NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
@@ -974,6 +1171,8 @@ nm_dns_systemd_resolved_init(NMDnsSystemdResolved *self)
         return;
     }
 
+    priv->update_pending = TRUE;
+
     priv->name_owner_changed_id =
         nm_dbus_connection_signal_subscribe_name_owner_changed(priv->dbus_connection,
                                                                SYSTEMD_RESOLVED_DBUS_SERVICE,
@@ -998,33 +1197,15 @@ nm_dns_systemd_resolved_new(void)
 static void
 dispose(GObject *object)
 {
-    NMDnsSystemdResolved              *self = NM_DNS_SYSTEMD_RESOLVED(object);
-    NMDnsSystemdResolvedPrivate       *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
-    NMDnsSystemdResolvedResolveHandle *handle;
-
-    while ((handle = c_list_first_entry(&priv->handle_lst_head,
-                                        NMDnsSystemdResolvedResolveHandle,
-                                        handle_lst))) {
-        gs_free_error GError *error = NULL;
-
-        nm_utils_error_set_cancelled(&error, TRUE, "NMDnsSystemdResolved");
-        _resolve_complete_error(handle, error);
-    }
-
-    free_pending_updates(self);
-
-    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->name_owner_changed_id);
-
-    nm_clear_g_cancellable(&priv->cancellable);
+    NMDnsSystemdResolved        *self = NM_DNS_SYSTEMD_RESOLVED(object);
+    NMDnsSystemdResolvedPrivate *priv = NM_DNS_SYSTEMD_RESOLVED_GET_PRIVATE(self);
 
-    nm_clear_g_source_inst(&priv->try_start_timeout_source);
+    stop(NM_DNS_PLUGIN(self));
 
     g_clear_object(&priv->dbus_connection);
-    nm_clear_pointer(&priv->dirty_interfaces, g_hash_table_unref);
+    nm_clear_pointer(&priv->dirty_interfaces, g_hash_table_destroy);
 
     G_OBJECT_CLASS(nm_dns_systemd_resolved_parent_class)->dispose(object);
-
-    nm_clear_g_free(&priv->dbus_owner);
 }
 
 static void
@@ -1035,7 +1216,9 @@ nm_dns_systemd_resolved_class_init(NMDnsSystemdResolvedClass *dns_class)
 
     object_class->dispose = dispose;
 
-    plugin_class->plugin_name = "systemd-resolved";
-    plugin_class->is_caching  = TRUE;
-    plugin_class->update      = update;
+    plugin_class->plugin_name        = "systemd-resolved";
+    plugin_class->is_caching         = TRUE;
+    plugin_class->stop               = stop;
+    plugin_class->update             = update;
+    plugin_class->get_update_pending = get_update_pending;
 }
diff --git a/src/core/dns/nm-dns-unbound.c b/src/core/dns/nm-dns-unbound.c
deleted file mode 100644
index 8a75cf08..00000000
--- a/src/core/dns/nm-dns-unbound.c
+++ /dev/null
@@ -1,84 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/*
- * Copyright (C) 2014 Red Hat, Inc.
- * Author: Pavel Å imerda <psimerda@redhat.com>
- */
-
-#include "src/core/nm-default-daemon.h"
-
-#include "nm-dns-unbound.h"
-
-#include "NetworkManagerUtils.h"
-
-/*****************************************************************************/
-
-struct _NMDnsUnbound {
-    NMDnsPlugin parent;
-};
-
-struct _NMDnsUnboundClass {
-    NMDnsPluginClass parent;
-};
-
-G_DEFINE_TYPE(NMDnsUnbound, nm_dns_unbound, NM_TYPE_DNS_PLUGIN)
-
-/*****************************************************************************/
-
-static gboolean
-update(NMDnsPlugin             *plugin,
-       const NMGlobalDnsConfig *global_config,
-       const CList             *ip_config_lst_head,
-       const char              *hostdomain,
-       GError                 **error)
-{
-    char                 *argv[] = {DNSSEC_TRIGGER_PATH, "--async", "--update", NULL};
-    gs_free_error GError *local  = NULL;
-    int                   status;
-
-    /* TODO: We currently call a script installed with the dnssec-trigger
-     * package that queries all information itself. Later, the dependency
-     * on that package will be optional and the only hard dependency will
-     * be unbound.
-     *
-     * Unbound configuration should be later handled by this plugin directly,
-     * without calling custom scripts. The dnssec-trigger functionality
-     * may be eventually merged into NetworkManager.
-     */
-    if (!g_spawn_sync("/", argv, NULL, 0, NULL, NULL, NULL, NULL, &status, &local)) {
-        nm_utils_error_set(error,
-                           NM_UTILS_ERROR_UNKNOWN,
-                           "error spawning dns-trigger: %s",
-                           local->message);
-        return FALSE;
-    }
-    if (status != 0) {
-        nm_utils_error_set(error,
-                           NM_UTILS_ERROR_UNKNOWN,
-                           "dns-trigger exited with error code %d",
-                           status);
-        return FALSE;
-    }
-    return TRUE;
-}
-
-/*****************************************************************************/
-
-static void
-nm_dns_unbound_init(NMDnsUnbound *unbound)
-{}
-
-NMDnsPlugin *
-nm_dns_unbound_new(void)
-{
-    return g_object_new(NM_TYPE_DNS_UNBOUND, NULL);
-}
-
-static void
-nm_dns_unbound_class_init(NMDnsUnboundClass *klass)
-{
-    NMDnsPluginClass *plugin_class = NM_DNS_PLUGIN_CLASS(klass);
-
-    plugin_class->plugin_name = "unbound";
-    plugin_class->is_caching  = TRUE;
-    plugin_class->update      = update;
-}
diff --git a/src/core/dns/nm-dns-unbound.h b/src/core/dns/nm-dns-unbound.h
deleted file mode 100644
index feb33099..00000000
--- a/src/core/dns/nm-dns-unbound.h
+++ /dev/null
@@ -1,27 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/*
- * Copyright (C) 2014 Red Hat, Inc.
- */
-
-#ifndef __NETWORKMANAGER_DNS_UNBOUND_H__
-#define __NETWORKMANAGER_DNS_UNBOUND_H__
-
-#include "nm-dns-plugin.h"
-
-#define NM_TYPE_DNS_UNBOUND (nm_dns_unbound_get_type())
-#define NM_DNS_UNBOUND(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DNS_UNBOUND, NMDnsUnbound))
-#define NM_DNS_UNBOUND_CLASS(klass) \
-    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DNS_UNBOUND, NMDnsUnboundClass))
-#define NM_IS_DNS_UNBOUND(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DNS_UNBOUND))
-#define NM_IS_DNS_UNBOUND_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DNS_UNBOUND))
-#define NM_DNS_UNBOUND_GET_CLASS(obj) \
-    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DNS_UNBOUND, NMDnsUnboundClass))
-
-typedef struct _NMDnsUnbound      NMDnsUnbound;
-typedef struct _NMDnsUnboundClass NMDnsUnboundClass;
-
-GType nm_dns_unbound_get_type(void);
-
-NMDnsPlugin *nm_dns_unbound_new(void);
-
-#endif /* __NETWORKMANAGER_DNS_UNBOUND_H__ */
diff --git a/src/core/main.c b/src/core/main.c
index 1a9c3a65..2eb230d9 100644
--- a/src/core/main.c
+++ b/src/core/main.c
@@ -293,6 +293,7 @@ main(int argc, char *argv[])
     GError                 *error_invalid_logging_config = NULL;
     const char *const      *warnings;
     int                     errsv;
+    gboolean                has_logging = FALSE;
 
     _nm_utils_is_manager_process = TRUE;
 
@@ -354,10 +355,14 @@ main(int argc, char *argv[])
         g_free(path);
     }
 
-    if (!nm_logging_setup(global_opt.opt_log_level,
-                          global_opt.opt_log_domains,
-                          &bad_domains,
-                          &error)) {
+    if (nm_config_kernel_command_line_nm_debug()) {
+        /* we honor kernel command line. If "nm.debug" is set, we always enable trace logging. */
+        nm_logging_setup("TRACE", "ALL", NULL, NULL);
+        has_logging = TRUE;
+    } else if (!nm_logging_setup(global_opt.opt_log_level,
+                                 global_opt.opt_log_domains,
+                                 &bad_domains,
+                                 &error)) {
         fprintf(stderr,
                 _("%s.  Please use --help to see a list of valid options.\n"),
                 error->message);
@@ -379,7 +384,7 @@ main(int argc, char *argv[])
     /* Initialize logging from config file *only* if not explicitly
      * specified by commandline.
      */
-    if (global_opt.opt_log_level == NULL && global_opt.opt_log_domains == NULL) {
+    if (!has_logging && !global_opt.opt_log_level && !global_opt.opt_log_domains) {
         if (!nm_logging_setup(nm_config_get_log_level(config),
                               nm_config_get_log_domains(config),
                               &bad_domains,
@@ -417,9 +422,10 @@ main(int argc, char *argv[])
     }
 
     nm_log_info(LOGD_CORE,
-                "NetworkManager (version " NM_DIST_VERSION ") is starting... (%s%s)",
-                nm_config_get_first_start(config) ? "for the first time" : "after a restart",
-                NM_MORE_ASSERTS != 0 ? ", asserts:" G_STRINGIFY(NM_MORE_ASSERTS) : "");
+                "NetworkManager (version " NM_DIST_VERSION ") is starting... (%s%sboot:%s)",
+                nm_config_get_first_start(config) ? "" : "after a restart, ",
+                NM_MORE_ASSERTS != 0 ? "asserts:" G_STRINGIFY(NM_MORE_ASSERTS) ", " : "",
+                nm_utils_boot_id_str());
 
     nm_log_info(LOGD_CORE,
                 "Read config: %s",
diff --git a/src/core/meson.build b/src/core/meson.build
index 2148d23b..f3359ad0 100644
--- a/src/core/meson.build
+++ b/src/core/meson.build
@@ -123,7 +123,6 @@ libNetworkManager = static_library(
     'dns/nm-dns-manager.c',
     'dns/nm-dns-plugin.c',
     'dns/nm-dns-systemd-resolved.c',
-    'dns/nm-dns-unbound.c',
     'dnsmasq/nm-dnsmasq-manager.c',
     'dnsmasq/nm-dnsmasq-utils.c',
     'ppp/nm-ppp-manager-call.c',
diff --git a/src/core/nm-config-data.c b/src/core/nm-config-data.c
index ddb7787f..0f512e38 100644
--- a/src/core/nm-config-data.c
+++ b/src/core/nm-config-data.c
@@ -839,6 +839,13 @@ nm_config_data_log(const NMConfigData  *self,
             _LOG(stream, prefix, "# no-auto-default specs \"%s\"", msg);
     }
 
+    if (nm_config_kernel_command_line_nm_debug()) {
+        _LOG(stream,
+             prefix,
+             "# /proc/cmdline contains \"" NM_CONFIG_KERNEL_CMDLINE_NM_DEBUG
+             "\". Debug log enabled");
+    }
+
 #undef _LOG
 }
 
@@ -1681,7 +1688,7 @@ nm_config_data_get_connection_default_int64(const NMConfigData *self,
     const char *value;
 
     value = nm_config_data_get_connection_default(self, property, device);
-    return _nm_utils_ascii_str_to_int64(value, 10, min, max, fallback);
+    return _nm_utils_ascii_str_to_int64(value, 0, min, max, fallback);
 }
 
 static const char *
diff --git a/src/core/nm-config.c b/src/core/nm-config.c
index ea1c2ab3..3d23d4f6 100644
--- a/src/core/nm-config.c
+++ b/src/core/nm-config.c
@@ -11,7 +11,9 @@
 #include <stdio.h>
 
 #include "nm-utils.h"
+#include "nm-dhcp-config.h"
 #include "devices/nm-device.h"
+#include "dhcp/nm-dhcp-options.h"
 #include "NetworkManagerUtils.h"
 #include "libnm-core-intern/nm-core-internal.h"
 #include "libnm-core-intern/nm-keyfile-internal.h"
@@ -573,93 +575,93 @@ nm_config_cmd_line_options_add_to_entries(NMConfigCmdLineOptions *cli, GOptionCo
 {
     GOptionGroup *group;
     GOptionEntry  config_options[] = {
-        {"config",
-         0,
-         0,
-         G_OPTION_ARG_FILENAME,
-         &cli->config_main_file,
-         N_("Config file location"),
-         DEFAULT_CONFIG_MAIN_FILE},
-        {"config-dir",
-         0,
-         0,
-         G_OPTION_ARG_FILENAME,
-         &cli->config_dir,
-         N_("Config directory location"),
-         DEFAULT_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",
-         0,
-         0,
-         G_OPTION_ARG_FILENAME,
-         &cli->intern_config_file,
-         N_("Internal config file location"),
-         DEFAULT_INTERN_CONFIG_FILE},
-        {"state-file",
-         0,
-         0,
-         G_OPTION_ARG_FILENAME,
-         &cli->state_file,
-         N_("State file location"),
-         DEFAULT_STATE_FILE},
-        {"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",
-         0,
-         0,
-         G_OPTION_ARG_STRING,
-         &cli->plugins,
-         N_("List of plugins separated by ','"),
-         NM_CONFIG_DEFAULT_MAIN_PLUGINS},
-        {"configure-and-quit",
-         0,
-         G_OPTION_FLAG_OPTIONAL_ARG,
-         G_OPTION_ARG_CALLBACK,
-         parse_configure_and_quit,
-         N_("Quit after initial configuration"),
-         NULL},
-        {"debug",
-         'd',
-         0,
-         G_OPTION_ARG_NONE,
-         &cli->is_debug,
-         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",
-         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",
-         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",
-         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},
+         {"config",
+          0,
+          0,
+          G_OPTION_ARG_FILENAME,
+          &cli->config_main_file,
+          N_("Config file location"),
+          DEFAULT_CONFIG_MAIN_FILE},
+         {"config-dir",
+          0,
+          0,
+          G_OPTION_ARG_FILENAME,
+          &cli->config_dir,
+          N_("Config directory location"),
+          DEFAULT_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",
+          0,
+          0,
+          G_OPTION_ARG_FILENAME,
+          &cli->intern_config_file,
+          N_("Internal config file location"),
+          DEFAULT_INTERN_CONFIG_FILE},
+         {"state-file",
+          0,
+          0,
+          G_OPTION_ARG_FILENAME,
+          &cli->state_file,
+          N_("State file location"),
+          DEFAULT_STATE_FILE},
+         {"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",
+          0,
+          0,
+          G_OPTION_ARG_STRING,
+          &cli->plugins,
+          N_("List of plugins separated by ','"),
+          NM_CONFIG_DEFAULT_MAIN_PLUGINS},
+         {"configure-and-quit",
+          0,
+          G_OPTION_FLAG_OPTIONAL_ARG,
+          G_OPTION_ARG_CALLBACK,
+          parse_configure_and_quit,
+          N_("Quit after initial configuration"),
+          NULL},
+         {"debug",
+          'd',
+          0,
+          G_OPTION_ARG_NONE,
+          &cli->is_debug,
+          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",
+          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",
+          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",
+          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},
     };
 
     g_return_if_fail(opt_ctx);
@@ -2573,13 +2575,16 @@ nm_config_device_state_write(int                            ifindex,
                              NMTernary                      nm_owned,
                              guint32                        route_metric_default_aspired,
                              guint32                        route_metric_default_effective,
-                             const char                    *next_server,
-                             const char                    *root_path,
-                             const char                    *dhcp_bootfile)
+                             NMDhcpConfig                  *dhcp4_config,
+                             NMDhcpConfig                  *dhcp6_config)
 {
     char    path[NM_STRLEN(NM_CONFIG_DEVICE_STATE_DIR "/") + DEVICE_STATE_FILENAME_LEN_MAX + 1];
-    GError *local                      = NULL;
-    nm_auto_unref_keyfile GKeyFile *kf = NULL;
+    GError *local                                 = NULL;
+    nm_auto_unref_keyfile GKeyFile *kf            = NULL;
+    const char                     *root_path     = NULL;
+    const char                     *next_server   = NULL;
+    const char                     *dhcp_bootfile = NULL;
+    int                             IS_IPv4;
 
     g_return_val_if_fail(ifindex > 0, FALSE);
     g_return_val_if_fail(!connection_uuid || *connection_uuid, FALSE);
@@ -2630,6 +2635,15 @@ nm_config_device_state_write(int                            ifindex,
                                  route_metric_default_aspired);
         }
     }
+
+    if (dhcp4_config) {
+        next_server   = nm_dhcp_config_get_option(dhcp4_config, "next_server");
+        root_path     = nm_dhcp_config_get_option(dhcp4_config, "root_path");
+        dhcp_bootfile = nm_dhcp_config_get_option(dhcp4_config, "filename");
+        if (!dhcp_bootfile)
+            dhcp_bootfile = nm_dhcp_config_get_option(dhcp4_config, "bootfile_name");
+    }
+
     if (next_server) {
         g_key_file_set_string(kf,
                               DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
@@ -2649,6 +2663,28 @@ nm_config_device_state_write(int                            ifindex,
                               dhcp_bootfile);
     }
 
+    for (IS_IPv4 = 1; IS_IPv4 >= 0; IS_IPv4--) {
+        NMDhcpConfig              *dhcp_config = IS_IPv4 ? dhcp4_config : dhcp6_config;
+        gs_free NMUtilsNamedValue *values      = NULL;
+        guint                      i;
+        guint                      num;
+
+        if (!dhcp_config)
+            continue;
+
+        values = nm_dhcp_config_get_option_values(dhcp_config, &num);
+        for (i = 0; i < num; i++) {
+            gs_free char *name_full = NULL;
+            const char   *prefix    = IS_IPv4 ? "dhcp4" : "dhcp6";
+
+            if (NM_STR_HAS_PREFIX(values[i].name, NM_DHCP_OPTION_REQPREFIX))
+                continue;
+
+            name_full = g_strdup_printf("%s.%s", prefix, values[i].name);
+            g_key_file_set_string(kf, prefix, name_full, values[i].value_str);
+        }
+    }
+
     if (!g_key_file_save_to_file(kf, path, &local)) {
         _LOGW("device-state: write #%d (%s) failed: %s", ifindex, path, local->message);
         g_error_free(local);
@@ -2978,6 +3014,15 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps
 
 /*****************************************************************************/
 
+gboolean
+nm_config_kernel_command_line_nm_debug(void)
+{
+    return (nm_strv_find_first(nm_utils_proc_cmdline_split(), -1, NM_CONFIG_KERNEL_CMDLINE_NM_DEBUG)
+            >= 0);
+}
+
+/*****************************************************************************/
+
 static gboolean
 init_sync(GInitable *initable, GCancellable *cancellable, GError **error)
 {
diff --git a/src/core/nm-config.h b/src/core/nm-config.h
index 2c23ff20..0dd159f4 100644
--- a/src/core/nm-config.h
+++ b/src/core/nm-config.h
@@ -18,6 +18,8 @@
 #define NM_IS_CONFIG_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_CONFIG))
 #define NM_CONFIG_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_CONFIG, NMConfigClass))
 
+#define NM_CONFIG_KERNEL_CMDLINE_NM_DEBUG "nm.debug"
+
 /* Properties */
 #define NM_CONFIG_CMD_LINE_OPTIONS        "cmd-line-options"
 #define NM_CONFIG_ATOMIC_SECTION_PREFIXES "atomic-section-prefixes"
@@ -183,9 +185,8 @@ gboolean                 nm_config_device_state_write(int
                                                       NMTernary                      nm_owned,
                                                       guint32                        route_metric_default_aspired,
                                                       guint32                        route_metric_default_effective,
-                                                      const char                    *next_server,
-                                                      const char                    *root_path,
-                                                      const char                    *dhcp_bootfile);
+                                                      NMDhcpConfig                  *dhcp4_config,
+                                                      NMDhcpConfig                  *dhcp6_config);
 
 void nm_config_device_state_prune_stale(GHashTable *preserve_ifindexes,
                                         NMPlatform *preserve_in_platform);
@@ -198,4 +199,8 @@ void               nm_config_clear_warnings(NMConfig *config);
 
 /*****************************************************************************/
 
+gboolean nm_config_kernel_command_line_nm_debug(void);
+
+/*****************************************************************************/
+
 #endif /* __NETWORKMANAGER_CONFIG_H__ */
diff --git a/src/core/nm-connectivity.c b/src/core/nm-connectivity.c
index eea8adb2..25ac3c7f 100644
--- a/src/core/nm-connectivity.c
+++ b/src/core/nm-connectivity.c
@@ -816,8 +816,7 @@ system_resolver_resolve_cb(GObject *source_object, GAsyncResult *res, gpointer u
             addr_family = AF_INET6;
             break;
         default:
-            addr_family = AF_UNSPEC;
-            break;
+            goto next;
         }
         if (cb_data->addr_family != AF_UNSPEC && cb_data->addr_family != addr_family)
             continue;
@@ -832,6 +831,7 @@ system_resolver_resolve_cb(GObject *source_object, GAsyncResult *res, gpointer u
 
         nm_str_buf_append(&strbuf_hosts,
                           nm_utils_inet_ntop(addr_family, g_inet_address_to_bytes(a), str_addr));
+next:;
     }
 
     g_list_free_full(list, g_object_unref);
diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c
index 60c286ee..480e9b28 100644
--- a/src/core/nm-core-utils.c
+++ b/src/core/nm-core-utils.c
@@ -30,7 +30,6 @@
 #include "libnm-glib-aux/nm-secret-utils.h"
 #include "libnm-glib-aux/nm-time-utils.h"
 #include "libnm-glib-aux/nm-str-buf.h"
-#include "libnm-systemd-shared/nm-sd-utils-shared.h"
 #include "nm-utils.h"
 #include "libnm-core-intern/nm-core-internal.h"
 #include "nm-setting-connection.h"
@@ -3094,7 +3093,10 @@ again:
     if (G_UNLIKELY(!proc_cmdline)) {
         gs_free char *str = NULL;
 
-        g_file_get_contents("/proc/cmdline", &str, NULL, NULL);
+        /* /run/NetworkManager/proc-cmdline can be used to overrule /proc/cmdline. */
+        if (!g_file_get_contents(NMRUNDIR "/proc-cmdline", &str, NULL, NULL))
+            g_file_get_contents("/proc/cmdline", &str, NULL, NULL);
+
         str = nm_str_realloc(str);
 
         proc_cmdline = str ?: "";
@@ -5117,7 +5119,7 @@ nm_utils_spawn_helper(const char *const  *args,
     fcntl(info->child_stdout, F_SETFL, fd_flags | O_NONBLOCK);
 
     /* Watch process stdin */
-    nm_str_buf_init(&info->out_buffer, 32, TRUE);
+    info->out_buffer = NM_STR_BUF_INIT(32, TRUE);
     for (arg = args; *arg; arg++) {
         nm_str_buf_append(&info->out_buffer, *arg);
         nm_str_buf_append_c(&info->out_buffer, '\0');
@@ -5131,7 +5133,7 @@ nm_utils_spawn_helper(const char *const  *args,
     g_source_attach(info->output_source, g_main_context_get_thread_default());
 
     /* Watch process stdout */
-    nm_str_buf_init(&info->in_buffer, NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, FALSE);
+    info->in_buffer    = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, FALSE);
     info->input_source = nm_g_unix_fd_source_new(info->child_stdout,
                                                  G_IO_IN | G_IO_ERR | G_IO_HUP,
                                                  G_PRIORITY_DEFAULT,
@@ -5226,7 +5228,7 @@ again:
  * @shortened: (out) (transfer full): on return, the shortened hostname
  *
  * Checks whether the input hostname is valid. If not, tries to shorten it
- * to HOST_NAME_MAX or to the first dot, whatever comes earlier.
+ * to HOST_NAME_MAX (64) or to the first dot, whatever comes earlier.
  * The new hostname is returned in @shortened.
  *
  * Returns: %TRUE if the input hostname was already valid or if was shortened
@@ -5242,7 +5244,7 @@ nm_utils_shorten_hostname(const char *hostname, char **shortened)
     nm_assert(hostname);
     nm_assert(shortened);
 
-    if (nm_sd_hostname_is_valid(hostname, FALSE)) {
+    if (nm_hostname_is_valid(hostname, FALSE)) {
         *shortened = NULL;
         return TRUE;
     }
@@ -5252,11 +5254,11 @@ nm_utils_shorten_hostname(const char *hostname, char **shortened)
         l = (dot - hostname);
     else
         l = strlen(hostname);
-    l = MIN(l, (gsize) HOST_NAME_MAX);
+    l = MIN(l, (gsize) NM_HOST_NAME_MAX);
 
     s = g_strndup(hostname, l);
 
-    if (!nm_sd_hostname_is_valid(s, FALSE)) {
+    if (!nm_hostname_is_valid(s, FALSE)) {
         *shortened = NULL;
         return FALSE;
     }
diff --git a/src/core/nm-dhcp-config.c b/src/core/nm-dhcp-config.c
index b32c9b70..394a3a49 100644
--- a/src/core/nm-dhcp-config.c
+++ b/src/core/nm-dhcp-config.c
@@ -125,6 +125,29 @@ nm_dhcp_config_set_lease(NMDhcpConfig *self, const NML3ConfigData *l3cd)
     _notify(self, PROP_OPTIONS);
 }
 
+NMUtilsNamedValue *
+nm_dhcp_config_get_option_values(NMDhcpConfig *self, guint *num)
+{
+    NMDhcpConfigPrivate *priv = NM_DHCP_CONFIG_GET_PRIVATE(self);
+    NMDhcpLease         *lease;
+    NMUtilsNamedValue   *buffer = NULL;
+
+    if (!priv->l3cd) {
+        NM_SET_OUT(num, 0);
+        return NULL;
+    }
+
+    lease = nm_l3_config_data_get_dhcp_lease(priv->l3cd, nm_dhcp_config_get_addr_family(self));
+    nm_utils_named_values_from_strdict_full(nm_dhcp_lease_get_options(lease),
+                                            num,
+                                            nm_strcmp_p_with_data,
+                                            NULL,
+                                            NULL,
+                                            0,
+                                            &buffer);
+    return buffer;
+}
+
 const char *
 nm_dhcp_config_get_option(NMDhcpConfig *self, const char *key)
 {
diff --git a/src/core/nm-dhcp-config.h b/src/core/nm-dhcp-config.h
index cae0e11a..cdca4ef1 100644
--- a/src/core/nm-dhcp-config.h
+++ b/src/core/nm-dhcp-config.h
@@ -30,7 +30,8 @@ int nm_dhcp_config_get_addr_family(NMDhcpConfig *self);
 
 void nm_dhcp_config_set_lease(NMDhcpConfig *self, const NML3ConfigData *l3cd);
 
-const char *nm_dhcp_config_get_option(NMDhcpConfig *self, const char *option);
+NMUtilsNamedValue *nm_dhcp_config_get_option_values(NMDhcpConfig *self, guint *num);
+const char        *nm_dhcp_config_get_option(NMDhcpConfig *self, const char *option);
 
 GVariant *nm_dhcp_config_get_options(NMDhcpConfig *self);
 
diff --git a/src/core/nm-dispatcher.c b/src/core/nm-dispatcher.c
index b6ff25ca..d3529ee4 100644
--- a/src/core/nm-dispatcher.c
+++ b/src/core/nm-dispatcher.c
@@ -56,6 +56,7 @@ struct NMDispatcherCallId {
     gpointer           user_data;
     const char        *log_ifname;
     const char        *log_con_uuid;
+    gint64             start_at_msec;
     NMDispatcherAction action;
     guint              idle_id;
     guint32            request_id;
@@ -82,6 +83,7 @@ static struct {
 
 static NMDispatcherCallId *
 dispatcher_call_id_new(guint32            request_id,
+                       gint64             start_at_msec,
                        NMDispatcherAction action,
                        NMDispatcherFunc   callback,
                        gpointer           user_data,
@@ -98,11 +100,12 @@ dispatcher_call_id_new(guint32            request_id,
 
     call_id = g_malloc(sizeof(NMDispatcherCallId) + l_log_ifname + l_log_con_uuid);
 
-    call_id->action     = action;
-    call_id->request_id = request_id;
-    call_id->callback   = callback;
-    call_id->user_data  = user_data;
-    call_id->idle_id    = 0;
+    call_id->action        = action;
+    call_id->start_at_msec = start_at_msec;
+    call_id->request_id    = request_id;
+    call_id->callback      = callback;
+    call_id->user_data     = user_data;
+    call_id->idle_id       = 0;
 
     extra_strings = &call_id->extra_strings[0];
 
@@ -366,6 +369,8 @@ dispatch_result_to_string(DispatchResult result)
 
 static void
 dispatcher_results_process(guint32     request_id,
+                           gint64      start_at_msec,
+                           gint64      now_msec,
                            const char *log_ifname,
                            const char *log_con_uuid,
                            GVariant   *v_results)
@@ -373,13 +378,22 @@ dispatcher_results_process(guint32     request_id,
     nm_auto_free_variant_iter GVariantIter *results = NULL;
     const char                             *script, *err;
     guint32                                 result;
+    gsize                                   n_children;
 
     g_variant_get(v_results, "(a(sus))", &results);
 
-    if (g_variant_iter_n_children(results) == 0) {
-        _LOG2D(request_id, log_ifname, log_con_uuid, "succeeded but no scripts invoked");
+    n_children = g_variant_iter_n_children(results);
+
+    _LOG2D(request_id,
+           log_ifname,
+           log_con_uuid,
+           "succeeded (after %ld.%03d sec, %zu scripts invoked)",
+           (long int) ((now_msec - start_at_msec) / 1000),
+           (int) ((now_msec - start_at_msec) % 1000),
+           n_children);
+
+    if (n_children == 0)
         return;
-    }
 
     while (g_variant_iter_next(results, "(&su&s)", &script, &result, &err)) {
         if (result == DISPATCH_RESULT_SUCCESS) {
@@ -402,19 +416,30 @@ dispatcher_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
     gs_unref_variant GVariant *ret     = NULL;
     gs_free_error GError      *error   = NULL;
     NMDispatcherCallId        *call_id = user_data;
+    gint64                     now_msec;
 
     nm_assert((gpointer) source == gl.dbus_connection);
 
+    now_msec = nm_utils_get_monotonic_timestamp_msec();
+
     ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
     if (!ret) {
+        NMLogLevel log_level = LOGL_DEBUG;
+
         if (_nm_dbus_error_has_name(error, "org.freedesktop.systemd1.LoadFailed")) {
             g_dbus_error_strip_remote_error(error);
-            _LOG3W(call_id, "failed to call dispatcher scripts: %s", error->message);
-        } else {
-            _LOG3D(call_id, "failed to call dispatcher scripts: %s", error->message);
+            log_level = LOGL_WARN;
         }
+        _NMLOG3(log_level,
+                call_id,
+                "failed to call dispatcher scripts (after %ld.%03d sec): %s",
+                (long int) ((now_msec - call_id->start_at_msec) / 1000),
+                (int) ((now_msec - call_id->start_at_msec) % 1000),
+                error->message);
     } else {
         dispatcher_results_process(call_id->request_id,
+                                   call_id->start_at_msec,
+                                   now_msec,
                                    call_id->log_ifname,
                                    call_id->log_con_uuid,
                                    ret);
@@ -481,6 +506,8 @@ _dispatcher_call(NMDispatcherAction    action,
     const char                *connectivity_state_string = "UNKNOWN";
     const char                *log_ifname;
     const char                *log_con_uuid;
+    gint64                     start_at_msec;
+    gint64                     now_msec;
 
     g_return_val_if_fail(!blocking || (!callback && !user_data), FALSE);
 
@@ -596,6 +623,8 @@ _dispatcher_call(NMDispatcherAction    action,
                       &vpn_ip6_props,
                       nm_logging_enabled(LOGL_DEBUG, LOGD_DISPATCH));
 
+    start_at_msec = nm_utils_get_monotonic_timestamp_msec();
+
     /* Send the action to the dispatcher */
     if (blocking) {
         gs_unref_variant GVariant *ret   = NULL;
@@ -612,17 +641,36 @@ _dispatcher_call(NMDispatcherAction    action,
                                           CALL_TIMEOUT,
                                           NULL,
                                           &error);
+
+        now_msec = nm_utils_get_monotonic_timestamp_msec();
+
         if (!ret) {
             g_dbus_error_strip_remote_error(error);
-            _LOG2W(request_id, log_ifname, log_con_uuid, "failed: %s", error->message);
+            _LOG2W(request_id,
+                   log_ifname,
+                   log_con_uuid,
+                   "failed (after %ld.%03d sec): %s",
+                   (long int) ((now_msec - start_at_msec) / 1000),
+                   (int) ((now_msec - start_at_msec) % 1000),
+                   error->message);
             return FALSE;
         }
-        dispatcher_results_process(request_id, log_ifname, log_con_uuid, ret);
+        dispatcher_results_process(request_id,
+                                   start_at_msec,
+                                   now_msec,
+                                   log_ifname,
+                                   log_con_uuid,
+                                   ret);
         return TRUE;
     }
 
-    call_id =
-        dispatcher_call_id_new(request_id, action, callback, user_data, log_ifname, log_con_uuid);
+    call_id = dispatcher_call_id_new(request_id,
+                                     start_at_msec,
+                                     action,
+                                     callback,
+                                     user_data,
+                                     log_ifname,
+                                     log_con_uuid);
 
     g_dbus_connection_call(gl.dbus_connection,
                            NM_DISPATCHER_DBUS_SERVICE,
diff --git a/src/core/nm-firewall-utils.c b/src/core/nm-firewall-utils.c
index 8fa8059e..1311f503 100644
--- a/src/core/nm-firewall-utils.c
+++ b/src/core/nm-firewall-utils.c
@@ -679,8 +679,7 @@ _fw_nft_set(gboolean add, const char *ip_iface, in_addr_t addr, guint8 plen)
                                               NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL,
                                               &ss1));
 
-    stdin_buf = g_bytes_new_static(nm_str_buf_get_str(&strbuf), strbuf.len);
-
+    stdin_buf = nm_str_buf_finalize_to_gbytes(&strbuf);
     _fw_nft_call_sync(stdin_buf, NULL);
 }
 
diff --git a/src/core/nm-hostname-manager.c b/src/core/nm-hostname-manager.c
index 64c2531e..ba0c6c9d 100644
--- a/src/core/nm-hostname-manager.c
+++ b/src/core/nm-hostname-manager.c
@@ -239,7 +239,7 @@ _set_hostname_read_file(NMHostnameManager *self)
 #if defined(HOSTNAME_PERSIST_GENTOO)
     hostname = read_hostname_gentoo(HOSTNAME_FILE);
 #elif defined(HOSTNAME_PERSIST_SLACKWARE)
-    hostname     = read_hostname_slackware(HOSTNAME_FILE);
+    hostname = read_hostname_slackware(HOSTNAME_FILE);
 #else
     if (g_file_get_contents(HOSTNAME_FILE, &hostname, NULL, NULL))
         g_strchomp(hostname);
@@ -321,39 +321,50 @@ nm_hostname_manager_get_transient_hostname(NMHostnameManager *self, char **hostn
     return TRUE;
 }
 
-gboolean
-nm_hostname_manager_write_hostname(NMHostnameManager *self, const char *hostname)
+/*****************************************************************************/
+
+static void
+_write_hostname_dbus_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    gs_unref_object GTask     *task  = G_TASK(user_data);
+    gs_unref_variant GVariant *res   = NULL;
+    GError                    *error = NULL;
+
+    res = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, &error);
+    if (!res) {
+        g_task_return_error(task, error);
+        return;
+    }
+    g_task_return_boolean(task, TRUE);
+}
+
+static void
+_write_hostname_on_idle_cb(gpointer user_data, GCancellable *cancellable)
 {
-    NMHostnameManagerPrivate  *priv;
-    char                      *hostname_eol;
-    gboolean                   ret;
-    gs_free_error GError      *error     = NULL;
-    const char                *file      = HOSTNAME_FILE;
-    gs_free char              *link_path = NULL;
-    gs_unref_variant GVariant *var       = NULL;
-    struct stat                file_stat;
+    gs_unref_object GTask    *task = G_TASK(user_data);
+    NMHostnameManager        *self;
+    NMHostnameManagerPrivate *priv;
+    const char               *hostname;
+    gs_free char             *hostname_eol = NULL;
+    gboolean                  ret;
+    gs_free_error GError     *error     = NULL;
+    const char               *file      = HOSTNAME_FILE;
+    gs_free char             *link_path = NULL;
+    struct stat               file_stat;
 #if HAVE_SELINUX
     gboolean fcon_was_set = FALSE;
     char    *fcon_prev    = NULL;
 #endif
 
-    g_return_val_if_fail(NM_IS_HOSTNAME_MANAGER(self), FALSE);
+    if (g_task_return_error_if_cancelled(task))
+        return;
 
+    self = g_task_get_source_object(task);
     priv = NM_HOSTNAME_MANAGER_GET_PRIVATE(self);
 
-    if (priv->hostnamed_proxy) {
-        var = g_dbus_proxy_call_sync(priv->hostnamed_proxy,
-                                     "SetStaticHostname",
-                                     g_variant_new("(sb)", hostname, FALSE),
-                                     G_DBUS_CALL_FLAGS_NONE,
-                                     -1,
-                                     NULL,
-                                     &error);
-        if (error)
-            _LOGW("could not set hostname: %s", error->message);
-
-        return !error;
-    }
+    nm_assert(!priv->hostnamed_proxy);
+
+    hostname = g_task_get_task_data(task);
 
     /* If the hostname file is a symbolic link, follow it to find where the
      * real file is located, otherwise g_file_set_contents will attempt to
@@ -363,13 +374,15 @@ nm_hostname_manager_write_hostname(NMHostnameManager *self, const char *hostname
         && (link_path = nm_utils_read_link_absolute(file, NULL)))
         file = link_path;
 
+    if (hostname) {
 #if defined(HOSTNAME_PERSIST_GENTOO)
-    hostname_eol = g_strdup_printf("#Generated by NetworkManager\n"
-                                   "hostname=\"%s\"\n",
-                                   hostname);
+        hostname_eol = g_strdup_printf("#Generated by NetworkManager\n"
+                                       "hostname=\"%s\"\n",
+                                       hostname);
 #else
-    hostname_eol = g_strdup_printf("%s\n", hostname);
+        hostname_eol = g_strdup_printf("%s\n", hostname);
 #endif
+    }
 
 #if HAVE_SELINUX
     /* Get default context for hostname file and set it for fscreate */
@@ -396,7 +409,7 @@ nm_hostname_manager_write_hostname(NMHostnameManager *self, const char *hostname
     }
 #endif
 
-    ret = g_file_set_contents(file, hostname_eol, -1, &error);
+    ret = g_file_set_contents(file, hostname_eol ?: "", -1, &error);
 
 #if HAVE_SELINUX
     /* Restore previous context and cleanup */
@@ -406,14 +419,63 @@ nm_hostname_manager_write_hostname(NMHostnameManager *self, const char *hostname
         freecon(fcon_prev);
 #endif
 
-    g_free(hostname_eol);
-
     if (!ret) {
-        _LOGW("could not save hostname to %s: %s", file, error->message);
-        return FALSE;
+        g_task_return_new_error(task,
+                                NM_UTILS_ERROR,
+                                NM_UTILS_ERROR_UNKNOWN,
+                                "could not save hostname to %s: %s",
+                                file,
+                                error->message);
+        return;
     }
 
-    return TRUE;
+    g_task_return_boolean(task, TRUE);
+}
+
+void
+nm_hostname_manager_write_hostname(NMHostnameManager  *self,
+                                   const char         *hostname,
+                                   GCancellable       *cancellable,
+                                   GAsyncReadyCallback callback,
+                                   gpointer            user_data)
+{
+    NMHostnameManagerPrivate *priv;
+    GTask                    *task;
+
+    g_return_if_fail(NM_IS_HOSTNAME_MANAGER(self));
+
+    priv = NM_HOSTNAME_MANAGER_GET_PRIVATE(self);
+
+    task =
+        nm_g_task_new(self, cancellable, nm_hostname_manager_write_hostname, callback, user_data);
+
+    g_task_set_task_data(task, g_strdup(hostname), g_free);
+
+    if (priv->hostnamed_proxy) {
+        g_dbus_proxy_call(priv->hostnamed_proxy,
+                          "SetStaticHostname",
+                          g_variant_new("(sb)", hostname ?: "", FALSE),
+                          G_DBUS_CALL_FLAGS_NONE,
+                          15000,
+                          cancellable,
+                          _write_hostname_dbus_cb,
+                          task);
+        return;
+    }
+
+    nm_utils_invoke_on_idle(cancellable, _write_hostname_on_idle_cb, task);
+}
+
+gboolean
+nm_hostname_manager_write_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),
+                         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 b871d177..31596819 100644
--- a/src/core/nm-hostname-manager.h
+++ b/src/core/nm-hostname-manager.h
@@ -36,7 +36,15 @@ NMHostnameManager *nm_hostname_manager_get(void);
 
 const char *nm_hostname_manager_get_static_hostname(NMHostnameManager *self);
 
-gboolean nm_hostname_manager_write_hostname(NMHostnameManager *self, const char *hostname);
+void nm_hostname_manager_write_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);
 
 void nm_hostname_manager_set_transient_hostname(NMHostnameManager             *self,
                                                 const char                    *hostname,
diff --git a/src/core/nm-l3-config-data.c b/src/core/nm-l3-config-data.c
index 2a722756..bcd0b62a 100644
--- a/src/core/nm-l3-config-data.c
+++ b/src/core/nm-l3-config-data.c
@@ -149,6 +149,8 @@ struct _NML3ConfigData {
 
     NMSettingIP6ConfigPrivacy ip6_privacy : 4;
 
+    NMMptcpFlags mptcp_flags : 18;
+
     bool is_sealed : 1;
 
     bool has_routes_with_type_local_4_set : 1;
@@ -582,6 +584,16 @@ nm_l3_config_data_log(const NML3ConfigData *self,
                                            NULL)));
     }
 
+    if (self->mptcp_flags != NM_MPTCP_FLAGS_NONE) {
+        gs_free char *s = NULL;
+
+        _L("mptcp-flags: %s",
+           (s = _nm_utils_enum_to_str_full(nm_mptcp_flags_get_type(),
+                                           self->mptcp_flags,
+                                           " ",
+                                           NULL)));
+    }
+
     if (self->ip6_token.id != 0) {
         _L("ipv6-token: %s",
            nm_utils_inet6_interface_identifier_to_token(&self->ip6_token, sbuf_addr));
@@ -694,6 +706,7 @@ nm_l3_config_data_new(NMDedupMultiIndex *multi_idx, int ifindex, NMIPConfigSourc
         .never_default_4               = NM_OPTION_BOOL_DEFAULT,
         .source                        = source,
         .ip6_privacy                   = NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN,
+        .mptcp_flags                   = NM_MPTCP_FLAGS_NONE,
         .ndisc_hop_limit_set           = FALSE,
         .ndisc_reachable_time_msec_set = FALSE,
         .ndisc_retrans_timer_msec_set  = FALSE,
@@ -1875,6 +1888,30 @@ nm_l3_config_data_set_ip6_token(NML3ConfigData *self, NMUtilsIPv6IfaceId ipv6_to
     return TRUE;
 }
 
+NMMptcpFlags
+nm_l3_config_data_get_mptcp_flags(const NML3ConfigData *self)
+{
+    nm_assert(!self || _NM_IS_L3_CONFIG_DATA(self, TRUE));
+
+    if (!self)
+        return NM_MPTCP_FLAGS_NONE;
+
+    return self->mptcp_flags;
+}
+
+gboolean
+nm_l3_config_data_set_mptcp_flags(NML3ConfigData *self, NMMptcpFlags mptcp_flags)
+{
+    nm_assert(_NM_IS_L3_CONFIG_DATA(self, FALSE));
+    nm_assert(!NM_FLAGS_ANY(mptcp_flags, ~_NM_MPTCP_FLAGS_ALL));
+
+    if (self->mptcp_flags == mptcp_flags)
+        return FALSE;
+    self->mptcp_flags = mptcp_flags;
+    nm_assert(self->mptcp_flags == mptcp_flags);
+    return TRUE;
+}
+
 NMProxyConfigMethod
 nm_l3_config_data_get_proxy_method(const NML3ConfigData *self)
 {
@@ -2324,6 +2361,7 @@ nm_l3_config_data_cmp_full(const NML3ConfigData *a,
         NM_CMP_DIRECT_REF_STRING(a->proxy_pac_url, b->proxy_pac_url);
         NM_CMP_DIRECT_REF_STRING(a->proxy_pac_script, b->proxy_pac_script);
         NM_CMP_DIRECT_UNSAFE(a->ip6_privacy, b->ip6_privacy);
+        NM_CMP_DIRECT_UNSAFE(a->mptcp_flags, b->mptcp_flags);
 
         NM_CMP_DIRECT_UNSAFE(a->ndisc_hop_limit_set, b->ndisc_hop_limit_set);
         if (a->ndisc_hop_limit_set)
@@ -2613,9 +2651,6 @@ nm_l3_config_data_add_dependent_device_routes(NML3ConfigData       *self,
             const gboolean has_peer = !IN6_IS_ADDR_UNSPECIFIED(&addr_src->a6.peer_address);
             int            routes_i;
 
-            if (addr_src->ax.plen == 0)
-                continue;
-
             if (NM_FLAGS_HAS(addr_src->a6.n_ifa_flags, IFA_F_NOPREFIXROUTE))
                 continue;
 
@@ -2999,10 +3034,10 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
                         gpointer                  hook_user_data)
 {
     static const guint32 x_default_route_metric_x[2]  = {NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP6,
-                                                        NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP4};
+                                                         NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP4};
     static const guint32 x_default_route_penalty_x[2] = {0, 0};
     static const int     x_default_dns_priority_x[2]  = {NM_DNS_PRIORITY_DEFAULT_NORMAL,
-                                                    NM_DNS_PRIORITY_DEFAULT_NORMAL};
+                                                         NM_DNS_PRIORITY_DEFAULT_NORMAL};
     guint32              default_route_table_coerced_x[2];
     NMDedupMultiIter     iter;
     const NMPObject     *obj;
@@ -3245,6 +3280,9 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
     if (self->ip6_privacy == NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN)
         self->ip6_privacy = src->ip6_privacy;
 
+    if (self->mptcp_flags == NM_MPTCP_FLAGS_NONE)
+        self->mptcp_flags = src->mptcp_flags;
+
     if (!self->ndisc_hop_limit_set && src->ndisc_hop_limit_set) {
         self->ndisc_hop_limit_set = TRUE;
         self->ndisc_hop_limit_val = src->ndisc_hop_limit_val;
diff --git a/src/core/nm-l3-config-data.h b/src/core/nm-l3-config-data.h
index 20a32c62..ca42d4a1 100644
--- a/src/core/nm-l3-config-data.h
+++ b/src/core/nm-l3-config-data.h
@@ -487,6 +487,10 @@ NMUtilsIPv6IfaceId nm_l3_config_data_get_ip6_token(const NML3ConfigData *self);
 
 gboolean nm_l3_config_data_set_ip6_token(NML3ConfigData *self, NMUtilsIPv6IfaceId ipv6_token);
 
+NMMptcpFlags nm_l3_config_data_get_mptcp_flags(const NML3ConfigData *self);
+
+gboolean nm_l3_config_data_set_mptcp_flags(NML3ConfigData *self, NMMptcpFlags mptcp_flags);
+
 const in_addr_t *nm_l3_config_data_get_wins(const NML3ConfigData *self, guint *out_len);
 
 gboolean nm_l3_config_data_add_wins(NML3ConfigData *self, in_addr_t wins);
diff --git a/src/core/nm-l3-ipv4ll.c b/src/core/nm-l3-ipv4ll.c
index 2aedab56..3fc1e165 100644
--- a/src/core/nm-l3-ipv4ll.c
+++ b/src/core/nm-l3-ipv4ll.c
@@ -672,7 +672,9 @@ _ipv4ll_platform_find_addr(NML3IPv4LL *self, const NML3AcdAddrInfo **out_acd_inf
     const NML3AcdAddrInfo      *acd_info;
     const NMPlatformIP4Address *addr;
 
-    nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP4_ADDRESS, nm_l3_ipv4ll_get_ifindex(self));
+    nmp_lookup_init_object_by_ifindex(&lookup,
+                                      NMP_OBJECT_TYPE_IP4_ADDRESS,
+                                      nm_l3_ipv4ll_get_ifindex(self));
     nm_platform_iter_obj_for_each (&iter, nm_l3_ipv4ll_get_platform(self), &lookup, &obj) {
         addr = NMP_OBJECT_CAST_IP4_ADDRESS(obj);
         if (!_ip4_address_is_link_local(addr))
diff --git a/src/core/nm-l3-ipv6ll.c b/src/core/nm-l3-ipv6ll.c
index 2b9a1a0e..2640c075 100644
--- a/src/core/nm-l3-ipv6ll.c
+++ b/src/core/nm-l3-ipv6ll.c
@@ -343,7 +343,9 @@ _pladdr_find_ll(NML3IPv6LL *self, gboolean *out_cur_addr_failed)
     if (!NM_IN_SET(self->state, NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS, NM_L3_IPV6LL_STATE_READY))
         cur_addr_check = FALSE;
 
-    nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP6_ADDRESS, nm_l3_ipv6ll_get_ifindex(self));
+    nmp_lookup_init_object_by_ifindex(&lookup,
+                                      NMP_OBJECT_TYPE_IP6_ADDRESS,
+                                      nm_l3_ipv6ll_get_ifindex(self));
 
     nm_platform_iter_obj_for_each (&iter, nm_l3_ipv6ll_get_platform(self), &lookup, &obj) {
         const NMPlatformIP6Address *pladdr = NMP_OBJECT_CAST_IP6_ADDRESS(obj);
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index af6099e9..69ea0d26 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -4,6 +4,8 @@
 
 #include "nm-l3cfg.h"
 
+#include "libnm-std-aux/nm-linux-compat.h"
+
 #include <net/if.h>
 #include <linux/if_addr.h>
 #include <linux/if_ether.h>
@@ -12,7 +14,7 @@
 #include "libnm-glib-aux/nm-time-utils.h"
 #include "libnm-platform/nm-platform.h"
 #include "libnm-platform/nmp-object.h"
-#include "libnm-platform/nmp-route-manager.h"
+#include "libnm-platform/nmp-global-tracker.h"
 #include "nm-netns.h"
 #include "n-acd/src/n-acd.h"
 #include "nm-l3-ipv4ll.h"
@@ -37,7 +39,6 @@ G_STATIC_ASSERT(NM_ACD_TIMEOUT_RFC5227_MSEC == N_ACD_TIMEOUT_RFC5227);
 #define ACD_ENSURE_RATELIMIT_MSEC               ((guint32) 4000u)
 #define ACD_WAIT_PROBING_EXTRA_TIME_MSEC        ((guint32) (1000u + ACD_ENSURE_RATELIMIT_MSEC))
 #define ACD_WAIT_PROBING_EXTRA_TIME2_MSEC       ((guint32) 1000u)
-#define ACD_MAX_TIMEOUT_MSEC                    ((guint32) 30000u)
 #define ACD_WAIT_TIME_PROBING_FULL_RESTART_MSEC ((guint32) 30000u)
 #define ACD_WAIT_TIME_CONFLICT_RESTART_MSEC     ((guint32) 120000u)
 #define ACD_WAIT_TIME_ANNOUNCE_RESTART_MSEC     ((guint32) 30000u)
@@ -271,6 +272,15 @@ typedef struct _NML3CfgPrivate {
         NMIPConfig *ipconfig_x[2];
     };
 
+    /* Whether we earlier configured MPTCP endpoints for the interface. */
+    union {
+        struct {
+            bool mptcp_set_6;
+            bool mptcp_set_4;
+        };
+        bool mptcp_set_x[2];
+    };
+
     /* This is for rate-limiting the creation of nacd instance. */
     GSource *nacd_instance_ensure_retry;
 
@@ -312,6 +322,9 @@ typedef struct _NML3CfgPrivate {
 
     bool changed_configs_configs : 1;
     bool changed_configs_acd_state : 1;
+
+    bool rp_filter_handled : 1;
+    bool rp_filter_set : 1;
 } NML3CfgPrivate;
 
 struct _NML3CfgClass {
@@ -322,6 +335,13 @@ G_DEFINE_TYPE(NML3Cfg, nm_l3cfg, G_TYPE_OBJECT)
 
 /*****************************************************************************/
 
+#define _NODEV_ROUTES_TAG(self, IS_IPv4) \
+    ((gconstpointer) (&(((const char *) (self))[0 + (!(IS_IPv4))])))
+
+#define _MPTCP_TAG(self, IS_IPv4) ((gconstpointer) (&(((const char *) (self))[2 + (!(IS_IPv4))])))
+
+/*****************************************************************************/
+
 #define _NMLOG_DOMAIN      LOGD_CORE
 #define _NMLOG_PREFIX_NAME "l3cfg"
 #define _NMLOG(level, ...)                                                    \
@@ -1880,10 +1900,10 @@ _l3_acd_data_add(NML3Cfg              *self,
 
     acd_data = _l3_acd_data_find(self, addr);
 
-    if (acd_timeout_msec > ACD_MAX_TIMEOUT_MSEC) {
+    if (acd_timeout_msec > NM_ACD_TIMEOUT_MAX_MSEC) {
         /* we limit the maximum timeout. Otherwise we have to handle integer overflow
          * when adding timeouts. */
-        acd_timeout_msec = ACD_MAX_TIMEOUT_MSEC;
+        acd_timeout_msec = NM_ACD_TIMEOUT_MAX_MSEC;
     }
 
     if (!acd_data) {
@@ -3243,8 +3263,8 @@ nm_l3cfg_add_config(NML3Cfg              *self,
     nm_assert(l3cd);
     nm_assert(nm_l3_config_data_get_ifindex(l3cd) == self->priv.ifindex);
 
-    if (acd_timeout_msec > ACD_MAX_TIMEOUT_MSEC)
-        acd_timeout_msec = ACD_MAX_TIMEOUT_MSEC;
+    if (acd_timeout_msec > NM_ACD_TIMEOUT_MAX_MSEC)
+        acd_timeout_msec = NM_ACD_TIMEOUT_MAX_MSEC;
 
     nm_assert(NM_IN_SET(acd_defend_type,
                         NM_L3_ACD_DEFEND_TYPE_NEVER,
@@ -3451,15 +3471,13 @@ nm_l3cfg_remove_config_all_dirty(NML3Cfg *self, gconstpointer tag)
 
 /*****************************************************************************/
 
-#define _NODEV_ROUTES_TAG(self, IS_IPv4) ((gconstpointer) (&(&(self)->priv.route_manager)[IS_IPv4]))
-
 static gboolean
 _nodev_routes_untrack(NML3Cfg *self, int addr_family)
 {
-    return nmp_route_manager_untrack_all(self->priv.route_manager,
-                                         _NODEV_ROUTES_TAG(self, NM_IS_IPv4(addr_family)),
-                                         FALSE,
-                                         TRUE);
+    return nmp_global_tracker_untrack_all(self->priv.global_tracker,
+                                          _NODEV_ROUTES_TAG(self, NM_IS_IPv4(addr_family)),
+                                          FALSE,
+                                          TRUE);
 }
 
 static void
@@ -3479,12 +3497,12 @@ _nodev_routes_sync(NML3Cfg          *self,
     for (i = 0; i < routes_nodev->len; i++) {
         const NMPObject *obj = routes_nodev->pdata[i];
 
-        if (nmp_route_manager_track(self->priv.route_manager,
-                                    obj_type,
-                                    NMP_OBJECT_CAST_IP_ROUTE(obj),
-                                    1,
-                                    _NODEV_ROUTES_TAG(self, IS_IPv4),
-                                    NULL))
+        if (nmp_global_tracker_track(self->priv.global_tracker,
+                                     obj_type,
+                                     NMP_OBJECT_CAST_IP_ROUTE(obj),
+                                     1,
+                                     _NODEV_ROUTES_TAG(self, IS_IPv4),
+                                     NULL))
             changed = TRUE;
     }
 
@@ -3492,8 +3510,11 @@ out_clear:
     if (_nodev_routes_untrack(self, addr_family))
         changed = TRUE;
 
-    if (changed || commit_type >= NM_L3_CFG_COMMIT_TYPE_REAPPLY)
-        nmp_route_manager_sync(self->priv.route_manager, NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4), FALSE);
+    if (changed || commit_type >= NM_L3_CFG_COMMIT_TYPE_REAPPLY) {
+        nmp_global_tracker_sync(self->priv.global_tracker,
+                                NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4),
+                                FALSE);
+    }
 }
 
 /*****************************************************************************/
@@ -4182,6 +4203,259 @@ _l3_commit_ip6_token(NML3Cfg *self, NML3CfgCommitType commit_type)
     }
 }
 
+/*****************************************************************************/
+
+static void
+_rp_filter_update(NML3Cfg *self, gboolean reapply)
+{
+    gboolean    rp_filter_relax = FALSE;
+    const char *ifname;
+    int         rf_val;
+
+    /* The rp_filter sysctl is only an IPv4 thing. We only enable the rp-filter
+     * handling, if we did anything to MPTCP about IPv4 addresses.
+     *
+     * While we only have one "connection.mptcp-flags=enabled" property, whether
+     * we handle MPTCP is still tracked per AF. In particular, with "enabled-on-global-iface"
+     * flag, which honors the AF-specific default route. */
+    if (self->priv.p->mptcp_set_4)
+        rp_filter_relax = TRUE;
+
+    if (!rp_filter_relax) {
+        if (self->priv.p->rp_filter_handled) {
+            self->priv.p->rp_filter_handled = FALSE;
+            if (self->priv.p->rp_filter_set) {
+                self->priv.p->rp_filter_set = FALSE;
+
+                ifname = nm_l3cfg_get_ifname(self, TRUE);
+                rf_val = nm_platform_sysctl_ip_conf_get_rp_filter_ipv4(self->priv.platform,
+                                                                       ifname,
+                                                                       FALSE,
+                                                                       NULL);
+                if (rf_val == 2) {
+                    /* We only relaxed from 1 to 2. Only if that is still the case, reset. */
+                    nm_platform_sysctl_ip_conf_set(self->priv.platform,
+                                                   AF_INET,
+                                                   ifname,
+                                                   "rp_filter",
+                                                   "1");
+                }
+            }
+        }
+        return;
+    }
+
+    if (self->priv.p->rp_filter_handled && !reapply) {
+        /* We only set rp_filter once (except reapply). */
+        return;
+    }
+
+    /* No matter whether we actually reset the value below, we set the "handled" flag
+     * so that we only do this once (except during reapply). */
+    self->priv.p->rp_filter_handled = TRUE;
+
+    ifname = nm_l3cfg_get_ifname(self, TRUE);
+
+    rf_val =
+        nm_platform_sysctl_ip_conf_get_rp_filter_ipv4(self->priv.platform, ifname, FALSE, NULL);
+
+    if (rf_val != 1) {
+        /* We only relax from strict (1) to loose (2). No other transition. Nothing to do. */
+        return;
+    }
+
+    /* We actually loosen the flag. We need to remember to reset it. */
+    self->priv.p->rp_filter_set = TRUE;
+    nm_platform_sysctl_ip_conf_set(self->priv.platform, AF_INET, ifname, "rp_filter", "2");
+}
+
+/*****************************************************************************/
+
+static gboolean
+_global_tracker_mptcp_untrack(NML3Cfg *self, int addr_family)
+{
+    return nmp_global_tracker_untrack_all(self->priv.global_tracker,
+                                          _MPTCP_TAG(self, NM_IS_IPv4(addr_family)),
+                                          FALSE,
+                                          TRUE);
+}
+
+static gboolean
+_l3_commit_mptcp_af(NML3Cfg          *self,
+                    NML3CfgCommitType commit_type,
+                    int               addr_family,
+                    gboolean         *out_reapply)
+{
+    const int    IS_IPv4 = NM_IS_IPv4(addr_family);
+    NMMptcpFlags mptcp_flags;
+    gboolean     reapply = FALSE;
+    gboolean     changed = FALSE;
+
+    nm_assert(NM_IS_L3CFG(self));
+    nm_assert(NM_IN_SET(commit_type,
+                        NM_L3_CFG_COMMIT_TYPE_NONE,
+                        NM_L3_CFG_COMMIT_TYPE_REAPPLY,
+                        NM_L3_CFG_COMMIT_TYPE_UPDATE));
+
+    if (commit_type >= NM_L3_CFG_COMMIT_TYPE_REAPPLY)
+        reapply = TRUE;
+
+    if (commit_type != NM_L3_CFG_COMMIT_TYPE_NONE && self->priv.p->combined_l3cd_commited)
+        mptcp_flags = nm_l3_config_data_get_mptcp_flags(self->priv.p->combined_l3cd_commited);
+    else
+        mptcp_flags = NM_MPTCP_FLAGS_DISABLED;
+
+    if (mptcp_flags == NM_MPTCP_FLAGS_NONE || NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_DISABLED))
+        mptcp_flags = NM_MPTCP_FLAGS_DISABLED;
+    else if (!NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_ALSO_WITHOUT_DEFAULT_ROUTE)) {
+        /* Whether MPTCP is enabled/disabled (per address family), depends on whether we have a unicast
+         * default route (in the main routing table). */
+        if (self->priv.p->combined_l3cd_commited
+            && nm_l3_config_data_get_best_default_route(self->priv.p->combined_l3cd_commited,
+                                                        addr_family))
+            mptcp_flags = NM_FLAGS_UNSET(mptcp_flags, NM_MPTCP_FLAGS_ALSO_WITHOUT_DEFAULT_ROUTE)
+                          | NM_MPTCP_FLAGS_ENABLED;
+        else
+            mptcp_flags = NM_MPTCP_FLAGS_DISABLED;
+    } else
+        mptcp_flags |= NM_MPTCP_FLAGS_ENABLED;
+
+    if (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_DISABLED)) {
+        if (!self->priv.p->mptcp_set_x[IS_IPv4] && !reapply) {
+            /* Nothing to configure, and we did not earlier configure MPTCP. Nothing to do. */
+            NM_SET_OUT(out_reapply, FALSE);
+            return FALSE;
+        }
+
+        self->priv.p->mptcp_set_x[IS_IPv4] = FALSE;
+        reapply                            = TRUE;
+    } else {
+        const NMPlatformIPXAddress *addr;
+        NMDedupMultiIter            iter;
+        const guint32               FLAGS =
+            (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_SIGNAL) ? MPTCP_PM_ADDR_FLAG_SIGNAL : 0)
+            | (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_SUBFLOW) ? MPTCP_PM_ADDR_FLAG_SUBFLOW : 0)
+            | (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_BACKUP) ? MPTCP_PM_ADDR_FLAG_BACKUP : 0)
+            | (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_FULLMESH) ? MPTCP_PM_ADDR_FLAG_FULLMESH
+                                                                  : 0);
+        NMPlatformMptcpAddr a = {
+            .ifindex     = self->priv.ifindex,
+            .id          = 0,
+            .flags       = FLAGS,
+            .addr_family = addr_family,
+            .port        = 0,
+        };
+        gboolean any_tracked = FALSE;
+
+        self->priv.p->mptcp_set_x[IS_IPv4] = TRUE;
+
+        if (self->priv.p->combined_l3cd_commited) {
+            gint32 addr_prio;
+
+            addr_prio = 100;
+
+            nm_l3_config_data_iter_ip_address_for_each (&iter,
+                                                        self->priv.p->combined_l3cd_commited,
+                                                        addr_family,
+                                                        (const NMPlatformIPAddress **) &addr) {
+                /* We want to evaluate the  with-{loopback,link_local}-{4,6} flags based on the actual
+                 * ifa_scope that the address will have once we configure it.
+                 * "addr" is an address we want to configure, we expect that it will
+                 * later have the scope nm_platform_ip_address_get_scope() based on
+                 * the address. */
+                switch (nm_platform_ip_address_get_scope(addr_family, addr->ax.address_ptr)) {
+                case RT_SCOPE_HOST:
+                    goto skip_addr;
+                case RT_SCOPE_LINK:
+                    goto skip_addr;
+                default:
+                    if (IS_IPv4) {
+                        /* We take all addresses, including rfc1918 private addresses
+                         * (nm_utils_ip_is_site_local()). */
+                    } else {
+                        if (nm_utils_ip6_is_ula(&addr->a6.address)) {
+                            /* Exclude unique local IPv6 addresses fc00::/7. */
+                            goto skip_addr;
+                        } else {
+                            /* We take all other addresses, including deprecated IN6_IS_ADDR_SITELOCAL()
+                             * (fec0::/10). */
+                        }
+                    }
+                    break;
+                }
+
+                a.addr = nm_ip_addr_init(addr_family, addr->ax.address_ptr);
+
+                /* We track the address with different priorities, that depends
+                 * on the order in which they are listed here. NMPGlobalTracker
+                 * will sort all tracked addresses by priority. That means, if we
+                 * have multiple interfaces then we will prefer the first address
+                 * of those interfaces, then the second, etc.
+                 *
+                 * That is relevant, because the overall number of addresses we
+                 * can configure in kernel is strongly limited (MPTCP_PM_ADDR_MAX). */
+                if (addr_prio > 10)
+                    addr_prio--;
+
+                if (nmp_global_tracker_track(self->priv.global_tracker,
+                                             NMP_OBJECT_TYPE_MPTCP_ADDR,
+                                             &a,
+                                             addr_prio,
+                                             _MPTCP_TAG(self, IS_IPv4),
+                                             NULL))
+                    changed = TRUE;
+
+                any_tracked = TRUE;
+
+skip_addr:
+                (void) 0;
+            }
+        }
+
+        if (!any_tracked) {
+            /* We need to make it known that this ifindex is used. Track a dummy object. */
+            if (nmp_global_tracker_track(
+                    self->priv.global_tracker,
+                    NMP_OBJECT_TYPE_MPTCP_ADDR,
+                    nmp_global_tracker_mptcp_addr_init_for_ifindex(&a, self->priv.ifindex),
+                    1,
+                    _MPTCP_TAG(self, IS_IPv4),
+                    NULL))
+                changed = TRUE;
+        }
+    }
+
+    if (_global_tracker_mptcp_untrack(self, addr_family))
+        changed = TRUE;
+
+    NM_SET_OUT(out_reapply, reapply);
+    return changed || reapply;
+}
+
+static void
+_l3_commit_mptcp(NML3Cfg *self, NML3CfgCommitType commit_type)
+{
+    gboolean changed   = FALSE;
+    gboolean reapply   = FALSE;
+    gboolean i_reapply = FALSE;
+
+    if (_l3_commit_mptcp_af(self, commit_type, AF_INET, &i_reapply))
+        changed = TRUE;
+    reapply |= i_reapply;
+    if (_l3_commit_mptcp_af(self, commit_type, AF_INET6, &i_reapply))
+        changed = TRUE;
+    reapply |= i_reapply;
+
+    nm_assert(commit_type < NM_L3_CFG_COMMIT_TYPE_REAPPLY || reapply);
+
+    if (changed)
+        nmp_global_tracker_sync_mptcp_addrs(self->priv.global_tracker, reapply);
+    else
+        nm_assert(!reapply);
+
+    _rp_filter_update(self, reapply);
+}
+
 static gboolean
 _l3_commit_one(NML3Cfg              *self,
                int                   addr_family,
@@ -4380,6 +4654,8 @@ _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is_idle)
     _l3_commit_one(self, AF_INET, commit_type, changed_combined_l3cd, l3cd_old);
     _l3_commit_one(self, AF_INET6, commit_type, changed_combined_l3cd, l3cd_old);
 
+    _l3_commit_mptcp(self, commit_type);
+
     _l3_acd_data_process_changes(self);
 
     if (self->priv.p->l3_config_datas) {
@@ -4626,7 +4902,9 @@ nm_l3cfg_has_commited_ip6_addresses_pending_dad(NML3Cfg *self)
      * Of course, all lookups are O(1) anyway, so in any case the operation is
      * O(n) (once "n" being the addresses in platform, and once in l3cd). */
 
-    nmp_lookup_init_object(&plat_lookup, NMP_OBJECT_TYPE_IP6_ADDRESS, self->priv.ifindex);
+    nmp_lookup_init_object_by_ifindex(&plat_lookup,
+                                      NMP_OBJECT_TYPE_IP6_ADDRESS,
+                                      self->priv.ifindex);
 
     nm_platform_iter_obj_for_each (&iter, self->priv.platform, &plat_lookup, &plat_obj) {
         const NMPlatformIP6Address *plat_addr = NMP_OBJECT_CAST_IP6_ADDRESS(plat_obj);
@@ -4763,7 +5041,8 @@ constructed(GObject *object)
     self->priv.platform = g_object_ref(nm_netns_get_platform(self->priv.netns));
     nm_assert(NM_IS_PLATFORM(self->priv.platform));
 
-    self->priv.route_manager = nmp_route_manager_ref(nm_netns_get_route_manager(self->priv.netns));
+    self->priv.global_tracker =
+        nmp_global_tracker_ref(nm_netns_get_global_tracker(self->priv.netns));
 
     _LOGT("created (netns=" NM_HASH_OBFUSCATE_PTR_FMT ")", NM_HASH_OBFUSCATE_PTR(self->priv.netns));
 
@@ -4785,6 +5064,7 @@ static void
 finalize(GObject *object)
 {
     NML3Cfg *self = NM_L3CFG(object);
+    gboolean changed;
 
     nm_assert(!self->priv.p->ipconfig_4);
     nm_assert(!self->priv.p->ipconfig_6);
@@ -4818,13 +5098,21 @@ finalize(GObject *object)
     nm_assert(c_list_is_empty(&self->priv.p->obj_state_zombie_lst_head));
 
     if (_nodev_routes_untrack(self, AF_INET))
-        nmp_route_manager_sync(self->priv.route_manager, NMP_OBJECT_TYPE_IP4_ROUTE, FALSE);
+        nmp_global_tracker_sync(self->priv.global_tracker, NMP_OBJECT_TYPE_IP4_ROUTE, FALSE);
     if (_nodev_routes_untrack(self, AF_INET6))
-        nmp_route_manager_sync(self->priv.route_manager, NMP_OBJECT_TYPE_IP6_ROUTE, FALSE);
+        nmp_global_tracker_sync(self->priv.global_tracker, NMP_OBJECT_TYPE_IP6_ROUTE, FALSE);
+
+    changed = FALSE;
+    if (_global_tracker_mptcp_untrack(self, AF_INET))
+        changed = TRUE;
+    if (_global_tracker_mptcp_untrack(self, AF_INET6))
+        changed = TRUE;
+    if (changed)
+        nmp_global_tracker_sync_mptcp_addrs(self->priv.global_tracker, FALSE);
 
     g_clear_object(&self->priv.netns);
     g_clear_object(&self->priv.platform);
-    nm_clear_pointer(&self->priv.route_manager, nmp_route_manager_unref);
+    nm_clear_pointer(&self->priv.global_tracker, nmp_global_tracker_unref);
 
     nm_clear_l3cd(&self->priv.p->combined_l3cd_merged);
     nm_clear_l3cd(&self->priv.p->combined_l3cd_commited);
diff --git a/src/core/nm-l3cfg.h b/src/core/nm-l3cfg.h
index 215d21e9..e0257623 100644
--- a/src/core/nm-l3cfg.h
+++ b/src/core/nm-l3cfg.h
@@ -10,6 +10,7 @@
 #define NM_L3CFG_CONFIG_PRIORITY_IPV6LL 1
 #define NM_L3CFG_CONFIG_PRIORITY_VPN    9
 #define NM_ACD_TIMEOUT_RFC5227_MSEC     9000u
+#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))
@@ -195,18 +196,18 @@ typedef struct {
 } NML3ConfigNotifyData;
 
 struct _NML3CfgPrivate;
-struct _NMPRouteManager;
+struct _NMPGlobalTracker;
 
 struct _NML3Cfg {
     GObject parent;
     struct {
-        struct _NML3CfgPrivate  *p;
-        NMNetns                 *netns;
-        NMPlatform              *platform;
-        struct _NMPRouteManager *route_manager;
-        const NMPObject         *plobj;
-        const NMPObject         *plobj_next;
-        int                      ifindex;
+        struct _NML3CfgPrivate   *p;
+        NMNetns                  *netns;
+        NMPlatform               *platform;
+        struct _NMPGlobalTracker *global_tracker;
+        const NMPObject          *plobj;
+        const NMPObject          *plobj_next;
+        int                       ifindex;
     } priv;
 };
 
diff --git a/src/core/nm-manager.c b/src/core/nm-manager.c
index 52153697..822df7ad 100644
--- a/src/core/nm-manager.c
+++ b/src/core/nm-manager.c
@@ -20,6 +20,7 @@
 #include "devices/nm-device-factory.h"
 #include "devices/nm-device-generic.h"
 #include "devices/nm-device.h"
+#include "dns/nm-dns-manager.h"
 #include "dhcp/nm-dhcp-manager.h"
 #include "libnm-core-aux-intern/nm-common-macros.h"
 #include "libnm-core-intern/nm-core-internal.h"
@@ -144,6 +145,9 @@ NM_GOBJECT_PROPERTIES_DEFINE(NMManager,
 typedef struct {
     NMPlatform *platform;
 
+    NMDnsManager *dns_mgr;
+    gulong        dns_mgr_update_pending_signal_id;
+
     GArray *capabilities;
 
     CList               active_connections_lst_head; /* Oldest ACs at the beginning */
@@ -209,6 +213,10 @@ typedef struct {
 
     unsigned connectivity_check_enabled_last : 2;
 
+    /* List of GDBusMethodInvocation of in progress Sleep() and Enable()
+     * calls. They return only if all in-flight deactivations finished. */
+    GSList *sleep_invocations;
+
     guint delete_volatile_connection_idle_id;
     CList delete_volatile_connection_lst_head;
 } NMManagerPrivate;
@@ -346,6 +354,9 @@ static NMActiveConnection *_new_active_connection(NMManager             *self,
 
 static void policy_activating_ac_changed(GObject *object, GParamSpec *pspec, gpointer user_data);
 
+static void device_has_pending_action_changed(NMDevice *device, GParamSpec *pspec, NMManager *self);
+static void check_if_startup_complete(NMManager *self);
+
 static gboolean find_master(NMManager             *self,
                             NMConnection          *connection,
                             NMDevice              *device,
@@ -1601,7 +1612,11 @@ manager_device_state_changed(NMDevice           *device,
         nm_settings_device_added(priv->settings, device);
 }
 
-static void device_has_pending_action_changed(NMDevice *device, GParamSpec *pspec, NMManager *self);
+static void
+_dns_mgr_update_pending_cb(NMDevice *device, GParamSpec *pspec, NMManager *self)
+{
+    check_if_startup_complete(self);
+}
 
 static void
 check_if_startup_complete(NMManager *self)
@@ -1616,6 +1631,20 @@ check_if_startup_complete(NMManager *self)
     if (!priv->devices_inited)
         return;
 
+    if (nm_dns_manager_get_update_pending(nm_manager_get_dns_manager(self))) {
+        if (priv->dns_mgr_update_pending_signal_id == 0) {
+            priv->dns_mgr_update_pending_signal_id =
+                g_signal_connect(nm_manager_get_dns_manager(self),
+                                 "notify::" NM_DNS_MANAGER_UPDATE_PENDING,
+                                 G_CALLBACK(_dns_mgr_update_pending_cb),
+                                 self);
+        }
+        return;
+    }
+
+    nm_clear_g_signal_handler(nm_manager_get_dns_manager(self),
+                              &priv->dns_mgr_update_pending_signal_id);
+
     c_list_for_each_entry (device, &priv->devices_lst_head, devices_lst) {
         reason = nm_device_has_pending_action_reason(device);
         if (reason) {
@@ -2221,6 +2250,18 @@ connection_updated_cb(NMSettings           *settings,
     connection_changed(self, sett_conn);
 }
 
+static void
+connections_changed(NMManager *self)
+{
+    NMManagerPrivate            *priv = NM_MANAGER_GET_PRIVATE(self);
+    NMSettingsConnection *const *connections;
+    guint                        i;
+
+    connections = nm_settings_get_connections_sorted_by_autoconnect_priority(priv->settings, NULL);
+    for (i = 0; connections[i]; i++)
+        connection_changed(self, connections[i]);
+}
+
 /*****************************************************************************/
 
 static void
@@ -6336,6 +6377,22 @@ done:
     g_clear_object(&subject);
 }
 
+static void
+sleep_devices_check_empty(NMManager *self)
+{
+    NMManagerPrivate      *priv = NM_MANAGER_GET_PRIVATE(self);
+    GDBusMethodInvocation *invocation;
+
+    if (g_hash_table_size(priv->sleep_devices) > 0)
+        return;
+
+    while (priv->sleep_invocations) {
+        invocation = priv->sleep_invocations->data;
+        g_dbus_method_invocation_return_value(invocation, NULL);
+        priv->sleep_invocations = g_slist_remove(priv->sleep_invocations, invocation);
+    }
+}
+
 static gboolean
 sleep_devices_add(NMManager *self, NMDevice *device, gboolean suspending)
 {
@@ -6378,6 +6435,9 @@ sleep_devices_remove(NMManager *self, NMDevice *device)
     g_signal_handlers_disconnect_by_func(device, device_sleep_cb, self);
     g_hash_table_remove(priv->sleep_devices, device);
     g_object_unref(device);
+
+    sleep_devices_check_empty(self);
+
     return TRUE;
 }
 
@@ -6389,9 +6449,6 @@ sleep_devices_clear(NMManager *self)
     NMSleepMonitorInhibitorHandle *handle;
     GHashTableIter                 iter;
 
-    if (!priv->sleep_devices)
-        return;
-
     g_hash_table_iter_init(&iter, priv->sleep_devices);
     while (g_hash_table_iter_next(&iter, (gpointer *) &device, (gpointer *) &handle)) {
         g_signal_handlers_disconnect_by_func(device, device_sleep_cb, self);
@@ -6400,6 +6457,8 @@ sleep_devices_clear(NMManager *self)
         g_object_unref(device);
         g_hash_table_iter_remove(&iter);
     }
+
+    sleep_devices_check_empty(self);
 }
 
 static void
@@ -6521,7 +6580,7 @@ do_sleep_wake(NMManager *self, gboolean sleeping_changed)
                 && !nm_device_get_unmanaged_flags(device, NM_UNMANAGED_SLEEPING)) {
                 /* DHCP leases of software devices could have gone stale
                  * so we need to renew them. */
-                nm_device_update_dynamic_ip_setup(device);
+                nm_device_update_dynamic_ip_setup(device, "wake up");
                 continue;
             }
 
@@ -6551,6 +6610,10 @@ do_sleep_wake(NMManager *self, gboolean sleeping_changed)
                                              FALSE,
                                              NM_DEVICE_STATE_REASON_NOW_MANAGED);
         }
+
+        /* Give the connections a chance to recreate the virtual devices.
+	 * We've torn them down on sleep. */
+        connections_changed(self);
     }
 
     nm_manager_update_state(self);
@@ -6628,7 +6691,10 @@ impl_manager_sleep(NMDBusObject                      *obj,
                             TRUE,
                             subject,
                             NULL);
-    g_dbus_method_invocation_return_value(invocation, NULL);
+
+    priv->sleep_invocations = g_slist_prepend(priv->sleep_invocations, invocation);
+    sleep_devices_check_empty(self);
+
     return;
 }
 
@@ -6668,10 +6734,11 @@ _internal_enable(NMManager *self, gboolean enable)
 static void
 enable_net_done_cb(NMAuthChain *chain, GDBusMethodInvocation *context, gpointer user_data)
 {
-    NMManager       *self = NM_MANAGER(user_data);
-    NMAuthCallResult result;
-    gboolean         enable;
-    NMAuthSubject   *subject;
+    NMManager        *self = NM_MANAGER(user_data);
+    NMManagerPrivate *priv = NM_MANAGER_GET_PRIVATE(self);
+    NMAuthCallResult  result;
+    gboolean          enable;
+    NMAuthSubject    *subject;
 
     nm_assert(G_IS_DBUS_METHOD_INVOCATION(context));
 
@@ -6696,8 +6763,10 @@ enable_net_done_cb(NMAuthChain *chain, GDBusMethodInvocation *context, gpointer
     }
 
     _internal_enable(self, enable);
-    g_dbus_method_invocation_return_value(context, NULL);
     nm_audit_log_control_op(NM_AUDIT_OP_NET_CONTROL, enable ? "on" : "off", TRUE, subject, NULL);
+
+    priv->sleep_invocations = g_slist_prepend(priv->sleep_invocations, context);
+    sleep_devices_check_empty(self);
 }
 
 static void
@@ -7014,10 +7083,6 @@ nm_manager_write_device_state(NMManager *self, NMDevice *device, int *out_ifinde
     guint32                        route_metric_default_aspired;
     guint32                        route_metric_default_effective;
     NMTernary                      nm_owned;
-    NMDhcpConfig                  *dhcp_config;
-    const char                    *next_server   = NULL;
-    const char                    *root_path     = NULL;
-    const char                    *dhcp_bootfile = NULL;
 
     NM_SET_OUT(out_ifindex, 0);
 
@@ -7059,15 +7124,6 @@ nm_manager_write_device_state(NMManager *self, NMDevice *device, int *out_ifinde
                                                               TRUE,
                                                               &route_metric_default_aspired);
 
-    dhcp_config = nm_device_get_dhcp_config(device, AF_INET);
-    if (dhcp_config) {
-        root_path     = nm_dhcp_config_get_option(dhcp_config, "root_path");
-        next_server   = nm_dhcp_config_get_option(dhcp_config, "next_server");
-        dhcp_bootfile = nm_dhcp_config_get_option(dhcp_config, "filename");
-        if (!dhcp_bootfile)
-            dhcp_bootfile = nm_dhcp_config_get_option(dhcp_config, "bootfile_name");
-    }
-
     if (!nm_config_device_state_write(ifindex,
                                       managed_type,
                                       perm_hw_addr_fake,
@@ -7075,9 +7131,8 @@ nm_manager_write_device_state(NMManager *self, NMDevice *device, int *out_ifinde
                                       nm_owned,
                                       route_metric_default_aspired,
                                       route_metric_default_effective,
-                                      next_server,
-                                      root_path,
-                                      dhcp_bootfile))
+                                      nm_device_get_dhcp_config(device, AF_INET),
+                                      nm_device_get_dhcp_config(device, AF_INET6)))
         return FALSE;
 
     NM_SET_OUT(out_ifindex, ifindex);
@@ -7119,9 +7174,8 @@ devices_inited_cb(gpointer user_data)
 gboolean
 nm_manager_start(NMManager *self, GError **error)
 {
-    NMManagerPrivate            *priv = NM_MANAGER_GET_PRIVATE(self);
-    NMSettingsConnection *const *connections;
-    guint                        i;
+    NMManagerPrivate *priv = NM_MANAGER_GET_PRIVATE(self);
+    guint             i;
 
     nm_device_factory_manager_load_factories(_register_device_factory, self);
 
@@ -7175,9 +7229,10 @@ nm_manager_start(NMManager *self, GError **error)
                      NM_SETTINGS_SIGNAL_CONNECTION_UPDATED,
                      G_CALLBACK(connection_updated_cb),
                      self);
-    connections = nm_settings_get_connections_sorted_by_autoconnect_priority(priv->settings, NULL);
-    for (i = 0; connections[i]; i++)
-        connection_changed(self, connections[i]);
+
+    /* Make sure virtual devices for all connections are created so
+     * that they could be autoconnected.  */
+    connections_changed(self);
 
     nm_clear_g_source(&priv->devices_inited_id);
     priv->devices_inited_id = g_idle_add_full(G_PRIORITY_LOW + 10, devices_inited_cb, self, NULL);
@@ -7790,6 +7845,28 @@ impl_manager_checkpoint_adjust_rollback_timeout(NMDBusObject
 
 /*****************************************************************************/
 
+NMDnsManager *
+nm_manager_get_dns_manager(NMManager *self)
+{
+    NMManagerPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_MANAGER(self), NULL);
+
+    priv = NM_MANAGER_GET_PRIVATE(self);
+
+    if (G_UNLIKELY(!priv->dns_mgr)) {
+        /* Initialize lazily on first use.
+         *
+         * But keep a reference. This is to ensure proper lifetimes between
+         * singleton instances (i.e. nm_dns_manager_get() outlives NMManager). */
+        priv->dns_mgr = g_object_ref(nm_dns_manager_get());
+    }
+
+    return priv->dns_mgr;
+}
+
+/*****************************************************************************/
+
 static void
 auth_mgr_changed(NMAuthManager *auth_manager, gpointer user_data)
 {
@@ -8250,6 +8327,9 @@ dispose(GObject *object)
         g_clear_object(&priv->concheck_mgr);
     }
 
+    nm_clear_g_signal_handler(priv->dns_mgr, &priv->dns_mgr_update_pending_signal_id);
+    g_clear_object(&priv->dns_mgr);
+
     if (priv->auth_mgr) {
         g_signal_handlers_disconnect_by_func(priv->auth_mgr, G_CALLBACK(auth_mgr_changed), self);
         g_clear_object(&priv->auth_mgr);
@@ -8300,8 +8380,10 @@ dispose(GObject *object)
 
     g_clear_object(&priv->vpn_manager);
 
-    sleep_devices_clear(self);
-    nm_clear_pointer(&priv->sleep_devices, g_hash_table_unref);
+    if (priv->sleep_devices) {
+        sleep_devices_clear(self);
+        nm_clear_pointer(&priv->sleep_devices, g_hash_table_unref);
+    }
 
     if (priv->sleep_monitor) {
         g_signal_handlers_disconnect_by_func(priv->sleep_monitor, sleeping_cb, self);
diff --git a/src/core/nm-manager.h b/src/core/nm-manager.h
index f8563c3a..fcb00227 100644
--- a/src/core/nm-manager.h
+++ b/src/core/nm-manager.h
@@ -200,6 +200,10 @@ NMMetered nm_manager_get_metered(NMManager *self);
 
 void nm_manager_notify_device_availability_maybe_changed(NMManager *self);
 
+struct _NMDnsManager;
+
+struct _NMDnsManager *nm_manager_get_dns_manager(NMManager *self);
+
 /*****************************************************************************/
 
 void nm_manager_device_auth_request(NMManager                     *self,
diff --git a/src/core/nm-netns.c b/src/core/nm-netns.c
index f120e43d..5ee63152 100644
--- a/src/core/nm-netns.c
+++ b/src/core/nm-netns.c
@@ -15,21 +15,21 @@
 #include "nm-l3cfg.h"
 #include "libnm-platform/nm-platform.h"
 #include "libnm-platform/nmp-netns.h"
-#include "libnm-platform/nmp-route-manager.h"
+#include "libnm-platform/nmp-global-tracker.h"
 
 /*****************************************************************************/
 
 NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_PLATFORM, );
 
 typedef struct {
-    NMNetns         *_self_signal_user_data;
-    NMPlatform      *platform;
-    NMPNetns        *platform_netns;
-    NMPRouteManager *route_manager;
-    GHashTable      *l3cfgs;
-    GHashTable      *shared_ips;
-    CList            l3cfg_signal_pending_lst_head;
-    GSource         *signal_pending_idle_source;
+    NMNetns          *_self_signal_user_data;
+    NMPlatform       *platform;
+    NMPNetns         *platform_netns;
+    NMPGlobalTracker *global_tracker;
+    GHashTable       *l3cfgs;
+    GHashTable       *shared_ips;
+    CList             l3cfg_signal_pending_lst_head;
+    GSource          *signal_pending_idle_source;
 } NMNetnsPrivate;
 
 struct _NMNetns {
@@ -79,10 +79,10 @@ nm_netns_get_platform(NMNetns *self)
     return NM_NETNS_GET_PRIVATE(self)->platform;
 }
 
-NMPRouteManager *
-nm_netns_get_route_manager(NMNetns *self)
+NMPGlobalTracker *
+nm_netns_get_global_tracker(NMNetns *self)
 {
-    return NM_NETNS_GET_PRIVATE(self)->route_manager;
+    return NM_NETNS_GET_PRIVATE(self)->global_tracker;
 }
 
 NMDedupMultiIndex *
@@ -397,14 +397,14 @@ constructed(GObject *object)
 
     priv->platform_netns = nm_platform_netns_get(priv->platform);
 
-    priv->route_manager = nmp_route_manager_new(priv->platform);
+    priv->global_tracker = nmp_global_tracker_new(priv->platform);
 
     /* Weakly track the default rules with a dummy user-tag. These
      * rules are always weekly tracked... */
-    nmp_route_manager_track_rule_default(priv->route_manager,
-                                         AF_UNSPEC,
-                                         0,
-                                         nm_netns_parent_class /* static dummy user-tag */);
+    nmp_global_tracker_track_rule_default(priv->global_tracker,
+                                          AF_UNSPEC,
+                                          0,
+                                          nm_netns_parent_class /* static dummy user-tag */);
 
     /* Also weakly track all existing rules. These were added before NetworkManager
      * starts, so they are probably none of NetworkManager's business.
@@ -414,12 +414,12 @@ constructed(GObject *object)
      * of NetworkManager, we just don't know.
      *
      * For that reason, whenever we will touch such rules later one, we make them
-     * fully owned and no longer weekly tracked. See %NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG. */
-    nmp_route_manager_track_rule_from_platform(priv->route_manager,
-                                               NULL,
-                                               AF_UNSPEC,
-                                               0,
-                                               NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG);
+     * fully owned and no longer weekly tracked. See %NMP_GLOBAL_TRACKER_EXTERN_WEAKLY_TRACKED_USER_TAG. */
+    nmp_global_tracker_track_rule_from_platform(priv->global_tracker,
+                                                NULL,
+                                                AF_UNSPEC,
+                                                0,
+                                                NMP_GLOBAL_TRACKER_EXTERN_WEAKLY_TRACKED_USER_TAG);
 
     G_OBJECT_CLASS(nm_netns_parent_class)->constructed(object);
 
@@ -469,7 +469,7 @@ dispose(GObject *object)
     g_clear_object(&priv->platform);
     nm_clear_pointer(&priv->l3cfgs, g_hash_table_unref);
 
-    nm_clear_pointer(&priv->route_manager, nmp_route_manager_unref);
+    nm_clear_pointer(&priv->global_tracker, nmp_global_tracker_unref);
 
     G_OBJECT_CLASS(nm_netns_parent_class)->dispose(object);
 }
diff --git a/src/core/nm-netns.h b/src/core/nm-netns.h
index deb1d1f0..17c1c637 100644
--- a/src/core/nm-netns.h
+++ b/src/core/nm-netns.h
@@ -29,7 +29,7 @@ NMNetns *nm_netns_new(struct _NMPlatform *platform);
 struct _NMPlatform *nm_netns_get_platform(NMNetns *self);
 NMPNetns           *nm_netns_get_platform_netns(NMNetns *self);
 
-struct _NMPRouteManager *nm_netns_get_route_manager(NMNetns *self);
+struct _NMPGlobalTracker *nm_netns_get_global_tracker(NMNetns *self);
 
 struct _NMDedupMultiIndex *nm_netns_get_multi_idx(NMNetns *self);
 
diff --git a/src/core/nm-policy.c b/src/core/nm-policy.c
index 1a824fa4..16b0211f 100644
--- a/src/core/nm-policy.c
+++ b/src/core/nm-policy.c
@@ -826,7 +826,7 @@ update_system_hostname(NMPolicy *self, const char *msg)
         && (nm_utils_is_specific_hostname(temp_hostname)
             || nm_utils_is_specific_hostname(priv->last_hostname))) {
         external_hostname = TRUE;
-        _LOGI(LOGD_DNS,
+        _LOGD(LOGD_DNS,
               "set-hostname: current hostname was changed outside NetworkManager: '%s'",
               temp_hostname);
         priv->dhcp_hostname = FALSE;
diff --git a/src/core/platform/nm-fake-platform.c b/src/core/platform/nm-fake-platform.c
index 7d8986d7..a1ca5434 100644
--- a/src/core/platform/nm-fake-platform.c
+++ b/src/core/platform/nm-fake-platform.c
@@ -323,6 +323,15 @@ link_add(NMPlatform            *platform,
         dev_lnk = nmp_object_new(NMP_OBJECT_TYPE_LNK_BRIDGE, props);
         break;
     }
+    case NM_LINK_TYPE_BOND:
+    {
+        const NMPlatformLnkBond *props = extra_data;
+
+        nm_assert(props);
+
+        dev_lnk = nmp_object_new(NMP_OBJECT_TYPE_LNK_BOND, props);
+        break;
+    }
     case NM_LINK_TYPE_VETH:
         veth_peer = extra_data;
         g_assert(veth_peer);
diff --git a/src/core/platform/tests/monitor.c b/src/core/platform/tests/monitor.c
index ff6fae65..8d0e0107 100644
--- a/src/core/platform/tests/monitor.c
+++ b/src/core/platform/tests/monitor.c
@@ -9,6 +9,7 @@
 #include <syslog.h>
 
 #include "libnm-platform/nm-linux-platform.h"
+#include "libnm-platform/nmp-object.h"
 
 #include "nm-test-utils-core.h"
 
@@ -25,14 +26,14 @@ read_argv(int *argc, char ***argv)
 {
     GOptionContext *context;
     GOptionEntry    options[] = {
-        {"no-persist",
-         'P',
-         G_OPTION_FLAG_REVERSE,
-         G_OPTION_ARG_NONE,
-         &global_opt.persist,
-         "Exit after processing netlink messages",
-         NULL},
-        {0},
+           {"no-persist",
+            'P',
+            G_OPTION_FLAG_REVERSE,
+            G_OPTION_ARG_NONE,
+            &global_opt.persist,
+            "Exit after processing netlink messages",
+            NULL},
+           {0},
     };
     gs_free_error GError *error = NULL;
 
@@ -50,6 +51,18 @@ read_argv(int *argc, char ***argv)
     return TRUE;
 }
 
+/*****************************************************************************/
+
+static void
+mptcp_addr_dump(NMPlatform *platform)
+{
+    gs_unref_ptrarray GPtrArray *addrs = NULL;
+
+    addrs = nm_platform_mptcp_addrs_dump(platform);
+}
+
+/*****************************************************************************/
+
 int
 main(int argc, char **argv)
 {
@@ -69,6 +82,8 @@ main(int argc, char **argv)
 
     nm_linux_platform_setup();
 
+    mptcp_addr_dump(NM_PLATFORM_GET);
+
     if (global_opt.persist)
         g_main_loop_run(loop);
 
diff --git a/src/core/platform/tests/test-common.c b/src/core/platform/tests/test-common.c
index d7fed220..34ec5cc2 100644
--- a/src/core/platform/tests/test-common.c
+++ b/src/core/platform/tests/test-common.c
@@ -70,7 +70,7 @@ _ipx_address_get_all(NMPlatform *self, int ifindex, NMPObjectType obj_type)
     g_assert(NM_IS_PLATFORM(self));
     g_assert(ifindex > 0);
     g_assert(NM_IN_SET(obj_type, NMP_OBJECT_TYPE_IP4_ADDRESS, NMP_OBJECT_TYPE_IP6_ADDRESS));
-    nmp_lookup_init_object(&lookup, obj_type, ifindex);
+    nmp_lookup_init_object_by_ifindex(&lookup, obj_type, ifindex);
     return nmp_cache_lookup_to_array(nm_platform_lookup(self, &lookup),
                                      obj_type,
                                      FALSE /*addresses are always visible. */);
@@ -103,7 +103,7 @@ nmtstp_platform_ip_address_find(NMPlatform *self, int ifindex, int addr_family,
     nm_assert(addr);
 
     if (ifindex > 0)
-        nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4), ifindex);
+        nmp_lookup_init_object_by_ifindex(&lookup, NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4), ifindex);
     else
         nmp_lookup_init_obj_type(&lookup, NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4));
 
@@ -187,7 +187,9 @@ _nmtstp_platform_ip_addresses_assert(const char        *filename,
 
         plat_addrs = nm_platform_lookup_clone(
             self,
-            nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4), ifindex),
+            nmp_lookup_init_object_by_ifindex(&lookup,
+                                              NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4),
+                                              ifindex),
             NULL,
             NULL);
 
@@ -1936,10 +1938,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);
diff --git a/src/core/platform/tests/test-common.h b/src/core/platform/tests/test-common.h
index 77f2c50d..da30b0ed 100644
--- a/src/core/platform/tests/test-common.h
+++ b/src/core/platform/tests/test-common.h
@@ -518,6 +518,11 @@ void nmtstp_link_delete(NMPlatform *platform,
 
 /*****************************************************************************/
 
+#define nmtst_object_new_mptcp_addr(...) \
+    nmp_object_new(NMP_OBJECT_TYPE_MPTCP_ADDR, &((const NMPlatformMptcpAddr){__VA_ARGS__}))
+
+/*****************************************************************************/
+
 extern int NMTSTP_ENV1_IFINDEX;
 extern int NMTSTP_ENV1_EX;
 
diff --git a/src/core/platform/tests/test-link.c b/src/core/platform/tests/test-link.c
index 48b321d2..b72bcb65 100644
--- a/src/core/platform/tests/test-link.c
+++ b/src/core/platform/tests/test-link.c
@@ -111,13 +111,16 @@ software_add(NMLinkType link_type, const char *name)
     {
         gboolean bond0_exists = !!nm_platform_link_get_by_ifname(NM_PLATFORM_GET, "bond0");
         int      r;
+        const NMPlatformLnkBond nm_platform_lnk_bond_default = {
+            .mode = 3,
+        };
 
-        r = nm_platform_link_bond_add(NM_PLATFORM_GET, name, NULL);
+        r = nm_platform_link_bond_add(NM_PLATFORM_GET, name, &nm_platform_lnk_bond_default, NULL);
 
         /* Check that bond0 is *not* automatically created. */
         if (!bond0_exists)
             g_assert(!nm_platform_link_get_by_ifname(NM_PLATFORM_GET, "bond0"));
-        return r >= 0;
+        return NMTST_NM_ERR_SUCCESS(r);
     }
     case NM_LINK_TYPE_TEAM:
         return NMTST_NM_ERR_SUCCESS(nm_platform_link_team_add(NM_PLATFORM_GET, name, NULL));
@@ -853,7 +856,7 @@ static void
 _test_wireguard_change(NMPlatform *platform, int ifindex, int test_mode)
 {
     const KeyPair self_key  = {"yOWEsaXFxX9/DOkQPzqB9RufZOpfSP4LZZCErP0N0Xo=",
-                              "s6pVT2xPwktor9O5bVOSzcPqBu9uzQOUzPQHXLU2jmk="};
+                               "s6pVT2xPwktor9O5bVOSzcPqBu9uzQOUzPQHXLU2jmk="};
     const KeyPair keys[100] = {
         {"+BDHMh11bkheGfvlQpqt8P/H7N1sPXtVi05XraZS0E8=",
          "QItu7PJadBVXFXGv55CMtVnbRHdrI6E2CGlu2N5oGx4=",
diff --git a/src/core/platform/tests/test-nmp-object.c b/src/core/platform/tests/test-nmp-object.c
index ddd5aa93..193e750f 100644
--- a/src/core/platform/tests/test-nmp-object.c
+++ b/src/core/platform/tests/test-nmp-object.c
@@ -551,7 +551,9 @@ test_cache_qdisc(void)
     g_assert(nmp_cache_lookup_obj(cache, obj1b) == obj1b);
     g_assert(nmp_cache_lookup_obj(cache, obj2) == obj2);
 
-    head_entry = nmp_cache_lookup(cache, nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_QDISC, 1));
+    head_entry =
+        nmp_cache_lookup(cache,
+                         nmp_lookup_init_object_by_ifindex(&lookup, NMP_OBJECT_TYPE_QDISC, 1));
     g_assert(head_entry->len == 2);
 
     nmp_cache_free(cache);
diff --git a/src/core/platform/tests/test-platform-general.c b/src/core/platform/tests/test-platform-general.c
index 908ad4dd..c18886c9 100644
--- a/src/core/platform/tests/test-platform-general.c
+++ b/src/core/platform/tests/test-platform-general.c
@@ -788,6 +788,64 @@ test_route_type_is_nodev(void)
 
 /*****************************************************************************/
 
+static void
+test_nmp_genl_family_type_from_name(void)
+{
+    int n_run;
+    int i;
+
+    for (i = 0; i < (int) _NMP_GENL_FAMILY_TYPE_NUM; i++) {
+        const char *name = nmp_genl_family_infos[i].name;
+
+        g_assert(name);
+        if (i > 0)
+            g_assert_cmpint(strcmp(nmp_genl_family_infos[i - 1].name, name), <, 0);
+    }
+
+    g_assert_cmpint(nmp_genl_family_type_from_name("ethtool"), ==, NMP_GENL_FAMILY_TYPE_ETHTOOL);
+    g_assert_cmpint(nmp_genl_family_type_from_name("mptcp_pm"), ==, NMP_GENL_FAMILY_TYPE_MPTCP_PM);
+    g_assert_cmpint(nmp_genl_family_type_from_name("nl80211"), ==, NMP_GENL_FAMILY_TYPE_NL80211);
+    g_assert_cmpint(nmp_genl_family_type_from_name("nl802154"), ==, NMP_GENL_FAMILY_TYPE_NL802154);
+    g_assert_cmpint(nmp_genl_family_type_from_name("wireguard"),
+                    ==,
+                    NMP_GENL_FAMILY_TYPE_WIREGUARD);
+
+    g_assert_cmpint(nmp_genl_family_type_from_name(NULL), ==, _NMP_GENL_FAMILY_TYPE_NONE);
+    g_assert_cmpint(nmp_genl_family_type_from_name("a"), ==, _NMP_GENL_FAMILY_TYPE_NONE);
+    g_assert_cmpint(nmp_genl_family_type_from_name("wireguara"), ==, _NMP_GENL_FAMILY_TYPE_NONE);
+    g_assert_cmpint(nmp_genl_family_type_from_name("wireguarb"), ==, _NMP_GENL_FAMILY_TYPE_NONE);
+    g_assert_cmpint(nmp_genl_family_type_from_name(""), ==, _NMP_GENL_FAMILY_TYPE_NONE);
+    g_assert_cmpint(nmp_genl_family_type_from_name("z"), ==, _NMP_GENL_FAMILY_TYPE_NONE);
+
+    for (n_run = 0; n_run < 20; n_run++) {
+        for (i = 0; i < (int) _NMP_GENL_FAMILY_TYPE_NUM; i++) {
+            const char *cname  = nmp_genl_family_infos[i].name;
+            const int   ch_idx = nmtst_get_rand_uint() % strlen(cname);
+            char        name[200];
+            char        ch;
+            gsize       l;
+
+            l = g_strlcpy(name, cname, sizeof(name));
+            g_assert_cmpint(l, <, sizeof(name));
+
+            if (n_run == 0) {
+                g_assert_cmpint(nmp_genl_family_type_from_name(cname), ==, i);
+                g_assert_cmpint(nmp_genl_family_type_from_name(name), ==, i);
+            }
+
+            /* randomly change one character in the name. Such a name becomes invalid.
+             * There are no two valid names which only differ by one characters. */
+            do {
+                ch = nmtst_get_rand_uint() % 256;
+            } while (cname[ch_idx] == ch);
+            name[ch_idx] = ch;
+            g_assert_cmpint(nmp_genl_family_type_from_name(name), ==, _NMP_GENL_FAMILY_TYPE_NONE);
+        }
+    }
+}
+
+/*****************************************************************************/
+
 NMTST_DEFINE();
 
 int
@@ -808,6 +866,8 @@ main(int argc, char **argv)
                          GINT_TO_POINTER(2),
                          test_platform_ip_address_pretty_sort_cmp);
     g_test_add_func("/general/test_route_type_is_nodev", test_route_type_is_nodev);
+    g_test_add_func("/nm-platform/test_nmp_genl_family_type_from_name",
+                    test_nmp_genl_family_type_from_name);
 
     return g_test_run();
 }
diff --git a/src/core/platform/tests/test-route.c b/src/core/platform/tests/test-route.c
index 85838267..148ed792 100644
--- a/src/core/platform/tests/test-route.c
+++ b/src/core/platform/tests/test-route.c
@@ -10,7 +10,7 @@
 
 #include "nm-core-utils.h"
 #include "libnm-platform/nm-platform-utils.h"
-#include "libnm-platform/nmp-route-manager.h"
+#include "libnm-platform/nmp-global-tracker.h"
 
 #include "test-common.h"
 
@@ -35,8 +35,9 @@ _wait_for_ipv4_addr_device_route(NMPlatform *platform,
 
         nmp_cache_iter_for_each (
             &iter,
-            nm_platform_lookup(platform,
-                               nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP4_ROUTE, ifindex)),
+            nm_platform_lookup(
+                platform,
+                nmp_lookup_init_object_by_ifindex(&lookup, NMP_OBJECT_TYPE_IP4_ROUTE, ifindex)),
             &o) {
             const NMPlatformIP4Route *r = NMP_OBJECT_CAST_IP4_ROUTE(o);
 
@@ -646,6 +647,9 @@ test_ip4_route_options(gconstpointer test_data)
             .initrwnd  = 50,
             .mtu       = 1350,
             .lock_cwnd = TRUE,
+            .mss       = 1300,
+            .quickack  = TRUE,
+            .rto_min   = 1000,
         });
         break;
     case 2:
@@ -1167,7 +1171,7 @@ _rule_check_kernel_support(NMPlatform *platform, int attribute)
                  .addr_family = AF_INET,
                  .priority    = PROBE_PRORITY,
                  .uid_range =
-                {
+                     {
                          .start = 0,
                          .end   = 0,
                 },
@@ -1643,8 +1647,8 @@ again:
 
     if (TEST_SYNC) {
         gs_unref_hashtable GHashTable *unique_priorities = g_hash_table_new(NULL, NULL);
-        nm_auto_unref_route_manager NMPRouteManager *route_manager =
-            nmp_route_manager_new(platform);
+        nm_auto_unref_global_tracker NMPGlobalTracker *global_tracker =
+            nmp_global_tracker_new(platform);
         gs_unref_ptrarray GPtrArray *objs_sync  = NULL;
         gconstpointer                USER_TAG_1 = &platform;
         gconstpointer                USER_TAG_2 = &unique_priorities;
@@ -1666,29 +1670,29 @@ again:
         }
 
         for (i = 0; i < objs_sync->len; i++) {
-            nmp_route_manager_track_rule(route_manager,
-                                         NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
-                                         1,
-                                         USER_TAG_1,
-                                         NULL);
+            nmp_global_tracker_track_rule(global_tracker,
+                                          NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
+                                          1,
+                                          USER_TAG_1,
+                                          NULL);
             if (nmtst_get_rand_bool()) {
                 /* this has no effect, because a negative priority (of same absolute value)
                  * has lower priority than the positive priority above. */
-                nmp_route_manager_track_rule(route_manager,
-                                             NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
-                                             -1,
-                                             USER_TAG_2,
-                                             NULL);
+                nmp_global_tracker_track_rule(global_tracker,
+                                              NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
+                                              -1,
+                                              USER_TAG_2,
+                                              NULL);
             }
             if (nmtst_get_rand_uint32() % objs_sync->len == 0) {
-                nmp_route_manager_sync(route_manager, NMP_OBJECT_TYPE_ROUTING_RULE, FALSE);
+                nmp_global_tracker_sync(global_tracker, NMP_OBJECT_TYPE_ROUTING_RULE, FALSE);
                 g_assert_cmpint(nmtstp_platform_routing_rules_get_count(platform, AF_UNSPEC),
                                 ==,
                                 i + 1);
             }
         }
 
-        nmp_route_manager_sync(route_manager, NMP_OBJECT_TYPE_ROUTING_RULE, FALSE);
+        nmp_global_tracker_sync(global_tracker, NMP_OBJECT_TYPE_ROUTING_RULE, FALSE);
         g_assert_cmpint(nmtstp_platform_routing_rules_get_count(platform, AF_UNSPEC),
                         ==,
                         objs_sync->len);
@@ -1696,37 +1700,37 @@ again:
         for (i = 0; i < objs_sync->len; i++) {
             switch (nmtst_get_rand_uint32() % 3) {
             case 0:
-                nmp_route_manager_untrack_rule(route_manager,
-                                               NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
-                                               USER_TAG_1);
-                nmp_route_manager_untrack_rule(route_manager,
-                                               NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
-                                               USER_TAG_1);
+                nmp_global_tracker_untrack_rule(global_tracker,
+                                                NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
+                                                USER_TAG_1);
+                nmp_global_tracker_untrack_rule(global_tracker,
+                                                NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
+                                                USER_TAG_1);
                 break;
             case 1:
-                nmp_route_manager_track_rule(route_manager,
-                                             NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
-                                             -1,
-                                             USER_TAG_1,
-                                             NULL);
+                nmp_global_tracker_track_rule(global_tracker,
+                                              NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
+                                              -1,
+                                              USER_TAG_1,
+                                              NULL);
                 break;
             case 2:
-                nmp_route_manager_track_rule(route_manager,
-                                             NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
-                                             -2,
-                                             USER_TAG_2,
-                                             NULL);
+                nmp_global_tracker_track_rule(global_tracker,
+                                              NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
+                                              -2,
+                                              USER_TAG_2,
+                                              NULL);
                 break;
             }
             if (nmtst_get_rand_uint32() % objs_sync->len == 0) {
-                nmp_route_manager_sync(route_manager, NMP_OBJECT_TYPE_ROUTING_RULE, FALSE);
+                nmp_global_tracker_sync(global_tracker, NMP_OBJECT_TYPE_ROUTING_RULE, FALSE);
                 g_assert_cmpint(nmtstp_platform_routing_rules_get_count(platform, AF_UNSPEC),
                                 ==,
                                 objs_sync->len - i - 1);
             }
         }
 
-        nmp_route_manager_sync(route_manager, NMP_OBJECT_TYPE_ROUTING_RULE, FALSE);
+        nmp_global_tracker_sync(global_tracker, NMP_OBJECT_TYPE_ROUTING_RULE, FALSE);
 
     } else {
         for (i = 0; i < objs->len;) {
@@ -1957,6 +1961,200 @@ test_blackhole(gconstpointer test_data)
 
 /*****************************************************************************/
 
+static gboolean
+_mptcp_has_permissions(void)
+{
+    static int has_permissions = -1;
+    int        p;
+
+    /* We create a new netns for testing, where we also have CAP_NET_ADMIN.
+     * However, that is not enough for configuring MPTCP endpoints. Probably
+     * you can only create them, by running the test as root. Detect the
+     * inability, to skip the test.
+     *
+     * See https://lore.kernel.org/mptcp/20220805115020.525181-1-thaller@redhat.com/T/#u */
+
+again:
+    p = g_atomic_int_get(&has_permissions);
+
+    if (p == -1) {
+        static gsize              lock;
+        const NMPlatformMptcpAddr mptcp_addr = (NMPlatformMptcpAddr){
+            .id          = 1,
+            .addr_family = AF_INET,
+            .addr.addr4  = nmtst_inet4_from_string("1.2.3.4"),
+        };
+        int r;
+
+        if (!g_once_init_enter(&lock))
+            goto again;
+
+        if (nmtst_get_rand_one_case_in(3)) {
+            gs_unref_ptrarray GPtrArray *arr = NULL;
+
+            arr = nm_platform_mptcp_addrs_dump(NM_PLATFORM_GET);
+            g_assert_cmpint(nm_g_ptr_array_len(arr), ==, 0);
+        }
+
+        r = nm_platform_mptcp_addr_update(NM_PLATFORM_GET, TRUE, &mptcp_addr);
+        if (r == 0)
+            p = TRUE;
+        else if (r == -EPERM)
+            p = FALSE;
+        else
+            g_assert_cmpint(r, ==, 0);
+
+        if (p) {
+            if (nmtst_get_rand_one_case_in(3)) {
+                gs_unref_ptrarray GPtrArray *arr = NULL;
+
+                arr = nm_platform_mptcp_addrs_dump(NM_PLATFORM_GET);
+                g_assert_cmpint(nm_g_ptr_array_len(arr), ==, 1);
+            }
+
+            r = nm_platform_mptcp_addr_update(NM_PLATFORM_GET, FALSE, &mptcp_addr);
+            g_assert_cmpint(r, ==, 0);
+        }
+
+        if (nmtst_get_rand_one_case_in(3)) {
+            gs_unref_ptrarray GPtrArray *arr = NULL;
+
+            arr = nm_platform_mptcp_addrs_dump(NM_PLATFORM_GET);
+            g_assert_cmpint(nm_g_ptr_array_len(arr), ==, 0);
+        }
+
+        g_atomic_int_set(&has_permissions, p);
+        g_once_init_leave(&lock, 1);
+    }
+
+    return p;
+}
+
+static gboolean
+_mptcp_skip_test(void)
+{
+    if (nm_platform_genl_get_family_id(NM_PLATFORM_GET, NMP_GENL_FAMILY_TYPE_MPTCP_PM) == 0) {
+        g_test_skip("mptcp not available");
+        return TRUE;
+    }
+
+    if (!_mptcp_has_permissions()) {
+        g_test_skip("No permissions to create MPTCP endpoints");
+        return TRUE;
+    }
+
+    return FALSE;
+}
+
+static void
+test_mptcp(gconstpointer test_data)
+{
+    const int                                      TEST_IDX = GPOINTER_TO_INT(test_data);
+    gs_unref_object NMPlatform                    *platform = g_object_ref(NM_PLATFORM_GET);
+    nm_auto_unref_global_tracker NMPGlobalTracker *global_tracker =
+        nmp_global_tracker_new(platform);
+    gconstpointer const          USER_TAG = &TEST_IDX;
+    const int                    IFINDEX  = nm_platform_link_get_ifindex(platform, DEVICE_NAME);
+    guint                        i;
+    guint                        j;
+    int                          r;
+    gs_unref_ptrarray GPtrArray *arr_external =
+        g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
+    gs_unref_ptrarray GPtrArray *arr_tracked =
+        g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
+    const NMPObject *obj;
+    gboolean         delete_extra;
+
+    g_assert_cmpint(IFINDEX, >, 0);
+
+    if (_mptcp_skip_test())
+        return;
+
+    j = nmtst_get_rand_uint32() % 5;
+    for (i = 0; i < j; i++) {
+        obj = nmtst_object_new_mptcp_addr(.id          = i + 1,
+                                          .ifindex     = IFINDEX,
+                                          .addr_family = AF_INET,
+                                          .addr.addr4  = htonl(0xC0A80001u + i));
+        g_ptr_array_add(arr_external, (gpointer) obj);
+        r = nm_platform_mptcp_addr_update(platform, TRUE, NMP_OBJECT_CAST_MPTCP_ADDR(obj));
+        g_assert_cmpint(r, ==, 0);
+    }
+
+    j = nmtst_get_rand_uint32() % 10;
+    for (i = 0; i < j; i++) {
+        obj = nmtst_object_new_mptcp_addr(.ifindex     = IFINDEX,
+                                          .addr_family = AF_INET,
+                                          .addr.addr4  = htonl(0xC0A80001u + i));
+        g_ptr_array_add(arr_tracked, (gpointer) obj);
+        nmp_global_tracker_track(global_tracker,
+                                 NMP_OBJECT_TYPE_MPTCP_ADDR,
+                                 NMP_OBJECT_CAST_MPTCP_ADDR(obj),
+                                 20 - i,
+                                 USER_TAG,
+                                 NULL);
+    }
+    for (i = 0; i < arr_tracked->len;) {
+        if (nmtst_get_rand_bool()) {
+            nmp_global_tracker_untrack(global_tracker,
+                                       NMP_OBJECT_TYPE_MPTCP_ADDR,
+                                       NMP_OBJECT_CAST_MPTCP_ADDR(arr_tracked->pdata[i]),
+                                       USER_TAG);
+            g_ptr_array_remove_index(arr_tracked, i);
+        } else
+            i++;
+    }
+
+    if (arr_tracked->len == 0 || nmtst_get_rand_bool()) {
+        NMPlatformMptcpAddr a;
+
+        /* Track a dummy object that marks the ifindex as managed. */
+        nmp_global_tracker_track(global_tracker,
+                                 NMP_OBJECT_TYPE_MPTCP_ADDR,
+                                 nmp_global_tracker_mptcp_addr_init_for_ifindex(&a, IFINDEX),
+                                 10,
+                                 USER_TAG,
+                                 NULL);
+    }
+
+    nmp_global_tracker_sync_mptcp_addrs(global_tracker, FALSE);
+
+    if (nmtst_get_rand_bool()) {
+        gboolean reapply;
+
+        nmp_global_tracker_untrack_all(global_tracker, USER_TAG, TRUE, FALSE);
+        reapply = nmtst_get_rand_bool();
+        nmp_global_tracker_sync_mptcp_addrs(global_tracker, reapply);
+
+        delete_extra = !reapply;
+    } else
+        delete_extra = TRUE;
+
+    if (delete_extra) {
+        gs_unref_ptrarray GPtrArray *arr = NULL;
+
+        /* We need to delete all MPTCP address again, because the next test uses the
+         * same netns (this test setup doesn't create a netns per test). */
+        arr = nm_platform_mptcp_addrs_dump(platform);
+        for (i = 0; i < nm_g_ptr_array_len(arr); i++) {
+            r = nm_platform_mptcp_addr_update(platform,
+                                              FALSE,
+                                              NMP_OBJECT_CAST_MPTCP_ADDR(arr->pdata[i]));
+            g_assert(NMTST_NM_ERR_SUCCESS(r));
+        }
+    }
+
+    {
+        gs_unref_ptrarray GPtrArray *arr = NULL;
+
+        arr = nm_platform_mptcp_addrs_dump(platform);
+        g_assert(arr);
+        g_assert_cmpint(arr->len, ==, 0);
+    }
+}
+
+/*****************************************************************************/
+
 NMTstpSetupFunc const _nmtstp_setup_platform_func = SETUP;
 
 void
@@ -1971,6 +2169,7 @@ _nmtstp_setup_tests(void)
 #define add_test_func(testpath, test_func) nmtstp_env1_add_test_func(testpath, test_func, TRUE)
 #define add_test_func_data(testpath, test_func, arg) \
     nmtstp_env1_add_test_func_data(testpath, test_func, arg, TRUE)
+
     add_test_func("/route/ip4", test_ip4_route);
     add_test_func("/route/ip6", test_ip6_route);
     add_test_func("/route/ip4_metric0", test_ip4_route_metric0);
@@ -1998,4 +2197,8 @@ _nmtstp_setup_tests(void)
         add_test_func_data("/route/blackhole/1", test_blackhole, GINT_TO_POINTER(1));
         add_test_func_data("/route/blackhole/2", test_blackhole, GINT_TO_POINTER(2));
     }
+    if (nmtstp_is_root_test()) {
+        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));
+    }
 }
diff --git a/src/core/platform/tests/test-tc.c b/src/core/platform/tests/test-tc.c
index db23a932..6a2019c8 100644
--- a/src/core/platform/tests/test-tc.c
+++ b/src/core/platform/tests/test-tc.c
@@ -30,10 +30,11 @@ qdiscs_lookup(int ifindex)
 {
     NMPLookup lookup;
 
-    return nm_platform_lookup_clone(NM_PLATFORM_GET,
-                                    nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_QDISC, ifindex),
-                                    NULL,
-                                    NULL);
+    return nm_platform_lookup_clone(
+        NM_PLATFORM_GET,
+        nmp_lookup_init_object_by_ifindex(&lookup, NMP_OBJECT_TYPE_QDISC, ifindex),
+        NULL,
+        NULL);
 }
 
 static void
diff --git a/src/core/settings/nm-settings.c b/src/core/settings/nm-settings.c
index 1ff66e25..b7d846c6 100644
--- a/src/core/settings/nm-settings.c
+++ b/src/core/settings/nm-settings.c
@@ -376,6 +376,8 @@ typedef struct {
 
     GHashTable *sce_idx;
 
+    GCancellable *shutdown_cancellable;
+
     CList sce_dirty_lst_head;
 
     CList connections_lst_head;
@@ -389,15 +391,15 @@ typedef struct {
     gint64      startup_complete_start_timestamp_msec;
     GHashTable *startup_complete_idx;
     CList       startup_complete_scd_lst_head;
-    guint       startup_complete_timeout_id;
+    GSource    *startup_complete_timeout_source;
+
+    GSource *kf_db_flush_idle_source_timestamps;
+    GSource *kf_db_flush_idle_source_seen_bssids;
 
     guint connections_len;
 
     guint connections_generation;
 
-    guint kf_db_flush_idle_id_timestamps;
-    guint kf_db_flush_idle_id_seen_bssids;
-
     bool kf_db_pruned_timestamps;
     bool kf_db_pruned_seen_bssid;
 
@@ -541,9 +543,9 @@ _startup_complete_timeout_cb(gpointer user_data)
     NMSettings        *self = user_data;
     NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE(self);
 
-    priv->startup_complete_timeout_id = 0;
+    nm_clear_g_source_inst(&priv->startup_complete_timeout_source);
     _startup_complete_check(self, 0);
-    return G_SOURCE_REMOVE;
+    return G_SOURCE_CONTINUE;
 }
 
 static void
@@ -567,7 +569,7 @@ _startup_complete_check(NMSettings *self, gint64 now_msec)
         return;
     }
 
-    nm_clear_g_source(&priv->startup_complete_timeout_id);
+    nm_clear_g_source_inst(&priv->startup_complete_timeout_source);
 
     if (c_list_is_empty(&priv->startup_complete_scd_lst_head))
         goto ready;
@@ -609,8 +611,10 @@ next_with_ready:
 
         timeout_msec = priv->startup_complete_start_timestamp_msec + scd_not_ready->timeout_msec
                        - nm_utils_get_monotonic_timestamp_msec();
-        priv->startup_complete_timeout_id =
-            g_timeout_add(NM_CLAMP(0, timeout_msec, 60000), _startup_complete_timeout_cb, self);
+        priv->startup_complete_timeout_source =
+            nm_g_timeout_add_source(NM_CLAMP(0, timeout_msec, 60000),
+                                    _startup_complete_timeout_cb,
+                                    self);
         _LOGT("startup-complete: wait for suitable device for connection \"%s\" (%s) which has "
               "\"connection.wait-device-timeout\" set",
               nm_settings_connection_get_id(scd_not_ready->sett_conn),
@@ -637,7 +641,7 @@ ready:
     _LOGT("startup-complete: ready, no more profiles to wait for");
     priv->startup_complete_start_timestamp_msec = 0;
     nm_assert(!priv->startup_complete_idx);
-    nm_assert(priv->startup_complete_timeout_id == 0);
+    nm_assert(!priv->startup_complete_timeout_source);
     _notify(self, PROP_STARTUP_COMPLETE);
 }
 
@@ -3464,44 +3468,93 @@ load_plugins(NMSettings *self, const char *const *plugins, GError **error)
 /*****************************************************************************/
 
 static void
-pk_hostname_cb(NMAuthChain *chain, GDBusMethodInvocation *context, gpointer user_data)
+_save_hostname_write_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMSettings                    *self;
+    GDBusMethodInvocation         *context;
+    gs_free char                  *hostname     = NULL;
+    gs_unref_object NMAuthSubject *auth_subject = NULL;
+    gs_unref_object GCancellable  *cancellable  = NULL;
+    gs_free_error GError          *error        = NULL;
+
+    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_audit_log_control_op(NM_AUDIT_OP_HOSTNAME_SAVE,
+                            hostname ?: "",
+                            !error,
+                            auth_subject,
+                            error ? error->message : NULL);
+
+    if (nm_utils_error_is_cancelled(error)) {
+        g_dbus_method_invocation_return_error_literal(context,
+                                                      NM_SETTINGS_ERROR,
+                                                      NM_SETTINGS_ERROR_FAILED,
+                                                      "NetworkManager is shutting down");
+        return;
+    }
+
+    if (error) {
+        g_dbus_method_invocation_take_error(context,
+                                            g_error_new(NM_SETTINGS_ERROR,
+                                                        NM_SETTINGS_ERROR_FAILED,
+                                                        "Saving the hostname failed: %s",
+                                                        error->message));
+        return;
+    }
+
+    g_dbus_method_invocation_return_value(context, NULL);
+}
+
+static void
+_save_hostname_pk_cb(NMAuthChain *chain, GDBusMethodInvocation *context, gpointer user_data)
 {
     NMSettings        *self = NM_SETTINGS(user_data);
     NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE(self);
     NMAuthCallResult   result;
-    GError            *error = NULL;
-    const char        *hostname;
+    gs_free char      *hostname = NULL;
 
     nm_assert(G_IS_DBUS_METHOD_INVOCATION(context));
 
     c_list_unlink(nm_auth_chain_parent_lst_list(chain));
 
     result   = nm_auth_chain_get_result(chain, NM_AUTH_PERMISSION_SETTINGS_MODIFY_HOSTNAME);
-    hostname = nm_auth_chain_get_data(chain, "hostname");
+    hostname = nm_auth_chain_steal_data(chain, "hostname");
 
-    /* If our NMSettingsConnection is already gone, do nothing */
     if (result != NM_AUTH_CALL_RESULT_YES) {
-        error = g_error_new_literal(NM_SETTINGS_ERROR,
-                                    NM_SETTINGS_ERROR_PERMISSION_DENIED,
-                                    NM_UTILS_ERROR_MSG_INSUFF_PRIV);
-    } else {
-        if (!nm_hostname_manager_write_hostname(priv->hostname_manager, hostname)) {
-            error = g_error_new_literal(NM_SETTINGS_ERROR,
-                                        NM_SETTINGS_ERROR_FAILED,
-                                        "Saving the hostname failed.");
-        }
+        nm_audit_log_control_op(NM_AUDIT_OP_HOSTNAME_SAVE,
+                                hostname ?: "",
+                                FALSE,
+                                nm_auth_chain_get_subject(chain),
+                                NM_UTILS_ERROR_MSG_INSUFF_PRIV);
+        g_dbus_method_invocation_return_error_literal(context,
+                                                      NM_SETTINGS_ERROR,
+                                                      NM_SETTINGS_ERROR_PERMISSION_DENIED,
+                                                      NM_UTILS_ERROR_MSG_INSUFF_PRIV);
+        return;
     }
 
-    nm_audit_log_control_op(NM_AUDIT_OP_HOSTNAME_SAVE,
-                            hostname,
-                            !error,
-                            nm_auth_chain_get_subject(chain),
-                            error ? error->message : NULL);
+    if (!priv->shutdown_cancellable) {
+        /* we only keep a weak pointer on the cancellable, so we can
+         * wrap it up after use. We almost never require this, because
+         * SaveHostname is almost never called. */
+        priv->shutdown_cancellable = g_cancellable_new();
+        g_object_add_weak_pointer(G_OBJECT(priv->shutdown_cancellable),
+                                  (gpointer *) &priv->shutdown_cancellable);
+    }
 
-    if (error)
-        g_dbus_method_invocation_take_error(context, error);
-    else
-        g_dbus_method_invocation_return_value(context, NULL);
+    nm_hostname_manager_write_hostname(
+        priv->hostname_manager,
+        hostname,
+        priv->shutdown_cancellable,
+        _save_hostname_write_cb,
+        nm_utils_user_data_pack(self,
+                                context,
+                                g_object_ref(nm_auth_chain_get_subject(chain)),
+                                hostname,
+                                g_object_ref(priv->shutdown_cancellable)));
+    g_steal_pointer(&hostname);
 }
 
 static void
@@ -3523,13 +3576,13 @@ impl_settings_save_hostname(NMDBusObject                      *obj,
     g_variant_get(parameters, "(&s)", &hostname);
 
     /* Minimal validation of the hostname */
-    if (!nm_utils_validate_hostname(hostname)) {
+    if (nm_str_not_empty(hostname) && !nm_utils_validate_hostname(hostname)) {
         error_code   = NM_SETTINGS_ERROR_INVALID_HOSTNAME;
         error_reason = "The hostname was too long or contained invalid characters";
         goto err;
     }
 
-    chain = nm_auth_chain_new_context(invocation, pk_hostname_cb, self);
+    chain = nm_auth_chain_new_context(invocation, _save_hostname_pk_cb, self);
     if (!chain) {
         error_code   = NM_SETTINGS_ERROR_PERMISSION_DENIED;
         error_reason = NM_UTILS_ERROR_MSG_REQ_AUTH_FAILED;
@@ -3538,7 +3591,7 @@ impl_settings_save_hostname(NMDBusObject                      *obj,
 
     c_list_link_tail(&priv->auth_lst_head, nm_auth_chain_parent_lst_list(chain));
     nm_auth_chain_add_call(chain, NM_AUTH_PERMISSION_SETTINGS_MODIFY_HOSTNAME, TRUE);
-    nm_auth_chain_set_data(chain, "hostname", g_strdup(hostname), g_free);
+    nm_auth_chain_set_data(chain, "hostname", nm_strdup_not_empty(hostname), g_free);
     return;
 err:
     nm_audit_log_control_op(NM_AUDIT_OP_HOSTNAME_SAVE, hostname, FALSE, invocation, error_reason);
@@ -3842,13 +3895,13 @@ _kf_db_got_dirty_flush(NMSettings *self, gboolean is_timestamps)
     NMKeyFileDB       *kf_db;
 
     if (is_timestamps) {
-        prefix                               = "timestamps";
-        kf_db                                = priv->kf_db_timestamps;
-        priv->kf_db_flush_idle_id_timestamps = 0;
+        prefix = "timestamps";
+        kf_db  = priv->kf_db_timestamps;
+        nm_clear_g_source_inst(&priv->kf_db_flush_idle_source_timestamps);
     } else {
-        prefix                                = "seen-bssids";
-        kf_db                                 = priv->kf_db_seen_bssids;
-        priv->kf_db_flush_idle_id_seen_bssids = 0;
+        prefix = "seen-bssids";
+        kf_db  = priv->kf_db_seen_bssids;
+        nm_clear_g_source_inst(&priv->kf_db_flush_idle_source_seen_bssids);
     }
 
     if (nm_key_file_db_is_dirty(kf_db))
@@ -3859,7 +3912,7 @@ _kf_db_got_dirty_flush(NMSettings *self, gboolean is_timestamps)
               nm_key_file_db_get_filename(kf_db));
     }
 
-    return G_SOURCE_REMOVE;
+    return G_SOURCE_CONTINUE;
 }
 
 static gboolean
@@ -3880,26 +3933,27 @@ _kf_db_got_dirty_fcn(NMKeyFileDB *kf_db, gpointer user_data)
     NMSettings        *self = user_data;
     NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE(self);
     GSourceFunc        idle_func;
-    guint             *p_id;
+    GSource          **p_source;
     const char        *prefix;
 
     if (priv->kf_db_timestamps == kf_db) {
         prefix    = "timestamps";
-        p_id      = &priv->kf_db_flush_idle_id_timestamps;
+        p_source  = &priv->kf_db_flush_idle_source_timestamps;
         idle_func = _kf_db_got_dirty_flush_timestamps_cb;
     } else if (priv->kf_db_seen_bssids == kf_db) {
         prefix    = "seen-bssids";
-        p_id      = &priv->kf_db_flush_idle_id_seen_bssids;
+        p_source  = &priv->kf_db_flush_idle_source_seen_bssids;
         idle_func = _kf_db_got_dirty_flush_seen_bssids_cb;
     } else {
         nm_assert_not_reached();
         return;
     }
 
-    if (*p_id != 0)
+    if (*p_source)
         return;
     _LOGT("[%s-keyfile]: schedule flushing changes to disk", prefix);
-    *p_id = g_idle_add_full(G_PRIORITY_LOW, idle_func, self, NULL);
+    *p_source =
+        nm_g_source_attach(nm_g_idle_source_new(G_PRIORITY_LOW, idle_func, self, NULL), NULL);
 }
 
 void
@@ -4100,7 +4154,7 @@ dispose(GObject *object)
     nm_assert(c_list_is_empty(&priv->sce_dirty_lst_head));
     nm_assert(g_hash_table_size(priv->sce_idx) == 0);
 
-    nm_clear_g_source(&priv->startup_complete_timeout_id);
+    nm_clear_g_source_inst(&priv->startup_complete_timeout_source);
     nm_clear_pointer(&priv->startup_complete_idx, g_hash_table_destroy);
     nm_assert(c_list_is_empty(&priv->startup_complete_scd_lst_head));
 
@@ -4121,6 +4175,12 @@ dispose(GObject *object)
         g_clear_object(&priv->session_monitor);
     }
 
+    if (priv->shutdown_cancellable) {
+        g_object_remove_weak_pointer(G_OBJECT(priv->shutdown_cancellable),
+                                     (gpointer *) &priv->shutdown_cancellable);
+        g_cancellable_cancel(g_steal_pointer(&priv->shutdown_cancellable));
+    }
+
     G_OBJECT_CLASS(nm_settings_parent_class)->dispose(object);
 }
 
@@ -4154,8 +4214,8 @@ finalize(GObject *object)
 
     g_clear_object(&priv->agent_mgr);
 
-    nm_clear_g_source(&priv->kf_db_flush_idle_id_timestamps);
-    nm_clear_g_source(&priv->kf_db_flush_idle_id_seen_bssids);
+    nm_clear_g_source_inst(&priv->kf_db_flush_idle_source_timestamps);
+    nm_clear_g_source_inst(&priv->kf_db_flush_idle_source_seen_bssids);
     _kf_db_to_file(self, TRUE, FALSE);
     _kf_db_to_file(self, FALSE, FALSE);
     nm_key_file_db_destroy(priv->kf_db_timestamps);
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 e62c0227..6452d72a 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
@@ -393,7 +393,9 @@ make_connection_setting(const char *file,
     const char             *v;
     gs_free char           *stable_id = NULL;
     const char *const      *iter;
-    int                     vint64, i_val;
+    gint32                  vint32;
+    gint64                  vint64;
+    int                     i_val;
 
     ifcfg_name = utils_get_ifcfg_name(file, TRUE);
     if (!ifcfg_name)
@@ -586,8 +588,8 @@ make_connection_setting(const char *file,
         break;
     }
 
-    vint64 = svGetValueInt64(ifcfg, "AUTH_RETRIES", 10, -1, G_MAXINT32, -1);
-    g_object_set(s_con, NM_SETTING_CONNECTION_AUTH_RETRIES, (int) vint64, NULL);
+    vint32 = svGetValueInt64(ifcfg, "AUTH_RETRIES", 10, -1, G_MAXINT32, -1);
+    g_object_set(s_con, NM_SETTING_CONNECTION_AUTH_RETRIES, (int) vint32, NULL);
 
     nm_clear_g_free(&value);
     v = svGetValue(ifcfg, "DEVTIMEOUT", &value);
@@ -643,6 +645,14 @@ make_connection_setting(const char *file,
         PARSE_WARNING("invalid DNS_OVER_TLS setting");
     g_object_set(s_con, NM_SETTING_CONNECTION_DNS_OVER_TLS, i_val, NULL);
 
+    i_val = NM_MPTCP_FLAGS_NONE;
+    if (!svGetValueEnum(ifcfg, "MPTCP_FLAGS", nm_mptcp_flags_get_type(), &i_val, NULL))
+        PARSE_WARNING("invalid MPTCP_FLAGS setting");
+    g_object_set(s_con, NM_SETTING_CONNECTION_MPTCP_FLAGS, (guint) i_val, NULL);
+
+    vint32 = svGetValueInt64(ifcfg, "WAIT_ACTIVATION_DELAY", 10, -1, G_MAXINT32, -1);
+    g_object_set(s_con, NM_SETTING_CONNECTION_WAIT_ACTIVATION_DELAY, (int) vint32, NULL);
+
     return NM_SETTING(s_con);
 }
 
@@ -772,7 +782,7 @@ 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_utils_ip4_netmask_to_prefix(a);
         else {
             if (base_addr)
                 prefix = nm_ip_address_get_prefix(base_addr);
@@ -845,6 +855,7 @@ typedef struct {
     union {
         guint8      uint8;
         guint32     uint32;
+        gboolean    boolean;
         const char *str;
         struct {
             guint32 uint32;
@@ -873,6 +884,9 @@ enum {
     PARSE_LINE_ATTR_ROUTE_INITCWND,
     PARSE_LINE_ATTR_ROUTE_INITRWND,
     PARSE_LINE_ATTR_ROUTE_MTU,
+    PARSE_LINE_ATTR_ROUTE_ADVMSS,
+    PARSE_LINE_ATTR_ROUTE_RTO_MIN,
+    PARSE_LINE_ATTR_ROUTE_QUICKACK,
 
     /* iproute2 arguments that only matter when parsing the file. */
     PARSE_LINE_ATTR_ROUTE_TO,
@@ -886,6 +900,7 @@ enum {
 #define PARSE_LINE_TYPE_UINT8            '8'
 #define PARSE_LINE_TYPE_UINT32           'u'
 #define PARSE_LINE_TYPE_UINT32_WITH_LOCK 'l'
+#define PARSE_LINE_TYPE_BOOL             'b'
 #define PARSE_LINE_TYPE_ADDR             'a'
 #define PARSE_LINE_TYPE_ADDR_WITH_PREFIX 'p'
 #define PARSE_LINE_TYPE_IFNAME           'i'
@@ -972,6 +987,11 @@ parse_route_line(const char *line,
                 .key  = NM_IP_ROUTE_ATTRIBUTE_CWND,
                 .type = PARSE_LINE_TYPE_UINT32_WITH_LOCK,
             },
+        [PARSE_LINE_ATTR_ROUTE_ADVMSS] =
+            {
+                .key  = NM_IP_ROUTE_ATTRIBUTE_ADVMSS,
+                .type = PARSE_LINE_TYPE_UINT32_WITH_LOCK,
+            },
         [PARSE_LINE_ATTR_ROUTE_INITCWND] =
             {
                 .key  = NM_IP_ROUTE_ATTRIBUTE_INITCWND,
@@ -987,7 +1007,16 @@ parse_route_line(const char *line,
                 .key  = NM_IP_ROUTE_ATTRIBUTE_MTU,
                 .type = PARSE_LINE_TYPE_UINT32_WITH_LOCK,
             },
-
+        [PARSE_LINE_ATTR_ROUTE_QUICKACK] =
+            {
+                .key  = NM_IP_ROUTE_ATTRIBUTE_QUICKACK,
+                .type = PARSE_LINE_TYPE_BOOL,
+            },
+        [PARSE_LINE_ATTR_ROUTE_RTO_MIN] =
+            {
+                .key  = NM_IP_ROUTE_ATTRIBUTE_RTO_MIN,
+                .type = PARSE_LINE_TYPE_UINT32,
+            },
         [PARSE_LINE_ATTR_ROUTE_TO] =
             {
                 .key                         = "to",
@@ -1090,6 +1119,9 @@ parse_route_line(const char *line,
             case PARSE_LINE_TYPE_UINT32_WITH_LOCK:
                 i_words++;
                 goto parse_line_type_uint32_with_lock;
+            case PARSE_LINE_TYPE_BOOL:
+                i_words++;
+                goto parse_line_type_bool;
             case PARSE_LINE_TYPE_ADDR:
                 i_words++;
                 goto parse_line_type_addr;
@@ -1218,6 +1250,22 @@ parse_line_type_uint32_with_lock:
         i_words++;
         goto next;
 
+parse_line_type_bool:
+        s = words[i_words];
+        if (!s)
+            goto err_word_missing_argument;
+        p_data->v.boolean = !!_nm_utils_ascii_str_to_int64(s, 10, 0, 1, 0);
+        if (errno) {
+            g_set_error(error,
+                        NM_SETTINGS_ERROR,
+                        NM_SETTINGS_ERROR_INVALID_CONNECTION,
+                        "Argument for \"%s\" is not a valid boolean number",
+                        w);
+            return -EINVAL;
+        }
+        i_words++;
+        goto next;
+
 parse_line_type_ifname:
         s = words[i_words];
         if (!s)
@@ -1370,6 +1418,9 @@ next:;
                                       p_info->key,
                                       g_variant_new_uint32(p_data->v.uint32_with_lock.uint32));
             break;
+        case PARSE_LINE_TYPE_BOOL:
+            nm_ip_route_set_attribute(route, p_info->key, g_variant_new_boolean(p_data->v.boolean));
+            break;
         case PARSE_LINE_TYPE_ADDR:
         case PARSE_LINE_TYPE_ADDR_WITH_PREFIX:
             nm_ip_route_set_attribute(
@@ -1445,7 +1496,7 @@ 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);
+        prefix = _nm_utils_ip4_netmask_to_prefix(netmask);
         if (netmask != _nm_utils_ip4_prefix_to_netmask(prefix)) {
             g_set_error(error,
                         NM_SETTINGS_ERROR,
@@ -1786,6 +1837,7 @@ make_ip4_setting(shvarFile *ifcfg,
     int                                priority;
     const char *const                 *item;
     guint32                            route_table;
+    int                                ipv4_link_local;
 
     nm_assert(out_has_defroute && !*out_has_defroute);
 
@@ -1858,6 +1910,14 @@ make_ip4_setting(shvarFile *ifcfg,
         route_table = 0;
     }
 
+    ipv4_link_local = NM_SETTING_IP4_LL_DEFAULT;
+    if (!svGetValueEnum(ifcfg,
+                        "IPV4_LINK_LOCAL",
+                        nm_setting_ip4_link_local_get_type(),
+                        &ipv4_link_local,
+                        NULL))
+        PARSE_WARNING("invalid IPV4_LINK_LOCAL setting");
+
     g_object_set(s_ip4,
                  NM_SETTING_IP_CONFIG_METHOD,
                  method,
@@ -1873,6 +1933,8 @@ make_ip4_setting(shvarFile *ifcfg,
                  svGetValueInt64(ifcfg, "IPV4_ROUTE_METRIC", 10, -1, G_MAXUINT32, -1),
                  NM_SETTING_IP_CONFIG_ROUTE_TABLE,
                  (guint) route_table,
+                 NM_SETTING_IP4_CONFIG_LINK_LOCAL,
+                 ipv4_link_local,
                  NULL);
 
     if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED))
@@ -2498,7 +2560,13 @@ make_ip6_setting(shvarFile *ifcfg, shvarFile *network_ifcfg, gboolean routes_rea
         }
     }
 
-    i_val = NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_EUI64;
+    /* IPv6 tokenized interface identifier */
+    nm_clear_g_free(&value);
+    v = svGetValueStr(ifcfg, "IPV6_TOKEN", &value);
+    if (v)
+        g_object_set(s_ip6, NM_SETTING_IP6_CONFIG_TOKEN, v, NULL);
+
+    i_val = NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_DEFAULT_OR_EUI64;
     if (!svGetValueEnum(ifcfg,
                         "IPV6_ADDR_GEN_MODE",
                         nm_setting_ip6_config_addr_gen_mode_get_type(),
@@ -2506,15 +2574,14 @@ make_ip6_setting(shvarFile *ifcfg, shvarFile *network_ifcfg, gboolean routes_rea
                         &local)) {
         PARSE_WARNING("%s", local->message);
         g_clear_error(&local);
+    } else if (errno == ENOENT) {
+        /* The key is not specified. If "v" (IPV6_TOKEN) is set,
+         * we default to EUI64. Otherwise, the connection would not verify. */
+        if (v)
+            i_val = NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_EUI64;
     }
     g_object_set(s_ip6, NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE, i_val, NULL);
 
-    /* IPv6 tokenized interface identifier */
-    nm_clear_g_free(&value);
-    v = svGetValueStr(ifcfg, "IPV6_TOKEN", &value);
-    if (v)
-        g_object_set(s_ip6, NM_SETTING_IP6_CONFIG_TOKEN, v, NULL);
-
     /* DNS servers
      * Pick up just IPv6 addresses (IPv4 addresses are taken by make_ip4_setting())
      */
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 15e01ba9..e1ef8174 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
@@ -960,6 +960,7 @@ const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[] = {
     _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),
+    _KEY_TYPE("IPV4_LINK_LOCAL", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV4_REQUIRED_TIMEOUT", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV4_ROUTE_METRIC", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV4_ROUTE_TABLE", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
@@ -1005,6 +1006,7 @@ const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[] = {
     _KEY_TYPE("MDNS", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("METRIC", NMS_IFCFG_KEY_TYPE_IS_NUMBERED),
     _KEY_TYPE("MODE", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
+    _KEY_TYPE("MPTCP_FLAGS", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("MTU", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("MUD_URL", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("MULTI_CONNECT", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
@@ -1061,6 +1063,7 @@ const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[] = {
     _KEY_TYPE("VLAN_INGRESS_PRIORITY_MAP", 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),
     _KEY_TYPE("WEP_KEY_FLAGS", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("WPA_ALLOW_WPA", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("WPA_ALLOW_WPA2", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
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 d7a1cfc6..d1f8dbad 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[253];
+extern const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[256];
 
 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 551f2b91..96efc9e5 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
@@ -2095,6 +2095,7 @@ write_connection_setting(NMSettingConnection *s_con, shvarFile *ifcfg)
     NMSettingConnectionMdns       mdns;
     NMSettingConnectionLlmnr      llmnr;
     NMSettingConnectionDnsOverTls dns_over_tls;
+    NMMptcpFlags                  mptcp_flags;
     guint32                       vuint32;
     const char                   *tmp, *mud_url;
 
@@ -2258,6 +2259,9 @@ write_connection_setting(NMSettingConnection *s_con, shvarFile *ifcfg)
     vint = nm_setting_connection_get_auth_retries(s_con);
     svSetValueInt64_cond(ifcfg, "AUTH_RETRIES", vint >= 0, vint);
 
+    vint32 = nm_setting_connection_get_wait_activation_delay(s_con);
+    svSetValueInt64_cond(ifcfg, "WAIT_ACTIVATION_DELAY", vint32 >= 0, vint32);
+
     vint32 = nm_setting_connection_get_wait_device_timeout(s_con);
     if (vint32 == -1) {
         /* pass */
@@ -2286,13 +2290,18 @@ write_connection_setting(NMSettingConnection *s_con, shvarFile *ifcfg)
                        nm_setting_connection_dns_over_tls_get_type(),
                        dns_over_tls);
     }
+
+    mptcp_flags = nm_setting_connection_get_mptcp_flags(s_con);
+    if (mptcp_flags != NM_MPTCP_FLAGS_NONE)
+        svSetValueEnum(ifcfg, "MPTCP_FLAGS", nm_mptcp_flags_get_type(), mptcp_flags);
 }
 
 static char *
 get_route_attributes_string(NMIPRoute *route, int family)
 {
     gs_free const char **names = NULL;
-    GVariant            *attr, *lock;
+    GVariant            *attr;
+    GVariant            *lock;
     GString             *str;
     guint                i, len;
 
@@ -2317,6 +2326,7 @@ get_route_attributes_string(NMIPRoute *route, int family)
             continue;
 
         if (NM_IN_STRSET(names[i],
+                         NM_IP_ROUTE_ATTRIBUTE_ADVMSS,
                          NM_IP_ROUTE_ATTRIBUTE_WINDOW,
                          NM_IP_ROUTE_ATTRIBUTE_CWND,
                          NM_IP_ROUTE_ATTRIBUTE_INITCWND,
@@ -2332,12 +2342,12 @@ get_route_attributes_string(NMIPRoute *route, int family)
                                    names[i],
                                    (lock && g_variant_get_boolean(lock)) ? "lock " : "",
                                    g_variant_get_uint32(attr));
-        } else if (strstr(names[i], "lock-")) {
+        } else if (NM_STR_HAS_PREFIX(names[i], "lock-")) {
             const char *n = &(names[i])[NM_STRLEN("lock-")];
 
-            attr = nm_ip_route_get_attribute(route, n);
-            if (!attr) {
-                g_string_append_printf(str, "%s lock 0", n);
+            if (!nm_ip_route_get_attribute(route, n)) {
+                if (g_variant_get_boolean(attr))
+                    g_string_append_printf(str, "%s lock 0", n);
             } else {
                 /* we also have a corresponding attribute with the numeric value. The
                  * lock setting is handled above. */
@@ -2346,8 +2356,12 @@ get_route_attributes_string(NMIPRoute *route, int family)
             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_streq(names[i], NM_IP_ROUTE_ATTRIBUTE_TABLE)) {
+        } else if (NM_IN_STRSET(names[i],
+                                NM_IP_ROUTE_ATTRIBUTE_TABLE,
+                                NM_IP_ROUTE_ATTRIBUTE_RTO_MIN)) {
             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));
         } else if (nm_streq(names[i], NM_IP_ROUTE_ATTRIBUTE_ONLINK)) {
             if (g_variant_get_boolean(attr))
                 g_string_append(str, "onlink");
@@ -2733,6 +2747,7 @@ write_ip4_setting(NMConnection *connection,
     gboolean                      has_netmask;
     NMDhcpHostnameFlags           flags;
     const char *const            *strv;
+    NMSettingIP4LinkLocal         ipv4_link_local;
 
     NM_SET_OUT(out_route_content_svformat, NULL);
     NM_SET_OUT(out_route_content, NULL);
@@ -2839,6 +2854,14 @@ write_ip4_setting(NMConnection *connection,
     value = nm_setting_ip_config_get_dhcp_hostname(s_ip4);
     svSetValueStr(ifcfg, "DHCP_HOSTNAME", value);
 
+    ipv4_link_local = nm_setting_ip4_config_get_link_local(NM_SETTING_IP4_CONFIG(s_ip4));
+    if (ipv4_link_local != NM_SETTING_IP4_LL_DEFAULT) {
+        svSetValueEnum(ifcfg,
+                       "IPV4_LINK_LOCAL",
+                       nm_setting_ip4_link_local_get_type(),
+                       ipv4_link_local);
+    }
+
     value = nm_setting_ip4_config_get_dhcp_fqdn(NM_SETTING_IP4_CONFIG(s_ip4));
     svSetValueStr(ifcfg, "DHCP_FQDN", value);
 
@@ -3149,7 +3172,7 @@ write_ip6_setting(NMConnection *connection, shvarFile *ifcfg, GString **out_rout
 
     /* IPv6 Address generation mode */
     addr_gen_mode = nm_setting_ip6_config_get_addr_gen_mode(NM_SETTING_IP6_CONFIG(s_ip6));
-    if (addr_gen_mode != NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_EUI64) {
+    if (addr_gen_mode != NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_DEFAULT_OR_EUI64) {
         svSetValueEnum(ifcfg,
                        "IPV6_ADDR_GEN_MODE",
                        nm_setting_ip6_config_addr_gen_mode_get_type(),
diff --git a/src/core/settings/plugins/ifcfg-rh/shvar.c b/src/core/settings/plugins/ifcfg-rh/shvar.c
index 2ca49558..fe8187c3 100644
--- a/src/core/settings/plugins/ifcfg-rh/shvar.c
+++ b/src/core/settings/plugins/ifcfg-rh/shvar.c
@@ -1328,6 +1328,7 @@ svGetValueEnum(shvarFile *s, const char *key, GType gtype, int *out_value, GErro
     if (!svalue) {
         /* don't touch out_value. The caller is supposed
          * to initialize it with the default value. */
+        errno = ENOENT;
         return TRUE;
     }
 
@@ -1339,10 +1340,12 @@ svGetValueEnum(shvarFile *s, const char *key, GType gtype, int *out_value, GErro
                     err_token,
                     svalue,
                     key);
+        errno = EINVAL;
         return FALSE;
     }
 
     NM_SET_OUT(out_value, value);
+    errno = 0;
     return TRUE;
 }
 
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-System_test-bridge-component-b.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-System_test-bridge-component-b.cexpected
index 71d2cda8..1330382d 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-System_test-bridge-component-b.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-System_test-bridge-component-b.cexpected
@@ -13,4 +13,4 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_User_1.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_User_1.cexpected
index a48be78a..15ced3ab 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_User_1.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_User_1.cexpected
@@ -28,7 +28,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test User 1"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Bond_Main.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Bond_Main.cexpected
index ccc0f35a..1aeb2cdc 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Bond_Main.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Bond_Main.cexpected
@@ -13,7 +13,7 @@ IPV4_FAILURE_FATAL=no
 IPV6INIT=no
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write Bond Main"
 UUID=${UUID}
 DEVICE=bond0
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Permissions.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Permissions.cexpected
index d4a978b6..a7df77e4 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Permissions.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Permissions.cexpected
@@ -7,7 +7,7 @@ IPV4_FAILURE_FATAL=no
 IPV6INIT=no
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write Permissions"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Proxy_Basic.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Proxy_Basic.cexpected
index 716eedc2..7fcd2c15 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Proxy_Basic.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Proxy_Basic.cexpected
@@ -9,7 +9,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write Proxy Basic"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Routing_Rules.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Routing_Rules.cexpected
index 0c2fa035..d03e9a0d 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Routing_Rules.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Routing_Rules.cexpected
@@ -3,12 +3,13 @@ PROXY_METHOD=none
 BROWSER_ONLY=no
 BOOTPROTO=dhcp
 DEFROUTE=yes
+IPV4_LINK_LOCAL=enabled
 IPV4_FAILURE_FATAL=no
 IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 ROUTING_RULE_1="priority 10 from 0.0.0.0/0 table 1"
 ROUTING_RULE_2="priority 10 to 192.167.8.0/24 table 2"
 ROUTING_RULE6_3="priority 10 from ::/0 table 10"
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_VLAN_reorder_hdr.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_VLAN_reorder_hdr.cexpected
index 9c2a1ff0..6a233664 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_VLAN_reorder_hdr.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_VLAN_reorder_hdr.cexpected
@@ -15,7 +15,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write VLAN reorder_hdr"
 UUID=${UUID}
 ONBOOT=no
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_AP_Mode.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_AP_Mode.cexpected
index ccec2b81..caeaaff8 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_AP_Mode.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_AP_Mode.cexpected
@@ -13,7 +13,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write Wi-Fi AP Mode"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Band_A.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Band_A.cexpected
index ddbd986f..7e3d4f02 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Band_A.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Band_A.cexpected
@@ -12,7 +12,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write Wi-Fi Band A"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Hidden.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Hidden.cexpected
index 495a24d3..01a091ec 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Hidden.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Hidden.cexpected
@@ -11,7 +11,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write Wi-Fi Hidden"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_always.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_always.cexpected
index aec6918a..7fa0fc98 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_always.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_always.cexpected
@@ -12,7 +12,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write Wi-Fi MAC always"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_default.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_default.cexpected
index 9d47163f..1b4ca948 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_default.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_default.cexpected
@@ -11,7 +11,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write Wi-Fi MAC default"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_missing.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_missing.cexpected
index 43c07ddb..089d1442 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_missing.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_missing.cexpected
@@ -12,7 +12,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write Wi-Fi MAC missing"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_never.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_never.cexpected
index 21f2e2de..880db3f7 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_never.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_MAC_never.cexpected
@@ -12,7 +12,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write Wi-Fi MAC never"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wifi_LEAP.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wifi_LEAP.cexpected
index f1b01a21..b9129c59 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wifi_LEAP.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wifi_LEAP.cexpected
@@ -13,7 +13,7 @@ IPV4_FAILURE_FATAL=no
 IPV6INIT=no
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write Wifi LEAP"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wifi_WEP_104_ASCII.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wifi_WEP_104_ASCII.cexpected
index 60540fbe..2dd8958b 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wifi_WEP_104_ASCII.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wifi_WEP_104_ASCII.cexpected
@@ -12,7 +12,7 @@ IPV4_FAILURE_FATAL=no
 IPV6INIT=no
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write Wifi WEP 104 ASCII"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_Auto-Negotiate.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_Auto-Negotiate.cexpected
index 214f5da5..8dd2cbff 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_Auto-Negotiate.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_Auto-Negotiate.cexpected
@@ -9,7 +9,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write Wired Auto-Negotiate"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_Static_Routes.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_Static_Routes.cexpected
index fe864c43..bc2806a9 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_Static_Routes.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_Static_Routes.cexpected
@@ -19,6 +19,7 @@ ARPING_WAIT=1
 IPV6INIT=no
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
+IPV6_ADDR_GEN_MODE=eui64
 NAME="Test Write Wired Static Routes"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_Wake-on-LAN.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_Wake-on-LAN.cexpected
index de66dcdd..34f89b5e 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_Wake-on-LAN.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_Wake-on-LAN.cexpected
@@ -9,7 +9,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write Wired Wake-on-LAN"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_match.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_match.cexpected
index 908cb917..318fae5d 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_match.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_Wired_match.cexpected
@@ -10,6 +10,7 @@ IPV4_FAILURE_FATAL=no
 IPV6INIT=no
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
+IPV6_ADDR_GEN_MODE=eui64
 NAME="Test Write Wired with Match setting"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-dcb-test.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-dcb-test.cexpected
index fef36f2f..0a9ef3d4 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-dcb-test.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-dcb-test.cexpected
@@ -33,7 +33,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME=dcb-test
 UUID=${UUID}
 DEVICE=eth0
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-random_wifi_connection.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-random_wifi_connection.cexpected
index 1f5e04ff..0a5c2ccf 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-random_wifi_connection.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-random_wifi_connection.cexpected
@@ -10,6 +10,7 @@ IPV4_FAILURE_FATAL=no
 IPV6INIT=no
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
+IPV6_ADDR_GEN_MODE=eui64
 NAME="random wifi connection"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-random_wifi_connection_2.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-random_wifi_connection_2.cexpected
index 3dcdbda0..8a874d24 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-random_wifi_connection_2.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-random_wifi_connection_2.cexpected
@@ -10,6 +10,7 @@ IPV4_FAILURE_FATAL=no
 IPV6INIT=no
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
+IPV6_ADDR_GEN_MODE=eui64
 NAME="random wifi connection 2"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-ip6-disabled.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-ip6-disabled.cexpected
index b3199c01..9cba7e20 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-ip6-disabled.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-ip6-disabled.cexpected
@@ -8,6 +8,7 @@ IPV6_DISABLED=yes
 IPV6INIT=no
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
+IPV6_ADDR_GEN_MODE=eui64
 NAME="Test Write Wired Disabled IP6"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-link_local b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-link_local
new file mode 100644
index 00000000..cfc963fb
--- /dev/null
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-link_local
@@ -0,0 +1,3 @@
+TYPE=Ethernet
+BOOTPROTO=dhcp
+IPV4_LINK_LOCAL=enabled
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-sriov-write.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-sriov-write.cexpected
index a77c615d..121ead79 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-sriov-write.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-sriov-write.cexpected
@@ -14,7 +14,7 @@ IPV4_FAILURE_FATAL=no
 IPV6INIT=no
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write SR-IOV config"
 UUID=${UUID}
 DEVICE=eth0
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-tc-write-empty.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-tc-write-empty.cexpected
index 1125ef55..8f513922 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-tc-write-empty.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-tc-write-empty.cexpected
@@ -11,7 +11,7 @@ IPV4_FAILURE_FATAL=no
 IPV6INIT=no
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write TC config"
 UUID=${UUID}
 DEVICE=eth0
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-tc-write.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-tc-write.cexpected
index d87efd72..698bda3a 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-tc-write.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-tc-write.cexpected
@@ -12,7 +12,7 @@ IPV4_FAILURE_FATAL=no
 IPV6INIT=no
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write TC config"
 UUID=${UUID}
 DEVICE=eth0
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-1.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-1.cexpected
index 5186f90c..11fd060e 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-1.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-1.cexpected
@@ -8,7 +8,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME=test_roundtrip_ethtool
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-2.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-2.cexpected
index 5632ebec..abd74bbd 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-2.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-2.cexpected
@@ -9,7 +9,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME=test_roundtrip_ethtool
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-3.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-3.cexpected
index 96eec43b..962cbdd5 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-3.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-3.cexpected
@@ -9,7 +9,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME=test_roundtrip_ethtool
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-4.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-4.cexpected
index 90250a30..213045df 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-4.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-4.cexpected
@@ -9,7 +9,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME=test_roundtrip_ethtool
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-5.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-5.cexpected
index 721eacb2..d3b9c056 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-5.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_roundtrip_ethtool-5.cexpected
@@ -9,7 +9,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME=test_roundtrip_ethtool
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_write_wired_auto_negotiate_on.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_write_wired_auto_negotiate_on.cexpected
index 42608576..542f5b5b 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_write_wired_auto_negotiate_on.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test_write_wired_auto_negotiate_on.cexpected
@@ -9,7 +9,7 @@ IPV6INIT=yes
 IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
-IPV6_ADDR_GEN_MODE=stable-privacy
+IPV6_ADDR_GEN_MODE=default
 NAME="Test Write Wired Auto-Negotiate"
 UUID=${UUID}
 ONBOOT=yes
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 9c05417e..717ad1ac 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"
+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"
 
 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 83e48b81..9fdae4bd 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
@@ -668,8 +668,8 @@ test_read_miscellaneous_variables(void)
     NMSettingWired               *s_wired;
     NMSettingIPConfig            *s_ip4;
     char                         *expected_mac_blacklist[3] = {"00:16:41:11:22:88",
-                                       "00:16:41:11:22:99",
-                                       "6a:5d:5a:fa:dd:f0"};
+                                                               "00:16:41:11:22:99",
+                                                               "6a:5d:5a:fa:dd:f0"};
     int                           mac_blacklist_num, i;
     guint64                       expected_timestamp = 0;
 
@@ -1352,6 +1352,7 @@ test_read_wired_static_routes(void)
     g_assert_cmpint(nm_ip_route_get_prefix(ip4_route), ==, 32);
     g_assert_cmpstr(nm_ip_route_get_next_hop(ip4_route), ==, "192.168.1.7");
     g_assert_cmpint(nm_ip_route_get_metric(ip4_route), ==, 3);
+    nmtst_assert_route_attribute_uint32(ip4_route, NM_IP_ROUTE_ATTRIBUTE_ADVMSS, 1300);
     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);
@@ -1360,6 +1361,8 @@ test_read_wired_static_routes(void)
     nmtst_assert_route_attribute_uint32(ip4_route, NM_IP_ROUTE_ATTRIBUTE_MTU, 9000);
     nmtst_assert_route_attribute_boolean(ip4_route, NM_IP_ROUTE_ATTRIBUTE_LOCK_MTU, TRUE);
     nmtst_assert_route_attribute_boolean(ip4_route, NM_IP_ROUTE_ATTRIBUTE_LOCK_INITCWND, TRUE);
+    nmtst_assert_route_attribute_uint32(ip4_route, NM_IP_ROUTE_ATTRIBUTE_RTO_MIN, 300);
+    nmtst_assert_route_attribute_boolean(ip4_route, NM_IP_ROUTE_ATTRIBUTE_QUICKACK, TRUE);
     nmtst_assert_route_attribute_string(ip4_route, NM_IP_ROUTE_ATTRIBUTE_SRC, "1.1.1.1");
     nmtst_assert_route_attribute_byte(ip4_route, NM_IP_ROUTE_ATTRIBUTE_SCOPE, 10);
 
@@ -2027,17 +2030,17 @@ test_read_wired_aliases_good(gconstpointer test_data)
     NMSettingIPConfig            *s_ip4;
     int                           expected_num_addresses;
     const char                   *expected_address_0[] = {"192.168.1.5",
-                                        "192.168.1.6",
-                                        "192.168.1.9",
-                                        "192.168.1.99",
-                                        NULL};
+                                                          "192.168.1.6",
+                                                          "192.168.1.9",
+                                                          "192.168.1.99",
+                                                          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,
@@ -4538,7 +4541,12 @@ test_write_routing_rules(void)
     _nm_connection_new_setting(connection, NM_TYPE_SETTING_WIRED);
 
     s_ip4 = _nm_connection_new_setting(connection, NM_TYPE_SETTING_IP4_CONFIG);
-    g_object_set(s_ip4, NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP4_CONFIG_METHOD_AUTO, NULL);
+    g_object_set(s_ip4,
+                 NM_SETTING_IP_CONFIG_METHOD,
+                 NM_SETTING_IP4_CONFIG_METHOD_AUTO,
+                 NM_SETTING_IP4_CONFIG_LINK_LOCAL,
+                 NM_SETTING_IP4_LL_ENABLED,
+                 NULL);
 
     s_ip6 = _nm_connection_new_setting(connection, NM_TYPE_SETTING_IP6_CONFIG);
     g_object_set(s_ip6, NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP6_CONFIG_METHOD_AUTO, NULL);
@@ -4634,6 +4642,34 @@ test_write_wired_dhcp_plus_ip(void)
 }
 
 static void
+test_read_write_link_local(void)
+{
+    nmtst_auto_unlinkfile char   *testfile   = NULL;
+    gs_unref_object NMConnection *connection = NULL;
+    gs_unref_object NMConnection *reread     = NULL;
+    NMSettingIPConfig            *s_ip4;
+
+    connection =
+        _connection_from_file(TEST_IFCFG_DIR "/ifcfg-test-link_local", NULL, TYPE_ETHERNET, NULL);
+
+    s_ip4 = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_IP4_CONFIG);
+    g_assert(nm_setting_ip4_config_get_link_local(NM_SETTING_IP4_CONFIG(s_ip4))
+             == NM_SETTING_IP4_LL_ENABLED);
+
+    g_object_set(s_ip4, NM_SETTING_IP4_CONFIG_LINK_LOCAL, NM_SETTING_IP4_LL_DISABLED, NULL);
+
+    _writer_new_connection(connection, TEST_SCRATCH_DIR, &testfile);
+
+    reread = _connection_from_file(testfile, NULL, TYPE_ETHERNET, NULL);
+
+    nmtst_assert_connection_equals(connection, TRUE, reread, FALSE);
+
+    s_ip4 = nmtst_connection_assert_setting(reread, NM_TYPE_SETTING_IP4_CONFIG);
+    g_assert(nm_setting_ip4_config_get_link_local(NM_SETTING_IP4_CONFIG(s_ip4))
+             == NM_SETTING_IP4_LL_DISABLED);
+}
+
+static void
 test_read_write_wired_dhcp_send_hostname(void)
 {
     nmtst_auto_unlinkfile char   *testfile   = NULL;
@@ -8153,8 +8189,8 @@ test_read_infiniband(void)
     char                         *unmanaged = NULL;
     const char                   *mac;
     char        expected_mac_address[INFINIBAND_ALEN] = {0x80, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55,
-                                                  0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc,
-                                                  0xdd, 0xee, 0xff, 0x00, 0x11, 0x22};
+                                                         0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc,
+                                                         0xdd, 0xee, 0xff, 0x00, 0x11, 0x22};
     const char *transport_mode;
 
     connection = _connection_from_file(TEST_IFCFG_DIR "/ifcfg-test-infiniband",
@@ -10245,6 +10281,7 @@ main(int argc, char **argv)
     g_test_add_func(TPATH "read-dhcp", test_read_wired_dhcp);
     g_test_add_func(TPATH "read-dhcp-plus-ip", test_read_wired_dhcp_plus_ip);
     g_test_add_func(TPATH "read-shared-plus-ip", test_read_wired_shared_plus_ip);
+    g_test_add_func(TPATH "read-write-link-local", test_read_write_link_local);
     g_test_add_func(TPATH "read-dhcp-send-hostname", test_read_write_wired_dhcp_send_hostname);
     g_test_add_func(TPATH "read-dhcpv6-hostname-fallback",
                     test_read_wired_dhcpv6_hostname_fallback);
diff --git a/src/core/settings/plugins/ifupdown/nms-ifupdown-parser.c b/src/core/settings/plugins/ifupdown/nms-ifupdown-parser.c
index c079266e..a1d6781b 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_utils_ip4_netmask_to_prefix(tmp_mask);
             }
         }
 
diff --git a/src/core/settings/plugins/keyfile/nms-keyfile-plugin.c b/src/core/settings/plugins/keyfile/nms-keyfile-plugin.c
index efb9bfce..1d7de8d2 100644
--- a/src/core/settings/plugins/keyfile/nms-keyfile-plugin.c
+++ b/src/core/settings/plugins/keyfile/nms-keyfile-plugin.c
@@ -26,8 +26,6 @@
 #include "libnm-core-intern/nm-core-internal.h"
 #include "libnm-core-intern/nm-keyfile-internal.h"
 
-#include "libnm-systemd-shared/nm-sd-utils-shared.h"
-
 #include "settings/nm-settings-plugin.h"
 #include "settings/nm-settings-storage.h"
 #include "settings/nm-settings-utils.h"
@@ -1247,9 +1245,9 @@ nms_keyfile_plugin_init(NMSKeyfilePlugin *plugin)
     /* dirname_libs are a set of read-only directories with lower priority than /etc or /run.
      * There is nothing complicated about having multiple of such directories, so dirname_libs
      * is a list (which currently only has at most one directory). */
-    priv->dirname_libs[0] = nm_sd_utils_path_simplify(g_strdup(NM_KEYFILE_PATH_NAME_LIB));
+    priv->dirname_libs[0] = nm_path_simplify(g_strdup(NM_KEYFILE_PATH_NAME_LIB));
     priv->dirname_libs[1] = NULL;
-    priv->dirname_run     = nm_sd_utils_path_simplify(g_strdup(NM_KEYFILE_PATH_NAME_RUN));
+    priv->dirname_run     = nm_path_simplify(g_strdup(NM_KEYFILE_PATH_NAME_RUN));
     priv->dirname_etc     = nm_config_data_get_value(NM_CONFIG_GET_DATA_ORIG,
                                                  NM_CONFIG_KEYFILE_GROUP_KEYFILE,
                                                  NM_CONFIG_KEYFILE_KEY_KEYFILE_PATH,
@@ -1262,9 +1260,9 @@ nms_keyfile_plugin_init(NMSKeyfilePlugin *plugin)
     } else if (!priv->dirname_etc || priv->dirname_etc[0] != '/') {
         /* either invalid path or unspecified. Use the default. */
         g_free(priv->dirname_etc);
-        priv->dirname_etc = nm_sd_utils_path_simplify(g_strdup(NM_KEYFILE_PATH_NAME_ETC_DEFAULT));
+        priv->dirname_etc = nm_path_simplify(g_strdup(NM_KEYFILE_PATH_NAME_ETC_DEFAULT));
     } else
-        nm_sd_utils_path_simplify(priv->dirname_etc);
+        nm_path_simplify(priv->dirname_etc);
 
     /* no duplicates */
     if (NM_IN_STRSET(priv->dirname_libs[0], priv->dirname_etc, priv->dirname_run))
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 11a8e416..1307d4b2 100644
--- a/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c
+++ b/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c
@@ -1805,8 +1805,8 @@ test_read_infiniband_connection(void)
     NMSettingInfiniband          *s_ib;
     const char                   *mac;
     guint8      expected_mac[INFINIBAND_ALEN] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
-                                            0x77, 0x88, 0x99, 0x01, 0x12, 0x23, 0x34,
-                                            0x45, 0x56, 0x67, 0x78, 0x89, 0x90};
+                                                 0x77, 0x88, 0x99, 0x01, 0x12, 0x23, 0x34,
+                                                 0x45, 0x56, 0x67, 0x78, 0x89, 0x90};
     const char *expected_id                   = "Test InfiniBand Connection";
     const char *expected_uuid                 = "4e80a56d-c99f-4aad-a6dd-b449bc398c57";
 
@@ -1924,6 +1924,7 @@ test_write_bridge_main(void)
     gs_unref_object NMConnection *connection = NULL;
     NMSettingConnection          *s_con;
     NMSettingBridge              *s_bridge;
+    NMSettingWired               *s_wired;
     NMSettingIPConfig            *s_ip4;
     NMSettingIPConfig            *s_ip6;
 
@@ -1953,6 +1954,11 @@ test_write_bridge_main(void)
     g_assert(s_bridge);
     nm_connection_add_setting(connection, NM_SETTING(s_bridge));
 
+    /* Ethernet setting */
+    s_wired = (NMSettingWired *) nm_setting_wired_new();
+    g_assert(s_wired);
+    nm_connection_add_setting(connection, NM_SETTING(s_wired));
+
     /* IP4 setting */
     s_ip4 = (NMSettingIPConfig *) nm_setting_ip4_config_new();
     g_assert(s_ip4);
diff --git a/src/core/supplicant/nm-supplicant-interface.c b/src/core/supplicant/nm-supplicant-interface.c
index e7ac0e58..fa8b4e37 100644
--- a/src/core/supplicant/nm-supplicant-interface.c
+++ b/src/core/supplicant/nm-supplicant-interface.c
@@ -3174,7 +3174,7 @@ _signal_handle(NMSupplicantInterface *self,
 
                         _set_p2p_assigned_addr(iface,
                                                addr_data,
-                                               nm_utils_ip4_netmask_to_prefix(netmask));
+                                               _nm_utils_ip4_netmask_to_prefix(netmask));
                     } else {
                         _LOGW("P2P: GroupStarted signaled invalid IP Address information");
                     }
diff --git a/src/core/tests/config/test-config.c b/src/core/tests/config/test-config.c
index fa7fae07..17a66b06 100644
--- a/src/core/tests/config/test-config.c
+++ b/src/core/tests/config/test-config.c
@@ -1103,9 +1103,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 eca098c4..de510c76 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",
diff --git a/src/core/tests/test-core.c b/src/core/tests/test-core.c
index 11a7f323..c260788e 100644
--- a/src/core/tests/test-core.c
+++ b/src/core/tests/test-core.c
@@ -2353,21 +2353,21 @@ test_nm_utils_dhcp_client_id_systemd_node_specific(gconstpointer test_data)
 {
     const int     TEST_IDX     = GPOINTER_TO_INT(test_data);
     const guint8  HASH_KEY[16] = {0x80,
-                                 0x11,
-                                 0x8c,
-                                 0xc2,
-                                 0xfe,
-                                 0x4a,
-                                 0x03,
-                                 0xee,
-                                 0x3e,
-                                 0xd6,
-                                 0x0c,
-                                 0x6f,
-                                 0x36,
-                                 0x39,
-                                 0x14,
-                                 0x09};
+                                  0x11,
+                                  0x8c,
+                                  0xc2,
+                                  0xfe,
+                                  0x4a,
+                                  0x03,
+                                  0xee,
+                                  0x3e,
+                                  0xd6,
+                                  0x0c,
+                                  0x6f,
+                                  0x36,
+                                  0x39,
+                                  0x14,
+                                  0x09};
     const guint16 duid_type_en = htons(2);
     const guint32 systemd_pen  = htonl(43793);
     const struct {
diff --git a/src/core/tests/test-l3cfg.c b/src/core/tests/test-l3cfg.c
index 924d98f1..a9ac4004 100644
--- a/src/core/tests/test-l3cfg.c
+++ b/src/core/tests/test-l3cfg.c
@@ -821,7 +821,7 @@ _test_l3_ipv6ll_find_lladdr(TestL3IPv6LLData *tdata, int ifindex)
 
     g_assert(tdata);
 
-    nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP6_ADDRESS, ifindex);
+    nmp_lookup_init_object_by_ifindex(&lookup, NMP_OBJECT_TYPE_IP6_ADDRESS, ifindex);
     nm_platform_iter_obj_for_each (&iter, tdata->f->platform, &lookup, &obj) {
         const NMPlatformIP6Address *a = NMP_OBJECT_CAST_IP6_ADDRESS(obj);
 
diff --git a/src/core/tests/test-systemd.c b/src/core/tests/test-systemd.c
index 71a6a60d..cf31ec33 100644
--- a/src/core/tests/test-systemd.c
+++ b/src/core/tests/test-systemd.c
@@ -13,27 +13,6 @@
 /*****************************************************************************/
 
 static void
-test_dhcp_create(void)
-{
-    sd_dhcp_client *client4 = NULL;
-    int             r;
-
-    r = sd_dhcp_client_new(&client4, FALSE);
-    g_assert(r == 0);
-    g_assert(client4);
-
-    if (/* never true */ client4 == (gpointer) &r) {
-        /* we don't want to call this, but ensure that the linker
-         * includes all these symbols. */
-        sd_dhcp_client_start(client4);
-    }
-
-    sd_dhcp_client_unref(client4);
-}
-
-/*****************************************************************************/
-
-static void
 test_lldp_create(void)
 {
     sd_lldp_rx *lldp = NULL;
@@ -119,141 +98,6 @@ test_sd_event(void)
 
 /*****************************************************************************/
 
-static void
-test_path_equal(void)
-{
-#define _path_equal_check(path, expected)                \
-    G_STMT_START                                         \
-    {                                                    \
-        const char   *_path0    = (path);                \
-        const char   *_expected = (expected);            \
-        gs_free char *_path     = g_strdup(_path0);      \
-        const char   *_path_result;                      \
-                                                         \
-        _path_result = nm_sd_utils_path_simplify(_path); \
-        g_assert(_path_result == _path);                 \
-        g_assert_cmpstr(_path, ==, _expected);           \
-    }                                                    \
-    G_STMT_END
-
-    _path_equal_check("", "");
-    _path_equal_check(".", ".");
-    _path_equal_check("..", "..");
-    _path_equal_check("/..", "/..");
-    _path_equal_check("//..", "/..");
-    _path_equal_check("/.", "/");
-    _path_equal_check("./", ".");
-    _path_equal_check("./.", ".");
-    _path_equal_check(".///.", ".");
-    _path_equal_check(".///./", ".");
-    _path_equal_check(".////", ".");
-    _path_equal_check("//..//foo/", "/../foo");
-    _path_equal_check("///foo//./bar/.", "/foo/bar");
-    _path_equal_check(".//./foo//./bar/.", "foo/bar");
-}
-
-/*****************************************************************************/
-
-static void
-_test_unbase64char(char ch, gboolean maybe_invalid)
-{
-    int r;
-
-    r = nm_sd_utils_unbase64char(ch, FALSE);
-
-    if (ch == '=') {
-        g_assert(!maybe_invalid);
-        g_assert_cmpint(r, <, 0);
-        g_assert_cmpint(nm_sd_utils_unbase64char(ch, TRUE), ==, G_MAXINT);
-    } else {
-        g_assert_cmpint(r, ==, nm_sd_utils_unbase64char(ch, TRUE));
-        if (r >= 0)
-            g_assert_cmpint(r, <=, 255);
-        if (!maybe_invalid)
-            g_assert_cmpint(r, >=, 0);
-    }
-}
-
-static void
-_test_unbase64mem_mem(const char *base64, const guint8 *expected_arr, gsize expected_len)
-{
-    gs_free char        *expected_base64 = NULL;
-    int                  r;
-    nm_auto_free guint8 *exp2_arr = NULL;
-    nm_auto_free guint8 *exp3_arr = NULL;
-    gsize                exp2_len;
-    gsize                exp3_len;
-    gsize                i;
-
-    expected_base64 = g_base64_encode(expected_arr, expected_len);
-
-    for (i = 0; expected_base64[i]; i++)
-        _test_unbase64char(expected_base64[i], FALSE);
-
-    r = nm_sd_utils_unbase64mem(expected_base64,
-                                strlen(expected_base64),
-                                TRUE,
-                                &exp2_arr,
-                                &exp2_len);
-    g_assert_cmpint(r, ==, 0);
-    g_assert_cmpmem(expected_arr, expected_len, exp2_arr, exp2_len);
-
-    if (!nm_streq(base64, expected_base64)) {
-        r = nm_sd_utils_unbase64mem(base64, strlen(base64), TRUE, &exp3_arr, &exp3_len);
-        g_assert_cmpint(r, ==, 0);
-        g_assert_cmpmem(expected_arr, expected_len, exp3_arr, exp3_len);
-    }
-}
-
-#define _test_unbase64mem(base64, expected_str) \
-    _test_unbase64mem_mem(base64, (const guint8 *) "" expected_str "", NM_STRLEN(expected_str))
-
-static void
-_test_unbase64mem_inval(const char *base64)
-{
-    gs_free guint8 *exp_arr = NULL;
-    gsize           exp_len = 0;
-    int             r;
-
-    r = nm_sd_utils_unbase64mem(base64, strlen(base64), TRUE, &exp_arr, &exp_len);
-    g_assert_cmpint(r, <, 0);
-    g_assert(!exp_arr);
-    g_assert(exp_len == 0);
-}
-
-static void
-test_nm_sd_utils_unbase64mem(void)
-{
-    gs_free char *rnd_base64 = NULL;
-    guint8        rnd_buf[30];
-    guint         i, rnd_len;
-
-    _test_unbase64mem("", "");
-    _test_unbase64mem("  ", "");
-    _test_unbase64mem(" Y Q == ", "a");
-    _test_unbase64mem(" Y   WJjZGV mZ 2g = ", "abcdefgh");
-    _test_unbase64mem_inval(" Y   %WJjZGV mZ 2g = ");
-    _test_unbase64mem_inval(" Y   %WJjZGV mZ 2g = a");
-    _test_unbase64mem("YQ==", "a");
-    _test_unbase64mem_inval("YQ==a");
-
-    rnd_len = nmtst_get_rand_uint32() % sizeof(rnd_buf);
-    for (i = 0; i < rnd_len; i++)
-        rnd_buf[i] = nmtst_get_rand_uint32() % 256;
-    rnd_base64 = g_base64_encode(rnd_buf, rnd_len);
-    _test_unbase64mem_mem(rnd_base64, rnd_buf, rnd_len);
-
-    _test_unbase64char('=', FALSE);
-    for (i = 0; i < 10; i++) {
-        char ch = nmtst_get_rand_uint32() % 256;
-
-        if (ch != '=')
-            _test_unbase64char(ch, TRUE);
-    }
-}
-
-/*****************************************************************************/
-
 NMTST_DEFINE();
 
 int
@@ -261,11 +105,8 @@ main(int argc, char **argv)
 {
     nmtst_init(&argc, &argv, TRUE);
 
-    g_test_add_func("/systemd/dhcp/create", test_dhcp_create);
     g_test_add_func("/systemd/lldp/create", test_lldp_create);
     g_test_add_func("/systemd/sd-event", test_sd_event);
-    g_test_add_func("/systemd/test_path_equal", test_path_equal);
-    g_test_add_func("/systemd/test_nm_sd_utils_unbase64mem", test_nm_sd_utils_unbase64mem);
 
     return g_test_run();
 }
diff --git a/src/core/tests/test-utils.c b/src/core/tests/test-utils.c
index ad9950dd..2bcb6f69 100644
--- a/src/core/tests/test-utils.c
+++ b/src/core/tests/test-utils.c
@@ -234,8 +234,10 @@ test_shorten_hostname(void)
      * system configuration (`getconf HOST_NAME_MAX`). On Linux
      * it's typically 64 characters, but POSIX allows up to
      * 255 characters.
+     *
+     * We use our own define NM_HOST_NAME_MAX, which is always 64.
      */
-    maxhost = g_strnfill(HOST_NAME_MAX, 'a');
+    maxhost = g_strnfill(NM_HOST_NAME_MAX, 'a');
 
     do_test_shorten_hostname("name1", TRUE, NULL);