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