On 8/12/25 4:56 PM, Ales Musil via dev wrote:
> From: Dumitru Ceara <dce...@redhat.com>
> 
> This implements an interface for maintaining (Linux) neighbor entries
> through Netlink.  Netlink was chosen for the same reason it was selected
> for maintaining (Linux) route tables for control planes managed by OVN
> in 0ed52a4d5aaf ("controller: Introduce route-exchange-netlink.").
> 
> Co-Authored-by: Ales Musil <amu...@redhat.com>
> Signed-off-by: Ales Musil <amu...@redhat.com>
> Signed-off-by: Dumitru Ceara <dce...@redhat.com>
> ---
> V3:
> - Addressed Xavier's comment.
> - Moved code that sets NLM_F_REPLACE in patch 1.
> - move change to ignore VLAN 0 and dst port 0 to this patch
> - fixed bug in ne_table_parse__() - missing ntohs() for port
> - added tests for the neighbor-exchange-netlink module
> 
> Changes in V2:
> - removed the ne_nl_neigh_filter
> - fixed memory leak of announced_neighbors hmap.
> ---
>  controller/automake.mk                 |   4 +
>  controller/neighbor-exchange-netlink.c | 470 +++++++++++++++++++++++++
>  controller/neighbor-exchange-netlink.h |  60 ++++
>  controller/neighbor.c                  |  77 ++++
>  controller/neighbor.h                  |  75 ++++
>  controller/test-ovn-netlink.c          | 120 +++++++
>  tests/automake.mk                      |  13 +-
>  tests/ovn-macros.at                    |   4 +
>  tests/system-common-macros.at          |  16 +
>  tests/system-dpdk-testsuite.at         |   1 +
>  tests/system-kmod-testsuite.at         |   1 +
>  tests/system-ovn-netlink.at            | 178 ++++++++++
>  tests/system-userspace-testsuite.at    |   1 +
>  tests/test-utils.c                     |  36 ++
>  tests/test-utils.h                     |  10 +-
>  15 files changed, 1063 insertions(+), 3 deletions(-)
>  create mode 100644 controller/neighbor-exchange-netlink.c
>  create mode 100644 controller/neighbor-exchange-netlink.h
>  create mode 100644 controller/neighbor.c
>  create mode 100644 controller/neighbor.h
>  create mode 100644 controller/test-ovn-netlink.c
>  create mode 100644 tests/system-ovn-netlink.at
> 
> diff --git a/controller/automake.mk b/controller/automake.mk
> index f0638ea97..3eb45475c 100644
> --- a/controller/automake.mk
> +++ b/controller/automake.mk
> @@ -30,6 +30,8 @@ controller_ovn_controller_SOURCES = \
>       controller/ofctrl.h \
>       controller/ofctrl-seqno.c \
>       controller/ofctrl-seqno.h \
> +     controller/neighbor.c \
> +     controller/neighbor.h \
>       controller/pinctrl.c \
>       controller/pinctrl.h \
>       controller/patch.c \
> @@ -65,6 +67,8 @@ controller_ovn_controller_SOURCES = \
>  
>  if HAVE_NETLINK
>  controller_ovn_controller_SOURCES += \
> +     controller/neighbor-exchange-netlink.h \
> +     controller/neighbor-exchange-netlink.c \
>       controller/route-exchange-netlink.h \
>       controller/route-exchange-netlink.c \
>       controller/route-exchange.c \
> diff --git a/controller/neighbor-exchange-netlink.c 
> b/controller/neighbor-exchange-netlink.c
> new file mode 100644
> index 000000000..a4e9cf006
> --- /dev/null
> +++ b/controller/neighbor-exchange-netlink.c
> @@ -0,0 +1,470 @@
> +/* Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#include <config.h>
> +#include <stdbool.h>
> +#include <linux/if_ether.h>
> +#include <linux/rtnetlink.h>
> +
> +#include "hmapx.h"
> +#include "lib/netlink.h"
> +#include "lib/netlink-socket.h"
> +#include "lib/packets.h"
> +#include "openvswitch/vlog.h"
> +
> +#include "neighbor-exchange-netlink.h"
> +#include "neighbor.h"
> +
> +VLOG_DEFINE_THIS_MODULE(neighbor_exchange_netlink);
> +
> +#define NETNL_REQ_BUFFER_SIZE 128
> +
> +static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
> +
> +/* Inspired from route_table_dump_one_table() in OVS. */
> +typedef void ne_table_handle_msg_callback(const struct ne_table_msg *,
> +                                          void *aux);
> +static bool ne_table_dump_one_ifindex(unsigned char address_family,
> +                                      int32_t if_index,
> +                                      ne_table_handle_msg_callback *,
> +                                      void *aux);
> +struct ne_msg_handle_data {
> +    /* Stores 'struct advertise_neighbor_entry'. */
> +    struct hmapx *neighbors_to_advertise;
> +
> +    /* Stores 'struct ne_nl_received_neigh'. */
> +    struct vector *learned_neighbors;
> +
> +    /* Stores 'struct advertise_neighbor_entry'. */
> +    const struct hmap *neighbors;
> +
> +    /* Non-zero error code if any netlink operation failed. */
> +    int ret;
> +};
> +
> +static void handle_ne_msg(const struct ne_table_msg *, void *data);
> +
> +static int ne_table_parse__(struct ofpbuf *, size_t ofs,
> +                            const struct nlmsghdr *,
> +                            const struct ndmsg *,
> +                            struct ne_table_msg *);
> +static int ne_nl_add_neigh(int32_t if_index, uint8_t family,
> +                           uint16_t state, uint8_t flags,
> +                           const struct eth_addr *,
> +                           const struct in6_addr *,
> +                           uint16_t port, uint16_t vlan);
> +static int ne_nl_del_neigh(int32_t if_index, uint8_t family,
> +                           const struct eth_addr *,
> +                           const struct in6_addr *,
> +                           uint16_t port, uint16_t vlan);
> +
> +/* Inserts all neigh entries listed in 'neighbors' (of type
> + * 'struct advertise_neighbor_entry') in the table associated to
> + * 'if_index'.  Populates 'learned_neighbors' with all neigh entries
> + * (struct ne_nl_received_neigh) that exist in the table associated to
> + * 'if_index'.
> + *
> + *

nit: One empty line should be enough.

> + * Returns 0 on success, errno on failure. */
> +int
> +ne_nl_sync_neigh(uint8_t family, int32_t if_index,
> +                 const struct hmap *neighbors,
> +                 struct vector *learned_neighbors)
> +{
> +    struct hmapx neighbors_to_advertise =
> +        HMAPX_INITIALIZER(&neighbors_to_advertise);
> +    struct advertise_neighbor_entry *an;
> +    int ret;
> +
> +    HMAP_FOR_EACH (an, node, neighbors) {
> +        hmapx_add(&neighbors_to_advertise, an);
> +    }
> +
> +    struct ne_msg_handle_data data = {
> +        .neighbors = neighbors,
> +        .neighbors_to_advertise = &neighbors_to_advertise,
> +        .learned_neighbors = learned_neighbors,
> +    };
> +    ne_table_dump_one_ifindex(family, if_index, handle_ne_msg, &data);
> +    ret = data.ret;
> +
> +    /* Add any remaining neighbors in the neighbors_to_advertise hmapx to the
> +     * system table. */
> +    struct hmapx_node *hn;
> +    HMAPX_FOR_EACH (hn, &neighbors_to_advertise) {
> +        an = hn->data;
> +        int err = ne_nl_add_neigh(if_index, family,
> +                                  NUD_NOARP,        /* state = static */
> +                                  0,                /* flags */
> +                                  &an->lladdr, &an->addr,
> +                                  0,                /* port */
> +                                  0);               /* vlan */
> +        if (err) {
> +            char addr_s[INET6_ADDRSTRLEN + 1];
> +            VLOG_WARN_RL(&rl, "Add neigh ifindex=%"PRId32
> +                              " eth=" ETH_ADDR_FMT " dst=%s"
> +                              " failed: %s",
> +                         if_index, ETH_ADDR_ARGS(an->lladdr),
> +                         ipv6_string_mapped(
> +                             addr_s, &an->addr) ? addr_s : "(invalid)",
> +                         ovs_strerror(err));
> +            if (!ret) {
> +                /* Report the first error value to the caller. */
> +                ret = err;
> +            }
> +        }
> +    }
> +    hmapx_destroy(&neighbors_to_advertise);
> +    return ret;
> +}
> +
> +/* OVN expects all static entries added on this ifindex to be OVN-owned.
> + * Everything else must be learnt. */
> +bool
> +ne_is_ovn_owned(const struct ne_nl_received_neigh *nd)
> +{
> +    return !(nd->state & NUD_PERMANENT) && (nd->state & NUD_NOARP)
> +           && !(nd->flags & NTF_EXT_LEARNED);

The NTF_EXT_LEARNED was introduced in Linux v3.19, may want to add an
ifndef-define for it somewhere at the top.

> +}
> +
> +static bool
> +ne_table_dump_one_ifindex(unsigned char address_family, int32_t if_index,
> +                          ne_table_handle_msg_callback *handle_msg_cb,
> +                          void *aux)
> +{
> +    uint64_t reply_stub[NL_DUMP_BUFSIZE / 8];
> +    struct ofpbuf request, reply, buf;
> +    struct ndmsg *rq_msg;
> +    bool filtered = true;
> +    struct nl_dump dump;
> +
> +    uint8_t request_stub[NETNL_REQ_BUFFER_SIZE];
> +    ofpbuf_use_stub(&request, request_stub, sizeof(request_stub));

nit: Don't parenthesize the argument of the sizeof.

> +
> +    nl_msg_put_nlmsghdr(&request, sizeof *rq_msg, RTM_GETNEIGH, 
> NLM_F_REQUEST);
> +    rq_msg = ofpbuf_put_zeros(&request, sizeof *rq_msg);
> +    rq_msg->ndm_family = address_family;
> +    if (if_index) {
> +        nl_msg_put_u32(&request, NDA_IFINDEX, if_index);
> +    }
> +
> +    nl_dump_start(&dump, NETLINK_ROUTE, &request);
> +    ofpbuf_uninit(&request);
> +
> +    ofpbuf_use_stub(&buf, reply_stub, sizeof reply_stub);
> +    while (nl_dump_next(&dump, &reply, &buf)) {
> +        struct ne_table_msg msg;
> +
> +        if (ne_table_parse(&reply, &msg)) {
> +            struct nlmsghdr *nlmsghdr = nl_msg_nlmsghdr(&reply);
> +
> +            /* Older kernels do not support filtering. */
> +            if (!(nlmsghdr->nlmsg_flags & NLM_F_DUMP_FILTERED)) {
> +                filtered = false;

If we got an unfiltered dump, we need to ignore this message, unless the
ifindex of the entry matches our current dump criteria.  If we handle this
message, the callback will delete all the entries that are not in the
current hash map of advertised neighbors, i.e. it will delete all the
neighbors for all other ports.  And it will also learn unrelated neighbours
that are not OVN-owned.

> +            }
> +            handle_msg_cb(&msg, aux);

The handle_msg_cb() may attempt to delete the entry from the kernel.
Neltink dumps are not very relaiable, i.e. entries can be dumped twice
or missed, in case the kernel tables are modified during the dump, but
this call actually makes it far more likely.  I don't think it's a good
idea in general to modify the table while dumping it, unless you're OK
with missed/duplicated entries.

It may be OK, the data will be re-synced next time, but the callers do not
check the result of ne_nl_sync_neigh(), so failures may stay unnoticed for
some time.  This may also cause random failures of the test that is the
only one that checks the return value.

> +        }
> +    }
> +    ofpbuf_uninit(&buf);
> +    nl_dump_done(&dump);
> +
> +    return filtered;
> +}
> +
> +static int
> +ne_table_parse__(struct ofpbuf *buf, size_t ofs, const struct nlmsghdr 
> *nlmsg,
> +                 const struct ndmsg *nd, struct ne_table_msg *change)
> +{
> +    bool parsed;
> +
> +    static const struct nl_policy policy[] = {
> +        [NDA_DST] = { .type = NL_A_U32, .optional = true  },

nit: Extra space at the end.

> +        [NDA_LLADDR] = { .type = NL_A_LL_ADDR, .optional = true },
> +        [NDA_PORT] = { .type = NL_A_U16, .optional = true },
> +    };
> +
> +    static const struct nl_policy policy6[] = {
> +        [NDA_DST] = { .type = NL_A_IPV6, .optional = true  },

Same.

> +        [NDA_LLADDR] = { .type = NL_A_LL_ADDR, .optional = true },
> +        [NDA_PORT] = { .type = NL_A_U16, .optional = true },
> +    };
> +
> +    static const struct nl_policy policy_bridge[] = {
> +        [NDA_DST] = { .type = NL_A_UNSPEC, .optional = true,
> +                      .min_len = sizeof(struct in_addr),
> +                      .max_len = sizeof(struct in6_addr)},
> +        [NDA_LLADDR] = { .type = NL_A_LL_ADDR, .optional = true },
> +        [NDA_PORT] = { .type = NL_A_U16, .optional = true },
> +        [NDA_VLAN] = { .type = NL_A_U16, .optional = true },
> +    };
> +
> +    BUILD_ASSERT(ARRAY_SIZE(policy) == ARRAY_SIZE(policy6));
> +    BUILD_ASSERT(ARRAY_SIZE(policy) == ARRAY_SIZE(policy_bridge));
> +    struct nlattr *attrs[ARRAY_SIZE(policy)];
> +
> +    if (nd->ndm_family == AF_INET) {
> +        parsed = nl_policy_parse(buf, ofs, policy, attrs,
> +                                 ARRAY_SIZE(policy));
> +    } else if (nd->ndm_family == AF_INET6) {
> +        parsed = nl_policy_parse(buf, ofs, policy6, attrs,
> +                                 ARRAY_SIZE(policy6));
> +    } else if (nd->ndm_family == AF_BRIDGE) {
> +        parsed = nl_policy_parse(buf, ofs, policy_bridge, attrs,
> +                                 ARRAY_SIZE(policy_bridge));
> +    } else {
> +        VLOG_WARN_RL(&rl, "received non AF_INET/AF_INET6/AF_BRIDGE rtnetlink 
> "
> +                          "neigh message");
> +        return 0;
> +    }
> +
> +    if (parsed) {
> +        *change = (struct ne_table_msg) {
> +            .nlmsg_type = nlmsg->nlmsg_type,
> +            .nd.if_index = nd->ndm_ifindex,
> +            .nd.family = nd->ndm_family,
> +            .nd.state = nd->ndm_state,
> +            .nd.flags = nd->ndm_flags,
> +            .nd.type = nd->ndm_type,
> +        };
> +
> +        if (attrs[NDA_DST]) {
> +            size_t nda_dst_size = nl_attr_get_size(attrs[NDA_DST]);
> +
> +            switch (nda_dst_size) {
> +            case sizeof(uint32_t):
> +                in6_addr_set_mapped_ipv4(&change->nd.addr,
> +                                         nl_attr_get_be32(attrs[NDA_DST]));
> +                break;
> +            case sizeof(struct in6_addr):
> +                change->nd.addr = nl_attr_get_in6_addr(attrs[NDA_DST]);
> +                break;
> +            default:
> +                VLOG_DBG_RL(&rl,
> +                            "neigh message contains non-IPv4/IPv6 NDA_DST");
> +                return 0;
> +            }
> +        }
> +
> +        if (attrs[NDA_LLADDR]) {
> +            if (nl_attr_get_size(attrs[NDA_LLADDR]) != ETH_ALEN) {
> +                VLOG_DBG_RL(&rl, "neigh message contains non-ETH 
> NDA_LLADDR");
> +                return 0;
> +            }
> +            change->nd.lladdr = nl_attr_get_eth_addr(attrs[NDA_LLADDR]);
> +        }
> +
> +        if (attrs[NDA_PORT]) {
> +            change->nd.port = ntohs(nl_attr_get_be16(attrs[NDA_PORT]));
> +        }
> +
> +        if (attrs[NDA_VLAN]) {
> +            change->nd.vlan = nl_attr_get_u16(attrs[NDA_VLAN]);
> +        }
> +    } else {
> +        VLOG_DBG_RL(&rl, "received unparseable rtnetlink neigh message");
> +        return 0;
> +    }
> +
> +    /* Success. */
> +    return RTNLGRP_NEIGH;
> +}
> +
> +static void
> +handle_ne_msg(const struct ne_table_msg *msg, void *data)
> +{
> +    struct ne_msg_handle_data *handle_data = data;
> +    const struct ne_nl_received_neigh *nd = &msg->nd;
> +
> +    /* OVN only manages VLAN 0 entries. */
> +    if (nd->vlan) {
> +        return;
> +    }
> +
> +    if (!ne_is_ovn_owned(nd)) {
> +        if (!handle_data->learned_neighbors) {
> +            return;
> +        }
> +
> +        /* Learn the non-OVN entry. */
> +        vector_push(handle_data->learned_neighbors, nd);
> +        return;
> +    }
> +
> +    /* This neighbor was presumably added by OVN, see if it's still valid.
> +     * OVN only adds neighbors with port set to 0, all others can be
> +     * removed. */
> +    if (!nd->port && handle_data->neighbors_to_advertise) {
> +        struct advertise_neighbor_entry *an =
> +            advertise_neigh_find(handle_data->neighbors, nd->lladdr,
> +                                 &nd->addr);
> +        if (an) {
> +            hmapx_find_and_delete(handle_data->neighbors_to_advertise, an);
> +            return;
> +        }
> +    }
> +
> +    int err = ne_nl_del_neigh(nd->if_index, nd->family,
> +                              &nd->lladdr, &nd->addr,
> +                              nd->port, nd->vlan);
> +    if (err) {
> +        char addr_s[INET6_ADDRSTRLEN + 1];
> +        VLOG_WARN_RL(&rl, "Delete neigh ifindex=%"PRId32" vlan=%"PRIu16
> +                          " eth=" ETH_ADDR_FMT " dst=%s port=%"PRIu16
> +                          " failed: %s",
> +                     nd->if_index, nd->vlan, ETH_ADDR_ARGS(nd->lladdr),
> +                     ipv6_string_mapped(addr_s, &nd->addr)
> +                     ? addr_s : "(invalid)",
> +                     nd->port,
> +                     ovs_strerror(err));
> +
> +        if (!handle_data->ret) {
> +            /* Report the first error value to the caller. */
> +            handle_data->ret = err;
> +        }
> +    }
> +}
> +
> +static int
> +ne_nl_add_neigh(int32_t if_index, uint8_t family,
> +                uint16_t state, uint8_t flags,
> +                const struct eth_addr *lladdr,
> +                const struct in6_addr *addr,
> +                uint16_t port, uint16_t vlan)
> +{
> +    uint32_t nl_flags = NLM_F_REQUEST | NLM_F_ACK |
> +                        NLM_F_CREATE | NLM_F_REPLACE;
> +    bool dst_set = !ipv6_is_zero(addr);
> +    struct ofpbuf request;
> +    uint8_t request_stub[NETNL_REQ_BUFFER_SIZE];
> +    ofpbuf_use_stub(&request, request_stub, sizeof(request_stub));
> +
> +    nl_msg_put_nlmsghdr(&request, 0, RTM_NEWNEIGH, nl_flags);
> +
> +    struct ndmsg *nd = ofpbuf_put_zeros(&request, sizeof *nd);
> +    *nd = (struct ndmsg) {
> +        .ndm_family = family,
> +        .ndm_ifindex = if_index,
> +        .ndm_state = state,
> +        .ndm_flags = flags,
> +    };
> +
> +    nl_msg_put_unspec(&request, NDA_LLADDR, lladdr, sizeof *lladdr);
> +    if (dst_set) {
> +        if (IN6_IS_ADDR_V4MAPPED(addr)) {
> +            nl_msg_put_be32(&request, NDA_DST, 
> in6_addr_get_mapped_ipv4(addr));
> +        } else {
> +            nl_msg_put_in6_addr(&request, NDA_DST, addr);
> +        }
> +    }
> +    if (port) {
> +        nl_msg_put_u16(&request, NDA_PORT, port);
> +    }
> +    if (vlan) {
> +        nl_msg_put_u16(&request, NDA_VLAN, vlan);
> +    }
> +
> +    if (VLOG_IS_DBG_ENABLED()) {
> +        struct ds msg = DS_EMPTY_INITIALIZER;
> +
> +        ds_put_format(&msg, "Adding neighbor ifindex %"PRId32 " for eth "
> +                            ETH_ADDR_FMT " port %"PRIu16" vlan %"PRIu16,
> +                            if_index, ETH_ADDR_ARGS(*lladdr),
> +                            port, vlan);
> +        if (dst_set) {
> +            ipv6_format_mapped(addr, &msg);
> +        }
> +        VLOG_DBG("%s", ds_cstr(&msg));
> +        ds_destroy(&msg);
> +    }
> +
> +    int err = nl_transact(NETLINK_ROUTE, &request, NULL);
> +
> +    ofpbuf_uninit(&request);
> +    return err;
> +}
> +
> +static int
> +ne_nl_del_neigh(int32_t if_index, uint8_t family,
> +                const struct eth_addr *lladdr,
> +                const struct in6_addr *addr,
> +                uint16_t port, uint16_t vlan)
> +{
> +    uint32_t flags = NLM_F_REQUEST | NLM_F_ACK;
> +    bool dst_set = !ipv6_is_zero(addr);
> +    struct ofpbuf request;
> +    uint8_t request_stub[NETNL_REQ_BUFFER_SIZE];
> +    ofpbuf_use_stub(&request, request_stub, sizeof(request_stub));

Parentheses.

> +
> +    nl_msg_put_nlmsghdr(&request, 0, RTM_DELNEIGH, flags);
> +
> +    struct ndmsg *nd = ofpbuf_put_zeros(&request, sizeof *nd);
> +    *nd = (struct ndmsg) {
> +        .ndm_family = family,
> +        .ndm_ifindex = if_index,
> +    };
> +
> +    nl_msg_put_unspec(&request, NDA_LLADDR, lladdr, sizeof *lladdr);
> +    if (dst_set) {
> +        if (IN6_IS_ADDR_V4MAPPED(addr)) {
> +            nl_msg_put_be32(&request, NDA_DST, 
> in6_addr_get_mapped_ipv4(addr));
> +        } else {
> +            nl_msg_put_in6_addr(&request, NDA_DST, addr);
> +        }
> +    }
> +    if (port) {
> +        nl_msg_put_u16(&request, NDA_PORT, port);
> +    }
> +    if (vlan) {
> +        nl_msg_put_u16(&request, NDA_VLAN, vlan);
> +    }
> +
> +    if (VLOG_IS_DBG_ENABLED()) {
> +        struct ds msg = DS_EMPTY_INITIALIZER;
> +
> +        ds_put_format(&msg, "Removing neighbor ifindex %"PRId32 " for eth "
> +                            ETH_ADDR_FMT " port %"PRIu16" vlan %"PRIu16,
> +                            if_index, ETH_ADDR_ARGS(*lladdr),
> +                            port, vlan);
> +        if (dst_set) {
> +            ds_put_char(&msg, ' ');
> +            ipv6_format_mapped(addr, &msg);
> +        }
> +        VLOG_DBG("%s", ds_cstr(&msg));
> +        ds_destroy(&msg);
> +    }
> +
> +    int err = nl_transact(NETLINK_ROUTE, &request, NULL);
> +
> +    ofpbuf_uninit(&request);
> +    return err;
> +}
> +
> +/* Parse Netlink message in buf, which is expected to contain a UAPI ndmsg
> + * header and associated neighbor attributes.
> + *
> + * Return RTNLGRP_NEIGH on success, and 0 on a parse error. */
> +int
> +ne_table_parse(struct ofpbuf *buf, void *change)
> +{
> +    struct nlmsghdr *nlmsg = ofpbuf_at(buf, 0, NLMSG_HDRLEN);
> +    struct ndmsg *nd = ofpbuf_at(buf, NLMSG_HDRLEN, sizeof *nd);
> +
> +    if (!nlmsg || !nd) {
> +        return 0;
> +    }
> +
> +    return ne_table_parse__(buf, NLMSG_HDRLEN + sizeof *nd,
> +                            nlmsg, nd, change);
> +}
> diff --git a/controller/neighbor-exchange-netlink.h 
> b/controller/neighbor-exchange-netlink.h
> new file mode 100644
> index 000000000..e154d1b6e
> --- /dev/null
> +++ b/controller/neighbor-exchange-netlink.h
> @@ -0,0 +1,60 @@
> +/* Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#ifndef NEIGHBOR_EXCHANGE_NETLINK_H
> +#define NEIGHBOR_EXCHANGE_NETLINK_H 1
> +
> +#include <netinet/in.h>
> +#include <stdint.h>
> +
> +#include "openvswitch/hmap.h"
> +#include "openvswitch/ofpbuf.h"
> +
> +#include "vec.h"
> +
> +struct ne_nl_received_neigh {
> +    int32_t if_index;
> +    uint8_t  family;        /* AF_INET/AF_INET6/AF_BRIDGE. */
> +
> +    struct eth_addr lladdr; /* Interface index where the neigh is learnt on. 
> */
> +    struct in6_addr addr;   /* In case of 'dst' entries non-zero;
> +                             * all zero otherwise. */
> +    uint16_t vlan;          /* Parsed from NDA_VLAN. */
> +    uint16_t port;          /* UDP port, e.g., for VXLAN,
> +                             * parsed from NDA_PORT. */
> +    uint16_t state;         /* A value out of NUD_*,
> +                             * from linux/neighbour.h. */
> +    uint8_t  flags;         /* A combination of NTF_* flags,
> +                             * from linux/neighbour.h. */
> +    uint8_t  type;          /* A value out of 'rtm_type' from 
> linux/rtnetlink.h
> +                             * e.g., RTN_UNICAST, RTN_MULTICAST. */
> +};
> +
> +/* A digested version of a neigh message sent down by the kernel to indicate
> + * that a neigh entry has changed. */
> +struct ne_table_msg {
> +    uint16_t nlmsg_type;            /* E.g. RTM_NEWNEIGH, RTM_DELNEIGH. */
> +    struct ne_nl_received_neigh nd; /* Data parsed from this message. */
> +};
> +
> +int ne_nl_sync_neigh(uint8_t family, int32_t if_index,
> +                     const struct hmap *neighbors,
> +                     struct vector *learned_neighbors);
> +
> +bool ne_is_ovn_owned(const struct ne_nl_received_neigh *nd);
> +
> +int ne_table_parse(struct ofpbuf *, void *change);
> +
> +#endif /* NEIGHBOR_EXCHANGE_NETLINK_H */
> diff --git a/controller/neighbor.c b/controller/neighbor.c
> new file mode 100644
> index 000000000..01f6de17c
> --- /dev/null
> +++ b/controller/neighbor.c
> @@ -0,0 +1,77 @@
> +/* Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#include <config.h>
> +
> +#include "lib/hash.h"
> +#include "lib/packets.h"
> +#include "lib/sset.h"
> +
> +#include "neighbor.h"
> +
> +static void neighbor_interface_monitor_destroy(
> +    struct neighbor_interface_monitor *);
> +
> +uint32_t
> +advertise_neigh_hash(const struct eth_addr *eth, const struct in6_addr *ip)
> +{
> +    return hash_bytes(ip, sizeof *ip, hash_bytes(eth, sizeof *eth, 0));
> +}
> +
> +struct advertise_neighbor_entry *
> +advertise_neigh_find(const struct hmap *neighbors, struct eth_addr mac,
> +                     const struct in6_addr *ip)
> +{
> +    uint32_t hash = advertise_neigh_hash(&mac, ip);
> +
> +    struct advertise_neighbor_entry *ne;
> +    HMAP_FOR_EACH_WITH_HASH (ne, node, hash, neighbors) {
> +        if (eth_addr_equals(ne->lladdr, mac) &&
> +            ipv6_addr_equals(&ne->addr, ip)) {
> +            return ne;
> +        }
> +    }
> +
> +    return NULL;
> +}
> +
> +void
> +neighbor_run(struct neighbor_ctx_in *n_ctx_in OVS_UNUSED,
> +             struct neighbor_ctx_out *n_ctx_out OVS_UNUSED)
> +{
> +    /* XXX: Not implemented yet. */
> +}
> +
> +void
> +neighbor_cleanup(struct vector *monitored_interfaces)
> +{
> +    struct neighbor_interface_monitor *nim;
> +    VECTOR_FOR_EACH (monitored_interfaces, nim) {
> +        neighbor_interface_monitor_destroy(nim);
> +    }
> +    vector_clear(monitored_interfaces);
> +}
> +
> +static void
> +neighbor_interface_monitor_destroy(struct neighbor_interface_monitor *nim)
> +{
> +    struct advertise_neighbor_entry *an;
> +
> +    HMAP_FOR_EACH_POP (an, node, &nim->announced_neighbors) {
> +        free(an);
> +    }
> +    hmap_destroy(&nim->announced_neighbors);
> +    free(nim);
> +}
> diff --git a/controller/neighbor.h b/controller/neighbor.h
> new file mode 100644
> index 000000000..0cc683b3e
> --- /dev/null
> +++ b/controller/neighbor.h
> @@ -0,0 +1,75 @@
> +/* Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#ifndef NEIGHBOR_H
> +#define NEIGHBOR_H 1
> +
> +#include <sys/types.h>
> +#include <netinet/in.h>
> +#include <net/if.h>
> +#include <stdint.h>
> +
> +#include "lib/sset.h"
> +#include "openvswitch/hmap.h"
> +
> +#include "vec.h"
> +
> +/* XXX: AF_BRIDGE doesn't seem to be defined on OSX. */
> +#ifdef __APPLE__

nit: We typically use __MACH__.

But also, what about windows build?  It also looks like all BSD systems
use AF_INET sockets with varying approaches of getting the data.
So, this whole AF_BRIDGE thing is Linux-specific.

We may need to remove the ifdef __APPLE__ and just keep the ifndef AF_BRIDGE,
if we don't want to create a stab implementation.

> +#ifndef AF_BRIDGE
> +#define AF_BRIDGE AF_UNSPEC
> +#endif
> +#endif /* __APPLE__ */
> +
> +enum neighbor_family {
> +    NEIGH_AF_INET = AF_INET,
> +    NEIGH_AF_INET6 = AF_INET6,
> +    NEIGH_AF_BRIDGE = AF_BRIDGE,
> +};
> +
> +struct neighbor_ctx_in {
> +};
> +
> +struct neighbor_ctx_out {
> +    /* Contains struct neighbor_interface_monitor pointers. */
> +    struct vector *monitored_interfaces;
> +};
> +
> +struct neighbor_interface_monitor {
> +    enum neighbor_family family;
> +    char if_name[IFNAMSIZ + 1];
> +
> +    /* Contains struct advertise_neighbor_entry - the entries that OVN
> +     * advertises on this interface. */
> +    struct hmap announced_neighbors;
> +};
> +
> +struct advertise_neighbor_entry {
> +    struct hmap_node node;
> +
> +    struct eth_addr lladdr;
> +    struct in6_addr addr;   /* In case of 'dst' entries non-zero;
> +                             * all zero otherwise. */
> +};
> +
> +uint32_t advertise_neigh_hash(const struct eth_addr *,
> +                              const struct in6_addr *);
> +struct advertise_neighbor_entry *advertise_neigh_find(
> +    const struct hmap *neighbors, struct eth_addr mac,
> +    const struct in6_addr *ip);
> +void neighbor_run(struct neighbor_ctx_in *, struct neighbor_ctx_out *);
> +void neighbor_cleanup(struct vector *monitored_interfaces);
> +
> +#endif /* NEIGHBOR_H */
> diff --git a/controller/test-ovn-netlink.c b/controller/test-ovn-netlink.c
> new file mode 100644
> index 000000000..4134d9f0a
> --- /dev/null
> +++ b/controller/test-ovn-netlink.c
> @@ -0,0 +1,120 @@
> +/* Copyright (c) 2025, Red Hat, Inc.
> + *
> + * Licensed under the Apache License, Version 2.0 (the "License");
> + * you may not use this file except in compliance with the License.
> + * You may obtain a copy of the License at:
> + *
> + *     http://www.apache.org/licenses/LICENSE-2.0
> + *
> + * Unless required by applicable law or agreed to in writing, software
> + * distributed under the License is distributed on an "AS IS" BASIS,
> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
> + * See the License for the specific language governing permissions and
> + * limitations under the License.
> + */
> +
> +#include <config.h>
> +
> +#include "openvswitch/hmap.h"
> +#include "packets.h"
> +#include "tests/ovstest.h"
> +#include "tests/test-utils.h"
> +
> +#include "neighbor-exchange-netlink.h"
> +#include "neighbor.h"
> +
> +static void
> +test_neighbor_sync(struct ovs_cmdl_context *ctx)
> +{
> +    struct advertise_neighbor_entry *e;
> +    unsigned int n_neighs_to_add;
> +    unsigned int shift = 1;
> +    unsigned int if_index;
> +
> +    const char *family_str = test_read_value(ctx, shift++, "address family");
> +    if (!family_str) {
> +        return;
> +    }
> +    enum neighbor_family family;
> +    if (!strcmp(family_str, "inet")) {
> +        family = NEIGH_AF_INET;
> +    } else if (!strcmp(family_str, "inet6")) {
> +        family = NEIGH_AF_INET6;
> +    } else if (!strcmp(family_str, "bridge")) {
> +        family = NEIGH_AF_BRIDGE;
> +    } else {
> +        fprintf(stderr, "Invalid address family %s\n", family_str);
> +        return;
> +    }
> +
> +    if (!test_read_uint_value(ctx, shift++, "if_index", &if_index)) {
> +        return;
> +    }
> +
> +    if (!test_read_uint_value(ctx, shift++, "number of neighbors to sync",
> +                              &n_neighs_to_add)) {
> +        return;
> +    }
> +
> +    struct hmap neighbors_to_add = HMAP_INITIALIZER(&neighbors_to_add);
> +    struct vector received_neighbors =
> +        VECTOR_EMPTY_INITIALIZER(struct ne_nl_received_neigh);
> +
> +    for (unsigned int i = 0; i < n_neighs_to_add; i++) {
> +        struct advertise_neighbor_entry *ane = xzalloc(sizeof *ane);
> +        if (!test_read_eth_addr_value(ctx, shift++, "MAC address",
> +                                      &ane->lladdr)) {
> +            free(ane);
> +            goto done;
> +        }
> +        if (shift < ctx->argc) {
> +            /* It might be that we're only adding L2 neighbors,
> +             * skip IP parsing then. */
> +            struct eth_addr ea;
> +            if (!eth_addr_from_string(ctx->argv[shift], &ea) &&
> +                !test_read_ipv6_mapped_value(ctx, shift++, "IP address",
> +                                             &ane->addr)) {
> +                free(ane);
> +                goto done;
> +            }
> +        }
> +        hmap_insert(&neighbors_to_add, &ane->node,
> +                    advertise_neigh_hash(&ane->lladdr, &ane->addr));
> +    }
> +
> +    ovs_assert(ne_nl_sync_neigh(family, if_index, &neighbors_to_add,
> +                                &received_neighbors) == 0);
> +
> +    struct ne_nl_received_neigh *ne;
> +    VECTOR_FOR_EACH_PTR (&received_neighbors, ne) {
> +        char addr_s[INET6_ADDRSTRLEN + 1];
> +        printf("Neighbor ifindex=%"PRId32" vlan=%"PRIu16" "
> +               "eth=" ETH_ADDR_FMT " dst=%s port=%"PRIu16"\n",
> +               ne->if_index, ne->vlan, ETH_ADDR_ARGS(ne->lladdr),
> +               ipv6_string_mapped(addr_s, &ne->addr) ? addr_s : "(invalid)",
> +               ne->port);
> +    }
> +
> +done:
> +    HMAP_FOR_EACH_POP (e, node, &neighbors_to_add) {
> +        free(e);
> +    }
> +    hmap_destroy(&neighbors_to_add);
> +    vector_destroy(&received_neighbors);
> +}
> +
> +static void
> +test_ovn_netlink(int argc, char *argv[])
> +{
> +    set_program_name(argv[0]);
> +    static const struct ovs_cmdl_command commands[] = {
> +        {"neighbor-sync", NULL, 2, INT_MAX, test_neighbor_sync, OVS_RO},
> +        {NULL, NULL, 0, 0, NULL, OVS_RO},
> +    };
> +    struct ovs_cmdl_context ctx;
> +    ctx.argc = argc - 1;
> +    ctx.argv = argv + 1;
> +    ovs_cmdl_run_command(&ctx, commands);
> +}
> +
> +OVSTEST_REGISTER("test-ovn-netlink", test_ovn_netlink);
> diff --git a/tests/automake.mk b/tests/automake.mk
> index b96932dc2..b2db67e99 100644
> --- a/tests/automake.mk
> +++ b/tests/automake.mk
> @@ -63,7 +63,8 @@ SYSTEM_USERSPACE_TESTSUITE_AT = \
>  
>  SYSTEM_TESTSUITE_AT = \
>       tests/system-common-macros.at \
> -     tests/system-ovn.at
> +     tests/system-ovn.at \
> +     tests/system-ovn-netlink.at
>  
>  PERF_TESTSUITE_AT = \
>       tests/perf-testsuite.at \
> @@ -292,6 +293,11 @@ tests_ovstest_SOURCES = \
>       lib/test-ovn-features.c \
>       northd/test-ipam.c
>  
> +if HAVE_NETLINK
> +tests_ovstest_SOURCES += \
> +     controller/test-ovn-netlink.c
> +endif
> +
>  tests_ovstest_LDADD = $(OVS_LIBDIR)/daemon.lo \
>      $(OVS_LIBDIR)/libopenvswitch.la lib/libovn.la \
>       controller/binding.$(OBJEXT) \
> @@ -312,7 +318,10 @@ tests_ovstest_LDADD = $(OVS_LIBDIR)/daemon.lo \
>  
>  if HAVE_NETLINK
>  tests_ovstest_LDADD += \
> -     controller/route-exchange-netlink.$(OBJEXT)
> +     controller/neighbor.$(OBJEXT) \
> +     controller/neighbor-exchange-netlink.$(OBJEXT) \
> +     controller/route-exchange-netlink.$(OBJEXT) \
> +     controller/test-ovn-netlink.$(OBJEXT)
>  endif
>  
>  # Python tests.
> diff --git a/tests/ovn-macros.at b/tests/ovn-macros.at
> index 334aa7fc6..3fb56d4be 100644
> --- a/tests/ovn-macros.at
> +++ b/tests/ovn-macros.at
> @@ -1402,6 +1402,10 @@ ovn_strip_collector_set() {
>      sed 's/collector_set=[[0-9]]*,\?/collector_set=??,/g'
>  }
>  
> +netlink_if_index() {
> +    ip -o link show dev $1 | awk -F: '{print $1}'
> +}
> +
>  OVS_END_SHELL_HELPERS
>  
>  m4_define([OVN_POPULATE_ARP], [AT_CHECK(ovn_populate_arp__, [0], [ignore])])
> diff --git a/tests/system-common-macros.at b/tests/system-common-macros.at
> index a49776f38..251a4c0b8 100644
> --- a/tests/system-common-macros.at
> +++ b/tests/system-common-macros.at
> @@ -598,3 +598,19 @@ m4_define([OVN_ROUTE_EQUAL],
>  m4_define([OVN_ROUTE_V6_EQUAL],
>    [OVS_WAIT_UNTIL_EQUAL([ip -6 route list vrf $1 | sed -e 
> 's|[[[[:space:]]]]*$||g' -e 's|proto 84|proto ovn|'], [$2])
>  ])
> +
> +# OVN_NEIGH_EQUAL([interface], [options], [match], [string to compare])
> +#
> +# Will dump all matching v4 neighbors on the mentioned interface. Trailing
> +# spaces will be removed.
> +m4_define([OVN_NEIGH_EQUAL],
> +  [OVS_WAIT_UNTIL_EQUAL([ip neigh show dev $1 $2 | sed -e 
> 's|[[[[:space:]]]]*$||g' | grep $3 | sort], [$4])
> +])
> +
> +# OVN_NEIGH_V6_EQUAL([interface], [options], [match], [string to compare])
> +#
> +# Will dump all matching v6 neighbors on the mentioned interface. Trailing
> +# spaces will be removed.
> +m4_define([OVN_NEIGH_V6_EQUAL],
> +  [OVS_WAIT_UNTIL_EQUAL([ip -6 neigh show dev $1 $2 | sed -e 
> 's|[[[[:space:]]]]*$||g' | grep $3 | sort], [$4])
> +])
> diff --git a/tests/system-dpdk-testsuite.at b/tests/system-dpdk-testsuite.at
> index 72ddc3913..2d14c1015 100644
> --- a/tests/system-dpdk-testsuite.at
> +++ b/tests/system-dpdk-testsuite.at
> @@ -23,3 +23,4 @@ m4_include([tests/ovn-macros.at])
>  m4_include([tests/system-common-macros.at])
>  m4_include([tests/system-dpdk-macros.at])
>  m4_include([tests/system-ovn.at])
> +m4_include([tests/system-ovn-netlink.at])
> diff --git a/tests/system-kmod-testsuite.at b/tests/system-kmod-testsuite.at
> index 5ba35babb..ec028e6eb 100644
> --- a/tests/system-kmod-testsuite.at
> +++ b/tests/system-kmod-testsuite.at
> @@ -25,3 +25,4 @@ m4_include([tests/system-kmod-macros.at])
>  
>  m4_include([tests/system-ovn.at])
>  m4_include([tests/system-ovn-kmod.at])
> +m4_include([tests/system-ovn-netlink.at])
> diff --git a/tests/system-ovn-netlink.at b/tests/system-ovn-netlink.at
> new file mode 100644
> index 000000000..6a21c0e56
> --- /dev/null
> +++ b/tests/system-ovn-netlink.at
> @@ -0,0 +1,178 @@
> +AT_BANNER([system-ovn-netlink])
> +
> +AT_SETUP([sync netlink neighbors - learn VXLAN VTEP neighbors])
> +AT_KEYWORDS([netlink-neighbors])
> +
> +check ip link add br-test type bridge
> +on_exit 'ip link del br-test'
> +check ip link set br-test address 00:00:00:00:00:01
> +check ip link set dev br-test up
> +
> +check ip link add vxlan-test type vxlan id 42 \
> +    dstport 4789 local 42.42.42.2 nolearning
> +on_exit 'ip link del vxlan-test'
> +check ip link set vxlan-test master br-test
> +check ip link set vxlan-test address 00:00:00:00:00:02
> +check ip link set dev vxlan-test up
> +
> +dnl Inject permanent (vxlan) entries.
> +check bridge fdb append 00:00:00:00:00:00 dev vxlan-test \
> +    dst 42.42.42.3 port 4789 self permanent
> +check bridge fdb append 00:00:00:00:00:00 dev vxlan-test \
> +    dst 42.42.42.4 port 4790 self permanent
> +
> +if_index=$(netlink_if_index vxlan-test)
> +OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovstest test-ovn-netlink neighbor-sync \
> +    bridge $if_index 0 | sort], [0], [dnl
> +Neighbor ifindex=$if_index vlan=0 eth=00:00:00:00:00:00 dst=42.42.42.3 port=0
> +Neighbor ifindex=$if_index vlan=0 eth=00:00:00:00:00:00 dst=42.42.42.4 
> port=4790
> +Neighbor ifindex=$if_index vlan=0 eth=00:00:00:00:00:02 dst=:: port=0
> +])
> +AT_CLEANUP
> +
> +AT_SETUP([sync netlink neighbors - learn VXLAN remote mac entries])
> +AT_KEYWORDS([netlink-neighbors])
> +
> +check ip link add br-test type bridge
> +on_exit 'ip link del br-test'
> +check ip link set br-test address 00:00:00:00:00:01
> +check ip link set dev br-test up
> +
> +check ip link add vxlan-test type vxlan id 42 \
> +    dstport 4789 local 42.42.42.2 nolearning
> +on_exit 'ip link del vxlan-test'
> +check ip link set vxlan-test master br-test
> +check ip link set vxlan-test address 00:00:00:00:00:02
> +check ip link set dev vxlan-test up
> +
> +dnl Inject externally learnt (vxlan) mac entries.
> +check bridge fdb add 00:00:00:00:00:03 dev vxlan-test \
> +    dst 42.42.42.3 static extern_learn
> +
> +if_index=$(netlink_if_index vxlan-test)
> +OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovstest test-ovn-netlink neighbor-sync \
> +    bridge $if_index 0 | sort], [0], [dnl
> +Neighbor ifindex=$if_index vlan=0 eth=00:00:00:00:00:02 dst=:: port=0
> +Neighbor ifindex=$if_index vlan=0 eth=00:00:00:00:00:03 dst=42.42.42.3 port=0
> +])
> +AT_CLEANUP
> +
> +AT_SETUP([sync netlink neighbors - inject OVN static mac entries])
> +AT_KEYWORDS([netlink-neighbors])
> +
> +check ip link add br-test type bridge
> +on_exit 'ip link del br-test'
> +check ip link set br-test address 00:00:00:00:00:01
> +check ip link set dev br-test up
> +
> +check ip link add lo-test type dummy
> +on_exit 'ip link del lo-test'
> +check ip link set lo-test master br-test
> +check ip link set lo-test address 00:00:00:00:00:02
> +check ip link set dev lo-test up
> +
> +dnl Let ovn inject some neighbor (mac) entries, we detect the lo-test mac and
> +dnl the L2 multicast ones.
> +if_index=$(netlink_if_index lo-test)
> +OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovstest test-ovn-netlink neighbor-sync \
> +    bridge $if_index 2 00:00:00:00:01:00 00:00:00:00:02:00 | sort], [0], [dnl
> +Neighbor ifindex=$if_index vlan=0 eth=00:00:00:00:00:02 dst=:: port=0
> +Neighbor ifindex=$if_index vlan=0 eth=01:00:5e:00:00:01 dst=:: port=0
> +Neighbor ifindex=$if_index vlan=0 eth=33:33:00:00:00:01 dst=:: port=0
> +])
> +
> +dnl Check that OVN installed its entries (these are always installed
> +dnl as "static").
> +OVS_WAIT_FOR_OUTPUT([bridge fdb show dev lo-test | grep static | sort], [0],
> +[dnl
> +00:00:00:00:01:00 master br-test static
> +00:00:00:00:01:00 vlan 1 master br-test static
> +00:00:00:00:02:00 master br-test static
> +00:00:00:00:02:00 vlan 1 master br-test static
> +])
> +
> +dnl Remove the static entries, rerun the OVN test binary, they should be
> +dnl readded.
> +check bridge fdb del 00:00:00:00:01:00 dev lo-test master static
> +check bridge fdb del 00:00:00:00:02:00 dev lo-test master static
> +
> +OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovstest test-ovn-netlink neighbor-sync \
> +    bridge $if_index 2 00:00:00:00:01:00 00:00:00:00:02:00 | sort], [0], [dnl
> +Neighbor ifindex=$if_index vlan=0 eth=00:00:00:00:00:02 dst=:: port=0
> +Neighbor ifindex=$if_index vlan=0 eth=01:00:5e:00:00:01 dst=:: port=0
> +Neighbor ifindex=$if_index vlan=0 eth=33:33:00:00:00:01 dst=:: port=0
> +])
> +
> +OVS_WAIT_FOR_OUTPUT([bridge fdb show dev lo-test | grep static | sort], [0],
> +[dnl
> +00:00:00:00:01:00 master br-test static
> +00:00:00:00:01:00 vlan 1 master br-test static
> +00:00:00:00:02:00 master br-test static
> +00:00:00:00:02:00 vlan 1 master br-test static
> +])
> +AT_CLEANUP
> +
> +AT_SETUP([sync netlink neighbors - learn IP neighbors])
> +AT_KEYWORDS([netlink-neighbors])
> +CHECK_VRF()
> +
> +dnl NOTE: To avoid affecting the default routing table configure a test
> +dnl interface into a separate vrf.
> +check ip link add vrf-ovn type vrf table 42
> +on_exit 'ip link del vrf-ovn'
> +check ip link add br-test type bridge
> +on_exit 'ip link del br-test'
> +check ip link set br-test master vrf-ovn
> +check ip link set br-test address 00:00:00:00:00:01
> +check ip address add dev br-test 20.20.20.1/24
> +check ip -6 address add dev br-test 20::1/64
> +check ip link set dev br-test up
> +
> +dnl Inject externally learnt IP neighbor entries.
> +check ip neigh add 10.10.10.10 \
> +    lladdr 00:00:00:00:10:00 dev br-test extern_learn
> +check ip -6 neigh add 10::10 \
> +    lladdr 00:00:00:00:10:00 dev br-test extern_learn
> +
> +dnl Let OVN inject some IPv4 neighbors too and make sure it learnt the
> +dnl external ones.
> +if_index=$(netlink_if_index br-test)
> +OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovstest test-ovn-netlink neighbor-sync \
> +    inet $if_index 1 00:00:00:00:20:00 20.20.20.20 | sort], [0], [dnl
> +Neighbor ifindex=$if_index vlan=0 eth=00:00:00:00:10:00 dst=10.10.10.10 
> port=0
> +])
> +
> +dnl Check that OVN installed its entries (these are always installed
> +dnl as "noarp").
> +OVN_NEIGH_EQUAL([br-test], [nud noarp], [20.20.20], [dnl
> +20.20.20.20 lladdr 00:00:00:00:20:00 NOARP])
> +
> +dnl Let OVN inject some neighbors too and make sure it learnt the extern 
> ones.
> +OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovstest test-ovn-netlink neighbor-sync \
> +    inet6 $if_index 1 00:00:00:00:20:00 20::20 | sort], [0], [dnl
> +Neighbor ifindex=$if_index vlan=0 eth=00:00:00:00:10:00 dst=10::10 port=0
> +])
> +
> +dnl Check that OVN installed its entries (these are always installed
> +dnl as "noarp").
> +OVN_NEIGH_V6_EQUAL([br-test], [nud noarp], [20::], [dnl
> +20::20 lladdr 00:00:00:00:20:00 NOARP])
> +
> +dnl Remove the "noarp" entries, rerun the OVN test binary, they should be
> +dnl readded.
> +check ip neigh del dev br-test 20.20.20.20
> +check ip -6 neigh del dev br-test 20::20
> +OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovstest test-ovn-netlink neighbor-sync \
> +    inet $if_index 1 00:00:00:00:20:00 20.20.20.20 | sort], [0], [dnl
> +Neighbor ifindex=$if_index vlan=0 eth=00:00:00:00:10:00 dst=10.10.10.10 
> port=0
> +])
> +OVS_WAIT_FOR_OUTPUT_UNQUOTED([ovstest test-ovn-netlink neighbor-sync \
> +    inet6 $if_index 1 00:00:00:00:20:00 20::20 | sort], [0], [dnl
> +Neighbor ifindex=$if_index vlan=0 eth=00:00:00:00:10:00 dst=10::10 port=0
> +])
> +
> +OVN_NEIGH_EQUAL([br-test], [nud noarp], [20.20.20], [dnl
> +20.20.20.20 lladdr 00:00:00:00:20:00 NOARP])
> +OVN_NEIGH_V6_EQUAL([br-test], [nud noarp], [20::], [dnl
> +20::20 lladdr 00:00:00:00:20:00 NOARP])
> +AT_CLEANUP
> diff --git a/tests/system-userspace-testsuite.at 
> b/tests/system-userspace-testsuite.at
> index 4022ae620..5349cf1a8 100644
> --- a/tests/system-userspace-testsuite.at
> +++ b/tests/system-userspace-testsuite.at
> @@ -24,3 +24,4 @@ m4_include([tests/system-userspace-macros.at])
>  m4_include([tests/system-common-macros.at])
>  
>  m4_include([tests/system-ovn.at])
> +m4_include([tests/system-ovn-netlink.at])
> diff --git a/tests/test-utils.c b/tests/test-utils.c
> index 1afdc150f..e55557066 100644
> --- a/tests/test-utils.c
> +++ b/tests/test-utils.c
> @@ -15,8 +15,10 @@
>  
>  #include <config.h>
>  
> +#include "packets.h"
>  #include "test-utils.h"
>  
> +#include "ovn-util.h"
>  #include "util.h"
>  
>  static bool
> @@ -78,3 +80,37 @@ test_read_ullong_value(struct ovs_cmdl_context *ctx, 
> unsigned int index,
>      }
>      return true;
>  }
> +
> +bool
> +test_read_eth_addr_value(struct ovs_cmdl_context *ctx, unsigned int index,
> +                         const char *descr, struct eth_addr *result)
> +{
> +    if (index >= ctx->argc) {
> +        fprintf(stderr, "Missing %s argument\n", descr);
> +        return false;
> +    }
> +
> +    const char *arg = ctx->argv[index];
> +    if (!eth_addr_from_string(arg, result)) {
> +        fprintf(stderr, "Invalid %s: %s\n", descr, arg);
> +        return false;
> +    }
> +    return true;
> +}
> +
> +bool
> +test_read_ipv6_mapped_value(struct ovs_cmdl_context *ctx, unsigned int index,
> +                           const char *descr, struct in6_addr *result)
> +{
> +    if (index >= ctx->argc) {
> +        fprintf(stderr, "Missing %s argument\n", descr);
> +        return false;
> +    }
> +
> +    const char *arg = ctx->argv[index];
> +    if (!ip46_parse(arg, result)) {
> +        fprintf(stderr, "Invalid %s: %s\n", descr, arg);
> +        return false;
> +    }
> +    return true;
> +}
> diff --git a/tests/test-utils.h b/tests/test-utils.h
> index 22341cea9..fef67e799 100644
> --- a/tests/test-utils.h
> +++ b/tests/test-utils.h
> @@ -16,6 +16,10 @@
>  #ifndef TEST_UTILS_H
>  #define TEST_UTILS_H 1
>  
> +#include <sys/types.h>
> +#include <netinet/in.h>
> +
> +#include "openvswitch/types.h"
>  #include "ovstest.h"
>  
>  bool test_read_uint_value(struct ovs_cmdl_context *ctx, unsigned int index,
> @@ -26,5 +30,9 @@ const char *test_read_value(struct ovs_cmdl_context *ctx, 
> unsigned int index,
>                              const char *descr);
>  bool test_read_ullong_value(struct ovs_cmdl_context *ctx, unsigned int index,
>                              const char *descr, unsigned long long int 
> *result);
> -
> +bool test_read_eth_addr_value(struct ovs_cmdl_context *ctx, unsigned int 
> index,
> +                              const char *descr, struct eth_addr *result);
> +bool test_read_ipv6_mapped_value(struct ovs_cmdl_context *ctx,
> +                                 unsigned int index, const char *descr,
> +                                 struct in6_addr *result);
>  #endif /* tests/test-utils.h */

_______________________________________________
dev mailing list
d...@openvswitch.org
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to