summary refs log tree commit diff
path: root/clients/common
diff options
context:
space:
mode:
Diffstat (limited to 'clients/common')
-rw-r--r--clients/common/meson.build2
-rw-r--r--clients/common/nm-client-utils.c223
-rw-r--r--clients/common/nm-client-utils.h10
-rw-r--r--clients/common/nm-meta-setting-desc.c280
-rw-r--r--clients/common/nm-meta-setting-desc.h3
-rw-r--r--clients/common/nm-secret-agent-simple.c4
-rw-r--r--clients/common/nm-vpn-helpers.c144
-rw-r--r--clients/common/nm-vpn-helpers.h4
-rw-r--r--clients/common/settings-docs.h17
-rw-r--r--clients/common/settings-docs.h.in17
-rw-r--r--clients/common/tests/test-clients-common.c73
11 files changed, 637 insertions, 140 deletions
diff --git a/clients/common/meson.build b/clients/common/meson.build
index c32bb5e7..cefcfce0 100644
--- a/clients/common/meson.build
+++ b/clients/common/meson.build
@@ -34,7 +34,7 @@ settings_docs = 'settings-docs.h'
 if enable_introspection
   settings_docs_source = custom_target(
     settings_docs,
-    input: nm_property_docs,
+    input: nm_settings_docs_xml_gir,
     output: settings_docs,
     command: [xsltproc, '--output', '@OUTPUT@', join_paths(meson.current_source_dir(), 'settings-docs.xsl'), '@INPUT@'],
   )
diff --git a/clients/common/nm-client-utils.c b/clients/common/nm-client-utils.c
index cf8b4785..d3a02161 100644
--- a/clients/common/nm-client-utils.c
+++ b/clients/common/nm-client-utils.c
@@ -6,8 +6,10 @@
 #include "nm-default.h"
 
 #include "nm-client-utils.h"
-#include "nm-utils.h"
 
+#include "nm-glib-aux/nm-secret-utils.h"
+#include "nm-glib-aux/nm-io-utils.h"
+#include "nm-utils.h"
 #include "nm-device-bond.h"
 #include "nm-device-bridge.h"
 #include "nm-device-team.h"
@@ -263,6 +265,38 @@ NM_UTILS_LOOKUP_STR_DEFINE (nmc_device_state_to_string, NMDeviceState,
 	NM_UTILS_LOOKUP_ITEM (NM_DEVICE_STATE_UNKNOWN,      N_("unknown")),
 )
 
+static
+NM_UTILS_LOOKUP_STR_DEFINE (_device_state_to_string, NMDeviceState,
+	NM_UTILS_LOOKUP_DEFAULT (NULL),
+	NM_UTILS_LOOKUP_ITEM (NM_DEVICE_STATE_PREPARE,      N_("connecting (externally)")),
+	NM_UTILS_LOOKUP_ITEM (NM_DEVICE_STATE_CONFIG,       N_("connecting (externally)")),
+	NM_UTILS_LOOKUP_ITEM (NM_DEVICE_STATE_NEED_AUTH,    N_("connecting (externally)")),
+	NM_UTILS_LOOKUP_ITEM (NM_DEVICE_STATE_IP_CONFIG,    N_("connecting (externally)")),
+	NM_UTILS_LOOKUP_ITEM (NM_DEVICE_STATE_IP_CHECK,     N_("connecting (externally)")),
+	NM_UTILS_LOOKUP_ITEM (NM_DEVICE_STATE_SECONDARIES,  N_("connecting (externally)")),
+	NM_UTILS_LOOKUP_ITEM (NM_DEVICE_STATE_ACTIVATED,    N_("connected (externally)")),
+	NM_UTILS_LOOKUP_ITEM (NM_DEVICE_STATE_DEACTIVATING, N_("deactivating (externally)")),
+	NM_UTILS_LOOKUP_ITEM (NM_DEVICE_STATE_FAILED,       N_("deactivating (externally)")),
+	NM_UTILS_LOOKUP_ITEM_IGNORE_OTHER (),
+)
+
+const char *
+nmc_device_state_to_string_with_external (NMDevice *device)
+{
+	NMActiveConnection *ac;
+	NMDeviceState state;
+	const char *s;
+
+	state = nm_device_get_state (device);
+
+	if (   (ac = nm_device_get_active_connection (device))
+	    && NM_FLAGS_HAS (nm_active_connection_get_state_flags (ac), NM_ACTIVATION_STATE_FLAG_EXTERNAL)
+	    && (s = _device_state_to_string (state)))
+		return s;
+
+	return nmc_device_state_to_string (state);
+}
+
 NM_UTILS_LOOKUP_STR_DEFINE (nmc_device_metered_to_string, NMMetered,
 	NM_UTILS_LOOKUP_DEFAULT (N_("unknown")),
 	NM_UTILS_LOOKUP_ITEM (NM_METERED_YES,       N_("yes")),
@@ -587,3 +621,190 @@ nmc_print_qrcode (const char *str)
 		}
 	}
 }
+
+/**
+ * nmc_utils_read_passwd_file:
+ * @passwd_file: file with passwords to parse
+ * @out_error_line: returns in case of a syntax error in the file, the line
+ *   on which it occurred.
+ * @error: location to store error, or %NULL
+ *
+ * Parse passwords given in @passwd_file and insert them into a hash table.
+ * Example of @passwd_file contents:
+ *   wifi.psk:tajne heslo
+ *   802-1x.password:krakonos
+ *   802-11-wireless-security:leap-password:my leap password
+ *
+ * Returns: (transfer full): hash table with parsed passwords, or %NULL on an error
+ */
+GHashTable *
+nmc_utils_read_passwd_file (const char *passwd_file,
+                            gssize *out_error_line,
+                            GError **error)
+{
+	nm_auto_clear_secret_ptr NMSecretPtr contents = { 0 };
+
+	NM_SET_OUT (out_error_line, -1);
+
+	if (!passwd_file)
+		return g_hash_table_new_full (nm_str_hash, g_str_equal, g_free, (GDestroyNotify) nm_free_secret);
+
+	if (!nm_utils_file_get_contents (-1,
+	                                 passwd_file,
+	                                 1024*1024,
+	                                 NM_UTILS_FILE_GET_CONTENTS_FLAG_SECRET,
+	                                 &contents.str,
+	                                 &contents.len,
+	                                 NULL,
+	                                 error))
+		return NULL;
+
+	return nmc_utils_parse_passwd_file (contents.str, out_error_line, error);
+}
+
+GHashTable *
+nmc_utils_parse_passwd_file (char *contents /* will be modified */,
+                             gssize *out_error_line,
+                             GError **error)
+{
+	gs_unref_hashtable GHashTable *pwds_hash = NULL;
+	const char *contents_str;
+	gsize contents_line;
+
+	pwds_hash = g_hash_table_new_full (nm_str_hash, g_str_equal, g_free, (GDestroyNotify) nm_free_secret);
+
+	NM_SET_OUT (out_error_line, -1);
+
+	contents_str = contents;
+	contents_line = 0;
+	while (contents_str[0]) {
+		nm_auto_free_secret char *l_hash_key = NULL;
+		nm_auto_free_secret char *l_hash_val = NULL;
+		const char *l_content_line;
+		const char *l_setting;
+		const char *l_prop;
+		const char *l_val;
+		const char *s;
+		gsize l_hash_val_len;
+
+		/* consume first line. As line delimiters we accept "\r\n", "\n", and "\r". */
+		l_content_line = contents_str;
+		s = l_content_line;
+		while (!NM_IN_SET (s[0], '\0', '\r', '\n'))
+			s++;
+		if (s[0] != '\0') {
+			if (   s[0] == '\r'
+			    && s[1] == '\n') {
+				((char *) s)[0] = '\0';
+				s += 2;
+			} else {
+				((char *) s)[0] = '\0';
+				s += 1;
+			}
+		}
+		contents_str = s;
+		contents_line++;
+
+		l_content_line = nm_str_skip_leading_spaces (l_content_line);
+		if (NM_IN_SET (l_content_line[0], '\0', '#')) {
+			/* a comment or empty line. Ignore. */
+			continue;
+		}
+
+		l_setting = l_content_line;
+
+		s = l_setting;
+		while (!NM_IN_SET (s[0], '\0', ':', '='))
+			s++;
+		if (s[0] == '\0') {
+			NM_SET_OUT (out_error_line, contents_line);
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("missing colon for \"<setting>.<property>:<secret>\" format"));
+			return NULL;
+		}
+		((char *) s)[0] = '\0';
+		s++;
+
+		l_val = s;
+
+		g_strchomp ((char *) l_setting);
+
+		nm_assert (nm_str_is_stripped (l_setting));
+
+		s = strchr (l_setting, '.');
+		if (!s) {
+			NM_SET_OUT (out_error_line, contents_line);
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("missing dot for \"<setting>.<property>:<secret>\" format"));
+			return NULL;
+		} else if (s == l_setting) {
+			NM_SET_OUT (out_error_line, contents_line);
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("missing setting for \"<setting>.<property>:<secret>\" format"));
+			return NULL;
+		}
+		((char *) s)[0] = '\0';
+		s++;
+
+		l_prop = s;
+		if (l_prop[0] == '\0') {
+			NM_SET_OUT (out_error_line, contents_line);
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("missing property for \"<setting>.<property>:<secret>\" format"));
+			return NULL;
+		}
+
+		/* Accept wifi-sec or wifi instead of cumbersome '802-11-wireless-security' */
+		if (NM_IN_STRSET (l_setting, "wifi-sec", "wifi"))
+			l_setting = NM_SETTING_WIRELESS_SECURITY_SETTING_NAME;
+
+		if (nm_setting_lookup_type (l_setting) == G_TYPE_INVALID) {
+			NM_SET_OUT (out_error_line, contents_line);
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("invalid setting name"));
+			return NULL;
+		}
+
+		if (   nm_streq (l_setting, "vpn")
+		    && NM_STR_HAS_PREFIX (l_prop, "secret.")) {
+			/* in 1.12.0, we wrongly required the VPN secrets to be named
+			 * "vpn.secret". It should be "vpn.secrets". Work around it
+			 * (rh#1628833). */
+			l_hash_key = g_strdup_printf ("vpn.secrets.%s", &l_prop[NM_STRLEN ("secret.")]);
+		} else
+			l_hash_key = g_strdup_printf ("%s.%s", l_setting, l_prop);
+
+		if (!g_utf8_validate (l_hash_key, -1, NULL)) {
+			NM_SET_OUT (out_error_line, contents_line);
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("property name is not UTF-8"));
+			return NULL;
+		}
+
+		/* Support backslash escaping in the secret value. We strip non-escaped leading/trailing whitespaces. */
+		s = nm_utils_buf_utf8safe_unescape (l_val, NM_UTILS_STR_UTF8_SAFE_UNESCAPE_STRIP_SPACES, &l_hash_val_len, (gpointer *) &l_hash_val);
+		if (!l_hash_val)
+			l_hash_val = g_strdup (s);
+
+		if (!g_utf8_validate (l_hash_val, -1, NULL)) {
+			/* In some cases it might make sense to support binary secrets (like the WPA-PSK which has no
+			 * defined encoding. However, all API that follows can only handle UTF-8, and no mechanism
+			 * to escape the secrets. Reject non-UTF-8 early. */
+			NM_SET_OUT (out_error_line, contents_line);
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("secret is not UTF-8"));
+			return NULL;
+		}
+
+		if (strlen (l_hash_val) != l_hash_val_len) {
+			NM_SET_OUT (out_error_line, contents_line);
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("secret is not UTF-8"));
+			return NULL;
+		}
+
+		g_hash_table_insert (pwds_hash, g_steal_pointer (&l_hash_key), g_steal_pointer (&l_hash_val));
+	}
+
+	return g_steal_pointer (&pwds_hash);
+}
diff --git a/clients/common/nm-client-utils.h b/clients/common/nm-client-utils.h
index 1e3085d5..28eac457 100644
--- a/clients/common/nm-client-utils.h
+++ b/clients/common/nm-client-utils.h
@@ -28,6 +28,8 @@ gboolean matches (const char *cmd, const char *pattern);
 /* FIXME: don't expose this function on its own, at least not from this file. */
 const char *nmc_bond_validate_mode (const char *mode, GError **error);
 
+const char *nmc_device_state_to_string_with_external (NMDevice *device);
+
 const char *nm_active_connection_state_reason_to_string (NMActiveConnectionStateReason reason);
 const char *nmc_device_state_to_string (NMDeviceState state);
 const char *nmc_device_reason_to_string (NMDeviceStateReason reason);
@@ -43,4 +45,12 @@ const char *nmc_password_subst_char (void);
 
 void nmc_print_qrcode (const char *str);
 
+GHashTable *nmc_utils_parse_passwd_file (char *contents,
+                                         gssize *out_error_line,
+                                         GError **error);
+
+GHashTable *nmc_utils_read_passwd_file (const char *passwd_file,
+                                        gssize *out_error_line,
+                                        GError **error);
+
 #endif /* __NM_CLIENT_UTILS_H__ */
diff --git a/clients/common/nm-meta-setting-desc.c b/clients/common/nm-meta-setting-desc.c
index 920a5af9..dd5fd6f3 100644
--- a/clients/common/nm-meta-setting-desc.c
+++ b/clients/common/nm-meta-setting-desc.c
@@ -783,6 +783,17 @@ _coerce_str_emptyunset (NMMetaAccessorGetType get_type,
 	return cstr;
 }
 
+#define RETURN_STR_EMPTYUNSET(get_type, is_default, cstr) \
+	G_STMT_START { \
+		char *_str = NULL; \
+		const char *_cstr; \
+		\
+		_cstr = _coerce_str_emptyunset ((get_type), (is_default), (cstr), &_str); \
+		if (_str) \
+			RETURN_STR_TO_FREE (_str); \
+		RETURN_STR_TEMPORARY (_cstr); \
+	} G_STMT_END
+
 static gboolean
 _is_default (const NMMetaPropertyInfo *property_info,
              NMSetting *setting)
@@ -836,6 +847,19 @@ _get_fcn_gobject_impl (const NMMetaPropertyInfo *property_info,
 	           || (   gtype_prop == G_TYPE_STRV
 	               && !glib_handles_str_transform));
 
+	if (gtype_prop == G_TYPE_STRING) {
+		nm_assert (glib_handles_str_transform);
+		nm_assert (!handle_emptyunset);
+		if (   property_info->property_typ_data
+		    && property_info->property_typ_data->subtype.gobject_string.handle_emptyunset) {
+			/* This string property can both be empty and NULL. We need to
+			 * signal them differently. */
+			cstr = g_value_get_string (&val);
+			nm_assert ((!!is_default) == (cstr == NULL));
+			RETURN_STR_EMPTYUNSET (get_type, is_default, NULL);
+		}
+	}
+
 	if (glib_handles_str_transform)
 		RETURN_STR_TEMPORARY (g_value_get_string (&val));
 
@@ -857,15 +881,9 @@ _get_fcn_gobject_impl (const NMMetaPropertyInfo *property_info,
 		if (strv && strv[0])
 			RETURN_STR_TO_FREE (g_strjoinv (",", (char **) strv));
 
-		/* special hack for handling properties that can be empty and unset
-		 * (see multilist.clear_emptyunset_fcn). */
 		if (handle_emptyunset) {
-			char *str = NULL;
-
-			cstr = _coerce_str_emptyunset (get_type, is_default, NULL, &str);
-			if (str)
-				RETURN_STR_TO_FREE (str);
-			RETURN_STR_TEMPORARY (cstr);
+			/* we need to express empty lists from unset lists differently. */
+			RETURN_STR_EMPTYUNSET (get_type, is_default, NULL);
 		}
 
 		return "";
@@ -1183,6 +1201,22 @@ _set_fcn_gobject_string (ARGS_SET_FCN)
 		return _gobject_property_reset_default (setting, property_info->property_name);
 
 	if (property_info->property_typ_data) {
+		if (property_info->property_typ_data->subtype.gobject_string.handle_emptyunset) {
+			if (   value
+			    && value[0]
+			    && NM_STRCHAR_ALL (value, ch, ch == ' ')) {
+				/* this string property can both be %NULL and empty. To express that, we coerce
+				 * a value of all whitespaces to dropping the first whitespace. That means,
+				 * " " gives "", "  " gives " ", and so on.
+				 *
+				 * This way the user can set the string value to "" (meaning NULL) and to
+				 * " " (meaning ""), and any other string.
+				 *
+				 * This is and non-obvious escaping mechanism. But out of all the possible
+				 * solutions, it seems the most sensible one. */
+				value++;
+			}
+		}
 		if (property_info->property_typ_data->subtype.gobject_string.validate_fcn) {
 			value = property_info->property_typ_data->subtype.gobject_string.validate_fcn (value, &to_free, error);
 			if (!value)
@@ -3397,35 +3431,6 @@ _objlist_set_fcn_ip_config_routing_rules (NMSetting *setting,
 }
 
 static gconstpointer
-_get_fcn_match_interface_name (ARGS_GET_FCN)
-{
-	NMSettingMatch *s_match = NM_SETTING_MATCH (setting);
-	GString *str = NULL;
-	guint i, num;
-
-	RETURN_UNSUPPORTED_GET_TYPE ();
-
-	num = nm_setting_match_get_num_interface_names (s_match);
-	for (i = 0; i < num; i++) {
-		const char *name;
-
-		name = nm_setting_match_get_interface_name (s_match, i);
-		if (!name || !name[0])
-			continue;
-		if (!str)
-			str = g_string_new ("");
-		else
-			g_string_append_c (str, ESCAPED_TOKENS_WITH_SPACES_DELIMTER);
-		nm_utils_escaped_tokens_escape_gstr (name, ESCAPED_TOKENS_WITH_SPACES_DELIMTERS, str);
-	}
-
-	NM_SET_OUT (out_is_default, num == 0);
-	if (!str)
-		return NULL;
-	RETURN_STR_TO_FREE (g_string_free (str, FALSE));
-}
-
-static gconstpointer
 _get_fcn_olpc_mesh_ssid (ARGS_GET_FCN)
 {
 	NMSettingOlpcMesh *s_olpc_mesh = NM_SETTING_OLPC_MESH (setting);
@@ -4092,25 +4097,38 @@ _gobject_enum_pre_set_notify_fcn_wireless_security_wep_key_type (const NMMetaPro
 static gconstpointer
 _get_fcn_ethtool (ARGS_GET_FCN)
 {
-	const char *s;
-	NMTernary val;
 	NMEthtoolID ethtool_id = property_info->property_typ_data->subtype.ethtool.ethtool_id;
+	const char *s;
+	guint32 u32;
+	gboolean b;
 
 	RETURN_UNSUPPORTED_GET_TYPE ();
 
-	val = nm_setting_ethtool_get_feature (NM_SETTING_ETHTOOL (setting),
-	                                      nm_ethtool_data[ethtool_id]->optname);
+	if (   nm_ethtool_id_is_coalesce (ethtool_id)
+	    || nm_ethtool_id_is_ring (ethtool_id)) {
+		if (!nm_setting_option_get_uint32 (setting,
+		                                   nm_ethtool_data[ethtool_id]->optname,
+		                                   &u32)) {
+			NM_SET_OUT (out_is_default, TRUE);
+			return NULL;
+		}
+
+		RETURN_STR_TO_FREE (nm_strdup_int (u32));
+	}
 
-	if (val == NM_TERNARY_TRUE)
-		s = N_("on");
-	else if (val == NM_TERNARY_FALSE)
-		s = N_("off");
-	else {
-		s = NULL;
+	nm_assert (nm_ethtool_id_is_feature (ethtool_id));
+
+	if (!nm_setting_option_get_boolean (setting,
+	                                    nm_ethtool_data[ethtool_id]->optname,
+	                                    &b)) {
 		NM_SET_OUT (out_is_default, TRUE);
+		return NULL;
 	}
 
-	if (s && get_type == NM_META_ACCESSOR_GET_TYPE_PRETTY)
+	s =   b
+	    ? N_("on")
+	    : N_("off");
+	if (get_type == NM_META_ACCESSOR_GET_TYPE_PRETTY)
 		s = gettext (s);
 	return s;
 }
@@ -4118,23 +4136,40 @@ _get_fcn_ethtool (ARGS_GET_FCN)
 static gboolean
 _set_fcn_ethtool (ARGS_SET_FCN)
 {
-	gs_free char *value_to_free = NULL;
-	NMTernary val;
 	NMEthtoolID ethtool_id = property_info->property_typ_data->subtype.ethtool.ethtool_id;
+	gs_free char *value_to_free = NULL;
+	gint64 i64;
+	gboolean b;
 
-	if (_SET_FCN_DO_RESET_DEFAULT (property_info, modifier, value)) {
-		val = NM_TERNARY_DEFAULT;
-		goto set;
+	if (_SET_FCN_DO_RESET_DEFAULT (property_info, modifier, value))
+		goto do_unset;
+
+	if (   nm_ethtool_id_is_coalesce (ethtool_id)
+	    || nm_ethtool_id_is_ring (ethtool_id)) {
+
+		i64 = _nm_utils_ascii_str_to_int64 (value, 10, 0, G_MAXUINT32, -1);
+		if (i64 == -1) {
+			g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_INVALID_ARGUMENT,
+			             _("'%s' is out of range [%"G_GUINT32_FORMAT", %"G_GUINT32_FORMAT"]"),
+			             value, 0, G_MAXUINT32);
+			return FALSE;
+		}
+
+		nm_setting_option_set_uint32 (setting,
+		                              nm_ethtool_data[ethtool_id]->optname,
+		                              i64);
+		return TRUE;
 	}
 
-	value = nm_strstrip_avoid_copy_a (300, value, &value_to_free);
+	nm_assert (nm_ethtool_id_is_feature (ethtool_id));
 
+	value = nm_strstrip_avoid_copy_a (300, value, &value_to_free);
 	if (NM_IN_STRSET (value, "1", "yes", "true", "on"))
-		val = NM_TERNARY_TRUE;
+		b = TRUE;
 	else if (NM_IN_STRSET (value, "0", "no", "false", "off"))
-		val = NM_TERNARY_FALSE;
+		b = FALSE;
 	else if (NM_IN_STRSET (value, "", "ignore", "default"))
-		val = NM_TERNARY_DEFAULT;
+		goto do_unset;
 	else {
 		g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_INVALID_ARGUMENT,
 		             _("'%s' is not valid; use 'on', 'off', or 'ignore'"),
@@ -4142,10 +4177,15 @@ _set_fcn_ethtool (ARGS_SET_FCN)
 		return FALSE;
 	}
 
-set:
-	nm_setting_ethtool_set_feature (NM_SETTING_ETHTOOL (setting),
-	                                nm_ethtool_data[ethtool_id]->optname,
-	                                val);
+	nm_setting_option_set_boolean (setting,
+	                               nm_ethtool_data[ethtool_id]->optname,
+	                               b);
+	return TRUE;
+
+do_unset:
+	nm_setting_option_set (setting,
+	                       nm_ethtool_data[ethtool_id]->optname,
+	                       NULL);
 	return TRUE;
 }
 
@@ -4165,10 +4205,14 @@ _complete_fcn_ethtool (ARGS_COMPLETE_FCN)
 		"ignore",
 		NULL,
 	};
+	NMEthtoolID ethtool_id = property_info->property_typ_data->subtype.ethtool.ethtool_id;
 
-	if (!text || !text[0])
-		return &v[7];
-	return v;
+	if (nm_ethtool_id_is_feature (ethtool_id)) {
+		if (!text || !text[0])
+			return &v[7];
+		return v;
+	}
+	return NULL;
 }
 
 /*****************************************************************************/
@@ -4905,10 +4949,38 @@ static const NMMetaPropertyInfo *const property_infos_BRIDGE[] = {
 	    .prompt =                       N_("Group forward mask [0]"),
 	    .property_type =                &_pt_gobject_int,
 	),
+	PROPERTY_INFO_WITH_DESC (NM_SETTING_BRIDGE_MULTICAST_HASH_MAX,
+	    .property_type =                &_pt_gobject_int,
+	    .hide_if_default =              TRUE,
+	),
+	PROPERTY_INFO_WITH_DESC (NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_COUNT,
+	    .property_type =                &_pt_gobject_int,
+	    .hide_if_default =              TRUE,
+	),
+	PROPERTY_INFO_WITH_DESC (NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL,
+	    .property_type =                &_pt_gobject_int,
+	    .hide_if_default =              TRUE,
+	),
+	PROPERTY_INFO_WITH_DESC (NM_SETTING_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL,
+	    .property_type =                &_pt_gobject_int,
+	    .hide_if_default =              TRUE,
+	),
 	PROPERTY_INFO_WITH_DESC (NM_SETTING_BRIDGE_MULTICAST_QUERIER,
 	    .property_type =                &_pt_gobject_bool,
 	    .hide_if_default =              TRUE,
 	),
+	PROPERTY_INFO_WITH_DESC (NM_SETTING_BRIDGE_MULTICAST_QUERIER_INTERVAL,
+	    .property_type =                &_pt_gobject_int,
+	    .hide_if_default =              TRUE,
+	),
+	PROPERTY_INFO_WITH_DESC (NM_SETTING_BRIDGE_MULTICAST_QUERY_INTERVAL,
+	    .property_type =                &_pt_gobject_int,
+	    .hide_if_default =              TRUE,
+	),
+	PROPERTY_INFO_WITH_DESC (NM_SETTING_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL,
+	    .property_type =                &_pt_gobject_int,
+	    .hide_if_default =              TRUE,
+	),
 	PROPERTY_INFO_WITH_DESC (NM_SETTING_BRIDGE_MULTICAST_QUERY_USE_IFADDR,
 	    .property_type =                &_pt_gobject_bool,
 	    .hide_if_default =              TRUE,
@@ -4919,6 +4991,14 @@ static const NMMetaPropertyInfo *const property_infos_BRIDGE[] = {
 	    .prompt =                       N_("Enable IGMP snooping [no]"),
 	    .property_type =                &_pt_gobject_bool,
 	),
+	PROPERTY_INFO_WITH_DESC (NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT,
+	    .property_type =                &_pt_gobject_int,
+	    .hide_if_default =              TRUE,
+	),
+	PROPERTY_INFO_WITH_DESC (NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL,
+	    .property_type =                &_pt_gobject_int,
+	    .hide_if_default =              TRUE,
+	),
 	PROPERTY_INFO_WITH_DESC (NM_SETTING_BRIDGE_MULTICAST_ROUTER,
 	    .property_type =                &_pt_gobject_string,
 	    .hide_if_default =              TRUE,
@@ -5227,6 +5307,10 @@ static const NMMetaPropertyInfo *const property_infos_CONNECTION[] = {
 	        ),
 	    ),
 	),
+	PROPERTY_INFO_WITH_DESC (NM_SETTING_CONNECTION_MUD_URL,
+	    .property_type =                &_pt_gobject_string,
+	    .hide_if_default =              TRUE,
+	),
 	PROPERTY_INFO_WITH_DESC (NM_SETTING_CONNECTION_WAIT_DEVICE_TIMEOUT,
 	    .property_type =                &_pt_gobject_int,
 	),
@@ -5386,6 +5470,32 @@ static const NMMetaPropertyInfo *const property_infos_ETHTOOL[] = {
 	PROPERTY_INFO_ETHTOOL (FEATURE_TX_UDP_TNL_CSUM_SEGMENTATION),
 	PROPERTY_INFO_ETHTOOL (FEATURE_TX_UDP_TNL_SEGMENTATION),
 	PROPERTY_INFO_ETHTOOL (FEATURE_TX_VLAN_STAG_HW_INSERT),
+	PROPERTY_INFO_ETHTOOL (COALESCE_ADAPTIVE_RX),
+	PROPERTY_INFO_ETHTOOL (COALESCE_ADAPTIVE_TX),
+	PROPERTY_INFO_ETHTOOL (COALESCE_PKT_RATE_HIGH),
+	PROPERTY_INFO_ETHTOOL (COALESCE_PKT_RATE_LOW),
+	PROPERTY_INFO_ETHTOOL (COALESCE_RX_FRAMES),
+	PROPERTY_INFO_ETHTOOL (COALESCE_RX_FRAMES_IRQ),
+	PROPERTY_INFO_ETHTOOL (COALESCE_RX_FRAMES_HIGH),
+	PROPERTY_INFO_ETHTOOL (COALESCE_RX_FRAMES_LOW),
+	PROPERTY_INFO_ETHTOOL (COALESCE_RX_USECS),
+	PROPERTY_INFO_ETHTOOL (COALESCE_RX_USECS_IRQ),
+	PROPERTY_INFO_ETHTOOL (COALESCE_RX_USECS_HIGH),
+	PROPERTY_INFO_ETHTOOL (COALESCE_RX_USECS_LOW),
+	PROPERTY_INFO_ETHTOOL (COALESCE_SAMPLE_INTERVAL),
+	PROPERTY_INFO_ETHTOOL (COALESCE_STATS_BLOCK_USECS),
+	PROPERTY_INFO_ETHTOOL (COALESCE_TX_FRAMES),
+	PROPERTY_INFO_ETHTOOL (COALESCE_TX_FRAMES_IRQ),
+	PROPERTY_INFO_ETHTOOL (COALESCE_TX_FRAMES_HIGH),
+	PROPERTY_INFO_ETHTOOL (COALESCE_TX_FRAMES_LOW),
+	PROPERTY_INFO_ETHTOOL (COALESCE_TX_USECS),
+	PROPERTY_INFO_ETHTOOL (COALESCE_TX_USECS_IRQ),
+	PROPERTY_INFO_ETHTOOL (COALESCE_TX_USECS_HIGH),
+	PROPERTY_INFO_ETHTOOL (COALESCE_TX_USECS_LOW),
+	PROPERTY_INFO_ETHTOOL (RING_RX),
+	PROPERTY_INFO_ETHTOOL (RING_RX_JUMBO),
+	PROPERTY_INFO_ETHTOOL (RING_RX_MINI),
+	PROPERTY_INFO_ETHTOOL (RING_TX),
 	NULL,
 };
 
@@ -6120,11 +6230,7 @@ static const NMMetaPropertyInfo *const property_infos_MACVLAN[] = {
 #define _CURRENT_NM_META_SETTING_TYPE NM_META_SETTING_TYPE_MATCH
 static const NMMetaPropertyInfo *const property_infos_MATCH[] = {
 	PROPERTY_INFO_WITH_DESC (NM_SETTING_MATCH_INTERFACE_NAME,
-	    .property_type = DEFINE_PROPERTY_TYPE (
-	        .get_fcn =                  _get_fcn_match_interface_name,
-	        .set_fcn =                  _set_fcn_multilist,
-	        .set_supports_remove =      TRUE,
-	    ),
+	    .property_type =                &_pt_multilist,
 	    .property_typ_data = DEFINE_PROPERTY_TYP_DATA (
 	        PROPERTY_TYP_DATA_SUBTYPE (multilist,
 	            .get_num_fcn_u   =      MULTILIST_GET_NUM_FCN_U       (NMSettingMatch, nm_setting_match_get_num_interface_names),
@@ -6135,6 +6241,42 @@ static const NMMetaPropertyInfo *const property_infos_MATCH[] = {
 	        ),
 	    ),
 	),
+	PROPERTY_INFO_WITH_DESC (NM_SETTING_MATCH_KERNEL_COMMAND_LINE,
+	    .property_type =                &_pt_multilist,
+	    .property_typ_data = DEFINE_PROPERTY_TYP_DATA (
+	        PROPERTY_TYP_DATA_SUBTYPE (multilist,
+	            .get_num_fcn_u   =      MULTILIST_GET_NUM_FCN_U       (NMSettingMatch, nm_setting_match_get_num_kernel_command_lines),
+	            .add2_fcn =             MULTILIST_ADD2_FCN            (NMSettingMatch, nm_setting_match_add_kernel_command_line),
+	            .remove_by_idx_fcn_u =  MULTILIST_REMOVE_BY_IDX_FCN_U (NMSettingMatch, nm_setting_match_remove_kernel_command_line),
+	            .remove_by_value_fcn =  MULTILIST_REMOVE_BY_VALUE_FCN (NMSettingMatch, nm_setting_match_remove_kernel_command_line_by_value),
+	            .strsplit_with_spaces = TRUE,
+	        ),
+	    ),
+	),
+	PROPERTY_INFO_WITH_DESC (NM_SETTING_MATCH_DRIVER,
+	    .property_type =                &_pt_multilist,
+	    .property_typ_data = DEFINE_PROPERTY_TYP_DATA (
+	        PROPERTY_TYP_DATA_SUBTYPE (multilist,
+	            .get_num_fcn_u   =      MULTILIST_GET_NUM_FCN_U       (NMSettingMatch, nm_setting_match_get_num_drivers),
+	            .add2_fcn =             MULTILIST_ADD2_FCN            (NMSettingMatch, nm_setting_match_add_driver),
+	            .remove_by_idx_fcn_u =  MULTILIST_REMOVE_BY_IDX_FCN_U (NMSettingMatch, nm_setting_match_remove_driver),
+	            .remove_by_value_fcn =  MULTILIST_REMOVE_BY_VALUE_FCN (NMSettingMatch, nm_setting_match_remove_driver_by_value),
+	            .strsplit_with_spaces = TRUE,
+	        ),
+	    ),
+	),
+	PROPERTY_INFO_WITH_DESC (NM_SETTING_MATCH_PATH,
+	    .property_type =                &_pt_multilist,
+	    .property_typ_data = DEFINE_PROPERTY_TYP_DATA (
+	        PROPERTY_TYP_DATA_SUBTYPE (multilist,
+	            .get_num_fcn_u   =      MULTILIST_GET_NUM_FCN_U       (NMSettingMatch, nm_setting_match_get_num_paths),
+	            .add2_fcn =             MULTILIST_ADD2_FCN            (NMSettingMatch, nm_setting_match_add_path),
+	            .remove_by_idx_fcn_u =  MULTILIST_REMOVE_BY_IDX_FCN_U (NMSettingMatch, nm_setting_match_remove_path),
+	            .remove_by_value_fcn =  MULTILIST_REMOVE_BY_VALUE_FCN (NMSettingMatch, nm_setting_match_remove_path_by_value),
+	            .strsplit_with_spaces = TRUE,
+	        ),
+	    ),
+	),
 	NULL
 };
 
diff --git a/clients/common/nm-meta-setting-desc.h b/clients/common/nm-meta-setting-desc.h
index 2ba48f5d..24689720 100644
--- a/clients/common/nm-meta-setting-desc.h
+++ b/clients/common/nm-meta-setting-desc.h
@@ -76,6 +76,7 @@ typedef enum {
 	NM_META_COLOR_CONNECTION_ACTIVATING,
 	NM_META_COLOR_CONNECTION_DISCONNECTING,
 	NM_META_COLOR_CONNECTION_INVISIBLE,
+	NM_META_COLOR_CONNECTION_EXTERNAL,
 	NM_META_COLOR_CONNECTION_UNKNOWN,
 	NM_META_COLOR_CONNECTIVITY_FULL,
 	NM_META_COLOR_CONNECTIVITY_LIMITED,
@@ -89,6 +90,7 @@ typedef enum {
 	NM_META_COLOR_DEVICE_PLUGIN_MISSING,
 	NM_META_COLOR_DEVICE_UNAVAILABLE,
 	NM_META_COLOR_DEVICE_DISABLED,
+	NM_META_COLOR_DEVICE_EXTERNAL,
 	NM_META_COLOR_DEVICE_UNKNOWN,
 	NM_META_COLOR_MANAGER_RUNNING,
 	NM_META_COLOR_MANAGER_STARTING,
@@ -257,6 +259,7 @@ struct _NMMetaPropertyTypData {
 		} gobject_int;
 		struct {
 			const char *(*validate_fcn) (const char *value, char **out_to_free, GError **error);
+			bool handle_emptyunset:1;
 		} gobject_string;
 		struct {
 			bool legacy_format:1;
diff --git a/clients/common/nm-secret-agent-simple.c b/clients/common/nm-secret-agent-simple.c
index ca9250ca..53217e6f 100644
--- a/clients/common/nm-secret-agent-simple.c
+++ b/clients/common/nm-secret-agent-simple.c
@@ -422,7 +422,7 @@ add_vpn_secrets (RequestData *request,
                  char **msg)
 {
 	NMSettingVpn *s_vpn = nm_connection_get_setting_vpn (request->connection);
-	const VpnPasswordName *secret_names, *p;
+	const NmcVpnPasswordName *p;
 	const char *vpn_msg = NULL;
 	char **iter;
 
@@ -439,7 +439,7 @@ add_vpn_secrets (RequestData *request,
 	NM_SET_OUT (msg, g_strdup (vpn_msg));
 
 	/* Now add what client thinks might be required, because hints may be empty or incomplete */
-	p = secret_names = nm_vpn_get_secret_names (nm_setting_vpn_get_service_type (s_vpn));
+	p = nm_vpn_get_secret_names (nm_setting_vpn_get_service_type (s_vpn));
 	while (p && p->name) {
 		add_vpn_secret_helper (secrets, s_vpn, p->name, _(p->ui_name));
 		p++;
diff --git a/clients/common/nm-vpn-helpers.c b/clients/common/nm-vpn-helpers.c
index 35ed4451..74ff52bb 100644
--- a/clients/common/nm-vpn-helpers.c
+++ b/clients/common/nm-vpn-helpers.c
@@ -103,63 +103,73 @@ nm_vpn_supports_ipv6 (NMConnection *connection)
 	return NM_FLAGS_HAS (capabilities, NM_VPN_EDITOR_PLUGIN_CAPABILITY_IPV6);
 }
 
-const VpnPasswordName *
+const NmcVpnPasswordName *
 nm_vpn_get_secret_names (const char *service_type)
 {
-	static const VpnPasswordName generic_vpn_secrets[] = {
-		{ "password", N_("Password") },
-		{ 0 }
-	};
-	static const VpnPasswordName openvpn_secrets[] = {
-		{ "password", N_("Password") },
-		{ "cert-pass", N_("Certificate password") },
-		{ "http-proxy-password", N_("HTTP proxy password") },
-		{ 0 }
-	};
-	static const VpnPasswordName vpnc_secrets[] = {
-		{ "Xauth password", N_("Password") },
-		{ "IPSec secret", N_("Group password") },
-		{ 0 }
-	};
-	static const VpnPasswordName swan_secrets[] = {
-		{ "xauthpassword", N_("Password") },
-		{ "pskvalue", N_("Group password") },
-		{ 0 }
-	};
-	static const VpnPasswordName openconnect_secrets[] = {
-		{ "gateway", N_("Gateway") },
-		{ "cookie", N_("Cookie") },
-		{ "gwcert", N_("Gateway certificate hash") },
-		{ 0 }
-	};
 	const char *type;
 
 	if (!service_type)
 		return NULL;
 
-	if (   !g_str_has_prefix (service_type, NM_DBUS_INTERFACE)
+	if (   !NM_STR_HAS_PREFIX (service_type, NM_DBUS_INTERFACE)
 	    || service_type[NM_STRLEN (NM_DBUS_INTERFACE)] != '.') {
 		/* all our well-known, hard-coded vpn-types start with NM_DBUS_INTERFACE. */
 		return NULL;
 	}
 
 	type = service_type + (NM_STRLEN (NM_DBUS_INTERFACE) + 1);
-	if (   !g_strcmp0 (type, "pptp")
-	    || !g_strcmp0 (type, "iodine")
-	    || !g_strcmp0 (type, "ssh")
-	    || !g_strcmp0 (type, "l2tp")
-	    || !g_strcmp0 (type, "fortisslvpn"))
-		 return generic_vpn_secrets;
-	else if (!g_strcmp0 (type, "openvpn"))
-		return openvpn_secrets;
-	else if (!g_strcmp0 (type, "vpnc"))
-		return vpnc_secrets;
-	else if (   !g_strcmp0 (type, "openswan")
-	         || !g_strcmp0 (type, "libreswan")
-	         || !g_strcmp0 (type, "strongswan"))
-		return swan_secrets;
-	else if (!g_strcmp0 (type, "openconnect"))
-		return openconnect_secrets;
+
+#define _VPN_PASSWORD_LIST(...) \
+	({ \
+		static const NmcVpnPasswordName _arr[] = { \
+			__VA_ARGS__ \
+			{ 0 }, \
+		}; \
+		_arr; \
+	})
+
+	if (NM_IN_STRSET (type, "pptp",
+	                        "iodine",
+	                        "ssh",
+	                        "l2tp",
+	                        "fortisslvpn")) {
+		return _VPN_PASSWORD_LIST (
+			{ "password", N_("Password") },
+		);
+	}
+
+	if (NM_IN_STRSET (type, "openvpn")) {
+		return _VPN_PASSWORD_LIST (
+			{ "password",            N_("Password") },
+			{ "cert-pass",           N_("Certificate password") },
+			{ "http-proxy-password", N_("HTTP proxy password") },
+		);
+	}
+
+	if (NM_IN_STRSET (type, "vpnc")) {
+		return _VPN_PASSWORD_LIST (
+			{ "Xauth password", N_("Password") },
+			{ "IPSec secret",   N_("Group password") },
+		);
+	};
+
+	if (NM_IN_STRSET (type, "openswan",
+	                        "libreswan",
+	                        "strongswan")) {
+		return _VPN_PASSWORD_LIST (
+			{ "xauthpassword", N_("Password") },
+			{ "pskvalue",      N_("Group password") },
+		);
+	};
+
+	if (NM_IN_STRSET (type, "openconnect")) {
+		return _VPN_PASSWORD_LIST (
+			{ "gateway", N_("Gateway") },
+			{ "cookie",  N_("Cookie") },
+			{ "gwcert",  N_("Gateway certificate hash") },
+		);
+	};
+
 	return NULL;
 }
 
@@ -339,6 +349,7 @@ nm_vpn_wireguard_import (const char *filename,
 	gsize line_nr;
 	gsize current_peer_start_line_nr = 0;
 	nm_auto_unref_wgpeer NMWireGuardPeer *current_peer = NULL;
+	gs_unref_ptrarray GPtrArray *data_dns_search = NULL;
 	gs_unref_ptrarray GPtrArray *data_dns_v4 = NULL;
 	gs_unref_ptrarray GPtrArray *data_dns_v6 = NULL;
 	gs_unref_ptrarray GPtrArray *data_addr_v4 = NULL;
@@ -528,20 +539,24 @@ nm_vpn_wireguard_import (const char *filename,
 					NMIPAddr addr_bin;
 					int addr_family;
 
-					if (!nm_utils_parse_inaddr_bin (AF_UNSPEC,
-					                                value_word,
-					                                &addr_family,
-					                                &addr_bin))
-						goto fail_invalid_value;
-
-					p_data_dns =   (addr_family == AF_INET)
-					             ? &data_dns_v4
-					             : &data_dns_v6;
-					if (!*p_data_dns)
-						*p_data_dns = g_ptr_array_new_with_free_func (g_free);
-
-					g_ptr_array_add (*p_data_dns,
-					                 nm_utils_inet_ntop_dup (addr_family, &addr_bin));
+					if (nm_utils_parse_inaddr_bin (AF_UNSPEC,
+					                               value_word,
+					                               &addr_family,
+					                               &addr_bin)) {
+						p_data_dns =   (addr_family == AF_INET)
+						             ? &data_dns_v4
+						             : &data_dns_v6;
+						if (!*p_data_dns)
+							*p_data_dns = g_ptr_array_new_with_free_func (g_free);
+
+						g_ptr_array_add (*p_data_dns,
+						                 nm_utils_inet_ntop_dup (addr_family, &addr_bin));
+						continue;
+					}
+
+					if (!data_dns_search)
+						data_dns_search = g_ptr_array_new_with_free_func (g_free);
+					g_ptr_array_add (data_dns_search, g_strdup (value_word));
 				}
 				continue;
 			}
@@ -729,6 +744,7 @@ fail_invalid_secret:
 		NMSettingIPConfig *s_ip     = is_v4 ? s_ip4                                 : s_ip6;
 		GPtrArray *data_dns         = is_v4 ? data_dns_v4                           : data_dns_v6;
 		GPtrArray *data_addr        = is_v4 ? data_addr_v4                          : data_addr_v6;
+		GPtrArray *data_dns_search2 = data_dns_search;
 
 		if (data_dns && !data_addr) {
 			/* When specifying "DNS", we also require an "Address" for the same address
@@ -737,6 +753,7 @@ fail_invalid_secret:
 			 *
 			 * We don't have addresses. Silently ignore the DNS setting. */
 			data_dns = NULL;
+			data_dns_search2 = NULL;
 		}
 
 		g_object_set (s_ip,
@@ -760,9 +777,14 @@ fail_invalid_secret:
 			for (i = 0; i < data_dns->len; i++)
 				nm_setting_ip_config_add_dns (s_ip, data_dns->pdata[i]);
 
-			/* the wg-quick file cannot handle search domains. When configuring a DNS server
-			 * in the wg-quick file, assume that the user want to use it for all searches. */
-			nm_setting_ip_config_add_dns_search (s_ip, "~");
+			/* Of the wg-quick doesn't specify a search domain, assume the user
+			 * wants to use the domain server for all searches. */
+			if (!data_dns_search2)
+				nm_setting_ip_config_add_dns_search (s_ip, "~");
+		}
+		if (data_dns_search2) {
+			for (i = 0; i < data_dns_search2->len; i++)
+				nm_setting_ip_config_add_dns_search (s_ip, data_dns_search2->pdata[i]);
 		}
 
 		if (data_table == _TABLE_AUTO) {
diff --git a/clients/common/nm-vpn-helpers.h b/clients/common/nm-vpn-helpers.h
index 611f09dd..f401c634 100644
--- a/clients/common/nm-vpn-helpers.h
+++ b/clients/common/nm-vpn-helpers.h
@@ -9,7 +9,7 @@
 typedef struct {
 	const char *name;
 	const char *ui_name;
-} VpnPasswordName;
+} NmcVpnPasswordName;
 
 GSList *nm_vpn_get_plugin_infos (void);
 
@@ -17,7 +17,7 @@ NMVpnEditorPlugin *nm_vpn_get_editor_plugin (const char *service_type, GError **
 
 gboolean nm_vpn_supports_ipv6 (NMConnection *connection);
 
-const VpnPasswordName * nm_vpn_get_secret_names (const char *service_type);
+const NmcVpnPasswordName *nm_vpn_get_secret_names (const char *service_type);
 
 gboolean nm_vpn_openconnect_authenticate_helper (const char *host,
                                                  char **cookie,
diff --git a/clients/common/settings-docs.h b/clients/common/settings-docs.h
index 3d6df9b5..d9bb7575 100644
--- a/clients/common/settings-docs.h
+++ b/clients/common/settings-docs.h
@@ -119,10 +119,19 @@
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_HELLO_TIME N_("The Spanning Tree Protocol (STP) hello time, in seconds.")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_MAC_ADDRESS N_("If specified, the MAC address of bridge. When creating a new bridge, this MAC address will be set. If this field is left unspecified, the \"ethernet.cloned-mac-address\" is referred instead to generate the initial MAC address. Note that setting \"ethernet.cloned-mac-address\" anyway overwrites the MAC address of the bridge later while activating the bridge. Hence, this property is deprecated. Deprecated: 1")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_MAX_AGE N_("The Spanning Tree Protocol (STP) maximum message age, in seconds.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_HASH_MAX N_("Set maximum size of multicast hash table (value must be a power of 2).")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_COUNT N_("Set the number of queries the bridge will send before stopping forwarding a multicast group after a \"leave\" message has been received.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL N_("Set interval (in deciseconds) between queries to find remaining members of a group, after a \"leave\" message is received.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL N_("Set delay (in deciseconds) after which the bridge will leave a group, if no membership reports for this group are received.")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_QUERIER N_("Enable or disable sending of multicast queries by the bridge. If not specified the option is disabled.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_QUERIER_INTERVAL N_("If no queries are seen after this delay (in deciseconds) has passed, the bridge will start to send its own queries.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_QUERY_INTERVAL N_("Interval (in deciseconds) between queries sent by the bridge after the end of the startup phase.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL N_("Set the Max Response Time/Max Response Delay (in deciseconds) for IGMP/MLD queries sent by the bridge.")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_QUERY_USE_IFADDR N_("If enabled the bridge's own IP address is used as the source address for IGMP queries otherwise the default of 0.0.0.0 is used.")
-#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_ROUTER N_("Sets bridge's multicast router. multicast-snooping must be enabled for this option to work. Supported values are: 'auto', 'disabled', 'enabled'. If not specified the default value is 'auto'.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_ROUTER N_("Sets bridge's multicast router. Multicast-snooping must be enabled for this option to work. Supported values are: 'auto', 'disabled', 'enabled'. If not specified the default value is 'auto'.")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_SNOOPING N_("Controls whether IGMP snooping is enabled for this bridge. Note that if snooping was automatically disabled due to hash collisions, the system may refuse to enable the feature until the collisions are resolved.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT N_("Set the number of IGMP queries to send during startup phase.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL N_("Sets the time (in deciseconds) between queries sent out at startup to determine membership information.")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_PRIORITY N_("Sets the Spanning Tree Protocol (STP) priority for this bridge.  Lower values are \"better\"; the lowest priority bridge will be elected the root bridge.")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_STP N_("Controls whether Spanning Tree Protocol (STP) is enabled for this bridge.")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_VLAN_DEFAULT_PVID N_("The default PVID for the ports of the bridge, that is the VLAN id assigned to incoming untagged frames.")
@@ -152,6 +161,7 @@
 #define DESCRIBE_DOC_NM_SETTING_CONNECTION_MASTER N_("Interface name of the master device or UUID of the master connection.")
 #define DESCRIBE_DOC_NM_SETTING_CONNECTION_MDNS N_("Whether mDNS is enabled for the connection. The permitted values are: \"yes\" (2) register hostname and resolving for the connection, \"no\" (0) disable mDNS for the interface, \"resolve\" (1) do not register hostname but allow resolving of mDNS host names and \"default\" (-1) to allow lookup of a global default in NetworkManager.conf. If unspecified, \"default\" ultimately depends on the DNS plugin (which for systemd-resolved currently means \"no\"). This feature requires a plugin which supports mDNS. Otherwise the setting has no effect. One such plugin is dns-systemd-resolved.")
 #define DESCRIBE_DOC_NM_SETTING_CONNECTION_METERED N_("Whether the connection is metered. When updating this property on a currently activated connection, the change takes effect immediately.")
+#define DESCRIBE_DOC_NM_SETTING_CONNECTION_MUD_URL N_("If configured, set to a Manufacturer Usage Description (MUD) URL that points to manufacturer-recommended network policies for IoT devices. It is transmitted as a DHCPv4 or DHCPv6 option. The value must be a valid URL starting with \"https://\". The special value \"none\" is allowed to indicate that no MUD URL is used. If the per-profile value is unspecified (the default), a global connection default gets consulted. If still unspecified, the ultimate default is \"none\".")
 #define DESCRIBE_DOC_NM_SETTING_CONNECTION_MULTI_CONNECT N_("Specifies whether the profile can be active multiple times at a particular moment. The value is of type NMConnectionMultiConnect.")
 #define DESCRIBE_DOC_NM_SETTING_CONNECTION_PERMISSIONS N_("An array of strings defining what access a given user has to this connection.  If this is NULL or empty, all users are allowed to access this connection; otherwise users are allowed if and only if they are in this list.  When this is not empty, the connection can be active only when one of the specified users is logged into an active session.  Each entry is of the form \"[type]:[id]:[reserved]\"; for example, \"user:dcbw:blah\". At this time only the \"user\" [type] is allowed.  Any other values are ignored and reserved for future use.  [id] is the username that this permission refers to, which may not contain the \":\" character. Any [reserved] information present must be ignored and is reserved for future use.  All of [type], [id], and [reserved] must be valid UTF-8.")
 #define DESCRIBE_DOC_NM_SETTING_CONNECTION_READ_ONLY N_("FALSE if the connection can be modified using the provided settings service's D-Bus interface with the right privileges, or TRUE if the connection is read-only and cannot be modified.")
@@ -270,7 +280,10 @@
 #define DESCRIBE_DOC_NM_SETTING_MACVLAN_PARENT N_("If given, specifies the parent interface name or parent connection UUID from which this MAC-VLAN interface should be created.  If this property is not specified, the connection must contain an \"802-3-ethernet\" setting with a \"mac-address\" property.")
 #define DESCRIBE_DOC_NM_SETTING_MACVLAN_PROMISCUOUS N_("Whether the interface should be put in promiscuous mode.")
 #define DESCRIBE_DOC_NM_SETTING_MACVLAN_TAP N_("Whether the interface should be a MACVTAP.")
+#define DESCRIBE_DOC_NM_SETTING_MATCH_DRIVER N_("A list of driver names to match. Each element is a shell wildcard pattern. When an element is prefixed with exclamation mark (!) the condition is inverted. A candidate driver name is considered matching when both these conditions are satisfied: (a) any of the elements not prefixed with '!' matches or there aren't such elements; (b) none of the elements prefixed with '!' match.")
 #define DESCRIBE_DOC_NM_SETTING_MATCH_INTERFACE_NAME N_("A list of interface names to match. Each element is a shell wildcard pattern.  When an element is prefixed with exclamation mark (!) the condition is inverted. A candidate interface name is considered matching when both these conditions are satisfied: (a) any of the elements not prefixed with '!' matches or there aren't such elements; (b) none of the elements prefixed with '!' match.")
+#define DESCRIBE_DOC_NM_SETTING_MATCH_KERNEL_COMMAND_LINE N_("A list of kernel command line arguments to match. This may be used to check whether a specific kernel command line option is set (or if prefixed with the exclamation mark unset). The argument must either be a single word, or an assignment (i.e. two words, separated \"=\"). In the former case the kernel command line is searched for the word appearing as is, or as left hand side of an assignment. In the latter case, the exact assignment is looked for with right and left hand side matching.")
+#define DESCRIBE_DOC_NM_SETTING_MATCH_PATH N_("A list of paths to match against the ID_PATH udev property of devices. ID_PATH represents the topological persistent path of a device. It typically contains a subsystem string (pci, usb, platform, etc.) and a subsystem-specific identifier. For PCI devices the path has the form \"pci-$domain:$bus:$device.$function\", where each variable is an hexadecimal value; for example \"pci-0000:0a:00.0\". The path of a device can be obtained with \"udevadm info /sys/class/net/$dev | grep ID_PATH=\" or by looking at the \"path\" property exported by NetworkManager (\"nmcli -f general.path device show $dev\"). Each element of the list is a shell wildcard pattern. When an element is prefixed with exclamation mark (!) the condition is inverted. A candidate path is considered matching when both these conditions are satisfied: (a) any of the elements not prefixed with '!' matches or there aren't such elements; (b) none of the elements prefixed with '!' match.")
 #define DESCRIBE_DOC_NM_SETTING_OVS_BRIDGE_DATAPATH_TYPE N_("The data path type. One of \"system\", \"netdev\" or empty.")
 #define DESCRIBE_DOC_NM_SETTING_OVS_BRIDGE_FAIL_MODE N_("The bridge failure mode. One of \"secure\", \"standalone\" or empty.")
 #define DESCRIBE_DOC_NM_SETTING_OVS_BRIDGE_MCAST_SNOOPING_ENABLE N_("Enable or disable multicast snooping.")
@@ -278,7 +291,7 @@
 #define DESCRIBE_DOC_NM_SETTING_OVS_BRIDGE_STP_ENABLE N_("Enable or disable STP.")
 #define DESCRIBE_DOC_NM_SETTING_OVS_DPDK_DEVARGS N_("Open vSwitch DPDK device arguments.")
 #define DESCRIBE_DOC_NM_SETTING_OVS_INTERFACE_TYPE N_("The interface type. Either \"internal\", \"system\", \"patch\", \"dpdk\", or empty.")
-#define DESCRIBE_DOC_NM_SETTING_OVS_PATCH_PEER N_("Specifies the unicast destination IP address of a remote Open vSwitch bridge port to connect to.")
+#define DESCRIBE_DOC_NM_SETTING_OVS_PATCH_PEER N_("Specifies the name of the interface for the other side of the patch. The patch on the other side must also set this interface as peer.")
 #define DESCRIBE_DOC_NM_SETTING_OVS_PORT_BOND_DOWNDELAY N_("The time port must be inactive in order to be considered down.")
 #define DESCRIBE_DOC_NM_SETTING_OVS_PORT_BOND_MODE N_("Bonding mode. One of \"active-backup\", \"balance-slb\", or \"balance-tcp\".")
 #define DESCRIBE_DOC_NM_SETTING_OVS_PORT_BOND_UPDELAY N_("The time port must be active before it starts forwarding traffic.")
diff --git a/clients/common/settings-docs.h.in b/clients/common/settings-docs.h.in
index 3d6df9b5..d9bb7575 100644
--- a/clients/common/settings-docs.h.in
+++ b/clients/common/settings-docs.h.in
@@ -119,10 +119,19 @@
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_HELLO_TIME N_("The Spanning Tree Protocol (STP) hello time, in seconds.")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_MAC_ADDRESS N_("If specified, the MAC address of bridge. When creating a new bridge, this MAC address will be set. If this field is left unspecified, the \"ethernet.cloned-mac-address\" is referred instead to generate the initial MAC address. Note that setting \"ethernet.cloned-mac-address\" anyway overwrites the MAC address of the bridge later while activating the bridge. Hence, this property is deprecated. Deprecated: 1")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_MAX_AGE N_("The Spanning Tree Protocol (STP) maximum message age, in seconds.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_HASH_MAX N_("Set maximum size of multicast hash table (value must be a power of 2).")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_COUNT N_("Set the number of queries the bridge will send before stopping forwarding a multicast group after a \"leave\" message has been received.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL N_("Set interval (in deciseconds) between queries to find remaining members of a group, after a \"leave\" message is received.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL N_("Set delay (in deciseconds) after which the bridge will leave a group, if no membership reports for this group are received.")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_QUERIER N_("Enable or disable sending of multicast queries by the bridge. If not specified the option is disabled.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_QUERIER_INTERVAL N_("If no queries are seen after this delay (in deciseconds) has passed, the bridge will start to send its own queries.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_QUERY_INTERVAL N_("Interval (in deciseconds) between queries sent by the bridge after the end of the startup phase.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL N_("Set the Max Response Time/Max Response Delay (in deciseconds) for IGMP/MLD queries sent by the bridge.")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_QUERY_USE_IFADDR N_("If enabled the bridge's own IP address is used as the source address for IGMP queries otherwise the default of 0.0.0.0 is used.")
-#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_ROUTER N_("Sets bridge's multicast router. multicast-snooping must be enabled for this option to work. Supported values are: 'auto', 'disabled', 'enabled'. If not specified the default value is 'auto'.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_ROUTER N_("Sets bridge's multicast router. Multicast-snooping must be enabled for this option to work. Supported values are: 'auto', 'disabled', 'enabled'. If not specified the default value is 'auto'.")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_SNOOPING N_("Controls whether IGMP snooping is enabled for this bridge. Note that if snooping was automatically disabled due to hash collisions, the system may refuse to enable the feature until the collisions are resolved.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT N_("Set the number of IGMP queries to send during startup phase.")
+#define DESCRIBE_DOC_NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL N_("Sets the time (in deciseconds) between queries sent out at startup to determine membership information.")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_PRIORITY N_("Sets the Spanning Tree Protocol (STP) priority for this bridge.  Lower values are \"better\"; the lowest priority bridge will be elected the root bridge.")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_STP N_("Controls whether Spanning Tree Protocol (STP) is enabled for this bridge.")
 #define DESCRIBE_DOC_NM_SETTING_BRIDGE_VLAN_DEFAULT_PVID N_("The default PVID for the ports of the bridge, that is the VLAN id assigned to incoming untagged frames.")
@@ -152,6 +161,7 @@
 #define DESCRIBE_DOC_NM_SETTING_CONNECTION_MASTER N_("Interface name of the master device or UUID of the master connection.")
 #define DESCRIBE_DOC_NM_SETTING_CONNECTION_MDNS N_("Whether mDNS is enabled for the connection. The permitted values are: \"yes\" (2) register hostname and resolving for the connection, \"no\" (0) disable mDNS for the interface, \"resolve\" (1) do not register hostname but allow resolving of mDNS host names and \"default\" (-1) to allow lookup of a global default in NetworkManager.conf. If unspecified, \"default\" ultimately depends on the DNS plugin (which for systemd-resolved currently means \"no\"). This feature requires a plugin which supports mDNS. Otherwise the setting has no effect. One such plugin is dns-systemd-resolved.")
 #define DESCRIBE_DOC_NM_SETTING_CONNECTION_METERED N_("Whether the connection is metered. When updating this property on a currently activated connection, the change takes effect immediately.")
+#define DESCRIBE_DOC_NM_SETTING_CONNECTION_MUD_URL N_("If configured, set to a Manufacturer Usage Description (MUD) URL that points to manufacturer-recommended network policies for IoT devices. It is transmitted as a DHCPv4 or DHCPv6 option. The value must be a valid URL starting with \"https://\". The special value \"none\" is allowed to indicate that no MUD URL is used. If the per-profile value is unspecified (the default), a global connection default gets consulted. If still unspecified, the ultimate default is \"none\".")
 #define DESCRIBE_DOC_NM_SETTING_CONNECTION_MULTI_CONNECT N_("Specifies whether the profile can be active multiple times at a particular moment. The value is of type NMConnectionMultiConnect.")
 #define DESCRIBE_DOC_NM_SETTING_CONNECTION_PERMISSIONS N_("An array of strings defining what access a given user has to this connection.  If this is NULL or empty, all users are allowed to access this connection; otherwise users are allowed if and only if they are in this list.  When this is not empty, the connection can be active only when one of the specified users is logged into an active session.  Each entry is of the form \"[type]:[id]:[reserved]\"; for example, \"user:dcbw:blah\". At this time only the \"user\" [type] is allowed.  Any other values are ignored and reserved for future use.  [id] is the username that this permission refers to, which may not contain the \":\" character. Any [reserved] information present must be ignored and is reserved for future use.  All of [type], [id], and [reserved] must be valid UTF-8.")
 #define DESCRIBE_DOC_NM_SETTING_CONNECTION_READ_ONLY N_("FALSE if the connection can be modified using the provided settings service's D-Bus interface with the right privileges, or TRUE if the connection is read-only and cannot be modified.")
@@ -270,7 +280,10 @@
 #define DESCRIBE_DOC_NM_SETTING_MACVLAN_PARENT N_("If given, specifies the parent interface name or parent connection UUID from which this MAC-VLAN interface should be created.  If this property is not specified, the connection must contain an \"802-3-ethernet\" setting with a \"mac-address\" property.")
 #define DESCRIBE_DOC_NM_SETTING_MACVLAN_PROMISCUOUS N_("Whether the interface should be put in promiscuous mode.")
 #define DESCRIBE_DOC_NM_SETTING_MACVLAN_TAP N_("Whether the interface should be a MACVTAP.")
+#define DESCRIBE_DOC_NM_SETTING_MATCH_DRIVER N_("A list of driver names to match. Each element is a shell wildcard pattern. When an element is prefixed with exclamation mark (!) the condition is inverted. A candidate driver name is considered matching when both these conditions are satisfied: (a) any of the elements not prefixed with '!' matches or there aren't such elements; (b) none of the elements prefixed with '!' match.")
 #define DESCRIBE_DOC_NM_SETTING_MATCH_INTERFACE_NAME N_("A list of interface names to match. Each element is a shell wildcard pattern.  When an element is prefixed with exclamation mark (!) the condition is inverted. A candidate interface name is considered matching when both these conditions are satisfied: (a) any of the elements not prefixed with '!' matches or there aren't such elements; (b) none of the elements prefixed with '!' match.")
+#define DESCRIBE_DOC_NM_SETTING_MATCH_KERNEL_COMMAND_LINE N_("A list of kernel command line arguments to match. This may be used to check whether a specific kernel command line option is set (or if prefixed with the exclamation mark unset). The argument must either be a single word, or an assignment (i.e. two words, separated \"=\"). In the former case the kernel command line is searched for the word appearing as is, or as left hand side of an assignment. In the latter case, the exact assignment is looked for with right and left hand side matching.")
+#define DESCRIBE_DOC_NM_SETTING_MATCH_PATH N_("A list of paths to match against the ID_PATH udev property of devices. ID_PATH represents the topological persistent path of a device. It typically contains a subsystem string (pci, usb, platform, etc.) and a subsystem-specific identifier. For PCI devices the path has the form \"pci-$domain:$bus:$device.$function\", where each variable is an hexadecimal value; for example \"pci-0000:0a:00.0\". The path of a device can be obtained with \"udevadm info /sys/class/net/$dev | grep ID_PATH=\" or by looking at the \"path\" property exported by NetworkManager (\"nmcli -f general.path device show $dev\"). Each element of the list is a shell wildcard pattern. When an element is prefixed with exclamation mark (!) the condition is inverted. A candidate path is considered matching when both these conditions are satisfied: (a) any of the elements not prefixed with '!' matches or there aren't such elements; (b) none of the elements prefixed with '!' match.")
 #define DESCRIBE_DOC_NM_SETTING_OVS_BRIDGE_DATAPATH_TYPE N_("The data path type. One of \"system\", \"netdev\" or empty.")
 #define DESCRIBE_DOC_NM_SETTING_OVS_BRIDGE_FAIL_MODE N_("The bridge failure mode. One of \"secure\", \"standalone\" or empty.")
 #define DESCRIBE_DOC_NM_SETTING_OVS_BRIDGE_MCAST_SNOOPING_ENABLE N_("Enable or disable multicast snooping.")
@@ -278,7 +291,7 @@
 #define DESCRIBE_DOC_NM_SETTING_OVS_BRIDGE_STP_ENABLE N_("Enable or disable STP.")
 #define DESCRIBE_DOC_NM_SETTING_OVS_DPDK_DEVARGS N_("Open vSwitch DPDK device arguments.")
 #define DESCRIBE_DOC_NM_SETTING_OVS_INTERFACE_TYPE N_("The interface type. Either \"internal\", \"system\", \"patch\", \"dpdk\", or empty.")
-#define DESCRIBE_DOC_NM_SETTING_OVS_PATCH_PEER N_("Specifies the unicast destination IP address of a remote Open vSwitch bridge port to connect to.")
+#define DESCRIBE_DOC_NM_SETTING_OVS_PATCH_PEER N_("Specifies the name of the interface for the other side of the patch. The patch on the other side must also set this interface as peer.")
 #define DESCRIBE_DOC_NM_SETTING_OVS_PORT_BOND_DOWNDELAY N_("The time port must be inactive in order to be considered down.")
 #define DESCRIBE_DOC_NM_SETTING_OVS_PORT_BOND_MODE N_("Bonding mode. One of \"active-backup\", \"balance-slb\", or \"balance-tcp\".")
 #define DESCRIBE_DOC_NM_SETTING_OVS_PORT_BOND_UPDELAY N_("The time port must be active before it starts forwarding traffic.")
diff --git a/clients/common/tests/test-clients-common.c b/clients/common/tests/test-clients-common.c
index 70ee2cb1..0b61e7bb 100644
--- a/clients/common/tests/test-clients-common.c
+++ b/clients/common/tests/test-clients-common.c
@@ -7,6 +7,7 @@
 
 #include "nm-meta-setting-access.h"
 #include "nm-vpn-helpers.h"
+#include "nm-client-utils.h"
 
 #include "nm-utils/nm-test-utils.h"
 
@@ -235,6 +236,77 @@ test_client_import_wireguard_missing (void)
 
 /*****************************************************************************/
 
+#define _do_test_parse_passwd_file(contents, success, exp_error_line, ...) \
+	G_STMT_START { \
+		static const NMUtilsNamedValue _values[] = { \
+			__VA_ARGS__ \
+		}; \
+		gs_free char *_contents = g_strndup (contents, NM_STRLEN (contents)); \
+		gs_unref_hashtable GHashTable *_secrets = NULL; \
+		gs_free_error GError *_local = NULL; \
+		gssize _error_line; \
+		GError **_p_local = nmtst_get_rand_bool () ? &_local : NULL; \
+		gssize *_p_error_line = nmtst_get_rand_bool () ? &_error_line : NULL; \
+		gboolean _success = !!(success); \
+		gssize _exp_error_line = (exp_error_line); \
+		int _i; \
+		\
+		g_assert (_success || (G_N_ELEMENTS (_values) == 0)); \
+		\
+		_secrets = nmc_utils_parse_passwd_file (_contents, _p_error_line, _p_local); \
+		\
+		g_assert (_success == (!!_secrets)); \
+		if (!_success) { \
+			if (_p_error_line) \
+				g_assert_cmpint (_exp_error_line, ==, *_p_error_line); \
+			if (_p_local) \
+				g_assert (_local); \
+		} else { \
+			if (_p_error_line) \
+				g_assert_cmpint (-1, ==, *_p_error_line); \
+			g_assert (!_local); \
+			\
+			for (_i = 0; _i < G_N_ELEMENTS (_values); _i++) { \
+				const NMUtilsNamedValue *_n = &_values[_i]; \
+				const char *_v; \
+				\
+				_v = g_hash_table_lookup (_secrets, _n->name); \
+				if (!_v) \
+					g_error ("cannot find key \"%s\"", _n->name); \
+				g_assert_cmpstr (_v, ==, _n->value_str); \
+			} \
+			\
+			g_assert_cmpint (g_hash_table_size (_secrets), ==, G_N_ELEMENTS (_values)); \
+		} \
+	} G_STMT_END
+
+#define _do_test_parse_passwd_file_bad( contents, exp_error_line) _do_test_parse_passwd_file (contents, FALSE, exp_error_line)
+#define _do_test_parse_passwd_file_good(contents, ...)            _do_test_parse_passwd_file (contents, TRUE,  -1, __VA_ARGS__)
+
+static void
+test_nmc_utils_parse_passwd_file (void)
+{
+	_do_test_parse_passwd_file_good ("");
+	_do_test_parse_passwd_file_bad ("x", 1);
+	_do_test_parse_passwd_file_bad ("\r\rx", 3);
+	_do_test_parse_passwd_file_good ("wifi.psk=abc",
+	                                 NM_UTILS_NAMED_VALUE_INIT ("802-11-wireless-security.psk", "abc") );
+	_do_test_parse_passwd_file_good ("wifi.psk:ABC\r"
+	                                 "wifi-sec.psk = abc ",
+	                                 NM_UTILS_NAMED_VALUE_INIT ("802-11-wireless-security.psk", "abc") );
+	_do_test_parse_passwd_file_good ("wifi.psk:  abc\r"
+	                                 "wifi-sec.psk2 = d\\145f\r\n"
+	                                 "  wifi.psk3 = e\\  \n"
+	                                 "  #wifi-sec.psk2 = \r\n"
+	                                 "  wifi-sec.psk4:",
+	                                 NM_UTILS_NAMED_VALUE_INIT ("802-11-wireless-security.psk",  "abc"),
+	                                 NM_UTILS_NAMED_VALUE_INIT ("802-11-wireless-security.psk2", "def"),
+	                                 NM_UTILS_NAMED_VALUE_INIT ("802-11-wireless-security.psk3", "e "),
+	                                 NM_UTILS_NAMED_VALUE_INIT ("802-11-wireless-security.psk4", "") );
+}
+
+/*****************************************************************************/
+
 NMTST_DEFINE ();
 
 int
@@ -248,6 +320,7 @@ main (int argc, char **argv)
 	g_test_add_func ("/client/import/wireguard/test2", test_client_import_wireguard_test2);
 	g_test_add_func ("/client/import/wireguard/test3", test_client_import_wireguard_test3);
 	g_test_add_func ("/client/import/wireguard/missing", test_client_import_wireguard_missing);
+	g_test_add_func ("/client/test_nmc_utils_parse_passwd_file", test_nmc_utils_parse_passwd_file);
 
 	return g_test_run ();
 }