Hi Ilya, On 8/14/25 2:35 PM, Ilya Maximets wrote: > On 8/12/25 4:56 PM, Ales Musil via dev wrote: >> From: Dumitru Ceara <dce...@redhat.com> >> >> For each (Linux) interface that's interesting for the OVN control plane, >> watch for neighbor (fdb/arp) changes. This allows us to reconcile on >> changed neighbor entries from outside routing agents (e.g., FRR). >> >> This is the neighbor equivalent of the support added for route tables in >> 673d90f1173f ("controller: Watch for route changes."). >> >> Signed-off-by: Dumitru Ceara <dce...@redhat.com> >> --- >> V3: >> - renamed fdb_table_change() to neighbor_table_change() >> - added tests >> >> Changes in V2: >> - fixed up log messages >> - changed code to make OVN only manage neighbor entries with VLAN 0 >> - added advertise_neigh_find() helper >> --- >> Makefile.am | 5 +- >> controller/automake.mk | 5 +- >> controller/neighbor-table-notify-stub.c | 57 ++++++ >> controller/neighbor-table-notify.c | 244 ++++++++++++++++++++++++ >> controller/neighbor-table-notify.h | 45 +++++ >> controller/test-ovn-netlink.c | 41 ++++ >> tests/automake.mk | 1 + >> tests/system-ovn-netlink.at | 55 ++++++ >> 8 files changed, 451 insertions(+), 2 deletions(-) >> create mode 100644 controller/neighbor-table-notify-stub.c >> create mode 100644 controller/neighbor-table-notify.c >> create mode 100644 controller/neighbor-table-notify.h >> >> diff --git a/Makefile.am b/Makefile.am >> index ea98fb5fb..d8e24d0ab 100644 >> --- a/Makefile.am >> +++ b/Makefile.am >> @@ -350,13 +350,16 @@ check-tabs: >> fi >> .PHONY: check-tabs >> >> +# NOTE: test-ovn-netlink.c excluded due to use of system() to execute >> +# ip neigh / bridge fdb commands provided as arguments by test suite. >> ALL_LOCAL += thread-safety-check >> thread-safety-check: >> @cd $(srcdir); \ >> if test -e .git && (git --version) >/dev/null 2>&1 && \ >> grep -n -f build-aux/thread-safety-blacklist \ >> `git ls-files | grep -v $(submodules) | grep '\.[ch]$$'` /dev/null \ >> - | $(EGREP) -v ':[ ]*/?\*'; \ >> + | $(EGREP) -v ':[ ]*/?\*' \ >> + | $(EGREP) -v '^controller/test-ovn-netlink.c'; \ >> then \ >> echo "See above for list of calls to functions that are"; \ >> echo "blacklisted due to thread safety issues"; \ >> diff --git a/controller/automake.mk b/controller/automake.mk >> index 3eb45475c..6af6ee2a9 100644 >> --- a/controller/automake.mk >> +++ b/controller/automake.mk >> @@ -63,18 +63,21 @@ controller_ovn_controller_SOURCES = \ >> controller/route.h \ >> controller/route.c \ >> controller/garp_rarp.h \ >> - controller/garp_rarp.c >> + controller/garp_rarp.c \ >> + controller/neighbor-table-notify.h >> >> if HAVE_NETLINK >> controller_ovn_controller_SOURCES += \ >> controller/neighbor-exchange-netlink.h \ >> controller/neighbor-exchange-netlink.c \ >> + controller/neighbor-table-notify.c \ >> controller/route-exchange-netlink.h \ >> controller/route-exchange-netlink.c \ >> controller/route-exchange.c \ >> controller/route-table-notify.c >> else >> controller_ovn_controller_SOURCES += \ >> + controller/neighbor-table-notify-stub.c \ >> controller/route-exchange-stub.c \ >> controller/route-table-notify-stub.c >> endif >> diff --git a/controller/neighbor-table-notify-stub.c >> b/controller/neighbor-table-notify-stub.c >> new file mode 100644 >> index 000000000..bb4fe5991 >> --- /dev/null >> +++ b/controller/neighbor-table-notify-stub.c >> @@ -0,0 +1,57 @@ >> +/* 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 "openvswitch/compiler.h" >> +#include "neighbor-table-notify.h" >> + >> +bool >> +neighbor_table_notify_run(void) >> +{ >> + return false; >> +} >> + >> +void >> +neighbor_table_notify_wait(void) >> +{ >> +} >> + >> +void >> +neighbor_table_add_watch_request( >> + struct hmap *neighbor_table_watches OVS_UNUSED, >> + int32_t if_index OVS_UNUSED, >> + const char *if_name OVS_UNUSED) >> +{ >> +} >> + >> +void >> +neighbor_table_watch_request_cleanup( >> + struct hmap *neighbor_table_watches OVS_UNUSED) >> +{ >> +} >> + >> +void >> +neighbor_table_notify_update_watches( >> + const struct hmap *neighbor_table_watches OVS_UNUSED) >> +{ >> +} >> + >> +void >> +neighbor_table_notify_destroy(void) >> +{ >> +} >> diff --git a/controller/neighbor-table-notify.c >> b/controller/neighbor-table-notify.c >> new file mode 100644 >> index 000000000..dd0b320c4 >> --- /dev/null >> +++ b/controller/neighbor-table-notify.c >> @@ -0,0 +1,244 @@ >> +/* 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 <linux/rtnetlink.h> >> +#include <net/if.h> >> + >> +#include "hash.h" >> +#include "hmapx.h" >> +#include "lib/util.h" >> +#include "netlink-notifier.h" >> +#include "openvswitch/vlog.h" >> + >> +#include "neighbor-exchange-netlink.h" >> +#include "neighbor-table-notify.h" >> + >> +VLOG_DEFINE_THIS_MODULE(neighbor_table_notify); >> + >> +struct neighbor_table_watch_request { >> + struct hmap_node node; >> + int32_t if_index; >> + char if_name[IFNAMSIZ + 1]; >> +}; >> + >> +struct neighbor_table_watch_entry { >> + struct hmap_node node; >> + int32_t if_index; >> + char if_name[IFNAMSIZ + 1]; >> +}; >> + >> +static struct hmap watches = HMAP_INITIALIZER(&watches); >> +static bool any_neighbor_table_changed; >> +static struct ne_table_msg nln_nmsg_change; >> + >> +static struct nln *nl_neighbor_handle; >> +static struct nln_notifier *nl_neighbor_notifier; >> + >> +static void neighbor_table_change(const void *change_, void *aux); >> + >> +static void >> +neighbor_table_register_notifiers(void) >> +{ >> + VLOG_INFO("Adding neighbor table watchers."); >> + ovs_assert(!nl_neighbor_handle); >> + >> + nl_neighbor_handle = nln_create(NETLINK_ROUTE, ne_table_parse, >> + &nln_nmsg_change); >> + ovs_assert(nl_neighbor_handle); >> + >> + nl_neighbor_notifier = >> + nln_notifier_create(nl_neighbor_handle, RTNLGRP_NEIGH, >> + neighbor_table_change, NULL); >> + if (!nl_neighbor_notifier) { >> + static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1); >> + VLOG_WARN_RL(&rl, "Failed to create neighbor table watcher."); >> + } >> +} >> + >> +static void >> +neighbor_table_deregister_notifiers(void) >> +{ >> + VLOG_INFO("Removing neighbor table watchers."); >> + ovs_assert(nl_neighbor_handle); >> + >> + nln_notifier_destroy(nl_neighbor_notifier); >> + nln_destroy(nl_neighbor_handle); >> + nl_neighbor_notifier = NULL; >> + nl_neighbor_handle = NULL; >> +} >> + >> +static uint32_t >> +neighbor_table_notify_hash_watch(int32_t if_index) >> +{ >> + /* To allow lookups triggered by netlink messages, don't include the >> + * if_name in the hash. The netlink updates only include if_index. */ >> + return hash_int(if_index, 0); >> +} >> + >> +static void >> +add_watch_entry(int32_t if_index, const char *if_name) >> +{ >> + VLOG_INFO("Registering new neighbor table watcher " >> + "for if_index %s (%"PRId32").", >> + if_name, if_index); >> + >> + struct neighbor_table_watch_entry *we; >> + uint32_t hash = neighbor_table_notify_hash_watch(if_index); >> + we = xzalloc(sizeof *we); >> + we->if_index = if_index; >> + ovs_strzcpy(we->if_name, if_name, IFNAMSIZ + 1); > > nit: sizeof wr->if_name. >
Ack. >> + hmap_insert(&watches, &we->node, hash); >> + >> + if (!nl_neighbor_handle) { >> + neighbor_table_register_notifiers(); >> + } >> +} >> + >> +static void >> +remove_watch_entry(struct neighbor_table_watch_entry *we) >> +{ >> + VLOG_INFO("Removing neighbor table watcher for table %s (%"PRId32").", > > "for table" ? > Ah, copy/paste. :D > Also, should these be debug logs, maybe? > Yeah, that's maybe better, I'll change it. >> + we->if_name, we->if_index); >> + hmap_remove(&watches, &we->node); >> + free(we); >> + >> + if (hmap_is_empty(&watches)) { >> + neighbor_table_deregister_notifiers(); >> + } >> +} >> + >> +bool >> +neighbor_table_notify_run(void) >> +{ >> + any_neighbor_table_changed = false; >> + >> + if (nl_neighbor_handle) { >> + nln_run(nl_neighbor_handle); >> + } >> + >> + return any_neighbor_table_changed; >> +} >> + >> +void >> +neighbor_table_notify_wait(void) >> +{ >> + if (nl_neighbor_handle) { >> + nln_wait(nl_neighbor_handle); >> + } >> +} >> + >> +void >> +neighbor_table_add_watch_request(struct hmap *neighbor_table_watches, >> + int32_t if_index, const char *if_name) >> +{ >> + struct neighbor_table_watch_request *wr = xzalloc(sizeof *wr); >> + >> + wr->if_index = if_index; >> + ovs_strzcpy(wr->if_name, if_name, IFNAMSIZ + 1); > > nit: sizeof wr->if_name. > Ack. >> + hmap_insert(neighbor_table_watches, &wr->node, >> + neighbor_table_notify_hash_watch(wr->if_index)); >> +} >> + >> +void >> +neighbor_table_watch_request_cleanup(struct hmap *neighbor_table_watches) >> +{ >> + struct neighbor_table_watch_request *wr; >> + HMAP_FOR_EACH_POP (wr, node, neighbor_table_watches) { >> + free(wr); >> + } >> +} >> + >> +static struct neighbor_table_watch_entry * >> +find_watch_entry(int32_t if_index, const char *if_name) >> +{ >> + struct neighbor_table_watch_entry *we; >> + uint32_t hash = neighbor_table_notify_hash_watch(if_index); >> + HMAP_FOR_EACH_WITH_HASH (we, node, hash, &watches) { >> + if (if_index == we->if_index && !strcmp(if_name, we->if_name)) { >> + return we; >> + } >> + } >> + return NULL; >> +} >> + >> +static struct neighbor_table_watch_entry * >> +find_watch_entry_by_if_index(int32_t if_index) >> +{ >> + struct neighbor_table_watch_entry *we; >> + uint32_t hash = neighbor_table_notify_hash_watch(if_index); >> + HMAP_FOR_EACH_WITH_HASH (we, node, hash, &watches) { >> + if (if_index == we->if_index) { >> + return we; >> + } >> + } >> + return NULL; >> +} >> + >> +void >> +neighbor_table_notify_update_watches(const struct hmap >> *neighbor_table_watches) >> +{ >> + struct hmapx sync_watches = HMAPX_INITIALIZER(&sync_watches); >> + struct neighbor_table_watch_entry *we; >> + HMAP_FOR_EACH (we, node, &watches) { >> + hmapx_add(&sync_watches, we); >> + } >> + >> + struct neighbor_table_watch_request *wr; >> + HMAP_FOR_EACH (wr, node, neighbor_table_watches) { >> + we = find_watch_entry(wr->if_index, wr->if_name); >> + if (we) { >> + hmapx_find_and_delete(&sync_watches, we); >> + } else { >> + add_watch_entry(wr->if_index, wr->if_name); >> + } >> + } >> + >> + struct hmapx_node *node; >> + HMAPX_FOR_EACH (node, &sync_watches) { >> + remove_watch_entry(node->data); >> + } >> + >> + hmapx_destroy(&sync_watches); >> +} >> + >> +void >> +neighbor_table_notify_destroy(void) >> +{ >> + struct neighbor_table_watch_entry *we; >> + HMAP_FOR_EACH_SAFE (we, node, &watches) { >> + remove_watch_entry(we); >> + } >> +} >> + >> +static void >> +neighbor_table_change(const void *change_, void *aux OVS_UNUSED) >> +{ >> + /* We currently track whether at least one recent neighbor table change >> + * was detected. If that's the case already there's no need to >> + * continue. */ >> + if (any_neighbor_table_changed) { >> + return; >> + } >> + >> + const struct ne_table_msg *change = change_; >> + >> + if (change && !ne_is_ovn_owned(&change->nd)) { >> + if (find_watch_entry_by_if_index(change->nd.if_index)) { >> + any_neighbor_table_changed = true; >> + } >> + } >> +} >> diff --git a/controller/neighbor-table-notify.h >> b/controller/neighbor-table-notify.h >> new file mode 100644 >> index 000000000..9f21271cc >> --- /dev/null >> +++ b/controller/neighbor-table-notify.h >> @@ -0,0 +1,45 @@ >> +/* 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_TABLE_NOTIFY_H >> +#define NEIGHBOR_TABLE_NOTIFY_H 1 >> + >> +#include <stdbool.h> >> +#include "openvswitch/hmap.h" >> + >> +/* Returns true if any neighbor table has changed enough that we need >> + * to learn new neighbor entries. */ >> +bool neighbor_table_notify_run(void); >> +void neighbor_table_notify_wait(void); >> + >> +/* Add a watch request to the hmap. The hmap should later be passed to >> + * neighbor_table_notify_update_watches*/ >> +void neighbor_table_add_watch_request(struct hmap *neighbor_table_watches, >> + int32_t if_index, const char >> *if_name); >> + >> +/* Cleanup all watch request in the provided hmap that where added using >> + * neighbor_table_add_watch_request. */ >> +void neighbor_table_watch_request_cleanup( >> + struct hmap *neighbor_table_watches); >> + >> +/* Updates the list of neighbor table watches that are currently active. >> + * hmap should contain struct neighbor_table_watch_request */ >> +void neighbor_table_notify_update_watches( >> + const struct hmap *neighbor_table_watches); >> + >> +/* Cleans up all neighbor table watches. */ >> +void neighbor_table_notify_destroy(void); >> + >> +#endif /* NEIGHBOR_TABLE_NOTIFY_H */ >> diff --git a/controller/test-ovn-netlink.c b/controller/test-ovn-netlink.c >> index 4134d9f0a..a26e2d478 100644 >> --- a/controller/test-ovn-netlink.c >> +++ b/controller/test-ovn-netlink.c >> @@ -21,6 +21,7 @@ >> #include "tests/test-utils.h" >> >> #include "neighbor-exchange-netlink.h" >> +#include "neighbor-table-notify.h" >> #include "neighbor.h" >> >> static void >> @@ -103,12 +104,52 @@ done: >> vector_destroy(&received_neighbors); >> } >> >> +static void >> +test_neighbor_table_notify(struct ovs_cmdl_context *ctx) >> +{ >> + unsigned int shift = 1; >> + >> + const char *if_name = test_read_value(ctx, shift++, "if_name"); >> + if (!if_name) { >> + return; >> + } >> + >> + unsigned int if_index; >> + if (!test_read_uint_value(ctx, shift++, "if_index", &if_index)) { >> + return; >> + } >> + >> + const char *cmd = test_read_value(ctx, shift++, "shell_command"); >> + if (!cmd) { >> + return; >> + } >> + >> + const char *notify = test_read_value(ctx, shift++, "should_notify"); >> + bool expect_notify = notify && !strcmp(notify, "true"); >> + >> + struct hmap table_watches = HMAP_INITIALIZER(&table_watches); >> + neighbor_table_add_watch_request(&table_watches, if_index, if_name); >> + neighbor_table_notify_update_watches(&table_watches); >> + >> + neighbor_table_notify_run(); >> + neighbor_table_notify_wait(); >> + >> + int rc = system(cmd); >> + if (rc) { >> + exit(rc); >> + } >> + ovs_assert(neighbor_table_notify_run() == expect_notify); >> + neighbor_table_watch_request_cleanup(&table_watches); >> +} >> + >> 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}, >> + {"neighbor-table-notify", NULL, 3, 4, >> + test_neighbor_table_notify, OVS_RO}, >> {NULL, NULL, 0, 0, NULL, OVS_RO}, >> }; >> struct ovs_cmdl_context ctx; >> diff --git a/tests/automake.mk b/tests/automake.mk >> index b2db67e99..ba6b6d7ba 100644 >> --- a/tests/automake.mk >> +++ b/tests/automake.mk >> @@ -320,6 +320,7 @@ if HAVE_NETLINK >> tests_ovstest_LDADD += \ >> controller/neighbor.$(OBJEXT) \ >> controller/neighbor-exchange-netlink.$(OBJEXT) \ >> + controller/neighbor-table-notify.$(OBJEXT) \ >> controller/route-exchange-netlink.$(OBJEXT) \ >> controller/test-ovn-netlink.$(OBJEXT) >> endif >> diff --git a/tests/system-ovn-netlink.at b/tests/system-ovn-netlink.at >> index 6a21c0e56..068dfdc63 100644 >> --- a/tests/system-ovn-netlink.at >> +++ b/tests/system-ovn-netlink.at >> @@ -176,3 +176,58 @@ OVN_NEIGH_EQUAL([br-test], [nud noarp], [20.20.20], [dnl >> OVN_NEIGH_V6_EQUAL([br-test], [nud noarp], [20::], [dnl >> 20::20 lladdr 00:00:00:00:20:00 NOARP]) >> AT_CLEANUP >> + >> +AT_SETUP([sync netlink neighbors - table notify]) >> +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 address add dev br-test 10.10.10.1/24 >> +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 >> +lo_if_index=$(netlink_if_index lo-test) >> + >> +check ip link add br-test-unused type bridge >> +on_exit 'ip link del br-test-unused' >> +check ip link set br-test-unused address 00:00:00:00:00:03 >> +check ip address add dev br-test-unused 20.20.20.1/24 >> +check ip link set dev br-test-unused up >> + >> +check ip link add lo-test-unused type dummy >> +on_exit 'ip link del lo-test-unused' >> +check ip link set lo-test-unused master br-test-unused >> +check ip link set lo-test-unused address 00:00:00:00:00:04 >> +check ip link set dev lo-test-unused up >> + >> +dnl Should notify if an entry is added to a bridge port monitored by OVN. >> +check ovstest test-ovn-netlink neighbor-table-notify lo-test $lo_if_index \ >> + 'bridge fdb add 00:00:00:00:00:05 dev lo-test' \ >> + true >> + >> +dnl Should NOT notify if an entry is added to a bridge port that's not >> +dnl monitored by OVN. >> +check ovstest test-ovn-netlink neighbor-table-notify lo-test $lo_if_index \ >> + 'bridge fdb add 00:00:00:00:00:05 dev lo-test-unused' \ >> + false >> + >> +br_if_index=$(netlink_if_index br-test) >> +dnl Should notify if an entry is added to a bridge that's monitored by >> +dnl OVN. >> +check ovstest test-ovn-netlink neighbor-table-notify br-test $br_if_index \ >> + 'ip neigh add 10.10.10.10 lladdr 00:00:00:00:10:00 \ >> + dev br-test extern_learn' \ >> + true >> + >> +dnl Should NOT notify if an entry is added to a bridge that's not monitored >> by >> +dnl OVN. >> +check ovstest test-ovn-netlink neighbor-table-notify br-test $br_if_index \ >> + 'ip neigh add 20.20.20.20 lladdr 00:00:00:00:20:00 \ >> + dev br-test-unused extern_learn' \ >> + false >> +AT_CLEANUP > Regards, Dumitru _______________________________________________ dev mailing list d...@openvswitch.org https://mail.openvswitch.org/mailman/listinfo/ovs-dev