Hi Ilya,

Thanks for the review!

On 8/14/25 1:35 PM, Ilya Maximets wrote:
> On 8/14/25 1:30 PM, Ilya Maximets wrote:
>> 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 +++++++
> 
> Also, would be better if we can move this file to tests/
> 

Ack.

>>>  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.
>>

Ack.

>>> + * 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.
>>

Sounds good.

>>> +}
>>> +
>>> +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.
>>

Ack.

>>> +
>>> +    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.
>>

Good point, you're right, I'll add extra filtering for this case.

>>> +            }
>>> +            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.
>>

Ah, I wasn't aware of that.  I'll delay deletes until the dump is done.
We should fix this for route-exchange as well though.

>> 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.
>>

I don't think that's correct, we check the return value of
ne_nl_sync_neigh().  But, in any case, I'll delay deletes as mentioned
above.

>>> +        }
>>> +    }
>>> +    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.
>>

Ack.

>>> +        [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.
>>

Ack.

>>> +        [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.
>>

Ack.

>>> +
>>> +    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.
>>

Ack, I'm going to do the latter.

>>> +#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 */
>>
> 

Regards,
Dumitru


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

Reply via email to