about summary refs log tree commit diff
path: root/src/core/settings
diff options
context:
space:
mode:
authorSebastien Bacher <seb128@ubuntu.com>2021-08-25 15:24:42 +0200
committerSebastien Bacher <seb128@ubuntu.com>2021-08-25 15:24:42 +0200
commitdbb91282fa488964fb20595f9494a9f0e4f36a58 (patch)
tree142bc942e5320b35514cdf03a5f9f47a9b89df0e /src/core/settings
parent5f2ede3a2813b0e9204befdcfc67509d34be71c6 (diff)
parentcfb80376641fa49137b9996130352697e7f8b436 (diff)
Update upstream source from tag 'upstream/1.32.10'
Update to upstream version '1.32.10'
with Debian dir fcf2778b50b013ede3e7375bc1d829e984175658
Diffstat (limited to 'src/core/settings')
-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
15 files changed, 509 insertions, 118 deletions
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");