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.

> +    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" ?

Also, should these be debug logs, maybe?

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

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

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

Reply via email to