From: Dumitru Ceara <dce...@redhat.com>

When monitoring and updating (Linux) neighbor tables through Netlink,
the interfaces in question are identified by if-index.  It's more
CMS-friendly to allow OVN to be configured to track interfaces by name.

In order to achieve that, we introduce a new host-if-monitor module
which tracks the if-index <-> if-name mapping for relevant interfaces.

NOTE: OVS has the lib/if-notifier.[ch] module already.  However, that is
not really useful for the OVN use case because it doesn't really pass
the struct rtnetlink_change message to its registered callbacks.

NOTE2: Interface name changes are not supported as RTM_DELLINK is not
sent in that case.  In order to change the host interface name users
should first bring the interface down and then change its name.

Signed-off-by: Dumitru Ceara <dce...@redhat.com>
---
V3:
- Add note about interface name change.
- Remove not-relevant-anymore mappings in host_if_monitor_update_watches().
- Added tests.
---
 controller/automake.mk            |   5 +-
 controller/host-if-monitor-stub.c |  43 ++++++++
 controller/host-if-monitor.c      | 161 ++++++++++++++++++++++++++++++
 controller/host-if-monitor.h      |  30 ++++++
 controller/test-ovn-netlink.c     |  36 +++++++
 tests/automake.mk                 |   1 +
 tests/system-ovn-netlink.at       |  24 +++++
 7 files changed, 299 insertions(+), 1 deletion(-)
 create mode 100644 controller/host-if-monitor-stub.c
 create mode 100644 controller/host-if-monitor.c
 create mode 100644 controller/host-if-monitor.h

diff --git a/controller/automake.mk b/controller/automake.mk
index 6af6ee2a9..6eca5e8ed 100644
--- a/controller/automake.mk
+++ b/controller/automake.mk
@@ -64,10 +64,12 @@ controller_ovn_controller_SOURCES = \
        controller/route.c \
        controller/garp_rarp.h \
        controller/garp_rarp.c \
-       controller/neighbor-table-notify.h
+       controller/neighbor-table-notify.h \
+       controller/host-if-monitor.h
 
 if HAVE_NETLINK
 controller_ovn_controller_SOURCES += \
+       controller/host-if-monitor.c \
        controller/neighbor-exchange-netlink.h \
        controller/neighbor-exchange-netlink.c \
        controller/neighbor-table-notify.c \
@@ -77,6 +79,7 @@ controller_ovn_controller_SOURCES += \
        controller/route-table-notify.c
 else
 controller_ovn_controller_SOURCES += \
+       controller/host-if-monitor-stub.c \
        controller/neighbor-table-notify-stub.c \
        controller/route-exchange-stub.c \
        controller/route-table-notify-stub.c
diff --git a/controller/host-if-monitor-stub.c 
b/controller/host-if-monitor-stub.c
new file mode 100644
index 000000000..b174cf996
--- /dev/null
+++ b/controller/host-if-monitor-stub.c
@@ -0,0 +1,43 @@
+/* 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 "host-if-monitor.h"
+
+void
+host_if_monitor_wait(void)
+{
+}
+
+bool
+host_if_monitor_run(void)
+{
+    return false;
+}
+
+void
+host_if_monitor_update_watches(const struct sset *if_names OVS_UNUSED)
+{
+}
+
+int32_t
+host_if_monitor_ifname_toindex(const char *if_name OVS_UNUSED)
+{
+    return 0;
+}
diff --git a/controller/host-if-monitor.c b/controller/host-if-monitor.c
new file mode 100644
index 000000000..5a3c920b0
--- /dev/null
+++ b/controller/host-if-monitor.c
@@ -0,0 +1,161 @@
+/* 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 "lib/rtnetlink.h"
+#include "lib/simap.h"
+#include "openvswitch/vlog.h"
+
+#include "host-if-monitor.h"
+
+VLOG_DEFINE_THIS_MODULE(host_if_monitor);
+
+static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+
+struct host_if_monitor {
+    struct nln_notifier *link_notifier;
+
+    struct sset watched_interfaces;
+    struct simap ifname_to_ifindex;
+
+    bool changes_detected;
+};
+
+static struct host_if_monitor monitor = (struct host_if_monitor) {
+    .link_notifier = NULL,
+    .watched_interfaces = SSET_INITIALIZER(&monitor.watched_interfaces),
+    .ifname_to_ifindex = SIMAP_INITIALIZER(&monitor.ifname_to_ifindex),
+    .changes_detected = false,
+};
+
+static void if_notifier_cb(const struct rtnetlink_change *, void *aux);
+
+void
+host_if_monitor_wait(void)
+{
+    rtnetlink_wait();
+}
+
+bool
+host_if_monitor_run(void)
+{
+    monitor.changes_detected = false;
+
+    /* If any relevant interface if-index <-> if-name mapping changes are
+     * dected, monitor.changes_detected will be updated accordingly by the
+     * if_notifier_cb(). */
+    rtnetlink_run();
+
+    return monitor.changes_detected;
+}
+
+void
+host_if_monitor_update_watches(const struct sset *if_names)
+{
+    struct sset new_if_names = SSET_INITIALIZER(&new_if_names);
+    const char *if_name;
+
+    /* The notifier only triggers the callback on interface updates.
+     * For newly added ones we need to fetch the initial if_index ourselves.
+     */
+    SSET_FOR_EACH (if_name, if_names) {
+        if (!sset_contains(&monitor.watched_interfaces, if_name)) {
+            sset_add(&new_if_names, if_name);
+        }
+    }
+
+    if (!sset_equals(&monitor.watched_interfaces, if_names)) {
+        sset_destroy(&monitor.watched_interfaces);
+        sset_clone(&monitor.watched_interfaces, if_names);
+
+        /* Remove mappings for if_names that are not tracked anymore. */
+        struct simap_node *sn;
+        SIMAP_FOR_EACH_SAFE (sn, &monitor.ifname_to_ifindex) {
+            if (!sset_contains(&monitor.watched_interfaces, sn->name)) {
+                simap_delete(&monitor.ifname_to_ifindex, sn);
+            }
+        }
+    }
+
+    if (!sset_is_empty(&monitor.watched_interfaces)) {
+        if (!monitor.link_notifier) {
+            VLOG_INFO_RL(&rl, "Enabling host interface monitor");
+            monitor.link_notifier =
+                rtnetlink_notifier_create(if_notifier_cb, &monitor);
+        }
+        /* Get initial state for new interfaces.
+         *
+         * NOTE: it's important that we have the initial state (if-index) for
+         * newly watched interfaces because of two reasons:
+         * - we need to be able to reconcile and preserve still valid learned
+         *   remote FDB entries and remote VTEPs
+         * - the if_notifier_cb is called only on updates of interfaces
+         *   therefore if existing interfaces don't change the notifier
+         *   callback is not called.
+         */
+        SSET_FOR_EACH (if_name, &new_if_names) {
+            simap_put(&monitor.ifname_to_ifindex, if_name,
+                      if_nametoindex(if_name));
+        }
+    } else {
+        if (monitor.link_notifier) {
+            VLOG_INFO_RL(&rl, "Disabling host interface monitor");
+            rtnetlink_notifier_destroy(monitor.link_notifier);
+            monitor.link_notifier = NULL;
+        }
+    }
+
+    sset_destroy(&new_if_names);
+}
+
+int32_t
+host_if_monitor_ifname_toindex(const char *if_name)
+{
+    return simap_get(&monitor.ifname_to_ifindex, if_name);
+}
+
+static void
+if_notifier_cb(const struct rtnetlink_change *change, void *aux OVS_UNUSED)
+{
+    if (!change || change->irrelevant) {
+        return;
+    }
+
+    /* NOTE: interface name changes are not supported as RTM_DELLINK is not
+     * sent in that case.  In order to change the host interface name users
+     * should first bring the interface down and then change its name. */
+    switch (change->nlmsg_type) {
+    case RTM_NEWLINK:
+        if ((change->ifi_flags & IFF_UP)
+            && sset_find(&monitor.watched_interfaces, change->ifname)) {
+            simap_put(&monitor.ifname_to_ifindex,
+                      change->ifname, change->if_index);
+            monitor.changes_detected = true;
+        }
+        break;
+    case RTM_DELLINK:
+        if (sset_find(&monitor.watched_interfaces, change->ifname)) {
+            simap_find_and_delete(&monitor.ifname_to_ifindex, change->ifname);
+            monitor.changes_detected = true;
+        }
+        break;
+    default:
+        break;
+    }
+}
diff --git a/controller/host-if-monitor.h b/controller/host-if-monitor.h
new file mode 100644
index 000000000..a41c5869c
--- /dev/null
+++ b/controller/host-if-monitor.h
@@ -0,0 +1,30 @@
+/* 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 HOST_IF_MONITOR_H
+#define HOST_IF_MONITOR_H 1
+
+#include <stdint.h>
+#include "lib/sset.h"
+#include "lib/smap.h"
+
+void host_if_monitor_wait(void);
+bool host_if_monitor_run(void);
+
+void host_if_monitor_update_watches(const struct sset *if_names);
+
+int32_t host_if_monitor_ifname_toindex(const char *if_name);
+
+#endif /* HOST_IF_MONITOR_H */
diff --git a/controller/test-ovn-netlink.c b/controller/test-ovn-netlink.c
index a26e2d478..f5126ba52 100644
--- a/controller/test-ovn-netlink.c
+++ b/controller/test-ovn-netlink.c
@@ -20,6 +20,7 @@
 #include "tests/ovstest.h"
 #include "tests/test-utils.h"
 
+#include "host-if-monitor.h"
 #include "neighbor-exchange-netlink.h"
 #include "neighbor-table-notify.h"
 #include "neighbor.h"
@@ -142,6 +143,40 @@ test_neighbor_table_notify(struct ovs_cmdl_context *ctx)
     neighbor_table_watch_request_cleanup(&table_watches);
 }
 
+static void
+test_host_if_monitor(struct ovs_cmdl_context *ctx)
+{
+    unsigned int shift = 1;
+
+    const char *if_name = test_read_value(ctx, shift++, "if_name");
+    if (!if_name) {
+        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 sset if_names = SSET_INITIALIZER(&if_names);
+    sset_add(&if_names, if_name);
+    host_if_monitor_update_watches(&if_names);
+
+    host_if_monitor_run();
+    host_if_monitor_wait();
+
+    int rc = system(cmd);
+    if (rc) {
+        exit(rc);
+    }
+    ovs_assert(host_if_monitor_run() == expect_notify);
+    printf("%"PRId32"\n", host_if_monitor_ifname_toindex(if_name));
+    sset_destroy(&if_names);
+}
+
 static void
 test_ovn_netlink(int argc, char *argv[])
 {
@@ -150,6 +185,7 @@ test_ovn_netlink(int argc, char *argv[])
         {"neighbor-sync", NULL, 2, INT_MAX, test_neighbor_sync, OVS_RO},
         {"neighbor-table-notify", NULL, 3, 4,
          test_neighbor_table_notify, OVS_RO},
+        {"host-if-monitor", NULL, 2, 3, test_host_if_monitor, 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 ba6b6d7ba..04d90e659 100644
--- a/tests/automake.mk
+++ b/tests/automake.mk
@@ -318,6 +318,7 @@ tests_ovstest_LDADD = $(OVS_LIBDIR)/daemon.lo \
 
 if HAVE_NETLINK
 tests_ovstest_LDADD += \
+       controller/host-if-monitor.$(OBJEXT) \
        controller/neighbor.$(OBJEXT) \
        controller/neighbor-exchange-netlink.$(OBJEXT) \
        controller/neighbor-table-notify.$(OBJEXT) \
diff --git a/tests/system-ovn-netlink.at b/tests/system-ovn-netlink.at
index 068dfdc63..b732470ef 100644
--- a/tests/system-ovn-netlink.at
+++ b/tests/system-ovn-netlink.at
@@ -231,3 +231,27 @@ check ovstest test-ovn-netlink neighbor-table-notify 
br-test $br_if_index \
         dev br-test-unused extern_learn' \
     false
 AT_CLEANUP
+
+AT_SETUP([netlink - host-if-monitor])
+AT_KEYWORDS([netlink])
+
+dnl Should notify if an interface whose name is monitored by OVN is added.
+on_exit 'ip link del lo-test'
+if_index=$(ovstest test-ovn-netlink host-if-monitor lo-test \
+    'ip link add lo-test type dummy && ip link set dev lo-test up' \
+    true)
+check test $? -eq 0
+AT_CHECK_UNQUOTED([netlink_if_index lo-test], [0], [dnl
+$if_index
+])
+
+dnl Should NOT notify if an interface whose name is not monitored by OVN
+dnl is added.
+check ip link del lo-test
+on_exit 'ip link del lo-test-unused'
+AT_CHECK([ovstest test-ovn-netlink host-if-monitor lo-test \
+    'ip link add lo-test-unused type dummy && ip link set dev lo-test-unused 
up' \
+    false], [0], [dnl
+0
+])
+AT_CLEANUP
-- 
2.50.1

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

Reply via email to