about summary refs log tree commit diff
path: root/src/libnm-platform
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2024-08-26 16:10:34 +0200
committerMichael Biebl <biebl@debian.org>2024-08-26 16:10:34 +0200
commit681dfc70ef98f6ed0c05bcb0fbff00e3fc0799ea (patch)
treede71fafaa05ed5aa070324c3b731206150098781 /src/libnm-platform
parente96d74409128c6a977e31f24fad9b267d2feb9a1 (diff)
New upstream version 1.48.10 upstream/1.48.10
Diffstat (limited to 'src/libnm-platform')
-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
7 files changed, 559 insertions, 31 deletions
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();
 }