about summary refs log tree commit diff
path: root/src/platform
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2020-06-28 18:58:14 +0200
committerMichael Biebl <biebl@debian.org>2020-06-28 18:58:14 +0200
commita54ac63bbf9b2c71026ac9028a8ffaf186cf3c82 (patch)
tree6a32883bd916c4096357b35298beff6db8bcd0b5 /src/platform
parent45e8e1149027529194982212c804c0468aa01d98 (diff)
New upstream version 1.25.90 upstream/1.25.90
Diffstat (limited to 'src/platform')
-rw-r--r--src/platform/nm-fake-platform.c15
-rw-r--r--src/platform/nm-linux-platform.c235
-rw-r--r--src/platform/nm-netlink.h14
-rw-r--r--src/platform/nm-platform-utils.c232
-rw-r--r--src/platform/nm-platform-utils.h23
-rw-r--r--src/platform/nm-platform.c223
-rw-r--r--src/platform/nm-platform.h47
-rw-r--r--src/platform/tests/meson.build2
-rw-r--r--src/platform/tests/test-common.c5
-rw-r--r--src/platform/tests/test-link.c10
-rw-r--r--src/platform/tests/test-tc.c223
11 files changed, 887 insertions, 142 deletions
diff --git a/src/platform/nm-fake-platform.c b/src/platform/nm-fake-platform.c
index 4a81dc49..d9cf2de5 100644
--- a/src/platform/nm-fake-platform.c
+++ b/src/platform/nm-fake-platform.c
@@ -268,7 +268,6 @@ link_add_pre (NMPlatform *platform,
 		g_assert (address_len == 0);
 
 	device->obj = o;
-	device->udi = g_strdup_printf ("fake:%d", ifindex);
 	device->ip6_lladdr = *nmtst_inet6_from_string (ip6_lladdr);
 
 	return device;
@@ -420,7 +419,6 @@ link_delete (NMPlatform *platform, int ifindex)
 		return FALSE;
 
 	obj_old = g_steal_pointer (&device->obj);
-	nm_clear_g_free (&device->udi);
 
 	cache_op = nmp_cache_remove (nm_platform_get_cache (platform),
 	                             obj_old,
@@ -633,16 +631,6 @@ link_set_mtu (NMPlatform *platform, int ifindex, guint32 mtu)
 	return 0;
 }
 
-static const char *
-link_get_udi (NMPlatform *platform, int ifindex)
-{
-	NMFakePlatformLink *device = link_get (platform, ifindex);
-
-	if (!device)
-		return NULL;
-	return device->udi;
-}
-
 static gboolean
 link_get_driver_info (NMPlatform *platform,
                       int ifindex,
@@ -1345,7 +1333,6 @@ finalize (GObject *object)
 	for (i = 0; i < priv->links->len; i++) {
 		NMFakePlatformLink *device = &g_array_index (priv->links, NMFakePlatformLink, i);
 
-		g_free (device->udi);
 		nm_clear_pointer (&device->obj, nmp_object_unref);
 	}
 	g_array_unref (priv->links);
@@ -1371,8 +1358,6 @@ nm_fake_platform_class_init (NMFakePlatformClass *klass)
 	platform_class->link_add = link_add;
 	platform_class->link_delete = link_delete;
 
-	platform_class->link_get_udi = link_get_udi;
-
 	platform_class->link_set_up = link_set_up;
 	platform_class->link_set_down = link_set_down;
 	platform_class->link_set_arp = link_set_arp;
diff --git a/src/platform/nm-linux-platform.c b/src/platform/nm-linux-platform.c
index a721fc7c..e2c45c88 100644
--- a/src/platform/nm-linux-platform.c
+++ b/src/platform/nm-linux-platform.c
@@ -279,6 +279,10 @@ struct _ifla_vf_vlan_info {
 
 /*****************************************************************************/
 
+#define PSCHED_TIME_UNITS_PER_SEC 1000000
+
+/*****************************************************************************/
+
 typedef enum {
 	INFINIBAND_ACTION_CREATE_CHILD,
 	INFINIBAND_ACTION_DELETE_CHILD,
@@ -3660,8 +3664,39 @@ _new_from_nl_routing_rule (struct nlmsghdr *nlh, gboolean id_only)
 	return g_steal_pointer (&obj);
 }
 
+static guint32
+psched_tick_to_time (NMPlatform *platform, guint32 tick)
+{
+	static gboolean initialized;
+	static double tick_in_usec = 1;
+
+	if (!initialized) {
+		gs_free char *params = NULL;
+		double clock_factor = 1;
+		guint32 clock_res;
+		guint32 t2us;
+		guint32 us2t;
+
+		initialized = TRUE;
+		params = nm_platform_sysctl_get (platform, NMP_SYSCTL_PATHID_ABSOLUTE ("/proc/net/psched"));
+		if (   !params
+		    || sscanf (params, "%08x%08x%08x", &t2us, &us2t, &clock_res) != 3) {
+			_LOGW ("packet scheduler parameters not available");
+		} else {
+			/* See tc_core_init() in iproute2 */
+			if (clock_res == 1000000000)
+				t2us = us2t;
+
+			clock_factor  = (double) clock_res / PSCHED_TIME_UNITS_PER_SEC;
+			tick_in_usec = (double) t2us / us2t * clock_factor;
+		}
+	}
+
+	return tick / tick_in_usec;
+}
+
 static NMPObject *
-_new_from_nl_qdisc (struct nlmsghdr *nlh, gboolean id_only)
+_new_from_nl_qdisc (NMPlatform *platform, struct nlmsghdr *nlh, gboolean id_only)
 {
 	static const struct nla_policy policy[] = {
 		[TCA_KIND] = { .type = NLA_STRING },
@@ -3669,7 +3704,7 @@ _new_from_nl_qdisc (struct nlmsghdr *nlh, gboolean id_only)
 	};
 	struct nlattr *tb[G_N_ELEMENTS (policy)];
 	const struct tcmsg *tcm;
-	NMPObject *obj;
+	nm_auto_nmpobj NMPObject *obj = NULL;
 
 	if (nlmsg_parse_arr (nlh,
 	                     sizeof (*tcm),
@@ -3700,42 +3735,77 @@ _new_from_nl_qdisc (struct nlmsghdr *nlh, gboolean id_only)
 		struct nlattr *options_attr;
 		int remaining;
 
-		nla_for_each_nested (options_attr, tb[TCA_OPTIONS], remaining) {
-			if (nla_len (options_attr) < sizeof (uint32_t))
-				continue;
+		if (nm_streq0 (obj->qdisc.kind, "sfq")) {
+			struct tc_sfq_qopt_v1 opt;
+
+			if (tb[TCA_OPTIONS]->nla_len >= nla_attr_size (sizeof (opt))) {
+				memcpy (&opt, nla_data (tb[TCA_OPTIONS]), sizeof (opt));
+				obj->qdisc.sfq.quantum = opt.v0.quantum;
+				obj->qdisc.sfq.perturb_period = opt.v0.perturb_period;
+				obj->qdisc.sfq.limit = opt.v0.limit;
+				obj->qdisc.sfq.divisor = opt.v0.divisor;
+				obj->qdisc.sfq.flows = opt.v0.flows;
+				obj->qdisc.sfq.depth = opt.depth;
+			}
+		} else if (nm_streq0 (obj->qdisc.kind, "tbf")) {
+			static const struct nla_policy tbf_policy[] = {
+				[TCA_TBF_PARMS]     = { .minlen = sizeof (struct tc_tbf_qopt) },
+				[TCA_TBF_RATE64]    = { .type = NLA_U64 },
+			};
+			struct nlattr *tbf_tb[G_N_ELEMENTS (tbf_policy)];
+			struct tc_tbf_qopt opt;
 
-			if (nm_streq0 (obj->qdisc.kind, "fq_codel")) {
-				switch (nla_type (options_attr)) {
-				case TCA_FQ_CODEL_LIMIT:
-					obj->qdisc.fq_codel.limit = nla_get_u32 (options_attr);
-					break;
-				case TCA_FQ_CODEL_FLOWS:
-					obj->qdisc.fq_codel.flows = nla_get_u32 (options_attr);
-					break;
-				case TCA_FQ_CODEL_TARGET:
-					obj->qdisc.fq_codel.target = nla_get_u32 (options_attr);
-					break;
-				case TCA_FQ_CODEL_INTERVAL:
-					obj->qdisc.fq_codel.interval = nla_get_u32 (options_attr);
-					break;
-				case TCA_FQ_CODEL_QUANTUM:
-					obj->qdisc.fq_codel.quantum = nla_get_u32 (options_attr);
-					break;
-				case TCA_FQ_CODEL_CE_THRESHOLD:
-					obj->qdisc.fq_codel.ce_threshold = nla_get_u32 (options_attr);
-					break;
-				case TCA_FQ_CODEL_MEMORY_LIMIT:
-					obj->qdisc.fq_codel.memory_limit = nla_get_u32 (options_attr);
-					break;
-				case TCA_FQ_CODEL_ECN:
-					obj->qdisc.fq_codel.ecn = !!nla_get_u32 (options_attr);
-					break;
+			if (nla_parse_nested_arr (tbf_tb, tb[TCA_OPTIONS], tbf_policy) < 0)
+				return NULL;
+			if (!tbf_tb[TCA_TBF_PARMS])
+				return NULL;
+
+			nla_memcpy_checked_size (&opt, tbf_tb[TCA_TBF_PARMS], sizeof (opt));
+			obj->qdisc.tbf.rate = opt.rate.rate;
+			if (tbf_tb[TCA_TBF_RATE64])
+				obj->qdisc.tbf.rate = nla_get_u64 (tbf_tb[TCA_TBF_RATE64]);
+			obj->qdisc.tbf.burst = ((double) obj->qdisc.tbf.rate *
+			                       psched_tick_to_time (platform, opt.buffer)) /
+			                       PSCHED_TIME_UNITS_PER_SEC;
+			obj->qdisc.tbf.limit = opt.limit;
+		} else {
+			nla_for_each_nested (options_attr, tb[TCA_OPTIONS], remaining) {
+				if (nla_len (options_attr) < sizeof (uint32_t))
+					continue;
+
+				if (nm_streq0 (obj->qdisc.kind, "fq_codel")) {
+					switch (nla_type (options_attr)) {
+					case TCA_FQ_CODEL_LIMIT:
+						obj->qdisc.fq_codel.limit = nla_get_u32 (options_attr);
+						break;
+					case TCA_FQ_CODEL_FLOWS:
+						obj->qdisc.fq_codel.flows = nla_get_u32 (options_attr);
+						break;
+					case TCA_FQ_CODEL_TARGET:
+						obj->qdisc.fq_codel.target = nla_get_u32 (options_attr);
+						break;
+					case TCA_FQ_CODEL_INTERVAL:
+						obj->qdisc.fq_codel.interval = nla_get_u32 (options_attr);
+						break;
+					case TCA_FQ_CODEL_QUANTUM:
+						obj->qdisc.fq_codel.quantum = nla_get_u32 (options_attr);
+						break;
+					case TCA_FQ_CODEL_CE_THRESHOLD:
+						obj->qdisc.fq_codel.ce_threshold = nla_get_u32 (options_attr);
+						break;
+					case TCA_FQ_CODEL_MEMORY_LIMIT:
+						obj->qdisc.fq_codel.memory_limit = nla_get_u32 (options_attr);
+						break;
+					case TCA_FQ_CODEL_ECN:
+						obj->qdisc.fq_codel.ecn = !!nla_get_u32 (options_attr);
+						break;
+					}
 				}
 			}
 		}
 	}
 
-	return obj;
+	return g_steal_pointer (&obj);
 }
 
 static NMPObject *
@@ -3811,7 +3881,7 @@ nmp_object_new_from_nl (NMPlatform *platform, const NMPCache *cache, struct nl_m
 	case RTM_NEWQDISC:
 	case RTM_DELQDISC:
 	case RTM_GETQDISC:
-		return _new_from_nl_qdisc (msghdr, id_only);
+		return _new_from_nl_qdisc (platform, msghdr, id_only);
 	case RTM_NEWTFILTER:
 	case RTM_DELTFILTER:
 	case RTM_GETTFILTER:
@@ -4644,36 +4714,72 @@ _nl_msg_new_qdisc (int nlmsg_type,
 		.tcm_info = qdisc->info,
 	};
 
-	msg = nlmsg_alloc_simple (nlmsg_type, nlmsg_flags);
+	msg = nlmsg_alloc_simple (nlmsg_type, nlmsg_flags | NMP_NLM_FLAG_F_ECHO);
 
 	if (nlmsg_append_struct (msg, &tcm) < 0)
 		goto nla_put_failure;
 
 	NLA_PUT_STRING (msg, TCA_KIND, qdisc->kind);
 
-	if (!(tc_options = nla_nest_start (msg, TCA_OPTIONS)))
-		goto nla_put_failure;
+	if (nm_streq (qdisc->kind, "sfq")) {
+		struct tc_sfq_qopt_v1 opt = { };
 
-	if (nm_streq (qdisc->kind, "fq_codel")) {
-		if (qdisc->fq_codel.limit)
-			NLA_PUT_U32 (msg, TCA_FQ_CODEL_LIMIT, qdisc->fq_codel.limit);
-		if (qdisc->fq_codel.flows)
-			NLA_PUT_U32 (msg, TCA_FQ_CODEL_FLOWS, qdisc->fq_codel.flows);
-		if (qdisc->fq_codel.target)
-			NLA_PUT_U32 (msg, TCA_FQ_CODEL_TARGET, qdisc->fq_codel.target);
-		if (qdisc->fq_codel.interval)
-			NLA_PUT_U32 (msg, TCA_FQ_CODEL_INTERVAL, qdisc->fq_codel.interval);
-		if (qdisc->fq_codel.quantum)
-			NLA_PUT_U32 (msg, TCA_FQ_CODEL_QUANTUM, qdisc->fq_codel.quantum);
-		if (qdisc->fq_codel.ce_threshold != NM_PLATFORM_FQ_CODEL_CE_THRESHOLD_DISABLED)
-			NLA_PUT_U32 (msg, TCA_FQ_CODEL_CE_THRESHOLD, qdisc->fq_codel.ce_threshold);
-		if (qdisc->fq_codel.memory_limit != NM_PLATFORM_FQ_CODEL_MEMORY_LIMIT_UNSET)
-			NLA_PUT_U32 (msg, TCA_FQ_CODEL_MEMORY_LIMIT, qdisc->fq_codel.memory_limit);
-		if (qdisc->fq_codel.ecn)
-			NLA_PUT_U32 (msg, TCA_FQ_CODEL_ECN, qdisc->fq_codel.ecn);
-	}
+		opt.v0.quantum = qdisc->sfq.quantum;
+		opt.v0.limit = qdisc->sfq.limit;
+		opt.v0.perturb_period = qdisc->sfq.perturb_period;
+		opt.v0.flows = qdisc->sfq.flows;
+		opt.v0.divisor = qdisc->sfq.divisor;
+		opt.depth = qdisc->sfq.depth;
 
-	nla_nest_end (msg, tc_options);
+		NLA_PUT (msg, TCA_OPTIONS, sizeof (opt), &opt);
+	} else if (nm_streq (qdisc->kind, "tbf")) {
+		struct tc_tbf_qopt opt = { };
+
+		if (!(tc_options = nla_nest_start (msg, TCA_OPTIONS)))
+			goto nla_put_failure;
+
+		opt.rate.rate =   (qdisc->tbf.rate >= (1ULL << 32))
+		                ? ~0U
+		                : (guint32) qdisc->tbf.rate;
+		if (qdisc->tbf.limit)
+			opt.limit = qdisc->tbf.limit;
+		else if (qdisc->tbf.latency) {
+			opt.limit = qdisc->tbf.rate * (double) qdisc->tbf.latency
+			            / PSCHED_TIME_UNITS_PER_SEC
+			            + qdisc->tbf.burst;
+		}
+
+		NLA_PUT (msg, TCA_TBF_PARMS, sizeof (opt), &opt);
+		if (qdisc->tbf.rate >= (1ULL << 32))
+			NLA_PUT_U64 (msg, TCA_TBF_RATE64, qdisc->tbf.rate);
+		NLA_PUT_U32 (msg, TCA_TBF_BURST, qdisc->tbf.burst);
+
+		nla_nest_end (msg, tc_options);
+	} else {
+		if (!(tc_options = nla_nest_start (msg, TCA_OPTIONS)))
+			goto nla_put_failure;
+
+		if (nm_streq (qdisc->kind, "fq_codel")) {
+			if (qdisc->fq_codel.limit)
+				NLA_PUT_U32 (msg, TCA_FQ_CODEL_LIMIT, qdisc->fq_codel.limit);
+			if (qdisc->fq_codel.flows)
+				NLA_PUT_U32 (msg, TCA_FQ_CODEL_FLOWS, qdisc->fq_codel.flows);
+			if (qdisc->fq_codel.target)
+				NLA_PUT_U32 (msg, TCA_FQ_CODEL_TARGET, qdisc->fq_codel.target);
+			if (qdisc->fq_codel.interval)
+				NLA_PUT_U32 (msg, TCA_FQ_CODEL_INTERVAL, qdisc->fq_codel.interval);
+			if (qdisc->fq_codel.quantum)
+				NLA_PUT_U32 (msg, TCA_FQ_CODEL_QUANTUM, qdisc->fq_codel.quantum);
+			if (qdisc->fq_codel.ce_threshold != NM_PLATFORM_FQ_CODEL_CE_THRESHOLD_DISABLED)
+				NLA_PUT_U32 (msg, TCA_FQ_CODEL_CE_THRESHOLD, qdisc->fq_codel.ce_threshold);
+			if (qdisc->fq_codel.memory_limit != NM_PLATFORM_FQ_CODEL_MEMORY_LIMIT_UNSET)
+				NLA_PUT_U32 (msg, TCA_FQ_CODEL_MEMORY_LIMIT, qdisc->fq_codel.memory_limit);
+			if (qdisc->fq_codel.ecn)
+				NLA_PUT_U32 (msg, TCA_FQ_CODEL_ECN, qdisc->fq_codel.ecn);
+		}
+
+		nla_nest_end (msg, tc_options);
+	}
 
 	return g_steal_pointer (&msg);
 
@@ -4697,7 +4803,7 @@ _nl_msg_new_tfilter (int nlmsg_type,
 		.tcm_info = tfilter->info,
 	};
 
-	msg = nlmsg_alloc_simple (nlmsg_type, nlmsg_flags);
+	msg = nlmsg_alloc_simple (nlmsg_type, nlmsg_flags | NMP_NLM_FLAG_F_ECHO);
 
 	if (nlmsg_append_struct (msg, &tcm) < 0)
 		goto nla_put_failure;
@@ -4789,7 +4895,8 @@ _genl_sock (NMLinuxPlatform *platform)
 			nm_assert (!_pathid); \
 			nm_assert (_path[0] == '/'); \
 			nm_assert (   g_str_has_prefix (_path, "/proc/sys/") \
-			           || g_str_has_prefix (_path, "/sys/")); \
+			           || g_str_has_prefix (_path, "/sys/") \
+			           || g_str_has_prefix (_path, "/proc/net")); \
 		} else { \
 			nm_assert (_pathid && _pathid[0] && _pathid[0] != '/'); \
 			nm_assert (_path[0] != '/'); \
@@ -7058,18 +7165,6 @@ link_set_noarp (NMPlatform *platform, int ifindex)
 	return (link_change_flags (platform, ifindex, IFF_NOARP, IFF_NOARP) >= 0);
 }
 
-static const char *
-link_get_udi (NMPlatform *platform, int ifindex)
-{
-	const NMPObject *obj = nm_platform_link_get_obj (platform, ifindex, TRUE);
-
-	if (   !obj
-	    || !obj->_link.netlink.is_in_netlink
-	    || !obj->_link.udev.device)
-		return NULL;
-	return udev_device_get_syspath (obj->_link.udev.device);
-}
-
 static int
 link_set_user_ipv6ll_enabled (NMPlatform *platform, int ifindex, gboolean enabled)
 {
@@ -9211,8 +9306,6 @@ nm_linux_platform_class_init (NMLinuxPlatformClass *klass)
 	platform_class->link_set_arp = link_set_arp;
 	platform_class->link_set_noarp = link_set_noarp;
 
-	platform_class->link_get_udi = link_get_udi;
-
 	platform_class->link_set_user_ipv6ll_enabled = link_set_user_ipv6ll_enabled;
 	platform_class->link_set_token = link_set_token;
 
diff --git a/src/platform/nm-netlink.h b/src/platform/nm-netlink.h
index 0ac5b3b7..a5124f52 100644
--- a/src/platform/nm-netlink.h
+++ b/src/platform/nm-netlink.h
@@ -77,17 +77,13 @@ struct nla_policy {
 /* static asserts that @tb and @policy are suitable arguments to nla_parse(). */
 #define _nl_static_assert_tb(tb, policy) \
 	G_STMT_START { \
-		\
 		G_STATIC_ASSERT_EXPR (G_N_ELEMENTS (tb) > 0); \
 		\
-		/* we allow @policy to be either NULL or a C array. */ \
-		G_STATIC_ASSERT_EXPR (   sizeof (policy) == sizeof (NULL) \
-		                      || G_N_ELEMENTS (tb) == (sizeof (policy) / sizeof (struct nla_policy))); \
-		\
-		/* For above check to work, we don't support policy being an array with same size as
-		 * sizeof(NULL), otherwise, the compile time check breaks down. */ \
-		G_STATIC_ASSERT_EXPR (sizeof (NULL) != G_N_ELEMENTS (tb) * sizeof (struct nla_policy)); \
-		\
+		/* We allow @policy to be either a C array or NULL. The sizeof()
+		 * must either match the expected array size or the sizeof(NULL),
+		 * but not both. */ \
+		G_STATIC_ASSERT_EXPR (  (sizeof (policy) == G_N_ELEMENTS (tb) * sizeof (struct nla_policy)) \
+		                      ^ (sizeof (policy) == sizeof (NULL))); \
 	} G_STMT_END
 
 /*****************************************************************************/
diff --git a/src/platform/nm-platform-utils.c b/src/platform/nm-platform-utils.c
index 98884cd7..6c14bc5c 100644
--- a/src/platform/nm-platform-utils.c
+++ b/src/platform/nm-platform-utils.c
@@ -264,16 +264,20 @@ out:
 
 static
 NM_UTILS_ENUM2STR_DEFINE (_ethtool_cmd_to_string, guint32,
+	NM_UTILS_ENUM2STR (ETHTOOL_GCOALESCE,  "ETHTOOL_GCOALESCE"),
 	NM_UTILS_ENUM2STR (ETHTOOL_GDRVINFO,   "ETHTOOL_GDRVINFO"),
 	NM_UTILS_ENUM2STR (ETHTOOL_GFEATURES,  "ETHTOOL_GFEATURES"),
 	NM_UTILS_ENUM2STR (ETHTOOL_GLINK,      "ETHTOOL_GLINK"),
 	NM_UTILS_ENUM2STR (ETHTOOL_GPERMADDR,  "ETHTOOL_GPERMADDR"),
+	NM_UTILS_ENUM2STR (ETHTOOL_GRINGPARAM, "ETHTOOL_GRINGPARAM"),
 	NM_UTILS_ENUM2STR (ETHTOOL_GSET,       "ETHTOOL_GSET"),
 	NM_UTILS_ENUM2STR (ETHTOOL_GSSET_INFO, "ETHTOOL_GSSET_INFO"),
 	NM_UTILS_ENUM2STR (ETHTOOL_GSTATS,     "ETHTOOL_GSTATS"),
 	NM_UTILS_ENUM2STR (ETHTOOL_GSTRINGS,   "ETHTOOL_GSTRINGS"),
 	NM_UTILS_ENUM2STR (ETHTOOL_GWOL,       "ETHTOOL_GWOL"),
+	NM_UTILS_ENUM2STR (ETHTOOL_SCOALESCE,  "ETHTOOL_SCOALESCE"),
 	NM_UTILS_ENUM2STR (ETHTOOL_SFEATURES,  "ETHTOOL_SFEATURES"),
+	NM_UTILS_ENUM2STR (ETHTOOL_SRINGPARAM, "ETHTOOL_SRINGPARAM"),
 	NM_UTILS_ENUM2STR (ETHTOOL_SSET,       "ETHTOOL_SSET"),
 	NM_UTILS_ENUM2STR (ETHTOOL_SWOL,       "ETHTOOL_SWOL"),
 );
@@ -516,8 +520,8 @@ _ASSERT_ethtool_feature_infos (void)
 		for (k = 0; k < i; k++)
 			g_assert (inf->ethtool_id != _ethtool_feature_infos[k].ethtool_id);
 
-		g_assert (!found[inf->ethtool_id - _NM_ETHTOOL_ID_FEATURE_FIRST]);
-		found[inf->ethtool_id - _NM_ETHTOOL_ID_FEATURE_FIRST] = TRUE;
+		g_assert (!found[_NM_ETHTOOL_ID_FEATURE_AS_IDX (inf->ethtool_id)]);
+		found[_NM_ETHTOOL_ID_FEATURE_AS_IDX (inf->ethtool_id)] = TRUE;
 
 		kstate.idx_kernel_name = inf->n_kernel_names - 1;
 		g_assert ((guint) kstate.idx_kernel_name == (guint) (inf->n_kernel_names - 1));
@@ -607,13 +611,13 @@ ethtool_get_features (SocketHandle *shandle)
 
 				nm_assert (states_plist_n < N_ETHTOOL_KERNEL_FEATURES + G_N_ELEMENTS (_ethtool_feature_infos));
 
-				if (!states->states_indexed[info->ethtool_id - _NM_ETHTOOL_ID_FEATURE_FIRST])
-					states->states_indexed[info->ethtool_id - _NM_ETHTOOL_ID_FEATURE_FIRST] = &states_plist0[states_plist_n];
+				if (!states->states_indexed[_NM_ETHTOOL_ID_FEATURE_AS_IDX (info->ethtool_id)])
+					states->states_indexed[_NM_ETHTOOL_ID_FEATURE_AS_IDX (info->ethtool_id)] = &states_plist0[states_plist_n];
 				((const NMEthtoolFeatureState **) states_plist0)[states_plist_n] = kstate;
 				states_plist_n++;
 			}
 
-			if (states && states->states_indexed[info->ethtool_id - _NM_ETHTOOL_ID_FEATURE_FIRST]) {
+			if (states && states->states_indexed[_NM_ETHTOOL_ID_FEATURE_AS_IDX (info->ethtool_id)]) {
 				nm_assert (states_plist_n < N_ETHTOOL_KERNEL_FEATURES + G_N_ELEMENTS (_ethtool_feature_infos));
 				nm_assert (!states_plist0[states_plist_n]);
 				states_plist_n++;
@@ -804,6 +808,224 @@ nmp_utils_ethtool_set_features (int ifindex,
 	return success;
 }
 
+static gboolean
+ethtool_get_coalesce (SocketHandle *shandle,
+                      NMEthtoolCoalesceState *coalesce)
+{
+	struct ethtool_coalesce eth_data;
+
+	eth_data.cmd = ETHTOOL_GCOALESCE;
+
+	if (_ethtool_call_handle (shandle,
+	                          &eth_data,
+	                          sizeof (struct ethtool_coalesce)) != 0)
+		return FALSE;
+
+	*coalesce = (NMEthtoolCoalesceState) {
+	    .s = {
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_USECS)]          = eth_data.rx_coalesce_usecs,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_FRAMES)]         = eth_data.rx_max_coalesced_frames,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_USECS_IRQ)]      = eth_data.rx_coalesce_usecs_irq,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_FRAMES_IRQ)]     = eth_data.rx_max_coalesced_frames_irq,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_USECS)]          = eth_data.tx_coalesce_usecs,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_FRAMES)]         = eth_data.tx_max_coalesced_frames,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_USECS_IRQ)]      = eth_data.tx_coalesce_usecs_irq,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_FRAMES_IRQ)]     = eth_data.tx_max_coalesced_frames_irq,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_STATS_BLOCK_USECS)] = eth_data.stats_block_coalesce_usecs,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_ADAPTIVE_RX)]       = eth_data.use_adaptive_rx_coalesce,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_ADAPTIVE_TX)]       = eth_data.use_adaptive_tx_coalesce,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_PKT_RATE_LOW)]      = eth_data.pkt_rate_low,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_USECS_LOW)]      = eth_data.rx_coalesce_usecs_low,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_FRAMES_LOW)]     = eth_data.rx_max_coalesced_frames_low,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_USECS_LOW)]      = eth_data.tx_coalesce_usecs_low,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_FRAMES_LOW)]     = eth_data.tx_max_coalesced_frames_low,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_PKT_RATE_HIGH)]     = eth_data.pkt_rate_high,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_USECS_HIGH)]     = eth_data.rx_coalesce_usecs_high,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_FRAMES_HIGH)]    = eth_data.rx_max_coalesced_frames_high,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_USECS_HIGH)]     = eth_data.tx_coalesce_usecs_high,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_FRAMES_HIGH)]    = eth_data.tx_max_coalesced_frames_high,
+	        [_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_SAMPLE_INTERVAL)]   = eth_data.rate_sample_interval,
+	    }
+	};
+	return TRUE;
+}
+
+
+gboolean
+nmp_utils_ethtool_get_coalesce (int ifindex,
+                                NMEthtoolCoalesceState *coalesce)
+{
+	nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT (ifindex);
+
+	g_return_val_if_fail (ifindex > 0, FALSE);
+	g_return_val_if_fail (coalesce, FALSE);
+
+	if (!ethtool_get_coalesce (&shandle, coalesce)) {
+		nm_log_trace (LOGD_PLATFORM, "ethtool[%d]: %s: failure getting coalesce settings",
+		              ifindex,
+		              "get-coalesce");
+		return FALSE;
+	}
+
+	nm_log_trace (LOGD_PLATFORM, "ethtool[%d]: %s: retrieved kernel coalesce settings",
+	              ifindex,
+	              "get-coalesce");
+	return TRUE;
+}
+
+static gboolean
+ethtool_set_coalesce (SocketHandle *shandle,
+                      const NMEthtoolCoalesceState *coalesce)
+{
+	struct ethtool_coalesce eth_data;
+	gboolean success;
+
+	nm_assert (shandle);
+	nm_assert (coalesce);
+
+	eth_data = (struct ethtool_coalesce) {
+		.cmd = ETHTOOL_SCOALESCE,
+		.rx_coalesce_usecs            = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_USECS)],
+		.rx_max_coalesced_frames      = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_FRAMES)],
+		.rx_coalesce_usecs_irq        = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_USECS_IRQ)],
+		.rx_max_coalesced_frames_irq  = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_FRAMES_IRQ)],
+		.tx_coalesce_usecs            = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_USECS)],
+		.tx_max_coalesced_frames      = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_FRAMES)],
+		.tx_coalesce_usecs_irq        = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_USECS_IRQ)],
+		.tx_max_coalesced_frames_irq  = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_FRAMES_IRQ)],
+		.stats_block_coalesce_usecs   = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_STATS_BLOCK_USECS)],
+		.use_adaptive_rx_coalesce     = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_ADAPTIVE_RX)],
+		.use_adaptive_tx_coalesce     = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_ADAPTIVE_TX)],
+		.pkt_rate_low                 = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_PKT_RATE_LOW)],
+		.rx_coalesce_usecs_low        = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_USECS_LOW)],
+		.rx_max_coalesced_frames_low  = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_FRAMES_LOW)],
+		.tx_coalesce_usecs_low        = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_USECS_LOW)],
+		.tx_max_coalesced_frames_low  = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_FRAMES_LOW)],
+		.pkt_rate_high                = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_PKT_RATE_HIGH)],
+		.rx_coalesce_usecs_high       = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_USECS_HIGH)],
+		.rx_max_coalesced_frames_high = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_RX_FRAMES_HIGH)],
+		.tx_coalesce_usecs_high       = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_USECS_HIGH)],
+		.tx_max_coalesced_frames_high = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_TX_FRAMES_HIGH)],
+		.rate_sample_interval         = coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX (NM_ETHTOOL_ID_COALESCE_SAMPLE_INTERVAL)],
+	};
+
+	success = (_ethtool_call_handle (shandle,
+	                                 &eth_data,
+	                                 sizeof (struct ethtool_coalesce)) == 0);
+	return success;
+}
+
+gboolean
+nmp_utils_ethtool_set_coalesce (int ifindex,
+                                const NMEthtoolCoalesceState *coalesce)
+{
+	nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT (ifindex);
+
+	g_return_val_if_fail (ifindex > 0, FALSE);
+	g_return_val_if_fail (coalesce, FALSE);
+
+	if (!ethtool_set_coalesce (&shandle, coalesce)) {
+		nm_log_trace (LOGD_PLATFORM, "ethtool[%d]: %s: failure setting coalesce settings",
+		              ifindex,
+		              "set-coalesce");
+		return FALSE;
+	}
+
+	nm_log_trace (LOGD_PLATFORM, "ethtool[%d]: %s: set kernel coalesce settings",
+	              ifindex,
+	              "set-coalesce");
+	return TRUE;
+}
+
+static gboolean
+ethtool_get_ring (SocketHandle *shandle,
+                  NMEthtoolRingState *ring)
+{
+	struct ethtool_ringparam eth_data;
+
+	eth_data.cmd = ETHTOOL_GRINGPARAM;
+
+	if (_ethtool_call_handle (shandle,
+	                          &eth_data,
+	                          sizeof (struct ethtool_ringparam)) != 0)
+		return FALSE;
+
+	ring->rx_pending       = eth_data.rx_pending;
+	ring->rx_jumbo_pending = eth_data.rx_jumbo_pending;
+	ring->rx_mini_pending  = eth_data.rx_mini_pending;
+	ring->tx_pending       = eth_data.tx_pending;
+
+	return TRUE;
+}
+
+gboolean
+nmp_utils_ethtool_get_ring (int ifindex,
+                            NMEthtoolRingState *ring)
+{
+	nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT (ifindex);
+
+	g_return_val_if_fail (ifindex > 0, FALSE);
+	g_return_val_if_fail (ring, FALSE);
+
+	if (!ethtool_get_ring (&shandle, ring)) {
+		nm_log_trace (LOGD_PLATFORM, "ethtool[%d]: %s: failure getting ring settings",
+		              ifindex,
+		              "get-ring");
+		return FALSE;
+	}
+
+	nm_log_trace (LOGD_PLATFORM, "ethtool[%d]: %s: retrieved kernel ring settings",
+	              ifindex,
+	              "get-ring");
+	return TRUE;
+}
+
+static gboolean
+ethtool_set_ring (SocketHandle *shandle,
+                  const NMEthtoolRingState *ring)
+{
+	gboolean success;
+	struct ethtool_ringparam eth_data;
+
+	g_return_val_if_fail (shandle, FALSE);
+	g_return_val_if_fail (ring, FALSE);
+
+	eth_data = (struct ethtool_ringparam) {
+		.cmd = ETHTOOL_SRINGPARAM,
+		.rx_pending       = ring->rx_pending,
+		.rx_jumbo_pending = ring->rx_jumbo_pending,
+		.rx_mini_pending  = ring->rx_mini_pending,
+		.tx_pending       = ring->tx_pending,
+	};
+
+	success = (_ethtool_call_handle (shandle,
+	                                 &eth_data,
+	                                 sizeof (struct ethtool_ringparam)) == 0);
+	return success;
+}
+
+gboolean
+nmp_utils_ethtool_set_ring (int ifindex,
+                            const NMEthtoolRingState *ring)
+{
+	nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT (ifindex);
+
+	g_return_val_if_fail (ifindex > 0, FALSE);
+	g_return_val_if_fail (ring, FALSE);
+
+	if (!ethtool_set_ring (&shandle, ring)) {
+		nm_log_trace (LOGD_PLATFORM, "ethtool[%d]: %s: failure setting ring settings",
+		              ifindex,
+		              "set-ring");
+		return FALSE;
+	}
+
+	nm_log_trace (LOGD_PLATFORM, "ethtool[%d]: %s: set kernel ring settings",
+	              ifindex,
+	              "set-ring");
+	return TRUE;
+}
+
 /*****************************************************************************/
 
 gboolean
diff --git a/src/platform/nm-platform-utils.h b/src/platform/nm-platform-utils.h
index a62d828c..062c8597 100644
--- a/src/platform/nm-platform-utils.h
+++ b/src/platform/nm-platform-utils.h
@@ -92,6 +92,29 @@ gboolean nmp_utils_ethtool_set_features (int ifindex,
                                          const NMTernary *requested /* indexed by NMEthtoolID - _NM_ETHTOOL_ID_FEATURE_FIRST */,
                                          gboolean do_set /* or reset */);
 
+struct _NMEthtoolCoalesceState {
+	guint32 s[_NM_ETHTOOL_ID_COALESCE_NUM /* indexed by (NMEthtoolID - _NM_ETHTOOL_ID_COALESCE_FIRST) */];
+};
+
+gboolean nmp_utils_ethtool_get_coalesce (int ifindex,
+                                         NMEthtoolCoalesceState *coalesce);
+
+gboolean nmp_utils_ethtool_set_coalesce (int ifindex,
+                                         const NMEthtoolCoalesceState *coalesce);
+
+struct _NMEthtoolRingState {
+	guint32 rx_pending;
+	guint32 rx_mini_pending;
+	guint32 rx_jumbo_pending;
+	guint32 tx_pending;
+};
+
+gboolean nmp_utils_ethtool_get_ring (int ifindex,
+                                     NMEthtoolRingState *ring);
+
+gboolean nmp_utils_ethtool_set_ring (int ifindex,
+                                     const NMEthtoolRingState *ring);
+
 /*****************************************************************************/
 
 gboolean nmp_utils_mii_supports_carrier_detect (int ifindex);
diff --git a/src/platform/nm-platform.c b/src/platform/nm-platform.c
index 9bbc3745..c023f435 100644
--- a/src/platform/nm-platform.c
+++ b/src/platform/nm-platform.c
@@ -41,6 +41,14 @@ G_STATIC_ASSERT (G_STRUCT_OFFSET (NMPlatformIPAddress, address_ptr) == G_STRUCT_
 G_STATIC_ASSERT (G_STRUCT_OFFSET (NMPlatformIPRoute, network_ptr) == G_STRUCT_OFFSET (NMPlatformIP4Route, network));
 G_STATIC_ASSERT (G_STRUCT_OFFSET (NMPlatformIPRoute, network_ptr) == G_STRUCT_OFFSET (NMPlatformIP6Route, network));
 
+G_STATIC_ASSERT (_nm_alignof (NMPlatformIPRoute) == _nm_alignof (NMPlatformIP4Route));
+G_STATIC_ASSERT (_nm_alignof (NMPlatformIPRoute) == _nm_alignof (NMPlatformIP6Route));
+G_STATIC_ASSERT (_nm_alignof (NMPlatformIPRoute) == _nm_alignof (NMPlatformIPXRoute));
+
+G_STATIC_ASSERT (_nm_alignof (NMPlatformIPAddress) == _nm_alignof (NMPlatformIP4Address));
+G_STATIC_ASSERT (_nm_alignof (NMPlatformIPAddress) == _nm_alignof (NMPlatformIP6Address));
+G_STATIC_ASSERT (_nm_alignof (NMPlatformIPAddress) == _nm_alignof (NMPlatformIPXAddress));
+
 /*****************************************************************************/
 
 G_STATIC_ASSERT (sizeof ( ((NMPLinkAddress *) NULL)->data ) == NM_UTILS_HWADDR_LEN_MAX);
@@ -310,6 +318,7 @@ NM_UTILS_LOOKUP_STR_DEFINE (_nmp_nlm_flag_to_string_lookup, NMPNlmFlags,
 	NM_UTILS_LOOKUP_ITEM_IGNORE (NMP_NLM_FLAG_F_APPEND),
 	NM_UTILS_LOOKUP_ITEM_IGNORE (NMP_NLM_FLAG_FMASK),
 	NM_UTILS_LOOKUP_ITEM_IGNORE (NMP_NLM_FLAG_SUPPRESS_NETLINK_FAILURE),
+	NM_UTILS_LOOKUP_ITEM_IGNORE (NMP_NLM_FLAG_F_ECHO),
 );
 
 #define _nmp_nlm_flag_to_string(flags) \
@@ -1403,6 +1412,27 @@ nm_platform_link_get_type_name (NMPlatform *self, int ifindex)
 	return obj->link.kind ?: "unknown";
 }
 
+static gboolean
+link_get_udev_property (NMPlatform *self,
+                        int ifindex,
+                        const char *name,
+                        const char **out_value)
+{
+	struct udev_device *udevice = NULL;
+	const char *uproperty;
+
+	udevice = nm_platform_link_get_udev_device (self, ifindex);
+	if (!udevice)
+		return FALSE;
+
+	uproperty = udev_device_get_property_value (udevice, name);
+	if (!uproperty)
+		return FALSE;
+
+	NM_SET_OUT (out_value, uproperty);
+	return TRUE;
+}
+
 /**
  * nm_platform_link_get_unmanaged:
  * @self: platform instance
@@ -1415,26 +1445,14 @@ nm_platform_link_get_type_name (NMPlatform *self, int ifindex)
 gboolean
 nm_platform_link_get_unmanaged (NMPlatform *self, int ifindex, gboolean *unmanaged)
 {
-	const NMPObject *link;
-	struct udev_device *udevice = NULL;
-	const char *uproperty;
+	const char *value;
 
-	_CHECK_SELF (self, klass, FALSE);
-
-	link = nmp_cache_lookup_link (nm_platform_get_cache (self), ifindex);
-	if (!link)
-		return FALSE;
-
-	udevice = link->_link.udev.device;
-	if (!udevice)
-		return FALSE;
-
-	uproperty = udev_device_get_property_value (udevice, "NM_UNMANAGED");
-	if (!uproperty)
-		return FALSE;
+	if (link_get_udev_property (self, ifindex, "NM_UNMANAGED", &value)) {
+		NM_SET_OUT (unmanaged, nm_udev_utils_property_as_boolean (value));
+		return TRUE;
+	}
 
-	*unmanaged = nm_udev_utils_property_as_boolean (uproperty);
-	return TRUE;
+	return FALSE;
 }
 
 /**
@@ -1571,7 +1589,6 @@ nm_platform_link_set_ipv6_token (NMPlatform *self, int ifindex, NMUtilsIPv6Iface
 	_CHECK_SELF (self, klass, FALSE);
 
 	g_return_val_if_fail (ifindex >= 0, FALSE);
-	g_return_val_if_fail (iid.id, FALSE);
 
 	if (klass->link_set_token)
 		return klass->link_set_token (self, ifindex, iid);
@@ -1581,13 +1598,20 @@ nm_platform_link_set_ipv6_token (NMPlatform *self, int ifindex, NMUtilsIPv6Iface
 const char *
 nm_platform_link_get_udi (NMPlatform *self, int ifindex)
 {
-	_CHECK_SELF (self, klass, FALSE);
+	struct udev_device *device;
 
-	g_return_val_if_fail (ifindex >= 0, NULL);
+	device = nm_platform_link_get_udev_device (self, ifindex);
+	return device ? udev_device_get_syspath (device) : NULL;
+}
 
-	if (klass->link_get_udi)
-		return klass->link_get_udi (self, ifindex);
-	return NULL;
+const char *
+nm_platform_link_get_path (NMPlatform *self, int ifindex)
+{
+	const char *value = NULL;
+
+	link_get_udev_property (self, ifindex, "ID_PATH", &value);
+
+	return value;
 }
 
 struct udev_device *
@@ -3214,6 +3238,56 @@ nm_platform_ethtool_set_features (NMPlatform *self,
 	return nmp_utils_ethtool_set_features (ifindex, features, requested, do_set);
 }
 
+gboolean
+nm_platform_ethtool_get_link_coalesce (NMPlatform *self,
+                                       int ifindex,
+                                       NMEthtoolCoalesceState *coalesce)
+{
+	_CHECK_SELF_NETNS (self, klass, netns, FALSE);
+
+	g_return_val_if_fail (ifindex > 0, FALSE);
+	g_return_val_if_fail (coalesce, FALSE);
+
+	return nmp_utils_ethtool_get_coalesce (ifindex, coalesce);
+}
+
+gboolean
+nm_platform_ethtool_set_coalesce (NMPlatform *self,
+                                  int ifindex,
+                                  const NMEthtoolCoalesceState *coalesce)
+{
+	_CHECK_SELF_NETNS (self, klass, netns, FALSE);
+
+	g_return_val_if_fail (ifindex > 0, FALSE);
+
+	return nmp_utils_ethtool_set_coalesce (ifindex, coalesce);
+}
+
+gboolean
+nm_platform_ethtool_get_link_ring (NMPlatform *self,
+                                   int ifindex,
+                                   NMEthtoolRingState *ring)
+{
+	_CHECK_SELF_NETNS (self, klass, netns, FALSE);
+
+	g_return_val_if_fail (ifindex > 0, FALSE);
+	g_return_val_if_fail (ring, FALSE);
+
+	return nmp_utils_ethtool_get_ring (ifindex, ring);
+}
+
+gboolean
+nm_platform_ethtool_set_ring (NMPlatform *self,
+                              int ifindex,
+                              const NMEthtoolRingState *ring)
+{
+	_CHECK_SELF_NETNS (self, klass, netns, FALSE);
+
+	g_return_val_if_fail (ifindex > 0, FALSE);
+
+	return nmp_utils_ethtool_set_ring (ifindex, ring);
+}
+
 /*****************************************************************************/
 
 const NMDedupMultiHeadEntry *
@@ -4978,12 +5052,15 @@ nm_platform_qdisc_sync (NMPlatform *self,
 
 	known_qdiscs_idx = g_hash_table_new ((GHashFunc) nmp_object_id_hash,
 	                                     (GEqualFunc) nmp_object_id_equal);
-
 	if (known_qdiscs) {
 		for (i = 0; i < known_qdiscs->len; i++) {
 			const NMPObject *q = g_ptr_array_index (known_qdiscs, i);
 
-			g_hash_table_insert (known_qdiscs_idx, (gpointer) q, (gpointer) q);
+			if (!g_hash_table_insert (known_qdiscs_idx, (gpointer) q, (gpointer) q)) {
+				_LOGW ("duplicate qdisc %s", nm_platform_qdisc_to_string (&q->qdisc, NULL, 0));
+				return FALSE;
+			}
+
 		}
 	}
 
@@ -4992,13 +5069,34 @@ nm_platform_qdisc_sync (NMPlatform *self,
 	                                                                NMP_OBJECT_TYPE_QDISC,
 	                                                                ifindex),
 	                                        NULL, NULL);
-
 	if (plat_qdiscs) {
 		for (i = 0; i < plat_qdiscs->len; i++) {
-			const NMPObject *q = g_ptr_array_index (plat_qdiscs, i);
+			const NMPObject *p = g_ptr_array_index (plat_qdiscs, i);
+			const NMPObject *k;
 
-			if (!g_hash_table_lookup (known_qdiscs_idx, q))
-				success &= nm_platform_object_delete (self, q);
+			/* look up known qdisc with same parent */
+			k = g_hash_table_lookup (known_qdiscs_idx, p);
+
+			if (k) {
+				const NMPlatformQdisc *qdisc_k = NMP_OBJECT_CAST_QDISC (k);
+				const NMPlatformQdisc *qdisc_p = NMP_OBJECT_CAST_QDISC (p);
+
+				/* check other fields */
+				if (   nm_platform_qdisc_cmp_full (qdisc_k, qdisc_p, FALSE) != 0
+				    || (   qdisc_k->handle != qdisc_p->handle
+				        && qdisc_k != 0)) {
+					k = NULL;
+				}
+			}
+
+			if (k) {
+				g_hash_table_remove (known_qdiscs_idx, k);
+			} else {
+				/* can't delete qdisc with zero handle */
+				if (TC_H_MAJ (p->qdisc.handle) != 0) {
+					success &= nm_platform_object_delete (self, p);
+				}
+			}
 		}
 	}
 
@@ -5006,8 +5104,10 @@ nm_platform_qdisc_sync (NMPlatform *self,
 		for (i = 0; i < known_qdiscs->len; i++) {
 			const NMPObject *q = g_ptr_array_index (known_qdiscs, i);
 
-			success &= (nm_platform_qdisc_add (self, NMP_NLM_FLAG_ADD,
-			                                   NMP_OBJECT_CAST_QDISC (q)) >= 0);
+			if (g_hash_table_contains (known_qdiscs_idx, q)) {
+				success &= (nm_platform_qdisc_add (self, NMP_NLM_FLAG_ADD,
+				                                   NMP_OBJECT_CAST_QDISC (q)) >= 0);
+			}
 		}
 	}
 
@@ -6402,6 +6502,26 @@ nm_platform_qdisc_to_string (const NMPlatformQdisc *qdisc, char *buf, gsize len)
 			nm_utils_strbuf_append (&buf, &len, " memory_limit %u", qdisc->fq_codel.memory_limit);
 		if (qdisc->fq_codel.ecn)
 			nm_utils_strbuf_append (&buf, &len, " ecn");
+	} else if (nm_streq0 (qdisc->kind, "sfq")) {
+		if (qdisc->sfq.quantum)
+			nm_utils_strbuf_append (&buf, &len, " quantum %u", qdisc->sfq.quantum);
+		if (qdisc->sfq.perturb_period)
+			nm_utils_strbuf_append (&buf, &len, " perturb %d", qdisc->sfq.perturb_period);
+		if (qdisc->sfq.limit)
+			nm_utils_strbuf_append (&buf, &len, " limit %u", (guint) qdisc->sfq.limit);
+		if (qdisc->sfq.divisor)
+			nm_utils_strbuf_append (&buf, &len, " divisor %u", qdisc->sfq.divisor);
+		if (qdisc->sfq.flows)
+			nm_utils_strbuf_append (&buf, &len, " flows %u", qdisc->sfq.flows);
+		if (qdisc->sfq.depth)
+			nm_utils_strbuf_append (&buf, &len, " depth %u", qdisc->sfq.depth);
+	} else if (nm_streq0 (qdisc->kind, "tbf")) {
+		nm_utils_strbuf_append (&buf, &len, " rate %"G_GUINT64_FORMAT, qdisc->tbf.rate);
+		nm_utils_strbuf_append (&buf, &len, " burst %u", qdisc->tbf.burst);
+		if (qdisc->tbf.limit)
+			nm_utils_strbuf_append (&buf, &len, " limit %u", qdisc->tbf.limit);
+		if (qdisc->tbf.latency)
+			nm_utils_strbuf_append (&buf, &len, " latency %uns", qdisc->tbf.latency);
 	}
 
 	return buf0;
@@ -6428,18 +6548,35 @@ nm_platform_qdisc_hash_update (const NMPlatformQdisc *obj, NMHashState *h)
 		                     obj->fq_codel.memory_limit,
 		                     NM_HASH_COMBINE_BOOLS (guint8,
 		                                            obj->fq_codel.ecn));
+	} else if (nm_streq0 (obj->kind, "sfq")) {
+		nm_hash_update_vals (h,
+		                     obj->sfq.quantum,
+		                     obj->sfq.perturb_period,
+		                     obj->sfq.limit,
+		                     obj->sfq.divisor,
+		                     obj->sfq.flows,
+		                     obj->sfq.depth);
+	} else if (nm_streq0 (obj->kind, "tbf")) {
+		nm_hash_update_vals (h,
+		                     obj->tbf.rate,
+		                     obj->tbf.burst,
+		                     obj->tbf.limit,
+		                     obj->tbf.latency);
 	}
 }
 
 int
-nm_platform_qdisc_cmp (const NMPlatformQdisc *a, const NMPlatformQdisc *b)
+nm_platform_qdisc_cmp_full (const NMPlatformQdisc *a,
+                            const NMPlatformQdisc *b,
+                            gboolean compare_handle)
 {
 	NM_CMP_SELF (a, b);
 	NM_CMP_FIELD (a, b, ifindex);
 	NM_CMP_FIELD (a, b, parent);
 	NM_CMP_FIELD_STR_INTERNED (a, b, kind);
 	NM_CMP_FIELD (a, b, addr_family);
-	NM_CMP_FIELD (a, b, handle);
+	if (compare_handle)
+		NM_CMP_FIELD (a, b, handle);
 	NM_CMP_FIELD (a, b, info);
 
 	if (nm_streq0 (a->kind, "fq_codel")) {
@@ -6451,11 +6588,29 @@ nm_platform_qdisc_cmp (const NMPlatformQdisc *a, const NMPlatformQdisc *b)
 		NM_CMP_FIELD (a, b, fq_codel.ce_threshold);
 		NM_CMP_FIELD (a, b, fq_codel.memory_limit);
 		NM_CMP_FIELD_UNSAFE (a, b, fq_codel.ecn);
+	} else if (nm_streq0 (a->kind, "sfq")) {
+		NM_CMP_FIELD (a, b, sfq.quantum);
+		NM_CMP_FIELD (a, b, sfq.perturb_period);
+		NM_CMP_FIELD (a, b, sfq.limit);
+		NM_CMP_FIELD (a, b, sfq.flows);
+		NM_CMP_FIELD (a, b, sfq.divisor);
+		NM_CMP_FIELD (a, b, sfq.depth);
+	} else if (nm_streq0 (a->kind, "tbf")) {
+		NM_CMP_FIELD (a, b, tbf.rate);
+		NM_CMP_FIELD (a, b, tbf.burst);
+		NM_CMP_FIELD (a, b, tbf.limit);
+		NM_CMP_FIELD (a, b, tbf.latency);
 	}
 
 	return 0;
 }
 
+int
+nm_platform_qdisc_cmp (const NMPlatformQdisc *a, const NMPlatformQdisc *b)
+{
+	return nm_platform_qdisc_cmp_full (a, b, TRUE);
+}
+
 const char *
 nm_platform_tfilter_to_string (const NMPlatformTfilter *tfilter, char *buf, gsize len)
 {
diff --git a/src/platform/nm-platform.h b/src/platform/nm-platform.h
index 7d10d909..aa3551a6 100644
--- a/src/platform/nm-platform.h
+++ b/src/platform/nm-platform.h
@@ -69,6 +69,8 @@ typedef gboolean (*NMPObjectPredicateFunc) (const NMPObject *obj,
 #define NM_GRE_KEY      0x2000
 
 typedef enum {
+	NMP_NLM_FLAG_F_ECHO         = 0x08, /* NLM_F_ECHO, Echo this request */
+
 	/* use our own platform enum for the nlmsg-flags. Otherwise, we'd have
 	 * to include <linux/netlink.h> */
 	NMP_NLM_FLAG_F_REPLACE      = 0x100, /* NLM_F_REPLACE, Override existing */
@@ -628,6 +630,22 @@ typedef struct {
 } NMPlatformQdiscFqCodel;
 
 typedef struct {
+	unsigned quantum;
+	int perturb_period;
+	guint32 limit;
+	unsigned divisor;
+	unsigned flows;
+	unsigned depth;
+} NMPlatformQdiscSfq;
+
+typedef struct {
+	guint64 rate;
+	guint32 burst;
+	guint32 limit;
+	guint32 latency;
+} NMPlatformQdiscTbf;
+
+typedef struct {
 	__NMPlatformObjWithIfindex_COMMON;
 
 	/* beware, kind is embedded in an NMPObject, hence you must
@@ -640,6 +658,8 @@ typedef struct {
 	guint32 info;
 	union {
 		NMPlatformQdiscFqCodel fq_codel;
+		NMPlatformQdiscSfq sfq;
+		NMPlatformQdiscTbf tbf;
 	};
 } NMPlatformQdisc;
 
@@ -990,9 +1010,6 @@ typedef struct {
 	gboolean (*link_set_arp) (NMPlatform *self, int ifindex);
 	gboolean (*link_set_noarp) (NMPlatform *self, int ifindex);
 
-	const char *(*link_get_udi) (NMPlatform *self, int ifindex);
-	struct udev_device *(*link_get_udev_device) (NMPlatform *self, int ifindex);
-
 	int (*link_set_user_ipv6ll_enabled) (NMPlatform *self, int ifindex, gboolean enabled);
 	gboolean (*link_set_token) (NMPlatform *self, int ifindex, NMUtilsIPv6IfaceId iid);
 
@@ -1613,6 +1630,7 @@ gboolean nm_platform_link_set_arp (NMPlatform *self, int ifindex);
 gboolean nm_platform_link_set_noarp (NMPlatform *self, int ifindex);
 
 const char *nm_platform_link_get_udi (NMPlatform *self, int ifindex);
+const char *nm_platform_link_get_path (NMPlatform *self, int ifindex);
 
 struct udev_device *nm_platform_link_get_udev_device (NMPlatform *self, int ifindex);
 
@@ -1907,6 +1925,9 @@ nm_platform_routing_rule_cmp_full (const NMPlatformRoutingRule *a, const NMPlatf
 }
 
 int nm_platform_qdisc_cmp (const NMPlatformQdisc *a, const NMPlatformQdisc *b);
+int nm_platform_qdisc_cmp_full (const NMPlatformQdisc *a,
+                                const NMPlatformQdisc *b,
+                                gboolean compare_handle);
 int nm_platform_tfilter_cmp (const NMPlatformTfilter *a, const NMPlatformTfilter *b);
 
 void nm_platform_link_hash_update (const NMPlatformLink *obj, NMHashState *h);
@@ -1954,6 +1975,26 @@ gboolean nm_platform_ethtool_set_features (NMPlatform *self,
                                            const NMTernary *requested /* indexed by NMEthtoolID - _NM_ETHTOOL_ID_FEATURE_FIRST */,
                                            gboolean do_set /* or reset */);
 
+typedef struct _NMEthtoolCoalesceState NMEthtoolCoalesceState;
+
+gboolean nm_platform_ethtool_get_link_coalesce (NMPlatform *self,
+                                                int ifindex,
+                                                NMEthtoolCoalesceState *coalesce);
+
+gboolean nm_platform_ethtool_set_coalesce (NMPlatform *self,
+                                           int ifindex,
+                                           const NMEthtoolCoalesceState *coalesce);
+
+typedef struct _NMEthtoolRingState NMEthtoolRingState;
+
+gboolean nm_platform_ethtool_get_link_ring (NMPlatform *self,
+                                            int ifindex,
+                                            NMEthtoolRingState *ring);
+
+gboolean nm_platform_ethtool_set_ring (NMPlatform *self,
+                                       int ifindex,
+                                       const NMEthtoolRingState *ring);
+
 const char * nm_platform_link_duplex_type_to_string (NMPlatformLinkDuplexType duplex);
 
 void nm_platform_ip4_dev_route_blacklist_set (NMPlatform *self,
diff --git a/src/platform/tests/meson.build b/src/platform/tests/meson.build
index 4a50bca9..f96850cc 100644
--- a/src/platform/tests/meson.build
+++ b/src/platform/tests/meson.build
@@ -14,6 +14,8 @@ test_units = [
   ['test-platform-general', 'test-platform-general.c', test_c_flags, default_test_timeout],
   ['test-route-fake', 'test-route.c', test_fake_c_flags, default_test_timeout],
   ['test-route-linux', 'test-route.c', test_linux_c_flags, default_test_timeout],
+  ['test-tc-fake', 'test-tc.c', test_fake_c_flags, default_test_timeout],
+  ['test-tc-linux', 'test-tc.c', test_linux_c_flags, default_test_timeout],
 ]
 
 foreach test_unit: test_units
diff --git a/src/platform/tests/test-common.c b/src/platform/tests/test-common.c
index f28dfa3a..9f6a29bc 100644
--- a/src/platform/tests/test-common.c
+++ b/src/platform/tests/test-common.c
@@ -557,6 +557,7 @@ nmtstp_wait_for_signal (NMPlatform *platform, gint64 timeout_msec)
 {
 	WaitForSignalData data = { 0 };
 	gulong id_link, id_ip4_address, id_ip6_address, id_ip4_route, id_ip6_route;
+	gulong id_qdisc, id_tfilter;
 
 	_init_platform (&platform, FALSE);
 
@@ -567,6 +568,8 @@ nmtstp_wait_for_signal (NMPlatform *platform, gint64 timeout_msec)
 	id_ip6_address = g_signal_connect (platform, NM_PLATFORM_SIGNAL_IP6_ADDRESS_CHANGED, G_CALLBACK (_wait_for_signal_cb), &data);
 	id_ip4_route   = g_signal_connect (platform, NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED, G_CALLBACK (_wait_for_signal_cb), &data);
 	id_ip6_route   = g_signal_connect (platform, NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED, G_CALLBACK (_wait_for_signal_cb), &data);
+	id_qdisc       = g_signal_connect (platform, NM_PLATFORM_SIGNAL_QDISC_CHANGED, G_CALLBACK (_wait_for_signal_cb), &data);
+	id_tfilter     = g_signal_connect (platform, NM_PLATFORM_SIGNAL_TFILTER_CHANGED, G_CALLBACK (_wait_for_signal_cb), &data);
 
 	/* if timeout_msec is negative, it means the wait-time already expired.
 	 * Maybe, we should do nothing and return right away, without even
@@ -589,6 +592,8 @@ nmtstp_wait_for_signal (NMPlatform *platform, gint64 timeout_msec)
 	g_assert (nm_clear_g_signal_handler (platform, &id_ip6_address));
 	g_assert (nm_clear_g_signal_handler (platform, &id_ip4_route));
 	g_assert (nm_clear_g_signal_handler (platform, &id_ip6_route));
+	g_assert (nm_clear_g_signal_handler (platform, &id_tfilter));
+	g_assert (nm_clear_g_signal_handler (platform, &id_qdisc));
 
 	nm_clear_pointer (&data.loop, g_main_loop_unref);
 
diff --git a/src/platform/tests/test-link.c b/src/platform/tests/test-link.c
index ea4a2c5a..d7285c83 100644
--- a/src/platform/tests/test-link.c
+++ b/src/platform/tests/test-link.c
@@ -3290,14 +3290,14 @@ test_ethtool_features_get (void)
 		g_ptr_array_add (gfree_keeper, requested);
 
 		if (i_run == 0) {
-			requested[NM_ETHTOOL_ID_FEATURE_RX]                    = NM_TERNARY_FALSE;
-			requested[NM_ETHTOOL_ID_FEATURE_TSO]                   = NM_TERNARY_FALSE;
-			requested[NM_ETHTOOL_ID_FEATURE_TX_TCP6_SEGMENTATION]  = NM_TERNARY_FALSE;
+			requested[_NM_ETHTOOL_ID_FEATURE_AS_IDX (NM_ETHTOOL_ID_FEATURE_RX)] = NM_TERNARY_FALSE;
+			requested[_NM_ETHTOOL_ID_FEATURE_AS_IDX (NM_ETHTOOL_ID_FEATURE_TSO)] = NM_TERNARY_FALSE;
+			requested[_NM_ETHTOOL_ID_FEATURE_AS_IDX (NM_ETHTOOL_ID_FEATURE_TX_TCP6_SEGMENTATION)] = NM_TERNARY_FALSE;
 		} else if (i_run == 1)
 			do_set = FALSE;
 		else if (i_run == 2) {
-			requested[NM_ETHTOOL_ID_FEATURE_TSO]                   = NM_TERNARY_FALSE;
-			requested[NM_ETHTOOL_ID_FEATURE_TX_TCP6_SEGMENTATION]  = NM_TERNARY_TRUE;
+			requested[_NM_ETHTOOL_ID_FEATURE_AS_IDX (NM_ETHTOOL_ID_FEATURE_TSO)] = NM_TERNARY_FALSE;
+			requested[_NM_ETHTOOL_ID_FEATURE_AS_IDX (NM_ETHTOOL_ID_FEATURE_TX_TCP6_SEGMENTATION)] = NM_TERNARY_TRUE;
 		} else if (i_run == 3)
 			do_set = FALSE;
 
diff --git a/src/platform/tests/test-tc.c b/src/platform/tests/test-tc.c
new file mode 100644
index 00000000..0c90fcfa
--- /dev/null
+++ b/src/platform/tests/test-tc.c
@@ -0,0 +1,223 @@
+// SPDX-License-Identifier: LGPL-2.1+
+
+#include "nm-default.h"
+
+#include <linux/pkt_sched.h>
+
+#include "nm-test-utils-core.h"
+#include "platform/nmp-object.h"
+#include "platform/nmp-netns.h"
+#include "platform/nm-platform-utils.h"
+#include "test-common.h"
+
+static NMPObject *
+qdisc_new (int ifindex, const char *kind, guint32 parent)
+{
+	NMPObject *obj;
+
+	obj = nmp_object_new (NMP_OBJECT_TYPE_QDISC, NULL);
+	obj->qdisc = (NMPlatformQdisc) {
+		.ifindex = ifindex,
+		.kind = kind,
+		.parent = parent,
+	};
+
+	return obj;
+}
+
+static GPtrArray *
+qdiscs_lookup (int ifindex)
+{
+	NMPLookup lookup;
+
+	return nm_platform_lookup_clone (NM_PLATFORM_GET,
+	                                 nmp_lookup_init_object (&lookup,
+	                                                         NMP_OBJECT_TYPE_QDISC,
+	                                                         ifindex),
+	                                 NULL, NULL);
+}
+
+static void
+test_qdisc1 (void)
+{
+	int ifindex;
+	gs_unref_ptrarray GPtrArray *known = NULL;
+	gs_unref_ptrarray GPtrArray *plat = NULL;
+	NMPObject *obj;
+	NMPlatformQdisc *qdisc;
+
+	ifindex = nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME);
+	g_assert_cmpint (ifindex, >, 0);
+
+	nmtstp_run_command       ("tc qdisc del dev %s root", DEVICE_NAME);
+	nmtstp_run_command_check ("tc qdisc add dev %s root sfq", DEVICE_NAME);
+
+	nmtstp_wait_for_signal (NM_PLATFORM_GET, 0);
+
+	known = g_ptr_array_new_with_free_func ((GDestroyNotify) nmp_object_unref);
+	g_ptr_array_add (known, qdisc_new (ifindex, "fq_codel", TC_H_ROOT));
+	g_ptr_array_add (known, qdisc_new (ifindex, "ingress", TC_H_INGRESS));
+
+	g_assert (nm_platform_qdisc_sync (NM_PLATFORM_GET, ifindex, known));
+	plat = qdiscs_lookup (ifindex);
+	g_assert (plat);
+	g_assert_cmpint (plat->len, ==, 2);
+
+	obj = plat->pdata[0];
+	qdisc = NMP_OBJECT_CAST_QDISC (obj);
+	g_assert_cmpint (qdisc->parent, ==, TC_H_ROOT);
+	g_assert_cmpstr (qdisc->kind, ==, "fq_codel");
+
+	obj = plat->pdata[1];
+	qdisc = NMP_OBJECT_CAST_QDISC (obj);
+	g_assert_cmpint (qdisc->parent, ==, TC_H_INGRESS);
+	g_assert_cmpstr (qdisc->kind, ==, "ingress");
+}
+
+static void
+test_qdisc_fq_codel (void)
+{
+	int ifindex;
+	gs_unref_ptrarray GPtrArray *known = NULL;
+	gs_unref_ptrarray GPtrArray *plat = NULL;
+	NMPObject *obj;
+	NMPlatformQdisc *qdisc;
+
+	ifindex = nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME);
+	g_assert_cmpint (ifindex, >, 0);
+
+	nmtstp_run_command ("tc qdisc del dev %s root", DEVICE_NAME);
+
+	nmtstp_wait_for_signal (NM_PLATFORM_GET, 0);
+
+	known = g_ptr_array_new_with_free_func ((GDestroyNotify) nmp_object_unref);
+	obj = qdisc_new (ifindex, "fq_codel", TC_H_ROOT);
+	obj->qdisc.handle = TC_H_MAKE (0x8142 << 16, 0);
+	obj->qdisc.fq_codel.limit = 2048;
+	obj->qdisc.fq_codel.flows = 64;
+	obj->qdisc.fq_codel.quantum = 1000;
+	g_ptr_array_add (known, obj);
+
+	g_assert (nm_platform_qdisc_sync (NM_PLATFORM_GET, ifindex, known));
+	plat = qdiscs_lookup (ifindex);
+	g_assert (plat);
+	g_assert_cmpint (plat->len, ==, 1);
+
+	obj = plat->pdata[0];
+	qdisc = NMP_OBJECT_CAST_QDISC (obj);
+	g_assert_cmpstr (qdisc->kind, ==, "fq_codel");
+	g_assert_cmpint (qdisc->handle, ==, TC_H_MAKE (0x8142 << 16, 0));
+	g_assert_cmpint (qdisc->parent, ==, TC_H_ROOT);
+	g_assert_cmpint (qdisc->fq_codel.limit, ==, 2048);
+	g_assert_cmpint (qdisc->fq_codel.flows, ==, 64);
+	g_assert_cmpint (qdisc->fq_codel.quantum, ==, 1000);
+}
+
+static void
+test_qdisc_sfq (void)
+{
+	int ifindex;
+	gs_unref_ptrarray GPtrArray *known = NULL;
+	gs_unref_ptrarray GPtrArray *plat = NULL;
+	NMPObject *obj;
+	NMPlatformQdisc *qdisc;
+
+	ifindex = nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME);
+	g_assert_cmpint (ifindex, >, 0);
+
+	nmtstp_run_command ("tc qdisc del dev %s root", DEVICE_NAME);
+
+	nmtstp_wait_for_signal (NM_PLATFORM_GET, 0);
+
+	known = g_ptr_array_new_with_free_func ((GDestroyNotify) nmp_object_unref);
+	obj = qdisc_new (ifindex, "sfq", TC_H_ROOT);
+	obj->qdisc.handle = TC_H_MAKE (0x8143 << 16, 0);
+	obj->qdisc.sfq.perturb_period = 10;
+	obj->qdisc.sfq.quantum = 1540;
+	obj->qdisc.sfq.flows = 256;
+	g_ptr_array_add (known, obj);
+
+	g_assert (nm_platform_qdisc_sync (NM_PLATFORM_GET, ifindex, known));
+	plat = qdiscs_lookup (ifindex);
+	g_assert (plat);
+	g_assert_cmpint (plat->len, ==, 1);
+
+	obj = plat->pdata[0];
+	qdisc = NMP_OBJECT_CAST_QDISC (obj);
+	g_assert_cmpstr (qdisc->kind, ==, "sfq");
+	g_assert_cmpint (qdisc->handle, ==, TC_H_MAKE (0x8143 << 16, 0));
+	g_assert_cmpint (qdisc->parent, ==, TC_H_ROOT);
+	g_assert_cmpint (qdisc->sfq.perturb_period, ==, 10);
+	g_assert_cmpint (qdisc->sfq.quantum, ==, 1540);
+	g_assert_cmpint (qdisc->sfq.flows, ==, 256);
+}
+
+static void
+test_qdisc_tbf (void)
+{
+	int ifindex;
+	gs_unref_ptrarray GPtrArray *known = NULL;
+	gs_unref_ptrarray GPtrArray *plat = NULL;
+	NMPObject *obj;
+	NMPlatformQdisc *qdisc;
+
+	ifindex = nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME);
+	g_assert_cmpint (ifindex, >, 0);
+
+	nmtstp_run_command ("tc qdisc del dev %s root", DEVICE_NAME);
+
+	nmtstp_wait_for_signal (NM_PLATFORM_GET, 0);
+
+	known = g_ptr_array_new_with_free_func ((GDestroyNotify) nmp_object_unref);
+	obj = qdisc_new (ifindex, "tbf", TC_H_ROOT);
+	obj->qdisc.handle = TC_H_MAKE (0x8143 << 16, 0);
+	obj->qdisc.tbf.rate = 1000000;
+	obj->qdisc.tbf.burst = 2000;
+	obj->qdisc.tbf.limit = 3000;
+	g_ptr_array_add (known, obj);
+
+	obj = qdisc_new (ifindex, "sfq", TC_H_MAKE (0x8143 << 16, 0));
+	obj->qdisc.handle = TC_H_MAKE (0x8005 << 16, 0);
+	g_ptr_array_add (known, obj);
+
+	g_assert (nm_platform_qdisc_sync (NM_PLATFORM_GET, ifindex, known));
+	plat = qdiscs_lookup (ifindex);
+	g_assert (plat);
+	g_assert_cmpint (plat->len, ==, 2);
+
+	obj = plat->pdata[0];
+	qdisc = NMP_OBJECT_CAST_QDISC (obj);
+	g_assert_cmpstr (qdisc->kind, ==, "tbf");
+	g_assert_cmpint (qdisc->handle, ==, TC_H_MAKE (0x8143 << 16, 0));
+	g_assert_cmpint (qdisc->parent, ==, TC_H_ROOT);
+	g_assert_cmpint (qdisc->tbf.rate, ==, 1000000);
+	g_assert_cmpint (qdisc->tbf.burst, ==, 2000);
+	g_assert_cmpint (qdisc->tbf.limit, ==, 3000);
+
+	obj = plat->pdata[1];
+	qdisc = NMP_OBJECT_CAST_QDISC (obj);
+	g_assert_cmpstr (qdisc->kind, ==, "sfq");
+	g_assert_cmpint (qdisc->parent, ==, TC_H_MAKE (0x8143 << 16, 0));
+	g_assert_cmpint (qdisc->handle, ==, TC_H_MAKE (0x8005 << 16, 0));
+}
+
+/*****************************************************************************/
+
+NMTstpSetupFunc const _nmtstp_setup_platform_func = SETUP;
+
+void
+_nmtstp_init_tests (int *argc, char ***argv)
+{
+	nmtst_init_with_logging (argc, argv, NULL, "ALL");
+}
+
+void
+_nmtstp_setup_tests (void)
+{
+	if (nmtstp_is_root_test ()) {
+		nmtstp_env1_add_test_func ("/link/qdisc/1", test_qdisc1, TRUE);
+		nmtstp_env1_add_test_func ("/link/qdisc/fq_codel", test_qdisc_fq_codel, TRUE);
+		nmtstp_env1_add_test_func ("/link/qdisc/sfq", test_qdisc_sfq, TRUE);
+		nmtstp_env1_add_test_func ("/link/qdisc/tbf", test_qdisc_tbf, TRUE);
+	}
+}