Commit 9d0c3ff988 for frr

commit 9d0c3ff988573f416f7e3da0b8227676140d0c3c
Author: Jafar Al-Gharaibeh <jafar@atcorp.com>
Date:   Fri Oct 2 14:04:47 2026 -0500

    zebra: don't log expected ethtool EOPNOTSUPP/ENODEV as errors

    Speed queries on devices without link settings already treat
    EOPNOTSUPP/ENODEV as "speed unknown", but the netlink error path logs
    them first and floods the log. Demote those to "debug zebra kernel".

    Signed-off-by: Jafar Al-Gharaibeh <jafar@atcorp.com>

diff --git a/zebra/kernel_netlink.c b/zebra/kernel_netlink.c
index 149a33c053..caabddc0a2 100644
--- a/zebra/kernel_netlink.c
+++ b/zebra/kernel_netlink.c
@@ -695,7 +695,13 @@ const char *nl_rttype_to_str(uint8_t rttype)
 	return lookup_msg(rttype_str, rttype, "");
 }

-static void netlink_parse_extended_ack(struct nlmsghdr *h, int proto)
+/*
+ * Parse and log the extended ack attached to a netlink ACK/error message.
+ * When @expected is true the error is a known, benign one (see
+ * netlink_error_is_expected()) and the text is only logged when kernel
+ * debugging is enabled.
+ */
+static void netlink_parse_extended_ack(struct nlmsghdr *h, int proto, bool expected)
 {
 	struct nlattr *tb[NLMSGERR_ATTR_MAX + 1] = {};
 	const struct nlmsgerr *err = (const struct nlmsgerr *)NLMSG_DATA(h);
@@ -739,7 +745,10 @@ static void netlink_parse_extended_ack(struct nlmsghdr *h, int proto)
 	if (msg && *msg != '\0') {
 		bool is_err = !!err->error;

-		if (is_err)
+		if (expected) {
+			if (IS_ZEBRA_DEBUG_KERNEL)
+				zlog_debug("Extended %s: %s", is_err ? "Error" : "Warning", msg);
+		} else if (is_err)
 			flog_err(EC_ZEBRA_NETLINK_EXTENDED_ERROR, "Extended Error: %s", msg);
 		else
 			flog_warn(EC_ZEBRA_NETLINK_EXTENDED_WARNING,
@@ -861,6 +870,48 @@ static int netlink_recv_msg(struct nlsock *nl, struct msghdr *msg)
 	return status;
 }

+/*
+ * Errors that occur because of races in link handling or because a type or
+ * operation is not supported by the kernel (or by a given device). These are
+ * expected and are only logged when kernel debugging is enabled.
+ */
+static bool netlink_error_is_expected(const struct nlsock *nl, int msg_type, int errnum,
+				      bool is_cmd)
+{
+	if (!is_cmd)
+		return false;
+
+	/* rtnetlink message types only mean something on NETLINK_ROUTE sockets */
+	if (nl->proto == NETLINK_ROUTE) {
+		switch (msg_type) {
+		case RTM_DELROUTE:
+			return -errnum == ENODEV || -errnum == ESRCH;
+		case RTM_NEWROUTE:
+			return -errnum == ENETDOWN || -errnum == EEXIST;
+		case RTM_DELNEXTHOP:
+			return -errnum == ENOENT || -errnum == ESRCH;
+		case RTM_NEWTUNNEL:
+		case RTM_DELTUNNEL:
+		case RTM_GETTUNNEL:
+			return -errnum == EOPNOTSUPP;
+		default:
+			return false;
+		}
+	}
+
+	/*
+	 * ethtool generic netlink: devices without link settings (tunnels,
+	 * tun/tap, wireguard, dummy, ...) answer ETHTOOL_MSG_LINKMODES_GET with
+	 * EOPNOTSUPP, and an interface can disappear (ENODEV) while its speed
+	 * query is in flight. Generic netlink message types are dynamically
+	 * assigned family ids, so match on the protocol and resolved family.
+	 */
+	if (nl->proto == NETLINK_GENERIC && msg_type == genl_family_ethtool())
+		return -errnum == EOPNOTSUPP || -errnum == ENODEV;
+
+	return false;
+}
+
 /*
  * netlink_parse_error - parse a netlink error message
  *
@@ -873,6 +924,7 @@ static int netlink_parse_error(const struct nlsock *nl, struct nlmsghdr *h,
 	struct nlmsgerr *err = (struct nlmsgerr *)NLMSG_DATA(h);
 	int errnum = err->error;
 	int msg_type = err->msg.nlmsg_type;
+	bool expected;

 	if (h->nlmsg_len < NLMSG_LENGTH(sizeof(struct nlmsgerr))) {
 		flog_err(EC_ZEBRA_NETLINK_LENGTH_ERROR,
@@ -884,8 +936,10 @@ static int netlink_parse_error(const struct nlsock *nl, struct nlmsghdr *h,
 	 * Parse the extended information before we actually handle it. At this
 	 * point in time we do not do anything other than report the issue.
 	 */
+	expected = errnum && netlink_error_is_expected(nl, msg_type, errnum, is_cmd);
+
 	if (h->nlmsg_flags & NLM_F_ACK_TLVS)
-		netlink_parse_extended_ack(h, nl->proto);
+		netlink_parse_extended_ack(h, nl->proto, expected);

 	/* If the error field is zero, then this is an ACK. */
 	if (err->error == 0) {
@@ -902,21 +956,21 @@ static int netlink_parse_error(const struct nlsock *nl, struct nlmsghdr *h,
 	 * Deal with errors that occur because of races in link handling
 	 * or types are not supported in kernel.
 	 */
-	if (is_cmd &&
-	    ((msg_type == RTM_DELROUTE &&
-	      (-errnum == ENODEV || -errnum == ESRCH)) ||
-	     (msg_type == RTM_NEWROUTE &&
-	      (-errnum == ENETDOWN || -errnum == EEXIST)) ||
-	     (msg_type == RTM_DELNEXTHOP &&
-	      (-errnum == ENOENT || -errnum == ESRCH)) ||
-	     ((msg_type == RTM_NEWTUNNEL || msg_type == RTM_DELTUNNEL ||
-	       msg_type == RTM_GETTUNNEL) &&
-	      (-errnum == EOPNOTSUPP)))) {
+	if (expected) {
 		if (IS_ZEBRA_DEBUG_KERNEL)
 			zlog_debug("%s: error: %s type=%s(%u), seq=%u, pid=%u", nl->name,
 				   safe_strerror(-errnum),
 				   nl_msg_type_to_str_sock(msg_type, nl->proto), msg_type,
 				   err->msg.nlmsg_seq, err->msg.nlmsg_pid);
+		/*
+		 * Route, nexthop, and tunnel races can be dropped. An ethtool
+		 * speed query must still fail the read: a zero return leaves
+		 * the speed unset, and the caller then stores 0 and schedules
+		 * another query. netlink_get_interface_speed() maps a negative
+		 * return plus EOPNOTSUPP/ENODEV to INTERFACE_SPEED_ERROR_READ.
+		 */
+		if (nl->proto == NETLINK_GENERIC && msg_type == genl_family_ethtool())
+			return -1;
 		return 0;
 	}