Patch Detail
get:
Show a patch.
patch:
Update a patch.
put:
Update a patch.
GET /api/patches/2218981/?format=api
{ "id": 2218981, "url": "http://patchwork.ozlabs.org/api/patches/2218981/?format=api", "web_url": "http://patchwork.ozlabs.org/project/ovn/patch/20260402082510.1417440-7-amusil@redhat.com/", "project": { "id": 68, "url": "http://patchwork.ozlabs.org/api/projects/68/?format=api", "name": "Open Virtual Network development", "link_name": "ovn", "list_id": "ovs-dev.openvswitch.org", "list_email": "ovs-dev@openvswitch.org", "web_url": "http://openvswitch.org/", "scm_url": "", "webscm_url": "", "list_archive_url": "", "list_archive_url_format": "", "commit_url_format": "" }, "msgid": "<20260402082510.1417440-7-amusil@redhat.com>", "list_archive_url": null, "date": "2026-04-02T08:25:10", "name": "[ovs-dev,6/6] controller: Add support for syncing the nexthop table.", "commit_ref": null, "pull_url": null, "state": "new", "archived": false, "hash": "6ecb8971b0742c2212e7da09ae4790ea53c9f47b", "submitter": { "id": 83634, "url": "http://patchwork.ozlabs.org/api/people/83634/?format=api", "name": "Ales Musil", "email": "amusil@redhat.com" }, "delegate": null, "mbox": "http://patchwork.ozlabs.org/project/ovn/patch/20260402082510.1417440-7-amusil@redhat.com/mbox/", "series": [ { "id": 498451, "url": "http://patchwork.ozlabs.org/api/series/498451/?format=api", "web_url": "http://patchwork.ozlabs.org/project/ovn/list/?series=498451", "date": "2026-04-02T08:25:04", "name": "Netlink notifier consolidation and nexthop table support", "version": 1, "mbox": "http://patchwork.ozlabs.org/series/498451/mbox/" } ], "comments": "http://patchwork.ozlabs.org/api/patches/2218981/comments/", "check": "success", "checks": "http://patchwork.ozlabs.org/api/patches/2218981/checks/", "tags": {}, "related": [], "headers": { "Return-Path": "<ovs-dev-bounces@openvswitch.org>", "X-Original-To": [ "incoming@patchwork.ozlabs.org", "dev@openvswitch.org" ], "Delivered-To": [ "patchwork-incoming@legolas.ozlabs.org", "ovs-dev@lists.linuxfoundation.org" ], "Authentication-Results": [ "legolas.ozlabs.org;\n\tdkim=fail reason=\"signature verification failed\" (1024-bit key;\n unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=IP8NiKhm;\n\tdkim-atps=neutral", "legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org\n (client-ip=2605:bc80:3010::137; helo=smtp4.osuosl.org;\n envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org)", "smtp4.osuosl.org;\n\tdkim=fail reason=\"signature verification failed\" (1024-bit key)\n header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=IP8NiKhm", "smtp1.osuosl.org; dmarc=pass (p=quarantine dis=none)\n header.from=redhat.com", "smtp1.osuosl.org;\n dkim=pass (1024-bit key) header.d=redhat.com header.i=@redhat.com\n header.a=rsa-sha256 header.s=mimecast20190719 header.b=IP8NiKhm" ], "Received": [ "from smtp4.osuosl.org (smtp4.osuosl.org [IPv6:2605:bc80:3010::137])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4fmZgR2MDnz1yCs\n\tfor <incoming@patchwork.ozlabs.org>; Thu, 02 Apr 2026 19:25:55 +1100 (AEDT)", "from localhost (localhost [127.0.0.1])\n\tby smtp4.osuosl.org (Postfix) with ESMTP id EFB6E40E1D;\n\tThu, 2 Apr 2026 08:25:53 +0000 (UTC)", "from smtp4.osuosl.org ([127.0.0.1])\n by localhost (smtp4.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP\n id 86Isl0FdpejD; Thu, 2 Apr 2026 08:25:52 +0000 (UTC)", "from lists.linuxfoundation.org (lf-lists.osuosl.org [140.211.9.56])\n\tby smtp4.osuosl.org (Postfix) with ESMTPS id 7A6B140DFC;\n\tThu, 2 Apr 2026 08:25:52 +0000 (UTC)", "from lf-lists.osuosl.org (localhost [127.0.0.1])\n\tby lists.linuxfoundation.org (Postfix) with ESMTP id 64424C0070;\n\tThu, 2 Apr 2026 08:25:52 +0000 (UTC)", "from smtp1.osuosl.org (smtp1.osuosl.org [IPv6:2605:bc80:3010::138])\n by lists.linuxfoundation.org (Postfix) with ESMTP id 76BADC003D\n for <dev@openvswitch.org>; Thu, 2 Apr 2026 08:25:51 +0000 (UTC)", "from localhost (localhost [127.0.0.1])\n by smtp1.osuosl.org (Postfix) with ESMTP id 607538156C\n for <dev@openvswitch.org>; Thu, 2 Apr 2026 08:25:29 +0000 (UTC)", "from smtp1.osuosl.org ([127.0.0.1])\n by localhost (smtp1.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP\n id MQdtqEQWZJvu for <dev@openvswitch.org>;\n Thu, 2 Apr 2026 08:25:27 +0000 (UTC)", "from us-smtp-delivery-124.mimecast.com\n (us-smtp-delivery-124.mimecast.com [170.10.133.124])\n by smtp1.osuosl.org (Postfix) with ESMTPS id B440E813BA\n for <dev@openvswitch.org>; Thu, 2 Apr 2026 08:25:26 +0000 (UTC)", "from mx-prod-mc-03.mail-002.prod.us-west-2.aws.redhat.com\n (ec2-54-186-198-63.us-west-2.compute.amazonaws.com [54.186.198.63]) by\n relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3,\n cipher=TLS_AES_256_GCM_SHA384) id us-mta-616-gHhKMCCDM1qoU0R4bnvMpA-1; Thu,\n 02 Apr 2026 04:25:24 -0400", "from mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com\n (mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.4])\n (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest\n SHA256)\n (No client certificate requested)\n by mx-prod-mc-03.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS\n id 501821955F13\n for <dev@openvswitch.org>; Thu, 2 Apr 2026 08:25:23 +0000 (UTC)", "from amusil.brq.redhat.com (unknown [10.43.17.233])\n by mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP\n id 4B57A30030CD; Thu, 2 Apr 2026 08:25:22 +0000 (UTC)" ], "X-Virus-Scanned": [ "amavis at osuosl.org", "amavis at osuosl.org" ], "X-Comment": "SPF check N/A for local connections - client-ip=140.211.9.56;\n helo=lists.linuxfoundation.org;\n envelope-from=ovs-dev-bounces@openvswitch.org; receiver=<UNKNOWN> ", "DKIM-Filter": [ "OpenDKIM Filter v2.11.0 smtp4.osuosl.org 7A6B140DFC", "OpenDKIM Filter v2.11.0 smtp1.osuosl.org B440E813BA" ], "Received-SPF": "Pass (mailfrom) identity=mailfrom; client-ip=170.10.133.124;\n helo=us-smtp-delivery-124.mimecast.com; envelope-from=amusil@redhat.com;\n receiver=<UNKNOWN>", "DMARC-Filter": "OpenDMARC Filter v1.4.2 smtp1.osuosl.org B440E813BA", "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n s=mimecast20190719; t=1775118325;\n h=from:from:reply-to:subject:subject:date:date:message-id:message-id:\n to:to:cc:cc:mime-version:mime-version:content-type:content-type:\n content-transfer-encoding:content-transfer-encoding:\n in-reply-to:in-reply-to:references:references;\n bh=CLWLfalx6IZGITSlBtoiaqukWQ/4RLZ739jnk9mx0qo=;\n b=IP8NiKhmkJ1ALiyllameZbs/BG3XbKlCqwNwnXR4Oc6WkZ3nOQ4qniuZ3mSUdxdstNtkqv\n QuN/X9N9jbVw/zmCuF7GHJj+f8Pga18U0sSsQ6F7aCdOB/7Vr71Nm5+NHjIlWYA73s64/v\n Yl1cogysarfoCEdvrIwUhtcuJuYw+Rw=", "X-MC-Unique": "gHhKMCCDM1qoU0R4bnvMpA-1", "X-Mimecast-MFC-AGG-ID": "gHhKMCCDM1qoU0R4bnvMpA_1775118323", "To": "dev@openvswitch.org", "Date": "Thu, 2 Apr 2026 10:25:10 +0200", "Message-ID": "<20260402082510.1417440-7-amusil@redhat.com>", "In-Reply-To": "<20260402082510.1417440-1-amusil@redhat.com>", "References": "<20260402082510.1417440-1-amusil@redhat.com>", "MIME-Version": "1.0", "X-Scanned-By": "MIMEDefang 3.4.1 on 10.30.177.4", "X-Mimecast-Spam-Score": "0", "X-Mimecast-MFC-PROC-ID": "KLcAG8AH58g-p_DVy41G9YrwkFZmMlzAk1wYULrFF9c_1775118323", "X-Mimecast-Originator": "redhat.com", "Subject": "[ovs-dev] [PATCH ovn 6/6] controller: Add support for syncing the\n nexthop table.", "X-BeenThere": "ovs-dev@openvswitch.org", "X-Mailman-Version": "2.1.30", "Precedence": "list", "List-Id": "<ovs-dev.openvswitch.org>", "List-Unsubscribe": "<https://mail.openvswitch.org/mailman/options/ovs-dev>,\n <mailto:ovs-dev-request@openvswitch.org?subject=unsubscribe>", "List-Archive": "<http://mail.openvswitch.org/pipermail/ovs-dev/>", "List-Post": "<mailto:ovs-dev@openvswitch.org>", "List-Help": "<mailto:ovs-dev-request@openvswitch.org?subject=help>", "List-Subscribe": "<https://mail.openvswitch.org/mailman/listinfo/ovs-dev>,\n <mailto:ovs-dev-request@openvswitch.org?subject=subscribe>", "From": "Ales Musil via dev <ovs-dev@openvswitch.org>", "Reply-To": "Ales Musil <amusil@redhat.com>", "Cc": "dceara@redhat.com", "Content-Type": "text/plain; charset=\"us-ascii\"", "Content-Transfer-Encoding": "7bit", "Errors-To": "ovs-dev-bounces@openvswitch.org", "Sender": "\"dev\" <ovs-dev-bounces@openvswitch.org>" }, "content": "The nexthop table contains entries about nexthop and groups of them.\nThis is utilized by EVPN in scenarios when there are remote VTEPs\nconfigured active-active multihoming. Add support for syncing the\ntable from netlink, those data will be later on used to configure\nthe multihoming for OVN EVPN.\n\nReported-at: https://redhat.atlassian.net/browse/FDP-3071\nSigned-off-by: Ales Musil <amusil@redhat.com>\n---\n configure.ac | 1 +\n controller/automake.mk | 3 +\n controller/nexthop-exchange-stub.c | 42 ++++++\n controller/nexthop-exchange.c | 230 +++++++++++++++++++++++++++++\n controller/nexthop-exchange.h | 61 ++++++++\n controller/ovn-netlink-notifier.c | 43 ++++++\n controller/ovn-netlink-notifier.h | 1 +\n m4/ovn.m4 | 16 ++\n tests/automake.mk | 2 +\n tests/system-ovn-netlink.at | 117 +++++++++++++++\n tests/test-ovn-netlink.c | 57 +++++++\n 11 files changed, 573 insertions(+)\n create mode 100644 controller/nexthop-exchange-stub.c\n create mode 100644 controller/nexthop-exchange.c\n create mode 100644 controller/nexthop-exchange.h", "diff": "diff --git a/configure.ac b/configure.ac\nindex 657769609..025dccc63 100644\n--- a/configure.ac\n+++ b/configure.ac\n@@ -174,6 +174,7 @@ OVS_CHECK_PRAGMA_MESSAGE\n OVN_CHECK_VERSION_SUFFIX\n OVN_CHECK_OVS\n OVN_CHECK_VIF_PLUG_PROVIDER\n+OVN_CHECK_LINUX_NEXTHOP_WEIGHT\n OVN_ENABLE_VIF_PLUG\n OVS_CTAGS_IDENTIFIERS\n AC_SUBST([OVS_CFLAGS])\ndiff --git a/controller/automake.mk b/controller/automake.mk\nindex c37b89b6c..a779250cb 100644\n--- a/controller/automake.mk\n+++ b/controller/automake.mk\n@@ -78,6 +78,8 @@ controller_ovn_controller_SOURCES += \\\n \tcontroller/neighbor-exchange-netlink.h \\\n \tcontroller/neighbor-exchange-netlink.c \\\n \tcontroller/neighbor-exchange.c \\\n+\tcontroller/nexthop-exchange.h \\\n+\tcontroller/nexthop-exchange.c \\\n \tcontroller/route-exchange-netlink.h \\\n \tcontroller/route-exchange-netlink.c \\\n \tcontroller/route-exchange.c\n@@ -86,6 +88,7 @@ controller_ovn_controller_SOURCES += \\\n \tcontroller/host-if-monitor-stub.c \\\n \tcontroller/ovn-netlink-notifier-stub.c \\\n \tcontroller/neighbor-exchange-stub.c \\\n+\tcontroller/nexthop-exchange-stub.c \\\n \tcontroller/route-exchange-stub.c\n endif\n \ndiff --git a/controller/nexthop-exchange-stub.c b/controller/nexthop-exchange-stub.c\nnew file mode 100644\nindex 000000000..2742dc7e2\n--- /dev/null\n+++ b/controller/nexthop-exchange-stub.c\n@@ -0,0 +1,42 @@\n+/* Copyright (c) 2026, Red Hat, Inc.\n+ *\n+ * Licensed under the Apache License, Version 2.0 (the \"License\");\n+ * you may not use this file except in compliance with the License.\n+ * You may obtain a copy of the License at:\n+ *\n+ * http://www.apache.org/licenses/LICENSE-2.0\n+ *\n+ * Unless required by applicable law or agreed to in writing, software\n+ * distributed under the License is distributed on an \"AS IS\" BASIS,\n+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+ * See the License for the specific language governing permissions and\n+ * limitations under the License.\n+ */\n+\n+#include <config.h>\n+\n+#include \"lib/netlink.h\"\n+#include \"openvswitch/hmap.h\"\n+#include \"openvswitch/ofpbuf.h\"\n+\n+#include \"nexthop-exchange.h\"\n+\n+/* Populates 'nexthops' with all nexthop entries\n+ * (struct nexthop_entry) with fdb flag set that exist in the table. */\n+void\n+nexthops_sync(struct hmap *nexthops OVS_UNUSED)\n+{\n+}\n+\n+void\n+nexthop_entry_format(struct ds *ds OVS_UNUSED,\n+ const struct nexthop_entry *nhe OVS_UNUSED)\n+{\n+}\n+\n+int\n+nh_table_parse(struct ofpbuf *buf OVS_UNUSED,\n+ struct nh_table_msg *change OVS_UNUSED)\n+{\n+ return 0;\n+}\ndiff --git a/controller/nexthop-exchange.c b/controller/nexthop-exchange.c\nnew file mode 100644\nindex 000000000..e1a5bfe1d\n--- /dev/null\n+++ b/controller/nexthop-exchange.c\n@@ -0,0 +1,230 @@\n+/* Copyright (c) 2026, Red Hat, Inc.\n+ *\n+ * Licensed under the Apache License, Version 2.0 (the \"License\");\n+ * you may not use this file except in compliance with the License.\n+ * You may obtain a copy of the License at:\n+ *\n+ * http://www.apache.org/licenses/LICENSE-2.0\n+ *\n+ * Unless required by applicable law or agreed to in writing, software\n+ * distributed under the License is distributed on an \"AS IS\" BASIS,\n+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+ * See the License for the specific language governing permissions and\n+ * limitations under the License.\n+ */\n+\n+#include <config.h>\n+#include <linux/rtnetlink.h>\n+#include <linux/nexthop.h>\n+\n+#include \"lib/netlink.h\"\n+#include \"lib/netlink-socket.h\"\n+#include \"openvswitch/ofpbuf.h\"\n+#include \"openvswitch/vlog.h\"\n+#include \"packets.h\"\n+\n+#include \"nexthop-exchange.h\"\n+\n+VLOG_DEFINE_THIS_MODULE(nexthop_exchange_netlink);\n+\n+#define NETNL_REQ_BUFFER_SIZE 128\n+\n+static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);\n+\n+static int nh_table_parse__(struct ofpbuf *, size_t ofs,\n+ const struct nlmsghdr *,\n+ struct nh_table_msg *);\n+static void nh_populate_grp_pointers(struct nexthop_entry *, struct hmap *);\n+\n+/* The following definition should be available in Linux 6.12 and might be\n+ * missing if we have older headers. */\n+#ifndef HAVE_NH_GRP_WEIGHT\n+static uint16_t\n+nexthop_grp_weight(const struct nexthop_grp *entry)\n+{\n+ return entry->weight + 1;\n+}\n+#endif\n+\n+/* Populates 'nexthops' with all nexthop entries\n+ * (struct nexthop_entry) with fdb flag set that exist in the table. */\n+void\n+nexthops_sync(struct hmap *nexthops)\n+{\n+ uint64_t reply_stub[NL_DUMP_BUFSIZE / 8];\n+ struct ofpbuf request, reply, buf;\n+ struct nl_dump dump;\n+\n+ uint8_t request_stub[NETNL_REQ_BUFFER_SIZE];\n+ ofpbuf_use_stub(&request, request_stub, sizeof request_stub);\n+\n+ nl_msg_put_nlmsghdr(&request, sizeof(struct nhmsg),\n+ RTM_GETNEXTHOP, NLM_F_REQUEST);\n+ ofpbuf_put_zeros(&request, sizeof(struct nhmsg));\n+ nl_dump_start(&dump, NETLINK_ROUTE, &request);\n+ ofpbuf_uninit(&request);\n+\n+ ofpbuf_use_stub(&buf, reply_stub, sizeof reply_stub);\n+ while (nl_dump_next(&dump, &reply, &buf)) {\n+ struct nh_table_msg msg;\n+\n+ if (!nh_table_parse(&reply, &msg)) {\n+ continue;\n+ }\n+\n+ hmap_insert(nexthops, &msg.nhe->hmap_node, msg.nhe->id);\n+ }\n+ ofpbuf_uninit(&buf);\n+ nl_dump_done(&dump);\n+\n+ struct nexthop_entry *nhe;\n+ HMAP_FOR_EACH (nhe, hmap_node, nexthops) {\n+ nh_populate_grp_pointers(nhe, nexthops);\n+ }\n+}\n+\n+void\n+nexthop_entry_format(struct ds *ds, const struct nexthop_entry *nhe)\n+{\n+ ds_put_format(ds, \"id=%\"PRIu32\", \", nhe->id);\n+ if (!nhe->n_grps) {\n+ ds_put_cstr(ds, \"address=\");\n+ ipv6_format_mapped(&nhe->addr, ds);\n+ } else {\n+ ds_put_cstr(ds, \"group=[\");\n+ for (size_t i = 0; i < nhe->n_grps; i++) {\n+ const struct nexthop_grp_entry *grp = &nhe->grps[i];\n+ ds_put_format(ds, \"%\"PRIu32\";\", grp->id);\n+ if (grp->gateway) {\n+ ipv6_format_mapped(&grp->gateway->addr, ds);\n+ ds_put_char(ds, ';');\n+ }\n+ ds_put_format(ds, \"%\"PRIu16\", \", grp->weight);\n+ }\n+\n+ ds_truncate(ds, ds->length - 2);\n+ ds_put_char(ds, ']');\n+ }\n+}\n+\n+/* Parse Netlink message in buf, which is expected to contain a UAPI nhmsg\n+ * header and associated nexthop attributes. This will allocate\n+ * 'struct nexthop_entry' which needs to be freed by the caller.\n+ *\n+ * Return RTNLGRP_NEXTHOP on success, and 0 on a parse error. */\n+int\n+nh_table_parse(struct ofpbuf *buf, struct nh_table_msg *change)\n+{\n+ struct nlmsghdr *nlmsg = ofpbuf_at(buf, 0, NLMSG_HDRLEN);\n+ struct nhmsg *nh = ofpbuf_at(buf, NLMSG_HDRLEN, sizeof *nh);\n+\n+ if (!nlmsg || !nh) {\n+ return 0;\n+ }\n+\n+ return nh_table_parse__(buf, NLMSG_HDRLEN + sizeof *nh,\n+ nlmsg, change);\n+}\n+\n+static int\n+nh_table_parse__(struct ofpbuf *buf, size_t ofs, const struct nlmsghdr *nlmsg,\n+ struct nh_table_msg *change)\n+{\n+ bool parsed;\n+\n+ static const struct nl_policy policy[] = {\n+ [NHA_ID] = { .type = NL_A_U32 },\n+ [NHA_FDB] = { .type = NL_A_FLAG, .optional = true },\n+ [NHA_GROUP] = { .type = NL_A_UNSPEC, .optional = true,\n+ .min_len = sizeof(struct nexthop_grp) },\n+ [NHA_GROUP_TYPE] = { .type = NL_A_U16, .optional = true },\n+ [NHA_GATEWAY] = { .type = NL_A_UNSPEC, .optional = true,\n+ .min_len = sizeof(struct in_addr),\n+ .max_len = sizeof(struct in6_addr) },\n+ };\n+\n+ struct nlattr *attrs[ARRAY_SIZE(policy)];\n+ parsed = nl_policy_parse(buf, ofs, policy, attrs, ARRAY_SIZE(policy));\n+\n+ if (!parsed) {\n+ VLOG_DBG_RL(&rl, \"received unparseable rtnetlink nexthop message\");\n+ return 0;\n+ }\n+\n+ if (!nl_attr_get_flag(attrs[NHA_FDB])) {\n+ return 0;\n+ }\n+\n+ const struct nexthop_grp *grps = NULL;\n+ struct in6_addr addr = in6addr_any;\n+ size_t n_grps = 0;\n+\n+ if (attrs[NHA_GATEWAY]) {\n+ size_t nda_dst_size = nl_attr_get_size(attrs[NHA_GATEWAY]);\n+\n+ switch (nda_dst_size) {\n+ case sizeof(uint32_t):\n+ in6_addr_set_mapped_ipv4(&addr,\n+ nl_attr_get_be32(attrs[NHA_GATEWAY]));\n+ break;\n+ case sizeof(struct in6_addr):\n+ addr = nl_attr_get_in6_addr(attrs[NHA_GATEWAY]);\n+ break;\n+ default:\n+ VLOG_DBG_RL(&rl,\n+ \"nexthop message contains non-IPv4/IPv6 NHA_GATEWAY\");\n+ return 0;\n+ }\n+ } else if (attrs[NHA_GROUP]) {\n+ n_grps = nl_attr_get_size(attrs[NHA_GROUP]) / sizeof *grps;\n+ grps = nl_attr_get(attrs[NHA_GROUP]);\n+ } else {\n+ VLOG_DBG_RL(&rl, \"missing group or gateway nexthop attribute\");\n+ return 0;\n+ }\n+\n+ size_t grp_size = n_grps * sizeof(struct nexthop_grp_entry);\n+ change->nlmsg_type = nlmsg->nlmsg_type;\n+ change->nhe = xmalloc(sizeof *change->nhe + grp_size);\n+ *change->nhe = (struct nexthop_entry) {\n+ .id = nl_attr_get_u32(attrs[NHA_ID]),\n+ .addr = addr,\n+ .n_grps = n_grps,\n+ };\n+\n+ for (size_t i = 0; i < n_grps; i++) {\n+ const struct nexthop_grp *grp = &grps[i];\n+ change->nhe->grps[i] = (struct nexthop_grp_entry) {\n+ .id = grp->id,\n+ .weight = nexthop_grp_weight(grp),\n+ /* We need to parse all entries first\n+ * before adjusting the references. */\n+ .gateway = NULL,\n+ };\n+ }\n+\n+ /* Success. */\n+ return RTNLGRP_NEXTHOP;\n+}\n+\n+static struct nexthop_entry *\n+nexthop_find(struct hmap *nexthops, uint32_t id)\n+{\n+ struct nexthop_entry *nhe;\n+ HMAP_FOR_EACH_WITH_HASH (nhe, hmap_node, id, nexthops) {\n+ if (nhe->id == id) {\n+ return nhe;\n+ }\n+ }\n+\n+ return NULL;\n+}\n+\n+static void\n+nh_populate_grp_pointers(struct nexthop_entry *nhe, struct hmap *nexthops)\n+{\n+ for (size_t i = 0; i < nhe->n_grps; i++) {\n+ struct nexthop_grp_entry *grp = &nhe->grps[i];\n+ grp->gateway = nexthop_find(nexthops, grp->id);\n+ }\n+}\ndiff --git a/controller/nexthop-exchange.h b/controller/nexthop-exchange.h\nnew file mode 100644\nindex 000000000..94f4e9bf2\n--- /dev/null\n+++ b/controller/nexthop-exchange.h\n@@ -0,0 +1,61 @@\n+/* Copyright (c) 2026, Red Hat, Inc.\n+ *\n+ * Licensed under the Apache License, Version 2.0 (the \"License\");\n+ * you may not use this file except in compliance with the License.\n+ * You may obtain a copy of the License at:\n+ *\n+ * http://www.apache.org/licenses/LICENSE-2.0\n+ *\n+ * Unless required by applicable law or agreed to in writing, software\n+ * distributed under the License is distributed on an \"AS IS\" BASIS,\n+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+ * See the License for the specific language governing permissions and\n+ * limitations under the License.\n+ */\n+\n+#ifndef NEXTHOP_EXCHANGE_H\n+#define NEXTHOP_EXCHANGE_H 1\n+\n+#include <netinet/in.h>\n+#include <stdint.h>\n+\n+#include \"openvswitch/hmap.h\"\n+\n+struct ds;\n+struct ofpbuf;\n+\n+struct nexthop_grp_entry {\n+ /* The id of the nexthop gateway. */\n+ uint32_t id;\n+ /* The weight of the entry. */\n+ uint16_t weight;\n+ /* The pointer to the gateway entry. */\n+ struct nexthop_entry *gateway;\n+};\n+\n+struct nexthop_entry {\n+ struct hmap_node hmap_node;\n+ /* The id of the nexthop. */\n+ uint32_t id;\n+ /* Nexthop IP address, zeroed in case of group entry. */\n+ struct in6_addr addr;\n+ /* Number of group entries, \"0\" in case gateway entry. */\n+ size_t n_grps;\n+ /* Array of group entries. */\n+ struct nexthop_grp_entry grps[];\n+};\n+\n+/* A digested version of a nexthop message sent down by the kernel to indicate\n+ * that a nexthop entry has changed. */\n+struct nh_table_msg {\n+ /* E.g. RTM_NEWNEXTHOP, RTM_DELNEXTHOP. */\n+ uint16_t nlmsg_type;\n+ /* The inner entry. */\n+ struct nexthop_entry *nhe;\n+};\n+\n+void nexthops_sync(struct hmap *);\n+void nexthop_entry_format(struct ds *ds, const struct nexthop_entry *nhe);\n+int nh_table_parse(struct ofpbuf *, struct nh_table_msg *change);\n+\n+#endif /* NEXTHOP_EXCHANGE_H */\ndiff --git a/controller/ovn-netlink-notifier.c b/controller/ovn-netlink-notifier.c\nindex defa1cd54..7d44f8765 100644\n--- a/controller/ovn-netlink-notifier.c\n+++ b/controller/ovn-netlink-notifier.c\n@@ -19,6 +19,7 @@\n #include <net/if.h>\n \n #include \"neighbor-exchange-netlink.h\"\n+#include \"nexthop-exchange.h\"\n #include \"netlink-notifier.h\"\n #include \"route-exchange-netlink.h\"\n #include \"route-table.h\"\n@@ -48,11 +49,14 @@ struct ovn_netlink_notifier {\n union ovn_notifier_msg_change {\n struct route_table_msg route;\n struct ne_table_msg neighbor;\n+ struct nh_table_msg nexthop;\n };\n \n static void ovn_netlink_route_change_handler(const void *change_, void *aux);\n static void ovn_netlink_neighbor_change_handler(const void *change_,\n void *aux);\n+static void ovn_netlink_nexthop_change_handler(const void *change_,\n+ void *aux);\n \n static struct ovn_netlink_notifier notifiers[OVN_NL_NOTIFIER_MAX] = {\n [OVN_NL_NOTIFIER_ROUTE_V4] = {\n@@ -73,6 +77,12 @@ static struct ovn_netlink_notifier notifiers[OVN_NL_NOTIFIER_MAX] = {\n .change_handler = ovn_netlink_neighbor_change_handler,\n .name = \"neighbor\",\n },\n+ [OVN_NL_NOTIFIER_NEXTHOP] = {\n+ .group = RTNLGRP_NEXTHOP,\n+ .msgs = VECTOR_EMPTY_INITIALIZER(struct nh_table_msg),\n+ .change_handler = ovn_netlink_nexthop_change_handler,\n+ .name = \"nexthop\",\n+ },\n };\n \n static struct nln *nln_handle;\n@@ -97,6 +107,11 @@ ovn_netlink_notifier_parse(struct ofpbuf *buf, void *change_)\n return ne_table_parse(buf, &change->neighbor);\n }\n \n+ if (nlmsg->nlmsg_type == RTM_NEWNEXTHOP ||\n+ nlmsg->nlmsg_type == RTM_DELNEXTHOP) {\n+ return nh_table_parse(buf, &change->nexthop);\n+ }\n+\n return 0;\n }\n \n@@ -136,6 +151,18 @@ ovn_netlink_neighbor_change_handler(const void *change_, void *aux)\n }\n }\n \n+static void\n+ovn_netlink_nexthop_change_handler(const void *change_, void *aux)\n+{\n+ if (!change_) {\n+ return;\n+ }\n+\n+ struct ovn_netlink_notifier *notifier = aux;\n+ const union ovn_notifier_msg_change *change = change_;\n+ vector_push(¬ifier->msgs, &change->nexthop);\n+}\n+\n static void\n ovn_netlink_register_notifier(enum ovn_netlink_notifier_type type)\n {\n@@ -214,6 +241,22 @@ ovn_netlink_notifier_flush(enum ovn_netlink_notifier_type type)\n {\n ovs_assert(type < OVN_NL_NOTIFIER_MAX);\n struct ovn_netlink_notifier *notifier = ¬ifiers[type];\n+\n+ switch (type) {\n+ case OVN_NL_NOTIFIER_NEXTHOP: {\n+ struct nh_table_msg *msg;\n+ VECTOR_FOR_EACH_PTR (¬ifier->msgs, msg) {\n+ free(msg->nhe);\n+ }\n+ break;\n+ }\n+ case OVN_NL_NOTIFIER_ROUTE_V4:\n+ case OVN_NL_NOTIFIER_ROUTE_V6:\n+ case OVN_NL_NOTIFIER_NEIGHBOR:\n+ case OVN_NL_NOTIFIER_MAX:\n+ break;\n+ }\n+\n vector_clear(¬ifier->msgs);\n }\n \ndiff --git a/controller/ovn-netlink-notifier.h b/controller/ovn-netlink-notifier.h\nindex b78fe466b..208a28d99 100644\n--- a/controller/ovn-netlink-notifier.h\n+++ b/controller/ovn-netlink-notifier.h\n@@ -24,6 +24,7 @@ enum ovn_netlink_notifier_type {\n OVN_NL_NOTIFIER_ROUTE_V4,\n OVN_NL_NOTIFIER_ROUTE_V6,\n OVN_NL_NOTIFIER_NEIGHBOR,\n+ OVN_NL_NOTIFIER_NEXTHOP,\n OVN_NL_NOTIFIER_MAX,\n };\n \ndiff --git a/m4/ovn.m4 b/m4/ovn.m4\nindex 8ccc1629e..93a959224 100644\n--- a/m4/ovn.m4\n+++ b/m4/ovn.m4\n@@ -585,3 +585,19 @@ AC_DEFUN([OVS_CHECK_LINUX_NETLINK], [\n [AC_DEFINE([HAVE_NLA_BITFIELD32], [1],\n [Define to 1 if struct nla_bitfield32 is available.])])\n ])\n+\n+dnl OVN_CHECK_LINUX_NEXTHOP_WEIGHT\n+dnl\n+dnl Configure Linux netlink nexthop compat.\n+AC_DEFUN([OVN_CHECK_LINUX_NEXTHOP_WEIGHT], [\n+ save_CFLAGS=\"$CFLAGS\"\n+ CFLAGS=\"$CFLAGS -Werror=implicit-function-declaration\"\n+ AC_COMPILE_IFELSE([\n+ AC_LANG_PROGRAM([#include <linux/nexthop.h>], [\n+ struct nexthop_grp grp = { 0 };\n+ nexthop_grp_weight(&grp);\n+ ])],\n+ [AC_DEFINE([HAVE_NH_GRP_WEIGHT], [1],\n+ [Define to 1 if nexthop_grp_weight() is available.])])\n+ CFLAGS=\"$save_CFLAGS\"\n+ ])\ndiff --git a/tests/automake.mk b/tests/automake.mk\nindex 75a4b00d7..46be217ae 100644\n--- a/tests/automake.mk\n+++ b/tests/automake.mk\n@@ -303,6 +303,8 @@ tests_ovstest_SOURCES += \\\n \tcontroller/host-if-monitor.h \\\n \tcontroller/neighbor-exchange-netlink.c \\\n \tcontroller/neighbor-exchange-netlink.h \\\n+\tcontroller/nexthop-exchange.c \\\n+\tcontroller/nexthop-exchange.h \\\n \tcontroller/neighbor.c \\\n \tcontroller/neighbor.h \\\n \tcontroller/ovn-netlink-notifier.c \\\ndiff --git a/tests/system-ovn-netlink.at b/tests/system-ovn-netlink.at\nindex 26382f087..e5a663921 100644\n--- a/tests/system-ovn-netlink.at\n+++ b/tests/system-ovn-netlink.at\n@@ -537,3 +537,120 @@ AT_CHECK_UNQUOTED([ovstest test-ovn-netlink route-table-notify \\\n ])\n \n AT_CLEANUP\n+\n+AT_SETUP([sync netlink nexthops - learn nexthops])\n+AT_KEYWORDS([netlink-nexthops])\n+\n+check ip nexthop add id 1 via 192.168.1.1 fdb\n+on_exit 'ip nexthop del id 1'\n+check ip nexthop add id 2 via 192.168.1.2 fdb\n+on_exit 'ip nexthop del id 2'\n+check ip nexthop add id 5 via 192.168.1.5 fdb\n+on_exit 'ip nexthop del id 5'\n+check ip nexthop add id 10 group 1/2 fdb\n+on_exit 'ip nexthop del id 10'\n+check ip nexthop add id 11 group 1 fdb\n+on_exit 'ip nexthop del id 11'\n+check ip nexthop add id 3 group 5 fdb\n+on_exit 'ip nexthop del id 3'\n+check ip nexthop add id 12 group 1,100/2,200 fdb\n+on_exit 'ip nexthop del id 12'\n+\n+AT_CHECK([ovstest test-ovn-netlink nexthop-sync | sort], [0], [dnl\n+Nexthop id=1, address=192.168.1.1\n+Nexthop id=10, group=[[1;192.168.1.1;1, 2;192.168.1.2;1]]\n+Nexthop id=11, group=[[1;192.168.1.1;1]]\n+Nexthop id=12, group=[[1;192.168.1.1;100, 2;192.168.1.2;200]]\n+Nexthop id=2, address=192.168.1.2\n+Nexthop id=3, group=[[5;192.168.1.5;1]]\n+Nexthop id=5, address=192.168.1.5\n+])\n+\n+AT_CLEANUP\n+\n+AT_SETUP([sync netlink nexthops - table notify])\n+AT_KEYWORDS([netlink-nexthops])\n+\n+ADD_NAMESPACES(nh)\n+NS_EXEC([nh], [ip link set up lo])\n+\n+dnl Should notify if an IPv4 nexthop is added.\n+NS_CHECK_EXEC([nh], [ovstest test-ovn-netlink nexthop-table-notify \\\n+ \"ip nexthop add id 1 via 10.0.0.1 fdb\"], [0], [dnl\n+Add nexthop id=1, address=10.0.0.1\n+])\n+\n+dnl Should notify if an IPv6 nexthop is added.\n+NS_CHECK_EXEC([nh], [ovstest test-ovn-netlink nexthop-table-notify \\\n+ \"ip nexthop add id 2 via fd20::2 fdb\"], [0], [dnl\n+Add nexthop id=2, address=fd20::2\n+])\n+\n+dnl Should notify if IPv4 nexthop group is added.\n+NS_CHECK_EXEC([nh], [ovstest test-ovn-netlink nexthop-table-notify \\\n+ \"ip nexthop add id 10 group 1 fdb\"], [0], [dnl\n+Add nexthop id=10, group=[[1;1]]\n+])\n+\n+dnl Should notify if IPv6 nexthop group is added.\n+NS_CHECK_EXEC([nh], [ovstest test-ovn-netlink nexthop-table-notify \\\n+ \"ip nexthop add id 11 group 2 fdb\"], [0], [dnl\n+Add nexthop id=11, group=[[2;1]]\n+])\n+\n+dnl Should notify if IPv4 nexthop group is removed.\n+NS_CHECK_EXEC([nh], [ovstest test-ovn-netlink nexthop-table-notify \\\n+ \"ip nexthop del id 10\"], [0], [dnl\n+Delete nexthop id=10, group=[[1;1]]\n+])\n+\n+dnl Should notify if IPv6 nexthop group is removed.\n+NS_CHECK_EXEC([nh], [ovstest test-ovn-netlink nexthop-table-notify \\\n+ \"ip nexthop del id 11\"], [0], [dnl\n+Delete nexthop id=11, group=[[2;1]]\n+])\n+\n+NS_EXEC([nh], [ip nexthop add id 3 via 10.0.0.3 fdb])\n+NS_EXEC([nh], [ip nexthop add id 10 group 1/3 fdb])\n+NS_EXEC([nh], [ip nexthop add id 4 via fd20::4 fdb])\n+NS_EXEC([nh], [ip nexthop add id 11 group 2/4 fdb])\n+\n+dnl Should notify if an IPv4 nexthop group is removed and group modified.\n+NS_CHECK_EXEC([nh], [ovstest test-ovn-netlink nexthop-table-notify \\\n+ \"ip nexthop del id 3\" | sort], [0], [dnl\n+Add nexthop id=10, group=[[1;1]]\n+Delete nexthop id=3, address=10.0.0.3\n+])\n+\n+dnl Should notify if an IPv6 nexthop group is removed and group modified.\n+NS_CHECK_EXEC([nh], [ovstest test-ovn-netlink nexthop-table-notify \\\n+ \"ip nexthop del id 4\" | sort], [0], [dnl\n+Add nexthop id=11, group=[[2;1]]\n+Delete nexthop id=4, address=fd20::4\n+])\n+\n+dnl Should notify if an IPv4 nexthop group is removed and group modified.\n+NS_CHECK_EXEC([nh], [ovstest test-ovn-netlink nexthop-table-notify \\\n+ \"ip nexthop del id 1\" | sort], [0], [dnl\n+Delete nexthop id=1, address=10.0.0.1\n+Delete nexthop id=10, group=[[1;1]]\n+])\n+\n+dnl Should notify if an IPv6 nexthop group is removed and group modified.\n+NS_CHECK_EXEC([nh], [ovstest test-ovn-netlink nexthop-table-notify \\\n+ \"ip nexthop del id 2\" | sort], [0], [dnl\n+Delete nexthop id=11, group=[[2;1]]\n+Delete nexthop id=2, address=fd20::2\n+])\n+\n+dnl Should NOT notify if a blackhole nexthop is added.\n+NS_CHECK_EXEC([nh], [ovstest test-ovn-netlink nexthop-table-notify \\\n+ \"ip nexthop add id 1 blackhole\"], [0], [dnl\n+])\n+\n+dnl Should NOT notify if non-FDB nexthop is added.\n+NS_CHECK_EXEC([nh], [ovstest test-ovn-netlink nexthop-table-notify \\\n+ \"ip nexthop add id 2 via 127.0.0.2 dev lo\"], [0], [dnl\n+])\n+\n+AT_CLEANUP\ndiff --git a/tests/test-ovn-netlink.c b/tests/test-ovn-netlink.c\nindex 555b2df99..783e31340 100644\n--- a/tests/test-ovn-netlink.c\n+++ b/tests/test-ovn-netlink.c\n@@ -23,6 +23,7 @@\n \n #include \"controller/host-if-monitor.h\"\n #include \"controller/neighbor-exchange-netlink.h\"\n+#include \"controller/nexthop-exchange.h\"\n #include \"controller/ovn-netlink-notifier.h\"\n #include \"controller/neighbor.h\"\n #include \"controller/route.h\"\n@@ -279,6 +280,59 @@ test_route_table_notify(struct ovs_cmdl_context *ctx)\n ovn_netlink_notifiers_destroy();\n }\n \n+static void\n+test_nexthop_sync(struct ovs_cmdl_context *ctx OVS_UNUSED)\n+{\n+ struct hmap nexthops = HMAP_INITIALIZER(&nexthops);\n+ struct ds ds = DS_EMPTY_INITIALIZER;\n+\n+ nexthops_sync(&nexthops);\n+\n+ struct nexthop_entry *nhe;\n+ HMAP_FOR_EACH (nhe, hmap_node, &nexthops) {\n+ ds_clear(&ds);\n+ nexthop_entry_format(&ds, nhe);\n+ printf(\"Nexthop %s\\n\", ds_cstr(&ds));\n+ }\n+\n+ HMAP_FOR_EACH_POP (nhe, hmap_node, &nexthops) {\n+ free(nhe);\n+ }\n+\n+ ds_destroy(&ds);\n+ hmap_destroy(&nexthops);\n+}\n+\n+static void\n+test_nexthop_table_notify(struct ovs_cmdl_context *ctx)\n+{\n+ unsigned int shift = 1;\n+\n+ const char *cmd = test_read_value(ctx, shift++, \"shell_command\");\n+ if (!cmd) {\n+ return;\n+ }\n+\n+ ovn_netlink_update_notifier(OVN_NL_NOTIFIER_NEXTHOP, true);\n+ run_command_under_notifier(cmd);\n+\n+ struct vector *msgs = ovn_netlink_get_msgs(OVN_NL_NOTIFIER_NEXTHOP);\n+\n+ struct ds ds = DS_EMPTY_INITIALIZER;\n+\n+ struct nh_table_msg *msg;\n+ VECTOR_FOR_EACH_PTR (msgs, msg) {\n+ ds_clear(&ds);\n+ nexthop_entry_format(&ds, msg->nhe);\n+ printf(\"%s nexthop %s\\n\",\n+ msg->nlmsg_type == RTM_NEWNEXTHOP ? \"Add\" : \"Delete\",\n+ ds_cstr(&ds));\n+ }\n+\n+ ds_destroy(&ds);\n+ ovn_netlink_notifiers_destroy();\n+}\n+\n static void\n test_ovn_netlink(int argc, char *argv[])\n {\n@@ -291,6 +345,9 @@ test_ovn_netlink(int argc, char *argv[])\n {\"route-sync\", NULL, 1, INT_MAX, test_route_sync, OVS_RO},\n {\"route-table-notify\", NULL, 1, 1,\n test_route_table_notify, OVS_RO},\n+ {\"nexthop-sync\", NULL, 0, 0, test_nexthop_sync, OVS_RO},\n+ {\"nexthop-table-notify\", NULL, 1, 1,\n+ test_nexthop_table_notify, OVS_RO},\n {NULL, NULL, 0, 0, NULL, OVS_RO},\n };\n struct ovs_cmdl_context ctx;\n", "prefixes": [ "ovs-dev", "6/6" ] }