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.").
Acked-by: Xavier Simonart <xsimo...@redhat.com> Co-Authored-by: Ales Musil <amu...@redhat.com> Signed-off-by: Ales Musil <amu...@redhat.com> Signed-off-by: Dumitru Ceara <dce...@redhat.com> --- V4: - Added Xavier's ack. - Addresed Ilya's comments: - nits - netlink dump filtering - delay netlink delete - moved test file - Also added tests for deletion of stale OVN entries. 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 | 490 +++++++++++++++++++++++++ controller/neighbor-exchange-netlink.h | 60 +++ controller/neighbor.c | 77 ++++ controller/neighbor.h | 73 ++++ tests/automake.mk | 17 +- 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 | 222 +++++++++++ tests/system-userspace-testsuite.at | 1 + tests/test-ovn-netlink.c | 120 ++++++ tests/test-utils.c | 36 ++ tests/test-utils.h | 10 +- 15 files changed, 1125 insertions(+), 7 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 tests/system-ovn-netlink.at create mode 100644 tests/test-ovn-netlink.c 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..305afda41 --- /dev/null +++ b/controller/neighbor-exchange-netlink.c @@ -0,0 +1,490 @@ +/* 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 + +/* NTF_EXT_LEARNED was introduced in Linux v3.19, define it if + * not available. */ +#ifndef NTF_EXT_LEARNED +#define NTF_EXT_LEARNED (1 << 4) +#endif + +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 void 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 ne_nl_received_neigh'. */ + struct vector *stale_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'. + * + * 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 vector stale_neighbors = + VECTOR_EMPTY_INITIALIZER(struct ne_nl_received_neigh); + 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_to_advertise = &neighbors_to_advertise, + .learned_neighbors = learned_neighbors, + .stale_neighbors = &stale_neighbors, + .neighbors = 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; + } + } + } + + /* Remove any stale neighbors from the system table. */ + struct ne_nl_received_neigh *ne; + VECTOR_FOR_EACH_PTR (&stale_neighbors, ne) { + int err = ne_nl_del_neigh(ne->if_index, ne->family, + &ne->lladdr, &ne->addr, + ne->port, ne->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", + ne->if_index, ne->vlan, ETH_ADDR_ARGS(ne->lladdr), + ipv6_string_mapped(addr_s, &ne->addr) + ? addr_s : "(invalid)", + ne->port, + ovs_strerror(err)); + if (!ret) { + /* Report the first error value to the caller. */ + ret = err; + } + } + } + + hmapx_destroy(&neighbors_to_advertise); + vector_destroy(&stale_neighbors); + 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); +} + +static void +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; + struct nl_dump dump; + + uint8_t request_stub[NETNL_REQ_BUFFER_SIZE]; + ofpbuf_use_stub(&request, request_stub, sizeof request_stub); + + 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 so, we + * filter ourselves. */ + if (!(nlmsghdr->nlmsg_flags & NLM_F_DUMP_FILTERED)) { + if (msg.nd.family != address_family + || (if_index && msg.nd.if_index != if_index)) { + continue; + } + } + handle_msg_cb(&msg, aux); + } + } + ofpbuf_uninit(&buf); + nl_dump_done(&dump); +} + +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 }, + [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 }, + [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; + } + } + + /* Store the entry for deletion. */ + vector_push(handle_data->stale_neighbors, nd); +} + +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); + + 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..ef48b6de9 --- /dev/null +++ b/controller/neighbor.h @@ -0,0 +1,73 @@ +/* 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 some systems, e.g., OSX. */ +#ifndef AF_BRIDGE +#define AF_BRIDGE AF_UNSPEC +#endif + +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/tests/automake.mk b/tests/automake.mk index b96932dc2..1b3e079ca 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,15 @@ tests_ovstest_SOURCES = \ lib/test-ovn-features.c \ northd/test-ipam.c +if HAVE_NETLINK +tests_ovstest_SOURCES += \ + controller/neighbor-exchange-netlink.c \ + controller/neighbor-exchange-netlink.h \ + controller/neighbor.c \ + controller/neighbor.h \ + tests/test-ovn-netlink.c +endif + tests_ovstest_LDADD = $(OVS_LIBDIR)/daemon.lo \ $(OVS_LIBDIR)/libopenvswitch.la lib/libovn.la \ controller/binding.$(OBJEXT) \ @@ -310,11 +320,6 @@ tests_ovstest_LDADD = $(OVS_LIBDIR)/daemon.lo \ controller/vif-plug.$(OBJEXT) \ northd/ipam.$(OBJEXT) -if HAVE_NETLINK -tests_ovstest_LDADD += \ - controller/route-exchange-netlink.$(OBJEXT) -endif - # Python tests. CHECK_PYFILES = \ tests/test-l7.py \ 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..86c716f95 --- /dev/null +++ b/tests/system-ovn-netlink.at @@ -0,0 +1,222 @@ +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 +]) + +dnl Inject some ovn-like entries, rerun the OVN test binary, they should +dnl be removed. +check bridge fdb add 00:00:00:00:03:00 dev lo-test master static +check bridge fdb add 00:00:00:00:04: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 IPv6 neighbors too and make sure it learnt the +dnl external 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]) + +dnl Inject some ovn-like entries, rerun the OVN test binary, they should +dnl be removed. +check ip neigh add 20.20.20.40 \ + lladdr 00:00:00:00:40:00 dev br-test nud noarp +check ip -6 neigh add 20::40 \ + lladdr 00:00:00:00:40:00 dev br-test nud noarp + +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-ovn-netlink.c b/tests/test-ovn-netlink.c new file mode 100644 index 000000000..a052e6787 --- /dev/null +++ b/tests/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 "controller/neighbor-exchange-netlink.h" +#include "controller/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/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 */ -- 2.50.1 _______________________________________________ dev mailing list d...@openvswitch.org https://mail.openvswitch.org/mailman/listinfo/ovs-dev