about summary refs log tree commit diff
path: root/src/core
diff options
context:
space:
mode:
authorSebastien Bacher <seb128@ubuntu.com>2021-08-25 15:23:22 +0200
committerSebastien Bacher <seb128@ubuntu.com>2021-08-25 15:23:22 +0200
commitcfb80376641fa49137b9996130352697e7f8b436 (patch)
tree4ac3eb90a08f27a8bff2372052f8ae78f7e7c3aa /src/core
parent35779c6675728fa6f0fd0a21cefb904408509c23 (diff)
New upstream version 1.32.10
Diffstat (limited to 'src/core')
-rw-r--r--src/core/devices/nm-acd-manager.c11
-rw-r--r--src/core/devices/nm-device-bond.c4
-rw-r--r--src/core/devices/nm-device.c248
-rw-r--r--src/core/devices/nm-device.h1
-rw-r--r--src/core/devices/wwan/nm-device-modem.c19
-rw-r--r--src/core/dhcp/nm-dhcp-nettools.c30
-rw-r--r--src/core/dhcp/nm-dhcp-options.c8
-rw-r--r--src/core/dhcp/nm-dhcp-options.h3
-rw-r--r--src/core/main.c4
-rw-r--r--src/core/nm-config-data.c81
-rw-r--r--src/core/nm-config-data.h4
-rw-r--r--src/core/nm-config.c34
-rw-r--r--src/core/nm-config.h3
-rw-r--r--src/core/nm-core-utils.c106
-rw-r--r--src/core/nm-core-utils.h4
-rw-r--r--src/core/nm-firewall-utils.c26
-rw-r--r--src/core/nm-firewalld-manager.c76
-rw-r--r--src/core/nm-firewalld-manager.h6
-rw-r--r--src/core/nm-ip4-config.c26
-rw-r--r--src/core/nm-ip6-config.c7
-rw-r--r--src/core/nm-manager.c92
-rw-r--r--src/core/nm-policy.c13
-rw-r--r--src/core/platform/nm-fake-platform.c3
-rw-r--r--src/core/platform/tests/test-common.c4
-rw-r--r--src/core/platform/tests/test-route.c104
-rw-r--r--src/core/ppp/meson.build1
-rw-r--r--src/core/settings/nm-settings-connection.c227
-rw-r--r--src/core/settings/nm-settings-connection.h2
-rw-r--r--src/core/settings/nm-settings.c63
-rwxr-xr-x[-rw-r--r--]src/core/settings/plugins/ifcfg-rh/nm-ifdown47
-rwxr-xr-x[-rw-r--r--]src/core/settings/plugins/ifcfg-rh/nm-ifup47
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c31
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c2
-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.c6
-rw-r--r--src/core/settings/plugins/ifcfg-rh/shvar.c136
-rw-r--r--src/core/settings/plugins/ifcfg-rh/shvar.h3
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-wpa-eap-suite-b-192-tls1
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-wpa-eap-ttls-tls1
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-write-unknown-4.expected6
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c53
-rw-r--r--src/core/tests/meson.build2
42 files changed, 1199 insertions, 348 deletions
diff --git a/src/core/devices/nm-acd-manager.c b/src/core/devices/nm-acd-manager.c
index 995abacd..145947bb 100644
--- a/src/core/devices/nm-acd-manager.c
+++ b/src/core/devices/nm-acd-manager.c
@@ -188,6 +188,7 @@ acd_event(int fd, GIOCondition condition, gpointer data)
         char          to_string_buffer[ACD_EVENT_TO_STRING_BUF_SIZE];
         gs_free char *hwaddr_str         = NULL;
         gboolean      check_probing_done = FALSE;
+        char          buf[ETH_ALEN * 3];
 
         switch (event->event) {
         case N_ACD_EVENT_READY:
@@ -202,8 +203,9 @@ acd_event(int fd, GIOCondition condition, gpointer data)
                           nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex),
                           acd_error_to_string(r));
                 } else {
-                    _LOGD("announcing address %s",
-                          _nm_utils_inet4_ntop(info->address, address_str));
+                    _LOGD("announcing address %s (hw-addr %s)",
+                          _nm_utils_inet4_ntop(info->address, address_str),
+                          _nm_utils_hwaddr_ntoa(self->hwaddr, ETH_ALEN, TRUE, buf, sizeof(buf)));
                 }
             }
             check_probing_done = TRUE;
@@ -394,6 +396,7 @@ nm_acd_manager_announce_addresses(NMAcdManager *self)
     int            r;
     int            fd;
     gboolean       success = TRUE;
+    char           buf[ETH_ALEN * 3];
 
     r = acd_init(self);
     if (r) {
@@ -428,7 +431,9 @@ nm_acd_manager_announce_addresses(NMAcdManager *self)
                       acd_error_to_string(r));
                 success = FALSE;
             } else
-                _LOGD("announcing address %s", _nm_utils_inet4_ntop(info->address, sbuf));
+                _LOGD("announcing address %s (hw-addr %s)",
+                      _nm_utils_inet4_ntop(info->address, sbuf),
+                      _nm_utils_hwaddr_ntoa(self->hwaddr, ETH_ALEN, TRUE, buf, sizeof(buf)));
         }
     }
 
diff --git a/src/core/devices/nm-device-bond.c b/src/core/devices/nm-device-bond.c
index 3b3b8fcd..b208e2c0 100644
--- a/src/core/devices/nm-device-bond.c
+++ b/src/core/devices/nm-device-bond.c
@@ -34,7 +34,7 @@
         NM_SETTING_BOND_OPTION_PACKETS_PER_SLAVE, NM_SETTING_BOND_OPTION_PRIMARY_RESELECT, \
         NM_SETTING_BOND_OPTION_RESEND_IGMP, NM_SETTING_BOND_OPTION_TLB_DYNAMIC_LB,         \
         NM_SETTING_BOND_OPTION_USE_CARRIER, NM_SETTING_BOND_OPTION_XMIT_HASH_POLICY,       \
-        NM_SETTING_BOND_OPTION_NUM_GRAT_ARP
+        NM_SETTING_BOND_OPTION_NUM_GRAT_ARP, NM_SETTING_BOND_OPTION_PEER_NOTIF_DELAY
 
 #define OPTIONS_REAPPLY_SUBSET                                                            \
     NM_SETTING_BOND_OPTION_MIIMON, NM_SETTING_BOND_OPTION_UPDELAY,                        \
@@ -46,7 +46,7 @@
         NM_SETTING_BOND_OPTION_MIN_LINKS, NM_SETTING_BOND_OPTION_PACKETS_PER_SLAVE,       \
         NM_SETTING_BOND_OPTION_PRIMARY_RESELECT, NM_SETTING_BOND_OPTION_RESEND_IGMP,      \
         NM_SETTING_BOND_OPTION_USE_CARRIER, NM_SETTING_BOND_OPTION_XMIT_HASH_POLICY,      \
-        NM_SETTING_BOND_OPTION_NUM_GRAT_ARP
+        NM_SETTING_BOND_OPTION_NUM_GRAT_ARP, NM_SETTING_BOND_OPTION_PEER_NOTIF_DELAY
 
 #define OPTIONS_REAPPLY_FULL                                     \
     OPTIONS_REAPPLY_SUBSET, NM_SETTING_BOND_OPTION_ACTIVE_SLAVE, \
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index 1c70ecb3..50386d00 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -499,8 +499,18 @@ typedef struct _NMDevicePrivate {
 
     NMDeviceStageState stage1_sriov_state : 3;
 
+    bool ip_config_started : 1;
+
     char *current_stable_id;
 
+    union {
+        struct {
+            GSource *ip_req_timeout_source_6;
+            GSource *ip_req_timeout_source_4;
+        };
+        GSource *ip_req_timeout_source_x[2];
+    };
+
     /* Proxy Configuration */
     NMProxyConfig *    proxy_config;
     NMPacrunnerConfId *pacrunner_conf_id;
@@ -766,6 +776,7 @@ static void sriov_op_cb(GError *error, gpointer user_data);
 
 static void device_ifindex_changed_cb(NMManager *manager, NMDevice *device_changed, NMDevice *self);
 static gboolean device_link_changed(NMDevice *self);
+static void     check_ip_state(NMDevice *self, gboolean may_fail, gboolean full_state_update);
 
 /*****************************************************************************/
 
@@ -1364,6 +1375,40 @@ out:
     return timeout;
 }
 
+static guint32
+_prop_get_ipvx_required_timeout(NMDevice *self, int addr_family)
+{
+    NMConnection *     connection;
+    NMSettingIPConfig *s_ip;
+    int                timeout;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert_addr_family(addr_family);
+
+    connection = nm_device_get_applied_connection(self);
+    if (!connection)
+        return 0;
+
+    s_ip = nm_connection_get_setting_ip_config(connection, addr_family);
+    if (!s_ip)
+        return 0;
+
+    timeout = nm_setting_ip_config_get_required_timeout(s_ip);
+    nm_assert(timeout >= -1);
+
+    if (timeout > -1)
+        return (guint32) timeout;
+
+    return nm_config_data_get_connection_default_int64(
+        NM_CONFIG_GET_DATA,
+        NM_IS_IPv4(addr_family) ? NM_CON_DEFAULT("ipv4.required-timeout")
+                                : NM_CON_DEFAULT("ipv6.required-timeout"),
+        self,
+        0,
+        G_MAXINT32,
+        0);
+}
+
 /**
  * _prop_get_ipvx_dhcp_iaid:
  * @self: the #NMDevice
@@ -2787,14 +2832,72 @@ _add_capabilities(NMDevice *self, NMDeviceCapabilities capabilities)
 
 /*****************************************************************************/
 
+static gboolean
+ip_required_timeout_x(NMDevice *self, int addr_family)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    _LOGD(LOGD_CORE,
+          "required-timeout expired for IPv%c",
+          nm_utils_addr_family_to_char(addr_family));
+    nm_clear_g_source_inst(&priv->ip_req_timeout_source_x[NM_IS_IPv4(addr_family)]);
+    check_ip_state(self, FALSE, TRUE);
+    return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+ip_required_timeout_4(gpointer data)
+{
+    return ip_required_timeout_x(data, AF_INET);
+}
+
+static gboolean
+ip_required_timeout_6(gpointer data)
+{
+    return ip_required_timeout_x(data, AF_INET6);
+}
+
 static void
 _set_ip_state(NMDevice *self, int addr_family, NMDeviceIPState new_state)
 {
     NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
     const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+    guint            timeout_msec;
+    int              v4;
 
     nm_assert_addr_family(addr_family);
 
+    if (new_state == NM_DEVICE_IP_STATE_CONF && !priv->ip_config_started) {
+        /* Start the required-timeout timers when one of IPv4/IPv6
+         * enters the CONF state. This means that if there is no carrier and
+         * ipv4.method=auto,ipv6.method=manual, the timeout for IPv4 will
+         * start as soon as connection is activated, even if DHCPv4 did not
+         * start yet.
+         */
+        priv->ip_config_started = TRUE;
+
+        for (v4 = 1; v4 >= 0; v4--) {
+            char buf[32];
+
+            nm_assert(!priv->ip_req_timeout_source_x[v4]);
+            if ((timeout_msec = _prop_get_ipvx_required_timeout(self, v4 ? AF_INET : AF_INET6))) {
+                _LOGD(LOGD_CORE,
+                      "required-timeout in %s msec for IPv%c",
+                      timeout_msec == G_MAXINT32 ? "∞" : nm_sprintf_buf(buf, "%u", timeout_msec),
+                      v4 ? '4' : '6');
+
+                if (timeout_msec == G_MAXINT32) {
+                    priv->ip_req_timeout_source_x[v4] = g_source_ref(nm_g_source_sentinel_get(0));
+                } else {
+                    priv->ip_req_timeout_source_x[v4] =
+                        nm_g_timeout_add_source(timeout_msec,
+                                                v4 ? ip_required_timeout_4 : ip_required_timeout_6,
+                                                self);
+                }
+            }
+        }
+    }
+
     if (priv->ip_state_x[IS_IPv4] == new_state)
         return;
 
@@ -4671,10 +4774,6 @@ nm_device_master_enslave_slave(NMDevice *self, NMDevice *slave, NMConnection *co
      */
     nm_device_update_hw_address(self);
 
-    /* Send ARP announcements if did not yet and have addresses. */
-    if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE && !priv->acd.announcing)
-        nm_device_arp_announce(self);
-
     /* Restart IP configuration if we're waiting for slaves.  Do this
      * after updating the hardware address as IP config may need the
      * new address.
@@ -5003,6 +5102,10 @@ nm_device_set_carrier(NMDevice *self, gboolean carrier)
             nm_device_remove_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE);
             _carrier_wait_check_queued_act_request(self);
         }
+
+        /* Send ARP announcements if did not yet and have carrier. */
+        if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE && !priv->acd.announcing)
+            nm_device_arp_announce(self);
     } else {
         if (priv->carrier_wait_id)
             nm_device_add_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE);
@@ -6576,6 +6679,7 @@ check_ip_state(NMDevice *self, gboolean may_fail, gboolean full_state_update)
     gboolean           ip4_disabled = FALSE, ip6_disabled = FALSE;
     NMSettingIPConfig *s_ip4, *s_ip6;
     NMDeviceState      state;
+    int                IS_IPv4;
 
     if (full_state_update && nm_device_get_state(self) != NM_DEVICE_STATE_IP_CONFIG)
         return;
@@ -6605,6 +6709,13 @@ check_ip_state(NMDevice *self, gboolean may_fail, gboolean full_state_update)
         return;
     }
 
+    for (IS_IPv4 = 1; IS_IPv4 >= 0; IS_IPv4--) {
+        if (priv->ip_state_x[IS_IPv4] == NM_DEVICE_IP_STATE_CONF
+            && priv->ip_req_timeout_source_x[IS_IPv4]) {
+            return;
+        }
+    }
+
     if ((priv->ip_state_4 == NM_DEVICE_IP_STATE_FAIL
          || (ip4_disabled && priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE))
         && (priv->ip_state_6 == NM_DEVICE_IP_STATE_FAIL
@@ -7128,6 +7239,16 @@ _get_maybe_ipv6_disabled(NMDevice *self)
     return (nm_platform_sysctl_get_int32(platform, NMP_SYSCTL_PATHID_ABSOLUTE(path), 0) == 0);
 }
 
+/*
+ * nm_device_generate_connection:
+ *
+ * Generates a connection from an existing interface.
+ *
+ * If the device doesn't have an IP configuration and it's not a port or a
+ * controller, then no connection gets generated and the function returns
+ * %NULL. In such case, @maybe_later is set to %TRUE if a connection can be
+ * generated later when an IP address is assigned to the interface.
+ */
 NMConnection *
 nm_device_generate_connection(NMDevice *self,
                               NMDevice *master,
@@ -7384,6 +7505,8 @@ check_connection_compatible(NMDevice *self, NMConnection *connection, GError **e
     gs_free char *        conn_iface = NULL;
     NMDeviceClass *       klass;
     NMSettingMatch *      s_match;
+    const GSList *        specs;
+    gboolean              has_match = FALSE;
 
     klass = NM_DEVICE_GET_CLASS(self);
     if (klass->connection_type_check_compatible) {
@@ -7460,6 +7583,15 @@ check_connection_compatible(NMDevice *self, NMConnection *connection, GError **e
         }
     }
 
+    specs =
+        nm_config_data_get_device_allowed_connections_specs(NM_CONFIG_GET_DATA, self, &has_match);
+    if (has_match && !nm_utils_connection_match_spec_list(connection, specs, FALSE)) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_DISALLOWED,
+                                   "device configuration doesn't allow this connection");
+        return FALSE;
+    }
+
     return TRUE;
 }
 
@@ -7526,7 +7658,7 @@ nm_device_check_slave_connection_compatible(NMDevice *self, NMConnection *slave)
  *
  * Returns: %TRUE if the device is capable of assuming connections, %FALSE if not
  */
-static gboolean
+gboolean
 nm_device_can_assume_connections(NMDevice *self)
 {
     return !!NM_DEVICE_GET_CLASS(self)->update_connection;
@@ -9857,6 +9989,7 @@ dhcp6_start_with_link_ready(NMDevice *self, NMConnection *connection)
     gboolean                    iaid_explicit;
     NMSettingConnection *       s_con;
     const NMPlatformIP6Address *ll_addr = NULL;
+    int                         ip_ifindex;
 
     g_return_val_if_fail(connection, FALSE);
 
@@ -9876,7 +10009,13 @@ dhcp6_start_with_link_ready(NMDevice *self, NMConnection *connection)
         return FALSE;
     }
 
-    pllink = nm_platform_link_get(nm_device_get_platform(self), nm_device_get_ip_ifindex(self));
+    ip_ifindex = nm_device_get_ip_ifindex(self);
+    if (ip_ifindex <= 0) {
+        _LOGD(LOGD_DHCP6, "can't start DHCPv6: interface is gone");
+        return FALSE;
+    }
+
+    pllink = nm_platform_link_get(nm_device_get_platform(self), ip_ifindex);
     if (pllink)
         hwaddr = nmp_link_address_get_as_bytes(&pllink->l_address);
 
@@ -9887,7 +10026,7 @@ dhcp6_start_with_link_ready(NMDevice *self, NMConnection *connection)
         nm_dhcp_manager_get(),
         nm_device_get_multi_index(self),
         nm_device_get_ip_iface(self),
-        nm_device_get_ip_ifindex(self),
+        ip_ifindex,
         &ll_addr->address,
         nm_connection_get_uuid(connection),
         nm_device_get_route_table(self, AF_INET6),
@@ -9997,7 +10136,9 @@ nm_device_request_ip6_prefixes(NMDevice *self, int needed_prefixes)
         _LOGD(LOGD_IP6, "ipv6-pd: asking DHCPv6 for %d prefixes", needed_prefixes);
         nm_device_dhcp6_renew(self, FALSE);
     } else {
-        _LOGI(LOGD_IP6, "ipv6-pd: device doesn't use DHCPv6, can't request prefixes");
+        priv->dhcp6.mode = NM_NDISC_DHCP_LEVEL_OTHERCONF;
+        _LOGD(LOGD_DEVICE | LOGD_DHCP6, "ipv6-pd: starting DHCPv6 to request a prefix");
+        dhcp6_start(self, FALSE);
     }
 }
 
@@ -10541,10 +10682,30 @@ _commit_mtu(NMDevice *self, const NMIP4Config *config)
         }
     }
 
+#define _IP6_MTU_SYS()                                                                         \
+    ({                                                                                         \
+        if (!ip6_mtu_sysctl.initialized) {                                                     \
+            ip6_mtu_sysctl.value       = nm_device_sysctl_ip_conf_get_int_checked(self,        \
+                                                                            AF_INET6,    \
+                                                                            "mtu",       \
+                                                                            10,          \
+                                                                            0,           \
+                                                                            G_MAXUINT32, \
+                                                                            0);          \
+            ip6_mtu_sysctl.initialized = TRUE;                                                 \
+        }                                                                                      \
+        ip6_mtu_sysctl.value;                                                                  \
+    })
+
     if (mtu_desired && NM_DEVICE_GET_CLASS(self)->mtu_force_set && !priv->mtu_force_set_done) {
         priv->mtu_force_set_done = TRUE;
-
         if (mtu_desired == mtu_plat) {
+            if (!priv->mtu_initial && !priv->ip6_mtu_initial) {
+                /* before touching any of the MTU parameters, record the
+                 * original setting to restore on deactivation. */
+                priv->mtu_initial     = mtu_plat;
+                priv->ip6_mtu_initial = _IP6_MTU_SYS();
+            }
             mtu_plat--;
             if (NM_DEVICE_GET_CLASS(self)->set_platform_mtu(self, mtu_desired - 1)) {
                 _LOGD(LOGD_DEVICE, "mtu: force-set MTU to %u", mtu_desired - 1);
@@ -10563,20 +10724,6 @@ _commit_mtu(NMDevice *self, const NMIP4Config *config)
           ip6_mtu == ip6_mtu_orig ? "" : nm_sprintf_buf(sbuf2, " (was %u)", (guint) ip6_mtu_orig),
           ifindex);
 
-#define _IP6_MTU_SYS()                                                                         \
-    ({                                                                                         \
-        if (!ip6_mtu_sysctl.initialized) {                                                     \
-            ip6_mtu_sysctl.value       = nm_device_sysctl_ip_conf_get_int_checked(self,        \
-                                                                            AF_INET6,    \
-                                                                            "mtu",       \
-                                                                            10,          \
-                                                                            0,           \
-                                                                            G_MAXUINT32, \
-                                                                            0);          \
-            ip6_mtu_sysctl.initialized = TRUE;                                                 \
-        }                                                                                      \
-        ip6_mtu_sysctl.value;                                                                  \
-    })
     if ((mtu_desired && mtu_desired != mtu_plat) || (ip6_mtu && ip6_mtu != _IP6_MTU_SYS())) {
         gboolean anticipated_failure = FALSE;
 
@@ -11805,11 +11952,12 @@ activate_stage5_ip_config_result_x(NMDevice *self, int addr_family)
     const char *     method;
     int              ip_ifindex;
     int              errsv;
-    gboolean         do_announce = FALSE;
 
     req = nm_device_get_act_request(self);
     g_assert(req);
 
+    nm_clear_g_source_inst(&priv->ip_req_timeout_source_x[IS_IPv4]);
+
     /* Interface must be IFF_UP before IP config can be applied */
     ip_ifindex = nm_device_get_ip_ifindex(self);
     g_return_if_fail(ip_ifindex);
@@ -11931,31 +12079,13 @@ activate_stage5_ip_config_result_x(NMDevice *self, int addr_family)
         }
     }
 
-    if (IS_IPv4) {
-        /* Send ARP announcements */
-
-        if (nm_device_is_master(self)) {
-            CList *    iter;
-            SlaveInfo *info;
-
-            /* Skip announcement if there are no device enslaved, for two reasons:
-             * 1) the master has a temporary MAC address until the first slave comes
-             * 2) announcements are going to be dropped anyway without slaves
-             */
-            do_announce = FALSE;
-
-            c_list_for_each (iter, &priv->slaves) {
-                info = c_list_entry(iter, SlaveInfo, lst_slave);
-                if (info->slave_is_enslaved) {
-                    do_announce = TRUE;
-                    break;
-                }
-            }
-        } else
-            do_announce = TRUE;
-
-        if (do_announce)
-            nm_device_arp_announce(self);
+    if (IS_IPv4 && priv->carrier) {
+        /* We send ARP announcements only when the link gets carrier,
+         * otherwise the announcements would be lost. Furthermore, for
+         * controllers having carrier implies that there is at least one
+         * port and therefore the MAC address is the correct one.
+         */
+        nm_device_arp_announce(self);
     }
 
     if (IS_IPv4) {
@@ -13697,18 +13827,24 @@ void
 nm_device_replace_vpn6_config(NMDevice *self, NMIP6Config *old, NMIP6Config *config)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMDeviceState    state;
 
     nm_assert(!old || NM_IS_IP6_CONFIG(old));
+    nm_assert(!old || nm_ip6_config_get_ifindex(old) > 0);
+    nm_assert(!old || nm_device_get_ip_ifindex(self) == 0
+              || nm_device_get_ip_ifindex(self) == nm_ip6_config_get_ifindex(old));
     nm_assert(!config || NM_IS_IP6_CONFIG(config));
-    nm_assert(!old || nm_ip6_config_get_ifindex(old) == nm_device_get_ip_ifindex(self));
-    nm_assert(!config || nm_ip6_config_get_ifindex(config) == nm_device_get_ip_ifindex(self));
+    nm_assert(!config || nm_ip6_config_get_ifindex(config) > 0);
+    nm_assert(!config || nm_device_get_ip_ifindex(self) == nm_ip6_config_get_ifindex(config));
 
     if (!_replace_vpn_config_in_list(&priv->vpn_configs_6, (GObject *) old, (GObject *) config))
         return;
 
-    /* NULL to use existing configs */
-    if (!ip_config_merge_and_apply(self, AF_INET6, TRUE))
-        _LOGW(LOGD_IP6, "failed to set VPN routes for device");
+    state = nm_device_get_state(self);
+    if (state >= NM_DEVICE_STATE_IP_CONFIG && state <= NM_DEVICE_STATE_ACTIVATED) {
+        if (!ip_config_merge_and_apply(self, AF_INET6, TRUE))
+            _LOGW(LOGD_IP6, "failed to set VPN routes for device");
+    }
 }
 
 NMIP6Config *
@@ -15777,6 +15913,10 @@ _cleanup_generic_pre(NMDevice *self, CleanupType cleanup_type)
 
     _cleanup_ip_pre(self, AF_INET, cleanup_type);
     _cleanup_ip_pre(self, AF_INET6, cleanup_type);
+
+    priv->ip_config_started = FALSE;
+    nm_clear_g_source_inst(&priv->ip_req_timeout_source_4);
+    nm_clear_g_source_inst(&priv->ip_req_timeout_source_6);
 }
 
 static void
diff --git a/src/core/devices/nm-device.h b/src/core/devices/nm-device.h
index 53211c6f..f59b6fa8 100644
--- a/src/core/devices/nm-device.h
+++ b/src/core/devices/nm-device.h
@@ -527,6 +527,7 @@ nm_device_check_connection_compatible(NMDevice *device, NMConnection *connection
 
 gboolean nm_device_check_slave_connection_compatible(NMDevice *device, NMConnection *connection);
 
+gboolean nm_device_can_assume_connections(NMDevice *self);
 gboolean nm_device_unmanage_on_quit(NMDevice *self);
 
 gboolean nm_device_spec_match_list(NMDevice *device, const GSList *specs);
diff --git a/src/core/devices/wwan/nm-device-modem.c b/src/core/devices/wwan/nm-device-modem.c
index a8d2a161..4d892e6f 100644
--- a/src/core/devices/wwan/nm-device-modem.c
+++ b/src/core/devices/wwan/nm-device-modem.c
@@ -193,15 +193,20 @@ modem_ip4_config_result(NMModem *modem, NMIP4Config *config, GError *error, gpoi
     NMDeviceModem *self   = NM_DEVICE_MODEM(user_data);
     NMDevice *     device = NM_DEVICE(self);
 
-    g_return_if_fail(nm_device_activate_ip4_state_in_conf(device) == TRUE);
+    if (!nm_device_activate_ip4_state_in_conf(device)) {
+        _LOGD(LOGD_MB | LOGD_IP4,
+              "retrieving IPv4 configuration while no longer in state IPv4 conf");
+        return;
+    }
 
     if (error) {
         _LOGW(LOGD_MB | LOGD_IP4, "retrieving IPv4 configuration failed: %s", error->message);
         nm_device_ip_method_failed(device, AF_INET, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
-    } else {
-        nm_device_set_dev2_ip_config(device, AF_INET, NM_IP_CONFIG_CAST(config));
-        nm_device_activate_schedule_ip_config_result(device, AF_INET, NULL);
+        return;
     }
+
+    nm_device_set_dev2_ip_config(device, AF_INET, NM_IP_CONFIG_CAST(config));
+    nm_device_activate_schedule_ip_config_result(device, AF_INET, NULL);
 }
 
 static void
@@ -218,7 +223,11 @@ modem_ip6_config_result(NMModem *    modem,
     gs_unref_object NMIP6Config *ignored    = NULL;
     gboolean                     got_config = !!config;
 
-    g_return_if_fail(nm_device_activate_ip6_state_in_conf(device) == TRUE);
+    if (!nm_device_activate_ip6_state_in_conf(device)) {
+        _LOGD(LOGD_MB | LOGD_IP6,
+              "retrieving IPv6 configuration while no longer in state IPv6 conf");
+        return;
+    }
 
     if (error) {
         _LOGW(LOGD_MB | LOGD_IP6, "retrieving IPv6 configuration failed: %s", error->message);
diff --git a/src/core/dhcp/nm-dhcp-nettools.c b/src/core/dhcp/nm-dhcp-nettools.c
index fe71f587..4bb6c833 100644
--- a/src/core/dhcp/nm-dhcp-nettools.c
+++ b/src/core/dhcp/nm-dhcp-nettools.c
@@ -694,12 +694,40 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
 
         /* https://tools.ietf.org/html/rfc2132#section-8.1 */
 
-        v_str = nm_utils_buf_utf8safe_escape((char *) l_data, l_data_len, 0, &to_free);
+        v_str = nm_utils_buf_utf8safe_escape((char *) l_data,
+                                             l_data_len,
+                                             NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL,
+                                             &to_free);
 
         nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NIS_DOMAIN, v_str ?: "");
         nm_ip4_config_set_nis_domain(ip4_config, v_str ?: "");
     }
 
+    r = n_dhcp4_client_lease_get_file(lease, &v_str);
+    if (r == 0) {
+        gs_free char *to_free = NULL;
+
+        v_str = nm_utils_buf_utf8safe_escape(v_str,
+                                             -1,
+                                             NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL,
+                                             &to_free);
+        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NM_FILENAME, v_str ?: "");
+    }
+
+    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)) {
+        gs_free char *to_free = NULL;
+
+        v_str = nm_utils_buf_utf8safe_escape((char *) l_data,
+                                             l_data_len,
+                                             NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL,
+                                             &to_free);
+        nm_dhcp_option_add_option(options,
+                                  AF_INET,
+                                  NM_DHCP_OPTION_DHCP4_BOOTFILE_NAME,
+                                  v_str ?: "");
+    }
+
     lease_parse_address_list(lease, ip4_config, NM_DHCP_OPTION_DHCP4_NIS_SERVERS, options, &sbuf);
 
     lease_parse_address_list(lease,
diff --git a/src/core/dhcp/nm-dhcp-options.c b/src/core/dhcp/nm-dhcp-options.c
index 8f0d7408..011f23e4 100644
--- a/src/core/dhcp/nm-dhcp-options.c
+++ b/src/core/dhcp/nm-dhcp-options.c
@@ -169,6 +169,7 @@ const NMDhcpOption _nm_dhcp_option_dhcp4_options[] = {
     REQ(NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS, "ip_address", FALSE),
     REQ(NM_DHCP_OPTION_DHCP4_NM_EXPIRY, "expiry", FALSE),
     REQ(NM_DHCP_OPTION_DHCP4_NM_NEXT_SERVER, "next_server", FALSE),
+    REQ(NM_DHCP_OPTION_DHCP4_NM_FILENAME, "filename", FALSE),
 };
 
 static const NMDhcpOption *const _sorted_options_4[G_N_ELEMENTS(_nm_dhcp_option_dhcp4_options)] = {
@@ -184,7 +185,7 @@ static const NMDhcpOption *const _sorted_options_4[G_N_ELEMENTS(_nm_dhcp_option_
     A(98),  A(99),  A(100), A(101), A(102), A(103), A(104), A(105), A(106), A(107), A(108), A(109),
     A(110), A(111), A(112), A(113), A(114), A(115), A(116), A(117), A(118), A(119), A(120), A(121),
     A(122), A(123), A(124), A(125), A(126), A(127), A(128), A(129), A(130), A(131), A(132), A(133),
-    A(134), A(15),  A(135), A(136), A(16),  A(137), A(138), A(139), A(140), A(141),
+    A(134), A(15),  A(135), A(136), A(16),  A(137), A(138), A(139), A(140), A(141), A(142),
 #undef A
 };
 
@@ -404,7 +405,10 @@ nm_dhcp_option_add_option_utf8safe_escape(GHashTable *  options,
     gs_free char *to_free = NULL;
     const char *  escaped;
 
-    escaped = nm_utils_buf_utf8safe_escape((char *) data, n_data, 0, &to_free);
+    escaped = nm_utils_buf_utf8safe_escape((char *) data,
+                                           n_data,
+                                           NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL,
+                                           &to_free);
     nm_dhcp_option_add_option(options, addr_family, option, escaped ?: "");
 }
 
diff --git a/src/core/dhcp/nm-dhcp-options.h b/src/core/dhcp/nm-dhcp-options.h
index 585f1187..8ed2039d 100644
--- a/src/core/dhcp/nm-dhcp-options.h
+++ b/src/core/dhcp/nm-dhcp-options.h
@@ -153,6 +153,7 @@ typedef enum {
     NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS  = 1024,
     NM_DHCP_OPTION_DHCP4_NM_EXPIRY      = 1025,
     NM_DHCP_OPTION_DHCP4_NM_NEXT_SERVER = 1026,
+    NM_DHCP_OPTION_DHCP4_NM_FILENAME    = 1027, /* 'file' DHCP header */
 } NMDhcpOptionDhcp4Options;
 
 typedef enum {
@@ -185,7 +186,7 @@ typedef struct {
     bool        include;
 } NMDhcpOption;
 
-extern const NMDhcpOption _nm_dhcp_option_dhcp4_options[142];
+extern const NMDhcpOption _nm_dhcp_option_dhcp4_options[143];
 extern const NMDhcpOption _nm_dhcp_option_dhcp6_options[16];
 
 static inline const char *
diff --git a/src/core/main.c b/src/core/main.c
index cfcdb860..3cb5c07f 100644
--- a/src/core/main.c
+++ b/src/core/main.c
@@ -304,14 +304,14 @@ main(int argc, char *argv[])
     const char *const *     warnings;
     int                     errsv;
 
+    _nm_utils_is_manager_process = TRUE;
+
     /* Known to cause a possible deadlock upon GDBus initialization:
      * https://bugzilla.gnome.org/show_bug.cgi?id=674885 */
     g_type_ensure(G_TYPE_SOCKET);
     g_type_ensure(G_TYPE_DBUS_CONNECTION);
     g_type_ensure(NM_TYPE_DBUS_MANAGER);
 
-    _nm_utils_is_manager_process = TRUE;
-
     main_loop = g_main_loop_new(NULL, FALSE);
 
     /* we determine a first-start (contrary to a restart during the same boot)
diff --git a/src/core/nm-config-data.c b/src/core/nm-config-data.c
index e127ea23..0cbff027 100644
--- a/src/core/nm-config-data.c
+++ b/src/core/nm-config-data.c
@@ -26,6 +26,15 @@ typedef struct {
         gboolean has;
         GSList * spec;
     } match_device;
+    union {
+        struct {
+            GSList * allowed_connections;
+            gboolean allowed_connections_has;
+        } device;
+    };
+    gboolean is_device;
+
+    /* List of key/value pairs in the section, sorted by key */
     gsize                    lookup_len;
     const NMUtilsNamedValue *lookup_idx;
 } MatchSectionInfo;
@@ -1436,13 +1445,13 @@ _match_section_infos_lookup(const MatchSectionInfo *match_section_infos,
             match = TRUE;
 
         if (match) {
-            *out_value = value;
+            NM_SET_OUT(out_value, value);
             return match_section_infos;
         }
     }
 
 out:
-    *out_value = NULL;
+    NM_SET_OUT(out_value, NULL);
     return NULL;
 }
 
@@ -1538,6 +1547,37 @@ nm_config_data_get_device_config_int64(const NMConfigData *self,
     return _nm_utils_ascii_str_to_int64(value, base, min, max, val_invalid);
 }
 
+const GSList *
+nm_config_data_get_device_allowed_connections_specs(const NMConfigData *self,
+                                                    NMDevice *          device,
+                                                    gboolean *          has_match)
+{
+    const NMConfigDataPrivate *priv;
+    const MatchSectionInfo *   connection_info;
+    const GSList *             ret = NULL;
+
+    g_return_val_if_fail(self, NULL);
+
+    priv = NM_CONFIG_DATA_GET_PRIVATE(self);
+
+    connection_info = _match_section_infos_lookup(&priv->device_infos[0],
+                                                  priv->keyfile,
+                                                  NM_CONFIG_KEYFILE_KEY_DEVICE_ALLOWED_CONNECTIONS,
+                                                  device,
+                                                  NULL,
+                                                  NULL,
+                                                  NULL);
+
+    if (connection_info) {
+        nm_assert(connection_info->device.allowed_connections_has);
+        ret = connection_info->device.allowed_connections;
+        NM_SET_OUT(has_match, TRUE);
+    } else
+        NM_SET_OUT(has_match, FALSE);
+
+    return ret;
+}
+
 const char *
 nm_config_data_get_connection_default(const NMConfigData *self,
                                       const char *        property,
@@ -1610,7 +1650,10 @@ _match_section_info_get_str(const MatchSectionInfo *m, GKeyFile *keyfile, const
 }
 
 static void
-_match_section_info_init(MatchSectionInfo *connection_info, GKeyFile *keyfile, char *group)
+_match_section_info_init(MatchSectionInfo *connection_info,
+                         GKeyFile *        keyfile,
+                         char *            group,
+                         gboolean          is_device)
 {
     char **            keys = NULL;
     gsize              n_keys;
@@ -1629,6 +1672,14 @@ _match_section_info_init(MatchSectionInfo *connection_info, GKeyFile *keyfile, c
     connection_info->stop_match =
         nm_config_keyfile_get_boolean(keyfile, group, NM_CONFIG_KEYFILE_KEY_STOP_MATCH, FALSE);
 
+    if (is_device) {
+        connection_info->device.allowed_connections =
+            nm_config_get_match_spec(keyfile,
+                                     group,
+                                     NM_CONFIG_KEYFILE_KEY_DEVICE_ALLOWED_CONNECTIONS,
+                                     &connection_info->device.allowed_connections_has);
+    }
+
     keys = g_key_file_get_keys(keyfile, group, &n_keys, NULL);
     nm_utils_strv_sort(keys, n_keys);
 
@@ -1680,9 +1731,13 @@ _match_section_infos_free(MatchSectionInfo *match_section_infos)
 
     if (!match_section_infos)
         return;
+
     for (m = match_section_infos; m->group_name; m++) {
         g_free(m->group_name);
         g_slist_free_full(m->match_device.spec, g_free);
+        if (m->is_device) {
+            g_slist_free_full(m->device.allowed_connections, g_free);
+        }
         for (i = 0; i < m->lookup_len; i++) {
             g_free(m->lookup_idx[i].name_mutable);
             g_free(m->lookup_idx[i].value_str_mutable);
@@ -1693,12 +1748,16 @@ _match_section_infos_free(MatchSectionInfo *match_section_infos)
 }
 
 static MatchSectionInfo *
-_match_section_infos_construct(GKeyFile *keyfile, const char *prefix)
+_match_section_infos_construct(GKeyFile *keyfile, gboolean is_device)
 {
     char **           groups;
     gsize             i, j, ngroups;
     char *            connection_tag      = NULL;
     MatchSectionInfo *match_section_infos = NULL;
+    const char *      prefix;
+
+    prefix =
+        is_device ? NM_CONFIG_KEYFILE_GROUPPREFIX_DEVICE : NM_CONFIG_KEYFILE_GROUPPREFIX_CONNECTION;
 
     /* get the list of existing [connection.\+]/[device.\+] sections.
      *
@@ -1730,13 +1789,17 @@ _match_section_infos_construct(GKeyFile *keyfile, const char *prefix)
     }
 
     match_section_infos = g_new0(MatchSectionInfo, ngroups + 1 + (connection_tag ? 1 : 0));
+    match_section_infos->is_device = is_device;
     for (i = 0; i < ngroups; i++) {
         /* pass ownership of @group on... */
-        _match_section_info_init(&match_section_infos[i], keyfile, groups[ngroups - i - 1]);
+        _match_section_info_init(&match_section_infos[i],
+                                 keyfile,
+                                 groups[ngroups - i - 1],
+                                 is_device);
     }
     if (connection_tag) {
         /* pass ownership of @connection_tag on... */
-        _match_section_info_init(&match_section_infos[i], keyfile, connection_tag);
+        _match_section_info_init(&match_section_infos[i], keyfile, connection_tag, is_device);
     }
     g_free(groups);
 
@@ -1950,10 +2013,8 @@ constructed(GObject *object)
 
     priv->keyfile = _merge_keyfiles(priv->keyfile_user, priv->keyfile_intern);
 
-    priv->connection_infos =
-        _match_section_infos_construct(priv->keyfile, NM_CONFIG_KEYFILE_GROUPPREFIX_CONNECTION);
-    priv->device_infos =
-        _match_section_infos_construct(priv->keyfile, NM_CONFIG_KEYFILE_GROUPPREFIX_DEVICE);
+    priv->connection_infos = _match_section_infos_construct(priv->keyfile, FALSE);
+    priv->device_infos     = _match_section_infos_construct(priv->keyfile, TRUE);
 
     priv->connectivity.enabled =
         nm_config_keyfile_get_boolean(priv->keyfile,
diff --git a/src/core/nm-config-data.h b/src/core/nm-config-data.h
index 42d69a8b..fa58d869 100644
--- a/src/core/nm-config-data.h
+++ b/src/core/nm-config-data.h
@@ -243,6 +243,10 @@ gint64   nm_config_data_get_device_config_int64(const NMConfigData *self,
                                                 gint64              val_no_match,
                                                 gint64              val_invalid);
 
+const GSList *nm_config_data_get_device_allowed_connections_specs(const NMConfigData *self,
+                                                                  NMDevice *          device,
+                                                                  gboolean *          has_match);
+
 char **  nm_config_data_get_groups(const NMConfigData *self);
 char **  nm_config_data_get_keys(const NMConfigData *self, const char *group);
 gboolean nm_config_data_is_intern_atomic_group(const NMConfigData *self, const char *group);
diff --git a/src/core/nm-config.c b/src/core/nm-config.c
index fdc450a0..817368d1 100644
--- a/src/core/nm-config.c
+++ b/src/core/nm-config.c
@@ -796,6 +796,7 @@ static gboolean
 _setting_is_device_spec(const char *group, const char *key)
 {
 #define _IS(group_v, key_v) (nm_streq(group, "" group_v "") && nm_streq(key, "" key_v ""))
+
     return _IS(NM_CONFIG_KEYFILE_GROUP_MAIN, NM_CONFIG_KEYFILE_KEY_MAIN_NO_AUTO_DEFAULT)
            || _IS(NM_CONFIG_KEYFILE_GROUP_MAIN, NM_CONFIG_KEYFILE_KEY_MAIN_IGNORE_CARRIER)
            || _IS(NM_CONFIG_KEYFILE_GROUP_MAIN, NM_CONFIG_KEYFILE_KEY_MAIN_ASSUME_IPV6LL_ONLY)
@@ -807,6 +808,13 @@ _setting_is_device_spec(const char *group, const char *key)
 }
 
 static gboolean
+_setting_is_connection_spec(const char *group, const char *key)
+{
+    return NM_STR_HAS_PREFIX(group, NM_CONFIG_KEYFILE_GROUPPREFIX_DEVICE)
+           && nm_streq(key, NM_CONFIG_KEYFILE_KEY_DEVICE_ALLOWED_CONNECTIONS);
+}
+
+static gboolean
 _setting_is_string_list(const char *group, const char *key)
 {
     return _IS(NM_CONFIG_KEYFILE_GROUP_MAIN, NM_CONFIG_KEYFILE_KEY_MAIN_PLUGINS)
@@ -878,6 +886,8 @@ static const ConfigGroup config_groups[] = {
                              NM_CONFIG_KEYFILE_KEY_DEVICE_IGNORE_CARRIER,
                              NM_CONFIG_KEYFILE_KEY_DEVICE_MANAGED,
                              NM_CONFIG_KEYFILE_KEY_DEVICE_SRIOV_NUM_VFS,
+                             NM_CONFIG_KEYFILE_KEY_DEVICE_KEEP_CONFIGURATION,
+                             NM_CONFIG_KEYFILE_KEY_DEVICE_ALLOWED_CONNECTIONS,
                              NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_BACKEND,
                              NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_SCAN_RAND_MAC_ADDRESS,
                              NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_SCAN_GENERATE_MAC_ADDRESS_MASK,
@@ -1059,7 +1069,8 @@ read_config(GKeyFile *  keyfile,
 
                 is_string_list = _setting_is_string_list(group, base_key);
 
-                if (is_string_list || _setting_is_device_spec(group, base_key)) {
+                if (is_string_list || _setting_is_device_spec(group, base_key)
+                    || _setting_is_connection_spec(group, base_key)) {
                     gs_unref_ptrarray  GPtrArray *new = g_ptr_array_new_with_free_func(g_free);
                     char **            iter_val;
                     gs_strfreev char **old_val = NULL;
@@ -2343,8 +2354,9 @@ _nm_config_state_set(NMConfig *self, gboolean allow_persist, gboolean force_pers
     "route-metric-default-aspired"
 #define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_ROUTE_METRIC_DEFAULT_EFFECTIVE \
     "route-metric-default-effective"
-#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_ROOT_PATH   "root-path"
-#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_NEXT_SERVER "next-server"
+#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_ROOT_PATH     "root-path"
+#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_NEXT_SERVER   "next-server"
+#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_DHCP_BOOTFILE "dhcp-bootfile"
 
 static NM_UTILS_LOOKUP_STR_DEFINE(
     _device_state_managed_type_to_str,
@@ -2565,7 +2577,8 @@ nm_config_device_state_write(int                            ifindex,
                              guint32                        route_metric_default_aspired,
                              guint32                        route_metric_default_effective,
                              const char *                   next_server,
-                             const char *                   root_path)
+                             const char *                   root_path,
+                             const char *                   dhcp_bootfile)
 {
     char    path[NM_STRLEN(NM_CONFIG_DEVICE_STATE_DIR "/") + DEVICE_STATE_FILENAME_LEN_MAX + 1];
     GError *local                      = NULL;
@@ -2632,6 +2645,12 @@ nm_config_device_state_write(int                            ifindex,
                               DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_ROOT_PATH,
                               root_path);
     }
+    if (dhcp_bootfile) {
+        g_key_file_set_string(kf,
+                              DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
+                              DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_DHCP_BOOTFILE,
+                              dhcp_bootfile);
+    }
 
     if (!g_key_file_save_to_file(kf, path, &local)) {
         _LOGW("device-state: write #%d (%s) failed: %s", ifindex, path, local->message);
@@ -2639,7 +2658,9 @@ nm_config_device_state_write(int                            ifindex,
         return FALSE;
     }
     _LOGT("device-state: write #%d (%s); managed=%s%s%s%s%s%s%s, "
-          "route-metric-default=%" G_GUINT32_FORMAT "-%" G_GUINT32_FORMAT "%s%s%s%s%s%s",
+          "route-metric-default=%" G_GUINT32_FORMAT "-%" G_GUINT32_FORMAT "%s%s%s"
+          "%s%s%s"
+          "%s%s%s",
           ifindex,
           path,
           _device_state_managed_type_to_str(managed),
@@ -2648,7 +2669,8 @@ nm_config_device_state_write(int                            ifindex,
           route_metric_default_aspired,
           route_metric_default_effective,
           NM_PRINT_FMT_QUOTED(next_server, ", next-server=", next_server, "", ""),
-          NM_PRINT_FMT_QUOTED(root_path, ", root-path=", root_path, "", ""));
+          NM_PRINT_FMT_QUOTED(root_path, ", root-path=", root_path, "", ""),
+          NM_PRINT_FMT_QUOTED(dhcp_bootfile, ", dhcp-bootfile=", dhcp_bootfile, "", ""));
     return TRUE;
 }
 
diff --git a/src/core/nm-config.h b/src/core/nm-config.h
index 7f1c0c4c..8279f4d2 100644
--- a/src/core/nm-config.h
+++ b/src/core/nm-config.h
@@ -185,7 +185,8 @@ gboolean                 nm_config_device_state_write(int
                                                       guint32                        route_metric_default_aspired,
                                                       guint32                        route_metric_default_effective,
                                                       const char *                   next_server,
-                                                      const char *                   root_path);
+                                                      const char *                   root_path,
+                                                      const char *                   dhcp_bootfile);
 
 void nm_config_device_state_prune_stale(GHashTable *preserve_ifindexes,
                                         NMPlatform *preserve_in_platform);
diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c
index 41d22565..8fdc7379 100644
--- a/src/core/nm-core-utils.c
+++ b/src/core/nm-core-utils.c
@@ -1484,6 +1484,112 @@ nm_match_spec_device(const GSList *specs,
     return _match_result(has_except, has_not_except, has_match, has_match_except);
 }
 
+typedef struct {
+    const char *uuid;
+    const char *id;
+    const char *origin;
+} MatchConnectionData;
+
+static gboolean
+match_connection_eval(const char *spec_str, const MatchConnectionData *match_data)
+{
+    if (spec_str[0] == '*' && spec_str[1] == '\0')
+        return TRUE;
+
+    if (_MATCH_CHECK(spec_str, "id:"))
+        return nm_streq0(spec_str, match_data->id);
+
+    if (_MATCH_CHECK(spec_str, "uuid:"))
+        return nm_streq0(spec_str, match_data->uuid);
+
+    if (_MATCH_CHECK(spec_str, "origin:"))
+        return nm_streq0(spec_str, match_data->origin);
+
+    return FALSE;
+}
+
+static NMMatchSpecMatchType
+match_spec_connection(const GSList *specs, const char *id, const char *uuid, const char *origin)
+{
+    const GSList *            iter;
+    gboolean                  has_match        = FALSE;
+    gboolean                  has_match_except = FALSE;
+    gboolean                  has_except       = FALSE;
+    gboolean                  has_not_except   = FALSE;
+    const char *              spec_str;
+    const MatchConnectionData match_data = {
+        .id     = nm_str_not_empty(id),
+        .uuid   = nm_str_not_empty(uuid),
+        .origin = nm_str_not_empty(origin),
+    };
+
+    if (!specs)
+        return NM_MATCH_SPEC_NO_MATCH;
+
+    for (iter = specs; iter; iter = iter->next) {
+        gboolean except;
+
+        spec_str = iter->data;
+
+        if (!spec_str || !*spec_str)
+            continue;
+
+        spec_str = match_except(spec_str, &except);
+
+        if (except)
+            has_except = TRUE;
+        else
+            has_not_except = TRUE;
+
+        if ((except && has_match_except) || (!except && has_match)) {
+            /* evaluating the match does not give new information. Skip it. */
+            continue;
+        }
+
+        if (!match_connection_eval(spec_str, &match_data))
+            continue;
+
+        if (except)
+            has_match_except = TRUE;
+        else
+            has_match = TRUE;
+    }
+
+    return _match_result(has_except, has_not_except, has_match, has_match_except);
+}
+
+int
+nm_utils_connection_match_spec_list(NMConnection *connection,
+                                    const GSList *specs,
+                                    int           no_match_value)
+{
+    NMMatchSpecMatchType m;
+    NMSettingUser *      s_user;
+    const char *         origin = NULL;
+
+    if (!specs)
+        return no_match_value;
+
+    s_user = _nm_connection_get_setting(connection, NM_TYPE_SETTING_USER);
+    if (s_user)
+        origin = nm_setting_user_get_data(s_user, NM_USER_TAG_ORIGIN);
+
+    m = match_spec_connection(specs,
+                              nm_connection_get_id(connection),
+                              nm_connection_get_uuid(connection),
+                              origin);
+    switch (m) {
+    case NM_MATCH_SPEC_MATCH:
+        return TRUE;
+    case NM_MATCH_SPEC_NEG_MATCH:
+        return FALSE;
+    case NM_MATCH_SPEC_NO_MATCH:
+        return no_match_value;
+    }
+    nm_assert_not_reached();
+    return no_match_value;
+}
+
 static gboolean
 match_config_eval(const char *str, const char *tag, guint cur_nm_version)
 {
diff --git a/src/core/nm-core-utils.h b/src/core/nm-core-utils.h
index bcb182b8..76c340d1 100644
--- a/src/core/nm-core-utils.h
+++ b/src/core/nm-core-utils.h
@@ -211,6 +211,10 @@ gboolean nm_utils_kernel_cmdline_match_check(const char *const *proc_cmdline,
                                              guint              num_patterns,
                                              GError **          error);
 
+int nm_utils_connection_match_spec_list(NMConnection *connection,
+                                        const GSList *specs,
+                                        int           no_match_value);
+
 /*****************************************************************************/
 
 gboolean nm_utils_connection_has_default_route(NMConnection *connection,
diff --git a/src/core/nm-firewall-utils.c b/src/core/nm-firewall-utils.c
index 3f225156..a2446553 100644
--- a/src/core/nm-firewall-utils.c
+++ b/src/core/nm-firewall-utils.c
@@ -613,17 +613,17 @@ _fw_nft_set(gboolean add, const char *ip_iface, in_addr_t addr, guint8 plen)
 
 #define _append(p_strbuf, fmt, ...) nm_str_buf_append_printf((p_strbuf), "" fmt "\n", ##__VA_ARGS__)
 
-    _append(&strbuf, "add table inet %s", table_name);
-    _append(&strbuf, "%s table inet %s", add ? "flush" : "delete", table_name);
+    _append(&strbuf, "add table ip %s", table_name);
+    _append(&strbuf, "%s table ip %s", add ? "flush" : "delete", table_name);
 
     if (add) {
         _append(&strbuf,
-                "add chain inet %s nat_postrouting {"
+                "add chain ip %s nat_postrouting {"
                 " type nat hook postrouting priority 100; policy accept; "
                 "};",
                 table_name);
         _append(&strbuf,
-                "add rule inet %s nat_postrouting ip saddr %s ip daddr != %s masquerade;",
+                "add rule ip %s nat_postrouting ip saddr %s ip daddr != %s masquerade;",
                 table_name,
                 str_subnet,
                 str_subnet);
@@ -634,41 +634,41 @@ _fw_nft_set(gboolean add, const char *ip_iface, in_addr_t addr, guint8 plen)
          */
         /*
         _append(&strbuf,
-                "add chain inet %s filter_input {"
+                "add chain ip %s filter_input {"
                 " type filter hook input priority 0; policy accept; "
                 "};",
                 table_name);
-        _append(&strbuf, "add rule inet %s filter_input tcp dport { 67, 53 } accept;", table_name);
-        _append(&strbuf, "add rule inet %s filter_input udp dport { 67, 53 } accept;", table_name);
+        _append(&strbuf, "add rule ip %s filter_input tcp dport { 67, 53 } accept;", table_name);
+        _append(&strbuf, "add rule ip %s filter_input udp dport { 67, 53 } accept;", table_name);
         */
 
         _append(&strbuf,
-                "add chain inet %s filter_forward {"
+                "add chain ip %s filter_forward {"
                 " type filter hook forward priority 0; policy accept; "
                 "};",
                 table_name);
         _append(&strbuf,
-                "add rule inet %s filter_forward ip daddr %s oifname \"%s\" "
+                "add rule ip %s filter_forward ip daddr %s oifname \"%s\" "
                 " ct state { established, related } accept;",
                 table_name,
                 str_subnet,
                 ip_iface);
         _append(&strbuf,
-                "add rule inet %s filter_forward ip saddr %s iifname \"%s\" accept;",
+                "add rule ip %s filter_forward ip saddr %s iifname \"%s\" accept;",
                 table_name,
                 str_subnet,
                 ip_iface);
         _append(&strbuf,
-                "add rule inet %s filter_forward iifname \"%s\" oifname \"%s\" accept;",
+                "add rule ip %s filter_forward iifname \"%s\" oifname \"%s\" accept;",
                 table_name,
                 ip_iface,
                 ip_iface);
         _append(&strbuf,
-                "add rule inet %s filter_forward iifname \"%s\" reject;",
+                "add rule ip %s filter_forward iifname \"%s\" reject;",
                 table_name,
                 ip_iface);
         _append(&strbuf,
-                "add rule inet %s filter_forward oifname \"%s\" reject;",
+                "add rule ip %s filter_forward oifname \"%s\" reject;",
                 table_name,
                 ip_iface);
     }
diff --git a/src/core/nm-firewalld-manager.c b/src/core/nm-firewalld-manager.c
index c562d978..7866894a 100644
--- a/src/core/nm-firewalld-manager.c
+++ b/src/core/nm-firewalld-manager.c
@@ -15,6 +15,7 @@
 
 #define FIREWALL_DBUS_SERVICE        "org.fedoraproject.FirewallD1"
 #define FIREWALL_DBUS_PATH           "/org/fedoraproject/FirewallD1"
+#define FIREWALL_DBUS_INTERFACE      "org.fedoraproject.FirewallD1"
 #define FIREWALL_DBUS_INTERFACE_ZONE "org.fedoraproject.FirewallD1.zone"
 
 /*****************************************************************************/
@@ -30,10 +31,12 @@ typedef struct {
 
     CList pending_calls;
 
+    char *name_owner;
+
+    guint reloaded_id;
     guint name_owner_changed_id;
 
     bool dbus_inited : 1;
-    bool running : 1;
 } NMFirewalldManagerPrivate;
 
 struct _NMFirewalldManager {
@@ -105,7 +108,7 @@ _ops_type_to_string(OpsType ops_type)
 }
 
 #define _NMLOG_DOMAIN      LOGD_FIREWALL
-#define _NMLOG_PREFIX_NAME "firewall"
+#define _NMLOG_PREFIX_NAME "firewalld"
 #define _NMLOG(level, call_id, ...)                                                 \
     G_STMT_START                                                                    \
     {                                                                               \
@@ -146,6 +149,14 @@ _ops_type_to_string(OpsType ops_type)
 
 /*****************************************************************************/
 
+static void
+_signal_emit_state_changed(NMFirewalldManager *self, NMFirewalldManagerStateChangedType signal_type)
+{
+    g_signal_emit(self, signals[STATE_CHANGED], 0, (int) signal_type);
+}
+
+/*****************************************************************************/
+
 static gboolean
 _get_running(NMFirewalldManagerPrivate *priv)
 {
@@ -154,7 +165,7 @@ _get_running(NMFirewalldManagerPrivate *priv)
      * service is indeed running. That is the time when we queue the
      * requests, and they will be started once the get-name-owner call
      * returns. */
-    return priv->running || (priv->dbus_connection && !priv->dbus_inited);
+    return priv->name_owner || (priv->dbus_connection && !priv->dbus_inited);
 }
 
 gboolean
@@ -315,7 +326,7 @@ _handle_dbus_start(NMFirewalldManager *self, NMFirewalldManagerCallId *call_id)
     GVariant *                 arg;
 
     nm_assert(call_id);
-    nm_assert(priv->running);
+    nm_assert(priv->name_owner);
     nm_assert(!call_id->is_idle);
     nm_assert(c_list_contains(&priv->pending_calls, &call_id->lst));
 
@@ -341,7 +352,7 @@ _handle_dbus_start(NMFirewalldManager *self, NMFirewalldManagerCallId *call_id)
     call_id->dbus.cancellable = g_cancellable_new();
 
     g_dbus_connection_call(priv->dbus_connection,
-                           FIREWALL_DBUS_SERVICE,
+                           priv->name_owner,
                            FIREWALL_DBUS_PATH,
                            FIREWALL_DBUS_INTERFACE_ZONE,
                            dbus_method,
@@ -378,10 +389,10 @@ _start_request(NMFirewalldManager *                self,
           iface,
           NM_PRINT_FMT_QUOTED(zone, "\"", zone, "\"", "default"),
           call_id->is_idle ? " (not running, simulate success)"
-                           : (!priv->running ? " (waiting to initialize)" : ""));
+                           : (!priv->name_owner ? " (waiting to initialize)" : ""));
 
     if (!call_id->is_idle) {
-        if (priv->running)
+        if (priv->name_owner)
             _handle_dbus_start(self, call_id);
         if (!call_id->callback) {
             /* if the user did not provide a callback, the call_id is useless.
@@ -463,6 +474,7 @@ name_owner_changed(NMFirewalldManager *self, const char *owner)
     gboolean                                       was_running;
     gboolean                                       now_running;
     gboolean                                       just_initied;
+    gboolean                                       name_owner_changed;
 
     owner = nm_str_not_empty(owner);
 
@@ -474,8 +486,8 @@ name_owner_changed(NMFirewalldManager *self, const char *owner)
     was_running  = _get_running(priv);
     just_initied = !priv->dbus_inited;
 
-    priv->dbus_inited = TRUE;
-    priv->running     = !!owner;
+    priv->dbus_inited  = TRUE;
+    name_owner_changed = nm_utils_strdup_reset(&priv->name_owner, owner);
 
     now_running = _get_running(priv);
 
@@ -495,7 +507,7 @@ name_owner_changed(NMFirewalldManager *self, const char *owner)
             nm_assert(!call_id->is_idle);
             nm_assert(call_id->dbus.arg);
 
-            if (priv->running) {
+            if (priv->name_owner) {
                 _LOGD(call_id, "initalizing: make D-Bus call");
                 _handle_dbus_start(self, call_id);
             } else {
@@ -511,8 +523,30 @@ name_owner_changed(NMFirewalldManager *self, const char *owner)
         }
     }
 
-    if (was_running != now_running)
-        g_signal_emit(self, signals[STATE_CHANGED], 0, FALSE);
+    if (just_initied)
+        _signal_emit_state_changed(self, NM_FIREWALLD_MANAGER_STATE_CHANGED_TYPE_INITIALIZED);
+    else if (was_running != now_running || name_owner_changed)
+        _signal_emit_state_changed(self,
+                                   NM_FIREWALLD_MANAGER_STATE_CHANGED_TYPE_NAME_OWNER_CHANGED);
+}
+
+static void
+reloaded_cb(GDBusConnection *connection,
+            const char *     sender_name,
+            const char *     object_path,
+            const char *     interface_name,
+            const char *     signal_name,
+            GVariant *       parameters,
+            gpointer         user_data)
+{
+    NMFirewalldManager *       self = user_data;
+    NMFirewalldManagerPrivate *priv = NM_FIREWALLD_MANAGER_GET_PRIVATE(self);
+
+    if (!nm_streq0(sender_name, priv->name_owner))
+        return;
+
+    _LOGT(NULL, "reloaded signal received");
+    _signal_emit_state_changed(self, NM_FIREWALLD_MANAGER_STATE_CHANGED_TYPE_RELOADED);
 }
 
 static void
@@ -541,7 +575,7 @@ get_name_owner_cb(const char *name_owner, GError *error, gpointer user_data)
     NMFirewalldManager *       self;
     NMFirewalldManagerPrivate *priv;
 
-    if (!name_owner && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+    if (nm_utils_error_is_cancelled(error))
         return;
 
     self = user_data;
@@ -568,6 +602,17 @@ nm_firewalld_manager_init(NMFirewalldManager *self)
         return;
     }
 
+    priv->reloaded_id = g_dbus_connection_signal_subscribe(priv->dbus_connection,
+                                                           FIREWALL_DBUS_SERVICE,
+                                                           FIREWALL_DBUS_INTERFACE,
+                                                           "Reloaded",
+                                                           FIREWALL_DBUS_PATH,
+                                                           NULL,
+                                                           G_DBUS_SIGNAL_FLAGS_NONE,
+                                                           reloaded_cb,
+                                                           self,
+                                                           NULL);
+
     priv->name_owner_changed_id =
         nm_dbus_connection_signal_subscribe_name_owner_changed(priv->dbus_connection,
                                                                FIREWALL_DBUS_SERVICE,
@@ -594,6 +639,7 @@ dispose(GObject *object)
      * we don't expect pending operations at this point. */
     nm_assert(c_list_is_empty(&priv->pending_calls));
 
+    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->reloaded_id);
     nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->name_owner_changed_id);
 
     nm_clear_g_cancellable(&priv->get_name_owner_cancellable);
@@ -616,8 +662,8 @@ nm_firewalld_manager_class_init(NMFirewalldManagerClass *klass)
                                           0,
                                           NULL,
                                           NULL,
-                                          g_cclosure_marshal_VOID__BOOLEAN,
+                                          g_cclosure_marshal_VOID__INT,
                                           G_TYPE_NONE,
                                           1,
-                                          G_TYPE_BOOLEAN /* initialized_now */);
+                                          G_TYPE_INT /* signal-type */);
 }
diff --git a/src/core/nm-firewalld-manager.h b/src/core/nm-firewalld-manager.h
index febb9bac..1f76beba 100644
--- a/src/core/nm-firewalld-manager.h
+++ b/src/core/nm-firewalld-manager.h
@@ -19,6 +19,12 @@
 
 #define NM_FIREWALLD_MANAGER_STATE_CHANGED "state-changed"
 
+typedef enum {
+    NM_FIREWALLD_MANAGER_STATE_CHANGED_TYPE_INITIALIZED,
+    NM_FIREWALLD_MANAGER_STATE_CHANGED_TYPE_NAME_OWNER_CHANGED,
+    NM_FIREWALLD_MANAGER_STATE_CHANGED_TYPE_RELOADED,
+} NMFirewalldManagerStateChangedType;
+
 typedef struct _NMFirewalldManagerCallId NMFirewalldManagerCallId;
 
 typedef struct _NMFirewalldManager      NMFirewalldManager;
diff --git a/src/core/nm-ip4-config.c b/src/core/nm-ip4-config.c
index 47f0ee3a..90531d02 100644
--- a/src/core/nm-ip4-config.c
+++ b/src/core/nm-ip4-config.c
@@ -163,6 +163,11 @@ _nm_ip_config_add_obj(NMDedupMultiIndex *          multi_idx,
                     obj_new_stackinit.ip_route.rt_source = obj_old->ip_route.rt_source;
                     modified                             = TRUE;
                 }
+                if (!obj_new->ip_route.is_external && obj_old->ip_route.is_external) {
+                    obj_new = nmp_object_stackinit_obj(&obj_new_stackinit, obj_new);
+                    obj_new_stackinit.ip_route.is_external = FALSE;
+                    modified                               = TRUE;
+                }
                 break;
             default:
                 nm_assert_not_reached();
@@ -507,22 +512,6 @@ _notify_routes(NMIP4Config *self)
 
 /*****************************************************************************/
 
-static int
-sort_captured_addresses(const CList *lst_a, const CList *lst_b, gconstpointer user_data)
-{
-    const NMPlatformIP4Address *addr_a =
-        NMP_OBJECT_CAST_IP4_ADDRESS(c_list_entry(lst_a, NMDedupMultiEntry, lst_entries)->obj);
-    const NMPlatformIP4Address *addr_b =
-        NMP_OBJECT_CAST_IP4_ADDRESS(c_list_entry(lst_b, NMDedupMultiEntry, lst_entries)->obj);
-
-    nm_assert(addr_a);
-    nm_assert(addr_b);
-
-    /* Primary addresses first */
-    return NM_FLAGS_HAS(addr_a->n_ifa_flags, IFA_F_SECONDARY)
-           - NM_FLAGS_HAS(addr_b->n_ifa_flags, IFA_F_SECONDARY);
-}
-
 NMIP4Config *
 nm_ip4_config_clone(const NMIP4Config *self)
 {
@@ -554,7 +543,7 @@ nm_ip4_config_capture(NMDedupMultiIndex *multi_idx, NMPlatform *platform, int if
 
     head_entry = nm_platform_lookup_object(platform, NMP_OBJECT_TYPE_IP4_ADDRESS, ifindex);
     if (head_entry) {
-        nmp_cache_iter_for_each (&iter, head_entry, &plobj) {
+        nmp_cache_iter_for_each_reverse (&iter, head_entry, &plobj) {
             if (!_nm_ip_config_add_obj(priv->multi_idx,
                                        &priv->idx_ip4_addresses_,
                                        ifindex,
@@ -566,9 +555,6 @@ nm_ip4_config_capture(NMDedupMultiIndex *multi_idx, NMPlatform *platform, int if
                                        NULL))
                 nm_assert_not_reached();
         }
-        head_entry = nm_ip4_config_lookup_addresses(self);
-        nm_assert(head_entry);
-        nm_dedup_multi_head_entry_sort(head_entry, sort_captured_addresses, NULL);
         _notify_addresses(self);
     }
 
diff --git a/src/core/nm-ip6-config.c b/src/core/nm-ip6-config.c
index d2ecf175..65e84737 100644
--- a/src/core/nm-ip6-config.c
+++ b/src/core/nm-ip6-config.c
@@ -316,7 +316,7 @@ nm_ip6_config_capture(NMDedupMultiIndex *       multi_idx,
 
     head_entry = nm_platform_lookup_object(platform, NMP_OBJECT_TYPE_IP6_ADDRESS, ifindex);
     if (head_entry) {
-        nmp_cache_iter_for_each (&iter, head_entry, &plobj) {
+        nmp_cache_iter_for_each_reverse (&iter, head_entry, &plobj) {
             if (!_nm_ip_config_add_obj(priv->multi_idx,
                                        &priv->idx_ip6_addresses_,
                                        ifindex,
@@ -328,11 +328,6 @@ nm_ip6_config_capture(NMDedupMultiIndex *       multi_idx,
                                        NULL))
                 nm_assert_not_reached();
         }
-        head_entry = nm_ip6_config_lookup_addresses(self);
-        nm_assert(head_entry);
-        nm_dedup_multi_head_entry_sort(head_entry,
-                                       sort_captured_addresses,
-                                       GINT_TO_POINTER(use_temporary));
         _notify_addresses(self);
     }
 
diff --git a/src/core/nm-manager.c b/src/core/nm-manager.c
index b66f7fbb..d859c1ea 100644
--- a/src/core/nm-manager.c
+++ b/src/core/nm-manager.c
@@ -2595,6 +2595,12 @@ get_existing_connection(NMManager *self, NMDevice *device, gboolean *out_generat
 
     nm_device_capture_initial_config(device);
 
+    if (!nm_device_can_assume_connections(device)) {
+        nm_device_assume_state_reset(device);
+        _LOG2D(LOGD_DEVICE, device, "assume: device cannot assume connection");
+        return NULL;
+    }
+
     if (ifindex) {
         int master_ifindex = nm_platform_link_get_master(priv->platform, ifindex);
 
@@ -2626,39 +2632,47 @@ get_existing_connection(NMManager *self, NMDevice *device, gboolean *out_generat
         }
     }
 
-    /* The core of the API is nm_device_generate_connection() function and
-     * update_connection() virtual method and the convenient connection_type
-     * class attribute. Subclasses supporting the new API must have
-     * update_connection() implemented, otherwise nm_device_generate_connection()
-     * returns NULL.
-     */
-    connection = nm_device_generate_connection(device, master, &maybe_later, &gen_error);
-    if (!connection) {
-        if (maybe_later) {
-            /* The device can generate a connection, but it failed for now.
-             * Give it a chance to match a connection from the state file. */
-            only_by_uuid = TRUE;
-        } else {
-            nm_device_assume_state_reset(device);
-            _LOG2D(LOGD_DEVICE,
-                   device,
-                   "assume: cannot generate connection: %s",
-                   gen_error->message);
-            return NULL;
+    if (nm_config_data_get_device_config_boolean(NM_CONFIG_GET_DATA,
+                                                 NM_CONFIG_KEYFILE_KEY_DEVICE_KEEP_CONFIGURATION,
+                                                 device,
+                                                 TRUE,
+                                                 TRUE)) {
+        /* The core of the API is nm_device_generate_connection() function, based on
+         * update_connection() virtual method and the @connection_type_supported
+         * class attribute. Devices that support assuming existing connections must
+         * have update_connection() implemented, otherwise
+         * nm_device_generate_connection() returns NULL. */
+        connection = nm_device_generate_connection(device, master, &maybe_later, &gen_error);
+        if (!connection) {
+            if (maybe_later) {
+                /* The device can potentially assume connections, but at this
+                 * time we can't generate a connection because no address is
+                 * configured. Allow the device to assume a connection indicated
+                 * in the state file by UUID. */
+                only_by_uuid = TRUE;
+            } else {
+                nm_device_assume_state_reset(device);
+                _LOG2D(LOGD_DEVICE,
+                       device,
+                       "assume: cannot generate connection: %s",
+                       gen_error->message);
+                return NULL;
+            }
         }
+    } else {
+        connection   = NULL;
+        only_by_uuid = TRUE;
+        g_set_error(&gen_error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_FAILED,
+                    "device %s has 'keep-configuration=no'",
+                    nm_device_get_iface(device));
     }
 
     nm_device_assume_state_get(device, &assume_state_guess_assume, &assume_state_connection_uuid);
 
-    /* Now we need to compare the generated connection to each configured
-     * connection. The comparison function is the heart of the connection
-     * assumption implementation and it must compare the connections very
-     * carefully to sort out various corner cases. Also, the comparison is
-     * not entirely symmetric.
-     *
-     * When no configured connection matches the generated connection, we keep
-     * the generated connection instead.
-     */
+    /* If the device state file indicates a connection that was active before NM
+     * restarted, perform basic sanity checks on it. */
     if (assume_state_connection_uuid
         && (connection_checked =
                 nm_settings_get_connection_by_uuid(priv->settings, assume_state_connection_uuid))
@@ -2692,8 +2706,9 @@ get_existing_connection(NMManager *self, NMDevice *device, gboolean *out_generat
         gs_free NMSettingsConnection **sett_conns = NULL;
         guint                          len, i, j;
 
-        /* the state file doesn't indicate a connection UUID to assume. Search the
-         * persistent connections for a matching candidate. */
+        /* @assume_state_guess_assume=TRUE means this is the first start of NM
+         * and the state file contains no UUID. Search persistent connections
+         * for a matching candidate. */
         sett_conns = nm_manager_get_activatable_connections(self, FALSE, FALSE, &len);
         if (len > 0) {
             for (i = 0, j = 0; i < len; i++) {
@@ -2766,6 +2781,8 @@ get_existing_connection(NMManager *self, NMDevice *device, gboolean *out_generat
         return matched;
     }
 
+    /* When no configured connection matches the generated connection, we keep
+     * the generated connection instead. */
     _LOG2D(LOGD_DEVICE,
            device,
            "assume: generated connection '%s' (%s)",
@@ -6803,8 +6820,9 @@ nm_manager_write_device_state(NMManager *self, NMDevice *device, int *out_ifinde
     guint32                        route_metric_default_effective;
     NMTernary                      nm_owned;
     NMDhcpConfig *                 dhcp_config;
-    const char *                   next_server = NULL;
-    const char *                   root_path   = NULL;
+    const char *                   next_server   = NULL;
+    const char *                   root_path     = NULL;
+    const char *                   dhcp_bootfile = NULL;
 
     NM_SET_OUT(out_ifindex, 0);
 
@@ -6848,8 +6866,11 @@ nm_manager_write_device_state(NMManager *self, NMDevice *device, int *out_ifinde
 
     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");
+        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,
@@ -6860,7 +6881,8 @@ nm_manager_write_device_state(NMManager *self, NMDevice *device, int *out_ifinde
                                       route_metric_default_aspired,
                                       route_metric_default_effective,
                                       next_server,
-                                      root_path))
+                                      root_path,
+                                      dhcp_bootfile))
         return FALSE;
 
     NM_SET_OUT(out_ifindex, ifindex);
diff --git a/src/core/nm-policy.c b/src/core/nm-policy.c
index e4914acf..e147e504 100644
--- a/src/core/nm-policy.c
+++ b/src/core/nm-policy.c
@@ -2519,14 +2519,15 @@ connection_added(NMSettings *settings, NMSettingsConnection *connection, gpointe
 }
 
 static void
-firewall_state_changed(NMFirewalldManager *manager, gboolean initialized_now, gpointer user_data)
+firewall_state_changed(NMFirewalldManager *manager, int signal_type_i, gpointer user_data)
 {
-    NMPolicy *       self = (NMPolicy *) user_data;
-    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
-    const CList *    tmp_lst;
-    NMDevice *       device;
+    const NMFirewalldManagerStateChangedType signal_type = signal_type_i;
+    NMPolicy *                               self        = user_data;
+    NMPolicyPrivate *                        priv        = NM_POLICY_GET_PRIVATE(self);
+    const CList *                            tmp_lst;
+    NMDevice *                               device;
 
-    if (initialized_now) {
+    if (signal_type == NM_FIREWALLD_MANAGER_STATE_CHANGED_TYPE_INITIALIZED) {
         /* the firewall manager was initializing, but all requests
          * so fare were queued and are already sent. No need to
          * re-update the firewall zone of the devices. */
diff --git a/src/core/platform/nm-fake-platform.c b/src/core/platform/nm-fake-platform.c
index 0108b581..b773df9a 100644
--- a/src/core/platform/nm-fake-platform.c
+++ b/src/core/platform/nm-fake-platform.c
@@ -1117,6 +1117,9 @@ ip_route_add(NMPlatform *             platform,
                                                 : NMP_OBJECT_TYPE_IP6_ROUTE,
                          (const NMPlatformObject *) route);
     r = NMP_OBJECT_CAST_IP_ROUTE(obj);
+
+    r->is_external = TRUE;
+
     nm_platform_ip_route_normalize(addr_family, r);
 
     switch (addr_family) {
diff --git a/src/core/platform/tests/test-common.c b/src/core/platform/tests/test-common.c
index f9a141bc..2fc9e836 100644
--- a/src/core/platform/tests/test-common.c
+++ b/src/core/platform/tests/test-common.c
@@ -1427,6 +1427,10 @@ nmtstp_link_bridge_normalize_jiffies_time(const NMPlatformLnkBridge *requested,
                                           const NMPlatformLnkBridge *kernel,
                                           NMPlatformLnkBridge *      dst)
 {
+    g_assert(requested);
+    g_assert(dst);
+    g_assert(kernel);
+
     if (dst != requested)
         *dst = *requested;
 
diff --git a/src/core/platform/tests/test-route.c b/src/core/platform/tests/test-route.c
index 2b9b2f8f..4b1db7fa 100644
--- a/src/core/platform/tests/test-route.c
+++ b/src/core/platform/tests/test-route.c
@@ -332,30 +332,33 @@ test_ip4_route(void)
     /* Test route listing */
     routes = nmtstp_ip4_route_get_all(NM_PLATFORM_GET, ifindex);
     memset(rts, 0, sizeof(rts));
-    rts[0].rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
-    rts[0].network   = gateway;
-    rts[0].plen      = 32;
-    rts[0].ifindex   = ifindex;
-    rts[0].gateway   = INADDR_ANY;
-    rts[0].metric    = metric;
-    rts[0].mss       = mss;
-    rts[0].scope_inv = nm_platform_route_scope_inv(RT_SCOPE_LINK);
-    rts[1].rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
-    rts[1].network   = network;
-    rts[1].plen      = plen;
-    rts[1].ifindex   = ifindex;
-    rts[1].gateway   = gateway;
-    rts[1].metric    = metric;
-    rts[1].mss       = mss;
-    rts[1].scope_inv = nm_platform_route_scope_inv(RT_SCOPE_UNIVERSE);
-    rts[2].rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
-    rts[2].network   = 0;
-    rts[2].plen      = 0;
-    rts[2].ifindex   = ifindex;
-    rts[2].gateway   = gateway;
-    rts[2].metric    = metric;
-    rts[2].mss       = mss;
-    rts[2].scope_inv = nm_platform_route_scope_inv(RT_SCOPE_UNIVERSE);
+    rts[0].rt_source   = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
+    rts[0].network     = gateway;
+    rts[0].plen        = 32;
+    rts[0].ifindex     = ifindex;
+    rts[0].gateway     = INADDR_ANY;
+    rts[0].metric      = metric;
+    rts[0].mss         = mss;
+    rts[0].is_external = TRUE;
+    rts[0].scope_inv   = nm_platform_route_scope_inv(RT_SCOPE_LINK);
+    rts[1].rt_source   = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
+    rts[1].network     = network;
+    rts[1].plen        = plen;
+    rts[1].ifindex     = ifindex;
+    rts[1].gateway     = gateway;
+    rts[1].metric      = metric;
+    rts[1].mss         = mss;
+    rts[1].scope_inv   = nm_platform_route_scope_inv(RT_SCOPE_UNIVERSE);
+    rts[1].is_external = TRUE;
+    rts[2].rt_source   = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
+    rts[2].network     = 0;
+    rts[2].plen        = 0;
+    rts[2].ifindex     = ifindex;
+    rts[2].gateway     = gateway;
+    rts[2].metric      = metric;
+    rts[2].mss         = mss;
+    rts[2].scope_inv   = nm_platform_route_scope_inv(RT_SCOPE_UNIVERSE);
+    rts[2].is_external = TRUE;
     g_assert_cmpint(routes->len, ==, 3);
     nmtst_platform_ip4_routes_equal_aptr((const NMPObject *const *) routes->pdata,
                                          rts,
@@ -489,30 +492,33 @@ test_ip6_route(void)
     /* Test route listing */
     routes = nmtstp_ip6_route_get_all(NM_PLATFORM_GET, ifindex);
     memset(rts, 0, sizeof(rts));
-    rts[0].rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
-    rts[0].network   = gateway;
-    rts[0].plen      = 128;
-    rts[0].ifindex   = ifindex;
-    rts[0].gateway   = in6addr_any;
-    rts[0].pref_src  = in6addr_any;
-    rts[0].metric    = metric;
-    rts[0].mss       = mss;
-    rts[1].rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
-    rts[1].network   = network;
-    rts[1].plen      = plen;
-    rts[1].ifindex   = ifindex;
-    rts[1].gateway   = gateway;
-    rts[1].pref_src  = pref_src;
-    rts[1].metric    = metric;
-    rts[1].mss       = mss;
-    rts[2].rt_source = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
-    rts[2].network   = in6addr_any;
-    rts[2].plen      = 0;
-    rts[2].ifindex   = ifindex;
-    rts[2].gateway   = gateway;
-    rts[2].pref_src  = in6addr_any;
-    rts[2].metric    = metric;
-    rts[2].mss       = mss;
+    rts[0].rt_source   = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
+    rts[0].network     = gateway;
+    rts[0].plen        = 128;
+    rts[0].ifindex     = ifindex;
+    rts[0].gateway     = in6addr_any;
+    rts[0].pref_src    = in6addr_any;
+    rts[0].metric      = metric;
+    rts[0].mss         = mss;
+    rts[0].is_external = TRUE;
+    rts[1].rt_source   = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
+    rts[1].network     = network;
+    rts[1].plen        = plen;
+    rts[1].ifindex     = ifindex;
+    rts[1].gateway     = gateway;
+    rts[1].pref_src    = pref_src;
+    rts[1].metric      = metric;
+    rts[1].mss         = mss;
+    rts[1].is_external = TRUE;
+    rts[2].rt_source   = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
+    rts[2].network     = in6addr_any;
+    rts[2].plen        = 0;
+    rts[2].ifindex     = ifindex;
+    rts[2].gateway     = gateway;
+    rts[2].pref_src    = in6addr_any;
+    rts[2].metric      = metric;
+    rts[2].mss         = mss;
+    rts[2].is_external = TRUE;
     g_assert_cmpint(routes->len, ==, 3);
     nmtst_platform_ip6_routes_equal_aptr((const NMPObject *const *) routes->pdata,
                                          rts,
@@ -709,6 +715,7 @@ test_ip4_route_options(gconstpointer test_data)
     for (i = 0; i < rts_n; i++) {
         rts_cmp[i] = rts_add[i];
         nm_platform_ip_route_normalize(AF_INET, NM_PLATFORM_IP_ROUTE_CAST(&rts_cmp[i]));
+        rts_cmp[i].is_external = TRUE;
     }
 
     routes = nmtstp_ip4_route_get_all(NM_PLATFORM_GET, IFINDEX);
@@ -880,6 +887,7 @@ test_ip6_route_options(gconstpointer test_data)
     for (i = 0; i < rts_n; i++) {
         rts_cmp[i] = rts_add[i];
         nm_platform_ip_route_normalize(AF_INET6, NM_PLATFORM_IP_ROUTE_CAST(&rts_cmp[i]));
+        rts_cmp[i].is_external = TRUE;
     }
 
     routes = nmtstp_ip6_route_get_all(NM_PLATFORM_GET, IFINDEX);
diff --git a/src/core/ppp/meson.build b/src/core/ppp/meson.build
index ffeb0eba..9ee46113 100644
--- a/src/core/ppp/meson.build
+++ b/src/core/ppp/meson.build
@@ -14,6 +14,7 @@ nm_pppd_plugin = shared_module(
   ],
   link_with: [
     libnm_core_impl,
+    libnm_log_null,
     libnm_glib_aux,
     libnm_std_aux,
     libc_siphash,
diff --git a/src/core/settings/nm-settings-connection.c b/src/core/settings/nm-settings-connection.c
index 641f3297..36ef6acb 100644
--- a/src/core/settings/nm-settings-connection.c
+++ b/src/core/settings/nm-settings-connection.c
@@ -11,6 +11,7 @@
 #include "c-list/src/c-list.h"
 
 #include "libnm-glib-aux/nm-keyfile-aux.h"
+#include "libnm-glib-aux/nm-c-list.h"
 #include "libnm-core-aux-intern/nm-common-macros.h"
 #include "nm-config.h"
 #include "nm-config-data.h"
@@ -30,6 +31,8 @@
 #define AUTOCONNECT_RETRIES_FOREVER     -1
 #define AUTOCONNECT_RESET_RETRIES_TIMER 300
 
+#define SEEN_BSSIDS_MAX 30
+
 #define _NM_SETTINGS_UPDATE2_FLAG_ALL_PERSIST_MODES                          \
     ((NMSettingsUpdate2Flags) (NM_SETTINGS_UPDATE2_FLAG_TO_DISK              \
                                | NM_SETTINGS_UPDATE2_FLAG_IN_MEMORY          \
@@ -59,6 +62,56 @@ nm_settings_connections_array_to_connections(NMSettingsConnection *const *connec
 
 /*****************************************************************************/
 
+typedef struct {
+    char  bssid[sizeof(NMEtherAddr) * 3];
+    CList seen_bssids_lst;
+} SeenBssidEntry;
+
+static inline SeenBssidEntry *
+_seen_bssid_entry_init_stale(SeenBssidEntry *entry, const NMEtherAddr *bssid_bin)
+{
+    _nm_utils_hwaddr_ntoa(bssid_bin, sizeof(NMEtherAddr), TRUE, entry->bssid, sizeof(entry->bssid));
+    return entry;
+}
+
+static inline SeenBssidEntry *
+_seen_bssid_entry_new_stale_bin(const NMEtherAddr *bssid_bin)
+{
+    return _seen_bssid_entry_init_stale(g_slice_new(SeenBssidEntry), bssid_bin);
+}
+
+static inline SeenBssidEntry *
+_seen_bssid_entry_new_stale_copy(const SeenBssidEntry *src)
+{
+    SeenBssidEntry *entry;
+
+    entry = g_slice_new(SeenBssidEntry);
+    memcpy(entry->bssid, src->bssid, sizeof(entry->bssid));
+    return entry;
+}
+
+static void
+_seen_bssid_entry_free(gpointer data)
+{
+    SeenBssidEntry *entry = data;
+
+    c_list_unlink_stale(&entry->seen_bssids_lst);
+    nm_g_slice_free(entry);
+}
+
+/*****************************************************************************/
+
+static GHashTable *
+_seen_bssids_hash_new(void)
+{
+    return g_hash_table_new_full(nm_str_hash,
+                                 g_str_equal,
+                                 (GDestroyNotify) _seen_bssid_entry_free,
+                                 NULL);
+}
+
+/*****************************************************************************/
+
 NM_GOBJECT_PROPERTIES_DEFINE(NMSettingsConnection, PROP_UNSAVED, PROP_FLAGS, PROP_FILENAME, );
 
 enum { UPDATED_INTERNAL, FLAGS_CHANGED, LAST_SIGNAL };
@@ -99,7 +152,8 @@ typedef struct _NMSettingsConnectionPrivate {
      */
     GVariant *agent_secrets;
 
-    GHashTable *seen_bssids; /* Up-to-date BSSIDs that's been seen for the connection */
+    CList       seen_bssids_lst_head;
+    GHashTable *seen_bssids_hash;
 
     guint64 timestamp; /* Up-to-date timestamp of connection use */
 
@@ -167,7 +221,9 @@ static const GDBusSignalInfo             signal_info_updated;
 static const GDBusSignalInfo             signal_info_removed;
 static const NMDBusInterfaceInfoExtended interface_info_settings_connection;
 
-static void update_agent_secrets_cache(NMSettingsConnection *self, NMConnection *new);
+static void  update_agent_secrets_cache(NMSettingsConnection *self, NMConnection *new);
+static guint _get_seen_bssids(NMSettingsConnection *self,
+                              const char *          strv_buf[static(SEEN_BSSIDS_MAX + 1)]);
 
 /*****************************************************************************/
 
@@ -246,14 +302,6 @@ nm_settings_connection_still_valid(NMSettingsConnection *self)
 
 /*****************************************************************************/
 
-static GHashTable *
-_seen_bssids_hash_new(void)
-{
-    return g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, NULL);
-}
-
-/*****************************************************************************/
-
 static void
 _getsettings_cached_clear(NMSettingsConnectionPrivate *priv)
 {
@@ -1300,8 +1348,8 @@ get_settings_auth_cb(NMSettingsConnection * self,
                      GError *               error,
                      gpointer               data)
 {
-    gs_free const char **            seen_bssids = NULL;
-    NMConnectionSerializationOptions options     = {};
+    const char *                     seen_bssids_strv[SEEN_BSSIDS_MAX + 1];
+    NMConnectionSerializationOptions options = {};
 
     if (error) {
         g_dbus_method_invocation_return_gerror(context, error);
@@ -1321,8 +1369,8 @@ get_settings_auth_cb(NMSettingsConnection * self,
      * from the same reason as timestamp. Thus we put it here to GetSettings()
      * return settings too.
      */
-    seen_bssids         = nm_settings_connection_get_seen_bssids(self);
-    options.seen_bssids = seen_bssids;
+    _get_seen_bssids(self, seen_bssids_strv);
+    options.seen_bssids = seen_bssids_strv;
 
     /* Secrets should *never* be returned by the GetSettings method, they
      * get returned by the GetSecrets method which can be better
@@ -2304,68 +2352,66 @@ _nm_settings_connection_register_kf_dbs(NMSettingsConnection *self,
 
     if (priv->kf_db_seen_bssids != kf_db_seen_bssids) {
         gs_strfreev char **tmp_strv = NULL;
-        gsize              i, len;
+        gsize              len;
+        gsize              i;
+        guint              result_len;
 
         nm_key_file_db_unref(priv->kf_db_seen_bssids);
         priv->kf_db_seen_bssids = nm_key_file_db_ref(kf_db_seen_bssids);
 
         tmp_strv = nm_key_file_db_get_string_list(priv->kf_db_seen_bssids, connection_uuid, &len);
 
-        nm_clear_pointer(&priv->seen_bssids, g_hash_table_unref);
+        if (priv->seen_bssids_hash)
+            g_hash_table_remove_all(priv->seen_bssids_hash);
 
-        if (len > 0) {
-            _LOGT("read %zu seen-bssids from keyfile database \"%s\"",
-                  len,
-                  nm_key_file_db_get_filename(priv->kf_db_seen_bssids));
-            priv->seen_bssids = _seen_bssids_hash_new();
-            for (i = len; i > 0;)
-                g_hash_table_add(priv->seen_bssids, g_steal_pointer(&tmp_strv[--i]));
-            nm_clear_g_free(&tmp_strv);
-        } else {
-            NMSettingWireless *s_wifi;
+        for (result_len = 0, i = 0; i < len; i++) {
+            NMEtherAddr     addr_bin;
+            SeenBssidEntry *entry;
 
-            _LOGT("no seen-bssids from keyfile database \"%s\"",
-                  nm_key_file_db_get_filename(priv->kf_db_seen_bssids));
+            nm_assert(result_len == nm_g_hash_table_size(priv->seen_bssids_hash));
+            if (result_len >= SEEN_BSSIDS_MAX)
+                break;
 
-            /* If this connection didn't have an entry in the seen-bssids database,
-             * maybe this is the first time we've read it in, so populate the
-             * seen-bssids list from the deprecated seen-bssids property of the
-             * wifi setting.
-             */
-            s_wifi =
-                nm_connection_get_setting_wireless(nm_settings_connection_get_connection(self));
-            if (s_wifi) {
-                len = nm_setting_wireless_get_num_seen_bssids(s_wifi);
-                if (len > 0) {
-                    priv->seen_bssids = _seen_bssids_hash_new();
-                    for (i = 0; i < len; i++) {
-                        const char *bssid = nm_setting_wireless_get_seen_bssid(s_wifi, i);
-
-                        g_hash_table_add(priv->seen_bssids, g_strdup(bssid));
-                    }
-                }
+            if (!_nm_utils_hwaddr_aton_exact(tmp_strv[i], &addr_bin, sizeof(addr_bin)))
+                continue;
+
+            if (!priv->seen_bssids_hash)
+                priv->seen_bssids_hash = _seen_bssids_hash_new();
+
+            entry = _seen_bssid_entry_new_stale_bin(&addr_bin);
+            if (!g_hash_table_insert(priv->seen_bssids_hash, entry, entry)) {
+                /* duplicate detected! The @entry key was freed by g_hash_table_insert(). */
+                continue;
             }
+            c_list_link_tail(&priv->seen_bssids_lst_head, &entry->seen_bssids_lst);
+            result_len++;
         }
+        if (result_len > 0) {
+            _LOGT("read %u seen-bssids from keyfile database \"%s\"",
+                  result_len,
+                  nm_key_file_db_get_filename(priv->kf_db_seen_bssids));
+        } else
+            nm_clear_pointer(&priv->seen_bssids_hash, g_hash_table_destroy);
+
+        nm_assert(nm_g_hash_table_size(priv->seen_bssids_hash) == result_len);
+        nm_assert(result_len <= SEEN_BSSIDS_MAX);
     }
 }
 
-/**
- * nm_settings_connection_get_seen_bssids:
- * @self: the #NMSettingsConnection
- *
- * Returns current list of seen BSSIDs for the connection.
- *
- * Returns: (transfer container) list of seen BSSIDs (in the standard hex-digits-and-colons notation).
- * The caller is responsible for freeing the list, but not the content.
- **/
-const char **
-nm_settings_connection_get_seen_bssids(NMSettingsConnection *self)
+static guint
+_get_seen_bssids(NMSettingsConnection *self, const char *strv_buf[static(SEEN_BSSIDS_MAX + 1)])
 {
-    g_return_val_if_fail(NM_IS_SETTINGS_CONNECTION(self), NULL);
+    NMSettingsConnectionPrivate *priv = NM_SETTINGS_CONNECTION_GET_PRIVATE(self);
+    SeenBssidEntry *             entry;
+    guint                        i;
 
-    return nm_utils_strdict_get_keys(NM_SETTINGS_CONNECTION_GET_PRIVATE(self)->seen_bssids,
-                                     TRUE,
-                                     NULL);
+    i = 0;
+    c_list_for_each_entry (entry, &priv->seen_bssids_lst_head, seen_bssids_lst) {
+        nm_assert(i <= SEEN_BSSIDS_MAX);
+        strv_buf[i++] = entry->bssid;
+    }
+    strv_buf[i] = NULL;
+    return i;
 }
 
 /**
@@ -2379,14 +2425,17 @@ gboolean
 nm_settings_connection_has_seen_bssid(NMSettingsConnection *self, const char *bssid)
 {
     NMSettingsConnectionPrivate *priv;
+    NMEtherAddr                  addr_bin;
 
     g_return_val_if_fail(NM_IS_SETTINGS_CONNECTION(self), FALSE);
     g_return_val_if_fail(bssid, FALSE);
+    nm_assert(_nm_utils_hwaddr_aton_exact(bssid, &addr_bin, sizeof(addr_bin)));
 
     priv = NM_SETTINGS_CONNECTION_GET_PRIVATE(self);
 
-    return priv->seen_bssids
-           && g_hash_table_contains(NM_SETTINGS_CONNECTION_GET_PRIVATE(self)->seen_bssids, bssid);
+    return priv->seen_bssids_hash
+           && g_hash_table_contains(NM_SETTINGS_CONNECTION_GET_PRIVATE(self)->seen_bssids_hash,
+                                    bssid);
 }
 
 /**
@@ -2401,15 +2450,47 @@ void
 nm_settings_connection_add_seen_bssid(NMSettingsConnection *self, const char *seen_bssid)
 {
     NMSettingsConnectionPrivate *priv = NM_SETTINGS_CONNECTION_GET_PRIVATE(self);
-    gs_free const char **        strv = NULL;
+    const char *                 seen_bssids_strv[SEEN_BSSIDS_MAX + 1];
+    NMEtherAddr                  addr_bin;
     const char *                 connection_uuid;
+    SeenBssidEntry               entry_stack;
+    SeenBssidEntry *             entry;
+    guint                        i;
+
+    g_return_if_fail(seen_bssid);
 
-    g_return_if_fail(seen_bssid != NULL);
+    if (!_nm_utils_hwaddr_aton_exact(seen_bssid, &addr_bin, sizeof(addr_bin)))
+        g_return_if_reached();
 
-    if (!priv->seen_bssids)
-        priv->seen_bssids = _seen_bssids_hash_new();
+    _seen_bssid_entry_init_stale(&entry_stack, &addr_bin);
 
-    g_hash_table_add(priv->seen_bssids, g_strdup(seen_bssid));
+    if (!priv->seen_bssids_hash) {
+        priv->seen_bssids_hash = _seen_bssids_hash_new();
+        entry                  = NULL;
+    } else
+        entry = g_hash_table_lookup(priv->seen_bssids_hash, &entry_stack);
+
+    if (entry) {
+        if (!nm_c_list_move_front(&priv->seen_bssids_lst_head, &entry->seen_bssids_lst)) {
+            /* no change. */
+            return;
+        }
+    } else {
+        entry = _seen_bssid_entry_new_stale_copy(&entry_stack);
+        c_list_link_front(&priv->seen_bssids_lst_head, &entry->seen_bssids_lst);
+        if (!g_hash_table_add(priv->seen_bssids_hash, entry))
+            nm_assert_not_reached();
+
+        if (g_hash_table_size(priv->seen_bssids_hash) > SEEN_BSSIDS_MAX) {
+            g_hash_table_remove(
+                priv->seen_bssids_hash,
+                c_list_last_entry(&priv->seen_bssids_lst_head, SeenBssidEntry, seen_bssids_lst));
+        }
+    }
+
+    nm_assert(g_hash_table_size(priv->seen_bssids_hash) <= SEEN_BSSIDS_MAX);
+    nm_assert(g_hash_table_size(priv->seen_bssids_hash)
+              == c_list_length(&priv->seen_bssids_lst_head));
 
     if (!priv->kf_db_seen_bssids)
         return;
@@ -2418,12 +2499,8 @@ nm_settings_connection_add_seen_bssid(NMSettingsConnection *self, const char *se
     if (!connection_uuid)
         return;
 
-    strv = nm_utils_strdict_get_keys(priv->seen_bssids, TRUE, NULL);
-
-    nm_key_file_db_set_string_list(priv->kf_db_seen_bssids,
-                                   connection_uuid,
-                                   strv ?: NM_PTRARRAY_EMPTY(const char *),
-                                   -1);
+    i = _get_seen_bssids(self, seen_bssids_strv);
+    nm_key_file_db_set_string_list(priv->kf_db_seen_bssids, connection_uuid, seen_bssids_strv, i);
 }
 
 /*****************************************************************************/
@@ -2634,7 +2711,7 @@ nm_settings_connection_init(NMSettingsConnection *self)
     self->_priv = priv;
 
     c_list_init(&self->_connections_lst);
-
+    c_list_init(&priv->seen_bssids_lst_head);
     c_list_init(&priv->call_ids_lst_head);
     c_list_init(&priv->auth_lst_head);
 
@@ -2672,7 +2749,7 @@ dispose(GObject *object)
 
     nm_clear_pointer(&priv->agent_secrets, g_variant_unref);
 
-    nm_clear_pointer(&priv->seen_bssids, g_hash_table_destroy);
+    nm_clear_pointer(&priv->seen_bssids_hash, g_hash_table_destroy);
 
     g_clear_object(&priv->agent_mgr);
 
diff --git a/src/core/settings/nm-settings-connection.h b/src/core/settings/nm-settings-connection.h
index 83a6a7f6..fa3dbcfb 100644
--- a/src/core/settings/nm-settings-connection.h
+++ b/src/core/settings/nm-settings-connection.h
@@ -340,8 +340,6 @@ gboolean nm_settings_connection_get_timestamp(NMSettingsConnection *self, guint6
 
 void nm_settings_connection_update_timestamp(NMSettingsConnection *self, guint64 timestamp);
 
-const char **nm_settings_connection_get_seen_bssids(NMSettingsConnection *self);
-
 gboolean nm_settings_connection_has_seen_bssid(NMSettingsConnection *self, const char *bssid);
 
 void nm_settings_connection_add_seen_bssid(NMSettingsConnection *self, const char *seen_bssid);
diff --git a/src/core/settings/nm-settings.c b/src/core/settings/nm-settings.c
index c876ea14..f9f98de7 100644
--- a/src/core/settings/nm-settings.c
+++ b/src/core/settings/nm-settings.c
@@ -388,6 +388,9 @@ typedef struct {
     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;
+
     bool started : 1;
 
     /* Whether NMSettingsConnections changed in a way that affects the comparison
@@ -495,6 +498,8 @@ _startup_complete_check_is_ready(NMSettings *          self,
     conn = nm_settings_connection_get_connection(sett_conn);
 
     nm_manager_for_each_device (priv->manager, device, tmp_lst) {
+        gs_free_error GError *error = NULL;
+
         if (!nm_device_is_real(device))
             continue;
 
@@ -505,7 +510,13 @@ _startup_complete_check_is_ready(NMSettings *          self,
             continue;
         }
 
-        if (!nm_device_check_connection_compatible(device, conn, NULL))
+        /* Check that device is compatible with the device. We are also happy
+         * with a device compatible but for which the connection is disallowed
+         * by NM configuration. */
+        if (!nm_device_check_connection_compatible(device, conn, &error)
+            && !g_error_matches(error,
+                                NM_UTILS_ERROR,
+                                NM_UTILS_ERROR_CONNECTION_AVAILABLE_DISALLOWED))
             continue;
 
         return TRUE;
@@ -3684,6 +3695,41 @@ again:
 
 /*****************************************************************************/
 
+static gboolean
+_kf_db_prune_predicate(const char *uuid, gpointer user_data)
+{
+    return !!nm_settings_get_connection_by_uuid(user_data, uuid);
+}
+
+static void
+_kf_db_to_file(NMSettings *self, gboolean is_timestamps, gboolean force_write)
+{
+    NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE(self);
+    NMKeyFileDB *      kf_db;
+    bool *             p_kf_db_pruned;
+
+    if (is_timestamps) {
+        kf_db          = priv->kf_db_timestamps;
+        p_kf_db_pruned = &priv->kf_db_pruned_timestamps;
+    } else {
+        kf_db          = priv->kf_db_seen_bssids;
+        p_kf_db_pruned = &priv->kf_db_pruned_seen_bssid;
+    }
+
+    if (!*p_kf_db_pruned) {
+        /* we only prune the DB once, because afterwards every
+         * add/remove of an connection will lead to a direct update. */
+        *p_kf_db_pruned = TRUE;
+        nm_key_file_db_prune(kf_db, _kf_db_prune_predicate, self);
+
+        /* once we also go over the directory, and see whether we
+         * have any left over temporary files to delete. */
+        nm_key_file_db_prune_tmp_files(kf_db);
+    }
+
+    nm_key_file_db_to_file(kf_db, force_write);
+}
+
 G_GNUC_PRINTF(4, 5)
 static void
 _kf_db_log_fcn(NMKeyFileDB *kf_db, int syslog_level, gpointer user_data, const char *fmt, ...)
@@ -3732,7 +3778,7 @@ _kf_db_got_dirty_flush(NMSettings *self, gboolean is_timestamps)
     }
 
     if (nm_key_file_db_is_dirty(kf_db))
-        nm_key_file_db_to_file(kf_db, FALSE);
+        _kf_db_to_file(self, is_timestamps, FALSE);
     else {
         _LOGT("[%s-keyfile]: skip saving changes to \"%s\"",
               prefix,
@@ -3785,15 +3831,10 @@ _kf_db_got_dirty_fcn(NMKeyFileDB *kf_db, gpointer user_data)
 void
 nm_settings_kf_db_write(NMSettings *self)
 {
-    NMSettingsPrivate *priv;
-
     g_return_if_fail(NM_IS_SETTINGS(self));
 
-    priv = NM_SETTINGS_GET_PRIVATE(self);
-    if (priv->kf_db_timestamps)
-        nm_key_file_db_to_file(priv->kf_db_timestamps, TRUE);
-    if (priv->kf_db_seen_bssids)
-        nm_key_file_db_to_file(priv->kf_db_seen_bssids, TRUE);
+    _kf_db_to_file(self, TRUE, TRUE);
+    _kf_db_to_file(self, FALSE, TRUE);
 }
 
 /*****************************************************************************/
@@ -4031,8 +4072,8 @@ finalize(GObject *object)
 
     nm_clear_g_source(&priv->kf_db_flush_idle_id_timestamps);
     nm_clear_g_source(&priv->kf_db_flush_idle_id_seen_bssids);
-    nm_key_file_db_to_file(priv->kf_db_timestamps, FALSE);
-    nm_key_file_db_to_file(priv->kf_db_seen_bssids, FALSE);
+    _kf_db_to_file(self, TRUE, FALSE);
+    _kf_db_to_file(self, FALSE, FALSE);
     nm_key_file_db_destroy(priv->kf_db_timestamps);
     nm_key_file_db_destroy(priv->kf_db_seen_bssids);
 
diff --git a/src/core/settings/plugins/ifcfg-rh/nm-ifdown b/src/core/settings/plugins/ifcfg-rh/nm-ifdown
index e7bd73ae..3b05ef1c 100644..100755
--- a/src/core/settings/plugins/ifcfg-rh/nm-ifdown
+++ b/src/core/settings/plugins/ifcfg-rh/nm-ifdown
@@ -1,3 +1,44 @@
-#!/bin/sh
-nmcli connection load "/etc/sysconfig/network-scripts/ifcfg-$1" &&
-exec nmcli connection down filename "/etc/sysconfig/network-scripts/ifcfg-$1"
+#!/bin/bash
+
+CONFIG="$1"
+
+if [ -z "${CONFIG}" ] ; then
+    echo $"Usage: ifdown <device name>" >&2
+    exit 1
+fi
+
+usage_and_fail() {
+    cat <<EOF >&2
+$1!
+
+See all profiles with \`nmcli connection\`.
+Reload files from disk with \`nmcli connection reload\`
+Deactivate the desired profile with \`nmcli connection down \"\$NAME\"\`
+EOF
+    exit 1
+}
+
+OLD_IFS="$IFS"
+
+re='^/etc/sysconfig/network-scripts/ifcfg-[^/]+$'
+for f in "/etc/sysconfig/network-scripts/ifcfg-$CONFIG" "/etc/sysconfig/network-scripts/$CONFIG" ; do
+    [[ "$f" =~ $re ]] || continue
+    [ -f "$f" ] || continue
+
+    nmcli connection load "$f" \
+    && nmcli connection down filename "$f" \
+    && exit 0
+
+    usage_and_fail "Failure to deactivate file \"$CONFIG\""
+done
+
+IFS=$'\n '
+APATHS=( $(nmcli -g GENERAL.DBUS-PATH connection show --active "$CONFIG" 2>/dev/null) )
+IFS="$OLD_IFS"
+if [ "${#APATHS[@]}" -eq 1 ] ; then
+    nmcli connection down apath "${APATHS[0]}" \
+    && exit 0
+    usage_and_fail "Failure to deactivate profile \"$CONFIG\" (apath ${APATHS[@]})"
+fi
+
+usage_and_fail "No suitable profile \"$CONFIG\" found"
diff --git a/src/core/settings/plugins/ifcfg-rh/nm-ifup b/src/core/settings/plugins/ifcfg-rh/nm-ifup
index 96637a92..6b32fe1b 100644..100755
--- a/src/core/settings/plugins/ifcfg-rh/nm-ifup
+++ b/src/core/settings/plugins/ifcfg-rh/nm-ifup
@@ -1,3 +1,44 @@
-#!/bin/sh
-nmcli connection load "/etc/sysconfig/network-scripts/ifcfg-$1" &&
-exec nmcli connection up filename "/etc/sysconfig/network-scripts/ifcfg-$1"
+#!/bin/bash
+
+CONFIG="$1"
+
+if [ -z "${CONFIG}" ] ; then
+    echo $"Usage: ifup <device name>" >&2
+    exit 1
+fi
+
+usage_and_fail() {
+    cat <<EOF >&2
+$1!
+
+See all profiles with \`nmcli connection\`.
+Reload files from disk with \`nmcli connection reload\`
+Activate the desired profile with \`nmcli connection up \"\$NAME\"\`
+EOF
+    exit 1
+}
+
+OLD_IFS="$IFS"
+
+re='^/etc/sysconfig/network-scripts/ifcfg-[^/]+$'
+for f in "/etc/sysconfig/network-scripts/ifcfg-$CONFIG" "/etc/sysconfig/network-scripts/$CONFIG" ; do
+    [[ "$f" =~ $re ]] || continue
+    [ -f "$f" ] || continue
+
+    nmcli connection load "$f" \
+    && nmcli connection up filename "$f" \
+    && exit 0
+
+    usage_and_fail "Failure to activate file \"$CONFIG\""
+done
+
+IFS=$'\n '
+UUIDS=( $(nmcli -g connection.uuid connection show "$CONFIG" 2>/dev/null) )
+IFS="$OLD_IFS"
+if [ "${#UUIDS[@]}" -eq 1 ] ; then
+    nmcli connection up uuid "${UUIDS[0]}" \
+    && exit 0
+    usage_and_fail "Failure to activate profile \"$CONFIG\" (uuid ${UUIDS[0]})"
+fi
+
+usage_and_fail "No suitable profile \"$CONFIG\" found"
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 24d67003..03b347f9 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
@@ -1895,6 +1895,8 @@ make_ip4_setting(shvarFile *ifcfg,
                  svGetValueBoolean(ifcfg, "DHCP_SEND_HOSTNAME", TRUE),
                  NM_SETTING_IP_CONFIG_DHCP_TIMEOUT,
                  (int) svGetValueInt64(ifcfg, "IPV4_DHCP_TIMEOUT", 10, 0, G_MAXINT32, 0),
+                 NM_SETTING_IP_CONFIG_REQUIRED_TIMEOUT,
+                 (int) svGetValueInt64(ifcfg, "IPV4_REQUIRED_TIMEOUT", 10, 0, G_MAXINT32, -1),
                  NULL);
 
     nm_clear_g_free(&value);
@@ -1985,7 +1987,11 @@ make_ip4_setting(shvarFile *ifcfg,
                 } else if (nm_utils_ipaddr_is_valid(AF_INET6, v)) {
                     /* Ignore IPv6 addresses */
                 } else {
-                    PARSE_WARNING("invalid DNS server address %s", v);
+                    g_set_error(error,
+                                NM_SETTINGS_ERROR,
+                                NM_SETTINGS_ERROR_INVALID_CONNECTION,
+                                "Invalid DNS server address '%s'",
+                                v);
                     return NULL;
                 }
             }
@@ -2152,6 +2158,8 @@ read_aliases(NMSettingIPConfig *s_ip4, gboolean read_defroute, const char *filen
                 continue;
             }
 
+            svWarnInvalid(parsed, "alias", _NMLOG_DOMAIN);
+
             device = svGetValueStr(parsed, "DEVICE", &device_value);
             if (!device) {
                 PARSE_WARNING("alias file '%s' has no DEVICE", full_path);
@@ -2403,6 +2411,8 @@ make_ip6_setting(shvarFile *ifcfg, shvarFile *network_ifcfg, gboolean routes_rea
                  svGetValueBoolean(ifcfg, "DHCPV6_SEND_HOSTNAME", TRUE),
                  NM_SETTING_IP_CONFIG_DHCP_TIMEOUT,
                  (int) svGetValueInt64(ifcfg, "IPV6_DHCP_TIMEOUT", 10, 0, G_MAXINT32, 0),
+                 NM_SETTING_IP_CONFIG_REQUIRED_TIMEOUT,
+                 (int) svGetValueInt64(ifcfg, "IPV6_REQUIRED_TIMEOUT", 10, 0, G_MAXINT32, -1),
                  NM_SETTING_IP6_CONFIG_RA_TIMEOUT,
                  (int) svGetValueInt64(ifcfg, "IPV6_RA_TIMEOUT", 10, 0, G_MAXINT32, 0),
                  NULL);
@@ -2504,7 +2514,11 @@ make_ip6_setting(shvarFile *ifcfg, shvarFile *network_ifcfg, gboolean routes_rea
         } else if (nm_utils_ipaddr_is_valid(AF_INET, v)) {
             /* Ignore IPv4 addresses */
         } else {
-            PARSE_WARNING("invalid DNS server address %s", v);
+            g_set_error(error,
+                        NM_SETTINGS_ERROR,
+                        NM_SETTINGS_ERROR_INVALID_CONNECTION,
+                        "Invalid DNS server address '%s'",
+                        v);
             return NULL;
         }
     }
@@ -6298,6 +6312,7 @@ connection_from_file_full(const char *filename,
     NMSetting *                   s_ip4;
     NMSetting *                   s_ip6;
     const char *                  ifcfg_name       = NULL;
+    gs_free char *                s_tmp            = NULL;
     gboolean                      has_ip4_defroute = FALSE;
     gboolean                      has_complex_routes_v4;
     gboolean                      has_complex_routes_v6;
@@ -6325,8 +6340,6 @@ connection_from_file_full(const char *filename,
     if (!main_ifcfg)
         return NULL;
 
-    network_ifcfg = svOpenFile(network_file, NULL);
-
     if (!svGetValueBoolean(main_ifcfg, "NM_CONTROLLED", TRUE)) {
         connection = create_unhandled_connection(filename, main_ifcfg, "unmanaged", out_unhandled);
         if (!connection) {
@@ -6340,6 +6353,16 @@ connection_from_file_full(const char *filename,
         return g_steal_pointer(&connection);
     }
 
+    if (NM_IN_STRSET(svGetValueStr(main_ifcfg, "DEVICE", &s_tmp), "lo")) {
+        /* "lo" is not handled by NetworkManager and we ignore it. */
+    } else
+        svWarnInvalid(main_ifcfg, "ifcfg", _NMLOG_DOMAIN);
+    nm_clear_g_free(&s_tmp);
+
+    network_ifcfg = svOpenFile(network_file, NULL);
+    /* we don't call svWarnInvalid(network_ifcfg), because we will load this file for
+     * every profile. So we would get a large number of duplicate warnings. */
+
     /* iBFT is handled by nm-initrd-generator during boot. */
     bootproto = svGetValueStr_cp(main_ifcfg, "BOOTPROTO");
     if (bootproto && !g_ascii_strcasecmp(bootproto, "ibft")) {
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 6f6035e1..febfc120 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
@@ -958,6 +958,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_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),
     _KEY_TYPE("IPV6ADDR", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
@@ -980,6 +981,7 @@ const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[] = {
     _KEY_TYPE("IPV6_PRIVACY", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV6_PRIVACY_PREFER_PUBLIC_IP", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV6_RA_TIMEOUT", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
+    _KEY_TYPE("IPV6_REQUIRED_TIMEOUT", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV6_RES_OPTIONS", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV6_ROUTE_METRIC", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IPV6_ROUTE_TABLE", 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 b61ce80a..b7751ec9 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[249];
+extern const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[251];
 
 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 1c5bbbb4..ef85b6bf 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
@@ -2840,6 +2840,9 @@ write_ip4_setting(NMConnection *connection,
     timeout = nm_setting_ip_config_get_dhcp_timeout(s_ip4);
     svSetValueInt64_cond(ifcfg, "IPV4_DHCP_TIMEOUT", timeout != 0, timeout);
 
+    timeout = nm_setting_ip_config_get_required_timeout(s_ip4);
+    svSetValueInt64_cond(ifcfg, "IPV4_REQUIRED_TIMEOUT", timeout != -1, timeout);
+
     svSetValueBoolean(ifcfg, "IPV4_FAILURE_FATAL", !nm_setting_ip_config_get_may_fail(s_ip4));
 
     route_metric = nm_setting_ip_config_get_route_metric(s_ip4);
@@ -3037,6 +3040,9 @@ write_ip6_setting(NMConnection *connection,
     timeout = nm_setting_ip_config_get_dhcp_timeout(s_ip6);
     svSetValueInt64_cond(ifcfg, "IPV6_DHCP_TIMEOUT", timeout != 0, timeout);
 
+    timeout = nm_setting_ip_config_get_required_timeout(s_ip6);
+    svSetValueInt64_cond(ifcfg, "IPV6_REQUIRED_TIMEOUT", timeout != -1, timeout);
+
     flags = nm_setting_ip_config_get_dhcp_hostname_flags(s_ip6);
     svSetValueInt64_cond(ifcfg,
                          "DHCPV6_HOSTNAME_FLAGS",
diff --git a/src/core/settings/plugins/ifcfg-rh/shvar.c b/src/core/settings/plugins/ifcfg-rh/shvar.c
index 80644b64..d82efb3c 100644
--- a/src/core/settings/plugins/ifcfg-rh/shvar.c
+++ b/src/core/settings/plugins/ifcfg-rh/shvar.c
@@ -172,7 +172,7 @@ _escape_ansic(const char *source)
             n_alloc += 2;
             break;
         default:
-            if ((*p < ' ') || (*p >= 0177))
+            if (!nm_ascii_is_regular(*p))
                 n_alloc += 4;
             else
                 n_alloc += 1;
@@ -221,7 +221,7 @@ _escape_ansic(const char *source)
             *q++ = *p;
             break;
         default:
-            if ((*p < ' ') || (*p >= 0177)) {
+            if (!nm_ascii_is_regular(*p)) {
                 *q++ = '\\';
                 *q++ = '0' + (((*p) >> 6) & 07);
                 *q++ = '0' + (((*p) >> 3) & 07);
@@ -255,20 +255,37 @@ svEscape(const char *s, char **to_free)
     gsize    slen;
     gsize    i;
     gsize    j;
+    gboolean all_ascii = TRUE;
 
     for (slen = 0; s[slen]; slen++) {
         if (_char_req_escape(s[slen]))
             mangle++;
         else if (_char_req_quotes(s[slen]))
             requires_quotes = TRUE;
-        else if (s[slen] < ' ') {
-            /* if the string contains newline we can only express it using ANSI C quotation
-             * (as we don't support line continuation).
-             * Additionally, ANSI control characters look odd with regular quotation, so handle
-             * them too. */
-            return (*to_free = _escape_ansic(s));
+        else if (!nm_ascii_is_regular(s[slen])) {
+            if (nm_ascii_is_ctrl_or_del(s[slen])) {
+                /* if the string contains newline we can only express it using ANSI C quotation
+                 * (as we don't support line continuation).
+                 * Additionally, ANSI control characters look odd with regular quotation, so handle
+                 * them too. */
+                return (*to_free = _escape_ansic(s));
+            }
+            all_ascii       = FALSE;
+            requires_quotes = TRUE;
         }
     }
+
+    if (!all_ascii && !g_utf8_validate(s, -1, NULL)) {
+        /* The string is not valid ASCII/UTF-8. We can escape that via
+         * _escape_ansic(), however the reader might have a problem to
+         * do something sensible with the blob later.
+         *
+         * This is really a bug of the caller, which should not present us with
+         * non-text in the first place. But at this place, we cannot handle the
+         * error better, so just escape it. */
+        return (*to_free = _escape_ansic(s));
+    }
+
     if (!mangle && !requires_quotes) {
         *to_free = NULL;
         return s;
@@ -372,6 +389,12 @@ _strbuf_init(NMStrBuf *str, const char *value, gsize i)
 const char *
 svUnescape(const char *value, char **to_free)
 {
+    return svUnescape_full(value, to_free, TRUE);
+}
+
+const char *
+svUnescape_full(const char *value, char **to_free, gboolean check_utf8)
+{
     NMStrBuf str                      = NM_STR_BUF_INIT(0, FALSE);
     int      looks_like_old_svescaped = -1;
     gsize    i;
@@ -646,6 +669,8 @@ out_value:
     }
 
     if (str.allocated > 0) {
+        if (check_utf8 && !nm_str_buf_utf8_validate(&str))
+            goto out_error;
         if (str.len == 0 || nm_str_buf_get_str_unsafe(&str)[0] == '\0') {
             nm_str_buf_destroy(&str);
             *to_free = NULL;
@@ -656,6 +681,11 @@ out_value:
         }
     }
 
+    if (check_utf8 && !g_utf8_validate(value, i, NULL)) {
+        *to_free = NULL;
+        return NULL;
+    }
+
     if (value[i] != '\0') {
         *to_free = g_strndup(value, i);
         return *to_free;
@@ -1120,9 +1150,8 @@ _svGetValue(shvarFile *s, const char *key, char **to_free)
     if (line && line->line) {
         v = svUnescape(line->line, to_free);
         if (!v) {
-            /* a wrongly quoted value is treated like the empty string.
-             * See also svWriteFile(), which handles unparsable values
-             * that way. */
+            /* a wrongly quoted value or non-UTF-8 is treated like the empty string.
+             * See also svWriteFile(), which handles unparsable values that way. */
             nm_assert(!*to_free);
             return "";
         }
@@ -1495,6 +1524,91 @@ svUnsetValue(shvarFile *s, const char *key)
 
 /*****************************************************************************/
 
+void
+svWarnInvalid(shvarFile *s, const char *file_type, NMLogDomain log_domain)
+{
+    shvarLine *line;
+    gsize      n;
+
+    if (!nm_logging_enabled(LOGL_WARN, log_domain))
+        return;
+
+    n = 0;
+    c_list_for_each_entry (line, &s->lst_head, lst) {
+        gs_free char *s_tmp = NULL;
+
+        n++;
+
+        if (!line->key) {
+            const char *str;
+
+            nm_assert(line->line);
+            str = nm_str_skip_leading_spaces(line->line);
+            if (!NM_IN_SET(str[0], '\0', '#')) {
+                nm_log_warn(log_domain,
+                            "ifcfg-rh: %s,%s:%zu: invalid line ignored",
+                            file_type,
+                            s->fileName,
+                            n);
+            }
+            continue;
+        }
+
+        if (g_hash_table_lookup(s->lst_idx, line) != line) {
+            nm_log_warn(
+                log_domain,
+                "ifcfg-rh: %s,%s:%zu: key %s is duplicated and the early occurrence ignored",
+                file_type,
+                s->fileName,
+                n,
+                line->key);
+            continue;
+        }
+
+        if (!line->line) {
+            /* the line is deleted via svUnsetValue(). Ignore. */
+            continue;
+        }
+
+        if (!svUnescape(line->line, &s_tmp)) {
+            if (!svUnescape_full(line->line, &s_tmp, FALSE)) {
+                nm_log_warn(log_domain,
+                            "ifcfg-rh: %s,%s:%zu: key %s is badly quoted and is treated as \"\"",
+                            file_type,
+                            s->fileName,
+                            n,
+                            line->key);
+            } else {
+                nm_log_warn(log_domain,
+                            "ifcfg-rh: %s,%s:%zu: key %s does not contain valid UTF-8 and is "
+                            "treated as \"\"",
+                            file_type,
+                            s->fileName,
+                            n,
+                            line->key);
+            }
+            continue;
+        }
+
+        /* TODO: we read different shell scripts, and whether a key is recognized
+         * depends on the type. For example, alias files only accept a subset of
+         * known keys.
+         *
+         * Basically, depending on the @file_type, different keys are valid. */
+        if (!nms_ifcfg_rh_utils_is_well_known_key(line->key)) {
+            nm_log_dbg(log_domain,
+                       "ifcfg-rh: %s,%s:%zu: key %s is unknown and ignored",
+                       file_type,
+                       s->fileName,
+                       n,
+                       line->key);
+            continue;
+        }
+    }
+}
+
+/*****************************************************************************/
+
 /* Write the current contents iff modified.  Returns FALSE on error
  * and TRUE on success.  Do not write if no values have been modified.
  * The mode argument is only used if creating the file, not if
diff --git a/src/core/settings/plugins/ifcfg-rh/shvar.h b/src/core/settings/plugins/ifcfg-rh/shvar.h
index 6965d873..cf91642f 100644
--- a/src/core/settings/plugins/ifcfg-rh/shvar.h
+++ b/src/core/settings/plugins/ifcfg-rh/shvar.h
@@ -107,6 +107,7 @@ void svCloseFile(shvarFile *s);
 
 const char *svEscape(const char *s, char **to_free);
 const char *svUnescape(const char *s, char **to_free);
+const char *svUnescape_full(const char *value, char **to_free, gboolean check_utf8);
 
 static inline void
 _nm_auto_shvar_file_close(shvarFile **p_s)
@@ -120,4 +121,6 @@ _nm_auto_shvar_file_close(shvarFile **p_s)
 }
 #define nm_auto_shvar_file_close nm_auto(_nm_auto_shvar_file_close)
 
+void svWarnInvalid(shvarFile *s, const char *file_type, NMLogDomain log_domain);
+
 #endif /* _SHVAR_H */
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-wpa-eap-suite-b-192-tls b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-wpa-eap-suite-b-192-tls
index 9a74bb4d..a21c301c 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-wpa-eap-suite-b-192-tls
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-wpa-eap-suite-b-192-tls
@@ -3,7 +3,6 @@ DEVICE=eth2
 HWADDR=00:16:41:11:22:33
 BOOTPROTO=dhcp
 ONBOOT=yes
-ONBOOT=yes
 USERCTL=yes
 IPV6INIT=no
 NM_CONTROLLED=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-wpa-eap-ttls-tls b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-wpa-eap-ttls-tls
index 42ed1d68..bb63d5e8 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-wpa-eap-ttls-tls
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-wpa-eap-ttls-tls
@@ -4,7 +4,6 @@ DEVICE=eth2
 HWADDR=00:16:41:11:22:33
 BOOTPROTO=dhcp
 ONBOOT=yes
-ONBOOT=yes
 USERCTL=yes
 IPV6INIT=no
 NM_CONTROLLED=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-write-unknown-4.expected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-write-unknown-4.expected
index 040ddc9d..92c03b12 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-write-unknown-4.expected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-write-unknown-4.expected
@@ -13,8 +13,10 @@
 #L2
 
 METRIC1=''
-METRIC2=$'\U0x'
-METRIC3=$'x\U0'
+METRIC2=
+#NM: METRIC2=$'\U0x'
+METRIC3=
+#NM: METRIC3=$'x\U0'
 
 #L4
 IPADDR=set-by-test1
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 6193b952..d9829f84 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
@@ -188,8 +188,8 @@ _assert_expected_content(NMConnection *connection, const char *filename, const c
         if (G_UNLIKELY(rewrite == 0)) {
             rewrite = (g_getenv("NMTST_IFCFG_RH_UPDATE_EXPECTED")
                        || nm_streq0(g_getenv("NM_TEST_REGENERATE"), "1"))
-                          ? -1
-                          : 1;
+                          ? 1
+                          : -1;
             if (!g_atomic_int_compare_and_exchange(&rewrite_static, 0, rewrite))
                 g_assert_not_reached();
         }
@@ -738,10 +738,13 @@ test_read_variables_corner_cases(void)
     const char *         mac;
     char                 expected_mac_address[ETH_ALEN] = {0x00, 0x16, 0x41, 0x11, 0x22, 0x33};
 
+    NMTST_EXPECT_NM_WARN("*key NAME is badly quoted and is treated as \"\"*");
+    NMTST_EXPECT_NM_WARN("*key ZONE is badly quoted and is treated as \"\"*");
     connection = _connection_from_file(TEST_IFCFG_DIR "/ifcfg-test-variables-corner-cases-1",
                                        NULL,
                                        TYPE_ETHERNET,
                                        NULL);
+    g_test_assert_expected_messages();
 
     /* ===== CONNECTION SETTING ===== */
     s_con = nm_connection_get_setting_connection(connection);
@@ -830,10 +833,12 @@ test_read_unrecognized(void)
     gs_free char *       unhandled_spec     = NULL;
     guint64              expected_timestamp = 0;
 
+    NMTST_EXPECT_NM_WARN("*key NAME is badly quoted and is treated as \"\"*");
     connection = _connection_from_file(TEST_IFCFG_DIR "/ifcfg-test-unrecognized",
                                        NULL,
                                        NULL,
                                        &unhandled_spec);
+    g_test_assert_expected_messages();
     g_assert_cmpstr(unhandled_spec, ==, "unrecognized:mac:00:11:22:33");
 
     /* ===== CONNECTION SETTING ===== */
@@ -1004,10 +1009,12 @@ test_read_wired_dhcp(void)
     char                 expected_mac_address[ETH_ALEN] = {0x00, 0x11, 0x22, 0x33, 0x44, 0xee};
     const char *         mac;
 
+    NMTST_EXPECT_NM_WARN("*key IPV6INIT is duplicated and the early occurrence ignored*");
     connection = _connection_from_file(TEST_IFCFG_DIR "/ifcfg-test-wired-dhcp",
                                        NULL,
                                        TYPE_ETHERNET,
                                        &unmanaged);
+    g_test_assert_expected_messages();
     g_assert(unmanaged == NULL);
 
     /* ===== CONNECTION SETTING ===== */
@@ -3583,10 +3590,12 @@ test_read_wifi_wpa_eap_tls(void)
     char *             unmanaged                 = NULL;
     const char *       expected_privkey_password = "test1";
 
+    NMTST_EXPECT_NM_WARN("*key ONBOOT is duplicated and the early occurrence ignored*");
     connection = _connection_from_file(TEST_IFCFG_DIR "/ifcfg-test-wifi-wpa-eap-tls",
                                        NULL,
                                        TYPE_ETHERNET,
                                        &unmanaged);
+    g_test_assert_expected_messages();
     g_assert(!unmanaged);
 
     /* ===== WIRELESS SETTING ===== */
@@ -3791,10 +3800,12 @@ test_read_wifi_wep_eap_ttls_chap(void)
     NMSetting8021x *           s_8021x;
     char *                     unmanaged = NULL;
 
+    NMTST_EXPECT_NM_WARN("*key ONBOOT is duplicated and the early occurrence ignored*");
     connection = _connection_from_file(TEST_IFCFG_DIR "/ifcfg-test-wifi-wep-eap-ttls-chap",
                                        NULL,
                                        TYPE_WIRELESS,
                                        &unmanaged);
+    g_test_assert_expected_messages();
     g_assert(!unmanaged);
 
     /* ===== WIRELESS SETTING ===== */
@@ -10476,7 +10487,7 @@ _svUnescape(const char *str, char **to_free)
         str = (str_free = g_strdup(str));
     }
 
-    s = svUnescape(str, to_free);
+    s = svUnescape_full(str, to_free, FALSE);
     if (*to_free) {
         g_assert(s == *to_free);
         g_assert(s[0]);
@@ -10484,6 +10495,37 @@ _svUnescape(const char *str, char **to_free)
         g_assert(s == NULL || (!s[0] && (s < str || s > strchr(str, '\0')))
                  || (s[0] && s >= str && s <= strchr(str, '\0')));
     }
+
+    {
+        const char *  s2;
+        gs_free char *to_free2 = NULL;
+
+        gboolean is_utf8 = s && g_utf8_validate(s, -1, NULL);
+
+        s2 = svUnescape_full(str, &to_free2, TRUE);
+        if (NM_IN_STRSET(str, "$'\\U0x'", "$'\\x0'", "$'\\008'", "$'\\08'")) {
+            g_assert_cmpstr(s2, ==, NULL);
+            g_assert(!to_free2);
+            g_assert_cmpstr(s, ==, "");
+            g_assert(!*to_free);
+        } else if (NM_IN_STRSET(str, "$'x\\U0'")) {
+            g_assert_cmpstr(s2, ==, NULL);
+            g_assert(!to_free2);
+            g_assert_cmpstr(s, ==, "x");
+            g_assert(*to_free == s);
+        } else if (!is_utf8) {
+            g_assert(!s2);
+            g_assert(!to_free2);
+        } else if (!to_free2) {
+            g_assert_cmpstr(s, ==, s2);
+            g_assert(s == s2);
+        } else {
+            g_assert_cmpstr(s, ==, s2);
+            g_assert(s != s2);
+            g_assert(s2 == to_free2);
+        }
+    }
+
     return s;
 }
 
@@ -10665,6 +10707,9 @@ test_svUnescape(void)
         V1("\"\\'\"''", "\\'"),
         V0("\"b\\~b\" ", "b\\~b"),
         V1("\"b\\~b\"x", "b\\~bx"),
+
+        V0("$'x\\U0'", "x"),
+        V0("$'\\U0x'", ""),
     };
     const UnescapeTestData data_ansi[] = {
         /* strings inside $''. They cannot be compared directly, but must
@@ -10851,7 +10896,7 @@ test_write_unknown(gconstpointer test_data)
         _svGetValue_check(sv, "METRIC", NULL);
         _svGetValue_check(sv, "METRIC1", "");
         _svGetValue_check(sv, "METRIC2", "");
-        _svGetValue_check(sv, "METRIC3", "x");
+        _svGetValue_check(sv, "METRIC3", "");
 
         _svGetValue_check(sv, "IPADDR", "set-by-test1");
         _svGetValue_check(sv, "IPADDR2", "set-by-test2");
diff --git a/src/core/tests/meson.build b/src/core/tests/meson.build
index 316f20cc..99fa0ae9 100644
--- a/src/core/tests/meson.build
+++ b/src/core/tests/meson.build
@@ -44,7 +44,9 @@ exe = executable(
   link_with: [
     libnm_systemd_shared,
     libnm_systemd_core,
+    libnm_glib_aux,
     libnm_log_null,
+    libnm_std_aux,
     libc_siphash,
   ],
 )