summary refs log tree commit diff
path: root/src/libnm-core-impl/nm-setting-tc-config.c
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2021-10-01 23:05:04 +0200
committerMichael Biebl <biebl@debian.org>2021-10-01 23:05:04 +0200
commite74c568b07b50b97873fb4ee1d776dedefbd54d6 (patch)
tree3469f17ea9af91f7ff169b890633bda68b0cf76e /src/libnm-core-impl/nm-setting-tc-config.c
parentbfe522304da217296e2a61040f58e35ec5d6f3f2 (diff)
New upstream version 1.32.12 upstream/1.32.12
Diffstat (limited to 'src/libnm-core-impl/nm-setting-tc-config.c')
-rw-r--r--src/libnm-core-impl/nm-setting-tc-config.c1886
1 files changed, 1886 insertions, 0 deletions
diff --git a/src/libnm-core-impl/nm-setting-tc-config.c b/src/libnm-core-impl/nm-setting-tc-config.c
new file mode 100644
index 00000000..f44c89cb
--- /dev/null
+++ b/src/libnm-core-impl/nm-setting-tc-config.c
@@ -0,0 +1,1886 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#include "libnm-core-impl/nm-default-libnm-core.h"
+
+#include "nm-setting-tc-config.h"
+
+#include <linux/pkt_sched.h>
+
+#include "nm-setting-private.h"
+
+/**
+ * SECTION:nm-setting-tc-config
+ * @short_description: Describes connection properties for the Linux Traffic Control
+ * @include: nm-setting-tc-config.h
+ **/
+
+/*****************************************************************************/
+
+G_DEFINE_BOXED_TYPE(NMTCQdisc, nm_tc_qdisc, nm_tc_qdisc_dup, nm_tc_qdisc_unref)
+
+struct NMTCQdisc {
+    guint refcount;
+
+    char *      kind;
+    guint32     handle;
+    guint32     parent;
+    GHashTable *attributes;
+};
+
+/**
+ * nm_tc_qdisc_new:
+ * @kind: name of the queueing discipline
+ * @parent: the parent queueing discipline
+ * @error: location to store error, or %NULL
+ *
+ * Creates a new #NMTCQdisc object.
+ *
+ * Returns: (transfer full): the new #NMTCQdisc object, or %NULL on error
+ *
+ * Since: 1.12
+ **/
+NMTCQdisc *
+nm_tc_qdisc_new(const char *kind, guint32 parent, GError **error)
+{
+    NMTCQdisc *qdisc;
+
+    if (!kind || !*kind) {
+        g_set_error(error,
+                    NM_CONNECTION_ERROR,
+                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                    _("kind is missing"));
+        return NULL;
+    }
+
+    if (strchr(kind, ' ') || strchr(kind, '\t')) {
+        g_set_error(error,
+                    NM_CONNECTION_ERROR,
+                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                    _("'%s' is not a valid kind"),
+                    kind);
+        return NULL;
+    }
+
+    if (!parent) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("parent handle missing"));
+        return NULL;
+    }
+
+    qdisc           = g_slice_new0(NMTCQdisc);
+    qdisc->refcount = 1;
+
+    qdisc->kind   = g_strdup(kind);
+    qdisc->parent = parent;
+
+    return qdisc;
+}
+
+/**
+ * nm_tc_qdisc_ref:
+ * @qdisc: the #NMTCQdisc
+ *
+ * Increases the reference count of the object.
+ *
+ * Since: 1.12
+ **/
+void
+nm_tc_qdisc_ref(NMTCQdisc *qdisc)
+{
+    g_return_if_fail(qdisc != NULL);
+    g_return_if_fail(qdisc->refcount > 0);
+
+    qdisc->refcount++;
+}
+
+/**
+ * nm_tc_qdisc_unref:
+ * @qdisc: the #NMTCQdisc
+ *
+ * Decreases the reference count of the object.  If the reference count
+ * reaches zero, the object will be destroyed.
+ *
+ * Since: 1.12
+ **/
+void
+nm_tc_qdisc_unref(NMTCQdisc *qdisc)
+{
+    g_return_if_fail(qdisc != NULL);
+    g_return_if_fail(qdisc->refcount > 0);
+
+    qdisc->refcount--;
+    if (qdisc->refcount == 0) {
+        g_free(qdisc->kind);
+        if (qdisc->attributes)
+            g_hash_table_unref(qdisc->attributes);
+        g_slice_free(NMTCQdisc, qdisc);
+    }
+}
+
+/**
+ * nm_tc_qdisc_equal:
+ * @qdisc: the #NMTCQdisc
+ * @other: the #NMTCQdisc to compare @qdisc to.
+ *
+ * Determines if two #NMTCQdisc objects contain the same kind, * handle
+ * and parent.
+ *
+ * Returns: %TRUE if the objects contain the same values, %FALSE if they do not.
+ *
+ * Since: 1.12
+ **/
+gboolean
+nm_tc_qdisc_equal(NMTCQdisc *qdisc, NMTCQdisc *other)
+{
+    GHashTableIter iter;
+    const char *   key;
+    GVariant *     value, *value2;
+    guint          n;
+
+    g_return_val_if_fail(qdisc != NULL, FALSE);
+    g_return_val_if_fail(qdisc->refcount > 0, FALSE);
+
+    g_return_val_if_fail(other != NULL, FALSE);
+    g_return_val_if_fail(other->refcount > 0, FALSE);
+
+    if (qdisc->handle != other->handle || qdisc->parent != other->parent
+        || g_strcmp0(qdisc->kind, other->kind) != 0)
+        return FALSE;
+
+    n = qdisc->attributes ? g_hash_table_size(qdisc->attributes) : 0;
+    if (n != (other->attributes ? g_hash_table_size(other->attributes) : 0))
+        return FALSE;
+    if (n) {
+        g_hash_table_iter_init(&iter, qdisc->attributes);
+        while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &value)) {
+            value2 = g_hash_table_lookup(other->attributes, key);
+            if (!value2)
+                return FALSE;
+            if (!g_variant_equal(value, value2))
+                return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+static guint
+_nm_tc_qdisc_hash(NMTCQdisc *qdisc)
+{
+    NMUtilsNamedValue attrs_static[30];
+    gs_free NMUtilsNamedValue *attrs_free = NULL;
+    const NMUtilsNamedValue *  attrs;
+    NMHashState                h;
+    guint                      length;
+    guint                      i;
+
+    attrs =
+        nm_utils_named_values_from_strdict(qdisc->attributes, &length, attrs_static, &attrs_free);
+
+    nm_hash_init(&h, 43869703);
+    nm_hash_update_vals(&h, qdisc->handle, qdisc->parent, length);
+    nm_hash_update_str0(&h, qdisc->kind);
+    for (i = 0; i < length; i++) {
+        const char *        key     = attrs[i].name;
+        GVariant *          variant = attrs[i].value_ptr;
+        const GVariantType *vtype;
+
+        vtype = g_variant_get_type(variant);
+
+        nm_hash_update_str(&h, key);
+        nm_hash_update_str(&h, (const char *) vtype);
+        if (g_variant_type_is_basic(vtype))
+            nm_hash_update_val(&h, g_variant_hash(variant));
+    }
+
+    return nm_hash_complete(&h);
+}
+
+/**
+ * nm_tc_qdisc_dup:
+ * @qdisc: the #NMTCQdisc
+ *
+ * Creates a copy of @qdisc
+ *
+ * Returns: (transfer full): a copy of @qdisc
+ *
+ * Since: 1.12
+ **/
+NMTCQdisc *
+nm_tc_qdisc_dup(NMTCQdisc *qdisc)
+{
+    NMTCQdisc *copy;
+
+    g_return_val_if_fail(qdisc != NULL, NULL);
+    g_return_val_if_fail(qdisc->refcount > 0, NULL);
+
+    copy = nm_tc_qdisc_new(qdisc->kind, qdisc->parent, NULL);
+    nm_tc_qdisc_set_handle(copy, qdisc->handle);
+
+    if (qdisc->attributes) {
+        GHashTableIter iter;
+        const char *   key;
+        GVariant *     value;
+
+        g_hash_table_iter_init(&iter, qdisc->attributes);
+        while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &value))
+            nm_tc_qdisc_set_attribute(copy, key, value);
+    }
+
+    return copy;
+}
+
+/**
+ * nm_tc_qdisc_get_kind:
+ * @qdisc: the #NMTCQdisc
+ *
+ * Returns:
+ *
+ * Since: 1.12
+ **/
+const char *
+nm_tc_qdisc_get_kind(NMTCQdisc *qdisc)
+{
+    g_return_val_if_fail(qdisc != NULL, NULL);
+    g_return_val_if_fail(qdisc->refcount > 0, NULL);
+
+    return qdisc->kind;
+}
+
+/**
+ * nm_tc_qdisc_get_handle:
+ * @qdisc: the #NMTCQdisc
+ *
+ * Returns: the queueing discipline handle
+ *
+ * Since: 1.12
+ **/
+guint32
+nm_tc_qdisc_get_handle(NMTCQdisc *qdisc)
+{
+    g_return_val_if_fail(qdisc != NULL, TC_H_UNSPEC);
+    g_return_val_if_fail(qdisc->refcount > 0, TC_H_UNSPEC);
+
+    return qdisc->handle;
+}
+
+/**
+ * nm_tc_qdisc_set_handle:
+ * @qdisc: the #NMTCQdisc
+ * @handle: the queueing discipline handle
+ *
+ * Sets the queueing discipline handle.
+ *
+ * Since: 1.12
+ **/
+void
+nm_tc_qdisc_set_handle(NMTCQdisc *qdisc, guint32 handle)
+{
+    g_return_if_fail(qdisc != NULL);
+    g_return_if_fail(qdisc->refcount > 0);
+
+    qdisc->handle = handle;
+}
+
+/**
+ * nm_tc_qdisc_get_parent:
+ * @qdisc: the #NMTCQdisc
+ *
+ * Returns: the parent class
+ *
+ * Since: 1.12
+ **/
+guint32
+nm_tc_qdisc_get_parent(NMTCQdisc *qdisc)
+{
+    g_return_val_if_fail(qdisc != NULL, TC_H_UNSPEC);
+    g_return_val_if_fail(qdisc->refcount > 0, TC_H_UNSPEC);
+
+    return qdisc->parent;
+}
+
+/**
+ * nm_tc_qdisc_get_attribute_names:
+ * @qdisc: the #NMTCQdisc
+ *
+ * Gets an array of attribute names defined on @qdisc.
+ *
+ * Returns: (transfer container): a %NULL-terminated array of attribute names
+ *   or %NULL if no attributes are set.
+ *
+ * Since: 1.18
+ **/
+const char **
+nm_tc_qdisc_get_attribute_names(NMTCQdisc *qdisc)
+{
+    g_return_val_if_fail(qdisc, NULL);
+
+    return nm_utils_strdict_get_keys(qdisc->attributes, TRUE, NULL);
+}
+
+GHashTable *
+_nm_tc_qdisc_get_attributes(NMTCQdisc *qdisc)
+{
+    nm_assert(qdisc);
+
+    return qdisc->attributes;
+}
+
+/**
+ * nm_tc_qdisc_get_attribute:
+ * @qdisc: the #NMTCQdisc
+ * @name: the name of an qdisc attribute
+ *
+ * Gets the value of the attribute with name @name on @qdisc
+ *
+ * Returns: (transfer none): the value of the attribute with name @name on
+ *   @qdisc, or %NULL if @qdisc has no such attribute.
+ *
+ * Since: 1.18
+ **/
+GVariant *
+nm_tc_qdisc_get_attribute(NMTCQdisc *qdisc, const char *name)
+{
+    g_return_val_if_fail(qdisc != NULL, NULL);
+    g_return_val_if_fail(name != NULL && *name != '\0', NULL);
+
+    if (qdisc->attributes)
+        return g_hash_table_lookup(qdisc->attributes, name);
+    else
+        return NULL;
+}
+
+/**
+ * nm_tc_qdisc_set_attribute:
+ * @qdisc: the #NMTCQdisc
+ * @name: the name of an qdisc attribute
+ * @value: (transfer none) (allow-none): the value
+ *
+ * Sets or clears the named attribute on @qdisc to the given value.
+ *
+ * Since: 1.18
+ **/
+void
+nm_tc_qdisc_set_attribute(NMTCQdisc *qdisc, const char *name, GVariant *value)
+{
+    g_return_if_fail(qdisc != NULL);
+    g_return_if_fail(name != NULL && *name != '\0');
+    g_return_if_fail(strcmp(name, "kind") != 0);
+
+    if (!qdisc->attributes) {
+        qdisc->attributes = g_hash_table_new_full(nm_str_hash,
+                                                  g_str_equal,
+                                                  g_free,
+                                                  (GDestroyNotify) g_variant_unref);
+    }
+
+    if (value)
+        g_hash_table_insert(qdisc->attributes, g_strdup(name), g_variant_ref_sink(value));
+    else
+        g_hash_table_remove(qdisc->attributes, name);
+}
+
+/*****************************************************************************/
+
+G_DEFINE_BOXED_TYPE(NMTCAction, nm_tc_action, nm_tc_action_dup, nm_tc_action_unref)
+
+struct NMTCAction {
+    guint refcount;
+
+    char *kind;
+
+    GHashTable *attributes;
+};
+
+/**
+ * nm_tc_action_new:
+ * @kind: name of the queueing discipline
+ * @error: location to store error, or %NULL
+ *
+ * Creates a new #NMTCAction object.
+ *
+ * Returns: (transfer full): the new #NMTCAction object, or %NULL on error
+ *
+ * Since: 1.12
+ **/
+NMTCAction *
+nm_tc_action_new(const char *kind, GError **error)
+{
+    NMTCAction *action;
+
+    if (!kind || !*kind) {
+        g_set_error(error,
+                    NM_CONNECTION_ERROR,
+                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                    _("kind is missing"));
+        return NULL;
+    }
+
+    if (strchr(kind, ' ') || strchr(kind, '\t')) {
+        g_set_error(error,
+                    NM_CONNECTION_ERROR,
+                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                    _("'%s' is not a valid kind"),
+                    kind);
+        return NULL;
+    }
+
+    action           = g_slice_new0(NMTCAction);
+    action->refcount = 1;
+
+    action->kind = g_strdup(kind);
+
+    return action;
+}
+
+/**
+ * nm_tc_action_ref:
+ * @action: the #NMTCAction
+ *
+ * Increases the reference count of the object.
+ *
+ * Since: 1.12
+ **/
+void
+nm_tc_action_ref(NMTCAction *action)
+{
+    g_return_if_fail(action != NULL);
+    g_return_if_fail(action->refcount > 0);
+
+    action->refcount++;
+}
+
+/**
+ * nm_tc_action_unref:
+ * @action: the #NMTCAction
+ *
+ * Decreases the reference count of the object.  If the reference count
+ * reaches zero, the object will be destroyed.
+ *
+ * Since: 1.12
+ **/
+void
+nm_tc_action_unref(NMTCAction *action)
+{
+    g_return_if_fail(action != NULL);
+    g_return_if_fail(action->refcount > 0);
+
+    action->refcount--;
+    if (action->refcount == 0) {
+        g_free(action->kind);
+        if (action->attributes)
+            g_hash_table_unref(action->attributes);
+        g_slice_free(NMTCAction, action);
+    }
+}
+
+/**
+ * nm_tc_action_equal:
+ * @action: the #NMTCAction
+ * @other: the #NMTCAction to compare @action to.
+ *
+ * Determines if two #NMTCAction objects contain the same kind, family,
+ * handle, parent and info.
+ *
+ * Returns: %TRUE if the objects contain the same values, %FALSE if they do not.
+ *
+ * Since: 1.12
+ **/
+gboolean
+nm_tc_action_equal(NMTCAction *action, NMTCAction *other)
+{
+    GHashTableIter iter;
+    const char *   key;
+    GVariant *     value, *value2;
+    guint          n;
+
+    g_return_val_if_fail(!action || action->refcount > 0, FALSE);
+    g_return_val_if_fail(!other || other->refcount > 0, FALSE);
+
+    if (action == other)
+        return TRUE;
+    if (!action || !other)
+        return FALSE;
+
+    if (g_strcmp0(action->kind, other->kind) != 0)
+        return FALSE;
+
+    n = action->attributes ? g_hash_table_size(action->attributes) : 0;
+    if (n != (other->attributes ? g_hash_table_size(other->attributes) : 0))
+        return FALSE;
+    if (n) {
+        g_hash_table_iter_init(&iter, action->attributes);
+        while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &value)) {
+            value2 = g_hash_table_lookup(other->attributes, key);
+            if (!value2)
+                return FALSE;
+            if (!g_variant_equal(value, value2))
+                return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+/**
+ * nm_tc_action_dup:
+ * @action: the #NMTCAction
+ *
+ * Creates a copy of @action
+ *
+ * Returns: (transfer full): a copy of @action
+ *
+ * Since: 1.12
+ **/
+NMTCAction *
+nm_tc_action_dup(NMTCAction *action)
+{
+    NMTCAction *copy;
+
+    g_return_val_if_fail(action != NULL, NULL);
+    g_return_val_if_fail(action->refcount > 0, NULL);
+
+    copy = nm_tc_action_new(action->kind, NULL);
+
+    if (action->attributes) {
+        GHashTableIter iter;
+        const char *   key;
+        GVariant *     value;
+
+        g_hash_table_iter_init(&iter, action->attributes);
+        while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &value))
+            nm_tc_action_set_attribute(copy, key, value);
+    }
+
+    return copy;
+}
+
+/**
+ * nm_tc_action_get_kind:
+ * @action: the #NMTCAction
+ *
+ * Returns:
+ *
+ * Since: 1.12
+ **/
+const char *
+nm_tc_action_get_kind(NMTCAction *action)
+{
+    g_return_val_if_fail(action != NULL, NULL);
+    g_return_val_if_fail(action->refcount > 0, NULL);
+
+    return action->kind;
+}
+
+/**
+ * nm_tc_action_get_attribute_names:
+ * @action: the #NMTCAction
+ *
+ * Gets an array of attribute names defined on @action.
+ *
+ * Returns: (transfer full): a %NULL-terminated array of attribute names,
+ *
+ * Since: 1.12
+ **/
+char **
+nm_tc_action_get_attribute_names(NMTCAction *action)
+{
+    const char **names;
+
+    g_return_val_if_fail(action, NULL);
+
+    names = nm_utils_strdict_get_keys(action->attributes, TRUE, NULL);
+    return nm_utils_strv_make_deep_copied_nonnull(names);
+}
+
+GHashTable *
+_nm_tc_action_get_attributes(NMTCAction *action)
+{
+    nm_assert(action);
+
+    return action->attributes;
+}
+
+/**
+ * nm_tc_action_get_attribute:
+ * @action: the #NMTCAction
+ * @name: the name of an action attribute
+ *
+ * Gets the value of the attribute with name @name on @action
+ *
+ * Returns: (transfer none): the value of the attribute with name @name on
+ *   @action, or %NULL if @action has no such attribute.
+ *
+ * Since: 1.12
+ **/
+GVariant *
+nm_tc_action_get_attribute(NMTCAction *action, const char *name)
+{
+    g_return_val_if_fail(action != NULL, NULL);
+    g_return_val_if_fail(name != NULL && *name != '\0', NULL);
+
+    if (action->attributes)
+        return g_hash_table_lookup(action->attributes, name);
+    else
+        return NULL;
+}
+
+/**
+ * nm_tc_action_set_attribute:
+ * @action: the #NMTCAction
+ * @name: the name of an action attribute
+ * @value: (transfer none) (allow-none): the value
+ *
+ * Sets or clears the named attribute on @action to the given value.
+ *
+ * Since: 1.12
+ **/
+void
+nm_tc_action_set_attribute(NMTCAction *action, const char *name, GVariant *value)
+{
+    g_return_if_fail(action != NULL);
+    g_return_if_fail(name != NULL && *name != '\0');
+    g_return_if_fail(strcmp(name, "kind") != 0);
+
+    if (!action->attributes) {
+        action->attributes = g_hash_table_new_full(nm_str_hash,
+                                                   g_str_equal,
+                                                   g_free,
+                                                   (GDestroyNotify) g_variant_unref);
+    }
+
+    if (value)
+        g_hash_table_insert(action->attributes, g_strdup(name), g_variant_ref_sink(value));
+    else
+        g_hash_table_remove(action->attributes, name);
+}
+
+/*****************************************************************************/
+
+G_DEFINE_BOXED_TYPE(NMTCTfilter, nm_tc_tfilter, nm_tc_tfilter_dup, nm_tc_tfilter_unref)
+
+struct NMTCTfilter {
+    guint refcount;
+
+    char *      kind;
+    guint32     handle;
+    guint32     parent;
+    NMTCAction *action;
+};
+
+/**
+ * nm_tc_tfilter_new:
+ * @kind: name of the queueing discipline
+ * @parent: the parent queueing discipline
+ * @error: location to store error, or %NULL
+ *
+ * Creates a new #NMTCTfilter object.
+ *
+ * Returns: (transfer full): the new #NMTCTfilter object, or %NULL on error
+ *
+ * Since: 1.12
+ **/
+NMTCTfilter *
+nm_tc_tfilter_new(const char *kind, guint32 parent, GError **error)
+{
+    NMTCTfilter *tfilter;
+
+    if (!kind || !*kind) {
+        g_set_error(error,
+                    NM_CONNECTION_ERROR,
+                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                    _("kind is missing"));
+        return NULL;
+    }
+
+    if (strchr(kind, ' ') || strchr(kind, '\t')) {
+        g_set_error(error,
+                    NM_CONNECTION_ERROR,
+                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                    _("'%s' is not a valid kind"),
+                    kind);
+        return NULL;
+    }
+
+    if (!parent) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("parent handle missing"));
+        return NULL;
+    }
+
+    tfilter           = g_slice_new0(NMTCTfilter);
+    tfilter->refcount = 1;
+
+    tfilter->kind   = g_strdup(kind);
+    tfilter->parent = parent;
+
+    return tfilter;
+}
+
+/**
+ * nm_tc_tfilter_ref:
+ * @tfilter: the #NMTCTfilter
+ *
+ * Increases the reference count of the object.
+ *
+ * Since: 1.12
+ **/
+void
+nm_tc_tfilter_ref(NMTCTfilter *tfilter)
+{
+    g_return_if_fail(tfilter != NULL);
+    g_return_if_fail(tfilter->refcount > 0);
+
+    tfilter->refcount++;
+}
+
+/**
+ * nm_tc_tfilter_unref:
+ * @tfilter: the #NMTCTfilter
+ *
+ * Decreases the reference count of the object.  If the reference count
+ * reaches zero, the object will be destroyed.
+ *
+ * Since: 1.12
+ **/
+void
+nm_tc_tfilter_unref(NMTCTfilter *tfilter)
+{
+    g_return_if_fail(tfilter != NULL);
+    g_return_if_fail(tfilter->refcount > 0);
+
+    tfilter->refcount--;
+    if (tfilter->refcount == 0) {
+        g_free(tfilter->kind);
+        if (tfilter->action)
+            nm_tc_action_unref(tfilter->action);
+        g_slice_free(NMTCTfilter, tfilter);
+    }
+}
+
+/**
+ * nm_tc_tfilter_equal:
+ * @tfilter: the #NMTCTfilter
+ * @other: the #NMTCTfilter to compare @tfilter to.
+ *
+ * Determines if two #NMTCTfilter objects contain the same kind, family,
+ * handle, parent and info.
+ *
+ * Returns: %TRUE if the objects contain the same values, %FALSE if they do not.
+ *
+ * Since: 1.12
+ **/
+gboolean
+nm_tc_tfilter_equal(NMTCTfilter *tfilter, NMTCTfilter *other)
+{
+    g_return_val_if_fail(tfilter != NULL, FALSE);
+    g_return_val_if_fail(tfilter->refcount > 0, FALSE);
+
+    g_return_val_if_fail(other != NULL, FALSE);
+    g_return_val_if_fail(other->refcount > 0, FALSE);
+
+    if (tfilter->handle != other->handle || tfilter->parent != other->parent
+        || g_strcmp0(tfilter->kind, other->kind) != 0
+        || !nm_tc_action_equal(tfilter->action, other->action))
+        return FALSE;
+
+    return TRUE;
+}
+
+static guint
+_nm_tc_tfilter_hash(NMTCTfilter *tfilter)
+{
+    NMHashState h;
+
+    nm_hash_init(&h, 63624437);
+    nm_hash_update_vals(&h, tfilter->handle, tfilter->parent);
+    nm_hash_update_str0(&h, tfilter->kind);
+
+    if (tfilter->action) {
+        gs_free NMUtilsNamedValue *attrs_free = NULL;
+        NMUtilsNamedValue          attrs_static[30];
+        const NMUtilsNamedValue *  attrs;
+        guint                      length;
+        guint                      i;
+
+        nm_hash_update_str0(&h, tfilter->action->kind);
+
+        attrs = nm_utils_named_values_from_strdict(tfilter->action->attributes,
+                                                   &length,
+                                                   attrs_static,
+                                                   &attrs_free);
+        for (i = 0; i < length; i++) {
+            GVariant *variant = attrs[i].value_ptr;
+
+            nm_hash_update_str(&h, attrs[i].name);
+            if (g_variant_type_is_basic(g_variant_get_type(variant))) {
+                guint attr_hash;
+
+                /* g_variant_hash() works only for basic types, thus
+                 * we ignore any non-basic attribute. Actions differing
+                 * only for non-basic attributes will collide. */
+                attr_hash = g_variant_hash(variant);
+                nm_hash_update_val(&h, attr_hash);
+            }
+        }
+    }
+
+    return nm_hash_complete(&h);
+}
+
+/**
+ * nm_tc_tfilter_dup:
+ * @tfilter: the #NMTCTfilter
+ *
+ * Creates a copy of @tfilter
+ *
+ * Returns: (transfer full): a copy of @tfilter
+ *
+ * Since: 1.12
+ **/
+NMTCTfilter *
+nm_tc_tfilter_dup(NMTCTfilter *tfilter)
+{
+    NMTCTfilter *copy;
+
+    g_return_val_if_fail(tfilter != NULL, NULL);
+    g_return_val_if_fail(tfilter->refcount > 0, NULL);
+
+    copy = nm_tc_tfilter_new(tfilter->kind, tfilter->parent, NULL);
+    nm_tc_tfilter_set_handle(copy, tfilter->handle);
+    nm_tc_tfilter_set_action(copy, tfilter->action);
+
+    return copy;
+}
+
+/**
+ * nm_tc_tfilter_get_kind:
+ * @tfilter: the #NMTCTfilter
+ *
+ * Returns:
+ *
+ * Since: 1.12
+ **/
+const char *
+nm_tc_tfilter_get_kind(NMTCTfilter *tfilter)
+{
+    g_return_val_if_fail(tfilter != NULL, NULL);
+    g_return_val_if_fail(tfilter->refcount > 0, NULL);
+
+    return tfilter->kind;
+}
+
+/**
+ * nm_tc_tfilter_get_handle:
+ * @tfilter: the #NMTCTfilter
+ *
+ * Returns: the queueing discipline handle
+ *
+ * Since: 1.12
+ **/
+guint32
+nm_tc_tfilter_get_handle(NMTCTfilter *tfilter)
+{
+    g_return_val_if_fail(tfilter != NULL, TC_H_UNSPEC);
+    g_return_val_if_fail(tfilter->refcount > 0, TC_H_UNSPEC);
+
+    return tfilter->handle;
+}
+
+/**
+ * nm_tc_tfilter_set_handle:
+ * @tfilter: the #NMTCTfilter
+ * @handle: the queueing discipline handle
+ *
+ * Sets the queueing discipline handle.
+ *
+ * Since: 1.12
+ **/
+void
+nm_tc_tfilter_set_handle(NMTCTfilter *tfilter, guint32 handle)
+{
+    g_return_if_fail(tfilter != NULL);
+    g_return_if_fail(tfilter->refcount > 0);
+
+    tfilter->handle = handle;
+}
+
+/**
+ * nm_tc_tfilter_get_parent:
+ * @tfilter: the #NMTCTfilter
+ *
+ * Returns: the parent class
+ *
+ * Since: 1.12
+ **/
+guint32
+nm_tc_tfilter_get_parent(NMTCTfilter *tfilter)
+{
+    g_return_val_if_fail(tfilter != NULL, TC_H_UNSPEC);
+    g_return_val_if_fail(tfilter->refcount > 0, TC_H_UNSPEC);
+
+    return tfilter->parent;
+}
+
+/**
+ * nm_tc_tfilter_get_action:
+ * @tfilter: the #NMTCTfilter
+ *
+ * Returns: the action associated with a traffic filter.
+ *
+ * Since: 1.12
+ **/
+NMTCAction *
+nm_tc_tfilter_get_action(NMTCTfilter *tfilter)
+{
+    g_return_val_if_fail(tfilter != NULL, TC_H_UNSPEC);
+    g_return_val_if_fail(tfilter->refcount > 0, TC_H_UNSPEC);
+
+    if (tfilter->action == NULL)
+        return NULL;
+
+    return tfilter->action;
+}
+
+/**
+ * nm_tc_tfilter_set_action:
+ * @tfilter: the #NMTCTfilter
+ * @action: the action object
+ *
+ * Sets the action associated with a traffic filter.
+ *
+ * Since: 1.12
+ **/
+void
+nm_tc_tfilter_set_action(NMTCTfilter *tfilter, NMTCAction *action)
+{
+    g_return_if_fail(tfilter != NULL);
+    g_return_if_fail(tfilter->refcount > 0);
+
+    if (action)
+        nm_tc_action_ref(action);
+    if (tfilter->action)
+        nm_tc_action_unref(tfilter->action);
+    tfilter->action = action;
+}
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMSettingTCConfig, PROP_QDISCS, PROP_TFILTERS, );
+
+/**
+ * NMSettingTCConfig:
+ *
+ * Linux Traffic Control Settings
+ *
+ * Since: 1.12
+ */
+struct _NMSettingTCConfig {
+    NMSetting  parent;
+    GPtrArray *qdiscs;
+    GPtrArray *tfilters;
+};
+
+struct _NMSettingTCConfigClass {
+    NMSettingClass parent;
+};
+
+G_DEFINE_TYPE(NMSettingTCConfig, nm_setting_tc_config, NM_TYPE_SETTING)
+
+/*****************************************************************************/
+
+/**
+ * nm_setting_tc_config_get_num_qdiscs:
+ * @setting: the #NMSettingTCConfig
+ *
+ * Returns: the number of configured queueing disciplines
+ *
+ * Since: 1.12
+ **/
+guint
+nm_setting_tc_config_get_num_qdiscs(NMSettingTCConfig *self)
+{
+    g_return_val_if_fail(NM_IS_SETTING_TC_CONFIG(self), 0);
+
+    return self->qdiscs->len;
+}
+
+/**
+ * nm_setting_tc_config_get_qdisc:
+ * @setting: the #NMSettingTCConfig
+ * @idx: index number of the qdisc to return
+ *
+ * Returns: (transfer none): the qdisc at index @idx
+ *
+ * Since: 1.12
+ **/
+NMTCQdisc *
+nm_setting_tc_config_get_qdisc(NMSettingTCConfig *self, guint idx)
+{
+    g_return_val_if_fail(NM_IS_SETTING_TC_CONFIG(self), NULL);
+    g_return_val_if_fail(idx < self->qdiscs->len, NULL);
+
+    return self->qdiscs->pdata[idx];
+}
+
+/**
+ * nm_setting_tc_config_add_qdisc:
+ * @setting: the #NMSettingTCConfig
+ * @qdisc: the qdisc to add
+ *
+ * Appends a new qdisc and associated information to the setting.  The
+ * given qdisc is duplicated internally and is not changed by this function.
+ * If an identical qdisc (considering attributes as well) already exists, the
+ * qdisc is not added and the function returns %FALSE.
+ *
+ * Returns: %TRUE if the qdisc was added; %FALSE if the qdisc was already known.
+ *
+ * Since: 1.12
+ **/
+gboolean
+nm_setting_tc_config_add_qdisc(NMSettingTCConfig *self, NMTCQdisc *qdisc)
+{
+    guint i;
+
+    g_return_val_if_fail(NM_IS_SETTING_TC_CONFIG(self), FALSE);
+    g_return_val_if_fail(qdisc != NULL, FALSE);
+
+    for (i = 0; i < self->qdiscs->len; i++) {
+        if (nm_tc_qdisc_equal(self->qdiscs->pdata[i], qdisc))
+            return FALSE;
+    }
+
+    g_ptr_array_add(self->qdiscs, nm_tc_qdisc_dup(qdisc));
+    _notify(self, PROP_QDISCS);
+    return TRUE;
+}
+
+/**
+ * nm_setting_tc_config_remove_qdisc:
+ * @setting: the #NMSettingTCConfig
+ * @idx: index number of the qdisc
+ *
+ * Removes the qdisc at index @idx.
+ *
+ * Since: 1.12
+ **/
+void
+nm_setting_tc_config_remove_qdisc(NMSettingTCConfig *self, guint idx)
+{
+    g_return_if_fail(NM_IS_SETTING_TC_CONFIG(self));
+
+    g_return_if_fail(idx < self->qdiscs->len);
+
+    g_ptr_array_remove_index(self->qdiscs, idx);
+    _notify(self, PROP_QDISCS);
+}
+
+/**
+ * nm_setting_tc_config_remove_qdisc_by_value:
+ * @setting: the #NMSettingTCConfig
+ * @qdisc: the qdisc to remove
+ *
+ * Removes the first matching qdisc that matches @qdisc.
+ *
+ * Returns: %TRUE if the qdisc was found and removed; %FALSE if it was not.
+ *
+ * Since: 1.12
+ **/
+gboolean
+nm_setting_tc_config_remove_qdisc_by_value(NMSettingTCConfig *self, NMTCQdisc *qdisc)
+{
+    guint i;
+
+    g_return_val_if_fail(NM_IS_SETTING_TC_CONFIG(self), FALSE);
+    g_return_val_if_fail(qdisc != NULL, FALSE);
+
+    for (i = 0; i < self->qdiscs->len; i++) {
+        if (nm_tc_qdisc_equal(self->qdiscs->pdata[i], qdisc)) {
+            g_ptr_array_remove_index(self->qdiscs, i);
+            _notify(self, PROP_QDISCS);
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+
+/**
+ * nm_setting_tc_config_clear_qdiscs:
+ * @setting: the #NMSettingTCConfig
+ *
+ * Removes all configured queueing disciplines.
+ *
+ * Since: 1.12
+ **/
+void
+nm_setting_tc_config_clear_qdiscs(NMSettingTCConfig *self)
+{
+    g_return_if_fail(NM_IS_SETTING_TC_CONFIG(self));
+
+    if (self->qdiscs->len != 0) {
+        g_ptr_array_set_size(self->qdiscs, 0);
+        _notify(self, PROP_QDISCS);
+    }
+}
+
+/*****************************************************************************/
+/**
+ * nm_setting_tc_config_get_num_tfilters:
+ * @setting: the #NMSettingTCConfig
+ *
+ * Returns: the number of configured queueing disciplines
+ *
+ * Since: 1.12
+ **/
+guint
+nm_setting_tc_config_get_num_tfilters(NMSettingTCConfig *self)
+{
+    g_return_val_if_fail(NM_IS_SETTING_TC_CONFIG(self), 0);
+
+    return self->tfilters->len;
+}
+
+/**
+ * nm_setting_tc_config_get_tfilter:
+ * @setting: the #NMSettingTCConfig
+ * @idx: index number of the tfilter to return
+ *
+ * Returns: (transfer none): the tfilter at index @idx
+ *
+ * Since: 1.12
+ **/
+NMTCTfilter *
+nm_setting_tc_config_get_tfilter(NMSettingTCConfig *self, guint idx)
+{
+    g_return_val_if_fail(NM_IS_SETTING_TC_CONFIG(self), NULL);
+    g_return_val_if_fail(idx < self->tfilters->len, NULL);
+
+    return self->tfilters->pdata[idx];
+}
+
+/**
+ * nm_setting_tc_config_add_tfilter:
+ * @setting: the #NMSettingTCConfig
+ * @tfilter: the tfilter to add
+ *
+ * Appends a new tfilter and associated information to the setting.  The
+ * given tfilter is duplicated internally and is not changed by this function.
+ * If an identical tfilter (considering attributes as well) already exists, the
+ * tfilter is not added and the function returns %FALSE.
+ *
+ * Returns: %TRUE if the tfilter was added; %FALSE if the tfilter was already known.
+ *
+ * Since: 1.12
+ **/
+gboolean
+nm_setting_tc_config_add_tfilter(NMSettingTCConfig *self, NMTCTfilter *tfilter)
+{
+    guint i;
+
+    g_return_val_if_fail(NM_IS_SETTING_TC_CONFIG(self), FALSE);
+    g_return_val_if_fail(tfilter != NULL, FALSE);
+
+    for (i = 0; i < self->tfilters->len; i++) {
+        if (nm_tc_tfilter_equal(self->tfilters->pdata[i], tfilter))
+            return FALSE;
+    }
+
+    g_ptr_array_add(self->tfilters, nm_tc_tfilter_dup(tfilter));
+    _notify(self, PROP_TFILTERS);
+    return TRUE;
+}
+
+/**
+ * nm_setting_tc_config_remove_tfilter:
+ * @setting: the #NMSettingTCConfig
+ * @idx: index number of the tfilter
+ *
+ * Removes the tfilter at index @idx.
+ *
+ * Since: 1.12
+ **/
+void
+nm_setting_tc_config_remove_tfilter(NMSettingTCConfig *self, guint idx)
+{
+    g_return_if_fail(NM_IS_SETTING_TC_CONFIG(self));
+    g_return_if_fail(idx < self->tfilters->len);
+
+    g_ptr_array_remove_index(self->tfilters, idx);
+    _notify(self, PROP_TFILTERS);
+}
+
+/**
+ * nm_setting_tc_config_remove_tfilter_by_value:
+ * @setting: the #NMSettingTCConfig
+ * @tfilter: the tfilter to remove
+ *
+ * Removes the first matching tfilter that matches @tfilter.
+ *
+ * Returns: %TRUE if the tfilter was found and removed; %FALSE if it was not.
+ *
+ * Since: 1.12
+ **/
+gboolean
+nm_setting_tc_config_remove_tfilter_by_value(NMSettingTCConfig *self, NMTCTfilter *tfilter)
+{
+    guint i;
+
+    g_return_val_if_fail(NM_IS_SETTING_TC_CONFIG(self), FALSE);
+    g_return_val_if_fail(tfilter != NULL, FALSE);
+
+    for (i = 0; i < self->tfilters->len; i++) {
+        if (nm_tc_tfilter_equal(self->tfilters->pdata[i], tfilter)) {
+            g_ptr_array_remove_index(self->tfilters, i);
+            _notify(self, PROP_TFILTERS);
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+
+/**
+ * nm_setting_tc_config_clear_tfilters:
+ * @setting: the #NMSettingTCConfig
+ *
+ * Removes all configured queueing disciplines.
+ *
+ * Since: 1.12
+ **/
+void
+nm_setting_tc_config_clear_tfilters(NMSettingTCConfig *self)
+{
+    g_return_if_fail(NM_IS_SETTING_TC_CONFIG(self));
+
+    if (self->tfilters->len != 0) {
+        g_ptr_array_set_size(self->tfilters, 0);
+        _notify(self, PROP_TFILTERS);
+    }
+}
+
+/*****************************************************************************/
+
+static gboolean
+verify(NMSetting *setting, NMConnection *connection, GError **error)
+{
+    NMSettingTCConfig *self = NM_SETTING_TC_CONFIG(setting);
+    guint              i;
+
+    if (self->qdiscs->len != 0) {
+        gs_unref_hashtable GHashTable *ht = NULL;
+
+        ht = g_hash_table_new((GHashFunc) _nm_tc_qdisc_hash, (GEqualFunc) nm_tc_qdisc_equal);
+        for (i = 0; i < self->qdiscs->len; i++) {
+            if (!g_hash_table_add(ht, self->qdiscs->pdata[i])) {
+                g_set_error_literal(error,
+                                    NM_CONNECTION_ERROR,
+                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                    _("there are duplicate TC qdiscs"));
+                g_prefix_error(error,
+                               "%s.%s: ",
+                               NM_SETTING_TC_CONFIG_SETTING_NAME,
+                               NM_SETTING_TC_CONFIG_QDISCS);
+                return FALSE;
+            }
+        }
+    }
+
+    if (self->tfilters->len != 0) {
+        gs_unref_hashtable GHashTable *ht = NULL;
+
+        ht = g_hash_table_new((GHashFunc) _nm_tc_tfilter_hash, (GEqualFunc) nm_tc_tfilter_equal);
+        for (i = 0; i < self->tfilters->len; i++) {
+            if (!g_hash_table_add(ht, self->tfilters->pdata[i])) {
+                g_set_error_literal(error,
+                                    NM_CONNECTION_ERROR,
+                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                    _("there are duplicate TC filters"));
+                g_prefix_error(error,
+                               "%s.%s: ",
+                               NM_SETTING_TC_CONFIG_SETTING_NAME,
+                               NM_SETTING_TC_CONFIG_TFILTERS);
+                return FALSE;
+            }
+        }
+    }
+
+    return TRUE;
+}
+
+static NMTernary
+compare_property(const NMSettInfoSetting *sett_info,
+                 guint                    property_idx,
+                 NMConnection *           con_a,
+                 NMSetting *              set_a,
+                 NMConnection *           con_b,
+                 NMSetting *              set_b,
+                 NMSettingCompareFlags    flags)
+{
+    NMSettingTCConfig *a_tc_config = NM_SETTING_TC_CONFIG(set_a);
+    NMSettingTCConfig *b_tc_config = NM_SETTING_TC_CONFIG(set_b);
+    guint              i;
+
+    if (nm_streq(sett_info->property_infos[property_idx].name, NM_SETTING_TC_CONFIG_QDISCS)) {
+        if (set_b) {
+            if (a_tc_config->qdiscs->len != b_tc_config->qdiscs->len)
+                return FALSE;
+            for (i = 0; i < a_tc_config->qdiscs->len; i++) {
+                if (!nm_tc_qdisc_equal(a_tc_config->qdiscs->pdata[i],
+                                       b_tc_config->qdiscs->pdata[i]))
+                    return FALSE;
+            }
+        }
+        return TRUE;
+    }
+
+    if (nm_streq(sett_info->property_infos[property_idx].name, NM_SETTING_TC_CONFIG_TFILTERS)) {
+        if (set_b) {
+            if (a_tc_config->tfilters->len != b_tc_config->tfilters->len)
+                return FALSE;
+            for (i = 0; i < a_tc_config->tfilters->len; i++) {
+                if (!nm_tc_tfilter_equal(a_tc_config->tfilters->pdata[i],
+                                         b_tc_config->tfilters->pdata[i]))
+                    return FALSE;
+            }
+        }
+        return TRUE;
+    }
+
+    return NM_SETTING_CLASS(nm_setting_tc_config_parent_class)
+        ->compare_property(sett_info, property_idx, con_a, set_a, con_b, set_b, flags);
+}
+
+/**
+ * _qdiscs_to_variant:
+ * @qdiscs: (element-type NMTCQdisc): an array of #NMTCQdisc objects
+ *
+ * Utility function to convert a #GPtrArray of #NMTCQdisc objects representing
+ * TC qdiscs into a #GVariant of type 'aa{sv}' representing an array
+ * of NetworkManager TC qdiscs.
+ *
+ * Returns: (transfer none): a new floating #GVariant representing @qdiscs.
+ **/
+static GVariant *
+_qdiscs_to_variant(GPtrArray *qdiscs)
+{
+    GVariantBuilder builder;
+    int             i;
+
+    g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}"));
+
+    if (qdiscs) {
+        for (i = 0; i < qdiscs->len; i++) {
+            NMUtilsNamedValue attrs_static[30];
+            gs_free NMUtilsNamedValue *attrs_free = NULL;
+            const NMUtilsNamedValue *  attrs;
+            NMTCQdisc *                qdisc = qdiscs->pdata[i];
+            guint                      length;
+            GVariantBuilder            qdisc_builder;
+            guint                      y;
+
+            g_variant_builder_init(&qdisc_builder, G_VARIANT_TYPE_VARDICT);
+
+            g_variant_builder_add(&qdisc_builder,
+                                  "{sv}",
+                                  "kind",
+                                  g_variant_new_string(nm_tc_qdisc_get_kind(qdisc)));
+
+            g_variant_builder_add(&qdisc_builder,
+                                  "{sv}",
+                                  "handle",
+                                  g_variant_new_uint32(nm_tc_qdisc_get_handle(qdisc)));
+
+            g_variant_builder_add(&qdisc_builder,
+                                  "{sv}",
+                                  "parent",
+                                  g_variant_new_uint32(nm_tc_qdisc_get_parent(qdisc)));
+
+            attrs = nm_utils_named_values_from_strdict(qdisc->attributes,
+                                                       &length,
+                                                       attrs_static,
+                                                       &attrs_free);
+            for (y = 0; y < length; y++) {
+                g_variant_builder_add(&qdisc_builder, "{sv}", attrs[y].name, attrs[y].value_ptr);
+            }
+
+            g_variant_builder_add(&builder, "a{sv}", &qdisc_builder);
+        }
+    }
+
+    return g_variant_builder_end(&builder);
+}
+
+/**
+ * _qdiscs_from_variant:
+ * @value: a #GVariant of type 'aa{sv}'
+ *
+ * Utility function to convert a #GVariant representing a list of TC qdiscs
+ * into a #GPtrArray of * #NMTCQdisc objects.
+ *
+ * Returns: (transfer full) (element-type NMTCQdisc): a newly allocated
+ *   #GPtrArray of #NMTCQdisc objects
+ **/
+static GPtrArray *
+_qdiscs_from_variant(GVariant *value)
+{
+    GPtrArray *  qdiscs;
+    GVariant *   qdisc_var;
+    GVariantIter iter;
+    GError *     error = NULL;
+
+    g_return_val_if_fail(g_variant_is_of_type(value, G_VARIANT_TYPE("aa{sv}")), NULL);
+
+    g_variant_iter_init(&iter, value);
+    qdiscs = g_ptr_array_new_with_free_func((GDestroyNotify) nm_tc_qdisc_unref);
+
+    while (g_variant_iter_next(&iter, "@a{sv}", &qdisc_var)) {
+        const char * kind;
+        guint32      parent;
+        NMTCQdisc *  qdisc;
+        GVariantIter qdisc_iter;
+        const char * key;
+        GVariant *   attr_value;
+
+        if (!g_variant_lookup(qdisc_var, "kind", "&s", &kind)
+            || !g_variant_lookup(qdisc_var, "parent", "u", &parent)) {
+            //g_warning ("Ignoring invalid qdisc");
+            goto next;
+        }
+
+        qdisc = nm_tc_qdisc_new(kind, parent, &error);
+        if (!qdisc) {
+            //g_warning ("Ignoring invalid qdisc: %s", error->message);
+            g_clear_error(&error);
+            goto next;
+        }
+
+        g_variant_iter_init(&qdisc_iter, qdisc_var);
+        while (g_variant_iter_next(&qdisc_iter, "{&sv}", &key, &attr_value)) {
+            if (strcmp(key, "kind") == 0 || strcmp(key, "parent") == 0) {
+                /* Already processed above */
+            } else if (strcmp(key, "handle") == 0) {
+                nm_tc_qdisc_set_handle(qdisc, g_variant_get_uint32(attr_value));
+            } else {
+                nm_tc_qdisc_set_attribute(qdisc, key, attr_value);
+            }
+            g_variant_unref(attr_value);
+        }
+
+        g_ptr_array_add(qdiscs, qdisc);
+next:
+        g_variant_unref(qdisc_var);
+    }
+
+    return qdiscs;
+}
+
+static GVariant *
+tc_qdiscs_get(const NMSettInfoSetting *               sett_info,
+              guint                                   property_idx,
+              NMConnection *                          connection,
+              NMSetting *                             setting,
+              NMConnectionSerializationFlags          flags,
+              const NMConnectionSerializationOptions *options)
+{
+    gs_unref_ptrarray GPtrArray *qdiscs = NULL;
+
+    g_object_get(setting, NM_SETTING_TC_CONFIG_QDISCS, &qdiscs, NULL);
+    return _qdiscs_to_variant(qdiscs);
+}
+
+static gboolean
+tc_qdiscs_set(NMSetting *         setting,
+              GVariant *          connection_dict,
+              const char *        property,
+              GVariant *          value,
+              NMSettingParseFlags parse_flags,
+              GError **           error)
+{
+    GPtrArray *qdiscs;
+
+    qdiscs = _qdiscs_from_variant(value);
+    g_object_set(setting, NM_SETTING_TC_CONFIG_QDISCS, qdiscs, NULL);
+    g_ptr_array_unref(qdiscs);
+
+    return TRUE;
+}
+
+static GVariant *
+_action_to_variant(NMTCAction *action)
+{
+    GVariantBuilder    builder;
+    gs_strfreev char **attrs = nm_tc_action_get_attribute_names(action);
+    int                i;
+
+    g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
+
+    g_variant_builder_add(&builder,
+                          "{sv}",
+                          "kind",
+                          g_variant_new_string(nm_tc_action_get_kind(action)));
+
+    for (i = 0; attrs[i]; i++) {
+        g_variant_builder_add(&builder,
+                              "{sv}",
+                              attrs[i],
+                              nm_tc_action_get_attribute(action, attrs[i]));
+    }
+
+    return g_variant_builder_end(&builder);
+}
+
+/**
+ * _tfilters_to_variant:
+ * @tfilters: (element-type NMTCTfilter): an array of #NMTCTfilter objects
+ *
+ * Utility function to convert a #GPtrArray of #NMTCTfilter objects representing
+ * TC tfilters into a #GVariant of type 'aa{sv}' representing an array
+ * of NetworkManager TC tfilters.
+ *
+ * Returns: (transfer none): a new floating #GVariant representing @tfilters.
+ **/
+static GVariant *
+_tfilters_to_variant(GPtrArray *tfilters)
+{
+    GVariantBuilder builder;
+    int             i;
+
+    g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}"));
+
+    if (tfilters) {
+        for (i = 0; i < tfilters->len; i++) {
+            NMTCTfilter *   tfilter = tfilters->pdata[i];
+            NMTCAction *    action  = nm_tc_tfilter_get_action(tfilter);
+            GVariantBuilder tfilter_builder;
+
+            g_variant_builder_init(&tfilter_builder, G_VARIANT_TYPE("a{sv}"));
+
+            g_variant_builder_add(&tfilter_builder,
+                                  "{sv}",
+                                  "kind",
+                                  g_variant_new_string(nm_tc_tfilter_get_kind(tfilter)));
+            g_variant_builder_add(&tfilter_builder,
+                                  "{sv}",
+                                  "handle",
+                                  g_variant_new_uint32(nm_tc_tfilter_get_handle(tfilter)));
+            g_variant_builder_add(&tfilter_builder,
+                                  "{sv}",
+                                  "parent",
+                                  g_variant_new_uint32(nm_tc_tfilter_get_parent(tfilter)));
+
+            if (action) {
+                g_variant_builder_add(&tfilter_builder,
+                                      "{sv}",
+                                      "action",
+                                      _action_to_variant(action));
+            }
+
+            g_variant_builder_add(&builder, "a{sv}", &tfilter_builder);
+        }
+    }
+
+    return g_variant_builder_end(&builder);
+}
+
+/**
+ * _tfilters_from_variant:
+ * @value: a #GVariant of type 'aa{sv}'
+ *
+ * Utility function to convert a #GVariant representing a list of TC tfilters
+ * into a #GPtrArray of * #NMTCTfilter objects.
+ *
+ * Returns: (transfer full) (element-type NMTCTfilter): a newly allocated
+ *   #GPtrArray of #NMTCTfilter objects
+ **/
+static GPtrArray *
+_tfilters_from_variant(GVariant *value)
+{
+    GPtrArray *  tfilters;
+    GVariant *   tfilter_var;
+    GVariantIter iter;
+    GError *     error = NULL;
+
+    g_return_val_if_fail(g_variant_is_of_type(value, G_VARIANT_TYPE("aa{sv}")), NULL);
+
+    g_variant_iter_init(&iter, value);
+    tfilters = g_ptr_array_new_with_free_func((GDestroyNotify) nm_tc_tfilter_unref);
+
+    while (g_variant_iter_next(&iter, "@a{sv}", &tfilter_var)) {
+        NMTCTfilter *tfilter = NULL;
+        const char * kind;
+        guint32      handle;
+        guint32      parent;
+        NMTCAction * action;
+        const char * action_kind = NULL;
+        char *       action_key;
+        GVariantIter action_iter;
+        GVariant *   action_var = NULL;
+        GVariant *   action_val;
+
+        if (!g_variant_lookup(tfilter_var, "kind", "&s", &kind)
+            || !g_variant_lookup(tfilter_var, "parent", "u", &parent)) {
+            //g_warning ("Ignoring invalid tfilter");
+            goto next;
+        }
+
+        tfilter = nm_tc_tfilter_new(kind, parent, &error);
+        if (!tfilter) {
+            //g_warning ("Ignoring invalid tfilter: %s", error->message);
+            g_clear_error(&error);
+            goto next;
+        }
+
+        if (g_variant_lookup(tfilter_var, "handle", "u", &handle))
+            nm_tc_tfilter_set_handle(tfilter, handle);
+
+        action_var = g_variant_lookup_value(tfilter_var, "action", G_VARIANT_TYPE_VARDICT);
+
+        if (action_var) {
+            if (!g_variant_lookup(action_var, "kind", "&s", &action_kind)) {
+                //g_warning ("Ignoring tfilter with invalid action");
+                goto next;
+            }
+
+            action = nm_tc_action_new(action_kind, &error);
+            if (!action) {
+                //g_warning ("Ignoring tfilter with invalid action: %s", error->message);
+                g_clear_error(&error);
+                goto next;
+            }
+
+            g_variant_iter_init(&action_iter, action_var);
+            while (g_variant_iter_next(&action_iter, "{&sv}", &action_key, &action_val)) {
+                if (strcmp(action_key, "kind") != 0)
+                    nm_tc_action_set_attribute(action, action_key, action_val);
+                g_variant_unref(action_val);
+            }
+
+            nm_tc_tfilter_set_action(tfilter, action);
+            nm_tc_action_unref(action);
+        }
+
+        nm_tc_tfilter_ref(tfilter);
+        g_ptr_array_add(tfilters, tfilter);
+next:
+        if (tfilter)
+            nm_tc_tfilter_unref(tfilter);
+        if (action_var)
+            g_variant_unref(action_var);
+        g_variant_unref(tfilter_var);
+    }
+
+    return tfilters;
+}
+
+static GVariant *
+tc_tfilters_get(const NMSettInfoSetting *               sett_info,
+                guint                                   property_idx,
+                NMConnection *                          connection,
+                NMSetting *                             setting,
+                NMConnectionSerializationFlags          flags,
+                const NMConnectionSerializationOptions *options)
+{
+    gs_unref_ptrarray GPtrArray *tfilters = NULL;
+
+    g_object_get(setting, NM_SETTING_TC_CONFIG_TFILTERS, &tfilters, NULL);
+    return _tfilters_to_variant(tfilters);
+}
+
+static gboolean
+tc_tfilters_set(NMSetting *         setting,
+                GVariant *          connection_dict,
+                const char *        property,
+                GVariant *          value,
+                NMSettingParseFlags parse_flags,
+                GError **           error)
+{
+    gs_unref_ptrarray GPtrArray *tfilters = NULL;
+
+    tfilters = _tfilters_from_variant(value);
+    g_object_set(setting, NM_SETTING_TC_CONFIG_TFILTERS, tfilters, NULL);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMSettingTCConfig *self = NM_SETTING_TC_CONFIG(object);
+
+    switch (prop_id) {
+    case PROP_QDISCS:
+        g_value_take_boxed(value,
+                           _nm_utils_copy_array(self->qdiscs,
+                                                (NMUtilsCopyFunc) nm_tc_qdisc_dup,
+                                                (GDestroyNotify) nm_tc_qdisc_unref));
+        break;
+    case PROP_TFILTERS:
+        g_value_take_boxed(value,
+                           _nm_utils_copy_array(self->tfilters,
+                                                (NMUtilsCopyFunc) nm_tc_tfilter_dup,
+                                                (GDestroyNotify) nm_tc_tfilter_unref));
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+static void
+set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+    NMSettingTCConfig *self = NM_SETTING_TC_CONFIG(object);
+
+    switch (prop_id) {
+    case PROP_QDISCS:
+        g_ptr_array_unref(self->qdiscs);
+        self->qdiscs = _nm_utils_copy_array(g_value_get_boxed(value),
+                                            (NMUtilsCopyFunc) nm_tc_qdisc_dup,
+                                            (GDestroyNotify) nm_tc_qdisc_unref);
+        break;
+    case PROP_TFILTERS:
+        g_ptr_array_unref(self->tfilters);
+        self->tfilters = _nm_utils_copy_array(g_value_get_boxed(value),
+                                              (NMUtilsCopyFunc) nm_tc_tfilter_dup,
+                                              (GDestroyNotify) nm_tc_tfilter_unref);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_setting_tc_config_init(NMSettingTCConfig *self)
+{
+    self->qdiscs   = g_ptr_array_new_with_free_func((GDestroyNotify) nm_tc_qdisc_unref);
+    self->tfilters = g_ptr_array_new_with_free_func((GDestroyNotify) nm_tc_tfilter_unref);
+}
+
+/**
+ * nm_setting_tc_config_new:
+ *
+ * Creates a new #NMSettingTCConfig object with default values.
+ *
+ * Returns: (transfer full): the new empty #NMSettingTCConfig object
+ *
+ * Since: 1.12
+ **/
+NMSetting *
+nm_setting_tc_config_new(void)
+{
+    return g_object_new(NM_TYPE_SETTING_TC_CONFIG, NULL);
+}
+
+static void
+finalize(GObject *object)
+{
+    NMSettingTCConfig *self = NM_SETTING_TC_CONFIG(object);
+
+    g_ptr_array_unref(self->qdiscs);
+    g_ptr_array_unref(self->tfilters);
+
+    G_OBJECT_CLASS(nm_setting_tc_config_parent_class)->finalize(object);
+}
+
+static void
+nm_setting_tc_config_class_init(NMSettingTCConfigClass *klass)
+{
+    GObjectClass *  object_class        = G_OBJECT_CLASS(klass);
+    NMSettingClass *setting_class       = NM_SETTING_CLASS(klass);
+    GArray *        properties_override = _nm_sett_info_property_override_create_array();
+
+    object_class->get_property = get_property;
+    object_class->set_property = set_property;
+    object_class->finalize     = finalize;
+
+    setting_class->compare_property = compare_property;
+    setting_class->verify           = verify;
+
+    /**
+     * NMSettingTCConfig:qdiscs: (type GPtrArray(NMTCQdisc))
+     *
+     * Array of TC queueing disciplines.
+     *
+     * When the #NMSettingTCConfig setting is present, qdiscs from this
+     * property are applied upon activation. If the property is empty,
+     * all qdiscs are removed and the device will only
+     * have the default qdisc assigned by kernel according to the
+     * "net.core.default_qdisc" sysctl.
+     *
+     * If the #NMSettingTCConfig setting is not present, NetworkManager
+     * doesn't touch the qdiscs present on the interface.
+     **/
+    /* ---ifcfg-rh---
+     * property: qdiscs
+     * variable: QDISC1(+), QDISC2(+), ..., TC_COMMIT(+)
+     * description: Queueing disciplines to set on the interface. When no
+     *  QDISC1, QDISC2, ..., FILTER1, FILTER2, ... keys are present,
+     *  NetworkManager doesn't touch qdiscs and filters present on the
+     *  interface, unless TC_COMMIT is set to 'yes'.
+     * example: QDISC1=ingress, QDISC2="root handle 1234: fq_codel"
+     * ---end---
+     */
+    obj_properties[PROP_QDISCS] = g_param_spec_boxed(NM_SETTING_TC_CONFIG_QDISCS,
+                                                     "",
+                                                     "",
+                                                     G_TYPE_PTR_ARRAY,
+                                                     G_PARAM_READWRITE | NM_SETTING_PARAM_INFERRABLE
+                                                         | G_PARAM_STATIC_STRINGS);
+    _nm_properties_override_gobj(properties_override,
+                                 obj_properties[PROP_QDISCS],
+                                 NM_SETT_INFO_PROPERT_TYPE_DBUS(NM_G_VARIANT_TYPE("aa{sv}"),
+                                                                .to_dbus_fcn   = tc_qdiscs_get,
+                                                                .from_dbus_fcn = tc_qdiscs_set, ));
+
+    /**
+     * NMSettingTCConfig:tfilters: (type GPtrArray(NMTCTfilter))
+     *
+     * Array of TC traffic filters.
+     *
+     * When the #NMSettingTCConfig setting is present, filters from this
+     * property are applied upon activation. If the property is empty,
+     * NetworkManager removes all the filters.
+     *
+     * If the #NMSettingTCConfig setting is not present, NetworkManager
+     * doesn't touch the filters present on the interface.
+     **/
+    /* ---ifcfg-rh---
+     * property: qdiscs
+     * variable: FILTER1(+), FILTER2(+), ..., TC_COMMIT(+)
+     * description: Traffic filters to set on the interface. When no
+     *  QDISC1, QDISC2, ..., FILTER1, FILTER2, ... keys are present,
+     *  NetworkManager doesn't touch qdiscs and filters present on the
+     *  interface, unless TC_COMMIT is set to 'yes'.
+     * example: FILTER1="parent ffff: matchall action simple sdata Input", ...
+     * ---end---
+     */
+    obj_properties[PROP_TFILTERS] = g_param_spec_boxed(
+        NM_SETTING_TC_CONFIG_TFILTERS,
+        "",
+        "",
+        G_TYPE_PTR_ARRAY,
+        G_PARAM_READWRITE | NM_SETTING_PARAM_INFERRABLE | G_PARAM_STATIC_STRINGS);
+    _nm_properties_override_gobj(
+        properties_override,
+        obj_properties[PROP_TFILTERS],
+        NM_SETT_INFO_PROPERT_TYPE_DBUS(NM_G_VARIANT_TYPE("aa{sv}"),
+                                       .to_dbus_fcn   = tc_tfilters_get,
+                                       .from_dbus_fcn = tc_tfilters_set, ));
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+
+    _nm_setting_class_commit_full(setting_class,
+                                  NM_META_SETTING_TYPE_TC_CONFIG,
+                                  NULL,
+                                  properties_override);
+}