about 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.build92
-rw-r--r--libnm-core/nm-connection.c51
-rw-r--r--libnm-core/nm-core-internal.h81
-rw-r--r--libnm-core/nm-crypto-gnutls.c2
-rw-r--r--libnm-core/nm-crypto-nss.c2
-rw-r--r--libnm-core/nm-crypto.c4
-rw-r--r--libnm-core/nm-dbus-interface.h18
-rw-r--r--libnm-core/nm-json.h2
-rw-r--r--libnm-core/nm-keyfile.c614
-rw-r--r--libnm-core/nm-setting-8021x.c2
-rw-r--r--libnm-core/nm-setting-bridge-port.c319
-rw-r--r--libnm-core/nm-setting-bridge-port.h18
-rw-r--r--libnm-core/nm-setting-bridge.c863
-rw-r--r--libnm-core/nm-setting-bridge.h59
-rw-r--r--libnm-core/nm-setting-connection.c7
-rw-r--r--libnm-core/nm-setting-ethtool.c2
-rw-r--r--libnm-core/nm-setting-infiniband.c25
-rw-r--r--libnm-core/nm-setting-ip-config.c2465
-rw-r--r--libnm-core/nm-setting-ip-config.h166
-rw-r--r--libnm-core/nm-setting-ip-tunnel.c4
-rw-r--r--libnm-core/nm-setting-macsec.c2
-rw-r--r--libnm-core/nm-setting-private.h4
-rw-r--r--libnm-core/nm-setting-sriov.c6
-rw-r--r--libnm-core/nm-setting-tc-config.c197
-rw-r--r--libnm-core/nm-setting-tc-config.h10
-rw-r--r--libnm-core/nm-setting-team-port.c27
-rw-r--r--libnm-core/nm-setting-team.c123
-rw-r--r--libnm-core/nm-setting-vlan.c122
-rw-r--r--libnm-core/nm-setting-vpn.c16
-rw-r--r--libnm-core/nm-setting-wifi-p2p.c2
-rw-r--r--libnm-core/nm-setting-wired.c18
-rw-r--r--libnm-core/nm-setting-wireguard.c2
-rw-r--r--libnm-core/nm-setting-wireless.c2
-rw-r--r--libnm-core/nm-utils-private.h80
-rw-r--r--libnm-core/nm-utils.c422
-rw-r--r--libnm-core/nm-version.h14
-rw-r--r--libnm-core/tests/meson.build2
-rw-r--r--libnm-core/tests/test-general.c516
-rw-r--r--libnm-core/tests/test-keyfile.c98
-rw-r--r--libnm-core/tests/test-setting.c368
40 files changed, 6156 insertions, 671 deletions
diff --git a/libnm-core/meson.build b/libnm-core/meson.build
index d10dd1c5..bc2d087a 100644
--- a/libnm-core/meson.build
+++ b/libnm-core/meson.build
@@ -139,14 +139,57 @@ libnm_core_enum = gnome.mkenums(
   install_dir: libnm_pkgincludedir,
 )
 
+libnm_core_enum_dep = declare_dependency(
+  sources: libnm_core_enum[1],
+)
+
+###############################################################################
+
+shared_nm_libnm_core_intern = static_library(
+    'nm-libnm-core-intern',
+    sources: files('../shared/nm-libnm-core-intern/nm-ethtool-utils.c',
+                   '../shared/nm-libnm-core-intern/nm-libnm-core-utils.c'),
+    c_args: [
+        '-DG_LOG_DOMAIN="@0@"'.format(libnm_name),
+        '-DNETWORKMANAGER_COMPILATION=NM_NETWORKMANAGER_COMPILATION_LIBNM_CORE',
+    ],
+    include_directories: [
+        top_inc,
+        shared_inc,
+        libnm_core_inc,
+    ],
+    dependencies: [
+        glib_dep,
+        shared_c_siphash_dep,
+        shared_nm_glib_aux_dep,
+        libnm_core_enum_dep,
+    ],
+)
+
+shared_nm_libnm_core_intern_dep = declare_dependency(
+    link_with: shared_nm_libnm_core_intern,
+    include_directories: [
+        top_inc,
+        shared_inc,
+    ],
+    dependencies: [
+        glib_dep,
+        shared_nm_glib_aux_dep,
+        libnm_systemd_shared_dep,
+    ],
+)
+
+###############################################################################
+
 deps = [
   dl_dep,
   libudev_dep,
   uuid_dep,
   glib_dep,
   shared_c_siphash_dep,
-  shared_nm_utils_base_dep,
-  shared_nm_utils_udev_dep,
+  shared_nm_glib_aux_dep,
+  shared_nm_udev_aux_dep,
+  shared_nm_libnm_core_intern_dep,
 ]
 
 cflags = [
@@ -188,7 +231,6 @@ endif
 libnm_core_sources_all =  libnm_core_sources
 libnm_core_sources_all += libnm_core_enum
 libnm_core_sources_all += shared_nm_meta_setting_c
-libnm_core_sources_all += shared_nm_ethtool_utils_c
 libnm_core_sources_all += [version_header]
 
 libnm_core = static_library(
@@ -199,16 +241,54 @@ libnm_core = static_library(
   c_args: cflags,
 )
 
-nm_core_dep = declare_dependency(
+libnm_core_dep = declare_dependency(
   sources: libnm_core_enum[1],
   include_directories: libnm_core_inc,
   dependencies: [
-    shared_nm_utils_base_dep,
+    shared_nm_libnm_core_intern_dep,
+    shared_nm_glib_aux_dep,
     shared_c_siphash_dep,
     libnm_systemd_shared_dep,
   ],
 )
 
+###############################################################################
+
+shared_nm_libnm_core_aux = static_library(
+    'nm-libnm-core-aux',
+    sources: files('../shared/nm-libnm-core-aux/nm-dispatcher-api.h'),
+    c_args: [
+        '-DG_LOG_DOMAIN="@0@"'.format(libnm_name),
+        '-DNETWORKMANAGER_COMPILATION=(NM_NETWORKMANAGER_COMPILATION_WITH_GLIB|NM_NETWORKMANAGER_COMPILATION_WITH_GLIB_I18N_LIB)',
+    ],
+    include_directories: [
+        top_inc,
+        shared_inc,
+        libnm_core_inc,
+    ],
+    dependencies: [
+        glib_dep,
+        shared_c_siphash_dep,
+        shared_nm_glib_aux_dep,
+    ],
+)
+
+shared_nm_libnm_core_aux_dep = declare_dependency(
+    link_with: shared_nm_libnm_core_aux,
+    include_directories: [
+        top_inc,
+        shared_inc,
+    ],
+    dependencies: [
+        libnm_core_dep,
+        glib_dep,
+        shared_nm_glib_aux_dep,
+        libnm_systemd_shared_dep,
+    ],
+)
+
+###############################################################################
+
 enums_to_docbook = join_paths(meson.source_root(), 'tools', 'enums-to-docbook.pl')
 
 docbooks = [
@@ -233,6 +313,8 @@ foreach docbook: docbooks
   content_files += xml.full_path()
 endforeach
 
+###############################################################################
+
 if enable_tests
   subdir('tests')
 endif
diff --git a/libnm-core/nm-connection.c b/libnm-core/nm-connection.c
index ea7730bf..3182e346 100644
--- a/libnm-core/nm-connection.c
+++ b/libnm-core/nm-connection.c
@@ -1059,23 +1059,14 @@ _normalize_infiniband_mtu (NMConnection *self, GHashTable *parameters)
 {
 	NMSettingInfiniband *s_infini = nm_connection_get_setting_infiniband (self);
 
-	if (s_infini) {
-		const char *transport_mode = nm_setting_infiniband_get_transport_mode (s_infini);
-		guint32 max_mtu = 0;
-
-		if (transport_mode) {
-			if (!strcmp (transport_mode, "datagram"))
-				max_mtu = 2044;
-			else if (!strcmp (transport_mode, "connected"))
-				max_mtu = 65520;
-
-			if (max_mtu && nm_setting_infiniband_get_mtu (s_infini) > max_mtu) {
-				g_object_set (s_infini, NM_SETTING_INFINIBAND_MTU, (guint) max_mtu, NULL);
-				return TRUE;
-			}
-		}
-	}
-	return FALSE;
+	if (   !s_infini
+	    || nm_setting_infiniband_get_mtu (s_infini) <= NM_INFINIBAND_MAX_MTU
+	    || !NM_IN_STRSET (nm_setting_infiniband_get_transport_mode (s_infini), "datagram",
+	                                                                           "connected"))
+		return FALSE;
+
+	g_object_set (s_infini, NM_SETTING_INFINIBAND_MTU, (guint) NM_INFINIBAND_MAX_MTU, NULL);
+	return TRUE;
 }
 
 static gboolean
@@ -1298,6 +1289,30 @@ _normalize_sriov_vf_order (NMConnection *self, GHashTable *parameters)
 }
 
 static gboolean
+_normalize_bridge_vlan_order (NMConnection *self, GHashTable *parameters)
+{
+	NMSettingBridge *s_bridge;
+
+	s_bridge = nm_connection_get_setting_bridge (self);
+	if (!s_bridge)
+		return FALSE;
+
+	return _nm_setting_bridge_sort_vlans (s_bridge);
+}
+
+static gboolean
+_normalize_bridge_port_vlan_order (NMConnection *self, GHashTable *parameters)
+{
+	NMSettingBridgePort *s_port;
+
+	s_port = nm_connection_get_setting_bridge_port (self);
+	if (!s_port)
+		return FALSE;
+
+	return _nm_setting_bridge_port_sort_vlans (s_port);
+}
+
+static gboolean
 _normalize_required_settings (NMConnection *self, GHashTable *parameters)
 {
 	NMSettingBluetooth *s_bt = nm_connection_get_setting_bluetooth (self);
@@ -1645,6 +1660,8 @@ nm_connection_normalize (NMConnection *connection,
 	was_modified |= _normalize_ovs_interface_type (connection, parameters);
 	was_modified |= _normalize_ip_tunnel_wired_setting (connection, parameters);
 	was_modified |= _normalize_sriov_vf_order (connection, parameters);
+	was_modified |= _normalize_bridge_vlan_order (connection, parameters);
+	was_modified |= _normalize_bridge_port_vlan_order (connection, parameters);
 
 	/* Verify anew. */
 	success = _nm_connection_verify (connection, error);
diff --git a/libnm-core/nm-core-internal.h b/libnm-core/nm-core-internal.h
index f2332f76..3e33dd93 100644
--- a/libnm-core/nm-core-internal.h
+++ b/libnm-core/nm-core-internal.h
@@ -88,6 +88,7 @@
 #include "nm-utils.h"
 #include "nm-vpn-dbus-interface.h"
 #include "nm-vpn-editor-plugin.h"
+#include "nm-libnm-core-intern/nm-libnm-core-utils.h"
 
 /* IEEE 802.1D-1998 timer values */
 #define NM_BR_MIN_HELLO_TIME    1
@@ -249,6 +250,8 @@ guint nm_setting_ethtool_init_features (NMSettingEthtool *setting,
 guint8 *_nm_utils_hwaddr_aton (const char *asc, gpointer buffer, gsize buffer_length, gsize *out_length);
 const char *nm_utils_hwaddr_ntoa_buf (gconstpointer addr, gsize addr_len, gboolean upper_case, char *buf, gsize buf_len);
 
+gboolean nm_utils_is_valid_iface_name_utf8safe (const char *utf8safe_name);
+
 GSList *    _nm_utils_hash_values_to_slist (GHashTable *hash);
 
 GHashTable *_nm_utils_copy_strdict (GHashTable *strdict);
@@ -259,19 +262,11 @@ const char **_nm_ip_address_get_attribute_names (const NMIPAddress *addr, gboole
 
 gboolean _nm_ip_route_attribute_validate_all (const NMIPRoute *route);
 const char **_nm_ip_route_get_attribute_names (const NMIPRoute *route, gboolean sorted, guint *out_length);
-GHashTable *_nm_ip_route_get_attributes_direct (NMIPRoute *route);
+GHashTable *_nm_ip_route_get_attributes (NMIPRoute *route);
 
 NMSriovVF *_nm_utils_sriov_vf_from_strparts (const char *index, const char *detail, gboolean ignore_unknown, GError **error);
 gboolean _nm_sriov_vf_attribute_validate_all (const NMSriovVF *vf, GError **error);
 
-static inline void
-_nm_auto_ip_route_unref (NMIPRoute **v)
-{
-	if (*v)
-		nm_ip_route_unref (*v);
-}
-#define nm_auto_ip_route_unref nm_auto (_nm_auto_ip_route_unref)
-
 GPtrArray *_nm_utils_copy_array (const GPtrArray *array,
                                  NMUtilsCopyFunc copy_func,
                                  GDestroyNotify free_func);
@@ -284,7 +279,6 @@ char **     _nm_utils_slist_to_strv (GSList *slist, gboolean deep_copy);
 
 GPtrArray * _nm_utils_strv_to_ptrarray (char **strv);
 char **     _nm_utils_ptrarray_to_strv (GPtrArray *ptrarray);
-gboolean    _nm_utils_strv_equal (char **strv1, char **strv2);
 
 gboolean _nm_utils_check_file (const char *filename,
                                gint64 check_owner,
@@ -538,6 +532,8 @@ gboolean _nm_utils_inet6_is_token (const struct in6_addr *in6addr);
 
 /*****************************************************************************/
 
+gboolean _nm_team_link_watchers_equal (GPtrArray *a, GPtrArray *b, gboolean ignore_order);
+
 gboolean _nm_utils_team_config_equal (const char *conf1, const char *conf2, gboolean port);
 GValue *_nm_utils_team_config_get (const char *conf,
                                    const char *key,
@@ -553,18 +549,6 @@ gboolean _nm_utils_team_config_set (char **conf,
 
 /*****************************************************************************/
 
-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);
-}
-
-/*****************************************************************************/
-
 guint32 _nm_utils_parse_tc_handle                (const char *str,
                                                   GError **error);
 void _nm_utils_string_append_tc_parent           (GString *string,
@@ -576,6 +560,9 @@ gboolean _nm_utils_string_append_tc_tfilter_rest (GString *string,
                                                   NMTCTfilter *tfilter,
                                                   GError **error);
 
+GHashTable *_nm_tc_qdisc_get_attributes (NMTCQdisc *qdisc);
+GHashTable *_nm_tc_action_get_attributes (NMTCAction *action);
+
 /*****************************************************************************/
 
 static inline gboolean
@@ -596,6 +583,8 @@ gboolean _nm_utils_dhcp_duid_valid (const char *duid, GBytes **out_duid_bin);
 /*****************************************************************************/
 
 gboolean _nm_setting_sriov_sort_vfs (NMSettingSriov *setting);
+gboolean _nm_setting_bridge_port_sort_vlans (NMSettingBridgePort *setting);
+gboolean _nm_setting_bridge_sort_vlans (NMSettingBridge *setting);
 
 /*****************************************************************************/
 
@@ -627,6 +616,47 @@ void _nm_wireguard_peer_set_public_key_bin (NMWireGuardPeer *self,
 
 /*****************************************************************************/
 
+const NMIPAddr *nm_ip_routing_rule_get_from_bin (const NMIPRoutingRule *self);
+void nm_ip_routing_rule_set_from_bin (NMIPRoutingRule *self,
+                                      gconstpointer from,
+                                      guint8 len);
+
+const NMIPAddr *nm_ip_routing_rule_get_to_bin (const NMIPRoutingRule *self);
+void nm_ip_routing_rule_set_to_bin (NMIPRoutingRule *self,
+                                    gconstpointer to,
+                                    guint8 len);
+
+gboolean nm_ip_routing_rule_get_xifname_bin (const NMIPRoutingRule *self,
+                                             gboolean iif /* or else oif */,
+                                             char out_xifname[static 16]);
+
+#define NM_IP_ROUTING_RULE_ATTR_ACTION      "action"
+#define NM_IP_ROUTING_RULE_ATTR_DPORT_END   "dport-end"
+#define NM_IP_ROUTING_RULE_ATTR_DPORT_START "dport-start"
+#define NM_IP_ROUTING_RULE_ATTR_FAMILY      "family"
+#define NM_IP_ROUTING_RULE_ATTR_FROM        "from"
+#define NM_IP_ROUTING_RULE_ATTR_FROM_LEN    "from-len"
+#define NM_IP_ROUTING_RULE_ATTR_FWMARK      "fwmark"
+#define NM_IP_ROUTING_RULE_ATTR_FWMASK      "fwmask"
+#define NM_IP_ROUTING_RULE_ATTR_IIFNAME     "iifname"
+#define NM_IP_ROUTING_RULE_ATTR_INVERT      "invert"
+#define NM_IP_ROUTING_RULE_ATTR_IPPROTO     "ipproto"
+#define NM_IP_ROUTING_RULE_ATTR_OIFNAME     "oifname"
+#define NM_IP_ROUTING_RULE_ATTR_PRIORITY    "priority"
+#define NM_IP_ROUTING_RULE_ATTR_SPORT_END   "sport-end"
+#define NM_IP_ROUTING_RULE_ATTR_SPORT_START "sport-start"
+#define NM_IP_ROUTING_RULE_ATTR_TABLE       "table"
+#define NM_IP_ROUTING_RULE_ATTR_TO          "to"
+#define NM_IP_ROUTING_RULE_ATTR_TOS         "tos"
+#define NM_IP_ROUTING_RULE_ATTR_TO_LEN      "to-len"
+
+NMIPRoutingRule *nm_ip_routing_rule_from_dbus (GVariant *variant,
+                                               gboolean strict,
+                                               GError **error);
+GVariant *nm_ip_routing_rule_to_dbus (const NMIPRoutingRule *self);
+
+/*****************************************************************************/
+
 typedef struct _NMSettInfoSetting  NMSettInfoSetting;
 typedef struct _NMSettInfoProperty NMSettInfoProperty;
 
@@ -762,13 +792,14 @@ gboolean _nm_connection_find_secret (NMConnection *self,
 
 /*****************************************************************************/
 
-#define nm_auto_unref_wgpeer nm_auto(_nm_auto_unref_wgpeer)
-NM_AUTO_DEFINE_FCN0 (NMWireGuardPeer *, _nm_auto_unref_wgpeer, nm_wireguard_peer_unref)
-
 gboolean nm_utils_base64secret_normalize (const char *base64_key,
                                           gsize required_key_len,
                                           char **out_base64_key_norm);
 
 /*****************************************************************************/
 
+void _nm_bridge_vlan_str_append_rest (const NMBridgeVlan *vlan,
+                                      GString *string,
+                                      gboolean leading_space);
+
 #endif
diff --git a/libnm-core/nm-crypto-gnutls.c b/libnm-core/nm-crypto-gnutls.c
index 6c897e6d..6b2f7587 100644
--- a/libnm-core/nm-crypto-gnutls.c
+++ b/libnm-core/nm-crypto-gnutls.c
@@ -30,7 +30,7 @@
 #include <gnutls/x509.h>
 #include <gnutls/pkcs12.h>
 
-#include "nm-utils/nm-secret-utils.h"
+#include "nm-glib-aux/nm-secret-utils.h"
 #include "nm-errors.h"
 
 /*****************************************************************************/
diff --git a/libnm-core/nm-crypto-nss.c b/libnm-core/nm-crypto-nss.c
index 711dde4b..25cc7777 100644
--- a/libnm-core/nm-crypto-nss.c
+++ b/libnm-core/nm-crypto-nss.c
@@ -35,7 +35,7 @@
 #include <ciferfam.h>
 #include <p12plcy.h>
 
-#include "nm-utils/nm-secret-utils.h"
+#include "nm-glib-aux/nm-secret-utils.h"
 #include "nm-errors.h"
 
 /*****************************************************************************/
diff --git a/libnm-core/nm-crypto.c b/libnm-core/nm-crypto.c
index 67f206c4..c7142216 100644
--- a/libnm-core/nm-crypto.c
+++ b/libnm-core/nm-crypto.c
@@ -29,8 +29,8 @@
 #include <unistd.h>
 #include <stdlib.h>
 
-#include "nm-utils/nm-secret-utils.h"
-#include "nm-utils/nm-io-utils.h"
+#include "nm-glib-aux/nm-secret-utils.h"
+#include "nm-glib-aux/nm-io-utils.h"
 
 #include "nm-crypto-impl.h"
 #include "nm-utils.h"
diff --git a/libnm-core/nm-dbus-interface.h b/libnm-core/nm-dbus-interface.h
index 038d2c9e..2e127c40 100644
--- a/libnm-core/nm-dbus-interface.h
+++ b/libnm-core/nm-dbus-interface.h
@@ -837,12 +837,26 @@ typedef enum /*< flags >*/ {
 #define NM_LLDP_ATTR_SYSTEM_NAME             "system-name"
 #define NM_LLDP_ATTR_SYSTEM_DESCRIPTION      "system-description"
 #define NM_LLDP_ATTR_SYSTEM_CAPABILITIES     "system-capabilities"
+#define NM_LLDP_ATTR_MANAGEMENT_ADDRESSES    "management-addresses"
+
 #define NM_LLDP_ATTR_IEEE_802_1_PVID         "ieee-802-1-pvid"
-#define NM_LLDP_ATTR_IEEE_802_1_PPVID        "ieee-802-1-ppvid"
-#define NM_LLDP_ATTR_IEEE_802_1_PPVID_FLAGS  "ieee-802-1-ppvid-flags"
+#define NM_LLDP_ATTR_IEEE_802_1_VLANS        "ieee-802-1-vlans"
+#define NM_LLDP_ATTR_IEEE_802_1_PPVIDS       "ieee-802-1-ppvids"
+
+#define NM_LLDP_ATTR_IEEE_802_3_MAC_PHY_CONF    "ieee-802-3-mac-phy-conf"
+#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"
+
+/* 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"
 #define NM_LLDP_ATTR_IEEE_802_1_VLAN_NAME    "ieee-802-1-vlan-name"
 
+/* These are deprecated in favor of NM_LLDP_ATTR_IEEE_802_1_PPVIDS,
+ * which can report multiple PPVIDs */
+#define NM_LLDP_ATTR_IEEE_802_1_PPVID        "ieee-802-1-ppvid"
+#define NM_LLDP_ATTR_IEEE_802_1_PPVID_FLAGS  "ieee-802-1-ppvid-flags"
+
 #define NM_LLDP_DEST_NEAREST_BRIDGE          "nearest-bridge"
 #define NM_LLDP_DEST_NEAREST_NON_TPMR_BRIDGE "nearest-non-tpmr-bridge"
 #define NM_LLDP_DEST_NEAREST_CUSTOMER_BRIDGE "nearest-customer-bridge"
diff --git a/libnm-core/nm-json.h b/libnm-core/nm-json.h
index 560b827a..b75a4f8e 100644
--- a/libnm-core/nm-json.h
+++ b/libnm-core/nm-json.h
@@ -45,7 +45,7 @@ gboolean nm_jansson_load (void);
 #define json_integer_value      (*_nm_jansson_json_integer_value)
 #define json_string_value       (*_nm_jansson_json_string_value)
 
-#include "nm-utils/nm-jansson.h"
+#include "nm-glib-aux/nm-jansson.h"
 #endif
 
 #endif /* __NM_JSON_H__ */
diff --git a/libnm-core/nm-keyfile.c b/libnm-core/nm-keyfile.c
index 05c6bf97..bf8d2193 100644
--- a/libnm-core/nm-keyfile.c
+++ b/libnm-core/nm-keyfile.c
@@ -31,9 +31,9 @@
 #include <arpa/inet.h>
 #include <linux/pkt_sched.h>
 
-#include "nm-utils/nm-secret-utils.h"
+#include "nm-glib-aux/nm-secret-utils.h"
 #include "systemd/nm-sd-utils-shared.h"
-#include "nm-common-macros.h"
+#include "nm-libnm-core-intern/nm-common-macros.h"
 #include "nm-core-internal.h"
 #include "nm-keyfile-utils.h"
 
@@ -41,6 +41,8 @@
 
 /*****************************************************************************/
 
+typedef struct _ParseInfoProperty ParseInfoProperty;
+
 typedef struct {
 	NMConnection *connection;
 	GKeyFile *keyfile;
@@ -534,13 +536,19 @@ typedef struct {
 	const char *s_key;
 	gint32 key_idx;
 	gint8 key_type;
-} IPAddrRouteBuildListData;
+} BuildListData;
+
+typedef enum {
+	BUILD_LIST_TYPE_ADDRESSES,
+	BUILD_LIST_TYPE_ROUTES,
+	BUILD_LIST_TYPE_ROUTING_RULES,
+} BuildListType;
 
 static int
-_ip_addrroute_build_lst_data_cmp (gconstpointer p_a, gconstpointer p_b, gpointer user_data)
+_build_list_data_cmp (gconstpointer p_a, gconstpointer p_b, gpointer user_data)
 {
-	const IPAddrRouteBuildListData *a = p_a;
-	const IPAddrRouteBuildListData *b = p_b;
+	const BuildListData *a = p_a;
+	const BuildListData *b = p_b;
 
 	NM_CMP_FIELD (a, b, key_idx);
 	NM_CMP_FIELD (a, b, key_type);
@@ -549,10 +557,25 @@ _ip_addrroute_build_lst_data_cmp (gconstpointer p_a, gconstpointer p_b, gpointer
 }
 
 static gboolean
-ip_addrroute_match_key_w_name_ (const char *key,
-                                const char *base_name,
-                                gsize base_name_l,
-                                gint32 *out_key_idx)
+_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;
 
@@ -595,100 +618,125 @@ ip_addrroute_match_key_w_name_ (const char *key,
 	return TRUE;
 }
 
-static gboolean
-ip_addrroute_match_key (const char *key,
-                        gboolean is_routes,
-                        gint32 *out_key_idx,
-                        gint8 *out_key_type)
-{
-#define ip_addrroute_match_key_w_name(key, base_name, out_key_idx) \
-	ip_addrroute_match_key_w_name_ (key, base_name, NM_STRLEN (base_name), out_key_idx)
-
-	if (is_routes) {
-		if (ip_addrroute_match_key_w_name (key, "route", out_key_idx))
-			NM_SET_OUT (out_key_type, 0);
-		else if (ip_addrroute_match_key_w_name (key, "routes", out_key_idx))
-			NM_SET_OUT (out_key_type, 1);
-		else
-			return FALSE;
-	} else {
-		if (ip_addrroute_match_key_w_name (key, "address", out_key_idx))
-			NM_SET_OUT (out_key_type, 0);
-		else if (ip_addrroute_match_key_w_name (key, "addresses", out_key_idx))
-			NM_SET_OUT (out_key_type, 1);
-		else
-			return FALSE;
-	}
-	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 void
-ip_address_or_route_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *setting_key)
+static BuildListData *
+_build_list_create (GKeyFile *keyfile,
+                    const char *group_name,
+                    BuildListType build_list_type,
+                    gsize *out_build_list_len,
+                    char ***out_keys_strv)
 {
-	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;
 	gsize i_keys, n_keys;
-	gs_free IPAddrRouteBuildListData *build_list = NULL;
-	gsize i_build_list, build_list_len = 0;
+	gs_free BuildListData *build_list = NULL;
+	gsize build_list_len = 0;
 
-	keys = nm_keyfile_plugin_kf_get_keys (info->keyfile, setting_name, &n_keys, NULL);
+	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;
+		return NULL;
 
-	/* first create a list of all relevant keys, and sort them. */
 	for (i_keys = 0; i_keys < n_keys; i_keys++) {
 		const char *s_key = keys[i_keys];
 		gint32 key_idx;
-		gint8 key_type;
-
-		if (!ip_addrroute_match_key (s_key, is_routes, &key_idx, &key_type))
-			continue;
+		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 (IPAddrRouteBuildListData, n_keys - i_keys);
+			build_list = g_new (BuildListData, n_keys - i_keys);
 
-		build_list[build_list_len].s_key = s_key;
-		build_list[build_list_len].key_idx = key_idx;
-		build_list[build_list_len].key_type = key_type;
-		build_list_len++;
+		build_list[build_list_len++] = (BuildListData) {
+			.s_key    = s_key,
+			.key_idx  = key_idx,
+			.key_type = key_type,
+		};
 	}
 
 	if (build_list_len == 0)
-		return;
+		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;
 
-	g_qsort_with_data (build_list,
-	                   build_list_len,
-	                   sizeof (IPAddrRouteBuildListData),
-	                   _ip_addrroute_build_lst_data_cmp,
-	                   NULL);
+	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 IPAddrRouteBuildListData *build_data = &build_list[i_build_list];
+		const char *s_key;
 		gpointer item;
 
-		if (   i_build_list + 1 < build_list_len
-		    && build_data->key_idx == build_data[1].key_idx
-		    && build_data->key_type == build_data[1].key_type
-		    && nm_streq (build_data->s_key, build_data[1].s_key)) {
-			/* the keyfile contains duplicate keys, which are both returned
-			 * by g_key_file_get_keys() (WHY??).
-			 *
-			 * Skip the earlier one. */
+		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,
-		                                     build_data->s_key,
+		                                     s_key,
 		                                     is_ipv6,
 		                                     is_routes,
 		                                     gateway ? NULL : &gateway,
@@ -696,7 +744,7 @@ ip_address_or_route_parser (KeyfileReaderInfo *info, NMSetting *setting, const c
 		if (item && is_routes) {
 			char options_key[128];
 
-			nm_sprintf_buf (options_key, "%s_options", build_data->s_key);
+			nm_sprintf_buf (options_key, "%s_options", s_key);
 			fill_route_attributes (info->keyfile,
 			                       item,
 			                       setting_name,
@@ -719,6 +767,63 @@ ip_address_or_route_parser (KeyfileReaderInfo *info, NMSetting *setting, const c
 }
 
 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) {
+			handle_warn (info, property_info->name, NM_KEYFILE_WARN_SEVERITY_WARN,
+			             _("invalid value for \"%s\": %s"),
+			             build_list[i_build_list].s_key,
+			             local->message);
+			if (info->error)
+				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;
@@ -1519,20 +1624,57 @@ team_config_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key
 }
 
 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, 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);
-	GPtrArray *qdiscs;
+	gs_unref_ptrarray GPtrArray *qdiscs = NULL;
 	gs_strfreev char **keys = NULL;
 	gsize n_keys = 0;
 	int i;
 
-	qdiscs = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_tc_qdisc_unref);
-
 	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;
@@ -1562,25 +1704,23 @@ qdisc_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
 
 	if (qdiscs->len >= 1)
 		g_object_set (setting, key, qdiscs, NULL);
-
-	g_ptr_array_unref (qdiscs);
 }
 
 static void
 tfilter_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
 {
 	const char *setting_name = nm_setting_get_name (setting);
-	GPtrArray *tfilters;
+	gs_unref_ptrarray GPtrArray *tfilters = NULL;
 	gs_strfreev char **keys = NULL;
 	gsize n_keys = 0;
 	int i;
 
-	tfilters = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_tc_tfilter_unref);
-
 	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;
@@ -1610,8 +1750,6 @@ tfilter_parser (KeyfileReaderInfo *info, NMSetting *setting, const char *key)
 
 	if (tfilters->len >= 1)
 		g_object_set (setting, key, tfilters, NULL);
-
-	g_ptr_array_unref (tfilters);
 }
 
 /*****************************************************************************/
@@ -1805,10 +1943,9 @@ write_ip_values (GKeyFile *file,
 
 		if (is_route) {
 			gs_free char *attributes = NULL;
-			GHashTable *hash;
 
-			hash = _nm_ip_route_get_attributes_direct (array->pdata[i]);
-			attributes = nm_utils_format_variant_attributes (hash, ',', '=');
+			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);
@@ -1847,6 +1984,76 @@ route_writer (KeyfileWriterInfo *info,
 }
 
 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);
+}
+
+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,
@@ -2157,24 +2364,40 @@ cert_writer (KeyfileWriterInfo *info,
 
 /*****************************************************************************/
 
-typedef struct {
+struct _ParseInfoProperty {
 	const char *property_name;
-	void (*parser) (KeyfileReaderInfo *info,
-	                NMSetting *setting,
-	                const char *key);
-	void (*writer) (KeyfileWriterInfo *info,
-	                NMSetting *setting,
-	                const char *key,
-	                const GValue *value);
+	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;
-} ParseInfoProperty;
+};
 
 #define PARSE_INFO_PROPERTY(_property_name, ...) \
 	(&((const ParseInfoProperty) { \
@@ -2276,6 +2499,20 @@ static const ParseInfoSetting *const parse_infos[_NM_META_SETTING_TYPE_NUM] = {
 			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,
@@ -2317,6 +2554,13 @@ static const ParseInfoSetting *const parse_infos[_NM_META_SETTING_TYPE_NUM] = {
 				.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,
@@ -2345,6 +2589,13 @@ static const ParseInfoSetting *const parse_infos[_NM_META_SETTING_TYPE_NUM] = {
 				.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,
@@ -2523,7 +2774,7 @@ static const ParseInfoSetting *const parse_infos[_NM_META_SETTING_TYPE_NUM] = {
 static const ParseInfoProperty *
 _parse_info_find (NMSetting *setting,
                   const char *property_name,
-                  const char **out_setting_name)
+                  const NMMetaSettingInfo **out_setting_info)
 {
 	const NMMetaSettingInfo *setting_info;
 	const ParseInfoSetting *pis;
@@ -2562,11 +2813,13 @@ _parse_info_find (NMSetting *setting,
 	if (   !NM_IS_SETTING (setting)
 	    || !(setting_info = NM_SETTING_GET_CLASS (setting)->setting_info)) {
 		/* handle invalid setting objects gracefully. */
-		*out_setting_name = NULL;
+		*out_setting_info = NULL;
 		return NULL;
 	}
 
-	*out_setting_name = setting_info->setting_name;
+	nm_assert (setting_info->setting_name);
+
+	*out_setting_info = setting_info;
 
 	if ((pis = parse_infos[setting_info->meta_type])) {
 		G_STATIC_ASSERT_EXPR (G_STRUCT_OFFSET (ParseInfoProperty, property_name) == 0);
@@ -2593,32 +2846,42 @@ read_one_setting_value (KeyfileReaderInfo *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 *setting_name;
 	const char *key;
 	GType type;
 	guint64 u64;
 	gint64 i64;
 
 	nm_assert (!info->error);
-	nm_assert (property_info->param_spec);
+	nm_assert (   !property_info->param_spec
+	           || nm_streq (property_info->param_spec->name, property_info->name));
 
-	if ((property_info->param_spec->flags & (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)) != G_PARAM_WRITABLE)
-		return;
+	key = property_info->name;
 
-	key = property_info->param_spec->name;
+	pip = _parse_info_find (setting, key, &setting_info);
 
-	pip = _parse_info_find (setting, key, &setting_name);
+	nm_assert (setting_info);
 
-	nm_assert (setting_name);
-
-	if (   !pip
-	    && nm_streq (key, NM_SETTING_NAME))
-		return;
+	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;
+		}
+	}
 
-	if (pip && pip->parser_skip)
-		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
@@ -2626,7 +2889,7 @@ read_one_setting_value (KeyfileReaderInfo *info,
 	 * 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_name, key, &err)) {
+	    && !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, NM_KEYFILE_WARN_SEVERITY_WARN,
@@ -2637,7 +2900,8 @@ read_one_setting_value (KeyfileReaderInfo *info,
 		return;
 	}
 
-	if (pip && pip->parser) {
+	if (   pip
+	    && pip->parser) {
 		pip->parser (info, setting, key);
 		return;
 	}
@@ -2647,11 +2911,11 @@ read_one_setting_value (KeyfileReaderInfo *info,
 	if (type == G_TYPE_STRING) {
 		gs_free char *str_val = NULL;
 
-		str_val = nm_keyfile_plugin_kf_get_string (keyfile, setting_name, key, &err);
+		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_name, key, &err);
+		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
@@ -2662,7 +2926,7 @@ read_one_setting_value (KeyfileReaderInfo *info,
 				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_name, key, &err);
+		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
@@ -2675,11 +2939,11 @@ read_one_setting_value (KeyfileReaderInfo *info,
 	} else if (type == G_TYPE_BOOLEAN) {
 		gboolean bool_val;
 
-		bool_val = nm_keyfile_plugin_kf_get_boolean (keyfile, setting_name, key, &err);
+		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_name, key, &err);
+		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);
@@ -2691,7 +2955,7 @@ read_one_setting_value (KeyfileReaderInfo *info,
 				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_name, key, &err);
+		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
@@ -2702,7 +2966,7 @@ read_one_setting_value (KeyfileReaderInfo *info,
 				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_name, key, &err);
+		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
@@ -2720,7 +2984,7 @@ read_one_setting_value (KeyfileReaderInfo *info,
 		int i;
 		gboolean already_warned = FALSE;
 
-		tmp = nm_keyfile_plugin_kf_get_integer_list (keyfile, setting_name, key, &length, NULL);
+		tmp = nm_keyfile_plugin_kf_get_integer_list (keyfile, setting_info->setting_name, key, &length, NULL);
 
 		array = g_byte_array_sized_new (length);
 		for (i = 0; i < length; i++) {
@@ -2747,14 +3011,14 @@ read_one_setting_value (KeyfileReaderInfo *info,
 		gs_strfreev char **sa = NULL;
 		gsize length;
 
-		sa = nm_keyfile_plugin_kf_get_string_list (keyfile, setting_name, key, &length, NULL);
+		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_name, key, &err);
+		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
@@ -2765,7 +3029,7 @@ read_one_setting_value (KeyfileReaderInfo *info,
 				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_name, key, &err);
+		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
@@ -2887,13 +3151,11 @@ _read_setting (KeyfileReaderInfo *info)
 	}
 
 	for (i = 0; i < sett_info->property_infos_len; i++) {
-		const NMSettInfoProperty *property_info = &sett_info->property_infos[i];
-
-		if (property_info->param_spec) {
-			read_one_setting_value (info, setting, property_info);
-			if (info->error)
-				goto out;
-		}
+		read_one_setting_value (info,
+		                        setting,
+		                        &sett_info->property_infos[i]);
+		if (info->error)
+			goto out;
 	}
 
 out:
@@ -2937,7 +3199,7 @@ _read_setting_wireguard_peer (KeyfileReaderInfo *info)
 	if (str) {
 		if (!nm_wireguard_peer_set_preshared_key (peer, str, FALSE)) {
 			if (!handle_warn (info, key, NM_KEYFILE_WARN_SEVERITY_WARN,
-			                  _("key '%s.%s' is not not a valid 256 bit key in base64 encoding"),
+			                  _("key '%s.%s' is not a valid 256 bit key in base64 encoding"),
 			                  info->group, key))
 				return;
 		}
@@ -2950,7 +3212,7 @@ _read_setting_wireguard_peer (KeyfileReaderInfo *info)
 		if (   i64 == -1
 		    || !_nm_setting_secret_flags_valid (i64)) {
 			if (!handle_warn (info, key, NM_KEYFILE_WARN_SEVERITY_WARN,
-			                  _("key '%s.%s' is not not a valid secret flag"),
+			                  _("key '%s.%s' is not a valid secret flag"),
 			                  info->group, key))
 				return;
 		} else
@@ -2962,7 +3224,7 @@ _read_setting_wireguard_peer (KeyfileReaderInfo *info)
 	if (errno != ENODATA) {
 		if (i64 == -1) {
 			if (!handle_warn (info, key, NM_KEYFILE_WARN_SEVERITY_WARN,
-			                  _("key '%s.%s' is not not a integer in range 0 to 2^32"),
+			                  _("key '%s.%s' is not a integer in range 0 to 2^32"),
 			                  info->group, key))
 				return;
 		} else
@@ -2974,7 +3236,7 @@ _read_setting_wireguard_peer (KeyfileReaderInfo *info)
 	if (str && str[0]) {
 		if (!nm_wireguard_peer_set_endpoint (peer, str, FALSE)) {
 			if (!handle_warn (info, key, NM_KEYFILE_WARN_SEVERITY_WARN,
-			                  _("key '%s.%s' is not not a valid endpoint"),
+			                  _("key '%s.%s' is not a valid endpoint"),
 			                  info->group, key))
 				return;
 		}
@@ -3202,40 +3464,47 @@ write_setting_value (KeyfileWriterInfo *info,
                      NMSetting *setting,
                      const NMSettInfoProperty *property_info)
 {
+	const NMMetaSettingInfo *setting_info;
 	const ParseInfoProperty *pip;
-	const char *setting_name;
 	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));
 
-	if (!property_info->param_spec)
-		return;
-
-	key = property_info->param_spec->name;
+	key = property_info->name;
 
-	pip = _parse_info_find (setting, key, &setting_name);
+	pip = _parse_info_find (setting, key, &setting_info);
 
-	if (!setting_name) {
-		/* 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 (!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;
 	}
 
-	if (   !pip
-	    && nm_streq (key, NM_SETTING_NAME))
-		return;
-
-	if (pip && 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
@@ -3259,11 +3528,12 @@ write_setting_value (KeyfileWriterInfo *info,
 
 	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_name, key, NULL));
+		nm_assert (!g_key_file_has_key (info->keyfile, setting_info->setting_name, key, NULL));
 		goto out_unset_value;
 	}
 
-	if (pip && pip->writer) {
+	if (   pip
+	    && pip->writer) {
 		pip->writer (info, setting, key, &value);
 		goto out_unset_value;
 	}
@@ -3274,27 +3544,27 @@ write_setting_value (KeyfileWriterInfo *info,
 
 		str = g_value_get_string (&value);
 		if (str)
-			nm_keyfile_plugin_kf_set_string (info->keyfile, setting_name, key, 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_name, key, numstr);
+		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_name, key, numstr);
+		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_name, key, numstr);
+		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_name, key, numstr);
+		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_name, key,
+		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_name, key, numstr);
+		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;
@@ -3304,22 +3574,22 @@ write_setting_value (KeyfileWriterInfo *info,
 		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_name, key, data, len);
+			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_name, key, (const char **const) array, g_strv_length (array));
+		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_name, key, numstr);
+		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_name, key, numstr);
+		nm_keyfile_plugin_kf_set_value (info->keyfile, setting_info->setting_name, key, numstr);
 	} else
 		g_return_if_reached ();
 
diff --git a/libnm-core/nm-setting-8021x.c b/libnm-core/nm-setting-8021x.c
index 8a23fb34..5114ff74 100644
--- a/libnm-core/nm-setting-8021x.c
+++ b/libnm-core/nm-setting-8021x.c
@@ -24,7 +24,7 @@
 
 #include "nm-setting-8021x.h"
 
-#include "nm-utils/nm-secret-utils.h"
+#include "nm-glib-aux/nm-secret-utils.h"
 #include "nm-utils.h"
 #include "nm-crypto.h"
 #include "nm-utils-private.h"
diff --git a/libnm-core/nm-setting-bridge-port.c b/libnm-core/nm-setting-bridge-port.c
index 614c883c..7a8b345f 100644
--- a/libnm-core/nm-setting-bridge-port.c
+++ b/libnm-core/nm-setting-bridge-port.c
@@ -42,22 +42,60 @@
 
 /*****************************************************************************/
 
-NM_GOBJECT_PROPERTIES_DEFINE_BASE (
+NM_GOBJECT_PROPERTIES_DEFINE (NMSettingBridgePort,
 	PROP_PRIORITY,
 	PROP_PATH_COST,
 	PROP_HAIRPIN_MODE,
+	PROP_VLANS,
 );
 
 typedef struct {
 	guint16 priority;
 	guint16 path_cost;
 	gboolean hairpin_mode;
+	GPtrArray *vlans;
 } NMSettingBridgePortPrivate;
 
 G_DEFINE_TYPE (NMSettingBridgePort, nm_setting_bridge_port, NM_TYPE_SETTING)
 
 #define NM_SETTING_BRIDGE_PORT_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), NM_TYPE_SETTING_BRIDGE_PORT, NMSettingBridgePortPrivate))
 
+static int
+vlan_ptr_cmp (gconstpointer a, gconstpointer b)
+{
+	const NMBridgeVlan *vlan_a = *(const NMBridgeVlan **) a;
+	const NMBridgeVlan *vlan_b = *(const NMBridgeVlan **) b;
+
+	return nm_bridge_vlan_cmp (vlan_a, vlan_b);
+}
+
+gboolean
+_nm_setting_bridge_port_sort_vlans (NMSettingBridgePort *setting)
+{
+	NMSettingBridgePortPrivate *priv;
+	gboolean need_sort = FALSE;
+	guint i;
+
+	priv = NM_SETTING_BRIDGE_PORT_GET_PRIVATE (setting);
+
+	for (i = 1; i < priv->vlans->len; i++) {
+		NMBridgeVlan *vlan_prev = priv->vlans->pdata[i - 1];
+		NMBridgeVlan *vlan = priv->vlans->pdata[i];
+
+		if (nm_bridge_vlan_cmp (vlan_prev, vlan) > 0) {
+			need_sort = TRUE;
+			break;
+		}
+	}
+
+	if (need_sort) {
+		g_ptr_array_sort (priv->vlans, vlan_ptr_cmp);
+		_notify (setting, PROP_VLANS);
+	}
+
+	return need_sort;
+}
+
 /*****************************************************************************/
 
 /**
@@ -102,11 +140,171 @@ nm_setting_bridge_port_get_hairpin_mode (NMSettingBridgePort *setting)
 	return NM_SETTING_BRIDGE_PORT_GET_PRIVATE (setting)->hairpin_mode;
 }
 
+/**
+ * nm_setting_bridge_port_add_vlan:
+ * @setting: the #NMSettingBridgePort
+ * @vlan: the vlan to add
+ *
+ * Appends a new vlan and associated information to the setting.  The
+ * given vlan gets sealed and a reference to it is added.
+ *
+ * Since: 1.18
+ **/
+void
+nm_setting_bridge_port_add_vlan (NMSettingBridgePort *setting,
+                                 NMBridgeVlan *vlan)
+{
+	NMSettingBridgePortPrivate *priv;
+
+	g_return_if_fail (NM_IS_SETTING_BRIDGE_PORT (setting));
+	g_return_if_fail (vlan);
+
+	priv = NM_SETTING_BRIDGE_PORT_GET_PRIVATE (setting);
+
+	nm_bridge_vlan_seal (vlan);
+	nm_bridge_vlan_ref (vlan);
+
+	g_ptr_array_add (priv->vlans, vlan);
+	_notify (setting, PROP_VLANS);
+}
+
+/**
+ * nm_setting_bridge_port_get_num_vlans:
+ * @setting: the #NMSettingBridgePort
+ *
+ * Returns: the number of VLANs
+ *
+ * Since: 1.18
+ **/
+guint
+nm_setting_bridge_port_get_num_vlans (NMSettingBridgePort *setting)
+{
+	NMSettingBridgePortPrivate *priv;
+
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE_PORT (setting), 0);
+	priv = NM_SETTING_BRIDGE_PORT_GET_PRIVATE (setting);
+
+	return priv->vlans->len;
+}
+
+/**
+ * nm_setting_bridge_port_get_vlan:
+ * @setting: the #NMSettingBridgePort
+ * @idx: index number of the VLAN to return
+ *
+ * Returns: (transfer none): the VLAN at index @idx
+ *
+ * Since: 1.18
+ **/
+NMBridgeVlan *
+nm_setting_bridge_port_get_vlan (NMSettingBridgePort *setting, guint idx)
+{
+	NMSettingBridgePortPrivate *priv;
+
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE_PORT (setting), NULL);
+	priv = NM_SETTING_BRIDGE_PORT_GET_PRIVATE (setting);
+
+	g_return_val_if_fail (idx < priv->vlans->len, NULL);
+
+	return priv->vlans->pdata[idx];
+}
+
+/**
+ * nm_setting_bridge_port_remove_vlan:
+ * @setting: the #NMSettingBridgePort
+ * @idx: index number of the VLAN.
+ *
+ * Removes the vlan at index @idx.
+ *
+ * Since: 1.18
+ **/
+void
+nm_setting_bridge_port_remove_vlan (NMSettingBridgePort *setting, guint idx)
+{
+	NMSettingBridgePortPrivate *priv;
+
+	g_return_if_fail (NM_IS_SETTING_BRIDGE_PORT (setting));
+	priv = NM_SETTING_BRIDGE_PORT_GET_PRIVATE (setting);
+
+	g_return_if_fail (idx < priv->vlans->len);
+
+	g_ptr_array_remove_index (priv->vlans, idx);
+	_notify (setting, PROP_VLANS);
+}
+
+/**
+ * nm_setting_bridge_port_remove_vlan_by_vid:
+ * @setting: the #NMSettingBridgePort
+ * @vid_start: the vlan start index
+ * @vid_end: the vlan end index
+ *
+ * Remove the VLAN with range @vid_start to @vid_end.
+ * If @vid_end is zero, it is assumed to be equal to @vid_start
+ * and so the single-id VLAN with id @vid_start is removed.
+ *
+ * Returns: %TRUE if the vlan was found and removed; %FALSE otherwise
+ *
+ * Since: 1.18
+ **/
+gboolean
+nm_setting_bridge_port_remove_vlan_by_vid (NMSettingBridgePort *setting,
+                                           guint16 vid_start,
+                                           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];
+		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);
+			_notify (setting, PROP_VLANS);
+			return TRUE;
+		}
+	}
+	return FALSE;
+}
+
+/**
+ * nm_setting_bridge_port_clear_vlans:
+ * @setting: the #NMSettingBridgePort
+ *
+ * Removes all configured VLANs.
+ *
+ * Since: 1.18
+ **/
+void
+nm_setting_bridge_port_clear_vlans (NMSettingBridgePort *setting)
+{
+	NMSettingBridgePortPrivate *priv;
+
+	g_return_if_fail (NM_IS_SETTING_BRIDGE_PORT (setting));
+	priv = NM_SETTING_BRIDGE_PORT_GET_PRIVATE (setting);
+
+	if (priv->vlans->len != 0) {
+		g_ptr_array_set_size (priv->vlans, 0);
+		_notify (setting, PROP_VLANS);
+	}
+}
+
 /*****************************************************************************/
 
 static gboolean
 verify (NMSetting *setting, NMConnection *connection, GError **error)
 {
+	NMSettingBridgePortPrivate *priv;
+
+	priv = NM_SETTING_BRIDGE_PORT_GET_PRIVATE (setting);
+
 	if (connection) {
 		NMSettingConnection *s_con;
 		const char *slave_type;
@@ -136,9 +334,58 @@ verify (NMSetting *setting, NMConnection *connection, GError **error)
 		}
 	}
 
+	if (!_nm_utils_bridge_vlan_verify_list (priv->vlans,
+	                                        FALSE,
+	                                        error,
+	                                        NM_SETTING_BRIDGE_PORT_SETTING_NAME,
+	                                        NM_SETTING_BRIDGE_PORT_VLANS))
+		return FALSE;
+
+	/* Failures from here on are NORMALIZABLE... */
+
+	if (!_nm_utils_bridge_vlan_verify_list (priv->vlans,
+	                                        TRUE,
+	                                        error,
+	                                        NM_SETTING_BRIDGE_PORT_SETTING_NAME,
+	                                        NM_SETTING_BRIDGE_PORT_VLANS))
+		return NM_SETTING_VERIFY_NORMALIZABLE;
+
 	return TRUE;
 }
 
+static NMTernary
+compare_property (const NMSettInfoSetting *sett_info,
+                  guint property_idx,
+                  NMSetting *setting,
+                  NMSetting *other,
+                  NMSettingCompareFlags flags)
+{
+	NMSettingBridgePortPrivate *priv_a;
+	NMSettingBridgePortPrivate *priv_b;
+	guint i;
+
+	if (nm_streq (sett_info->property_infos[property_idx].name, NM_SETTING_BRIDGE_PORT_VLANS)) {
+		if (other) {
+			priv_a = NM_SETTING_BRIDGE_PORT_GET_PRIVATE (setting);
+			priv_b = NM_SETTING_BRIDGE_PORT_GET_PRIVATE (other);
+
+			if (priv_a->vlans->len != priv_b->vlans->len)
+				return FALSE;
+			for (i = 0; i < priv_a->vlans->len; i++) {
+				if (nm_bridge_vlan_cmp (priv_a->vlans->pdata[i], priv_b->vlans->pdata[i]))
+					return FALSE;
+			}
+		}
+		return TRUE;
+	}
+
+	return NM_SETTING_CLASS (nm_setting_bridge_port_parent_class)->compare_property (sett_info,
+	                                                                                 property_idx,
+	                                                                                 setting,
+	                                                                                 other,
+	                                                                                 flags);
+}
+
 /*****************************************************************************/
 
 static void
@@ -157,6 +404,11 @@ get_property (GObject *object, guint prop_id,
 	case PROP_HAIRPIN_MODE:
 		g_value_set_boolean (value, priv->hairpin_mode);
 		break;
+	case PROP_VLANS:
+		g_value_take_boxed (value, _nm_utils_copy_array (priv->vlans,
+		                                                 (NMUtilsCopyFunc) nm_bridge_vlan_ref,
+		                                                 (GDestroyNotify) nm_bridge_vlan_unref));
+		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
 		break;
@@ -179,6 +431,12 @@ set_property (GObject *object, guint prop_id,
 	case PROP_HAIRPIN_MODE:
 		priv->hairpin_mode = g_value_get_boolean (value);
 		break;
+	case PROP_VLANS:
+		g_ptr_array_unref (priv->vlans);
+		priv->vlans = _nm_utils_copy_array (g_value_get_boxed (value),
+		                                    (NMUtilsCopyFunc) _nm_bridge_vlan_dup_and_seal,
+		                                    (GDestroyNotify) nm_bridge_vlan_unref);
+		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
 		break;
@@ -190,6 +448,9 @@ set_property (GObject *object, guint prop_id,
 static void
 nm_setting_bridge_port_init (NMSettingBridgePort *setting)
 {
+	NMSettingBridgePortPrivate *priv = NM_SETTING_BRIDGE_PORT_GET_PRIVATE (setting);
+
+	priv->vlans = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_bridge_vlan_unref);
 }
 
 /**
@@ -206,16 +467,29 @@ nm_setting_bridge_port_new (void)
 }
 
 static void
+finalize (GObject *object)
+{
+	NMSettingBridgePortPrivate *priv = NM_SETTING_BRIDGE_PORT_GET_PRIVATE (object);
+
+	g_ptr_array_unref (priv->vlans);
+
+	G_OBJECT_CLASS (nm_setting_bridge_port_parent_class)->finalize (object);
+}
+
+static void
 nm_setting_bridge_port_class_init (NMSettingBridgePortClass *klass)
 {
 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
 	NMSettingClass *setting_class = NM_SETTING_CLASS (klass);
+	GArray *properties_override = _nm_sett_info_property_override_create_array ();
 
 	g_type_class_add_private (klass, sizeof (NMSettingBridgePortPrivate));
 
+	object_class->finalize = finalize;
 	object_class->get_property = get_property;
 	object_class->set_property = set_property;
 
+	setting_class->compare_property = compare_property;
 	setting_class->verify = verify;
 
 	/**
@@ -280,7 +554,48 @@ nm_setting_bridge_port_class_init (NMSettingBridgePortClass *klass)
 	                          NM_SETTING_PARAM_INFERRABLE |
 	                          G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * NMSettingBridgePort:vlans: (type GPtrArray(NMBridgeVlan))
+	 *
+	 * Array of bridge VLAN objects. In addition to the VLANs
+	 * specified here, the port will also have the default-pvid
+	 * VLAN configured on the bridge by the bridge.vlan-default-pvid
+	 * property.
+	 *
+	 * In nmcli the VLAN list can be specified with the following
+	 * syntax:
+	 *
+	 *  $vid [pvid] [untagged] [, $vid [pvid] [untagged]]...
+	 *
+	 * where $vid is either a single id between 1 and 4094 or a
+	 * range, represented as a couple of ids separated by a dash.
+	 *
+	 * Since: 1.18
+	 **/
+	/* ---ifcfg-rh---
+	 * property: vlans
+	 * variable: BRIDGE_PORT_VLANS
+	 * description: List of VLANs on the bridge port
+	 * example: BRIDGE_PORT_VLANS="1 pvid untagged,20,300-400 untagged"
+	 * ---end---
+	 */
+	obj_properties[PROP_VLANS] =
+	    g_param_spec_boxed (NM_SETTING_BRIDGE_PORT_VLANS, "", "",
+	                        G_TYPE_PTR_ARRAY,
+	                        G_PARAM_READWRITE |
+	                        NM_SETTING_PARAM_INFERRABLE |
+	                        G_PARAM_STATIC_STRINGS);
+
+	_properties_override_add_override (properties_override,
+	                                   obj_properties[PROP_VLANS],
+	                                   G_VARIANT_TYPE ("aa{sv}"),
+	                                   _nm_utils_bridge_vlans_to_dbus,
+	                                   _nm_utils_bridge_vlans_from_dbus,
+	                                   NULL);
+
 	g_object_class_install_properties (object_class, _PROPERTY_ENUMS_LAST, obj_properties);
 
-	_nm_setting_class_commit (setting_class, NM_META_SETTING_TYPE_BRIDGE_PORT);
+	_nm_setting_class_commit_full (setting_class, NM_META_SETTING_TYPE_BRIDGE_PORT,
+	                               NULL, properties_override);
+
 }
diff --git a/libnm-core/nm-setting-bridge-port.h b/libnm-core/nm-setting-bridge-port.h
index 0ff6d9f1..5b75c1ec 100644
--- a/libnm-core/nm-setting-bridge-port.h
+++ b/libnm-core/nm-setting-bridge-port.h
@@ -27,6 +27,7 @@
 #endif
 
 #include "nm-setting.h"
+#include "nm-setting-bridge.h"
 
 G_BEGIN_DECLS
 
@@ -42,6 +43,7 @@ G_BEGIN_DECLS
 #define NM_SETTING_BRIDGE_PORT_PRIORITY     "priority"
 #define NM_SETTING_BRIDGE_PORT_PATH_COST    "path-cost"
 #define NM_SETTING_BRIDGE_PORT_HAIRPIN_MODE "hairpin-mode"
+#define NM_SETTING_BRIDGE_PORT_VLANS        "vlans"
 
 /**
  * NMSettingBridgePort:
@@ -69,6 +71,22 @@ guint16     nm_setting_bridge_port_get_path_cost    (NMSettingBridgePort *settin
 
 gboolean    nm_setting_bridge_port_get_hairpin_mode (NMSettingBridgePort *setting);
 
+NM_AVAILABLE_IN_1_18
+void          nm_setting_bridge_port_add_vlan (NMSettingBridgePort *setting,
+                                               NMBridgeVlan *vlan);
+NM_AVAILABLE_IN_1_18
+guint         nm_setting_bridge_port_get_num_vlans (NMSettingBridgePort *setting);
+NM_AVAILABLE_IN_1_18
+NMBridgeVlan *nm_setting_bridge_port_get_vlan (NMSettingBridgePort *setting, guint idx);
+NM_AVAILABLE_IN_1_18
+void          nm_setting_bridge_port_remove_vlan (NMSettingBridgePort *setting, guint idx);
+NM_AVAILABLE_IN_1_18
+gboolean      nm_setting_bridge_port_remove_vlan_by_vid (NMSettingBridgePort *setting,
+                                                         guint16 vid_start,
+                                                         guint16 vid_end);
+NM_AVAILABLE_IN_1_18
+void          nm_setting_bridge_port_clear_vlans (NMSettingBridgePort *setting);
+
 G_END_DECLS
 
 #endif /* __NM_SETTING_BRIDGE_PORT_H__ */
diff --git a/libnm-core/nm-setting-bridge.c b/libnm-core/nm-setting-bridge.c
index e418013c..c6aca021 100644
--- a/libnm-core/nm-setting-bridge.c
+++ b/libnm-core/nm-setting-bridge.c
@@ -40,7 +40,7 @@
 
 /*****************************************************************************/
 
-NM_GOBJECT_PROPERTIES_DEFINE_BASE (
+NM_GOBJECT_PROPERTIES_DEFINE (NMSettingBridge,
 	PROP_MAC_ADDRESS,
 	PROP_STP,
 	PROP_PRIORITY,
@@ -50,6 +50,9 @@ NM_GOBJECT_PROPERTIES_DEFINE_BASE (
 	PROP_AGEING_TIME,
 	PROP_GROUP_FORWARD_MASK,
 	PROP_MULTICAST_SNOOPING,
+	PROP_VLAN_FILTERING,
+	PROP_VLAN_DEFAULT_PVID,
+	PROP_VLANS,
 );
 
 typedef struct {
@@ -62,6 +65,9 @@ typedef struct {
 	guint32  ageing_time;
 	guint16  group_forward_mask;
 	gboolean multicast_snooping;
+	gboolean vlan_filtering;
+	guint16  vlan_default_pvid;
+	GPtrArray *vlans;
 } NMSettingBridgePrivate;
 
 G_DEFINE_TYPE (NMSettingBridge, nm_setting_bridge, NM_TYPE_SETTING)
@@ -70,6 +76,506 @@ G_DEFINE_TYPE (NMSettingBridge, nm_setting_bridge, NM_TYPE_SETTING)
 
 /*****************************************************************************/
 
+G_DEFINE_BOXED_TYPE (NMBridgeVlan, nm_bridge_vlan, _nm_bridge_vlan_dup, nm_bridge_vlan_unref)
+
+struct _NMBridgeVlan {
+	guint refcount;
+	guint16 vid_start;
+	guint16 vid_end;
+	bool untagged:1;
+	bool pvid:1;
+	bool sealed:1;
+};
+
+static gboolean
+NM_IS_BRIDGE_VLAN (const NMBridgeVlan *self, gboolean also_sealed)
+{
+	return    self
+	       && self->refcount > 0
+	       && (also_sealed || !self->sealed);
+}
+
+/**
+ * nm_bridge_vlan_new:
+ * @vid_start: the start VLAN id, must be between 1 and 4094.
+ * @vid_end: the end VLAN id, must be 0 or between @vid_start and 4094.
+ *
+ * Creates a new #NMBridgeVlan object for the given VLAN id range.
+ * Setting @vid_end to 0 is equivalent to setting it to @vid_start
+ * and creates a single-id VLAN.
+ *
+ * Returns: (transfer full): the new #NMBridgeVlan object.
+ *
+ * Since: 1.18
+ **/
+NMBridgeVlan *
+nm_bridge_vlan_new (guint16 vid_start, guint16 vid_end)
+{
+	NMBridgeVlan *vlan;
+
+	if (vid_end == 0)
+		vid_end = vid_start;
+
+	g_return_val_if_fail (vid_start >= NM_BRIDGE_VLAN_VID_MIN, NULL);
+	g_return_val_if_fail (vid_end <= NM_BRIDGE_VLAN_VID_MAX, NULL);
+	g_return_val_if_fail (vid_start <= vid_end, NULL);
+
+	vlan = g_slice_new0 (NMBridgeVlan);
+	vlan->refcount = 1;
+	vlan->vid_start = vid_start;
+	vlan->vid_end = vid_end;
+
+	return vlan;
+}
+
+/**
+ * nm_bridge_vlan_ref:
+ * @vlan: the #NMBridgeVlan
+ *
+ * Increases the reference count of the object.
+ *
+ * Returns: the input argument @vlan object.
+ *
+ * Since: 1.18
+ **/
+NMBridgeVlan *
+nm_bridge_vlan_ref (NMBridgeVlan *vlan)
+{
+	g_return_val_if_fail (NM_IS_BRIDGE_VLAN (vlan, TRUE), NULL);
+
+	nm_assert (vlan->refcount < G_MAXUINT);
+
+	vlan->refcount++;
+	return vlan;
+}
+
+/**
+ * nm_bridge_vlan_unref:
+ * @vlan: the #NMBridgeVlan
+ *
+ * Decreases the reference count of the object.  If the reference count
+ * reaches zero the object will be destroyed.
+ *
+ * Since: 1.18
+ **/
+void
+nm_bridge_vlan_unref (NMBridgeVlan *vlan)
+{
+	g_return_if_fail (NM_IS_BRIDGE_VLAN (vlan, TRUE));
+
+	if (--vlan->refcount == 0)
+		g_slice_free (NMBridgeVlan, vlan);
+}
+
+/**
+ * nm_bridge_vlan_cmp:
+ * @a: a #NMBridgeVlan
+ * @b: another #NMBridgeVlan
+ *
+ * Compare two bridge VLAN objects.
+ *
+ * Returns: zero of the two instances are equivalent or
+ *   a non-zero integer otherwise. This defines a total ordering
+ *   over the VLANs. Whether a VLAN is sealed or not does not
+ *   affect the comparison.
+ *
+ * Since: 1.18
+ **/
+int
+nm_bridge_vlan_cmp (const NMBridgeVlan *a, const NMBridgeVlan *b)
+{
+	g_return_val_if_fail (NM_IS_BRIDGE_VLAN (a, TRUE), 0);
+	g_return_val_if_fail (NM_IS_BRIDGE_VLAN (b, TRUE), 0);
+
+	NM_CMP_SELF (a, b);
+	NM_CMP_FIELD (a, b, vid_start);
+	NM_CMP_FIELD (a, b, vid_end);
+	NM_CMP_FIELD_BOOL (a, b, untagged);
+	NM_CMP_FIELD_BOOL (a, b, pvid);
+
+	return 0;
+}
+
+NMBridgeVlan *
+_nm_bridge_vlan_dup (const NMBridgeVlan *vlan)
+{
+	g_return_val_if_fail (NM_IS_BRIDGE_VLAN (vlan, TRUE), NULL);
+
+	if (vlan->sealed) {
+		nm_bridge_vlan_ref ((NMBridgeVlan *) vlan);
+		return (NMBridgeVlan *) vlan;
+	}
+
+	return nm_bridge_vlan_new_clone (vlan);
+}
+
+NMBridgeVlan *
+_nm_bridge_vlan_dup_and_seal (const NMBridgeVlan *vlan)
+{
+	NMBridgeVlan *new;
+
+	g_return_val_if_fail (NM_IS_BRIDGE_VLAN (vlan, TRUE), NULL);
+
+	new = _nm_bridge_vlan_dup (vlan);
+	nm_bridge_vlan_seal (new);
+
+	return new;
+}
+
+/**
+ * nm_bridge_vlan_get_vid_range:
+ * @vlan: the #NMBridgeVlan
+ * @vid_start: location to store the VLAN id range start.
+ * @vid_end: location to store the VLAN id range end
+ *
+ * Gets the VLAN id range.
+ *
+ * Returns: %TRUE is the VLAN specifies a range, %FALSE if it is
+ * a single-id VLAN.
+ *
+ * Since: 1.18
+ **/
+gboolean
+nm_bridge_vlan_get_vid_range (const NMBridgeVlan *vlan,
+                              guint16 *vid_start,
+                              guint16 *vid_end)
+{
+	g_return_val_if_fail (NM_IS_BRIDGE_VLAN (vlan, TRUE), 0);
+
+	NM_SET_OUT (vid_start, vlan->vid_start);
+	NM_SET_OUT (vid_end, vlan->vid_end);
+
+	return vlan->vid_start != vlan->vid_end;
+}
+
+/**
+ * nm_bridge_vlan_is_untagged:
+ * @vlan: the #NMBridgeVlan
+ *
+ * Returns whether the VLAN is untagged.
+ *
+ * Returns: %TRUE if the VLAN is untagged, %FALSE otherwise
+ *
+ * Since: 1.18
+ **/
+gboolean
+nm_bridge_vlan_is_untagged (const NMBridgeVlan *vlan)
+{
+	g_return_val_if_fail (NM_IS_BRIDGE_VLAN (vlan, TRUE), FALSE);
+
+	return vlan->untagged;
+}
+
+/**
+ * nm_bridge_vlan_is_pvid:
+ * @vlan: the #NMBridgeVlan
+ *
+ * Returns whether the VLAN is the PVID for the port.
+ *
+ * Returns: %TRUE if the VLAN is the PVID
+ *
+ * Since: 1.18
+ **/
+gboolean
+nm_bridge_vlan_is_pvid (const NMBridgeVlan *vlan)
+{
+	g_return_val_if_fail (NM_IS_BRIDGE_VLAN (vlan, TRUE), FALSE);
+
+	return vlan->pvid;
+}
+
+/**
+ * nm_bridge_vlan_set_untagged:
+ * @vlan: the #NMBridgeVlan
+ * @value: the new value
+ *
+ * Change the value of the untagged property of the VLAN.
+ *
+ * Since: 1.18
+ **/
+void
+nm_bridge_vlan_set_untagged (NMBridgeVlan *vlan, gboolean value)
+{
+	g_return_if_fail (NM_IS_BRIDGE_VLAN (vlan, FALSE));
+
+	vlan->untagged = value;
+}
+
+/**
+ * nm_bridge_vlan_set_pvid:
+ * @vlan: the #NMBridgeVlan
+ * @value: the new value
+ *
+ * Change the value of the PVID property of the VLAN. It
+ * is invalid to set the value to %TRUE for non-single-id
+ * VLANs.
+ *
+ * Since: 1.18
+ **/
+void
+nm_bridge_vlan_set_pvid (NMBridgeVlan *vlan, gboolean value)
+{
+	g_return_if_fail (NM_IS_BRIDGE_VLAN (vlan, FALSE));
+	g_return_if_fail (!value || vlan->vid_start == vlan->vid_end);
+
+	vlan->pvid = value;
+}
+
+/**
+ * nm_bridge_vlan_is_sealed:
+ * @vlan: the #NMBridgeVlan instance
+ *
+ * Returns: whether @self is sealed or not.
+ *
+ * Since: 1.18
+ */
+gboolean
+nm_bridge_vlan_is_sealed (const NMBridgeVlan *vlan)
+{
+	g_return_val_if_fail (NM_IS_BRIDGE_VLAN (vlan, TRUE), FALSE);
+
+	return vlan->sealed;
+}
+
+/**
+ * nm_bridge_vlan_seal:
+ * @vlan: the #NMBridgeVlan instance
+ *
+ * Seal the #NMBridgeVlan instance. Afterwards, it is a bug
+ * to call all functions that modify the instance (except ref/unref).
+ * A sealed instance cannot be unsealed again, but you can create
+ * an unsealed copy with nm_bridge_vlan_new_clone().
+ *
+ * Since: 1.18
+ */
+void
+nm_bridge_vlan_seal (NMBridgeVlan *vlan)
+{
+	g_return_if_fail (NM_IS_BRIDGE_VLAN (vlan, TRUE));
+
+	vlan->sealed = TRUE;
+}
+
+/**
+ * nm_bridge_vlan_new_clone:
+ * @vlan: the #NMBridgeVlan instance to copy
+ *
+ * Returns: (transfer full): a clone of @vlan. This instance
+ *   is always unsealed.
+ *
+ * Since: 1.18
+ */
+NMBridgeVlan *
+nm_bridge_vlan_new_clone (const NMBridgeVlan *vlan)
+{
+	NMBridgeVlan *copy;
+
+	g_return_val_if_fail (NM_IS_BRIDGE_VLAN (vlan, TRUE), NULL);
+
+	copy = nm_bridge_vlan_new (vlan->vid_start, vlan->vid_end);
+	copy->untagged = vlan->untagged;
+	copy->pvid = vlan->pvid;
+
+	return copy;
+}
+
+void
+_nm_bridge_vlan_str_append_rest (const NMBridgeVlan *vlan,
+                                 GString *string,
+                                 gboolean leading_space)
+{
+	if (nm_bridge_vlan_is_pvid (vlan)) {
+		if (leading_space)
+			g_string_append_c (string, ' ');
+		g_string_append (string, "pvid");
+		leading_space = TRUE;
+	}
+	if (nm_bridge_vlan_is_untagged (vlan)) {
+		if (leading_space)
+			g_string_append_c (string, ' ');
+		g_string_append (string, "untagged");
+		leading_space = TRUE;
+	}
+}
+
+/**
+ * nm_bridge_vlan_to_str:
+ * @vlan: the %NMBridgeVlan
+ * @error: location of the error
+ *
+ * Convert a %NMBridgeVlan to a string.
+ *
+ * Returns: formatted string or %NULL
+ *
+ * Since: 1.18
+ */
+char *
+nm_bridge_vlan_to_str (const NMBridgeVlan *vlan, GError **error)
+{
+	GString *string;
+
+	g_return_val_if_fail (vlan, NULL);
+	g_return_val_if_fail (!error || !*error, NULL);
+
+	/* The function never fails at the moment, but it might in the
+	 * future if more parameters are added to the object that could
+	 * make it invalid. */
+
+	string = g_string_sized_new (28);
+
+	if (vlan->vid_start == vlan->vid_end)
+		g_string_append_printf (string, "%u", vlan->vid_start);
+	else
+		g_string_append_printf (string, "%u-%u", vlan->vid_start, vlan->vid_end);
+
+	_nm_bridge_vlan_str_append_rest (vlan, string, TRUE);
+
+	return g_string_free (string, FALSE);
+}
+
+/**
+ * nm_bridge_vlan_from_str:
+ * @str: the string representation of a bridge VLAN
+ * @error: location of the error
+ *
+ * Parses the string representation of the queueing
+ * discipline to a %NMBridgeVlan instance.
+ *
+ * Returns: the %NMBridgeVlan or %NULL
+ *
+ * Since: 1.18
+ */
+NMBridgeVlan *
+nm_bridge_vlan_from_str (const char *str, GError **error)
+{
+	NMBridgeVlan *vlan = NULL;
+	gs_free const char **tokens = NULL;
+	guint i, vid_start, vid_end = 0;
+	gboolean pvid = FALSE;
+	gboolean untagged = FALSE;
+	char *c;
+
+	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 || !tokens[0]) {
+		g_set_error_literal (error,
+		                     NM_CONNECTION_ERROR,
+		                     NM_CONNECTION_ERROR_FAILED,
+		                     "missing VLAN id");
+		return NULL;
+	}
+
+	c = strchr (tokens[0], '-');
+	if (c)
+		*c = '\0';
+
+	vid_start = _nm_utils_ascii_str_to_uint64 (tokens[0],
+	                                           10,
+	                                           NM_BRIDGE_VLAN_VID_MIN,
+	                                           NM_BRIDGE_VLAN_VID_MAX,
+	                                           G_MAXUINT);
+	if (vid_start == G_MAXUINT) {
+		g_set_error (error,
+		             NM_CONNECTION_ERROR,
+		             NM_CONNECTION_ERROR_FAILED,
+		             "invalid VLAN id range start '%s', must be in [1,4094]", tokens[0]);
+		return NULL;
+	}
+
+	if (c) {
+		vid_end = _nm_utils_ascii_str_to_uint64 (c + 1,
+		                                         10,
+		                                         NM_BRIDGE_VLAN_VID_MIN,
+		                                         NM_BRIDGE_VLAN_VID_MAX,
+		                                         G_MAXUINT);
+		if (vid_end == G_MAXUINT) {
+			g_set_error (error,
+			             NM_CONNECTION_ERROR,
+			             NM_CONNECTION_ERROR_FAILED,
+			             "invalid VLAN id range end '%s', must be in [1,4094]", c + 1);
+			return NULL;
+		}
+		if (vid_end < vid_start) {
+			g_set_error (error,
+			             NM_CONNECTION_ERROR,
+			             NM_CONNECTION_ERROR_FAILED,
+			             "invalid VLAN id range %u-%u, start VLAN id must be less than end VLAN id",
+			             vid_start, vid_end);
+			return NULL;
+		}
+	} else
+		vid_end = vid_start;
+
+	for (i = 1; tokens[i]; i++) {
+		if (nm_streq (tokens[i], "pvid")) {
+			if (vid_start != vid_end) {
+				g_set_error_literal (error,
+				                     NM_CONNECTION_ERROR,
+				                     NM_CONNECTION_ERROR_FAILED,
+				                     "a VLAN range can't be a PVID");
+				return NULL;
+			}
+			pvid = TRUE;
+		} else if (nm_streq (tokens[i], "untagged"))
+			untagged = TRUE;
+		else {
+			g_set_error (error,
+			             NM_CONNECTION_ERROR,
+			             NM_CONNECTION_ERROR_FAILED,
+			             "invalid option '%s'", tokens[i]);
+			return NULL;
+		}
+	}
+
+	vlan = nm_bridge_vlan_new (vid_start, vid_end);
+	nm_bridge_vlan_set_pvid (vlan, pvid);
+	nm_bridge_vlan_set_untagged (vlan, untagged);
+
+	return vlan;
+}
+
+/*****************************************************************************/
+
+static int
+vlan_ptr_cmp (gconstpointer a, gconstpointer b)
+{
+	const NMBridgeVlan *vlan_a = *(const NMBridgeVlan **) a;
+	const NMBridgeVlan *vlan_b = *(const NMBridgeVlan **) b;
+
+	return nm_bridge_vlan_cmp (vlan_a, vlan_b);
+}
+
+gboolean
+_nm_setting_bridge_sort_vlans (NMSettingBridge *setting)
+{
+	NMSettingBridgePrivate *priv;
+	gboolean need_sort = FALSE;
+	guint i;
+
+	priv = NM_SETTING_BRIDGE_GET_PRIVATE (setting);
+
+	for (i = 1; i < priv->vlans->len; i++) {
+		NMBridgeVlan *vlan_prev = priv->vlans->pdata[i - 1];
+		NMBridgeVlan *vlan = priv->vlans->pdata[i];
+
+		if (nm_bridge_vlan_cmp (vlan_prev, vlan) > 0) {
+			need_sort = TRUE;
+			break;
+		}
+	}
+
+	if (need_sort) {
+		g_ptr_array_sort (priv->vlans, vlan_ptr_cmp);
+		_notify (setting, PROP_VLANS);
+	}
+
+	return need_sort;
+}
+
+/*****************************************************************************/
+
 /**
  * nm_setting_bridge_get_mac_address:
  * @setting: the #NMSettingBridge
@@ -200,6 +706,194 @@ nm_setting_bridge_get_multicast_snooping (NMSettingBridge *setting)
 	return NM_SETTING_BRIDGE_GET_PRIVATE (setting)->multicast_snooping;
 }
 
+/**
+ * nm_setting_bridge_get_vlan_filtering:
+ * @setting: the #NMSettingBridge
+ *
+ * Returns: the #NMSettingBridge:vlan-filtering property of the setting
+ *
+ * Since: 1.18
+ **/
+gboolean
+nm_setting_bridge_get_vlan_filtering (NMSettingBridge *setting)
+{
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE (setting), FALSE);
+
+	return NM_SETTING_BRIDGE_GET_PRIVATE (setting)->vlan_filtering;
+}
+
+/**
+ * nm_setting_bridge_get_vlan_default_pvid:
+ * @setting: the #NMSettingBridge
+ *
+ * Returns: the #NMSettingBridge:vlan-default-pvid property of the setting
+ *
+ * Since: 1.18
+ **/
+guint16
+nm_setting_bridge_get_vlan_default_pvid (NMSettingBridge *setting)
+{
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE (setting), 1);
+
+	return NM_SETTING_BRIDGE_GET_PRIVATE (setting)->vlan_default_pvid;
+}
+
+/**
+ * nm_setting_bridge_add_vlan:
+ * @setting: the #NMSettingBridge
+ * @vlan: the vlan to add
+ *
+ * Appends a new vlan and associated information to the setting.  The
+ * given vlan gets sealed and a reference to it is added.
+ *
+ * Since: 1.18
+ **/
+void
+nm_setting_bridge_add_vlan (NMSettingBridge *setting,
+                            NMBridgeVlan *vlan)
+{
+	NMSettingBridgePrivate *priv;
+
+	g_return_if_fail (NM_IS_SETTING_BRIDGE (setting));
+	g_return_if_fail (vlan);
+
+	priv = NM_SETTING_BRIDGE_GET_PRIVATE (setting);
+
+	nm_bridge_vlan_seal (vlan);
+	nm_bridge_vlan_ref (vlan);
+
+	g_ptr_array_add (priv->vlans, vlan);
+	_notify (setting, PROP_VLANS);
+}
+
+/**
+ * nm_setting_bridge_get_num_vlans:
+ * @setting: the #NMSettingBridge
+ *
+ * Returns: the number of VLANs
+ *
+ * Since: 1.18
+ **/
+guint
+nm_setting_bridge_get_num_vlans (NMSettingBridge *setting)
+{
+	NMSettingBridgePrivate *priv;
+
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE (setting), 0);
+	priv = NM_SETTING_BRIDGE_GET_PRIVATE (setting);
+
+	return priv->vlans->len;
+}
+
+/**
+ * nm_setting_bridge_get_vlan:
+ * @setting: the #NMSettingBridge
+ * @idx: index number of the VLAN to return
+ *
+ * Returns: (transfer none): the VLAN at index @idx
+ *
+ * Since: 1.18
+ **/
+NMBridgeVlan *
+nm_setting_bridge_get_vlan (NMSettingBridge *setting, guint idx)
+{
+	NMSettingBridgePrivate *priv;
+
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE (setting), NULL);
+	priv = NM_SETTING_BRIDGE_GET_PRIVATE (setting);
+
+	g_return_val_if_fail (idx < priv->vlans->len, NULL);
+
+	return priv->vlans->pdata[idx];
+}
+
+/**
+ * nm_setting_bridge_remove_vlan:
+ * @setting: the #NMSettingBridge
+ * @idx: index number of the VLAN.
+ *
+ * Removes the vlan at index @idx.
+ *
+ * Since: 1.18
+ **/
+void
+nm_setting_bridge_remove_vlan (NMSettingBridge *setting, guint idx)
+{
+	NMSettingBridgePrivate *priv;
+
+	g_return_if_fail (NM_IS_SETTING_BRIDGE (setting));
+	priv = NM_SETTING_BRIDGE_GET_PRIVATE (setting);
+
+	g_return_if_fail (idx < priv->vlans->len);
+
+	g_ptr_array_remove_index (priv->vlans, idx);
+	_notify (setting, PROP_VLANS);
+}
+
+/**
+ * nm_setting_bridge_remove_vlan_by_vid:
+ * @setting: the #NMSettingBridge
+ * @vid_start: the vlan start index
+ * @vid_end: the vlan end index
+ *
+ * Remove the VLAN with range @vid_start to @vid_end.
+ * If @vid_end is zero, it is assumed to be equal to @vid_start
+ * and so the single-id VLAN with id @vid_start is removed.
+ *
+ * Returns: %TRUE if the vlan was found and removed; %FALSE otherwise
+ *
+ * Since: 1.18
+ **/
+gboolean
+nm_setting_bridge_remove_vlan_by_vid (NMSettingBridge *setting,
+                                      guint16 vid_start,
+                                      guint16 vid_end)
+{
+	NMSettingBridgePrivate *priv;
+	NMBridgeVlan *vlan;
+	guint i;
+
+	g_return_val_if_fail (NM_IS_SETTING_BRIDGE (setting), FALSE);
+	priv = NM_SETTING_BRIDGE_GET_PRIVATE (setting);
+
+	if (vid_end == 0)
+		vid_end = vid_start;
+
+	for (i = 0; i < priv->vlans->len; i++) {
+		vlan = (NMBridgeVlan *) priv->vlans->pdata[i];
+		if (vlan->vid_start == vid_start && vlan->vid_end == vid_end) {
+			g_ptr_array_remove_index (priv->vlans, i);
+			_notify (setting, PROP_VLANS);
+			return TRUE;
+		}
+	}
+	return FALSE;
+}
+
+/**
+ * nm_setting_bridge_clear_vlans:
+ * @setting: the #NMSettingBridge
+ *
+ * Removes all configured VLANs.
+ *
+ * Since: 1.18
+ **/
+void
+nm_setting_bridge_clear_vlans (NMSettingBridge *setting)
+{
+	NMSettingBridgePrivate *priv;
+
+	g_return_if_fail (NM_IS_SETTING_BRIDGE (setting));
+	priv = NM_SETTING_BRIDGE_GET_PRIVATE (setting);
+
+	if (priv->vlans->len != 0) {
+		g_ptr_array_set_size (priv->vlans, 0);
+		_notify (setting, PROP_VLANS);
+	}
+}
+
+/*****************************************************************************/
+
 static gboolean
 check_range (guint32 val,
              guint32 min,
@@ -278,7 +972,59 @@ verify (NMSetting *setting, NMConnection *connection, GError **error)
 		return FALSE;
 	}
 
-	return _nm_connection_verify_required_interface_name (connection, error);
+	if (!_nm_connection_verify_required_interface_name (connection, error))
+		return FALSE;
+
+	if (!_nm_utils_bridge_vlan_verify_list (priv->vlans,
+	                                        FALSE,
+	                                        error,
+	                                        NM_SETTING_BRIDGE_SETTING_NAME,
+	                                        NM_SETTING_BRIDGE_VLANS))
+		return FALSE;
+
+	/* Failures from here on are NORMALIZABLE... */
+
+	if (!_nm_utils_bridge_vlan_verify_list (priv->vlans,
+	                                        TRUE,
+	                                        error,
+	                                        NM_SETTING_BRIDGE_SETTING_NAME,
+	                                        NM_SETTING_BRIDGE_VLANS))
+		return NM_SETTING_VERIFY_NORMALIZABLE;
+
+	return TRUE;
+}
+
+static NMTernary
+compare_property (const NMSettInfoSetting *sett_info,
+                  guint property_idx,
+                  NMSetting *setting,
+                  NMSetting *other,
+                  NMSettingCompareFlags flags)
+{
+	NMSettingBridgePrivate *priv_a;
+	NMSettingBridgePrivate *priv_b;
+	guint i;
+
+	if (nm_streq (sett_info->property_infos[property_idx].name, NM_SETTING_BRIDGE_VLANS)) {
+		if (other) {
+			priv_a = NM_SETTING_BRIDGE_GET_PRIVATE (setting);
+			priv_b = NM_SETTING_BRIDGE_GET_PRIVATE (other);
+
+			if (priv_a->vlans->len != priv_b->vlans->len)
+				return FALSE;
+			for (i = 0; i < priv_a->vlans->len; i++) {
+				if (nm_bridge_vlan_cmp (priv_a->vlans->pdata[i], priv_b->vlans->pdata[i]))
+					return FALSE;
+			}
+		}
+		return TRUE;
+	}
+
+	return NM_SETTING_CLASS (nm_setting_bridge_parent_class)->compare_property (sett_info,
+	                                                                            property_idx,
+	                                                                            setting,
+	                                                                            other,
+	                                                                            flags);
 }
 
 /*****************************************************************************/
@@ -318,6 +1064,17 @@ get_property (GObject *object, guint prop_id,
 	case PROP_MULTICAST_SNOOPING:
 		g_value_set_boolean (value, priv->multicast_snooping);
 		break;
+	case PROP_VLAN_FILTERING:
+		g_value_set_boolean (value, priv->vlan_filtering);
+		break;
+	case PROP_VLAN_DEFAULT_PVID:
+		g_value_set_uint (value, priv->vlan_default_pvid);
+		break;
+	case PROP_VLANS:
+		g_value_take_boxed (value, _nm_utils_copy_array (priv->vlans,
+		                                                 (NMUtilsCopyFunc) nm_bridge_vlan_ref,
+		                                                 (GDestroyNotify) nm_bridge_vlan_unref));
+		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
 		break;
@@ -360,6 +1117,18 @@ set_property (GObject *object, guint prop_id,
 	case PROP_MULTICAST_SNOOPING:
 		priv->multicast_snooping = g_value_get_boolean (value);
 		break;
+	case PROP_VLAN_FILTERING:
+		priv->vlan_filtering = g_value_get_boolean (value);
+		break;
+	case PROP_VLAN_DEFAULT_PVID:
+		priv->vlan_default_pvid = g_value_get_uint (value);
+		break;
+	case PROP_VLANS:
+		g_ptr_array_unref (priv->vlans);
+		priv->vlans = _nm_utils_copy_array (g_value_get_boxed (value),
+		                                    (NMUtilsCopyFunc) _nm_bridge_vlan_dup_and_seal,
+		                                    (GDestroyNotify) nm_bridge_vlan_unref);
+		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
 		break;
@@ -371,6 +1140,9 @@ set_property (GObject *object, guint prop_id,
 static void
 nm_setting_bridge_init (NMSettingBridge *setting)
 {
+	NMSettingBridgePrivate *priv = NM_SETTING_BRIDGE_GET_PRIVATE (setting);
+
+	priv->vlans = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_bridge_vlan_unref);
 }
 
 /**
@@ -392,6 +1164,7 @@ finalize (GObject *object)
 	NMSettingBridgePrivate *priv = NM_SETTING_BRIDGE_GET_PRIVATE (object);
 
 	g_free (priv->mac_address);
+	g_ptr_array_unref (priv->vlans);
 
 	G_OBJECT_CLASS (nm_setting_bridge_parent_class)->finalize (object);
 }
@@ -409,6 +1182,7 @@ nm_setting_bridge_class_init (NMSettingBridgeClass *klass)
 	object_class->set_property = set_property;
 	object_class->finalize     = finalize;
 
+	setting_class->compare_property = compare_property;
 	setting_class->verify = verify;
 
 	/**
@@ -627,6 +1401,91 @@ nm_setting_bridge_class_init (NMSettingBridgeClass *klass)
 	                          NM_SETTING_PARAM_INFERRABLE |
 	                          G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * NMSettingBridge:vlan-filtering:
+	 *
+	 * Control whether VLAN filtering is enabled on the bridge.
+	 *
+	 * Since: 1.18
+	 **/
+	/* ---ifcfg-rh---
+	 * property: vlan-filtering
+	 * variable: BRIDGING_OPTS: vlan_filtering=
+	 * values: 0 or 1
+	 * default: 0
+	 * description: VLAN filtering support.
+	 * ---end---
+	 */
+	obj_properties[PROP_VLAN_FILTERING] =
+	    g_param_spec_boolean (NM_SETTING_BRIDGE_VLAN_FILTERING, "", "",
+	                          FALSE,
+	                          G_PARAM_READWRITE |
+	                          G_PARAM_CONSTRUCT |
+	                          NM_SETTING_PARAM_INFERRABLE |
+	                          G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * NMSettingBridge:vlan-default-pvid:
+	 *
+	 * The default PVID for the ports of the bridge, that is the VLAN id
+	 * assigned to incoming untagged frames.
+	 *
+	 * Since: 1.18
+	 **/
+	/* ---ifcfg-rh---
+	 * property: vlan-default-pvid
+	 * variable: BRIDGING_OPTS: default_pvid=
+	 * values: 0 - 4094
+	 * default: 1
+	 * description: default VLAN PVID.
+	 * ---end---
+	 */
+	obj_properties[PROP_VLAN_DEFAULT_PVID] =
+	    g_param_spec_uint (NM_SETTING_BRIDGE_VLAN_DEFAULT_PVID, "", "",
+	                       0, NM_BRIDGE_VLAN_VID_MAX, 1,
+	                       G_PARAM_READWRITE |
+	                       G_PARAM_CONSTRUCT |
+	                       NM_SETTING_PARAM_INFERRABLE |
+	                       G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * NMSettingBridge:vlans: (type GPtrArray(NMBridgeVlan))
+	 *
+	 * Array of bridge VLAN objects. In addition to the VLANs
+	 * specified here, the bridge will also have the default-pvid
+	 * VLAN configured  by the bridge.vlan-default-pvid property.
+	 *
+	 * In nmcli the VLAN list can be specified with the following
+	 * syntax:
+	 *
+	 *  $vid [pvid] [untagged] [, $vid [pvid] [untagged]]...
+	 *
+	 * where $vid is either a single id between 1 and 4094 or a
+	 * range, represented as a couple of ids separated by a dash.
+	 *
+	 * Since: 1.18
+	 **/
+	/* ---ifcfg-rh---
+	 * property: vlans
+	 * variable: BRIDGE_VLANS
+	 * description: List of VLANs on the bridge
+	 * example: BRIDGE_VLANS="1 pvid untagged,20,300-400 untagged"
+	 * ---end---
+	 */
+	obj_properties[PROP_VLANS] =
+	    g_param_spec_boxed (NM_SETTING_BRIDGE_VLANS, "", "",
+	                        G_TYPE_PTR_ARRAY,
+	                        G_PARAM_READWRITE |
+	                        NM_SETTING_PARAM_INFERRABLE |
+	                        G_PARAM_STATIC_STRINGS);
+
+	_properties_override_add_override (properties_override,
+	                                   obj_properties[PROP_VLANS],
+	                                   G_VARIANT_TYPE ("aa{sv}"),
+	                                   _nm_utils_bridge_vlans_to_dbus,
+	                                   _nm_utils_bridge_vlans_from_dbus,
+	                                   NULL);
+
 	/* ---dbus---
 	 * property: interface-name
 	 * format: string
diff --git a/libnm-core/nm-setting-bridge.h b/libnm-core/nm-setting-bridge.h
index d640dde3..c01ab35c 100644
--- a/libnm-core/nm-setting-bridge.h
+++ b/libnm-core/nm-setting-bridge.h
@@ -48,6 +48,12 @@ G_BEGIN_DECLS
 #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_VLAN_FILTERING     "vlan-filtering"
+#define NM_SETTING_BRIDGE_VLAN_DEFAULT_PVID  "vlan-default-pvid"
+#define NM_SETTING_BRIDGE_VLANS              "vlans"
+
+#define NM_BRIDGE_VLAN_VID_MIN            1
+#define NM_BRIDGE_VLAN_VID_MAX            4094
 
 /**
  * NMSettingBridge:
@@ -65,6 +71,8 @@ typedef struct {
 	gpointer padding[4];
 } NMSettingBridgeClass;
 
+typedef struct _NMBridgeVlan NMBridgeVlan;
+
 GType nm_setting_bridge_get_type (void);
 
 NMSetting *  nm_setting_bridge_new                (void);
@@ -86,6 +94,57 @@ NM_AVAILABLE_IN_1_10
 guint16      nm_setting_bridge_get_group_forward_mask (NMSettingBridge *setting);
 
 gboolean     nm_setting_bridge_get_multicast_snooping (NMSettingBridge *setting);
+NM_AVAILABLE_IN_1_18
+gboolean     nm_setting_bridge_get_vlan_filtering (NMSettingBridge *setting);
+NM_AVAILABLE_IN_1_18
+guint16      nm_setting_bridge_get_vlan_default_pvid (NMSettingBridge *setting);
+NM_AVAILABLE_IN_1_18
+void          nm_setting_bridge_add_vlan (NMSettingBridge *setting,
+                                          NMBridgeVlan *vlan);
+NM_AVAILABLE_IN_1_18
+guint         nm_setting_bridge_get_num_vlans (NMSettingBridge *setting);
+NM_AVAILABLE_IN_1_18
+NMBridgeVlan *nm_setting_bridge_get_vlan (NMSettingBridge *setting, guint idx);
+NM_AVAILABLE_IN_1_18
+void          nm_setting_bridge_remove_vlan (NMSettingBridge *setting, guint idx);
+NM_AVAILABLE_IN_1_18
+gboolean      nm_setting_bridge_remove_vlan_by_vid (NMSettingBridge *setting,
+                                                    guint16 vid_start,
+                                                    guint16 vid_end);
+NM_AVAILABLE_IN_1_18
+void          nm_setting_bridge_clear_vlans (NMSettingBridge *setting);
+
+NM_AVAILABLE_IN_1_18
+GType          nm_bridge_vlan_get_type (void);
+NM_AVAILABLE_IN_1_18
+NMBridgeVlan * nm_bridge_vlan_new (guint16 vid_start, guint16 vid_end);
+NM_AVAILABLE_IN_1_18
+NMBridgeVlan * nm_bridge_vlan_ref (NMBridgeVlan *vlan);
+NM_AVAILABLE_IN_1_18
+void           nm_bridge_vlan_unref (NMBridgeVlan *vlan);
+NM_AVAILABLE_IN_1_18
+NMBridgeVlan * nm_bridge_vlan_new_clone (const NMBridgeVlan *vlan);
+NM_AVAILABLE_IN_1_18
+int            nm_bridge_vlan_cmp (const NMBridgeVlan *a, const NMBridgeVlan *b);
+NM_AVAILABLE_IN_1_18
+void           nm_bridge_vlan_seal (NMBridgeVlan *vlan);
+NM_AVAILABLE_IN_1_18
+gboolean       nm_bridge_vlan_is_sealed (const NMBridgeVlan *vlan);
+NM_AVAILABLE_IN_1_18
+void           nm_bridge_vlan_set_untagged (NMBridgeVlan *vlan, gboolean value);
+NM_AVAILABLE_IN_1_18
+void           nm_bridge_vlan_set_pvid (NMBridgeVlan *vlan, gboolean value);
+NM_AVAILABLE_IN_1_18
+gboolean       nm_bridge_vlan_get_vid_range (const NMBridgeVlan *vlan, guint16 *vid_start, guint16 *vid_end);
+NM_AVAILABLE_IN_1_18
+gboolean       nm_bridge_vlan_is_untagged (const NMBridgeVlan *vlan);
+NM_AVAILABLE_IN_1_18
+gboolean       nm_bridge_vlan_is_pvid (const NMBridgeVlan *vlan);
+
+NM_AVAILABLE_IN_1_18
+char         * nm_bridge_vlan_to_str (const NMBridgeVlan *vlan, GError **error);
+NM_AVAILABLE_IN_1_18
+NMBridgeVlan * nm_bridge_vlan_from_str (const char *str, GError **error);
 
 G_END_DECLS
 
diff --git a/libnm-core/nm-setting-connection.c b/libnm-core/nm-setting-connection.c
index e8d008e3..e4fb70c2 100644
--- a/libnm-core/nm-setting-connection.c
+++ b/libnm-core/nm-setting-connection.c
@@ -397,8 +397,7 @@ nm_setting_connection_add_permission (NMSettingConnection *setting,
 	GSList *iter;
 
 	g_return_val_if_fail (NM_IS_SETTING_CONNECTION (setting), FALSE);
-	g_return_val_if_fail (ptype, FALSE);
-	g_return_val_if_fail (strlen (ptype) > 0, FALSE);
+	g_return_val_if_fail (ptype && ptype[0], FALSE);
 	g_return_val_if_fail (detail == NULL, FALSE);
 
 	/* Only "user" for now... */
@@ -470,9 +469,9 @@ nm_setting_connection_remove_permission_by_value (NMSettingConnection *setting,
 	GSList *iter;
 
 	g_return_val_if_fail (NM_IS_SETTING_CONNECTION (setting), FALSE);
-	g_return_val_if_fail (ptype, FALSE);
-	g_return_val_if_fail (strlen (ptype) > 0, FALSE);
+	g_return_val_if_fail (ptype && ptype[0], FALSE);
 	g_return_val_if_fail (detail == NULL, FALSE);
+	g_return_val_if_fail (pitem != NULL, FALSE);
 
 	/* Only "user" for now... */
 	g_return_val_if_fail (strcmp (ptype, "user") == 0, FALSE);
diff --git a/libnm-core/nm-setting-ethtool.c b/libnm-core/nm-setting-ethtool.c
index 7bdbcb1a..827e3a62 100644
--- a/libnm-core/nm-setting-ethtool.c
+++ b/libnm-core/nm-setting-ethtool.c
@@ -24,7 +24,7 @@
 #include "nm-setting-ethtool.h"
 
 #include "nm-setting-private.h"
-#include "nm-ethtool-utils.h"
+#include "nm-libnm-core-intern/nm-ethtool-utils.h"
 
 /*****************************************************************************/
 
diff --git a/libnm-core/nm-setting-infiniband.c b/libnm-core/nm-setting-infiniband.c
index 9348fa2b..bde7eda9 100644
--- a/libnm-core/nm-setting-infiniband.c
+++ b/libnm-core/nm-setting-infiniband.c
@@ -170,7 +170,6 @@ verify (NMSetting *setting, NMConnection *connection, GError **error)
 {
 	NMSettingConnection *s_con = NULL;
 	NMSettingInfinibandPrivate *priv = NM_SETTING_INFINIBAND_GET_PRIVATE (setting);
-	guint32 normerr_max_mtu = 0;
 
 	if (priv->mac_address && !nm_utils_hwaddr_valid (priv->mac_address, INFINIBAND_ALEN)) {
 		g_set_error_literal (error,
@@ -181,13 +180,8 @@ verify (NMSetting *setting, NMConnection *connection, GError **error)
 		return FALSE;
 	}
 
-	if (!g_strcmp0 (priv->transport_mode, "datagram")) {
-		if (priv->mtu > 2044)
-			normerr_max_mtu = 2044;
-	} else if (!g_strcmp0 (priv->transport_mode, "connected")) {
-		if (priv->mtu > 65520)
-			normerr_max_mtu = 65520;
-	} else {
+	if (!NM_IN_STRSET (priv->transport_mode, "datagram",
+	                                         "connected")) {
 		g_set_error_literal (error,
 		                     NM_CONNECTION_ERROR,
 		                     NM_CONNECTION_ERROR_INVALID_PROPERTY,
@@ -271,12 +265,21 @@ verify (NMSetting *setting, NMConnection *connection, GError **error)
 
 	/* *** errors above here should be always fatal, below NORMALIZABLE_ERROR *** */
 
-	if (normerr_max_mtu > 0) {
+	if (priv->mtu > NM_INFINIBAND_MAX_MTU) {
+		/* Traditionally, MTU for "datagram" mode was limited to 2044
+		 * and for "connected" mode it was 65520.
+		 *
+		 * This is no longer the case, and both transport modes use the same
+		 * maximum of 65520 (NM_INFINIBAND_MAX_MTU).
+		 *
+		 * Note that this is the MTU in the connection profile. Whether
+		 * we will be able to configure large MTUs later (during activation)
+		 * is unknown at this point. */
 		g_set_error (error,
 		             NM_CONNECTION_ERROR,
 		             NM_CONNECTION_ERROR_INVALID_PROPERTY,
-		             _("mtu for transport mode '%s' can be at most %d but it is %d"),
-		             priv->transport_mode, normerr_max_mtu, priv->mtu);
+		             _("mtu can be at most %u but it is %u"),
+		             NM_INFINIBAND_MAX_MTU, priv->mtu);
 		g_prefix_error (error, "%s.%s: ", NM_SETTING_INFINIBAND_SETTING_NAME, NM_SETTING_INFINIBAND_MTU);
 		return NM_SETTING_VERIFY_NORMALIZABLE_ERROR;
 	}
diff --git a/libnm-core/nm-setting-ip-config.c b/libnm-core/nm-setting-ip-config.c
index 5250ae43..f362945f 100644
--- a/libnm-core/nm-setting-ip-config.c
+++ b/libnm-core/nm-setting-ip-config.c
@@ -25,6 +25,7 @@
 #include "nm-setting-ip-config.h"
 
 #include <arpa/inet.h>
+#include <linux/fib_rules.h>
 
 #include "nm-setting-ip4-config.h"
 #include "nm-setting-ip6-config.h"
@@ -204,7 +205,8 @@ struct NMIPAddress {
  **/
 NMIPAddress *
 nm_ip_address_new (int family,
-                   const char *addr, guint prefix,
+                   const char *addr,
+                   guint prefix,
                    GError **error)
 {
 	NMIPAddress *address;
@@ -242,7 +244,8 @@ nm_ip_address_new (int family,
  **/
 NMIPAddress *
 nm_ip_address_new_binary (int family,
-                          gconstpointer addr, guint prefix,
+                          gconstpointer addr,
+                          guint prefix,
                           GError **error)
 {
 	NMIPAddress *address;
@@ -1113,7 +1116,7 @@ nm_ip_route_set_metric (NMIPRoute *route,
 }
 
 GHashTable *
-_nm_ip_route_get_attributes_direct (NMIPRoute *route)
+_nm_ip_route_get_attributes (NMIPRoute *route)
 {
 	nm_assert (route);
 
@@ -1217,7 +1220,7 @@ static const NMVariantAttributeSpec * const ip_route_attribute_spec[] = {
 	ATTR_SPEC_PTR (NM_IP_ROUTE_ATTRIBUTE_SRC,             G_VARIANT_TYPE_STRING,   TRUE,  TRUE, 'a'),
 	ATTR_SPEC_PTR (NM_IP_ROUTE_ATTRIBUTE_FROM,            G_VARIANT_TYPE_STRING,   FALSE, TRUE, 'p'),
 	ATTR_SPEC_PTR (NM_IP_ROUTE_ATTRIBUTE_TOS,             G_VARIANT_TYPE_BYTE,     TRUE,  FALSE, 0 ),
-	ATTR_SPEC_PTR (NM_IP_ROUTE_ATTRIBUTE_ONLINK,          G_VARIANT_TYPE_BOOLEAN,  TRUE,  FALSE, 0 ),
+	ATTR_SPEC_PTR (NM_IP_ROUTE_ATTRIBUTE_ONLINK,          G_VARIANT_TYPE_BOOLEAN,  TRUE,  TRUE,  0 ),
 	ATTR_SPEC_PTR (NM_IP_ROUTE_ATTRIBUTE_WINDOW,          G_VARIANT_TYPE_UINT32,   TRUE,  TRUE,  0 ),
 	ATTR_SPEC_PTR (NM_IP_ROUTE_ATTRIBUTE_CWND,            G_VARIANT_TYPE_UINT32,   TRUE,  TRUE,  0 ),
 	ATTR_SPEC_PTR (NM_IP_ROUTE_ATTRIBUTE_INITCWND,        G_VARIANT_TYPE_UINT32,   TRUE,  TRUE,  0 ),
@@ -1384,6 +1387,2109 @@ _nm_ip_route_attribute_validate_all (const NMIPRoute *route)
 
 /*****************************************************************************/
 
+struct NMIPRoutingRule {
+	NMIPAddr from_bin;
+	NMIPAddr to_bin;
+	char *from_str;
+	char *to_str;
+	char *iifname;
+	char *oifname;
+	guint ref_count;
+	guint32 priority;
+	guint32 table;
+	guint32 fwmark;
+	guint32 fwmask;
+	guint16 sport_start;
+	guint16 sport_end;
+	guint16 dport_start;
+	guint16 dport_end;
+	guint8 action;
+	guint8 from_len;
+	guint8 to_len;
+	guint8 tos;
+	guint8 ipproto;
+	bool is_v4:1;
+	bool sealed:1;
+	bool priority_has:1;
+	bool from_has:1;
+	bool from_valid:1;
+	bool to_has:1;
+	bool to_valid:1;
+	bool invert:1;
+};
+
+static NMIPRoutingRule *_ip_routing_rule_dup (const NMIPRoutingRule *rule);
+
+G_DEFINE_BOXED_TYPE (NMIPRoutingRule, nm_ip_routing_rule, _ip_routing_rule_dup, nm_ip_routing_rule_unref)
+
+static gboolean
+NM_IS_IP_ROUTING_RULE (const NMIPRoutingRule *self,
+                       gboolean also_sealed)
+{
+	return    self
+	       && self->ref_count > 0
+	       && (   also_sealed
+	           || !self->sealed);
+}
+
+static int
+_ip_routing_rule_get_addr_family (const NMIPRoutingRule *self)
+{
+	nm_assert (NM_IS_IP_ROUTING_RULE (self, TRUE));
+
+	return self->is_v4 ? AF_INET : AF_INET6;
+}
+
+static int
+_ip_routing_rule_get_addr_size (const NMIPRoutingRule *self)
+{
+	nm_assert (NM_IS_IP_ROUTING_RULE (self, TRUE));
+
+	return self->is_v4 ? sizeof (struct in_addr) : sizeof (struct in6_addr);
+}
+
+static NMIPRoutingRule *
+_ip_routing_rule_dup (const NMIPRoutingRule *rule)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (rule, TRUE), NULL);
+
+	if (rule->sealed)
+		return nm_ip_routing_rule_ref ((NMIPRoutingRule *) rule);
+	return nm_ip_routing_rule_new_clone (rule);
+}
+
+/**
+ * nm_ip_routing_rule_new:
+ * @addr_family: the address family of the routing rule. Must be either
+ *   %AF_INET (2) or %AF_INET6 (10).
+ *
+ * Returns: (transfer full): a newly created rule instance with the
+ *   provided address family. The instance is unsealed.
+ *
+ * Since: 1.18
+ */
+NMIPRoutingRule *
+nm_ip_routing_rule_new (int addr_family)
+{
+	NMIPRoutingRule *self;
+
+	g_return_val_if_fail (NM_IN_SET (addr_family, AF_INET, AF_INET6), NULL);
+
+	self = g_slice_new (NMIPRoutingRule);
+	*self = (NMIPRoutingRule) {
+		.ref_count    = 1,
+		.is_v4        = (addr_family == AF_INET),
+		.action       = FR_ACT_TO_TBL,
+		.table        = RT_TABLE_MAIN,
+	};
+	return self;
+}
+
+/**
+ * nm_ip_routing_rule_new_clone:
+ * @rule: the #NMIPRoutingRule to clone.
+ *
+ * Returns: (transfer full): a newly created rule instance with
+ *   the same settings as @rule. Note that the instance will
+ *   always be unsealred.
+ *
+ * Since: 1.18
+ */
+NMIPRoutingRule *
+nm_ip_routing_rule_new_clone (const NMIPRoutingRule *rule)
+{
+	NMIPRoutingRule *self;
+
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (rule, TRUE), NULL);
+
+	self = g_slice_new (NMIPRoutingRule);
+	*self = (NMIPRoutingRule) {
+		.ref_count    = 1,
+		.sealed       = FALSE,
+		.is_v4        = rule->is_v4,
+
+		.priority     = rule->priority,
+		.priority_has = rule->priority_has,
+
+		.invert       = rule->invert,
+
+		.tos          = rule->tos,
+
+		.fwmark       = rule->fwmark,
+		.fwmask       = rule->fwmask,
+
+		.sport_start  = rule->sport_start,
+		.sport_end    = rule->sport_end,
+		.dport_start  = rule->dport_start,
+		.dport_end    = rule->dport_end,
+
+		.ipproto      = rule->ipproto,
+
+		.from_len     = rule->from_len,
+		.from_bin     = rule->from_bin,
+		.from_str     =   (   rule->from_has
+		                   && !rule->from_valid)
+		                ? g_strdup (rule->from_str)
+		                : NULL,
+		.from_has     = rule->from_has,
+		.from_valid   = rule->from_valid,
+
+		.to_len       = rule->to_len,
+		.to_bin       = rule->to_bin,
+		.to_str       =   (   rule->to_has
+		                   && !rule->to_valid)
+		                ? g_strdup (rule->to_str)
+		                : NULL,
+		.to_has       = rule->to_has,
+		.to_valid     = rule->to_valid,
+
+		.iifname      = g_strdup (rule->iifname),
+		.oifname      = g_strdup (rule->oifname),
+
+		.action       = rule->action,
+		.table        = rule->table,
+	};
+	return self;
+}
+
+/**
+ * nm_ip_routing_rule_ref:
+ * @self: (allow-none): the #NMIPRoutingRule instance
+ *
+ * Increases the reference count of the instance.
+ * This is not thread-safe.
+ *
+ * Returns: (transfer full): the @self argument with incremented
+ *  reference count.
+ *
+ * Since: 1.18
+ */
+NMIPRoutingRule *
+nm_ip_routing_rule_ref (NMIPRoutingRule *self)
+{
+	if (!self)
+		return NULL;
+
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), NULL);
+
+	nm_assert (self->ref_count < G_MAXUINT);
+	self->ref_count++;
+	return self;
+}
+
+/**
+ * nm_ip_routing_rule_unref:
+ * @self: (allow-none): the #NMIPRoutingRule instance
+ *
+ * Decreases the reference count of the instance and destroys
+ * the instance if the reference count reaches zero.
+ * This is not thread-safe.
+ *
+ * Since: 1.18
+ */
+void
+nm_ip_routing_rule_unref (NMIPRoutingRule *self)
+{
+	if (!self)
+		return;
+
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE));
+
+	if (--self->ref_count > 0)
+		return;
+
+	g_free (self->from_str);
+	g_free (self->to_str);
+	g_free (self->iifname);
+	g_free (self->oifname);
+
+	g_slice_free (NMIPRoutingRule, self);
+}
+
+/**
+ * nm_ip_routing_rule_is_sealed:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: whether @self is sealed. Once sealed, an instance
+ *   cannot be modified nor unsealed.
+ *
+ * Since: 1.18
+ */
+gboolean
+nm_ip_routing_rule_is_sealed (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), FALSE);
+
+	return self->sealed;
+}
+
+/**
+ * nm_ip_routing_rule_seal:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Seals the routing rule. Afterwards, the instance can no longer be
+ * modfied, and it is a bug to call any of the accessors that would
+ * modify the rule. If @self was already sealed, this has no effect.
+ *
+ * Since: 1.18
+ */
+void
+nm_ip_routing_rule_seal (NMIPRoutingRule *self)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE));
+
+	self->sealed = TRUE;
+}
+
+/**
+ * nm_ip_routing_rule_get_addr_family:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: the address family of the rule. Either %AF_INET or %AF_INET6.
+ *
+ * Since: 1.18
+ */
+int
+nm_ip_routing_rule_get_addr_family (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), AF_UNSPEC);
+
+	return _ip_routing_rule_get_addr_family (self);
+}
+
+/**
+ * nm_ip_routing_rule_get_priority:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: the priority. A valid priority is in the range from
+ *   0 to %G_MAXUINT32. If unset, -1 is returned.
+ *
+ * Since: 1.18
+ */
+gint64
+nm_ip_routing_rule_get_priority (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), -1);
+
+	return   self->priority_has
+	       ? (gint64) self->priority
+	       : (gint64) -1;
+}
+
+/**
+ * nm_ip_routing_rule_set_priority:
+ * @self: the #NMIPRoutingRule instance
+ * @priority: the priority to set
+ *
+ * A valid priority ranges from 0 to %G_MAXUINT32. "-1" is also allowed
+ * to reset the priority. It is a bug calling this function with any
+ * other value.
+ *
+ * Since: 1.18
+ */
+void
+nm_ip_routing_rule_set_priority (NMIPRoutingRule *self, gint64 priority)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, FALSE));
+
+	if (   priority >= 0
+	    && priority <= (gint64) G_MAXUINT32) {
+		self->priority = (guint32) priority;
+		self->priority_has = TRUE;
+	} else {
+		g_return_if_fail (priority == -1);
+		self->priority = 0;
+		self->priority_has = FALSE;
+	}
+}
+
+/**
+ * nm_ip_routing_rule_get_invert:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: the "invert" setting of the rule.
+ *
+ * Since: 1.18
+ */
+gboolean
+nm_ip_routing_rule_get_invert (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), FALSE);
+
+	return self->invert;
+}
+
+/**
+ * nm_ip_routing_rule_set_invert:
+ * @self: the #NMIPRoutingRule instance
+ * @invert: the new value to set
+ *
+ * Since: 1.18
+ */
+void
+nm_ip_routing_rule_set_invert (NMIPRoutingRule *self, gboolean invert)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, FALSE));
+
+	self->invert = invert;
+}
+
+/**
+ * nm_ip_routing_rule_get_from_len:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: the set prefix length for the from/src parameter.
+ *
+ * Since: 1.18
+ */
+guint8
+nm_ip_routing_rule_get_from_len (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), 0);
+
+	return self->from_len;
+}
+
+/**
+ * nm_ip_routing_rule_get_from:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: (transfer none): the set from/src parameter or
+ *   %NULL, if no value is set.
+ *
+ * Since: 1.18
+ */
+const char *
+nm_ip_routing_rule_get_from (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), NULL);
+
+	if (!self->from_has)
+		return NULL;
+	if (!self->from_str) {
+		nm_assert (self->from_valid);
+		((NMIPRoutingRule *) self)->from_str = nm_utils_inet_ntop_dup (_ip_routing_rule_get_addr_family (self),
+		                                                               &self->from_bin);
+	}
+	return self->from_str;
+}
+
+const NMIPAddr *
+nm_ip_routing_rule_get_from_bin (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), NULL);
+
+	return   (   self->from_has
+	          && self->from_valid)
+	       ? &self->from_bin
+	       : NULL;
+}
+
+void
+nm_ip_routing_rule_set_from_bin (NMIPRoutingRule *self,
+                                 gconstpointer from,
+                                 guint8 len)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, FALSE));
+
+	nm_clear_g_free (&self->from_str);
+
+	if (!from) {
+		self->from_has = FALSE;
+		self->from_len = len;
+		return;
+	}
+
+	self->from_has = TRUE;
+	self->from_len = len;
+	self->from_valid = TRUE;
+	nm_ip_addr_set (_ip_routing_rule_get_addr_family (self),
+	                &self->from_bin,
+	                from);
+}
+
+/**
+ * nm_ip_routing_rule_set_from:
+ * @self: the #NMIPRoutingRule instance
+ * @from: (allow-none): the from/src address to set.
+ *   The address family must match.
+ * @len: the corresponding prefix length of the address.
+ *
+ * Setting invalid values is accepted, but will later fail
+ * during nm_ip_routing_rule_validate().
+ *
+ * Since: 1.18
+ */
+void
+nm_ip_routing_rule_set_from (NMIPRoutingRule *self,
+                             const char *from,
+                             guint8 len)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, FALSE));
+
+	if (!from) {
+		nm_clear_g_free (&self->from_str);
+		self->from_has = FALSE;
+		self->from_len = len;
+		return;
+	}
+
+	nm_clear_g_free (&self->from_str);
+	self->from_has = TRUE;
+	self->from_len = len;
+	self->from_valid = nm_utils_parse_inaddr_bin (_ip_routing_rule_get_addr_family (self),
+	                                              from,
+	                                              NULL,
+	                                              &self->from_bin);
+	if (!self->from_valid)
+		self->from_str = g_strdup (from);
+}
+
+/**
+ * nm_ip_routing_rule_get_to_len:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: the set prefix length for the to/dst parameter.
+ *
+ * Since: 1.18
+ */
+guint8
+nm_ip_routing_rule_get_to_len (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), 0);
+
+	return self->to_len;
+}
+
+/**
+ * nm_ip_routing_rule_get_to:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: (transfer none): the set to/dst parameter or
+ *   %NULL, if no value is set.
+ *
+ * Since: 1.18
+ */
+const char *
+nm_ip_routing_rule_get_to (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), NULL);
+
+	if (!self->to_has)
+		return NULL;
+	if (!self->to_str) {
+		nm_assert (self->to_valid);
+		((NMIPRoutingRule *) self)->to_str = nm_utils_inet_ntop_dup (_ip_routing_rule_get_addr_family (self),
+		                                                             &self->to_bin);
+	}
+	return self->to_str;
+}
+
+const NMIPAddr *
+nm_ip_routing_rule_get_to_bin (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), NULL);
+
+	return   (   self->to_has
+	          && self->to_valid)
+	       ? &self->to_bin
+	       : NULL;
+}
+
+void
+nm_ip_routing_rule_set_to_bin (NMIPRoutingRule *self,
+                               gconstpointer to,
+                               guint8 len)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, FALSE));
+
+	nm_clear_g_free (&self->to_str);
+
+	if (!to) {
+		self->to_has = FALSE;
+		self->to_len = len;
+		return;
+	}
+
+	self->to_has = TRUE;
+	self->to_len = len;
+	self->to_valid = TRUE;
+	nm_ip_addr_set (_ip_routing_rule_get_addr_family (self),
+	                &self->to_bin,
+	                to);
+}
+
+/**
+ * nm_ip_routing_rule_set_to:
+ * @self: the #NMIPRoutingRule instance
+ * @to: (allow-none): the to/dst address to set.
+ *   The address family must match.
+ * @len: the corresponding prefix length of the address.
+ *   If @to is %NULL, this valid is ignored.
+ *
+ * Setting invalid values is accepted, but will later fail
+ * during nm_ip_routing_rule_validate().
+ *
+ * Since: 1.18
+ */
+void
+nm_ip_routing_rule_set_to (NMIPRoutingRule *self,
+                           const char *to,
+                           guint8 len)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, FALSE));
+
+	if (!to) {
+		nm_clear_g_free (&self->to_str);
+		self->to_has = FALSE;
+		self->to_len = len;
+		return;
+	}
+
+	nm_clear_g_free (&self->to_str);
+	self->to_has = TRUE;
+	self->to_len = len;
+	self->to_valid = nm_utils_parse_inaddr_bin (_ip_routing_rule_get_addr_family (self),
+	                                            to,
+	                                            NULL,
+	                                            &self->to_bin);
+	if (!self->to_valid)
+		self->to_str = g_strdup (to);
+}
+
+/**
+ * nm_ip_routing_rule_get_tos:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: the tos of the rule.
+ *
+ * Since: 1.18
+ */
+guint8
+nm_ip_routing_rule_get_tos (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), 0);
+
+	return self->tos;
+}
+
+/**
+ * nm_ip_routing_rule_set_tos:
+ * @self: the #NMIPRoutingRule instance
+ * @tos: the tos to set
+ *
+ * Since: 1.18
+ */
+void
+nm_ip_routing_rule_set_tos (NMIPRoutingRule *self, guint8 tos)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, FALSE));
+
+	self->tos = tos;
+}
+
+/**
+ * nm_ip_routing_rule_get_ipproto:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: the ipproto of the rule.
+ *
+ * Since: 1.18
+ */
+guint8
+nm_ip_routing_rule_get_ipproto (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), 0);
+
+	return self->ipproto;
+}
+
+/**
+ * nm_ip_routing_rule_set_ipproto:
+ * @self: the #NMIPRoutingRule instance
+ * @ipproto: the ipproto to set
+ *
+ * Since: 1.18
+ */
+void
+nm_ip_routing_rule_set_ipproto (NMIPRoutingRule *self, guint8 ipproto)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, FALSE));
+
+	self->ipproto = ipproto;
+}
+
+/**
+ * nm_ip_routing_rule_get_source_port_start:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: the source port start setting.
+ *
+ * Since: 1.18
+ */
+guint16
+nm_ip_routing_rule_get_source_port_start (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), 0);
+
+	return self->sport_start;
+}
+
+/**
+ * nm_ip_routing_rule_get_source_port_end:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: the source port end setting.
+ *
+ * Since: 1.18
+ */
+guint16
+nm_ip_routing_rule_get_source_port_end (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), 0);
+
+	return self->sport_end;
+}
+
+/**
+ * nm_ip_routing_rule_set_source_port:
+ * @self: the #NMIPRoutingRule instance
+ * @start: the start port to set.
+ * @end: the end port to set.
+ *
+ * Since: 1.18
+ */
+void
+nm_ip_routing_rule_set_source_port (NMIPRoutingRule *self, guint16 start, guint16 end)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, FALSE));
+
+	self->sport_start = start;
+	self->sport_end   = end;
+}
+
+/**
+ * nm_ip_routing_rule_get_destination_port_start:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: the destination port start setting.
+ *
+ * Since: 1.18
+ */
+guint16
+nm_ip_routing_rule_get_destination_port_start (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), 0);
+
+	return self->dport_start;
+}
+
+/**
+ * nm_ip_routing_rule_get_destination_port_end:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: the destination port end setting.
+ *
+ * Since: 1.18
+ */
+guint16
+nm_ip_routing_rule_get_destination_port_end (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), 0);
+
+	return self->dport_end;
+}
+
+/**
+ * nm_ip_routing_rule_set_destination_port:
+ * @self: the #NMIPRoutingRule instance
+ * @start: the start port to set.
+ * @end: the end port to set.
+ *
+ * Since: 1.18
+ */
+void
+nm_ip_routing_rule_set_destination_port (NMIPRoutingRule *self, guint16 start, guint16 end)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, FALSE));
+
+	self->dport_start = start;
+	self->dport_end   = end;
+}
+
+/**
+ * nm_ip_routing_rule_get_fwmark:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: the fwmark setting.
+ *
+ * Since: 1.18
+ */
+guint32
+nm_ip_routing_rule_get_fwmark (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), 0);
+
+	return self->fwmark;
+}
+
+/**
+ * nm_ip_routing_rule_get_fwmask:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: the fwmask setting.
+ *
+ * Since: 1.18
+ */
+guint32
+nm_ip_routing_rule_get_fwmask (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), 0);
+
+	return self->fwmask;
+}
+
+/**
+ * nm_ip_routing_rule_set_fwmark:
+ * @self: the #NMIPRoutingRule instance
+ * @fwmark: the fwmark
+ * @fwmask: the fwmask
+ *
+ * Since: 1.18
+ */
+void
+nm_ip_routing_rule_set_fwmark (NMIPRoutingRule *self, guint32 fwmark, guint32 fwmask)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, FALSE));
+
+	self->fwmark = fwmark;
+	self->fwmask = fwmask;
+}
+
+/**
+ * nm_ip_routing_rule_get_iifname:
+ * @self: the #NMIPRoutingRule instance.
+ *
+ * Returns: (transfer none): the set iifname or %NULL if unset.
+ *
+ * Since: 1.18
+ */
+const char *
+nm_ip_routing_rule_get_iifname (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), NULL);
+
+	return self->iifname;
+}
+
+gboolean
+nm_ip_routing_rule_get_xifname_bin (const NMIPRoutingRule *self,
+                                    gboolean iif /* or else oif */,
+                                    char out_xifname[static 16 /* IFNAMSIZ */])
+{
+	gs_free gpointer bin_to_free = NULL;
+	const char *xifname;
+	gconstpointer bin;
+	gsize len;
+
+	nm_assert (NM_IS_IP_ROUTING_RULE (self, TRUE));
+	nm_assert (out_xifname);
+
+	xifname = iif ? self->iifname : self->oifname;
+
+	if (!xifname)
+		return FALSE;
+
+	bin = nm_utils_buf_utf8safe_unescape (xifname, &len, &bin_to_free);
+
+	strncpy (out_xifname, bin, 16 /* IFNAMSIZ */);
+	out_xifname[15] = '\0';
+	return TRUE;
+}
+
+/**
+ * nm_ip_routing_rule_set_iifname:
+ * @self: the #NMIPRoutingRule instance.
+ * @iifname: (allow-none): the iifname to set or %NULL to unset.
+ *
+ * The name supports C backslash escaping for non-UTF-8 characters.
+ * Note that nm_ip_routing_rule_from_string() too uses backslash
+ * escaping when tokenizing the words by whitespace. So, in string
+ * representation you'd get double backslashs.
+ *
+ * Since: 1.18
+ */
+void
+nm_ip_routing_rule_set_iifname (NMIPRoutingRule *self, const char *iifname)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, FALSE));
+
+	g_free (self->iifname);
+	self->iifname = g_strdup (iifname);
+}
+
+/**
+ * nm_ip_routing_rule_get_oifname:
+ * @self: the #NMIPRoutingRule instance.
+ *
+ * Returns: (transfer none): the set oifname or %NULL if unset.
+ *
+ * Since: 1.18
+ */
+const char *
+nm_ip_routing_rule_get_oifname (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), NULL);
+
+	return self->oifname;
+}
+
+/**
+ * nm_ip_routing_rule_set_oifname:
+ * @self: the #NMIPRoutingRule instance.
+ * @oifname: (allow-none): the oifname to set or %NULL to unset.
+ *
+ * The name supports C backslash escaping for non-UTF-8 characters.
+ * Note that nm_ip_routing_rule_from_string() too uses backslash
+ * escaping when tokenizing the words by whitespace. So, in string
+ * representation you'd get double backslashs.
+ *
+ * Since: 1.18
+ */
+void
+nm_ip_routing_rule_set_oifname (NMIPRoutingRule *self, const char *oifname)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, FALSE));
+
+	g_free (self->oifname);
+	self->oifname = g_strdup (oifname);
+}
+
+/**
+ * nm_ip_routing_rule_get_action:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: the set action.
+ *
+ * Since: 1.18
+ */
+guint8
+nm_ip_routing_rule_get_action (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), 0);
+
+	return self->action;
+}
+
+/**
+ * nm_ip_routing_rule_set_action:
+ * @self: the #NMIPRoutingRule instance
+ * @action: the action to set
+ *
+ * Note that currently only certain actions are allowed. nm_ip_routing_rule_validate()
+ * will reject unsupported actions as invalid.
+ *
+ * Since: 1.18
+ */
+void
+nm_ip_routing_rule_set_action (NMIPRoutingRule *self, guint8 action)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, FALSE));
+
+	self->action = action;
+}
+
+/**
+ * nm_ip_routing_rule_get_table:
+ * @self: the #NMIPRoutingRule instance
+ *
+ * Returns: the set table.
+ *
+ * Since: 1.18
+ */
+guint32
+nm_ip_routing_rule_get_table (const NMIPRoutingRule *self)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), 0);
+
+	return self->table;
+}
+
+/**
+ * nm_ip_routing_rule_set_table:
+ * @self: the #NMIPRoutingRule instance
+ * @table: the table to set
+ *
+ * Since: 1.18
+ */
+void
+nm_ip_routing_rule_set_table (NMIPRoutingRule *self, guint32 table)
+{
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (self, FALSE));
+
+	self->table = table;
+}
+
+/**
+ * nm_ip_routing_rule_cmp:
+ * @rule: (allow-none): the #NMIPRoutingRule instance to compare
+ * @other: (allow-none): the other #NMIPRoutingRule instance to compare
+ *
+ * Returns: zero, a positive, or a negative integer to indicate
+ *   equality or how the arguments compare.
+ *
+ * Since: 1.18
+ */
+int
+nm_ip_routing_rule_cmp (const NMIPRoutingRule *rule,
+                        const NMIPRoutingRule *other)
+{
+	NM_CMP_SELF (rule, other);
+
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (rule, TRUE), 0);
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (other, TRUE), 0);
+
+	NM_CMP_FIELD_UNSAFE (rule, other, priority_has);
+	if (rule->priority_has)
+		NM_CMP_FIELD (rule, other, priority);
+
+	NM_CMP_FIELD_UNSAFE (rule, other, is_v4);
+
+	NM_CMP_FIELD_UNSAFE (rule, other, invert);
+
+	NM_CMP_FIELD (rule, other, tos);
+
+	NM_CMP_FIELD (rule, other, fwmark);
+	NM_CMP_FIELD (rule, other, fwmask);
+
+	NM_CMP_FIELD (rule, other, action);
+
+	NM_CMP_FIELD (rule, other, table);
+
+	NM_CMP_FIELD (rule, other, sport_start);
+	NM_CMP_FIELD (rule, other, sport_end);
+	NM_CMP_FIELD (rule, other, dport_start);
+	NM_CMP_FIELD (rule, other, dport_end);
+
+	NM_CMP_FIELD (rule, other, ipproto);
+
+	/* We compare the plain strings, not the binary values after utf8safe unescaping.
+	 *
+	 * The reason is, that the rules differ already when the direct strings differ, not
+	 * only when the unescaped names differ. */
+	NM_CMP_FIELD_STR0 (rule, other, iifname);
+	NM_CMP_FIELD_STR0 (rule, other, oifname);
+
+	NM_CMP_FIELD (rule, other, from_len);
+
+	NM_CMP_FIELD_UNSAFE (rule, other, from_has);
+	if (rule->from_has) {
+		NM_CMP_FIELD_UNSAFE (rule, other, from_valid);
+		if (rule->from_valid) {
+			NM_CMP_RETURN (memcmp (&rule->from_bin,
+			                       &other->from_bin,
+			                       _ip_routing_rule_get_addr_size (rule)));
+		} else
+			NM_CMP_FIELD_STR (rule, other, from_str);
+	}
+
+	NM_CMP_FIELD (rule, other, to_len);
+
+	NM_CMP_FIELD_UNSAFE (rule, other, to_has);
+	if (rule->to_has) {
+		NM_CMP_FIELD_UNSAFE (rule, other, to_valid);
+		if (rule->to_valid) {
+			NM_CMP_RETURN (memcmp (&rule->to_bin,
+			                       &other->to_bin,
+			                       _ip_routing_rule_get_addr_size (rule)));
+		} else
+			NM_CMP_FIELD_STR (rule, other, to_str);
+	}
+
+	return 0;
+}
+
+static gboolean
+_rr_xport_range_valid (guint16 xport_start, guint16 xport_end)
+{
+	if (xport_start == 0)
+		return (xport_end == 0);
+
+	return    xport_start <= xport_end
+	       && xport_end < 0xFFFFu;
+}
+
+static gboolean
+_rr_xport_range_parse (char *str, gint64 *out_start, guint16 *out_end)
+{
+	guint16 start, end;
+	gint64 i64;
+	char *s;
+
+	s = strchr (str, '-');
+	if (s)
+		*(s++) = '\0';
+
+	i64 = _nm_utils_ascii_str_to_int64 (str, 10, 0, 0xFFFF, -1);
+	if (i64 == -1)
+		return FALSE;
+
+	start = i64;
+	if (s) {
+		i64 = _nm_utils_ascii_str_to_int64 (s, 10, 0, 0xFFFF, -1);
+		if (i64 == -1)
+			return FALSE;
+		end = i64;
+	} else
+		end = start;
+
+	*out_start = start;
+	*out_end = end;
+	return TRUE;
+}
+
+/**
+ * nm_ip_routing_rule_validate:
+ * @self: the #NMIPRoutingRule instance to validate
+ * @error: (allow-none) (out): the error result if validation fails.
+ *
+ * Returns: %TRUE if the rule validates.
+ *
+ * Since: 1.18
+ */
+gboolean
+nm_ip_routing_rule_validate (const NMIPRoutingRule *self,
+                             GError **error)
+{
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), FALSE);
+	g_return_val_if_fail (!error || !*error, FALSE);
+
+	/* Kernel may be more flexible about validating. We do a strict validation
+	 * here and reject certain settings eagerly. We can always relax it later. */
+
+	if (!self->priority_has) {
+		/* iproute2 accepts not specifying the priority, in which case kernel will select
+		 * an unused priority. We don't allow for that, and will always require the user to
+		 * select a priority.
+		 *
+		 * Note that if the user selects priority 0 or a non-unique priority, this is problematic
+		 * due to kernel bugs rh#1685816 and rh#1685816. It may result in NetworkManager wrongly being
+		 * unable to add a rule or deleting the wrong rule.
+		 * This problem is not at all specific to the priority, it affects all rules that
+		 * have default values which confuse kernel. But setting a unique priority avoids
+		 * this problem nicely. */
+		g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		                     _("invalid priority"));
+		return FALSE;
+	}
+
+	if (NM_IN_SET (self->action, FR_ACT_TO_TBL)) {
+		if (self->table == 0) {
+			/* with IPv4, kernel allows a table (in RTM_NEWRULE) of zero to automatically select
+			 * an unused table. We don't. The user needs to specify the table.
+			 *
+			 * For IPv6, kernel doesn't allow a table of zero, so we are consistent here. */
+			g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+			                     _("missing table"));
+			return FALSE;
+		}
+	} else {
+		/* whitelist the actions that we currently. */
+		g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		                     _("invalid action"));
+		return FALSE;
+	}
+
+	if (self->from_len == 0) {
+		if (self->from_has) {
+			g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+			                     _("has from/src but the prefix-length is zero"));
+			return FALSE;
+		}
+	} else if (   self->from_len > 0
+	           && self->from_len <= 8 * _ip_routing_rule_get_addr_size (self)) {
+		if (!self->from_has) {
+			g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+			                     _("missing from/src for a non zero prefix-length"));
+			return FALSE;
+		}
+		if (!self->from_valid) {
+			g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+			                     _("invalid from/src"));
+			return FALSE;
+		}
+	} else {
+		g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		                     _("invalid prefix length for from/src"));
+		return FALSE;
+	}
+
+	if (self->to_len == 0) {
+		if (self->to_has) {
+			g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+			                     _("has to/dst but the prefix-length is zero"));
+			return FALSE;
+		}
+	} else if (   self->to_len > 0
+	           && self->to_len <= 8 * _ip_routing_rule_get_addr_size (self)) {
+		if (!self->to_has) {
+			g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+			                     _("missing to/dst for a non zero prefix-length"));
+			return FALSE;
+		}
+		if (!self->to_valid) {
+			g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+			                     _("invalid to/dst"));
+			return FALSE;
+		}
+	} else {
+		g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		                     _("invalid prefix length for to/dst"));
+		return FALSE;
+	}
+
+	if (   self->iifname
+	    && (   !g_utf8_validate (self->iifname, -1, NULL)
+	        || !nm_utils_is_valid_iface_name_utf8safe (self->iifname))) {
+		g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		                     _("invalid iifname"));
+		return FALSE;
+	}
+
+	if (   self->oifname
+	    && (   !g_utf8_validate (self->oifname, -1, NULL)
+	        || !nm_utils_is_valid_iface_name_utf8safe (self->oifname))) {
+		g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		                     _("invalid oifname"));
+		return FALSE;
+	}
+
+	if (!_rr_xport_range_valid (self->sport_start, self->sport_end)) {
+		g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		                     _("invalid source port range"));
+		return FALSE;
+	}
+
+	if (!_rr_xport_range_valid (self->dport_start, self->dport_end)) {
+		g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		                     _("invalid destination port range"));
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+/*****************************************************************************/
+
+typedef enum {
+	RR_DBUS_ATTR_ACTION,
+	RR_DBUS_ATTR_DPORT_END,
+	RR_DBUS_ATTR_DPORT_START,
+	RR_DBUS_ATTR_FAMILY,
+	RR_DBUS_ATTR_FROM,
+	RR_DBUS_ATTR_FROM_LEN,
+	RR_DBUS_ATTR_FWMARK,
+	RR_DBUS_ATTR_FWMASK,
+	RR_DBUS_ATTR_IIFNAME,
+	RR_DBUS_ATTR_INVERT,
+	RR_DBUS_ATTR_IPPROTO,
+	RR_DBUS_ATTR_OIFNAME,
+	RR_DBUS_ATTR_PRIORITY,
+	RR_DBUS_ATTR_SPORT_END,
+	RR_DBUS_ATTR_SPORT_START,
+	RR_DBUS_ATTR_TABLE,
+	RR_DBUS_ATTR_TO,
+	RR_DBUS_ATTR_TOS,
+	RR_DBUS_ATTR_TO_LEN,
+
+	_RR_DBUS_ATTR_NUM,
+} RRDbusAttr;
+
+typedef struct {
+	const char *name;
+	const GVariantType *dbus_type;
+} RRDbusData;
+
+static const RRDbusData rr_dbus_data[_RR_DBUS_ATTR_NUM] = {
+#define _D(attr, _name, type) [attr] = { .name = _name, .dbus_type = type, }
+	_D (RR_DBUS_ATTR_ACTION,      NM_IP_ROUTING_RULE_ATTR_ACTION,      G_VARIANT_TYPE_BYTE),
+	_D (RR_DBUS_ATTR_DPORT_END,   NM_IP_ROUTING_RULE_ATTR_DPORT_END,   G_VARIANT_TYPE_UINT16),
+	_D (RR_DBUS_ATTR_DPORT_START, NM_IP_ROUTING_RULE_ATTR_DPORT_START, G_VARIANT_TYPE_UINT16),
+	_D (RR_DBUS_ATTR_FAMILY,      NM_IP_ROUTING_RULE_ATTR_FAMILY,      G_VARIANT_TYPE_INT32),
+	_D (RR_DBUS_ATTR_FROM,        NM_IP_ROUTING_RULE_ATTR_FROM,        G_VARIANT_TYPE_STRING),
+	_D (RR_DBUS_ATTR_FROM_LEN,    NM_IP_ROUTING_RULE_ATTR_FROM_LEN,    G_VARIANT_TYPE_BYTE),
+	_D (RR_DBUS_ATTR_FWMARK,      NM_IP_ROUTING_RULE_ATTR_FWMARK,      G_VARIANT_TYPE_UINT32),
+	_D (RR_DBUS_ATTR_FWMASK,      NM_IP_ROUTING_RULE_ATTR_FWMASK,      G_VARIANT_TYPE_UINT32),
+	_D (RR_DBUS_ATTR_IIFNAME,     NM_IP_ROUTING_RULE_ATTR_IIFNAME,     G_VARIANT_TYPE_STRING),
+	_D (RR_DBUS_ATTR_INVERT,      NM_IP_ROUTING_RULE_ATTR_INVERT,      G_VARIANT_TYPE_BOOLEAN),
+	_D (RR_DBUS_ATTR_IPPROTO,     NM_IP_ROUTING_RULE_ATTR_IPPROTO,     G_VARIANT_TYPE_BYTE),
+	_D (RR_DBUS_ATTR_OIFNAME,     NM_IP_ROUTING_RULE_ATTR_OIFNAME,     G_VARIANT_TYPE_STRING),
+	_D (RR_DBUS_ATTR_PRIORITY,    NM_IP_ROUTING_RULE_ATTR_PRIORITY,    G_VARIANT_TYPE_UINT32),
+	_D (RR_DBUS_ATTR_SPORT_END,   NM_IP_ROUTING_RULE_ATTR_SPORT_END,   G_VARIANT_TYPE_UINT16),
+	_D (RR_DBUS_ATTR_SPORT_START, NM_IP_ROUTING_RULE_ATTR_SPORT_START, G_VARIANT_TYPE_UINT16),
+	_D (RR_DBUS_ATTR_TABLE,       NM_IP_ROUTING_RULE_ATTR_TABLE,       G_VARIANT_TYPE_UINT32),
+	_D (RR_DBUS_ATTR_TO,          NM_IP_ROUTING_RULE_ATTR_TO,          G_VARIANT_TYPE_STRING),
+	_D (RR_DBUS_ATTR_TOS,         NM_IP_ROUTING_RULE_ATTR_TOS,         G_VARIANT_TYPE_BYTE),
+	_D (RR_DBUS_ATTR_TO_LEN,      NM_IP_ROUTING_RULE_ATTR_TO_LEN,      G_VARIANT_TYPE_BYTE),
+#undef _D
+};
+
+static void
+_rr_variants_free (GVariant *(*p_variants)[])
+{
+	int i;
+
+	for (i = 0; i < _RR_DBUS_ATTR_NUM; i++) {
+		if ((*p_variants)[i])
+			g_variant_unref ((*p_variants)[i]);
+	}
+}
+
+NMIPRoutingRule *
+nm_ip_routing_rule_from_dbus (GVariant *variant,
+                              gboolean strict,
+                              GError **error)
+{
+	nm_auto (_rr_variants_free) GVariant *variants[_RR_DBUS_ATTR_NUM] = { };
+	nm_auto_unref_ip_routing_rule NMIPRoutingRule *self = NULL;
+	RRDbusAttr attr;
+	GVariantIter iter;
+	const char *iter_key;
+	GVariant *iter_val;
+	int addr_family;
+	int i;
+
+	g_variant_iter_init (&iter, variant);
+
+#if NM_MORE_ASSERTS > 10
+	for (attr = 0; attr < _RR_DBUS_ATTR_NUM; attr++) {
+		nm_assert (rr_dbus_data[attr].name);
+		nm_assert (g_variant_type_string_is_valid ((const char *) rr_dbus_data[attr].dbus_type));
+	}
+#endif
+
+	while (g_variant_iter_next (&iter, "{&sv}", &iter_key, &iter_val)) {
+		gs_unref_variant GVariant *iter_val2 = iter_val;
+
+		for (attr = 0; attr < _RR_DBUS_ATTR_NUM; attr++) {
+			if (nm_streq (iter_key, rr_dbus_data[attr].name)) {
+				if (variants[attr]) {
+					if (strict) {
+						g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+						             _("duplicate key %s"),
+						             iter_key);
+						return NULL;
+					}
+					g_variant_unref (variants[attr]);
+				}
+				variants[attr] = g_steal_pointer (&iter_val2);
+				break;
+			}
+		}
+
+		if (   attr >= _RR_DBUS_ATTR_NUM
+		    && strict) {
+			g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+			             _("invalid key \"%s\""),
+			             iter_key);
+			return NULL;
+		}
+	}
+
+	for (attr = 0; attr < _RR_DBUS_ATTR_NUM; attr++) {
+		if (!variants[attr])
+			continue;
+		if (!g_variant_is_of_type (variants[attr], rr_dbus_data[attr].dbus_type)) {
+			g_set_error (error,
+			             NM_CONNECTION_ERROR,
+			             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+			             _("invalid variant type '%s' for \"%s\""),
+			             (const char *) rr_dbus_data[attr].dbus_type,
+			             rr_dbus_data[attr].name);
+			return NULL;
+		}
+	}
+
+	if (!variants[RR_DBUS_ATTR_FAMILY]) {
+		g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		                     _("missing \""NM_IP_ROUTING_RULE_ATTR_FAMILY"\""));
+		return NULL;
+	}
+	addr_family = g_variant_get_int32 (variants[RR_DBUS_ATTR_FAMILY]);
+	if (!NM_IN_SET (addr_family, AF_INET, AF_INET6)) {
+		g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		                     _("invalid \""NM_IP_ROUTING_RULE_ATTR_FAMILY"\""));
+		return NULL;
+	}
+
+	self = nm_ip_routing_rule_new (addr_family);
+
+	if (variants[RR_DBUS_ATTR_PRIORITY])
+		nm_ip_routing_rule_set_priority (self, g_variant_get_uint32 (variants[RR_DBUS_ATTR_PRIORITY]));
+
+	if (variants[RR_DBUS_ATTR_INVERT])
+		nm_ip_routing_rule_set_invert (self, g_variant_get_boolean (variants[RR_DBUS_ATTR_INVERT]));
+
+	if (variants[RR_DBUS_ATTR_TOS])
+		nm_ip_routing_rule_set_tos (self, g_variant_get_byte (variants[RR_DBUS_ATTR_TOS]));
+
+	if (variants[RR_DBUS_ATTR_IPPROTO])
+		nm_ip_routing_rule_set_ipproto (self, g_variant_get_byte (variants[RR_DBUS_ATTR_IPPROTO]));
+
+	for (i = 0; i < 2; i++) {
+		GVariant *v_start = variants[i ? RR_DBUS_ATTR_SPORT_START : RR_DBUS_ATTR_DPORT_START];
+		GVariant *v_end   = variants[i ? RR_DBUS_ATTR_SPORT_END   : RR_DBUS_ATTR_DPORT_END];
+		guint16 start, end;
+
+		if (!v_start && !v_end)
+			continue;
+
+		/* if start or end is missing, it defaults to the other parameter, respectively. */
+		if (v_start)
+			start = g_variant_get_uint16 (v_start);
+		else
+			start = g_variant_get_uint16 (v_end);
+		if (v_end)
+			end = g_variant_get_uint16 (v_end);
+		else
+			end = g_variant_get_uint16 (v_start);
+
+		if (i)
+			nm_ip_routing_rule_set_source_port (self, start, end);
+		else
+			nm_ip_routing_rule_set_destination_port (self, start, end);
+	}
+
+	if (   variants[RR_DBUS_ATTR_FWMARK]
+	    || variants[RR_DBUS_ATTR_FWMASK]) {
+		nm_ip_routing_rule_set_fwmark (self,
+		                               variants[RR_DBUS_ATTR_FWMARK] ? g_variant_get_uint32 (variants[RR_DBUS_ATTR_FWMARK]) : 0u,
+		                               variants[RR_DBUS_ATTR_FWMASK] ? g_variant_get_uint32 (variants[RR_DBUS_ATTR_FWMASK]) : 0u);
+	}
+
+	if (   variants[RR_DBUS_ATTR_FROM]
+	    || variants[RR_DBUS_ATTR_FROM_LEN]) {
+		nm_ip_routing_rule_set_from (self,
+		                             variants[RR_DBUS_ATTR_FROM]     ? g_variant_get_string (variants[RR_DBUS_ATTR_FROM], NULL) : NULL,
+		                             variants[RR_DBUS_ATTR_FROM_LEN] ? g_variant_get_byte   (variants[RR_DBUS_ATTR_FROM_LEN])   : 0u);
+	}
+
+	if (   variants[RR_DBUS_ATTR_TO]
+	    || variants[RR_DBUS_ATTR_TO_LEN]) {
+		nm_ip_routing_rule_set_to (self,
+		                           variants[RR_DBUS_ATTR_TO]     ? g_variant_get_string (variants[RR_DBUS_ATTR_TO], NULL) : NULL,
+		                           variants[RR_DBUS_ATTR_TO_LEN] ? g_variant_get_byte   (variants[RR_DBUS_ATTR_TO_LEN])   : 0u);
+	}
+
+	if (variants[RR_DBUS_ATTR_IIFNAME])
+		nm_ip_routing_rule_set_iifname (self, g_variant_get_string (variants[RR_DBUS_ATTR_IIFNAME], NULL));
+
+	if (variants[RR_DBUS_ATTR_OIFNAME])
+		nm_ip_routing_rule_set_oifname (self, g_variant_get_string (variants[RR_DBUS_ATTR_OIFNAME], NULL));
+
+	if (variants[RR_DBUS_ATTR_ACTION])
+		nm_ip_routing_rule_set_action (self, g_variant_get_byte (variants[RR_DBUS_ATTR_ACTION]));
+
+	if (variants[RR_DBUS_ATTR_TABLE])
+		nm_ip_routing_rule_set_table (self, g_variant_get_uint32 (variants[RR_DBUS_ATTR_TABLE]));
+
+	if (   strict
+	    && !nm_ip_routing_rule_validate (self, error))
+		return NULL;
+
+	return g_steal_pointer (&self);
+}
+
+static void
+_rr_to_dbus_add (GVariantBuilder *builder,
+                 RRDbusAttr attr,
+                 GVariant *value)
+{
+	nm_assert (builder);
+	nm_assert (value);
+	nm_assert (g_variant_is_floating (value));
+	nm_assert (g_variant_is_of_type (value, rr_dbus_data[attr].dbus_type));
+
+	g_variant_builder_add (builder,
+	                       "{sv}",
+	                       rr_dbus_data[attr].name,
+	                       value);
+}
+
+GVariant *
+nm_ip_routing_rule_to_dbus (const NMIPRoutingRule *self)
+{
+	GVariantBuilder builder;
+	char addr_str[NM_UTILS_INET_ADDRSTRLEN];
+
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), NULL);
+
+	g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT);
+
+	_rr_to_dbus_add (&builder, RR_DBUS_ATTR_FAMILY, g_variant_new_int32 (_ip_routing_rule_get_addr_family (self)));
+
+	if (self->invert)
+		_rr_to_dbus_add (&builder, RR_DBUS_ATTR_INVERT, g_variant_new_boolean (TRUE));
+
+	if (self->priority_has)
+		_rr_to_dbus_add (&builder, RR_DBUS_ATTR_PRIORITY, g_variant_new_uint32 (self->priority));
+
+	if (self->tos != 0)
+		_rr_to_dbus_add (&builder, RR_DBUS_ATTR_TOS, g_variant_new_byte (self->tos));
+
+	if (self->ipproto != 0)
+		_rr_to_dbus_add (&builder, RR_DBUS_ATTR_IPPROTO, g_variant_new_byte (self->ipproto));
+
+	if (self->fwmark != 0)
+		_rr_to_dbus_add (&builder, RR_DBUS_ATTR_FWMARK, g_variant_new_uint32 (self->fwmark));
+
+	if (self->fwmask != 0)
+		_rr_to_dbus_add (&builder, RR_DBUS_ATTR_FWMASK, g_variant_new_uint32 (self->fwmask));
+
+	if (   self->sport_start != 0
+	    || self->sport_end != 0) {
+		_rr_to_dbus_add (&builder, RR_DBUS_ATTR_SPORT_START, g_variant_new_uint16 (self->sport_start));
+		if (self->sport_start != self->sport_end)
+			_rr_to_dbus_add (&builder, RR_DBUS_ATTR_SPORT_END, g_variant_new_uint16 (self->sport_end));
+	}
+
+	if (   self->dport_start != 0
+	    || self->dport_end != 0) {
+		_rr_to_dbus_add (&builder, RR_DBUS_ATTR_DPORT_START, g_variant_new_uint16 (self->dport_start));
+		if (self->dport_start != self->dport_end)
+			_rr_to_dbus_add (&builder, RR_DBUS_ATTR_DPORT_END, g_variant_new_uint16 (self->dport_end));
+	}
+
+	if (   self->from_has
+	    || self->from_len != 0) {
+		_rr_to_dbus_add (&builder,
+		                 RR_DBUS_ATTR_FROM,
+		                 g_variant_new_string (   self->from_str
+		                                       ?: nm_utils_inet_ntop (_ip_routing_rule_get_addr_family (self),
+		                                                              &self->from_bin,
+		                                                              addr_str)));
+		_rr_to_dbus_add (&builder, RR_DBUS_ATTR_FROM_LEN, g_variant_new_byte (self->from_len));
+	}
+
+	if (   self->to_has
+	    || self->to_len != 0) {
+		_rr_to_dbus_add (&builder,
+		                 RR_DBUS_ATTR_TO,
+		                 g_variant_new_string (   self->to_str
+		                                       ?: nm_utils_inet_ntop (_ip_routing_rule_get_addr_family (self),
+		                                                              &self->to_bin,
+		                                                              addr_str)));
+		_rr_to_dbus_add (&builder, RR_DBUS_ATTR_TO_LEN, g_variant_new_byte (self->to_len));
+	}
+
+	if (self->iifname)
+		_rr_to_dbus_add (&builder, RR_DBUS_ATTR_IIFNAME, g_variant_new_string (self->iifname));
+
+	if (self->oifname)
+		_rr_to_dbus_add (&builder, RR_DBUS_ATTR_OIFNAME, g_variant_new_string (self->oifname));
+
+	if (self->action != FR_ACT_TO_TBL)
+		_rr_to_dbus_add (&builder, RR_DBUS_ATTR_ACTION, g_variant_new_byte (self->action));
+
+	if (self->table != 0)
+		_rr_to_dbus_add (&builder, RR_DBUS_ATTR_TABLE, g_variant_new_uint32 (self->table));
+
+	return g_variant_builder_end (&builder);;
+}
+
+/*****************************************************************************/
+
+static gboolean
+_rr_string_validate (gboolean for_from /* or else to-string */,
+                     NMIPRoutingRuleAsStringFlags to_string_flags,
+                     GHashTable *extra_args,
+                     GError **error)
+{
+	if (NM_FLAGS_ANY (to_string_flags, ~(  NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET
+	                                     | NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET6
+	                                     | NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE))) {
+		g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+		                     _("Unsupported to-string-flags argument"));
+		return FALSE;
+	}
+
+	if (   extra_args
+	    && g_hash_table_size (extra_args) > 0) {
+		g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+		                     _("Unsupported extra-argument"));
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static int
+_rr_string_addr_family_from_flags (NMIPRoutingRuleAsStringFlags to_string_flags)
+{
+	if (NM_FLAGS_HAS (to_string_flags, NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET)) {
+		if (!NM_FLAGS_HAS (to_string_flags, NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET6))
+			return AF_INET;
+	} else if (NM_FLAGS_HAS (to_string_flags, NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET6))
+		return AF_INET6;
+	return AF_UNSPEC;
+}
+
+/**
+ * nm_ip_routing_rule_from_string:
+ * @str: the string representation to convert to an #NMIPRoutingRule
+ * @to_string_flags: #NMIPRoutingRuleAsStringFlags for controlling the
+ *   string conversion.
+ * @extra_args: (allow-none): extra arguments for controlling the string
+ *   conversion. Currently not extra arguments are supported.
+ * @error: (allow-none) (out): the error reason.
+ *
+ * Returns: (transfer full): the new #NMIPRoutingRule or %NULL on error.
+ *
+ * Since: 1.18
+ */
+NMIPRoutingRule *
+nm_ip_routing_rule_from_string (const char *str,
+                                NMIPRoutingRuleAsStringFlags to_string_flags,
+                                GHashTable *extra_args,
+                                GError **error)
+{
+	nm_auto_unref_ip_routing_rule NMIPRoutingRule *self = NULL;
+	gs_free const char **tokens = NULL;
+	gsize i_token;
+	gboolean any_words = FALSE;
+	char *word0 = NULL;
+	char *word1 = NULL;
+	char *word_from = NULL;
+	char *word_to = NULL;
+	char *word_iifname = NULL;
+	char *word_oifname = NULL;
+	gint64 i64_priority = -1;
+	gint64 i64_table = -1;
+	gint64 i64_tos = -1;
+	gint64 i64_fwmark = -1;
+	gint64 i64_fwmask = -1;
+	gint64 i64_sport_start = -1;
+	gint64 i64_ipproto = -1;
+	guint16 sport_end = 0;
+	gint64 i64_dport_start = -1;
+	guint16 dport_end = 0;
+	gboolean val_invert = FALSE;
+	int addr_family = AF_UNSPEC;
+	NMIPAddr val_from = { };
+	NMIPAddr val_to = { };
+	int val_from_len = -1;
+	int val_to_len = -1;
+	char *s;
+
+	g_return_val_if_fail (str, NULL);
+
+	if (!_rr_string_validate (TRUE, to_string_flags, extra_args, error))
+		return NULL;
+
+	/* NM_IP_ROUTING_RULE_TO_STRING_TYPE_IPROUTE gives a string representation that is
+	 * partly compatibly with iproute2. That is, the part after `ip -[46] rule add $ARGS`.
+	 * There are differences though:
+	 *
+	 * - trying to convert an invalid rule to string may not be possible. The reason is for
+	 *   example that an invalid rule can have nm_ip_routing_rule_get_from() like "bogus",
+	 *   but we don't write that as "from bogus". In general, if you try to convert an invalid
+	 *   rule to string, the operation may fail or the result may itself not be parsable.
+	 *   Of course, valid rules can be converted to string and read back the same (round-trip).
+	 *
+	 * - iproute2 in may regards is flexible about the command lines. For example
+	 *   - for tables it accepts table names from /etc/iproute2/rt_tables
+	 *   - key names like "preference" can be abbreviated to "prefe", we don't do that.
+	 *   - the "preference"/"priority" may be unspecified, in which kernel automatically
+	 *     chooses an unused priority (during `ip rule add`). We don't allow for that, the
+	 *     priority must be explicitly set.
+	 *
+	 * - iproute2 does not support any escaping. Well, it's the shell that supports quoting
+	 *   and escaping and splits the command line. We need to split the command line ourself,
+	 *   but we don't support full shell quotation.
+	 *   from-string tokenizes words at (ASCII) whitespaces (removing the whitespaces).
+	 *   It also supports backslash escaping (e.g. to contain whitespace), but it does
+	 *   not support special escape sequences. Values are taken literally, meaning
+	 *   "\n\ \111" gives results in "n 111".
+	 *   The strings really shouldn't contain any special characters that require escaping,
+	 *   but that's the rule.
+	 *   This also goes together with the @allow_escaping parameter of nm_utils_strsplit_set().
+	 *   If you concatenate multiple rule expressions with a delimiter, the delimiter inside
+	 *   each word can be backslash escaped, and nm_utils_strsplit_set(allow_escaping=TRUE) will
+	 *   properly split the words, preserving the backslashes, which then will be removed by
+	 *   nm_ip_routing_rule_from_string().
+	 */
+
+	addr_family = _rr_string_addr_family_from_flags (to_string_flags);
+
+	tokens = nm_utils_escaped_tokens_split (str, NM_ASCII_SPACES);
+	for (i_token = 0; tokens && tokens[i_token]; i_token++) {
+		char *str_word = (char *) tokens[i_token];
+
+		any_words = TRUE;
+		if (!word0)
+			word0 = str_word;
+		else {
+			nm_assert (!word1);
+			word1 = str_word;
+		}
+
+		/* iproute2 matches keywords with any partial prefix. We don't allow
+		 * for that flexiblity. */
+
+		if (NM_IN_STRSET (word0, "from")) {
+			if (!word1)
+				continue;
+			if (word_from)
+				goto next_fail_word0_duplicate_key;
+			word_from = word1;
+			goto next_words_consumed;
+		}
+		if (NM_IN_STRSET (word0, "to")) {
+			if (!word1)
+				continue;
+			if (word_to)
+				goto next_fail_word0_duplicate_key;
+			word_to = word1;
+			goto next_words_consumed;
+		}
+		if (NM_IN_STRSET (word0, "not")) {
+			/* we accept multiple "not" specifiers. */
+			val_invert = TRUE;
+			goto next_words_consumed;
+		}
+		if (NM_IN_STRSET (word0, "priority",
+		                         "order",
+		                         "pref",
+		                         "preference")) {
+			if (!word1)
+				continue;
+			if (i64_priority != -1)
+				goto next_fail_word0_duplicate_key;
+			i64_priority = _nm_utils_ascii_str_to_int64 (word1, 0, 0, G_MAXUINT32, -1);
+			if (i64_priority == -1)
+				goto next_fail_word1_invalid_value;
+			goto next_words_consumed;
+		}
+		if (NM_IN_STRSET (word0, "table",
+		                         "lookup")) {
+			if (!word1)
+				continue;
+			if (i64_table != -1)
+				goto next_fail_word0_duplicate_key;
+			i64_table = _nm_utils_ascii_str_to_int64 (word1, 0, 1, G_MAXUINT32, -1);
+			if (i64_table == -1)
+				goto next_fail_word1_invalid_value;
+			goto next_words_consumed;
+		}
+		if (NM_IN_STRSET (word0, "tos",
+		                         "dsfield")) {
+			if (!word1)
+				continue;
+			if (i64_tos != -1)
+				goto next_fail_word0_duplicate_key;
+			i64_tos = _nm_utils_ascii_str_to_int64 (word1, 16, 0, G_MAXUINT8, -1);
+			if (i64_tos == -1)
+				goto next_fail_word1_invalid_value;
+			goto next_words_consumed;
+		}
+		if (NM_IN_STRSET (word0, "ipproto")) {
+			if (!word1)
+				continue;
+			if (i64_ipproto != -1)
+				goto next_fail_word0_duplicate_key;
+			i64_ipproto = _nm_utils_ascii_str_to_int64 (word1, 10, 0, G_MAXUINT8, -1);
+			if (i64_ipproto == -1)
+				goto next_fail_word1_invalid_value;
+			goto next_words_consumed;
+		}
+		if (NM_IN_STRSET (word0, "sport")) {
+			if (!word1)
+				continue;
+			if (i64_sport_start != -1)
+				goto next_fail_word0_duplicate_key;
+			if (!_rr_xport_range_parse (word1, &i64_sport_start, &sport_end))
+				goto next_fail_word1_invalid_value;
+			goto next_words_consumed;
+		}
+		if (NM_IN_STRSET (word0, "dport")) {
+			if (!word1)
+				continue;
+			if (i64_dport_start != -1)
+				goto next_fail_word0_duplicate_key;
+			if (!_rr_xport_range_parse (word1, &i64_dport_start, &dport_end))
+				goto next_fail_word1_invalid_value;
+			goto next_words_consumed;
+		}
+		if (NM_IN_STRSET (word0, "fwmark")) {
+			if (!word1)
+				continue;
+			if (i64_fwmark != -1)
+				goto next_fail_word0_duplicate_key;
+			s = strchr (word1, '/');
+			if (s)
+				*(s++) = '\0';
+			i64_fwmark = _nm_utils_ascii_str_to_int64 (word1, 0, 0, G_MAXUINT32, -1);
+			if (i64_fwmark == -1)
+				goto next_fail_word1_invalid_value;
+			if (s) {
+				i64_fwmask = _nm_utils_ascii_str_to_int64 (s, 0, 0, G_MAXUINT32, -1);
+				if (i64_fwmask == -1)
+					goto next_fail_word1_invalid_value;
+			} else
+				i64_fwmask = 0xFFFFFFFFu;
+			goto next_words_consumed;
+		}
+		if (NM_IN_STRSET (word0, "iif",
+		                         "dev")) {
+			if (!word1)
+				continue;
+			if (word_iifname)
+				goto next_fail_word0_duplicate_key;
+			word_iifname = word1;
+			goto next_words_consumed;
+		}
+		if (NM_IN_STRSET (word0, "oif")) {
+			if (!word1)
+				continue;
+			if (word_oifname)
+				goto next_fail_word0_duplicate_key;
+			word_oifname = word1;
+			goto next_words_consumed;
+		}
+
+		/* also the action is still unsupported. For the moment, we only support
+		 * FR_ACT_TO_TBL, which is the default (by not expressing it on the command
+		 * line). */
+		g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+		             _("unsupported key \"%s\""),
+		             word0);
+		return FALSE;
+next_fail_word0_duplicate_key:
+		g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+		             _("duplicate key \"%s\""),
+		             word0);
+		return FALSE;
+next_fail_word1_invalid_value:
+		g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+		             _("invalid value for \"%s\""),
+		             word0);
+		return FALSE;
+next_words_consumed:
+		word0 = NULL;
+		word1 = NULL;
+	}
+
+	if (!any_words) {
+		g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+		                     _("empty text does not describe a rule"));
+		return FALSE;
+	}
+
+	if (word0) {
+		g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+		             _("missing argument for \"%s\""),
+		             word0);
+		return FALSE;
+	}
+
+	if (!NM_IN_STRSET (word_from, NULL, "all")) {
+		if (!nm_utils_parse_inaddr_prefix_bin (addr_family,
+		                                       word_from,
+		                                       &addr_family,
+		                                       &val_from,
+		                                       &val_from_len)) {
+			g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+			                     _("invalid \"from\" part"));
+			return FALSE;
+		}
+		if (val_from_len == -1)
+			val_from_len = nm_utils_addr_family_to_size (addr_family) * 8;
+	}
+
+	if (!NM_IN_STRSET (word_to, NULL, "all")) {
+		if (!nm_utils_parse_inaddr_prefix_bin (addr_family,
+		                                       word_to,
+		                                       &addr_family,
+		                                       &val_to,
+		                                       &val_to_len)) {
+			g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+			                     _("invalid \"to\" part"));
+			return FALSE;
+		}
+		if (val_to_len == -1)
+			val_to_len = nm_utils_addr_family_to_size (addr_family) * 8;
+	}
+
+	if (!NM_IN_SET (addr_family, AF_INET, AF_INET6)) {
+		g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+		             _("cannot detect address family for rule"));
+		return FALSE;
+	}
+
+	self = nm_ip_routing_rule_new (addr_family);
+
+	if (val_invert)
+		self->invert = TRUE;
+
+	if (i64_priority != -1)
+		nm_ip_routing_rule_set_priority (self, i64_priority);
+
+	if (i64_tos != -1)
+		nm_ip_routing_rule_set_tos (self, i64_tos);
+
+	if (i64_ipproto != -1)
+		nm_ip_routing_rule_set_ipproto (self, i64_ipproto);
+
+	if (i64_fwmark != -1)
+		nm_ip_routing_rule_set_fwmark (self, i64_fwmark, i64_fwmask);
+
+	if (i64_sport_start != -1)
+		nm_ip_routing_rule_set_source_port (self, i64_sport_start, sport_end);
+
+	if (i64_dport_start != -1)
+		nm_ip_routing_rule_set_destination_port (self, i64_dport_start, dport_end);
+
+	if (   val_from_len > 0
+	    || (   val_from_len == 0
+	        && !nm_ip_addr_is_null (addr_family, &val_from))) {
+		nm_ip_routing_rule_set_from_bin (self,
+		                                 &val_from,
+		                                 val_from_len);
+	}
+
+	if (   val_to_len > 0
+	    || (   val_to_len == 0
+	        && !nm_ip_addr_is_null (addr_family, &val_to))) {
+		nm_ip_routing_rule_set_to_bin (self,
+		                               &val_to,
+		                               val_to_len);
+	}
+
+	if (word_iifname)
+		nm_ip_routing_rule_set_iifname (self, word_iifname);
+
+	if (word_oifname)
+		nm_ip_routing_rule_set_oifname (self, word_oifname);
+
+	if (i64_table != -1)
+		nm_ip_routing_rule_set_table (self, i64_table);
+
+	if (NM_FLAGS_HAS (to_string_flags, NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE)) {
+		gs_free_error GError *local = NULL;
+
+		if (!nm_ip_routing_rule_validate (self, &local)) {
+			g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+			             _("rule is invalid: %s"),
+			             local->message);
+			return NULL;
+		}
+	}
+
+	return g_steal_pointer (&self);
+}
+
+static void
+_rr_string_append_inet_addr (GString *str,
+                             gboolean is_from /* or else is-to */,
+                             gboolean required,
+                             int addr_family,
+                             const NMIPAddr *addr_bin,
+                             guint8 addr_len)
+{
+	char addr_str[NM_UTILS_INET_ADDRSTRLEN];
+
+	if (addr_len == 0) {
+		if (required) {
+			g_string_append_printf (nm_gstring_add_space_delimiter (str),
+			                        "%s %s/0",
+			                        is_from ? "from" : "to",
+			                          (addr_family == AF_INET)
+			                        ? "0.0.0.0"
+			                        : "::");
+		}
+		return;
+	}
+
+	g_string_append_printf (nm_gstring_add_space_delimiter (str),
+	                        "%s %s",
+	                        is_from ? "from" : "to",
+	                        nm_utils_inet_ntop (addr_family,
+	                                            addr_bin,
+	                                            addr_str));
+	if (addr_len != nm_utils_addr_family_to_size (addr_family) * 8) {
+		g_string_append_printf (str,
+		                        "/%u",
+		                        addr_len);
+	}
+}
+
+/**
+ * nm_ip_routing_rule_to_string:
+ * @self: the #NMIPRoutingRule instance to convert to string.
+ * @to_string_flags: #NMIPRoutingRuleAsStringFlags for controlling the
+ *   string conversion.
+ * @extra_args: (allow-none): extra arguments for controlling the string
+ *   conversion. Currently not extra arguments are supported.
+ * @error: (allow-none) (out): the error reason.
+ *
+ * Returns: (transfer full): the string representation or %NULL on error.
+ *
+ * Since: 1.18
+ */
+char *
+nm_ip_routing_rule_to_string (const NMIPRoutingRule *self,
+                              NMIPRoutingRuleAsStringFlags to_string_flags,
+                              GHashTable *extra_args,
+                              GError **error)
+{
+	nm_auto_free_gstring GString *str = NULL;
+	int addr_family;
+
+	g_return_val_if_fail (NM_IS_IP_ROUTING_RULE (self, TRUE), NULL);
+
+	if (!_rr_string_validate (FALSE, to_string_flags, extra_args, error))
+		return NULL;
+
+	addr_family = nm_ip_routing_rule_get_addr_family (self);
+
+	if (!NM_IN_SET (_rr_string_addr_family_from_flags (to_string_flags),
+	                AF_UNSPEC,
+	                addr_family)) {
+		g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+		                     _("invalid address family"));
+		return NULL;
+	}
+
+	/* It is only guaranteed that valid rules can be expressed as string.
+	 *
+	 * Still, unless requested proceed to convert to string without validating and
+	 * hope for the best.
+	 *
+	 * That is, because self->from_str might contain an invalid IP address (indicated
+	 * by self->from_valid). But we don't support serializing such arbitrary strings
+	 * as "from %s". */
+	if (NM_FLAGS_HAS (to_string_flags, NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE)) {
+		gs_free_error GError *local = NULL;
+
+		if (!nm_ip_routing_rule_validate (self, &local)) {
+			g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_FAILED,
+			             _("rule is invalid: %s"),
+			             local->message);
+			return NULL;
+		}
+	}
+
+	str = g_string_sized_new (30);
+
+	if (self->invert)
+		g_string_append (str, "not");
+
+	if (self->priority_has) {
+		g_string_append_printf (nm_gstring_add_space_delimiter (str),
+		                        "priority %u",
+		                        (guint) self->priority);
+	}
+
+	_rr_string_append_inet_addr (str,
+	                             TRUE,
+	                             (   !self->to_has
+	                              || !self->to_valid),
+	                             addr_family,
+	                             &self->from_bin,
+	                               (self->from_has && self->from_valid)
+	                             ? self->from_len
+	                             : 0);
+
+	_rr_string_append_inet_addr (str,
+	                             FALSE,
+	                             FALSE,
+	                             addr_family,
+	                             &self->to_bin,
+	                               (self->to_has && self->to_valid)
+	                             ? self->to_len
+	                             : 0);
+
+	if (self->tos != 0) {
+		g_string_append_printf (nm_gstring_add_space_delimiter (str),
+		                        "tos 0x%02x",
+		                        (guint) self->tos);
+	}
+
+	if (self->ipproto != 0) {
+		g_string_append_printf (nm_gstring_add_space_delimiter (str),
+		                        "ipproto %u",
+		                        (guint) self->ipproto);
+	}
+
+	if (   self->fwmark != 0
+	    || self->fwmask != 0) {
+		if (self->fwmark != 0) {
+			g_string_append_printf (nm_gstring_add_space_delimiter (str),
+			                        "fwmark 0x%x",
+			                        self->fwmark);
+		} else {
+			g_string_append_printf (nm_gstring_add_space_delimiter (str),
+			                        "fwmark 0");
+		}
+		if (self->fwmask != 0xFFFFFFFFu) {
+			if (self->fwmask != 0)
+				g_string_append_printf (str, "/0x%x", self->fwmask);
+			else
+				g_string_append_printf (str, "/0");
+		}
+	}
+
+	if (   self->sport_start != 0
+	    || self->sport_end != 0) {
+		g_string_append_printf (nm_gstring_add_space_delimiter (str),
+		                        "sport %u",
+		                        self->sport_start);
+		if (self->sport_start != self->sport_end) {
+			g_string_append_printf (str,
+			                        "-%u",
+			                        self->sport_end);
+		}
+	}
+
+	if (   self->dport_start != 0
+	    || self->dport_end != 0) {
+		g_string_append_printf (nm_gstring_add_space_delimiter (str),
+		                        "dport %u",
+		                        self->dport_start);
+		if (self->dport_start != self->dport_end) {
+			g_string_append_printf (str,
+			                        "-%u",
+			                        self->dport_end);
+		}
+	}
+
+	if (self->iifname) {
+		g_string_append (nm_gstring_add_space_delimiter (str),
+		                 "iif ");
+		nm_utils_escaped_tokens_escape_gstr (self->iifname,
+		                                     NM_ASCII_SPACES,
+		                                     str);
+	}
+
+	if (self->oifname) {
+		g_string_append (nm_gstring_add_space_delimiter (str),
+		                 "oif ");
+		nm_utils_escaped_tokens_escape_gstr (self->oifname,
+		                                     NM_ASCII_SPACES,
+		                                     str);
+	}
+
+	if (self->table != 0) {
+		g_string_append_printf (nm_gstring_add_space_delimiter (str),
+		                        "table %u",
+		                        (guint) self->table);
+	}
+
+	return g_string_free (g_steal_pointer (&str), FALSE);
+}
+
+/*****************************************************************************/
+
 NM_GOBJECT_PROPERTIES_DEFINE (NMSettingIPConfig,
 	PROP_METHOD,
 	PROP_DNS,
@@ -1413,6 +3519,7 @@ typedef struct {
 	int dns_priority;
 	GPtrArray *addresses;  /* array of NMIPAddress */
 	GPtrArray *routes;     /* array of NMIPRoute */
+	GPtrArray *routing_rules;
 	gint64 route_metric;
 	guint32 route_table;
 	char *gateway;
@@ -2314,6 +4421,225 @@ nm_setting_ip_config_get_route_table (NMSettingIPConfig *setting)
 	return NM_SETTING_IP_CONFIG_GET_PRIVATE (setting)->route_table;
 }
 
+/*****************************************************************************/
+
+static void
+_routing_rules_notify (NMSettingIPConfig *setting)
+{
+	_nm_setting_emit_property_changed (NM_SETTING (setting));
+}
+
+/**
+ * nm_setting_ip_config_get_num_routing_rules:
+ * @setting: the #NMSettingIPConfig
+ *
+ * Returns: the number of configured routing rules
+ *
+ * Since: 1.18
+ **/
+guint
+nm_setting_ip_config_get_num_routing_rules (NMSettingIPConfig *setting)
+{
+	NMSettingIPConfigPrivate *priv;
+
+	g_return_val_if_fail (NM_IS_SETTING_IP_CONFIG (setting), 0);
+
+	priv = NM_SETTING_IP_CONFIG_GET_PRIVATE (setting);
+	return priv->routing_rules ? priv->routing_rules->len : 0u;
+}
+
+/**
+ * nm_setting_ip_config_get_routing_rule:
+ * @setting: the #NMSettingIPConfig
+ * @idx: index number of the routing_rule to return
+ *
+ * Returns: (transfer none): the routing rule at index @idx
+ *
+ * Since: 1.18
+ **/
+NMIPRoutingRule *
+nm_setting_ip_config_get_routing_rule (NMSettingIPConfig *setting, guint idx)
+{
+	NMSettingIPConfigPrivate *priv;
+
+	g_return_val_if_fail (NM_IS_SETTING_IP_CONFIG (setting), NULL);
+
+	priv = NM_SETTING_IP_CONFIG_GET_PRIVATE (setting);
+
+	g_return_val_if_fail (priv->routing_rules && idx < priv->routing_rules->len, NULL);
+
+	return priv->routing_rules->pdata[idx];
+}
+
+/**
+ * nm_setting_ip_config_add_routing_rule:
+ * @setting: the #NMSettingIPConfig
+ * @routing_rule: the #NMIPRoutingRule to add. The address family
+ *   of the added rule must be compatible with the setting.
+ *
+ * Appends a new routing-rule and associated information to the setting. The
+ * given routing rules gets sealed and the reference count is incremented.
+ * The function does not check whether an identical rule already exists
+ * and always appends the rule to the end of the list.
+ *
+ * Since: 1.18
+ **/
+void
+nm_setting_ip_config_add_routing_rule (NMSettingIPConfig *setting,
+                                       NMIPRoutingRule *routing_rule)
+{
+	NMSettingIPConfigPrivate *priv;
+
+	g_return_if_fail (NM_IS_SETTING_IP_CONFIG (setting));
+	g_return_if_fail (NM_IS_IP_ROUTING_RULE (routing_rule, TRUE));
+	g_return_if_fail (_ip_routing_rule_get_addr_family (routing_rule) == NM_SETTING_IP_CONFIG_GET_FAMILY (setting));
+
+	priv = NM_SETTING_IP_CONFIG_GET_PRIVATE (setting);
+
+	if (!priv->routing_rules)
+		priv->routing_rules = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_ip_routing_rule_unref);
+
+	nm_ip_routing_rule_seal (routing_rule);
+	g_ptr_array_add (priv->routing_rules, nm_ip_routing_rule_ref (routing_rule));
+	_routing_rules_notify (setting);
+}
+
+/**
+ * nm_setting_ip_config_remove_routing_rule:
+ * @setting: the #NMSettingIPConfig
+ * @idx: index number of the routing_rule
+ *
+ * Removes the routing_rule at index @idx.
+ *
+ * Since: 1.18
+ **/
+void
+nm_setting_ip_config_remove_routing_rule (NMSettingIPConfig *setting,
+                                          guint idx)
+{
+	NMSettingIPConfigPrivate *priv;
+
+	g_return_if_fail (NM_IS_SETTING_IP_CONFIG (setting));
+
+	priv = NM_SETTING_IP_CONFIG_GET_PRIVATE (setting);
+	g_return_if_fail (priv->routing_rules && idx < priv->routing_rules->len);
+
+	g_ptr_array_remove_index (priv->routing_rules, idx);
+	_routing_rules_notify (setting);
+}
+
+/**
+ * nm_setting_ip_config_clear_routing_rules:
+ * @setting: the #NMSettingIPConfig
+ *
+ * Removes all configured routing rules.
+ *
+ * Since: 1.18
+ **/
+void
+nm_setting_ip_config_clear_routing_rules (NMSettingIPConfig *setting)
+{
+	NMSettingIPConfigPrivate *priv = NM_SETTING_IP_CONFIG_GET_PRIVATE (setting);
+
+	g_return_if_fail (NM_IS_SETTING_IP_CONFIG (setting));
+
+	if (   priv->routing_rules
+	    && priv->routing_rules->len > 0) {
+		g_ptr_array_set_size (priv->routing_rules, 0);
+		_routing_rules_notify (setting);
+	}
+}
+
+static GVariant *
+_routing_rules_dbus_only_synth (const NMSettInfoSetting *sett_info,
+                                guint property_idx,
+                                NMConnection *connection,
+                                NMSetting *setting,
+                                NMConnectionSerializationFlags flags)
+{
+	NMSettingIPConfig *self = NM_SETTING_IP_CONFIG (setting);
+	NMSettingIPConfigPrivate *priv;
+	GVariantBuilder builder;
+	gboolean any = FALSE;
+	guint i;
+
+	priv = NM_SETTING_IP_CONFIG_GET_PRIVATE (self);
+
+	if (   !priv->routing_rules
+	    || priv->routing_rules->len == 0)
+		return NULL;
+
+	for (i = 0; i < priv->routing_rules->len; i++) {
+		GVariant *variant;
+
+		variant = nm_ip_routing_rule_to_dbus (priv->routing_rules->pdata[i]);
+		if (!variant)
+			continue;
+
+		if (!any) {
+			any = TRUE;
+			g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}"));
+		}
+		g_variant_builder_add (&builder, "@a{sv}", variant);
+	}
+
+	return any ? g_variant_builder_end (&builder) : NULL;
+}
+
+static gboolean
+_routing_rules_dbus_only_set (NMSetting     *setting,
+                              GVariant      *connection_dict,
+                              const char    *property,
+                              GVariant      *value,
+                              NMSettingParseFlags parse_flags,
+                              GError       **error)
+{
+	GVariantIter iter_rules;
+	GVariant *rule_var;
+	guint i_rule;
+	gboolean success = FALSE;
+	gboolean rules_changed = FALSE;
+
+	nm_assert (g_variant_is_of_type (value, G_VARIANT_TYPE ("aa{sv}")));
+
+	g_variant_iter_init (&iter_rules, value);
+
+	i_rule = 0;
+	while (g_variant_iter_next (&iter_rules, "@a{sv}", &rule_var)) {
+		_nm_unused gs_unref_variant GVariant *rule_var_unref = rule_var;
+		nm_auto_unref_ip_routing_rule NMIPRoutingRule *rule = NULL;
+		gs_free_error GError *local = NULL;
+
+		i_rule++;
+
+		rule = nm_ip_routing_rule_from_dbus (rule_var,
+		                                     NM_FLAGS_HAS (parse_flags, NM_SETTING_PARSE_FLAGS_STRICT),
+		                                     &local);
+		if (!rule) {
+			if (NM_FLAGS_HAS (parse_flags, NM_SETTING_PARSE_FLAGS_STRICT)) {
+				g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_MISSING_PROPERTY,
+				             _("rule #%u is invalid: %s"),
+				             i_rule,
+				             local->message);
+				goto out;
+			}
+			continue;
+		}
+
+		nm_setting_ip_config_add_routing_rule (NM_SETTING_IP_CONFIG (setting), rule);
+		rules_changed = TRUE;
+	}
+
+	success = TRUE;
+
+out:
+	if (rules_changed)
+		_routing_rules_notify (NM_SETTING_IP_CONFIG (setting));
+	return success;
+}
+
+/*****************************************************************************/
+
 /**
  * nm_setting_ip_config_get_ignore_auto_routes:
  * @setting: the #NMSettingIPConfig
@@ -2600,6 +4926,33 @@ verify (NMSetting *setting, NMConnection *connection, GError **error)
 		}
 	}
 
+	if (priv->routing_rules) {
+		for (i = 0; i < priv->routing_rules->len; i++) {
+			NMIPRoutingRule *rule = priv->routing_rules->pdata[i];
+			gs_free_error GError *local = NULL;
+
+			if (_ip_routing_rule_get_addr_family (rule) != NM_SETTING_IP_CONFIG_GET_FAMILY (setting)) {
+				g_set_error (error,
+				             NM_CONNECTION_ERROR,
+				             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+				             _("%u. rule has wrong address-family"),
+				             i + 1);
+				g_prefix_error (error, "%s.%s: ", nm_setting_get_name (setting), NM_SETTING_IP_CONFIG_ROUTING_RULES);
+				return FALSE;
+			}
+			if (!nm_ip_routing_rule_validate (rule, &local)) {
+				g_set_error (error,
+				             NM_CONNECTION_ERROR,
+				             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+				             _("%u. rule is invalid: %s"),
+				             i + 1,
+				             local->message);
+				g_prefix_error (error, "%s.%s: ", nm_setting_get_name (setting), NM_SETTING_IP_CONFIG_ROUTES);
+				return FALSE;
+			}
+		}
+	}
+
 	if (priv->gateway && priv->never_default) {
 		g_set_error (error,
 		             NM_CONNECTION_ERROR,
@@ -2654,6 +5007,24 @@ compare_property (const NMSettInfoSetting *sett_info,
 		return TRUE;
 	}
 
+	if (nm_streq (sett_info->property_infos[property_idx].name, NM_SETTING_IP_CONFIG_ROUTING_RULES)) {
+		if (other) {
+			guint n;
+
+			a_priv = NM_SETTING_IP_CONFIG_GET_PRIVATE (setting);
+			b_priv = NM_SETTING_IP_CONFIG_GET_PRIVATE (other);
+
+			n = (a_priv->routing_rules) ? a_priv->routing_rules->len : 0u;
+			if (n != (b_priv->routing_rules ? b_priv->routing_rules->len : 0u))
+				return FALSE;
+			for (i = 0; i < n; i++) {
+				if (nm_ip_routing_rule_cmp (a_priv->routing_rules->pdata[i], b_priv->routing_rules->pdata[i]) != 0)
+					return FALSE;
+			}
+		}
+		return TRUE;
+	}
+
 	return NM_SETTING_CLASS (nm_setting_ip_config_parent_class)->compare_property (sett_info,
 	                                                                               property_idx,
 	                                                                               setting,
@@ -2661,6 +5032,72 @@ compare_property (const NMSettInfoSetting *sett_info,
 	                                                                               flags);
 }
 
+static void
+duplicate_copy_properties (const NMSettInfoSetting *sett_info,
+                           NMSetting *src,
+                           NMSetting *dst)
+{
+	NMSettingIPConfigPrivate *priv_src = NM_SETTING_IP_CONFIG_GET_PRIVATE (src);
+	NMSettingIPConfigPrivate *priv_dst = NM_SETTING_IP_CONFIG_GET_PRIVATE (dst);
+	guint i;
+	gboolean changed = FALSE;
+
+	NM_SETTING_CLASS (nm_setting_ip_config_parent_class)->duplicate_copy_properties (sett_info,
+	                                                                                 src,
+	                                                                                 dst);
+
+	if (   priv_dst->routing_rules
+	    && priv_dst->routing_rules->len > 0) {
+		changed = TRUE;
+		g_ptr_array_set_size (priv_dst->routing_rules, 0);
+	}
+	if (   priv_src->routing_rules
+	    && priv_src->routing_rules->len > 0) {
+		changed = TRUE;
+		if (!priv_dst->routing_rules)
+			priv_dst->routing_rules = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_ip_routing_rule_unref);
+		for (i = 0; i < priv_src->routing_rules->len; i++) {
+			g_ptr_array_add (priv_dst->routing_rules,
+			                 nm_ip_routing_rule_ref (priv_src->routing_rules->pdata[i]));
+		}
+	}
+	if (changed)
+		_routing_rules_notify (NM_SETTING_IP_CONFIG (dst));
+}
+
+static void
+enumerate_values (const NMSettInfoProperty *property_info,
+                  NMSetting *setting,
+                  NMSettingValueIterFn func,
+                  gpointer user_data)
+{
+	if (nm_streq (property_info->name, NM_SETTING_IP_CONFIG_ROUTING_RULES)) {
+		NMSettingIPConfigPrivate *priv = NM_SETTING_IP_CONFIG_GET_PRIVATE (setting);
+		nm_auto_unset_gvalue GValue value = G_VALUE_INIT;
+		GPtrArray *ptr = NULL;
+		guint i;
+
+		if (priv->routing_rules && priv->routing_rules->len > 0) {
+			ptr = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_ip_routing_rule_unref);
+			for (i = 0; i < priv->routing_rules->len; i++)
+				g_ptr_array_add (ptr, nm_ip_routing_rule_ref (priv->routing_rules->pdata[i]));
+		}
+		g_value_init (&value, G_TYPE_PTR_ARRAY);
+		g_value_take_boxed (&value, ptr);
+		func (setting,
+		      property_info->name,
+		      &value,
+		      0,
+		      user_data);
+		return;
+	}
+
+	NM_SETTING_CLASS (nm_setting_ip_config_parent_class)->enumerate_values (property_info,
+	                                                                        setting,
+	                                                                        func,
+	                                                                        user_data);
+}
+
 /*****************************************************************************/
 
 static gboolean
@@ -2693,6 +5130,18 @@ _nm_sett_info_property_override_create_array_ip_config (void)
 	                                   ip_gateway_set,
 	                                   NULL);
 
+	/* ---dbus---
+	 * property: routing-rules
+	 * format: array of 'a{sv}'
+	 * description: Array of dictionaries for routing rules.
+	 * ---end---
+	 */
+	_properties_override_add_dbus_only (properties_override,
+	                                    NM_SETTING_IP_CONFIG_ROUTING_RULES,
+	                                    G_VARIANT_TYPE ("aa{sv}"),
+	                                    _routing_rules_dbus_only_synth,
+	                                    _routing_rules_dbus_only_set);
+
 	return properties_override;
 }
 
@@ -2900,6 +5349,8 @@ finalize (GObject *object)
 		g_ptr_array_unref (priv->dns_options);
 	g_ptr_array_unref (priv->addresses);
 	g_ptr_array_unref (priv->routes);
+	if (priv->routing_rules)
+		g_ptr_array_unref (priv->routing_rules);
 
 	G_OBJECT_CLASS (nm_setting_ip_config_parent_class)->finalize (object);
 }
@@ -2916,8 +5367,10 @@ nm_setting_ip_config_class_init (NMSettingIPConfigClass *klass)
 	object_class->set_property = set_property;
 	object_class->finalize     = finalize;
 
-	setting_class->verify           = verify;
-	setting_class->compare_property = compare_property;
+	setting_class->verify                    = verify;
+	setting_class->compare_property          = compare_property;
+	setting_class->duplicate_copy_properties = duplicate_copy_properties;
+	setting_class->enumerate_values          = enumerate_values;
 
 	/**
 	 * NMSettingIPConfig:method:
diff --git a/libnm-core/nm-setting-ip-config.h b/libnm-core/nm-setting-ip-config.h
index 39cb36a4..76a94914 100644
--- a/libnm-core/nm-setting-ip-config.h
+++ b/libnm-core/nm-setting-ip-config.h
@@ -159,6 +159,155 @@ gboolean     nm_ip_route_attribute_validate  (const char *name,
 #define NM_IP_ROUTE_ATTRIBUTE_LOCK_INITRWND  "lock-initrwnd"
 #define NM_IP_ROUTE_ATTRIBUTE_LOCK_MTU       "lock-mtu"
 
+/*****************************************************************************/
+
+typedef struct NMIPRoutingRule NMIPRoutingRule;
+
+NM_AVAILABLE_IN_1_18
+GType nm_ip_routing_rule_get_type (void);
+
+NM_AVAILABLE_IN_1_18
+NMIPRoutingRule *nm_ip_routing_rule_new (int addr_family);
+
+NM_AVAILABLE_IN_1_18
+NMIPRoutingRule *nm_ip_routing_rule_new_clone (const NMIPRoutingRule *rule);
+
+NM_AVAILABLE_IN_1_18
+NMIPRoutingRule *nm_ip_routing_rule_ref (NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+void nm_ip_routing_rule_unref (NMIPRoutingRule *self);
+
+NM_AVAILABLE_IN_1_18
+gboolean nm_ip_routing_rule_is_sealed (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+void nm_ip_routing_rule_seal (NMIPRoutingRule *self);
+
+NM_AVAILABLE_IN_1_18
+int nm_ip_routing_rule_get_addr_family (const NMIPRoutingRule *self);
+
+NM_AVAILABLE_IN_1_18
+gboolean nm_ip_routing_rule_get_invert (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+void nm_ip_routing_rule_set_invert (NMIPRoutingRule *self, gboolean invert);
+
+NM_AVAILABLE_IN_1_18
+gint64 nm_ip_routing_rule_get_priority (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+void nm_ip_routing_rule_set_priority (NMIPRoutingRule *self, gint64 priority);
+
+NM_AVAILABLE_IN_1_18
+guint8 nm_ip_routing_rule_get_tos (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+void nm_ip_routing_rule_set_tos (NMIPRoutingRule *self, guint8 tos);
+
+NM_AVAILABLE_IN_1_18
+guint8 nm_ip_routing_rule_get_ipproto (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+void nm_ip_routing_rule_set_ipproto (NMIPRoutingRule *self, guint8 ipproto);
+
+NM_AVAILABLE_IN_1_18
+guint16 nm_ip_routing_rule_get_source_port_start (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+guint16 nm_ip_routing_rule_get_source_port_end (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+void nm_ip_routing_rule_set_source_port (NMIPRoutingRule *self, guint16 start, guint16 end);
+
+NM_AVAILABLE_IN_1_18
+guint16 nm_ip_routing_rule_get_destination_port_start (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+guint16 nm_ip_routing_rule_get_destination_port_end (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+void nm_ip_routing_rule_set_destination_port (NMIPRoutingRule *self, guint16 start, guint16 end);
+
+NM_AVAILABLE_IN_1_18
+guint32 nm_ip_routing_rule_get_fwmark (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+guint32 nm_ip_routing_rule_get_fwmask (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+void nm_ip_routing_rule_set_fwmark (NMIPRoutingRule *self, guint32 fwmark, guint32 fwmask);
+
+NM_AVAILABLE_IN_1_18
+guint8 nm_ip_routing_rule_get_from_len (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+const char *nm_ip_routing_rule_get_from (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+void nm_ip_routing_rule_set_from (NMIPRoutingRule *self,
+                                  const char *from,
+                                  guint8 len);
+
+NM_AVAILABLE_IN_1_18
+guint8 nm_ip_routing_rule_get_to_len (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+const char *nm_ip_routing_rule_get_to (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+void nm_ip_routing_rule_set_to (NMIPRoutingRule *self,
+                                const char *to,
+                                guint8 len);
+
+NM_AVAILABLE_IN_1_18
+const char *nm_ip_routing_rule_get_iifname (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+void nm_ip_routing_rule_set_iifname (NMIPRoutingRule *self, const char *iifname);
+
+NM_AVAILABLE_IN_1_18
+const char *nm_ip_routing_rule_get_oifname (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+void nm_ip_routing_rule_set_oifname (NMIPRoutingRule *self, const char *oifname);
+
+NM_AVAILABLE_IN_1_18
+guint8 nm_ip_routing_rule_get_action (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+void nm_ip_routing_rule_set_action (NMIPRoutingRule *self, guint8 action);
+
+NM_AVAILABLE_IN_1_18
+guint32 nm_ip_routing_rule_get_table (const NMIPRoutingRule *self);
+NM_AVAILABLE_IN_1_18
+void nm_ip_routing_rule_set_table (NMIPRoutingRule *self, guint32 table);
+
+NM_AVAILABLE_IN_1_18
+int nm_ip_routing_rule_cmp (const NMIPRoutingRule *rule,
+                            const NMIPRoutingRule *other);
+
+NM_AVAILABLE_IN_1_18
+gboolean nm_ip_routing_rule_validate (const NMIPRoutingRule *self,
+                                      GError **error);
+
+/**
+ * NMIPRoutingRuleAsStringFlags:
+ * @NM_IP_ROUTING_RULE_AS_STRING_FLAGS_NONE: no flags selected.
+ * @NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET: whether to allow parsing
+ *   IPv4 addresses.
+ * @NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET6: whether to allow parsing
+ *   IPv6 addresses. If both @NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET and
+ *   @NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET6 are unset, it's the same
+ *   as setting them both.
+ * @NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE: if set, ensure that the
+ *   rule verfies or fail.
+ *
+ * Since: 1.18
+ */
+typedef enum { /*< flags >*/
+	NM_IP_ROUTING_RULE_AS_STRING_FLAGS_NONE     = 0,
+
+	NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET  = 0x1,
+	NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET6 = 0x2,
+	NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE = 0x4,
+} NMIPRoutingRuleAsStringFlags;
+
+NM_AVAILABLE_IN_1_18
+NMIPRoutingRule *nm_ip_routing_rule_from_string (const char *str,
+                                                 NMIPRoutingRuleAsStringFlags to_string_flags,
+                                                 GHashTable *extra_args,
+                                                 GError **error);
+
+NM_AVAILABLE_IN_1_18
+char *nm_ip_routing_rule_to_string (const NMIPRoutingRule *self,
+                                    NMIPRoutingRuleAsStringFlags to_string_flags,
+                                    GHashTable *extra_args,
+                                    GError **error);
+
+/*****************************************************************************/
+
 #define NM_TYPE_SETTING_IP_CONFIG            (nm_setting_ip_config_get_type ())
 #define NM_SETTING_IP_CONFIG(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), NM_TYPE_SETTING_IP_CONFIG, NMSettingIPConfig))
 #define NM_SETTING_IP_CONFIG_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), NM_TYPE_SETTING_IPCONFIG, NMSettingIPConfigClass))
@@ -187,6 +336,9 @@ gboolean     nm_ip_route_attribute_validate  (const char *name,
 #define NM_SETTING_IP_CONFIG_DAD_TIMEOUT        "dad-timeout"
 #define NM_SETTING_IP_CONFIG_DHCP_TIMEOUT       "dhcp-timeout"
 
+/* these are not real GObject properties. */
+#define NM_SETTING_IP_CONFIG_ROUTING_RULES      "routing-rules"
+
 #define NM_SETTING_DNS_OPTION_DEBUG                     "debug"
 #define NM_SETTING_DNS_OPTION_NDOTS                     "ndots"
 #define NM_SETTING_DNS_OPTION_TIMEOUT                   "timeout"
@@ -289,6 +441,20 @@ gint64        nm_setting_ip_config_get_route_metric           (NMSettingIPConfig
 NM_AVAILABLE_IN_1_10
 guint32       nm_setting_ip_config_get_route_table            (NMSettingIPConfig *setting);
 
+NM_AVAILABLE_IN_1_18
+guint         nm_setting_ip_config_get_num_routing_rules      (NMSettingIPConfig *setting);
+NM_AVAILABLE_IN_1_18
+NMIPRoutingRule *nm_setting_ip_config_get_routing_rule        (NMSettingIPConfig *setting,
+                                                               guint              idx);
+NM_AVAILABLE_IN_1_18
+void          nm_setting_ip_config_add_routing_rule           (NMSettingIPConfig *setting,
+                                                               NMIPRoutingRule   *routing_rule);
+NM_AVAILABLE_IN_1_18
+void          nm_setting_ip_config_remove_routing_rule        (NMSettingIPConfig *setting,
+                                                               guint              idx);
+NM_AVAILABLE_IN_1_18
+void          nm_setting_ip_config_clear_routing_rules        (NMSettingIPConfig *setting);
+
 gboolean      nm_setting_ip_config_get_ignore_auto_routes     (NMSettingIPConfig *setting);
 gboolean      nm_setting_ip_config_get_ignore_auto_dns        (NMSettingIPConfig *setting);
 
diff --git a/libnm-core/nm-setting-ip-tunnel.c b/libnm-core/nm-setting-ip-tunnel.c
index 9fdb5481..42c44c1c 100644
--- a/libnm-core/nm-setting-ip-tunnel.c
+++ b/libnm-core/nm-setting-ip-tunnel.c
@@ -60,7 +60,7 @@ typedef struct {
 	char *output_key;
 	guint encapsulation_limit;
 	guint flow_label;
-	guint mtu;
+	guint32 mtu;
 	guint32 flags;
 } NMSettingIPTunnelPrivate;
 
@@ -819,7 +819,7 @@ nm_setting_ip_tunnel_class_init (NMSettingIPTunnelClass *klass)
 	 **/
 	obj_properties[PROP_MTU] =
 	    g_param_spec_uint (NM_SETTING_IP_TUNNEL_MTU, "", "",
-	                       0, G_MAXUINT, 0,
+	                       0, G_MAXUINT32, 0,
 	                       G_PARAM_READWRITE |
 	                       G_PARAM_CONSTRUCT |
 	                       NM_SETTING_PARAM_FUZZY_IGNORE |
diff --git a/libnm-core/nm-setting-macsec.c b/libnm-core/nm-setting-macsec.c
index 040aa17d..19d1989e 100644
--- a/libnm-core/nm-setting-macsec.c
+++ b/libnm-core/nm-setting-macsec.c
@@ -24,7 +24,7 @@
 
 #include <stdlib.h>
 
-#include "nm-utils/nm-secret-utils.h"
+#include "nm-glib-aux/nm-secret-utils.h"
 
 #include "nm-utils.h"
 #include "nm-core-types-internal.h"
diff --git a/libnm-core/nm-setting-private.h b/libnm-core/nm-setting-private.h
index 2af74ec4..b423eb5c 100644
--- a/libnm-core/nm-setting-private.h
+++ b/libnm-core/nm-setting-private.h
@@ -26,6 +26,7 @@
 #endif
 
 #include "nm-setting.h"
+#include "nm-setting-bridge.h"
 #include "nm-connection.h"
 #include "nm-core-enum-types.h"
 
@@ -204,6 +205,9 @@ gboolean _nm_setting_should_compare_secret_property (NMSetting *setting,
                                                      const char *secret_name,
                                                      NMSettingCompareFlags flags);
 
+NMBridgeVlan *_nm_bridge_vlan_dup (const NMBridgeVlan *vlan);
+NMBridgeVlan *_nm_bridge_vlan_dup_and_seal (const NMBridgeVlan *vlan);
+
 /*****************************************************************************/
 
 #endif  /* NM_SETTING_PRIVATE_H */
diff --git a/libnm-core/nm-setting-sriov.c b/libnm-core/nm-setting-sriov.c
index 9c7306e0..b662ca2c 100644
--- a/libnm-core/nm-setting-sriov.c
+++ b/libnm-core/nm-setting-sriov.c
@@ -94,7 +94,7 @@ _vf_vlan_create_hash (void)
 }
 
 /**
- * nm_srio_vf_new:
+ * nm_sriov_vf_new:
  * @index: the VF index
  *
  * Creates a new #NMSriovVF object.
@@ -894,8 +894,10 @@ _nm_setting_sriov_sort_vfs (NMSettingSriov *setting)
 		}
 	}
 
-	if (need_sort)
+	if (need_sort) {
 		g_ptr_array_sort (setting->vfs, vf_index_compare);
+		_notify (setting, PROP_VFS);
+	}
 
 	return need_sort;
 }
diff --git a/libnm-core/nm-setting-tc-config.c b/libnm-core/nm-setting-tc-config.c
index 7425ae73..dc2f4f64 100644
--- a/libnm-core/nm-setting-tc-config.c
+++ b/libnm-core/nm-setting-tc-config.c
@@ -41,6 +41,7 @@ struct NMTCQdisc {
 	char *kind;
 	guint32 handle;
 	guint32 parent;
+	GHashTable *attributes;
 };
 
 /**
@@ -62,7 +63,15 @@ nm_tc_qdisc_new (const char *kind,
 {
 	NMTCQdisc *qdisc;
 
-	if (!kind || !*kind || strchr (kind, ' ') || strchr (kind, '\t')) {
+	if (!kind || !*kind) {
+		g_set_error (error,
+		             NM_CONNECTION_ERROR,
+		             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		             _("kind is missing"));
+		return NULL;
+	}
+
+	if (strchr (kind, ' ') || strchr (kind, '\t')) {
 		g_set_error (error,
 		             NM_CONNECTION_ERROR,
 		             NM_CONNECTION_ERROR_INVALID_PROPERTY,
@@ -122,6 +131,8 @@ nm_tc_qdisc_unref (NMTCQdisc *qdisc)
 	qdisc->refcount--;
 	if (qdisc->refcount == 0) {
 		g_free (qdisc->kind);
+		if (qdisc->attributes)
+			g_hash_table_unref (qdisc->attributes);
 		g_slice_free (NMTCQdisc, qdisc);
 	}
 }
@@ -141,6 +152,11 @@ nm_tc_qdisc_unref (NMTCQdisc *qdisc)
 gboolean
 nm_tc_qdisc_equal (NMTCQdisc *qdisc, NMTCQdisc *other)
 {
+	GHashTableIter iter;
+	const char *key;
+	GVariant *value, *value2;
+	guint n;
+
 	g_return_val_if_fail (qdisc != NULL, FALSE);
 	g_return_val_if_fail (qdisc->refcount > 0, FALSE);
 
@@ -152,19 +168,53 @@ nm_tc_qdisc_equal (NMTCQdisc *qdisc, NMTCQdisc *other)
 	    || g_strcmp0 (qdisc->kind, other->kind) != 0)
 		return FALSE;
 
+	n = qdisc->attributes ? g_hash_table_size (qdisc->attributes) : 0;
+	if (n != (other->attributes ? g_hash_table_size (other->attributes) : 0))
+		return FALSE;
+	if (n) {
+		g_hash_table_iter_init (&iter, qdisc->attributes);
+		while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &value)) {
+			value2 = g_hash_table_lookup (other->attributes, key);
+			if (!value2)
+				return FALSE;
+			if (!g_variant_equal (value, value2))
+				return FALSE;
+		}
+	}
+
 	return TRUE;
 }
 
 static guint
 _nm_tc_qdisc_hash (NMTCQdisc *qdisc)
 {
+	gs_free const char **names = NULL;
+	GVariant *variant;
 	NMHashState h;
+	guint length;
+	guint i;
+
+	names = nm_utils_strdict_get_keys (qdisc->attributes, TRUE, &length);
 
 	nm_hash_init (&h, 43869703);
 	nm_hash_update_vals (&h,
 	                     qdisc->handle,
-	                     qdisc->parent);
+	                     qdisc->parent,
+	                     length);
 	nm_hash_update_str0 (&h, qdisc->kind);
+	for (i = 0; i < length; i++) {
+		const GVariantType *vtype;
+
+		variant = g_hash_table_lookup (qdisc->attributes, names[i]);
+
+		vtype = g_variant_get_type (variant);
+
+		nm_hash_update_str (&h, names[i]);
+		nm_hash_update_str (&h, (const char *) vtype);
+		if (g_variant_type_is_basic (vtype))
+			nm_hash_update_val (&h, g_variant_hash (variant));
+	}
+
 	return nm_hash_complete (&h);
 }
 
@@ -189,6 +239,16 @@ nm_tc_qdisc_dup (NMTCQdisc *qdisc)
 	copy = nm_tc_qdisc_new (qdisc->kind, qdisc->parent, NULL);
 	nm_tc_qdisc_set_handle (copy, qdisc->handle);
 
+	if (qdisc->attributes) {
+		GHashTableIter iter;
+		const char *key;
+		GVariant *value;
+
+		g_hash_table_iter_init (&iter, qdisc->attributes);
+		while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &value))
+			nm_tc_qdisc_set_attribute (copy, key, value);
+	}
+
 	return copy;
 }
 
@@ -261,6 +321,85 @@ nm_tc_qdisc_get_parent (NMTCQdisc *qdisc)
 	return qdisc->parent;
 }
 
+/**
+ * nm_tc_qdisc_get_attribute_names:
+ * @qdisc: the #NMTCQdisc
+ *
+ * Gets an array of attribute names defined on @qdisc.
+ *
+ * Returns: (transfer container): a %NULL-terminated array of attribute names
+ *   or %NULL if no attributes are set.
+ *
+ * Since: 1.18
+ **/
+const char **
+nm_tc_qdisc_get_attribute_names (NMTCQdisc *qdisc)
+{
+	g_return_val_if_fail (qdisc, NULL);
+
+	return nm_utils_strdict_get_keys (qdisc->attributes, TRUE, NULL);
+}
+
+GHashTable *
+_nm_tc_qdisc_get_attributes (NMTCQdisc *qdisc)
+{
+	nm_assert (qdisc);
+
+	return qdisc->attributes;
+}
+
+/**
+ * nm_tc_qdisc_get_attribute:
+ * @qdisc: the #NMTCQdisc
+ * @name: the name of an qdisc attribute
+ *
+ * Gets the value of the attribute with name @name on @qdisc
+ *
+ * Returns: (transfer none): the value of the attribute with name @name on
+ *   @qdisc, or %NULL if @qdisc has no such attribute.
+ *
+ * Since: 1.18
+ **/
+GVariant *
+nm_tc_qdisc_get_attribute (NMTCQdisc *qdisc, const char *name)
+{
+	g_return_val_if_fail (qdisc != NULL, NULL);
+	g_return_val_if_fail (name != NULL && *name != '\0', NULL);
+
+	if (qdisc->attributes)
+		return g_hash_table_lookup (qdisc->attributes, name);
+	else
+		return NULL;
+}
+
+/**
+ * nm_tc_qdisc_set_attribute:
+ * @qdisc: the #NMTCQdisc
+ * @name: the name of an qdisc attribute
+ * @value: (transfer none) (allow-none): the value
+ *
+ * Sets or clears the named attribute on @qdisc to the given value.
+ *
+ * Since: 1.18
+ **/
+void
+nm_tc_qdisc_set_attribute (NMTCQdisc *qdisc, const char *name, GVariant *value)
+{
+	g_return_if_fail (qdisc != NULL);
+	g_return_if_fail (name != NULL && *name != '\0');
+	g_return_if_fail (strcmp (name, "kind") != 0);
+
+	if (!qdisc->attributes) {
+		qdisc->attributes = g_hash_table_new_full (nm_str_hash, g_str_equal,
+		                                           g_free, (GDestroyNotify) g_variant_unref);
+	}
+
+	if (value)
+		g_hash_table_insert (qdisc->attributes, g_strdup (name), g_variant_ref_sink (value));
+	else
+		g_hash_table_remove (qdisc->attributes, name);
+}
+
 /*****************************************************************************/
 
 G_DEFINE_BOXED_TYPE (NMTCAction, nm_tc_action, nm_tc_action_dup, nm_tc_action_unref)
@@ -290,7 +429,15 @@ nm_tc_action_new (const char *kind,
 {
 	NMTCAction *action;
 
-	if (!kind || !*kind || strchr (kind, ' ') || strchr (kind, '\t')) {
+	if (!kind || !*kind) {
+		g_set_error (error,
+		             NM_CONNECTION_ERROR,
+		             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		             _("kind is missing"));
+		return NULL;
+	}
+
+	if (strchr (kind, ' ') || strchr (kind, '\t')) {
 		g_set_error (error,
 		             NM_CONNECTION_ERROR,
 		             NM_CONNECTION_ERROR_INVALID_PROPERTY,
@@ -464,6 +611,14 @@ nm_tc_action_get_attribute_names (NMTCAction *action)
 	return nm_utils_strv_make_deep_copied_nonnull (names);
 }
 
+GHashTable *
+_nm_tc_action_get_attributes (NMTCAction *action)
+{
+	nm_assert (action);
+
+	return action->attributes;
+}
+
 /**
  * nm_tc_action_get_attribute:
  * @action: the #NMTCAction
@@ -544,7 +699,15 @@ nm_tc_tfilter_new (const char *kind,
 {
 	NMTCTfilter *tfilter;
 
-	if (!kind || !*kind || strchr (kind, ' ') || strchr (kind, '\t')) {
+	if (!kind || !*kind) {
+		g_set_error (error,
+		             NM_CONNECTION_ERROR,
+		             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+		             _("kind is missing"));
+		return NULL;
+	}
+
+	if (strchr (kind, ' ') || strchr (kind, '\t')) {
 		g_set_error (error,
 		             NM_CONNECTION_ERROR,
 		             NM_CONNECTION_ERROR_INVALID_PROPERTY,
@@ -1222,7 +1385,10 @@ _qdiscs_to_variant (GPtrArray *qdiscs)
 	if (qdiscs) {
 		for (i = 0; i < qdiscs->len; i++) {
 			NMTCQdisc *qdisc = qdiscs->pdata[i];
+			guint length;
+			gs_free const char **attrs = nm_utils_strdict_get_keys (qdisc->attributes, TRUE, &length);
 			GVariantBuilder qdisc_builder;
+			guint y;
 
 			g_variant_builder_init (&qdisc_builder, G_VARIANT_TYPE_VARDICT);
 
@@ -1235,6 +1401,11 @@ _qdiscs_to_variant (GPtrArray *qdiscs)
 			g_variant_builder_add (&qdisc_builder, "{sv}", "parent",
 			                       g_variant_new_uint32 (nm_tc_qdisc_get_parent (qdisc)));
 
+			for (y = 0; y < length; y++) {
+				g_variant_builder_add (&qdisc_builder, "{sv}", attrs[y],
+				                       g_hash_table_lookup (qdisc->attributes, attrs[y]));
+			}
+
 			g_variant_builder_add (&builder, "a{sv}", &qdisc_builder);
 		}
 	}
@@ -1267,9 +1438,11 @@ _qdiscs_from_variant (GVariant *value)
 
 	while (g_variant_iter_next (&iter, "@a{sv}", &qdisc_var)) {
 		const char *kind;
-		guint32 handle;
 		guint32 parent;
 		NMTCQdisc *qdisc;
+		GVariantIter qdisc_iter;
+		const char *key;
+		GVariant *attr_value;
 
 		if (   !g_variant_lookup (qdisc_var, "kind", "&s", &kind)
 		    || !g_variant_lookup (qdisc_var, "parent", "u", &parent)) {
@@ -1284,8 +1457,18 @@ _qdiscs_from_variant (GVariant *value)
 			goto next;
 		}
 
-		if (g_variant_lookup (qdisc_var, "handle", "u", &handle))
-			nm_tc_qdisc_set_handle (qdisc, handle);
+		g_variant_iter_init (&qdisc_iter, qdisc_var);
+		while (g_variant_iter_next (&qdisc_iter, "{&sv}", &key, &attr_value)) {
+			if (   strcmp (key, "kind") == 0
+			    || strcmp (key, "parent") == 0) {
+				/* Already processed above */
+			} else if (strcmp (key, "handle") == 0) {
+				nm_tc_qdisc_set_handle (qdisc, g_variant_get_uint32 (attr_value));
+			} else {
+				nm_tc_qdisc_set_attribute (qdisc, key, attr_value);
+			}
+			g_variant_unref (attr_value);
+		}
 
 		g_ptr_array_add (qdiscs, qdisc);
 next:
diff --git a/libnm-core/nm-setting-tc-config.h b/libnm-core/nm-setting-tc-config.h
index 6296b1ab..ab99e3cc 100644
--- a/libnm-core/nm-setting-tc-config.h
+++ b/libnm-core/nm-setting-tc-config.h
@@ -59,6 +59,16 @@ void        nm_tc_qdisc_set_handle           (NMTCQdisc *qdisc,
 NM_AVAILABLE_IN_1_12
 guint32     nm_tc_qdisc_get_parent           (NMTCQdisc *qdisc);
 
+NM_AVAILABLE_IN_1_18
+const char **nm_tc_qdisc_get_attribute_names (NMTCQdisc *qdisc);
+NM_AVAILABLE_IN_1_18
+GVariant   *nm_tc_qdisc_get_attribute        (NMTCQdisc *qdisc,
+                                              const char *name);
+NM_AVAILABLE_IN_1_18
+void        nm_tc_qdisc_set_attribute        (NMTCQdisc *qdisc,
+                                              const char *name,
+                                              GVariant *value);
+
 typedef struct NMTCAction NMTCAction;
 
 NM_AVAILABLE_IN_1_12
diff --git a/libnm-core/nm-setting-team-port.c b/libnm-core/nm-setting-team-port.c
index 1db8f22a..b2c57ffa 100644
--- a/libnm-core/nm-setting-team-port.c
+++ b/libnm-core/nm-setting-team-port.c
@@ -396,31 +396,18 @@ compare_property (const NMSettInfoSetting *sett_info,
 {
 	NMSettingTeamPortPrivate *a_priv;
 	NMSettingTeamPortPrivate *b_priv;
-	guint i, j;
 
 	if (nm_streq (sett_info->property_infos[property_idx].name, NM_SETTING_TEAM_PORT_LINK_WATCHERS)) {
 
 		if (NM_FLAGS_HAS (flags, NM_SETTING_COMPARE_FLAG_INFERRABLE))
 			return NM_TERNARY_DEFAULT;
-
-		if (other) {
-			a_priv = NM_SETTING_TEAM_PORT_GET_PRIVATE (setting);
-			b_priv = NM_SETTING_TEAM_PORT_GET_PRIVATE (other);
-
-			if (a_priv->link_watchers->len != b_priv->link_watchers->len)
-				return FALSE;
-			for (i = 0; i < a_priv->link_watchers->len; i++) {
-				for (j = 0; j < b_priv->link_watchers->len; j++) {
-					if (nm_team_link_watcher_equal (a_priv->link_watchers->pdata[i],
-					                                b_priv->link_watchers->pdata[j])) {
-						break;
-					}
-				}
-				if (j == b_priv->link_watchers->len)
-					return FALSE;
-			}
-		}
-		return TRUE;
+		if (!other)
+			return TRUE;
+		a_priv = NM_SETTING_TEAM_PORT_GET_PRIVATE (setting);
+		b_priv = NM_SETTING_TEAM_PORT_GET_PRIVATE (other);
+		return _nm_team_link_watchers_equal (a_priv->link_watchers,
+		                                     b_priv->link_watchers,
+		                                     TRUE);
 	}
 
 	if (nm_streq (sett_info->property_infos[property_idx].name, NM_SETTING_TEAM_PORT_CONFIG)) {
diff --git a/libnm-core/nm-setting-team.c b/libnm-core/nm-setting-team.c
index 42ee3eec..9fd070c1 100644
--- a/libnm-core/nm-setting-team.c
+++ b/libnm-core/nm-setting-team.c
@@ -396,6 +396,34 @@ nm_team_link_watcher_equal (NMTeamLinkWatcher *watcher, NMTeamLinkWatcher *other
 	return TRUE;
 }
 
+gboolean
+_nm_team_link_watchers_equal (GPtrArray *a, GPtrArray *b, gboolean ignore_order)
+{
+	guint i, j;
+
+	if (a->len != b->len)
+		return FALSE;
+	if (ignore_order) {
+		/* FIXME: comparing this way is O(n^2). Don't do that, instead
+		 *        add nm_team_link_watcher_cmp(), sort both lists, and
+		 *        compare step by step. */
+		for (i = 0; i < a->len; i++) {
+			for (j = 0; j < b->len; j++) {
+				if (nm_team_link_watcher_equal (a->pdata[i], b->pdata[j]))
+					break;
+			}
+			if (j == b->len)
+				return FALSE;
+		}
+	} else {
+		for (i = 0; i < a->len; i++) {
+			if (!nm_team_link_watcher_equal (a->pdata[i], b->pdata[i]))
+				return FALSE;
+		}
+	}
+	return TRUE;
+}
+
 /**
  * nm_team_link_watcher_dup:
  * @watcher: the #NMTeamLinkWatcher
@@ -929,13 +957,14 @@ nm_setting_team_remove_runner_tx_hash_by_value (NMSettingTeam *setting,
 
 	g_return_val_if_fail (NM_IS_SETTING_TEAM (setting), FALSE);
 	g_return_val_if_fail (txhash != NULL, FALSE);
-	g_return_val_if_fail (txhash[0] != '\0', FALSE);
 
-	for (i = 0; i < priv->runner_tx_hash->len; i++) {
-		if (nm_streq (txhash, priv->runner_tx_hash->pdata[i])) {
-			g_ptr_array_remove_index (priv->runner_tx_hash, i);
-			_notify (setting, PROP_RUNNER_TX_HASH);
-			return TRUE;
+	if (priv->runner_tx_hash) {
+		for (i = 0; i < priv->runner_tx_hash->len; i++) {
+			if (nm_streq (txhash, priv->runner_tx_hash->pdata[i])) {
+				g_ptr_array_remove_index (priv->runner_tx_hash, i);
+				_notify (setting, PROP_RUNNER_TX_HASH);
+				return TRUE;
+			}
 		}
 	}
 	return FALSE;
@@ -974,6 +1003,7 @@ nm_setting_team_get_runner_tx_hash (NMSettingTeam *setting, guint idx)
 	NMSettingTeamPrivate *priv = NM_SETTING_TEAM_GET_PRIVATE (setting);
 
 	g_return_val_if_fail (NM_IS_SETTING_TEAM (setting), NULL);
+	g_return_val_if_fail (priv->runner_tx_hash, NULL);
 	g_return_val_if_fail (idx < priv->runner_tx_hash->len, NULL);
 
 	return priv->runner_tx_hash->pdata[idx];
@@ -994,6 +1024,7 @@ nm_setting_team_remove_runner_tx_hash (NMSettingTeam *setting, guint idx)
 	NMSettingTeamPrivate *priv = NM_SETTING_TEAM_GET_PRIVATE (setting);
 
 	g_return_if_fail (NM_IS_SETTING_TEAM (setting));
+	g_return_if_fail (priv->runner_tx_hash);
 	g_return_if_fail (idx < priv->runner_tx_hash->len);
 
 	g_ptr_array_remove_index (priv->runner_tx_hash, idx);
@@ -1239,9 +1270,9 @@ verify (NMSetting *setting, NMConnection *connection, GError **error)
 
 		if (!name) {
 			g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_MISSING_SETTING,
-				     _("missing link watcher name"));
+			             _("missing link watcher name"));
 			g_prefix_error (error, "%s.%s: ", nm_setting_get_name (setting),
-					NM_SETTING_TEAM_LINK_WATCHERS);
+			                NM_SETTING_TEAM_LINK_WATCHERS);
 			return FALSE;
 		}
 		if (!NM_IN_STRSET (name,
@@ -1249,9 +1280,9 @@ verify (NMSetting *setting, NMConnection *connection, GError **error)
 		                   NM_TEAM_LINK_WATCHER_ARP_PING,
 		                   NM_TEAM_LINK_WATCHER_NSNA_PING)) {
 			g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_SETTING,
-				     _("unknown link watcher \"%s\""), name);
+			             _("unknown link watcher \"%s\""), name);
 			g_prefix_error (error, "%s.%s: ", nm_setting_get_name (setting),
-					NM_SETTING_TEAM_LINK_WATCHERS);
+			                NM_SETTING_TEAM_LINK_WATCHERS);
 			return FALSE;
 		}
 
@@ -1260,17 +1291,17 @@ verify (NMSetting *setting, NMConnection *connection, GError **error)
 		                  NM_TEAM_LINK_WATCHER_NSNA_PING)
 		    && !nm_team_link_watcher_get_target_host (link_watcher)) {
 			g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_MISSING_SETTING,
-				     _("missing target host"));
+			             _("missing target host"));
 			g_prefix_error (error, "%s.%s: ", nm_setting_get_name (setting),
-					NM_SETTING_TEAM_LINK_WATCHERS);
+			                NM_SETTING_TEAM_LINK_WATCHERS);
 			return FALSE;
 		}
 		if (nm_streq (name, NM_TEAM_LINK_WATCHER_ARP_PING)
 		    && !nm_team_link_watcher_get_source_host (link_watcher)) {
 			g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_MISSING_SETTING,
-				     _("missing source address"));
+			             _("missing source address"));
 			g_prefix_error (error, "%s.%s: ", nm_setting_get_name (setting),
-					NM_SETTING_TEAM_LINK_WATCHERS);
+			                NM_SETTING_TEAM_LINK_WATCHERS);
 			return FALSE;
 		}
 	}
@@ -1288,31 +1319,17 @@ compare_property (const NMSettInfoSetting *sett_info,
                   NMSettingCompareFlags flags)
 {
 	NMSettingTeamPrivate *a_priv, *b_priv;
-	guint i, j;
 
 	if (nm_streq (sett_info->property_infos[property_idx].name, NM_SETTING_TEAM_LINK_WATCHERS)) {
-
 		if (NM_FLAGS_HAS (flags, NM_SETTING_COMPARE_FLAG_INFERRABLE))
 			return NM_TERNARY_DEFAULT;
-
-		if (other) {
-			a_priv = NM_SETTING_TEAM_GET_PRIVATE (setting);
-			b_priv = NM_SETTING_TEAM_GET_PRIVATE (other);
-
-			if (a_priv->link_watchers->len != b_priv->link_watchers->len)
-				return FALSE;
-			for (i = 0; i < a_priv->link_watchers->len; i++) {
-				for (j = 0; j < b_priv->link_watchers->len; j++) {
-					if (nm_team_link_watcher_equal (a_priv->link_watchers->pdata[i],
-					                                b_priv->link_watchers->pdata[j])) {
-						break;
-					}
-				}
-				if (j == b_priv->link_watchers->len)
-					return FALSE;
-			}
-		}
-		return TRUE;
+		if (!other)
+			return TRUE;
+		a_priv = NM_SETTING_TEAM_GET_PRIVATE (setting);
+		b_priv = NM_SETTING_TEAM_GET_PRIVATE (other);
+		return _nm_team_link_watchers_equal (a_priv->link_watchers,
+		                                     b_priv->link_watchers,
+		                                     TRUE);
 	}
 
 	if (nm_streq (sett_info->property_infos[property_idx].name, NM_SETTING_TEAM_CONFIG)) {
@@ -1350,7 +1367,7 @@ _align_team_properties (NMSettingTeam *setting)
 {
 	NMSettingTeamPrivate *priv = NM_SETTING_TEAM_GET_PRIVATE (setting);
 	char **strv;
-	int i;
+	gsize i;
 
 	priv->notify_peers_count =          JSON_TO_VAL (int, PROP_NOTIFY_PEERS_COUNT);
 	priv->notify_peers_interval =       JSON_TO_VAL (int, PROP_NOTIFY_PEERS_INTERVAL);
@@ -1372,16 +1389,24 @@ _align_team_properties (NMSettingTeam *setting)
 	priv->runner_tx_balancer =       JSON_TO_VAL (string, PROP_RUNNER_TX_BALANCER);
 	priv->runner_agg_select_policy = JSON_TO_VAL (string, PROP_RUNNER_AGG_SELECT_POLICY);
 
-	if (priv->runner_tx_hash) {
-		g_ptr_array_unref (priv->runner_tx_hash);
-		priv->runner_tx_hash = NULL;
-	}
 	strv = JSON_TO_VAL (strv, PROP_RUNNER_TX_HASH);
-	if (strv) {
-		for (i = 0; strv[i]; i++)
-			nm_setting_team_add_runner_tx_hash (setting, strv[i]);
-		g_strfreev (strv);
+	if (_nm_utils_strv_cmp_n ((  priv->runner_tx_hash
+	                           ? (const char *const*) priv->runner_tx_hash->pdata
+	                           : NULL),
+	                          (  priv->runner_tx_hash
+	                           ? (gssize) priv->runner_tx_hash->len
+	                           : (gssize) -1),
+	                          NM_CAST_STRV_CC (strv),
+	                          -1) != 0) {
+		nm_clear_pointer (&priv->runner_tx_hash, g_ptr_array_unref);
+		if (strv) {
+			priv->runner_tx_hash = g_ptr_array_new_full (NM_PTRARRAY_LEN (strv), g_free);
+			for (i = 0; strv[i]; i++)
+				g_ptr_array_add (priv->runner_tx_hash, strv[i]);
+			nm_clear_g_free (&strv);
+		}
 	}
+	nm_clear_pointer (&strv, g_strfreev);
 
 	g_ptr_array_unref (priv->link_watchers);
 	priv->link_watchers = JSON_TO_VAL (ptr_array, PROP_LINK_WATCHERS);
@@ -1419,8 +1444,10 @@ get_property (GObject *object, guint prop_id,
 		g_value_set_string (value, nm_setting_team_get_runner_hwaddr_policy (setting));
 		break;
 	case PROP_RUNNER_TX_HASH:
-		g_value_take_boxed (value, priv->runner_tx_hash ?
-		                    _nm_utils_ptrarray_to_strv (priv->runner_tx_hash): NULL);
+		g_value_take_boxed (value,
+		                      priv->runner_tx_hash
+		                    ? _nm_utils_ptrarray_to_strv (priv->runner_tx_hash)
+		                    : NULL);
 		break;
 	case PROP_RUNNER_TX_BALANCER:
 		g_value_set_string (value, nm_setting_team_get_runner_tx_balancer (setting));
@@ -1772,9 +1799,9 @@ nm_setting_team_class_init (NMSettingTeamClass *klass)
 	obj_properties[PROP_RUNNER_TX_HASH] =
 	    g_param_spec_boxed (NM_SETTING_TEAM_RUNNER_TX_HASH, "", "",
 	                        G_TYPE_STRV,
-	                             G_PARAM_READWRITE |
+	                        G_PARAM_READWRITE |
 	                        NM_SETTING_PARAM_INFERRABLE |
-	                             G_PARAM_STATIC_STRINGS);
+	                        G_PARAM_STATIC_STRINGS);
 
 	/**
 	 * NMSettingTeam:runner-tx-balancer:
diff --git a/libnm-core/nm-setting-vlan.c b/libnm-core/nm-setting-vlan.c
index 5b8a49d4..8a220ad8 100644
--- a/libnm-core/nm-setting-vlan.c
+++ b/libnm-core/nm-setting-vlan.c
@@ -25,6 +25,7 @@
 
 #include <stdlib.h>
 
+#include "nm-libnm-core-intern/nm-libnm-core-utils.h"
 #include "nm-utils.h"
 #include "nm-core-types-internal.h"
 #include "nm-setting-connection.h"
@@ -106,48 +107,33 @@ nm_setting_vlan_get_flags (NMSettingVlan *setting)
 	return NM_SETTING_VLAN_GET_PRIVATE (setting)->flags;
 }
 
-static guint32
-get_max_prio (NMVlanPriorityMap map, gboolean from)
+static NMVlanQosMapping *
+priority_map_new (guint32 from, guint32 to)
 {
-	if (map == NM_VLAN_INGRESS_MAP)
-		return from ? MAX_8021P_PRIO : MAX_SKB_PRIO;
-	else if (map == NM_VLAN_EGRESS_MAP)
-		return from ? MAX_SKB_PRIO : MAX_8021P_PRIO;
-	g_assert_not_reached ();
+	NMVlanQosMapping *mapping;
+
+	mapping = g_new (NMVlanQosMapping, 1);
+	*mapping = (NMVlanQosMapping) {
+		.from = from,
+		.to   = to,
+	};
+	return mapping;
 }
 
 static NMVlanQosMapping *
 priority_map_new_from_str (NMVlanPriorityMap map, const char *str)
 {
-	NMVlanQosMapping *p = NULL;
-	char **t = NULL;
-	guint32 len;
-	guint64 from, to;
+	guint32 from, to;
 
-	g_return_val_if_fail (str && str[0], NULL);
-
-	t = g_strsplit (str, ":", 0);
-	len = g_strv_length (t);
-	if (len == 2) {
-		from = g_ascii_strtoull (t[0], NULL, 10);
-		to = g_ascii_strtoull (t[1], NULL, 10);
-
-		if ((from <= get_max_prio (map, TRUE)) && (to <= get_max_prio (map, FALSE))) {
-			G_STATIC_ASSERT (sizeof (*p) == sizeof (p->from) + sizeof (p->to));
-			p = g_malloc (sizeof (NMVlanQosMapping));
-			p->from = from;
-			p->to = to;
-		}
-	}
-
-	g_strfreev (t);
-	return p;
+	if (!nm_utils_vlan_priority_map_parse_str (map, str, FALSE, &from, &to, NULL))
+		return NULL;
+	return priority_map_new (from, to);
 }
 
 static void
 priority_map_free (NMVlanQosMapping *map)
 {
-	g_return_if_fail (map != NULL);
+	nm_assert (map);
 	g_free (map);
 }
 
@@ -158,7 +144,7 @@ get_map (NMSettingVlan *self, NMVlanPriorityMap map)
 		return NM_SETTING_VLAN_GET_PRIVATE (self)->ingress_priority_map;
 	else if (map == NM_VLAN_EGRESS_MAP)
 		return NM_SETTING_VLAN_GET_PRIVATE (self)->egress_priority_map;
-	g_assert_not_reached ();
+	nm_assert_not_reached ();
 	return NULL;
 }
 
@@ -207,7 +193,7 @@ set_map (NMSettingVlan *self, NMVlanPriorityMap map, GSList *list)
 		NM_SETTING_VLAN_GET_PRIVATE (self)->egress_priority_map = list;
 		_notify (self, PROP_EGRESS_PRIORITY_MAP);
 	} else
-		g_assert_not_reached ();
+		nm_assert_not_reached ();
 }
 
 static gboolean
@@ -489,6 +475,36 @@ nm_setting_vlan_remove_priority (NMSettingVlan *setting,
 	set_map (setting, map, g_slist_delete_link (list, item));
 }
 
+static gboolean
+priority_map_remove_by_value (NMSettingVlan *setting,
+                              NMVlanPriorityMap map,
+                              guint32 from,
+                              guint32 to,
+                              gboolean wildcard_to)
+{
+	GSList *list = NULL, *iter = NULL;
+	NMVlanQosMapping *item;
+
+	nm_assert (NM_IS_SETTING_VLAN (setting));
+	nm_assert (NM_IN_SET (map, NM_VLAN_INGRESS_MAP, NM_VLAN_EGRESS_MAP));
+
+	list = get_map (setting, map);
+	for (iter = list; iter; iter = g_slist_next (iter)) {
+		item = iter->data;
+
+		if (item->from != from)
+			continue;
+		if (   !wildcard_to
+		    && item->to != to)
+			continue;
+
+		priority_map_free ((NMVlanQosMapping *) (iter->data));
+		set_map (setting, map, g_slist_delete_link (list, iter));
+		return TRUE;
+	}
+	return FALSE;
+}
+
 /**
  * nm_setting_vlan_remove_priority_by_value:
  * @setting: the #NMSettingVlan
@@ -508,22 +524,10 @@ nm_setting_vlan_remove_priority_by_value (NMSettingVlan *setting,
                                           guint32 from,
                                           guint32 to)
 {
-	GSList *list = NULL, *iter = NULL;
-	NMVlanQosMapping *item;
-
 	g_return_val_if_fail (NM_IS_SETTING_VLAN (setting), FALSE);
 	g_return_val_if_fail (map == NM_VLAN_INGRESS_MAP || map == NM_VLAN_EGRESS_MAP, FALSE);
 
-	list = get_map (setting, map);
-	for (iter = list; iter; iter = g_slist_next (iter)) {
-		item = iter->data;
-		if (item->from == from && item->to == to) {
-			priority_map_free ((NMVlanQosMapping *) (iter->data));
-			set_map (setting, map, g_slist_delete_link (list, iter));
-			return TRUE;
-		}
-	}
-	return FALSE;
+	return priority_map_remove_by_value (setting, map, from, to, FALSE);
 }
 
 /**
@@ -543,19 +547,15 @@ nm_setting_vlan_remove_priority_str_by_value (NMSettingVlan *setting,
                                               NMVlanPriorityMap map,
                                               const char *str)
 {
-	NMVlanQosMapping *item;
-	gboolean found;
+	gboolean is_wildcard_to;
+	guint32 from, to;
 
 	g_return_val_if_fail (NM_IS_SETTING_VLAN (setting), FALSE);
 	g_return_val_if_fail (map == NM_VLAN_INGRESS_MAP || map == NM_VLAN_EGRESS_MAP, FALSE);
 
-	item = priority_map_new_from_str (map, str);
-	if (!item)
+	if (!nm_utils_vlan_priority_map_parse_str (map, str, TRUE, &from, &to, &is_wildcard_to))
 		return FALSE;
-
-	found = nm_setting_vlan_remove_priority_by_value (setting, map, item->from, item->to);
-	g_free (item);
-	return found;
+	return priority_map_remove_by_value (setting, map, from, to, is_wildcard_to);
 }
 
 /**
@@ -703,18 +703,16 @@ static GSList *
 priority_strv_to_maplist (NMVlanPriorityMap map, char **strv)
 {
 	GSList *list = NULL;
-	int i;
+	gsize i;
 
 	for (i = 0; strv && strv[i]; i++) {
-		NMVlanQosMapping *item;
+		guint32 from, to;
 
-		item = priority_map_new_from_str (map, strv[i]);
-		if (item) {
-			if (!check_replace_duplicate_priority (list, item->from, item->to))
-				list = g_slist_prepend (list, item);
-			else
-				g_free (item);
-		}
+		if (!nm_utils_vlan_priority_map_parse_str (map, strv[i], FALSE, &from, &to, NULL))
+			continue;
+		if (check_replace_duplicate_priority (list, from, to))
+			continue;
+		list = g_slist_prepend (list, priority_map_new (from, to));
 	}
 	return g_slist_sort (list, prio_map_compare);
 }
diff --git a/libnm-core/nm-setting-vpn.c b/libnm-core/nm-setting-vpn.c
index 20a0ae14..606b9d7f 100644
--- a/libnm-core/nm-setting-vpn.c
+++ b/libnm-core/nm-setting-vpn.c
@@ -25,7 +25,7 @@
 
 #include <stdlib.h>
 
-#include "nm-utils/nm-secret-utils.h"
+#include "nm-glib-aux/nm-secret-utils.h"
 #include "nm-utils.h"
 #include "nm-utils-private.h"
 #include "nm-setting-private.h"
@@ -173,10 +173,8 @@ nm_setting_vpn_add_data_item (NMSettingVpn *setting,
                               const char *item)
 {
 	g_return_if_fail (NM_IS_SETTING_VPN (setting));
-	g_return_if_fail (key != NULL);
-	g_return_if_fail (strlen (key) > 0);
-	g_return_if_fail (item != NULL);
-	g_return_if_fail (strlen (item) > 0);
+	g_return_if_fail (key && key[0]);
+	g_return_if_fail (item && item[0]);
 
 	g_hash_table_insert (NM_SETTING_VPN_GET_PRIVATE (setting)->data,
 	                     g_strdup (key), g_strdup (item));
@@ -242,6 +240,7 @@ nm_setting_vpn_remove_data_item (NMSettingVpn *setting, const char *key)
 	gboolean found;
 
 	g_return_val_if_fail (NM_IS_SETTING_VPN (setting), FALSE);
+	g_return_val_if_fail (key, FALSE);
 
 	found = g_hash_table_remove (NM_SETTING_VPN_GET_PRIVATE (setting)->data, key);
 	if (found)
@@ -350,10 +349,8 @@ nm_setting_vpn_add_secret (NMSettingVpn *setting,
                            const char *secret)
 {
 	g_return_if_fail (NM_IS_SETTING_VPN (setting));
-	g_return_if_fail (key != NULL);
-	g_return_if_fail (strlen (key) > 0);
-	g_return_if_fail (secret != NULL);
-	g_return_if_fail (strlen (secret) > 0);
+	g_return_if_fail (key && key[0]);
+	g_return_if_fail (secret && secret[0]);
 
 	g_hash_table_insert (NM_SETTING_VPN_GET_PRIVATE (setting)->secrets,
 	                     g_strdup (key), g_strdup (secret));
@@ -419,6 +416,7 @@ nm_setting_vpn_remove_secret (NMSettingVpn *setting, const char *key)
 	gboolean found;
 
 	g_return_val_if_fail (NM_IS_SETTING_VPN (setting), FALSE);
+	g_return_val_if_fail (key, FALSE);
 
 	found = g_hash_table_remove (NM_SETTING_VPN_GET_PRIVATE (setting)->secrets, key);
 	if (found)
diff --git a/libnm-core/nm-setting-wifi-p2p.c b/libnm-core/nm-setting-wifi-p2p.c
index da978720..490c2ef9 100644
--- a/libnm-core/nm-setting-wifi-p2p.c
+++ b/libnm-core/nm-setting-wifi-p2p.c
@@ -24,7 +24,7 @@
 #include <net/ethernet.h>
 
 #include "nm-utils.h"
-#include "nm-common-macros.h"
+#include "nm-libnm-core-intern/nm-common-macros.h"
 #include "nm-utils-private.h"
 #include "nm-setting-private.h"
 
diff --git a/libnm-core/nm-setting-wired.c b/libnm-core/nm-setting-wired.c
index ef23294f..182e7193 100644
--- a/libnm-core/nm-setting-wired.c
+++ b/libnm-core/nm-setting-wired.c
@@ -27,7 +27,7 @@
 #include <net/ethernet.h>
 
 #include "nm-utils.h"
-#include "nm-common-macros.h"
+#include "nm-libnm-core-intern/nm-common-macros.h"
 #include "nm-utils-private.h"
 #include "nm-setting-private.h"
 
@@ -483,8 +483,7 @@ nm_setting_wired_get_s390_option_by_key (NMSettingWired *setting,
                                          const char *key)
 {
 	g_return_val_if_fail (NM_IS_SETTING_WIRED (setting), NULL);
-	g_return_val_if_fail (key != NULL, NULL);
-	g_return_val_if_fail (strlen (key), NULL);
+	g_return_val_if_fail (key && key[0], NULL);
 
 	return g_hash_table_lookup (NM_SETTING_WIRED_GET_PRIVATE (setting)->s390_options, key);
 }
@@ -508,17 +507,11 @@ nm_setting_wired_add_s390_option (NMSettingWired *setting,
                                   const char *key,
                                   const char *value)
 {
-	size_t value_len;
-
 	g_return_val_if_fail (NM_IS_SETTING_WIRED (setting), FALSE);
-	g_return_val_if_fail (key != NULL, FALSE);
-	g_return_val_if_fail (strlen (key), FALSE);
+	g_return_val_if_fail (key && key[0], FALSE);
 	g_return_val_if_fail (g_strv_contains (valid_s390_opts, key), FALSE);
 	g_return_val_if_fail (value != NULL, FALSE);
 
-	value_len = strlen (value);
-	g_return_val_if_fail (value_len > 0 && value_len < 200, FALSE);
-
 	g_hash_table_insert (NM_SETTING_WIRED_GET_PRIVATE (setting)->s390_options,
 	                     g_strdup (key),
 	                     g_strdup (value));
@@ -544,8 +537,7 @@ nm_setting_wired_remove_s390_option (NMSettingWired *setting,
 	gboolean found;
 
 	g_return_val_if_fail (NM_IS_SETTING_WIRED (setting), FALSE);
-	g_return_val_if_fail (key != NULL, FALSE);
-	g_return_val_if_fail (strlen (key), FALSE);
+	g_return_val_if_fail (key && key[0], FALSE);
 
 	found = g_hash_table_remove (NM_SETTING_WIRED_GET_PRIVATE (setting)->s390_options, key);
 	if (found)
@@ -683,7 +675,7 @@ verify (NMSetting *setting, NMConnection *connection, GError **error)
 	g_hash_table_iter_init (&iter, priv->s390_options);
 	while (g_hash_table_iter_next (&iter, (gpointer) &key, (gpointer) &value)) {
 		if (   !g_strv_contains (valid_s390_opts, key)
-		    || !strlen (value)
+		    || value[0] == '\0'
 		    || (strlen (value) > 200)) {
 			g_set_error (error,
 			             NM_CONNECTION_ERROR,
diff --git a/libnm-core/nm-setting-wireguard.c b/libnm-core/nm-setting-wireguard.c
index 8c5b25a5..861b2abd 100644
--- a/libnm-core/nm-setting-wireguard.c
+++ b/libnm-core/nm-setting-wireguard.c
@@ -24,7 +24,7 @@
 #include "nm-setting-private.h"
 #include "nm-utils-private.h"
 #include "nm-connection-private.h"
-#include "nm-utils/nm-secret-utils.h"
+#include "nm-glib-aux/nm-secret-utils.h"
 
 /*****************************************************************************/
 
diff --git a/libnm-core/nm-setting-wireless.c b/libnm-core/nm-setting-wireless.c
index b4cb1056..d8056c6c 100644
--- a/libnm-core/nm-setting-wireless.c
+++ b/libnm-core/nm-setting-wireless.c
@@ -27,7 +27,7 @@
 #include <net/ethernet.h>
 
 #include "nm-utils.h"
-#include "nm-common-macros.h"
+#include "nm-libnm-core-intern/nm-common-macros.h"
 #include "nm-utils-private.h"
 #include "nm-setting-private.h"
 
diff --git a/libnm-core/nm-utils-private.h b/libnm-core/nm-utils-private.h
index 46af26ed..a1a1369a 100644
--- a/libnm-core/nm-utils-private.h
+++ b/libnm-core/nm-utils-private.h
@@ -103,8 +103,31 @@ void        _nm_utils_format_variant_attributes_full (GString *str,
                                                       char key_value_separator);
 gboolean    _nm_sriov_vf_parse_vlans (NMSriovVF *vf, const char *str, GError **error);
 
+GVariant *  _nm_utils_bridge_vlans_to_dbus (NMSetting *setting, const char *property);
+gboolean    _nm_utils_bridge_vlans_from_dbus (NMSetting *setting,
+                                              GVariant *connection_dict,
+                                              const char *property,
+                                              GVariant *value,
+                                              NMSettingParseFlags parse_flags,
+                                              GError **error);
+gboolean    _nm_utils_bridge_vlan_verify_list (GPtrArray *vlans,
+                                               gboolean check_normalizable,
+                                               GError **error,
+                                               const char *setting,
+                                               const char *property);
+
 /* JSON to GValue conversion macros */
 
+static inline void
+_nm_auto_unset_and_free_gvalue (GValue **ptr)
+{
+	if (*ptr) {
+		g_value_unset (*ptr);
+		g_free (*ptr);
+	}
+}
+#define nm_auto_unset_and_free_gvalue nm_auto(_nm_auto_unset_and_free_gvalue)
+
 typedef struct {
 	const char *key1;
 	const char *key2;
@@ -121,16 +144,13 @@ _nm_utils_json_extract_int (char *conf,
                             _NMUtilsTeamPropertyKeys key,
                             gboolean is_port)
 {
-	gs_free GValue *t_value = NULL;
-	int ret;
+	nm_auto_unset_and_free_gvalue GValue *t_value = NULL;
 
 	t_value = _nm_utils_team_config_get (conf, key.key1, key.key2, key.key3, is_port);
-	if (!t_value)
+	if (   !t_value
+	    || !G_VALUE_HOLDS_INT (t_value))
 		return key.default_int;
-
-	ret = g_value_get_int (t_value);
-	g_value_unset (t_value);
-	return ret;
+	return g_value_get_int (t_value);
 }
 
 static inline gboolean
@@ -138,16 +158,13 @@ _nm_utils_json_extract_boolean (char *conf,
                                 _NMUtilsTeamPropertyKeys key,
                                 gboolean is_port)
 {
-	gs_free GValue *t_value = NULL;
-	gboolean ret;
+	nm_auto_unset_and_free_gvalue GValue *t_value = NULL;
 
 	t_value = _nm_utils_team_config_get (conf, key.key1, key.key2, key.key3, is_port);
-	if (!t_value)
+	if (   !t_value
+	    || !G_VALUE_HOLDS_BOOLEAN (t_value))
 		return key.default_bool;
-
-	ret = g_value_get_boolean (t_value);
-	g_value_unset (t_value);
-	return ret;
+	return g_value_get_boolean (t_value);
 }
 
 static inline char *
@@ -155,16 +172,13 @@ _nm_utils_json_extract_string (char *conf,
                                _NMUtilsTeamPropertyKeys key,
                                gboolean is_port)
 {
-	gs_free GValue *t_value = NULL;
-	char *ret;
+	nm_auto_unset_and_free_gvalue GValue *t_value = NULL;
 
 	t_value = _nm_utils_team_config_get (conf, key.key1, key.key2, key.key3, is_port);
-	if (!t_value)
+	if (   !t_value
+	    || !G_VALUE_HOLDS_STRING (t_value))
 		return g_strdup (key.default_str);
-
-	ret = g_value_dup_string (t_value);
-	g_value_unset (t_value);
-	return ret;
+	return g_value_dup_string (t_value);
 }
 
 static inline char **
@@ -172,39 +186,37 @@ _nm_utils_json_extract_strv (char *conf,
                              _NMUtilsTeamPropertyKeys key,
                              gboolean is_port)
 {
-	gs_free GValue *t_value = NULL;
-	char **ret;
+	nm_auto_unset_and_free_gvalue GValue *t_value = NULL;
 
 	t_value = _nm_utils_team_config_get (conf, key.key1, key.key2, key.key3, is_port);
-	if (!t_value)
+	if (   !t_value
+	    || !G_TYPE_CHECK_VALUE_TYPE (t_value, G_TYPE_STRV))
 		return NULL;
-
-	ret = g_strdupv (g_value_get_boxed (t_value));
-	g_value_unset (t_value);
-	return ret;
+	return    g_strdupv (g_value_get_boxed (t_value))
+	       ?: g_new0 (char *, 1);
 }
 
 static inline GPtrArray *
 _nm_utils_json_extract_ptr_array (char *conf,
-                             _NMUtilsTeamPropertyKeys key,
-                             gboolean is_port)
+                                  _NMUtilsTeamPropertyKeys key,
+                                  gboolean is_port)
 {
-	gs_free GValue *t_value = NULL;
+	nm_auto_unset_and_free_gvalue GValue *t_value = NULL;
 	GPtrArray *data, *ret;
 	guint i;
 
 	ret = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_team_link_watcher_unref);
+
 	t_value = _nm_utils_team_config_get (conf, key.key1, key.key2, key.key3, is_port);
-	if (!t_value)
+	if (   !t_value
+	    || !G_TYPE_CHECK_VALUE_TYPE (t_value, G_TYPE_PTR_ARRAY))
 		return ret;
 
 	data = g_value_get_boxed (t_value);
 	if (!data)
 		return ret;
-
 	for (i = 0; i < data->len; i++)
 		g_ptr_array_add (ret, nm_team_link_watcher_dup (data->pdata[i]));
-	g_value_unset (t_value);
 	return ret;
 }
 
diff --git a/libnm-core/nm-utils.c b/libnm-core/nm-utils.c
index d276cfe6..04d5b1b5 100644
--- a/libnm-core/nm-utils.c
+++ b/libnm-core/nm-utils.c
@@ -37,15 +37,16 @@
 #include "nm-json.h"
 #endif
 
-#include "nm-utils/nm-enum-utils.h"
-#include "nm-utils/nm-secret-utils.h"
+#include "nm-glib-aux/nm-enum-utils.h"
+#include "nm-glib-aux/nm-secret-utils.h"
 #include "systemd/nm-sd-utils-shared.h"
-#include "nm-common-macros.h"
+#include "nm-libnm-core-intern/nm-common-macros.h"
 #include "nm-utils-private.h"
 #include "nm-setting-private.h"
 #include "nm-crypto.h"
 #include "nm-setting-bond.h"
 #include "nm-setting-bridge.h"
+#include "nm-setting-bridge-port.h"
 #include "nm-setting-infiniband.h"
 #include "nm-setting-ip6-config.h"
 #include "nm-setting-team.h"
@@ -1034,30 +1035,6 @@ _nm_utils_ptrarray_to_strv (GPtrArray *ptrarray)
 	return strv;
 }
 
-/**
- * _nm_utils_strv_equal:
- * @strv1: a string array
- * @strv2: a string array
- *
- * Compare NULL-terminated string arrays for equality.
- *
- * Returns: %TRUE if the arrays are equal, %FALSE otherwise.
- **/
-gboolean
-_nm_utils_strv_equal (char **strv1, char **strv2)
-{
-	if (strv1 == strv2)
-		return TRUE;
-
-	if (!strv1 || !strv2)
-		return FALSE;
-
-	for ( ; *strv1 && *strv2 && !strcmp (*strv1, *strv2); strv1++, strv2++)
-		;
-
-	return !*strv1 && !*strv2;
-}
-
 static gboolean
 device_supports_ap_ciphers (guint32 dev_caps,
                             guint32 ap_flags,
@@ -2332,6 +2309,15 @@ static const NMVariantAttributeSpec * const tc_object_attribute_spec[] = {
 	NULL,
 };
 
+typedef struct {
+	const char *kind;
+	const NMVariantAttributeSpec * const *attrs;
+} NMQdiscAttributeSpec;
+
+static const NMQdiscAttributeSpec *const tc_qdisc_attribute_spec[] = {
+	NULL,
+};
+
 /*****************************************************************************/
 
 /**
@@ -2348,6 +2334,7 @@ _nm_utils_string_append_tc_qdisc_rest (GString *string, NMTCQdisc *qdisc)
 {
 	guint32 handle = nm_tc_qdisc_get_handle (qdisc);
 	const char *kind = nm_tc_qdisc_get_kind (qdisc);
+	gs_free char *str = NULL;
 
 	if (handle != TC_H_UNSPEC && strcmp (kind, "ingress") != 0) {
 		g_string_append (string, "handle ");
@@ -2356,6 +2343,13 @@ _nm_utils_string_append_tc_qdisc_rest (GString *string, NMTCQdisc *qdisc)
 	}
 
 	g_string_append (string, kind);
+
+	str = nm_utils_format_variant_attributes (_nm_tc_qdisc_get_attributes (qdisc),
+	                                          ' ', ' ');
+	if (str) {
+		g_string_append_c (string, ' ');
+		g_string_append (string, str);
+	}
 }
 
 /**
@@ -2463,7 +2457,7 @@ _tc_read_common_opts (const char *str,
  * @str: the string representation of a qdisc
  * @error: location of the error
  *
- * Parces the tc style string qdisc representation of the queueing
+ * Parses the tc style string qdisc representation of the queueing
  * discipline to a %NMTCQdisc instance. Supports a subset of the tc language.
  *
  * Returns: the %NMTCQdisc or %NULL
@@ -2478,21 +2472,36 @@ nm_utils_tc_qdisc_from_str (const char *str, GError **error)
 	gs_free char *kind = NULL;
 	gs_free char *rest = NULL;
 	NMTCQdisc *qdisc = NULL;
-	gs_unref_hashtable GHashTable *ht = NULL;
+	gs_unref_hashtable GHashTable *options = NULL;
+	GHashTableIter iter;
+	gpointer key, value;
+	guint i;
 
 	nm_assert (str);
 	nm_assert (!error || !*error);
 
-	ht = nm_utils_parse_variant_attributes (str,
-	                                        ' ', ' ', FALSE,
-	                                        tc_object_attribute_spec,
-	                                        error);
-	if (!ht)
-		return NULL;
-
 	if (!_tc_read_common_opts (str, &handle, &parent, &kind, &rest, error))
 		return NULL;
 
+	for (i = 0; rest && tc_qdisc_attribute_spec[i]; i++) {
+		if (strcmp (tc_qdisc_attribute_spec[i]->kind, kind) == 0) {
+			options = nm_utils_parse_variant_attributes (rest,
+			                                             ' ', ' ', FALSE,
+			                                             tc_qdisc_attribute_spec[i]->attrs,
+			                                             error);
+			if (!options)
+				return NULL;
+			break;
+		}
+	}
+	nm_clear_pointer (&rest, g_free);
+
+	if (options) {
+		value = g_hash_table_lookup (options, "");
+		if (value)
+			rest = g_variant_dup_string (value, NULL);
+	}
+
 	if (rest) {
 		g_set_error (error, 1, 0, _("unsupported qdisc option: '%s'."), rest);
 		return NULL;
@@ -2504,8 +2513,15 @@ nm_utils_tc_qdisc_from_str (const char *str, GError **error)
 
 	nm_tc_qdisc_set_handle (qdisc, handle);
 
+	if (options) {
+		g_hash_table_iter_init (&iter, options);
+		while (g_hash_table_iter_next (&iter, &key, &value))
+			nm_tc_qdisc_set_attribute (qdisc, key, g_variant_ref_sink (value));
+	}
+
 	return qdisc;
 }
+
 /*****************************************************************************/
 
 static const NMVariantAttributeSpec * const tc_action_simple_attribute_spec[] = {
@@ -2522,24 +2538,14 @@ static const NMVariantAttributeSpec * const tc_action_attribute_spec[] = {
 static gboolean
 _string_append_tc_action (GString *string, NMTCAction *action, GError **error)
 {
-	gs_unref_hashtable GHashTable *ht = NULL;
 	const char *kind = nm_tc_action_get_kind (action);
-	gs_strfreev char **attr_names = NULL;
 	gs_free char *str = NULL;
-	int i;
-
-	ht = g_hash_table_new_full (nm_str_hash, g_str_equal, NULL, NULL);
 
 	g_string_append (string, kind);
 
-	attr_names = nm_tc_action_get_attribute_names (action);
-	for (i = 0; attr_names[i]; i++) {
-		g_hash_table_insert (ht, attr_names[i],
-		                     nm_tc_action_get_attribute (action, attr_names[i]));
-	}
-
-	if (i) {
-		str = nm_utils_format_variant_attributes (ht, ' ', ' ');
+	str = nm_utils_format_variant_attributes (_nm_tc_action_get_attributes (action),
+	                                          ' ', ' ');
+	if (str) {
 		g_string_append_c (string, ' ');
 		g_string_append (string, str);
 	}
@@ -2578,7 +2584,7 @@ nm_utils_tc_action_to_str (NMTCAction *action, GError **error)
  * @str: the string representation of a action
  * @error: location of the error
  *
- * Parces the tc style string action representation of the queueing
+ * Parses the tc style string action representation of the queueing
  * discipline to a %NMTCAction instance. Supports a subset of the tc language.
  *
  * Returns: the %NMTCAction or %NULL
@@ -2731,7 +2737,7 @@ static const NMVariantAttributeSpec * const tc_tfilter_attribute_spec[] = {
  * @str: the string representation of a tfilter
  * @error: location of the error
  *
- * Parces the tc style string tfilter representation of the queueing
+ * Parses the tc style string tfilter representation of the queueing
  * discipline to a %NMTCTfilter instance. Supports a subset of the tc language.
  *
  * Returns: the %NMTCTfilter or %NULL
@@ -2872,7 +2878,7 @@ _nm_sriov_vf_parse_vlans (NMSriovVF *vf, const char *str, GError **error)
 	gs_free const char **vlans = NULL;
 	guint i;
 
-	vlans = nm_utils_strsplit_set (str, ";", FALSE);
+	vlans = nm_utils_strsplit_set (str, ";");
 	if (!vlans) {
 		g_set_error_literal (error,
 		                     NM_CONNECTION_ERROR,
@@ -2986,8 +2992,7 @@ nm_utils_sriov_vf_from_str (const char *str, GError **error)
 
 	detail = strchr (str, ' ');
 	if (detail) {
-		index_free = g_strndup (str, detail - str);
-		str = index_free;
+		str = nm_strndup_a (200, str, detail - str, &index_free);
 		detail++;
 	}
 
@@ -4565,6 +4570,29 @@ _nm_utils_generate_mac_address_mask_parse (const char *value,
 
 /*****************************************************************************/
 
+gboolean
+nm_utils_is_valid_iface_name_utf8safe (const char *utf8safe_name)
+{
+	gs_free gpointer bin_to_free = NULL;
+	gconstpointer bin;
+	gsize len;
+
+	g_return_val_if_fail (utf8safe_name, FALSE);
+
+	bin = nm_utils_buf_utf8safe_unescape (utf8safe_name, &len, &bin_to_free);
+
+	if (bin_to_free) {
+		/* some unescaping happened... */
+
+		if (len != strlen (bin)) {
+			/* there are embedded NUL chars. Invalid. */
+			return FALSE;
+		}
+	}
+
+	return nm_utils_is_valid_iface_name (bin, NULL);
+}
+
 /**
  * nm_utils_is_valid_iface_name:
  * @name: Name of interface
@@ -5430,11 +5458,11 @@ _json_team_add_defaults (json_t *json,
 
 	if (nm_streq (runner, NM_SETTING_TEAM_RUNNER_ACTIVEBACKUP)) {
 		_json_add_object (json, "notify_peers", "count", NULL,
-				  json_integer (NM_SETTING_TEAM_NOTIFY_PEERS_COUNT_ACTIVEBACKUP_DEFAULT));
+		                  json_integer (NM_SETTING_TEAM_NOTIFY_PEERS_COUNT_ACTIVEBACKUP_DEFAULT));
 		_json_add_object (json, "mcast_rejoin", "count", NULL,
-				  json_integer (NM_SETTING_TEAM_NOTIFY_MCAST_COUNT_ACTIVEBACKUP_DEFAULT));
+		                  json_integer (NM_SETTING_TEAM_NOTIFY_MCAST_COUNT_ACTIVEBACKUP_DEFAULT));
 	} else if (   nm_streq (runner, NM_SETTING_TEAM_RUNNER_LOADBALANCE)
-		   || nm_streq (runner, NM_SETTING_TEAM_RUNNER_LACP)) {
+	           || nm_streq (runner, NM_SETTING_TEAM_RUNNER_LACP)) {
 		json_element = json_array ();
 		json_array_append_new (json_element, json_string ("eth"));
 		json_array_append_new (json_element, json_string ("ipv4"));
@@ -5538,6 +5566,7 @@ _json_team_normalize_defaults (json_t *json, gboolean reset)
 {
 	json_t *json_element;
 	const char *runner = NM_SETTING_TEAM_RUNNER_DEFAULT;
+	gs_free char *runner_free = NULL;
 	int notify_peers_count = 0, notify_peers_interval = 0;
 	int mcast_rejoin_count = 0, mcast_rejoin_interval = 0;
 	int runner_tx_balancer_interval = -1;
@@ -5546,7 +5575,8 @@ _json_team_normalize_defaults (json_t *json, gboolean reset)
 
 	json_element = _json_find_object (json, "runner", "name", NULL);
 	if (json_element) {
-		runner = json_string_value (json_element);
+		runner_free = g_strdup (json_string_value (json_element));
+		runner = runner_free;
 		_json_delete_object_on_string_match (json, "runner", "name", NULL,
 		                                     NM_SETTING_TEAM_RUNNER_DEFAULT);
 	}
@@ -5922,11 +5952,9 @@ _nm_utils_team_config_get (const char *conf,
 				if (json_is_string (str_element))
 					g_ptr_array_add (data, g_strdup (json_string_value (str_element)));
 			}
-			if (data->len) {
-				g_value_init (value, G_TYPE_STRV);
-				g_value_take_boxed (value, _nm_utils_ptrarray_to_strv (data));
-			}
-			g_ptr_array_free (data, TRUE);
+			g_ptr_array_add (data, NULL);
+			g_value_init (value, G_TYPE_STRV);
+			g_value_take_boxed (value, g_ptr_array_free (data, FALSE));
 		} else {
 			g_assert_not_reached ();
 			g_free (value);
@@ -5948,14 +5976,13 @@ _nm_utils_team_config_set (char **conf,
                            const char *key3,
                            const GValue *value)
 {
-	json_t *json, *json_element, *json_link, *json_value = NULL;
+	nm_auto_decref_json json_t *json = NULL;
+	nm_auto_decref_json json_t *json_value = NULL;
+	json_t *json_element;
+	json_t *json_link;
 	json_error_t jerror;
-	gboolean updated = FALSE;
-	char **strv;
-	GPtrArray *array;
 	const char *iter_key = key;
-	int i;
-	NMTeamLinkWatcher *watcher;
+	gs_free char *conf_new = NULL;
 
 	g_return_val_if_fail (key, FALSE);
 
@@ -5966,14 +5993,12 @@ _nm_utils_team_config_set (char **conf,
 	if (!json)
 		return FALSE;
 
-	/* no new value? delete element */
 	if (!value) {
-		updated = _json_del_object (json, key, key2, key3);
+		if (!_json_del_object (json, key, key2, key3))
+			return FALSE;
 		goto done;
 	}
 
-	/* insert new value */
-	updated = TRUE;
 	if (G_VALUE_HOLDS_STRING (value))
 		json_value = json_string (g_value_get_string (value));
 	else if (G_VALUE_HOLDS_INT (value))
@@ -5982,53 +6007,56 @@ _nm_utils_team_config_set (char **conf,
 		json_value = json_boolean (g_value_get_boolean (value));
 	else if (G_VALUE_HOLDS_BOXED (value)) {
 		if (nm_streq (key, "link_watch")) {
+			gboolean has_array = FALSE;
+			GPtrArray *array;
+			guint i;
+
 			array = g_value_get_boxed (value);
-			if (!array || !array->len) {
-				updated = FALSE;
-				goto done;
-			}
+			if (!array || !array->len)
+				return FALSE;
 
-			/*
-			 * json_value:   will hold the final link_watcher json (array) object
-			 * json_element: is the next link_watcher to append to json_value
-			 * json_link:    used to transit the json_value from a single link_watcher
-			 *               object to an array of link watcher objects
-			 */
-			json_value = NULL;
 			for (i = 0; i < array->len; i++) {
-				watcher = array->pdata[i];
-				json_element = _nm_utils_team_link_watcher_to_json (watcher);
-				if (!json_element)
+				json_t *el;
+
+				el = _nm_utils_team_link_watcher_to_json (array->pdata[i]);
+				if (!el)
 					continue;
+				/* if there is only one watcher, it is added as-is. If there
+				 * are multiple watchers, they are added in an array. */
 				if (!json_value) {
-					json_value = json_element;
+					json_value = el;
 					continue;
 				}
-				if (!json_is_array (json_value)) {
-					json_link = json_value;
-					json_value = json_array ();
-					json_array_append_new (json_value, json_link);
+				if (!has_array) {
+					json_t *el_arr;
+
+					has_array = TRUE;
+					el_arr = json_array();
+					json_array_append_new (el_arr, json_value);
+					json_value = el_arr;
 				}
-				json_array_append_new (json_value, json_element);
+				json_array_append_new (json_value, el);
 			}
 		} else if (   nm_streq (key, "runner")
 		           && nm_streq0 (key2, "tx_hash")) {
+			const char *const*strv;
+			gsize i;
+
 			strv = g_value_get_boxed (value);
-			if (!strv) {
-				updated = FALSE;
-				goto done;
-			}
+			if (!strv)
+				return FALSE;
+
 			json_value = json_array ();
 			for (i = 0; strv[i]; i++)
 				json_array_append_new (json_value, json_string (strv[i]));
 		} else {
-			updated = FALSE;
-			goto done;
+			nm_assert_not_reached ();
+			return FALSE;
 		}
+
 	} else {  /* G_VALUE_HOLDS_? */
-		g_assert_not_reached ();
-		updated = FALSE;
-		goto done;
+		nm_assert_not_reached ();
+		return FALSE;
 	}
 
 	/* Simplest case: first level key only */
@@ -6054,22 +6082,19 @@ _nm_utils_team_config_set (char **conf,
 		iter_key = key3;
 	}
 
-	json_object_set_new (json_element, iter_key, json_value);
+	json_object_set_new (json_element, iter_key, g_steal_pointer (&json_value));
 
 done:
-	if (updated) {
-		_json_team_normalize_defaults (json, (   nm_streq0 (key, "runner")
-		                                      && nm_streq0 (key2, "name")));
-		g_free (*conf);
-		*conf = json_dumps (json, JSON_PRESERVE_ORDER);
-		/* Don't save an empty config */
-		if (nm_streq0 (*conf, "{}")) {
-			g_free (*conf);
-			*conf = NULL;
-		}
-	}
-	json_decref (json);
-	return updated;
+	_json_team_normalize_defaults (json, (   nm_streq0 (key, "runner")
+	                                      && nm_streq0 (key2, "name")));
+	conf_new = json_dumps (json, JSON_PRESERVE_ORDER);
+	if (nm_streq0 (conf_new, "{}"))
+		nm_clear_g_free (&conf_new);
+	if (nm_streq0 (conf_new, *conf))
+		return FALSE;
+	g_free (*conf);
+	*conf = g_steal_pointer (&conf_new);
+	return TRUE;
 }
 
 #else /* !WITH_JSON_VALIDATION */
@@ -6701,7 +6726,7 @@ nm_utils_base64secret_decode (const char *base64_key,
 
 	base64_key_len = strlen (base64_key);
 
-	r = nm_sd_utils_unbase64mem (base64_key, base64_key_len, &bin_arr, &bin_len);
+	r = nm_sd_utils_unbase64mem (base64_key, base64_key_len, TRUE, &bin_arr, &bin_len);
 	if (r < 0)
 		return FALSE;
 	if (bin_len != required_key_len) {
@@ -6740,3 +6765,166 @@ nm_utils_base64secret_normalize (const char *base64_key,
 	nm_explicit_bzero (buf, required_key_len);
 	return TRUE;
 }
+
+GVariant *
+_nm_utils_bridge_vlans_to_dbus (NMSetting *setting, const char *property)
+{
+	gs_unref_ptrarray GPtrArray *vlans = NULL;
+	GVariantBuilder builder;
+	guint i;
+
+	g_object_get (setting, property, &vlans, NULL);
+	g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}"));
+
+	if (vlans) {
+		for (i = 0; i < vlans->len; i++) {
+			NMBridgeVlan *vlan = vlans->pdata[i];
+			GVariantBuilder vlan_builder;
+			guint16 vid_start, vid_end;
+
+			nm_bridge_vlan_get_vid_range (vlan, &vid_start, &vid_end);
+
+			g_variant_builder_init (&vlan_builder, G_VARIANT_TYPE_VARDICT);
+			g_variant_builder_add (&vlan_builder, "{sv}", "vid-start",
+			                       g_variant_new_uint16 (vid_start));
+			g_variant_builder_add (&vlan_builder, "{sv}", "vid-end",
+			                       g_variant_new_uint16 (vid_end));
+			g_variant_builder_add (&vlan_builder, "{sv}", "pvid",
+			                       g_variant_new_boolean (nm_bridge_vlan_is_pvid (vlan)));
+			g_variant_builder_add (&vlan_builder, "{sv}", "untagged",
+			                       g_variant_new_boolean (nm_bridge_vlan_is_untagged (vlan)));
+			g_variant_builder_add (&builder, "a{sv}", &vlan_builder);
+		}
+	}
+
+	return g_variant_builder_end (&builder);
+}
+
+gboolean
+_nm_utils_bridge_vlans_from_dbus (NMSetting *setting,
+                                  GVariant *connection_dict,
+                                  const char *property,
+                                  GVariant *value,
+                                  NMSettingParseFlags parse_flags,
+                                  GError **error)
+{
+	gs_unref_ptrarray GPtrArray *vlans = NULL;
+	GVariantIter vlan_iter;
+	GVariant *vlan_var;
+
+	g_return_val_if_fail (g_variant_is_of_type (value, G_VARIANT_TYPE ("aa{sv}")), FALSE);
+
+	vlans = g_ptr_array_new_with_free_func ((GDestroyNotify) nm_bridge_vlan_unref);
+	g_variant_iter_init (&vlan_iter, value);
+	while (g_variant_iter_next (&vlan_iter, "@a{sv}", &vlan_var)) {
+		_nm_unused gs_unref_variant GVariant *var_unref = vlan_var;
+		NMBridgeVlan *vlan;
+		guint16 vid_start, vid_end;
+		gboolean pvid = FALSE, untagged = FALSE;
+
+		if (!g_variant_lookup (vlan_var, "vid-start", "q", &vid_start))
+			continue;
+		if (   vid_start < NM_BRIDGE_VLAN_VID_MIN
+		    || vid_start > NM_BRIDGE_VLAN_VID_MAX)
+			continue;
+
+		if (!g_variant_lookup (vlan_var, "vid-end", "q", &vid_end))
+			continue;
+		if (   vid_end < NM_BRIDGE_VLAN_VID_MIN
+		    || vid_end > NM_BRIDGE_VLAN_VID_MAX)
+			continue;
+		if (vid_start > vid_end)
+			continue;
+
+		g_variant_lookup (vlan_var, "pvid", "b", &pvid);
+		if (pvid && vid_start != vid_end)
+			continue;
+		g_variant_lookup (vlan_var, "untagged", "b", &untagged);
+
+		vlan = nm_bridge_vlan_new (vid_start, vid_end);
+		nm_bridge_vlan_set_untagged (vlan, untagged);
+		nm_bridge_vlan_set_pvid (vlan, pvid);
+		g_ptr_array_add (vlans, vlan);
+	}
+
+	g_object_set (setting, property, vlans, NULL);
+
+	return TRUE;
+}
+
+gboolean
+_nm_utils_bridge_vlan_verify_list (GPtrArray *vlans,
+                                   gboolean check_normalizable,
+                                   GError **error,
+                                   const char *setting,
+                                   const char *property)
+{
+	guint i;
+	gs_unref_hashtable GHashTable *h = NULL;
+	gboolean pvid_found = FALSE;
+
+	if (   !vlans
+	    || vlans->len <= 1)
+		return TRUE;
+
+	if (check_normalizable) {
+		guint16 vid_prev_end, vid_start, vid_end;
+
+		nm_assert (_nm_utils_bridge_vlan_verify_list (vlans, FALSE, NULL, setting, property));
+
+		nm_bridge_vlan_get_vid_range (vlans->pdata[0], NULL, &vid_prev_end);
+		for (i = 1; i < vlans->len; i++) {
+			const NMBridgeVlan *vlan = vlans->pdata[i];
+
+			nm_bridge_vlan_get_vid_range (vlan, &vid_start, &vid_end);
+
+			if (vid_prev_end > vid_start) {
+				g_set_error (error,
+				             NM_CONNECTION_ERROR,
+				             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+				             _("Bridge VLANs %d and %d are not sorted by ascending vid"),
+				             vid_prev_end,
+				             vid_start);
+				g_prefix_error (error, "%s.%s: ", setting, property);
+				return FALSE;
+			}
+
+			vid_prev_end = vid_end;
+		}
+		return TRUE;
+	}
+
+	h = g_hash_table_new (nm_direct_hash, NULL);
+	for (i = 0; i < vlans->len; i++) {
+		NMBridgeVlan *vlan = vlans->pdata[i];
+		guint16 v, vid_start, vid_end;
+
+		nm_bridge_vlan_get_vid_range (vlan, &vid_start, &vid_end);
+
+		for (v = vid_start; v <= vid_end; v++) {
+			if (!nm_g_hash_table_add (h, GUINT_TO_POINTER (v))) {
+				g_set_error (error,
+				             NM_CONNECTION_ERROR,
+				             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+				             _("duplicate bridge VLAN vid %u"), v);
+				g_prefix_error (error, "%s.%s: ", setting, property);
+				return FALSE;
+			}
+		}
+
+		if (nm_bridge_vlan_is_pvid (vlan)) {
+			if (   vid_start != vid_end
+			    || pvid_found) {
+				g_set_error_literal (error,
+				                     NM_CONNECTION_ERROR,
+				                     NM_CONNECTION_ERROR_INVALID_PROPERTY,
+				                     _("only one VLAN can be the PVID"));
+				g_prefix_error (error, "%s.%s: ", setting, property);
+				return FALSE;
+			}
+			pvid_found = TRUE;
+		}
+	}
+
+	return TRUE;
+}
diff --git a/libnm-core/nm-version.h b/libnm-core/nm-version.h
index 7fe2e45b..0ac2955b 100644
--- a/libnm-core/nm-version.h
+++ b/libnm-core/nm-version.h
@@ -188,4 +188,18 @@
 # define NM_AVAILABLE_IN_1_16
 #endif
 
+#if NM_VERSION_MIN_REQUIRED >= NM_VERSION_1_18
+# define NM_DEPRECATED_IN_1_18           G_DEPRECATED
+# define NM_DEPRECATED_IN_1_18_FOR(f)    G_DEPRECATED_FOR(f)
+#else
+# define NM_DEPRECATED_IN_1_18
+# define NM_DEPRECATED_IN_1_18_FOR(f)
+#endif
+
+#if NM_VERSION_MAX_ALLOWED < NM_VERSION_1_18
+# define NM_AVAILABLE_IN_1_18            G_UNAVAILABLE(1,18)
+#else
+# define NM_AVAILABLE_IN_1_18
+#endif
+
 #endif  /* NM_VERSION_H */
diff --git a/libnm-core/tests/meson.build b/libnm-core/tests/meson.build
index 7d772c80..1afb9c4b 100644
--- a/libnm-core/tests/meson.build
+++ b/libnm-core/tests/meson.build
@@ -34,7 +34,7 @@ foreach test_unit: test_units
     'libnm-core-' + test_unit,
     [test_unit + '.c'] + enum,
     dependencies: [
-      nm_core_dep,
+      libnm_core_dep,
       libnm_systemd_shared_no_logging_dep,
     ],
     c_args: [
diff --git a/libnm-core/tests/test-general.c b/libnm-core/tests/test-general.c
index 7d97296a..a6abf391 100644
--- a/libnm-core/tests/test-general.c
+++ b/libnm-core/tests/test-general.c
@@ -23,8 +23,8 @@
 
 #include "nm-default.h"
 
-#include "nm-utils/c-list-util.h"
-#include "nm-utils/nm-enum-utils.h"
+#include "nm-std-aux/c-list-util.h"
+#include "nm-glib-aux/nm-enum-utils.h"
 
 #include "nm-utils.h"
 #include "nm-setting-private.h"
@@ -63,8 +63,8 @@
 #include "nm-setting-wpan.h"
 #include "nm-simple-connection.h"
 #include "nm-keyfile-internal.h"
-#include "nm-utils/nm-dedup-multi.h"
-#include "nm-ethtool-utils.h"
+#include "nm-glib-aux/nm-dedup-multi.h"
+#include "nm-libnm-core-intern/nm-ethtool-utils.h"
 
 #include "test-general-enums.h"
 
@@ -80,6 +80,29 @@ G_STATIC_ASSERT (sizeof (bool) <= sizeof (int));
 
 /*****************************************************************************/
 
+static void
+test_nm_ascii_spaces (void)
+{
+	int i;
+	const char *const S = NM_ASCII_SPACES;
+
+	for (i = 0; S[i]; i++)
+		g_assert (!strchr (&S[i + 1], S[i]));
+
+	for (i = 0; S[i] != '\0'; i++)
+		g_assert (g_ascii_isspace (S[i]));
+
+	g_assert (!g_ascii_isspace ((char) 0));
+	for (i = 1; i < 0x100; i++) {
+		if (g_ascii_isspace ((char) i))
+			g_assert (strchr (S, (char) i));
+		else
+			g_assert (!strchr (S, (char) i));
+	}
+}
+
+/*****************************************************************************/
+
 typedef struct _nm_packed {
 	int v0;
 	char v1;
@@ -236,51 +259,324 @@ test_nm_g_slice_free_fcn (void)
 /*****************************************************************************/
 
 static void
-_do_test_nm_utils_strsplit_set (gboolean escape, const char *str, ...)
+_do_test_nm_utils_strsplit_set_f_one (NMUtilsStrsplitSetFlags flags,
+                                      const char *str,
+                                      gsize words_len,
+                                      const char *const*exp_words)
 {
-	gs_unref_ptrarray GPtrArray *args_array = g_ptr_array_new ();
-	const char *const*args;
+#define DELIMITERS           " \n"
+#define DELIMITERS_C         ' ', '\n'
+
 	gs_free const char **words = NULL;
-	const char *arg;
-	gsize i;
-	va_list ap;
+	gsize i, j, k;
+	const gboolean f_allow_escaping = NM_FLAGS_HAS (flags, NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING);
+	const gboolean f_preserve_empty = NM_FLAGS_HAS (flags, NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY);
+	const char *s1;
+	gsize initial_offset;
+	gs_strfreev char **words_g = NULL;
+
+	g_assert (!NM_FLAGS_ANY (flags, ~(  NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING
+	                                  | NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY)));
+
+	/* assert that the epected words are valid (and don't contain unescaped delimiters). */
+	for (i = 0; i < words_len; i++) {
+		const char *w = exp_words[i];
+
+		g_assert (w);
+		if (!f_preserve_empty)
+			g_assert (w[0]);
+		for (k = 0; w[k]; ) {
+			if (   f_allow_escaping
+			    && w[k] == '\\') {
+				k++;
+				if (w[k] == '\0')
+					break;
+				k++;
+				continue;
+			}
+			g_assert (!NM_IN_SET (w[k], DELIMITERS_C));
+			k++;
+		}
+		if (!f_allow_escaping)
+			g_assert (!NM_STRCHAR_ANY (w, ch, NM_IN_SET (ch, DELIMITERS_C)));
+	}
 
-	va_start (ap, str);
-	while ((arg = va_arg (ap, const char *)))
-		g_ptr_array_add (args_array, (gpointer) arg);
-	va_end (ap);
-	g_ptr_array_add (args_array, NULL);
+	initial_offset = (f_preserve_empty || !str)
+	                 ? 0u
+	                 : strspn (str, DELIMITERS);
+
+	/* first compare our expected values with what g_strsplit_set() would
+	 * do. */
+	words_g = str ? g_strsplit_set (str, DELIMITERS, -1) : NULL;
+	if (str == NULL) {
+		g_assert_cmpint (words_len, ==, 0);
+		g_assert (!words_g);
+	} else if (nm_streq0 (str, "")) {
+		g_assert_cmpint (words_len, ==, 0);
+		g_assert (words_g);
+		g_assert (!words_g[0]);
+	} else {
+		g_assert (words_g);
+		g_assert (words_g[0]);
+		if (!f_allow_escaping) {
+			if (!f_preserve_empty) {
+				for (i = 0, j = 0; words_g[i]; i++) {
+					if (words_g[i][0] == '\0')
+						g_free (words_g[i]);
+					else
+						words_g[j++] = words_g[i];
+				}
+				words_g[j] = NULL;
+			}
+			if (f_preserve_empty)
+				g_assert_cmpint (words_len, >, 0);
+			for (i = 0; i < words_len; i++) {
+				g_assert (exp_words[i]);
+				g_assert_cmpstr (exp_words[i], ==, words_g[i]);
+			}
+			g_assert (words_g[words_len] == NULL);
+			g_assert_cmpint (NM_PTRARRAY_LEN (words_g), ==, words_len);
+			g_assert (_nm_utils_strv_cmp_n (exp_words, words_len, NM_CAST_STRV_CC (words_g), -1) == 0);
+		}
+	}
 
-	args = (const char *const*) args_array->pdata;
+	if (   flags == NM_UTILS_STRSPLIT_SET_FLAGS_NONE
+	    && nmtst_get_rand_bool ())
+		words = nm_utils_strsplit_set (str, DELIMITERS);
+	else if (   flags == NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY
+	         && nmtst_get_rand_bool ())
+		words = nm_utils_strsplit_set_with_empty (str, DELIMITERS);
+	else
+		words = nm_utils_strsplit_set_full (str, DELIMITERS, flags);
 
-	words = nm_utils_strsplit_set (str, " \t\n", escape);
+	g_assert_cmpint (NM_PTRARRAY_LEN (words), ==, words_len);
 
-	if (!args[0]) {
+	if (words_len == 0) {
 		g_assert (!words);
 		g_assert (   !str
-		          || NM_STRCHAR_ALL (str, ch, NM_IN_SET (ch, ' ', '\t', '\n')));
+		          || NM_STRCHAR_ALL (str, ch, NM_IN_SET (ch, DELIMITERS_C)));
 		return;
 	}
+
 	g_assert (words);
-	for (i = 0; args[i] || words[i]; i++) {
-		g_assert (args[i]);
-		g_assert (words[i]);
-		g_assert (args[i][0]);
-		g_assert (escape || NM_STRCHAR_ALL (args[i], ch, !NM_IN_SET (ch, ' ', '\t', '\n')));
-		g_assert_cmpstr (args[i], ==, words[i]);
+	for (i = 0; i < words_len; i++)
+		g_assert_cmpstr (exp_words[i], ==, words[i]);
+	g_assert (words[words_len] == NULL);
+
+	g_assert (_nm_utils_strv_cmp_n (exp_words, words_len, words, -1) == 0);
+
+	s1 = words[0];
+	g_assert (s1 >= (char *) &words[words_len + 1]);
+	s1 = &s1[strlen (str)];
+	for (i = 1; i < words_len; i++) {
+		g_assert (&(words[i - 1])[strlen (words[i - 1])] < words[i]);
+		g_assert (words[i] <= s1);
+	}
+
+	/* while strsplit removes all delimiters, we can relatively easily find them
+	 * in the original string. Assert that the original string and the pointer offsets
+	 * of words correspond. In particular, find idx_delim_after and idx_delim_before
+	 * to determine which delimiter was after/before a word. */
+	{
+		gsize idx_word_start;
+		gsize idx_delim_after_old = G_MAXSIZE;
+
+		idx_word_start = initial_offset;
+		for (i = 0; i < words_len; i++) {
+			const gsize l_i = strlen (words[i]);
+			gsize idx_delim_after;
+			gsize idx_delim_before;
+
+			/* find the delimiter *after* words[i]. We can do that by looking at the next
+			 * word and calculating the pointer difference.
+			 *
+			 * The delimiter after the very last word is '\0' and requires strlen() to find. */
+			idx_delim_after = initial_offset + ((words[i] - words[0]) + l_i);
+			if (idx_delim_after != idx_word_start + l_i) {
+				g_assert (!f_preserve_empty);
+				g_assert_cmpint (idx_word_start + l_i, <, idx_delim_after);
+				idx_word_start = idx_delim_after - l_i;
+			}
+			if (i + 1 < words_len) {
+				gsize x = initial_offset + ((words[i + 1] - words[0]) - 1);
+
+				if (idx_delim_after != x) {
+					g_assert (!f_preserve_empty);
+					g_assert_cmpint (idx_delim_after, <, x);
+					for (k = idx_delim_after; k <= x; k++)
+						g_assert (NM_IN_SET (str[k], DELIMITERS_C));
+				}
+				g_assert (NM_IN_SET (str[idx_delim_after], DELIMITERS_C));
+			} else {
+				if (f_preserve_empty)
+					g_assert (NM_IN_SET (str[idx_delim_after], '\0'));
+				else
+					g_assert (NM_IN_SET (str[idx_delim_after], '\0', DELIMITERS_C));
+			}
+
+			/* find the delimiter *before* words[i]. */
+			if (i == 0) {
+				/* there is only a delimiter *before*, with !f_preserve_empty and leading
+				 * delimiters. */
+				idx_delim_before = G_MAXSIZE;
+				if (initial_offset > 0) {
+					g_assert (!f_preserve_empty);
+					idx_delim_before = initial_offset - 1;
+				}
+			} else
+				idx_delim_before = initial_offset + (words[i] - words[0]) - 1;
+			if (idx_delim_before != G_MAXSIZE)
+				g_assert (NM_IN_SET (str[idx_delim_before], DELIMITERS_C));
+			if (idx_delim_after_old != idx_delim_before) {
+				g_assert (!f_preserve_empty);
+				if (i == 0) {
+					g_assert_cmpint (initial_offset, >, 0);
+					g_assert_cmpint (idx_delim_before, !=, G_MAXSIZE);
+					g_assert_cmpint (idx_delim_before, ==, initial_offset - 1);
+				} else {
+					g_assert_cmpint (idx_delim_after_old, !=, G_MAXSIZE);
+					g_assert_cmpint (idx_delim_before, !=, G_MAXSIZE);
+					g_assert_cmpint (idx_delim_after_old, <, idx_delim_before);
+					for (k = idx_delim_after_old; k <= idx_delim_before; k++)
+						g_assert (NM_IN_SET (str[k], DELIMITERS_C));
+				}
+			}
+
+			for (k = 0; k < l_i; ) {
+				if (   f_allow_escaping
+				    && str[idx_word_start + k] == '\\') {
+					k++;
+					if (k >= l_i)
+						break;
+					k++;
+					continue;
+				}
+				g_assert (!NM_IN_SET (str[idx_word_start + k], DELIMITERS_C));
+				k++;
+			}
+			g_assert (strncmp (words[i], &str[idx_word_start], l_i) == 0);
+
+			if (i > 0) {
+				const char *s = &(words[i - 1])[strlen (words[i - 1]) + 1];
+
+				if (s != words[i]) {
+					g_assert (!f_preserve_empty);
+					g_assert (s < words[i]);
+				}
+			}
+
+			idx_word_start += l_i + 1;
+			idx_delim_after_old = idx_delim_after;
+		}
 	}
 }
 
-#define do_test_nm_utils_strsplit_set(str, ...) \
-	_do_test_nm_utils_strsplit_set (str, ##__VA_ARGS__, NULL)
+static void
+_do_test_nm_utils_strsplit_set_f (NMUtilsStrsplitSetFlags flags,
+                                  const char *str,
+                                  gsize words_len,
+                                  const char *const*exp_words)
+{
+	_do_test_nm_utils_strsplit_set_f_one (flags, str, words_len, exp_words);
+
+	if (NM_FLAGS_HAS (flags, NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY)) {
+		gs_unref_ptrarray GPtrArray *exp_words2 = NULL;
+		gsize k;
+
+		exp_words2 = g_ptr_array_new ();
+		for (k = 0; k < words_len; k++) {
+			if (exp_words[k][0] != '\0')
+				g_ptr_array_add (exp_words2, (gpointer) exp_words[k]);
+		}
+
+		_do_test_nm_utils_strsplit_set_f_one (flags & (~NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY),
+		                                      str,
+		                                      exp_words2->len,
+		                                      (const char *const*) exp_words2->pdata);
+	}
+}
+
+#define do_test_nm_utils_strsplit_set_f(flags, str, ...) \
+	_do_test_nm_utils_strsplit_set_f (flags, \
+	                                  str, \
+	                                  NM_NARG (__VA_ARGS__), \
+	                                  NM_MAKE_STRV (__VA_ARGS__))
+
+#define do_test_nm_utils_strsplit_set(allow_escaping, str, ...) \
+	do_test_nm_utils_strsplit_set_f (  (allow_escaping) \
+	                                 ? NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING \
+	                                 : NM_UTILS_STRSPLIT_SET_FLAGS_NONE, \
+	                                 str, \
+	                                 ##__VA_ARGS__)
+
+static void
+_do_test_nm_utils_strsplit_set_simple (NMUtilsStrsplitSetFlags flags,
+                                       const char *str,
+                                       gsize words_len,
+                                       const char *const*exp_words)
+{
+	gs_free const char **tokens = NULL;
+	gsize n_tokens;
+
+	tokens = nm_utils_strsplit_set_full (str, DELIMITERS, flags);
+
+	if (!tokens) {
+		g_assert_cmpint (words_len, ==, 0);
+		return;
+	}
+
+	g_assert (str && str[0]);
+	g_assert_cmpint (words_len, >, 0);
+	n_tokens = NM_PTRARRAY_LEN (tokens);
+
+	if (_nm_utils_strv_cmp_n (exp_words, words_len, tokens, -1) != 0) {
+		gsize i;
+
+		g_print (">>> split \"%s\" (flags %x) got %zu tokens (%zu expected)\n", str, (guint) flags, n_tokens, words_len);
+		for (i = 0; i < NM_MAX (n_tokens, words_len); i++) {
+			const char *s1 = i < n_tokens  ? tokens[i]    : NULL;
+			const char *s2 = i < words_len ? exp_words[i] : NULL;
+
+			g_print (">>> [%zu]: %s - %s%s%s vs. %s%s%s\n",
+			         i,
+			         nm_streq0 (s1, s2) ? "same" : "diff",
+			         NM_PRINT_FMT_QUOTE_STRING (s1),
+			         NM_PRINT_FMT_QUOTE_STRING (s2));
+		}
+		g_assert_not_reached ();
+	}
+	g_assert_cmpint (words_len, ==, NM_PTRARRAY_LEN (tokens));
+}
+#define do_test_nm_utils_strsplit_set_simple(flags, str, ...) \
+	_do_test_nm_utils_strsplit_set_simple ((flags), (str), NM_NARG (__VA_ARGS__), NM_MAKE_STRV (__VA_ARGS__))
 
 static void
 test_nm_utils_strsplit_set (void)
 {
+	gs_unref_ptrarray GPtrArray *words_exp = NULL;
+	guint test_run;
+
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_NONE, NULL);
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_NONE, "");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_NONE, " ");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_NONE, "a  b", "a", "b");
+
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, NULL);
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, " ", "", "");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "  ", "", "", "");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "a  ", "a", "", "");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "a  b", "a", "", "b");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, " ab  b", "", "ab", "", "b");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "ab  b", "ab", "", "b");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "abb", "abb");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "abb  bb ", "abb", "", "bb", "");
+	do_test_nm_utils_strsplit_set_f (NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, "abb bcb ", "abb", "bcb", "");
+
 	do_test_nm_utils_strsplit_set (FALSE, NULL);
 	do_test_nm_utils_strsplit_set (FALSE, "");
-	do_test_nm_utils_strsplit_set (FALSE, "\t");
-	do_test_nm_utils_strsplit_set (FALSE, " \t\n");
+	do_test_nm_utils_strsplit_set (FALSE, "\n");
+	do_test_nm_utils_strsplit_set (TRUE, " \t\n", "\t");
 	do_test_nm_utils_strsplit_set (FALSE, "a", "a");
 	do_test_nm_utils_strsplit_set (FALSE, "a b", "a", "b");
 	do_test_nm_utils_strsplit_set (FALSE, "a\rb", "a\rb");
@@ -300,6 +596,76 @@ test_nm_utils_strsplit_set (void)
 	do_test_nm_utils_strsplit_set (TRUE, "foo\\", "foo\\");
 	do_test_nm_utils_strsplit_set (TRUE, "bar foo\\", "bar", "foo\\");
 	do_test_nm_utils_strsplit_set (TRUE, "\\ a b\\ \\  c", "\\ a", "b\\ \\ ", "c");
+
+	words_exp = g_ptr_array_new_with_free_func (g_free);
+	for (test_run = 0; test_run < 100; test_run++) {
+		gboolean f_allow_escaping = nmtst_get_rand_bool ();
+		guint words_len = nmtst_get_rand_int () % 100;
+		gs_free char *str = NULL;
+		guint i;
+
+		g_ptr_array_set_size (words_exp, 0);
+		for (i = 0; i < words_len; i++) {
+			guint word_len;
+			char *word;
+			guint j;
+
+			word_len = nmtst_get_rand_int ();
+			if ((word_len % 100) < 30)
+				word_len = 0;
+			else
+				word_len = (word_len >> 10) % 100;
+			word = g_new (char, word_len + 3);
+			for (j = 0; j < word_len; ) {
+				guint32 p = nmtst_get_rand_int ();
+				static const char delimiters_arr[] = { DELIMITERS_C };
+				static const char regular_chars[] = "abcdefghijklmnopqrstuvwxyz";
+
+				if (   !f_allow_escaping
+				    || (p % 1000) < 700) {
+					if (((p >> 20) % 100) < 20)
+						word[j++] = '\\';
+					word[j++] = regular_chars[(p >> 11) % (G_N_ELEMENTS (regular_chars) - 1)];
+					continue;
+				}
+				word[j++] = '\\';
+				word[j++] = delimiters_arr[(p >> 11) % G_N_ELEMENTS (delimiters_arr)];
+			}
+			word[j] = '\0';
+			g_ptr_array_add (words_exp, word);
+		}
+		g_ptr_array_add (words_exp, NULL);
+
+		str = g_strjoinv (" ", (char **) words_exp->pdata);
+
+		if (   str[0] == '\0'
+		    && words_len > 0) {
+			g_assert (words_len == 1);
+			g_assert_cmpstr (words_exp->pdata[0], ==, "");
+			words_len = 0;
+		}
+
+		_do_test_nm_utils_strsplit_set_f (  (f_allow_escaping ? NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING : NM_UTILS_STRSPLIT_SET_FLAGS_NONE)
+		                                  | NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY,
+		                                  str,
+		                                  words_len,
+		                                  (const char *const*) words_exp->pdata);
+	}
+
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED, "\t", "\t");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED | NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP, "\t");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED | NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP | NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY,
+	                                      "\t", "");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED | NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP | NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY,
+	                                      "\t\\\t\t\t\\\t", "\t\t\t\t");
+
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED, "\ta", "\ta");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED | NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP, "\ta", "a");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED, "\ta\\ b\t\\ ", "\ta b\t ");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED | NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP, "\ta\\ b\t\\ \t", "a b\t ");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED, "a\\  b", "a ", "b");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED, "\ta\\  b", "\ta ", "b");
+	do_test_nm_utils_strsplit_set_simple (NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED | NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP, "\ta\\  b", "a ", "b");
 }
 
 /*****************************************************************************/
@@ -809,54 +1175,54 @@ test_setting_vpn_items (void)
 	nm_setting_vpn_remove_data_item (s_vpn, "foobar4-flags");
 
 	/* Try to add some blank values and make sure they are rejected */
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key != NULL));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_data_item (s_vpn, NULL, NULL);
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (strlen (key) > 0));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_data_item (s_vpn, "", "");
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (item != NULL));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (item && item[0]));
 	nm_setting_vpn_add_data_item (s_vpn, "foobar1", NULL);
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (strlen (item) > 0));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (item && item[0]));
 	nm_setting_vpn_add_data_item (s_vpn, "foobar1", "");
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key != NULL));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_data_item (s_vpn, NULL, "blahblah1");
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (strlen (key) > 0));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_data_item (s_vpn, "", "blahblah1");
 	g_test_assert_expected_messages ();
 
 	nm_setting_vpn_foreach_data_item (s_vpn, vpn_check_empty_func, NULL);
 
 	/* Try to add some blank secrets and make sure they are rejected */
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key != NULL));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_secret (s_vpn, NULL, NULL);
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (strlen (key) > 0));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_secret (s_vpn, "", "");
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (secret != NULL));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (secret && secret[0]));
 	nm_setting_vpn_add_secret (s_vpn, "foobar1", NULL);
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (strlen (secret) > 0));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (secret && secret[0]));
 	nm_setting_vpn_add_secret (s_vpn, "foobar1", "");
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key != NULL));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_secret (s_vpn, NULL, "blahblah1");
 	g_test_assert_expected_messages ();
 
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (strlen (key) > 0));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0]));
 	nm_setting_vpn_add_secret (s_vpn, "", "blahblah1");
 	g_test_assert_expected_messages ();
 
@@ -2347,7 +2713,7 @@ test_setting_connection_permissions_helpers (void)
 	g_assert (!success);
 
 	/* Ensure a bad [type] is rejected */
-	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (ptype));
+	NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (ptype && ptype[0]));
 	success = nm_setting_connection_add_permission (s_con, NULL, "blah", NULL);
 	g_test_assert_expected_messages ();
 	g_assert (!success);
@@ -2687,6 +3053,7 @@ test_connection_diff_a_only (void)
 			{ NM_SETTING_IP_CONFIG_ROUTES,             NM_SETTING_DIFF_RESULT_IN_A },
 			{ NM_SETTING_IP_CONFIG_ROUTE_METRIC,       NM_SETTING_DIFF_RESULT_IN_A },
 			{ NM_SETTING_IP_CONFIG_ROUTE_TABLE,        NM_SETTING_DIFF_RESULT_IN_A },
+			{ NM_SETTING_IP_CONFIG_ROUTING_RULES,      NM_SETTING_DIFF_RESULT_IN_A },
 			{ NM_SETTING_IP_CONFIG_IGNORE_AUTO_ROUTES, NM_SETTING_DIFF_RESULT_IN_A },
 			{ NM_SETTING_IP_CONFIG_IGNORE_AUTO_DNS,    NM_SETTING_DIFF_RESULT_IN_A },
 			{ NM_SETTING_IP4_CONFIG_DHCP_CLIENT_ID,    NM_SETTING_DIFF_RESULT_IN_A },
@@ -4713,6 +5080,7 @@ test_connection_normalize_infiniband_mtu (void)
 {
 	gs_unref_object NMConnection *con = NULL;
 	NMSettingInfiniband *s_infini;
+	guint mtu_regular = nmtst_rand_select (2044, 2045, 65520);
 
 	con = nmtst_create_minimal_connection ("test_connection_normalize_infiniband_mtu", NULL,
 	                                       NM_SETTING_INFINIBAND_SETTING_NAME, NULL);
@@ -4725,26 +5093,26 @@ test_connection_normalize_infiniband_mtu (void)
 
 	g_object_set (s_infini,
 	              NM_SETTING_INFINIBAND_TRANSPORT_MODE, "datagram",
-	              NM_SETTING_INFINIBAND_MTU, (guint) 2044,
+	              NM_SETTING_INFINIBAND_MTU, (guint) mtu_regular,
 	              NULL);
 	nmtst_assert_connection_verifies_and_normalizable (con);
 	nmtst_connection_normalize (con);
-	g_assert_cmpint (2044, ==, nm_setting_infiniband_get_mtu (s_infini));
+	g_assert_cmpint (mtu_regular, ==, nm_setting_infiniband_get_mtu (s_infini));
 
 	g_object_set (s_infini,
 	              NM_SETTING_INFINIBAND_TRANSPORT_MODE, "datagram",
-	              NM_SETTING_INFINIBAND_MTU, (guint) 2045,
+	              NM_SETTING_INFINIBAND_MTU, (guint) 65521,
 	              NULL);
 	nmtst_assert_connection_verifies_after_normalization (con, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
 	nmtst_connection_normalize (con);
-	g_assert_cmpint (2044, ==, nm_setting_infiniband_get_mtu (s_infini));
+	g_assert_cmpint (65520, ==, nm_setting_infiniband_get_mtu (s_infini));
 
 	g_object_set (s_infini,
 	              NM_SETTING_INFINIBAND_TRANSPORT_MODE, "connected",
-	              NM_SETTING_INFINIBAND_MTU, (guint) 65520,
+	              NM_SETTING_INFINIBAND_MTU, (guint) mtu_regular,
 	              NULL);
 	nmtst_assert_connection_verifies_without_normalization (con);
-	g_assert_cmpint (65520, ==, nm_setting_infiniband_get_mtu (s_infini));
+	g_assert_cmpint (mtu_regular, ==, nm_setting_infiniband_get_mtu (s_infini));
 
 	g_object_set (s_infini,
 	              NM_SETTING_INFINIBAND_TRANSPORT_MODE, "connected",
@@ -5540,7 +5908,7 @@ _sock_addr_endpoint (const char *endpoint,
 	SockAddrUnion sockaddr = { };
 
 	g_assert (endpoint);
-	g_assert (!host == (port == -1));
+	g_assert ((!host) == (port == -1));
 	g_assert (port >= -1 && port <= G_MAXUINT16);
 
 	ep = nm_sock_addr_endpoint_new (endpoint);
@@ -7706,53 +8074,6 @@ test_ethtool_offload (void)
 	g_assert_cmpstr (d->optname, ==, NM_ETHTOOL_OPTNAME_FEATURE_RXHASH);
 }
 
-static void
-test_nm_utils_escape_spaces (void)
-{
-	char *to_free;
-
-	g_assert_cmpstr (_nm_utils_escape_spaces (NULL, &to_free), ==, NULL);
-	g_free (to_free);
-
-	g_assert_cmpstr (_nm_utils_escape_spaces ("", &to_free), ==, "");
-	g_free (to_free);
-
-	g_assert_cmpstr (_nm_utils_escape_spaces (" ", &to_free), ==, "\\ ");
-	g_free (to_free);
-
-	g_assert_cmpstr (_nm_utils_escape_spaces ("\t ", &to_free), ==, "\\\t\\ ");
-	g_free (to_free);
-
-	g_assert_cmpstr (_nm_utils_escape_spaces ("abc", &to_free), ==, "abc");
-	g_free (to_free);
-
-	g_assert_cmpstr (_nm_utils_escape_spaces ("abc def", &to_free), ==, "abc\\ def");
-	g_free (to_free);
-
-	g_assert_cmpstr (_nm_utils_escape_spaces ("abc\tdef", &to_free), ==, "abc\\\tdef");
-	g_free (to_free);
-}
-
-static void
-test_nm_utils_unescape_spaces (void)
-{
-#define CHECK_STR(in, out) \
-	G_STMT_START { \
-		gs_free char *str = g_strdup (in); \
-		\
-		g_assert_cmpstr (_nm_utils_unescape_spaces (str), ==, out); \
-	} G_STMT_END
-
-	CHECK_STR ("\\a", "\\a");
-	CHECK_STR ("foobar", "foobar");
-	CHECK_STR ("foo bar", "foo bar");
-	CHECK_STR ("foo\\ bar", "foo bar");
-	CHECK_STR ("foo\\", "foo\\");
-	CHECK_STR ("\\\\\t", "\\\t");
-
-#undef CHECK_STR
-}
-
 /*****************************************************************************/
 
 NMTST_DEFINE ();
@@ -7761,6 +8082,7 @@ int main (int argc, char **argv)
 {
 	nmtst_init (&argc, &argv, TRUE);
 
+	g_test_add_func ("/core/general/test_nm_ascii_spaces", test_nm_ascii_spaces);
 	g_test_add_func ("/core/general/test_nm_hash", test_nm_hash);
 	g_test_add_func ("/core/general/test_nm_g_slice_free_fcn", test_nm_g_slice_free_fcn);
 	g_test_add_func ("/core/general/test_c_list_sort", test_c_list_sort);
@@ -7905,8 +8227,6 @@ int main (int argc, char **argv)
 	g_test_add_func ("/core/general/_nm_utils_dns_option_find_idx", test_nm_utils_dns_option_find_idx);
 	g_test_add_func ("/core/general/_nm_utils_validate_json", test_nm_utils_check_valid_json);
 	g_test_add_func ("/core/general/_nm_utils_team_config_equal", test_nm_utils_team_config_equal);
-	g_test_add_func ("/core/general/_nm_utils_escape_spaces", test_nm_utils_escape_spaces);
-	g_test_add_func ("/core/general/_nm_utils_unescape_spaces", test_nm_utils_unescape_spaces);
 	g_test_add_func ("/core/general/test_nm_utils_enum", test_nm_utils_enum);
 	g_test_add_func ("/core/general/nm-set-out", test_nm_set_out);
 	g_test_add_func ("/core/general/route_attributes/parse", test_route_attributes_parse);
diff --git a/libnm-core/tests/test-keyfile.c b/libnm-core/tests/test-keyfile.c
index 9b9d1c04..157e8f18 100644
--- a/libnm-core/tests/test-keyfile.c
+++ b/libnm-core/tests/test-keyfile.c
@@ -744,6 +744,102 @@ test_vpn_1 (void)
 
 /*****************************************************************************/
 
+static void
+test_bridge_vlans (void)
+{
+	gs_unref_keyfile GKeyFile *keyfile = NULL;
+	gs_unref_object NMConnection *con = NULL;
+	NMSettingBridge *s_bridge;
+	NMBridgeVlan *vlan;
+	guint16 vid, vid_end;
+
+	con = nmtst_create_connection_from_keyfile (
+	      "[connection]\n"
+	      "id=t\n"
+	      "type=bridge\n"
+	      "interface-name=br4\n"
+	      "\n"
+	      "[bridge]\n"
+	      "vlans=900 ,  1 pvid  untagged, 100-123 untagged\n"
+	      "",
+	      "/test_bridge_port/vlans");
+	s_bridge = NM_SETTING_BRIDGE (nm_connection_get_setting (con, NM_TYPE_SETTING_BRIDGE));
+	g_assert (s_bridge);
+	g_assert_cmpuint (nm_setting_bridge_get_num_vlans (s_bridge), ==, 3);
+
+	vlan = nm_setting_bridge_get_vlan (s_bridge, 0);
+	g_assert (vlan);
+	nm_bridge_vlan_get_vid_range (vlan, &vid, &vid_end);
+	g_assert_cmpuint (vid, ==, 1);
+	g_assert_cmpuint (vid_end, ==, 1);
+	g_assert_cmpint  (nm_bridge_vlan_is_pvid (vlan), ==, TRUE);
+	g_assert_cmpint  (nm_bridge_vlan_is_untagged (vlan), ==, TRUE);
+
+	vlan = nm_setting_bridge_get_vlan (s_bridge, 1);
+	g_assert (vlan);
+	nm_bridge_vlan_get_vid_range (vlan, &vid, &vid_end);
+	g_assert_cmpuint (vid, ==, 100);
+	g_assert_cmpuint (vid_end, ==, 123);
+	g_assert_cmpint  (nm_bridge_vlan_is_pvid (vlan), ==, FALSE);
+	g_assert_cmpint  (nm_bridge_vlan_is_untagged (vlan), ==, TRUE);
+
+	vlan = nm_setting_bridge_get_vlan (s_bridge, 2);
+	g_assert (vlan);
+	nm_bridge_vlan_get_vid_range (vlan, &vid, &vid_end);
+	g_assert_cmpuint (vid, ==, 900);
+	g_assert_cmpuint (vid_end, ==, 900);
+	g_assert_cmpint  (nm_bridge_vlan_is_pvid (vlan), ==, FALSE);
+	g_assert_cmpint  (nm_bridge_vlan_is_untagged (vlan), ==, FALSE);
+
+	CLEAR (&con, &keyfile);
+}
+
+static void
+test_bridge_port_vlans (void)
+{
+	gs_unref_keyfile GKeyFile *keyfile = NULL;
+	gs_unref_object NMConnection *con = NULL;
+	NMSettingBridgePort *s_port;
+	NMBridgeVlan *vlan;
+	guint16 vid_start, vid_end;
+
+	con = nmtst_create_connection_from_keyfile (
+	      "[connection]\n"
+	      "id=t\n"
+	      "type=dummy\n"
+	      "interface-name=dummy1\n"
+	      "master=br0\n"
+	      "slave-type=bridge\n"
+	      "\n"
+	      "[bridge-port]\n"
+	      "vlans=4094 pvid , 10-20 untagged\n"
+	      "",
+	      "/test_bridge_port/vlans");
+	s_port = NM_SETTING_BRIDGE_PORT (nm_connection_get_setting (con, NM_TYPE_SETTING_BRIDGE_PORT));
+	g_assert (s_port);
+	g_assert_cmpuint (nm_setting_bridge_port_get_num_vlans (s_port), ==, 2);
+
+	vlan = nm_setting_bridge_port_get_vlan (s_port, 0);
+	g_assert (vlan);
+	nm_bridge_vlan_get_vid_range (vlan, &vid_start, &vid_end);
+	g_assert_cmpuint (vid_start, ==, 10);
+	g_assert_cmpuint (vid_end, ==, 20);
+	g_assert_cmpint  (nm_bridge_vlan_is_pvid (vlan), ==, FALSE);
+	g_assert_cmpint  (nm_bridge_vlan_is_untagged (vlan), ==, TRUE);
+
+	vlan = nm_setting_bridge_port_get_vlan (s_port, 1);
+	g_assert (vlan);
+	nm_bridge_vlan_get_vid_range (vlan, &vid_start, &vid_end);
+	g_assert_cmpuint (vid_start, ==, 4094);
+	g_assert_cmpuint (vid_end, ==, 4094);
+	g_assert_cmpint  (nm_bridge_vlan_is_pvid (vlan), ==, TRUE);
+	g_assert_cmpint  (nm_bridge_vlan_is_untagged (vlan), ==, FALSE);
+
+	CLEAR (&con, &keyfile);
+}
+
+/*****************************************************************************/
+
 NMTST_DEFINE ();
 
 int main (int argc, char **argv)
@@ -757,6 +853,8 @@ int main (int argc, char **argv)
 	g_test_add_func ("/core/keyfile/test_team_conf_read/invalid", test_team_conf_read_invalid);
 	g_test_add_func ("/core/keyfile/test_user/1", test_user_1);
 	g_test_add_func ("/core/keyfile/test_vpn/1", test_vpn_1);
+	g_test_add_func ("/core/keyfile/bridge/vlans", test_bridge_vlans);
+	g_test_add_func ("/core/keyfile/bridge-port/vlans", test_bridge_port_vlans);
 
 	return g_test_run ();
 }
diff --git a/libnm-core/tests/test-setting.c b/libnm-core/tests/test-setting.c
index 2011273a..03100a03 100644
--- a/libnm-core/tests/test-setting.c
+++ b/libnm-core/tests/test-setting.c
@@ -20,6 +20,7 @@
 #include "nm-default.h"
 
 #include <linux/pkt_sched.h>
+#include <net/if.h>
 
 #include "nm-utils.h"
 #include "nm-utils-private.h"
@@ -1658,6 +1659,76 @@ test_sriov_parse_vlans (void)
 	test_sriov_parse_vlan_one ("1.2.ad;2.0.q;5;3", TRUE, {1, 2, 1}, {2, 0, 0}, {3, 0, 0}, {5, 0, 0});
 }
 
+static void
+test_bridge_vlans (void)
+{
+	NMBridgeVlan *v1, *v2;
+	GError *error = NULL;
+	guint16 vid_start, vid_end;
+	char *str;
+
+	v1 = nm_bridge_vlan_from_str ("1 foobar", &error);
+	nmtst_assert_no_success (v1, error);
+	g_clear_error (&error);
+
+	v1 = nm_bridge_vlan_from_str ("4095", &error);
+	nmtst_assert_no_success (v1, error);
+	g_clear_error (&error);
+
+	/* test ranges */
+	v1 = nm_bridge_vlan_from_str ("2-1000 untagged", &error);
+	nmtst_assert_success (v1, error);
+	g_assert_cmpint (nm_bridge_vlan_get_vid_range (v1, &vid_start, &vid_end), ==, TRUE);
+	g_assert_cmpuint (vid_start, ==, 2);
+	g_assert_cmpuint (vid_end, ==, 1000);
+	g_assert_cmpint (nm_bridge_vlan_is_pvid (v1), ==, FALSE);
+	g_assert_cmpint (nm_bridge_vlan_is_untagged (v1), ==, TRUE);
+	nm_bridge_vlan_unref (v1);
+
+	/* test comparison (1) */
+	v1 = nm_bridge_vlan_from_str ("10 untagged", &error);
+	nmtst_assert_success (v1, error);
+
+	g_assert_cmpint (nm_bridge_vlan_get_vid_range (v1, &vid_start, &vid_end), ==, FALSE);
+	g_assert_cmpuint (vid_start, ==, 10);
+	g_assert_cmpuint (vid_end, ==, 10);
+	g_assert_cmpint (nm_bridge_vlan_is_sealed (v1), ==, FALSE);
+	g_assert_cmpint (nm_bridge_vlan_is_pvid (v1), ==, FALSE);
+	g_assert_cmpint (nm_bridge_vlan_is_untagged (v1), ==, TRUE);
+
+	nm_bridge_vlan_set_pvid (v1, TRUE);
+	nm_bridge_vlan_set_untagged (v1, FALSE);
+	nm_bridge_vlan_seal (v1);
+
+	g_assert_cmpint (nm_bridge_vlan_is_sealed (v1), ==, TRUE);
+	g_assert_cmpint (nm_bridge_vlan_is_pvid (v1), ==, TRUE);
+	g_assert_cmpint (nm_bridge_vlan_is_untagged (v1), ==, FALSE);
+
+	str = nm_bridge_vlan_to_str (v1, &error);
+	nmtst_assert_success (str, error);
+	g_assert_cmpstr (str, ==, "10 pvid");
+	g_clear_pointer (&str, g_free);
+
+	v2 = nm_bridge_vlan_from_str ("  10  pvid  ", &error);
+	nmtst_assert_success (v2, error);
+
+	g_assert_cmpint (nm_bridge_vlan_cmp (v1, v2), ==, 0);
+
+	nm_bridge_vlan_unref (v1);
+	nm_bridge_vlan_unref (v2);
+
+	/* test comparison (2) */
+	v1 = nm_bridge_vlan_from_str ("10", &error);
+	nmtst_assert_success (v1, error);
+	v2 = nm_bridge_vlan_from_str ("20", &error);
+	nmtst_assert_success (v2, error);
+
+	g_assert_cmpint (nm_bridge_vlan_cmp (v1, v2), <, 0);
+
+	nm_bridge_vlan_unref (v1);
+	nm_bridge_vlan_unref (v2);
+}
+
 /*****************************************************************************/
 
 static void
@@ -2304,7 +2375,16 @@ test_roundtrip_conversion (gconstpointer test_data)
 	NMSettingConnection *s_con = NULL;
 	NMSettingWired *s_eth = NULL;
 	NMSettingWireGuard *s_wg = NULL;
+	union {
+		struct {
+			NMSettingIPConfig  *s_6;
+			NMSettingIPConfig  *s_4;
+		};
+		NMSettingIPConfig *s_x[2];
+	} s_ip;
+	int is_ipv4;
 	guint i;
+	gboolean success;
 
 	switch (MODE) {
 	case 0:
@@ -2491,6 +2571,89 @@ test_roundtrip_conversion (gconstpointer test_data)
 		_rndt_wg_peers_assert_equal (s_wg, wg_peers, TRUE, TRUE, FALSE);
 		break;
 
+	case 3:
+		con = nmtst_create_minimal_connection (ID, UUID, NM_SETTING_WIRED_SETTING_NAME, &s_con);
+		g_object_set (s_con,
+		              NM_SETTING_CONNECTION_INTERFACE_NAME,
+		              INTERFACE_NAME,
+		              NULL);
+		nmtst_connection_normalize (con);
+
+		s_eth = NM_SETTING_WIRED (nm_connection_get_setting (con, NM_TYPE_SETTING_WIRED));
+		g_assert (NM_IS_SETTING_WIRED (s_eth));
+
+		g_object_set (s_eth,
+		              NM_SETTING_WIRED_MTU,
+		              ETH_MTU,
+		              NULL);
+
+		s_ip.s_4 = NM_SETTING_IP_CONFIG (nm_connection_get_setting (con, NM_TYPE_SETTING_IP4_CONFIG));
+		g_assert (NM_IS_SETTING_IP4_CONFIG (s_ip.s_4));
+
+		s_ip.s_6 = NM_SETTING_IP_CONFIG (nm_connection_get_setting (con, NM_TYPE_SETTING_IP6_CONFIG));
+		g_assert (NM_IS_SETTING_IP6_CONFIG (s_ip.s_6));
+
+		for (is_ipv4 = 0; is_ipv4 < 2; is_ipv4++) {
+			g_assert (NM_IS_SETTING_IP_CONFIG (s_ip.s_x[is_ipv4]));
+			for (i = 0; i < 3; i++) {
+				char addrstr[NM_UTILS_INET_ADDRSTRLEN];
+
+				nm_auto_unref_ip_routing_rule NMIPRoutingRule *rr = NULL;
+
+				rr = nm_ip_routing_rule_new (is_ipv4 ? AF_INET : AF_INET6);
+				nm_ip_routing_rule_set_priority (rr, i + 1);
+				if (i > 0) {
+					if (is_ipv4)
+						nm_sprintf_buf (addrstr, "192.168.%u.0", i);
+					else
+						nm_sprintf_buf (addrstr, "1:2:3:%x::", 10 + i);
+					nm_ip_routing_rule_set_from (rr, addrstr, is_ipv4 ? 24 + i : 64 + i);
+				}
+				nm_ip_routing_rule_set_table (rr, 1000 + i);
+
+				success = nm_ip_routing_rule_validate (rr, &error);
+				nmtst_assert_success (success, error);
+
+				nm_setting_ip_config_add_routing_rule (s_ip.s_x[is_ipv4], rr);
+			}
+		}
+
+		g_ptr_array_add (kf_data_arr,
+		    g_strdup_printf ("[connection]\n"
+		                     "id=%s\n"
+		                     "uuid=%s\n"
+		                     "type=ethernet\n"
+		                     "interface-name=%s\n"
+		                     "permissions=\n"
+		                     "\n"
+		                     "[ethernet]\n"
+		                     "mac-address-blacklist=\n"
+		                     "%s" /* mtu */
+		                     "\n"
+		                     "[ipv4]\n"
+		                     "dns-search=\n"
+		                     "method=auto\n"
+		                     "routing-rule1=priority 1 from 0.0.0.0/0 table 1000\n"
+		                     "routing-rule2=priority 2 from 192.168.1.0/25 table 1001\n"
+		                     "routing-rule3=priority 3 from 192.168.2.0/26 table 1002\n"
+		                     "\n"
+		                     "[ipv6]\n"
+		                     "addr-gen-mode=stable-privacy\n"
+		                     "dns-search=\n"
+		                     "method=auto\n"
+		                     "routing-rule1=priority 1 from ::/0 table 1000\n"
+		                     "routing-rule2=priority 2 from 1:2:3:b::/65 table 1001\n"
+		                     "routing-rule3=priority 3 from 1:2:3:c::/66 table 1002\n"
+		                     "",
+		                     ID,
+		                     UUID,
+		                     INTERFACE_NAME,
+		                       (ETH_MTU != 0)
+		                     ? nm_sprintf_bufa (100, "mtu=%u\n", ETH_MTU)
+		                     : ""));
+
+		break;
+
 	default:
 		g_assert_not_reached ();
 	}
@@ -2618,6 +2781,204 @@ test_roundtrip_conversion (gconstpointer test_data)
 
 /*****************************************************************************/
 
+static NMIPRoutingRule *
+_rr_from_str_get_impl (const char *str, const char *const*aliases)
+{
+	nm_auto_unref_ip_routing_rule NMIPRoutingRule *rr = NULL;
+	gs_free_error GError *error = NULL;
+	gboolean vbool;
+	int addr_family;
+	int i;
+	NMIPRoutingRuleAsStringFlags to_string_flags;
+
+	rr = nm_ip_routing_rule_from_string (str,
+	                                     NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE,
+	                                     NULL,
+	                                     &error);
+	nmtst_assert_success (rr, error);
+
+	addr_family = nm_ip_routing_rule_get_addr_family (rr);
+	g_assert (NM_IN_SET (addr_family, AF_INET, AF_INET6));
+
+	if (addr_family == AF_INET)
+		to_string_flags = NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET;
+	else
+		to_string_flags = NM_IP_ROUTING_RULE_AS_STRING_FLAGS_AF_INET6;
+
+	for (i = 0; TRUE; i++) {
+		nm_auto_unref_ip_routing_rule NMIPRoutingRule *rr2 = NULL;
+		gs_free char *str1 = NULL;
+		gs_unref_variant GVariant *variant1 = NULL;
+		const char *cstr1;
+
+		switch (i) {
+		case 0:
+			rr2 = nm_ip_routing_rule_ref (rr);
+			break;
+
+		case 1:
+			rr2 = nm_ip_routing_rule_from_string (str,
+			                                        NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE
+			                                      | (nmtst_get_rand_bool () ? to_string_flags : NM_IP_ROUTING_RULE_AS_STRING_FLAGS_NONE),
+			                                      NULL,
+			                                      &error);
+			nmtst_assert_success (rr, error);
+			break;
+
+		case 2:
+			str1 = nm_ip_routing_rule_to_string (rr,
+			                                       NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE
+			                                      | (nmtst_get_rand_bool () ? to_string_flags : NM_IP_ROUTING_RULE_AS_STRING_FLAGS_NONE),
+			                                     NULL,
+			                                     &error);
+			nmtst_assert_success (str1 && str1[0], error);
+
+			g_assert_cmpstr (str, ==, str1);
+
+			rr2 = nm_ip_routing_rule_from_string (str1,
+			                                        NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE
+			                                      | (nmtst_get_rand_bool () ? to_string_flags : NM_IP_ROUTING_RULE_AS_STRING_FLAGS_NONE),
+			                                      NULL,
+			                                      &error);
+			nmtst_assert_success (rr, error);
+			break;
+
+		case 3:
+			variant1 = nm_ip_routing_rule_to_dbus (rr);
+			g_assert (variant1);
+			g_assert (g_variant_is_floating (variant1));
+			g_assert (g_variant_is_of_type (variant1, G_VARIANT_TYPE_VARDICT));
+
+			rr2 = nm_ip_routing_rule_from_dbus (variant1,
+			                                    TRUE,
+			                                    &error);
+			nmtst_assert_success (rr, error);
+			break;
+
+		default:
+			if (!aliases || !aliases[0])
+				goto done;
+			cstr1 = (aliases++)[0];
+			rr2 = nm_ip_routing_rule_from_string (cstr1,
+			                                        NM_IP_ROUTING_RULE_AS_STRING_FLAGS_VALIDATE
+			                                      | (nmtst_get_rand_bool () ? to_string_flags : NM_IP_ROUTING_RULE_AS_STRING_FLAGS_NONE),
+			                                      NULL,
+			                                      &error);
+			nmtst_assert_success (rr, error);
+			break;
+		}
+
+		g_assert (rr2);
+		vbool = nm_ip_routing_rule_validate (rr, &error);
+		nmtst_assert_success (vbool, error);
+		vbool = nm_ip_routing_rule_validate (rr2, &error);
+		nmtst_assert_success (vbool, error);
+
+		g_assert_cmpint (nm_ip_routing_rule_cmp (rr, rr2), ==, 0);
+		g_assert_cmpint (nm_ip_routing_rule_cmp (rr2, rr), ==, 0);
+	}
+
+done:
+	return g_steal_pointer (&rr);
+}
+#define _rr_from_str_get(a, ...) _rr_from_str_get_impl (a, &(NM_MAKE_STRV (NULL, ##__VA_ARGS__))[1])
+
+#define _rr_from_str(...) \
+	G_STMT_START { \
+		nm_auto_unref_ip_routing_rule NMIPRoutingRule *_rr = NULL; \
+		\
+		_rr = _rr_from_str_get (__VA_ARGS__); \
+		g_assert (_rr); \
+	} G_STMT_END
+
+static void
+test_routing_rule (gconstpointer test_data)
+{
+	nm_auto_unref_ip_routing_rule NMIPRoutingRule *rr1 = NULL;
+	gboolean success;
+	char ifname_buf[16];
+
+	_rr_from_str ("priority 5 from 0.0.0.0 table 1",
+	              "  from 0.0.0.0  priority  5 lookup 1 ");
+	_rr_from_str ("priority 5 from 0.0.0.0/0 table 4");
+	_rr_from_str ("priority 5 to 0.0.0.0 table 6");
+	_rr_from_str ("priority 5 to 0.0.0.0 table 254",
+	              "priority 5 to 0.0.0.0/32");
+	_rr_from_str ("priority 5 from 1.2.3.4 table 15",
+	              "priority 5 from 1.2.3.4/32 table  0xF ",
+	              "priority 5 from 1.2.3.4/32 to 0.0.0.0/0 lookup 15 ");
+	_rr_from_str ("priority 5 from 1.2.3.4 to 0.0.0.0 table 8");
+	_rr_from_str ("priority 5 to a:b:c:: tos 0x16 table 25",
+	              "priority 5 to a:b:c::/128 table 0x19 tos 16",
+	              "priority 5 to a:b:c::/128 lookup 0x19 dsfield 16",
+	              "priority 5 to a:b:c::/128 lookup 0x19 dsfield 16 fwmark 0/0x00",
+	              "priority 5 to a:b:c:: from all lookup 0x19 dsfield 16 fwmark 0x0/0");
+	_rr_from_str ("priority 5 from :: fwmark 0 table 25",
+	              "priority 5 from ::/128 to all table 0x19 fwmark 0/0xFFFFFFFF",
+	              "priority 5 from :: to ::/0 table 0x19 fwmark 0x00/4294967295");
+	_rr_from_str ("priority 5 from :: iif aab table 25");
+	_rr_from_str ("priority 5 from :: iif aab oif er table 25",
+	              "priority 5 from :: table 0x19 dev aab oif er");
+	_rr_from_str ("priority 5 from :: iif a\\\\303b table 25");
+	_rr_from_str ("priority 5 to 0.0.0.0 sport 10 table 6",
+	              "priority 5 to 0.0.0.0 sport 10-10 table 6");
+	_rr_from_str ("not priority 5 to 0.0.0.0 dport 10-133 table 6",
+	              "priority 5 to 0.0.0.0 not dport 10-133 not table 6",
+	              "priority 5 to 0.0.0.0 not dport 10-\\ 133 not table 6");
+	_rr_from_str ("priority 5 to 0.0.0.0 ipproto 10 sport 10 table 6");
+
+	rr1 = _rr_from_str_get ("priority 5 from :: iif aab table 25");
+	g_assert_cmpstr (nm_ip_routing_rule_get_iifname (rr1), ==, "aab");
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, FALSE, ifname_buf);
+	g_assert (!success);
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, TRUE, ifname_buf);
+	g_assert_cmpstr (ifname_buf, ==, "aab");
+	g_assert (success);
+	nm_clear_pointer (&rr1, nm_ip_routing_rule_unref);
+
+	rr1 = _rr_from_str_get ("priority 5 from :: iif a\\\\303\\\\261xb table 254");
+	g_assert_cmpstr (nm_ip_routing_rule_get_iifname (rr1), ==, "a\\303\\261xb");
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, FALSE, ifname_buf);
+	g_assert (!success);
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, TRUE, ifname_buf);
+	g_assert_cmpstr (ifname_buf, ==, "a\303\261xb");
+	g_assert (success);
+	nm_clear_pointer (&rr1, nm_ip_routing_rule_unref);
+
+	rr1 = _rr_from_str_get ("priority 5 from :: oif \\\\101=\\\\303\\\\261xb table 7");
+	g_assert_cmpstr (nm_ip_routing_rule_get_oifname (rr1), ==, "\\101=\\303\\261xb");
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, FALSE, ifname_buf);
+	g_assert_cmpstr (ifname_buf, ==, "A=\303\261xb");
+	g_assert (success);
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, TRUE, ifname_buf);
+	g_assert (!success);
+	nm_clear_pointer (&rr1, nm_ip_routing_rule_unref);
+
+	rr1 = _rr_from_str_get ("priority 5 to 0.0.0.0 tos 0x10 table 7");
+	g_assert_cmpstr (NULL, ==, nm_ip_routing_rule_get_from (rr1));
+	g_assert (!nm_ip_routing_rule_get_from_bin (rr1));
+	g_assert_cmpint (0, ==, nm_ip_routing_rule_get_from_len (rr1));
+	g_assert_cmpstr ("0.0.0.0", ==, nm_ip_routing_rule_get_to (rr1));
+	g_assert (nm_ip_addr_is_null (AF_INET, nm_ip_routing_rule_get_to_bin (rr1)));
+	g_assert_cmpint (32, ==, nm_ip_routing_rule_get_to_len (rr1));
+	g_assert_cmpint (7, ==, nm_ip_routing_rule_get_table (rr1));
+	g_assert_cmpint (0x10, ==, nm_ip_routing_rule_get_tos (rr1));
+	nm_clear_pointer (&rr1, nm_ip_routing_rule_unref);
+
+	rr1 = _rr_from_str_get ("priority 5 from :: iif a\\\\303\\\\261,x;b table 254",
+	                        "priority 5 from :: iif a\\\\303\\\\261,x;b table 254");
+	g_assert_cmpstr (nm_ip_routing_rule_get_iifname (rr1), ==, "a\\303\\261,x;b");
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, FALSE, ifname_buf);
+	g_assert (!success);
+	success = nm_ip_routing_rule_get_xifname_bin (rr1, TRUE, ifname_buf);
+	g_assert_cmpstr (ifname_buf, ==, "a\303\261,x;b");
+	g_assert (success);
+	nm_clear_pointer (&rr1, nm_ip_routing_rule_unref);
+
+}
+
+/*****************************************************************************/
+
 NMTST_DEFINE ();
 
 int
@@ -2663,6 +3024,8 @@ main (int argc, char **argv)
 	g_test_add_func ("/libnm/settings/tc_config/setting/duplicates", test_tc_config_setting_duplicates);
 	g_test_add_func ("/libnm/settings/tc_config/dbus", test_tc_config_dbus);
 
+	g_test_add_func ("/libnm/settings/bridge/vlans", test_bridge_vlans);
+
 #if WITH_JSON_VALIDATION
 	g_test_add_func ("/libnm/settings/team/sync_runner_from_config_roundrobin",
 	                 test_runner_roundrobin_sync_from_config);
@@ -2694,9 +3057,12 @@ main (int argc, char **argv)
 	g_test_add_func ("/libnm/settings/team-port/sycn_from_config_full", test_team_port_full_config);
 #endif
 
-	g_test_add_data_func ("/libnm/settings/roundtrip-conversion/general/0", GINT_TO_POINTER (0), test_roundtrip_conversion);
+	g_test_add_data_func ("/libnm/settings/roundtrip-conversion/general/0",   GINT_TO_POINTER (0), test_roundtrip_conversion);
 	g_test_add_data_func ("/libnm/settings/roundtrip-conversion/wireguard/1", GINT_TO_POINTER (1), test_roundtrip_conversion);
 	g_test_add_data_func ("/libnm/settings/roundtrip-conversion/wireguard/2", GINT_TO_POINTER (2), test_roundtrip_conversion);
+	g_test_add_data_func ("/libnm/settings/roundtrip-conversion/general/3",   GINT_TO_POINTER (3), test_roundtrip_conversion);
+
+	g_test_add_data_func ("/libnm/settings/routing-rule/1", GINT_TO_POINTER (0), test_routing_rule);
 
 	return g_test_run ();
 }