{"id":2218981,"url":"http://patchwork.ozlabs.org/api/patches/2218981/?format=json","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=json","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=json","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=json","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(&notifier->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 = &notifiers[type];\n+\n+    switch (type) {\n+    case OVN_NL_NOTIFIER_NEXTHOP: {\n+        struct nh_table_msg *msg;\n+        VECTOR_FOR_EACH_PTR (&notifier->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(&notifier->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"]}