about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/core/devices/nm-device-bridge.c180
-rw-r--r--src/core/devices/nm-device-bridge.h2
-rw-r--r--src/core/devices/nm-device.c2
-rw-r--r--src/core/devices/nm-lldp-listener.c15
-rw-r--r--src/core/nm-policy.c275
-rw-r--r--src/libnm-lldp/nm-lldp-neighbor.c47
-rw-r--r--src/libnm-lldp/nm-lldp-neighbor.h4
-rw-r--r--src/libnm-lldp/nm-lldp-rx-internal.h2
-rw-r--r--src/libnm-lldp/nm-lldp-rx.c2
-rw-r--r--src/libnm-lldp/nm-lldp-rx.h2
-rw-r--r--src/libnm-platform/nm-linux-platform.c172
-rw-r--r--src/libnm-platform/nm-platform-utils.c83
-rw-r--r--src/libnm-platform/nm-platform-utils.h7
-rw-r--r--src/libnm-platform/nm-platform.c49
-rw-r--r--src/libnm-platform/nm-platform.h33
-rw-r--r--src/libnm-platform/nmp-base.h9
-rw-r--r--src/libnm-platform/tests/test-nm-platform.c237
-rw-r--r--src/nmcli/connections.c11
18 files changed, 994 insertions, 138 deletions
diff --git a/src/core/devices/nm-device-bridge.c b/src/core/devices/nm-device-bridge.c
index b256056d..7a496d96 100644
--- a/src/core/devices/nm-device-bridge.c
+++ b/src/core/devices/nm-device-bridge.c
@@ -13,6 +13,7 @@
 #include "NetworkManagerUtils.h"
 #include "nm-device-private.h"
 #include "libnm-platform/nm-platform.h"
+#include "libnm-platform/nm-platform-utils.h"
 #include "nm-device-factory.h"
 #include "libnm-core-aux-intern/nm-libnm-core-utils.h"
 #include "libnm-core-intern/nm-core-internal.h"
@@ -419,20 +420,18 @@ static const Option controller_options[] = {
         0,
     }};
 
-static const NMPlatformBridgeVlan **
-setting_vlans_to_platform(GPtrArray *array)
+static NMPlatformBridgeVlan *
+setting_vlans_to_platform(GPtrArray *array, guint *out_len)
 {
-    NMPlatformBridgeVlan **arr;
-    NMPlatformBridgeVlan  *p_data;
-    guint                  i;
+    NMPlatformBridgeVlan *arr;
+    guint                 i;
 
-    if (!array || !array->len)
+    if (!array || !array->len) {
+        *out_len = 0;
         return NULL;
+    }
 
-    G_STATIC_ASSERT_EXPR(_nm_alignof(NMPlatformBridgeVlan *) >= _nm_alignof(NMPlatformBridgeVlan));
-    arr    = g_malloc((sizeof(NMPlatformBridgeVlan *) * (array->len + 1))
-                   + (sizeof(NMPlatformBridgeVlan) * (array->len)));
-    p_data = (NMPlatformBridgeVlan *) &arr[array->len + 1];
+    arr = g_new(NMPlatformBridgeVlan, array->len);
 
     for (i = 0; i < array->len; i++) {
         NMBridgeVlan *vlan = array->pdata[i];
@@ -440,16 +439,16 @@ setting_vlans_to_platform(GPtrArray *array)
 
         nm_bridge_vlan_get_vid_range(vlan, &vid_start, &vid_end);
 
-        p_data[i] = (NMPlatformBridgeVlan){
+        arr[i] = (NMPlatformBridgeVlan){
             .vid_start = vid_start,
             .vid_end   = vid_end,
             .pvid      = nm_bridge_vlan_is_pvid(vlan),
             .untagged  = nm_bridge_vlan_is_untagged(vlan),
         };
-        arr[i] = &p_data[i];
     }
-    arr[i] = NULL;
-    return (const NMPlatformBridgeVlan **) arr;
+
+    *out_len = array->len;
+    return arr;
 }
 
 static void
@@ -639,15 +638,16 @@ is_bridge_pvid_changed(NMDevice *device, NMSettingBridge *s_bridge)
 static gboolean
 bridge_set_vlan_options(NMDevice *device, NMSettingBridge *s_bridge, gboolean is_reapply)
 {
-    NMDeviceBridge                      *self = NM_DEVICE_BRIDGE(device);
-    gconstpointer                        hwaddr;
-    size_t                               length;
-    gboolean                             enabled;
-    guint16                              pvid;
-    NMPlatform                          *plat;
-    int                                  ifindex;
-    gs_unref_ptrarray GPtrArray         *vlans      = NULL;
-    gs_free const NMPlatformBridgeVlan **plat_vlans = NULL;
+    NMDeviceBridge               *self = NM_DEVICE_BRIDGE(device);
+    gconstpointer                 hwaddr;
+    size_t                        length;
+    gboolean                      enabled;
+    guint16                       pvid;
+    NMPlatform                   *plat;
+    int                           ifindex;
+    gs_unref_ptrarray GPtrArray  *vlans      = NULL;
+    gs_free NMPlatformBridgeVlan *plat_vlans = NULL;
+    guint                         num_vlans;
 
     if (self->vlan_configured)
         return TRUE;
@@ -664,7 +664,7 @@ bridge_set_vlan_options(NMDevice *device, NMSettingBridge *s_bridge, gboolean is
                                                 .vlan_filtering_val    = FALSE,
                                                 .vlan_default_pvid_has = TRUE,
                                                 .vlan_default_pvid_val = 1}));
-        nm_platform_link_set_bridge_vlans(plat, ifindex, FALSE, NULL);
+        nm_platform_link_set_bridge_vlans(plat, ifindex, FALSE, NULL, 0);
         return TRUE;
     }
 
@@ -696,7 +696,7 @@ bridge_set_vlan_options(NMDevice *device, NMSettingBridge *s_bridge, gboolean is
                                                 .vlan_default_pvid_val = 0}));
 
         /* Clear all existing VLANs */
-        if (!nm_platform_link_set_bridge_vlans(plat, ifindex, FALSE, NULL))
+        if (!nm_platform_link_set_bridge_vlans(plat, ifindex, FALSE, NULL, 0))
             return FALSE;
 
         /* Now set the default PVID. After this point the kernel creates
@@ -714,8 +714,9 @@ bridge_set_vlan_options(NMDevice *device, NMSettingBridge *s_bridge, gboolean is
     /* Create VLANs only after setting the default PVID, so that
      * any PVID VLAN overrides the bridge's default PVID. */
     g_object_get(s_bridge, NM_SETTING_BRIDGE_VLANS, &vlans, NULL);
-    plat_vlans = setting_vlans_to_platform(vlans);
-    if (plat_vlans && !nm_platform_link_set_bridge_vlans(plat, ifindex, FALSE, plat_vlans))
+    plat_vlans = setting_vlans_to_platform(vlans, &num_vlans);
+    if (plat_vlans
+        && !nm_platform_link_set_bridge_vlans(plat, ifindex, FALSE, plat_vlans, num_vlans))
         return FALSE;
 
     nm_platform_link_set_bridge_info(plat,
@@ -728,6 +729,121 @@ bridge_set_vlan_options(NMDevice *device, NMSettingBridge *s_bridge, gboolean is
     return TRUE;
 }
 
+static NMPlatformBridgeVlan *
+merge_bridge_vlan_default_pvid(NMPlatformBridgeVlan *vlans, guint *num_vlans, guint default_pvid)
+{
+    NMPlatformBridgeVlan *vlan;
+    gboolean              has_pvid = FALSE;
+    guint                 i;
+
+    for (i = 0; i < *num_vlans; i++) {
+        if (vlans[i].pvid) {
+            has_pvid = TRUE;
+            break;
+        }
+    }
+
+    /* search if the list of VLANs already contains the default PVID */
+    vlan = NULL;
+    for (i = 0; i < *num_vlans; i++) {
+        if (default_pvid >= vlans[i].vid_start && default_pvid <= vlans[i].vid_end) {
+            vlan = &vlans[i];
+            break;
+        }
+    }
+
+    if (!vlan) {
+        /* VLAN id not found, append the default PVID at the end.
+         * Set the PVID flag only if the port didn't have one. */
+        vlans = g_realloc_n(vlans, *num_vlans + 1, sizeof(NMPlatformBridgeVlan));
+        (*num_vlans)++;
+        vlans[*num_vlans - 1] = (NMPlatformBridgeVlan){
+            .vid_start = default_pvid,
+            .vid_end   = default_pvid,
+            .untagged  = TRUE,
+            .pvid      = !has_pvid,
+        };
+    }
+
+    return vlans;
+}
+
+void
+nm_device_reapply_bridge_port_vlans(NMDevice *device)
+{
+    NMDevice                     *self = device; /* for logging */
+    NMSettingBridgePort          *s_bridge_port;
+    NMDevice                     *controller;
+    NMSettingBridge              *s_bridge;
+    gs_unref_ptrarray GPtrArray  *tmp_vlans         = NULL;
+    gs_free NMPlatformBridgeVlan *setting_vlans     = NULL;
+    gs_free NMPlatformBridgeVlan *plat_vlans        = NULL;
+    guint                         num_setting_vlans = 0;
+    guint                         num_plat_vlans    = 0;
+    NMPlatform                   *plat;
+    int                           ifindex;
+    gboolean                      do_reapply;
+
+    s_bridge_port = nm_device_get_applied_setting(device, NM_TYPE_SETTING_BRIDGE_PORT);
+    if (!s_bridge_port)
+        return;
+
+    controller = nm_device_get_controller(device);
+    if (!controller)
+        return;
+
+    s_bridge = nm_device_get_applied_setting(controller, NM_TYPE_SETTING_BRIDGE);
+    if (!s_bridge)
+        return;
+
+    if (nm_setting_bridge_get_vlan_filtering(s_bridge)) {
+        g_object_get(s_bridge_port, NM_SETTING_BRIDGE_PORT_VLANS, &tmp_vlans, NULL);
+        setting_vlans = setting_vlans_to_platform(tmp_vlans, &num_setting_vlans);
+
+        /* During a regular activation, we first set the default_pvid on the bridge
+        * (which creates the PVID VLAN on the port) and then add the VLANs on the port.
+        * This ensures that the PVID VLAN is inherited from the bridge, but it's
+        * overridden if the port specifies one.
+        * During a reapply on the port, we are not going to touch the bridge and
+        * so we need to merge manually the PVID from the bridge with the port VLANs. */
+        setting_vlans =
+            merge_bridge_vlan_default_pvid(setting_vlans,
+                                           &num_setting_vlans,
+                                           nm_setting_bridge_get_vlan_default_pvid(s_bridge));
+    }
+
+    plat    = nm_device_get_platform(device);
+    ifindex = nm_device_get_ifindex(device);
+
+    if (!nm_platform_link_get_bridge_vlans(plat, ifindex, &plat_vlans, &num_plat_vlans)) {
+        _LOGD(LOGD_DEVICE, "reapply-bridge-port-vlans: can't get current VLANs from platform");
+        do_reapply = TRUE;
+    } else {
+        nmp_utils_bridge_vlan_normalize(setting_vlans, &num_setting_vlans);
+        nmp_utils_bridge_vlan_normalize(plat_vlans, &num_plat_vlans);
+        if (!nmp_utils_bridge_normalized_vlans_equal(setting_vlans,
+                                                     num_setting_vlans,
+                                                     plat_vlans,
+                                                     num_plat_vlans)) {
+            _LOGD(LOGD_DEVICE, "reapply-bridge-port-vlans: VLANs in platform need reapply");
+            do_reapply = TRUE;
+        } else {
+            _LOGD(LOGD_DEVICE, "reapply-bridge-port-vlans: VLANs in platform didn't change");
+            do_reapply = FALSE;
+        }
+    }
+
+    if (do_reapply) {
+        nm_platform_link_set_bridge_vlans(plat, ifindex, TRUE, NULL, 0);
+        if (num_setting_vlans > 0)
+            nm_platform_link_set_bridge_vlans(plat,
+                                              ifindex,
+                                              TRUE,
+                                              setting_vlans,
+                                              num_setting_vlans);
+    }
+}
+
 static void
 _platform_lnk_bridge_init_from_setting(NMSettingBridge *s_bridge, NMPlatformLnkBridge *props)
 {
@@ -937,13 +1053,14 @@ attach_port(NMDevice                  *device,
             bridge_set_vlan_options(device, s_bridge, FALSE);
 
         if (nm_setting_bridge_get_vlan_filtering(s_bridge)) {
-            gs_free const NMPlatformBridgeVlan **plat_vlans = NULL;
-            gs_unref_ptrarray GPtrArray         *vlans      = NULL;
+            gs_free NMPlatformBridgeVlan *plat_vlans = NULL;
+            gs_unref_ptrarray GPtrArray  *vlans      = NULL;
+            guint                         num_vlans;
 
             if (s_port)
                 g_object_get(s_port, NM_SETTING_BRIDGE_PORT_VLANS, &vlans, NULL);
 
-            plat_vlans = setting_vlans_to_platform(vlans);
+            plat_vlans = setting_vlans_to_platform(vlans, &num_vlans);
 
             /* Since the link was just enportd, there are no existing VLANs
              * (except for the default one) and so there's no need to flush. */
@@ -952,7 +1069,8 @@ attach_port(NMDevice                  *device,
                 && !nm_platform_link_set_bridge_vlans(nm_device_get_platform(port),
                                                       nm_device_get_ifindex(port),
                                                       TRUE,
-                                                      plat_vlans))
+                                                      plat_vlans,
+                                                      num_vlans))
                 return FALSE;
         }
 
diff --git a/src/core/devices/nm-device-bridge.h b/src/core/devices/nm-device-bridge.h
index 6d9f1614..f9be7580 100644
--- a/src/core/devices/nm-device-bridge.h
+++ b/src/core/devices/nm-device-bridge.h
@@ -27,4 +27,6 @@ extern const NMBtVTableNetworkServer *nm_bt_vtable_network_server;
 
 void _nm_device_bridge_notify_unregister_bt_nap(NMDevice *device, const char *reason);
 
+void nm_device_reapply_bridge_port_vlans(NMDevice *device);
+
 #endif /* __NETWORKMANAGER_DEVICE_BRIDGE_H__ */
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index 725241d5..799aca04 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -13881,6 +13881,8 @@ check_and_reapply_connection(NMDevice            *self,
     if (priv->state >= NM_DEVICE_STATE_ACTIVATED)
         nm_device_update_metered(self);
 
+    nm_device_reapply_bridge_port_vlans(self);
+
     sett_conn = nm_device_get_settings_connection(self);
     if (sett_conn) {
         nm_settings_connection_autoconnect_blocked_reason_set(
diff --git a/src/core/devices/nm-lldp-listener.c b/src/core/devices/nm-lldp-listener.c
index ac7e97f0..59c8f54c 100644
--- a/src/core/devices/nm-lldp-listener.c
+++ b/src/core/devices/nm-lldp-listener.c
@@ -704,9 +704,16 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
 
 /*****************************************************************************/
 
+static void
+nmtst_lldp_event_handler(NMLldpRX *lldp, NMLldpRXEvent event, NMLldpNeighbor *n, void *user_data)
+{
+    g_assert_not_reached();
+}
+
 GVariant *
 nmtst_lldp_parse_from_raw(const guint8 *raw_data, gsize raw_len)
 {
+    nm_auto(nm_lldp_rx_unrefp) NMLldpRX             *lldp_rx     = NULL;
     nm_auto(nm_lldp_neighbor_unrefp) NMLldpNeighbor *neighbor_nm = NULL;
     nm_auto(lldp_neighbor_freep) LldpNeighbor       *neigh       = NULL;
     GVariant                                        *variant;
@@ -714,7 +721,13 @@ nmtst_lldp_parse_from_raw(const guint8 *raw_data, gsize raw_len)
     g_assert(raw_data);
     g_assert(raw_len > 0);
 
-    neighbor_nm = nm_lldp_neighbor_new_from_raw(raw_data, raw_len);
+    lldp_rx = nm_lldp_rx_new(&((NMLldpRXConfig){
+        .ifindex       = 1,
+        .neighbors_max = MAX_NEIGHBORS,
+        .callback      = nmtst_lldp_event_handler,
+    }));
+
+    neighbor_nm = nm_lldp_neighbor_new_from_raw(lldp_rx, raw_data, raw_len);
     g_assert(neighbor_nm);
 
     neigh = lldp_neighbor_new(neighbor_nm);
diff --git a/src/core/nm-policy.c b/src/core/nm-policy.c
index 694f5905..93b52526 100644
--- a/src/core/nm-policy.c
+++ b/src/core/nm-policy.c
@@ -17,6 +17,7 @@
 
 #include "NetworkManagerUtils.h"
 #include "devices/nm-device.h"
+#include "devices/nm-device-factory.h"
 #include "dns/nm-dns-manager.h"
 #include "nm-act-request.h"
 #include "nm-auth-utils.h"
@@ -47,6 +48,10 @@ NM_GOBJECT_PROPERTIES_DEFINE(NMPolicy,
                              PROP_ACTIVATING_IP4_AC,
                              PROP_ACTIVATING_IP6_AC, );
 
+#define HOSTNAME_RETRY_INTERVAL_MIN        30U
+#define HOSTNAME_RETRY_INTERVAL_MAX        (60U * 60 * 12) /* 12 hours */
+#define HOSTNAME_RETRY_INTERVAL_MULTIPLIER 8U
+
 typedef struct {
     NMManager          *manager;
     NMNetns            *netns;
@@ -78,14 +83,21 @@ typedef struct {
     char                *orig_hostname;     /* hostname at NM start time */
     char                *cur_hostname;      /* hostname we want to assign */
     char                *cur_hostname_full; /* similar to @last_hostname, but before shortening */
-    char *
-        last_hostname; /* last hostname NM set (to detect if someone else changed it in the meanwhile) */
+    char                *last_hostname;     /* last hostname NM set (to detect if someone else
+                                             * changed it in the meanwhile) */
+    struct {
+        GSource *source;
+        guint    interval_sec;
+        gboolean do_restart; /* when something changes, set this to TRUE so that the next retry
+                              * will restart from the lowest timeout. */
+    } hostname_retry;
 
     bool changing_hostname : 1; /* hostname set operation in progress */
     bool dhcp_hostname : 1;     /* current hostname was set from dhcp */
     bool updating_dns : 1;
 
     GArray *ip6_prefix_delegations; /* pool of ip6 prefixes delegated to all devices */
+
 } NMPolicyPrivate;
 
 struct _NMPolicy {
@@ -134,9 +146,10 @@ _PRIV_TO_SELF(NMPolicyPrivate *priv)
 
 /*****************************************************************************/
 
-static void      update_system_hostname(NMPolicy *self, const char *msg);
-static void      nm_policy_device_recheck_auto_activate_all_schedule(NMPolicy *self);
+static void update_system_hostname(NMPolicy *self, const char *msg, gboolean reset_retry_interval);
+static void nm_policy_device_recheck_auto_activate_all_schedule(NMPolicy *self);
 static NMDevice *get_default_device(NMPolicy *self, int addr_family);
+static gboolean  hostname_retry_cb(gpointer user_data);
 
 /*****************************************************************************/
 
@@ -557,7 +570,56 @@ _get_hostname(NMPolicy *self)
 }
 
 static void
-_set_hostname(NMPolicy *self, const char *new_hostname, const char *msg)
+hostname_retry_schedule(NMPolicy *self)
+{
+    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
+
+    if (priv->hostname_retry.source && !priv->hostname_retry.do_restart)
+        return;
+
+    nm_clear_g_source_inst(&priv->hostname_retry.source);
+
+    if (priv->hostname_retry.do_restart)
+        priv->hostname_retry.interval_sec = 0;
+
+    priv->hostname_retry.interval_sec *= HOSTNAME_RETRY_INTERVAL_MULTIPLIER;
+    priv->hostname_retry.interval_sec = NM_CLAMP(priv->hostname_retry.interval_sec,
+                                                 HOSTNAME_RETRY_INTERVAL_MIN,
+                                                 HOSTNAME_RETRY_INTERVAL_MAX);
+
+    _LOGT(LOGD_DNS,
+          "hostname-retry: schedule in %u seconds%s",
+          priv->hostname_retry.interval_sec,
+          priv->hostname_retry.do_restart ? " (restarted)" : "");
+    priv->hostname_retry.source =
+        nm_g_timeout_add_seconds_source(priv->hostname_retry.interval_sec, hostname_retry_cb, self);
+
+    priv->hostname_retry.do_restart = FALSE;
+}
+
+static gboolean
+hostname_retry_cb(gpointer user_data)
+{
+    NMPolicy        *self = NM_POLICY(user_data);
+    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
+    const CList     *tmp_lst;
+    NMDevice        *device;
+
+    _LOGT(LOGD_DNS, "hostname-retry: timeout");
+
+    nm_clear_g_source_inst(&priv->hostname_retry.source);
+
+    /* Clear any cached DNS results before retrying */
+    nm_manager_for_each_device (priv->manager, device, tmp_lst) {
+        nm_device_clear_dns_lookup_data(device, "hostname retry timeout");
+    }
+    update_system_hostname(self, "hostname retry timeout", FALSE);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+_set_hostname(NMPolicy *self, const char *new_hostname, const char *msg, gboolean do_retry)
 {
     NMPolicyPrivate *priv         = NM_POLICY_GET_PRIVATE(self);
     gs_free char    *old_hostname = NULL;
@@ -611,6 +673,15 @@ _set_hostname(NMPolicy *self, const char *new_hostname, const char *msg)
         priv->updating_dns = FALSE;
     }
 
+    if (!do_retry) {
+        _LOGT(LOGD_DNS, "hostname-retry: clear");
+        nm_clear_g_source_inst(&priv->hostname_retry.source);
+        priv->hostname_retry.interval_sec = 0;
+        priv->hostname_retry.do_restart   = FALSE;
+    } else if (!priv->hostname_retry.source) {
+        hostname_retry_schedule(self);
+    }
+
     /* Finally, set kernel hostname */
     nm_assert(!priv->cur_hostname || priv->cur_hostname[0]);
     name = priv->cur_hostname ?: FALLBACK_HOSTNAME4;
@@ -796,7 +867,7 @@ device_dns_lookup_done(NMDevice *device, gpointer user_data)
 
     g_signal_handlers_disconnect_by_func(device, device_dns_lookup_done, self);
 
-    update_system_hostname(self, "lookup finished");
+    update_system_hostname(self, "lookup finished", FALSE);
 }
 
 static void
@@ -809,12 +880,28 @@ device_carrier_changed(NMDevice *device, GParamSpec *pspec, gpointer user_data)
     if (nm_device_has_carrier(device)) {
         g_signal_handlers_disconnect_by_func(device, device_carrier_changed, priv);
         msg = g_strdup_printf("device '%s' got carrier", nm_device_get_iface(device));
-        update_system_hostname(self, msg);
+        update_system_hostname(self, msg, TRUE);
     }
 }
 
+/*
+ * This function evaluates different sources (static configuration, DHCP, DNS, ...)
+ * to set the system hostname.
+ *
+ * When the function needs to perform a blocking action like a DNS resolution, it
+ * subscribes to a signal for the completion event, registering a callback that
+ * invokes this function again. In the new invocation, any previous DNS result is
+ * cached and doesn't need a new resolution.
+ *
+ * In case no hostname is found when after sources have been evaluated, it schedules
+ * a timer to retry later with an interval that is increased at each attempt. When
+ * this function is called after something changed (for example, carrier went up, a
+ * new address was added), @reset_retry_interval should be set to TRUE so that the
+ * next retry will use the smallest interval. In this way, it can quickly adapt to
+ * temporary misconfigurations at boot or when the network environment changes.
+ */
 static void
-update_system_hostname(NMPolicy *self, const char *msg)
+update_system_hostname(NMPolicy *self, const char *msg, gboolean reset_retry_interval)
 {
     NMPolicyPrivate       *priv = NM_POLICY_GET_PRIVATE(self);
     const char            *configured_hostname;
@@ -829,6 +916,9 @@ update_system_hostname(NMPolicy *self, const char *msg)
 
     g_return_if_fail(self != NULL);
 
+    if (reset_retry_interval)
+        priv->hostname_retry.do_restart = TRUE;
+
     if (priv->hostname_mode == NM_POLICY_HOSTNAME_MODE_NONE) {
         _LOGT(LOGD_DNS, "set-hostname: hostname is unmanaged");
         return;
@@ -871,7 +961,7 @@ update_system_hostname(NMPolicy *self, const char *msg)
     /* Try a persistent hostname first */
     configured_hostname = nm_hostname_manager_get_static_hostname(priv->hostname_manager);
     if (configured_hostname && nm_utils_is_specific_hostname(configured_hostname)) {
-        _set_hostname(self, configured_hostname, "from system configuration");
+        _set_hostname(self, configured_hostname, "from system configuration", FALSE);
         priv->dhcp_hostname = FALSE;
         return;
     }
@@ -908,7 +998,10 @@ update_system_hostname(NMPolicy *self, const char *msg)
                 if (dhcp_hostname && dhcp_hostname[0]) {
                     p = nm_str_skip_leading_spaces(dhcp_hostname);
                     if (p[0]) {
-                        _set_hostname(self, p, info->IS_IPv4 ? "from DHCPv4" : "from DHCPv6");
+                        _set_hostname(self,
+                                      p,
+                                      info->IS_IPv4 ? "from DHCPv4" : "from DHCPv6",
+                                      FALSE);
                         priv->dhcp_hostname = TRUE;
                         return;
                     }
@@ -936,7 +1029,7 @@ update_system_hostname(NMPolicy *self, const char *msg)
                                      priv);
                 }
                 if (result) {
-                    _set_hostname(self, result, "from address lookup");
+                    _set_hostname(self, result, "from address lookup", FALSE);
                     return;
                 }
                 if (wait) {
@@ -951,8 +1044,10 @@ update_system_hostname(NMPolicy *self, const char *msg)
     }
 
     /* If an hostname was set outside NetworkManager keep it */
-    if (external_hostname)
+    if (external_hostname) {
+        hostname_retry_schedule(self);
         return;
+    }
 
     if (priv->hostname_mode == NM_POLICY_HOSTNAME_MODE_DHCP) {
         /* In dhcp hostname-mode, the hostname is updated only if it comes from
@@ -961,7 +1056,7 @@ update_system_hostname(NMPolicy *self, const char *msg)
          * so reset the hostname to the previous value
          */
         if (priv->dhcp_hostname) {
-            _set_hostname(self, priv->orig_hostname, "reset dhcp hostname");
+            _set_hostname(self, priv->orig_hostname, "reset dhcp hostname", TRUE);
             priv->dhcp_hostname = FALSE;
         }
         return;
@@ -973,11 +1068,11 @@ update_system_hostname(NMPolicy *self, const char *msg)
      * set externally to NM
      */
     if (priv->orig_hostname) {
-        _set_hostname(self, priv->orig_hostname, "from system startup");
+        _set_hostname(self, priv->orig_hostname, "from system startup", TRUE);
         return;
     }
 
-    _set_hostname(self, NULL, "no hostname found");
+    _set_hostname(self, NULL, "no hostname found", TRUE);
 }
 
 static void
@@ -1254,7 +1349,7 @@ update_routing_and_dns(NMPolicy *self, gboolean force_update, NMDevice *changed_
     update_ip6_routing(self, force_update);
 
     /* Update the system hostname */
-    update_system_hostname(self, "routing and dns");
+    update_system_hostname(self, "routing and dns", FALSE);
 
     nm_dns_manager_end_updates(priv->dns_manager, __func__);
 }
@@ -1571,7 +1666,7 @@ _static_hostname_changed_cb(NMHostnameManager *hostname_manager,
     NMPolicyPrivate *priv = user_data;
     NMPolicy        *self = _PRIV_TO_SELF(priv);
 
-    update_system_hostname(self, "hostname changed");
+    update_system_hostname(self, "hostname changed", FALSE);
 }
 
 void
@@ -1774,6 +1869,74 @@ _connection_autoconnect_retries_set(NMPolicy             *self,
 }
 
 static void
+unblock_autoconnect_for_children(NMPolicy   *self,
+                                 const char *parent_device,
+                                 const char *parent_uuid_settings,
+                                 const char *parent_uuid_applied,
+                                 const char *parent_mac_addr,
+                                 gboolean    reset_devcon_autoconnect)
+{
+    NMPolicyPrivate             *priv = NM_POLICY_GET_PRIVATE(self);
+    NMSettingsConnection *const *connections;
+    gboolean                     changed;
+    guint                        i;
+
+    _LOGT(LOGD_CORE,
+          "block-autoconnect: unblocking child profiles for parent ifname=%s%s%s, uuid=%s%s%s"
+          "%s%s%s",
+          NM_PRINT_FMT_QUOTE_STRING(parent_device),
+          NM_PRINT_FMT_QUOTE_STRING(parent_uuid_settings),
+          NM_PRINT_FMT_QUOTED(parent_uuid_applied,
+                              ", applied-uuid=\"",
+                              parent_uuid_applied,
+                              "\"",
+                              ""));
+
+    changed     = FALSE;
+    connections = nm_settings_get_connections(priv->settings, NULL);
+    for (i = 0; connections[i]; i++) {
+        NMSettingsConnection *sett_conn = connections[i];
+        NMConnection         *connection;
+        NMDeviceFactory      *factory;
+        const char           *parent_name = NULL;
+
+        connection = nm_settings_connection_get_connection(sett_conn);
+        factory    = nm_device_factory_manager_find_factory_for_connection(connection);
+        if (factory)
+            parent_name = nm_device_factory_get_connection_parent(factory, connection);
+
+        if (!parent_name)
+            continue;
+
+        if (!NM_IN_STRSET(parent_name,
+                          parent_device,
+                          parent_uuid_applied,
+                          parent_uuid_settings,
+                          parent_mac_addr))
+            continue;
+
+        if (reset_devcon_autoconnect) {
+            if (nm_manager_devcon_autoconnect_retries_reset(priv->manager, NULL, sett_conn))
+                changed = TRUE;
+        }
+
+        /* unblock the devices associated with that connection */
+        if (nm_manager_devcon_autoconnect_blocked_reason_set(
+                priv->manager,
+                NULL,
+                sett_conn,
+                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
+                FALSE)) {
+            if (!nm_settings_connection_autoconnect_is_blocked(sett_conn))
+                changed = TRUE;
+        }
+    }
+
+    if (changed)
+        nm_policy_device_recheck_auto_activate_all_schedule(self);
+}
+
+static void
 unblock_autoconnect_for_ports(NMPolicy   *self,
                               const char *controller_device,
                               const char *controller_uuid_settings,
@@ -1856,16 +2019,21 @@ unblock_autoconnect_for_ports_for_sett_conn(NMPolicy *self, NMSettingsConnection
 }
 
 static void
-activate_slave_connections(NMPolicy *self, NMDevice *device)
-{
-    const char   *master_device;
-    const char   *master_uuid_settings = NULL;
-    const char   *master_uuid_applied  = NULL;
+activate_port_or_children_connections(NMPolicy *self,
+                                      NMDevice *device,
+                                      gboolean  activate_children_connections_only)
+{
+    const char   *controller_device;
+    const char   *controller_uuid_settings = NULL;
+    const char   *controller_uuid_applied  = NULL;
+    const char   *parent_mac_addr          = NULL;
     NMActRequest *req;
     gboolean      internal_activation = FALSE;
 
-    master_device = nm_device_get_iface(device);
-    nm_assert(master_device);
+    controller_device = nm_device_get_iface(device);
+    nm_assert(controller_device);
+
+    parent_mac_addr = nm_device_get_permanent_hw_address(device);
 
     req = nm_device_get_act_request(device);
     if (req) {
@@ -1875,25 +2043,33 @@ activate_slave_connections(NMPolicy *self, NMDevice *device)
 
         sett_conn = nm_active_connection_get_settings_connection(NM_ACTIVE_CONNECTION(req));
         if (sett_conn)
-            master_uuid_settings = nm_settings_connection_get_uuid(sett_conn);
+            controller_uuid_settings = nm_settings_connection_get_uuid(sett_conn);
 
         connection = nm_active_connection_get_applied_connection(NM_ACTIVE_CONNECTION(req));
         if (connection)
-            master_uuid_applied = nm_connection_get_uuid(connection);
+            controller_uuid_applied = nm_connection_get_uuid(connection);
 
-        if (nm_streq0(master_uuid_settings, master_uuid_applied))
-            master_uuid_applied = NULL;
+        if (nm_streq0(controller_uuid_settings, controller_uuid_applied))
+            controller_uuid_applied = NULL;
 
         subject = nm_active_connection_get_subject(NM_ACTIVE_CONNECTION(req));
         internal_activation =
             subject && (nm_auth_subject_get_subject_type(subject) == NM_AUTH_SUBJECT_TYPE_INTERNAL);
     }
 
-    unblock_autoconnect_for_ports(self,
-                                  master_device,
-                                  master_uuid_settings,
-                                  master_uuid_applied,
-                                  !internal_activation);
+    if (!activate_children_connections_only) {
+        unblock_autoconnect_for_ports(self,
+                                      controller_device,
+                                      controller_uuid_settings,
+                                      controller_uuid_applied,
+                                      !internal_activation);
+    }
+    unblock_autoconnect_for_children(self,
+                                     controller_device,
+                                     controller_uuid_settings,
+                                     controller_uuid_applied,
+                                     parent_mac_addr,
+                                     !internal_activation);
 }
 
 static gboolean
@@ -2061,13 +2237,12 @@ device_state_changed(NMDevice           *device,
                 }
                 break;
             case NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED:
-                /* A connection that fails due to dependency-failed is not
-                 * able to reconnect until the master connection activates
-                 * again; when this happens, the master clears the blocked
-                 * reason for all its slaves in activate_slave_connections()
-                 * and tries to reconnect them. For this to work, the slave
-                 * should be marked as blocked when it fails with
-                 * dependency-failed.
+                /* A connection that fails due to dependency-failed is not able to
+                 * reconnect until the connection it depends on activates again;
+                 * when this happens, the controller or parent clears the blocked
+                 * reason for all its dependent devices in activate_port_or_children_connections()
+                 * and tries to reconnect them. For this to work, the port should
+                 * be marked as blocked when it fails with dependency-failed.
                  */
                 _LOGD(LOGD_DEVICE,
                       "block-autoconnect: connection[%p] (%s) now blocked from autoconnect due to "
@@ -2111,6 +2286,11 @@ device_state_changed(NMDevice           *device,
         }
         break;
     case NM_DEVICE_STATE_ACTIVATED:
+        if (nm_device_get_device_type(device) == NM_DEVICE_TYPE_OVS_INTERFACE) {
+            /* When parent is ovs-interface, the kernel link is only created in stage3, we have to
+            * delay unblocking the children and schedule them for activation until parent is activated */
+            activate_port_or_children_connections(self, device, TRUE);
+        }
         if (sett_conn) {
             /* Reset auto retries back to default since connection was successful */
             nm_manager_devcon_autoconnect_retries_reset(priv->manager, device, sett_conn);
@@ -2133,7 +2313,7 @@ device_state_changed(NMDevice           *device,
         update_ip_dns(self, AF_INET6, device);
         update_ip4_routing(self, TRUE);
         update_ip6_routing(self, TRUE);
-        update_system_hostname(self, "routing and dns");
+        update_system_hostname(self, "routing and dns", TRUE);
         nm_dns_manager_end_updates(priv->dns_manager, __func__);
 
         break;
@@ -2195,9 +2375,9 @@ device_state_changed(NMDevice           *device,
         break;
 
     case NM_DEVICE_STATE_PREPARE:
-        /* Reset auto-connect retries of all slaves and schedule them for
+        /* Reset auto-connect retries of all ports or children and schedule them for
          * activation. */
-        activate_slave_connections(self, device);
+        activate_port_or_children_connections(self, device, FALSE);
 
         /* Now that the device state is progressing, we don't care
          * anymore for the AC state. */
@@ -2281,7 +2461,7 @@ device_l3cd_changed(NMDevice             *device,
         update_ip6_routing(self, TRUE);
         /* FIXME: since we already monitor platform addresses changes,
          * this is probably no longer necessary? */
-        update_system_hostname(self, "ip conf");
+        update_system_hostname(self, "ip conf", FALSE);
     } else {
         nm_dns_manager_set_ip_config(priv->dns_manager,
                                      AF_UNSPEC,
@@ -2303,7 +2483,7 @@ device_platform_address_changed(NMDevice *device, gpointer user_data)
 
     state = nm_device_get_state(device);
     if (state > NM_DEVICE_STATE_DISCONNECTED && state < NM_DEVICE_STATE_DEACTIVATING) {
-        update_system_hostname(self, "address changed");
+        update_system_hostname(self, "address changed", TRUE);
     }
 }
 
@@ -2642,7 +2822,7 @@ dns_config_changed(NMDnsManager *dns_manager, gpointer user_data)
             nm_device_clear_dns_lookup_data(device, "DNS configuration changed");
         }
 
-        update_system_hostname(self, "DNS configuration changed");
+        update_system_hostname(self, "DNS configuration changed", FALSE);
     }
 
     nm_dispatcher_call_dns_change();
@@ -2913,7 +3093,7 @@ constructed(GObject *object)
     G_OBJECT_CLASS(nm_policy_parent_class)->constructed(object);
 
     _LOGD(LOGD_DNS, "hostname-mode: %s", _hostname_mode_to_string(priv->hostname_mode));
-    update_system_hostname(self, "initial hostname");
+    update_system_hostname(self, "initial hostname", FALSE);
 }
 
 NMPolicy *
@@ -2971,6 +3151,7 @@ dispose(GObject *object)
 
     nm_clear_g_source_inst(&priv->reset_connections_retries_idle_source);
     nm_clear_g_source_inst(&priv->device_recheck_auto_activate_all_idle_source);
+    nm_clear_g_source_inst(&priv->hostname_retry.source);
 
     nm_clear_g_free(&priv->orig_hostname);
     nm_clear_g_free(&priv->cur_hostname);
diff --git a/src/libnm-lldp/nm-lldp-neighbor.c b/src/libnm-lldp/nm-lldp-neighbor.c
index 0379cf38..3a76ea31 100644
--- a/src/libnm-lldp/nm-lldp-neighbor.c
+++ b/src/libnm-lldp/nm-lldp-neighbor.c
@@ -65,6 +65,7 @@ parse_string(NMLldpRX *lldp_rx, char **s, const void *q, size_t n)
     const char *p = q;
     char       *k;
 
+    nm_assert(lldp_rx);
     nm_assert(s);
     nm_assert(p || n == 0);
 
@@ -99,31 +100,33 @@ parse_string(NMLldpRX *lldp_rx, char **s, const void *q, size_t n)
 }
 
 int
-nm_lldp_neighbor_parse(NMLldpNeighbor *n)
+nm_lldp_neighbor_parse(NMLldpRX *lldp_rx, NMLldpNeighbor *n)
 {
     struct ether_header h;
     const uint8_t      *p;
     size_t              left;
     int                 r;
 
+    nm_assert(lldp_rx);
     nm_assert(n);
+    nm_assert(!n->lldp_rx);
 
     if (n->raw_size < sizeof(struct ether_header)) {
-        _LOG2D(n->lldp_rx, "Received truncated packet, ignoring.");
+        _LOG2D(lldp_rx, "Received truncated packet, ignoring.");
         return -NME_UNSPEC;
     }
 
     memcpy(&h, NM_LLDP_NEIGHBOR_RAW(n), sizeof(h));
 
     if (h.ether_type != htobe16(NM_ETHERTYPE_LLDP)) {
-        _LOG2D(n->lldp_rx, "Received packet with wrong type, ignoring.");
+        _LOG2D(lldp_rx, "Received packet with wrong type, ignoring.");
         return -NME_UNSPEC;
     }
 
     if (h.ether_dhost[0] != 0x01 || h.ether_dhost[1] != 0x80 || h.ether_dhost[2] != 0xc2
         || h.ether_dhost[3] != 0x00 || h.ether_dhost[4] != 0x00
         || !NM_IN_SET(h.ether_dhost[5], 0x00, 0x03, 0x0e)) {
-        _LOG2D(n->lldp_rx, "Received packet with wrong destination address, ignoring.");
+        _LOG2D(lldp_rx, "Received packet with wrong destination address, ignoring.");
         return -NME_UNSPEC;
     }
 
@@ -138,7 +141,7 @@ nm_lldp_neighbor_parse(NMLldpNeighbor *n)
         uint16_t length;
 
         if (left < 2) {
-            _LOG2D(n->lldp_rx, "TLV lacks header, ignoring.");
+            _LOG2D(lldp_rx, "TLV lacks header, ignoring.");
             return -NME_UNSPEC;
         }
 
@@ -147,14 +150,14 @@ nm_lldp_neighbor_parse(NMLldpNeighbor *n)
         p += 2, left -= 2;
 
         if (left < length) {
-            _LOG2D(n->lldp_rx, "TLV truncated, ignoring datagram.");
+            _LOG2D(lldp_rx, "TLV truncated, ignoring datagram.");
             return -NME_UNSPEC;
         }
 
         switch (type) {
         case NM_LLDP_TYPE_END:
             if (length != 0) {
-                _LOG2D(n->lldp_rx, "End marker TLV not zero-sized, ignoring datagram.");
+                _LOG2D(lldp_rx, "End marker TLV not zero-sized, ignoring datagram.");
                 return -NME_UNSPEC;
             }
 
@@ -166,12 +169,12 @@ nm_lldp_neighbor_parse(NMLldpNeighbor *n)
         case NM_LLDP_TYPE_CHASSIS_ID:
             if (length < 2 || length > 256) {
                 /* includes the chassis subtype, hence one extra byte */
-                _LOG2D(n->lldp_rx, "Chassis ID field size out of range, ignoring datagram.");
+                _LOG2D(lldp_rx, "Chassis ID field size out of range, ignoring datagram.");
                 return -NME_UNSPEC;
             }
 
             if (n->id.chassis_id) {
-                _LOG2D(n->lldp_rx, "Duplicate chassis ID field, ignoring datagram.");
+                _LOG2D(lldp_rx, "Duplicate chassis ID field, ignoring datagram.");
                 return -NME_UNSPEC;
             }
 
@@ -182,12 +185,12 @@ nm_lldp_neighbor_parse(NMLldpNeighbor *n)
         case NM_LLDP_TYPE_PORT_ID:
             if (length < 2 || length > 256) {
                 /* includes the port subtype, hence one extra byte */
-                _LOG2D(n->lldp_rx, "Port ID field size out of range, ignoring datagram.");
+                _LOG2D(lldp_rx, "Port ID field size out of range, ignoring datagram.");
                 return -NME_UNSPEC;
             }
 
             if (n->id.port_id) {
-                _LOG2D(n->lldp_rx, "Duplicate port ID field, ignoring datagram.");
+                _LOG2D(lldp_rx, "Duplicate port ID field, ignoring datagram.");
                 return -NME_UNSPEC;
             }
 
@@ -197,12 +200,12 @@ nm_lldp_neighbor_parse(NMLldpNeighbor *n)
 
         case NM_LLDP_TYPE_TTL:
             if (length != 2) {
-                _LOG2D(n->lldp_rx, "TTL field has wrong size, ignoring datagram.");
+                _LOG2D(lldp_rx, "TTL field has wrong size, ignoring datagram.");
                 return -NME_UNSPEC;
             }
 
             if (n->has_ttl) {
-                _LOG2D(n->lldp_rx, "Duplicate TTL field, ignoring datagram.");
+                _LOG2D(lldp_rx, "Duplicate TTL field, ignoring datagram.");
                 return -NME_UNSPEC;
             }
 
@@ -211,26 +214,26 @@ nm_lldp_neighbor_parse(NMLldpNeighbor *n)
             break;
 
         case NM_LLDP_TYPE_PORT_DESCRIPTION:
-            r = parse_string(n->lldp_rx, &n->port_description, p, length);
+            r = parse_string(lldp_rx, &n->port_description, p, length);
             if (r < 0)
                 return r;
             break;
 
         case NM_LLDP_TYPE_SYSTEM_NAME:
-            r = parse_string(n->lldp_rx, &n->system_name, p, length);
+            r = parse_string(lldp_rx, &n->system_name, p, length);
             if (r < 0)
                 return r;
             break;
 
         case NM_LLDP_TYPE_SYSTEM_DESCRIPTION:
-            r = parse_string(n->lldp_rx, &n->system_description, p, length);
+            r = parse_string(lldp_rx, &n->system_description, p, length);
             if (r < 0)
                 return r;
             break;
 
         case NM_LLDP_TYPE_SYSTEM_CAPABILITIES:
             if (length != 4) {
-                _LOG2D(n->lldp_rx, "System capabilities field has wrong size.");
+                _LOG2D(lldp_rx, "System capabilities field has wrong size.");
                 return -NME_UNSPEC;
             }
 
@@ -241,13 +244,13 @@ nm_lldp_neighbor_parse(NMLldpNeighbor *n)
 
         case NM_LLDP_TYPE_PRIVATE:
             if (length < 4) {
-                _LOG2D(n->lldp_rx, "Found private TLV that is too short, ignoring.");
+                _LOG2D(lldp_rx, "Found private TLV that is too short, ignoring.");
                 return -NME_UNSPEC;
             }
 
             /* RFC 8520: MUD URL */
             if (memcmp(p, NM_LLDP_OUI_IANA_MUD, sizeof(NM_LLDP_OUI_IANA_MUD)) == 0) {
-                r = parse_string(n->lldp_rx,
+                r = parse_string(lldp_rx,
                                  &n->mud_url,
                                  p + sizeof(NM_LLDP_OUI_IANA_MUD),
                                  length - sizeof(NM_LLDP_OUI_IANA_MUD));
@@ -262,7 +265,7 @@ nm_lldp_neighbor_parse(NMLldpNeighbor *n)
 
 end_marker:
     if (!n->id.chassis_id || !n->id.port_id || !n->has_ttl) {
-        _LOG2D(n->lldp_rx, "One or more mandatory TLV missing in datagram. Ignoring.");
+        _LOG2D(lldp_rx, "One or more mandatory TLV missing in datagram. Ignoring.");
         return -NME_UNSPEC;
     }
 
@@ -741,7 +744,7 @@ nm_lldp_neighbor_new(size_t raw_size)
 }
 
 NMLldpNeighbor *
-nm_lldp_neighbor_new_from_raw(const void *raw, size_t raw_size)
+nm_lldp_neighbor_new_from_raw(NMLldpRX *lldp_rx, const void *raw, size_t raw_size)
 {
     nm_auto(nm_lldp_neighbor_unrefp) NMLldpNeighbor *n = NULL;
     int                                              r;
@@ -752,7 +755,7 @@ nm_lldp_neighbor_new_from_raw(const void *raw, size_t raw_size)
 
     nm_memcpy(NM_LLDP_NEIGHBOR_RAW(n), raw, raw_size);
 
-    r = nm_lldp_neighbor_parse(n);
+    r = nm_lldp_neighbor_parse(lldp_rx, n);
     if (r < 0)
         return NULL;
 
diff --git a/src/libnm-lldp/nm-lldp-neighbor.h b/src/libnm-lldp/nm-lldp-neighbor.h
index 1adc967e..038591a0 100644
--- a/src/libnm-lldp/nm-lldp-neighbor.h
+++ b/src/libnm-lldp/nm-lldp-neighbor.h
@@ -75,11 +75,13 @@ NM_LLDP_NEIGHBOR_TLV_DATA(const NMLldpNeighbor *n)
     return ((uint8_t *) NM_LLDP_NEIGHBOR_RAW(n)) + n->rindex + 2;
 }
 
+struct _NMLldpRX;
+
 int nm_lldp_neighbor_prioq_compare_func(const void *a, const void *b);
 
 void            nm_lldp_neighbor_unlink(NMLldpNeighbor *n);
 NMLldpNeighbor *nm_lldp_neighbor_new(size_t raw_size);
-int             nm_lldp_neighbor_parse(NMLldpNeighbor *n);
+int             nm_lldp_neighbor_parse(struct _NMLldpRX *lldp_rx, NMLldpNeighbor *n);
 void            nm_lldp_neighbor_start_ttl(NMLldpNeighbor *n);
 
 #endif /* __NM_LLDP_NEIGHBOR_H__ */
diff --git a/src/libnm-lldp/nm-lldp-rx-internal.h b/src/libnm-lldp/nm-lldp-rx-internal.h
index 47d063ae..1296a9d3 100644
--- a/src/libnm-lldp/nm-lldp-rx-internal.h
+++ b/src/libnm-lldp/nm-lldp-rx-internal.h
@@ -34,7 +34,7 @@ struct _NMLldpRX {
         NMLldpRX        *_lldp_rx = (lldp_rx);                                   \
                                                                                  \
         if (_NMLOG2_ENABLED(_level)) {                                           \
-            _nm_log(level,                                                       \
+            _nm_log(_level,                                                      \
                     _NMLOG2_DOMAIN,                                              \
                     0,                                                           \
                     _lldp_rx->config.log_ifname,                                 \
diff --git a/src/libnm-lldp/nm-lldp-rx.c b/src/libnm-lldp/nm-lldp-rx.c
index 345c6d56..90414b3e 100644
--- a/src/libnm-lldp/nm-lldp-rx.c
+++ b/src/libnm-lldp/nm-lldp-rx.c
@@ -255,7 +255,7 @@ lldp_rx_receive_datagram(int fd, GIOCondition condition, gpointer user_data)
     } else
         n->timestamp_usec = nm_utils_get_monotonic_timestamp_usec();
 
-    r = nm_lldp_neighbor_parse(n);
+    r = nm_lldp_neighbor_parse(lldp_rx, n);
     if (r < 0) {
         _LOG2D(lldp_rx, "Failure parsing invalid LLDP datagram.");
         return G_SOURCE_CONTINUE;
diff --git a/src/libnm-lldp/nm-lldp-rx.h b/src/libnm-lldp/nm-lldp-rx.h
index a3f38053..d96ffcd8 100644
--- a/src/libnm-lldp/nm-lldp-rx.h
+++ b/src/libnm-lldp/nm-lldp-rx.h
@@ -68,7 +68,7 @@ NMLldpNeighbor **nm_lldp_rx_get_neighbors(NMLldpRX *lldp_rx, guint *out_len);
 
 /*****************************************************************************/
 
-NMLldpNeighbor *nm_lldp_neighbor_new_from_raw(const void *raw, size_t raw_size);
+NMLldpNeighbor *nm_lldp_neighbor_new_from_raw(NMLldpRX *lldp_rx, const void *raw, size_t raw_size);
 
 NMLldpNeighbor *nm_lldp_neighbor_ref(NMLldpNeighbor *n);
 NMLldpNeighbor *nm_lldp_neighbor_unref(NMLldpNeighbor *n);
diff --git a/src/libnm-platform/nm-linux-platform.c b/src/libnm-platform/nm-linux-platform.c
index dd4be614..53f678fc 100644
--- a/src/libnm-platform/nm-linux-platform.c
+++ b/src/libnm-platform/nm-linux-platform.c
@@ -187,6 +187,9 @@ G_STATIC_ASSERT(RTA_MAX == (__RTA_MAX - 1));
 
 /*****************************************************************************/
 
+/* Added in kernel 5.19, dated July 31, 2022 */
+#define IFLA_BOND_SLAVE_PRIO 9
+
 #define IFLA_BOND_ACTIVE_PORT      IFLA_BOND_ACTIVE_SLAVE
 #define IFLA_BOND_PORT_PRIO        IFLA_BOND_SLAVE_PRIO
 #define IFLA_BOND_ALL_PORTS_ACTIVE IFLA_BOND_ALL_SLAVES_ACTIVE
@@ -353,7 +356,8 @@ struct _ifla_vf_vlan_info {
 
 /*****************************************************************************/
 
-#define RESYNC_RETRIES 50
+#define RESYNC_RETRIES         50
+#define RESYNC_BACKOFF_SECONDS 1
 
 /*****************************************************************************/
 
@@ -9402,17 +9406,20 @@ nla_put_failure:
 }
 
 static gboolean
-link_set_bridge_vlans(NMPlatform                        *platform,
-                      int                                ifindex,
-                      gboolean                           on_controller,
-                      const NMPlatformBridgeVlan *const *vlans)
+link_set_bridge_vlans(NMPlatform                 *platform,
+                      int                         ifindex,
+                      gboolean                    on_controller,
+                      const NMPlatformBridgeVlan *vlans,
+                      guint                       num_vlans)
 {
     nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
     struct nlattr               *list;
     struct bridge_vlan_info      vinfo = {};
     guint                        i;
 
-    nlmsg = _nl_msg_new_link_full(vlans ? RTM_SETLINK : RTM_DELLINK,
+    nm_assert(num_vlans == 0 || vlans);
+
+    nlmsg = _nl_msg_new_link_full(num_vlans > 0 ? RTM_SETLINK : RTM_DELLINK,
                                   0,
                                   ifindex,
                                   NULL,
@@ -9430,10 +9437,10 @@ link_set_bridge_vlans(NMPlatform                        *platform,
                 IFLA_BRIDGE_FLAGS,
                 on_controller ? BRIDGE_FLAGS_CONTROLLER : BRIDGE_FLAGS_SELF);
 
-    if (vlans) {
+    if (num_vlans > 0) {
         /* Add VLANs */
-        for (i = 0; vlans[i]; i++) {
-            const NMPlatformBridgeVlan *vlan     = vlans[i];
+        for (i = 0; i < num_vlans; i++) {
+            const NMPlatformBridgeVlan *vlan     = &vlans[i];
             gboolean                    is_range = vlan->vid_start != vlan->vid_end;
 
             vinfo.vid   = vlan->vid_start;
@@ -9470,6 +9477,138 @@ nla_put_failure:
     g_return_val_if_reached(FALSE);
 }
 
+typedef struct {
+    int     ifindex;
+    GArray *vlans;
+} BridgeVlanData;
+
+static int
+get_bridge_vlans_cb(const struct nl_msg *msg, void *arg)
+{
+    static const struct nla_policy policy[] = {
+        [IFLA_AF_SPEC] = {.type = NLA_NESTED},
+    };
+    struct nlattr    *tb[G_N_ELEMENTS(policy)];
+    gboolean          is_range = FALSE;
+    BridgeVlanData   *data     = arg;
+    struct ifinfomsg *ifinfo;
+    struct nlattr    *attr;
+    int               rem;
+
+    if (nlmsg_parse_arr(nlmsg_hdr(msg), sizeof(struct ifinfomsg), tb, policy) < 0)
+        return NL_SKIP;
+
+    ifinfo = NLMSG_DATA(nlmsg_hdr(msg));
+    if (ifinfo->ifi_index != data->ifindex)
+        return NL_SKIP;
+
+    if (!tb[IFLA_AF_SPEC])
+        return NL_SKIP;
+
+    nla_for_each_nested (attr, tb[IFLA_AF_SPEC], rem) {
+        struct bridge_vlan_info vlan_info;
+        NMPlatformBridgeVlan    vlan = {};
+
+        if (nla_type(attr) != IFLA_BRIDGE_VLAN_INFO)
+            continue;
+
+        if (!data->vlans)
+            data->vlans = g_array_new(0, FALSE, sizeof(NMPlatformBridgeVlan));
+
+        vlan_info = *nla_data_as(struct bridge_vlan_info, attr);
+
+        if (is_range) {
+            nm_g_array_index(data->vlans, NMPlatformBridgeVlan, data->vlans->len - 1).vid_end =
+                vlan_info.vid;
+            is_range = FALSE;
+            continue;
+        } else {
+            vlan.vid_start = vlan_info.vid;
+            vlan.vid_end   = vlan_info.vid;
+            vlan.untagged  = vlan_info.flags & BRIDGE_VLAN_INFO_UNTAGGED;
+            vlan.pvid      = vlan_info.flags & BRIDGE_VLAN_INFO_PVID;
+
+            if (vlan_info.flags & BRIDGE_VLAN_INFO_RANGE_BEGIN)
+                is_range = TRUE;
+        }
+
+        g_array_append_val(data->vlans, vlan);
+    }
+
+    return NL_OK;
+}
+
+static gboolean
+link_get_bridge_vlans(NMPlatform            *platform,
+                      int                    ifindex,
+                      NMPlatformBridgeVlan **out_vlans,
+                      guint                 *out_num_vlans)
+{
+    gboolean                     ret   = FALSE;
+    nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
+    struct nl_sock              *sk    = NULL;
+    BridgeVlanData               data;
+    int                          nle;
+
+    nlmsg = _nl_msg_new_link_full(RTM_GETLINK, NLM_F_DUMP, 0, NULL, AF_BRIDGE, 0, 0, 0);
+    if (!nlmsg)
+        g_return_val_if_reached(FALSE);
+
+    nle = nl_socket_new(&sk, NETLINK_ROUTE, NL_SOCKET_FLAGS_DISABLE_MSG_PEEK, 0, 0);
+    if (nle < 0) {
+        _LOGD("get-bridge-vlan: error opening socket: %s (%d)", nm_strerror(nle), nle);
+        ret = FALSE;
+        goto err;
+    }
+
+    NLA_PUT_U32(nlmsg, IFLA_EXT_MASK, RTEXT_FILTER_BRVLAN_COMPRESSED);
+
+    nle = nl_send_auto(sk, nlmsg);
+    if (nle < 0) {
+        _LOGD("get-bridge-vlans: failed sending request: %s (%d)", nm_strerror(nle), nle);
+        ret = FALSE;
+        goto err;
+    }
+
+    data = ((BridgeVlanData){
+        .ifindex = ifindex,
+    });
+
+    do {
+        nle = nl_recvmsgs(sk,
+                          &((const struct nl_cb){
+                              .valid_cb  = get_bridge_vlans_cb,
+                              .valid_arg = &data,
+                          }));
+    } while (nle == -EAGAIN);
+
+    if (nle < 0) {
+        _LOGD("get-bridge-vlan: recv failed: %s (%d)", nm_strerror(nle), nle);
+        ret = FALSE;
+        goto err;
+    }
+
+    if (data.vlans) {
+        NM_SET_OUT(out_vlans, &nm_g_array_index(data.vlans, NMPlatformBridgeVlan, 0));
+        NM_SET_OUT(out_num_vlans, data.vlans->len);
+    } else {
+        NM_SET_OUT(out_vlans, NULL);
+        NM_SET_OUT(out_num_vlans, 0);
+    }
+
+    if (data.vlans)
+        g_array_free(data.vlans, !out_vlans);
+
+    ret = TRUE;
+err:
+    if (sk)
+        nl_socket_free(sk);
+    return ret;
+
+nla_put_failure:
+    g_return_val_if_reached(FALSE);
+}
+
 static gboolean
 link_set_bridge_info(NMPlatform                            *platform,
                      int                                    ifindex,
@@ -10965,6 +11104,20 @@ event_handler_read_netlink(NMPlatform        *platform,
                               }
                               _reason;
                           }));
+
+                    if (nle == -ENOBUFS) {
+                        /* Netlink notifications are coming faster than what
+                         * we can process them. Backoff a bit so we give some
+                         * time for this burst to finish, and we don't
+                         * contribute to starve the system contending for the
+                         * kernel's RTNL lock.
+                         */
+                        _LOGI("netlink[%s]: backoff for %d seconds before the resync.",
+                              nmp_netlink_protocol_info(netlink_protocol)->name,
+                              RESYNC_BACKOFF_SECONDS);
+                        sleep(RESYNC_BACKOFF_SECONDS);
+                    }
+
                     _netlink_recv_handle(platform, netlink_protocol, FALSE);
                     delayed_action_wait_for_nl_response_complete_all(
                         platform,
@@ -11809,6 +11962,7 @@ nm_linux_platform_class_init(NMLinuxPlatformClass *klass)
     platform_class->link_set_sriov_params_async        = link_set_sriov_params_async;
     platform_class->link_set_sriov_vfs                 = link_set_sriov_vfs;
     platform_class->link_set_bridge_vlans              = link_set_bridge_vlans;
+    platform_class->link_get_bridge_vlans              = link_get_bridge_vlans;
     platform_class->link_set_bridge_info               = link_set_bridge_info;
 
     platform_class->link_get_physical_port_id = link_get_physical_port_id;
diff --git a/src/libnm-platform/nm-platform-utils.c b/src/libnm-platform/nm-platform-utils.c
index 6f3ad05c..3f70f5fe 100644
--- a/src/libnm-platform/nm-platform-utils.c
+++ b/src/libnm-platform/nm-platform-utils.c
@@ -2275,6 +2275,89 @@ nmp_utils_lifetime_get(guint32  timestamp,
 
 /*****************************************************************************/
 
+static int
+bridge_vlan_compare(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+    const NMPlatformBridgeVlan *vlan_a = a;
+    const NMPlatformBridgeVlan *vlan_b = b;
+
+    return (int) vlan_a->vid_start - (int) vlan_b->vid_start;
+}
+
+/**
+ * nmp_utils_bridge_vlan_normalize:
+ * @vlans: the array of VLAN ranges
+ * @num_vlans: the number of VLAN ranges in the array. On return, it contains
+ *    the new number.
+ *
+ * Sort the VLAN ranges and merge those that are contiguous or overlapping. It
+ * must not contain invalid data such as 2 overlapping ranges with different
+ * flags.
+ */
+void
+nmp_utils_bridge_vlan_normalize(NMPlatformBridgeVlan *vlans, guint *num_vlans)
+{
+    guint i;
+
+    if (*num_vlans <= 1)
+        return;
+
+    g_qsort_with_data(vlans, *num_vlans, sizeof(NMPlatformBridgeVlan), bridge_vlan_compare, NULL);
+
+    /* Merge VLAN ranges that are contiguous or overlap */
+    i = 0;
+    while (i < *num_vlans - 1) {
+        guint    j         = i + 1;
+        gboolean can_merge = vlans[j].vid_start <= vlans[i].vid_end + 1
+                             && vlans[j].pvid == vlans[i].pvid
+                             && vlans[j].untagged == vlans[i].untagged;
+
+        if (can_merge) {
+            vlans[i].vid_end = NM_MAX(vlans[i].vid_end, vlans[j].vid_end);
+            for (; j < *num_vlans - 1; j++)
+                vlans[j] = vlans[j + 1];
+            *num_vlans -= 1;
+        } else {
+            i++;
+        }
+    }
+}
+
+/**
+ * nmp_utils_bridge_normalized_vlans_equal:
+ * @vlans_a: the first array of bridge VLANs
+ * @num_vlans_a: the number of elements of first array
+ * @vlans_b: the second array of bridge VLANs
+ * @num_vlans_b: the number of elements of second array
+ *
+ * Given two arrays of bridge VLAN ranges, compare if they are equal,
+ * i.e. if they represent the same set of VLANs with the same attributes.
+ * The input arrays must be normalized (sorted and without overlapping or
+ * duplicated ranges). Normalize with nmp_utils_bridge_vlan_normalize().
+ */
+gboolean
+nmp_utils_bridge_normalized_vlans_equal(const NMPlatformBridgeVlan *vlans_a,
+                                        guint                       num_vlans_a,
+                                        const NMPlatformBridgeVlan *vlans_b,
+                                        guint                       num_vlans_b)
+{
+    guint i;
+
+    if (num_vlans_a != num_vlans_b)
+        return FALSE;
+
+    for (i = 0; i < num_vlans_a; i++) {
+        if (vlans_a[i].vid_start != vlans_b[i].vid_start || vlans_a[i].vid_end != vlans_b[i].vid_end
+            || vlans_a[i].pvid != vlans_b[i].pvid || vlans_a[i].untagged != vlans_b[i].untagged) {
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
 static const char *
 _trunk_first_line(char *str)
 {
diff --git a/src/libnm-platform/nm-platform-utils.h b/src/libnm-platform/nm-platform-utils.h
index 18fc6155..96ac22ef 100644
--- a/src/libnm-platform/nm-platform-utils.h
+++ b/src/libnm-platform/nm-platform-utils.h
@@ -99,4 +99,11 @@ guint32 nmp_utils_lifetime_get(guint32  timestamp,
 int nmp_utils_modprobe(GError **error, gboolean suppress_error_logging, const char *arg1, ...)
     G_GNUC_NULL_TERMINATED;
 
+void nmp_utils_bridge_vlan_normalize(NMPlatformBridgeVlan *vlans, guint *num_vlans);
+
+gboolean nmp_utils_bridge_normalized_vlans_equal(const NMPlatformBridgeVlan *vlans_a,
+                                                 guint                       num_vlans_a,
+                                                 const NMPlatformBridgeVlan *vlans_b,
+                                                 guint                       num_vlans_b);
+
 #endif /* __NM_PLATFORM_UTILS_H__ */
diff --git a/src/libnm-platform/nm-platform.c b/src/libnm-platform/nm-platform.c
index f82de7f9..af04f29f 100644
--- a/src/libnm-platform/nm-platform.c
+++ b/src/libnm-platform/nm-platform.c
@@ -2070,10 +2070,11 @@ nm_platform_link_set_sriov_vfs(NMPlatform *self, int ifindex, const NMPlatformVF
 }
 
 gboolean
-nm_platform_link_set_bridge_vlans(NMPlatform                        *self,
-                                  int                                ifindex,
-                                  gboolean                           on_controller,
-                                  const NMPlatformBridgeVlan *const *vlans)
+nm_platform_link_set_bridge_vlans(NMPlatform                 *self,
+                                  int                         ifindex,
+                                  gboolean                    on_controller,
+                                  const NMPlatformBridgeVlan *vlans,
+                                  guint                       num_vlans)
 {
     guint i;
     _CHECK_SELF(self, klass, FALSE);
@@ -2085,9 +2086,9 @@ nm_platform_link_set_bridge_vlans(NMPlatform                        *self,
                vlans ? "setting" : "clearing",
                on_controller ? "controller" : "self");
         if (vlans) {
-            for (i = 0; vlans[i]; i++) {
+            for (i = 0; i < num_vlans; i++) {
                 char                        sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
-                const NMPlatformBridgeVlan *vlan = vlans[i];
+                const NMPlatformBridgeVlan *vlan = &vlans[i];
 
                 _LOG3D("link:   bridge VLAN %s",
                        nm_platform_bridge_vlan_to_string(vlan, sbuf, sizeof(sbuf)));
@@ -2095,7 +2096,41 @@ nm_platform_link_set_bridge_vlans(NMPlatform                        *self,
         }
     }
 
-    return klass->link_set_bridge_vlans(self, ifindex, on_controller, vlans);
+    return klass->link_set_bridge_vlans(self, ifindex, on_controller, vlans, num_vlans);
+}
+
+gboolean
+nm_platform_link_get_bridge_vlans(NMPlatform            *self,
+                                  int                    ifindex,
+                                  NMPlatformBridgeVlan **out_vlans,
+                                  guint                 *out_num_vlans)
+{
+    char     sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+    gboolean ret;
+    guint    i;
+
+    _CHECK_SELF(self, klass, FALSE);
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+    g_return_val_if_fail(out_vlans, FALSE);
+    g_return_val_if_fail(out_num_vlans, FALSE);
+
+    _LOG3D("link: getting bridge VLANs");
+
+    ret = klass->link_get_bridge_vlans(self, ifindex, out_vlans, out_num_vlans);
+
+    if (_LOGD_ENABLED()) {
+        if (!ret) {
+            _LOG3D("link: failure while getting bridge vlans");
+        } else {
+            for (i = 0; i < *out_num_vlans; i++) {
+                _LOG3D("link:   bridge VLAN %s",
+                       nm_platform_bridge_vlan_to_string(&(*out_vlans)[i], sbuf, sizeof(sbuf)));
+            }
+        }
+    }
+
+    return ret;
 }
 
 gboolean
diff --git a/src/libnm-platform/nm-platform.h b/src/libnm-platform/nm-platform.h
index b05b1297..e33be813 100644
--- a/src/libnm-platform/nm-platform.h
+++ b/src/libnm-platform/nm-platform.h
@@ -742,13 +742,6 @@ typedef struct {
 } NMPlatformVF;
 
 typedef struct {
-    guint16 vid_start;
-    guint16 vid_end;
-    bool    untagged : 1;
-    bool    pvid : 1;
-} NMPlatformBridgeVlan;
-
-typedef struct {
     guint16 vlan_default_pvid_val;
     bool    vlan_filtering_val : 1;
     bool    vlan_default_pvid_has : 1;
@@ -1185,10 +1178,15 @@ typedef struct {
                                         gpointer                callback_data,
                                         GCancellable           *cancellable);
     gboolean (*link_set_sriov_vfs)(NMPlatform *self, int ifindex, const NMPlatformVF *const *vfs);
-    gboolean (*link_set_bridge_vlans)(NMPlatform                        *self,
-                                      int                                ifindex,
-                                      gboolean                           on_controller,
-                                      const NMPlatformBridgeVlan *const *vlans);
+    gboolean (*link_set_bridge_vlans)(NMPlatform                 *self,
+                                      int                         ifindex,
+                                      gboolean                    on_controller,
+                                      const NMPlatformBridgeVlan *vlans,
+                                      guint                       num_vlans);
+    gboolean (*link_get_bridge_vlans)(NMPlatform            *self,
+                                      int                    ifindex,
+                                      NMPlatformBridgeVlan **out_vlans,
+                                      guint                 *out_num_vlans);
     gboolean (*link_set_bridge_info)(NMPlatform                            *self,
                                      int                                    ifindex,
                                      const NMPlatformLinkSetBridgeInfoData *bridge_info);
@@ -2049,10 +2047,15 @@ void nm_platform_link_set_sriov_params_async(NMPlatform             *self,
 
 gboolean
 nm_platform_link_set_sriov_vfs(NMPlatform *self, int ifindex, const NMPlatformVF *const *vfs);
-gboolean nm_platform_link_set_bridge_vlans(NMPlatform                        *self,
-                                           int                                ifindex,
-                                           gboolean                           on_controller,
-                                           const NMPlatformBridgeVlan *const *vlans);
+gboolean nm_platform_link_set_bridge_vlans(NMPlatform                 *self,
+                                           int                         ifindex,
+                                           gboolean                    on_controller,
+                                           const NMPlatformBridgeVlan *vlans,
+                                           guint                       num_vlans);
+gboolean nm_platform_link_get_bridge_vlans(NMPlatform            *self,
+                                           int                    ifindex,
+                                           NMPlatformBridgeVlan **out_vlans,
+                                           guint                 *out_num_vlans);
 gboolean nm_platform_link_set_bridge_info(NMPlatform                            *self,
                                           int                                    ifindex,
                                           const NMPlatformLinkSetBridgeInfoData *bridge_info);
diff --git a/src/libnm-platform/nmp-base.h b/src/libnm-platform/nmp-base.h
index 70b5d1bc..c7d487e2 100644
--- a/src/libnm-platform/nmp-base.h
+++ b/src/libnm-platform/nmp-base.h
@@ -39,6 +39,15 @@ typedef enum {
 /*****************************************************************************/
 
 typedef struct {
+    guint16 vid_start;
+    guint16 vid_end;
+    bool    untagged : 1;
+    bool    pvid : 1;
+} NMPlatformBridgeVlan;
+
+/*****************************************************************************/
+
+typedef struct {
     /* We don't want to include <linux/ethtool.h> in header files,
      * thus create a ABI compatible version of struct ethtool_drvinfo.*/
     guint32 _private_cmd;
diff --git a/src/libnm-platform/tests/test-nm-platform.c b/src/libnm-platform/tests/test-nm-platform.c
index 5fc8a5dd..37707875 100644
--- a/src/libnm-platform/tests/test-nm-platform.c
+++ b/src/libnm-platform/tests/test-nm-platform.c
@@ -191,6 +191,239 @@ test_nmp_link_mode_all_advertised_modes_bits(void)
 /*****************************************************************************/
 
 static void
+test_nmp_utils_bridge_vlans_normalize(void)
+{
+    NMPlatformBridgeVlan vlans[10];
+    NMPlatformBridgeVlan expect[10];
+    guint                vlans_len;
+
+    /* Single one is unmodified */
+    vlans[0] = (NMPlatformBridgeVlan){
+        .vid_start = 1,
+        .vid_end   = 10,
+        .untagged  = TRUE,
+    };
+    expect[0] = (NMPlatformBridgeVlan){
+        .vid_start = 1,
+        .vid_end   = 10,
+        .untagged  = TRUE,
+    };
+    vlans_len = 1;
+    nmp_utils_bridge_vlan_normalize(vlans, &vlans_len);
+    g_assert(vlans_len == 1);
+    g_assert(nmp_utils_bridge_normalized_vlans_equal(vlans, vlans_len, expect, vlans_len));
+
+    /* Not merged if flags are different */
+    vlans[0] = (NMPlatformBridgeVlan){
+        .vid_start = 1,
+        .vid_end   = 10,
+        .untagged  = TRUE,
+    };
+    vlans[1] = (NMPlatformBridgeVlan){
+        .vid_start = 11,
+        .vid_end   = 11,
+        .pvid      = TRUE,
+    };
+    vlans[2] = (NMPlatformBridgeVlan){
+        .vid_start = 20,
+        .vid_end   = 25,
+    };
+    vlans[3] = (NMPlatformBridgeVlan){
+        .vid_start = 26,
+        .vid_end   = 30,
+        .untagged  = TRUE,
+    };
+    vlans[4] = (NMPlatformBridgeVlan){
+        .vid_start = 40,
+        .vid_end   = 40,
+        .untagged  = TRUE,
+    };
+    vlans[5] = (NMPlatformBridgeVlan){
+        .vid_start = 40,
+        .vid_end   = 40,
+        .untagged  = TRUE,
+        .pvid      = TRUE,
+    };
+    expect[0] = (NMPlatformBridgeVlan){
+        .vid_start = 1,
+        .vid_end   = 10,
+        .untagged  = TRUE,
+    };
+    expect[1] = (NMPlatformBridgeVlan){
+        .vid_start = 11,
+        .vid_end   = 11,
+        .pvid      = TRUE,
+    };
+    expect[2] = (NMPlatformBridgeVlan){
+        .vid_start = 20,
+        .vid_end   = 25,
+    };
+    expect[3] = (NMPlatformBridgeVlan){
+        .vid_start = 26,
+        .vid_end   = 30,
+        .untagged  = TRUE,
+    };
+    expect[4] = (NMPlatformBridgeVlan){
+        .vid_start = 40,
+        .vid_end   = 40,
+        .untagged  = TRUE,
+    };
+    expect[5] = (NMPlatformBridgeVlan){
+        .vid_start = 40,
+        .vid_end   = 40,
+        .untagged  = TRUE,
+        .pvid      = TRUE,
+    };
+    vlans_len = 6;
+    nmp_utils_bridge_vlan_normalize(vlans, &vlans_len);
+    g_assert(vlans_len == 6);
+    g_assert(nmp_utils_bridge_normalized_vlans_equal(vlans, vlans_len, expect, vlans_len));
+
+    /* Overlapping and contiguous ranges are merged */
+    vlans[0] = (NMPlatformBridgeVlan){
+        .vid_start = 1,
+        .vid_end   = 10,
+        .untagged  = TRUE,
+    };
+    vlans[1] = (NMPlatformBridgeVlan){
+        .vid_start = 11,
+        .vid_end   = 20,
+        .untagged  = TRUE,
+    };
+    vlans[2] = (NMPlatformBridgeVlan){
+        .vid_start = 19,
+        .vid_end   = 30,
+        .untagged  = TRUE,
+    };
+    expect[0] = (NMPlatformBridgeVlan){
+        .vid_start = 1,
+        .vid_end   = 30,
+        .untagged  = TRUE,
+    };
+    vlans_len = 3;
+    nmp_utils_bridge_vlan_normalize(vlans, &vlans_len);
+    g_assert(vlans_len == 1);
+    g_assert(nmp_utils_bridge_normalized_vlans_equal(vlans, vlans_len, expect, vlans_len));
+
+    vlans[0] = (NMPlatformBridgeVlan){
+        .vid_start = 20,
+        .vid_end   = 20,
+    };
+    vlans[1] = (NMPlatformBridgeVlan){
+        .vid_start = 4,
+        .vid_end   = 4,
+        .pvid      = TRUE,
+    };
+    vlans[2] = (NMPlatformBridgeVlan){
+        .vid_start = 33,
+        .vid_end   = 33,
+    };
+    vlans[3] = (NMPlatformBridgeVlan){
+        .vid_start = 100,
+        .vid_end   = 100,
+        .untagged  = TRUE,
+    };
+    vlans[4] = (NMPlatformBridgeVlan){
+        .vid_start = 34,
+        .vid_end   = 40,
+    };
+    vlans[5] = (NMPlatformBridgeVlan){
+        .vid_start = 21,
+        .vid_end   = 32,
+    };
+    expect[0] = (NMPlatformBridgeVlan){
+        .vid_start = 4,
+        .vid_end   = 4,
+        .pvid      = TRUE,
+    };
+    expect[1] = (NMPlatformBridgeVlan){
+        .vid_start = 20,
+        .vid_end   = 40,
+    };
+    expect[2] = (NMPlatformBridgeVlan){
+        .vid_start = 100,
+        .vid_end   = 100,
+        .untagged  = TRUE,
+    };
+    vlans_len = 6;
+    nmp_utils_bridge_vlan_normalize(vlans, &vlans_len);
+    g_assert(vlans_len == 3);
+    g_assert(nmp_utils_bridge_normalized_vlans_equal(vlans, vlans_len, expect, vlans_len));
+}
+
+static void
+test_nmp_utils_bridge_normalized_vlans_equal(void)
+{
+    NMPlatformBridgeVlan a[10];
+    NMPlatformBridgeVlan b[10];
+
+    /* Both empty */
+    g_assert(nmp_utils_bridge_normalized_vlans_equal(NULL, 0, NULL, 0));
+    g_assert(nmp_utils_bridge_normalized_vlans_equal(a, 0, b, 0));
+    g_assert(nmp_utils_bridge_normalized_vlans_equal(a, 0, NULL, 0));
+    g_assert(nmp_utils_bridge_normalized_vlans_equal(NULL, 0, b, 0));
+
+    /* One empty, other not */
+    a[0] = (NMPlatformBridgeVlan){
+        .vid_start = 1,
+        .vid_end   = 10,
+        .untagged  = TRUE,
+    };
+    g_assert(!nmp_utils_bridge_normalized_vlans_equal(a, 1, NULL, 0));
+    g_assert(!nmp_utils_bridge_normalized_vlans_equal(NULL, 0, a, 1));
+
+    /* Equal range + VLAN */
+    a[0] = (NMPlatformBridgeVlan){
+        .vid_start = 1,
+        .vid_end   = 10,
+        .untagged  = TRUE,
+    };
+    a[1] = (NMPlatformBridgeVlan){
+        .vid_start = 11,
+        .vid_end   = 11,
+        .pvid      = TRUE,
+    };
+    b[0] = (NMPlatformBridgeVlan){
+        .vid_start = 1,
+        .vid_end   = 10,
+        .untagged  = TRUE,
+    };
+    b[1] = (NMPlatformBridgeVlan){
+        .vid_start = 11,
+        .vid_end   = 11,
+        .pvid      = TRUE,
+    };
+    g_assert(nmp_utils_bridge_normalized_vlans_equal(a, 2, b, 2));
+    g_assert(nmp_utils_bridge_normalized_vlans_equal(b, 2, a, 2));
+
+    /* Different flag */
+    b[1].pvid = FALSE;
+    g_assert(!nmp_utils_bridge_normalized_vlans_equal(a, 2, b, 2));
+    g_assert(!nmp_utils_bridge_normalized_vlans_equal(b, 2, a, 2));
+
+    /* Different ranges */
+    a[0] = (NMPlatformBridgeVlan){
+        .vid_start = 1,
+        .vid_end   = 30,
+        .untagged  = TRUE,
+    };
+    b[0] = (NMPlatformBridgeVlan){
+        .vid_start = 1,
+        .vid_end   = 29,
+        .untagged  = TRUE,
+    };
+    g_assert(!nmp_utils_bridge_normalized_vlans_equal(a, 1, b, 1));
+    g_assert(!nmp_utils_bridge_normalized_vlans_equal(b, 1, a, 1));
+
+    b[0].vid_start = 2;
+    b[0].vid_end   = 30;
+    g_assert(!nmp_utils_bridge_normalized_vlans_equal(a, 1, b, 1));
+    g_assert(!nmp_utils_bridge_normalized_vlans_equal(b, 1, a, 1));
+}
+
+/*****************************************************************************/
+
+static void
 test_nmpclass_consistency(void)
 {
     NMPObjectType obj_type;
@@ -252,6 +485,10 @@ main(int argc, char **argv)
     g_test_add_func("/nm-platform/test_nmp_link_mode_all_advertised_modes_bits",
                     test_nmp_link_mode_all_advertised_modes_bits);
     g_test_add_func("/nm-platform/test_nmpclass_consistency", test_nmpclass_consistency);
+    g_test_add_func("/nm-platform/test_nmp_utils_bridge_vlans_normalize",
+                    test_nmp_utils_bridge_vlans_normalize);
+    g_test_add_func("/nm-platform/nmp-utils-bridge-vlans-equal",
+                    test_nmp_utils_bridge_normalized_vlans_equal);
 
     return g_test_run();
 }
diff --git a/src/nmcli/connections.c b/src/nmcli/connections.c
index 9eac7b9a..7e192388 100644
--- a/src/nmcli/connections.c
+++ b/src/nmcli/connections.c
@@ -6576,8 +6576,15 @@ extract_setting_and_property(const char *prompt, const char *line, char **settin
             p2   = dot + 1;
             num1 = strcspn(p1, ".");
             num2 = len > num1 + 1 ? len - num1 - 1 : 0;
-            sett = num1 > 0 ? g_strndup(p1, num1) : sett;
-            prop = num2 > 0 ? g_strndup(p2, num2) : prop;
+            if (num1 > 0) {
+                g_free(sett);
+                sett = g_strndup(p1, num1);
+            }
+
+            if (num2 > 0) {
+                g_free(prop);
+                prop = g_strndup(p2, num2);
+            }
         } else {
             if (!prop)
                 prop = len > 0 ? g_strndup(p1, len) : NULL;