summary refs log tree commit diff
path: root/libnm-core
diff options
context:
space:
mode:
Diffstat (limited to 'libnm-core')
-rw-r--r--libnm-core/meson.build15
-rw-r--r--libnm-core/nm-core-internal.h111
-rw-r--r--libnm-core/nm-core-types.h3
-rw-r--r--libnm-core/nm-crypto-nss.c2
-rw-r--r--libnm-core/nm-dbus-interface.h12
-rw-r--r--libnm-core/nm-dbus-types.xml15
-rw-r--r--libnm-core/nm-keyfile/nm-keyfile-internal.h212
-rw-r--r--libnm-core/nm-keyfile/nm-keyfile-utils.c701
-rw-r--r--libnm-core/nm-keyfile/nm-keyfile-utils.h73
-rw-r--r--libnm-core/nm-keyfile/nm-keyfile.c4445
-rw-r--r--libnm-core/nm-libnm-core-aux/README.md21
-rw-r--r--libnm-core/nm-libnm-core-aux/nm-dispatcher-api.h45
-rw-r--r--libnm-core/nm-libnm-core-aux/nm-libnm-core-aux.c358
-rw-r--r--libnm-core/nm-libnm-core-aux/nm-libnm-core-aux.h39
-rw-r--r--libnm-core/nm-libnm-core-intern/README.md21
-rw-r--r--libnm-core/nm-libnm-core-intern/nm-auth-subject.c446
-rw-r--r--libnm-core/nm-libnm-core-intern/nm-auth-subject.h56
-rw-r--r--libnm-core/nm-libnm-core-intern/nm-common-macros.h61
-rw-r--r--libnm-core/nm-libnm-core-intern/nm-ethtool-utils.c276
-rw-r--r--libnm-core/nm-libnm-core-intern/nm-ethtool-utils.h163
-rw-r--r--libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.c180
-rw-r--r--libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.h115
-rw-r--r--libnm-core/nm-setting-bridge-port.c16
-rw-r--r--libnm-core/nm-setting-bridge.c544
-rw-r--r--libnm-core/nm-setting-bridge.h72
-rw-r--r--libnm-core/nm-setting-connection.c83
-rw-r--r--libnm-core/nm-setting-connection.h4
-rw-r--r--libnm-core/nm-setting-ethtool.c221
-rw-r--r--libnm-core/nm-setting-ethtool.h46
-rw-r--r--libnm-core/nm-setting-ip-config.c2
-rw-r--r--libnm-core/nm-setting-match.c670
-rw-r--r--libnm-core/nm-setting-match.h60
-rw-r--r--libnm-core/nm-setting-ovs-patch.c12
-rw-r--r--libnm-core/nm-setting-wireless.c4
-rw-r--r--libnm-core/nm-setting.c385
-rw-r--r--libnm-core/nm-setting.h40
-rw-r--r--libnm-core/nm-utils-private.h5
-rw-r--r--libnm-core/nm-utils.c176
-rw-r--r--libnm-core/nm-version.h14
-rw-r--r--libnm-core/tests/test-general.c13
-rw-r--r--libnm-core/tests/test-keyfile.c4
-rw-r--r--libnm-core/tests/test-setting.c319
42 files changed, 9561 insertions, 499 deletions
diff --git a/libnm-core/meson.build b/libnm-core/meson.build
index 955e1776..89acaf72 100644
--- a/libnm-core/meson.build
+++ b/libnm-core/meson.build
@@ -88,7 +88,11 @@ libnm_core_enum_sources = gnome.mkenums_simple(
 
 libnm_libnm_core_intern = static_library(
   'nm-libnm-core-intern',
-  sources: nm_ethtool_utils_source + nm_libnm_core_utils_source + nm_auth_subject_source + [libnm_core_enum_sources[1]],
+  sources: files(
+    'nm-libnm-core-intern/nm-auth-subject.c',
+    'nm-libnm-core-intern/nm-ethtool-utils.c',
+    'nm-libnm-core-intern/nm-libnm-core-utils.c',
+  ) + [libnm_core_enum_sources[1]],
   dependencies: libnm_core_nm_default_dep,
   c_args: common_c_flags,
 )
@@ -221,7 +225,9 @@ c_flags = [
 
 libnm_libnm_core_aux = static_library(
   'nm-libnm-core-aux',
-  sources: nm_libnm_core_aux_source + [libnm_core_enum_sources[1]],
+  sources: files(
+    'nm-libnm-core-aux/nm-libnm-core-aux.c',
+  ) + [libnm_core_enum_sources[1]],
   dependencies: libnm_utils_base_dep,
   c_args: c_flags,
   link_with: libnm_libnm_core_intern,
@@ -236,7 +242,10 @@ libnm_libnm_core_aux_dep = declare_dependency(
 
 libnm_keyfile = static_library(
   'nm-keyfile',
-  sources: nm_keyfile_source + [libnm_core_enum_sources[1]],
+  sources: files(
+    'nm-keyfile/nm-keyfile-utils.c',
+    'nm-keyfile/nm-keyfile.c',
+  ) + [libnm_core_enum_sources[1]],
   dependencies: libnm_utils_base_dep,
   c_args: [
     '-DG_LOG_DOMAIN="@0@"'.format(libnm_name),
diff --git a/libnm-core/nm-core-internal.h b/libnm-core/nm-core-internal.h
index b9abac40..6d7661b9 100644
--- a/libnm-core/nm-core-internal.h
+++ b/libnm-core/nm-core-internal.h
@@ -78,24 +78,82 @@
 #include "nm-libnm-core-intern/nm-libnm-core-utils.h"
 
 /* IEEE 802.1D-1998 timer values */
-#define NM_BR_MIN_HELLO_TIME    1
-#define NM_BR_MAX_HELLO_TIME    10
+#define NM_BRIDGE_HELLO_TIME_MIN 1u
+#define NM_BRIDGE_HELLO_TIME_DEF 2u
+#define NM_BRIDGE_HELLO_TIME_MAX 10u
 
-#define NM_BR_MIN_FORWARD_DELAY 2
-#define NM_BR_MAX_FORWARD_DELAY 30
+#define NM_BRIDGE_FORWARD_DELAY_MIN 2u
+#define NM_BRIDGE_FORWARD_DELAY_DEF 15u
+#define NM_BRIDGE_FORWARD_DELAY_MAX 30u
 
-#define NM_BR_MIN_MAX_AGE       6
-#define NM_BR_MAX_MAX_AGE       40
+#define NM_BRIDGE_MAX_AGE_MIN 6u
+#define NM_BRIDGE_MAX_AGE_DEF 20u
+#define NM_BRIDGE_MAX_AGE_MAX 40u
 
 /* IEEE 802.1D-1998 Table 7.4 */
-#define NM_BR_MIN_AGEING_TIME   0
-#define NM_BR_MAX_AGEING_TIME   1000000
+#define NM_BRIDGE_AGEING_TIME_MIN 0u
+#define NM_BRIDGE_AGEING_TIME_DEF 300u
+#define NM_BRIDGE_AGEING_TIME_MAX 1000000u
 
-#define NM_BR_PORT_MAX_PRIORITY 63
-#define NM_BR_PORT_DEF_PRIORITY 32
+#define NM_BRIDGE_PORT_PRIORITY_MIN 0u
+#define NM_BRIDGE_PORT_PRIORITY_DEF 32u
+#define NM_BRIDGE_PORT_PRIORITY_MAX 63u
 
-#define NM_BR_PORT_MAX_PATH_COST 65535
-#define NM_BR_PORT_DEF_PATH_COST 100
+#define NM_BRIDGE_PORT_PATH_COST_MIN 0u
+#define NM_BRIDGE_PORT_PATH_COST_DEF 100u
+#define NM_BRIDGE_PORT_PATH_COST_MAX 65535u
+
+#define NM_BRIDGE_MULTICAST_HASH_MAX_MIN 1u
+#define NM_BRIDGE_MULTICAST_HASH_MAX_DEF 4096u
+#define NM_BRIDGE_MULTICAST_HASH_MAX_MAX ((guint) G_MAXUINT32)
+
+#define NM_BRIDGE_STP_DEF TRUE
+
+#define NM_BRIDGE_PRIORITY_MIN 0u
+#define NM_BRIDGE_PRIORITY_DEF 0x8000u
+#define NM_BRIDGE_PRIORITY_MAX ((guint) G_MAXUINT16)
+
+#define NM_BRIDGE_MULTICAST_LAST_MEMBER_COUNT_MIN 0u
+#define NM_BRIDGE_MULTICAST_LAST_MEMBER_COUNT_DEF 2u
+#define NM_BRIDGE_MULTICAST_LAST_MEMBER_COUNT_MAX ((guint) G_MAXUINT32)
+
+#define NM_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL_MIN ((guint64) 0)
+#define NM_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL_DEF ((guint64) 100)
+#define NM_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL_MAX G_MAXUINT64
+
+#define NM_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL_MIN ((guint64) 0)
+#define NM_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL_DEF ((guint64) 26000)
+#define NM_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL_MAX G_MAXUINT64
+
+#define NM_BRIDGE_MULTICAST_QUERIER_INTERVAL_MIN ((guint64) 0)
+#define NM_BRIDGE_MULTICAST_QUERIER_INTERVAL_DEF ((guint64) 25500)
+#define NM_BRIDGE_MULTICAST_QUERIER_INTERVAL_MAX G_MAXUINT64
+
+#define NM_BRIDGE_MULTICAST_QUERIER_DEF FALSE
+
+#define NM_BRIDGE_MULTICAST_QUERY_INTERVAL_MIN ((guint64) 0)
+#define NM_BRIDGE_MULTICAST_QUERY_INTERVAL_DEF ((guint64) 12500)
+#define NM_BRIDGE_MULTICAST_QUERY_INTERVAL_MAX G_MAXUINT64
+
+#define NM_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL_MIN ((guint64) 0)
+#define NM_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL_DEF ((guint64) 1000)
+#define NM_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL_MAX G_MAXUINT64
+
+#define NM_BRIDGE_MULTICAST_QUERY_USE_IFADDR_DEF FALSE
+
+#define NM_BRIDGE_MULTICAST_SNOOPING_DEF TRUE
+
+#define NM_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT_MIN 0u
+#define NM_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT_DEF 2u
+#define NM_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT_MAX ((guint) G_MAXUINT32)
+
+#define NM_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL_MIN ((guint64) 0)
+#define NM_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL_DEF ((guint64) 3125)
+#define NM_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL_MAX G_MAXUINT64
+
+#define NM_BRIDGE_VLAN_STATS_ENABLED_DEF FALSE
+
+#define NM_BRIDGE_VLAN_DEFAULT_PVID_DEF 1u
 
 /* NM_SETTING_COMPARE_FLAG_INFERRABLE: check whether a device-generated
  * connection can be replaced by a already-defined connection. This flag only
@@ -252,29 +310,18 @@ gboolean _nm_setting_get_property (NMSetting *setting, const char *name, GValue
 
 /*****************************************************************************/
 
-GHashTable *_nm_setting_gendata_hash (NMSetting *setting,
-                                      gboolean create_if_necessary);
-
-void _nm_setting_gendata_notify (NMSetting *setting,
-                                 gboolean keys_changed);
-
-guint _nm_setting_gendata_get_all (NMSetting *setting,
-                                   const char *const**out_names,
-                                   GVariant *const**out_values);
-
-gboolean _nm_setting_gendata_reset_from_hash (NMSetting *setting,
-                                              GHashTable *new);
-
-void _nm_setting_gendata_to_gvalue (NMSetting *setting,
-                                    GValue *value);
+GHashTable *_nm_setting_option_hash (NMSetting *setting,
+                                     gboolean create_if_necessary);
 
-GVariant *nm_setting_gendata_get (NMSetting *setting,
-                                  const char *name);
+void _nm_setting_option_notify (NMSetting *setting,
+                                gboolean keys_changed);
 
-const char *const*nm_setting_gendata_get_all_names (NMSetting *setting,
-                                                    guint *out_len);
+guint _nm_setting_option_get_all (NMSetting *setting,
+                                  const char *const**out_names,
+                                  GVariant *const**out_values);
 
-GVariant *const*nm_setting_gendata_get_all_values (NMSetting *setting);
+gboolean _nm_setting_option_clear (NMSetting *setting,
+                                   const char *optname);
 
 /*****************************************************************************/
 
diff --git a/libnm-core/nm-core-types.h b/libnm-core/nm-core-types.h
index d1ce6310..ce59ec03 100644
--- a/libnm-core/nm-core-types.h
+++ b/libnm-core/nm-core-types.h
@@ -65,4 +65,7 @@ typedef struct _NMSettingWirelessSecurity NMSettingWirelessSecurity;
 typedef struct _NMSettingWpan             NMSettingWpan;
 typedef struct _NMSimpleConnection        NMSimpleConnection;
 
+NM_AVAILABLE_IN_1_26
+typedef gboolean (*NMUtilsPredicateStr) (const char *str);
+
 #endif  /* __NM_CORE_TYPES_H__ */
diff --git a/libnm-core/nm-crypto-nss.c b/libnm-core/nm-crypto-nss.c
index 60c8dfb5..5dbbef00 100644
--- a/libnm-core/nm-crypto-nss.c
+++ b/libnm-core/nm-crypto-nss.c
@@ -8,6 +8,7 @@
 
 #include "nm-crypto-impl.h"
 
+NM_PRAGMA_WARNING_DISABLE ("-Wstrict-prototypes")
 #include <prinit.h>
 #include <nss.h>
 #include <pk11pub.h>
@@ -17,6 +18,7 @@
 #include <p12.h>
 #include <ciferfam.h>
 #include <p12plcy.h>
+NM_PRAGMA_WARNING_REENABLE
 
 #include "nm-glib-aux/nm-secret-utils.h"
 #include "nm-errors.h"
diff --git a/libnm-core/nm-dbus-interface.h b/libnm-core/nm-dbus-interface.h
index 28ee89bf..4c85dae4 100644
--- a/libnm-core/nm-dbus-interface.h
+++ b/libnm-core/nm-dbus-interface.h
@@ -351,6 +351,8 @@ typedef enum { /*< underscore_name=nm_802_11_ap_flags, flags >*/
  * supported
  * @NM_802_11_AP_SEC_KEY_MGMT_OWE: WPA/RSN Opportunistic Wireless Encryption is
  * supported
+ * @NM_802_11_AP_SEC_KEY_MGMT_OWE_TM: WPA/RSN Opportunistic Wireless Encryption
+ * transition mode is supported. Since: 1.26.
  *
  * 802.11 access point security and authentication flags.  These flags describe
  * the current security requirements of an access point as determined from the
@@ -370,6 +372,7 @@ typedef enum { /*< underscore_name=nm_802_11_ap_security_flags, flags >*/
 	NM_802_11_AP_SEC_KEY_MGMT_802_1X = 0x00000200,
 	NM_802_11_AP_SEC_KEY_MGMT_SAE    = 0x00000400,
 	NM_802_11_AP_SEC_KEY_MGMT_OWE    = 0x00000800,
+	NM_802_11_AP_SEC_KEY_MGMT_OWE_TM = 0x00001000,
 } NM80211ApSecurityFlags;
 
 /**
@@ -857,6 +860,7 @@ typedef enum /*< flags >*/ {
 #undef NM_AVAILABLE_IN_1_8
 #endif
 
+#define NM_LLDP_ATTR_RAW                     "raw"
 #define NM_LLDP_ATTR_DESTINATION             "destination"
 #define NM_LLDP_ATTR_CHASSIS_ID_TYPE         "chassis-id-type"
 #define NM_LLDP_ATTR_CHASSIS_ID              "chassis-id"
@@ -876,6 +880,8 @@ typedef enum /*< flags >*/ {
 #define NM_LLDP_ATTR_IEEE_802_3_POWER_VIA_MDI   "ieee-802-3-power-via-mdi"
 #define NM_LLDP_ATTR_IEEE_802_3_MAX_FRAME_SIZE  "ieee-802-3-max-frame-size"
 
+#define NM_LLDP_ATTR_MUD_URL                 "mud-url"
+
 /* These are deprecated in favor of NM_LLDP_ATTR_IEEE_802_1_VLANS,
  * which can report multiple VLANs */
 #define NM_LLDP_ATTR_IEEE_802_1_VID          "ieee-802-1-vid"
@@ -992,6 +998,8 @@ typedef enum { /*< skip >*/
  *  currently active but deleted on disconnect. Volatile connections are
  *  always unsaved, but they are also no backing file on disk and are entirely
  *  in-memory only.
+ * @NM_SETTINGS_CONNECTION_FLAG_EXTERNAL: the profile was generated to represent
+ *  an external configuration of a networking device. Since: 1.26
  *
  * Flags describing the current activation state.
  *
@@ -1002,6 +1010,7 @@ typedef enum { /*< flags >*/
 	NM_SETTINGS_CONNECTION_FLAG_UNSAVED                    = 0x01,
 	NM_SETTINGS_CONNECTION_FLAG_NM_GENERATED               = 0x02,
 	NM_SETTINGS_CONNECTION_FLAG_VOLATILE                   = 0x04,
+	NM_SETTINGS_CONNECTION_FLAG_EXTERNAL                   = 0x08,
 } NMSettingsConnectionFlags;
 
 /**
@@ -1018,6 +1027,8 @@ typedef enum { /*< flags >*/
  *   of the activation is bound to the visilibity of the connection profile,
  *   which in turn depends on "connection.permissions" and whether a session
  *   for the user exists. Since: 1.16
+ * @NM_ACTIVATION_STATE_FLAG_EXTERNAL: the active connection was generated to
+ *  represent an external configuration of a networking device. Since: 1.26
  *
  * Flags describing the current activation state.
  *
@@ -1033,6 +1044,7 @@ typedef enum { /*< flags >*/
 	NM_ACTIVATION_STATE_FLAG_IP6_READY                            = 0x10,
 	NM_ACTIVATION_STATE_FLAG_MASTER_HAS_SLAVES                    = 0x20,
 	NM_ACTIVATION_STATE_FLAG_LIFETIME_BOUND_TO_PROFILE_VISIBILITY = 0x40,
+	NM_ACTIVATION_STATE_FLAG_EXTERNAL                             = 0x80,
 } NMActivationStateFlags;
 
 /**
diff --git a/libnm-core/nm-dbus-types.xml b/libnm-core/nm-dbus-types.xml
index 0a5501ae..7ff4961e 100644
--- a/libnm-core/nm-dbus-types.xml
+++ b/libnm-core/nm-dbus-types.xml
@@ -587,6 +587,11 @@
               <entry role="enum_member_value"><para>= <literal>0x00000800</literal></para><para></para></entry>
               <entry role="enum_member_description"><para>WPA/RSN Opportunistic Wireless Encryption is supported</para><para></para></entry>
             </row>
+            <row role="constant">
+              <entry role="enum_member_name"><para>NM_802_11_AP_SEC_KEY_MGMT_OWE_TM</para><para></para></entry>
+              <entry role="enum_member_value"><para>= <literal>0x00001000</literal></para><para></para></entry>
+              <entry role="enum_member_description"><para>WPA/RSN Opportunistic Wireless Encryption transition mode is supported. Since: 1.26.</para><para></para></entry>
+            </row>
           </tbody>
         </tgroup>
       </informaltable>
@@ -1712,6 +1717,11 @@
               <entry role="enum_member_value"><para>= <literal>0x04</literal></para><para></para></entry>
               <entry role="enum_member_description"><para>The connection will be deleted when it disconnects. That is for in-memory connections (unsaved), which are currently active but deleted on disconnect. Volatile connections are always unsaved, but they are also no backing file on disk and are entirely in-memory only.</para><para></para></entry>
             </row>
+            <row role="constant">
+              <entry role="enum_member_name"><para>NM_SETTINGS_CONNECTION_FLAG_EXTERNAL</para><para></para></entry>
+              <entry role="enum_member_value"><para>= <literal>0x08</literal></para><para></para></entry>
+              <entry role="enum_member_description"><para>the profile was generated to represent an external configuration of a networking device. Since: 1.26</para><para></para></entry>
+            </row>
           </tbody>
         </tgroup>
       </informaltable>
@@ -1772,6 +1782,11 @@
               <entry role="enum_member_value"><para>= <literal>0x40</literal></para><para></para></entry>
               <entry role="enum_member_description"><para>the lifetime of the activation is bound to the visilibity of the connection profile, which in turn depends on "connection.permissions" and whether a session for the user exists. Since: 1.16</para><para></para></entry>
             </row>
+            <row role="constant">
+              <entry role="enum_member_name"><para>NM_ACTIVATION_STATE_FLAG_EXTERNAL</para><para></para></entry>
+              <entry role="enum_member_value"><para>= <literal>0x80</literal></para><para></para></entry>
+              <entry role="enum_member_description"><para>the active connection was generated to represent an external configuration of a networking device. Since: 1.26</para><para></para></entry>
+            </row>
           </tbody>
         </tgroup>
       </informaltable>
diff --git a/libnm-core/nm-keyfile/nm-keyfile-internal.h b/libnm-core/nm-keyfile/nm-keyfile-internal.h
new file mode 100644
index 00000000..b6ac71d6
--- /dev/null
+++ b/libnm-core/nm-keyfile/nm-keyfile-internal.h
@@ -0,0 +1,212 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2008 Novell, Inc.
+ * Copyright (C) 2015 Red Hat, Inc.
+ */
+
+#ifndef __NM_KEYFILE_INTERNAL_H__
+#define __NM_KEYFILE_INTERNAL_H__
+
+#if !((NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_CORE_INTERNAL)
+#error Cannot use this header.
+#endif
+
+#include <sys/types.h>
+
+#include "nm-connection.h"
+#include "nm-setting-8021x.h"
+
+#include "nm-core-internal.h"
+#include "nm-meta-setting.h"
+
+/*****************************************************************************/
+
+#define NM_KEYFILE_CERT_SCHEME_PREFIX_PATH "file://"
+#define NM_KEYFILE_CERT_SCHEME_PREFIX_PKCS11 "pkcs11:"
+#define NM_KEYFILE_CERT_SCHEME_PREFIX_BLOB "data:;base64,"
+
+char *nm_keyfile_detect_unqualified_path_scheme (const char *base_dir,
+                                                 gconstpointer pdata,
+                                                 gsize data_len,
+                                                 gboolean consider_exists,
+                                                 gboolean *out_exists);
+
+typedef enum { /*< flags >*/
+	NM_KEYFILE_HANDLER_FLAGS_NONE = 0,
+} NMKeyfileHandlerFlags;
+
+typedef enum {
+	NM_KEYFILE_HANDLER_TYPE_WARN  = 1,
+	NM_KEYFILE_HANDLER_TYPE_WRITE_CERT = 2,
+} NMKeyfileHandlerType;
+
+typedef struct _NMKeyfileHandlerData NMKeyfileHandlerData;
+
+/**
+ * NMKeyfileReadHandler:
+ *
+ * Hook to nm_keyfile_read(). The user might fail the reading by setting
+ * @error.
+ *
+ * Returns: should return TRUE, if the reading was handled. Otherwise,
+ * a default action will be performed that depends on the @handler_type.
+ * For %NM_KEYFILE_HANDLER_TYPE_WARN handler_type, the default action is doing nothing.
+ */
+typedef gboolean (*NMKeyfileReadHandler) (GKeyFile *keyfile,
+                                          NMConnection *connection,
+                                          NMKeyfileHandlerType handler_type,
+                                          NMKeyfileHandlerData *handler_data,
+                                          void *user_data);
+
+typedef enum {
+	NM_KEYFILE_WARN_SEVERITY_DEBUG                  = 1000,
+	NM_KEYFILE_WARN_SEVERITY_INFO                   = 2000,
+	NM_KEYFILE_WARN_SEVERITY_INFO_MISSING_FILE      = 2901,
+	NM_KEYFILE_WARN_SEVERITY_WARN                   = 3000,
+} NMKeyfileWarnSeverity;
+
+NMConnection *nm_keyfile_read (GKeyFile *keyfile,
+                               const char *base_dir,
+                               NMKeyfileHandlerFlags handler_flags,
+                               NMKeyfileReadHandler handler,
+                               void *user_data,
+                               GError **error);
+
+gboolean nm_keyfile_read_ensure_id (NMConnection *connection,
+                                    const char *fallback_id);
+
+gboolean nm_keyfile_read_ensure_uuid (NMConnection *connection,
+                                      const char *fallback_uuid_seed);
+
+/*****************************************************************************/
+
+/**
+ * NMKeyfileWriteHandler:
+ *
+ * This is a hook to tweak the serialization.
+ *
+ * Handler for certain properties or events that are not entirely contained
+ * within the keyfile or that might be serialized differently. The @handler_type and
+ * @handler_data arguments tell which kind of argument we have at hand.
+ *
+ * Currently only the handler_type %NM_KEYFILE_HANDLER_TYPE_WRITE_CERT is supported, which provides
+ * @handler_data as %NMKeyfileHandlerDataWriteCert. However, this handler should be generic enough
+ * to support other types as well.
+ *
+ * This don't have to be only "properties". For example, nm_keyfile_read() uses
+ * a similar handler to push warnings to the caller.
+ *
+ * If the handler raises an error, it should set the @error value. This causes
+ * the an overall failure.
+ *
+ * Returns: whether the issue was handled. If the type was unhandled,
+ * a default action will be performed. This might be raise an error,
+ * do some fallback parsing, or do nothing.
+ */
+typedef gboolean (*NMKeyfileWriteHandler) (NMConnection *connection,
+                                           GKeyFile *keyfile,
+                                           NMKeyfileHandlerType handler_type,
+                                           NMKeyfileHandlerData *handler_data,
+                                           void *user_data);
+
+GKeyFile *nm_keyfile_write (NMConnection *connection,
+                            NMKeyfileHandlerFlags handler_flags,
+                            NMKeyfileWriteHandler handler,
+                            void *user_data,
+                            GError **error);
+
+/*****************************************************************************/
+
+/**
+ * NMKeyfileHandlerDataWarn:
+ *
+ * this struct is passed as @handler_data for the @NMKeyfileReadHandler of
+ * handler_type %NM_KEYFILE_HANDLER_TYPE_WARN.
+ */
+typedef struct {
+	NMKeyfileWarnSeverity severity;
+	char *message;
+	const char *fmt;
+	va_list ap;
+} NMKeyfileHandlerDataWarn;
+
+/**
+ * NMKeyfileHandlerDataWriteCert:
+ *
+ * this struct is passed as @handler_data for the @NMKeyfileWriteHandler of
+ * handler_type %NM_KEYFILE_HANDLER_TYPE_WRITE_CERT.
+ */
+typedef struct {
+	const NMSetting8021xSchemeVtable *vtable;
+} NMKeyfileHandlerDataWriteCert;
+
+struct _NMKeyfileHandlerData {
+	NMKeyfileHandlerType type;
+
+	GError **p_error;
+
+	const char *kf_group_name;
+	const char *kf_key;
+
+	NMSetting *cur_setting;
+	const char *cur_property;
+
+	union {
+		NMKeyfileHandlerDataWarn      warn;
+		NMKeyfileHandlerDataWriteCert write_cert;
+	};
+};
+
+/*****************************************************************************/
+
+void nm_keyfile_handler_data_fail_with_error (NMKeyfileHandlerData *handler_data,
+                                              GError *src);
+
+void nm_keyfile_handler_data_get_context (const NMKeyfileHandlerData *handler_data,
+                                          const char **out_kf_group_name,
+                                          const char **out_kf_key_name,
+                                          NMSetting **out_cur_setting,
+                                          const char **out_cur_property_name);
+
+void nm_keyfile_handler_data_warn_get (const NMKeyfileHandlerData *handler_data,
+                                       const char **out_message,
+                                       NMKeyfileWarnSeverity *out_severity);
+
+const char *_nm_keyfile_handler_data_warn_get_message (const NMKeyfileHandlerData *handler_data);
+
+/*****************************************************************************/
+
+char *nm_keyfile_plugin_kf_get_string (GKeyFile *kf, const char *group, const char *key, GError **error);
+void nm_keyfile_plugin_kf_set_string (GKeyFile *kf, const char *group, const char *key, const char *value);
+
+int nm_key_file_get_boolean (GKeyFile *kf, const char *group, const char *key, int default_value);
+
+void _nm_keyfile_copy (GKeyFile *dst, GKeyFile *src);
+gboolean _nm_keyfile_a_contains_all_in_b (GKeyFile *kf_a, GKeyFile *kf_b);
+gboolean _nm_keyfile_equals (GKeyFile *kf_a, GKeyFile *kf_b, gboolean consider_order);
+gboolean _nm_keyfile_has_values (GKeyFile *keyfile);
+
+/*****************************************************************************/
+
+#define NM_KEYFILE_GROUP_NMMETA                 ".nmmeta"
+#define NM_KEYFILE_KEY_NMMETA_NM_GENERATED      "nm-generated"
+#define NM_KEYFILE_KEY_NMMETA_VOLATILE          "volatile"
+#define NM_KEYFILE_KEY_NMMETA_EXTERNAL          "external"
+#define NM_KEYFILE_KEY_NMMETA_SHADOWED_STORAGE  "shadowed-storage"
+#define NM_KEYFILE_KEY_NMMETA_SHADOWED_OWNED    "shadowed-owned"
+
+#define NM_KEYFILE_PATH_NAME_LIB                 NMLIBDIR  "/system-connections"
+#define NM_KEYFILE_PATH_NAME_ETC_DEFAULT         NMCONFDIR "/system-connections"
+#define NM_KEYFILE_PATH_NAME_RUN                 NMRUNDIR  "/system-connections"
+
+#define NM_KEYFILE_PATH_SUFFIX_NMCONNECTION      ".nmconnection"
+
+#define NM_KEYFILE_PATH_SUFFIX_NMMETA            ".nmmeta"
+
+#define NM_KEYFILE_PATH_NMMETA_SYMLINK_NULL      "/dev/null"
+
+gboolean nm_keyfile_utils_ignore_filename (const char *filename, gboolean require_extension);
+
+char *nm_keyfile_utils_create_filename (const char *filename, gboolean with_extension);
+
+#endif /* __NM_KEYFILE_INTERNAL_H__ */
diff --git a/libnm-core/nm-keyfile/nm-keyfile-utils.c b/libnm-core/nm-keyfile/nm-keyfile-utils.c
new file mode 100644
index 00000000..0ffce40b
--- /dev/null
+++ b/libnm-core/nm-keyfile/nm-keyfile-utils.c
@@ -0,0 +1,701 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2010 Red Hat, Inc.
+ */
+
+#include "nm-default.h"
+
+#include "nm-keyfile-utils.h"
+
+#include <stdlib.h>
+
+#include "nm-glib-aux/nm-str-buf.h"
+
+#include "nm-keyfile-internal.h"
+#include "nm-setting-wired.h"
+#include "nm-setting-wireless.h"
+#include "nm-setting-wireless-security.h"
+
+/*****************************************************************************/
+
+/**
+ * nm_key_file_get_boolean:
+ * @kf: the #GKeyFile
+ * @group: the group
+ * @key: the key
+ * @default_value: the default value if the value is set or not parsable as a boolean.
+ *
+ * Replacement for g_key_file_get_boolean() (which uses g_key_file_parse_value_as_boolean()).
+ * g_key_file_get_boolean() seems odd to me, because it accepts trailing ASCII whitespace,
+ * but not leading.
+ * This uses _nm_utils_ascii_str_to_bool(), which accepts trailing and leading whitespace,
+ * case-insensitive words, and also strings like "on" and "off".
+ * _nm_utils_ascii_str_to_bool() is our way to parse booleans from string, and we should
+ * use that one consistently.
+ *
+ * Also, it doesn't have g_key_file_get_boolean()'s odd API to require an error argument
+ * to detect parsing failures.
+ *
+ * Returns: either %TRUE or %FALSE if the key exists and is parsable as a boolean.
+ *   Otherwise, @default_value. Sets errno to ENODATA, EINVAL or 0, depending on whether
+ *   the key exists, whether the value is invalid, or success.
+ */
+int
+nm_key_file_get_boolean (GKeyFile *kf, const char *group, const char *key, int default_value)
+{
+	int v;
+	gs_free char *value = NULL;
+
+	value = g_key_file_get_value (kf, group, key, NULL);
+
+	if (!value) {
+		errno = ENODATA;
+		return default_value;
+	}
+	v = _nm_utils_ascii_str_to_bool (value, -1);
+	if (v != -1) {
+		errno = 0;
+		return v;
+	}
+	errno = EINVAL;
+	return default_value;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+	const char *setting;
+	const char *alias;
+} SettingAlias;
+
+static const SettingAlias alias_list[] = {
+	{ NM_SETTING_WIRED_SETTING_NAME, "ethernet" },
+	{ NM_SETTING_WIRELESS_SETTING_NAME, "wifi" },
+	{ NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, "wifi-security" },
+};
+
+const char *
+nm_keyfile_plugin_get_alias_for_setting_name (const char *setting_name)
+{
+	guint i;
+
+	g_return_val_if_fail (setting_name != NULL, NULL);
+
+	for (i = 0; i < G_N_ELEMENTS (alias_list); i++) {
+		if (nm_streq (setting_name, alias_list[i].setting))
+			return alias_list[i].alias;
+	}
+	return NULL;
+}
+
+const char *
+nm_keyfile_plugin_get_setting_name_for_alias (const char *alias)
+{
+	guint i;
+
+	g_return_val_if_fail (alias != NULL, NULL);
+
+	for (i = 0; i < G_N_ELEMENTS (alias_list); i++) {
+		if (nm_streq (alias, alias_list[i].alias))
+			return alias_list[i].setting;
+	}
+	return NULL;
+}
+
+/*****************************************************************************/
+
+char **
+nm_keyfile_plugin_kf_get_string_list (GKeyFile *kf,
+                                      const char *group,
+                                      const char *key,
+                                      gsize *out_length,
+                                      GError **error)
+{
+	char **list;
+	const char *alias;
+	GError *local = NULL;
+	gsize l;
+
+	list = g_key_file_get_string_list (kf, group, key, &l, &local);
+	if (nm_g_error_matches (local, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND)) {
+		alias = nm_keyfile_plugin_get_alias_for_setting_name (group);
+		if (alias) {
+			g_clear_error (&local);
+			list = g_key_file_get_string_list (kf, alias, key, &l, &local);
+		}
+	}
+	if (local)
+		g_propagate_error (error, local);
+	if (!list)
+		l = 0;
+	NM_SET_OUT (out_length, l);
+	return list;
+}
+
+guint *
+nm_keyfile_plugin_kf_get_integer_list_uint (GKeyFile *key_file,
+                                            const char *group_name,
+                                            const char *key,
+                                            gsize *out_length,
+                                            GError **error)
+{
+	GError *key_file_error = NULL;
+	gs_strfreev char **values = NULL;
+	gs_free guint *int_values = NULL;
+	gsize i, num_ints;
+
+	g_return_val_if_fail (key_file != NULL, NULL);
+	g_return_val_if_fail (group_name != NULL, NULL);
+	g_return_val_if_fail (key != NULL, NULL);
+
+	NM_SET_OUT (out_length, 0);
+
+	values = nm_keyfile_plugin_kf_get_string_list (key_file, group_name, key, &num_ints, &key_file_error);
+
+	if (key_file_error)
+		g_propagate_error (error, key_file_error);
+	if (!values)
+		return NULL;
+
+	int_values = g_new (guint, num_ints);
+
+	for (i = 0; i < num_ints; i++) {
+		gint64 v;
+
+		G_STATIC_ASSERT_EXPR (sizeof (v) > sizeof (guint));
+		v = _nm_utils_ascii_str_to_int64 (values[i], 10, 0, G_MAXUINT, -1);
+		if (v == -1) {
+			g_set_error (error,
+			             G_KEY_FILE_ERROR,
+			             G_KEY_FILE_ERROR_INVALID_VALUE,
+			             _("Value cannot be interpreted as a list of numbers."));
+			return NULL;
+		}
+
+		int_values[i] = v;
+	}
+
+	NM_SET_OUT (out_length, num_ints);
+	return g_steal_pointer (&int_values);
+}
+
+void
+nm_keyfile_plugin_kf_set_string_list (GKeyFile *kf,
+                                      const char *group,
+                                      const char *key,
+                                      const char *const*list,
+                                      gsize length)
+{
+	const char *alias;
+
+	alias = nm_keyfile_plugin_get_alias_for_setting_name (group);
+	g_key_file_set_string_list (kf, alias ?: group, key, list, length);
+}
+
+void
+nm_keyfile_plugin_kf_set_integer_list_uint (GKeyFile *kf,
+                                            const char *group,
+                                            const char *key,
+                                            const guint *data,
+                                            gsize length)
+{
+	nm_auto_str_buf NMStrBuf strbuf = { };
+	gsize i;
+
+	g_return_if_fail (kf);
+	g_return_if_fail (!length || data);
+	g_return_if_fail (group && group[0]);
+	g_return_if_fail (key && key[0]);
+
+	nm_str_buf_init (&strbuf, length * 4u + 2u, FALSE);
+	for (i = 0; i < length; i++)
+		nm_str_buf_append_printf (&strbuf, "%u;", data[i]);
+	nm_keyfile_plugin_kf_set_value (kf, group, key, nm_str_buf_get_str (&strbuf));
+}
+
+void
+nm_keyfile_plugin_kf_set_integer_list_uint8 (GKeyFile *kf,
+                                             const char *group,
+                                             const char *key,
+                                             const guint8 *data,
+                                             gsize length)
+{
+	nm_auto_str_buf NMStrBuf strbuf = { };
+	gsize i;
+
+	g_return_if_fail (kf);
+	g_return_if_fail (!length || data);
+	g_return_if_fail (group && group[0]);
+	g_return_if_fail (key && key[0]);
+
+	nm_str_buf_init (&strbuf, length * 4u + 2u, FALSE);
+	for (i = 0; i < length; i++)
+		nm_str_buf_append_printf (&strbuf, "%u;", (guint) data[i]);
+	nm_keyfile_plugin_kf_set_value (kf, group, key, nm_str_buf_get_str (&strbuf));
+}
+
+#define DEFINE_KF_WRAPPER_GET(fcn_name, get_ctype, key_file_get_fcn) \
+get_ctype \
+fcn_name (GKeyFile *kf, \
+          const char *group, \
+          const char *key, \
+          GError **error) \
+{ \
+	get_ctype val; \
+	const char *alias; \
+	GError *local = NULL; \
+\
+	val = key_file_get_fcn (kf, group, key, &local); \
+	if (nm_g_error_matches (local, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND)) { \
+		alias = nm_keyfile_plugin_get_alias_for_setting_name (group); \
+		if (alias) { \
+			g_clear_error (&local); \
+			val = key_file_get_fcn (kf, alias, key, &local); \
+		} \
+	} \
+	if (local) \
+		g_propagate_error (error, local); \
+	return val; \
+}
+
+DEFINE_KF_WRAPPER_GET (nm_keyfile_plugin_kf_get_string,  char *,   g_key_file_get_string);
+DEFINE_KF_WRAPPER_GET (nm_keyfile_plugin_kf_get_boolean, gboolean, g_key_file_get_boolean);
+DEFINE_KF_WRAPPER_GET (nm_keyfile_plugin_kf_get_value,   char *,   g_key_file_get_value);
+
+#define DEFINE_KF_WRAPPER_SET(fcn_name, set_ctype, key_file_set_fcn) \
+void \
+fcn_name (GKeyFile *kf, \
+          const char *group, \
+          const char *key, \
+          set_ctype value) \
+{ \
+	const char *alias; \
+\
+	alias = nm_keyfile_plugin_get_alias_for_setting_name (group); \
+	key_file_set_fcn (kf, alias ?: group, key, value); \
+}
+
+DEFINE_KF_WRAPPER_SET (nm_keyfile_plugin_kf_set_string,  const char *, g_key_file_set_string);
+DEFINE_KF_WRAPPER_SET (nm_keyfile_plugin_kf_set_boolean, gboolean,     g_key_file_set_boolean);
+DEFINE_KF_WRAPPER_SET (nm_keyfile_plugin_kf_set_value,   const char *, g_key_file_set_value);
+
+gint64
+nm_keyfile_plugin_kf_get_int64 (GKeyFile *kf,
+                                const char *group,
+                                const char *key,
+                                guint base,
+                                gint64 min,
+                                gint64 max,
+                                gint64 fallback,
+                                GError **error)
+{
+	gs_free char *s = NULL;
+	int errsv;
+	gint64 v;
+
+	s = nm_keyfile_plugin_kf_get_value (kf, group, key, error);
+	if (!s) {
+		errno = ENODATA;
+		return fallback;
+	}
+
+	v = _nm_utils_ascii_str_to_int64 (s, base, min, max, fallback);
+	errsv = errno;
+	if (   errsv != 0
+	    && error) {
+		g_set_error (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
+		             _("value is not an integer in range [%lld, %lld]"),
+		             (long long) min, (long long) max);
+		errno = errsv;
+	}
+	return v;
+}
+
+char **
+nm_keyfile_plugin_kf_get_keys (GKeyFile *kf,
+                               const char *group,
+                               gsize *out_length,
+                               GError **error)
+{
+	char **keys;
+	const char *alias;
+	GError *local = NULL;
+	gsize l;
+
+	keys = g_key_file_get_keys (kf, group, &l, &local);
+	if (nm_g_error_matches (local, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND)) {
+		alias = nm_keyfile_plugin_get_alias_for_setting_name (group);
+		if (alias) {
+			g_clear_error (&local);
+			keys = g_key_file_get_keys (kf, alias, &l, error ? &local : NULL);
+		}
+	}
+	nm_assert ((!local) != (!keys));
+	if (!keys)
+		l = 0;
+	nm_assert (l == NM_PTRARRAY_LEN (keys));
+	NM_SET_OUT (out_length, l);
+	if (local)
+		g_propagate_error (error, local);
+	return keys;
+}
+
+gboolean
+nm_keyfile_plugin_kf_has_key (GKeyFile *kf,
+                              const char *group,
+                              const char *key,
+                              GError **error)
+{
+	gboolean has;
+	const char *alias;
+	GError *local = NULL;
+
+	has = g_key_file_has_key (kf, group, key, &local);
+	if (nm_g_error_matches (local, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND)) {
+		alias = nm_keyfile_plugin_get_alias_for_setting_name (group);
+		if (alias) {
+			g_clear_error (&local);
+			has = g_key_file_has_key (kf, alias, key, &local);
+		}
+	}
+	if (local)
+		g_propagate_error (error, local);
+	return has;
+}
+
+/*****************************************************************************/
+
+void
+_nm_keyfile_copy (GKeyFile *dst, GKeyFile *src)
+{
+	gs_strfreev char **groups = NULL;
+	guint g, k;
+
+	groups = g_key_file_get_groups (src, NULL);
+	for (g = 0; groups && groups[g]; g++) {
+		const char *group = groups[g];
+		gs_strfreev char **keys = NULL;
+
+		keys = g_key_file_get_keys (src, group, NULL, NULL);
+		if (!keys)
+			continue;
+
+		for (k = 0; keys[k]; k++) {
+			const char *key = keys[k];
+			gs_free char *value = NULL;
+
+			value = g_key_file_get_value (src, group, key, NULL);
+			if (value)
+				g_key_file_set_value (dst, group, key, value);
+			else
+				g_key_file_remove_key (dst, group, key, NULL);
+		}
+	}
+}
+
+/*****************************************************************************/
+
+gboolean
+_nm_keyfile_a_contains_all_in_b (GKeyFile *kf_a, GKeyFile *kf_b)
+{
+	gs_strfreev char **groups = NULL;
+	guint i, j;
+
+	if (kf_a == kf_b)
+		return TRUE;
+	if (!kf_a || !kf_b)
+		return FALSE;
+
+	groups = g_key_file_get_groups (kf_a, NULL);
+	for (i = 0; groups && groups[i]; i++) {
+		gs_strfreev char **keys = NULL;
+
+		keys = g_key_file_get_keys (kf_a, groups[i], NULL, NULL);
+		if (!keys)
+			continue;
+
+		for (j = 0; keys[j]; j++) {
+			gs_free char *key_a = g_key_file_get_value (kf_a, groups[i], keys[j], NULL);
+			gs_free char *key_b = g_key_file_get_value (kf_b, groups[i], keys[j], NULL);
+
+			if (g_strcmp0 (key_a, key_b) != 0)
+				return FALSE;
+		}
+	}
+	return TRUE;
+}
+
+static gboolean
+_nm_keyfile_equals_ordered (GKeyFile *kf_a, GKeyFile *kf_b)
+{
+	gs_strfreev char **groups = NULL;
+	gs_strfreev char **groups_b = NULL;
+	guint i, j;
+
+	if (kf_a == kf_b)
+		return TRUE;
+	if (!kf_a || !kf_b)
+		return FALSE;
+
+	groups = g_key_file_get_groups (kf_a, NULL);
+	groups_b = g_key_file_get_groups (kf_b, NULL);
+	if (!groups && !groups_b)
+		return TRUE;
+	if (!groups || !groups_b)
+		return FALSE;
+	for (i = 0; groups[i] && groups_b[i] && !strcmp (groups[i], groups_b[i]); i++)
+		;
+	if (groups[i] || groups_b[i])
+		return FALSE;
+
+	for (i = 0; groups[i]; i++) {
+		gs_strfreev char **keys = NULL;
+		gs_strfreev char **keys_b = NULL;
+
+		keys = g_key_file_get_keys (kf_a, groups[i], NULL, NULL);
+		keys_b = g_key_file_get_keys (kf_b, groups[i], NULL, NULL);
+
+		if ((!keys) != (!keys_b))
+			return FALSE;
+		if (!keys)
+			continue;
+
+		for (j = 0; keys[j] && keys_b[j] && !strcmp (keys[j], keys_b[j]); j++)
+			;
+		if (keys[j] || keys_b[j])
+			return FALSE;
+
+		for (j = 0; keys[j]; j++) {
+			gs_free char *key_a = g_key_file_get_value (kf_a, groups[i], keys[j], NULL);
+			gs_free char *key_b = g_key_file_get_value (kf_b, groups[i], keys[j], NULL);
+
+			if (g_strcmp0 (key_a, key_b) != 0)
+				return FALSE;
+		}
+	}
+	return TRUE;
+}
+
+gboolean
+_nm_keyfile_equals (GKeyFile *kf_a, GKeyFile *kf_b, gboolean consider_order)
+{
+	if (!consider_order) {
+		return    _nm_keyfile_a_contains_all_in_b (kf_a, kf_b)
+		       && _nm_keyfile_a_contains_all_in_b (kf_b, kf_a);
+	} else {
+		return _nm_keyfile_equals_ordered (kf_a, kf_b);
+	}
+}
+
+gboolean
+_nm_keyfile_has_values (GKeyFile *keyfile)
+{
+	gs_strfreev char **groups = NULL;
+
+	g_return_val_if_fail (keyfile, FALSE);
+
+	groups = g_key_file_get_groups (keyfile, NULL);
+	return groups && groups[0];
+}
+
+/*****************************************************************************/
+
+static const char *
+_keyfile_key_encode (const char *name,
+                     char **out_to_free)
+{
+	gsize len, i;
+	GString *str;
+
+	nm_assert (name);
+	nm_assert (out_to_free && !*out_to_free);
+
+	/* See g_key_file_is_key_name().
+	 *
+	 * GKeyFile allows all UTF-8 characters (even non-well formed sequences),
+	 * except:
+	 *  - no empty keys
+	 *  - no leading/trailing ' '
+	 *  - no '=', '[', ']'
+	 *
+	 * We do something more strict here. All non-ASCII characters, all non-printable
+	 * characters, and all invalid characters are escaped with "\\XX".
+	 *
+	 * We don't escape \\, unless it is followed by two hex digits.
+	 */
+
+	if (!name[0]) {
+		/* empty keys are are backslash encoded. Note that usually
+		 * \\00 is not a valid encode, the only exception is the empty
+		 * word. */
+		return "\\00";
+	}
+
+	/* find the first character that needs escaping. */
+	i = 0;
+	if (name[0] != ' ') {
+		for (;; i++) {
+			const guchar ch = (guchar) name[i];
+
+			if (ch == '\0')
+				return name;
+
+			if (   ch < 0x20
+			    || ch >= 127
+			    || NM_IN_SET (ch, '=', '[', ']')
+			    || (   ch == '\\'
+			        && g_ascii_isxdigit (name[i + 1])
+			        && g_ascii_isxdigit (name[i + 2]))
+			    || (   ch == ' '
+			        && name[i + 1] == '\0'))
+				break;
+		}
+	} else if (name[1] == '\0')
+		return "\\20";
+
+	len = i + strlen (&name[i]);
+	nm_assert (len == strlen (name));
+	str = g_string_sized_new (len + 15);
+
+	if (name[0] == ' ') {
+		nm_assert (i == 0);
+		g_string_append (str, "\\20");
+		i = 1;
+	} else
+		g_string_append_len (str, name, i);
+
+	for (;; i++) {
+		const guchar ch = (guchar) name[i];
+
+		if (ch == '\0')
+			break;
+
+		if (   ch < 0x20
+		    || ch >= 127
+		    || NM_IN_SET (ch, '=', '[', ']')
+		    || (   ch == '\\'
+		        && g_ascii_isxdigit (name[i + 1])
+		        && g_ascii_isxdigit (name[i + 2]))
+		    || (   ch == ' '
+		        && name[i + 1] == '\0'))
+			g_string_append_printf (str, "\\%02X", ch);
+		else
+			g_string_append_c (str, (char) ch);
+	}
+
+	return (*out_to_free = g_string_free (str, FALSE));
+}
+
+static const char *
+_keyfile_key_decode (const char *key,
+                     char **out_to_free)
+{
+	gsize i, len;
+	GString *str;
+
+	nm_assert (key);
+	nm_assert (out_to_free && !*out_to_free);
+
+	if (!key[0])
+		return "";
+
+	for (i = 0; TRUE; i++) {
+		const char ch = key[i];
+
+		if (ch == '\0')
+			return key;
+		if (   ch == '\\'
+		    && g_ascii_isxdigit (key[i + 1])
+		    && g_ascii_isxdigit (key[i + 2]))
+			break;
+	}
+
+	len = i + strlen (&key[i]);
+
+	if (   len == 3
+	    && nm_streq (key, "\\00"))
+		return "";
+
+	nm_assert (len == strlen (key));
+	str = g_string_sized_new (len + 3);
+
+	g_string_append_len (str, key, i);
+	for (;;) {
+		const char ch = key[i];
+		char ch1, ch2;
+		unsigned v;
+
+		if (ch == '\0')
+			break;
+
+		if (   ch == '\\'
+		    && g_ascii_isxdigit ((ch1 = key[i + 1]))
+		    && g_ascii_isxdigit ((ch2 = key[i + 2]))) {
+			v = (g_ascii_xdigit_value (ch1) << 4) + g_ascii_xdigit_value (ch2);
+			if (v != 0) {
+				g_string_append_c (str, (char) v);
+				i += 3;
+				continue;
+			}
+		}
+		g_string_append_c (str, ch);
+		i++;
+	}
+
+	return (*out_to_free = g_string_free (str, FALSE));
+}
+
+/*****************************************************************************/
+
+const char *
+nm_keyfile_key_encode (const char *name,
+                       char **out_to_free)
+{
+	const char *key;
+
+	key = _keyfile_key_encode (name, out_to_free);
+#if NM_MORE_ASSERTS > 5
+	nm_assert (key);
+	nm_assert (!*out_to_free || key == *out_to_free);
+	nm_assert (!*out_to_free || !nm_streq0 (name, key));
+	{
+		gs_free char *to_free2 = NULL;
+		const char *name2;
+
+		name2 = _keyfile_key_decode (key, &to_free2);
+		/* name2, the result of encode()+decode() is identical to name.
+		 * That is because
+		 *   - encode() is a injective function.
+		 *   - decode() is a surjective function, however for output
+		 *     values of encode() is behaves injective too. */
+		nm_assert (nm_streq0 (name2, name));
+	}
+#endif
+	return key;
+}
+
+const char *
+nm_keyfile_key_decode (const char *key,
+                       char **out_to_free)
+{
+	const char *name;
+
+	name = _keyfile_key_decode (key, out_to_free);
+#if NM_MORE_ASSERTS > 5
+	nm_assert (name);
+	nm_assert (!*out_to_free || name == *out_to_free);
+	{
+		gs_free char *to_free2 = NULL;
+		const char *key2;
+
+		key2 = _keyfile_key_encode (name, &to_free2);
+		/* key2, the result of decode+encode may not be idential
+		 * to the original key. That is, decode() is a surjective
+		 * function mapping different keys to the same name.
+		 * However, decode() behaves injective for input that
+		 * are valid output of encode(). */
+		nm_assert (key2);
+	}
+#endif
+	return name;
+}
diff --git a/libnm-core/nm-keyfile/nm-keyfile-utils.h b/libnm-core/nm-keyfile/nm-keyfile-utils.h
new file mode 100644
index 00000000..4764311b
--- /dev/null
+++ b/libnm-core/nm-keyfile/nm-keyfile-utils.h
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2010 - 2015 Red Hat, Inc.
+ */
+
+#ifndef __NM_KEYFILE_UTILS_H__
+#define __NM_KEYFILE_UTILS_H__
+
+#if !((NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_CORE_INTERNAL)
+#error Cannot use this header.
+#endif
+
+/*****************************************************************************/
+
+#include "nm-glib-aux/nm-shared-utils.h"
+
+/*****************************************************************************/
+
+#define NM_KEYFILE_GROUP_VPN_SECRETS          "vpn-secrets"
+#define NM_KEYFILE_GROUPPREFIX_WIREGUARD_PEER "wireguard-peer."
+
+#define nm_keyfile_error_is_not_found(error) \
+	nm_g_error_matches (error, \
+	                    G_KEY_FILE_ERROR, \
+	                    G_KEY_FILE_ERROR_GROUP_NOT_FOUND, \
+	                    G_KEY_FILE_ERROR_KEY_NOT_FOUND)
+
+const char *nm_keyfile_plugin_get_alias_for_setting_name (const char *setting_name);
+
+const char *nm_keyfile_plugin_get_setting_name_for_alias (const char *alias);
+
+/*****************************************************************************/
+
+guint   *nm_keyfile_plugin_kf_get_integer_list_uint (GKeyFile *kf, const char *group, const char *key, gsize *out_length, GError **error);
+char   **nm_keyfile_plugin_kf_get_string_list       (GKeyFile *kf, const char *group, const char *key, gsize *out_length, GError **error);
+char    *nm_keyfile_plugin_kf_get_string            (GKeyFile *kf, const char *group, const char *key, GError **error);
+gboolean nm_keyfile_plugin_kf_get_boolean           (GKeyFile *kf, const char *group, const char *key, GError **error);
+char    *nm_keyfile_plugin_kf_get_value             (GKeyFile *kf, const char *group, const char *key, GError **error);
+
+void nm_keyfile_plugin_kf_set_integer_list_uint8 (GKeyFile *kf, const char *group, const char *key, const guint8     *list, gsize length);
+void nm_keyfile_plugin_kf_set_integer_list_uint  (GKeyFile *kf, const char *group, const char *key, const guint      *list, gsize length);
+void nm_keyfile_plugin_kf_set_string_list        (GKeyFile *kf, const char *group, const char *key, const char *const*list, gsize length);
+
+void nm_keyfile_plugin_kf_set_string       (GKeyFile *kf, const char *group, const char *key, const char *value);
+void nm_keyfile_plugin_kf_set_boolean      (GKeyFile *kf, const char *group, const char *key, gboolean    value);
+void nm_keyfile_plugin_kf_set_value        (GKeyFile *kf, const char *group, const char *key, const char *value);
+
+gint64 nm_keyfile_plugin_kf_get_int64 (GKeyFile *kf,
+                                       const char *group,
+                                       const char *key,
+                                       guint base,
+                                       gint64 min,
+                                       gint64 max,
+                                       gint64 fallback,
+                                       GError **error);
+
+char **nm_keyfile_plugin_kf_get_keys (GKeyFile *kf,
+                                      const char *group,
+                                      gsize *out_length,
+                                      GError **error);
+
+gboolean nm_keyfile_plugin_kf_has_key (GKeyFile *kf,
+                                       const char *group,
+                                       const char *key,
+                                       GError **error);
+
+const char *nm_keyfile_key_encode (const char *name,
+                                   char **out_to_free);
+
+const char *nm_keyfile_key_decode (const char *key,
+                                   char **out_to_free);
+
+#endif  /* __NM_KEYFILE_UTILS_H__ */
diff --git a/libnm-core/nm-keyfile/nm-keyfile.c b/libnm-core/nm-keyfile/nm-keyfile.c
new file mode 100644
index 00000000..77af2b64
--- /dev/null
+++ b/libnm-core/nm-keyfile/nm-keyfile.c
@@ -0,0 +1,4445 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2008 - 2009 Novell, Inc.
+ * Copyright (C) 2008 - 2017 Red Hat, Inc.
+ */
+
+#include "nm-default.h"
+
+#include "nm-keyfile-internal.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <arpa/inet.h>
+#include <linux/pkt_sched.h>
+
+#include "nm-glib-aux/nm-secret-utils.h"
+#include "systemd/nm-sd-utils-shared.h"
+#include "nm-libnm-core-intern/nm-common-macros.h"
+#include "nm-core-internal.h"
+#include "nm-keyfile-utils.h"
+
+#include "nm-setting-user.h"
+
+/*****************************************************************************/
+
+typedef struct _ParseInfoProperty ParseInfoProperty;
+
+typedef struct {
+	NMConnection *connection;
+	GKeyFile *keyfile;
+	const char *base_dir;
+	NMKeyfileReadHandler read_handler;
+	void *user_data;
+	GError *error;
+	const char *group;
+	NMSetting *setting;
+} KeyfileReaderInfo;
+
+typedef struct {
+	NMConnection *connection;
+	GKeyFile *keyfile;
+	GError *error;
+	NMKeyfileWriteHandler write_handler;
+	void *user_data;
+} KeyfileWriterInfo;
+
+/*****************************************************************************/
+
+static void
+_key_file_handler_data_init (NMKeyfileHandlerData *handler_data,
+                             NMKeyfileHandlerType handler_type,
+                             const char *kf_group_name,
+                             const char *kf_key,
+                             NMSetting *cur_setting,
+                             const char *cur_property,
+                             GError **p_error)
+{
+	nm_assert (handler_data);
+	nm_assert (p_error && !*p_error);
+
+	handler_data->type = handler_type;
+	handler_data->p_error = p_error;
+	handler_data->kf_group_name = kf_group_name;
+	handler_data->kf_key = kf_key;
+	handler_data->cur_setting = cur_setting;
+	handler_data->cur_property = cur_property;
+}
+
+static void
+_key_file_handler_data_init_read (NMKeyfileHandlerData *handler_data,
+                                  NMKeyfileHandlerType handler_type,
+                                  KeyfileReaderInfo *info,
+                                  const char *kf_key,
+                                  const char *cur_property)
+{
+	_key_file_handler_data_init (handler_data,
+	                             handler_type,
+	                             info->group,
+	                             kf_key,
+	                             info->setting,
+	                             cur_property,
+	                             &info->error);
+}
+
+static void
+_key_file_handler_data_init_write (NMKeyfileHandlerData *handler_data,
+                                   NMKeyfileHandlerType handler_type,
+                                   KeyfileWriterInfo *info,
+                                   const char *kf_group,
+                                   const char *kf_key,
+                                   NMSetting *cur_setting,
+                                   const char *cur_property)
+{
+	_key_file_handler_data_init (handler_data,
+	                             handler_type,
+	                             kf_group,
+	                             kf_key,
+	                             cur_setting,
+	                             cur_property,
+	                             &info->error);
+}
+
+_nm_printf (5, 6)
+static void
+_handle_warn (KeyfileReaderInfo *info,
+              const char *kf_key,
+              const char *cur_property,
+              NMKeyfileWarnSeverity severity,
+              const char *fmt,
+              ...)
+{
+	NMKeyfileHandlerData handler_data;
+
+	_key_file_handler_data_init_read (&handler_data,
+	                                  NM_KEYFILE_HANDLER_TYPE_WARN,
+	                                  info,
+	                                  kf_key,
+	                                  cur_property);
+	handler_data.warn = (NMKeyfileHandlerDataWarn) {
+		.severity = severity,
+		.message  = NULL,
+		.fmt      = fmt,
+	};
+
+	va_start (handler_data.warn.ap, fmt);
+
+	info->read_handler (info->keyfile,
+	                    info->connection,
+	                    NM_KEYFILE_HANDLER_TYPE_WARN,
+	                    &handler_data,
+	                    info->user_data);
+
+	va_end (handler_data.warn.ap);
+
+	g_free (handler_data.warn.message);
+}
+
+#define handle_warn(arg_info, arg_kf_key, arg_property_name, arg_severity, ...) \
+	({ \
+		KeyfileReaderInfo *_info = (arg_info); \
+		\
+		nm_assert (!_info->error); \
+		\
+		if (_info->read_handler) { \
+			_handle_warn (_info, \
+			              (arg_kf_key), \
+			              (arg_property_name), \
+			              (arg_severity), \
+			              __VA_ARGS__); \
+		} \
+		_info->error == NULL; \
+	})
+
+/*****************************************************************************/
+
+static gboolean
+_secret_flags_persist_secret (NMSettingSecretFlags flags)
+{
+	return flags == NM_SETTING_SECRET_FLAG_NONE;
+}
+
+/*****************************************************************************/
+/* Some setting properties also contain setting names, such as
+ * NMSettingConnection's 'type' property (which specifies the base type of the
+ * connection, e.g. ethernet or wifi) or 'slave-type' (specifies type of slave
+ * connection, e.g. bond or bridge). This function handles translating those
+ * properties' values to the real setting name if they are an alias.
+ */
+static void
+setting_alias_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
+{
+	const char *setting_name = nm_setting_get_name (setting);
+	const char *key_setting_name;
+	gs_free char *s = NULL;
+
+	s = nm_keyfile_plugin_kf_get_string (info->keyfile, setting_name, key, NULL);
+	if (!s)
+		return;
+
+	key_setting_name = nm_keyfile_plugin_get_setting_name_for_alias (s);
+	g_object_set (G_OBJECT (setting),
+	              key,
+	              key_setting_name ?: s,
+	              NULL);
+}
+
+static void
+sriov_vfs_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
+{
+	const char *setting_name = nm_setting_get_name (setting);
+	gs_unref_ptrarray GPtrArray *vfs = NULL;
+	gs_strfreev char **keys = NULL;
+	gsize n_keys = 0;
+	int i;
+
+	keys = nm_keyfile_plugin_kf_get_keys (info->keyfile, setting_name, &n_keys, NULL);
+	if (n_keys == 0)
+		return;
+
+	vfs = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_sriov_vf_unref);
+
+	for (i = 0; i < n_keys; i++) {
+		gs_free char *value = NULL;
+		NMSriovVF *vf;
+		const char *rest;
+
+		if (!g_str_has_prefix (keys[i], "vf."))
+			continue;
+
+		rest = &keys[i][3];
+
+		if (!NM_STRCHAR_ALL (rest, ch, g_ascii_isdigit (ch)))
+			continue;
+
+		value = nm_keyfile_plugin_kf_get_string (info->keyfile,
+		                                         setting_name,
+		                                         keys[i],
+		                                         NULL);
+
+		vf = _nm_utils_sriov_vf_from_strparts (rest, value, TRUE, NULL);
+		if (vf)
+			g_ptr_array_add (vfs, vf);
+	}
+
+	g_object_set (G_OBJECT (setting),
+	              key, vfs,
+	              NULL);
+}
+
+static void
+read_array_of_uint (GKeyFile *file,
+                    NMSetting *setting,
+                    const char *key)
+{
+	gs_unref_array GArray *array = NULL;
+	gs_free_error GError *error = NULL;
+	gs_free guint *tmp = NULL;
+	gsize length;
+
+	tmp = nm_keyfile_plugin_kf_get_integer_list_uint (file, nm_setting_get_name (setting), key, &length, &error);
+	if (error)
+		return;
+
+	array = g_array_sized_new (FALSE, FALSE, sizeof (guint), length);
+	g_array_append_vals (array, tmp, length);
+	g_object_set (setting, key, array, NULL);
+}
+
+static gboolean
+get_one_int (KeyfileReaderInfo *info,
+             const char *kf_key,
+             const char *property_name,
+             const char *str,
+             guint32 max_val,
+             guint32 *out)
+{
+	gint64 tmp;
+
+	nm_assert ((!info) == (!property_name));
+	nm_assert ((!info) == (!kf_key));
+
+	if (!str || !str[0]) {
+		if (info) {
+			handle_warn (info,
+			             kf_key,
+			             property_name,
+			             NM_KEYFILE_WARN_SEVERITY_WARN,
+			             _("ignoring missing number"));
+		}
+		return FALSE;
+	}
+
+	tmp = _nm_utils_ascii_str_to_int64 (str, 10, 0, max_val, -1);
+	if (tmp == -1) {
+		if (info) {
+			handle_warn (info,
+			             kf_key,
+			             property_name,
+			             NM_KEYFILE_WARN_SEVERITY_WARN,
+			             _("ignoring invalid number '%s'"),
+			             str);
+		}
+		return FALSE;
+	}
+
+	*out = (guint32) tmp;
+	return TRUE;
+}
+
+static gpointer
+build_address (KeyfileReaderInfo *info,
+               const char *kf_key,
+               const char *property_name,
+               int family,
+               const char *address_str,
+               guint32 plen)
+{
+	NMIPAddress *addr;
+	GError *error = NULL;
+
+	g_return_val_if_fail (address_str, NULL);
+
+	addr = nm_ip_address_new (family, address_str, plen, &error);
+	if (!addr) {
+		handle_warn (info,
+		             kf_key,
+		             property_name,
+		             NM_KEYFILE_WARN_SEVERITY_WARN,
+		             _("ignoring invalid %s address: %s"),
+		             family == AF_INET ? "IPv4" : "IPv6",
+		             error->message);
+		g_error_free (error);
+	}
+
+	return addr;
+}
+
+static gpointer
+build_route (KeyfileReaderInfo *info,
+             const char *kf_key,
+             const char *property_name,
+             int family,
+             const char *dest_str,
+             guint32 plen,
+             const char *gateway_str,
+             const char *metric_str)
+{
+	NMIPRoute *route;
+	guint32 u32;
+	gint64 metric = -1;
+	GError *error = NULL;
+
+	g_return_val_if_fail (dest_str, NULL);
+
+	/* Next hop */
+	if (gateway_str && gateway_str[0]) {
+		if (!nm_utils_ipaddr_is_valid (family, gateway_str)) {
+			/* Try workaround for routes written by broken keyfile writer.
+			 * Due to bug bgo#719851, an older version of writer would have
+			 * written "a:b:c:d::/plen,metric" if the gateway was ::, instead
+			 * of "a:b:c:d::/plen,,metric" or "a:b:c:d::/plen,::,metric"
+			 * Try workaround by interpreting gateway_str as metric to accept such
+			 * invalid routes. This broken syntax should not be not officially
+			 * supported.
+			 **/
+			if (   family == AF_INET6
+			    && !metric_str
+			    && get_one_int (NULL, NULL, NULL, gateway_str, G_MAXUINT32, &u32)) {
+				metric = u32;
+				gateway_str = NULL;
+			} else {
+				handle_warn (info,
+				             kf_key,
+				             property_name,
+				             NM_KEYFILE_WARN_SEVERITY_WARN,
+				             _("ignoring invalid gateway '%s' for %s route"),
+				             gateway_str,
+				             family == AF_INET ? "IPv4" : "IPv6");
+				return NULL;
+			}
+		}
+	} else
+		gateway_str = NULL;
+
+	/* parse metric, default to -1 */
+	if (metric_str) {
+		if (!get_one_int (info,
+		                  kf_key,
+		                  property_name,
+		                  metric_str,
+		                  G_MAXUINT32,
+		                  &u32))
+			return NULL;
+		metric = u32;
+	}
+
+	route = nm_ip_route_new (family,
+	                         dest_str,
+	                         plen,
+	                         gateway_str,
+	                         metric,
+	                         &error);
+	if (!route) {
+		handle_warn (info,
+		             kf_key,
+		             property_name,
+		             NM_KEYFILE_WARN_SEVERITY_WARN,
+		             _("ignoring invalid %s route: %s"),
+		             family == AF_INET ? "IPv4" : "IPv6",
+		             error->message);
+		g_error_free (error);
+	}
+
+	return route;
+}
+
+/* On success, returns pointer to the zero-terminated field (original @current).
+ * The @current * pointer target is set to point to the rest of the input
+ * or %NULL if there is no more input. Sets error to %NULL for convenience.
+ *
+ * On failure, returns %NULL (unspecified). The @current pointer target is
+ * resets to its original value to allow skipping fields. The @error target
+ * is set to the character that breaks the parsing or %NULL if @current was %NULL.
+ *
+ * When @current target is %NULL, gracefully fail returning %NULL while
+ * leaving the @current target %NULL end setting @error to %NULL;
+ */
+static const char *
+read_field (char **current, const char **out_err_str, const char *characters, const char *delimiters)
+{
+	const char *start;
+
+	nm_assert (current);
+	nm_assert (out_err_str);
+	nm_assert (characters);
+	nm_assert (delimiters);
+
+	*out_err_str = NULL;
+
+	if (!*current) {
+		/* graceful failure, leave '*current' NULL */
+		return NULL;
+	}
+
+	/* fail on empty input */
+	if (!**current)
+		return NULL;
+
+	/* remember beginning of input */
+	start = *current;
+
+	while (**current && strchr (characters, **current))
+		(*current)++;
+	if (**current)
+		if (strchr (delimiters, **current)) {
+			/* success, more data available */
+			*(*current)++ = '\0';
+			return start;
+		} else {
+			/* error, bad character */
+			*out_err_str = *current;
+			*current = (char *) start;
+			return NULL;
+		}
+	else {
+		/* success, end of input */
+		*current = NULL;
+		return start;
+	}
+}
+
+/*****************************************************************************/
+
+#define NM_DBUS_SERVICE_OPENCONNECT    "org.freedesktop.NetworkManager.openconnect"
+#define NM_OPENCONNECT_KEY_GATEWAY     "gateway"
+#define NM_OPENCONNECT_KEY_COOKIE      "cookie"
+#define NM_OPENCONNECT_KEY_GWCERT      "gwcert"
+#define NM_OPENCONNECT_KEY_XMLCONFIG   "xmlconfig"
+#define NM_OPENCONNECT_KEY_LASTHOST    "lasthost"
+#define NM_OPENCONNECT_KEY_AUTOCONNECT "autoconnect"
+#define NM_OPENCONNECT_KEY_CERTSIGS    "certsigs"
+
+static void
+openconnect_fix_secret_flags (NMSetting *setting)
+{
+	NMSettingVpn *s_vpn;
+	NMSettingSecretFlags flags;
+
+	/* Huge hack.  There were some openconnect changes that needed to happen
+	 * pretty late, too late to get into distros.  Migration has already
+	 * happened for many people, and their secret flags are wrong.  But we
+	 * don't want to requrie re-migration, so we have to fix it up here. Ugh.
+	 */
+
+	if (!NM_IS_SETTING_VPN (setting))
+		return;
+
+	s_vpn = NM_SETTING_VPN (setting);
+
+	if (!nm_streq0 (nm_setting_vpn_get_service_type (s_vpn), NM_DBUS_SERVICE_OPENCONNECT))
+		return;
+
+	/* These are different for every login session, and should not be stored */
+	flags = NM_SETTING_SECRET_FLAG_NOT_SAVED;
+	nm_setting_set_secret_flags (NM_SETTING (s_vpn), NM_OPENCONNECT_KEY_GATEWAY, flags, NULL);
+	nm_setting_set_secret_flags (NM_SETTING (s_vpn), NM_OPENCONNECT_KEY_COOKIE, flags, NULL);
+	nm_setting_set_secret_flags (NM_SETTING (s_vpn), NM_OPENCONNECT_KEY_GWCERT, flags, NULL);
+
+	/* These are purely internal data for the auth-dialog, and should be stored */
+	flags = 0;
+	nm_setting_set_secret_flags (NM_SETTING (s_vpn), NM_OPENCONNECT_KEY_XMLCONFIG, flags, NULL);
+	nm_setting_set_secret_flags (NM_SETTING (s_vpn), NM_OPENCONNECT_KEY_LASTHOST, flags, NULL);
+	nm_setting_set_secret_flags (NM_SETTING (s_vpn), NM_OPENCONNECT_KEY_AUTOCONNECT, flags, NULL);
+	nm_setting_set_secret_flags (NM_SETTING (s_vpn), NM_OPENCONNECT_KEY_CERTSIGS, flags, NULL);
+}
+
+/*****************************************************************************/
+
+#define IP_ADDRESS_CHARS "0123456789abcdefABCDEF:.%"
+#define DIGITS "0123456789"
+#define DELIMITERS "/;,"
+
+/* The following IPv4 and IPv6 address formats are supported:
+ *
+ * address (DEPRECATED)
+ * address/plen
+ * address/gateway (DEPRECATED)
+ * address/plen,gateway
+ *
+ * The following IPv4 and IPv6 route formats are supported:
+ *
+ * address/plen (NETWORK dev DEVICE)
+ * address/plen,gateway (NETWORK via GATEWAY dev DEVICE)
+ * address/plen,,metric (NETWORK dev DEVICE metric METRIC)
+ * address/plen,gateway,metric (NETWORK via GATEWAY dev DEVICE metric METRIC)
+ *
+ * For backward, forward and sideward compatibility, slash (/),
+ * semicolon (;) and comma (,) are interchangeable. The choice of
+ * separator in the above examples is therefore not significant.
+ *
+ * Leaving out the prefix length is discouraged and DEPRECATED. The
+ * default value of IPv6 prefix length was 64 and has not been
+ * changed. The default for IPv4 is now 24, which is the closest
+ * IPv4 equivalent. These defaults may just as well be changed to
+ * match the iproute2 defaults (32 for IPv4 and 128 for IPv6).
+ */
+static gpointer
+read_one_ip_address_or_route (KeyfileReaderInfo *info,
+                              const char *property_name,
+                              const char *setting_name,
+                              const char *kf_key,
+                              gboolean ipv6,
+                              gboolean route,
+                              char **out_gateway,
+                              NMSetting *setting)
+{
+	guint plen;
+	gpointer result;
+	const char *address_str;
+	const char *plen_str;
+	const char *gateway_str;
+	const char *metric_str;
+	const char *err_str = NULL;
+	char *current;
+	gs_free char *value = NULL;
+	gs_free char *value_orig = NULL;
+
+#define VALUE_ORIG()   (value_orig ?: (value_orig = nm_keyfile_plugin_kf_get_string (info->keyfile, setting_name, kf_key, NULL)))
+
+	value = nm_keyfile_plugin_kf_get_string (info->keyfile, setting_name, kf_key, NULL);
+	if (!value)
+		return NULL;
+
+	current = value;
+
+	/* get address field */
+	address_str = read_field (&current, &err_str, IP_ADDRESS_CHARS, DELIMITERS);
+	if (err_str) {
+		handle_warn (info,
+		             kf_key,
+		             property_name,
+		             NM_KEYFILE_WARN_SEVERITY_WARN,
+		             _("unexpected character '%c' for address %s: '%s' (position %td)"),
+		             *err_str,
+		             kf_key,
+		             VALUE_ORIG (),
+		             err_str - current);
+		return NULL;
+	}
+	/* get prefix length field (skippable) */
+	plen_str = read_field (&current, &err_str, DIGITS, DELIMITERS);
+	/* get gateway field */
+	gateway_str = read_field (&current, &err_str, IP_ADDRESS_CHARS, DELIMITERS);
+	if (err_str) {
+		handle_warn (info,
+		             kf_key,
+		             property_name,
+		             NM_KEYFILE_WARN_SEVERITY_WARN,
+		             _("unexpected character '%c' for %s: '%s' (position %td)"),
+		             *err_str,
+		             kf_key,
+		             VALUE_ORIG (),
+		             err_str - current);
+		return NULL;
+	}
+	/* for routes, get metric */
+	if (route) {
+		metric_str = read_field (&current, &err_str, DIGITS, DELIMITERS);
+		if (err_str) {
+			handle_warn (info,
+			             kf_key,
+			             property_name,
+			             NM_KEYFILE_WARN_SEVERITY_WARN,
+			             _("unexpected character '%c' in prefix length for %s: '%s' (position %td)"),
+			             *err_str,
+			             kf_key,
+			             VALUE_ORIG (),
+			             err_str - current);
+			return NULL;
+		}
+	} else
+		metric_str = NULL;
+	if (current) {
+		/* there is still some data */
+		if (*current) {
+			/* another field follows */
+			handle_warn (info,
+			             kf_key,
+			             property_name,
+			             NM_KEYFILE_WARN_SEVERITY_WARN,
+			             _("garbage at the end of value %s: '%s'"),
+			             kf_key,
+			             VALUE_ORIG ());
+			return NULL;
+		} else {
+			/* semicolon at the end of input */
+			if (!handle_warn (info,
+			                  kf_key,
+			                  property_name,
+			                  NM_KEYFILE_WARN_SEVERITY_INFO,
+			                  _("deprecated semicolon at the end of value %s: '%s'"),
+			                  kf_key,
+			                  VALUE_ORIG ()))
+				return NULL;
+		}
+	}
+
+#define DEFAULT_PREFIX(for_route, for_ipv6) ( (for_route) ? ( (for_ipv6) ? 128 : 24 ) : ( (for_ipv6) ? 64 : 24 ) )
+
+	/* parse plen, fallback to defaults */
+	if (plen_str) {
+		if (!get_one_int (info,
+		                  kf_key,
+		                  property_name,
+		                  plen_str,
+		                  ipv6 ? 128 : 32,
+		                  &plen)) {
+			plen = DEFAULT_PREFIX (route, ipv6);
+			if (   info->error
+			    || !handle_warn (info,
+			                     kf_key,
+			                     property_name,
+			                     NM_KEYFILE_WARN_SEVERITY_WARN,
+			                     _("invalid prefix length for %s '%s', defaulting to %d"),
+			                     kf_key,
+			                     VALUE_ORIG (),
+			                     plen))
+				return NULL;
+		}
+	} else {
+		plen = DEFAULT_PREFIX (route, ipv6);
+		if (!handle_warn (info,
+		                  kf_key,
+		                  property_name,
+		                  NM_KEYFILE_WARN_SEVERITY_WARN,
+		                  _("missing prefix length for %s '%s', defaulting to %d"),
+		                  kf_key,
+		                  VALUE_ORIG (),
+		                  plen))
+			return NULL;
+	}
+
+	/* build the appropriate data structure for NetworkManager settings */
+	if (route) {
+		result = build_route (info,
+		                      kf_key,
+		                      property_name,
+		                      ipv6 ? AF_INET6 : AF_INET,
+		                      address_str,
+		                      plen,
+		                      gateway_str,
+		                      metric_str);
+	} else {
+		result = build_address (info,
+		                        kf_key,
+		                        property_name,
+		                        ipv6 ? AF_INET6 : AF_INET,
+		                        address_str,
+		                        plen);
+		if (!result)
+			return NULL;
+		if (gateway_str)
+			NM_SET_OUT (out_gateway, g_strdup (gateway_str));
+	}
+
+#undef VALUE_ORIG
+
+	return result;
+}
+
+static void
+fill_route_attributes (GKeyFile *kf, NMIPRoute *route, const char *setting, const char *key, int family)
+{
+	gs_free char *value = NULL;
+	gs_unref_hashtable GHashTable *hash = NULL;
+	GHashTableIter iter;
+	char *name;
+	GVariant *variant;
+
+	value = nm_keyfile_plugin_kf_get_string (kf, setting, key, NULL);
+	if (!value || !value[0])
+		return;
+
+	hash = nm_utils_parse_variant_attributes (value, ',', '=', TRUE,
+	                                          nm_ip_route_get_variant_attribute_spec (),
+	                                          NULL);
+	if (hash) {
+		g_hash_table_iter_init (&iter, hash);
+		while (g_hash_table_iter_next (&iter, (gpointer *) &name, (gpointer *) &variant)) {
+			if (nm_ip_route_attribute_validate (name, variant, family, NULL, NULL))
+				nm_ip_route_set_attribute (route, name, g_variant_ref (variant));
+		}
+	}
+}
+
+typedef struct {
+	const char *s_key;
+	gint32 key_idx;
+	gint8 key_type;
+} BuildListData;
+
+typedef enum {
+	BUILD_LIST_TYPE_ADDRESSES,
+	BUILD_LIST_TYPE_ROUTES,
+	BUILD_LIST_TYPE_ROUTING_RULES,
+} BuildListType;
+
+static int
+_build_list_data_cmp (gconstpointer p_a, gconstpointer p_b, gpointer user_data)
+{
+	const BuildListData *a = p_a;
+	const BuildListData *b = p_b;
+
+	NM_CMP_FIELD (a, b, key_idx);
+	NM_CMP_FIELD (a, b, key_type);
+	NM_CMP_FIELD_STR (a, b, s_key);
+	return 0;
+}
+
+static gboolean
+_build_list_data_is_shadowed (const BuildListData *build_list,
+                              gsize build_list_len,
+                              gsize idx)
+{
+	/* the keyfile contains duplicate keys, which are both returned
+	 * by g_key_file_get_keys() (WHY??).
+	 *
+	 * Skip the earlier one. */
+	return    idx + 1 < build_list_len
+	       && build_list[idx].key_idx == build_list[idx + 1].key_idx
+	       && build_list[idx].key_type == build_list[idx + 1].key_type
+	       && nm_streq (build_list[idx].s_key, build_list[idx + 1].s_key);
+}
+
+static gboolean
+_build_list_match_key_w_name_impl (const char *key,
+                                   const char *base_name,
+                                   gsize base_name_l,
+                                   gint32 *out_key_idx)
+{
+	gint64 v;
+
+	/* some very strict parsing. */
+
+	/* the key must start with base_name. */
+	if (strncmp (key, base_name, base_name_l) != 0)
+		return FALSE;
+
+	key += base_name_l;
+	if (key[0] == '\0') {
+		/* if key is identical to base_name, that's good. */
+		NM_SET_OUT (out_key_idx, -1);
+		return TRUE;
+	}
+
+	/* if base_name is followed by a zero, then it must be
+	 * only a zero, nothing else. */
+	if (key[0] == '0') {
+		if (key[1] != '\0')
+			return FALSE;
+		NM_SET_OUT (out_key_idx, 0);
+		return TRUE;
+	}
+
+	/* otherwise, it can only be followed by a non-zero decimal. */
+	if (!(key[0] >= '1' && key[0] <= '9'))
+		return FALSE;
+	/* and all remaining chars must be decimals too. */
+	if (!NM_STRCHAR_ALL (&key[1], ch, g_ascii_isdigit (ch)))
+		return FALSE;
+
+	/* and it must be convertible to a (positive) int. */
+	v = _nm_utils_ascii_str_to_int64 (key, 10, 0, G_MAXINT32, -1);
+	if (v < 0)
+		return FALSE;
+
+	/* good */
+	NM_SET_OUT (out_key_idx, v);
+	return TRUE;
+}
+
+#define _build_list_match_key_w_name(key, base_name, out_key_idx) \
+	_build_list_match_key_w_name_impl (key, base_name, NM_STRLEN (base_name), out_key_idx)
+
+static BuildListData *
+_build_list_create (GKeyFile *keyfile,
+                    const char *group_name,
+                    BuildListType build_list_type,
+                    gsize *out_build_list_len,
+                    char ***out_keys_strv)
+{
+	gs_strfreev char **keys = NULL;
+	gsize i_keys, n_keys;
+	gs_free BuildListData *build_list = NULL;
+	gsize build_list_len = 0;
+
+	nm_assert (out_build_list_len && *out_build_list_len == 0);
+	nm_assert (out_keys_strv && !*out_keys_strv);
+
+	keys = nm_keyfile_plugin_kf_get_keys (keyfile, group_name, &n_keys, NULL);
+	if (n_keys == 0)
+		return NULL;
+
+	for (i_keys = 0; i_keys < n_keys; i_keys++) {
+		const char *s_key = keys[i_keys];
+		gint32 key_idx;
+		gint8 key_type = 0;
+
+		switch (build_list_type) {
+		case BUILD_LIST_TYPE_ROUTES:
+			if (_build_list_match_key_w_name (s_key, "route", &key_idx))
+				key_type = 0;
+			else if (_build_list_match_key_w_name (s_key, "routes", &key_idx))
+				key_type = 1;
+			else
+				continue;
+			break;
+		case BUILD_LIST_TYPE_ADDRESSES:
+			if (_build_list_match_key_w_name (s_key, "address", &key_idx))
+				key_type = 0;
+			else if (_build_list_match_key_w_name (s_key, "addresses", &key_idx))
+				key_type = 1;
+			else
+				continue;
+			break;
+		case BUILD_LIST_TYPE_ROUTING_RULES:
+			if (_build_list_match_key_w_name (s_key, "routing-rule", &key_idx))
+				key_type = 0;
+			else
+				continue;
+			break;
+		default:
+			nm_assert_not_reached ();
+			break;
+		}
+
+		if (G_UNLIKELY (!build_list))
+			build_list = g_new (BuildListData, n_keys - i_keys);
+
+		build_list[build_list_len++] = (BuildListData) {
+			.s_key    = s_key,
+			.key_idx  = key_idx,
+			.key_type = key_type,
+		};
+	}
+
+	if (build_list_len == 0)
+		return NULL;
+
+	if (build_list_len > 1) {
+		g_qsort_with_data (build_list,
+		                   build_list_len,
+		                   sizeof (BuildListData),
+		                   _build_list_data_cmp,
+		                   NULL);
+	}
+
+	*out_build_list_len = build_list_len;
+	*out_keys_strv = g_steal_pointer (&keys);
+	return g_steal_pointer (&build_list);
+}
+
+static void
+ip_address_or_route_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *setting_key)
+{
+	const char *setting_name = nm_setting_get_name (setting);
+	gboolean is_ipv6 = nm_streq (setting_name, "ipv6");
+	gboolean is_routes = nm_streq (setting_key, "routes");
+	gs_free char *gateway = NULL;
+	gs_unref_ptrarray GPtrArray *list = NULL;
+	gs_strfreev char **keys = NULL;
+	gs_free BuildListData *build_list = NULL;
+	gsize i_build_list, build_list_len = 0;
+
+	build_list = _build_list_create (info->keyfile,
+	                                 setting_name,
+	                                   is_routes
+	                                 ? BUILD_LIST_TYPE_ROUTES
+	                                 : BUILD_LIST_TYPE_ADDRESSES,
+	                                 &build_list_len,
+	                                 &keys);
+	if (!build_list)
+		return;
+
+	list = g_ptr_array_new_with_free_func (is_routes
+	                                       ? (GDestroyNotify) nm_ip_route_unref
+	                                       : (GDestroyNotify) nm_ip_address_unref);
+
+	for (i_build_list = 0; i_build_list < build_list_len; i_build_list++) {
+		const char *s_key;
+		gpointer item;
+
+		if (_build_list_data_is_shadowed (build_list, build_list_len, i_build_list))
+			continue;
+
+		s_key = build_list[i_build_list].s_key;
+		item = read_one_ip_address_or_route (info,
+		                                     setting_key,
+		                                     setting_name,
+		                                     s_key,
+		                                     is_ipv6,
+		                                     is_routes,
+		                                     gateway ? NULL : &gateway,
+		                                     setting);
+		if (item && is_routes) {
+			char options_key[128];
+
+			nm_sprintf_buf (options_key, "%s_options", s_key);
+			fill_route_attributes (info->keyfile,
+			                       item,
+			                       setting_name,
+			                       options_key,
+			                       is_ipv6 ? AF_INET6 : AF_INET);
+		}
+
+		if (info->error)
+			return;
+
+		if (item)
+			g_ptr_array_add (list, item);
+	}
+
+	if (list->len >= 1)
+		g_object_set (setting, setting_key, list, NULL);
+
+	if (gateway)
+		g_object_set (setting, "gateway", gateway, NULL);
+}
+
+static void
+ip_routing_rule_parser_full (KeyfileReaderInfo *info,
+                             const NMMetaSettingInfo *setting_info,
+                             const NMSettInfoProperty *property_info,
+                             const ParseInfoProperty *pip,
+                             NMSetting *setting)
+{
+	const char *setting_name = nm_setting_get_name (setting);
+	gboolean is_ipv6 = nm_streq (setting_name, "ipv6");
+	gs_strfreev char **keys = NULL;
+	gs_free BuildListData *build_list = NULL;
+	gsize i_build_list, build_list_len = 0;
+
+	build_list = _build_list_create (info->keyfile,
+	                                 setting_name,
+	                                 BUILD_LIST_TYPE_ROUTING_RULES,
+	                                 &build_list_len,
+	                                 &keys);
+	if (!build_list)
+		return;
+
+	for (i_build_list = 0; i_build_list < build_list_len; i_build_list++) {
+		nm_auto_unref_ip_routing_rule NMIPRoutingRule *rule = NULL;
+		gs_free char *value = NULL;
+		gs_free_error GError *local = NULL;
+
+		if (_build_list_data_is_shadowed (build_list, build_list_len, i_build_list))
+			continue;
+
+		value = nm_keyfile_plugin_kf_get_string (info->keyfile,
+		                                         setting_name,
+		                                         build_list[i_build_list].s_key,
+		                                         NULL);
+		if (!value)
+			continue;
+
+		rule = nm_ip_routing_rule_from_string (value,
+		                                       (  NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE
+		                                        | (  is_ipv6
+		                                           ? NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET6
+		                                           : NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET)),
+		                                       NULL,
+		                                       &local);
+		if (!rule) {
+			if (!handle_warn (info,
+			                  build_list[i_build_list].s_key,
+			                  property_info->name,
+			                  NM_KEYFILE_WARN_SEVERITY_WARN,
+			                  _("invalid value for \"%s\": %s"),
+			                  build_list[i_build_list].s_key,
+			                  local->message))
+				return;
+			continue;
+		}
+
+		nm_setting_ip_config_add_routing_rule (NM_SETTING_IP_CONFIG (setting), rule);
+	}
+}
+
+static void
+ip_dns_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
+{
+	int addr_family;
+	gs_strfreev char **list = NULL;
+	gsize i, n, length;
+
+	nm_assert (NM_IS_SETTING_IP4_CONFIG (setting) || NM_IS_SETTING_IP6_CONFIG (setting));
+
+	list = nm_keyfile_plugin_kf_get_string_list (info->keyfile,
+	                                             nm_setting_get_name (setting),
+	                                             key,
+	                                             &length,
+	                                             NULL);
+	nm_assert (length == NM_PTRARRAY_LEN (list));
+	if (length == 0)
+		return;
+
+	addr_family = NM_IS_SETTING_IP4_CONFIG (setting) ? AF_INET : AF_INET6;
+
+	n = 0;
+	for (i = 0; i < length; i++) {
+		NMIPAddr addr;
+
+		if (inet_pton (addr_family, list[i], &addr) <= 0) {
+			if (!handle_warn (info,
+			                  key,
+			                  key,
+			                  NM_KEYFILE_WARN_SEVERITY_WARN,
+			                  _("ignoring invalid DNS server IPv%c address '%s'"),
+			                  nm_utils_addr_family_to_char (addr_family),
+			                  list[i])) {
+				do {
+					nm_clear_g_free (&list[i]);
+				} while (++i < length);
+				return;
+			}
+			nm_clear_g_free (&list[i]);
+			continue;
+		}
+
+		if (n != i)
+			list[n] = g_steal_pointer (&list[i]);
+		n++;
+	}
+
+	g_object_set (setting, key, list, NULL);
+}
+
+static void
+ip6_addr_gen_mode_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
+{
+	NMSettingIP6ConfigAddrGenMode addr_gen_mode;
+	const char *setting_name = nm_setting_get_name (setting);
+	gs_free char *s = NULL;
+
+	s = nm_keyfile_plugin_kf_get_string (info->keyfile, setting_name, key, NULL);
+	if (s) {
+		if (!nm_utils_enum_from_str (nm_setting_ip6_config_addr_gen_mode_get_type (), s,
+		                             (int *) &addr_gen_mode, NULL)) {
+			handle_warn (info,
+			             key,
+			             key,
+			             NM_KEYFILE_WARN_SEVERITY_WARN,
+			             _("invalid option '%s', use one of [%s]"),
+			             s,
+			             "eui64,stable-privacy");
+			return;
+		}
+	} else
+		addr_gen_mode = NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_EUI64;
+
+	g_object_set (G_OBJECT (setting), key, (int) addr_gen_mode, NULL);
+}
+
+static void
+mac_address_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key, gsize addr_len, gboolean cloned_mac_addr)
+{
+	const char *setting_name = nm_setting_get_name (setting);
+	char addr_str[NM_UTILS_HWADDR_LEN_MAX * 3];
+	guint8 addr_bin[NM_UTILS_HWADDR_LEN_MAX];
+	gs_free char *tmp_string = NULL;
+	gs_free guint *int_list = NULL;
+	const char *mac_str;
+	gsize int_list_len;
+	gsize i;
+
+	nm_assert (addr_len > 0);
+	nm_assert (addr_len <= NM_UTILS_HWADDR_LEN_MAX);
+
+	tmp_string = nm_keyfile_plugin_kf_get_string (info->keyfile, setting_name, key, NULL);
+
+	if (   cloned_mac_addr
+	    && NM_CLONED_MAC_IS_SPECIAL (tmp_string)) {
+		mac_str = tmp_string;
+		goto out;
+	}
+
+	if (   tmp_string
+	    && nm_utils_hwaddr_aton (tmp_string, addr_bin, addr_len))
+		goto good_addr_bin;
+
+	/* Old format; list of ints */
+	int_list = nm_keyfile_plugin_kf_get_integer_list_uint (info->keyfile, setting_name, key, &int_list_len, NULL);
+	if (int_list_len == addr_len) {
+		for (i = 0; i < addr_len; i++) {
+			const guint val = int_list[i];
+
+			if (val > 255)
+				break;
+			addr_bin[i] = (guint8) val;
+		}
+		if (i == addr_len)
+			goto good_addr_bin;
+	}
+
+	handle_warn (info,
+	             key,
+	             key,
+	             NM_KEYFILE_WARN_SEVERITY_WARN,
+	             _("ignoring invalid MAC address"));
+	return;
+
+good_addr_bin:
+	nm_utils_bin2hexstr_full (addr_bin, addr_len, ':', TRUE, addr_str);
+	mac_str = addr_str;
+
+out:
+	g_object_set (setting, key, mac_str, NULL);
+}
+
+static void
+mac_address_parser_ETHER (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
+{
+	mac_address_parser (info, setting, key, ETH_ALEN, FALSE);
+}
+
+static void
+mac_address_parser_ETHER_cloned (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
+{
+	mac_address_parser (info, setting, key, ETH_ALEN, TRUE);
+}
+
+static void
+mac_address_parser_INFINIBAND (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
+{
+	mac_address_parser (info, setting, key, INFINIBAND_ALEN, FALSE);
+}
+
+static void
+read_hash_of_string (GKeyFile *file, NMSetting *setting, const char *key)
+{
+	gs_strfreev char **keys = NULL;
+	const char *const*iter;
+	const char *setting_name = nm_setting_get_name (setting);
+	gboolean is_vpn;
+	gsize n_keys;
+
+	nm_assert (   (NM_IS_SETTING_VPN (setting)  && nm_streq (key, NM_SETTING_VPN_DATA))
+	           || (NM_IS_SETTING_VPN (setting)  && nm_streq (key, NM_SETTING_VPN_SECRETS))
+	           || (NM_IS_SETTING_BOND (setting) && nm_streq (key, NM_SETTING_BOND_OPTIONS))
+	           || (NM_IS_SETTING_USER (setting) && nm_streq (key, NM_SETTING_USER_DATA)));
+
+	keys = nm_keyfile_plugin_kf_get_keys (file, setting_name, &n_keys, NULL);
+	if (n_keys == 0)
+		return;
+
+	if (   (is_vpn = NM_IS_SETTING_VPN (setting))
+	    || NM_IS_SETTING_BOND (setting)) {
+		for (iter = (const char *const*) keys; *iter; iter++) {
+			gs_free char *to_free = NULL;
+			gs_free char *value = NULL;
+			const char *name;
+
+			value = nm_keyfile_plugin_kf_get_string (file, setting_name, *iter, NULL);
+			if (!value)
+				continue;
+
+			name = nm_keyfile_key_decode (*iter, &to_free);
+
+			if (is_vpn) {
+				/* Add any item that's not a class property to the data hash */
+				if (!g_object_class_find_property (G_OBJECT_GET_CLASS (setting), name))
+					nm_setting_vpn_add_data_item (NM_SETTING_VPN (setting), name, value);
+			} else {
+				if (!nm_streq (name, "interface-name"))
+					nm_setting_bond_add_option (NM_SETTING_BOND (setting), name, value);
+			}
+		}
+		openconnect_fix_secret_flags (setting);
+		return;
+	}
+
+	if (NM_IS_SETTING_USER (setting)) {
+		gs_unref_hashtable GHashTable *data = NULL;
+
+		data = g_hash_table_new_full (nm_str_hash, g_str_equal, g_free, g_free);
+		for (iter = (const char *const*) keys; *iter; iter++) {
+			gs_free char *to_free = NULL;
+			char *value = NULL;
+			const char *name;
+
+			value = nm_keyfile_plugin_kf_get_string (file, setting_name, *iter, NULL);
+			if (!value)
+				continue;
+			name = nm_keyfile_key_decode (*iter, &to_free);
+			g_hash_table_insert (data,
+			                     g_steal_pointer (&to_free) ?: g_strdup (name),
+			                     value);
+		}
+		g_object_set (setting, NM_SETTING_USER_DATA, data, NULL);
+		return;
+	}
+
+	nm_assert_not_reached ();
+}
+
+static gsize
+unescape_semicolons (char *str)
+{
+	gsize i, j;
+
+	for (i = 0, j = 0; str[i]; ) {
+		if (str[i] == '\\' && str[i+1] == ';')
+			i++;
+		str[j++] = str[i++];;
+	}
+	nm_explicit_bzero (&str[j], i - j);
+	return j;
+}
+
+static GBytes *
+get_bytes (KeyfileReaderInfo *info,
+           const char *setting_name,
+           const char *key,
+           gboolean zero_terminate,
+           gboolean unescape_semicolon)
+{
+	nm_auto_free_secret char *tmp_string = NULL;
+	gboolean may_be_int_list = TRUE;
+	gsize length;
+	GBytes *result;
+
+	/* New format: just a string
+	 * Old format: integer list; e.g. 11;25;38;
+	 */
+	tmp_string = nm_keyfile_plugin_kf_get_string (info->keyfile, setting_name, key, NULL);
+	if (!tmp_string)
+		return NULL;
+
+	/* if the string is empty, we return an empty GBytes array.
+	 * Note that for NM_SETTING_802_1X_PASSWORD_RAW both %NULL and
+	 * an empty GBytes are valid, and shall be destinguished. */
+	if (!tmp_string[0]) {
+		/* note that even if @zero_terminate is TRUE, we return an empty
+		 * byte-array. The reason is that zero_terminate is there to terminate
+		 * *valid* strings. It's not there to terminated invalid (empty) strings.
+		 */
+		return g_bytes_new_static ("", 0);
+	}
+
+	for (length = 0; tmp_string[length]; length++) {
+		const char ch = tmp_string[length];
+
+		if (   !g_ascii_isspace (ch)
+		    && !g_ascii_isdigit (ch)
+		    && ch != ';') {
+			may_be_int_list = FALSE;
+			length += strlen (&tmp_string[length]);
+			break;
+		}
+	}
+
+	/* Try to parse the string as a integer list. */
+	if (may_be_int_list && length > 0) {
+		nm_auto_free_secret_buf NMSecretBuf *bin = NULL;
+		const char *const s = tmp_string;
+		gsize i, d;
+
+		bin = nm_secret_buf_new (length / 2 + 3);
+
+#define DIGIT(c) ((c) - '0')
+		i = 0;
+		d = 0;
+		while (TRUE) {
+			int n;
+
+			/* leading whitespace */
+			while (g_ascii_isspace (s[i]))
+				i++;
+			if (s[i] == '\0')
+				break;
+			/* then expect 1 to 3 digits */
+			if (!g_ascii_isdigit (s[i])) {
+				d = 0;
+				break;
+			}
+			n = DIGIT (s[i]);
+			i++;
+			if (g_ascii_isdigit (s[i])) {
+				n = 10 * n + DIGIT (s[i]);
+				i++;
+				if (g_ascii_isdigit (s[i])) {
+					n = 10 * n + DIGIT (s[i]);
+					i++;
+				}
+			}
+			if (n > 255) {
+				d = 0;
+				break;
+			}
+
+			nm_assert (d < bin->len);
+			bin->bin[d++] = n;
+
+			/* allow whitespace after the digit. */
+			while (g_ascii_isspace (s[i]))
+				i++;
+			/* need a semicolon as separator. */
+			if (s[i] != ';') {
+				d = 0;
+				break;
+			}
+			i++;
+		}
+#undef DIGIT
+
+		/* Old format; list of ints. We already did a strict validation of the
+		 * string format before. We expect that this conversion cannot fail. */
+		if (d > 0) {
+			/* note that @zero_terminate does not add a terminating '\0' to
+			 * binary data as an integer list. If the bytes are expressed as
+			 * an integer list, all potential NUL characters are supposed to
+			 * be included there explicitly.
+			 *
+			 * However, in the spirit of defensive programming, we do append a
+			 * NUL character to the buffer, although this character is hidden
+			 * and only a mitigation for bugs. */
+
+			if (d + 10 < bin->len) {
+				/* hm, too much unused memory. Copy the memory to a suitable
+				 * sized buffer. */
+				return nm_secret_copy_to_gbytes (bin->bin, d);
+			}
+
+			nm_assert (d < bin->len);
+			bin->bin[d] = '\0';
+			return nm_secret_buf_to_gbytes_take (g_steal_pointer (&bin), d);
+		}
+	}
+
+	/* Handle as a simple string (ie, new format) */
+	if (unescape_semicolon)
+		length = unescape_semicolons (tmp_string);
+	if (zero_terminate)
+		length++;
+	if (length == 0)
+		return NULL;
+
+	result = g_bytes_new_with_free_func (tmp_string,
+	                                     length,
+	                                     (GDestroyNotify) nm_free_secret,
+	                                     tmp_string);
+	tmp_string = NULL;
+	return result;
+}
+
+static void
+ssid_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
+{
+	const char *setting_name = nm_setting_get_name (setting);
+	gs_unref_bytes GBytes *bytes = NULL;
+
+	bytes = get_bytes (info, setting_name, key, FALSE, TRUE);
+	if (!bytes) {
+		handle_warn (info,
+		             key,
+		             key,
+		             NM_KEYFILE_WARN_SEVERITY_WARN,
+		             _("ignoring invalid SSID"));
+		return;
+	}
+	g_object_set (setting, key, bytes, NULL);
+}
+
+static void
+password_raw_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
+{
+	const char *setting_name = nm_setting_get_name (setting);
+	gs_unref_bytes GBytes *bytes = NULL;
+
+	bytes = get_bytes (info, setting_name, key, FALSE, TRUE);
+	if (!bytes) {
+		handle_warn (info,
+		             key,
+		             key,
+		             NM_KEYFILE_WARN_SEVERITY_WARN,
+		             _("ignoring invalid raw password"));
+		return;
+	}
+	g_object_set (setting, key, bytes, NULL);
+}
+
+static char *
+get_cert_path (const char *base_dir, const guint8 *cert_path, gsize cert_path_len)
+{
+	const char *base;
+	char *p = NULL, *path, *tmp;
+
+	g_return_val_if_fail (base_dir != NULL, NULL);
+	g_return_val_if_fail (cert_path != NULL, NULL);
+
+	path = g_strndup ((char *) cert_path, cert_path_len);
+
+	if (path[0] == '/')
+		return path;
+
+	base = path;
+	p = strrchr (path, '/');
+	if (p)
+		base = p + 1;
+
+	tmp = g_build_path ("/", base_dir, base, NULL);
+	g_free (path);
+	return tmp;
+}
+
+static const char *certext[] = { ".pem", ".cert", ".crt", ".cer", ".p12", ".der", ".key" };
+
+static gboolean
+has_cert_ext (const char *path)
+{
+	int i;
+
+	for (i = 0; i < G_N_ELEMENTS (certext); i++) {
+		if (g_str_has_suffix (path, certext[i]))
+			return TRUE;
+	}
+	return FALSE;
+}
+
+char *
+nm_keyfile_detect_unqualified_path_scheme (const char *base_dir,
+                                           gconstpointer pdata,
+                                           gsize data_len,
+                                           gboolean consider_exists,
+                                           gboolean *out_exists)
+{
+	const char *data = pdata;
+	gboolean exists = FALSE;
+	gsize validate_len;
+	gsize path_len, pathuri_len;
+	gs_free char *path = NULL;
+	gs_free char *pathuri = NULL;
+
+	g_return_val_if_fail (base_dir && base_dir[0] == '/', NULL);
+
+	if (!pdata)
+		return NULL;
+	if (data_len == -1)
+		data_len = strlen (data);
+	if (data_len > 500 || data_len < 1)
+		return NULL;
+
+	/* If there's a trailing zero tell g_utf8_validate() to validate until the zero */
+	if (data[data_len - 1] == '\0') {
+		/* setting it to -1, would mean we accept data to contain NUL characters before the
+		 * end. Don't accept any NUL in [0 .. data_len-1[ . */
+		validate_len = data_len - 1;
+	} else
+		validate_len = data_len;
+	if (   validate_len == 0
+	    || g_utf8_validate ((const char *) data, validate_len, NULL) == FALSE)
+		 return NULL;
+
+	/* Might be a bare path without the file:// prefix; in that case
+	 * if it's an absolute path, use that, otherwise treat it as a
+	 * relative path to the current directory.
+	 */
+
+	path = get_cert_path (base_dir, (const guint8 *) data, data_len);
+
+	/* FIXME(keyfile-parse-in-memory): it is wrong that keyfile reader makes decisions based on
+	 * the file systems content. The serialization/parsing should be entirely in-memory. */
+	if (   !memchr (data, '/', data_len)
+	    && !has_cert_ext (path)) {
+		if (!consider_exists)
+			return NULL;
+		exists = g_file_test (path, G_FILE_TEST_EXISTS);
+		if (!exists)
+			return NULL;
+	} else if (out_exists)
+		exists = g_file_test (path, G_FILE_TEST_EXISTS);
+
+	/* Construct the proper value as required for the PATH scheme.
+	 *
+	 * When returning TRUE, we must also be sure that @data_len does not look like
+	 * the deprecated format of list of integers. With this implementation that is the
+	 * case, as long as @consider_exists is FALSE. */
+	path_len = strlen (path);
+	pathuri_len = (NM_STRLEN (NM_KEYFILE_CERT_SCHEME_PREFIX_PATH) + 1) + path_len;
+	pathuri = g_new (char, pathuri_len);
+	memcpy (pathuri, NM_KEYFILE_CERT_SCHEME_PREFIX_PATH, NM_STRLEN (NM_KEYFILE_CERT_SCHEME_PREFIX_PATH));
+	memcpy (&pathuri[NM_STRLEN (NM_KEYFILE_CERT_SCHEME_PREFIX_PATH)], path, path_len + 1);
+	if (nm_setting_802_1x_check_cert_scheme (pathuri, pathuri_len, NULL) != NM_SETTING_802_1X_CK_SCHEME_PATH)
+		return NULL;
+
+	NM_SET_OUT (out_exists, exists);
+	return g_steal_pointer (&pathuri);
+}
+
+#define HAS_SCHEME_PREFIX(bin, bin_len, scheme) \
+	({ \
+		const char *const _bin = (bin); \
+		const gsize _bin_len = (bin_len); \
+		\
+		nm_assert (_bin && _bin_len > 0); \
+		\
+		(   _bin_len > NM_STRLEN (scheme) + 1 \
+		 && _bin[_bin_len - 1] == '\0' \
+		 && memcmp (_bin, scheme, NM_STRLEN (scheme)) == 0); \
+	})
+
+static void
+cert_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
+{
+	const char *setting_name = nm_setting_get_name (setting);
+	gs_unref_bytes GBytes *bytes = NULL;
+	const char *bin = NULL;
+	gsize bin_len = 0;
+	char *path;
+	gboolean path_exists;
+
+	bytes = get_bytes (info, setting_name, key, TRUE, FALSE);
+	if (bytes)
+		bin = g_bytes_get_data (bytes, &bin_len);
+	if (bin_len == 0) {
+		if (!info->error) {
+			handle_warn (info,
+			             key,
+			             key,
+			             NM_KEYFILE_WARN_SEVERITY_WARN,
+			             _("invalid key/cert value"));
+		}
+		return;
+	}
+
+	if (HAS_SCHEME_PREFIX (bin, bin_len, NM_KEYFILE_CERT_SCHEME_PREFIX_PATH)) {
+		const char *path2 = &bin[NM_STRLEN (NM_KEYFILE_CERT_SCHEME_PREFIX_PATH)];
+		gs_free char *path2_free = NULL;
+
+		if (nm_setting_802_1x_check_cert_scheme (bin, bin_len, NULL) != NM_SETTING_802_1X_CK_SCHEME_PATH) {
+			handle_warn (info,
+			             key,
+			             key,
+			             NM_KEYFILE_WARN_SEVERITY_WARN,
+			             _("invalid key/cert value path \"%s\""),
+			             bin);
+			return;
+		}
+
+		g_object_set (setting, key, bytes, NULL);
+
+		if (path2[0] != '/') {
+			/* we want to read absolute paths because we use keyfile as exchange
+			 * between different processes which might not have the same cwd. */
+			path2_free = get_cert_path (info->base_dir, (const guint8 *) path2,
+			                            bin_len - NM_STRLEN (NM_KEYFILE_CERT_SCHEME_PREFIX_PATH) - 1);
+			path2 = path2_free;
+		}
+
+		/* FIXME(keyfile-parse-in-memory): keyfile reader must not access the file system and
+		 * (in a first step) only operate in memory-only. If the presence of files should be checked,
+		 * then by invoking a callback (and possibly keyfile settings plugin would
+		 * collect the file names to be checked and check them later). */
+		if (!g_file_test (path2, G_FILE_TEST_EXISTS)) {
+			handle_warn (info,
+			             key,
+			             key,
+			             NM_KEYFILE_WARN_SEVERITY_INFO_MISSING_FILE,
+			             _("certificate or key file '%s' does not exist"),
+			             path2);
+		}
+		return;
+	}
+
+	if (HAS_SCHEME_PREFIX (bin, bin_len, NM_KEYFILE_CERT_SCHEME_PREFIX_PKCS11)) {
+		if (nm_setting_802_1x_check_cert_scheme (bin, bin_len, NULL) != NM_SETTING_802_1X_CK_SCHEME_PKCS11) {
+			handle_warn (info,
+			             key,
+			             key,
+			             NM_KEYFILE_WARN_SEVERITY_WARN,
+			             _("invalid PKCS#11 URI \"%s\""),
+			             bin);
+			return;
+		}
+
+		g_object_set (setting, key, bytes, NULL);
+		return;
+	}
+
+	if (HAS_SCHEME_PREFIX (bin, bin_len, NM_KEYFILE_CERT_SCHEME_PREFIX_BLOB)) {
+		const char *cdata = bin + NM_STRLEN (NM_KEYFILE_CERT_SCHEME_PREFIX_BLOB);
+		gsize cdata_len = bin_len - NM_STRLEN (NM_KEYFILE_CERT_SCHEME_PREFIX_BLOB) - 1;
+		gs_free guchar *bin_decoded = NULL;
+		gsize bin_decoded_len = 0;
+		gsize i;
+		gboolean valid_base64;
+		gs_unref_bytes GBytes *val = NULL;
+
+		/* Let's be strict here. We expect valid base64, no funny stuff!!
+		 * We didn't write such invalid data ourselfes and refuse to read it as blob. */
+		if ((valid_base64 = (cdata_len % 4 == 0))) {
+			for (i = 0; i < cdata_len; i++) {
+				char c = cdata[i];
+
+				if (!(   (c >= 'a' && c <= 'z')
+				      || (c >= 'A' && c <= 'Z')
+				      || (c >= '0' && c <= '9')
+				      || (c == '+' || c == '/'))) {
+					if (c != '=' || i < cdata_len - 2)
+						valid_base64 = FALSE;
+					else {
+						for (; i < cdata_len; i++) {
+							if (cdata[i] != '=')
+								valid_base64 = FALSE;
+						}
+					}
+					break;
+				}
+			}
+		}
+		if (valid_base64)
+			bin_decoded = g_base64_decode (cdata, &bin_decoded_len);
+
+		if (bin_decoded_len == 0) {
+			handle_warn (info,
+			             key,
+			             key,
+			             NM_KEYFILE_WARN_SEVERITY_WARN,
+			             _("invalid key/cert value data:;base64, is not base64"));
+			return;
+		}
+
+		if (nm_setting_802_1x_check_cert_scheme (bin_decoded, bin_decoded_len, NULL) != NM_SETTING_802_1X_CK_SCHEME_BLOB) {
+			/* The blob probably starts with "file://". Setting the cert data will confuse NMSetting8021x.
+			 * In fact this is a limitation of NMSetting8021x which does not support setting blobs that start
+			 * with file://. Just warn and return TRUE to signal that we ~handled~ the setting. */
+			handle_warn (info,
+			             key,
+			             key,
+			             NM_KEYFILE_WARN_SEVERITY_WARN,
+			             _("invalid key/cert value data:;base64,file://"));
+			return;
+		}
+
+		val = g_bytes_new_take (g_steal_pointer (&bin_decoded), bin_decoded_len);
+		g_object_set (setting, key, val, NULL);
+		return;
+	}
+
+	/* If not, it might be a plain path */
+	path = nm_keyfile_detect_unqualified_path_scheme (info->base_dir, bin, bin_len, TRUE, &path_exists);
+	if (path) {
+		gs_unref_bytes GBytes *val = NULL;
+
+		/* Construct the proper value as required for the PATH scheme */
+		val = g_bytes_new_take (path, strlen (path) + 1);
+		g_object_set (setting, key, val, NULL);
+
+		/* Warn if the certificate didn't exist */
+		if (!path_exists) {
+			handle_warn (info,
+			             key,
+			             key,
+			             NM_KEYFILE_WARN_SEVERITY_INFO_MISSING_FILE,
+			             _("certificate or key file '%s' does not exist"),
+			             path);
+		}
+		return;
+	}
+
+	if (nm_setting_802_1x_check_cert_scheme (bin, bin_len, NULL) != NM_SETTING_802_1X_CK_SCHEME_BLOB) {
+		/* The blob probably starts with "file://" but contains invalid characters for a path.
+		 * Setting the cert data will confuse NMSetting8021x.
+		 * In fact, NMSetting8021x does not support setting such binary data, so just warn and
+		 * continue. */
+		handle_warn (info,
+		             key,
+		             key,
+		             NM_KEYFILE_WARN_SEVERITY_WARN,
+		             _("invalid key/cert value is not a valid blob"));
+		return;
+	}
+
+	g_object_set (setting, key, bytes, NULL);
+}
+
+static int
+_parity_from_char (int ch)
+{
+#if NM_MORE_ASSERTS > 5
+	{
+		static char check = 0;
+
+		if (check == 0) {
+			nm_auto_unref_gtypeclass GEnumClass *klass = g_type_class_ref (NM_TYPE_SETTING_SERIAL_PARITY);
+			guint i;
+
+			check = 1;
+
+			/* In older versions, parity was G_TYPE_CHAR/gint8, and the character
+			 * value was stored as integer.
+			 * For example parity=69 equals parity=E, meaning NM_SETTING_SERIAL_PARITY_EVEN.
+			 *
+			 * That means, certain values are reserved. Assert that these numbers
+			 * are not reused when we extend NMSettingSerialParity enum.
+			 * Actually, since NM_SETTING_SERIAL_PARITY is g_param_spec_enum(),
+			 * we anyway cannot extend the enum without breaking API...
+			 *
+			 * [1] commit "a91e60902e libnm-core: make NMSettingSerial:parity an enum"
+			 * [2] https://cgit.freedesktop.org/NetworkManager/NetworkManager/commit/?id=a91e60902eabae1de93d61323dae6ac894b5d40f
+			 */
+			g_assert (G_IS_ENUM_CLASS (klass));
+			for (i = 0; i < klass->n_values; i++) {
+				const GEnumValue *v = &klass->values[i];
+				int num = v->value;
+
+				g_assert (_parity_from_char (num) == -1);
+				g_assert (!NM_IN_SET (num, 'e', 'E', 'o', 'O', 'n', 'N'));
+			}
+		}
+	}
+#endif
+
+	switch (ch) {
+	case 'E':
+	case 'e':
+		return NM_SETTING_SERIAL_PARITY_EVEN;
+	case 'O':
+	case 'o':
+		return NM_SETTING_SERIAL_PARITY_ODD;
+	case 'N':
+	case 'n':
+		return NM_SETTING_SERIAL_PARITY_NONE;
+	}
+
+	return -1;
+}
+
+static void
+parity_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
+{
+	const char *setting_name = nm_setting_get_name (setting);
+	gs_free_error GError *err = NULL;
+	int parity;
+	gs_free char *tmp_str = NULL;
+	gint64 i64;
+
+	/* Keyfile traditionally stored this as the ASCII value for 'E', 'o', or 'n'.
+	 * We now accept either that or the (case-insensitive) character itself (but
+	 * still always write it the old way, for backward compatibility).
+	 */
+	tmp_str = nm_keyfile_plugin_kf_get_value (info->keyfile, setting_name, key, &err);
+	if (err)
+		goto out_err;
+
+	if (   tmp_str
+	    && tmp_str[0] != '\0'
+	    && tmp_str[1] == '\0') {
+		/* the ASCII characters like 'E' are taken directly... */
+		parity = _parity_from_char (tmp_str[0]);
+		if (parity >= 0)
+			goto parity_good;
+	}
+
+	i64 = _nm_utils_ascii_str_to_int64 (tmp_str, 0, G_MININT, G_MAXINT, G_MININT64);
+	if (   i64 != G_MININT64
+	    && errno == 0) {
+
+		if ((parity = _parity_from_char (i64)) >= 0) {
+			/* another oddity: the string is a valid number. However, if the numeric values
+			 * is one of the supported ASCII codes, accept it (like 69 for 'E').
+			 */
+			goto parity_good;
+		}
+
+		/* Finally, take the numeric value as is. */
+		parity = i64;
+		goto parity_good;
+	}
+
+	handle_warn (info,
+	             key,
+	             key,
+	             NM_KEYFILE_WARN_SEVERITY_WARN,
+	             _("invalid parity value '%s'"),
+	             tmp_str ?: "");
+	return;
+
+parity_good:
+	nm_g_object_set_property_enum (G_OBJECT (setting), key, NM_TYPE_SETTING_SERIAL_PARITY, parity, &err);
+
+out_err:
+	if (!err)
+		return;
+	if (nm_keyfile_error_is_not_found (err)) {
+		/* ignore such errors. The key is not present. */
+		return;
+	}
+	handle_warn (info,
+	             key,
+	             key,
+	             NM_KEYFILE_WARN_SEVERITY_WARN,
+	             _("invalid setting: %s"),
+	             err->message);
+}
+
+static void
+team_config_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
+{
+	const char *setting_name = nm_setting_get_name (setting);
+	gs_free char *conf = NULL;
+	gs_free_error GError *error = NULL;
+
+	conf = nm_keyfile_plugin_kf_get_string (info->keyfile, setting_name, key, NULL);
+
+	g_object_set (G_OBJECT (setting), key, conf, NULL);
+
+	if (   conf
+	    && !nm_setting_verify (setting, NULL, &error)) {
+		handle_warn (info,
+		             key,
+		             key,
+		             NM_KEYFILE_WARN_SEVERITY_WARN,
+		             _("ignoring invalid team configuration: %s"),
+		             error->message);
+		g_object_set (G_OBJECT (setting), key, NULL, NULL);
+	}
+}
+
+static void
+bridge_vlan_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
+{
+	gs_unref_ptrarray GPtrArray *vlans = NULL;
+	gs_free char *value = NULL;
+	gs_free const char **strv = NULL;
+	const char *const *iter;
+	GError *local = NULL;
+	NMBridgeVlan *vlan;
+
+	value = nm_keyfile_plugin_kf_get_string (info->keyfile,
+	                                         nm_setting_get_name (setting),
+	                                         key,
+	                                         NULL);
+	if (!value || !value[0])
+		return;
+
+	vlans = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_bridge_vlan_unref);
+
+	strv = nm_utils_escaped_tokens_split (value, ",");
+	if (strv) {
+		for (iter = strv; *iter; iter++) {
+			vlan = nm_bridge_vlan_from_str (*iter, &local);
+			if (!vlan) {
+				handle_warn (info,
+				             key,
+				             key,
+				             NM_KEYFILE_WARN_SEVERITY_WARN,
+				             "invalid bridge VLAN: %s",
+				             local->message);
+				g_clear_error (&local);
+				continue;
+			}
+			g_ptr_array_add (vlans, vlan);
+		}
+	}
+
+	if (vlans->len > 0)
+		g_object_set (setting, key, vlans, NULL);
+}
+
+static void
+qdisc_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
+{
+	const char *setting_name = nm_setting_get_name (setting);
+	gs_unref_ptrarray GPtrArray *qdiscs = NULL;
+	gs_strfreev char **keys = NULL;
+	gsize n_keys = 0;
+	int i;
+
+	keys = nm_keyfile_plugin_kf_get_keys (info->keyfile, setting_name, &n_keys, NULL);
+	if (n_keys == 0)
+		return;
+
+	qdiscs = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_tc_qdisc_unref);
+
+	for (i = 0; i < n_keys; i++) {
+		NMTCQdisc *qdisc;
+		const char *qdisc_parent;
+		gs_free char *qdisc_rest = NULL;
+		gs_free char *qdisc_str = NULL;
+		gs_free_error GError *err = NULL;
+
+		if (!g_str_has_prefix (keys[i], "qdisc."))
+			continue;
+
+		qdisc_parent = keys[i] + sizeof ("qdisc.") - 1;
+		qdisc_rest = nm_keyfile_plugin_kf_get_string (info->keyfile, setting_name, keys[i], NULL);
+		qdisc_str = g_strdup_printf ("%s%s %s",
+		                             _nm_utils_parse_tc_handle (qdisc_parent, NULL) != TC_H_UNSPEC ? "parent " : "",
+		                             qdisc_parent,
+		                             qdisc_rest);
+
+		qdisc = nm_utils_tc_qdisc_from_str (qdisc_str, &err);
+		if (!qdisc) {
+			handle_warn (info,
+			             keys[i],
+			             key,
+			             NM_KEYFILE_WARN_SEVERITY_WARN,
+			             _("invalid qdisc: %s"),
+			             err->message);
+		} else {
+			g_ptr_array_add (qdiscs, qdisc);
+		}
+	}
+
+	if (qdiscs->len >= 1)
+		g_object_set (setting, key, qdiscs, NULL);
+}
+
+static void
+tfilter_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
+{
+	const char *setting_name = nm_setting_get_name (setting);
+	gs_unref_ptrarray GPtrArray *tfilters = NULL;
+	gs_strfreev char **keys = NULL;
+	gsize n_keys = 0;
+	int i;
+
+	keys = nm_keyfile_plugin_kf_get_keys (info->keyfile, setting_name, &n_keys, NULL);
+	if (n_keys == 0)
+		return;
+
+	tfilters = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_tc_tfilter_unref);
+
+	for (i = 0; i < n_keys; i++) {
+		NMTCTfilter *tfilter;
+		const char *tfilter_parent;
+		gs_free char *tfilter_rest = NULL;
+		gs_free char *tfilter_str = NULL;
+		gs_free_error GError *err = NULL;
+
+		if (!g_str_has_prefix (keys[i], "tfilter."))
+			continue;
+
+		tfilter_parent = keys[i] + sizeof ("tfilter.") - 1;
+		tfilter_rest = nm_keyfile_plugin_kf_get_string (info->keyfile, setting_name, keys[i], NULL);
+		tfilter_str = g_strdup_printf ("%s%s %s",
+		                             _nm_utils_parse_tc_handle (tfilter_parent, NULL) != TC_H_UNSPEC ? "parent " : "",
+		                             tfilter_parent,
+		                             tfilter_rest);
+
+		tfilter = nm_utils_tc_tfilter_from_str (tfilter_str, &err);
+		if (!tfilter) {
+			handle_warn (info,
+			             keys[i],
+			             key,
+			             NM_KEYFILE_WARN_SEVERITY_WARN,
+			             _("invalid tfilter: %s"),
+			             err->message);
+		} else {
+			g_ptr_array_add (tfilters, tfilter);
+		}
+	}
+
+	if (tfilters->len >= 1)
+		g_object_set (setting, key, tfilters, NULL);
+}
+
+/*****************************************************************************/
+
+/* Some setting properties also contain setting names, such as
+ * NMSettingConnection's 'type' property (which specifies the base type of the
+ * connection, eg ethernet or wifi) or the 802-11-wireless setting's
+ * 'security' property which specifies whether or not the AP requires
+ * encryption.  This function handles translating those properties' values
+ * from the real setting name to the more-readable alias.
+ */
+static void
+setting_alias_writer (KeyfileWriterInfo *info,
+                      NMSetting *setting,
+                      const char *key,
+                      const GValue *value)
+{
+	const char *str, *alias;
+
+	str = g_value_get_string (value);
+	alias = nm_keyfile_plugin_get_alias_for_setting_name (str);
+	nm_keyfile_plugin_kf_set_string (info->keyfile,
+	                                 nm_setting_get_name (setting),
+	                                 key,
+	                                 alias ?: str);
+}
+
+static void
+sriov_vfs_writer (KeyfileWriterInfo *info,
+                  NMSetting *setting,
+                  const char *key,
+                  const GValue *value)
+{
+	GPtrArray *vfs;
+	guint i;
+
+	vfs = g_value_get_boxed (value);
+	if (!vfs)
+		return;
+
+	for (i = 0; i < vfs->len; i++) {
+		const NMSriovVF *vf = vfs->pdata[i];
+		gs_free char *kf_value = NULL;
+		char kf_key[32];
+
+		kf_value = nm_utils_sriov_vf_to_str (vf, TRUE, NULL);
+		if (!kf_value)
+			continue;
+
+		nm_sprintf_buf (kf_key, "vf.%u", nm_sriov_vf_get_index (vf));
+
+		nm_keyfile_plugin_kf_set_string (info->keyfile,
+		                                 nm_setting_get_name (setting),
+		                                 kf_key,
+		                                 kf_value);
+	}
+}
+
+static void
+write_array_of_uint (GKeyFile *file,
+                     NMSetting *setting,
+                     const char *key,
+                     const GValue *value)
+{
+	GArray *array;
+
+	array = g_value_get_boxed (value);
+
+	nm_assert (!array || g_array_get_element_size (array) == sizeof (guint));
+
+	if (!array || !array->len)
+		return;
+
+	nm_keyfile_plugin_kf_set_integer_list_uint (file,
+	                                            nm_setting_get_name (setting),
+	                                            key,
+	                                            (const guint *) array->data,
+	                                            array->len);
+}
+
+static void
+dns_writer (KeyfileWriterInfo *info,
+            NMSetting *setting,
+            const char *key,
+            const GValue *value)
+{
+	char **list;
+
+	list = g_value_get_boxed (value);
+	if (list && list[0]) {
+		nm_keyfile_plugin_kf_set_string_list (info->keyfile, nm_setting_get_name (setting), key,
+		                                      (const char **) list, g_strv_length (list));
+	}
+}
+
+static void
+ip6_addr_gen_mode_writer (KeyfileWriterInfo *info,
+                          NMSetting *setting,
+                          const char *key,
+                          const GValue *value)
+{
+	NMSettingIP6ConfigAddrGenMode addr_gen_mode;
+	gs_free char *str = NULL;
+
+	addr_gen_mode = (NMSettingIP6ConfigAddrGenMode) g_value_get_int (value);
+	str = nm_utils_enum_to_str (nm_setting_ip6_config_addr_gen_mode_get_type (),
+	                            addr_gen_mode);
+	nm_keyfile_plugin_kf_set_string (info->keyfile,
+	                                 nm_setting_get_name (setting),
+	                                 key,
+	                                 str);
+}
+
+static void
+write_ip_values (GKeyFile *file,
+                 const char *setting_name,
+                 GPtrArray *array,
+                 const char *gateway,
+                 gboolean is_route)
+{
+	nm_auto_free_gstring GString *output = NULL;
+	int addr_family;
+	guint i;
+	const char *addr;
+	const char *gw;
+	guint32 plen;
+	char key_name[64];
+	char *key_name_idx;
+
+	if (!array->len)
+		return;
+
+	addr_family =   nm_streq (setting_name, NM_SETTING_IP4_CONFIG_SETTING_NAME)
+	              ? AF_INET
+	              : AF_INET6;
+
+	strcpy (key_name, is_route ? "route" : "address");
+	key_name_idx = key_name + strlen (key_name);
+
+	output = g_string_sized_new (2*INET_ADDRSTRLEN + 10);
+	for (i = 0; i < array->len; i++) {
+		gint64 metric = -1;
+
+		if (is_route) {
+			NMIPRoute *route = array->pdata[i];
+
+			addr = nm_ip_route_get_dest (route);
+			plen = nm_ip_route_get_prefix (route);
+			gw = nm_ip_route_get_next_hop (route);
+			metric = nm_ip_route_get_metric (route);
+		} else {
+			NMIPAddress *address = array->pdata[i];
+
+			addr = nm_ip_address_get_address (address);
+			plen = nm_ip_address_get_prefix (address);
+			gw =   (i == 0)
+			     ? gateway
+			     : NULL;
+		}
+
+		g_string_set_size (output, 0);
+		g_string_append_printf (output, "%s/%u", addr, plen);
+		if (   metric != -1
+		    || gw) {
+			/* Older versions of the plugin do not support the form
+			 * "a.b.c.d/plen,,metric", so, we always have to write the
+			 * gateway, even if there isn't one.
+			 * The current version supports reading of the above form.
+			 */
+			if (!gw) {
+				if (addr_family == AF_INET)
+					gw = "0.0.0.0";
+				else
+					gw = "::";
+			}
+
+			g_string_append_printf (output, ",%s", gw);
+			if (   is_route
+			    && metric != -1)
+				g_string_append_printf (output, ",%lu", (unsigned long) metric);
+		}
+
+		sprintf (key_name_idx, "%u", i + 1);
+		nm_keyfile_plugin_kf_set_string (file, setting_name, key_name, output->str);
+
+		if (is_route) {
+			gs_free char *attributes = NULL;
+
+			attributes = nm_utils_format_variant_attributes (_nm_ip_route_get_attributes (array->pdata[i]),
+			                                                 ',', '=');
+			if (attributes) {
+				g_strlcat (key_name, "_options", sizeof (key_name));
+				nm_keyfile_plugin_kf_set_string (file, setting_name, key_name, attributes);
+			}
+		}
+	}
+}
+
+static void
+addr_writer (KeyfileWriterInfo *info,
+             NMSetting *setting,
+             const char *key,
+             const GValue *value)
+{
+	GPtrArray *array;
+	const char *setting_name = nm_setting_get_name (setting);
+	const char *gateway = nm_setting_ip_config_get_gateway (NM_SETTING_IP_CONFIG (setting));
+
+	array = (GPtrArray *) g_value_get_boxed (value);
+	if (array && array->len)
+		write_ip_values (info->keyfile, setting_name, array, gateway, FALSE);
+}
+
+static void
+route_writer (KeyfileWriterInfo *info,
+              NMSetting *setting,
+              const char *key,
+              const GValue *value)
+{
+	GPtrArray *array;
+	const char *setting_name = nm_setting_get_name (setting);
+
+	array = (GPtrArray *) g_value_get_boxed (value);
+	if (array && array->len)
+		write_ip_values (info->keyfile, setting_name, array, NULL, TRUE);
+}
+
+static void
+bridge_vlan_writer (KeyfileWriterInfo *info,
+                    NMSetting *setting,
+                    const char *key,
+                    const GValue *value)
+{
+	NMBridgeVlan *vlan;
+	GPtrArray *vlans;
+	GString *string;
+	guint i;
+
+	vlans = (GPtrArray *) g_value_get_boxed (value);
+	if (!vlans || !vlans->len)
+		return;
+
+	string = g_string_new ("");
+	for (i = 0; i < vlans->len; i++) {
+		gs_free char *vlan_str = NULL;
+
+		vlan = vlans->pdata[i];
+		vlan_str = nm_bridge_vlan_to_str (vlan, NULL);
+		if (!vlan_str)
+			continue;
+		if (string->len > 0)
+			g_string_append (string, ",");
+		nm_utils_escaped_tokens_escape_gstr_assert (vlan_str, ",", string);
+	}
+
+	nm_keyfile_plugin_kf_set_string (info->keyfile,
+	                                 nm_setting_get_name (setting),
+	                                 "vlans",
+	                                 string->str);
+
+	g_string_free (string, TRUE);
+}
+
+
+#define ETHERNET_S390_OPTIONS_GROUP_NAME "ethernet-s390-options"
+
+static void
+wired_s390_options_parser_full (KeyfileReaderInfo *info,
+                                const NMMetaSettingInfo *setting_info,
+                                const NMSettInfoProperty *property_info,
+                                const ParseInfoProperty *pip,
+                                NMSetting *setting)
+{
+	NMSettingWired *s_wired = NM_SETTING_WIRED (setting);
+	gs_strfreev char **keys = NULL;
+	gsize n_keys;
+	gsize i;
+
+	keys = nm_keyfile_plugin_kf_get_keys (info->keyfile, ETHERNET_S390_OPTIONS_GROUP_NAME, &n_keys, NULL);
+	for (i = 0; i < n_keys; i++) {
+		gs_free char *value = NULL;
+		gs_free char *key_to_free = NULL;
+
+		value = nm_keyfile_plugin_kf_get_string (info->keyfile,
+		                                         ETHERNET_S390_OPTIONS_GROUP_NAME,
+		                                         keys[i],
+		                                         NULL);
+		if (!value)
+			continue;
+
+		nm_setting_wired_add_s390_option (s_wired,
+		                                  nm_keyfile_key_decode (keys[i],
+		                                                         &key_to_free),
+		                                  value);
+	}
+}
+
+static void
+wired_s390_options_writer_full (KeyfileWriterInfo *info,
+                                const NMMetaSettingInfo *setting_info,
+                                const NMSettInfoProperty *property_info,
+                                const ParseInfoProperty *pip,
+                                NMSetting *setting)
+{
+	NMSettingWired *s_wired = NM_SETTING_WIRED (setting);
+	guint i, n;
+
+	n = nm_setting_wired_get_num_s390_options (s_wired);
+	for (i = 0; i < n; i++) {
+		const char *opt_key;
+		const char *opt_val;
+		gs_free char *key_to_free = NULL;
+
+		nm_setting_wired_get_s390_option (s_wired, i, &opt_key, &opt_val);
+		nm_keyfile_plugin_kf_set_string (info->keyfile,
+		                                 ETHERNET_S390_OPTIONS_GROUP_NAME,
+		                                 nm_keyfile_key_encode (opt_key, &key_to_free),
+		                                 opt_val);
+	}
+}
+
+static void
+ip_routing_rule_writer_full (KeyfileWriterInfo *info,
+                             const NMMetaSettingInfo *setting_info,
+                             const NMSettInfoProperty *property_info,
+                             const ParseInfoProperty *pip,
+                             NMSetting *setting)
+{
+	const char *setting_name = nm_setting_get_name (setting);
+	NMSettingIPConfig *s_ip = NM_SETTING_IP_CONFIG (setting);
+	guint i, j, n;
+	char key_name_full[100] = "routing-rule";
+	char *key_name_num = &key_name_full[NM_STRLEN ("routing-rule")];
+
+	n = nm_setting_ip_config_get_num_routing_rules (s_ip);
+	j = 0;
+	for (i = 0; i < n; i++) {
+		NMIPRoutingRule *rule = nm_setting_ip_config_get_routing_rule (s_ip, i);
+		gs_free char *str = NULL;
+
+		str = nm_ip_routing_rule_to_string (rule,
+		                                    NM_IP_ROUTING_RULE_AS_STRING_FLAGS_NONE,
+		                                    NULL,
+		                                    NULL);
+		if (!str)
+			continue;
+
+		sprintf (key_name_num, "%u", ++j);
+		nm_keyfile_plugin_kf_set_string (info->keyfile,
+		                                 setting_name,
+		                                 key_name_full,
+		                                 str);
+	}
+}
+
+static void
+qdisc_writer (KeyfileWriterInfo *info,
+              NMSetting *setting,
+              const char *key,
+              const GValue *value)
+{
+	gsize i;
+	GPtrArray *array;
+
+	array = (GPtrArray *) g_value_get_boxed (value);
+	if (!array || !array->len)
+		return;
+
+	for (i = 0; i < array->len; i++) {
+		NMTCQdisc *qdisc = array->pdata[i];
+		GString *key_name = g_string_sized_new (16);
+		GString *value_str = g_string_sized_new (60);
+
+		g_string_append (key_name, "qdisc.");
+		_nm_utils_string_append_tc_parent (key_name, NULL,
+		                                   nm_tc_qdisc_get_parent (qdisc));
+		_nm_utils_string_append_tc_qdisc_rest (value_str, qdisc);
+
+		nm_keyfile_plugin_kf_set_string (info->keyfile,
+		                                 NM_SETTING_TC_CONFIG_SETTING_NAME,
+		                                 key_name->str,
+		                                 value_str->str);
+
+		g_string_free (key_name, TRUE);
+		g_string_free (value_str, TRUE);
+	}
+}
+
+static void
+tfilter_writer (KeyfileWriterInfo *info,
+              NMSetting *setting,
+              const char *key,
+              const GValue *value)
+{
+	gsize i;
+	GPtrArray *array;
+
+	array = (GPtrArray *) g_value_get_boxed (value);
+	if (!array || !array->len)
+		return;
+
+	for (i = 0; i < array->len; i++) {
+		NMTCTfilter *tfilter = array->pdata[i];
+		GString *key_name = g_string_sized_new (16);
+		GString *value_str = g_string_sized_new (60);
+
+		g_string_append (key_name, "tfilter.");
+		_nm_utils_string_append_tc_parent (key_name, NULL,
+		                                   nm_tc_tfilter_get_parent (tfilter));
+		_nm_utils_string_append_tc_tfilter_rest (value_str, tfilter, NULL);
+
+		nm_keyfile_plugin_kf_set_string (info->keyfile,
+		                                 NM_SETTING_TC_CONFIG_SETTING_NAME,
+		                                 key_name->str,
+		                                 value_str->str);
+
+		g_string_free (key_name, TRUE);
+		g_string_free (value_str, TRUE);
+	}
+}
+
+static void
+write_hash_of_string (GKeyFile *file,
+                      NMSetting *setting,
+                      const char *key,
+                      const GValue *value)
+{
+	GHashTable *hash;
+	const char *group_name = nm_setting_get_name (setting);
+	gboolean vpn_secrets = FALSE;
+	gs_free const char **keys = NULL;
+	guint i, l;
+
+	nm_assert (   (NM_IS_SETTING_VPN (setting)  && nm_streq (key, NM_SETTING_VPN_DATA))
+	           || (NM_IS_SETTING_VPN (setting)  && nm_streq (key, NM_SETTING_VPN_SECRETS))
+	           || (NM_IS_SETTING_BOND (setting) && nm_streq (key, NM_SETTING_BOND_OPTIONS))
+	           || (NM_IS_SETTING_USER (setting) && nm_streq (key, NM_SETTING_USER_DATA)));
+
+	/* Write VPN secrets out to a different group to keep them separate */
+	if (   NM_IS_SETTING_VPN (setting)
+	    && nm_streq (key, NM_SETTING_VPN_SECRETS)) {
+		group_name = NM_KEYFILE_GROUP_VPN_SECRETS;
+		vpn_secrets = TRUE;
+	}
+
+	hash = g_value_get_boxed (value);
+
+	keys = nm_utils_strdict_get_keys (hash, TRUE, &l);
+	for (i = 0; i < l; i++) {
+		gs_free char *to_free = NULL;
+		const char *property, *data;
+
+		property = keys[i];
+
+		/* Handle VPN secrets specially; they are nested in the property's hash;
+		 * we don't want to write them if the secret is not saved, not required,
+		 * or owned by a user's secret agent.
+		 */
+		if (vpn_secrets) {
+			NMSettingSecretFlags secret_flags = NM_SETTING_SECRET_FLAG_NONE;
+
+			if (!nm_setting_get_secret_flags (setting, property, &secret_flags, NULL))
+				nm_assert_not_reached ();
+			if (!_secret_flags_persist_secret (secret_flags))
+				continue;
+		}
+
+		data = g_hash_table_lookup (hash, property);
+		nm_keyfile_plugin_kf_set_string (file, group_name,
+		                                 nm_keyfile_key_encode (property, &to_free),
+		                                 data);
+	}
+}
+
+static void
+ssid_writer (KeyfileWriterInfo *info,
+             NMSetting *setting,
+             const char *key,
+             const GValue *value)
+{
+	GBytes *bytes;
+	const guint8 *ssid_data;
+	gsize ssid_len;
+	const char *setting_name = nm_setting_get_name (setting);
+	gboolean new_format = TRUE;
+	gsize semicolons = 0;
+	gsize i;
+
+	g_return_if_fail (G_VALUE_HOLDS (value, G_TYPE_BYTES));
+
+	bytes = g_value_get_boxed (value);
+	if (!bytes)
+		return;
+	ssid_data = g_bytes_get_data (bytes, &ssid_len);
+	if (!ssid_data || !ssid_len) {
+		nm_keyfile_plugin_kf_set_string (info->keyfile, setting_name, key, "");
+		return;
+	}
+
+	/* Check whether each byte is printable.  If not, we have to use an
+	 * integer list, otherwise we can just use a string.
+	 */
+	for (i = 0; i < ssid_len; i++) {
+		const char c = ssid_data[i];
+
+		if (!g_ascii_isprint (c)) {
+			new_format = FALSE;
+			break;
+		}
+		if (c == ';')
+			semicolons++;
+	}
+
+	if (new_format) {
+		gs_free char *ssid = NULL;
+
+		if (semicolons == 0)
+			ssid = g_strndup ((char *) ssid_data, ssid_len);
+		else {
+			/* Escape semicolons with backslashes to make strings
+			 * containing ';', such as '16;17;' unambiguous */
+			gsize j = 0;
+
+			ssid = g_malloc (ssid_len + semicolons + 1);
+			for (i = 0; i < ssid_len; i++) {
+				if (ssid_data[i] == ';')
+					ssid[j++] = '\\';
+				ssid[j++] = ssid_data[i];
+			}
+			ssid[j] = '\0';
+		}
+		nm_keyfile_plugin_kf_set_string (info->keyfile, setting_name, key, ssid);
+	} else
+		nm_keyfile_plugin_kf_set_integer_list_uint8 (info->keyfile, setting_name, key, ssid_data, ssid_len);
+}
+
+static void
+password_raw_writer (KeyfileWriterInfo *info,
+                     NMSetting *setting,
+                     const char *key,
+                     const GValue *value)
+{
+	const char *setting_name = nm_setting_get_name (setting);
+	GBytes *array;
+	gsize len;
+	const guint8 *data;
+
+	g_return_if_fail (G_VALUE_HOLDS (value, G_TYPE_BYTES));
+
+	array = (GBytes *) g_value_get_boxed (value);
+	if (!array)
+		return;
+	data = g_bytes_get_data (array, &len);
+	if (!data)
+		len = 0;
+	nm_keyfile_plugin_kf_set_integer_list_uint8 (info->keyfile, setting_name, key, data, len);
+}
+
+/*****************************************************************************/
+
+static void
+cert_writer_default (NMConnection *connection,
+                     GKeyFile *file,
+                     NMSetting8021x *setting,
+                     const char *setting_name,
+                     const NMSetting8021xSchemeVtable *vtable)
+{
+	NMSetting8021xCKScheme scheme;
+
+	scheme = vtable->scheme_func (setting);
+	if (scheme == NM_SETTING_802_1X_CK_SCHEME_PATH) {
+		gs_free char *path_free = NULL;
+		gs_free char *base_dir = NULL;
+		gs_free char *tmp = NULL;
+		const char *path;
+
+		path = vtable->path_func (setting);
+		g_assert (path);
+
+		/* If the path is relative, make it an absolute path.
+		 * Relative paths make a keyfile not easily usable in another
+		 * context. */
+		if (path[0] && path[0] != '/') {
+			base_dir = g_get_current_dir ();
+			path_free = g_strconcat (base_dir, "/", path, NULL);
+			path = path_free;
+		} else
+			base_dir = g_path_get_dirname (path);
+
+		/* path cannot start with "file://" or "data:;base64,", because it is an absolute path.
+		 * Still, make sure that a prefix-less path will be recognized. This can happen
+		 * for example if the path is longer then 500 chars. */
+		tmp = nm_keyfile_detect_unqualified_path_scheme (base_dir, path, -1, FALSE, NULL);
+		if (tmp)
+			nm_clear_g_free (&tmp);
+		else {
+			tmp = g_strconcat (NM_KEYFILE_CERT_SCHEME_PREFIX_PATH, path, NULL);
+			path = tmp;
+		}
+
+		/* Path contains at least a '/', hence it cannot be recognized as the old
+		 * binary format consisting of a list of integers. */
+
+		nm_keyfile_plugin_kf_set_string (file, setting_name, vtable->setting_key, path);
+	} else if (scheme == NM_SETTING_802_1X_CK_SCHEME_BLOB) {
+		GBytes *blob;
+		const guint8 *blob_data;
+		gsize blob_len;
+		gs_free char *blob_base64 = NULL;
+		gs_free char *val = NULL;
+
+		blob = vtable->blob_func (setting);
+		g_assert (blob);
+		blob_data = g_bytes_get_data (blob, &blob_len);
+
+		blob_base64 = g_base64_encode (blob_data, blob_len);
+		val = g_strconcat (NM_KEYFILE_CERT_SCHEME_PREFIX_BLOB, blob_base64, NULL);
+
+		nm_keyfile_plugin_kf_set_string (file, setting_name, vtable->setting_key, val);
+	} else if (scheme == NM_SETTING_802_1X_CK_SCHEME_PKCS11) {
+		nm_keyfile_plugin_kf_set_string (file, setting_name, vtable->setting_key,
+		                                 vtable->uri_func (setting));
+	} else {
+		/* scheme_func() returns UNKNOWN in all other cases. The only valid case
+		 * where a scheme is allowed to be UNKNOWN, is unsetting the value. In this
+		 * case, we don't expect the writer to be called, because the default value
+		 * will not be serialized.
+		 * The only other reason for the scheme to be UNKNOWN is an invalid cert.
+		 * But our connection verifies, so that cannot happen either. */
+		g_return_if_reached ();
+	}
+}
+
+static void
+cert_writer (KeyfileWriterInfo *info,
+             NMSetting *setting,
+             const char *key,
+             const GValue *value)
+{
+	const NMSetting8021xSchemeVtable *vtable = NULL;
+	const char *setting_name;
+	guint i;
+
+	for (i = 0; nm_setting_8021x_scheme_vtable[i].setting_key; i++) {
+		if (nm_streq0 (nm_setting_8021x_scheme_vtable[i].setting_key, key)) {
+			vtable = &nm_setting_8021x_scheme_vtable[i];
+			break;
+		}
+	}
+	if (!vtable)
+		g_return_if_reached ();
+
+	setting_name = nm_setting_get_name (NM_SETTING (setting));
+
+	if (info->write_handler) {
+		NMKeyfileHandlerData handler_data;
+
+		_key_file_handler_data_init_write (&handler_data,
+		                                   NM_KEYFILE_HANDLER_TYPE_WRITE_CERT,
+		                                   info,
+		                                   setting_name,
+		                                   vtable->setting_key,
+		                                   setting,
+		                                   key);
+		handler_data.write_cert = (NMKeyfileHandlerDataWriteCert) {
+			.vtable  = vtable,
+		};
+
+		if (info->write_handler (info->connection,
+		                         info->keyfile,
+		                         NM_KEYFILE_HANDLER_TYPE_WRITE_CERT,
+		                         &handler_data,
+		                         info->user_data))
+			return;
+		if (info->error)
+			return;
+	}
+
+	cert_writer_default (info->connection,
+	                     info->keyfile,
+	                     NM_SETTING_802_1X (setting),
+	                     setting_name,
+	                     vtable);
+}
+
+/*****************************************************************************/
+
+struct _ParseInfoProperty {
+	const char *property_name;
+	union {
+		void (*parser) (KeyfileReaderInfo *info,
+		                NMSetting *setting,
+		                const char *key);
+		void (*parser_full) (KeyfileReaderInfo *info,
+		                     const NMMetaSettingInfo *setting_info,
+		                     const NMSettInfoProperty *property_info,
+		                     const ParseInfoProperty *pip,
+		                     NMSetting *setting);
+	};
+	union {
+		void (*writer) (KeyfileWriterInfo *info,
+		                NMSetting *setting,
+		                const char *key,
+		                const GValue *value);
+		void (*writer_full) (KeyfileWriterInfo *info,
+		                     const NMMetaSettingInfo *setting_info,
+		                     const NMSettInfoProperty *property_info,
+		                     const ParseInfoProperty *pip,
+		                     NMSetting *setting);
+	};
+	bool parser_skip;
+	bool parser_no_check_key:1;
+	bool writer_skip:1;
+	bool has_writer_full:1;
+	bool has_parser_full:1;
+
+	/* usually, we skip to write values that have their
+	 * default value. By setting this flag to TRUE, also
+	 * default values are written. */
+	bool writer_persist_default:1;
+};
+
+#define PARSE_INFO_PROPERTY(_property_name, ...) \
+	(&((const ParseInfoProperty) { \
+		.property_name = _property_name, \
+		__VA_ARGS__ \
+	}))
+
+#define PARSE_INFO_PROPERTIES(...) \
+	.properties = ((const ParseInfoProperty*const[]) { \
+		__VA_ARGS__ \
+		NULL, \
+	})
+
+typedef struct {
+	const ParseInfoProperty*const*properties;
+} ParseInfoSetting;
+
+#define PARSE_INFO_SETTING(setting_type, ...) \
+	[setting_type] = (&((const ParseInfoSetting) { \
+		__VA_ARGS__ \
+	}))
+
+static const ParseInfoSetting *const parse_infos[_NM_META_SETTING_TYPE_NUM] = {
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_WIRELESS,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_WIRELESS_BSSID,
+				.parser        = mac_address_parser_ETHER,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_WIRELESS_CLONED_MAC_ADDRESS,
+				.parser        = mac_address_parser_ETHER_cloned,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_WIRELESS_MAC_ADDRESS,
+				.parser        = mac_address_parser_ETHER,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_WIRELESS_SSID,
+				.parser        = ssid_parser,
+				.writer        = ssid_writer,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_802_1X,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_802_1X_CA_CERT,
+				.parser        = cert_parser,
+				.writer        = cert_writer,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_802_1X_CLIENT_CERT,
+				.parser        = cert_parser,
+				.writer        = cert_writer,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_802_1X_PASSWORD_RAW,
+				.parser        = password_raw_parser,
+				.writer        = password_raw_writer,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_802_1X_PHASE2_CA_CERT,
+				.parser        = cert_parser,
+				.writer        = cert_writer,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_802_1X_PHASE2_CLIENT_CERT,
+				.parser        = cert_parser,
+				.writer        = cert_writer,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_802_1X_PHASE2_PRIVATE_KEY,
+				.parser        = cert_parser,
+				.writer        = cert_writer,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_802_1X_PRIVATE_KEY,
+				.parser        = cert_parser,
+				.writer        = cert_writer,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_WIRED,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_WIRED_CLONED_MAC_ADDRESS,
+				.parser        = mac_address_parser_ETHER_cloned,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_WIRED_MAC_ADDRESS,
+				.parser        = mac_address_parser_ETHER,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_WIRED_S390_OPTIONS,
+				.parser_no_check_key = TRUE,
+				.parser_full   = wired_s390_options_parser_full,
+				.writer_full   = wired_s390_options_writer_full,
+				.has_parser_full = TRUE,
+				.has_writer_full = TRUE,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_BLUETOOTH,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_BLUETOOTH_BDADDR,
+				.parser        = mac_address_parser_ETHER,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_BOND,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_BOND_OPTIONS,
+				.parser_no_check_key = TRUE,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_BRIDGE,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_BRIDGE_MAC_ADDRESS,
+				.parser        = mac_address_parser_ETHER,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_BRIDGE_VLANS,
+				.parser_no_check_key = TRUE,
+				.parser        = bridge_vlan_parser,
+				.writer        = bridge_vlan_writer,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_BRIDGE_PORT,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_BRIDGE_PORT_VLANS,
+				.parser_no_check_key = TRUE,
+				.parser        = bridge_vlan_parser,
+				.writer        = bridge_vlan_writer,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_CONNECTION,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_CONNECTION_READ_ONLY,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_CONNECTION_TYPE,
+				.parser        = setting_alias_parser,
+				.writer        = setting_alias_writer,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_INFINIBAND,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_INFINIBAND_MAC_ADDRESS,
+				.parser        = mac_address_parser_INFINIBAND,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_IP4_CONFIG,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_IP_CONFIG_ADDRESSES,
+				.parser_no_check_key = TRUE,
+				.parser        = ip_address_or_route_parser,
+				.writer        = addr_writer,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_IP_CONFIG_DNS,
+				.parser_no_check_key = TRUE,
+				.parser        = ip_dns_parser,
+				.writer        = dns_writer,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_IP_CONFIG_GATEWAY,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_IP_CONFIG_ROUTES,
+				.parser_no_check_key = TRUE,
+				.parser        = ip_address_or_route_parser,
+				.writer        = route_writer,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_IP_CONFIG_ROUTING_RULES,
+				.parser_no_check_key = TRUE,
+				.parser_full   = ip_routing_rule_parser_full,
+				.writer_full   = ip_routing_rule_writer_full,
+				.has_parser_full = TRUE,
+				.has_writer_full = TRUE,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_IP6_CONFIG,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE,
+				.parser_no_check_key = TRUE,
+				.parser        = ip6_addr_gen_mode_parser,
+				.writer        = ip6_addr_gen_mode_writer,
+				.writer_persist_default = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_IP_CONFIG_ADDRESSES,
+				.parser_no_check_key = TRUE,
+				.parser        = ip_address_or_route_parser,
+				.writer        = addr_writer,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_IP_CONFIG_DNS,
+				.parser_no_check_key = TRUE,
+				.parser        = ip_dns_parser,
+				.writer        = dns_writer,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_IP_CONFIG_GATEWAY,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_IP_CONFIG_ROUTES,
+				.parser_no_check_key = TRUE,
+				.parser        = ip_address_or_route_parser,
+				.writer        = route_writer,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_IP_CONFIG_ROUTING_RULES,
+				.parser_no_check_key = TRUE,
+				.parser_full   = ip_routing_rule_parser_full,
+				.writer_full   = ip_routing_rule_writer_full,
+				.has_parser_full = TRUE,
+				.has_writer_full = TRUE,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_SERIAL,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_SERIAL_PARITY,
+				.parser        = parity_parser,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_SRIOV,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_SRIOV_VFS,
+				.parser_no_check_key = TRUE,
+				.parser        = sriov_vfs_parser,
+				.writer        = sriov_vfs_writer,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_TC_CONFIG,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_TC_CONFIG_QDISCS,
+				.parser_no_check_key = TRUE,
+				.parser        = qdisc_parser,
+				.writer        = qdisc_writer,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TC_CONFIG_TFILTERS,
+				.parser_no_check_key = TRUE,
+				.parser        = tfilter_parser,
+				.writer        = tfilter_writer,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_TEAM,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_CONFIG,
+				.parser        = team_config_parser,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_LINK_WATCHERS,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_MCAST_REJOIN_COUNT,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_MCAST_REJOIN_INTERVAL,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_NOTIFY_PEERS_COUNT,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_NOTIFY_PEERS_INTERVAL,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_RUNNER,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_RUNNER_ACTIVE,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_RUNNER_AGG_SELECT_POLICY,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_RUNNER_FAST_RATE,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_RUNNER_HWADDR_POLICY,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_RUNNER_MIN_PORTS,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_RUNNER_SYS_PRIO,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_RUNNER_TX_BALANCER,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_RUNNER_TX_BALANCER_INTERVAL,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_RUNNER_TX_HASH,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_TEAM_PORT,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_CONFIG,
+				.parser        = team_config_parser,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_PORT_LACP_KEY,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_PORT_LACP_PRIO,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_PORT_LINK_WATCHERS,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_PORT_PRIO,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_PORT_QUEUE_ID,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_TEAM_PORT_STICKY,
+				.parser_skip   = TRUE,
+				.writer_skip   = TRUE,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_USER,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_USER_DATA,
+				.parser_no_check_key = TRUE,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_VLAN,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_VLAN_FLAGS,
+				.writer_persist_default = TRUE,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_VPN,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_VPN_DATA,
+				.parser_no_check_key = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_VPN_PERSISTENT,
+				.parser_no_check_key = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_VPN_SECRETS,
+				.parser_no_check_key = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_VPN_SERVICE_TYPE,
+				.parser_no_check_key = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_VPN_TIMEOUT,
+				.parser_no_check_key = TRUE,
+			),
+			PARSE_INFO_PROPERTY (NM_SETTING_VPN_USER_NAME,
+				.parser_no_check_key = TRUE,
+			),
+		),
+	),
+	PARSE_INFO_SETTING (NM_META_SETTING_TYPE_WIMAX,
+		PARSE_INFO_PROPERTIES (
+			PARSE_INFO_PROPERTY (NM_SETTING_WIMAX_MAC_ADDRESS,
+				.parser        = mac_address_parser_ETHER,
+			),
+		),
+	),
+};
+
+static void
+_parse_info_find (NMSetting *setting,
+                  const char *property_name,
+                  const NMMetaSettingInfo **out_setting_info,
+                  const ParseInfoSetting **out_parse_info_setting,
+                  const ParseInfoProperty **out_parse_info_property)
+{
+	const NMMetaSettingInfo *setting_info;
+	const ParseInfoSetting *pis;
+	const ParseInfoProperty *pip;
+
+#if NM_MORE_ASSERTS > 10
+	{
+		guint i, j;
+		static int asserted = FALSE;
+
+		if (!asserted) {
+			for (i = 0; i < G_N_ELEMENTS (parse_infos); i++) {
+				pis = parse_infos[i];
+
+				if (!pis)
+					continue;
+				if (!pis->properties)
+					continue;
+
+				g_assert (pis->properties[0]);
+				for (j = 0; pis->properties[j]; j++) {
+					const ParseInfoProperty *pip0;
+					const ParseInfoProperty *pipj = pis->properties[j];
+
+					g_assert (pipj->property_name);
+					if (   j > 0
+					    && (pip0 = pis->properties[j - 1])
+					    && strcmp (pip0->property_name, pipj->property_name) >= 0) {
+						g_error ("Wrong order at index #%d.%d: \"%s.%s\" before \"%s.%s\"",
+						         i, j - 1,
+						         nm_meta_setting_infos[i].setting_name, pip0->property_name,
+						         nm_meta_setting_infos[i].setting_name, pipj->property_name);
+					}
+				}
+			}
+			asserted = TRUE;
+		}
+	}
+#endif
+
+	if (   !NM_IS_SETTING (setting)
+	    || !(setting_info = NM_SETTING_GET_CLASS (setting)->setting_info)) {
+		/* handle invalid setting objects gracefully. */
+		NM_SET_OUT (out_setting_info, NULL);
+		NM_SET_OUT (out_parse_info_setting, NULL);
+		NM_SET_OUT (out_parse_info_property, NULL);
+		return;
+	}
+
+	nm_assert (setting_info->setting_name);
+	nm_assert (_NM_INT_NOT_NEGATIVE (setting_info->meta_type));
+	nm_assert (setting_info->meta_type < G_N_ELEMENTS (parse_infos));
+
+	pis = parse_infos[setting_info->meta_type];
+
+	pip = NULL;
+	if (   pis
+	    && property_name) {
+		gssize idx;
+
+		G_STATIC_ASSERT_EXPR (G_STRUCT_OFFSET (ParseInfoProperty, property_name) == 0);
+
+		idx = nm_utils_ptrarray_find_binary_search ((gconstpointer *) pis->properties,
+		                                            NM_PTRARRAY_LEN (pis->properties),
+		                                            &property_name,
+		                                            nm_strcmp_p_with_data,
+		                                            NULL,
+		                                            NULL,
+		                                            NULL);
+		if (idx >= 0)
+			pip = pis->properties[idx];
+	}
+
+	NM_SET_OUT (out_setting_info, setting_info);
+	NM_SET_OUT (out_parse_info_setting, pis);
+	NM_SET_OUT (out_parse_info_property, pip);
+}
+
+/*****************************************************************************/
+
+static void
+read_one_setting_value (KeyfileReaderInfo *info,
+                        NMSetting *setting,
+                        const NMSettInfoProperty *property_info)
+{
+	GKeyFile *keyfile = info->keyfile;
+	gs_free_error GError *err = NULL;
+	const NMMetaSettingInfo *setting_info;
+	const ParseInfoProperty *pip;
+	gs_free char *tmp_str = NULL;
+	const char *key;
+	GType type;
+	guint64 u64;
+	gint64 i64;
+
+	nm_assert (!info->error);
+	nm_assert (   !property_info->param_spec
+	           || nm_streq (property_info->param_spec->name, property_info->name));
+
+	key = property_info->name;
+
+	_parse_info_find (setting, key, &setting_info, NULL, &pip);
+
+	nm_assert (setting_info);
+
+	if (!pip) {
+		if (nm_streq (key, NM_SETTING_NAME))
+			return;
+		if (!property_info->param_spec)
+			return;
+		if ((property_info->param_spec->flags & (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)) != G_PARAM_WRITABLE)
+			return;
+	} else {
+		if (pip->parser_skip)
+			return;
+		if (pip->has_parser_full) {
+			pip->parser_full (info, setting_info, property_info, pip, setting);
+			return;
+		}
+	}
+
+	nm_assert (property_info->param_spec);
+	nm_assert ((property_info->param_spec->flags & (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)) == G_PARAM_WRITABLE);
+
+	/* Check for the exact key in the GKeyFile if required.  Most setting
+	 * properties map 1:1 to a key in the GKeyFile, but for those properties
+	 * like IP addresses and routes where more than one value is actually
+	 * encoded by the setting property, this won't be true.
+	 */
+	if (   (!pip || !pip->parser_no_check_key)
+	    && !nm_keyfile_plugin_kf_has_key (keyfile, setting_info->setting_name, key, &err)) {
+		/* Key doesn't exist or an error occurred, thus nothing to do. */
+		if (err) {
+			if (!handle_warn (info,
+			                  key,
+			                  key,
+			                  NM_KEYFILE_WARN_SEVERITY_WARN,
+			                  _("error loading setting value: %s"),
+			                  err->message))
+				return;
+		}
+		return;
+	}
+
+	if (   pip
+	    && pip->parser) {
+		pip->parser (info, setting, key);
+		return;
+	}
+
+	type = G_PARAM_SPEC_VALUE_TYPE (property_info->param_spec);
+
+	if (type == G_TYPE_STRING) {
+		gs_free char *str_val = NULL;
+
+		str_val = nm_keyfile_plugin_kf_get_string (keyfile, setting_info->setting_name, key, &err);
+		if (!err)
+			nm_g_object_set_property_string_take (G_OBJECT (setting), key, g_steal_pointer (&str_val), &err);
+	} else if (type == G_TYPE_UINT) {
+		tmp_str = nm_keyfile_plugin_kf_get_value (keyfile, setting_info->setting_name, key, &err);
+		if (!err) {
+			u64 = _nm_utils_ascii_str_to_uint64 (tmp_str, 0, 0, G_MAXUINT, G_MAXUINT64);
+			if (   u64 == G_MAXUINT64
+			    && errno != 0) {
+				g_set_error_literal (&err, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
+				                     _("value cannot be interpreted as integer"));
+			} else
+				nm_g_object_set_property_uint (G_OBJECT (setting), key, u64, &err);
+		}
+	} else if (type == G_TYPE_INT) {
+		tmp_str = nm_keyfile_plugin_kf_get_value (keyfile, setting_info->setting_name, key, &err);
+		if (!err) {
+			i64 = _nm_utils_ascii_str_to_int64 (tmp_str, 0, G_MININT, G_MAXINT, G_MININT64);
+			if (   i64 == G_MININT64
+			    && errno != 0) {
+				g_set_error_literal (&err, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
+				                     _("value cannot be interpreted as integer"));
+			} else
+				nm_g_object_set_property_int (G_OBJECT (setting), key, i64, &err);
+		}
+	} else if (type == G_TYPE_BOOLEAN) {
+		gboolean bool_val;
+
+		bool_val = nm_keyfile_plugin_kf_get_boolean (keyfile, setting_info->setting_name, key, &err);
+		if (!err)
+			nm_g_object_set_property_boolean (G_OBJECT (setting), key, bool_val, &err);
+	} else if (type == G_TYPE_CHAR) {
+		tmp_str = nm_keyfile_plugin_kf_get_value (keyfile, setting_info->setting_name, key, &err);
+		if (!err) {
+			/* As documented by glib, G_TYPE_CHAR is really a (signed!) gint8. */
+			i64 = _nm_utils_ascii_str_to_int64 (tmp_str, 0, G_MININT8, G_MAXINT8, G_MININT64);
+			if (   i64 == G_MININT64
+			    && errno != 0) {
+				g_set_error_literal (&err, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
+				                     _("value cannot be interpreted as integer"));
+			} else
+				nm_g_object_set_property_char (G_OBJECT (setting), key, i64, &err);
+		}
+	} else if (type == G_TYPE_UINT64) {
+		tmp_str = nm_keyfile_plugin_kf_get_value (keyfile, setting_info->setting_name, key, &err);
+		if (!err) {
+			u64 = _nm_utils_ascii_str_to_uint64 (tmp_str, 0, 0, G_MAXUINT64, G_MAXUINT64);
+			if (   u64 == G_MAXUINT64
+			    && errno != 0) {
+				g_set_error_literal (&err, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
+				                     _("value cannot be interpreted as integer"));
+			} else
+				nm_g_object_set_property_uint64 (G_OBJECT (setting), key, u64, &err);
+		}
+	} else if (type == G_TYPE_INT64) {
+		tmp_str = nm_keyfile_plugin_kf_get_value (keyfile, setting_info->setting_name, key, &err);
+		if (!err) {
+			i64 = _nm_utils_ascii_str_to_int64 (tmp_str, 0, G_MININT64, G_MAXINT64, G_MAXINT64);
+			if (   i64 == G_MAXINT64
+			    && errno != 0) {
+				g_set_error_literal (&err, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
+				                     _("value cannot be interpreted as integer"));
+			} else
+				nm_g_object_set_property_int64 (G_OBJECT (setting), key, i64, &err);
+		}
+	} else if (type == G_TYPE_BYTES) {
+		nm_auto_unref_bytearray GByteArray *array = NULL;
+		gs_unref_bytes GBytes *bytes= NULL;
+		gs_free guint *tmp = NULL;
+		gsize length;
+		int i;
+		gboolean already_warned = FALSE;
+
+		tmp = nm_keyfile_plugin_kf_get_integer_list_uint (keyfile, setting_info->setting_name, key, &length, NULL);
+
+		array = g_byte_array_sized_new (length);
+		for (i = 0; i < length; i++) {
+			const guint val = tmp[i];
+			unsigned char v = (unsigned char) (val & 0xFF);
+
+			if (val > 255u) {
+				if (   !already_warned
+				    && !handle_warn (info,
+				                     key,
+				                     key,
+				                     NM_KEYFILE_WARN_SEVERITY_WARN,
+				                     _("ignoring invalid byte element '%u' (not between 0 and 255 inclusive)"),
+				                     val))
+					return;
+				already_warned = TRUE;
+			} else
+				g_byte_array_append (array, (const unsigned char *) &v, sizeof (v));
+		}
+
+		bytes = g_byte_array_free_to_bytes (g_steal_pointer (&array));
+		g_object_set (setting, key, bytes, NULL);
+	} else if (type == G_TYPE_STRV) {
+		gs_strfreev char **sa = NULL;
+		gsize length;
+
+		sa = nm_keyfile_plugin_kf_get_string_list (keyfile, setting_info->setting_name, key, &length, NULL);
+		g_object_set (setting, key, sa, NULL);
+	} else if (type == G_TYPE_HASH_TABLE) {
+		read_hash_of_string (keyfile, setting, key);
+	} else if (type == G_TYPE_ARRAY) {
+		read_array_of_uint (keyfile, setting, key);
+	} else if (G_TYPE_IS_FLAGS (type)) {
+		tmp_str = nm_keyfile_plugin_kf_get_value (keyfile, setting_info->setting_name, key, &err);
+		if (!err) {
+			u64 = _nm_utils_ascii_str_to_uint64 (tmp_str, 0, 0, G_MAXUINT, G_MAXUINT64);
+			if (   u64 == G_MAXUINT64
+			    && errno != 0) {
+				g_set_error_literal (&err, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
+				                     _("value cannot be interpreted as integer"));
+			} else
+				nm_g_object_set_property_flags (G_OBJECT (setting), key, type, u64, &err);
+		}
+	} else if (G_TYPE_IS_ENUM (type)) {
+		tmp_str = nm_keyfile_plugin_kf_get_value (keyfile, setting_info->setting_name, key, &err);
+		if (!err) {
+			i64 = _nm_utils_ascii_str_to_int64 (tmp_str, 0, G_MININT, G_MAXINT, G_MAXINT64);
+			if (   i64 == G_MAXINT64
+			    && errno != 0) {
+				g_set_error_literal (&err, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
+				                     _("value cannot be interpreted as integer"));
+			} else
+				nm_g_object_set_property_enum (G_OBJECT (setting), key, type, i64, &err);
+		}
+	} else
+		g_return_if_reached ();
+
+	if (err) {
+		if (nm_keyfile_error_is_not_found (err)) {
+			/* ignore such errors. The key is not present. */
+		} else {
+			handle_warn (info,
+			             key,
+			             key,
+			             NM_KEYFILE_WARN_SEVERITY_WARN,
+			             _("invalid setting: %s"),
+			             err->message);
+		}
+	}
+}
+
+static void
+_read_setting (KeyfileReaderInfo *info)
+{
+	const NMSettInfoSetting *sett_info;
+	gs_unref_object NMSetting *setting = NULL;
+	const char *alias;
+	GType type;
+	guint i;
+
+	alias = nm_keyfile_plugin_get_setting_name_for_alias (info->group);
+	if (!alias)
+		alias = info->group;
+
+	type = nm_setting_lookup_type (alias);
+	if (!type) {
+		handle_warn (info,
+		             NULL,
+		             NULL,
+		             NM_KEYFILE_WARN_SEVERITY_WARN,
+		             _("invalid setting name '%s'"),
+		             info->group);
+		return;
+	}
+
+	setting = g_object_new (type, NULL);
+
+	info->setting = setting;
+
+	sett_info = _nm_setting_class_get_sett_info (NM_SETTING_GET_CLASS (setting));
+
+	if (sett_info->detail.gendata_info) {
+		gs_free char **keys = NULL;
+		gsize k, n_keys;
+
+		keys = g_key_file_get_keys (info->keyfile, info->group, &n_keys, NULL);
+		if (!keys)
+			n_keys = 0;
+		if (n_keys > 0) {
+			GHashTable *h = _nm_setting_option_hash (setting, TRUE);
+
+			nm_utils_strv_sort (keys, n_keys);
+			for (k = 0; k < n_keys; k++) {
+				gs_free char *key = keys[k];
+				gs_free_error GError *local = NULL;
+				const GVariantType *variant_type;
+				GVariant *variant;
+
+				/* a GKeyFile can return duplicate keys, there is just no API to make sense
+				 * of them. Skip them. */
+				if (   k + 1 < n_keys
+				    && nm_streq (key, keys[k + 1]))
+					continue;
+
+				/* currently, the API is very simple. The setting class just returns
+				 * the desired variant type, and keyfile reader will try to parse
+				 * it accordingly. Note, that this does currently not allow, that
+				 * a particular key can contain different variant types, nor is it
+				 * very flexible in general.
+				 *
+				 * We add flexibility when we need it. Keep it simple for now. */
+				variant_type = sett_info->detail.gendata_info->get_variant_type (sett_info,
+				                                                                 key,
+				                                                                 &local);
+				if (!variant_type) {
+					if (!handle_warn (info,
+					                  key,
+					                  NULL,
+					                  NM_KEYFILE_WARN_SEVERITY_WARN,
+					                  _("invalid key '%s.%s'"),
+					                  info->group,
+					                  key))
+						break;
+					continue;
+				}
+
+				if (g_variant_type_equal (variant_type, G_VARIANT_TYPE_BOOLEAN)) {
+					gboolean v;
+
+					v = g_key_file_get_boolean (info->keyfile,
+					                            info->group,
+					                            key,
+					                            &local);
+					if (local) {
+						if (!handle_warn (info,
+						                  key,
+						                  key,
+						                  NM_KEYFILE_WARN_SEVERITY_WARN,
+						                  _("key '%s.%s' is not boolean"),
+						                  info->group,
+						                  key))
+							break;
+						continue;
+					}
+					variant = g_variant_new_boolean (v);
+				} else if (g_variant_type_equal (variant_type, G_VARIANT_TYPE_UINT32)) {
+					guint64 v;
+
+					v = g_key_file_get_uint64 (info->keyfile,
+					                           info->group,
+					                           key,
+					                           &local);
+
+					if (local) {
+						if (!handle_warn (info,
+						                  key,
+						                  key,
+						                  NM_KEYFILE_WARN_SEVERITY_WARN,
+						                  _("key '%s.%s' is not a uint32"),
+						                  info->group,
+						                  key))
+							break;
+						continue;
+					}
+					variant = g_variant_new_uint32 ((guint32) v);
+				} else {
+					nm_assert_not_reached ();
+					continue;
+				}
+
+				g_hash_table_insert (h,
+				                     g_steal_pointer (&key),
+				                     g_variant_take_ref (variant));
+			}
+			for (; k < n_keys; k++)
+				g_free (keys[k]);
+		}
+	}
+
+	for (i = 0; i < sett_info->property_infos_len; i++) {
+		read_one_setting_value (info,
+		                        setting,
+		                        &sett_info->property_infos[i]);
+		if (info->error)
+			goto out;
+	}
+
+out:
+	info->setting = NULL;
+	if (!info->error)
+		nm_connection_add_setting (info->connection, g_steal_pointer (&setting));
+}
+
+static void
+_read_setting_wireguard_peer (KeyfileReaderInfo *info)
+{
+	gs_unref_object NMSettingWireGuard *s_wg_new = NULL;
+	nm_auto_unref_wgpeer NMWireGuardPeer *peer = NULL;
+	gs_free_error GError *error = NULL;
+	NMSettingWireGuard *s_wg;
+	gs_free char *str = NULL;
+	const char *cstr = NULL;
+	const char *key;
+	gint64 i64;
+	gs_strfreev char **sa = NULL;
+	gsize n_sa;
+
+	peer = nm_wireguard_peer_new ();
+
+	nm_assert (g_str_has_prefix (info->group, NM_KEYFILE_GROUPPREFIX_WIREGUARD_PEER));
+	cstr = &info->group[NM_STRLEN (NM_KEYFILE_GROUPPREFIX_WIREGUARD_PEER)];
+	if (   !nm_utils_base64secret_normalize (cstr, NM_WIREGUARD_PUBLIC_KEY_LEN, &str)
+	    || !nm_streq0 (str, cstr)) {
+		/* the group name must be identical to the normalized(!) key, so that it
+		 * is uniquely identified. */
+		handle_warn (info,
+		             NULL,
+		             NM_SETTING_WIREGUARD_PEERS,
+		             NM_KEYFILE_WARN_SEVERITY_WARN,
+		             _("invalid peer public key in section '%s'"),
+		             info->group);
+		return;
+	}
+	nm_wireguard_peer_set_public_key (peer, cstr, TRUE);
+	nm_clear_g_free (&str);
+
+	key = NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY;
+	str = nm_keyfile_plugin_kf_get_string (info->keyfile, info->group, key, NULL);
+	if (str) {
+		if (!nm_wireguard_peer_set_preshared_key (peer, str, FALSE)) {
+			if (!handle_warn (info,
+			                  key,
+			                  NM_SETTING_WIREGUARD_PEERS,
+			                  NM_KEYFILE_WARN_SEVERITY_WARN,
+			                  _("key '%s.%s' is not a valid 256 bit key in base64 encoding"),
+			                  info->group,
+			                  key))
+				return;
+		}
+		nm_clear_g_free (&str);
+	}
+
+	key = NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY_FLAGS;
+	i64 = nm_keyfile_plugin_kf_get_int64 (info->keyfile, info->group, key, 0, 0, NM_SETTING_SECRET_FLAG_ALL, -1, NULL);
+	if (errno != ENODATA) {
+		if (   i64 == -1
+		    || !_nm_setting_secret_flags_valid (i64)) {
+			if (!handle_warn (info,
+			                  key,
+			                  NM_SETTING_WIREGUARD_PEERS,
+			                  NM_KEYFILE_WARN_SEVERITY_WARN,
+			                  _("key '%s.%s' is not a valid secret flag"),
+			                  info->group,
+			                  key))
+				return;
+		} else
+			nm_wireguard_peer_set_preshared_key_flags (peer, i64);
+	}
+
+	key = NM_WIREGUARD_PEER_ATTR_PERSISTENT_KEEPALIVE;
+	i64 = nm_keyfile_plugin_kf_get_int64 (info->keyfile, info->group, key, 0, 0, G_MAXUINT32, -1, NULL);
+	if (errno != ENODATA) {
+		if (i64 == -1) {
+			if (!handle_warn (info,
+			                  key,
+			                  NM_SETTING_WIREGUARD_PEERS,
+			                  NM_KEYFILE_WARN_SEVERITY_WARN,
+			                  _("key '%s.%s' is not a integer in range 0 to 2^32"),
+			                  info->group,
+			                  key))
+				return;
+		} else
+			nm_wireguard_peer_set_persistent_keepalive (peer, i64);
+	}
+
+	key = NM_WIREGUARD_PEER_ATTR_ENDPOINT;
+	str = nm_keyfile_plugin_kf_get_string (info->keyfile, info->group, key, NULL);
+	if (str && str[0]) {
+		if (!nm_wireguard_peer_set_endpoint (peer, str, FALSE)) {
+			if (!handle_warn (info,
+			                  key,
+			                  NM_SETTING_WIREGUARD_PEERS,
+			                  NM_KEYFILE_WARN_SEVERITY_WARN,
+			                  _("key '%s.%s' is not a valid endpoint"),
+			                  info->group,
+			                  key))
+				return;
+		}
+	}
+	nm_clear_g_free (&str);
+
+	key = NM_WIREGUARD_PEER_ATTR_ALLOWED_IPS;
+	sa = nm_keyfile_plugin_kf_get_string_list (info->keyfile, info->group, key, &n_sa, NULL);
+	if (n_sa > 0) {
+		gboolean has_error = FALSE;
+		gsize i;
+
+		for (i = 0; i < n_sa; i++) {
+			if (!nm_utils_parse_inaddr_prefix_bin (AF_UNSPEC, sa[i], NULL, NULL, NULL)) {
+				has_error = TRUE;
+				continue;
+			}
+			nm_wireguard_peer_append_allowed_ip (peer, sa[i], TRUE);
+		}
+		if (has_error) {
+			if (!handle_warn (info,
+			                  key,
+			                  NM_SETTING_WIREGUARD_PEERS,
+			                  NM_KEYFILE_WARN_SEVERITY_WARN,
+			                  _("key '%s.%s' has invalid allowed-ips"),
+			                  info->group,
+			                  key))
+				return;
+		}
+	}
+
+	if (info->error)
+		return;
+
+	if (!nm_wireguard_peer_is_valid (peer, TRUE, TRUE, &error)) {
+		handle_warn (info,
+		             NULL,
+		             NM_SETTING_WIREGUARD_PEERS,
+		             NM_KEYFILE_WARN_SEVERITY_WARN,
+		             _("peer '%s' is invalid: %s"),
+		             info->group,
+		             error->message);
+		return;
+	}
+
+	s_wg = NM_SETTING_WIREGUARD (nm_connection_get_setting (info->connection, NM_TYPE_SETTING_WIREGUARD));
+	if (!s_wg) {
+		s_wg_new = NM_SETTING_WIREGUARD (nm_setting_wireguard_new ());
+		s_wg = s_wg_new;
+	}
+
+	nm_setting_wireguard_append_peer (s_wg, peer);
+
+	if (s_wg_new) {
+		nm_connection_add_setting (info->connection,
+		                           NM_SETTING (g_steal_pointer (&s_wg_new)));
+	}
+}
+
+static void
+_read_setting_vpn_secrets (KeyfileReaderInfo *info)
+{
+	gs_strfreev char **keys = NULL;
+	gsize i, n_keys;
+	NMSettingVpn *s_vpn;
+
+	s_vpn = nm_connection_get_setting_vpn (info->connection);
+	if (!s_vpn) {
+		/* if we don't also have a [vpn] section (which must be parsed earlier),
+		 * we don't do anything. */
+		nm_assert (!g_key_file_has_group (info->keyfile, "vpn"));
+		return;
+	}
+
+	keys = nm_keyfile_plugin_kf_get_keys (info->keyfile, NM_KEYFILE_GROUP_VPN_SECRETS, &n_keys, NULL);
+	for (i = 0; i < n_keys; i++) {
+		gs_free char *secret = NULL;
+
+		secret = nm_keyfile_plugin_kf_get_string (info->keyfile, NM_KEYFILE_GROUP_VPN_SECRETS, keys[i], NULL);
+		if (secret)
+			nm_setting_vpn_add_secret (s_vpn, keys[i], secret);
+	}
+}
+
+gboolean
+nm_keyfile_read_ensure_id (NMConnection *connection,
+                           const char *fallback_id)
+{
+	NMSettingConnection *s_con;
+
+	g_return_val_if_fail (NM_IS_CONNECTION (connection), FALSE);
+	g_return_val_if_fail (fallback_id, FALSE);
+
+	s_con = nm_connection_get_setting_connection (connection);
+	g_return_val_if_fail (NM_IS_SETTING_CONNECTION (s_con), FALSE);
+
+	if (nm_setting_connection_get_id (s_con))
+		return FALSE;
+
+	g_object_set (s_con, NM_SETTING_CONNECTION_ID, fallback_id, NULL);
+	return TRUE;
+}
+
+gboolean
+nm_keyfile_read_ensure_uuid (NMConnection *connection,
+                             const char *fallback_uuid_seed)
+{
+	NMSettingConnection *s_con;
+	gs_free char *hashed_uuid = NULL;
+
+	g_return_val_if_fail (NM_IS_CONNECTION (connection), FALSE);
+	g_return_val_if_fail (fallback_uuid_seed, FALSE);
+
+	s_con = nm_connection_get_setting_connection (connection);
+	g_return_val_if_fail (NM_IS_SETTING_CONNECTION (s_con), FALSE);
+
+	if (nm_setting_connection_get_uuid (s_con))
+		return FALSE;
+
+	hashed_uuid = _nm_utils_uuid_generate_from_strings ("keyfile", fallback_uuid_seed, NULL);
+	g_object_set (s_con, NM_SETTING_CONNECTION_UUID, hashed_uuid, NULL);
+	return TRUE;
+}
+
+/**
+ * nm_keyfile_read:
+ * @keyfile: the keyfile from which to create the connection
+ * @base_dir: when reading certificates from files with relative name,
+ *   the relative path is made absolute using @base_dir. This must
+ *   be an absolute path.
+ * @handler_flags: the #NMKeyfileHandlerFlags.
+ * @handler: read handler
+ * @user_data: user data for read handler
+ * @error: error
+ *
+ * Tries to create a NMConnection from a keyfile. The resulting keyfile is
+ * not normalized and might not even verify.
+ *
+ * Returns: (transfer full): on success, returns the created connection.
+ */
+NMConnection *
+nm_keyfile_read (GKeyFile *keyfile,
+                 const char *base_dir,
+                 NMKeyfileHandlerFlags handler_flags,
+                 NMKeyfileReadHandler handler,
+                 void *user_data,
+                 GError **error)
+{
+	gs_unref_object NMConnection *connection = NULL;
+	NMSettingConnection *s_con;
+	gs_strfreev char **groups = NULL;
+	gsize n_groups;
+	gsize i;
+	gboolean vpn_secrets = FALSE;
+	KeyfileReaderInfo info;
+
+	g_return_val_if_fail (keyfile, NULL);
+	g_return_val_if_fail (!error || !*error, NULL);
+	g_return_val_if_fail (base_dir && base_dir[0] == '/', NULL);
+	g_return_val_if_fail (handler_flags == NM_KEYFILE_HANDLER_FLAGS_NONE, NULL);
+
+	connection = nm_simple_connection_new ();
+
+	info = (KeyfileReaderInfo) {
+		.connection   = connection,
+		.keyfile      = keyfile,
+		.base_dir     = base_dir,
+		.read_handler = handler,
+		.user_data    = user_data,
+	};
+
+	groups = g_key_file_get_groups (keyfile, &n_groups);
+	if (!groups)
+		n_groups = 0;
+
+	for (i = 0; i < n_groups; i++) {
+
+		info.group = groups[i];
+
+		if (nm_streq (groups[i], NM_KEYFILE_GROUP_VPN_SECRETS)) {
+			/* Only read out secrets when needed */
+			vpn_secrets = TRUE;
+		} else if (NM_STR_HAS_PREFIX (groups[i], NM_KEYFILE_GROUPPREFIX_WIREGUARD_PEER))
+			_read_setting_wireguard_peer (&info);
+		else if (NM_IN_STRSET (groups[i], NM_KEYFILE_GROUP_NMMETA,
+		                                  ETHERNET_S390_OPTIONS_GROUP_NAME)) {
+			/* pass */
+		} else
+			_read_setting (&info);
+
+		info.group = NULL;
+
+		if (info.error)
+			goto out_with_info_error;
+	}
+
+	s_con = nm_connection_get_setting_connection (connection);
+	if (!s_con) {
+		s_con = NM_SETTING_CONNECTION (nm_setting_connection_new ());
+		nm_connection_add_setting (connection, NM_SETTING (s_con));
+	}
+
+	/* Make sure that we have 'interface-name' even if it was specified in the
+	 * "wrong" (ie, deprecated) group.
+	 */
+	if (   !nm_setting_connection_get_interface_name (s_con)
+	    && nm_setting_connection_get_connection_type (s_con)) {
+		gs_free char *interface_name = NULL;
+
+		interface_name = g_key_file_get_string (keyfile,
+		                                        nm_setting_connection_get_connection_type (s_con),
+		                                        "interface-name",
+		                                        NULL);
+		if (interface_name)
+			g_object_set (s_con, NM_SETTING_CONNECTION_INTERFACE_NAME, interface_name, NULL);
+	}
+
+	if (vpn_secrets) {
+		info.group = NM_KEYFILE_GROUP_VPN_SECRETS;
+		_read_setting_vpn_secrets (&info);
+		info.group = NULL;;
+		if (info.error)
+			goto out_with_info_error;
+	}
+
+	return g_steal_pointer (&connection);
+
+out_with_info_error:
+	g_propagate_error (error, info.error);
+	return NULL;
+}
+
+/*****************************************************************************/
+
+static void
+write_setting_value (KeyfileWriterInfo *info,
+                     NMSetting *setting,
+                     const NMSettInfoProperty *property_info)
+{
+	const NMMetaSettingInfo *setting_info;
+	const ParseInfoProperty *pip;
+	const char *key;
+	char numstr[64];
+	GValue value;
+	GType type;
+
+	nm_assert (!info->error);
+	nm_assert (   !property_info->param_spec
+	           || nm_streq (property_info->param_spec->name, property_info->name));
+
+	key = property_info->name;
+
+	_parse_info_find (setting, key, &setting_info, NULL, &pip);
+
+	if (!pip) {
+		if (!setting_info) {
+			/* the setting type is unknown. That is highly unexpected
+			 * (and as this is currently only called from NetworkManager
+			 * daemon, not possible).
+			 *
+			 * Still, handle it gracefully, because later keyfile writer will become
+			 * public API of libnm, where @setting is (untrusted) user input.
+			 *
+			 * Gracefully here just means: ignore the setting. */
+			return;
+		}
+		if (!property_info->param_spec)
+			return;
+		if (nm_streq (key, NM_SETTING_NAME))
+			return;
+	} else {
+		if (pip->has_writer_full) {
+			pip->writer_full (info, setting_info, property_info, pip, setting);
+			return;
+		}
+		if (pip->writer_skip)
+			return;
+	}
+
+	nm_assert (property_info->param_spec);
+
+	/* Don't write secrets that are owned by user secret agents or aren't
+	 * supposed to be saved.  VPN secrets are handled specially though since
+	 * the secret flags there are in a third-level hash in the 'secrets'
+	 * property.
+	 */
+	if (   (property_info->param_spec->flags & NM_SETTING_PARAM_SECRET)
+	    && !NM_IS_SETTING_VPN (setting)) {
+		NMSettingSecretFlags secret_flags = NM_SETTING_SECRET_FLAG_NONE;
+
+		if (!nm_setting_get_secret_flags (setting, key, &secret_flags, NULL))
+			g_return_if_reached ();
+		if (!_secret_flags_persist_secret (secret_flags))
+			return;
+	}
+
+	value = (GValue) { 0 };
+
+	g_value_init (&value, G_PARAM_SPEC_VALUE_TYPE (property_info->param_spec));
+	g_object_get_property (G_OBJECT (setting), property_info->param_spec->name, &value);
+
+	if (   (!pip || !pip->writer_persist_default)
+	    && g_param_value_defaults (property_info->param_spec, &value)) {
+		nm_assert (!g_key_file_has_key (info->keyfile, setting_info->setting_name, key, NULL));
+		goto out_unset_value;
+	}
+
+	if (   pip
+	    && pip->writer) {
+		pip->writer (info, setting, key, &value);
+		goto out_unset_value;
+	}
+
+	type = G_VALUE_TYPE (&value);
+	if (type == G_TYPE_STRING) {
+		const char *str;
+
+		str = g_value_get_string (&value);
+		if (str)
+			nm_keyfile_plugin_kf_set_string (info->keyfile, setting_info->setting_name, key, str);
+	} else if (type == G_TYPE_UINT) {
+		nm_sprintf_buf (numstr, "%u", g_value_get_uint (&value));
+		nm_keyfile_plugin_kf_set_value (info->keyfile, setting_info->setting_name, key, numstr);
+	} else if (type == G_TYPE_INT) {
+		nm_sprintf_buf (numstr, "%d", g_value_get_int (&value));
+		nm_keyfile_plugin_kf_set_value (info->keyfile, setting_info->setting_name, key, numstr);
+	} else if (type == G_TYPE_UINT64) {
+		nm_sprintf_buf (numstr, "%" G_GUINT64_FORMAT, g_value_get_uint64 (&value));
+		nm_keyfile_plugin_kf_set_value (info->keyfile, setting_info->setting_name, key, numstr);
+	} else if (type == G_TYPE_INT64) {
+		nm_sprintf_buf (numstr, "%" G_GINT64_FORMAT, g_value_get_int64 (&value));
+		nm_keyfile_plugin_kf_set_value (info->keyfile, setting_info->setting_name, key, numstr);
+	} else if (type == G_TYPE_BOOLEAN) {
+		nm_keyfile_plugin_kf_set_value (info->keyfile, setting_info->setting_name, key,
+		                                  g_value_get_boolean (&value)
+		                                ? "true"
+		                                : "false");
+	} else if (type == G_TYPE_CHAR) {
+		nm_sprintf_buf (numstr, "%d", (int) g_value_get_schar (&value));
+		nm_keyfile_plugin_kf_set_value (info->keyfile, setting_info->setting_name, key, numstr);
+	} else if (type == G_TYPE_BYTES) {
+		GBytes *bytes;
+		const guint8 *data;
+		gsize len = 0;
+
+		bytes = g_value_get_boxed (&value);
+		data = bytes ? g_bytes_get_data (bytes, &len) : NULL;
+
+		if (data != NULL && len > 0)
+			nm_keyfile_plugin_kf_set_integer_list_uint8 (info->keyfile, setting_info->setting_name, key, data, len);
+	} else if (type == G_TYPE_STRV) {
+		char **array;
+
+		array = (char **) g_value_get_boxed (&value);
+		nm_keyfile_plugin_kf_set_string_list (info->keyfile, setting_info->setting_name, key, (const char **const) array, g_strv_length (array));
+	} else if (type == G_TYPE_HASH_TABLE) {
+		write_hash_of_string (info->keyfile, setting, key, &value);
+	} else if (type == G_TYPE_ARRAY) {
+		write_array_of_uint (info->keyfile, setting, key, &value);
+	} else if (G_VALUE_HOLDS_FLAGS (&value)) {
+		nm_sprintf_buf (numstr, "%u", g_value_get_flags (&value));
+		nm_keyfile_plugin_kf_set_value (info->keyfile, setting_info->setting_name, key, numstr);
+	} else if (G_VALUE_HOLDS_ENUM (&value)) {
+		nm_sprintf_buf (numstr, "%d", g_value_get_enum (&value));
+		nm_keyfile_plugin_kf_set_value (info->keyfile, setting_info->setting_name, key, numstr);
+	} else
+		g_return_if_reached ();
+
+out_unset_value:
+	g_value_unset (&value);
+}
+
+static void
+_write_setting_wireguard (NMSetting *setting, KeyfileWriterInfo *info)
+{
+	NMSettingWireGuard *s_wg;
+	guint i_peer, n_peers;
+
+	s_wg = NM_SETTING_WIREGUARD (setting);
+
+	n_peers = nm_setting_wireguard_get_peers_len (s_wg);
+	for (i_peer = 0; i_peer < n_peers; i_peer++) {
+		NMWireGuardPeer *peer = nm_setting_wireguard_get_peer (s_wg, i_peer);
+		const char *public_key;
+		char group[NM_STRLEN (NM_KEYFILE_GROUPPREFIX_WIREGUARD_PEER) + 200];
+		NMSettingSecretFlags secret_flags;
+		gboolean any_key = FALSE;
+		guint i_aip, n_aip;
+		const char *cstr;
+		guint32 u32;
+
+		public_key = nm_wireguard_peer_get_public_key (peer);
+		if (   !public_key
+		    || !public_key[0]
+		    || !NM_STRCHAR_ALL (public_key, ch, nm_sd_utils_unbase64char (ch, TRUE) >= 0)) {
+			/* invalid peer. Skip it */
+			continue;
+		}
+
+		if (g_snprintf (group,
+		                sizeof (group),
+		                "%s%s",
+		                NM_KEYFILE_GROUPPREFIX_WIREGUARD_PEER,
+		                nm_wireguard_peer_get_public_key (peer)) >= sizeof (group)) {
+			/* Too long. Not a valid public key. Skip the peer. */
+			continue;
+		}
+
+		cstr = nm_wireguard_peer_get_endpoint (peer);
+		if (cstr) {
+			g_key_file_set_string (info->keyfile, group, NM_WIREGUARD_PEER_ATTR_ENDPOINT, cstr);
+			any_key = TRUE;
+		}
+
+		secret_flags = nm_wireguard_peer_get_preshared_key_flags (peer);
+		if (_secret_flags_persist_secret (secret_flags)) {
+			cstr = nm_wireguard_peer_get_preshared_key (peer);
+			if (cstr) {
+				g_key_file_set_string (info->keyfile, group, NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY, cstr);
+				any_key = TRUE;
+			}
+		}
+
+		/* usually, we don't persist the secret-flags 0 (because they are the default).
+		 * For WireGuard peers, the default secret-flags for preshared-key are 4 (not-required).
+		 * So, in this case behave differently: a missing preshared-key-flag setting means
+		 * "not-required". */
+		if (secret_flags != NM_SETTING_SECRET_FLAG_NOT_REQUIRED) {
+			g_key_file_set_int64 (info->keyfile, group, NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY_FLAGS, secret_flags);
+			any_key = TRUE;
+		}
+
+		u32 = nm_wireguard_peer_get_persistent_keepalive (peer);
+		if (u32) {
+			g_key_file_set_uint64 (info->keyfile, group, NM_WIREGUARD_PEER_ATTR_PERSISTENT_KEEPALIVE, u32);
+			any_key = TRUE;
+		}
+
+		n_aip = nm_wireguard_peer_get_allowed_ips_len (peer);
+		if (n_aip > 0) {
+			gs_free const char **strv = NULL;
+
+			strv = g_new (const char *, ((gsize) n_aip) + 1);
+			for (i_aip = 0; i_aip < n_aip; i_aip++)
+				strv[i_aip] = nm_wireguard_peer_get_allowed_ip (peer, i_aip, NULL);
+			strv[n_aip] = NULL;
+			g_key_file_set_string_list (info->keyfile, group, NM_WIREGUARD_PEER_ATTR_ALLOWED_IPS,
+			                            strv, n_aip);
+			any_key = TRUE;
+		}
+
+		if (!any_key) {
+			/* we cannot omit all keys. At an empty endpoint. */
+			g_key_file_set_string (info->keyfile, group, NM_WIREGUARD_PEER_ATTR_ENDPOINT, "");
+		}
+	}
+}
+
+GKeyFile *
+nm_keyfile_write (NMConnection *connection,
+                  NMKeyfileHandlerFlags handler_flags,
+                  NMKeyfileWriteHandler handler,
+                  void *user_data,
+                  GError **error)
+{
+	gs_unref_keyfile GKeyFile *keyfile = NULL;
+	KeyfileWriterInfo info;
+	gs_free NMSetting **settings = NULL;
+	guint i, j, n_settings = 0;
+
+	g_return_val_if_fail (NM_IS_CONNECTION (connection), NULL);
+	g_return_val_if_fail (!error || !*error, NULL);
+	g_return_val_if_fail (handler_flags == NM_KEYFILE_HANDLER_FLAGS_NONE, NULL);
+
+	if (!nm_connection_verify (connection, error))
+		return NULL;
+
+	keyfile = g_key_file_new ();
+
+	info = (KeyfileWriterInfo) {
+		.connection    = connection,
+		.keyfile       = keyfile,
+		.error         = NULL,
+		.write_handler = handler,
+		.user_data     = user_data,
+	};
+
+	settings = nm_connection_get_settings (connection, &n_settings);
+	for (i = 0; i < n_settings; i++) {
+		const NMSettInfoSetting *sett_info;
+		NMSetting *setting = settings[i];
+		const char *setting_name;
+		const char *setting_alias;
+
+		sett_info = _nm_setting_class_get_sett_info (NM_SETTING_GET_CLASS (setting));
+
+		setting_name = sett_info->setting_class->setting_info->setting_name;
+
+		if (sett_info->detail.gendata_info) {
+			guint k, n_keys;
+			const char *const*keys;
+
+			nm_assert (!nm_keyfile_plugin_get_alias_for_setting_name (sett_info->setting_class->setting_info->setting_name));
+
+			n_keys = _nm_setting_option_get_all (setting, &keys, NULL);
+
+			if (n_keys > 0) {
+				GHashTable *h = _nm_setting_option_hash (setting, FALSE);
+
+				for (k = 0; k < n_keys; k++) {
+					const char *key = keys[k];
+					GVariant *v;
+
+					v = g_hash_table_lookup (h, key);
+
+					if (g_variant_is_of_type (v, G_VARIANT_TYPE_BOOLEAN)) {
+						g_key_file_set_boolean (info.keyfile,
+						                        setting_name,
+						                        key,
+						                        g_variant_get_boolean (v));
+					} else if (g_variant_is_of_type (v, G_VARIANT_TYPE_UINT32)) {
+						g_key_file_set_uint64 (info.keyfile,
+						                       setting_name,
+						                       key,
+						                       (guint64) g_variant_get_uint32 (v));
+					} else {
+						/* BUG: The variant type is not implemented. Since the connection
+						 * verifies, this can only mean we either wrongly didn't reject
+						 * the connection as invalid, or we didn't properly implement the
+						 * variant type. */
+						nm_assert_not_reached ();
+						continue;
+					}
+				}
+			}
+		}
+
+		for (j = 0; j < sett_info->property_infos_len; j++) {
+			const NMSettInfoProperty *property_info = _nm_sett_info_property_info_get_sorted (sett_info, j);
+
+			write_setting_value (&info, setting, property_info);
+			if (info.error)
+				goto out_with_info_error;
+		}
+
+		setting_alias = nm_keyfile_plugin_get_alias_for_setting_name (setting_name);
+		if (   (   setting_alias
+		        && g_key_file_has_group (info.keyfile, setting_alias))
+		    || g_key_file_has_group (info.keyfile, setting_name)) {
+			/* we have a section for the setting. Nothing to do. */
+		} else {
+			/* ensure the group is present. There is no API for that, so add and remove
+			 * a dummy key. */
+			g_key_file_set_value  (info.keyfile, setting_alias ?: setting_name, ".X", "1");
+			g_key_file_remove_key (info.keyfile, setting_alias ?: setting_name, ".X", NULL);
+		}
+
+		if (NM_IS_SETTING_WIREGUARD (setting)) {
+			_write_setting_wireguard (setting, &info);
+			if (info.error)
+				goto out_with_info_error;
+		}
+
+		nm_assert (!info.error);
+	}
+
+	nm_assert (!info.error);
+
+	return g_steal_pointer (&keyfile);
+
+out_with_info_error:
+	g_propagate_error (error, info.error);
+	return NULL;
+}
+
+/*****************************************************************************/
+
+static const char temp_letters[] =
+"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+
+/*
+ * Check '.[a-zA-Z0-9]{6}' file suffix used for temporary files by g_file_set_contents() (mkstemp()).
+ */
+static gboolean
+check_mkstemp_suffix (const char *path)
+{
+	const char *ptr;
+
+	nm_assert (path);
+
+	/* Matches *.[a-zA-Z0-9]{6} suffix of mkstemp()'s temporary files */
+	ptr = strrchr (path, '.');
+	if (   ptr
+	    && strspn (&ptr[1], temp_letters) == 6
+	    && ptr[7] == '\0')
+		return TRUE;
+	return FALSE;
+}
+
+static gboolean
+_check_suffix_impl (const char *base, const char *tag, gsize tag_len)
+{
+	gsize len;
+
+	nm_assert (base);
+	nm_assert (tag);
+	nm_assert (strlen (tag) == tag_len);
+
+	len = strlen (base);
+	if (   len > tag_len
+	    && !g_ascii_strcasecmp (base + len - tag_len, tag))
+		return TRUE;
+	return FALSE;
+}
+#define check_suffix(base, tag) _check_suffix_impl ((base), ""tag"", NM_STRLEN (tag))
+
+#define SWP_TAG ".swp"
+#define SWPX_TAG ".swpx"
+#define PEM_TAG ".pem"
+#define DER_TAG ".der"
+
+gboolean
+nm_keyfile_utils_ignore_filename (const char *filename, gboolean require_extension)
+{
+	const char *base;
+	gsize l;
+
+	/* ignore_filename() must mirror nm_keyfile_utils_create_filename() */
+
+	g_return_val_if_fail (filename, TRUE);
+
+	base = strrchr (filename, '/');
+	if (base)
+		base++;
+	else
+		base = filename;
+
+	if (!base[0]) {
+		/* this check above with strrchr() also rejects "/some/path/with/trailing/slash/",
+		 * but that is fine, because such a path would name a directory, and we are not
+		 * interested in directories. */
+		return TRUE;
+	}
+
+	if (base[0] == '.') {
+		/* don't allow hidden files */
+		return TRUE;
+	}
+
+	l = strlen (base);
+
+	if (require_extension) {
+		if (   l <= NM_STRLEN (NM_KEYFILE_PATH_SUFFIX_NMCONNECTION)
+		    || !NM_STR_HAS_SUFFIX (base, NM_KEYFILE_PATH_SUFFIX_NMCONNECTION))
+			return TRUE;
+		return FALSE;
+	}
+
+	/* Ignore backup files */
+	if (base[l - 1] == '~')
+		return TRUE;
+
+	/* Ignore temporary files
+	 *
+	 * This check is also important to ignore .nmload files (see
+	 * %NM_KEYFILE_PATH_SUFFIX_NMMETA). */
+	if (check_mkstemp_suffix (base))
+		return TRUE;
+
+	/* Ignore 802.1x certificates and keys */
+	if (   check_suffix (base, PEM_TAG)
+	    || check_suffix (base, DER_TAG))
+		return TRUE;
+
+	return FALSE;
+}
+
+char *
+nm_keyfile_utils_create_filename (const char *name,
+                                  gboolean with_extension)
+{
+	GString *str;
+	const char *f = name;
+	/* keyfile used to escape with '*', do not change that behavior.
+	 *
+	 * But for newly added escapings, use '_' instead.
+	 * Also, @with_extension is new-style. */
+	const char ESCAPE_CHAR = with_extension ? '_' : '*';
+	const char ESCAPE_CHAR2 = '_';
+
+	g_return_val_if_fail (name && name[0], NULL);
+
+	str = g_string_sized_new (60);
+
+	/* Convert '/' to ESCAPE_CHAR */
+	for (f = name; f[0]; f++) {
+		if (f[0] == '/')
+			g_string_append_c (str, ESCAPE_CHAR);
+		else
+			g_string_append_c (str, f[0]);
+	}
+
+	/* nm_keyfile_utils_create_filename() must avoid anything that ignore_filename() would reject.
+	 * We can escape here more aggressivly then what we would read back. */
+	if (str->str[0] == '.')
+		str->str[0] = ESCAPE_CHAR2;
+	if (str->str[str->len - 1] == '~')
+		str->str[str->len - 1] = ESCAPE_CHAR2;
+	if (   check_mkstemp_suffix (str->str)
+	    || check_suffix (str->str, PEM_TAG)
+	    || check_suffix (str->str, DER_TAG))
+		g_string_append_c (str, ESCAPE_CHAR2);
+
+	if (with_extension)
+		g_string_append (str, NM_KEYFILE_PATH_SUFFIX_NMCONNECTION);
+
+	/* nm_keyfile_utils_create_filename() must mirror ignore_filename() */
+	nm_assert (!strchr (str->str, '/'));
+	nm_assert (!nm_keyfile_utils_ignore_filename (str->str, with_extension));
+
+	return g_string_free (str, FALSE);;
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_keyfile_handler_data_fail_with_error:
+ * @handler_data: the #NMKeyfileHandlerData
+ * @src: (transfer full): error to move into the return location
+ *
+ * Set the error for the handler. This lets the operation fail
+ * with the provided error. You may only set the error once.
+ *
+ * @src must be non-%NULL.
+ *
+ * Note that @src is no longer valid after this call. If you want
+ * to keep using the same GError*, you need to set it to %NULL
+ * after calling this function on it.
+ */
+void
+nm_keyfile_handler_data_fail_with_error (NMKeyfileHandlerData *handler_data,
+                                         GError *src)
+{
+	g_return_if_fail (handler_data);
+	g_return_if_fail (handler_data->p_error && !*handler_data->p_error);
+	g_return_if_fail (src);
+
+	*handler_data->p_error = src;
+}
+
+/**
+ * nm_keyfile_handler_data_get_context:
+ * @handler_data: the #NMKeyfileHandlerData for any event.
+ * @out_kf_group_name: (out) (allow-none) (transfer none): if the event is in the
+ *   context of a keyfile group, the group name.
+ * @out_kf_key_name: (out) (allow-none) (transfer none): if the event is in the
+ *   context of a keyfile value, the key name.
+ * @out_cur_setting: (out) (allow-none) (transfer none): if the event happens while
+ *   handling a particular #NMSetting instance.
+ * @out_cur_property_name: (out) (allow-none) (transfer none): the property name if applicable.
+ *
+ * Get context information of the current event. This function can be called
+ * on all events, but the context information may be unset.
+ */
+void
+nm_keyfile_handler_data_get_context (const NMKeyfileHandlerData *handler_data,
+                                     const char **out_kf_group_name,
+                                     const char **out_kf_key_name,
+                                     NMSetting **out_cur_setting,
+                                     const char **out_cur_property_name)
+{
+	g_return_if_fail (handler_data);
+
+	NM_SET_OUT (out_kf_group_name, handler_data->kf_group_name);
+	NM_SET_OUT (out_kf_key_name, handler_data->kf_key);
+	NM_SET_OUT (out_cur_setting, handler_data->cur_setting);
+	NM_SET_OUT (out_cur_property_name, handler_data->cur_property);
+}
+
+const char *
+_nm_keyfile_handler_data_warn_get_message (const NMKeyfileHandlerData *handler_data)
+{
+	nm_assert (handler_data);
+	nm_assert (handler_data->type == NM_KEYFILE_HANDLER_TYPE_WARN);
+
+	if (!handler_data->warn.message) {
+		/* we cast the const away. @handler_data is const w.r.t. visible mutations
+		 * from POV of the user. Internally, we construct the message in
+		 * a lazy manner. It's like a mutable field in C++. */
+		NM_PRAGMA_WARNING_DISABLE ("-Wformat-nonliteral")
+		((NMKeyfileHandlerData *) handler_data)->warn.message = g_strdup_vprintf (handler_data->warn.fmt,
+		                                                                          ((NMKeyfileHandlerData *) handler_data)->warn.ap);
+		NM_PRAGMA_WARNING_REENABLE
+	}
+	return handler_data->warn.message;
+}
+
+/**
+ * nm_keyfile_handler_data_warn_get:
+ * @handler_data: the #NMKeyfileHandlerData for a %NM_KEYFILE_HANDLER_TYPE_WARN
+ *  event.
+ * @out_message: (out) (allow-none) (transfer none): the warning message.
+ * @out_severity: (out) (allow-none): the #NMKeyfileWarnSeverity warning severity.
+ */
+void
+nm_keyfile_handler_data_warn_get (const NMKeyfileHandlerData *handler_data,
+                                  const char **out_message,
+                                  NMKeyfileWarnSeverity *out_severity)
+{
+	g_return_if_fail (handler_data);
+	g_return_if_fail (handler_data->type == NM_KEYFILE_HANDLER_TYPE_WARN);
+
+	NM_SET_OUT (out_message, _nm_keyfile_handler_data_warn_get_message (handler_data));
+	NM_SET_OUT (out_severity, handler_data->warn.severity);
+}
diff --git a/libnm-core/nm-libnm-core-aux/README.md b/libnm-core/nm-libnm-core-aux/README.md
new file mode 100644
index 00000000..01f361b4
--- /dev/null
+++ b/libnm-core/nm-libnm-core-aux/README.md
@@ -0,0 +1,21 @@
+nm-libnm-core-aux is a static library that:
+
+ - uses parts of "libnm-core", that are public API of "libnm"
+ - can be statically linked into users of libnm-core (like libnm
+   and NetworkManager).
+ - that can also be statically linked into other users of libnm.
+
+Basically, it is a static library with utility functions that extends
+libnm-core (the part that is public API of libnm), but can also be
+used without full libnm.
+
+That means:
+
+  - you can use it everywhere where you either statically link
+    with libnm-core, or dynamically link with libnm.
+  - you cannot use it inside libnm-core itself. This is the difference
+    between nm-libnm-core-intern and nm-libnm-core-aux.
+
+Also, since nm-libnm-core-aux itself only uses public (stable)
+API of libnm, you theoretically can copy the sources into your
+own source tree.
diff --git a/libnm-core/nm-libnm-core-aux/nm-dispatcher-api.h b/libnm-core/nm-libnm-core-aux/nm-dispatcher-api.h
new file mode 100644
index 00000000..96f512e0
--- /dev/null
+++ b/libnm-core/nm-libnm-core-aux/nm-dispatcher-api.h
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2008 - 2012 Red Hat, Inc.
+ */
+
+#ifndef __NM_DISPACHER_API_H__
+#define __NM_DISPACHER_API_H__
+
+#define NM_DISPATCHER_DBUS_SERVICE   "org.freedesktop.nm_dispatcher"
+#define NM_DISPATCHER_DBUS_INTERFACE "org.freedesktop.nm_dispatcher"
+#define NM_DISPATCHER_DBUS_PATH      "/org/freedesktop/nm_dispatcher"
+
+#define NMD_CONNECTION_PROPS_PATH         "path"
+#define NMD_CONNECTION_PROPS_FILENAME     "filename"
+#define NMD_CONNECTION_PROPS_EXTERNAL     "external"
+
+#define NMD_DEVICE_PROPS_INTERFACE        "interface"
+#define NMD_DEVICE_PROPS_IP_INTERFACE     "ip-interface"
+#define NMD_DEVICE_PROPS_TYPE             "type"
+#define NMD_DEVICE_PROPS_STATE            "state"
+#define NMD_DEVICE_PROPS_PATH             "path"
+
+/* Actions */
+#define NMD_ACTION_HOSTNAME     "hostname"
+#define NMD_ACTION_PRE_UP       "pre-up"
+#define NMD_ACTION_UP           "up"
+#define NMD_ACTION_PRE_DOWN     "pre-down"
+#define NMD_ACTION_DOWN         "down"
+#define NMD_ACTION_VPN_PRE_UP   "vpn-pre-up"
+#define NMD_ACTION_VPN_UP       "vpn-up"
+#define NMD_ACTION_VPN_PRE_DOWN "vpn-pre-down"
+#define NMD_ACTION_VPN_DOWN     "vpn-down"
+#define NMD_ACTION_DHCP4_CHANGE "dhcp4-change"
+#define NMD_ACTION_DHCP6_CHANGE "dhcp6-change"
+#define NMD_ACTION_CONNECTIVITY_CHANGE "connectivity-change"
+
+typedef enum {
+	DISPATCH_RESULT_UNKNOWN = 0,
+	DISPATCH_RESULT_SUCCESS = 1,
+	DISPATCH_RESULT_EXEC_FAILED = 2,
+	DISPATCH_RESULT_FAILED = 3,
+	DISPATCH_RESULT_TIMEOUT = 4,
+} DispatchResult;
+
+#endif /* __NM_DISPACHER_API_H__ */
diff --git a/libnm-core/nm-libnm-core-aux/nm-libnm-core-aux.c b/libnm-core/nm-libnm-core-aux/nm-libnm-core-aux.c
new file mode 100644
index 00000000..f827726f
--- /dev/null
+++ b/libnm-core/nm-libnm-core-aux/nm-libnm-core-aux.c
@@ -0,0 +1,358 @@
+// SPDX-License-Identifier: LGPL-2.1+
+/*
+ * Copyright (C) 2019 Red Hat, Inc.
+ */
+
+#include "nm-default.h"
+
+#include "nm-libnm-core-aux.h"
+
+#include "nm-libnm-core-intern/nm-libnm-core-utils.h"
+
+/*****************************************************************************/
+
+typedef enum {
+	KEY_TYPE_STRING,
+	KEY_TYPE_INT,
+	KEY_TYPE_BOOL,
+} KeyType;
+
+typedef struct {
+	const char *str_val;
+	union {
+		int vint;
+		bool vbool;
+	} typ_val;
+} ParseData;
+
+typedef struct {
+	const char *name;
+	NMTeamLinkWatcherType watcher_type;
+	KeyType key_type;
+	union {
+		int         (*fint)    (const NMTeamLinkWatcher *watcher);
+		gboolean    (*fbool)   (const NMTeamLinkWatcher *watcher);
+		const char *(*fstring) (const NMTeamLinkWatcher *watcher);
+	} get_fcn;
+	union {
+		int vint;
+		bool vbool;
+	} def_val;
+} TeamLinkWatcherKeyInfo;
+
+static gboolean
+_team_link_watcher_validate_active (const NMTeamLinkWatcher *watcher)
+{
+	return NM_FLAGS_HAS (nm_team_link_watcher_get_flags (watcher), NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_VALIDATE_ACTIVE);
+}
+
+static gboolean
+_team_link_watcher_validate_inactive (const NMTeamLinkWatcher *watcher)
+{
+	return NM_FLAGS_HAS (nm_team_link_watcher_get_flags (watcher), NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_VALIDATE_INACTIVE);
+}
+
+static gboolean
+_team_link_watcher_send_always (const NMTeamLinkWatcher *watcher)
+{
+	return NM_FLAGS_HAS (nm_team_link_watcher_get_flags (watcher), NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_SEND_ALWAYS);
+}
+
+static const TeamLinkWatcherKeyInfo _team_link_watcher_key_infos[_NM_TEAM_LINK_WATCHER_KEY_NUM] = {
+
+#define _KEY_INFO(key_id, _name, _watcher_type, _key_type, ...) \
+	[key_id] = { .name = ""_name"", .watcher_type = (_watcher_type), .key_type = _key_type, ##__VA_ARGS__ }
+
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_NAME,              "name",              NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL | NM_TEAM_LINK_WATCHER_TYPE_NSNAPING | NM_TEAM_LINK_WATCHER_TYPE_ARPING, KEY_TYPE_STRING, .get_fcn.fstring = nm_team_link_watcher_get_name,        ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_DELAY_UP,          "delay-up",          NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL,                                                                         KEY_TYPE_INT,    .get_fcn.fint    = nm_team_link_watcher_get_delay_up,    ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_DELAY_DOWN,        "delay-down",        NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL,                                                                         KEY_TYPE_INT,    .get_fcn.fint    = nm_team_link_watcher_get_delay_down,  ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_INIT_WAIT,         "init-wait",         NM_TEAM_LINK_WATCHER_TYPE_NSNAPING | NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                     KEY_TYPE_INT,    .get_fcn.fint    = nm_team_link_watcher_get_init_wait,   ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_INTERVAL,          "interval",          NM_TEAM_LINK_WATCHER_TYPE_NSNAPING | NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                     KEY_TYPE_INT,    .get_fcn.fint    = nm_team_link_watcher_get_interval,    ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_MISSED_MAX,        "missed-max",        NM_TEAM_LINK_WATCHER_TYPE_NSNAPING | NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                     KEY_TYPE_INT,    .get_fcn.fint    = nm_team_link_watcher_get_missed_max,  .def_val.vint = 3, ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_TARGET_HOST,       "target-host",       NM_TEAM_LINK_WATCHER_TYPE_NSNAPING | NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                     KEY_TYPE_STRING, .get_fcn.fstring = nm_team_link_watcher_get_target_host, ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_VLANID,            "vlanid",            NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                                                          KEY_TYPE_INT,    .get_fcn.fint    = nm_team_link_watcher_get_vlanid,      .def_val.vint = -1, ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_SOURCE_HOST,       "source-host",       NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                                                          KEY_TYPE_STRING, .get_fcn.fstring = nm_team_link_watcher_get_source_host, ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_VALIDATE_ACTIVE,   "validate-active",   NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                                                          KEY_TYPE_BOOL,   .get_fcn.fbool   = _team_link_watcher_validate_active,   ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_VALIDATE_INACTIVE, "validate-inactive", NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                                                          KEY_TYPE_BOOL,   .get_fcn.fbool   = _team_link_watcher_validate_inactive, ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_SEND_ALWAYS,       "send-always",       NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                                                          KEY_TYPE_BOOL,   .get_fcn.fbool   = _team_link_watcher_send_always,       ),
+
+};
+
+static NMTeamLinkWatcherType
+_team_link_watcher_get_watcher_type_from_name (const char *name)
+{
+	if (name) {
+		if (nm_streq (name, NM_TEAM_LINK_WATCHER_ETHTOOL))
+			return NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL;
+		if (nm_streq (name, NM_TEAM_LINK_WATCHER_NSNA_PING))
+			return NM_TEAM_LINK_WATCHER_TYPE_NSNAPING;
+		if (nm_streq (name, NM_TEAM_LINK_WATCHER_ARP_PING))
+			return NM_TEAM_LINK_WATCHER_TYPE_ARPING;
+	}
+	return NM_TEAM_LINK_WATCHER_TYPE_NONE;
+}
+
+static const char *
+_parse_data_get_str (const ParseData parse_data[static _NM_TEAM_LINK_WATCHER_KEY_NUM],
+                     NMTeamLinkWatcherKeyId key_id)
+{
+	nm_assert (_NM_INT_NOT_NEGATIVE (key_id) && key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM);
+	nm_assert (_team_link_watcher_key_infos[key_id].key_type == KEY_TYPE_STRING);
+
+	return parse_data[key_id].str_val;
+}
+
+static int
+_parse_data_get_int (const ParseData parse_data[static _NM_TEAM_LINK_WATCHER_KEY_NUM],
+                     NMTeamLinkWatcherKeyId key_id)
+{
+	nm_assert (_NM_INT_NOT_NEGATIVE (key_id) && key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM);
+	nm_assert (_team_link_watcher_key_infos[key_id].key_type == KEY_TYPE_INT);
+
+	if (parse_data[key_id].str_val)
+		return parse_data[key_id].typ_val.vint;
+	return _team_link_watcher_key_infos[key_id].def_val.vint;
+}
+
+static int
+_parse_data_get_bool (const ParseData parse_data[static _NM_TEAM_LINK_WATCHER_KEY_NUM],
+                      NMTeamLinkWatcherKeyId key_id)
+{
+	nm_assert (_NM_INT_NOT_NEGATIVE (key_id) && key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM);
+	nm_assert (_team_link_watcher_key_infos[key_id].key_type == KEY_TYPE_BOOL);
+
+	if (parse_data[key_id].str_val)
+		return parse_data[key_id].typ_val.vbool;
+	return _team_link_watcher_key_infos[key_id].def_val.vbool;
+}
+
+char *
+nm_utils_team_link_watcher_to_string (const NMTeamLinkWatcher *watcher)
+{
+	nm_auto_free_gstring GString *str = NULL;
+	const char *name;
+	NMTeamLinkWatcherType watcher_type;
+	NMTeamLinkWatcherKeyId key_id;
+
+	if (!watcher)
+		return NULL;
+
+	str = g_string_new (NULL);
+
+	name = nm_team_link_watcher_get_name (watcher);
+	g_string_append_printf (str, "name=%s", name ?: "");
+
+	watcher_type = _team_link_watcher_get_watcher_type_from_name (name);
+
+	for (key_id = 0; key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM; key_id++) {
+		const TeamLinkWatcherKeyInfo *info = &_team_link_watcher_key_infos[key_id];
+		const char *vstr;
+		int vint;
+		bool vbool;
+
+		nm_assert (info->name && info->name && NM_STRCHAR_ALL (info->name, ch,((ch >= 'a' && ch <= 'z') || NM_IN_SET (ch, '-'))));
+		nm_assert (NM_IN_SET (info->key_type, KEY_TYPE_STRING,
+		                                      KEY_TYPE_INT,
+		                                      KEY_TYPE_BOOL));
+
+		if (key_id == NM_TEAM_LINK_WATCHER_KEY_NAME)
+			continue;
+
+		if (!NM_FLAGS_ALL (info->watcher_type, watcher_type))
+			continue;
+
+		switch (info->key_type) {
+		case KEY_TYPE_STRING:
+			vstr = info->get_fcn.fstring (watcher);
+			if (vstr) {
+				g_string_append_printf (nm_gstring_add_space_delimiter (str),
+				                        "%s=%s", info->name, vstr);
+			}
+			break;
+		case KEY_TYPE_INT:
+			vint = info->get_fcn.fint (watcher);
+			if (vint != info->def_val.vint) {
+				g_string_append_printf (nm_gstring_add_space_delimiter (str),
+				                        "%s=%d", info->name, vint);
+			}
+			break;
+		case KEY_TYPE_BOOL:
+			vbool = info->get_fcn.fbool (watcher);
+			if (vbool != info->def_val.vbool) {
+				g_string_append_printf (nm_gstring_add_space_delimiter (str),
+				                        "%s=%s", info->name, vbool ? "true" : "false");
+			}
+			break;
+		}
+	}
+
+	return g_string_free (g_steal_pointer (&str), FALSE);
+}
+
+NMTeamLinkWatcher *
+nm_utils_team_link_watcher_from_string (const char *str,
+                                        GError **error)
+{
+	gs_free const char **tokens = NULL;
+	ParseData parse_data[_NM_TEAM_LINK_WATCHER_KEY_NUM] = { };
+	NMTeamLinkWatcherType watcher_type;
+	NMTeamLinkWatcherKeyId key_id;
+	gsize i_token;
+	NMTeamLinkWatcher *watcher;
+	int errsv;
+
+	g_return_val_if_fail (str, NULL);
+	g_return_val_if_fail (!error || !*error, NULL);
+
+	tokens = nm_utils_escaped_tokens_split (str, NM_ASCII_SPACES);
+	if (!tokens) {
+		g_set_error (error, 1, 0, "'%s' is not valid", str);
+		return NULL;
+	}
+
+	for (i_token = 0; tokens[i_token]; i_token++) {
+		const TeamLinkWatcherKeyInfo *info;
+		const char *key = tokens[i_token];
+		const char *val;
+
+		val = strchr (key, '=');
+		if (!val) {
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("'%s' is not valid: properties should be specified as 'key=value'"),
+			                    key);
+			return NULL;
+		}
+		((char *) val)[0] = '\0';
+		val++;
+
+		for (key_id = 0; key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM; key_id++) {
+			info = &_team_link_watcher_key_infos[key_id];
+			if (nm_streq (key, info->name))
+				break;
+		}
+
+		if (key_id == _NM_TEAM_LINK_WATCHER_KEY_NUM) {
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("'%s' is not a valid key"), key);
+			return NULL;
+		}
+
+		if (parse_data[key_id].str_val) {
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("duplicate key '%s'"), key);
+			return NULL;
+		}
+
+		parse_data[key_id].str_val = val;
+
+		if (info->key_type == KEY_TYPE_INT) {
+			gint64 v64;
+
+			v64 = _nm_utils_ascii_str_to_int64 (val, 10, G_MININT, G_MAXINT, G_MAXINT64);
+			if (   v64 == G_MAXINT64
+			    && ((errsv = errno) != 0)) {
+				if (errsv == ERANGE) {
+					nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+					                    _("number for '%s' is out of range"), key);
+				} else {
+					nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+					                    _("value for '%s' must be a number"), key);
+				}
+				return NULL;
+			}
+			parse_data[key_id].typ_val.vint = v64;
+		} else if (info->key_type == KEY_TYPE_BOOL) {
+			int vbool;
+
+			vbool = _nm_utils_ascii_str_to_bool (val, -1);
+			if (vbool == -1) {
+				nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+				                    _("value for '%s' must be a boolean"), key);
+				return NULL;
+			}
+			parse_data[key_id].typ_val.vbool = vbool;
+		}
+	}
+
+	if (!parse_data[NM_TEAM_LINK_WATCHER_KEY_NAME].str_val) {
+		nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+		                    _("missing 'name' attribute"));
+		return NULL;
+	}
+
+	watcher_type = _team_link_watcher_get_watcher_type_from_name (parse_data[NM_TEAM_LINK_WATCHER_KEY_NAME].str_val);
+	if (watcher_type == NM_TEAM_LINK_WATCHER_TYPE_NONE) {
+		nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+		                    _("invalid 'name' \"%s\""),
+		                    parse_data[NM_TEAM_LINK_WATCHER_KEY_NAME].str_val);
+		return NULL;
+	}
+
+	for (key_id = 0; key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM; key_id++) {
+		const TeamLinkWatcherKeyInfo *info = &_team_link_watcher_key_infos[key_id];
+
+		if (!parse_data[key_id].str_val)
+			continue;
+		if (!NM_FLAGS_ALL (info->watcher_type, watcher_type)) {
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("attribute '%s' is invalid for \"%s\""),
+			                    info->name,
+			                    parse_data[NM_TEAM_LINK_WATCHER_KEY_NAME].str_val);
+			return NULL;
+		}
+	}
+
+	switch (watcher_type) {
+	case NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL:
+		watcher = nm_team_link_watcher_new_ethtool (_parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_DELAY_UP),
+		                                            _parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_DELAY_DOWN),
+		                                            error);
+		break;
+	case NM_TEAM_LINK_WATCHER_TYPE_NSNAPING:
+		watcher = nm_team_link_watcher_new_nsna_ping (_parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_INIT_WAIT),
+		                                              _parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_INTERVAL),
+		                                              _parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_MISSED_MAX),
+		                                              _parse_data_get_str (parse_data, NM_TEAM_LINK_WATCHER_KEY_TARGET_HOST),
+		                                              error);
+		break;
+	default:
+		nm_assert (watcher_type == NM_TEAM_LINK_WATCHER_TYPE_ARPING);
+		watcher = nm_team_link_watcher_new_arp_ping2 (_parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_INIT_WAIT),
+		                                              _parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_INTERVAL),
+		                                              _parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_MISSED_MAX),
+		                                              _parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_VLANID),
+		                                              _parse_data_get_str (parse_data, NM_TEAM_LINK_WATCHER_KEY_TARGET_HOST),
+		                                              _parse_data_get_str (parse_data, NM_TEAM_LINK_WATCHER_KEY_SOURCE_HOST),
+		                                              (  NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_NONE
+		                                               | (_parse_data_get_bool (parse_data, NM_TEAM_LINK_WATCHER_KEY_VALIDATE_ACTIVE)   ? NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_VALIDATE_ACTIVE   : NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_NONE)
+		                                               | (_parse_data_get_bool (parse_data, NM_TEAM_LINK_WATCHER_KEY_VALIDATE_INACTIVE) ? NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_VALIDATE_INACTIVE : NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_NONE)
+		                                               | (_parse_data_get_bool (parse_data, NM_TEAM_LINK_WATCHER_KEY_SEND_ALWAYS)       ? NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_SEND_ALWAYS       : NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_NONE)
+		                                              ),
+		                                              error);
+		break;
+	}
+
+#if NM_MORE_ASSERTS > 5
+	if (watcher) {
+		gs_free char *str2 = NULL;
+		nm_auto_unref_team_link_watcher NMTeamLinkWatcher *watcher2 = NULL;
+		static _nm_thread_local int recursive;
+
+		nm_assert (!error || !*error);
+		if (recursive == 0) {
+			recursive = 1;
+			str2 = nm_utils_team_link_watcher_to_string (watcher);
+			nm_assert (str2);
+			watcher2 = nm_utils_team_link_watcher_from_string (str2, NULL);
+			nm_assert (watcher2);
+			nm_assert (nm_team_link_watcher_equal (watcher, watcher2));
+			nm_assert (nm_team_link_watcher_equal (watcher2, watcher));
+			nm_assert (recursive == 1);
+			recursive = 0;
+		}
+	} else
+		nm_assert (!error || *error);
+#endif
+
+	return watcher;
+}
diff --git a/libnm-core/nm-libnm-core-aux/nm-libnm-core-aux.h b/libnm-core/nm-libnm-core-aux/nm-libnm-core-aux.h
new file mode 100644
index 00000000..8a835205
--- /dev/null
+++ b/libnm-core/nm-libnm-core-aux/nm-libnm-core-aux.h
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: LGPL-2.1+
+/*
+ * Copyright (C) 2019 Red Hat, Inc.
+ */
+
+#ifndef __NM_LIBNM_CORE_AUX_H__
+#define __NM_LIBNM_CORE_AUX_H__
+
+#include "nm-setting-team.h"
+
+typedef enum {
+	NM_TEAM_LINK_WATCHER_TYPE_NONE     = 0,
+	NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL  = (1u << 0),
+	NM_TEAM_LINK_WATCHER_TYPE_NSNAPING = (1u << 1),
+	NM_TEAM_LINK_WATCHER_TYPE_ARPING   = (1u << 2),
+} NMTeamLinkWatcherType;
+
+typedef enum {
+	NM_TEAM_LINK_WATCHER_KEY_NAME,
+	NM_TEAM_LINK_WATCHER_KEY_DELAY_UP,
+	NM_TEAM_LINK_WATCHER_KEY_DELAY_DOWN,
+	NM_TEAM_LINK_WATCHER_KEY_INIT_WAIT,
+	NM_TEAM_LINK_WATCHER_KEY_INTERVAL,
+	NM_TEAM_LINK_WATCHER_KEY_MISSED_MAX,
+	NM_TEAM_LINK_WATCHER_KEY_TARGET_HOST,
+	NM_TEAM_LINK_WATCHER_KEY_VLANID,
+	NM_TEAM_LINK_WATCHER_KEY_SOURCE_HOST,
+	NM_TEAM_LINK_WATCHER_KEY_VALIDATE_ACTIVE,
+	NM_TEAM_LINK_WATCHER_KEY_VALIDATE_INACTIVE,
+	NM_TEAM_LINK_WATCHER_KEY_SEND_ALWAYS,
+	_NM_TEAM_LINK_WATCHER_KEY_NUM,
+} NMTeamLinkWatcherKeyId;
+
+char *nm_utils_team_link_watcher_to_string (const NMTeamLinkWatcher *watcher);
+
+NMTeamLinkWatcher *nm_utils_team_link_watcher_from_string (const char *str,
+                                                           GError **error);
+
+#endif /* __NM_LIBNM_CORE_AUX_H__ */
diff --git a/libnm-core/nm-libnm-core-intern/README.md b/libnm-core/nm-libnm-core-intern/README.md
new file mode 100644
index 00000000..2dcc3c80
--- /dev/null
+++ b/libnm-core/nm-libnm-core-intern/README.md
@@ -0,0 +1,21 @@
+nm-libnm-core-intern is a static library that:
+
+ - uses parts of "libnm-core", that are public API of "libnm"
+ - that is statically linked into libnm-core (and thus libnm
+   and NetworkManager).
+ - that can also be statically linked into other users of libnm.
+
+Basically, it is a static library with utility functions that extends
+libnm-core (the part that is public API of libnm), but it is used
+by libnm-core.
+
+That means:
+
+  - you can use it everywhere where you either statically link
+    with libnm-core, or dynamically link with libnm.
+  - you can even use it inside of libnm-core itself. This is the difference
+    between nm-libnm-core-intern and nm-libnm-core-aux.
+
+Also, since nm-libnm-core-intern itself only uses public (stable)
+API of libnm, you theoretically can copy the sources into your
+own source tree.
diff --git a/libnm-core/nm-libnm-core-intern/nm-auth-subject.c b/libnm-core/nm-libnm-core-intern/nm-auth-subject.c
new file mode 100644
index 00000000..15c2d652
--- /dev/null
+++ b/libnm-core/nm-libnm-core-intern/nm-auth-subject.c
@@ -0,0 +1,446 @@
+// SPDX-License-Identifier: LGPL-2.1+
+/*
+ * Copyright (C) 2013 - 2014 Red Hat, Inc.
+ */
+
+/**
+ * SECTION:nm-auth-subject
+ * @short_description: Encapsulates authentication information about a requestor
+ *
+ * #NMAuthSubject encpasulates identifying information about an entity that
+ * makes requests, like process identifier and user UID.
+ */
+
+#include "nm-default.h"
+
+#include "nm-auth-subject.h"
+
+#include <stdlib.h>
+
+enum {
+	PROP_0,
+	PROP_SUBJECT_TYPE,
+	PROP_UNIX_PROCESS_DBUS_SENDER,
+	PROP_UNIX_PROCESS_PID,
+	PROP_UNIX_PROCESS_UID,
+	PROP_UNIX_SESSION_ID,
+
+	PROP_LAST,
+};
+
+typedef struct {
+	NMAuthSubjectType subject_type;
+	struct {
+		gulong pid;
+		gulong uid;
+		guint64 start_time;
+		char *dbus_sender;
+	} unix_process;
+
+	struct {
+		char *id;
+	} unix_session;
+} NMAuthSubjectPrivate;
+
+struct _NMAuthSubject {
+	GObject parent;
+	NMAuthSubjectPrivate _priv;
+};
+
+struct _NMAuthSubjectClass {
+	GObjectClass parent;
+};
+
+G_DEFINE_TYPE (NMAuthSubject, nm_auth_subject, G_TYPE_OBJECT)
+
+#define NM_AUTH_SUBJECT_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMAuthSubject, NM_IS_AUTH_SUBJECT)
+
+/*****************************************************************************/
+
+#define CHECK_SUBJECT(self, error_value) \
+	NMAuthSubjectPrivate *priv; \
+	g_return_val_if_fail (NM_IS_AUTH_SUBJECT (self), error_value); \
+	priv = NM_AUTH_SUBJECT_GET_PRIVATE (self); \
+
+#define CHECK_SUBJECT_TYPED(self, expected_subject_type, error_value) \
+	CHECK_SUBJECT (self, error_value); \
+	g_return_val_if_fail (priv->subject_type == (expected_subject_type), error_value);
+
+const char *
+nm_auth_subject_to_string (NMAuthSubject *self, char *buf, gsize buf_len)
+{
+	CHECK_SUBJECT (self, NULL);
+
+	switch (priv->subject_type) {
+	case NM_AUTH_SUBJECT_TYPE_UNIX_PROCESS:
+		g_snprintf (buf, buf_len, "unix-process[pid=%lu, uid=%lu, start=%llu]",
+		            (unsigned long) priv->unix_process.pid,
+		            (unsigned long) priv->unix_process.uid,
+		            (unsigned long long) priv->unix_process.start_time);
+		break;
+	case NM_AUTH_SUBJECT_TYPE_INTERNAL:
+		g_strlcpy (buf, "internal", buf_len);
+		break;
+	case NM_AUTH_SUBJECT_TYPE_UNIX_SESSION:
+		g_snprintf (buf, buf_len, "unix-session[id=%s]",
+		            priv->unix_session.id);
+		break;
+	default:
+		g_strlcpy (buf, "invalid", buf_len);
+		break;
+	}
+	return buf;
+}
+
+/* returns a floating variant */
+GVariant *
+nm_auth_subject_unix_to_polkit_gvariant (NMAuthSubject *self)
+{
+	GVariantBuilder builder;
+	CHECK_SUBJECT (self, NULL);
+
+	switch (priv->subject_type) {
+
+	case NM_AUTH_SUBJECT_TYPE_UNIX_SESSION:
+		g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}"));
+		g_variant_builder_add (&builder, "{sv}", "session-id",
+		                       g_variant_new_string (priv->unix_session.id));
+		return g_variant_new ("(sa{sv})", "unix-session", &builder);
+
+	case NM_AUTH_SUBJECT_TYPE_UNIX_PROCESS:
+		g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}"));
+		g_variant_builder_add (&builder, "{sv}", "pid",
+		                       g_variant_new_uint32 (priv->unix_process.pid));
+		g_variant_builder_add (&builder, "{sv}", "start-time",
+		                       g_variant_new_uint64 (priv->unix_process.start_time));
+		g_variant_builder_add (&builder, "{sv}", "uid",
+		                       g_variant_new_int32 (priv->unix_process.uid));
+		return g_variant_new ("(sa{sv})", "unix-process", &builder);
+
+	default:
+		g_return_val_if_reached (NULL);
+	}
+}
+
+NMAuthSubjectType
+nm_auth_subject_get_subject_type (NMAuthSubject *subject)
+{
+	CHECK_SUBJECT (subject, NM_AUTH_SUBJECT_TYPE_INVALID);
+
+	return priv->subject_type;
+}
+
+gulong
+nm_auth_subject_get_unix_process_pid (NMAuthSubject *subject)
+{
+	CHECK_SUBJECT_TYPED (subject, NM_AUTH_SUBJECT_TYPE_UNIX_PROCESS, G_MAXULONG);
+
+	return priv->unix_process.pid;
+}
+
+gulong
+nm_auth_subject_get_unix_process_uid (NMAuthSubject *subject)
+{
+	CHECK_SUBJECT_TYPED (subject, NM_AUTH_SUBJECT_TYPE_UNIX_PROCESS, G_MAXULONG);
+
+	return priv->unix_process.uid;
+}
+
+const char *
+nm_auth_subject_get_unix_process_dbus_sender (NMAuthSubject *subject)
+{
+	CHECK_SUBJECT_TYPED (subject, NM_AUTH_SUBJECT_TYPE_UNIX_PROCESS, NULL);
+
+	return priv->unix_process.dbus_sender;
+}
+
+const char *
+nm_auth_subject_get_unix_session_id (NMAuthSubject *subject)
+{
+	CHECK_SUBJECT_TYPED (subject, NM_AUTH_SUBJECT_TYPE_UNIX_SESSION, NULL);
+
+	return priv->unix_session.id;
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_auth_subject_new_internal():
+ *
+ * Creates a new auth subject representing the NetworkManager process itself.
+ *
+ * Returns: the new #NMAuthSubject
+ */
+NMAuthSubject *
+nm_auth_subject_new_internal (void)
+{
+	return NM_AUTH_SUBJECT (g_object_new (NM_TYPE_AUTH_SUBJECT,
+                                          NM_AUTH_SUBJECT_SUBJECT_TYPE, (int) NM_AUTH_SUBJECT_TYPE_INTERNAL,
+                                          NULL));
+}
+
+/**
+ * nm_auth_subject_new_unix_session():
+ *
+ * Creates a new auth subject representing a given unix session.
+ *
+ * Returns: the new #NMAuthSubject
+ */
+NMAuthSubject *
+nm_auth_subject_new_unix_session (const char *session_id)
+{
+	return NM_AUTH_SUBJECT (g_object_new (NM_TYPE_AUTH_SUBJECT,
+	                                      NM_AUTH_SUBJECT_SUBJECT_TYPE, (int) NM_AUTH_SUBJECT_TYPE_UNIX_SESSION,
+	                                      NM_AUTH_SUBJECT_UNIX_SESSION_ID, session_id,
+	                                      NULL));
+}
+
+/**
+ * nm_auth_subject_new_unix_process():
+ *
+ * Creates a new auth subject representing a given unix process.
+ *
+ * Returns: the new #NMAuthSubject
+ */
+NMAuthSubject *
+nm_auth_subject_new_unix_process (const char *dbus_sender, gulong pid, gulong uid)
+{
+	return NM_AUTH_SUBJECT (g_object_new (NM_TYPE_AUTH_SUBJECT,
+	                                      NM_AUTH_SUBJECT_SUBJECT_TYPE, (int) NM_AUTH_SUBJECT_TYPE_UNIX_PROCESS,
+	                                      NM_AUTH_SUBJECT_UNIX_PROCESS_DBUS_SENDER, dbus_sender,
+	                                      NM_AUTH_SUBJECT_UNIX_PROCESS_PID, pid,
+	                                      NM_AUTH_SUBJECT_UNIX_PROCESS_UID, uid,
+	                                      NULL));
+}
+
+/**
+ * nm_auth_subject_new_unix_process_self():
+ *
+ * Creates a new auth subject representing the current executing process.
+ *
+ * Returns: the new #NMAuthSubject
+ */
+NMAuthSubject *
+nm_auth_subject_new_unix_process_self (void)
+{
+	return nm_auth_subject_new_unix_process (NULL, getpid(), getuid());
+}
+
+/*****************************************************************************/
+
+static void
+get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+	NMAuthSubjectPrivate *priv = NM_AUTH_SUBJECT_GET_PRIVATE (object);
+
+	switch (prop_id) {
+	case PROP_SUBJECT_TYPE:
+		g_value_set_int (value, priv->subject_type);
+		break;
+	case PROP_UNIX_PROCESS_DBUS_SENDER:
+		g_value_set_string (value, priv->unix_process.dbus_sender);
+		break;
+	case PROP_UNIX_PROCESS_PID:
+		g_value_set_ulong (value, priv->unix_process.pid);
+		break;
+	case PROP_UNIX_PROCESS_UID:
+		g_value_set_ulong (value, priv->unix_process.uid);
+		break;
+	case PROP_UNIX_SESSION_ID:
+		g_value_set_string (value, priv->unix_session.id);
+		break;
+	default:
+		 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		 break;
+	}
+}
+
+static void
+set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+	NMAuthSubjectPrivate *priv = NM_AUTH_SUBJECT_GET_PRIVATE (object);
+	NMAuthSubjectType subject_type;
+	int i;
+	const char *str;
+	gulong id;
+
+	switch (prop_id) {
+	case PROP_SUBJECT_TYPE:
+		/* construct-only */
+		i = g_value_get_int (value);
+		g_return_if_fail (NM_IN_SET (i,
+		                             (int) NM_AUTH_SUBJECT_TYPE_INTERNAL,
+		                             (int) NM_AUTH_SUBJECT_TYPE_UNIX_PROCESS,
+		                             (int) NM_AUTH_SUBJECT_TYPE_UNIX_SESSION));
+		subject_type = i;
+		priv->subject_type |= subject_type;
+		g_return_if_fail (priv->subject_type == subject_type);
+		break;
+	case PROP_UNIX_PROCESS_DBUS_SENDER:
+		/* construct-only */
+		if ((str = g_value_get_string (value))) {
+			priv->subject_type |= NM_AUTH_SUBJECT_TYPE_UNIX_PROCESS;
+			g_return_if_fail (priv->subject_type == NM_AUTH_SUBJECT_TYPE_UNIX_PROCESS);
+			priv->unix_process.dbus_sender = g_strdup (str);
+		}
+		break;
+	case PROP_UNIX_PROCESS_PID:
+		/* construct-only */
+		if ((id = g_value_get_ulong (value)) != G_MAXULONG) {
+			priv->subject_type |= NM_AUTH_SUBJECT_TYPE_UNIX_PROCESS;
+			g_return_if_fail (priv->subject_type == NM_AUTH_SUBJECT_TYPE_UNIX_PROCESS);
+			priv->unix_process.pid = id;
+		}
+		break;
+	case PROP_UNIX_PROCESS_UID:
+		/* construct-only */
+		if ((id = g_value_get_ulong (value)) != G_MAXULONG) {
+			priv->subject_type |= NM_AUTH_SUBJECT_TYPE_UNIX_PROCESS;
+			g_return_if_fail (priv->subject_type == NM_AUTH_SUBJECT_TYPE_UNIX_PROCESS);
+			priv->unix_process.uid = id;
+		}
+		break;
+	case PROP_UNIX_SESSION_ID:
+		/* construct-only */
+		if ((str = g_value_get_string (value))) {
+			priv->subject_type |= NM_AUTH_SUBJECT_TYPE_UNIX_SESSION;
+			g_return_if_fail (priv->subject_type == NM_AUTH_SUBJECT_TYPE_UNIX_SESSION);
+			priv->unix_session.id = g_strdup (str);
+		}
+		break;
+	default:
+		 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		 break;
+	}
+}
+
+static void
+_clear_private (NMAuthSubject *self)
+{
+	NMAuthSubjectPrivate *priv = NM_AUTH_SUBJECT_GET_PRIVATE (self);
+
+	priv->subject_type = NM_AUTH_SUBJECT_TYPE_INVALID;
+	priv->unix_process.pid = G_MAXULONG;
+	priv->unix_process.uid = G_MAXULONG;
+	nm_clear_g_free (&priv->unix_process.dbus_sender);
+
+	nm_clear_g_free (&priv->unix_session.id);
+}
+
+static void
+nm_auth_subject_init (NMAuthSubject *self)
+{
+	_clear_private (self);
+}
+
+static void
+constructed (GObject *object)
+{
+	NMAuthSubject *self = NM_AUTH_SUBJECT (object);
+	NMAuthSubjectPrivate *priv = NM_AUTH_SUBJECT_GET_PRIVATE (self);
+
+	/* validate that the created instance. */
+
+	switch (priv->subject_type) {
+	case NM_AUTH_SUBJECT_TYPE_INTERNAL:
+		priv->unix_process.pid = G_MAXULONG;
+		priv->unix_process.uid = 0;  /* internal uses 'root' user */
+		return;
+	case NM_AUTH_SUBJECT_TYPE_UNIX_PROCESS:
+		/* Ensure pid and uid to be representable as int32.
+		 * DBUS treats them as uint32, polkit library as int. */
+		if (priv->unix_process.pid > MIN (G_MAXINT, G_MAXINT32))
+			break;
+		if (priv->unix_process.uid > MIN (G_MAXINT, G_MAXINT32)) {
+			/* for uid==-1, libpolkit-gobject-1 detects the user based on the process id.
+			 * Don't bother and require the user id as parameter. */
+			break;
+		}
+
+		priv->unix_process.start_time = nm_utils_get_start_time_for_pid (priv->unix_process.pid, NULL, NULL);
+
+		if (!priv->unix_process.start_time) {
+			/* Is the process already gone? Then fail creation of the auth subject
+			 * by clearing the type. */
+			if (kill (priv->unix_process.pid, 0) != 0)
+				_clear_private (self);
+
+			/* Otherwise, although we didn't detect a start_time, the process is still around.
+			 * That could be due to procfs mounted with hidepid. So just accept the request.
+			 *
+			 * Polkit on the other side, will accept 0 and try to lookup /proc/$PID/stat
+			 * itself (and if it fails to do so, assume a start-time of 0 and proceed).
+			 * The only combination that would fail here, is when NM is able to read the
+			 * start-time, but polkit is not. */
+		}
+		return;
+	case NM_AUTH_SUBJECT_TYPE_UNIX_SESSION:
+		return;
+	default:
+		break;
+	}
+
+	_clear_private (self);
+	g_return_if_reached ();
+}
+
+static void
+finalize (GObject *object)
+{
+	_clear_private ((NMAuthSubject *) object);
+
+	G_OBJECT_CLASS (nm_auth_subject_parent_class)->finalize (object);
+}
+
+static void
+nm_auth_subject_class_init (NMAuthSubjectClass *config_class)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (config_class);
+
+	object_class->get_property = get_property;
+	object_class->set_property = set_property;
+	object_class->constructed = constructed;
+	object_class->finalize = finalize;
+
+	g_object_class_install_property
+	    (object_class, PROP_SUBJECT_TYPE,
+	     g_param_spec_int (NM_AUTH_SUBJECT_SUBJECT_TYPE, "", "",
+	                       NM_AUTH_SUBJECT_TYPE_INVALID,
+	                       NM_AUTH_SUBJECT_TYPE_UNIX_SESSION,
+	                       NM_AUTH_SUBJECT_TYPE_INVALID,
+	                       G_PARAM_READWRITE |
+	                       G_PARAM_CONSTRUCT_ONLY |
+	                       G_PARAM_STATIC_STRINGS));
+
+	g_object_class_install_property
+	    (object_class, PROP_UNIX_PROCESS_DBUS_SENDER,
+	     g_param_spec_string (NM_AUTH_SUBJECT_UNIX_PROCESS_DBUS_SENDER, "", "",
+	                          NULL,
+	                          G_PARAM_READWRITE |
+	                          G_PARAM_CONSTRUCT_ONLY |
+	                          G_PARAM_STATIC_STRINGS));
+
+	g_object_class_install_property
+	     (object_class, PROP_UNIX_PROCESS_PID,
+	      g_param_spec_ulong (NM_AUTH_SUBJECT_UNIX_PROCESS_PID, "", "",
+	                          0, G_MAXULONG, G_MAXULONG,
+	                          G_PARAM_READWRITE |
+	                          G_PARAM_CONSTRUCT_ONLY |
+	                          G_PARAM_STATIC_STRINGS));
+
+	g_object_class_install_property
+	     (object_class, PROP_UNIX_PROCESS_UID,
+	      g_param_spec_ulong (NM_AUTH_SUBJECT_UNIX_PROCESS_UID, "", "",
+	                          0, G_MAXULONG, G_MAXULONG,
+	                          G_PARAM_READWRITE |
+	                          G_PARAM_CONSTRUCT_ONLY |
+	                          G_PARAM_STATIC_STRINGS));
+
+	g_object_class_install_property
+	     (object_class, PROP_UNIX_SESSION_ID,
+	      g_param_spec_string (NM_AUTH_SUBJECT_UNIX_SESSION_ID, "", "",
+	                           NULL,
+	                           G_PARAM_READWRITE |
+	                           G_PARAM_CONSTRUCT_ONLY |
+	                           G_PARAM_STATIC_STRINGS));
+}
diff --git a/libnm-core/nm-libnm-core-intern/nm-auth-subject.h b/libnm-core/nm-libnm-core-intern/nm-auth-subject.h
new file mode 100644
index 00000000..fc37b8e2
--- /dev/null
+++ b/libnm-core/nm-libnm-core-intern/nm-auth-subject.h
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: LGPL-2.1+
+/*
+ * Copyright (C) 2013 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_AUTH_SUBJECT_H__
+#define __NETWORKMANAGER_AUTH_SUBJECT_H__
+
+#define NM_TYPE_AUTH_SUBJECT            (nm_auth_subject_get_type ())
+#define NM_AUTH_SUBJECT(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), NM_TYPE_AUTH_SUBJECT, NMAuthSubject))
+#define NM_AUTH_SUBJECT_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), NM_TYPE_AUTH_SUBJECT, NMAuthSubjectClass))
+#define NM_IS_AUTH_SUBJECT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NM_TYPE_AUTH_SUBJECT))
+#define NM_IS_AUTH_SUBJECT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), NM_TYPE_AUTH_SUBJECT))
+#define NM_AUTH_SUBJECT_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), NM_TYPE_AUTH_SUBJECT, NMAuthSubjectClass))
+
+typedef enum {
+	NM_AUTH_SUBJECT_TYPE_INVALID      = 0,
+	NM_AUTH_SUBJECT_TYPE_INTERNAL     = 1,
+	NM_AUTH_SUBJECT_TYPE_UNIX_PROCESS = 2,
+	NM_AUTH_SUBJECT_TYPE_UNIX_SESSION = 4,
+} NMAuthSubjectType;
+
+#define NM_AUTH_SUBJECT_SUBJECT_TYPE                           "subject-type"
+#define NM_AUTH_SUBJECT_UNIX_PROCESS_DBUS_SENDER               "unix-process-dbus-sender"
+#define NM_AUTH_SUBJECT_UNIX_PROCESS_PID                       "unix-process-pid"
+#define NM_AUTH_SUBJECT_UNIX_PROCESS_UID                       "unix-process-uid"
+#define NM_AUTH_SUBJECT_UNIX_SESSION_ID                        "unix-session-id"
+
+typedef struct _NMAuthSubjectClass NMAuthSubjectClass;
+typedef struct _NMAuthSubject NMAuthSubject;
+
+GType nm_auth_subject_get_type (void);
+
+NMAuthSubject *nm_auth_subject_new_internal (void);
+
+NMAuthSubject *nm_auth_subject_new_unix_session (const char *session_id);
+
+NMAuthSubject *nm_auth_subject_new_unix_process (const char *dbus_sender, gulong pid, gulong uid);
+
+NMAuthSubject *nm_auth_subject_new_unix_process_self (void);
+
+NMAuthSubjectType nm_auth_subject_get_subject_type (NMAuthSubject *subject);
+
+gulong nm_auth_subject_get_unix_process_pid (NMAuthSubject *subject);
+
+const char *nm_auth_subject_get_unix_process_dbus_sender (NMAuthSubject *subject);
+
+gulong nm_auth_subject_get_unix_process_uid (NMAuthSubject *subject);
+
+const char *nm_auth_subject_get_unix_session_id (NMAuthSubject *subject);
+
+const char *nm_auth_subject_to_string (NMAuthSubject *self, char *buf, gsize buf_len);
+
+GVariant *nm_auth_subject_unix_to_polkit_gvariant (NMAuthSubject *self);
+
+#endif /* __NETWORKMANAGER_AUTH_SUBJECT_H__ */
diff --git a/libnm-core/nm-libnm-core-intern/nm-common-macros.h b/libnm-core/nm-libnm-core-intern/nm-common-macros.h
new file mode 100644
index 00000000..fcfe6919
--- /dev/null
+++ b/libnm-core/nm-libnm-core-intern/nm-common-macros.h
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: LGPL-2.1+
+/*
+ * Copyright (C) 2016 Red Hat, Inc.
+ */
+
+#ifndef __NM_COMMON_MACROS_H__
+#define __NM_COMMON_MACROS_H__
+
+/*****************************************************************************/
+
+#define NM_AUTH_PERMISSION_ENABLE_DISABLE_NETWORK     "org.freedesktop.NetworkManager.enable-disable-network"
+#define NM_AUTH_PERMISSION_SLEEP_WAKE                 "org.freedesktop.NetworkManager.sleep-wake"
+#define NM_AUTH_PERMISSION_ENABLE_DISABLE_WIFI        "org.freedesktop.NetworkManager.enable-disable-wifi"
+#define NM_AUTH_PERMISSION_ENABLE_DISABLE_WWAN        "org.freedesktop.NetworkManager.enable-disable-wwan"
+#define NM_AUTH_PERMISSION_ENABLE_DISABLE_WIMAX       "org.freedesktop.NetworkManager.enable-disable-wimax"
+#define NM_AUTH_PERMISSION_NETWORK_CONTROL            "org.freedesktop.NetworkManager.network-control"
+#define NM_AUTH_PERMISSION_WIFI_SHARE_PROTECTED       "org.freedesktop.NetworkManager.wifi.share.protected"
+#define NM_AUTH_PERMISSION_WIFI_SHARE_OPEN            "org.freedesktop.NetworkManager.wifi.share.open"
+#define NM_AUTH_PERMISSION_SETTINGS_MODIFY_SYSTEM     "org.freedesktop.NetworkManager.settings.modify.system"
+#define NM_AUTH_PERMISSION_SETTINGS_MODIFY_OWN        "org.freedesktop.NetworkManager.settings.modify.own"
+#define NM_AUTH_PERMISSION_SETTINGS_MODIFY_HOSTNAME   "org.freedesktop.NetworkManager.settings.modify.hostname"
+#define NM_AUTH_PERMISSION_SETTINGS_MODIFY_GLOBAL_DNS "org.freedesktop.NetworkManager.settings.modify.global-dns"
+#define NM_AUTH_PERMISSION_RELOAD                     "org.freedesktop.NetworkManager.reload"
+#define NM_AUTH_PERMISSION_CHECKPOINT_ROLLBACK        "org.freedesktop.NetworkManager.checkpoint-rollback"
+#define NM_AUTH_PERMISSION_ENABLE_DISABLE_STATISTICS  "org.freedesktop.NetworkManager.enable-disable-statistics"
+#define NM_AUTH_PERMISSION_ENABLE_DISABLE_CONNECTIVITY_CHECK "org.freedesktop.NetworkManager.enable-disable-connectivity-check"
+#define NM_AUTH_PERMISSION_WIFI_SCAN                  "org.freedesktop.NetworkManager.wifi.scan"
+
+#define NM_CLONED_MAC_PRESERVE                          "preserve"
+#define NM_CLONED_MAC_PERMANENT                         "permanent"
+#define NM_CLONED_MAC_RANDOM                            "random"
+#define NM_CLONED_MAC_STABLE                            "stable"
+
+static inline gboolean
+NM_CLONED_MAC_IS_SPECIAL (const char *str)
+{
+	return NM_IN_STRSET (str,
+	                     NM_CLONED_MAC_PRESERVE,
+	                     NM_CLONED_MAC_PERMANENT,
+	                     NM_CLONED_MAC_RANDOM,
+	                     NM_CLONED_MAC_STABLE);
+}
+
+#define NM_IAID_MAC                "mac"
+#define NM_IAID_PERM_MAC           "perm-mac"
+#define NM_IAID_IFNAME             "ifname"
+#define NM_IAID_STABLE             "stable"
+
+#define NM_CONNECTION_MUD_URL_NONE "none"
+
+static inline gboolean
+NM_IAID_IS_SPECIAL (const char *str)
+{
+	return NM_IN_STRSET (str,
+	                     NM_IAID_MAC,
+	                     NM_IAID_PERM_MAC,
+	                     NM_IAID_IFNAME,
+	                     NM_IAID_STABLE);
+}
+
+#endif /* __NM_COMMON_MACROS_H__ */
diff --git a/libnm-core/nm-libnm-core-intern/nm-ethtool-utils.c b/libnm-core/nm-libnm-core-intern/nm-ethtool-utils.c
new file mode 100644
index 00000000..3e3c28d3
--- /dev/null
+++ b/libnm-core/nm-libnm-core-intern/nm-ethtool-utils.c
@@ -0,0 +1,276 @@
+// SPDX-License-Identifier: LGPL-2.1+
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#include "nm-default.h"
+
+#include "nm-ethtool-utils.h"
+
+#include "nm-setting-ethtool.h"
+
+/*****************************************************************************/
+
+#define ETHT_DATA(xname) \
+	[NM_ETHTOOL_ID_##xname] = (&((const NMEthtoolData) { \
+	   .optname = NM_ETHTOOL_OPTNAME_##xname, \
+	   .id = NM_ETHTOOL_ID_##xname, \
+	}))
+
+const NMEthtoolData *const nm_ethtool_data[_NM_ETHTOOL_ID_NUM + 1] = {
+	/* indexed by NMEthtoolID */
+	ETHT_DATA (COALESCE_ADAPTIVE_RX),
+	ETHT_DATA (COALESCE_ADAPTIVE_TX),
+	ETHT_DATA (COALESCE_PKT_RATE_HIGH),
+	ETHT_DATA (COALESCE_PKT_RATE_LOW),
+	ETHT_DATA (COALESCE_RX_FRAMES),
+	ETHT_DATA (COALESCE_RX_FRAMES_HIGH),
+	ETHT_DATA (COALESCE_RX_FRAMES_IRQ),
+	ETHT_DATA (COALESCE_RX_FRAMES_LOW),
+	ETHT_DATA (COALESCE_RX_USECS),
+	ETHT_DATA (COALESCE_RX_USECS_HIGH),
+	ETHT_DATA (COALESCE_RX_USECS_IRQ),
+	ETHT_DATA (COALESCE_RX_USECS_LOW),
+	ETHT_DATA (COALESCE_SAMPLE_INTERVAL),
+	ETHT_DATA (COALESCE_STATS_BLOCK_USECS),
+	ETHT_DATA (COALESCE_TX_FRAMES),
+	ETHT_DATA (COALESCE_TX_FRAMES_HIGH),
+	ETHT_DATA (COALESCE_TX_FRAMES_IRQ),
+	ETHT_DATA (COALESCE_TX_FRAMES_LOW),
+	ETHT_DATA (COALESCE_TX_USECS),
+	ETHT_DATA (COALESCE_TX_USECS_HIGH),
+	ETHT_DATA (COALESCE_TX_USECS_IRQ),
+	ETHT_DATA (COALESCE_TX_USECS_LOW),
+	ETHT_DATA (FEATURE_ESP_HW_OFFLOAD),
+	ETHT_DATA (FEATURE_ESP_TX_CSUM_HW_OFFLOAD),
+	ETHT_DATA (FEATURE_FCOE_MTU),
+	ETHT_DATA (FEATURE_GRO),
+	ETHT_DATA (FEATURE_GSO),
+	ETHT_DATA (FEATURE_HIGHDMA),
+	ETHT_DATA (FEATURE_HW_TC_OFFLOAD),
+	ETHT_DATA (FEATURE_L2_FWD_OFFLOAD),
+	ETHT_DATA (FEATURE_LOOPBACK),
+	ETHT_DATA (FEATURE_LRO),
+	ETHT_DATA (FEATURE_NTUPLE),
+	ETHT_DATA (FEATURE_RX),
+	ETHT_DATA (FEATURE_RXHASH),
+	ETHT_DATA (FEATURE_RXVLAN),
+	ETHT_DATA (FEATURE_RX_ALL),
+	ETHT_DATA (FEATURE_RX_FCS),
+	ETHT_DATA (FEATURE_RX_GRO_HW),
+	ETHT_DATA (FEATURE_RX_UDP_TUNNEL_PORT_OFFLOAD),
+	ETHT_DATA (FEATURE_RX_VLAN_FILTER),
+	ETHT_DATA (FEATURE_RX_VLAN_STAG_FILTER),
+	ETHT_DATA (FEATURE_RX_VLAN_STAG_HW_PARSE),
+	ETHT_DATA (FEATURE_SG),
+	ETHT_DATA (FEATURE_TLS_HW_RECORD),
+	ETHT_DATA (FEATURE_TLS_HW_TX_OFFLOAD),
+	ETHT_DATA (FEATURE_TSO),
+	ETHT_DATA (FEATURE_TX),
+	ETHT_DATA (FEATURE_TXVLAN),
+	ETHT_DATA (FEATURE_TX_CHECKSUM_FCOE_CRC),
+	ETHT_DATA (FEATURE_TX_CHECKSUM_IPV4),
+	ETHT_DATA (FEATURE_TX_CHECKSUM_IPV6),
+	ETHT_DATA (FEATURE_TX_CHECKSUM_IP_GENERIC),
+	ETHT_DATA (FEATURE_TX_CHECKSUM_SCTP),
+	ETHT_DATA (FEATURE_TX_ESP_SEGMENTATION),
+	ETHT_DATA (FEATURE_TX_FCOE_SEGMENTATION),
+	ETHT_DATA (FEATURE_TX_GRE_CSUM_SEGMENTATION),
+	ETHT_DATA (FEATURE_TX_GRE_SEGMENTATION),
+	ETHT_DATA (FEATURE_TX_GSO_PARTIAL),
+	ETHT_DATA (FEATURE_TX_GSO_ROBUST),
+	ETHT_DATA (FEATURE_TX_IPXIP4_SEGMENTATION),
+	ETHT_DATA (FEATURE_TX_IPXIP6_SEGMENTATION),
+	ETHT_DATA (FEATURE_TX_NOCACHE_COPY),
+	ETHT_DATA (FEATURE_TX_SCATTER_GATHER),
+	ETHT_DATA (FEATURE_TX_SCATTER_GATHER_FRAGLIST),
+	ETHT_DATA (FEATURE_TX_SCTP_SEGMENTATION),
+	ETHT_DATA (FEATURE_TX_TCP6_SEGMENTATION),
+	ETHT_DATA (FEATURE_TX_TCP_ECN_SEGMENTATION),
+	ETHT_DATA (FEATURE_TX_TCP_MANGLEID_SEGMENTATION),
+	ETHT_DATA (FEATURE_TX_TCP_SEGMENTATION),
+	ETHT_DATA (FEATURE_TX_UDP_SEGMENTATION),
+	ETHT_DATA (FEATURE_TX_UDP_TNL_CSUM_SEGMENTATION),
+	ETHT_DATA (FEATURE_TX_UDP_TNL_SEGMENTATION),
+	ETHT_DATA (FEATURE_TX_VLAN_STAG_HW_INSERT),
+	ETHT_DATA (RING_RX),
+	ETHT_DATA (RING_RX_JUMBO),
+	ETHT_DATA (RING_RX_MINI),
+	ETHT_DATA (RING_TX),
+	[_NM_ETHTOOL_ID_NUM] = NULL,
+};
+
+static const guint8 _by_name[_NM_ETHTOOL_ID_NUM] = {
+	/* sorted by optname. */
+	NM_ETHTOOL_ID_COALESCE_ADAPTIVE_RX,
+	NM_ETHTOOL_ID_COALESCE_ADAPTIVE_TX,
+	NM_ETHTOOL_ID_COALESCE_PKT_RATE_HIGH,
+	NM_ETHTOOL_ID_COALESCE_PKT_RATE_LOW,
+	NM_ETHTOOL_ID_COALESCE_RX_FRAMES,
+	NM_ETHTOOL_ID_COALESCE_RX_FRAMES_HIGH,
+	NM_ETHTOOL_ID_COALESCE_RX_FRAMES_IRQ,
+	NM_ETHTOOL_ID_COALESCE_RX_FRAMES_LOW,
+	NM_ETHTOOL_ID_COALESCE_RX_USECS,
+	NM_ETHTOOL_ID_COALESCE_RX_USECS_HIGH,
+	NM_ETHTOOL_ID_COALESCE_RX_USECS_IRQ,
+	NM_ETHTOOL_ID_COALESCE_RX_USECS_LOW,
+	NM_ETHTOOL_ID_COALESCE_SAMPLE_INTERVAL,
+	NM_ETHTOOL_ID_COALESCE_STATS_BLOCK_USECS,
+	NM_ETHTOOL_ID_COALESCE_TX_FRAMES,
+	NM_ETHTOOL_ID_COALESCE_TX_FRAMES_HIGH,
+	NM_ETHTOOL_ID_COALESCE_TX_FRAMES_IRQ,
+	NM_ETHTOOL_ID_COALESCE_TX_FRAMES_LOW,
+	NM_ETHTOOL_ID_COALESCE_TX_USECS,
+	NM_ETHTOOL_ID_COALESCE_TX_USECS_HIGH,
+	NM_ETHTOOL_ID_COALESCE_TX_USECS_IRQ,
+	NM_ETHTOOL_ID_COALESCE_TX_USECS_LOW,
+	NM_ETHTOOL_ID_FEATURE_ESP_HW_OFFLOAD,
+	NM_ETHTOOL_ID_FEATURE_ESP_TX_CSUM_HW_OFFLOAD,
+	NM_ETHTOOL_ID_FEATURE_FCOE_MTU,
+	NM_ETHTOOL_ID_FEATURE_GRO,
+	NM_ETHTOOL_ID_FEATURE_GSO,
+	NM_ETHTOOL_ID_FEATURE_HIGHDMA,
+	NM_ETHTOOL_ID_FEATURE_HW_TC_OFFLOAD,
+	NM_ETHTOOL_ID_FEATURE_L2_FWD_OFFLOAD,
+	NM_ETHTOOL_ID_FEATURE_LOOPBACK,
+	NM_ETHTOOL_ID_FEATURE_LRO,
+	NM_ETHTOOL_ID_FEATURE_NTUPLE,
+	NM_ETHTOOL_ID_FEATURE_RX,
+	NM_ETHTOOL_ID_FEATURE_RX_ALL,
+	NM_ETHTOOL_ID_FEATURE_RX_FCS,
+	NM_ETHTOOL_ID_FEATURE_RX_GRO_HW,
+	NM_ETHTOOL_ID_FEATURE_RX_UDP_TUNNEL_PORT_OFFLOAD,
+	NM_ETHTOOL_ID_FEATURE_RX_VLAN_FILTER,
+	NM_ETHTOOL_ID_FEATURE_RX_VLAN_STAG_FILTER,
+	NM_ETHTOOL_ID_FEATURE_RX_VLAN_STAG_HW_PARSE,
+	NM_ETHTOOL_ID_FEATURE_RXHASH,
+	NM_ETHTOOL_ID_FEATURE_RXVLAN,
+	NM_ETHTOOL_ID_FEATURE_SG,
+	NM_ETHTOOL_ID_FEATURE_TLS_HW_RECORD,
+	NM_ETHTOOL_ID_FEATURE_TLS_HW_TX_OFFLOAD,
+	NM_ETHTOOL_ID_FEATURE_TSO,
+	NM_ETHTOOL_ID_FEATURE_TX,
+	NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_FCOE_CRC,
+	NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_IP_GENERIC,
+	NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_IPV4,
+	NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_IPV6,
+	NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_SCTP,
+	NM_ETHTOOL_ID_FEATURE_TX_ESP_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_FCOE_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_GRE_CSUM_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_GRE_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_GSO_PARTIAL,
+	NM_ETHTOOL_ID_FEATURE_TX_GSO_ROBUST,
+	NM_ETHTOOL_ID_FEATURE_TX_IPXIP4_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_IPXIP6_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_NOCACHE_COPY,
+	NM_ETHTOOL_ID_FEATURE_TX_SCATTER_GATHER,
+	NM_ETHTOOL_ID_FEATURE_TX_SCATTER_GATHER_FRAGLIST,
+	NM_ETHTOOL_ID_FEATURE_TX_SCTP_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_TCP_ECN_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_TCP_MANGLEID_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_TCP_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_TCP6_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_UDP_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_UDP_TNL_CSUM_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_UDP_TNL_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_VLAN_STAG_HW_INSERT,
+	NM_ETHTOOL_ID_FEATURE_TXVLAN,
+	NM_ETHTOOL_ID_RING_RX,
+	NM_ETHTOOL_ID_RING_RX_JUMBO,
+	NM_ETHTOOL_ID_RING_RX_MINI,
+	NM_ETHTOOL_ID_RING_TX,
+};
+
+/*****************************************************************************/
+
+static void
+_ASSERT_data (void)
+{
+	int i;
+
+	if (!NM_MORE_ASSERT_ONCE (10))
+		return;
+
+	G_STATIC_ASSERT_EXPR (_NM_ETHTOOL_ID_FIRST == 0);
+	G_STATIC_ASSERT_EXPR (_NM_ETHTOOL_ID_LAST == _NM_ETHTOOL_ID_NUM - 1);
+	G_STATIC_ASSERT_EXPR (_NM_ETHTOOL_ID_NUM > 0);
+
+	nm_assert (NM_PTRARRAY_LEN (nm_ethtool_data) == _NM_ETHTOOL_ID_NUM);
+	nm_assert (G_N_ELEMENTS (_by_name)           == _NM_ETHTOOL_ID_NUM);
+	nm_assert (G_N_ELEMENTS (nm_ethtool_data)    == _NM_ETHTOOL_ID_NUM + 1);
+
+	for (i = 0; i < _NM_ETHTOOL_ID_NUM; i++) {
+		const NMEthtoolData *d = nm_ethtool_data[i];
+
+		nm_assert (d);
+		nm_assert (d->id == (NMEthtoolID) i);
+		nm_assert (d->optname && d->optname[0]);
+	}
+
+	for (i = 0; i < _NM_ETHTOOL_ID_NUM; i++) {
+		NMEthtoolID id = _by_name[i];
+		const NMEthtoolData *d;
+
+		nm_assert (id >= 0);
+		nm_assert (id < _NM_ETHTOOL_ID_NUM);
+
+		d = nm_ethtool_data[id];
+		if (i > 0) {
+			/* since we assert that all optnames are sorted strictly monotonically increasing,
+			 * it also follows that there are no duplicates in the _by_name.
+			 * It also follows, that all names in nm_ethtool_data are unique. */
+			if (strcmp (nm_ethtool_data[_by_name[i - 1]]->optname, d->optname) >= 0) {
+				g_error ("nm_ethtool_data is not sorted asciibetically: %u/%s should be after %u/%s",
+				         i - 1, nm_ethtool_data[_by_name[i - 1]]->optname,
+				         i, d->optname);
+			}
+		}
+	}
+}
+
+static int
+_by_name_cmp (gconstpointer a,
+              gconstpointer b,
+              gpointer user_data)
+{
+	const guint8 *p_id = a;
+	const char *optname = b;
+
+	nm_assert (p_id && p_id >= _by_name && p_id <= &_by_name[_NM_ETHTOOL_ID_NUM]);
+	nm_assert (*p_id < _NM_ETHTOOL_ID_NUM);
+
+	return strcmp (nm_ethtool_data[*p_id]->optname, optname);
+}
+
+const NMEthtoolData *
+nm_ethtool_data_get_by_optname (const char *optname)
+{
+	gssize idx;
+
+	if (!optname)
+		return NULL;
+
+	_ASSERT_data ();
+
+	idx = nm_utils_array_find_binary_search ((gconstpointer *) _by_name,
+	                                         sizeof (_by_name[0]),
+	                                         _NM_ETHTOOL_ID_NUM,
+	                                         optname,
+	                                         _by_name_cmp,
+	                                         NULL);
+	return (idx < 0) ? NULL : nm_ethtool_data[_by_name[idx]];
+}
+
+NMEthtoolType
+nm_ethtool_id_to_type (NMEthtoolID id)
+{
+	if (nm_ethtool_id_is_coalesce (id))
+		return NM_ETHTOOL_TYPE_COALESCE;
+	if (nm_ethtool_id_is_feature (id))
+		return NM_ETHTOOL_TYPE_FEATURE;
+	if (nm_ethtool_id_is_ring (id))
+		return NM_ETHTOOL_TYPE_RING;
+
+	return NM_ETHTOOL_TYPE_UNKNOWN;
+}
diff --git a/libnm-core/nm-libnm-core-intern/nm-ethtool-utils.h b/libnm-core/nm-libnm-core-intern/nm-ethtool-utils.h
new file mode 100644
index 00000000..cf971edb
--- /dev/null
+++ b/libnm-core/nm-libnm-core-intern/nm-ethtool-utils.h
@@ -0,0 +1,163 @@
+// SPDX-License-Identifier: LGPL-2.1+
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#ifndef __NM_ETHTOOL_UTILS_H__
+#define __NM_ETHTOOL_UTILS_H__
+
+/*****************************************************************************/
+
+typedef enum {
+	NM_ETHTOOL_ID_UNKNOWN = -1,
+
+	_NM_ETHTOOL_ID_FIRST = 0,
+
+	_NM_ETHTOOL_ID_COALESCE_FIRST = _NM_ETHTOOL_ID_FIRST,
+	NM_ETHTOOL_ID_COALESCE_ADAPTIVE_RX = _NM_ETHTOOL_ID_COALESCE_FIRST,
+	NM_ETHTOOL_ID_COALESCE_ADAPTIVE_TX,
+	NM_ETHTOOL_ID_COALESCE_PKT_RATE_HIGH,
+	NM_ETHTOOL_ID_COALESCE_PKT_RATE_LOW,
+	NM_ETHTOOL_ID_COALESCE_RX_FRAMES,
+	NM_ETHTOOL_ID_COALESCE_RX_FRAMES_HIGH,
+	NM_ETHTOOL_ID_COALESCE_RX_FRAMES_IRQ,
+	NM_ETHTOOL_ID_COALESCE_RX_FRAMES_LOW,
+	NM_ETHTOOL_ID_COALESCE_RX_USECS,
+	NM_ETHTOOL_ID_COALESCE_RX_USECS_HIGH,
+	NM_ETHTOOL_ID_COALESCE_RX_USECS_IRQ,
+	NM_ETHTOOL_ID_COALESCE_RX_USECS_LOW,
+	NM_ETHTOOL_ID_COALESCE_SAMPLE_INTERVAL,
+	NM_ETHTOOL_ID_COALESCE_STATS_BLOCK_USECS,
+	NM_ETHTOOL_ID_COALESCE_TX_FRAMES,
+	NM_ETHTOOL_ID_COALESCE_TX_FRAMES_HIGH,
+	NM_ETHTOOL_ID_COALESCE_TX_FRAMES_IRQ,
+	NM_ETHTOOL_ID_COALESCE_TX_FRAMES_LOW,
+	NM_ETHTOOL_ID_COALESCE_TX_USECS,
+	NM_ETHTOOL_ID_COALESCE_TX_USECS_HIGH,
+	NM_ETHTOOL_ID_COALESCE_TX_USECS_IRQ,
+	NM_ETHTOOL_ID_COALESCE_TX_USECS_LOW,
+	_NM_ETHTOOL_ID_COALESCE_LAST = NM_ETHTOOL_ID_COALESCE_TX_USECS_LOW,
+
+	_NM_ETHTOOL_ID_FEATURE_FIRST = _NM_ETHTOOL_ID_COALESCE_LAST + 1,
+	NM_ETHTOOL_ID_FEATURE_ESP_HW_OFFLOAD = _NM_ETHTOOL_ID_FEATURE_FIRST,
+	NM_ETHTOOL_ID_FEATURE_ESP_TX_CSUM_HW_OFFLOAD,
+	NM_ETHTOOL_ID_FEATURE_FCOE_MTU,
+	NM_ETHTOOL_ID_FEATURE_GRO,
+	NM_ETHTOOL_ID_FEATURE_GSO,
+	NM_ETHTOOL_ID_FEATURE_HIGHDMA,
+	NM_ETHTOOL_ID_FEATURE_HW_TC_OFFLOAD,
+	NM_ETHTOOL_ID_FEATURE_L2_FWD_OFFLOAD,
+	NM_ETHTOOL_ID_FEATURE_LOOPBACK,
+	NM_ETHTOOL_ID_FEATURE_LRO,
+	NM_ETHTOOL_ID_FEATURE_NTUPLE,
+	NM_ETHTOOL_ID_FEATURE_RX,
+	NM_ETHTOOL_ID_FEATURE_RXHASH,
+	NM_ETHTOOL_ID_FEATURE_RXVLAN,
+	NM_ETHTOOL_ID_FEATURE_RX_ALL,
+	NM_ETHTOOL_ID_FEATURE_RX_FCS,
+	NM_ETHTOOL_ID_FEATURE_RX_GRO_HW,
+	NM_ETHTOOL_ID_FEATURE_RX_UDP_TUNNEL_PORT_OFFLOAD,
+	NM_ETHTOOL_ID_FEATURE_RX_VLAN_FILTER,
+	NM_ETHTOOL_ID_FEATURE_RX_VLAN_STAG_FILTER,
+	NM_ETHTOOL_ID_FEATURE_RX_VLAN_STAG_HW_PARSE,
+	NM_ETHTOOL_ID_FEATURE_SG,
+	NM_ETHTOOL_ID_FEATURE_TLS_HW_RECORD,
+	NM_ETHTOOL_ID_FEATURE_TLS_HW_TX_OFFLOAD,
+	NM_ETHTOOL_ID_FEATURE_TSO,
+	NM_ETHTOOL_ID_FEATURE_TX,
+	NM_ETHTOOL_ID_FEATURE_TXVLAN,
+	NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_FCOE_CRC,
+	NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_IPV4,
+	NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_IPV6,
+	NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_IP_GENERIC,
+	NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_SCTP,
+	NM_ETHTOOL_ID_FEATURE_TX_ESP_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_FCOE_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_GRE_CSUM_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_GRE_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_GSO_PARTIAL,
+	NM_ETHTOOL_ID_FEATURE_TX_GSO_ROBUST,
+	NM_ETHTOOL_ID_FEATURE_TX_IPXIP4_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_IPXIP6_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_NOCACHE_COPY,
+	NM_ETHTOOL_ID_FEATURE_TX_SCATTER_GATHER,
+	NM_ETHTOOL_ID_FEATURE_TX_SCATTER_GATHER_FRAGLIST,
+	NM_ETHTOOL_ID_FEATURE_TX_SCTP_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_TCP6_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_TCP_ECN_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_TCP_MANGLEID_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_TCP_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_UDP_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_UDP_TNL_CSUM_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_UDP_TNL_SEGMENTATION,
+	NM_ETHTOOL_ID_FEATURE_TX_VLAN_STAG_HW_INSERT,
+	_NM_ETHTOOL_ID_FEATURE_LAST = NM_ETHTOOL_ID_FEATURE_TX_VLAN_STAG_HW_INSERT,
+
+	_NM_ETHTOOL_ID_RING_FIRST = _NM_ETHTOOL_ID_FEATURE_LAST + 1,
+	NM_ETHTOOL_ID_RING_RX = _NM_ETHTOOL_ID_RING_FIRST,
+	NM_ETHTOOL_ID_RING_RX_JUMBO,
+	NM_ETHTOOL_ID_RING_RX_MINI,
+	NM_ETHTOOL_ID_RING_TX,
+	_NM_ETHTOOL_ID_RING_LAST = NM_ETHTOOL_ID_RING_TX,
+
+	_NM_ETHTOOL_ID_LAST = _NM_ETHTOOL_ID_RING_LAST,
+
+	_NM_ETHTOOL_ID_COALESCE_NUM = (_NM_ETHTOOL_ID_COALESCE_LAST - _NM_ETHTOOL_ID_COALESCE_FIRST + 1),
+	_NM_ETHTOOL_ID_FEATURE_NUM = (_NM_ETHTOOL_ID_FEATURE_LAST - _NM_ETHTOOL_ID_FEATURE_FIRST + 1),
+	_NM_ETHTOOL_ID_RING_NUM = (_NM_ETHTOOL_ID_RING_LAST - _NM_ETHTOOL_ID_RING_FIRST + 1),
+	_NM_ETHTOOL_ID_NUM = (_NM_ETHTOOL_ID_LAST - _NM_ETHTOOL_ID_FIRST + 1),
+} NMEthtoolID;
+
+#define _NM_ETHTOOL_ID_FEATURE_AS_IDX(ethtool_id)  ((ethtool_id) - _NM_ETHTOOL_ID_FEATURE_FIRST)
+#define _NM_ETHTOOL_ID_COALESCE_AS_IDX(ethtool_id) ((ethtool_id) - _NM_ETHTOOL_ID_COALESCE_FIRST)
+
+typedef enum {
+	NM_ETHTOOL_TYPE_UNKNOWN,
+	NM_ETHTOOL_TYPE_COALESCE,
+	NM_ETHTOOL_TYPE_FEATURE,
+	NM_ETHTOOL_TYPE_RING,
+} NMEthtoolType;
+
+typedef struct {
+	const char *optname;
+	NMEthtoolID id;
+} NMEthtoolData;
+
+extern const NMEthtoolData *const nm_ethtool_data[_NM_ETHTOOL_ID_NUM + 1];
+
+const NMEthtoolData *nm_ethtool_data_get_by_optname (const char *optname);
+
+NMEthtoolType nm_ethtool_id_to_type (NMEthtoolID id);
+
+/****************************************************************************/
+
+static inline NMEthtoolID
+nm_ethtool_id_get_by_name (const char *optname)
+{
+	const NMEthtoolData *d;
+
+	d = nm_ethtool_data_get_by_optname (optname);
+	return d ? d->id : NM_ETHTOOL_ID_UNKNOWN;
+}
+
+static inline gboolean
+nm_ethtool_id_is_feature (NMEthtoolID id)
+{
+	return id >= _NM_ETHTOOL_ID_FEATURE_FIRST && id <= _NM_ETHTOOL_ID_FEATURE_LAST;
+}
+
+static inline gboolean
+nm_ethtool_id_is_coalesce (NMEthtoolID id)
+{
+	return id >= _NM_ETHTOOL_ID_COALESCE_FIRST && id <= _NM_ETHTOOL_ID_COALESCE_LAST;
+}
+
+static inline gboolean
+nm_ethtool_id_is_ring (NMEthtoolID id)
+{
+	return id >= _NM_ETHTOOL_ID_RING_FIRST && id <= _NM_ETHTOOL_ID_RING_LAST;
+}
+
+/****************************************************************************/
+
+#endif /* __NM_ETHTOOL_UTILS_H__ */
diff --git a/libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.c b/libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.c
new file mode 100644
index 00000000..df2f2e77
--- /dev/null
+++ b/libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.c
@@ -0,0 +1,180 @@
+// SPDX-License-Identifier: LGPL-2.1+
+
+#include "nm-default.h"
+
+#include "nm-libnm-core-utils.h"
+
+#include "nm-common-macros.h"
+
+/*****************************************************************************/
+
+gboolean
+nm_utils_vlan_priority_map_parse_str (NMVlanPriorityMap map_type,
+                                      const char *str,
+                                      gboolean allow_wildcard_to,
+                                      guint32 *out_from,
+                                      guint32 *out_to,
+                                      gboolean *out_has_wildcard_to)
+{
+	const char *s2;
+	gint64 v1, v2;
+
+	nm_assert (str);
+
+	s2 = strchr (str, ':');
+
+	if (!s2) {
+		if (!allow_wildcard_to)
+			return FALSE;
+		v1 = _nm_utils_ascii_str_to_int64 (str, 10, 0, G_MAXUINT32, -1);
+		v2 = -1;
+	} else {
+		gs_free char *s1_free = NULL;
+		gsize s1_len = (s2 - str);
+
+		s2 = nm_str_skip_leading_spaces (&s2[1]);
+		if (   s2[0] == '\0'
+		    || (   s2[0] == '*'
+		        && NM_STRCHAR_ALL (&s2[1], ch, g_ascii_isspace (ch)))) {
+			if (!allow_wildcard_to)
+				return FALSE;
+			v2 = -1;
+		} else {
+			v2 = _nm_utils_ascii_str_to_int64 (s2, 10, 0, G_MAXUINT32, -1);
+			if (   v2 < 0
+			    || (guint32) v2 > nm_utils_vlan_priority_map_get_max_prio (map_type, FALSE))
+				return FALSE;
+		}
+
+		v1 = _nm_utils_ascii_str_to_int64 (nm_strndup_a (100, str, s1_len, &s1_free),
+		                                   10, 0, G_MAXUINT32, -1);
+	}
+
+	if (   v1 < 0
+	    || (guint32) v1 > nm_utils_vlan_priority_map_get_max_prio (map_type, TRUE))
+		return FALSE;
+
+	NM_SET_OUT (out_from, v1);
+	NM_SET_OUT (out_to,   v2 < 0
+	                    ? 0u
+	                    : (guint) v2);
+	NM_SET_OUT (out_has_wildcard_to, v2 < 0);
+	return TRUE;
+}
+
+/*****************************************************************************/
+
+const char *const nm_auth_permission_names_by_idx[NM_CLIENT_PERMISSION_LAST] = {
+	[NM_CLIENT_PERMISSION_CHECKPOINT_ROLLBACK - 1]               = NM_AUTH_PERMISSION_CHECKPOINT_ROLLBACK,
+	[NM_CLIENT_PERMISSION_ENABLE_DISABLE_CONNECTIVITY_CHECK - 1] = NM_AUTH_PERMISSION_ENABLE_DISABLE_CONNECTIVITY_CHECK,
+	[NM_CLIENT_PERMISSION_ENABLE_DISABLE_NETWORK - 1]            = NM_AUTH_PERMISSION_ENABLE_DISABLE_NETWORK,
+	[NM_CLIENT_PERMISSION_ENABLE_DISABLE_STATISTICS - 1]         = NM_AUTH_PERMISSION_ENABLE_DISABLE_STATISTICS,
+	[NM_CLIENT_PERMISSION_ENABLE_DISABLE_WIFI - 1]               = NM_AUTH_PERMISSION_ENABLE_DISABLE_WIFI,
+	[NM_CLIENT_PERMISSION_ENABLE_DISABLE_WIMAX - 1]              = NM_AUTH_PERMISSION_ENABLE_DISABLE_WIMAX,
+	[NM_CLIENT_PERMISSION_ENABLE_DISABLE_WWAN - 1]               = NM_AUTH_PERMISSION_ENABLE_DISABLE_WWAN,
+	[NM_CLIENT_PERMISSION_NETWORK_CONTROL - 1]                   = NM_AUTH_PERMISSION_NETWORK_CONTROL,
+	[NM_CLIENT_PERMISSION_RELOAD - 1]                            = NM_AUTH_PERMISSION_RELOAD,
+	[NM_CLIENT_PERMISSION_SETTINGS_MODIFY_GLOBAL_DNS - 1]        = NM_AUTH_PERMISSION_SETTINGS_MODIFY_GLOBAL_DNS,
+	[NM_CLIENT_PERMISSION_SETTINGS_MODIFY_HOSTNAME - 1]          = NM_AUTH_PERMISSION_SETTINGS_MODIFY_HOSTNAME,
+	[NM_CLIENT_PERMISSION_SETTINGS_MODIFY_OWN - 1]               = NM_AUTH_PERMISSION_SETTINGS_MODIFY_OWN,
+	[NM_CLIENT_PERMISSION_SETTINGS_MODIFY_SYSTEM - 1]            = NM_AUTH_PERMISSION_SETTINGS_MODIFY_SYSTEM,
+	[NM_CLIENT_PERMISSION_SLEEP_WAKE - 1]                        = NM_AUTH_PERMISSION_SLEEP_WAKE,
+	[NM_CLIENT_PERMISSION_WIFI_SCAN - 1]                         = NM_AUTH_PERMISSION_WIFI_SCAN,
+	[NM_CLIENT_PERMISSION_WIFI_SHARE_OPEN - 1]                   = NM_AUTH_PERMISSION_WIFI_SHARE_OPEN,
+	[NM_CLIENT_PERMISSION_WIFI_SHARE_PROTECTED - 1]              = NM_AUTH_PERMISSION_WIFI_SHARE_PROTECTED,
+};
+
+const NMClientPermission nm_auth_permission_sorted[NM_CLIENT_PERMISSION_LAST] = {
+	NM_CLIENT_PERMISSION_CHECKPOINT_ROLLBACK,
+	NM_CLIENT_PERMISSION_ENABLE_DISABLE_CONNECTIVITY_CHECK,
+	NM_CLIENT_PERMISSION_ENABLE_DISABLE_NETWORK,
+	NM_CLIENT_PERMISSION_ENABLE_DISABLE_STATISTICS,
+	NM_CLIENT_PERMISSION_ENABLE_DISABLE_WIFI,
+	NM_CLIENT_PERMISSION_ENABLE_DISABLE_WIMAX,
+	NM_CLIENT_PERMISSION_ENABLE_DISABLE_WWAN,
+	NM_CLIENT_PERMISSION_NETWORK_CONTROL,
+	NM_CLIENT_PERMISSION_RELOAD,
+	NM_CLIENT_PERMISSION_SETTINGS_MODIFY_GLOBAL_DNS,
+	NM_CLIENT_PERMISSION_SETTINGS_MODIFY_HOSTNAME,
+	NM_CLIENT_PERMISSION_SETTINGS_MODIFY_OWN,
+	NM_CLIENT_PERMISSION_SETTINGS_MODIFY_SYSTEM,
+	NM_CLIENT_PERMISSION_SLEEP_WAKE,
+	NM_CLIENT_PERMISSION_WIFI_SCAN,
+	NM_CLIENT_PERMISSION_WIFI_SHARE_OPEN,
+	NM_CLIENT_PERMISSION_WIFI_SHARE_PROTECTED,
+};
+
+const char *
+nm_auth_permission_to_string (NMClientPermission permission)
+{
+	if (permission < 1)
+		return NULL;
+	if (permission > NM_CLIENT_PERMISSION_LAST)
+		return NULL;
+	return nm_auth_permission_names_by_idx[permission - 1];
+}
+
+#define AUTH_PERMISSION_PREFIX "org.freedesktop.NetworkManager."
+
+static int
+_nm_auth_permission_from_string_cmp (gconstpointer a, gconstpointer b, gpointer user_data)
+{
+	const NMClientPermission *const p = a;
+	const char *const needle = b;
+	const char *ss = nm_auth_permission_names_by_idx[*p - 1];
+
+	nm_assert (NM_STR_HAS_PREFIX (ss, AUTH_PERMISSION_PREFIX));
+	nm_assert (ss[NM_STRLEN (AUTH_PERMISSION_PREFIX)] != '\0');
+
+	return strcmp (&ss[NM_STRLEN (AUTH_PERMISSION_PREFIX)], needle);
+}
+
+NMClientPermission
+nm_auth_permission_from_string (const char *str)
+{
+	gssize idx;
+
+	if (!str)
+		return NM_CLIENT_PERMISSION_NONE;
+
+	if (!NM_STR_HAS_PREFIX (str, AUTH_PERMISSION_PREFIX))
+		return NM_CLIENT_PERMISSION_NONE;
+	idx = nm_utils_array_find_binary_search (nm_auth_permission_sorted,
+	                                         sizeof (nm_auth_permission_sorted[0]),
+	                                         G_N_ELEMENTS (nm_auth_permission_sorted),
+	                                         &str[NM_STRLEN (AUTH_PERMISSION_PREFIX)],
+	                                         _nm_auth_permission_from_string_cmp,
+	                                         NULL);
+	if (idx < 0)
+		return NM_CLIENT_PERMISSION_NONE;
+	return nm_auth_permission_sorted[idx];
+}
+
+/*****************************************************************************/
+
+NMClientPermissionResult
+nm_client_permission_result_from_string (const char *nm)
+{
+	if (!nm)
+		return NM_CLIENT_PERMISSION_RESULT_UNKNOWN;
+	if (nm_streq (nm, "yes"))
+		return NM_CLIENT_PERMISSION_RESULT_YES;
+	if (nm_streq (nm, "no"))
+		return NM_CLIENT_PERMISSION_RESULT_NO;
+	if (nm_streq (nm, "auth"))
+		return NM_CLIENT_PERMISSION_RESULT_AUTH;
+	return NM_CLIENT_PERMISSION_RESULT_UNKNOWN;
+}
+
+const char *
+nm_client_permission_result_to_string (NMClientPermissionResult permission)
+{
+	switch (permission) {
+	case NM_CLIENT_PERMISSION_RESULT_YES:     return "yes";
+	case NM_CLIENT_PERMISSION_RESULT_NO:      return "no";
+	case NM_CLIENT_PERMISSION_RESULT_AUTH:    return "auth";
+	case NM_CLIENT_PERMISSION_RESULT_UNKNOWN: return "unknown";
+	}
+	nm_assert_not_reached ();
+	return NULL;
+}
diff --git a/libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.h b/libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.h
new file mode 100644
index 00000000..a35a5e15
--- /dev/null
+++ b/libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.h
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: LGPL-2.1+
+
+#ifndef __NM_LIBNM_SHARED_UTILS_H__
+#define __NM_LIBNM_SHARED_UTILS_H__
+
+/****************************************************************************/
+
+#include "nm-setting-bridge.h"
+#include "nm-setting-connection.h"
+#include "nm-setting-ip-config.h"
+#include "nm-setting-ip4-config.h"
+#include "nm-setting-ip6-config.h"
+#include "nm-setting-sriov.h"
+#include "nm-setting-team.h"
+#include "nm-setting-vlan.h"
+#include "nm-setting-wireguard.h"
+
+/****************************************************************************/
+
+#define nm_auto_unref_ip_address nm_auto (_nm_ip_address_unref)
+NM_AUTO_DEFINE_FCN0 (NMIPAddress *, _nm_ip_address_unref, nm_ip_address_unref)
+
+#define nm_auto_unref_ip_route nm_auto (_nm_auto_unref_ip_route)
+NM_AUTO_DEFINE_FCN0 (NMIPRoute *, _nm_auto_unref_ip_route, nm_ip_route_unref)
+
+#define nm_auto_unref_ip_routing_rule nm_auto(_nm_auto_unref_ip_routing_rule)
+NM_AUTO_DEFINE_FCN0 (NMIPRoutingRule *, _nm_auto_unref_ip_routing_rule, nm_ip_routing_rule_unref)
+
+#define nm_auto_unref_sriov_vf nm_auto (_nm_auto_unref_sriov_vf)
+NM_AUTO_DEFINE_FCN0 (NMSriovVF *, _nm_auto_unref_sriov_vf, nm_sriov_vf_unref)
+
+#define nm_auto_unref_tc_qdisc nm_auto (_nm_auto_unref_tc_qdisc)
+NM_AUTO_DEFINE_FCN0 (NMTCQdisc *, _nm_auto_unref_tc_qdisc, nm_tc_qdisc_unref)
+
+#define nm_auto_unref_tc_tfilter nm_auto (_nm_auto_unref_tc_tfilter)
+NM_AUTO_DEFINE_FCN0 (NMTCTfilter *, _nm_auto_unref_tc_tfilter, nm_tc_tfilter_unref)
+
+#define nm_auto_unref_bridge_vlan nm_auto (_nm_auto_unref_bridge_vlan)
+NM_AUTO_DEFINE_FCN0 (NMBridgeVlan *, _nm_auto_unref_bridge_vlan, nm_bridge_vlan_unref)
+
+#define nm_auto_unref_team_link_watcher nm_auto (_nm_auto_unref_team_link_watcher)
+NM_AUTO_DEFINE_FCN0 (NMTeamLinkWatcher *, _nm_auto_unref_team_link_watcher, nm_team_link_watcher_unref)
+
+#define nm_auto_unref_wgpeer nm_auto (_nm_auto_unref_wgpeer)
+NM_AUTO_DEFINE_FCN0 (NMWireGuardPeer *, _nm_auto_unref_wgpeer, nm_wireguard_peer_unref)
+
+/****************************************************************************/
+
+static inline guint32
+nm_utils_vlan_priority_map_get_max_prio (NMVlanPriorityMap map, gboolean from)
+{
+	if (map == NM_VLAN_INGRESS_MAP) {
+		return   from
+		       ? 7u /* MAX_8021P_PRIO */
+		       : (guint32) G_MAXUINT32 /* MAX_SKB_PRIO */;
+	}
+	nm_assert (map == NM_VLAN_EGRESS_MAP);
+	return   from
+	       ? (guint32) G_MAXUINT32 /* MAX_SKB_PRIO */
+	       : 7u /* MAX_8021P_PRIO */;
+}
+
+gboolean nm_utils_vlan_priority_map_parse_str (NMVlanPriorityMap map_type,
+                                               const char *str,
+                                               gboolean allow_wildcard_to,
+                                               guint32 *out_from,
+                                               guint32 *out_to,
+                                               gboolean *out_has_wildcard_to);
+
+/*****************************************************************************/
+
+static inline int
+nm_setting_ip_config_get_addr_family (NMSettingIPConfig *s_ip)
+{
+	if (NM_IS_SETTING_IP4_CONFIG (s_ip))
+		return AF_INET;
+	if (NM_IS_SETTING_IP6_CONFIG (s_ip))
+		return AF_INET6;
+	g_return_val_if_reached (AF_UNSPEC);
+}
+
+/*****************************************************************************/
+
+/* The maximum MTU for infiniband.
+ *
+ * This is both in transport-mode "datagram" and "connected"
+ * and they both have the same maximum define.
+ *
+ * Note that in the past, MTU in "datagram" mode was restricted
+ * to 2044 bytes. That is no longer the case and we accept large
+ * MTUs.
+ *
+ * This define is the maxiumum for the MTU in a connection profile (the
+ * setting). Whether large MTUs can be configured later (at activation time)
+ * depends on other factors. */
+#define NM_INFINIBAND_MAX_MTU ((guint) 65520)
+
+/*****************************************************************************/
+
+#define _NM_CAPABILITY_MAX NM_CAPABILITY_OVS
+
+/*****************************************************************************/
+
+extern const char *const nm_auth_permission_names_by_idx[NM_CLIENT_PERMISSION_LAST];
+extern const NMClientPermission nm_auth_permission_sorted[NM_CLIENT_PERMISSION_LAST];
+
+const char *nm_auth_permission_to_string (NMClientPermission permission);
+NMClientPermission nm_auth_permission_from_string (const char *str);
+
+/*****************************************************************************/
+
+NMClientPermissionResult nm_client_permission_result_from_string (const char *nm);
+const char *nm_client_permission_result_to_string (NMClientPermissionResult permission);
+
+#endif /* __NM_LIBNM_SHARED_UTILS_H__ */
diff --git a/libnm-core/nm-setting-bridge-port.c b/libnm-core/nm-setting-bridge-port.c
index 5fcf1e60..92ac28ec 100644
--- a/libnm-core/nm-setting-bridge-port.c
+++ b/libnm-core/nm-setting-bridge-port.c
@@ -236,18 +236,20 @@ nm_setting_bridge_port_remove_vlan_by_vid (NMSettingBridgePort *setting,
                                            guint16 vid_end)
 {
 	NMSettingBridgePortPrivate *priv;
-	guint16 v_start, v_end;
-	NMBridgeVlan *vlan;
 	guint i;
 
 	if (vid_end == 0)
 		vid_end = vid_start;
 
 	g_return_val_if_fail (NM_IS_SETTING_BRIDGE_PORT (setting), FALSE);
+
 	priv = NM_SETTING_BRIDGE_PORT_GET_PRIVATE (setting);
 
 	for (i = 0; i < priv->vlans->len; i++) {
-		vlan = (NMBridgeVlan *) priv->vlans->pdata[i];
+		NMBridgeVlan *vlan = priv->vlans->pdata[i];
+		guint16 v_start = 0;
+		guint16 v_end = 0;
+
 		nm_bridge_vlan_get_vid_range (vlan, &v_start, &v_end);
 		if (v_start == vid_start && v_end == vid_end) {
 			g_ptr_array_remove_index (priv->vlans, i);
@@ -440,8 +442,8 @@ nm_setting_bridge_port_init (NMSettingBridgePort *setting)
 
 	priv->vlans = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_bridge_vlan_unref);
 
-	priv->priority = NM_BR_PORT_DEF_PRIORITY;
-	priv->path_cost = NM_BR_PORT_DEF_PATH_COST;
+	priv->priority = NM_BRIDGE_PORT_PRIORITY_DEF;
+	priv->path_cost = NM_BRIDGE_PORT_PATH_COST_DEF;
 }
 
 /**
@@ -498,7 +500,7 @@ nm_setting_bridge_port_class_init (NMSettingBridgePortClass *klass)
 	 */
 	obj_properties[PROP_PRIORITY] =
 	    g_param_spec_uint (NM_SETTING_BRIDGE_PORT_PRIORITY, "", "",
-	                       0, NM_BR_PORT_MAX_PRIORITY, NM_BR_PORT_DEF_PRIORITY,
+	                       NM_BRIDGE_PORT_PRIORITY_MIN, NM_BRIDGE_PORT_PRIORITY_MAX, NM_BRIDGE_PORT_PRIORITY_DEF,
 	                       G_PARAM_READWRITE |
 	                       NM_SETTING_PARAM_INFERRABLE |
 	                       G_PARAM_STATIC_STRINGS);
@@ -519,7 +521,7 @@ nm_setting_bridge_port_class_init (NMSettingBridgePortClass *klass)
 	 */
 	obj_properties[PROP_PATH_COST] =
 	    g_param_spec_uint (NM_SETTING_BRIDGE_PORT_PATH_COST, "", "",
-	                       0, NM_BR_PORT_MAX_PATH_COST, NM_BR_PORT_DEF_PATH_COST,
+	                       NM_BRIDGE_PORT_PATH_COST_MIN, NM_BRIDGE_PORT_PATH_COST_MAX, NM_BRIDGE_PORT_PATH_COST_DEF,
 	                       G_PARAM_READWRITE |
 	                       G_PARAM_STATIC_STRINGS);
 
diff --git a/libnm-core/nm-setting-bridge.c b/libnm-core/nm-setting-bridge.c
index 957520f4..2ead2739 100644
--- a/libnm-core/nm-setting-bridge.c
+++ b/libnm-core/nm-setting-bridge.c
@@ -22,18 +22,6 @@
  * necessary for bridging connections.
  **/
 
-#define BRIDGE_AGEING_TIME_DEFAULT                300
-#define BRIDGE_FORWARD_DELAY_DEFAULT              15
-#define BRIDGE_HELLO_TIME_DEFAULT                 2
-#define BRIDGE_MAX_AGE_DEFAULT                    20
-#define BRIDGE_MULTICAST_QUERIER_DEFAULT          FALSE
-#define BRIDGE_MULTICAST_QUERY_USE_IFADDR_DEFAULT FALSE
-#define BRIDGE_MULTICAST_SNOOPING_DEFAULT         TRUE
-#define BRIDGE_PRIORITY_DEFAULT                   0x8000
-#define BRIDGE_STP_DEFAULT                        TRUE
-#define BRIDGE_VLAN_DEFAULT_PVID_DEFAULT          1
-#define BRIDGE_VLAN_STATS_ENABLED_DEFAULT         FALSE
-
 /*****************************************************************************/
 
 NM_GOBJECT_PROPERTIES_DEFINE (NMSettingBridge,
@@ -46,10 +34,19 @@ NM_GOBJECT_PROPERTIES_DEFINE (NMSettingBridge,
 	PROP_AGEING_TIME,
 	PROP_GROUP_ADDRESS,
 	PROP_GROUP_FORWARD_MASK,
+	PROP_MULTICAST_HASH_MAX,
+	PROP_MULTICAST_LAST_MEMBER_COUNT,
+	PROP_MULTICAST_LAST_MEMBER_INTERVAL,
+	PROP_MULTICAST_MEMBERSHIP_INTERVAL,
 	PROP_MULTICAST_ROUTER,
 	PROP_MULTICAST_QUERIER,
+	PROP_MULTICAST_QUERIER_INTERVAL,
+	PROP_MULTICAST_QUERY_INTERVAL,
+	PROP_MULTICAST_QUERY_RESPONSE_INTERVAL,
 	PROP_MULTICAST_QUERY_USE_IFADDR,
 	PROP_MULTICAST_SNOOPING,
+	PROP_MULTICAST_STARTUP_QUERY_COUNT,
+	PROP_MULTICAST_STARTUP_QUERY_INTERVAL,
 	PROP_VLAN_FILTERING,
 	PROP_VLAN_DEFAULT_PVID,
 	PROP_VLAN_PROTOCOL,
@@ -63,7 +60,16 @@ typedef struct {
 	char *   multicast_router;
 	char *   group_address;
 	char *   vlan_protocol;
+	guint64  multicast_last_member_interval;
+	guint64  multicast_membership_interval;
+	guint64  multicast_querier_interval;
+	guint64  multicast_query_interval;
+	guint64  multicast_query_response_interval;
+	guint64  multicast_startup_query_interval;
 	guint32  ageing_time;
+	guint32  multicast_hash_max;
+	guint32  multicast_last_member_count;
+	guint32  multicast_startup_query_count;
 	guint16  priority;
 	guint16  forward_delay;
 	guint16  hello_time;
@@ -1010,6 +1016,150 @@ nm_setting_bridge_get_multicast_querier (const NMSettingBridge *setting)
 	return NM_SETTING_BRIDGE_GET_PRIVATE (setting)->multicast_querier;
 }
 
+/**
+ * nm_setting_bridge_get_multicast_hash_max:
+ * @setting: the #NMSettingBridge
+ *
+ * Returns: the #NMSettingBridge:multicast-hash-max property of the setting
+ *
+ * Since 1.26
+ **/
+guint32
+nm_setting_bridge_get_multicast_hash_max (const NMSettingBridge *setting)
+{
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE (setting), 0);
+
+	return NM_SETTING_BRIDGE_GET_PRIVATE (setting)->multicast_hash_max;
+}
+
+/**
+ * nm_setting_bridge_get_multicast_last_member_count:
+ * @setting: the #NMSettingBridge
+ *
+ * Returns: the #NMSettingBridge:multicast-last-member-count property of the setting
+ *
+ * Since 1.26
+ **/
+guint32
+nm_setting_bridge_get_multicast_last_member_count (const NMSettingBridge *setting)
+{
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE (setting), 0);
+
+	return NM_SETTING_BRIDGE_GET_PRIVATE (setting)->multicast_last_member_count;
+}
+
+/**
+ * nm_setting_bridge_get_multicast_last_member_interval:
+ * @setting: the #NMSettingBridge
+ *
+ * Returns: the #NMSettingBridge:multicast-last-member-interval property of the setting
+ *
+ * Since 1.26
+ **/
+guint64
+nm_setting_bridge_get_multicast_last_member_interval (const NMSettingBridge *setting)
+{
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE (setting), 0);
+
+	return NM_SETTING_BRIDGE_GET_PRIVATE (setting)->multicast_last_member_interval;
+}
+
+/**
+ * nm_setting_bridge_get_multicast_membership_interval:
+ * @setting: the #NMSettingBridge
+ *
+ * Returns: the #NMSettingBridge:multicast-membership-interval property of the setting
+ *
+ * Since 1.26
+ **/
+guint64
+nm_setting_bridge_get_multicast_membership_interval (const NMSettingBridge *setting)
+{
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE (setting), 0);
+
+	return NM_SETTING_BRIDGE_GET_PRIVATE (setting)->multicast_membership_interval;
+}
+
+/**
+ * nm_setting_bridge_get_multicast_querier_interval:
+ * @setting: the #NMSettingBridge
+ *
+ * Returns: the #NMSettingBridge:multicast-querier-interval property of the setting
+ *
+ * Since 1.26
+ **/
+guint64
+nm_setting_bridge_get_multicast_querier_interval (const NMSettingBridge *setting)
+{
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE (setting), 0);
+
+	return NM_SETTING_BRIDGE_GET_PRIVATE (setting)->multicast_querier_interval;
+}
+
+/**
+ * nm_setting_bridge_get_multicast_query_interval:
+ * @setting: the #NMSettingBridge
+ *
+ * Returns: the #NMSettingBridge:multicast-query-interval property of the setting
+ *
+ * Since 1.26
+ **/
+guint64
+nm_setting_bridge_get_multicast_query_interval (const NMSettingBridge *setting)
+{
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE (setting), 0);
+
+	return NM_SETTING_BRIDGE_GET_PRIVATE (setting)->multicast_query_interval;
+}
+
+/**
+ * nm_setting_bridge_get_multicast_query_response_interval:
+ * @setting: the #NMSettingBridge
+ *
+ * Returns: the #NMSettingBridge:multicast-query-response-interval property of the setting
+ *
+ * Since 1.26
+ **/
+guint64
+nm_setting_bridge_get_multicast_query_response_interval (const NMSettingBridge *setting)
+{
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE (setting), 0);
+
+	return NM_SETTING_BRIDGE_GET_PRIVATE (setting)->multicast_query_response_interval;
+}
+
+/**
+ * nm_setting_bridge_get_multicast_startup_query_count:
+ * @setting: the #NMSettingBridge
+ *
+ * Returns: the #NMSettingBridge:multicast-query-response-interval property of the setting
+ *
+ * Since 1.26
+ **/
+guint32
+nm_setting_bridge_get_multicast_startup_query_count (const NMSettingBridge *setting)
+{
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE (setting), 0);
+
+	return NM_SETTING_BRIDGE_GET_PRIVATE (setting)->multicast_startup_query_count;
+}
+
+/**
+ * nm_setting_bridge_get_multicast_startup_query_interval:
+ * @setting: the #NMSettingBridge
+ *
+ * Returns: the #NMSettingBridge:multicast-startup-query-interval property of the setting
+ *
+ * Since 1.26
+ **/
+guint64
+nm_setting_bridge_get_multicast_startup_query_interval (const NMSettingBridge *setting)
+{
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE (setting), 0);
+
+	return NM_SETTING_BRIDGE_GET_PRIVATE (setting)->multicast_startup_query_interval;
+}
+
 /*****************************************************************************/
 
 static gboolean
@@ -1050,32 +1200,32 @@ verify (NMSetting *setting, NMConnection *connection, GError **error)
 	}
 
 	if (!check_range (priv->forward_delay,
-	                  NM_BR_MIN_FORWARD_DELAY,
-	                  NM_BR_MAX_FORWARD_DELAY,
+	                  NM_BRIDGE_FORWARD_DELAY_MIN,
+	                  NM_BRIDGE_FORWARD_DELAY_MAX,
 	                  !priv->stp,
 	                  NM_SETTING_BRIDGE_FORWARD_DELAY,
 	                  error))
 		return FALSE;
 
 	if (!check_range (priv->hello_time,
-	                  NM_BR_MIN_HELLO_TIME,
-	                  NM_BR_MAX_HELLO_TIME,
+	                  NM_BRIDGE_HELLO_TIME_MIN,
+	                  NM_BRIDGE_HELLO_TIME_MAX,
 	                  !priv->stp,
 	                  NM_SETTING_BRIDGE_HELLO_TIME,
 	                  error))
 		return FALSE;
 
 	if (!check_range (priv->max_age,
-	                  NM_BR_MIN_MAX_AGE,
-	                  NM_BR_MAX_MAX_AGE,
+	                  NM_BRIDGE_MAX_AGE_MIN,
+	                  NM_BRIDGE_MAX_AGE_MAX,
 	                  !priv->stp,
 	                  NM_SETTING_BRIDGE_MAX_AGE,
 	                  error))
 		return FALSE;
 
 	if (!check_range (priv->ageing_time,
-	                  NM_BR_MIN_AGEING_TIME,
-	                  NM_BR_MAX_AGEING_TIME,
+	                  NM_BRIDGE_AGEING_TIME_MIN,
+	                  NM_BRIDGE_AGEING_TIME_MAX,
 	                  !priv->stp,
 	                  NM_SETTING_BRIDGE_AGEING_TIME,
 	                  error))
@@ -1122,31 +1272,27 @@ verify (NMSetting *setting, NMConnection *connection, GError **error)
 		return FALSE;
 	}
 
-	if (priv->multicast_router) {
-		if (!NM_IN_STRSET (priv->multicast_router,
-		                   "auto",
-		                   "enabled",
-		                   "disabled")) {
-			g_set_error_literal (error,
-			                     NM_CONNECTION_ERROR,
-			                     NM_CONNECTION_ERROR_INVALID_PROPERTY,
-			                     _("is not a valid option"));
-			g_prefix_error (error, "%s.%s: ", NM_SETTING_BRIDGE_SETTING_NAME, NM_SETTING_BRIDGE_MULTICAST_ROUTER);
-			return FALSE;
-		}
+	if (!NM_IN_STRSET (priv->multicast_router,
+	                   NULL,
+	                   "auto",
+	                   "enabled",
+	                   "disabled")) {
+		g_set_error_literal (error,
+		                     NM_CONNECTION_ERROR,
+		                     NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		                     _("is not a valid option"));
+		g_prefix_error (error, "%s.%s: ", NM_SETTING_BRIDGE_SETTING_NAME, NM_SETTING_BRIDGE_MULTICAST_ROUTER);
+		return FALSE;
+	}
 
-		if (   NM_IN_STRSET (priv->multicast_router,
-		                     "auto",
-		                     "enabled")
-		    && !priv->multicast_snooping) {
-			g_set_error (error,
-			             NM_CONNECTION_ERROR,
-			             NM_CONNECTION_ERROR_INVALID_PROPERTY,
-			             _("'%s' option requires '%s' option to be enabled"),
-			             NM_SETTING_BRIDGE_MULTICAST_ROUTER, NM_SETTING_BRIDGE_MULTICAST_SNOOPING);
-			g_prefix_error (error, "%s.%s: ", NM_SETTING_BRIDGE_SETTING_NAME, NM_SETTING_BRIDGE_MULTICAST_ROUTER);
-			return FALSE;
-		}
+	if (!nm_utils_is_power_of_two (priv->multicast_hash_max)) {
+		g_set_error (error,
+		             NM_CONNECTION_ERROR,
+		             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		             _("'%s' option must be a power of 2"),
+		             NM_SETTING_BRIDGE_MULTICAST_HASH_MAX);
+		g_prefix_error (error, "%s.%s: ", NM_SETTING_BRIDGE_SETTING_NAME, NM_SETTING_BRIDGE_MULTICAST_HASH_MAX);
+		return FALSE;
 	}
 
 	/* Failures from here on are NORMALIZABLE... */
@@ -1235,6 +1381,18 @@ get_property (GObject *object, guint prop_id,
 	case PROP_GROUP_FORWARD_MASK:
 		g_value_set_uint (value, priv->group_forward_mask);
 		break;
+	case PROP_MULTICAST_HASH_MAX:
+		g_value_set_uint (value, priv->multicast_hash_max);
+		break;
+	case PROP_MULTICAST_LAST_MEMBER_COUNT:
+		g_value_set_uint (value, priv->multicast_last_member_count);
+		break;
+	case PROP_MULTICAST_LAST_MEMBER_INTERVAL:
+		g_value_set_uint64 (value, priv->multicast_last_member_interval);
+		break;
+	case PROP_MULTICAST_MEMBERSHIP_INTERVAL:
+		g_value_set_uint64 (value, priv->multicast_membership_interval);
+		break;
 	case PROP_MULTICAST_SNOOPING:
 		g_value_set_boolean (value, priv->multicast_snooping);
 		break;
@@ -1244,9 +1402,24 @@ get_property (GObject *object, guint prop_id,
 	case PROP_MULTICAST_QUERIER:
 		g_value_set_boolean (value, priv->multicast_querier);
 		break;
+	case PROP_MULTICAST_QUERIER_INTERVAL:
+		g_value_set_uint64 (value, priv->multicast_querier_interval);
+		break;
+	case PROP_MULTICAST_QUERY_INTERVAL:
+		g_value_set_uint64 (value, priv->multicast_query_interval);
+		break;
+	case PROP_MULTICAST_QUERY_RESPONSE_INTERVAL:
+		g_value_set_uint64 (value, priv->multicast_query_response_interval);
+		break;
 	case PROP_MULTICAST_QUERY_USE_IFADDR:
 		g_value_set_boolean (value, priv->multicast_query_use_ifaddr);
 		break;
+	case PROP_MULTICAST_STARTUP_QUERY_COUNT:
+		g_value_set_uint (value, priv->multicast_startup_query_count);
+		break;
+	case PROP_MULTICAST_STARTUP_QUERY_INTERVAL:
+		g_value_set_uint64 (value, priv->multicast_startup_query_interval);
+		break;
 	case PROP_VLAN_FILTERING:
 		g_value_set_boolean (value, priv->vlan_filtering);
 		break;
@@ -1308,6 +1481,18 @@ set_property (GObject *object, guint prop_id,
 	case PROP_GROUP_FORWARD_MASK:
 		priv->group_forward_mask = (guint16) g_value_get_uint (value);
 		break;
+	case PROP_MULTICAST_HASH_MAX:
+		priv->multicast_hash_max = g_value_get_uint (value);
+		break;
+	case PROP_MULTICAST_LAST_MEMBER_COUNT:
+		priv->multicast_last_member_count = g_value_get_uint (value);
+		break;
+	case PROP_MULTICAST_LAST_MEMBER_INTERVAL:
+		priv->multicast_last_member_interval = g_value_get_uint64 (value);
+		break;
+	case PROP_MULTICAST_MEMBERSHIP_INTERVAL:
+		priv->multicast_membership_interval = g_value_get_uint64 (value);
+		break;
 	case PROP_MULTICAST_SNOOPING:
 		priv->multicast_snooping = g_value_get_boolean (value);
 		break;
@@ -1318,9 +1503,24 @@ set_property (GObject *object, guint prop_id,
 	case PROP_MULTICAST_QUERIER:
 		priv->multicast_querier = g_value_get_boolean (value);
 		break;
+	case PROP_MULTICAST_QUERIER_INTERVAL:
+		priv->multicast_querier_interval = g_value_get_uint64 (value);
+		break;
+	case PROP_MULTICAST_QUERY_INTERVAL:
+		priv->multicast_query_interval = g_value_get_uint64 (value);
+		break;
+	case PROP_MULTICAST_QUERY_RESPONSE_INTERVAL:
+		priv->multicast_query_response_interval = g_value_get_uint64 (value);
+		break;
 	case PROP_MULTICAST_QUERY_USE_IFADDR:
 		priv->multicast_query_use_ifaddr = g_value_get_boolean (value);
 		break;
+	case PROP_MULTICAST_STARTUP_QUERY_COUNT:
+		priv->multicast_startup_query_count = g_value_get_uint (value);
+		break;
+	case PROP_MULTICAST_STARTUP_QUERY_INTERVAL:
+		priv->multicast_startup_query_interval = g_value_get_uint64 (value);
+		break;
 	case PROP_VLAN_FILTERING:
 		priv->vlan_filtering = g_value_get_boolean (value);
 		break;
@@ -1355,17 +1555,27 @@ nm_setting_bridge_init (NMSettingBridge *setting)
 
 	priv->vlans = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_bridge_vlan_unref);
 
-	priv->ageing_time                = BRIDGE_AGEING_TIME_DEFAULT;
-	priv->forward_delay              = BRIDGE_FORWARD_DELAY_DEFAULT;
-	priv->hello_time                 = BRIDGE_HELLO_TIME_DEFAULT;
-	priv->max_age                    = BRIDGE_MAX_AGE_DEFAULT;
-	priv->multicast_snooping         = BRIDGE_MULTICAST_SNOOPING_DEFAULT;
-	priv->priority                   = BRIDGE_PRIORITY_DEFAULT;
-	priv->stp                        = BRIDGE_STP_DEFAULT;
-	priv->vlan_default_pvid          = BRIDGE_VLAN_DEFAULT_PVID_DEFAULT;
-	priv->vlan_stats_enabled         = BRIDGE_VLAN_STATS_ENABLED_DEFAULT;
-	priv->multicast_query_use_ifaddr = BRIDGE_MULTICAST_QUERY_USE_IFADDR_DEFAULT;
-	priv->multicast_querier          = BRIDGE_MULTICAST_QUERIER_DEFAULT;
+	priv->ageing_time                       = NM_BRIDGE_AGEING_TIME_DEF;
+	priv->forward_delay                     = NM_BRIDGE_FORWARD_DELAY_DEF;
+	priv->hello_time                        = NM_BRIDGE_HELLO_TIME_DEF;
+	priv->max_age                           = NM_BRIDGE_MAX_AGE_DEF;
+	priv->multicast_last_member_count       = NM_BRIDGE_MULTICAST_LAST_MEMBER_COUNT_DEF;
+	priv->multicast_last_member_interval    = NM_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL_DEF;
+	priv->multicast_membership_interval     = NM_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL_DEF;
+	priv->multicast_hash_max                = NM_BRIDGE_MULTICAST_HASH_MAX_DEF;
+	priv->multicast_snooping                = NM_BRIDGE_MULTICAST_SNOOPING_DEF;
+	priv->priority                          = NM_BRIDGE_PRIORITY_DEF;
+	priv->stp                               = NM_BRIDGE_STP_DEF;
+	priv->vlan_default_pvid                 = NM_BRIDGE_VLAN_DEFAULT_PVID_DEF;
+	priv->multicast_query_interval          = NM_BRIDGE_MULTICAST_QUERY_INTERVAL_DEF;
+	priv->multicast_query_response_interval = NM_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL_DEF;
+	priv->multicast_querier_interval        = NM_BRIDGE_MULTICAST_QUERIER_INTERVAL_DEF;
+	priv->multicast_startup_query_count     = NM_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT_DEF;
+	priv->multicast_startup_query_interval  = NM_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL_DEF;
+
+	nm_assert (priv->multicast_querier          == NM_BRIDGE_MULTICAST_QUERIER_DEF);
+	nm_assert (priv->multicast_query_use_ifaddr == NM_BRIDGE_MULTICAST_QUERY_USE_IFADDR_DEF);
+	nm_assert (priv->vlan_stats_enabled         == NM_BRIDGE_VLAN_STATS_ENABLED_DEF);
 }
 
 /**
@@ -1461,7 +1671,7 @@ nm_setting_bridge_class_init (NMSettingBridgeClass *klass)
 	 */
 	obj_properties[PROP_STP] =
 	    g_param_spec_boolean (NM_SETTING_BRIDGE_STP, "", "",
-	                          BRIDGE_STP_DEFAULT,
+	                          NM_BRIDGE_STP_DEF,
 	                          G_PARAM_READWRITE |
 	                          NM_SETTING_PARAM_INFERRABLE |
 	                          G_PARAM_STATIC_STRINGS);
@@ -1483,7 +1693,7 @@ nm_setting_bridge_class_init (NMSettingBridgeClass *klass)
 	 */
 	obj_properties[PROP_PRIORITY] =
 	    g_param_spec_uint (NM_SETTING_BRIDGE_PRIORITY, "", "",
-	                       0, G_MAXUINT16, BRIDGE_PRIORITY_DEFAULT,
+	                       NM_BRIDGE_PRIORITY_MIN, NM_BRIDGE_PRIORITY_MAX, NM_BRIDGE_PRIORITY_DEF,
 	                       G_PARAM_READWRITE |
 	                       NM_SETTING_PARAM_INFERRABLE |
 	                       G_PARAM_STATIC_STRINGS);
@@ -1503,7 +1713,7 @@ nm_setting_bridge_class_init (NMSettingBridgeClass *klass)
 	 */
 	obj_properties[PROP_FORWARD_DELAY] =
 	    g_param_spec_uint (NM_SETTING_BRIDGE_FORWARD_DELAY, "", "",
-	                       0, NM_BR_MAX_FORWARD_DELAY, BRIDGE_FORWARD_DELAY_DEFAULT,
+	                       0, NM_BRIDGE_FORWARD_DELAY_MAX, NM_BRIDGE_FORWARD_DELAY_DEF,
 	                       G_PARAM_READWRITE |
 	                       NM_SETTING_PARAM_INFERRABLE |
 	                       G_PARAM_STATIC_STRINGS);
@@ -1523,7 +1733,7 @@ nm_setting_bridge_class_init (NMSettingBridgeClass *klass)
 	 */
 	obj_properties[PROP_HELLO_TIME] =
 	    g_param_spec_uint (NM_SETTING_BRIDGE_HELLO_TIME, "", "",
-	                       0, NM_BR_MAX_HELLO_TIME, BRIDGE_HELLO_TIME_DEFAULT,
+	                       0, NM_BRIDGE_HELLO_TIME_MAX, NM_BRIDGE_HELLO_TIME_DEF,
 	                       G_PARAM_READWRITE |
 	                       NM_SETTING_PARAM_INFERRABLE |
 	                       G_PARAM_STATIC_STRINGS);
@@ -1543,7 +1753,7 @@ nm_setting_bridge_class_init (NMSettingBridgeClass *klass)
 	 */
 	obj_properties[PROP_MAX_AGE] =
 	    g_param_spec_uint (NM_SETTING_BRIDGE_MAX_AGE, "", "",
-	                       0, NM_BR_MAX_MAX_AGE, BRIDGE_MAX_AGE_DEFAULT,
+	                       0, NM_BRIDGE_MAX_AGE_MAX, NM_BRIDGE_MAX_AGE_DEF,
 	                       G_PARAM_READWRITE |
 	                       NM_SETTING_PARAM_INFERRABLE |
 	                       G_PARAM_STATIC_STRINGS);
@@ -1563,7 +1773,7 @@ nm_setting_bridge_class_init (NMSettingBridgeClass *klass)
 	 */
 	obj_properties[PROP_AGEING_TIME] =
 	    g_param_spec_uint (NM_SETTING_BRIDGE_AGEING_TIME, "", "",
-	                       NM_BR_MIN_AGEING_TIME, NM_BR_MAX_AGEING_TIME, BRIDGE_AGEING_TIME_DEFAULT,
+	                       NM_BRIDGE_AGEING_TIME_MIN, NM_BRIDGE_AGEING_TIME_MAX, NM_BRIDGE_AGEING_TIME_DEF,
 	                       G_PARAM_READWRITE |
 	                       NM_SETTING_PARAM_INFERRABLE |
 	                       G_PARAM_STATIC_STRINGS);
@@ -1607,7 +1817,7 @@ nm_setting_bridge_class_init (NMSettingBridgeClass *klass)
 	 */
 	obj_properties[PROP_MULTICAST_SNOOPING] =
 	    g_param_spec_boolean (NM_SETTING_BRIDGE_MULTICAST_SNOOPING, "", "",
-	                          BRIDGE_MULTICAST_SNOOPING_DEFAULT,
+	                          NM_BRIDGE_MULTICAST_SNOOPING_DEF,
 	                          G_PARAM_READWRITE |
 	                          NM_SETTING_PARAM_INFERRABLE |
 	                          G_PARAM_STATIC_STRINGS);
@@ -1652,7 +1862,7 @@ nm_setting_bridge_class_init (NMSettingBridgeClass *klass)
 	 */
 	obj_properties[PROP_VLAN_DEFAULT_PVID] =
 	    g_param_spec_uint (NM_SETTING_BRIDGE_VLAN_DEFAULT_PVID, "", "",
-	                       0, NM_BRIDGE_VLAN_VID_MAX, BRIDGE_VLAN_DEFAULT_PVID_DEFAULT,
+	                       0, NM_BRIDGE_VLAN_VID_MAX, NM_BRIDGE_VLAN_DEFAULT_PVID_DEF,
 	                       G_PARAM_READWRITE |
 	                       NM_SETTING_PARAM_INFERRABLE |
 	                       G_PARAM_STATIC_STRINGS);
@@ -1767,7 +1977,7 @@ nm_setting_bridge_class_init (NMSettingBridgeClass *klass)
 	 */
 	obj_properties[PROP_VLAN_STATS_ENABLED] =
 	    g_param_spec_boolean (NM_SETTING_BRIDGE_VLAN_STATS_ENABLED, "", "",
-	                          BRIDGE_VLAN_STATS_ENABLED_DEFAULT,
+	                          NM_BRIDGE_VLAN_STATS_ENABLED_DEF,
 	                          G_PARAM_READWRITE |
 	                          NM_SETTING_PARAM_INFERRABLE |
 	                          G_PARAM_STATIC_STRINGS);
@@ -1775,8 +1985,8 @@ nm_setting_bridge_class_init (NMSettingBridgeClass *klass)
 	/**
 	 * NMSettingBridge:multicast-router:
 	 *
-	 * Sets bridge's multicast router.
-	 * multicast-snooping must be enabled for this option to work.
+	 * 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'.
@@ -1816,7 +2026,7 @@ nm_setting_bridge_class_init (NMSettingBridgeClass *klass)
 	 */
 	obj_properties[PROP_MULTICAST_QUERY_USE_IFADDR] =
 	    g_param_spec_boolean (NM_SETTING_BRIDGE_MULTICAST_QUERY_USE_IFADDR, "", "",
-	                          BRIDGE_MULTICAST_QUERY_USE_IFADDR_DEFAULT,
+	                          NM_BRIDGE_MULTICAST_QUERY_USE_IFADDR_DEF,
 	                          G_PARAM_READWRITE |
 	                          NM_SETTING_PARAM_INFERRABLE |
 	                          G_PARAM_STATIC_STRINGS);
@@ -1838,11 +2048,209 @@ nm_setting_bridge_class_init (NMSettingBridgeClass *klass)
 	 */
 	obj_properties[PROP_MULTICAST_QUERIER] =
 	    g_param_spec_boolean (NM_SETTING_BRIDGE_MULTICAST_QUERIER, "", "",
-	                          BRIDGE_MULTICAST_QUERIER_DEFAULT,
+	                          NM_BRIDGE_MULTICAST_QUERIER_DEF,
 	                          G_PARAM_READWRITE |
 	                          NM_SETTING_PARAM_INFERRABLE |
 	                          G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * NMSettingBridge:multicast-hash-max:
+	 *
+	 * Set maximum size of multicast hash table (value must be a power of 2).
+	 **/
+	/* ---ifcfg-rh---
+	 * property: multicast-hash-max
+	 * variable: BRIDGING_OPTS: multicast_hash_max=
+	 * default: 4096
+	 * example: BRIDGING_OPTS="multicast_hash_max=8192"
+	 * ---end---
+	 *
+	 * Since: 1.26
+	 */
+	obj_properties[PROP_MULTICAST_HASH_MAX] =
+	    g_param_spec_uint (NM_SETTING_BRIDGE_MULTICAST_HASH_MAX, "", "",
+	                       NM_BRIDGE_MULTICAST_HASH_MAX_MIN, NM_BRIDGE_MULTICAST_HASH_MAX_MAX, NM_BRIDGE_MULTICAST_HASH_MAX_DEF,
+	                       G_PARAM_READWRITE |
+	                       NM_SETTING_PARAM_INFERRABLE |
+	                       G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * NMSettingBridge:multicast-last-member-count:
+	 *
+	 * Set the number of queries the bridge will send before
+	 * stopping forwarding a multicast group after a "leave"
+	 * message has been received.
+	 **/
+	/* ---ifcfg-rh---
+	 * property: multicast-last-member-count
+	 * variable: BRIDGING_OPTS: multicast_last_member_count=
+	 * default: 2
+	 * example: BRIDGING_OPTS="multicast_last_member_count=4"
+	 * ---end---
+	 *
+	 * Since: 1.26
+	 */
+	obj_properties[PROP_MULTICAST_LAST_MEMBER_COUNT] =
+	    g_param_spec_uint (NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_COUNT, "", "",
+	                       NM_BRIDGE_MULTICAST_LAST_MEMBER_COUNT_MIN, NM_BRIDGE_MULTICAST_LAST_MEMBER_COUNT_MAX, NM_BRIDGE_MULTICAST_LAST_MEMBER_COUNT_DEF,
+	                       G_PARAM_READWRITE |
+	                       NM_SETTING_PARAM_INFERRABLE |
+	                       G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * NMSettingBridge:multicast-last-member-interval:
+	 *
+	 * Set interval (in deciseconds) between queries to find remaining
+	 * members of a group, after a "leave" message is received.
+	 **/
+	/* ---ifcfg-rh---
+	 * property: multicast-last-member-interval
+	 * variable: BRIDGING_OPTS: multicast_last_member_interval=
+	 * default: 100
+	 * example: BRIDGING_OPTS="multicast_last_member_interval=200"
+	 * ---end---
+	 *
+	 * Since: 1.26
+	 */
+	obj_properties[PROP_MULTICAST_LAST_MEMBER_INTERVAL] =
+	    g_param_spec_uint64 (NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL, "", "",
+	                         NM_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL_MIN, NM_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL_MAX, NM_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL_DEF,
+	                         G_PARAM_READWRITE |
+	                         NM_SETTING_PARAM_INFERRABLE |
+	                         G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * NMSettingBridge:multicast-membership-interval:
+	 *
+	 * Set delay (in deciseconds) after which the bridge will
+	 * leave a group, if no membership reports for this
+	 * group are received.
+	 **/
+	/* ---ifcfg-rh---
+	 * property: multicast-membership-interval
+	 * variable: BRIDGING_OPTS: multicast_membership_interval=
+	 * default: 26000
+	 * example: BRIDGING_OPTS="multicast_membership_interval=16000"
+	 * ---end---
+	 *
+	 * Since: 1.26
+	 */
+	obj_properties[PROP_MULTICAST_MEMBERSHIP_INTERVAL] =
+	    g_param_spec_uint64 (NM_SETTING_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL, "", "",
+	                         NM_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL_MIN, NM_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL_MAX, NM_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL_DEF,
+	                         G_PARAM_READWRITE |
+	                         NM_SETTING_PARAM_INFERRABLE |
+	                         G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * NMSettingBridge:multicast-querier-interval:
+	 *
+	 * If no queries are seen after this delay (in deciseconds) has passed,
+	 * the bridge will start to send its own queries.
+	 **/
+	/* ---ifcfg-rh---
+	 * property: multicast-querier-interval
+	 * variable: BRIDGING_OPTS: multicast_querier_interval=
+	 * default: 25500
+	 * example: BRIDGING_OPTS="multicast_querier_interval=20000"
+	 * ---end---
+	 *
+	 * Since: 1.26
+	 */
+	obj_properties[PROP_MULTICAST_QUERIER_INTERVAL] =
+	    g_param_spec_uint64 (NM_SETTING_BRIDGE_MULTICAST_QUERIER_INTERVAL, "", "",
+	                         NM_BRIDGE_MULTICAST_QUERIER_INTERVAL_MIN, NM_BRIDGE_MULTICAST_QUERIER_INTERVAL_MAX, NM_BRIDGE_MULTICAST_QUERIER_INTERVAL_DEF,
+	                         G_PARAM_READWRITE |
+	                         NM_SETTING_PARAM_INFERRABLE |
+	                         G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * NMSettingBridge:multicast-query-interval:
+	 *
+	 * Interval (in deciseconds) between queries sent
+	 * by the bridge after the end of the startup phase.
+	 **/
+	/* ---ifcfg-rh---
+	 * property: multicast-query-interval
+	 * variable: BRIDGING_OPTS: multicast_query_interval=
+	 * default: 12500
+	 * example: BRIDGING_OPTS="multicast_query_interval=22500"
+	 * ---end---
+	 *
+	 * Since: 1.26
+	 */
+	obj_properties[PROP_MULTICAST_QUERY_INTERVAL] =
+	    g_param_spec_uint64 (NM_SETTING_BRIDGE_MULTICAST_QUERY_INTERVAL, "", "",
+	                         NM_BRIDGE_MULTICAST_QUERY_INTERVAL_MIN, NM_BRIDGE_MULTICAST_QUERY_INTERVAL_MAX, NM_BRIDGE_MULTICAST_QUERY_INTERVAL_DEF,
+	                         G_PARAM_READWRITE |
+	                         NM_SETTING_PARAM_INFERRABLE |
+	                         G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * NMSettingBridge:multicast-query-response-interval:
+	 *
+	 * Set the Max Response Time/Max Response Delay
+	 * (in deciseconds) for IGMP/MLD queries sent by the bridge.
+	 **/
+	/* ---ifcfg-rh---
+	 * property: multicast-query-response-interval
+	 * variable: BRIDGING_OPTS: multicast_query_response_interval=
+	 * default: 1000
+	 * example: BRIDGING_OPTS="multicast_query_response_interval=2000"
+	 * ---end---
+	 *
+	 * Since: 1.26
+	 */
+	obj_properties[PROP_MULTICAST_QUERY_RESPONSE_INTERVAL] =
+	    g_param_spec_uint64 (NM_SETTING_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL, "", "",
+	                         NM_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL_MIN, NM_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL_MAX, NM_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL_DEF,
+	                         G_PARAM_READWRITE |
+	                         NM_SETTING_PARAM_INFERRABLE |
+	                         G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * NMSettingBridge:multicast-startup-query-count:
+	 *
+	 * Set the number of IGMP queries to send during startup phase.
+	 **/
+	/* ---ifcfg-rh---
+	 * property: multicast-startup-query-count
+	 * variable: BRIDGING_OPTS: multicast_startup_query_count=
+	 * default: 2
+	 * example: BRIDGING_OPTS="multicast_startup_query_count=4"
+	 * ---end---
+	 *
+	 * Since: 1.26
+	 */
+	obj_properties[PROP_MULTICAST_STARTUP_QUERY_COUNT] =
+	    g_param_spec_uint (NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT, "", "",
+	                       NM_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT_MIN, NM_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT_MAX, NM_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT_DEF,
+	                       G_PARAM_READWRITE |
+	                       NM_SETTING_PARAM_INFERRABLE |
+	                       G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * NMSettingBridge:multicast-startup-query-interval:
+	 *
+	 * Sets the time (in deciseconds) between queries sent out
+	 * at startup to determine membership information.
+	 **/
+	/* ---ifcfg-rh---
+	 * property: multicast-startup-query-interval
+	 * variable: BRIDGING_OPTS: multicast_startup_query_interval=
+	 * default: 3125
+	 * example: BRIDGING_OPTS="multicast_startup_query_interval=4000"
+	 * ---end---
+	 *
+	 * Since: 1.26
+	 */
+	obj_properties[PROP_MULTICAST_STARTUP_QUERY_INTERVAL] =
+	    g_param_spec_uint64 (NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL, "", "",
+	                         NM_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL_MIN, NM_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL_MAX, NM_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL_DEF,
+	                         G_PARAM_READWRITE |
+	                         NM_SETTING_PARAM_INFERRABLE |
+	                         G_PARAM_STATIC_STRINGS);
+
 	g_object_class_install_properties (object_class, _PROPERTY_ENUMS_LAST, obj_properties);
 
 	_nm_setting_class_commit_full (setting_class, NM_META_SETTING_TYPE_BRIDGE,
diff --git a/libnm-core/nm-setting-bridge.h b/libnm-core/nm-setting-bridge.h
index 6bd5100f..5f0ef0b3 100644
--- a/libnm-core/nm-setting-bridge.h
+++ b/libnm-core/nm-setting-bridge.h
@@ -23,24 +23,33 @@ G_BEGIN_DECLS
 
 #define NM_SETTING_BRIDGE_SETTING_NAME "bridge"
 
-#define NM_SETTING_BRIDGE_MAC_ADDRESS                     "mac-address"
-#define NM_SETTING_BRIDGE_STP                             "stp"
-#define NM_SETTING_BRIDGE_PRIORITY                        "priority"
-#define NM_SETTING_BRIDGE_FORWARD_DELAY                   "forward-delay"
-#define NM_SETTING_BRIDGE_HELLO_TIME                      "hello-time"
-#define NM_SETTING_BRIDGE_MAX_AGE                         "max-age"
-#define NM_SETTING_BRIDGE_AGEING_TIME                     "ageing-time"
-#define NM_SETTING_BRIDGE_GROUP_FORWARD_MASK              "group-forward-mask"
-#define NM_SETTING_BRIDGE_MULTICAST_SNOOPING              "multicast-snooping"
-#define NM_SETTING_BRIDGE_MULTICAST_ROUTER                "multicast-router"
-#define NM_SETTING_BRIDGE_MULTICAST_QUERIER               "multicast-querier"
-#define NM_SETTING_BRIDGE_MULTICAST_QUERY_USE_IFADDR      "multicast-query-use-ifaddr"
-#define NM_SETTING_BRIDGE_VLAN_FILTERING                  "vlan-filtering"
-#define NM_SETTING_BRIDGE_VLAN_DEFAULT_PVID               "vlan-default-pvid"
-#define NM_SETTING_BRIDGE_VLANS                           "vlans"
-#define NM_SETTING_BRIDGE_GROUP_ADDRESS                   "group-address"
-#define NM_SETTING_BRIDGE_VLAN_PROTOCOL                   "vlan-protocol"
-#define NM_SETTING_BRIDGE_VLAN_STATS_ENABLED              "vlan-stats-enabled"
+#define NM_SETTING_BRIDGE_MAC_ADDRESS                       "mac-address"
+#define NM_SETTING_BRIDGE_STP                               "stp"
+#define NM_SETTING_BRIDGE_PRIORITY                          "priority"
+#define NM_SETTING_BRIDGE_FORWARD_DELAY                     "forward-delay"
+#define NM_SETTING_BRIDGE_HELLO_TIME                        "hello-time"
+#define NM_SETTING_BRIDGE_MAX_AGE                           "max-age"
+#define NM_SETTING_BRIDGE_AGEING_TIME                       "ageing-time"
+#define NM_SETTING_BRIDGE_GROUP_FORWARD_MASK                "group-forward-mask"
+#define NM_SETTING_BRIDGE_MULTICAST_HASH_MAX                "multicast-hash-max"
+#define NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_COUNT       "multicast-last-member-count"
+#define NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL    "multicast-last-member-interval"
+#define NM_SETTING_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL     "multicast-membership-interval"
+#define NM_SETTING_BRIDGE_MULTICAST_SNOOPING                "multicast-snooping"
+#define NM_SETTING_BRIDGE_MULTICAST_ROUTER                  "multicast-router"
+#define NM_SETTING_BRIDGE_MULTICAST_QUERIER                 "multicast-querier"
+#define NM_SETTING_BRIDGE_MULTICAST_QUERIER_INTERVAL        "multicast-querier-interval"
+#define NM_SETTING_BRIDGE_MULTICAST_QUERY_INTERVAL          "multicast-query-interval"
+#define NM_SETTING_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL "multicast-query-response-interval"
+#define NM_SETTING_BRIDGE_MULTICAST_QUERY_USE_IFADDR        "multicast-query-use-ifaddr"
+#define NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT     "multicast-startup-query-count"
+#define NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL  "multicast-startup-query-interval"
+#define NM_SETTING_BRIDGE_VLAN_FILTERING                    "vlan-filtering"
+#define NM_SETTING_BRIDGE_VLAN_DEFAULT_PVID                 "vlan-default-pvid"
+#define NM_SETTING_BRIDGE_VLANS                             "vlans"
+#define NM_SETTING_BRIDGE_GROUP_ADDRESS                     "group-address"
+#define NM_SETTING_BRIDGE_VLAN_PROTOCOL                     "vlan-protocol"
+#define NM_SETTING_BRIDGE_VLAN_STATS_ENABLED                "vlan-stats-enabled"
 
 #define NM_BRIDGE_VLAN_VID_MIN            1
 #define NM_BRIDGE_VLAN_VID_MAX            4094
@@ -140,6 +149,33 @@ gboolean nm_setting_bridge_get_multicast_query_use_ifaddr (const NMSettingBridge
 NM_AVAILABLE_IN_1_24
 gboolean nm_setting_bridge_get_multicast_querier (const NMSettingBridge *setting);
 
+NM_AVAILABLE_IN_1_26
+guint32 nm_setting_bridge_get_multicast_hash_max (const NMSettingBridge *setting);
+
+NM_AVAILABLE_IN_1_26
+guint32 nm_setting_bridge_get_multicast_last_member_count (const NMSettingBridge *setting);
+
+NM_AVAILABLE_IN_1_26
+guint64 nm_setting_bridge_get_multicast_last_member_interval (const NMSettingBridge *setting);
+
+NM_AVAILABLE_IN_1_26
+guint64 nm_setting_bridge_get_multicast_membership_interval (const NMSettingBridge *setting);
+
+NM_AVAILABLE_IN_1_26
+guint64 nm_setting_bridge_get_multicast_querier_interval (const NMSettingBridge *setting);
+
+NM_AVAILABLE_IN_1_26
+guint64 nm_setting_bridge_get_multicast_query_interval (const NMSettingBridge *setting);
+
+NM_AVAILABLE_IN_1_26
+guint64 nm_setting_bridge_get_multicast_query_response_interval (const NMSettingBridge *setting);
+
+NM_AVAILABLE_IN_1_26
+guint32 nm_setting_bridge_get_multicast_startup_query_count (const NMSettingBridge *setting);
+
+NM_AVAILABLE_IN_1_26
+guint64 nm_setting_bridge_get_multicast_startup_query_interval (const NMSettingBridge *setting);
+
 G_END_DECLS
 
 #endif /* __NM_SETTING_BRIDGE_H__ */
diff --git a/libnm-core/nm-setting-connection.c b/libnm-core/nm-setting-connection.c
index 2e8fa37a..018129be 100644
--- a/libnm-core/nm-setting-connection.c
+++ b/libnm-core/nm-setting-connection.c
@@ -8,6 +8,7 @@
 
 #include "nm-setting-connection.h"
 
+#include "nm-libnm-core-intern/nm-common-macros.h"
 #include "nm-utils.h"
 #include "nm-utils-private.h"
 #include "nm-core-enum-types.h"
@@ -16,6 +17,7 @@
 #include "nm-setting-bridge.h"
 #include "nm-setting-team.h"
 #include "nm-setting-vlan.h"
+#include "systemd/nm-sd-utils-shared.h"
 
 /**
  * SECTION:nm-setting-connection
@@ -63,6 +65,7 @@ NM_GOBJECT_PROPERTIES_DEFINE (NMSettingConnection,
 	PROP_STABLE_ID,
 	PROP_AUTH_RETRIES,
 	PROP_WAIT_DEVICE_TIMEOUT,
+	PROP_MUD_URL,
 );
 
 typedef struct {
@@ -76,6 +79,7 @@ typedef struct {
 	char *master;
 	char *slave_type;
 	char *zone;
+	char *mud_url;
 	guint64 timestamp;
 	int autoconnect_priority;
 	int autoconnect_retries;
@@ -751,6 +755,23 @@ nm_setting_connection_get_secondary (NMSettingConnection *setting, guint32 idx)
 }
 
 /**
+ * nm_setting_connection_get_mud_url:
+ * @setting: the #NMSettingConnection
+ *
+ * Returns the value contained in the #NMSettingConnection:mud-url
+ * property.
+ *
+ * Since: 1.26
+ **/
+const char *
+nm_setting_connection_get_mud_url (NMSettingConnection *setting)
+{
+	g_return_val_if_fail (NM_IS_SETTING_CONNECTION (setting), NULL);
+
+	return NM_SETTING_CONNECTION_GET_PRIVATE (setting)->mud_url;
+}
+
+/**
  * nm_setting_connection_add_secondary:
  * @setting: the #NMSettingConnection
  * @sec_uuid: the secondary connection UUID to add
@@ -1211,6 +1232,31 @@ after_interface_name:
 		return FALSE;
 	}
 
+	if (priv->mud_url) {
+		if (!priv->mud_url[0]) {
+			g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+			                     _("property is empty"));
+			g_prefix_error (error, "%s.%s: ", nm_setting_get_name (setting), NM_SETTING_CONNECTION_MUD_URL);
+			return FALSE;
+		}
+		if (nm_streq (priv->mud_url, NM_CONNECTION_MUD_URL_NONE)) {
+			/* pass */
+		} else {
+			if (strlen (priv->mud_url) > 255) {
+				g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+				                     _("DHCP option cannot be longer than 255 characters"));
+				g_prefix_error (error, "%s.%s: ", nm_setting_get_name (setting), NM_SETTING_CONNECTION_MUD_URL);
+				return FALSE;
+			}
+			if (!nm_sd_http_url_is_valid_https (priv->mud_url)) {
+				g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+				                     _("MUD URL is not a valid URL"));
+				g_prefix_error (error, "%s.%s: ", nm_setting_get_name (setting), NM_SETTING_CONNECTION_MUD_URL);
+				return FALSE;
+			}
+		}
+	}
+
 	/* *** errors above here should be always fatal, below NORMALIZABLE_ERROR *** */
 
 	if (!priv->uuid) {
@@ -1467,6 +1513,9 @@ get_property (GObject *object, guint prop_id,
 	case PROP_WAIT_DEVICE_TIMEOUT:
 		g_value_set_int (value, priv->wait_device_timeout);
 		break;
+	case PROP_MUD_URL:
+		g_value_set_string (value, priv->mud_url);
+		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
 		break;
@@ -1562,6 +1611,10 @@ set_property (GObject *object, guint prop_id,
 	case PROP_WAIT_DEVICE_TIMEOUT:
 		priv->wait_device_timeout = g_value_get_int (value);
 		break;
+	case PROP_MUD_URL:
+		g_free (priv->mud_url);
+		priv->mud_url = g_value_dup_string (value);
+		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
 		break;
@@ -1611,6 +1664,7 @@ finalize (GObject *object)
 	g_free (priv->zone);
 	g_free (priv->master);
 	g_free (priv->slave_type);
+	g_free (priv->mud_url);
 	g_slist_free_full (priv->permissions, (GDestroyNotify) permission_free);
 	g_slist_free_full (priv->secondaries, g_free);
 
@@ -2286,6 +2340,35 @@ nm_setting_connection_class_init (NMSettingConnectionClass *klass)
 	                      G_PARAM_READWRITE |
 	                      G_PARAM_STATIC_STRINGS);
 
+
+	/**
+	 * NMSettingConnection:mud-url:
+	 *
+	 * 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".
+	 *
+	 * Since: 1.26
+	 **/
+	/* ---ifcfg-rh---
+	 * property: mud-url
+	 * variable: MUD_URL
+	 * values: a valid URL that points to recommended policy for this device
+	 * description: MUD_URL to be sent by device (See RFC 8520).
+	 * example: https://yourdevice.example.com/model.json
+	 * ---end---
+	 */
+	obj_properties[PROP_MUD_URL] =
+	    g_param_spec_string (NM_SETTING_CONNECTION_MUD_URL, "", "",
+	                         NULL,
+	                         G_PARAM_READWRITE |
+	                         G_PARAM_STATIC_STRINGS);
+
 	g_object_class_install_properties (object_class, _PROPERTY_ENUMS_LAST, obj_properties);
 
 	_nm_setting_class_commit_full (setting_class, NM_META_SETTING_TYPE_CONNECTION,
diff --git a/libnm-core/nm-setting-connection.h b/libnm-core/nm-setting-connection.h
index b93e9b4d..9a6f09a8 100644
--- a/libnm-core/nm-setting-connection.h
+++ b/libnm-core/nm-setting-connection.h
@@ -52,6 +52,7 @@ G_BEGIN_DECLS
 #define NM_SETTING_CONNECTION_MDNS           "mdns"
 #define NM_SETTING_CONNECTION_LLMNR          "llmnr"
 #define NM_SETTING_CONNECTION_WAIT_DEVICE_TIMEOUT "wait-device-timeout"
+#define NM_SETTING_CONNECTION_MUD_URL        "mud-url"
 
 /* Types for property values */
 /**
@@ -204,6 +205,9 @@ NMSettingConnectionLlmnr  nm_setting_connection_get_llmnr (NMSettingConnection *
 NM_AVAILABLE_IN_1_20
 gint32 nm_setting_connection_get_wait_device_timeout (NMSettingConnection *setting);
 
+NM_AVAILABLE_IN_1_26
+const char *nm_setting_connection_get_mud_url (NMSettingConnection *setting);
+
 G_END_DECLS
 
 #endif /* __NM_SETTING_CONNECTION_H__ */
diff --git a/libnm-core/nm-setting-ethtool.c b/libnm-core/nm-setting-ethtool.c
index a8976b69..17fbdc62 100644
--- a/libnm-core/nm-setting-ethtool.c
+++ b/libnm-core/nm-setting-ethtool.c
@@ -22,6 +22,21 @@
 
 /*****************************************************************************/
 
+static const GVariantType *
+get_variant_type_from_ethtool_id (NMEthtoolID ethtool_id)
+{
+	if (nm_ethtool_id_is_feature (ethtool_id))
+		return G_VARIANT_TYPE_BOOLEAN;
+
+	if (   nm_ethtool_id_is_coalesce (ethtool_id)
+	    || nm_ethtool_id_is_ring (ethtool_id))
+		return G_VARIANT_TYPE_UINT32;
+
+	return NULL;
+}
+
+/*****************************************************************************/
+
 /**
  * nm_ethtool_optname_is_feature:
  * @optname: (allow-none): the option name to check
@@ -42,6 +57,38 @@ nm_ethtool_optname_is_feature (const char *optname)
 	return optname && nm_ethtool_id_is_feature (nm_ethtool_id_get_by_name (optname));
 }
 
+/**
+ * nm_ethtool_optname_is_coalesce:
+ * @optname: (allow-none): the option name to check
+ *
+ * Checks whether @optname is a valid option name for a coalesce setting.
+ *
+ * %Returns: %TRUE, if @optname is valid
+ *
+ * Since: 1.26
+ */
+gboolean
+nm_ethtool_optname_is_coalesce (const char *optname)
+{
+	return optname && nm_ethtool_id_is_coalesce (nm_ethtool_id_get_by_name (optname));
+}
+
+/**
+ * nm_ethtool_optname_is_ring:
+ * @optname: (allow-none): the option name to check
+ *
+ * Checks whether @optname is a valid option name for a ring setting.
+ *
+ * %Returns: %TRUE, if @optname is valid
+ *
+ * Since: 1.26
+ */
+gboolean
+nm_ethtool_optname_is_ring (const char *optname)
+{
+	return optname && nm_ethtool_id_is_ring (nm_ethtool_id_get_by_name (optname));
+}
+
 /*****************************************************************************/
 
 /**
@@ -65,14 +112,6 @@ G_DEFINE_TYPE (NMSettingEthtool, nm_setting_ethtool, NM_TYPE_SETTING)
 
 /*****************************************************************************/
 
-static void
-_notify_attributes (NMSettingEthtool *self)
-{
-	_nm_setting_gendata_notify (NM_SETTING (self), TRUE);
-}
-
-/*****************************************************************************/
-
 /**
  * nm_setting_ethtool_get_feature:
  * @setting: the #NMSettingEthtool
@@ -88,24 +127,23 @@ _notify_attributes (NMSettingEthtool *self)
  *   is enabled, disabled, or left untouched.
  *
  * Since: 1.14
+ *
+ * Deprecated: 1.26: use nm_setting_option_get_boolean() instead.
  */
 NMTernary
 nm_setting_ethtool_get_feature (NMSettingEthtool *setting,
                                 const char *optname)
 {
-	GVariant *v;
+	gboolean v;
 
 	g_return_val_if_fail (NM_IS_SETTING_ETHTOOL (setting), NM_TERNARY_DEFAULT);
 	g_return_val_if_fail (optname && nm_ethtool_optname_is_feature (optname), NM_TERNARY_DEFAULT);
 
-	v = nm_setting_gendata_get (NM_SETTING (setting), optname);
-	if (   v
-	    && g_variant_is_of_type (v, G_VARIANT_TYPE_BOOLEAN)) {
-		return g_variant_get_boolean (v)
-		       ? NM_TERNARY_TRUE
-		       : NM_TERNARY_FALSE;
-	}
-	return NM_TERNARY_DEFAULT;
+	if (!nm_setting_option_get_boolean (NM_SETTING (setting), optname, &v))
+		return NM_TERNARY_DEFAULT;
+	return   v
+	       ? NM_TERNARY_TRUE
+	       : NM_TERNARY_FALSE;
 }
 
 /**
@@ -121,49 +159,24 @@ nm_setting_ethtool_get_feature (NMSettingEthtool *setting,
  * nm_ethtool_optname_is_feature().
  *
  * Since: 1.14
+ *
+ * Deprecated: 1.26: use nm_setting_option_set() or nm_setting_option_set_boolean() instead.
  */
 void
 nm_setting_ethtool_set_feature (NMSettingEthtool *setting,
                                 const char *optname,
                                 NMTernary value)
 {
-	GHashTable *hash;
-	GVariant *v;
-
 	g_return_if_fail (NM_IS_SETTING_ETHTOOL (setting));
 	g_return_if_fail (optname && nm_ethtool_optname_is_feature (optname));
 	g_return_if_fail (NM_IN_SET (value, NM_TERNARY_DEFAULT,
 	                                    NM_TERNARY_FALSE,
 	                                    NM_TERNARY_TRUE));
 
-	hash = _nm_setting_gendata_hash (NM_SETTING (setting),
-	                                 value != NM_TERNARY_DEFAULT);
-
-	if (value == NM_TERNARY_DEFAULT) {
-		if (hash) {
-			if (g_hash_table_remove (hash, optname))
-				_notify_attributes (setting);
-		}
-		return;
-	}
-
-	v = g_hash_table_lookup (hash, optname);
-	if (   v
-	    && g_variant_is_of_type (v, G_VARIANT_TYPE_BOOLEAN)) {
-		if (g_variant_get_boolean (v)) {
-			if (value == NM_TERNARY_TRUE)
-				return;
-		} else {
-			if (value == NM_TERNARY_FALSE)
-				return;
-		}
-	}
-
-	v = g_variant_ref_sink (g_variant_new_boolean (value != NM_TERNARY_FALSE));
-	g_hash_table_insert (hash,
-	                     g_strdup (optname),
-	                     v);
-	_notify_attributes (setting);
+	if (value == NM_TERNARY_DEFAULT)
+		nm_setting_option_set (NM_SETTING (setting), optname, NULL);
+	else
+		nm_setting_option_set_boolean (NM_SETTING (setting), optname, (value != NM_TERNARY_FALSE));
 }
 
 /**
@@ -173,33 +186,20 @@ nm_setting_ethtool_set_feature (NMSettingEthtool *setting,
  * Clears all offload features settings
  *
  * Since: 1.14
+ *
+ * Deprecated: 1.26: use nm_setting_option_clear_by_name() with nm_ethtool_optname_is_feature() predicate instead.
  */
 void
 nm_setting_ethtool_clear_features (NMSettingEthtool *setting)
 {
-	GHashTable *hash;
-	GHashTableIter iter;
-	const char *name;
-	gboolean changed = FALSE;
-
 	g_return_if_fail (NM_IS_SETTING_ETHTOOL (setting));
 
-	hash = _nm_setting_gendata_hash (NM_SETTING (setting), FALSE);
-	if (!hash)
-		return;
-
-	g_hash_table_iter_init (&iter, hash);
-	while (g_hash_table_iter_next (&iter, (gpointer *) &name, NULL)) {
-		if (nm_ethtool_optname_is_feature (name)) {
-			g_hash_table_iter_remove (&iter);
-			changed = TRUE;
-		}
-	}
-
-	if (changed)
-		_notify_attributes (setting);
+	nm_setting_option_clear_by_name (NM_SETTING (setting),
+	                                 nm_ethtool_optname_is_feature);
 }
 
+/*****************************************************************************/
+
 guint
 nm_setting_ethtool_init_features (NMSettingEthtool *setting,
                                   NMTernary *requested /* indexed by NMEthtoolID - _NM_ETHTOOL_ID_FEATURE_FIRST */)
@@ -217,7 +217,7 @@ nm_setting_ethtool_init_features (NMSettingEthtool *setting,
 	for (i = 0; i < _NM_ETHTOOL_ID_FEATURE_NUM; i++)
 		requested[i] = NM_TERNARY_DEFAULT;
 
-	hash = _nm_setting_gendata_hash (NM_SETTING (setting), FALSE);
+	hash = _nm_setting_option_hash (NM_SETTING (setting), FALSE);
 	if (!hash)
 		return 0;
 
@@ -230,9 +230,9 @@ nm_setting_ethtool_init_features (NMSettingEthtool *setting,
 		if (!g_variant_is_of_type (variant, G_VARIANT_TYPE_BOOLEAN))
 			continue;
 
-		requested[ethtool_id - _NM_ETHTOOL_ID_FEATURE_FIRST] = g_variant_get_boolean (variant)
-		                                                       ? NM_TERNARY_TRUE
-		                                                       : NM_TERNARY_FALSE;
+		requested[_NM_ETHTOOL_ID_FEATURE_AS_IDX (ethtool_id)] =   g_variant_get_boolean (variant)
+		                                                        ? NM_TERNARY_TRUE
+		                                                        : NM_TERNARY_FALSE;
 		n_req++;
 	}
 
@@ -255,16 +255,23 @@ nm_setting_ethtool_init_features (NMSettingEthtool *setting,
  *   @setting and may get invalidated when @setting gets modified.
  *
  * Since: 1.20
+ *
+ * Deprecated: 1.26: use nm_setting_option_get_all_names() instead.
  */
 const char **
 nm_setting_ethtool_get_optnames (NMSettingEthtool *setting,
                                  guint *out_length)
 {
+	const char *const*names;
+	guint len;
+
 	g_return_val_if_fail (NM_IS_SETTING_ETHTOOL (setting), NULL);
 
-	return nm_utils_strdict_get_keys (_nm_setting_gendata_hash (NM_SETTING (setting), FALSE),
-	                                  TRUE,
-	                                  out_length);
+	names = nm_setting_option_get_all_names (NM_SETTING (setting), &len);
+	NM_SET_OUT (out_length, len);
+	return   len > 0
+	       ? nm_memdup (names, sizeof (names[0]) * (((gsize) len) + 1u))
+	       : NULL;
 }
 
 /*****************************************************************************/
@@ -272,37 +279,54 @@ nm_setting_ethtool_get_optnames (NMSettingEthtool *setting,
 static gboolean
 verify (NMSetting *setting, NMConnection *connection, GError **error)
 {
-	GHashTable *hash;
-	GHashTableIter iter;
-	const char *optname;
-	GVariant *variant;
+	const char *const*optnames;
+	GVariant *const*variants;
+	guint len;
+	guint i;
 
-	hash = _nm_setting_gendata_hash (setting, FALSE);
+	len = _nm_setting_option_get_all (setting, &optnames, &variants);
 
-	if (!hash)
-		goto out;
+	for (i = 0; i < len; i++) {
+		const char *optname = optnames[i];
+		GVariant *variant = variants[i];
+		const GVariantType *variant_type;
+		NMEthtoolID ethtool_id;
 
-	g_hash_table_iter_init (&iter, hash);
-	while (g_hash_table_iter_next (&iter, (gpointer *) &optname, (gpointer *) &variant)) {
-		if (!nm_ethtool_optname_is_feature (optname)) {
+		ethtool_id = nm_ethtool_id_get_by_name (optname);
+		variant_type = get_variant_type_from_ethtool_id (ethtool_id);
+
+		if (!variant_type) {
 			g_set_error_literal (error,
 			                     NM_CONNECTION_ERROR,
 			                     NM_CONNECTION_ERROR_INVALID_PROPERTY,
-			                     _("unsupported offload feature"));
+			                     _("unsupported ethtool setting"));
 			g_prefix_error (error, "%s.%s: ", NM_SETTING_ETHTOOL_SETTING_NAME, optname);
 			return FALSE;
 		}
-		if (!g_variant_is_of_type (variant, G_VARIANT_TYPE_BOOLEAN)) {
+
+		if (!g_variant_is_of_type (variant, variant_type)) {
 			g_set_error_literal (error,
 			                     NM_CONNECTION_ERROR,
 			                     NM_CONNECTION_ERROR_INVALID_PROPERTY,
-			                     _("offload feature has invalid variant type"));
+			                     _("setting has invalid variant type"));
 			g_prefix_error (error, "%s.%s: ", NM_SETTING_ETHTOOL_SETTING_NAME, optname);
 			return FALSE;
 		}
+
+		if (NM_IN_SET (ethtool_id,
+		               NM_ETHTOOL_ID_COALESCE_ADAPTIVE_RX,
+		               NM_ETHTOOL_ID_COALESCE_ADAPTIVE_TX)) {
+			if (!NM_IN_SET (g_variant_get_uint32 (variant), 0, 1)) {
+				g_set_error_literal (error,
+				                     NM_CONNECTION_ERROR,
+				                     NM_CONNECTION_ERROR_INVALID_PROPERTY,
+				                     _("coalesce option must be either 0 or 1"));
+				g_prefix_error (error, "%s.%s: ", NM_SETTING_ETHTOOL_SETTING_NAME, optname);
+				return FALSE;
+			}
+		}
 	}
 
-out:
 	return TRUE;
 }
 
@@ -313,15 +337,20 @@ get_variant_type (const NMSettInfoSetting *sett_info,
                   const char *name,
                   GError **error)
 {
-	if (nm_ethtool_optname_is_feature (name))
-		return G_VARIANT_TYPE_BOOLEAN;
+	const GVariantType *variant_type;
 
-	g_set_error (error,
-	             NM_CONNECTION_ERROR,
-	             NM_CONNECTION_ERROR_INVALID_PROPERTY,
-	             _("unknown ethtool option '%s'"),
-	             name);
-	return NULL;
+	variant_type = get_variant_type_from_ethtool_id (nm_ethtool_id_get_by_name (name));
+
+	if (!variant_type) {
+		g_set_error (error,
+		             NM_CONNECTION_ERROR,
+		             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		             _("unknown ethtool option '%s'"),
+		             name);
+		return NULL;
+	}
+
+	return variant_type;
 }
 
 /*****************************************************************************/
diff --git a/libnm-core/nm-setting-ethtool.h b/libnm-core/nm-setting-ethtool.h
index ba8c6a6e..27cb9658 100644
--- a/libnm-core/nm-setting-ethtool.h
+++ b/libnm-core/nm-setting-ethtool.h
@@ -69,9 +69,43 @@ G_BEGIN_DECLS
 #define NM_ETHTOOL_OPTNAME_FEATURE_TX_UDP_TNL_SEGMENTATION      "feature-tx-udp_tnl-segmentation"
 #define NM_ETHTOOL_OPTNAME_FEATURE_TX_VLAN_STAG_HW_INSERT       "feature-tx-vlan-stag-hw-insert"
 
+#define NM_ETHTOOL_OPTNAME_COALESCE_ADAPTIVE_RX                 "coalesce-adaptive-rx"
+#define NM_ETHTOOL_OPTNAME_COALESCE_ADAPTIVE_TX                 "coalesce-adaptive-tx"
+#define NM_ETHTOOL_OPTNAME_COALESCE_PKT_RATE_HIGH               "coalesce-pkt-rate-high"
+#define NM_ETHTOOL_OPTNAME_COALESCE_PKT_RATE_LOW                "coalesce-pkt-rate-low"
+#define NM_ETHTOOL_OPTNAME_COALESCE_RX_FRAMES                   "coalesce-rx-frames"
+#define NM_ETHTOOL_OPTNAME_COALESCE_RX_FRAMES_HIGH              "coalesce-rx-frames-high"
+#define NM_ETHTOOL_OPTNAME_COALESCE_RX_FRAMES_IRQ               "coalesce-rx-frames-irq"
+#define NM_ETHTOOL_OPTNAME_COALESCE_RX_FRAMES_LOW               "coalesce-rx-frames-low"
+#define NM_ETHTOOL_OPTNAME_COALESCE_RX_USECS                    "coalesce-rx-usecs"
+#define NM_ETHTOOL_OPTNAME_COALESCE_RX_USECS_HIGH               "coalesce-rx-usecs-high"
+#define NM_ETHTOOL_OPTNAME_COALESCE_RX_USECS_IRQ                "coalesce-rx-usecs-irq"
+#define NM_ETHTOOL_OPTNAME_COALESCE_RX_USECS_LOW                "coalesce-rx-usecs-low"
+#define NM_ETHTOOL_OPTNAME_COALESCE_SAMPLE_INTERVAL             "coalesce-sample-interval"
+#define NM_ETHTOOL_OPTNAME_COALESCE_STATS_BLOCK_USECS           "coalesce-stats-block-usecs"
+#define NM_ETHTOOL_OPTNAME_COALESCE_TX_FRAMES                   "coalesce-tx-frames"
+#define NM_ETHTOOL_OPTNAME_COALESCE_TX_FRAMES_HIGH              "coalesce-tx-frames-high"
+#define NM_ETHTOOL_OPTNAME_COALESCE_TX_FRAMES_IRQ               "coalesce-tx-frames-irq"
+#define NM_ETHTOOL_OPTNAME_COALESCE_TX_FRAMES_LOW               "coalesce-tx-frames-low"
+#define NM_ETHTOOL_OPTNAME_COALESCE_TX_USECS                    "coalesce-tx-usecs"
+#define NM_ETHTOOL_OPTNAME_COALESCE_TX_USECS_HIGH               "coalesce-tx-usecs-high"
+#define NM_ETHTOOL_OPTNAME_COALESCE_TX_USECS_IRQ                "coalesce-tx-usecs-irq"
+#define NM_ETHTOOL_OPTNAME_COALESCE_TX_USECS_LOW                "coalesce-tx-usecs-low"
+
+#define NM_ETHTOOL_OPTNAME_RING_RX                              "ring-rx"
+#define NM_ETHTOOL_OPTNAME_RING_RX_JUMBO                        "ring-rx-jumbo"
+#define NM_ETHTOOL_OPTNAME_RING_RX_MINI                         "ring-rx-mini"
+#define NM_ETHTOOL_OPTNAME_RING_TX                              "ring-tx"
+
 NM_AVAILABLE_IN_1_20
 gboolean nm_ethtool_optname_is_feature (const char *optname);
 
+NM_AVAILABLE_IN_1_26
+gboolean nm_ethtool_optname_is_coalesce (const char *optname);
+
+NM_AVAILABLE_IN_1_26
+gboolean nm_ethtool_optname_is_ring (const char *optname);
+
 /*****************************************************************************/
 
 #define NM_TYPE_SETTING_ETHTOOL            (nm_setting_ethtool_get_type ())
@@ -95,20 +129,24 @@ NMSetting        *nm_setting_ethtool_new (void);
 
 /*****************************************************************************/
 
+NM_AVAILABLE_IN_1_20
+NM_DEPRECATED_IN_1_26
+const char **     nm_setting_ethtool_get_optnames (NMSettingEthtool *setting,
+                                                   guint *out_length);
+
 NM_AVAILABLE_IN_1_14
+NM_DEPRECATED_IN_1_26
 NMTernary         nm_setting_ethtool_get_feature (NMSettingEthtool *setting,
                                                   const char *optname);
 NM_AVAILABLE_IN_1_14
+NM_DEPRECATED_IN_1_26
 void              nm_setting_ethtool_set_feature (NMSettingEthtool *setting,
                                                   const char *optname,
                                                   NMTernary value);
 NM_AVAILABLE_IN_1_14
+NM_DEPRECATED_IN_1_26
 void              nm_setting_ethtool_clear_features (NMSettingEthtool *setting);
 
-NM_AVAILABLE_IN_1_20
-const char **     nm_setting_ethtool_get_optnames (NMSettingEthtool *setting,
-                                                   guint *out_length);
-
 G_END_DECLS
 
 #endif /* __NM_SETTING_ETHTOOL_H__ */
diff --git a/libnm-core/nm-setting-ip-config.c b/libnm-core/nm-setting-ip-config.c
index 36f09b69..09a34283 100644
--- a/libnm-core/nm-setting-ip-config.c
+++ b/libnm-core/nm-setting-ip-config.c
@@ -2186,7 +2186,7 @@ nm_ip_routing_rule_get_xifname_bin (const NMIPRoutingRule *self,
 	if (!xifname)
 		return FALSE;
 
-	bin = nm_utils_buf_utf8safe_unescape (xifname, &len, &bin_to_free);
+	bin = nm_utils_buf_utf8safe_unescape (xifname, NM_UTILS_STR_UTF8_SAFE_FLAG_NONE, &len, &bin_to_free);
 
 	strncpy (out_xifname, bin, 16 /* IFNAMSIZ */);
 	out_xifname[15] = '\0';
diff --git a/libnm-core/nm-setting-match.c b/libnm-core/nm-setting-match.c
index 3ad58daf..8fb513e5 100644
--- a/libnm-core/nm-setting-match.c
+++ b/libnm-core/nm-setting-match.c
@@ -20,6 +20,9 @@
 
 NM_GOBJECT_PROPERTIES_DEFINE (NMSettingMatch,
 	PROP_INTERFACE_NAME,
+	PROP_KERNEL_COMMAND_LINE,
+	PROP_DRIVER,
+	PROP_PATH,
 );
 
 /**
@@ -31,7 +34,10 @@ NM_GOBJECT_PROPERTIES_DEFINE (NMSettingMatch,
  */
 struct _NMSettingMatch {
 	NMSetting parent;
-	GPtrArray *interface_name;
+	GArray *interface_name;
+	GArray *kernel_command_line;
+	GArray *driver;
+	GArray *path;
 };
 
 struct _NMSettingMatchClass {
@@ -55,7 +61,7 @@ nm_setting_match_get_num_interface_names (NMSettingMatch *setting)
 {
 	g_return_val_if_fail (NM_IS_SETTING_MATCH (setting), 0);
 
-	return setting->interface_name->len;
+	return nm_g_array_len (setting->interface_name);
 }
 
 /**
@@ -72,9 +78,9 @@ nm_setting_match_get_interface_name (NMSettingMatch *setting, int idx)
 {
 	g_return_val_if_fail (NM_IS_SETTING_MATCH (setting), NULL);
 
-	g_return_val_if_fail (idx >= 0 && idx < setting->interface_name->len, NULL);
+	g_return_val_if_fail (setting->interface_name && idx >= 0 && idx < setting->interface_name->len, NULL);
 
-	return setting->interface_name->pdata[idx];
+	return g_array_index (setting->interface_name, const char *, idx);
 }
 
 /**
@@ -94,7 +100,7 @@ nm_setting_match_add_interface_name (NMSettingMatch *setting,
 	g_return_if_fail (interface_name != NULL);
 	g_return_if_fail (interface_name[0] != '\0');
 
-	g_ptr_array_add (setting->interface_name, g_strdup (interface_name));
+	nm_strvarray_add (nm_strvarray_ensure (&setting->interface_name), interface_name);
 	_notify (setting, PROP_INTERFACE_NAME);
 }
 
@@ -112,9 +118,9 @@ nm_setting_match_remove_interface_name (NMSettingMatch *setting, int idx)
 {
 	g_return_if_fail (NM_IS_SETTING_MATCH (setting));
 
-	g_return_if_fail (idx >= 0 && idx < setting->interface_name->len);
+	g_return_if_fail (setting->interface_name && idx >= 0 && idx < setting->interface_name->len);
 
-	g_ptr_array_remove_index (setting->interface_name, idx);
+	g_array_remove_index (setting->interface_name, idx);
 	_notify (setting, PROP_INTERFACE_NAME);
 }
 
@@ -139,9 +145,12 @@ nm_setting_match_remove_interface_name_by_value (NMSettingMatch *setting,
 	g_return_val_if_fail (interface_name != NULL, FALSE);
 	g_return_val_if_fail (interface_name[0] != '\0', FALSE);
 
+	if (!setting->interface_name)
+		return FALSE;
+
 	for (i = 0; i < setting->interface_name->len; i++) {
-		if (nm_streq (interface_name, setting->interface_name->pdata[i])) {
-			g_ptr_array_remove_index (setting->interface_name, i);
+		if (nm_streq (interface_name, g_array_index (setting->interface_name, const char *, i))) {
+			g_array_remove_index (setting->interface_name, i);
 			_notify (setting, PROP_INTERFACE_NAME);
 			return TRUE;
 		}
@@ -162,8 +171,8 @@ nm_setting_match_clear_interface_names (NMSettingMatch *setting)
 {
 	g_return_if_fail (NM_IS_SETTING_MATCH (setting));
 
-	if (setting->interface_name->len != 0) {
-		g_ptr_array_set_size (setting->interface_name, 0);
+	if (nm_g_array_len (setting->interface_name) != 0) {
+		nm_clear_pointer (&setting->interface_name, g_array_unref);
 		_notify (setting, PROP_INTERFACE_NAME);
 	}
 }
@@ -171,11 +180,14 @@ nm_setting_match_clear_interface_names (NMSettingMatch *setting)
 /**
  * nm_setting_match_get_interface_names:
  * @setting: the #NMSettingMatch
- * @length: (out): the length of the returned interface names array.
+ * @length: (out) (allow-none): the length of the returned interface names array.
  *
  * Returns all the interface names.
  *
- * Returns: (transfer none) (array length=length): the configured interface names.
+ * Returns: (transfer none) (array length=length): the NULL terminated list of
+ *   configured interface names.
+ *
+ * Before 1.26, the returned array was not %NULL terminated and you MUST provide a length.
  *
  * Since: 1.14
  **/
@@ -183,10 +195,458 @@ const char *const *
 nm_setting_match_get_interface_names (NMSettingMatch *setting, guint *length)
 {
 	g_return_val_if_fail (NM_IS_SETTING_MATCH (setting), NULL);
-	g_return_val_if_fail (length, NULL);
 
-	NM_SET_OUT (length, setting->interface_name->len);
-	return (const char *const *) setting->interface_name->pdata;
+	return nm_strvarray_get_strv (&setting->interface_name, length);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_setting_match_get_num_kernel_command_lines:
+ * @setting: the #NMSettingMatch
+ *
+ * Returns: the number of configured kernel command line arguments
+ *
+ * Since: 1.26
+ **/
+guint
+nm_setting_match_get_num_kernel_command_lines (NMSettingMatch *setting)
+{
+	g_return_val_if_fail (NM_IS_SETTING_MATCH (setting), 0);
+
+	return nm_g_array_len (setting->kernel_command_line);
+}
+
+/**
+ * nm_setting_match_get_kernel_command_line:
+ * @setting: the #NMSettingMatch
+ * @idx: index number of the kernel command line argument to return
+ *
+ * Returns: the kernel command line argument at index @idx
+ *
+ * Since: 1.26
+ **/
+const char *
+nm_setting_match_get_kernel_command_line (NMSettingMatch *setting, guint idx)
+{
+	g_return_val_if_fail (NM_IS_SETTING_MATCH (setting), NULL);
+
+	g_return_val_if_fail (setting->kernel_command_line && idx < setting->kernel_command_line->len, NULL);
+
+	return g_array_index (setting->kernel_command_line, const char *, idx);
+}
+
+/**
+ * nm_setting_match_add_kernel_command_line:
+ * @setting: the #NMSettingMatch
+ * @kernel_command_line: the kernel command line argument to add
+ *
+ * Adds a new kernel command line argument to the setting.
+ *
+ * Since: 1.26
+ **/
+void
+nm_setting_match_add_kernel_command_line (NMSettingMatch *setting,
+                                          const char *kernel_command_line)
+{
+	g_return_if_fail (NM_IS_SETTING_MATCH (setting));
+	g_return_if_fail (kernel_command_line != NULL);
+	g_return_if_fail (kernel_command_line[0] != '\0');
+
+	nm_strvarray_add (nm_strvarray_ensure (&setting->kernel_command_line), kernel_command_line);
+	_notify (setting, PROP_KERNEL_COMMAND_LINE);
+}
+
+/**
+ * nm_setting_match_remove_kernel_command_line:
+ * @setting: the #NMSettingMatch
+ * @idx: index number of the kernel command line argument
+ *
+ * Removes the kernel command line argument at index @idx.
+ *
+ * Since: 1.26
+ **/
+void
+nm_setting_match_remove_kernel_command_line (NMSettingMatch *setting, guint idx)
+{
+	g_return_if_fail (NM_IS_SETTING_MATCH (setting));
+
+	g_return_if_fail (setting->kernel_command_line && idx < setting->kernel_command_line->len);
+
+	g_array_remove_index (setting->kernel_command_line, idx);
+	_notify (setting, PROP_KERNEL_COMMAND_LINE);
+}
+
+/**
+ * nm_setting_match_remove_kernel_command_line_by_value:
+ * @setting: the #NMSettingMatch
+ * @kernel_command_line: the kernel command line argument name to remove
+ *
+ * Removes @kernel_command_line.
+ *
+ * Returns: %TRUE if the kernel command line argument was found and removed; %FALSE if it was not.
+ *
+ * Since: 1.26
+ **/
+gboolean
+nm_setting_match_remove_kernel_command_line_by_value (NMSettingMatch *setting,
+                                                      const char *kernel_command_line)
+{
+	guint i;
+
+	g_return_val_if_fail (NM_IS_SETTING_MATCH (setting), FALSE);
+	g_return_val_if_fail (kernel_command_line != NULL, FALSE);
+	g_return_val_if_fail (kernel_command_line[0] != '\0', FALSE);
+
+	if (!setting->kernel_command_line)
+		return FALSE;
+
+	for (i = 0; i < setting->kernel_command_line->len; i++) {
+		if (nm_streq (kernel_command_line, g_array_index (setting->kernel_command_line, const char *, i))) {
+			g_array_remove_index (setting->kernel_command_line, i);
+			_notify (setting, PROP_KERNEL_COMMAND_LINE);
+			return TRUE;
+		}
+	}
+	return FALSE;
+}
+
+/**
+ * nm_setting_match_clear_kernel_command_lines:
+ * @setting: the #NMSettingMatch
+ *
+ * Removes all configured kernel command line arguments.
+ *
+ * Since: 1.26
+ **/
+void
+nm_setting_match_clear_kernel_command_lines (NMSettingMatch *setting)
+{
+	g_return_if_fail (NM_IS_SETTING_MATCH (setting));
+
+	if (nm_g_array_len (setting->kernel_command_line) != 0) {
+		nm_clear_pointer (&setting->kernel_command_line, g_array_unref);
+		_notify (setting, PROP_KERNEL_COMMAND_LINE);
+	}
+}
+
+/**
+ * nm_setting_match_get_kernel_command_lines:
+ * @setting: the #NMSettingMatch
+ * @length: (out) (allow-none): the length of the returned interface names array.
+ *
+ * Returns all the interface names.
+ *
+ * Returns: (transfer none) (array length=length): the configured interface names.
+ *
+ * Since: 1.26
+ **/
+const char *const *
+nm_setting_match_get_kernel_command_lines (NMSettingMatch *setting, guint *length)
+{
+	g_return_val_if_fail (NM_IS_SETTING_MATCH (setting), NULL);
+
+	return nm_strvarray_get_strv (&setting->kernel_command_line, length);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_setting_match_get_num_drivers:
+ * @setting: the #NMSettingMatch
+ *
+ * Returns: the number of configured drivers
+ *
+ * Since: 1.26
+ **/
+guint
+nm_setting_match_get_num_drivers (NMSettingMatch *setting)
+{
+	g_return_val_if_fail (NM_IS_SETTING_MATCH (setting), 0);
+
+	return nm_g_array_len (setting->driver);
+}
+
+/**
+ * nm_setting_match_get_driver:
+ * @setting: the #NMSettingMatch
+ * @idx: index number of the DNS search domain to return
+ *
+ * Returns: the driver at index @idx
+ *
+ * Since: 1.26
+ **/
+const char *
+nm_setting_match_get_driver (NMSettingMatch *setting, guint idx)
+{
+	g_return_val_if_fail (NM_IS_SETTING_MATCH (setting), NULL);
+
+	g_return_val_if_fail (setting->driver && idx < setting->driver->len, NULL);
+
+	return g_array_index (setting->driver, const char *, idx);
+}
+
+/**
+ * nm_setting_match_add_driver:
+ * @setting: the #NMSettingMatch
+ * @driver: the driver to add
+ *
+ * Adds a new driver to the setting.
+ *
+ * Since: 1.26
+ **/
+void
+nm_setting_match_add_driver (NMSettingMatch *setting,
+                             const char *driver)
+{
+	g_return_if_fail (NM_IS_SETTING_MATCH (setting));
+	g_return_if_fail (driver != NULL);
+	g_return_if_fail (driver[0] != '\0');
+
+	nm_strvarray_add (nm_strvarray_ensure (&setting->driver), driver);
+	_notify (setting, PROP_DRIVER);
+}
+
+/**
+ * nm_setting_match_remove_driver:
+ * @setting: the #NMSettingMatch
+ * @idx: index number of the driver
+ *
+ * Removes the driver at index @idx.
+ *
+ * Since: 1.26
+ **/
+void
+nm_setting_match_remove_driver (NMSettingMatch *setting, guint idx)
+{
+	g_return_if_fail (NM_IS_SETTING_MATCH (setting));
+
+	g_return_if_fail (setting->driver && idx < setting->driver->len);
+
+	g_array_remove_index (setting->driver, idx);
+	_notify (setting, PROP_DRIVER);
+}
+
+/**
+ * nm_setting_match_remove_driver_by_value:
+ * @setting: the #NMSettingMatch
+ * @driver: the driver to remove
+ *
+ * Removes @driver.
+ *
+ * Returns: %TRUE if the driver was found and removed; %FALSE if it was not.
+ *
+ * Since: 1.26
+ **/
+gboolean
+nm_setting_match_remove_driver_by_value (NMSettingMatch *setting,
+                                         const char *driver)
+{
+	guint i;
+
+	g_return_val_if_fail (NM_IS_SETTING_MATCH (setting), FALSE);
+	g_return_val_if_fail (driver != NULL, FALSE);
+	g_return_val_if_fail (driver[0] != '\0', FALSE);
+
+	if (!setting->driver)
+		return FALSE;
+
+	for (i = 0; i < setting->driver->len; i++) {
+		if (nm_streq (driver, g_array_index (setting->driver, const char *, i))) {
+			g_array_remove_index (setting->driver, i);
+			_notify (setting, PROP_DRIVER);
+			return TRUE;
+		}
+	}
+	return FALSE;
+}
+
+/**
+ * nm_setting_match_clear_drivers:
+ * @setting: the #NMSettingMatch
+ *
+ * Removes all configured drivers.
+ *
+ * Since: 1.26
+ **/
+void
+nm_setting_match_clear_drivers (NMSettingMatch *setting)
+{
+	g_return_if_fail (NM_IS_SETTING_MATCH (setting));
+
+	if (nm_g_array_len (setting->driver) != 0) {
+		nm_clear_pointer (&setting->driver, g_array_unref);
+		_notify (setting, PROP_DRIVER);
+	}
+}
+
+/**
+ * nm_setting_match_get_drivers:
+ * @setting: the #NMSettingMatch
+ * @length: (out) (allow-none): the length of the returned interface names array.
+ *
+ * Returns all the drivers.
+ *
+ * Returns: (transfer none) (array length=length): the configured drivers.
+ *
+ * Since: 1.26
+ **/
+const char *const *
+nm_setting_match_get_drivers (NMSettingMatch *setting, guint *length)
+{
+	g_return_val_if_fail (NM_IS_SETTING_MATCH (setting), NULL);
+
+	return nm_strvarray_get_strv (&setting->driver, length);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_setting_match_get_num_paths:
+ * @setting: the #NMSettingMatch
+ *
+ * Returns: the number of configured paths
+ *
+ * Since: 1.26
+ **/
+guint
+nm_setting_match_get_num_paths (NMSettingMatch *setting)
+{
+	g_return_val_if_fail (NM_IS_SETTING_MATCH (setting), 0);
+
+	return nm_g_array_len (setting->path);
+}
+
+/**
+ * nm_setting_match_get_path:
+ * @setting: the #NMSettingMatch
+ * @idx: index number of the path to return
+ *
+ * Returns: the path at index @idx
+ *
+ * Since: 1.26
+ **/
+const char *
+nm_setting_match_get_path (NMSettingMatch *setting, guint idx)
+{
+	g_return_val_if_fail (NM_IS_SETTING_MATCH (setting), NULL);
+
+	g_return_val_if_fail (setting->path && idx < setting->path->len, NULL);
+
+	return g_array_index (setting->path, const char *, idx);
+}
+
+/**
+ * nm_setting_match_add_path:
+ * @setting: the #NMSettingMatch
+ * @path: the path to add
+ *
+ * Adds a new path to the setting.
+ *
+ * Since: 1.26
+ **/
+void
+nm_setting_match_add_path (NMSettingMatch *setting,
+                           const char *path)
+{
+	g_return_if_fail (NM_IS_SETTING_MATCH (setting));
+	g_return_if_fail (path != NULL);
+	g_return_if_fail (path[0] != '\0');
+
+	nm_strvarray_add (nm_strvarray_ensure (&setting->path), path);
+	_notify (setting, PROP_PATH);
+}
+
+/**
+ * nm_setting_match_remove_path:
+ * @setting: the #NMSettingMatch
+ * @idx: index number of the path
+ *
+ * Removes the path at index @idx.
+ *
+ * Since: 1.26
+ **/
+void
+nm_setting_match_remove_path (NMSettingMatch *setting, guint idx)
+{
+	g_return_if_fail (NM_IS_SETTING_MATCH (setting));
+
+	g_return_if_fail (setting->path && idx < setting->path->len);
+
+	g_array_remove_index (setting->path, idx);
+	_notify (setting, PROP_PATH);
+}
+
+/**
+ * nm_setting_match_remove_path_by_value:
+ * @setting: the #NMSettingMatch
+ * @path: the path to remove
+ *
+ * Removes @path.
+ *
+ * Returns: %TRUE if the path was found and removed; %FALSE if it was not.
+ *
+ * Since: 1.26
+ **/
+gboolean
+nm_setting_match_remove_path_by_value (NMSettingMatch *setting,
+                                       const char *path)
+{
+	guint i;
+
+	g_return_val_if_fail (NM_IS_SETTING_MATCH (setting), FALSE);
+	g_return_val_if_fail (path != NULL, FALSE);
+	g_return_val_if_fail (path[0] != '\0', FALSE);
+
+	if (!setting->path)
+		return FALSE;
+
+	for (i = 0; i < setting->path->len; i++) {
+		if (nm_streq (path, g_array_index (setting->path, const char *, i))) {
+			g_array_remove_index (setting->path, i);
+			_notify (setting, PROP_PATH);
+			return TRUE;
+		}
+	}
+	return FALSE;
+}
+
+/**
+ * nm_setting_match_clear_paths:
+ * @setting: the #NMSettingMatch
+ *
+ * Removes all configured paths.
+ *
+ * Since: 1.26
+ **/
+void
+nm_setting_match_clear_paths (NMSettingMatch *setting)
+{
+	g_return_if_fail (NM_IS_SETTING_MATCH (setting));
+
+	if (nm_g_array_len (setting->path) != 0) {
+		nm_clear_pointer (&setting->path, g_array_unref);
+		_notify (setting, PROP_PATH);
+	}
+}
+
+/**
+ * nm_setting_match_get_paths:
+ * @setting: the #NMSettingMatch
+ * @length: (out) (allow-none): the length of the returned paths array.
+ *
+ * Returns all the paths.
+ *
+ * Returns: (transfer none) (array length=length): the configured paths.
+ *
+ * Since: 1.26
+ **/
+const char *const *
+nm_setting_match_get_paths (NMSettingMatch *setting, guint *length)
+{
+	g_return_val_if_fail (NM_IS_SETTING_MATCH (setting), NULL);
+
+	return nm_strvarray_get_strv (&setting->path, length);
 }
 
 /*****************************************************************************/
@@ -199,7 +659,16 @@ get_property (GObject *object, guint prop_id,
 
 	switch (prop_id) {
 	case PROP_INTERFACE_NAME:
-		g_value_take_boxed (value, _nm_utils_ptrarray_to_strv (self->interface_name));
+		g_value_set_boxed (value, nm_strvarray_get_strv (&self->interface_name, NULL));
+		break;
+	case PROP_KERNEL_COMMAND_LINE:
+		g_value_set_boxed (value, nm_strvarray_get_strv (&self->kernel_command_line, NULL));
+		break;
+	case PROP_DRIVER:
+		g_value_set_boxed (value, nm_strvarray_get_strv (&self->driver, NULL));
+		break;
+	case PROP_PATH:
+		g_value_set_boxed (value, nm_strvarray_get_strv (&self->path, NULL));
 		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@@ -215,8 +684,16 @@ set_property (GObject *object, guint prop_id,
 
 	switch (prop_id) {
 	case PROP_INTERFACE_NAME:
-		g_ptr_array_unref (self->interface_name);
-		self->interface_name = _nm_utils_strv_to_ptrarray (g_value_get_boxed (value));
+		nm_strvarray_set_strv (&self->interface_name, g_value_get_boxed (value));
+		break;
+	case PROP_KERNEL_COMMAND_LINE:
+		nm_strvarray_set_strv (&self->kernel_command_line, g_value_get_boxed (value));
+		break;
+	case PROP_DRIVER:
+		nm_strvarray_set_strv (&self->driver, g_value_get_boxed (value));
+		break;
+	case PROP_PATH:
+		nm_strvarray_set_strv (&self->path, g_value_get_boxed (value));
 		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@@ -229,7 +706,6 @@ set_property (GObject *object, guint prop_id,
 static void
 nm_setting_match_init (NMSettingMatch *setting)
 {
-	setting->interface_name = g_ptr_array_new_with_free_func (g_free);
 }
 
 /**
@@ -247,12 +723,79 @@ nm_setting_match_new (void)
 	return (NMSetting *) g_object_new (NM_TYPE_SETTING_MATCH, NULL);
 }
 
+static gboolean
+verify (NMSetting *setting, NMConnection *connection, GError **error)
+{
+	NMSettingMatch *self = NM_SETTING_MATCH (setting);
+	guint i;
+
+	if (self->interface_name) {
+		for (i = 0; i < self->interface_name->len; i++) {
+			if (nm_str_is_empty (g_array_index (self->interface_name, const char *, i))) {
+				g_set_error (error,
+				             NM_CONNECTION_ERROR,
+				             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+				             _("is empty"));
+				g_prefix_error (error, "%s.%s: ", NM_SETTING_MATCH_SETTING_NAME,
+				                NM_SETTING_MATCH_INTERFACE_NAME);
+				return FALSE;
+			}
+		}
+	}
+
+	if (self->kernel_command_line) {
+		for (i = 0; i < self->kernel_command_line->len; i++) {
+			if (nm_str_is_empty (g_array_index (self->kernel_command_line, const char *, i))) {
+				g_set_error (error,
+				             NM_CONNECTION_ERROR,
+				             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+				             _("is empty"));
+				g_prefix_error (error, "%s.%s: ", NM_SETTING_MATCH_SETTING_NAME,
+				                NM_SETTING_MATCH_KERNEL_COMMAND_LINE);
+				return FALSE;
+			}
+		}
+	}
+
+	if (self->driver) {
+		for (i = 0; i < self->driver->len; i++) {
+			if (nm_str_is_empty (g_array_index (self->driver, const char *, i))) {
+				g_set_error (error,
+				             NM_CONNECTION_ERROR,
+				             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+				             _("is empty"));
+				g_prefix_error (error, "%s.%s: ", NM_SETTING_MATCH_SETTING_NAME,
+				                NM_SETTING_MATCH_DRIVER);
+				return FALSE;
+			}
+		}
+	}
+
+	if (self->path) {
+		for (i = 0; i < self->path->len; i++) {
+			if (nm_str_is_empty (g_array_index (self->path, const char *, i))) {
+				g_set_error (error,
+				             NM_CONNECTION_ERROR,
+				             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+				             _("is empty"));
+				g_prefix_error (error, "%s.%s: ", NM_SETTING_MATCH_SETTING_NAME,
+				                NM_SETTING_MATCH_PATH);
+				return FALSE;
+			}
+		}
+	}
+
+	return TRUE;
+}
+
 static void
 finalize (GObject *object)
 {
 	NMSettingMatch *self = NM_SETTING_MATCH (object);
 
-	g_ptr_array_unref (self->interface_name);
+	nm_clear_pointer (&self->interface_name, g_array_unref);
+	nm_clear_pointer (&self->kernel_command_line, g_array_unref);
+	nm_clear_pointer (&self->driver, g_array_unref);
 
 	G_OBJECT_CLASS (nm_setting_match_parent_class)->finalize (object);
 }
@@ -267,6 +810,8 @@ nm_setting_match_class_init (NMSettingMatchClass *klass)
 	object_class->set_property = set_property;
 	object_class->finalize     = finalize;
 
+	setting_class->verify      = verify;
+
 	/**
 	 * NMSettingMatch:interface-name
 	 *
@@ -288,6 +833,89 @@ nm_setting_match_class_init (NMSettingMatchClass *klass)
 	                        G_PARAM_READWRITE |
 	                        G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * NMSettingMatch:kernel-command-line
+	 *
+	 * 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.
+	 *
+	 * Since: 1.26
+	 **/
+	obj_properties[PROP_KERNEL_COMMAND_LINE] =
+	    g_param_spec_boxed (NM_SETTING_MATCH_KERNEL_COMMAND_LINE, "", "",
+	                        G_TYPE_STRV,
+	                        NM_SETTING_PARAM_FUZZY_IGNORE |
+	                        G_PARAM_READWRITE |
+	                        G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * NMSettingMatch:driver
+	 *
+	 * 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.
+	 *
+	 * Since: 1.26
+	 **/
+	obj_properties[PROP_DRIVER] =
+	    g_param_spec_boxed (NM_SETTING_MATCH_DRIVER, "", "",
+	                        G_TYPE_STRV,
+	                        NM_SETTING_PARAM_FUZZY_IGNORE |
+	                        G_PARAM_READWRITE |
+	                        G_PARAM_STATIC_STRINGS);
+
+
+	/**
+	 * NMSettingMatch:path
+	 *
+	 * 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.
+	 *
+	 * Since: 1.26
+	 **/
+	/* ---ifcfg-rh---
+	 * property: path
+	 * variable: MATCH_PATH
+	 * description: space-separated list of paths to match against the udev
+	 *   property ID_PATHS of devices
+	 * example: MATCH_PATH="pci-0000:01:00.0 pci-0000:0c:00.0"
+	 * ---end---
+	 */
+	obj_properties[PROP_PATH] =
+	    g_param_spec_boxed (NM_SETTING_MATCH_PATH, "", "",
+	                        G_TYPE_STRV,
+	                        NM_SETTING_PARAM_FUZZY_IGNORE |
+	                        G_PARAM_READWRITE |
+	                        G_PARAM_STATIC_STRINGS);
+
 	g_object_class_install_properties (object_class, _PROPERTY_ENUMS_LAST, obj_properties);
 
 	_nm_setting_class_commit (setting_class, NM_META_SETTING_TYPE_MATCH);
diff --git a/libnm-core/nm-setting-match.h b/libnm-core/nm-setting-match.h
index e15911ad..63bd4847 100644
--- a/libnm-core/nm-setting-match.h
+++ b/libnm-core/nm-setting-match.h
@@ -23,7 +23,10 @@ G_BEGIN_DECLS
 
 #define NM_SETTING_MATCH_SETTING_NAME      "match"
 
-#define NM_SETTING_MATCH_INTERFACE_NAME    "interface-name"
+#define NM_SETTING_MATCH_INTERFACE_NAME      "interface-name"
+#define NM_SETTING_MATCH_KERNEL_COMMAND_LINE "kernel-command-line"
+#define NM_SETTING_MATCH_DRIVER              "driver"
+#define NM_SETTING_MATCH_PATH                "path"
 
 typedef struct _NMSettingMatchClass NMSettingMatchClass;
 
@@ -49,6 +52,61 @@ NM_AVAILABLE_IN_1_14
 void nm_setting_match_clear_interface_names (NMSettingMatch *setting);
 NM_AVAILABLE_IN_1_14
 const char *const *nm_setting_match_get_interface_names (NMSettingMatch *setting, guint *length);
+
+
+NM_AVAILABLE_IN_1_26
+guint nm_setting_match_get_num_kernel_command_lines (NMSettingMatch *setting);
+NM_AVAILABLE_IN_1_26
+const char *nm_setting_match_get_kernel_command_line (NMSettingMatch *setting, guint idx);
+NM_AVAILABLE_IN_1_26
+void nm_setting_match_remove_kernel_command_line (NMSettingMatch *setting, guint idx);
+NM_AVAILABLE_IN_1_26
+gboolean nm_setting_match_remove_kernel_command_line_by_value (NMSettingMatch *setting,
+                                                               const char *kernel_command_line);
+NM_AVAILABLE_IN_1_26
+void nm_setting_match_add_kernel_command_line (NMSettingMatch *setting,
+                                               const char *kernel_command_line);
+NM_AVAILABLE_IN_1_26
+void nm_setting_match_clear_kernel_command_lines (NMSettingMatch *setting);
+NM_AVAILABLE_IN_1_26
+const char *const *nm_setting_match_get_kernel_command_lines (NMSettingMatch *setting, guint *length);
+
+
+NM_AVAILABLE_IN_1_26
+guint nm_setting_match_get_num_drivers (NMSettingMatch *setting);
+NM_AVAILABLE_IN_1_26
+const char *nm_setting_match_get_driver (NMSettingMatch *setting, guint idx);
+NM_AVAILABLE_IN_1_26
+void nm_setting_match_remove_driver (NMSettingMatch *setting, guint idx);
+NM_AVAILABLE_IN_1_26
+gboolean nm_setting_match_remove_driver_by_value (NMSettingMatch *setting,
+                                                  const char *driver);
+NM_AVAILABLE_IN_1_26
+void nm_setting_match_add_driver (NMSettingMatch *setting,
+                                  const char *driver);
+NM_AVAILABLE_IN_1_26
+void nm_setting_match_clear_drivers (NMSettingMatch *setting);
+NM_AVAILABLE_IN_1_26
+const char *const *nm_setting_match_get_drivers (NMSettingMatch *setting, guint *length);
+
+
+NM_AVAILABLE_IN_1_26
+guint nm_setting_match_get_num_paths (NMSettingMatch *setting);
+NM_AVAILABLE_IN_1_26
+const char *nm_setting_match_get_path (NMSettingMatch *setting, guint idx);
+NM_AVAILABLE_IN_1_26
+void nm_setting_match_remove_path (NMSettingMatch *setting, guint idx);
+NM_AVAILABLE_IN_1_26
+gboolean nm_setting_match_remove_path_by_value (NMSettingMatch *setting,
+                                                const char *path);
+NM_AVAILABLE_IN_1_26
+void nm_setting_match_add_path (NMSettingMatch *setting,
+                                const char *path);
+NM_AVAILABLE_IN_1_26
+void nm_setting_match_clear_paths (NMSettingMatch *setting);
+NM_AVAILABLE_IN_1_26
+const char *const *nm_setting_match_get_paths (NMSettingMatch *setting, guint *length);
+
 G_END_DECLS
 
 #endif /* NM_SETTING_MATCH_H */
diff --git a/libnm-core/nm-setting-ovs-patch.c b/libnm-core/nm-setting-ovs-patch.c
index 6a9c9f56..5135e6ae 100644
--- a/libnm-core/nm-setting-ovs-patch.c
+++ b/libnm-core/nm-setting-ovs-patch.c
@@ -81,13 +81,7 @@ verify (NMSetting *setting, NMConnection *connection, GError **error)
 		return FALSE;
 	}
 
-	if (   !nm_utils_ipaddr_is_valid (AF_INET, self->peer)
-	    && !nm_utils_ipaddr_is_valid (AF_INET6, self->peer)) {
-		g_set_error (error,
-		             NM_CONNECTION_ERROR,
-		             NM_CONNECTION_ERROR_INVALID_PROPERTY,
-		             _("'%s' is not a valid IP address"),
-		             self->peer);
+	if (!nm_utils_ifname_valid (self->peer, NMU_IFACE_OVS, error)) {
 		g_prefix_error (error, "%s.%s: ",
 		                NM_SETTING_OVS_PATCH_SETTING_NAME,
 		                NM_SETTING_OVS_PATCH_PEER);
@@ -179,8 +173,8 @@ nm_setting_ovs_patch_class_init (NMSettingOvsPatchClass *klass)
 	/**
 	 * NMSettingOvsPatch:peer:
 	 *
-	 * Specifies the unicast destination IP address of a remote Open vSwitch
-	 * bridge port to connect to.
+	 * 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.
 	 *
 	 * Since: 1.10
 	 **/
diff --git a/libnm-core/nm-setting-wireless.c b/libnm-core/nm-setting-wireless.c
index 2ef51088..124f9c45 100644
--- a/libnm-core/nm-setting-wireless.c
+++ b/libnm-core/nm-setting-wireless.c
@@ -222,8 +222,8 @@ nm_setting_wireless_ap_security_compatible (NMSettingWireless *s_wireless,
 			    && !(ap_rsn & NM_802_11_AP_SEC_KEY_MGMT_SAE))
 				return FALSE;
 		} else if (!strcmp (key_mgmt, "owe")) {
-			if (   !(ap_wpa & NM_802_11_AP_SEC_KEY_MGMT_OWE)
-			    && !(ap_rsn & NM_802_11_AP_SEC_KEY_MGMT_OWE))
+			if (   !NM_FLAGS_ANY (ap_wpa, NM_802_11_AP_SEC_KEY_MGMT_OWE | NM_802_11_AP_SEC_KEY_MGMT_OWE_TM)
+			    && !NM_FLAGS_ANY (ap_rsn, NM_802_11_AP_SEC_KEY_MGMT_OWE | NM_802_11_AP_SEC_KEY_MGMT_OWE_TM))
 				return FALSE;
 		}
 
diff --git a/libnm-core/nm-setting.c b/libnm-core/nm-setting.c
index a75302f0..dfb2bed9 100644
--- a/libnm-core/nm-setting.c
+++ b/libnm-core/nm-setting.c
@@ -715,7 +715,7 @@ _nm_setting_to_dbus (NMSetting *setting,
 
 	g_variant_builder_init (&builder, NM_VARIANT_TYPE_SETTING);
 
-	n_properties = _nm_setting_gendata_get_all (setting, &gendata_keys, NULL);
+	n_properties = _nm_setting_option_get_all (setting, &gendata_keys, NULL);
 	for (i = 0; i < n_properties; i++) {
 		g_variant_builder_add (&builder,
 		                       "{sv}",
@@ -873,7 +873,18 @@ init_from_dbus (NMSetting *setting,
 				g_hash_table_remove (keys, key);
 		}
 
-		_nm_setting_gendata_notify (setting, TRUE);
+		_nm_setting_option_notify (setting, TRUE);
+
+		/* Currently, only NMSettingEthtool supports gendata based options, and
+		 * that one has no other properties (except "name"). That means, we
+		 * consumed all options above.
+		 *
+		 * In the future it may be interesting to have settings that are both
+		 * based on gendata and regular properties. In that case, we would need
+		 * to handle this case differently. */
+		nm_assert (nm_streq (G_OBJECT_TYPE_NAME (setting), "NMSettingEthtool"));
+		nm_assert (sett_info->property_infos_len == 1);
+
 		return TRUE;
 	}
 
@@ -1781,7 +1792,7 @@ nm_setting_enumerate_values (NMSetting *setting,
 		/* the properties of this setting are not real GObject properties.
 		 * Hence, this API makes little sense (or does it?). Still, call
 		 * @func with each value. */
-		n_properties = _nm_setting_gendata_get_all (setting, &names, NULL);
+		n_properties = _nm_setting_option_get_all (setting, &names, NULL);
 		if (n_properties > 0) {
 			gs_strfreev char **keys = g_strdupv ((char **) names);
 			GHashTable *h = _gendata_hash (setting, FALSE)->hash;
@@ -2378,7 +2389,7 @@ _gendata_hash (NMSetting *setting, gboolean create_if_necessary)
 }
 
 GHashTable *
-_nm_setting_gendata_hash (NMSetting *setting, gboolean create_if_necessary)
+_nm_setting_option_hash (NMSetting *setting, gboolean create_if_necessary)
 {
 	GenData *gendata;
 
@@ -2387,8 +2398,8 @@ _nm_setting_gendata_hash (NMSetting *setting, gboolean create_if_necessary)
 }
 
 void
-_nm_setting_gendata_notify (NMSetting *setting,
-                            gboolean names_changed)
+_nm_setting_option_notify (NMSetting *setting,
+                           gboolean names_changed)
 {
 	GenData *gendata;
 
@@ -2422,23 +2433,10 @@ out:
 	_nm_setting_emit_property_changed (setting);
 }
 
-GVariant *
-nm_setting_gendata_get (NMSetting *setting,
-                        const char *name)
-{
-	GenData *gendata;
-
-	g_return_val_if_fail (NM_IS_SETTING (setting), NULL);
-	g_return_val_if_fail (name, NULL);
-
-	gendata = _gendata_hash (setting, FALSE);
-	return gendata ? g_hash_table_lookup (gendata->hash, name) : NULL;
-}
-
 guint
-_nm_setting_gendata_get_all (NMSetting *setting,
-                             const char *const**out_names,
-                             GVariant *const**out_values)
+_nm_setting_option_get_all (NMSetting *setting,
+                            const char *const**out_names,
+                            GVariant *const**out_values)
 {
 	GenData *gendata;
 	GHashTable *hash;
@@ -2484,125 +2482,322 @@ out_zero:
 }
 
 /**
- * nm_setting_gendata_get_all_names:
+ * nm_setting_option_get_all_names:
  * @setting: the #NMSetting
  * @out_len: (allow-none) (out):
  *
- * Gives the number of generic data elements and optionally returns all their
- * key names and values. This API is low level access and unless you know what you
- * are doing, it might not be what you want.
+ * Gives the name of all set options.
  *
  * Returns: (array length=out_len zero-terminated=1) (transfer none):
  *   A %NULL terminated array of key names. If no names are present, this returns
  *   %NULL. The returned array and the names are owned by %NMSetting and might be invalidated
- *   soon.
+ *   by the next operation.
  *
- * Since: 1.14
+ * Since: 1.26
  **/
 const char *const*
-nm_setting_gendata_get_all_names (NMSetting *setting,
-                                  guint *out_len)
+nm_setting_option_get_all_names (NMSetting *setting,
+                                 guint *out_len)
 {
 	const char *const*names;
 	guint len;
 
 	g_return_val_if_fail (NM_IS_SETTING (setting), NULL);
 
-	len = _nm_setting_gendata_get_all (setting, &names, NULL);
+	len = _nm_setting_option_get_all (setting, &names, NULL);
 	NM_SET_OUT (out_len, len);
 	return names;
 }
 
-/**
- * nm_setting_gendata_get_all_values:
- * @setting: the #NMSetting
- *
- * Gives the number of generic data elements and optionally returns all their
- * key names and values. This API is low level access and unless you know what you
- * are doing, it might not be what you want.
- *
- * Returns: (array zero-terminated=1) (transfer none):
- *   A %NULL terminated array of #GVariant. If no data is present, this returns
- *   %NULL. The returned array and the variants are owned by %NMSetting and might be invalidated
- *   soon. The sort order of nm_setting_gendata_get_all_names() and nm_setting_gendata_get_all_values()
- *   is consistent. That means, the nth value has the nth name returned by nm_setting_gendata_get_all_names().
- *
- * Since: 1.14
- **/
-GVariant *const*
-nm_setting_gendata_get_all_values (NMSetting *setting)
+gboolean
+_nm_setting_option_clear (NMSetting *setting,
+                          const char *optname)
 {
-	GVariant *const*values;
+	GHashTable *ht;
 
-	g_return_val_if_fail (NM_IS_SETTING (setting), NULL);
+	nm_assert (NM_IS_SETTING (setting));
+	nm_assert (nm_str_not_empty (optname));
+
+	ht = _nm_setting_option_hash (setting, FALSE);
+	if (!ht)
+		return FALSE;
 
-	_nm_setting_gendata_get_all (setting, NULL, &values);
-	return values;
+	return g_hash_table_remove (ht, optname);
 }
 
+/**
+ * nm_setting_option_clear_by_name:
+ * @setting: the #NMSetting
+ * @predicate: (allow-none) (scope call): the predicate for which names
+ *   should be clear.
+ *   If the predicate returns %TRUE for an option name, the option
+ *   gets removed. If %NULL, all options will be removed.
+ *
+ * Since: 1.26
+ */
 void
-_nm_setting_gendata_to_gvalue (NMSetting *setting,
-                               GValue *value)
+nm_setting_option_clear_by_name (NMSetting *setting,
+                                 NMUtilsPredicateStr predicate)
 {
-	GenData *gendata;
-	GHashTable *new;
-	const char *key;
-	GVariant *val;
+	GHashTable *hash;
 	GHashTableIter iter;
+	const char *name;
+	gboolean changed = FALSE;
 
-	nm_assert (NM_IS_SETTING (setting));
-	nm_assert (value);
-	nm_assert (G_TYPE_CHECK_VALUE_TYPE ((value), G_TYPE_HASH_TABLE));
+	g_return_if_fail (NM_IS_SETTING (setting));
 
-	new = g_hash_table_new_full (nm_str_hash, g_str_equal, g_free, (GDestroyNotify) g_variant_unref);
+	hash = _nm_setting_option_hash (NM_SETTING (setting), FALSE);
+	if (!hash)
+		return;
 
-	gendata = _gendata_hash (setting, FALSE);
-	if (gendata) {
-		g_hash_table_iter_init (&iter, gendata->hash);
-		while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &val))
-			g_hash_table_insert (new, g_strdup (key), g_variant_ref (val));
+	if (!predicate) {
+		changed = (g_hash_table_size (hash) > 0);
+		if (changed)
+			g_hash_table_remove_all (hash);
+	} else {
+		g_hash_table_iter_init (&iter, hash);
+		while (g_hash_table_iter_next (&iter, (gpointer *) &name, NULL)) {
+			if (predicate (name)) {
+				g_hash_table_iter_remove (&iter);
+				changed = TRUE;
+			}
+		}
 	}
 
-	g_value_take_boxed (value, new);
+	if (changed)
+		_nm_setting_option_notify (setting, TRUE);
 }
 
-gboolean
-_nm_setting_gendata_reset_from_hash (NMSetting *setting,
-                                     GHashTable *new)
+/*****************************************************************************/
+
+/**
+ * nm_setting_option_get:
+ * @setting: the #NMSetting
+ * @opt_name: the option name to request.
+ *
+ * Returns: (transfer none): the #GVariant or %NULL if the option
+ *   is not set.
+ *
+ * Since: 1.26.
+ */
+GVariant *
+nm_setting_option_get (NMSetting *setting,
+                      const char *opt_name)
 {
 	GenData *gendata;
-	GHashTableIter iter;
-	const char *key;
-	GVariant *val;
-	guint num;
 
-	nm_assert (NM_IS_SETTING (setting));
-	nm_assert (new);
+	g_return_val_if_fail (NM_IS_SETTING (setting), FALSE);
+	g_return_val_if_fail (opt_name, FALSE);
 
-	num = new ? g_hash_table_size (new) : 0;
+	gendata = _gendata_hash (setting, FALSE);
+	return gendata ? g_hash_table_lookup (gendata->hash, opt_name) : NULL;
+}
+
+/**
+ * nm_setting_option_get_boolean:
+ * @setting: the #NMSetting
+ * @opt_name: the option to get
+ * @out_value: (allow-none) (out): the optional output value.
+ *   If the option is unset, %FALSE will be returned.
+ *
+ * Returns: %TRUE if @opt_name is set to a boolean variant.
+ *
+ * Since: 1.26
+ */
+gboolean
+nm_setting_option_get_boolean (NMSetting *setting,
+                               const char *opt_name,
+                               gboolean *out_value)
+{
+	GVariant *v;
 
-	gendata = _gendata_hash (setting, num > 0);
+	v = nm_setting_option_get (NM_SETTING (setting), opt_name);
+	if (   v
+	    && g_variant_is_of_type (v, G_VARIANT_TYPE_BOOLEAN)) {
+		NM_SET_OUT (out_value, g_variant_get_boolean (v));
+		return TRUE;
+	}
+	NM_SET_OUT (out_value, FALSE);
+	return FALSE;
+}
 
-	if (num == 0) {
-		if (   !gendata
-		    || g_hash_table_size (gendata->hash) == 0)
-			return FALSE;
+/**
+ * nm_setting_option_get_uint32:
+ * @setting: the #NMSetting
+ * @opt_name: the option to get
+ * @out_value: (allow-none) (out): the optional output value.
+ *   If the option is unset, 0 will be returned.
+ *
+ * Returns: %TRUE if @opt_name is set to a uint32 variant.
+ *
+ * Since: 1.26
+ */
+gboolean
+nm_setting_option_get_uint32 (NMSetting *setting,
+                              const char *opt_name,
+                              guint32 *out_value)
+{
+	GVariant *v;
 
-		g_hash_table_remove_all (gendata->hash);
-		_nm_setting_gendata_notify (setting, TRUE);
+	v = nm_setting_option_get (NM_SETTING (setting), opt_name);
+	if (   v
+	    && g_variant_is_of_type (v, G_VARIANT_TYPE_UINT32)) {
+		NM_SET_OUT (out_value, g_variant_get_uint32 (v));
 		return TRUE;
 	}
+	NM_SET_OUT (out_value, 0);
+	return FALSE;
+}
+
+/**
+ * nm_setting_option_set:
+ * @setting: the #NMSetting
+ * @opt_name: the option name to set
+ * @variant: (allow-none): the variant to set.
+ *
+ * If @variant is %NULL, this clears the option if it is set.
+ * Otherwise, @variant is set as the option. If @variant is
+ * a floating reference, it will be consumed.
+ *
+ * Note that not all setting types support options. It is a bug
+ * setting a variant to a setting that doesn't support it.
+ * Currently only #NMSettingEthtool supports it.
+ *
+ * Since: 1.26
+ */
+void
+nm_setting_option_set (NMSetting *setting,
+                       const char *opt_name,
+                       GVariant *variant)
+{
+	GVariant *old_variant;
+	gboolean changed_name;
+	gboolean changed_value;
+	GHashTable *hash;
 
-	/* let's not bother to find out whether the new hash has any different
-	 * content the current gendata. Just replace it. */
-	g_hash_table_remove_all (gendata->hash);
-	if (num > 0) {
-		g_hash_table_iter_init (&iter, new);
-		while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &val))
-			g_hash_table_insert (gendata->hash, g_strdup (key), g_variant_ref (val));
+	g_return_if_fail (NM_IS_SETTING (setting));
+	g_return_if_fail (opt_name);
+
+	hash = _nm_setting_option_hash (setting, variant != NULL);
+
+	if (!variant) {
+		if (hash) {
+			if (g_hash_table_remove (hash, opt_name))
+				_nm_setting_option_notify (setting, TRUE);
+		}
+		return;
 	}
-	_nm_setting_gendata_notify (setting, TRUE);
-	return TRUE;
+
+	/* Currently, it is a bug setting any option, unless the setting type supports it.
+	 * And currently, only NMSettingEthtool supports it.
+	 *
+	 * In the future, more setting types may support it. Or we may relax this so
+	 * that options can be attached to all setting types (to indicate "unsupported"
+	 * settings for forward compatibility).
+	 *
+	 * As it is today, internal code will only add gendata options to NMSettingEthtool,
+	 * and there exists not public API to add such options. Still, it is permissible
+	 * to call get(), clear() and set(variant=NULL) also on settings that don't support
+	 * it, as these operations don't add options.
+	 */
+	g_return_if_fail (_nm_setting_class_get_sett_info (NM_SETTING_GET_CLASS (setting))->detail.gendata_info);
+
+	old_variant = g_hash_table_lookup (hash, opt_name);
+
+	changed_name = (old_variant == NULL);
+	changed_value =    changed_name
+	                || !g_variant_equal (old_variant, variant);
+
+	/* We always want to replace the variant, even if it has
+	 * the same value according to g_variant_equal(). The reason
+	 * is that we want to take a reference on @variant, because
+	 * that is what the user might expect. */
+	g_hash_table_insert (hash,
+	                     g_strdup (opt_name),
+	                     g_variant_ref_sink (variant));
+
+	if (changed_value)
+		_nm_setting_option_notify (setting, !changed_name);
+}
+
+/**
+ * nm_setting_option_set_boolean:
+ * @setting: the #NMSetting
+ * @value: the value to set.
+ *
+ * Like nm_setting_option_set() to set a boolean GVariant.
+ *
+ * Since: 1.26
+ */
+void
+nm_setting_option_set_boolean (NMSetting *setting,
+                               const char *opt_name,
+                               gboolean value)
+{
+	GVariant *old_variant;
+	gboolean changed_name;
+	gboolean changed_value;
+	GHashTable *hash;
+
+	g_return_if_fail (NM_IS_SETTING (setting));
+	g_return_if_fail (opt_name);
+
+	value = (!!value);
+
+	hash = _nm_setting_option_hash (setting, TRUE);
+
+	old_variant = g_hash_table_lookup (hash, opt_name);
+
+	changed_name = (old_variant == NULL);
+	changed_value =    changed_name
+	                || (   !g_variant_is_of_type (old_variant, G_VARIANT_TYPE_BOOLEAN)
+	                    || g_variant_get_boolean (old_variant) != value);
+
+	g_hash_table_insert (hash,
+	                     g_strdup (opt_name),
+	                     g_variant_ref_sink (g_variant_new_boolean (value)));
+
+	if (changed_value)
+		_nm_setting_option_notify (setting, !changed_name);
+}
+
+/**
+ * nm_setting_option_set_uint32:
+ * @setting: the #NMSetting
+ * @value: the value to set.
+ *
+ * Like nm_setting_option_set() to set a uint32 GVariant.
+ *
+ * Since: 1.26
+ */
+void
+nm_setting_option_set_uint32 (NMSetting *setting,
+                              const char *opt_name,
+                              guint32 value)
+{
+	GVariant *old_variant;
+	gboolean changed_name;
+	gboolean changed_value;
+	GHashTable *hash;
+
+	g_return_if_fail (NM_IS_SETTING (setting));
+	g_return_if_fail (opt_name);
+
+	hash = _nm_setting_option_hash (setting, TRUE);
+
+	old_variant = g_hash_table_lookup (hash, opt_name);
+
+	changed_name = (old_variant == NULL);
+	changed_value =    changed_name
+	                || (   !g_variant_is_of_type (old_variant, G_VARIANT_TYPE_UINT32)
+	                    || g_variant_get_uint32 (old_variant) != value);
+
+	g_hash_table_insert (hash,
+	                     g_strdup (opt_name),
+	                     g_variant_ref_sink (g_variant_new_uint32 (value)));
+
+	if (changed_value)
+		_nm_setting_option_notify (setting, !changed_name);
 }
 
 /*****************************************************************************/
diff --git a/libnm-core/nm-setting.h b/libnm-core/nm-setting.h
index 26c1b1ee..01cd1503 100644
--- a/libnm-core/nm-setting.h
+++ b/libnm-core/nm-setting.h
@@ -333,6 +333,46 @@ gboolean    nm_setting_set_secret_flags (NMSetting *setting,
 
 /*****************************************************************************/
 
+NM_AVAILABLE_IN_1_26
+GVariant *nm_setting_option_get (NMSetting *setting,
+                                 const char *opt_name);
+
+NM_AVAILABLE_IN_1_26
+gboolean nm_setting_option_get_boolean (NMSetting *setting,
+                                        const char *opt_name,
+                                        gboolean *out_value);
+
+NM_AVAILABLE_IN_1_26
+gboolean nm_setting_option_get_uint32 (NMSetting *setting,
+                                       const char *opt_name,
+                                       guint32 *out_value);
+
+NM_AVAILABLE_IN_1_26
+void nm_setting_option_set (NMSetting *setting,
+                            const char *opt_name,
+                            GVariant *variant);
+
+NM_AVAILABLE_IN_1_26
+void nm_setting_option_set_uint32 (NMSetting *setting,
+                                   const char *opt_name,
+                                   guint32 value);
+
+NM_AVAILABLE_IN_1_26
+void nm_setting_option_set_boolean (NMSetting *setting,
+                                    const char *opt_name,
+                                    gboolean value);
+
+NM_AVAILABLE_IN_1_26
+const char *const*nm_setting_option_get_all_names (NMSetting *setting,
+                                                   guint *out_len);
+
+
+NM_AVAILABLE_IN_1_26
+void nm_setting_option_clear_by_name (NMSetting *setting,
+                                      NMUtilsPredicateStr predicate);
+
+/*****************************************************************************/
+
 const GVariantType *nm_setting_get_dbus_property_type (NMSetting *setting,
                                                        const char *property_name);
 
diff --git a/libnm-core/nm-utils-private.h b/libnm-core/nm-utils-private.h
index f3e9d3f1..dd3785be 100644
--- a/libnm-core/nm-utils-private.h
+++ b/libnm-core/nm-utils-private.h
@@ -67,11 +67,6 @@ char *      _nm_utils_hwaddr_canonical_or_invalid (const char *mac, gssize lengt
 
 gboolean    _nm_utils_hwaddr_link_local_valid (const char *mac);
 
-void        _nm_utils_format_variant_attributes_full (GString *str,
-                                                      const NMUtilsNamedValue *values,
-                                                      guint num_values,
-                                                      char attr_separator,
-                                                      char key_value_separator);
 gboolean    _nm_sriov_vf_parse_vlans (NMSriovVF *vf, const char *str, GError **error);
 
 gboolean    _nm_utils_bridge_vlan_verify_list (GPtrArray *vlans,
diff --git a/libnm-core/nm-utils.c b/libnm-core/nm-utils.c
index fed3f110..c88afb32 100644
--- a/libnm-core/nm-utils.c
+++ b/libnm-core/nm-utils.c
@@ -48,6 +48,20 @@
 
 /*****************************************************************************/
 
+/**
+ * NMUtilsPredicateStr:
+ * @str: the name to check.
+ *
+ * This function takes a string argument and returns either %TRUE or %FALSE.
+ * It is a general purpose predicate, for example used by nm_setting_option_clear_by_name().
+ *
+ * Returns: %TRUE if the predicate function matches.
+ *
+ * Since: 1.26
+ */
+
+/*****************************************************************************/
+
 struct _NMSockAddrEndpoint {
 	const char *host;
 	guint16 port;
@@ -1252,7 +1266,7 @@ nm_utils_security_valid (NMUtilsSecurityType type,
 			return FALSE;
 		if (!have_ap)
 			return TRUE;
-		if (!(ap_rsn & NM_802_11_AP_SEC_KEY_MGMT_OWE))
+		if (!NM_FLAGS_ANY (ap_rsn, NM_802_11_AP_SEC_KEY_MGMT_OWE | NM_802_11_AP_SEC_KEY_MGMT_OWE_TM))
 			return FALSE;
 		return TRUE;
 	case NMU_SEC_INVALID:
@@ -2286,11 +2300,29 @@ fail:
 }
 
 static const NMVariantAttributeSpec *const tc_object_attribute_spec[] = {
-	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("root",   G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE,                                         ),
-	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("parent", G_VARIANT_TYPE_STRING,                                           .str_type = 'a', ),
-	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("handle", G_VARIANT_TYPE_STRING,                                           .str_type = 'a', ),
-	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("kind",   G_VARIANT_TYPE_STRING,  .no_value = TRUE,                        .str_type = 'a', ),
-	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("",       G_VARIANT_TYPE_STRING,  .no_value = TRUE, .consumes_rest = TRUE, .str_type = 'a', ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("root",   G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE,                        ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("parent", G_VARIANT_TYPE_STRING,                                           ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("handle", G_VARIANT_TYPE_STRING,                                           ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("kind",   G_VARIANT_TYPE_STRING,  .no_value = TRUE,                        ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("",       G_VARIANT_TYPE_STRING,  .no_value = TRUE, .consumes_rest = TRUE, ),
+	NULL,
+};
+
+static const NMVariantAttributeSpec *const tc_qdisc_sfq_spec[] = {
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("quantum",     G_VARIANT_TYPE_UINT32,                    ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("perturb",     G_VARIANT_TYPE_INT32,                     ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("limit",       G_VARIANT_TYPE_UINT32,                    ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("divisor",     G_VARIANT_TYPE_UINT32,                    ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("flows",       G_VARIANT_TYPE_UINT32,                    ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("depth",       G_VARIANT_TYPE_UINT32,                    ),
+	NULL,
+};
+
+static const NMVariantAttributeSpec *const tc_qdisc_tbf_spec[] = {
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("rate",        G_VARIANT_TYPE_UINT64,                    ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("burst",       G_VARIANT_TYPE_UINT32,                    ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("limit",       G_VARIANT_TYPE_UINT32,                    ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("latency",     G_VARIANT_TYPE_UINT32,                    ),
 	NULL,
 };
 
@@ -2321,6 +2353,8 @@ typedef struct {
 
 static const NMQdiscAttributeSpec *const tc_qdisc_attribute_spec[] = {
 	&(const NMQdiscAttributeSpec) { "fq_codel", tc_qdisc_fq_codel_spec },
+	&(const NMQdiscAttributeSpec) { "sfq",      tc_qdisc_sfq_spec },
+	&(const NMQdiscAttributeSpec) { "tbf",      tc_qdisc_tbf_spec },
 	NULL,
 };
 
@@ -2500,7 +2534,7 @@ nm_utils_tc_qdisc_from_str (const char *str, GError **error)
 			break;
 		}
 	}
-	nm_clear_pointer (&rest, g_free);
+	nm_clear_g_free (&rest);
 
 	if (options) {
 		value = g_hash_table_lookup (options, "");
@@ -2536,17 +2570,17 @@ static const NMVariantAttributeSpec *const tc_action_simple_attribute_spec[] = {
 };
 
 static const NMVariantAttributeSpec *const tc_action_mirred_attribute_spec[] = {
-	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("egress",   G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE,                  ),
-	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("ingress",  G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE,                  ),
-	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("mirror",   G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE,                  ),
-	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("redirect", G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE,                  ),
-	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("dev",      G_VARIANT_TYPE_STRING,  .no_value = TRUE, .str_type = 'a', ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("egress",   G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE, ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("ingress",  G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE, ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("mirror",   G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE, ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("redirect", G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE, ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("dev",      G_VARIANT_TYPE_STRING,                    ),
 	NULL,
 };
 
 static const NMVariantAttributeSpec *const tc_action_attribute_spec[] = {
-	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("kind",    G_VARIANT_TYPE_STRING, .no_value = TRUE,                        .str_type = 'a', ),
-	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("",        G_VARIANT_TYPE_STRING, .no_value = TRUE, .consumes_rest = TRUE, .str_type = 'a', ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("kind",    G_VARIANT_TYPE_STRING, .no_value = TRUE,                        ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("",        G_VARIANT_TYPE_STRING, .no_value = TRUE, .consumes_rest = TRUE, ),
 	NULL,
 };
 
@@ -2744,8 +2778,8 @@ nm_utils_tc_tfilter_to_str (NMTCTfilter *tfilter, GError **error)
 }
 
 static const NMVariantAttributeSpec *const tc_tfilter_attribute_spec[] = {
-	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("action", G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE,                                         ),
-	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("",       G_VARIANT_TYPE_STRING,  .no_value = TRUE, .consumes_rest = TRUE, .str_type = 'a', ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("action", G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE,                        ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE ("",       G_VARIANT_TYPE_STRING,  .no_value = TRUE, .consumes_rest = TRUE, ),
 	NULL,
 };
 
@@ -4682,7 +4716,7 @@ nm_utils_is_valid_iface_name_utf8safe (const char *utf8safe_name)
 
 	g_return_val_if_fail (utf8safe_name, FALSE);
 
-	bin = nm_utils_buf_utf8safe_unescape (utf8safe_name, &len, &bin_to_free);
+	bin = nm_utils_buf_utf8safe_unescape (utf8safe_name, NM_UTILS_STR_UTF8_SAFE_FLAG_NONE, &len, &bin_to_free);
 
 	if (bin_to_free) {
 		/* some unescaping happened... */
@@ -5449,23 +5483,6 @@ nm_utils_is_json_object (const char *str, GError **error)
 }
 
 static char *
-attribute_escape (const char *src, char c1, char c2)
-{
-	char *ret, *dest;
-
-	dest = ret = g_malloc (strlen (src) * 2 + 1);
-
-	while (*src) {
-		if (*src == c1 || *src == c2 || *src == '\\')
-			*dest++ = '\\';
-		*dest++ = *src++;
-	}
-	*dest++ = '\0';
-
-	return ret;
-}
-
-static char *
 attribute_unescape (const char *start, const char *end)
 {
 	char *ret, *dest;
@@ -5657,6 +5674,24 @@ nm_utils_parse_variant_attributes (const char *string,
 					return NULL;
 				}
 				variant = g_variant_new_uint32 (num);
+			} else if (g_variant_type_equal ((*s)->type, G_VARIANT_TYPE_INT32)) {
+					gint64 num = _nm_utils_ascii_str_to_int64 (value, 10, G_MININT32, G_MAXINT32, G_MAXINT64);
+
+					if (num == G_MAXINT64) {
+						g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+						             _("invalid int32 value '%s' for attribute '%s'"), value, (*s)->name);
+						return NULL;
+					}
+					variant = g_variant_new_int32 (num);
+			} else if (g_variant_type_equal ((*s)->type, G_VARIANT_TYPE_UINT64)) {
+					guint64 num = _nm_utils_ascii_str_to_uint64 (value, 10, 0, G_MAXUINT64, G_MAXUINT64);
+
+					if (num == G_MAXUINT64 && errno != 0) {
+						g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+						             _("invalid uint64 value '%s' for attribute '%s'"), value, (*s)->name);
+						return NULL;
+					}
+					variant = g_variant_new_uint64 (num);
 			} else if (g_variant_type_equal ((*s)->type, G_VARIANT_TYPE_BYTE)) {
 				gint64 num = _nm_utils_ascii_str_to_int64 (value, 10, 0, G_MAXUINT8, -1);
 
@@ -5699,55 +5734,6 @@ next:
 	return g_steal_pointer (&ht);
 }
 
-void
-_nm_utils_format_variant_attributes_full (GString *str,
-                                          const NMUtilsNamedValue *values,
-                                          guint num_values,
-                                          char attr_separator,
-                                          char key_value_separator)
-{
-	const char *name, *value;
-	GVariant *variant;
-	char *escaped;
-	char buf[64];
-	char sep = 0;
-	guint i;
-
-	for (i = 0; i < num_values; i++) {
-		name = values[i].name;
-		variant = (GVariant *) values[i].value_ptr;
-		value = NULL;
-
-		if (g_variant_is_of_type (variant, G_VARIANT_TYPE_UINT32))
-			value = nm_sprintf_buf (buf, "%u", g_variant_get_uint32 (variant));
-		else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_BYTE))
-			value = nm_sprintf_buf (buf, "%hhu", g_variant_get_byte (variant));
-		else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_BOOLEAN))
-			value = g_variant_get_boolean (variant) ? "true" : "false";
-		else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING))
-			value = g_variant_get_string (variant, NULL);
-		else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_BYTESTRING))
-			value = g_variant_get_bytestring (variant);
-		else
-			continue;
-
-		if (sep)
-			g_string_append_c (str, sep);
-
-		escaped = attribute_escape (name, attr_separator, key_value_separator);
-		g_string_append (str, escaped);
-		g_free (escaped);
-
-		g_string_append_c (str, key_value_separator);
-
-		escaped = attribute_escape (value, attr_separator, key_value_separator);
-		g_string_append (str, escaped);
-		g_free (escaped);
-
-		sep = attr_separator;
-	}
-}
-
 /*
  * nm_utils_format_variant_attributes:
  * @attributes: (element-type utf8 GVariant): a #GHashTable mapping attribute names to #GVariant values
@@ -5766,25 +5752,9 @@ nm_utils_format_variant_attributes (GHashTable *attributes,
                                     char attr_separator,
                                     char key_value_separator)
 {
-	GString *str = NULL;
-	gs_free NMUtilsNamedValue *values = NULL;
-	guint len;
-
-	g_return_val_if_fail (attr_separator, NULL);
-	g_return_val_if_fail (key_value_separator, NULL);
-
-	if (!attributes || !g_hash_table_size (attributes))
-		return NULL;
-
-	values = nm_utils_named_values_from_str_dict (attributes, &len);
-
-	str = g_string_new ("");
-	_nm_utils_format_variant_attributes_full (str,
-	                                          values,
-	                                          len,
-	                                          attr_separator,
-	                                          key_value_separator);
-	return g_string_free (str, FALSE);
+	return _nm_utils_format_variant_attributes (attributes,
+	                                            attr_separator,
+	                                            key_value_separator);
 }
 
 /*****************************************************************************/
@@ -5865,7 +5835,7 @@ nm_utils_base64secret_decode (const char *base64_key,
                               gsize required_key_len,
                               guint8 *out_key)
 {
-	gs_free guint8 *bin_arr = NULL;
+	nm_auto_free guint8 *bin_arr = NULL;
 	gsize base64_key_len;
 	gsize bin_len;
 	int r;
diff --git a/libnm-core/nm-version.h b/libnm-core/nm-version.h
index 195fc196..20384037 100644
--- a/libnm-core/nm-version.h
+++ b/libnm-core/nm-version.h
@@ -229,6 +229,20 @@
 # define NM_AVAILABLE_IN_1_24
 #endif
 
+#if NM_VERSION_MIN_REQUIRED >= NM_VERSION_1_26
+# define NM_DEPRECATED_IN_1_26           G_DEPRECATED
+# define NM_DEPRECATED_IN_1_26_FOR(f)    G_DEPRECATED_FOR(f)
+#else
+# define NM_DEPRECATED_IN_1_26
+# define NM_DEPRECATED_IN_1_26_FOR(f)
+#endif
+
+#if NM_VERSION_MAX_ALLOWED < NM_VERSION_1_26
+# define NM_AVAILABLE_IN_1_26            G_UNAVAILABLE(1,26)
+#else
+# define NM_AVAILABLE_IN_1_26
+#endif
+
 /*
  * Synchronous API for calling D-Bus in libnm is deprecated. See
  * https://developer.gnome.org/libnm/stable/usage.html#sync-api
diff --git a/libnm-core/tests/test-general.c b/libnm-core/tests/test-general.c
index 2b1f1e3d..a6fb7007 100644
--- a/libnm-core/tests/test-general.c
+++ b/libnm-core/tests/test-general.c
@@ -3334,6 +3334,7 @@ test_connection_diff_a_only (void)
 			{ NM_SETTING_CONNECTION_AUTH_RETRIES,         NM_SETTING_DIFF_RESULT_IN_A },
 			{ NM_SETTING_CONNECTION_MDNS,                 NM_SETTING_DIFF_RESULT_IN_A },
 			{ NM_SETTING_CONNECTION_LLMNR,                NM_SETTING_DIFF_RESULT_IN_A },
+			{ NM_SETTING_CONNECTION_MUD_URL,              NM_SETTING_DIFF_RESULT_IN_A },
 			{ NM_SETTING_CONNECTION_WAIT_DEVICE_TIMEOUT,  NM_SETTING_DIFF_RESULT_IN_A },
 			{ NULL, NM_SETTING_DIFF_RESULT_UNKNOWN }
 		} },
@@ -7814,7 +7815,7 @@ _do_test_utils_str_utf8safe_unescape (const char *str, const char *expected, gsi
 	gs_free gpointer buf_free_1 = NULL;
 	gs_free char *str_free_1 = NULL;
 
-	s = nm_utils_buf_utf8safe_unescape (str, &l, &buf_free_1);
+	s = nm_utils_buf_utf8safe_unescape (str, NM_UTILS_STR_UTF8_SAFE_FLAG_NONE, &l, &buf_free_1);
 	g_assert_cmpint (expected_len, ==, l);
 	g_assert_cmpstr (s, ==, expected);
 
@@ -7838,7 +7839,7 @@ _do_test_utils_str_utf8safe_unescape (const char *str, const char *expected, gsi
 	if (   expected
 	    && l == strlen (expected)) {
 		/* there are no embeeded NULs. Check that nm_utils_str_utf8safe_unescape() yields the same result. */
-		s = nm_utils_str_utf8safe_unescape (str, &str_free_1);
+		s = nm_utils_str_utf8safe_unescape (str, NM_UTILS_STR_UTF8_SAFE_FLAG_NONE, &str_free_1);
 		g_assert_cmpstr (s, ==, expected);
 		if (strchr (str, '\\')) {
 			g_assert (str_free_1 != str);
@@ -7907,10 +7908,10 @@ _do_test_utils_str_utf8safe (const char *str, gsize str_len, const char *expecte
 			g_assert (g_utf8_validate (str, -1, NULL));
 		}
 
-		g_assert (str == nm_utils_str_utf8safe_unescape (str_safe, &str_free_4));
+		g_assert (str == nm_utils_str_utf8safe_unescape (str_safe, NM_UTILS_STR_UTF8_SAFE_FLAG_NONE, &str_free_4));
 		g_assert (!str_free_4);
 
-		str_free_5 = nm_utils_str_utf8safe_unescape_cp (str_safe);
+		str_free_5 = nm_utils_str_utf8safe_unescape_cp (str_safe, NM_UTILS_STR_UTF8_SAFE_FLAG_NONE);
 		if (str) {
 			g_assert (str_free_5 != str);
 			g_assert_cmpstr (str_free_5, ==, str);
@@ -7934,11 +7935,11 @@ _do_test_utils_str_utf8safe (const char *str, gsize str_len, const char *expecte
 		str_free_6 = g_strcompress (str_safe);
 		g_assert_cmpstr (str, ==, str_free_6);
 
-		str_free_7 = nm_utils_str_utf8safe_unescape_cp (str_safe);
+		str_free_7 = nm_utils_str_utf8safe_unescape_cp (str_safe, NM_UTILS_STR_UTF8_SAFE_FLAG_NONE);
 		g_assert (str_free_7 != str);
 		g_assert_cmpstr (str_free_7, ==, str);
 
-		s = nm_utils_str_utf8safe_unescape (str_safe, &str_free_8);
+		s = nm_utils_str_utf8safe_unescape (str_safe, NM_UTILS_STR_UTF8_SAFE_FLAG_NONE, &str_free_8);
 		g_assert (str_free_8 != str);
 		g_assert (s == str_free_8);
 		g_assert_cmpstr (str_free_8, ==, str);
diff --git a/libnm-core/tests/test-keyfile.c b/libnm-core/tests/test-keyfile.c
index 47a27571..23149b61 100644
--- a/libnm-core/tests/test-keyfile.c
+++ b/libnm-core/tests/test-keyfile.c
@@ -143,7 +143,7 @@ _nm_keyfile_write (NMConnection *connection,
 
 	g_assert (NM_IS_CONNECTION (connection));
 
-	kf = nm_keyfile_write (connection, handler, user_data, &error);
+	kf = nm_keyfile_write (connection, NM_KEYFILE_HANDLER_FLAGS_NONE, handler, user_data, &error);
 	g_assert_no_error (error);
 	g_assert (kf);
 	return kf;
@@ -167,7 +167,7 @@ _nm_keyfile_read (GKeyFile *keyfile,
 	base_dir = g_path_get_dirname (keyfile_name);
 	filename = g_path_get_basename (keyfile_name);
 
-	con = nm_keyfile_read (keyfile, base_dir, read_handler, read_data, &error);
+	con = nm_keyfile_read (keyfile, base_dir, NM_KEYFILE_HANDLER_FLAGS_NONE, read_handler, read_data, &error);
 	g_assert_no_error (error);
 	g_assert (NM_IS_CONNECTION (con));
 
diff --git a/libnm-core/tests/test-setting.c b/libnm-core/tests/test-setting.c
index 072be57a..359c20c4 100644
--- a/libnm-core/tests/test-setting.c
+++ b/libnm-core/tests/test-setting.c
@@ -1499,7 +1499,53 @@ test_team_setting (void)
 /*****************************************************************************/
 
 static void
-test_ethtool_1 (void)
+_setting_ethtool_set_feature (NMSettingEthtool *s_ethtool,
+                              const char *opt_name,
+                              NMTernary value)
+{
+	g_assert (NM_IS_SETTING_ETHTOOL (s_ethtool));
+
+	if (nmtst_get_rand_bool ()) {
+		nm_setting_ethtool_set_feature (s_ethtool, opt_name, value);
+		return;
+	}
+
+	if (value == NM_TERNARY_DEFAULT) {
+		nm_setting_option_set (NM_SETTING (s_ethtool), opt_name, NULL);
+		return;
+	}
+
+	if (nmtst_get_rand_bool ())
+		nm_setting_option_set_boolean (NM_SETTING (s_ethtool), opt_name, value);
+	else
+		nm_setting_option_set (NM_SETTING (s_ethtool), opt_name, g_variant_new_boolean (value));
+}
+
+static NMTernary
+_setting_ethtool_get_feature (NMSettingEthtool *s_ethtool,
+                              const char *opt_name)
+{
+	GVariant *v;
+	gboolean b;
+
+	switch (nmtst_get_rand_uint32 () % 3) {
+	case 0:
+		return nm_setting_ethtool_get_feature (s_ethtool, opt_name);
+	case 1:
+		if (!nm_setting_option_get_boolean (NM_SETTING (s_ethtool), opt_name, &b))
+			return NM_TERNARY_DEFAULT;
+		return b;
+	default:
+		v = nm_setting_option_get (NM_SETTING (s_ethtool), opt_name);
+		if (   !v
+		    || !g_variant_is_of_type (v, G_VARIANT_TYPE_BOOLEAN))
+			return NM_TERNARY_DEFAULT;
+		return g_variant_get_boolean (v);
+	}
+}
+
+static void
+test_ethtool_features (void)
 {
 	gs_unref_object NMConnection *con = NULL;
 	gs_unref_object NMConnection *con2 = NULL;
@@ -1519,16 +1565,87 @@ test_ethtool_1 (void)
 	s_ethtool = NM_SETTING_ETHTOOL (nm_setting_ethtool_new ());
 	nm_connection_add_setting (con, NM_SETTING (s_ethtool));
 
-	nm_setting_ethtool_set_feature (s_ethtool,
-	                                NM_ETHTOOL_OPTNAME_FEATURE_RX,
-	                                NM_TERNARY_TRUE);
-	nm_setting_ethtool_set_feature (s_ethtool,
-	                                NM_ETHTOOL_OPTNAME_FEATURE_LRO,
-	                                NM_TERNARY_FALSE);
+	_setting_ethtool_set_feature (s_ethtool,
+	                              NM_ETHTOOL_OPTNAME_FEATURE_RX,
+	                              NM_TERNARY_TRUE);
+	_setting_ethtool_set_feature (s_ethtool,
+	                              NM_ETHTOOL_OPTNAME_FEATURE_LRO,
+	                              NM_TERNARY_FALSE);
+
+	g_assert_cmpint (_setting_ethtool_get_feature (s_ethtool, NM_ETHTOOL_OPTNAME_FEATURE_RX), ==, NM_TERNARY_TRUE);
+	g_assert_cmpint (_setting_ethtool_get_feature (s_ethtool, NM_ETHTOOL_OPTNAME_FEATURE_LRO), ==, NM_TERNARY_FALSE);
+	g_assert_cmpint (_setting_ethtool_get_feature (s_ethtool, NM_ETHTOOL_OPTNAME_FEATURE_SG),  ==, NM_TERNARY_DEFAULT);
+
+	nmtst_connection_normalize (con);
+
+	variant = nm_connection_to_dbus (con, NM_CONNECTION_SERIALIZE_ALL);
+
+	con2 = nm_simple_connection_new_from_dbus (variant, &error);
+	nmtst_assert_success (con2, error);
+
+	s_ethtool2 = NM_SETTING_ETHTOOL (nm_connection_get_setting (con2, NM_TYPE_SETTING_ETHTOOL));
+
+	g_assert_cmpint (_setting_ethtool_get_feature (s_ethtool2, NM_ETHTOOL_OPTNAME_FEATURE_RX),  ==, NM_TERNARY_TRUE);
+	g_assert_cmpint (_setting_ethtool_get_feature (s_ethtool2, NM_ETHTOOL_OPTNAME_FEATURE_LRO), ==, NM_TERNARY_FALSE);
+	g_assert_cmpint (_setting_ethtool_get_feature (s_ethtool2, NM_ETHTOOL_OPTNAME_FEATURE_SG),  ==, NM_TERNARY_DEFAULT);
+
+	nmtst_assert_connection_verifies_without_normalization (con2);
+
+	nmtst_assert_connection_equals (con, FALSE, con2, FALSE);
+
+	keyfile = nm_keyfile_write (con, NM_KEYFILE_HANDLER_FLAGS_NONE, NULL, NULL, &error);
+	nmtst_assert_success (keyfile, error);
+
+	con3 = nm_keyfile_read (keyfile,
+	                        "/ignored/current/working/directory/for/loading/relative/paths",
+	                        NM_KEYFILE_HANDLER_FLAGS_NONE,
+	                        NULL,
+	                        NULL,
+	                        &error);
+	nmtst_assert_success (con3, error);
+
+	nm_keyfile_read_ensure_id (con3, "unused-because-already-has-id");
+	nm_keyfile_read_ensure_uuid (con3, "unused-because-already-has-uuid");
+
+	nmtst_connection_normalize (con3);
+
+	nmtst_assert_connection_equals (con, FALSE, con3, FALSE);
+
+	s_ethtool3 = NM_SETTING_ETHTOOL (nm_connection_get_setting (con3, NM_TYPE_SETTING_ETHTOOL));
+
+	g_assert_cmpint (_setting_ethtool_get_feature (s_ethtool3, NM_ETHTOOL_OPTNAME_FEATURE_RX),  ==, NM_TERNARY_TRUE);
+	g_assert_cmpint (_setting_ethtool_get_feature (s_ethtool3, NM_ETHTOOL_OPTNAME_FEATURE_LRO), ==, NM_TERNARY_FALSE);
+	g_assert_cmpint (_setting_ethtool_get_feature (s_ethtool3, NM_ETHTOOL_OPTNAME_FEATURE_SG),  ==, NM_TERNARY_DEFAULT);
+}
+
+static void
+test_ethtool_coalesce (void)
+{
+	gs_unref_object NMConnection *con = NULL;
+	gs_unref_object NMConnection *con2 = NULL;
+	gs_unref_object NMConnection *con3 = NULL;
+	gs_unref_variant GVariant *variant = NULL;
+	gs_free_error GError *error = NULL;
+	gs_unref_keyfile GKeyFile *keyfile = NULL;
+	NMSettingConnection *s_con;
+	NMSettingEthtool *s_ethtool;
+	NMSettingEthtool *s_ethtool2;
+	NMSettingEthtool *s_ethtool3;
+	guint32 u32;
+
+	con = nmtst_create_minimal_connection ("ethtool-coalesce",
+	                                        NULL,
+	                                        NM_SETTING_WIRED_SETTING_NAME,
+	                                        &s_con);
+	s_ethtool = NM_SETTING_ETHTOOL (nm_setting_ethtool_new ());
+	nm_connection_add_setting (con, NM_SETTING (s_ethtool));
+
+	nm_setting_option_set_uint32 (NM_SETTING (s_ethtool),
+	                              NM_ETHTOOL_OPTNAME_COALESCE_RX_FRAMES,
+	                              4);
 
-	g_assert_cmpint (nm_setting_ethtool_get_feature (s_ethtool, NM_ETHTOOL_OPTNAME_FEATURE_RX), ==, NM_TERNARY_TRUE);
-	g_assert_cmpint (nm_setting_ethtool_get_feature (s_ethtool, NM_ETHTOOL_OPTNAME_FEATURE_LRO), ==, NM_TERNARY_FALSE);
-	g_assert_cmpint (nm_setting_ethtool_get_feature (s_ethtool, NM_ETHTOOL_OPTNAME_FEATURE_SG),  ==, NM_TERNARY_DEFAULT);
+	g_assert_true (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool), NM_ETHTOOL_OPTNAME_COALESCE_RX_FRAMES, &u32));
+	g_assert_cmpuint (u32, ==, 4);
 
 	nmtst_connection_normalize (con);
 
@@ -1539,19 +1656,19 @@ test_ethtool_1 (void)
 
 	s_ethtool2 = NM_SETTING_ETHTOOL (nm_connection_get_setting (con2, NM_TYPE_SETTING_ETHTOOL));
 
-	g_assert_cmpint (nm_setting_ethtool_get_feature (s_ethtool2, NM_ETHTOOL_OPTNAME_FEATURE_RX),  ==, NM_TERNARY_TRUE);
-	g_assert_cmpint (nm_setting_ethtool_get_feature (s_ethtool2, NM_ETHTOOL_OPTNAME_FEATURE_LRO), ==, NM_TERNARY_FALSE);
-	g_assert_cmpint (nm_setting_ethtool_get_feature (s_ethtool2, NM_ETHTOOL_OPTNAME_FEATURE_SG),  ==, NM_TERNARY_DEFAULT);
+	g_assert_true (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool2), NM_ETHTOOL_OPTNAME_COALESCE_RX_FRAMES, &u32));
+	g_assert_cmpuint (u32, ==, 4);
 
 	nmtst_assert_connection_verifies_without_normalization (con2);
 
 	nmtst_assert_connection_equals (con, FALSE, con2, FALSE);
 
-	keyfile = nm_keyfile_write (con, NULL, NULL, &error);
+	keyfile = nm_keyfile_write (con, NM_KEYFILE_HANDLER_FLAGS_NONE, NULL, NULL, &error);
 	nmtst_assert_success (keyfile, error);
 
 	con3 = nm_keyfile_read (keyfile,
 	                        "/ignored/current/working/directory/for/loading/relative/paths",
+	                        NM_KEYFILE_HANDLER_FLAGS_NONE,
 	                        NULL,
 	                        NULL,
 	                        &error);
@@ -1566,9 +1683,109 @@ test_ethtool_1 (void)
 
 	s_ethtool3 = NM_SETTING_ETHTOOL (nm_connection_get_setting (con3, NM_TYPE_SETTING_ETHTOOL));
 
-	g_assert_cmpint (nm_setting_ethtool_get_feature (s_ethtool3, NM_ETHTOOL_OPTNAME_FEATURE_RX),  ==, NM_TERNARY_TRUE);
-	g_assert_cmpint (nm_setting_ethtool_get_feature (s_ethtool3, NM_ETHTOOL_OPTNAME_FEATURE_LRO), ==, NM_TERNARY_FALSE);
-	g_assert_cmpint (nm_setting_ethtool_get_feature (s_ethtool3, NM_ETHTOOL_OPTNAME_FEATURE_SG),  ==, NM_TERNARY_DEFAULT);
+	g_assert_true (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool3), NM_ETHTOOL_OPTNAME_COALESCE_RX_FRAMES, &u32));
+	g_assert_cmpuint (u32, ==, 4);
+
+
+	nm_setting_option_set (NM_SETTING (s_ethtool), NM_ETHTOOL_OPTNAME_COALESCE_RX_FRAMES, NULL);
+	g_assert_false (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool), NM_ETHTOOL_OPTNAME_COALESCE_RX_FRAMES, NULL));
+
+	nm_setting_option_set_uint32 (NM_SETTING (s_ethtool),
+	                              NM_ETHTOOL_OPTNAME_COALESCE_TX_FRAMES,
+	                              8);
+
+	g_assert_true (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool), NM_ETHTOOL_OPTNAME_COALESCE_TX_FRAMES, &u32));
+	g_assert_cmpuint (u32, ==, 8);
+
+	nm_setting_option_clear_by_name (NM_SETTING (s_ethtool), nm_ethtool_optname_is_coalesce);
+	g_assert_false (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool), NM_ETHTOOL_OPTNAME_COALESCE_RX_FRAMES, NULL));
+	g_assert_false (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool), NM_ETHTOOL_OPTNAME_COALESCE_TX_FRAMES, NULL));
+	g_assert_false (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool), NM_ETHTOOL_OPTNAME_COALESCE_TX_USECS, NULL));
+}
+
+static void
+test_ethtool_ring (void)
+{
+	gs_unref_object NMConnection *con = NULL;
+	gs_unref_object NMConnection *con2 = NULL;
+	gs_unref_object NMConnection *con3 = NULL;
+	gs_unref_variant GVariant *variant = NULL;
+	gs_free_error GError *error = NULL;
+	gs_unref_keyfile GKeyFile *keyfile = NULL;
+	NMSettingConnection *s_con;
+	NMSettingEthtool *s_ethtool;
+	NMSettingEthtool *s_ethtool2;
+	NMSettingEthtool *s_ethtool3;
+	guint32 out_value;
+
+	con = nmtst_create_minimal_connection ("ethtool-ring",
+	                                        NULL,
+	                                        NM_SETTING_WIRED_SETTING_NAME,
+	                                        &s_con);
+	s_ethtool = NM_SETTING_ETHTOOL (nm_setting_ethtool_new ());
+	nm_connection_add_setting (con, NM_SETTING (s_ethtool));
+
+	nm_setting_option_set_uint32 (NM_SETTING (s_ethtool),
+	                              NM_ETHTOOL_OPTNAME_RING_RX_JUMBO,
+	                              4);
+
+	g_assert_true (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool), NM_ETHTOOL_OPTNAME_RING_RX_JUMBO, &out_value));
+	g_assert_cmpuint (out_value, ==, 4);
+
+	nmtst_connection_normalize (con);
+
+	variant = nm_connection_to_dbus (con, NM_CONNECTION_SERIALIZE_ALL);
+
+	con2 = nm_simple_connection_new_from_dbus (variant, &error);
+	nmtst_assert_success (con2, error);
+
+	s_ethtool2 = NM_SETTING_ETHTOOL (nm_connection_get_setting (con2, NM_TYPE_SETTING_ETHTOOL));
+
+	g_assert_true (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool2), NM_ETHTOOL_OPTNAME_RING_RX_JUMBO, &out_value));
+	g_assert_cmpuint (out_value, ==, 4);
+
+	nmtst_assert_connection_verifies_without_normalization (con2);
+
+	nmtst_assert_connection_equals (con, FALSE, con2, FALSE);
+
+	keyfile = nm_keyfile_write (con, NM_KEYFILE_HANDLER_FLAGS_NONE, NULL, NULL, &error);
+	nmtst_assert_success (keyfile, error);
+
+	con3 = nm_keyfile_read (keyfile,
+	                        "/ignored/current/working/directory/for/loading/relative/paths",
+	                        NM_KEYFILE_HANDLER_FLAGS_NONE,
+	                        NULL,
+	                        NULL,
+	                        &error);
+	nmtst_assert_success (con3, error);
+
+	nm_keyfile_read_ensure_id (con3, "unused-because-already-has-id");
+	nm_keyfile_read_ensure_uuid (con3, "unused-because-already-has-uuid");
+
+	nmtst_connection_normalize (con3);
+
+	nmtst_assert_connection_equals (con, FALSE, con3, FALSE);
+
+	s_ethtool3 = NM_SETTING_ETHTOOL (nm_connection_get_setting (con3, NM_TYPE_SETTING_ETHTOOL));
+
+	g_assert_true (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool3), NM_ETHTOOL_OPTNAME_RING_RX_JUMBO, &out_value));
+	g_assert_cmpuint (out_value, ==, 4);
+
+
+	nm_setting_option_set (NM_SETTING (s_ethtool), NM_ETHTOOL_OPTNAME_RING_RX_JUMBO, NULL);
+	g_assert_false (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool), NM_ETHTOOL_OPTNAME_RING_RX_JUMBO, NULL));
+
+	nm_setting_option_set_uint32 (NM_SETTING (s_ethtool),
+	                              NM_ETHTOOL_OPTNAME_RING_RX_JUMBO,
+	                              8);
+
+	g_assert_true (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool), NM_ETHTOOL_OPTNAME_RING_RX_JUMBO, &out_value));
+	g_assert_cmpuint (out_value, ==, 8);
+
+	nm_setting_option_clear_by_name (NM_SETTING (s_ethtool), nm_ethtool_optname_is_ring);
+	g_assert_false (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool), NM_ETHTOOL_OPTNAME_RING_RX_JUMBO, NULL));
+	g_assert_false (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool), NM_ETHTOOL_OPTNAME_RING_RX, NULL));
+	g_assert_false (nm_setting_option_get_uint32 (NM_SETTING (s_ethtool), NM_ETHTOOL_OPTNAME_RING_TX, NULL));
 }
 
 /*****************************************************************************/
@@ -1917,14 +2134,16 @@ _test_verify_options_bridge (gboolean expected_result,
 		case G_TYPE_UINT: {
 				guint uvalue;
 
-				uvalue = _nm_utils_ascii_str_to_uint64 (option_val, 10, 0, G_MAXUINT, 0);
+				uvalue = _nm_utils_ascii_str_to_uint64 (option_val, 10, 0, G_MAXUINT, -1);
+				g_assert (errno == 0);
 				g_object_set (s_bridge, option_key, uvalue, NULL);
 			}
 			break;
 		case G_TYPE_BOOLEAN: {
-				gboolean bvalue;
+				int bvalue;
 
-				bvalue = _nm_utils_ascii_str_to_bool (option_val, FALSE);
+				bvalue = _nm_utils_ascii_str_to_bool (option_val, -1);
+				g_assert (bvalue != -1);
 				g_object_set (s_bridge, option_key, bvalue, NULL);
 			}
 			break;
@@ -1980,10 +2199,10 @@ test_bridge_verify (void)
 	/* multicast-router */
 	test_verify_options_bridge (FALSE,
 	                            "multicast-router",   "nonsense");
-	test_verify_options_bridge (FALSE,
+	test_verify_options_bridge (TRUE,
 	                            "multicast-snooping", "no",
 	                            "multicast-router",   "auto");
-	test_verify_options_bridge (FALSE,
+	test_verify_options_bridge (TRUE,
 	                            "multicast-snooping", "no",
 	                            "multicast-router",   "enabled");
 	test_verify_options_bridge (TRUE,
@@ -1998,6 +2217,13 @@ test_bridge_verify (void)
 	test_verify_options_bridge (TRUE,
 	                            "multicast-snooping", "yes",
 	                            "multicast-router",   "disabled");
+	/* multicast-hash-max */
+	test_verify_options_bridge (TRUE,
+	                            "multicast-hash-max",   "1024");
+	test_verify_options_bridge (TRUE,
+	                            "multicast-hash-max",   "8192");
+	test_verify_options_bridge (FALSE,
+	                            "multicast-hash-max",   "3");
 }
 
 /*****************************************************************************/
@@ -2008,6 +2234,7 @@ test_tc_config_qdisc (void)
 	NMTCQdisc *qdisc1, *qdisc2;
 	char *str;
 	GError *error = NULL;
+	GVariant *variant;
 
 	qdisc1 = nm_tc_qdisc_new ("fq_codel", TC_H_ROOT, &error);
 	nmtst_assert_success (qdisc1, error);
@@ -2070,6 +2297,43 @@ test_tc_config_qdisc (void)
 
 	nm_tc_qdisc_unref (qdisc1);
 	nm_tc_qdisc_unref (qdisc2);
+
+#define CHECK_ATTRIBUTE(qdisc, name, vtype, type, value) \
+	variant = nm_tc_qdisc_get_attribute (qdisc, name); \
+	g_assert (variant); \
+	g_assert (g_variant_is_of_type (variant, vtype)); \
+	g_assert_cmpint (g_variant_get_ ## type(variant), ==, value);
+
+	qdisc1 = nm_utils_tc_qdisc_from_str ("handle 1235 root sfq perturb 10 quantum 1480 "
+	                                     "limit 9000 flows 1024 divisor 500 depth 12",
+	                                     &error);
+	nmtst_assert_success (qdisc1, error);
+
+	g_assert_cmpstr (nm_tc_qdisc_get_kind (qdisc1), ==, "sfq");
+	g_assert (nm_tc_qdisc_get_handle (qdisc1) == TC_H_MAKE (0x1235u << 16, 0x0000u));
+	g_assert (nm_tc_qdisc_get_parent (qdisc1) == TC_H_ROOT);
+	CHECK_ATTRIBUTE (qdisc1, "perturb", G_VARIANT_TYPE_INT32,  int32,  10);
+	CHECK_ATTRIBUTE (qdisc1, "quantum", G_VARIANT_TYPE_UINT32, uint32, 1480);
+	CHECK_ATTRIBUTE (qdisc1, "limit",   G_VARIANT_TYPE_UINT32, uint32, 9000);
+	CHECK_ATTRIBUTE (qdisc1, "flows",   G_VARIANT_TYPE_UINT32, uint32, 1024);
+	CHECK_ATTRIBUTE (qdisc1, "divisor", G_VARIANT_TYPE_UINT32, uint32, 500);
+	CHECK_ATTRIBUTE (qdisc1, "depth",   G_VARIANT_TYPE_UINT32, uint32, 12);
+	nm_tc_qdisc_unref (qdisc1);
+
+	qdisc1 = nm_utils_tc_qdisc_from_str ("handle 1235 root tbf rate 1000000 burst 5000 limit 10000",
+	                                     &error);
+	nmtst_assert_success (qdisc1, error);
+
+	g_assert_cmpstr (nm_tc_qdisc_get_kind (qdisc1), ==, "tbf");
+	g_assert (nm_tc_qdisc_get_handle (qdisc1) == TC_H_MAKE (0x1235u << 16, 0x0000u));
+	g_assert (nm_tc_qdisc_get_parent (qdisc1) == TC_H_ROOT);
+	CHECK_ATTRIBUTE (qdisc1, "rate",  G_VARIANT_TYPE_UINT64, uint64, 1000000);
+	CHECK_ATTRIBUTE (qdisc1, "burst", G_VARIANT_TYPE_UINT32, uint32, 5000);
+	CHECK_ATTRIBUTE (qdisc1, "limit", G_VARIANT_TYPE_UINT32, uint32, 10000);
+	nm_tc_qdisc_unref (qdisc1);
+
+
+#undef CHECK_ATTRIBUTE
 }
 
 static void
@@ -3045,7 +3309,7 @@ test_roundtrip_conversion (gconstpointer test_data)
 	{
 		gs_unref_keyfile GKeyFile *kf = NULL;
 
-		kf = nm_keyfile_write (con, NULL, NULL, &error);
+		kf = nm_keyfile_write (con, NM_KEYFILE_HANDLER_FLAGS_NONE, NULL, NULL, &error);
 		nmtst_assert_success (kf, error);
 
 		/* the first kf_data_arr entry is special: it must be what the writer would
@@ -3140,7 +3404,7 @@ test_roundtrip_conversion (gconstpointer test_data)
 			{
 				gs_unref_keyfile GKeyFile *kf = NULL;
 
-				kf = nm_keyfile_write (con2, NULL, NULL, &error);
+				kf = nm_keyfile_write (con2, NM_KEYFILE_HANDLER_FLAGS_NONE, NULL, NULL, &error);
 				nmtst_assert_success (kf, error);
 				nmtst_keyfile_assert_data (kf, kf_data_arr->pdata[0], -1);
 			}
@@ -3444,7 +3708,7 @@ test_empty_setting (void)
 
 	nmtst_assert_connection_verifies_without_normalization (con);
 
-	kf = nm_keyfile_write (con, NULL, NULL, &error);
+	kf = nm_keyfile_write (con, NM_KEYFILE_HANDLER_FLAGS_NONE, NULL, NULL, &error);
 	nmtst_assert_success (kf, error);
 
 	g_assert (g_key_file_has_group (kf, "gsm"));
@@ -3452,6 +3716,7 @@ test_empty_setting (void)
 
 	con2 = nm_keyfile_read (kf,
 	                        "/ignored/current/working/directory/for/loading/relative/paths",
+	                        NM_KEYFILE_HANDLER_FLAGS_NONE,
 	                        NULL,
 	                        NULL,
 	                        &error);
@@ -3763,7 +4028,9 @@ main (int argc, char **argv)
 	g_test_add_func ("/libnm/settings/dcb/priorities", test_dcb_priorities_valid);
 	g_test_add_func ("/libnm/settings/dcb/bandwidth-sums", test_dcb_bandwidth_sums);
 
-	g_test_add_func ("/libnm/settings/ethtool/1", test_ethtool_1);
+	g_test_add_func ("/libnm/settings/ethtool/features", test_ethtool_features);
+	g_test_add_func ("/libnm/settings/ethtool/coalesce", test_ethtool_coalesce);
+	g_test_add_func ("/libnm/settings/ethtool/ring", test_ethtool_ring);
 
 	g_test_add_func ("/libnm/settings/sriov/vf", test_sriov_vf);
 	g_test_add_func ("/libnm/settings/sriov/vf-dup", test_sriov_vf_dup);