When  Logical Switch has dynamic-routing-vni and
dynamic-routing-maintain-evpn=mvd set, ovn-controller
automatically creates and manages the Linux kernel interfaces required
for EVPN/BGP: a VRF (ovnvrf<vni>), a bridge (ovnbr<vni>), a VXLAN
interface (ovnvxlan<vni>) and a dummy interface (ovnlo<vni>).
For L3 VNI support, dynamic-routing-evpn-lrp must be set
to the name of a Logical Router Port whose MAC and IP address will be
applied to the bridge interface.

Signed-off-by: Alexandra Rukomoinikova <[email protected]>
---
 NEWS                                   |   7 +
 controller/automake.mk                 |   2 +
 controller/host-if-monitor.c           |  15 ++
 controller/host-if-monitor.h           |   2 +-
 controller/neighbor-exchange-netlink.c | 102 +++++++-
 controller/neighbor-exchange-netlink.h |  11 +
 controller/neighbor-exchange-stub.c    |  10 +
 controller/neighbor-exchange.c         | 308 +++++++++++++++++++++++--
 controller/neighbor-exchange.h         |   8 +
 controller/neighbor.c                  | 185 ++++++++++++---
 controller/neighbor.h                  |  34 +++
 controller/netlink-utils.c             | 148 ++++++++++++
 controller/netlink-utils.h             |  42 ++++
 controller/ovn-controller.c            |  20 ++
 controller/physical.c                  | 157 -------------
 controller/route-exchange-netlink.c    |  45 +---
 controller/route-exchange-netlink.h    |   8 -
 controller/route-exchange.c            |   1 +
 lib/ovn-util.c                         | 157 +++++++++++++
 lib/ovn-util.h                         |  26 ++-
 northd/en-datapath-logical-switch.c    |  14 ++
 tests/automake.mk                      |   2 +
 tests/system-ovn.at                    | 179 ++++++++++++++
 23 files changed, 1217 insertions(+), 266 deletions(-)
 create mode 100644 controller/netlink-utils.c
 create mode 100644 controller/netlink-utils.h

diff --git a/NEWS b/NEWS
index 68cca917e..060ab1359 100644
--- a/NEWS
+++ b/NEWS
@@ -61,6 +61,13 @@ Post v26.03.0
      (type 11) and Parameter Problem (type 12) - generated by an external
      router are un-NATed correctly.  This makes Path MTU discovery and
      traceroute work through stateless NAT.
+   - Add other_config "dynamic-routing-maintain-evpn" option for Logical
+     Switches. When set to "mvd", ovn-controller creates and maintains
+     Linux kernel interfaces required for EVPN/VXLAN itself: VRF, bridge,
+     VXLAN device and, for FDB redistribution, a dummy interface.
+     Other_config "dynamic-routing-maintain-evpn-lrp" option names a Logical
+     Router Port whose MAC applied to the bridge interface for router port
+     MAC-IP redistribution support.
 
 OVN v26.03.0 - xxx xx xxxx
 --------------------------
diff --git a/controller/automake.mk b/controller/automake.mk
index a779250cb..dfbb1d4ae 100644
--- a/controller/automake.mk
+++ b/controller/automake.mk
@@ -73,6 +73,8 @@ controller_ovn_controller_SOURCES = \
 
 if HAVE_NETLINK
 controller_ovn_controller_SOURCES += \
+       controller/netlink-utils.h \
+       controller/netlink-utils.c \
        controller/host-if-monitor.c \
        controller/ovn-netlink-notifier.c \
        controller/neighbor-exchange-netlink.h \
diff --git a/controller/host-if-monitor.c b/controller/host-if-monitor.c
index 65110d2a3..bbd86aaa5 100644
--- a/controller/host-if-monitor.c
+++ b/controller/host-if-monitor.c
@@ -65,6 +65,21 @@ host_if_monitor_run(void)
     return monitor.changes_detected;
 }
 
+void
+host_if_monitor_invalidate(const char *if_name)
+{
+    if (!sset_contains(&monitor.watched_interfaces, if_name)) {
+        return;
+    }
+
+    int32_t ifindex = if_nametoindex(if_name);
+    if (ifindex) {
+        simap_put(&monitor.ifname_to_ifindex, if_name, ifindex);
+    } else {
+        simap_find_and_delete(&monitor.ifname_to_ifindex, if_name);
+    }
+}
+
 void
 host_if_monitor_update_watches(const struct sset *if_names)
 {
diff --git a/controller/host-if-monitor.h b/controller/host-if-monitor.h
index a41c5869c..f625d839c 100644
--- a/controller/host-if-monitor.h
+++ b/controller/host-if-monitor.h
@@ -22,7 +22,7 @@
 
 void host_if_monitor_wait(void);
 bool host_if_monitor_run(void);
-
+void host_if_monitor_invalidate(const char *if_name);
 void host_if_monitor_update_watches(const struct sset *if_names);
 
 int32_t host_if_monitor_ifname_toindex(const char *if_name);
diff --git a/controller/neighbor-exchange-netlink.c 
b/controller/neighbor-exchange-netlink.c
index f01bfda5b..23df6d501 100644
--- a/controller/neighbor-exchange-netlink.c
+++ b/controller/neighbor-exchange-netlink.c
@@ -14,8 +14,11 @@
  */
 
 #include <config.h>
+#include <errno.h>
 #include <stdbool.h>
+
 #include <linux/if_ether.h>
+#include <linux/if_link.h>
 #include <linux/rtnetlink.h>
 
 #include "hmapx.h"
@@ -23,20 +26,13 @@
 #include "lib/netlink-socket.h"
 #include "lib/packets.h"
 #include "openvswitch/vlog.h"
+#include "netlink-utils.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. */
@@ -535,3 +531,93 @@ ne_table_parse(struct ofpbuf *buf, void *change)
     return ne_table_parse__(buf, NLMSG_HDRLEN + sizeof *nd,
                             nlmsg, nd, change);
 }
+
+int32_t
+ne_nl_ifindex_get(const char *ifname)
+{
+    return nl_ifindex_get(ifname);
+}
+
+int
+ne_nl_create_bridge(const char *ifname)
+{
+    return nl_create_device(ifname, "bridge");
+}
+
+int
+ne_nl_create_lo(const char *ifname)
+{
+    return nl_create_device(ifname, "dummy");
+}
+
+int
+ne_nl_create_vxlan(const char *ifname, uint32_t vni,
+                   const struct in6_addr *local_ip, uint16_t dst_port,
+                   int32_t link_ifindex)
+{
+    int err = 0;
+    struct ifinfomsg *ifinfo;
+    struct ofpbuf request;
+    ofpbuf_init(&request, 0);
+
+    nl_msg_put_nlmsghdr(&request, 0, RTM_NEWLINK,
+                        NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL);
+    ifinfo = ofpbuf_put_zeros(&request, sizeof *ifinfo);
+    nl_msg_put_string(&request, IFLA_IFNAME, ifname);
+
+    ifinfo->ifi_change = ifinfo->ifi_flags = IFF_UP;
+    size_t linkinfo_off = nl_msg_start_nested(&request, IFLA_LINKINFO);
+    nl_msg_put_string(&request, IFLA_INFO_KIND, "vxlan");
+    size_t infodata_off = nl_msg_start_nested(&request, IFLA_INFO_DATA);
+
+    nl_msg_put_u32(&request, IFLA_VXLAN_ID, vni);
+    nl_msg_put_u32(&request, IFLA_VXLAN_LINK, link_ifindex);
+    nl_msg_put_u8(&request, IFLA_VXLAN_LEARNING, 0);
+    nl_msg_put_be16(&request, IFLA_VXLAN_PORT, htons(dst_port));
+
+    if (IN6_IS_ADDR_V4MAPPED(local_ip)) {
+        ovs_be32 ipv4 = in6_addr_get_mapped_ipv4(local_ip);
+        nl_msg_put_be32(&request, IFLA_VXLAN_LOCAL, ipv4);
+    } else {
+        nl_msg_put_in6_addr(&request, IFLA_VXLAN_LOCAL6, local_ip);
+    }
+
+    nl_msg_end_nested(&request, infodata_off);
+    nl_msg_end_nested(&request, linkinfo_off);
+
+    err = nl_transact(NETLINK_ROUTE, &request, NULL);
+    ofpbuf_uninit(&request);
+    return err;
+}
+
+int
+ne_nl_set_master(const char *slave, const char *master)
+{
+    return nl_set_master(slave, master);
+}
+
+int
+ne_nl_set_iface_mac_addr(const char *ifname, const struct eth_addr *mac)
+{
+    return nl_set_iface_mac(ifname, mac);
+}
+
+int
+ne_nl_delete_iface(const char *ifname)
+{
+    return nl_delete_device(ifname);
+}
+
+int
+ne_nl_create_vrf(const char *ifname, uint32_t table_id)
+{
+    if (!TABLE_ID_VALID(table_id)) {
+        VLOG_WARN_RL(&rl,
+                     "attempt to create VRF using invalid table id %"PRIu32,
+                     table_id);
+        return EINVAL;
+    }
+
+    return nl_create_vrf(ifname, table_id);
+}
+
diff --git a/controller/neighbor-exchange-netlink.h 
b/controller/neighbor-exchange-netlink.h
index 659fe72d8..37badb226 100644
--- a/controller/neighbor-exchange-netlink.h
+++ b/controller/neighbor-exchange-netlink.h
@@ -50,6 +50,17 @@ struct ne_table_msg {
     struct ne_nl_received_neigh nd; /* Data parsed from this message. */
 };
 
+int32_t ne_nl_ifindex_get(const char *ifname);
+int ne_nl_create_vxlan(const char *ifname, uint32_t vni,
+                       const struct in6_addr *local_ip, uint16_t dst_port,
+                       int32_t link_ifindex);
+int ne_nl_create_bridge(const char *ifname);
+int ne_nl_create_lo(const char *ifname);
+int ne_nl_create_vrf(const char *ifname, uint32_t table_id);
+int ne_nl_set_master(const char *slave, const char *master);
+int ne_nl_set_iface_mac_addr(const char *ifname, const struct eth_addr *mac);
+int ne_nl_delete_iface(const char *ifname);
+
 int ne_nl_sync_neigh(uint8_t family, int32_t if_index,
                      const struct hmap *neighbors,
                      struct vector *learned_neighbors);
diff --git a/controller/neighbor-exchange-stub.c 
b/controller/neighbor-exchange-stub.c
index a1c89ed2b..5a0912b73 100644
--- a/controller/neighbor-exchange-stub.c
+++ b/controller/neighbor-exchange-stub.c
@@ -45,3 +45,13 @@ void
 evpn_static_entries_clear(struct hmap *static_entries OVS_UNUSED)
 {
 }
+
+void
+neighbor_exchange_maintain_evpn_cleanup_all(void)
+{
+}
+
+void
+neighbor_exchange_maintain_evpn_destroy(void)
+{
+}
diff --git a/controller/neighbor-exchange.c b/controller/neighbor-exchange.c
index 43626cc21..f271cdf67 100644
--- a/controller/neighbor-exchange.c
+++ b/controller/neighbor-exchange.c
@@ -15,12 +15,16 @@
 
 #include <config.h>
 
+#include <errno.h>
 #include <linux/neighbour.h>
+#include <net/if.h>
 
 #include "host-if-monitor.h"
+#include "lib/sset.h"
 #include "neighbor.h"
 #include "neighbor-exchange.h"
 #include "neighbor-exchange-netlink.h"
+#include "route-exchange-netlink.h"
 #include "openvswitch/poll-loop.h"
 #include "openvswitch/vlog.h"
 #include "ovn-util.h"
@@ -29,23 +33,7 @@
 #include "unixctl.h"
 
 VLOG_DEFINE_THIS_MODULE(neighbor_exchange);
-
-static uint32_t evpn_remote_vtep_hash(const struct in6_addr *ip,
-                                      uint16_t port, uint32_t vni);
-static void evpn_remote_vtep_add(struct hmap *remote_vteps, struct in6_addr ip,
-                                 uint16_t port, uint32_t vni);
-static struct evpn_remote_vtep *evpn_remote_vtep_find(
-    const struct hmap *remote_vteps, const struct in6_addr *ip,
-    uint16_t port, uint32_t vni);
-static void evpn_static_entry_add(struct hmap *static_entries,
-                                  struct eth_addr mac, struct in6_addr ip,
-                                  uint32_t vni, uint32_t nh_id);
-static struct evpn_static_entry *evpn_static_entry_find(
-    const struct hmap *static_entries, struct eth_addr mac,
-    struct in6_addr ip, uint32_t vni, uint32_t nh_id);
-static uint32_t evpn_static_entry_hash(const struct eth_addr *mac,
-                                       const struct in6_addr *ip,
-                                       uint32_t vni, uint32_t nh_id);
+static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
 
 /* Last neighbor_exchange netlink operation. */
 static int neighbor_exchange_nl_status;
@@ -65,10 +53,296 @@ static int neighbor_exchange_nl_status;
         }                                          \
     } while (0)
 
+struct maintained_evpn_entry {
+    struct hmap_node node;
+    struct neighbor_ovn_maintain_entry entry;
+};
+
+static struct hmap maintained_evpn_entries =
+    HMAP_INITIALIZER(&maintained_evpn_entries);
+
+static struct maintained_evpn_entry *
+maintained_evpn_entry_find(const struct hmap *entries, uint32_t vni)
+{
+    struct maintained_evpn_entry *me;
+    HMAP_FOR_EACH_WITH_HASH (me, node, hash_int(vni, 0), entries) {
+        if (me->entry.vni == vni) {
+            return me;
+        }
+    }
+
+    return NULL;
+}
+
+static void
+evpn_delete_evpn_devices(struct neighbor_ovn_maintain_entry *entry)
+{
+    int error;
+
+    error = ne_nl_delete_iface(entry->br_if_name);
+    if (error && error != ENODEV) {
+        VLOG_WARN_RL(&rl, "Unable to delete bridge interface %s: %s",
+                     entry->br_if_name, ovs_strerror(error));
+        SET_NEIGHBOR_EXCHANGE_NL_STATUS(error);
+    }
+
+    error = ne_nl_delete_iface(entry->vxlan_if_name);
+    if (error && error != ENODEV) {
+        VLOG_WARN_RL(&rl, "Unable to delete VXLAN interface %s: %s",
+                     entry->vxlan_if_name, ovs_strerror(error));
+        SET_NEIGHBOR_EXCHANGE_NL_STATUS(error);
+    }
+
+    error = ne_nl_delete_iface(entry->vrf_if_name);
+    if (error && error != ENODEV) {
+        VLOG_WARN_RL(&rl, "Unable to delete vrf interface %s: %s",
+                     entry->vrf_if_name, ovs_strerror(error));
+        SET_NEIGHBOR_EXCHANGE_NL_STATUS(error);
+    }
+
+    error = ne_nl_delete_iface(entry->lo_if_name);
+    if (error && error != ENODEV) {
+        VLOG_WARN_RL(&rl, "Unable to delete dummy interface %s: %s",
+                     entry->lo_if_name, ovs_strerror(error));
+        SET_NEIGHBOR_EXCHANGE_NL_STATUS(error);
+    }
+
+    /* Refresh the host-if-monitor ifindex cache for the interfaces we
+     * monitor for neighbor sync, so a lookup later in this same engine
+     * iteration doesn't use a stale ifindex. */
+    host_if_monitor_invalidate(entry->br_if_name);
+    host_if_monitor_invalidate(entry->vxlan_if_name);
+    host_if_monitor_invalidate(entry->lo_if_name);
+}
+
+static void
+set_bridge_evpn_device_addr(struct neighbor_ovn_maintain_entry *entry)
+{
+    int err;
+    if (entry->br_config.has_addr) {
+        err = ne_nl_set_iface_mac_addr(entry->br_if_name,
+                                       &entry->br_config.lladdr);
+        if (err) {
+            VLOG_WARN_RL(&rl, "Unable set mac address on interface %s: %s",
+                         entry->br_if_name, ovs_strerror(err));
+            SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
+        }
+    }
+}
+
+static void
+evpn_create_devices(struct neighbor_ovn_maintain_entry *entry)
+{
+    static char *link_dev = "vxlan_sys_4789";
+    int32_t link_ifindex = ne_nl_ifindex_get(link_dev);
+    int err;
+
+    if (!link_ifindex) {
+        err = ENODEV;
+        VLOG_WARN_RL(&rl,
+                     "Unable to find OVS system vxlan interface");
+        SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
+    }
+
+    if (entry->br_config.has_addr
+        && nrm_mode_IP_is_set(entry->redistribute_mode)) {
+        err = ne_nl_create_vrf(entry->vrf_if_name, entry->vni);
+        if (err && err != EEXIST) {
+            VLOG_WARN_RL(&rl,
+                         "Unable to create VRF %s for datapath %s",
+                         entry->vrf_if_name, ovs_strerror(err));
+            SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
+        }
+    }
+
+    err = ne_nl_create_vxlan(entry->vxlan_if_name, entry->vni,
+                             &entry->local_ip, entry->fake_vxlan_port,
+                             link_ifindex);
+    if (err && err != EEXIST) {
+        VLOG_WARN_RL(&rl,
+                     "Unable to create VXLAN interface %s for "
+                     "VNI %"PRIu32": %s",
+                     entry->vxlan_if_name, entry->vni, ovs_strerror(err));
+        SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
+    }
+
+    err = ne_nl_create_bridge(entry->br_if_name);
+    if (err && err != EEXIST) {
+        VLOG_WARN_RL(&rl,
+                     "Unable to create bridge interface %s for "
+                     "VNI %"PRIu32": %s",
+                     entry->br_if_name, entry->vni, ovs_strerror(err));
+        SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
+    }
+
+    set_bridge_evpn_device_addr(entry);
+
+    if (entry->br_config.has_addr
+        && nrm_mode_IP_is_set(entry->redistribute_mode)) {
+        err = ne_nl_set_master(entry->br_if_name, entry->vrf_if_name);
+        if (err) {
+            VLOG_WARN_RL(&rl, "Unable to enslave %s to bridge %s: %s",
+                         entry->br_if_name, entry->vrf_if_name,
+                         ovs_strerror(err));
+            SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
+        }
+    }
+
+    if (nrm_mode_FDB_is_set(entry->redistribute_mode)) {
+        err = ne_nl_create_lo(entry->lo_if_name);
+        if (err && err != EEXIST) {
+            VLOG_WARN_RL(&rl,
+                         "Unable to create dummy interface %s for "
+                         "VNI %"PRIu32": %s",
+                         entry->lo_if_name, entry->vni, ovs_strerror(err));
+            SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
+        }
+        err = ne_nl_set_master(entry->lo_if_name, entry->br_if_name);
+        if (err) {
+            VLOG_WARN_RL(&rl, "Unable to enslave %s to bridge %s: %s",
+                         entry->lo_if_name, entry->br_if_name,
+                         ovs_strerror(err));
+            SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
+        }
+    }
+
+    err = ne_nl_set_master(entry->vxlan_if_name, entry->br_if_name);
+    if (err) {
+        VLOG_WARN_RL(&rl, "Unable to enslave %s to bridge %s: %s",
+                     entry->vxlan_if_name, entry->br_if_name,
+                     ovs_strerror(err));
+        SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
+    }
+}
+
+/* Returns true if a change between 'old_entry' and 'entry' requires
+ * deleting and recreating the whole VRF/bridge/VXLAN/lo interface stack,
+ * rather than updating it in place.  This is the case when:
+ *
+ * - The VXLAN device's local IP or UDP destination port changed: Netlink
+ *   offers no way to change either on an existing VXLAN interface.
+ *
+ * - The bridge gained or lost its L3 address ('has_addr'): this changes
+ *   whether a VRF is needed at all.
+ *
+ * - The redistribution mode changed (FDB and/or IP bits): this changes
+ *   whether the loopback advertise interface is needed at all.
+ *
+ * Recreating the whole stack in these cases is simpler and less
+ * error-prone than trying to patch each interface individually. */
+static bool
+should_destroy_interfaces(const struct neighbor_ovn_maintain_entry *entry,
+                          const struct neighbor_ovn_maintain_entry *old_entry)
+{
+    return !ipv6_addr_equals(&old_entry->local_ip, &entry->local_ip) ||
+           old_entry->fake_vxlan_port != entry->fake_vxlan_port ||
+           old_entry->br_config.has_addr != entry->br_config.has_addr ||
+           old_entry->redistribute_mode != entry->redistribute_mode ||
+           (entry->br_config.has_addr
+           && !eth_addr_equals(old_entry->br_config.lladdr,
+                               entry->br_config.lladdr));
+}
+
+static void
+maintain_evpn_devices(struct neighbor_ovn_maintain_entry *entry,
+                        struct neighbor_ovn_maintain_entry *old_entry)
+{
+    if (!old_entry) {
+        evpn_create_devices(entry);
+        return;
+    }
+
+    if (should_destroy_interfaces(entry, old_entry)) {
+        /* Something changed that cannot be updated without recreation:
+         *  - local IP,
+         *  - VXLAN port
+         *  - has_addr var or redistribute_mode)
+         * tear down and recreate interfaces. */
+        evpn_delete_evpn_devices(entry);
+        evpn_create_devices(entry);
+        return;
+    }
+}
+
+static void
+neighbor_exchange_maintain_evpn_run(const struct vector *maintain_evpn)
+{
+    CLEAR_NEIGHBOR_EXCHANGE_NL_STATUS();
+
+    struct hmap old_maintained_evpn_entries =
+        HMAP_INITIALIZER(&old_maintained_evpn_entries);
+    hmap_swap(&maintained_evpn_entries, &old_maintained_evpn_entries);
+
+    struct neighbor_ovn_maintain_entry *entry;
+    VECTOR_FOR_EACH_PTR (maintain_evpn, entry) {
+        struct maintained_evpn_entry *old_me =
+            maintained_evpn_entry_find(&old_maintained_evpn_entries,
+                                        entry->vni);
+
+        maintain_evpn_devices(entry, old_me ? &old_me->entry : NULL);
+
+        struct maintained_evpn_entry *me;
+        if (old_me) {
+            hmap_remove(&old_maintained_evpn_entries, &old_me->node);
+            me = old_me;
+        } else {
+            me = xmalloc(sizeof *me);
+        }
+        me->entry = *entry;
+        hmap_insert(&maintained_evpn_entries, &me->node,
+                    hash_int(entry->vni, 0));
+    }
+
+    struct maintained_evpn_entry *stale_me;
+    HMAP_FOR_EACH_POP (stale_me, node, &old_maintained_evpn_entries) {
+        evpn_delete_evpn_devices(&stale_me->entry);
+        free(stale_me);
+    }
+    hmap_destroy(&old_maintained_evpn_entries);
+}
+
+void
+neighbor_exchange_maintain_evpn_cleanup_all(void)
+{
+    struct maintained_evpn_entry *me;
+    HMAP_FOR_EACH (me, node, &maintained_evpn_entries) {
+        evpn_delete_evpn_devices(&me->entry);
+    }
+}
+
+void
+neighbor_exchange_maintain_evpn_destroy(void)
+{
+    struct maintained_evpn_entry *me;
+    HMAP_FOR_EACH_POP (me, node, &maintained_evpn_entries) {
+        free(me);
+    }
+    hmap_destroy(&maintained_evpn_entries);
+}
+
+static uint32_t evpn_remote_vtep_hash(const struct in6_addr *ip,
+                                      uint16_t port, uint32_t vni);
+static void evpn_remote_vtep_add(struct hmap *remote_vteps, struct in6_addr ip,
+                                 uint16_t port, uint32_t vni);
+static struct evpn_remote_vtep *evpn_remote_vtep_find(
+    const struct hmap *remote_vteps, const struct in6_addr *ip,
+    uint16_t port, uint32_t vni);
+static void evpn_static_entry_add(struct hmap *static_entries,
+                                  struct eth_addr mac, struct in6_addr ip,
+                                  uint32_t vni, uint32_t nh_id);
+static struct evpn_static_entry *evpn_static_entry_find(
+    const struct hmap *static_entries, struct eth_addr mac,
+    struct in6_addr ip, uint32_t vni, uint32_t nh_id);
+static uint32_t evpn_static_entry_hash(const struct eth_addr *mac,
+                                       const struct in6_addr *ip,
+                                       uint32_t vni, uint32_t nh_id);
+
 void
 neighbor_exchange_run(const struct neighbor_exchange_ctx_in *n_ctx_in,
                       struct neighbor_exchange_ctx_out *n_ctx_out)
 {
+    neighbor_exchange_maintain_evpn_run(n_ctx_in->maintain_evpn);
+
     struct neighbor_interface_monitor *nim;
 
     struct sset if_names = SSET_INITIALIZER(&if_names);
diff --git a/controller/neighbor-exchange.h b/controller/neighbor-exchange.h
index 456ff23df..e0098b2c7 100644
--- a/controller/neighbor-exchange.h
+++ b/controller/neighbor-exchange.h
@@ -27,6 +27,9 @@ struct unixctl_conn;
 struct neighbor_exchange_ctx_in {
     /* Contains struct neighbor_interface_monitor pointers. */
     const struct vector *monitored_interfaces;
+    /* Contains struct neighbor_ovn_maintain_entry for VNIs whose kernel
+     * interfaces OVN must create/maintain.  May be NULL. */
+    const struct vector *maintain_evpn;
 };
 
 struct neighbor_exchange_ctx_out {
@@ -73,4 +76,9 @@ void evpn_remote_vtep_list(struct unixctl_conn *, int argc,
                            const char *argv[], void *data_);
 void evpn_static_entries_clear(struct hmap *static_entries);
 
+/* EVPN kernel interface lifecycle.  Call cleanup_all on graceful shutdown,
+ * destroy to release module-level state. */
+void neighbor_exchange_maintain_evpn_cleanup_all(void);
+void neighbor_exchange_maintain_evpn_destroy(void);
+
 #endif  /* NEIGHBOR_EXCHANGE_H */
diff --git a/controller/neighbor.c b/controller/neighbor.c
index 72fabe205..05873c445 100644
--- a/controller/neighbor.c
+++ b/controller/neighbor.c
@@ -27,6 +27,7 @@
 #include "neighbor.h"
 
 VLOG_DEFINE_THIS_MODULE(neighbor);
+static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
 
 static const char *neighbor_opt_name[] = {
     [NEIGH_IFACE_BRIDGE] = "dynamic-routing-bridge-ifname",
@@ -87,13 +88,111 @@ neigh_parse_device_name(struct sset *device_names, struct 
local_datapath *ld,
                                      neighbor_opt_name[type], "");
     sset_from_delimited_string(device_names, names, ",");
     if (sset_is_empty(device_names)) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
         VLOG_WARN_RL(&rl, "Datapath "UUID_FMT" misses %s",
                      UUID_ARGS(&ld->datapath->header_.uuid),
                      neighbor_opt_name[type]);
     }
 }
 
+static bool
+create_maintain_evpn_entry(const struct neighbor_ctx_in *n_ctx_in,
+                           const struct local_datapath *ld, uint32_t vni,
+                           enum neigh_redistribute_mode redistribute_mode,
+                           enum neighbor_mainatain_mode mainatain_mode,
+                           struct neighbor_ovn_maintain_entry *entry)
+{
+    if (vni > UINT16_MAX - 60000) {
+        VLOG_WARN_RL(&rl, "dynamic-routing-vni %"PRIu32" for datapath "
+                     UUID_FMT" is too large for dynamic-routing-maintain-evpn "
+                     "(max supported VNI is %u)",
+                     vni, UUID_ARGS(&ld->datapath->header_.uuid),
+                     UINT16_MAX - 60000);
+        return false;
+    }
+
+    *entry = (struct neighbor_ovn_maintain_entry) {
+        .redistribute_mode = redistribute_mode,
+        .mainatain_mode = mainatain_mode,
+        .vni = vni,
+        .fake_vxlan_port = 60000 + vni,
+    };
+
+    snprintf(entry->vxlan_if_name, sizeof entry->vxlan_if_name,
+             "ovnvxlan%"PRIu32, vni);
+    snprintf(entry->br_if_name, sizeof entry->br_if_name,
+             "ovnbr%"PRIu32, vni);
+    snprintf(entry->lo_if_name, sizeof entry->lo_if_name,
+             "ovnlo%"PRIu32, vni);
+    snprintf(entry->vrf_if_name, sizeof entry->vrf_if_name,
+             "ovnvrf%"PRIu32, vni);
+
+    const struct in6_addr *local_ip = NULL;
+    local_ip = evpn_local_ip_map_lookup(n_ctx_in->evpn_local_ip_map,
+                                        vni, true);
+    if (!local_ip) {
+        local_ip = evpn_local_ip_map_lookup(n_ctx_in->evpn_local_ip_map,
+                                            vni, false);
+    }
+
+    if (local_ip) {
+        entry->local_ip = *local_ip;
+    } else {
+        VLOG_WARN_RL(&rl, "dynamic-routing-maintain-evpn set for datapath "
+                     UUID_FMT" VNI %"PRIu32" but no ovn-evpn-local-ip "
+                     "configured",
+                     UUID_ARGS(&ld->datapath->header_.uuid), vni);
+        return false;
+    }
+
+    if (nrm_mode_IP_is_set(redistribute_mode)) {
+        const char *lrp_name = smap_get(&ld->datapath->external_ids,
+                                        "dynamic-routing-maintain-evpn-lrp");
+        if (!lrp_name) {
+            VLOG_WARN_RL(&rl, "dynamic-routing-maintain-evpn=mvd is set with "
+                         "IP redistribution on datapath "UUID_FMT" but "
+                         "dynamic-routing-maintain-evpn-lrp is not configured",
+                         UUID_ARGS(&ld->datapath->header_.uuid));
+            return false;
+        }
+
+        const struct sbrec_port_binding *pb =
+            lport_lookup_by_name(n_ctx_in->sbrec_pb_by_name, lrp_name);
+        if (!pb) {
+            VLOG_WARN_RL(&rl, "dynamic-routing-evpn-lrp: port \"%s\" "
+                              "not found", lrp_name);
+            return false;
+        }
+
+        entry->br_config.has_addr =
+            extract_sbrec_binding_first_mac(pb, &entry->br_config.lladdr);
+        if (!entry->br_config.has_addr) {
+            VLOG_WARN_RL(&rl, "Unable to parse mac address on router "
+                         "port %s for configuring EVPN devices", lrp_name);
+            return false;
+        }
+    }
+
+    return true;
+}
+
+static enum neighbor_mainatain_mode
+parse_neigh_maintain_mode(const struct smap *external_ids)
+{
+    const char *val = smap_get(external_ids, "dynamic-routing-maintain-evpn");
+    if (!val) {
+        return NEIGH_MAINTAIN_NONE;
+    }
+
+    if (!strcmp(val, "mvd")) {
+        return NEIGH_MAINTAIN_MVD;
+    }
+
+    VLOG_WARN_RL(&rl, "dynamic-routing-maintain-evpn: unknown value \"%s\", "
+                 "expected \"mvd\"", val);
+
+    return NEIGH_MAINTAIN_NONE;
+}
+
 void
 neighbor_run(struct neighbor_ctx_in *n_ctx_in,
              struct neighbor_ctx_out *n_ctx_out)
@@ -115,26 +214,51 @@ neighbor_run(struct neighbor_ctx_in *n_ctx_in,
             continue;
         }
 
+        enum neigh_redistribute_mode redistribute_mode =
+            parse_neigh_dynamic_redistribute(&ld->datapath->external_ids);
+
+        enum neighbor_mainatain_mode mainatain_mode =
+            parse_neigh_maintain_mode(&ld->datapath->external_ids);
+
+        struct neighbor_ovn_maintain_entry maintain_entry;
+        if (mainatain_mode != NEIGH_MAINTAIN_NONE) {
+            if (create_maintain_evpn_entry(n_ctx_in, ld, (uint32_t) vni,
+                                           redistribute_mode, mainatain_mode,
+                                           &maintain_entry)) {
+                vector_push(n_ctx_out->maintain_evpn, &maintain_entry);
+            } else {
+                continue;
+            }
+        }
+
         struct sset device_names;
-        neigh_parse_device_name(&device_names, ld, NEIGH_IFACE_VXLAN);
+        if (mainatain_mode != NEIGH_MAINTAIN_NONE) {
+            sset_init(&device_names);
+            sset_add(&device_names, maintain_entry.vxlan_if_name);
+        } else {
+            neigh_parse_device_name(&device_names, ld, NEIGH_IFACE_VXLAN);
+        }
         const char *name;
         SSET_FOR_EACH (name, &device_names) {
-            struct neighbor_interface_monitor *vxlan =
+            struct neighbor_interface_monitor *nim =
                 neighbor_interface_monitor_alloc(NEIGH_AF_BRIDGE,
                                                  NEIGH_IFACE_VXLAN, vni, name);
-            vector_push(n_ctx_out->monitored_interfaces, &vxlan);
+            vector_push(n_ctx_out->monitored_interfaces, &nim);
         }
         sset_destroy(&device_names);
 
         struct neighbor_interface_monitor *lo = NULL;
-        neigh_parse_device_name(&device_names, ld, NEIGH_IFACE_LOOPBACK);
-        if (sset_count(&device_names) > 1) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
-            VLOG_WARN_RL(&rl, "Datapath "UUID_FMT" too many names provided "
-                              "for loopback device",
-                         UUID_ARGS(&ld->datapath->header_.uuid));
+        if (mainatain_mode != NEIGH_MAINTAIN_NONE) {
+            sset_init(&device_names);
+            sset_add(&device_names, maintain_entry.lo_if_name);
+        } else {
+            neigh_parse_device_name(&device_names, ld, NEIGH_IFACE_LOOPBACK);
+            if (sset_count(&device_names) > 1) {
+                VLOG_WARN_RL(&rl, "Datapath "UUID_FMT" too many names for "
+                                  "loopback device",
+                             UUID_ARGS(&ld->datapath->header_.uuid));
+            }
         }
-
         if (!sset_is_empty(&device_names)) {
             lo = neighbor_interface_monitor_alloc(NEIGH_AF_BRIDGE,
                                                   NEIGH_IFACE_LOOPBACK, vni,
@@ -145,45 +269,42 @@ neighbor_run(struct neighbor_ctx_in *n_ctx_in,
 
         struct neighbor_interface_monitor *br_v4 = NULL;
         struct neighbor_interface_monitor *br_v6 = NULL;
-        neigh_parse_device_name(&device_names, ld, NEIGH_IFACE_BRIDGE);
-        if (sset_count(&device_names) > 1) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
-            VLOG_WARN_RL(&rl, "Datapath "UUID_FMT" too many names provided "
-                              "for bridge device",
-                         UUID_ARGS(&ld->datapath->header_.uuid));
+        if (mainatain_mode != NEIGH_MAINTAIN_NONE) {
+            sset_init(&device_names);
+            sset_add(&device_names, maintain_entry.br_if_name);
+        } else {
+            neigh_parse_device_name(&device_names, ld, NEIGH_IFACE_BRIDGE);
+            if (sset_count(&device_names) > 1) {
+                VLOG_WARN_RL(&rl, "Datapath "UUID_FMT" too many names for "
+                                  "bridge device",
+                             UUID_ARGS(&ld->datapath->header_.uuid));
+            }
         }
-
         if (!sset_is_empty(&device_names)) {
-            br_v4 =
-                neighbor_interface_monitor_alloc(NEIGH_AF_INET,
-                                                 NEIGH_IFACE_BRIDGE, vni,
-                                                 SSET_FIRST(&device_names));
+            br_v4 = neighbor_interface_monitor_alloc(
+                NEIGH_AF_INET, NEIGH_IFACE_BRIDGE, vni,
+                SSET_FIRST(&device_names));
             vector_push(n_ctx_out->monitored_interfaces, &br_v4);
-
-            br_v6 =
-                neighbor_interface_monitor_alloc(NEIGH_AF_INET6,
-                                                 NEIGH_IFACE_BRIDGE, vni,
-                                                 SSET_FIRST(&device_names));
+            br_v6 = neighbor_interface_monitor_alloc(
+                NEIGH_AF_INET6, NEIGH_IFACE_BRIDGE, vni,
+                SSET_FIRST(&device_names));
             vector_push(n_ctx_out->monitored_interfaces, &br_v6);
         }
         sset_destroy(&device_names);
 
-        enum neigh_redistribute_mode mode =
-            parse_neigh_dynamic_redistribute(&ld->datapath->external_ids);
-        if (nrm_mode_FDB_is_set(mode) && lo) {
+        if (nrm_mode_FDB_is_set(redistribute_mode) && lo) {
             neighbor_collect_mac_to_advertise(n_ctx_in,
                                               &lo->announced_neighbors,
                                               n_ctx_out->advertised_pbs,
                                               ld->datapath);
         }
-        if (nrm_mode_IP_is_set(mode) && br_v4 && br_v6) {
+        if (nrm_mode_IP_is_set(redistribute_mode) && br_v4 && br_v6) {
             neighbor_collect_ip_mac_to_advertise(n_ctx_in,
                                                  &br_v4->announced_neighbors,
                                                  &br_v6->announced_neighbors,
                                                  n_ctx_out->advertised_pbs,
                                                  ld->datapath);
         }
-
     }
 }
 
diff --git a/controller/neighbor.h b/controller/neighbor.h
index e3adc87d1..bb440535f 100644
--- a/controller/neighbor.h
+++ b/controller/neighbor.h
@@ -23,6 +23,7 @@
 
 #include "lib/sset.h"
 #include "openvswitch/hmap.h"
+#include "lib/ovn-util.h"
 
 #include "vec.h"
 
@@ -49,6 +50,7 @@ struct neighbor_ctx_in {
     /* Index for Port Binding by name. */
     struct ovsdb_idl_index *sbrec_pb_by_name;
     const struct sbrec_chassis *chassis;
+    struct evpn_local_ip_map *evpn_local_ip_map;
 };
 
 struct neighbor_ctx_out {
@@ -56,6 +58,10 @@ struct neighbor_ctx_out {
     struct vector *monitored_interfaces;
     /* Contains set of port binding names that are currently advertised. */
     struct sset *advertised_pbs;
+    /* Contains struct neighbor_ovn_maintain_entry for VNIs that need
+     * maintained interfaces.  May be NULL if caller does not need this. */
+    struct vector *maintain_evpn;
+    struct evpn_local_ip_map *evpn_ip_map;
 };
 
 enum neighbor_interface_type {
@@ -64,6 +70,34 @@ enum neighbor_interface_type {
     NEIGH_IFACE_LOOPBACK,
 };
 
+enum neighbor_mainatain_mode {
+    NEIGH_MAINTAIN_NONE,
+    NEIGH_MAINTAIN_MVD,
+};
+
+/* IP/MAC to configure on the bridge interface so BGP can use it as next-hop.
+ * Filled from the LRP named by dynamic-routing-evpn-lrp. */
+struct neighbor_bridge_iface_config {
+    bool has_addr;
+    struct eth_addr lladdr;
+    struct in6_addr addr;    /* IPv4 stored as mapped IPv4-in-IPv6. */
+    uint8_t prefix_len;
+};
+
+struct neighbor_ovn_maintain_entry {
+    enum neigh_redistribute_mode redistribute_mode;
+    enum neighbor_mainatain_mode mainatain_mode;
+    char vxlan_if_name[IFNAMSIZ + 1];
+    char vrf_if_name[IFNAMSIZ + 1];
+    char br_if_name[IFNAMSIZ + 1];
+    char lo_if_name[IFNAMSIZ + 1];
+    struct neighbor_bridge_iface_config br_config;
+    struct in6_addr local_ip;
+    uint32_t vni;
+    /* Formula: 60000 + vni (valid for VNI <= 5535). */
+    uint16_t fake_vxlan_port;
+};
+
 struct neighbor_interface_monitor {
     enum neighbor_family family;
     char if_name[IFNAMSIZ + 1];
diff --git a/controller/netlink-utils.c b/controller/netlink-utils.c
new file mode 100644
index 000000000..4055f8809
--- /dev/null
+++ b/controller/netlink-utils.c
@@ -0,0 +1,148 @@
+/* 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 <errno.h>
+#include <linux/if_ether.h>
+#include <linux/if_link.h>
+#include <linux/rtnetlink.h>
+
+#include "lib/netlink.h"
+#include "lib/netlink-socket.h"
+#include "lib/packets.h"
+
+#include "openvswitch/ofpbuf.h"
+
+#include "netlink-utils.h"
+
+int32_t
+nl_ifindex_get(const char *ifname)
+{
+    return (int32_t) if_nametoindex(ifname);
+}
+
+int
+nl_create_device(const char *ifname, const char *kind)
+{
+    struct ifinfomsg *ifinfo;
+    struct ofpbuf request;
+    ofpbuf_init(&request, 0);
+
+    nl_msg_put_nlmsghdr(&request, 0, RTM_NEWLINK,
+                        NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL);
+    ifinfo = ofpbuf_put_zeros(&request, sizeof *ifinfo);
+    nl_msg_put_string(&request, IFLA_IFNAME, ifname);
+    ifinfo->ifi_change = ifinfo->ifi_flags = IFF_UP;
+
+    size_t linkinfo_off = nl_msg_start_nested(&request, IFLA_LINKINFO);
+    nl_msg_put_string(&request, IFLA_INFO_KIND, kind);
+    nl_msg_end_nested(&request, linkinfo_off);
+
+    int err = nl_transact(NETLINK_ROUTE, &request, NULL);
+    ofpbuf_uninit(&request);
+    return err;
+}
+
+int
+nl_create_vrf(const char *ifname, uint32_t table_id)
+{
+    size_t linkinfo_off, infodata_off;
+    struct ifinfomsg *ifinfo;
+    int err;
+
+    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_NEWLINK,
+                        NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL);
+    ifinfo = ofpbuf_put_zeros(&request, sizeof *ifinfo);
+    nl_msg_put_string(&request, IFLA_IFNAME, ifname);
+
+    ifinfo->ifi_change = ifinfo->ifi_flags = IFF_UP;
+    linkinfo_off = nl_msg_start_nested(&request, IFLA_LINKINFO);
+    nl_msg_put_string(&request, IFLA_INFO_KIND, "vrf");
+    infodata_off = nl_msg_start_nested(&request, IFLA_INFO_DATA);
+    nl_msg_put_u32(&request, IFLA_VRF_TABLE, table_id);
+    nl_msg_end_nested(&request, infodata_off);
+    nl_msg_end_nested(&request, linkinfo_off);
+
+    err = nl_transact(NETLINK_ROUTE, &request, NULL);
+
+    ofpbuf_uninit(&request);
+    return err;
+}
+
+int
+nl_delete_device(const char *ifname)
+{
+    struct ifinfomsg *ifinfo;
+    int err;
+
+    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_DELLINK, NLM_F_REQUEST | NLM_F_ACK);
+    ifinfo = ofpbuf_put_zeros(&request, sizeof *ifinfo);
+    nl_msg_put_string(&request, IFLA_IFNAME, ifname);
+    err = nl_transact(NETLINK_ROUTE, &request, NULL);
+
+    ofpbuf_uninit(&request);
+    return err;
+}
+
+int
+nl_set_master(const char *slave, const char *master)
+{
+    int err = 0;
+    int32_t master_idx = nl_ifindex_get(master);
+    if (!master_idx) {
+        return ENODEV;
+    }
+
+    struct ofpbuf request;
+    ofpbuf_init(&request, 0);
+
+    nl_msg_put_nlmsghdr(&request, 0, RTM_NEWLINK,
+                        NLM_F_REQUEST | NLM_F_ACK);
+    ofpbuf_put_zeros(&request, sizeof(struct ifinfomsg));
+    nl_msg_put_string(&request, IFLA_IFNAME, slave);
+    nl_msg_put_u32(&request, IFLA_MASTER, master_idx);
+
+    err = nl_transact(NETLINK_ROUTE, &request, NULL);
+    ofpbuf_uninit(&request);
+    return err;
+}
+
+int
+nl_set_iface_mac(const char *ifname, const struct eth_addr *mac)
+{
+    int err = 0;
+    struct ofpbuf request;
+    ofpbuf_init(&request, 0);
+
+    nl_msg_put_nlmsghdr(&request, 0, RTM_NEWLINK,
+                        NLM_F_REQUEST | NLM_F_ACK);
+    ofpbuf_put_zeros(&request, sizeof(struct ifinfomsg));
+    nl_msg_put_string(&request, IFLA_IFNAME, ifname);
+    nl_msg_put_unspec(&request, IFLA_ADDRESS, mac, sizeof *mac);
+
+    err = nl_transact(NETLINK_ROUTE, &request, NULL);
+    ofpbuf_uninit(&request);
+    return err;
+}
diff --git a/controller/netlink-utils.h b/controller/netlink-utils.h
new file mode 100644
index 000000000..ab5424dfe
--- /dev/null
+++ b/controller/netlink-utils.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2025 Canonical, Ltd.
+ * Copyright (c) 2025, STACKIT GmbH & Co. KG
+ *
+ * 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 NETLINK_UTILS_H
+#define NETLINK_UTILS_H 1
+
+#include <stdint.h>
+#include <linux/rtnetlink.h>
+#include <netinet/in.h>
+#include <net/if.h>
+
+#define NETNL_REQ_BUFFER_SIZE 128
+
+#define TABLE_ID_VALID(table_id) (table_id != RT_TABLE_UNSPEC &&              \
+                                  table_id != RT_TABLE_COMPAT &&              \
+                                  table_id != RT_TABLE_DEFAULT &&             \
+                                  table_id != RT_TABLE_MAIN &&                \
+                                  table_id != RT_TABLE_LOCAL &&               \
+                                  table_id != RT_TABLE_MAX)
+
+int32_t nl_ifindex_get(const char *ifname);
+int nl_create_device(const char *ifname, const char *kind);
+int nl_create_vrf(const char *ifname, uint32_t table_id);
+int nl_delete_device(const char *ifname);
+int nl_set_iface_mac(const char *ifname,
+                     const struct eth_addr *mac);
+int nl_set_master(const char *slave, const char *master);
+#endif /* netlink-utils.h */
\ No newline at end of file
diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
index 552e87b53..4b1fcec26 100644
--- a/controller/ovn-controller.c
+++ b/controller/ovn-controller.c
@@ -6074,6 +6074,9 @@ struct ed_type_neighbor {
     struct vector monitored_interfaces;
     /* Contains set of PB names that are currently advertised. */
     struct sset advertised_pbs;
+    /* Contains struct neighbor_ovn_maintain_entry, one per VNI
+     * whose EVPN interfaces are auto-created and maintained. */
+    struct vector maintain_evpn;
 };
 
 static void *
@@ -6086,6 +6089,8 @@ en_neighbor_init(struct engine_node *node OVS_UNUSED,
         .monitored_interfaces =
             VECTOR_EMPTY_INITIALIZER(struct neighbor_interface_monitor *),
         .advertised_pbs = SSET_INITIALIZER(&data->advertised_pbs),
+        .maintain_evpn =
+            VECTOR_EMPTY_INITIALIZER(struct neighbor_ovn_maintain_entry),
     };
     return data;
 }
@@ -6097,6 +6102,7 @@ en_neighbor_cleanup(void *data)
 
     neighbor_cleanup(&ne_data->monitored_interfaces);
     vector_destroy(&ne_data->monitored_interfaces);
+    vector_destroy(&ne_data->maintain_evpn);
     sset_destroy(&ne_data->advertised_pbs);
 }
 
@@ -6131,22 +6137,32 @@ en_neighbor_run(struct engine_node *node OVS_UNUSED, 
void *data)
         chassis_lookup_by_name(sbrec_chassis_by_name, chassis_id);
     ovs_assert(chassis);
 
+    struct evpn_local_ip_map evpn_ip_map = {
+        .vni_ip4 = HMAP_INITIALIZER(&evpn_ip_map.vni_ip4),
+        .vni_ip6 = HMAP_INITIALIZER(&evpn_ip_map.vni_ip6),
+    };
+    evpn_local_ip_map_init(&evpn_ip_map, &chassis->other_config);
+
     struct neighbor_ctx_in n_ctx_in = {
         .local_datapaths = &rt_data->local_datapaths,
         .sbrec_pb_by_dp = sbrec_port_binding_by_datapath,
         .sbrec_amb_by_dp = sbrec_advertised_mac_binding_by_datapath,
         .sbrec_pb_by_name = sbrec_port_binding_by_name,
         .chassis = chassis,
+        .evpn_local_ip_map = &evpn_ip_map,
     };
 
     struct neighbor_ctx_out n_ctx_out = {
         .monitored_interfaces = &ne_data->monitored_interfaces,
         .advertised_pbs = &ne_data->advertised_pbs,
+        .maintain_evpn = &ne_data->maintain_evpn,
     };
 
     neighbor_cleanup(&ne_data->monitored_interfaces);
     sset_clear(&ne_data->advertised_pbs);
+    vector_clear(&ne_data->maintain_evpn);
     neighbor_run(&n_ctx_in, &n_ctx_out);
+    evpn_local_ip_map_destroy(&evpn_ip_map);
 
     return EN_UPDATED;
 }
@@ -6479,6 +6495,7 @@ en_neighbor_exchange_run(struct engine_node *node, void 
*data_)
 
     struct neighbor_exchange_ctx_in n_ctx_in = {
         .monitored_interfaces = &neighbor_data->monitored_interfaces,
+        .maintain_evpn = &neighbor_data->maintain_evpn,
     };
     struct neighbor_exchange_ctx_out n_ctx_out = {
         .neighbor_table_watches = &nt_notify->watches,
@@ -8458,6 +8475,8 @@ loop_done:
         route_exchange_cleanup_vrfs();
     }
 
+    neighbor_exchange_maintain_evpn_cleanup_all();
+
     /* The engine cleanup should happen only after threads have been
      * destroyed and joined in case they are accessing engine data. */
     pinctrl_destroy();
@@ -8494,6 +8513,7 @@ loop_done:
     dns_resolve_destroy();
     route_exchange_destroy();
     ovn_netlink_notifiers_destroy();
+    neighbor_exchange_maintain_evpn_destroy();
 
     exit(retval);
 }
diff --git a/controller/physical.c b/controller/physical.c
index 89ff2785e..0277d472d 100644
--- a/controller/physical.c
+++ b/controller/physical.c
@@ -3231,163 +3231,6 @@ physical_eval_remote_chassis_flows(const struct 
physical_ctx *ctx,
     ofpbuf_uninit(&ingress_ofpacts);
 }
 
-struct vni_local_ip {
-    struct hmap_node hmap_node;
-    struct in6_addr ip;
-    uint32_t vni;
-};
-
-struct evpn_local_ip_map {
-    struct hmap vni_ip4;         /* Per VNI local IPv4 vni_local_ips. */
-    struct hmap vni_ip6;         /* Per VNI local IPv6 vni_local_ips. */
-    struct in6_addr default_ip4; /* Default local IPv4. */
-    struct in6_addr default_ip6; /* Default local IPv6. */
-};
-
-static const struct in6_addr *
-evpn_local_ip_lookup(const struct hmap *map, uint32_t vni)
-{
-    struct vni_local_ip *e;
-    HMAP_FOR_EACH_WITH_HASH (e, hmap_node, hash_add(vni, 0), map) {
-        if (e->vni == vni) {
-            return &e->ip;
-        }
-    }
-    return NULL;
-}
-
-static ovs_be32
-evpn_local_ip_find_v4(const struct evpn_local_ip_map *vni_ip_map,
-                      uint32_t vni)
-{
-    const struct in6_addr *addr = evpn_local_ip_lookup(&vni_ip_map->vni_ip4,
-                                                       vni);
-    if (addr) {
-        return in6_addr_get_mapped_ipv4(addr);
-    }
-
-    if (ipv6_addr_is_set(&vni_ip_map->default_ip4)) {
-        return in6_addr_get_mapped_ipv4(&vni_ip_map->default_ip4);
-    }
-
-    return 0;
-}
-
-static const struct in6_addr *
-evpn_local_ip_find_v6(const struct evpn_local_ip_map *vni_ip_map,
-                      uint32_t vni)
-{
-    const struct in6_addr *addr = evpn_local_ip_lookup(&vni_ip_map->vni_ip6,
-                                                       vni);
-    if (addr) {
-        return addr;
-    }
-
-    if (ipv6_addr_is_set(&vni_ip_map->default_ip6)) {
-        return &vni_ip_map->default_ip6;
-    }
-
-    return NULL;
-}
-
-static void
-evpn_local_ip_map_init(struct evpn_local_ip_map *vni_ip_map,
-                       const struct smap *config)
-{
-    char *tokstr, *token, *ptr0 = NULL;
-
-    const char *local_ip_str = smap_get_def(config, "ovn-evpn-local-ip", "");
-    tokstr = xstrdup(local_ip_str);
-    for (token = strtok_r(tokstr, ",", &ptr0); token;
-         token = strtok_r(NULL, ",", &ptr0)) {
-        char *ptr1 = NULL, *vni_str = strtok_r(token, "-", &ptr1);
-        char *ip_str = strtok_r(NULL, "-", &ptr1);
-        struct in6_addr ip;
-        uint32_t vni;
-
-        if (ptr1 && *ptr1) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-            VLOG_WARN_RL(&rl, "Malformed 'ovn-evpn-local-ip': %s", tokstr);
-            break;
-        }
-
-        if (!ip_str) { /* default IP */
-            if (!ip46_parse(vni_str, &ip)) {
-                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-                VLOG_WARN_RL(&rl, "Invalid IP %s in 'ovn-evpn-local-ip'",
-                             vni_str);
-                continue;
-            }
-            struct in6_addr *ip_ptr = IN6_IS_ADDR_V4MAPPED(&ip)
-                                      ? &vni_ip_map->default_ip4
-                                      : &vni_ip_map->default_ip6;
-            if (ipv6_addr_is_set(ip_ptr)) {
-                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-                VLOG_WARN_RL(&rl, "Default ovn-evpn-local-ip IP%d "
-                                  "already configured",
-                             IN6_IS_ADDR_V4MAPPED(&ip) ? 4 : 6);
-                continue;
-            }
-            *ip_ptr = ip;
-        } else {
-            if (!ip46_parse(ip_str, &ip)) {
-                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-                VLOG_WARN_RL(&rl,
-                             "EVPN enabled, but required 'ovn-evpn-local-ip' "
-                             "is missing or invalid for vni %s", vni_str);
-                continue;
-            }
-
-            if (!vni_str) {
-                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-                VLOG_WARN_RL(&rl, "Required 'ovn-evpn-local-ip' VNI not "
-                                  "configured for IP %s",
-                             ip_str);
-                continue;
-            }
-
-            if (!ovs_scan(vni_str, "%u", &vni) || !ovn_is_valid_vni(vni)) {
-                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-                VLOG_WARN_RL(&rl, "Invalid VNI %s in 'ovn-evpn-local-ip'",
-                             vni_str);
-                continue;
-            }
-
-            struct hmap *map = IN6_IS_ADDR_V4MAPPED(&ip)
-                               ? &vni_ip_map->vni_ip4
-                               : &vni_ip_map->vni_ip6;
-            if (evpn_local_ip_lookup(map, vni)) {
-                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-                VLOG_WARN_RL(&rl, "Duplicated VNI entry: %s", vni_str);
-                continue;
-            }
-
-            struct vni_local_ip *e = xmalloc(sizeof *e);
-            *e = (struct vni_local_ip) {
-                .vni = vni,
-                .ip = ip,
-            };
-            hmap_insert(map, &e->hmap_node, hash_add(vni, 0));
-        }
-    }
-    free(tokstr);
-}
-
-static void
-evpn_local_ip_map_destroy(struct evpn_local_ip_map *map)
-{
-    struct vni_local_ip *e;
-    HMAP_FOR_EACH_POP (e, hmap_node, &map->vni_ip4) {
-        free(e);
-    }
-    hmap_destroy(&map->vni_ip4);
-
-    HMAP_FOR_EACH_POP (e, hmap_node, &map->vni_ip6) {
-        free(e);
-    }
-    hmap_destroy(&map->vni_ip6);
-}
-
 static void
 physical_consider_evpn_binding(const struct evpn_binding *binding,
                                const struct evpn_local_ip_map *vni_ip_map,
diff --git a/controller/route-exchange-netlink.c 
b/controller/route-exchange-netlink.c
index 8f1615c4e..e280fae1d 100644
--- a/controller/route-exchange-netlink.c
+++ b/controller/route-exchange-netlink.c
@@ -31,13 +31,12 @@
 #include "route-table.h"
 #include "route.h"
 #include "vec.h"
+#include "netlink-utils.h"
 
 #include "route-exchange-netlink.h"
 
 VLOG_DEFINE_THIS_MODULE(route_exchange_netlink);
 
-#define NETNL_REQ_BUFFER_SIZE 128
-
 static void re_nl_encode_nexthop(struct ofpbuf *, bool dst_is_ipv4,
                                  const struct in6_addr *);
 
@@ -51,51 +50,13 @@ re_nl_create_vrf(const char *ifname, uint32_t table_id)
                      table_id);
         return EINVAL;
     }
-
-    size_t linkinfo_off, infodata_off;
-    struct ifinfomsg *ifinfo;
-    int err;
-
-    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_NEWLINK,
-                        NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL);
-    ifinfo = ofpbuf_put_zeros(&request, sizeof *ifinfo);
-    nl_msg_put_string(&request, IFLA_IFNAME, ifname);
-
-    ifinfo->ifi_change = ifinfo->ifi_flags = IFF_UP;
-    linkinfo_off = nl_msg_start_nested(&request, IFLA_LINKINFO);
-    nl_msg_put_string(&request, IFLA_INFO_KIND, "vrf");
-    infodata_off = nl_msg_start_nested(&request, IFLA_INFO_DATA);
-    nl_msg_put_u32(&request, IFLA_VRF_TABLE, table_id);
-    nl_msg_end_nested(&request, infodata_off);
-    nl_msg_end_nested(&request, linkinfo_off);
-
-    err = nl_transact(NETLINK_ROUTE, &request, NULL);
-
-    ofpbuf_uninit(&request);
-    return err;
+    return nl_create_vrf(ifname, table_id);
 }
 
 int
 re_nl_delete_vrf(const char *ifname)
 {
-    struct ifinfomsg *ifinfo;
-    int err;
-
-    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_DELLINK, NLM_F_REQUEST | NLM_F_ACK);
-    ifinfo = ofpbuf_put_zeros(&request, sizeof *ifinfo);
-    nl_msg_put_string(&request, IFLA_IFNAME, ifname);
-    err = nl_transact(NETLINK_ROUTE, &request, NULL);
-
-    ofpbuf_uninit(&request);
-    return err;
+    return nl_delete_device(ifname);
 }
 
 void
diff --git a/controller/route-exchange-netlink.h 
b/controller/route-exchange-netlink.h
index f79ae05db..af3985f81 100644
--- a/controller/route-exchange-netlink.h
+++ b/controller/route-exchange-netlink.h
@@ -29,13 +29,6 @@
 #define RTPROT_OVN 84
 #endif
 
-#define TABLE_ID_VALID(table_id) (table_id != RT_TABLE_UNSPEC &&              \
-                                  table_id != RT_TABLE_COMPAT &&              \
-                                  table_id != RT_TABLE_DEFAULT &&             \
-                                  table_id != RT_TABLE_MAIN &&                \
-                                  table_id != RT_TABLE_LOCAL &&               \
-                                  table_id != RT_TABLE_MAX)
-
 struct in6_addr;
 struct hmap;
 struct vector;
@@ -51,7 +44,6 @@ struct re_nl_received_route_node {
 
 int re_nl_create_vrf(const char *ifname, uint32_t table_id);
 int re_nl_delete_vrf(const char *ifname);
-
 int re_nl_add_route(uint32_t table_id, const struct advertise_route_entry *);
 int re_nl_delete_route(uint32_t table_id,
                        const struct advertise_route_entry *);
diff --git a/controller/route-exchange.c b/controller/route-exchange.c
index b86eb43bf..879be9940 100644
--- a/controller/route-exchange.c
+++ b/controller/route-exchange.c
@@ -34,6 +34,7 @@
 #include "route.h"
 #include "route-exchange.h"
 #include "route-exchange-netlink.h"
+#include "netlink-utils.h"
 
 VLOG_DEFINE_THIS_MODULE(route_exchange);
 static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
diff --git a/lib/ovn-util.c b/lib/ovn-util.c
index 80346d5bb..6da3ae8fb 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -1894,3 +1894,160 @@ port_contains_duplicate_ip(struct lport_addresses 
*laddrs1,
 
     return false;
 }
+
+const struct in6_addr *
+evpn_local_ip_lookup(const struct hmap *map, uint32_t vni)
+{
+    struct vni_local_ip *e;
+    HMAP_FOR_EACH_WITH_HASH (e, hmap_node, hash_add(vni, 0), map) {
+        if (e->vni == vni) {
+            return &e->ip;
+        }
+    }
+    return NULL;
+}
+
+ovs_be32
+evpn_local_ip_find_v4(const struct evpn_local_ip_map *vni_ip_map,
+                      uint32_t vni)
+{
+    const struct in6_addr *addr = evpn_local_ip_lookup(&vni_ip_map->vni_ip4,
+                                                       vni);
+    if (addr) {
+        return in6_addr_get_mapped_ipv4(addr);
+    }
+
+    if (ipv6_addr_is_set(&vni_ip_map->default_ip4)) {
+        return in6_addr_get_mapped_ipv4(&vni_ip_map->default_ip4);
+    }
+
+    return 0;
+}
+
+const struct in6_addr *
+evpn_local_ip_find_v6(const struct evpn_local_ip_map *vni_ip_map,
+                      uint32_t vni)
+{
+    const struct in6_addr *addr = evpn_local_ip_lookup(&vni_ip_map->vni_ip6,
+                                                       vni);
+    if (addr) {
+        return addr;
+    }
+
+    if (ipv6_addr_is_set(&vni_ip_map->default_ip6)) {
+        return &vni_ip_map->default_ip6;
+    }
+
+    return NULL;
+}
+
+const struct in6_addr *
+evpn_local_ip_map_lookup(const struct evpn_local_ip_map *map,
+                         uint32_t vni, bool ipv4)
+{
+    const struct hmap *hmap = ipv4 ? &map->vni_ip4 : &map->vni_ip6;
+    const struct in6_addr *addr = evpn_local_ip_lookup(hmap, vni);
+    if (addr) {
+        return addr;
+    }
+    const struct in6_addr *def = ipv4 ? &map->default_ip4 : &map->default_ip6;
+    return ipv6_addr_is_set(def) ? def : NULL;
+}
+
+void
+evpn_local_ip_map_init(struct evpn_local_ip_map *vni_ip_map,
+                       const struct smap *config)
+{
+    char *tokstr, *token, *ptr0 = NULL;
+
+    const char *local_ip_str = smap_get_def(config, "ovn-evpn-local-ip", "");
+    tokstr = xstrdup(local_ip_str);
+    for (token = strtok_r(tokstr, ",", &ptr0); token;
+         token = strtok_r(NULL, ",", &ptr0)) {
+        char *ptr1 = NULL, *vni_str = strtok_r(token, "-", &ptr1);
+        char *ip_str = strtok_r(NULL, "-", &ptr1);
+        struct in6_addr ip;
+        uint32_t vni;
+
+        if (ptr1 && *ptr1) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "Malformed 'ovn-evpn-local-ip': %s", tokstr);
+            break;
+        }
+
+        if (!ip_str) { /* default IP */
+            if (!ip46_parse(vni_str, &ip)) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "Invalid IP %s in 'ovn-evpn-local-ip'",
+                             vni_str);
+                continue;
+            }
+            struct in6_addr *ip_ptr = IN6_IS_ADDR_V4MAPPED(&ip)
+                                      ? &vni_ip_map->default_ip4
+                                      : &vni_ip_map->default_ip6;
+            if (ipv6_addr_is_set(ip_ptr)) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "Default ovn-evpn-local-ip IP%d "
+                                  "already configured",
+                             IN6_IS_ADDR_V4MAPPED(&ip) ? 4 : 6);
+                continue;
+            }
+            *ip_ptr = ip;
+        } else {
+            if (!ip46_parse(ip_str, &ip)) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl,
+                             "EVPN enabled, but required 'ovn-evpn-local-ip' "
+                             "is missing or invalid for vni %s", vni_str);
+                continue;
+            }
+
+            if (!vni_str) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "Required 'ovn-evpn-local-ip' VNI not "
+                                  "configured for IP %s",
+                             ip_str);
+                continue;
+            }
+
+            if (!ovs_scan(vni_str, "%u", &vni) || !ovn_is_valid_vni(vni)) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "Invalid VNI %s in 'ovn-evpn-local-ip'",
+                             vni_str);
+                continue;
+            }
+
+            struct hmap *map = IN6_IS_ADDR_V4MAPPED(&ip)
+                               ? &vni_ip_map->vni_ip4
+                               : &vni_ip_map->vni_ip6;
+            if (evpn_local_ip_lookup(map, vni)) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "Duplicated VNI entry: %s", vni_str);
+                continue;
+            }
+
+            struct vni_local_ip *e = xmalloc(sizeof *e);
+            *e = (struct vni_local_ip) {
+                .vni = vni,
+                .ip = ip,
+            };
+            hmap_insert(map, &e->hmap_node, hash_add(vni, 0));
+        }
+    }
+    free(tokstr);
+}
+
+void
+evpn_local_ip_map_destroy(struct evpn_local_ip_map *map)
+{
+    struct vni_local_ip *e;
+    HMAP_FOR_EACH_POP (e, hmap_node, &map->vni_ip4) {
+        free(e);
+    }
+    hmap_destroy(&map->vni_ip4);
+
+    HMAP_FOR_EACH_POP (e, hmap_node, &map->vni_ip6) {
+        free(e);
+    }
+    hmap_destroy(&map->vni_ip6);
+}
diff --git a/lib/ovn-util.h b/lib/ovn-util.h
index b911a59b0..55ab372cf 100644
--- a/lib/ovn-util.h
+++ b/lib/ovn-util.h
@@ -124,7 +124,6 @@ bool extract_sbrec_binding_first_mac(const struct 
sbrec_port_binding *binding,
 
 bool extract_lrp_networks__(char *mac, char **networks, size_t n_networks,
                             struct lport_addresses *laddrs);
-
 bool lport_addresses_is_empty(const struct lport_addresses *);
 void init_lport_addresses(struct lport_addresses *);
 void destroy_lport_addresses(struct lport_addresses *);
@@ -830,4 +829,29 @@ parse_neigh_dynamic_redistribute(const struct smap 
*options);
 
 size_t *shuffled_range(size_t n);
 
+struct vni_local_ip {
+    struct hmap_node hmap_node;
+    struct in6_addr ip;
+    uint32_t vni;
+};
+
+struct evpn_local_ip_map {
+    struct hmap vni_ip4;         /* Per VNI local IPv4 vni_local_ips. */
+    struct hmap vni_ip6;         /* Per VNI local IPv6 vni_local_ips. */
+    struct in6_addr default_ip4; /* Default local IPv4. */
+    struct in6_addr default_ip6; /* Default local IPv6. */
+};
+
+const struct in6_addr *evpn_local_ip_lookup(const struct hmap *map,
+                                            uint32_t vni);
+ovs_be32 evpn_local_ip_find_v4(const struct evpn_local_ip_map *vni_ip_map,
+                               uint32_t vni);
+const struct in6_addr *evpn_local_ip_find_v6(
+    const struct evpn_local_ip_map *vni_ip_map, uint32_t vni);
+void evpn_local_ip_map_init(struct evpn_local_ip_map *vni_ip_map,
+                            const struct smap *config);
+const struct in6_addr *
+evpn_local_ip_map_lookup(const struct evpn_local_ip_map *map,
+                         uint32_t vni, bool ipv4);
+void evpn_local_ip_map_destroy(struct evpn_local_ip_map *map);
 #endif /* OVN_UTIL_H */
diff --git a/northd/en-datapath-logical-switch.c 
b/northd/en-datapath-logical-switch.c
index ad6d042bc..a1ba217ba 100644
--- a/northd/en-datapath-logical-switch.c
+++ b/northd/en-datapath-logical-switch.c
@@ -180,6 +180,20 @@ gather_external_ids(const struct nbrec_logical_switch *nbs,
             smap_add(external_ids, "dynamic-routing-arp-prefer-local",
                      prefer_evpn_arp_local);
         }
+
+        const char *ovn_maintain_evpn_mode =
+            smap_get(&nbs->other_config, "dynamic-routing-maintain-evpn");
+        if (ovn_maintain_evpn_mode) {
+            smap_add(external_ids, "dynamic-routing-maintain-evpn",
+                     ovn_maintain_evpn_mode);
+        }
+
+        const char *ovn_maintain_evpn_lrp =
+            smap_get(&nbs->other_config, "dynamic-routing-maintain-evpn-lrp");
+        if (ovn_maintain_evpn_lrp) {
+            smap_add(external_ids, "dynamic-routing-maintain-evpn-lrp",
+                     ovn_maintain_evpn_lrp);
+        }
     }
 
     /* For backwards-compatibility, also store the NB UUID in
diff --git a/tests/automake.mk b/tests/automake.mk
index 8084357b5..1aa9f45c2 100644
--- a/tests/automake.mk
+++ b/tests/automake.mk
@@ -291,6 +291,8 @@ tests_ovstest_SOURCES = \
 
 if HAVE_NETLINK
 tests_ovstest_SOURCES += \
+       controller/netlink-utils.h \
+       controller/netlink-utils.c \
        controller/host-if-monitor.c \
        controller/host-if-monitor.h \
        controller/neighbor-exchange-netlink.c \
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index 87839f4a3..9a7e78263 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -22094,3 +22094,182 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port 
patch-.*/d
 /connection dropped.*/d"])
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([dynamic-routing - EVPN maintain-evpn mvd])
+AT_KEYWORDS([dynamic-routing evpn maintain-evpn])
+
+CHECK_VRF()
+
+vni=100
+VRF_RESERVE([$vni])
+
+ovn_start
+OVS_TRAFFIC_VSWITCHD_START()
+ADD_BR([br-int])
+
+check ovs-vsctl \
+    -- set Open_vSwitch . external-ids:system-id=hv1 \
+    -- set Open_vSwitch . 
external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+    -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+    -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+    -- set Open_vSwitch . external-ids:ovn-evpn-local-ip=169.0.0.1 \
+    -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+
+start_daemon ovn-controller
+ovs-vsctl add-port br-int p1 -- \
+    set Interface p1 external_ids:iface-id=ls-p1 -- \
+    set Interface p1 type=internal
+check ovn-nbctl ls-add ls-evpn
+check ovn-nbctl lsp-add ls-evpn ls-p1
+check ovn-nbctl lsp-set-addresses ls-p1 "00:00:00:00:00:02 20.20.20.2"
+
+AS_BOX([Create EVPN interface stack - L2 VNI])
+check ovn-nbctl \
+    -- set Logical_Switch ls-evpn \
+            other_config:dynamic-routing-vni=$vni \
+            other_config:dynamic-routing-maintain-evpn=mvd \
+            other_config:dynamic-routing-redistribute=fdb
+check ovn-nbctl --wait=hv sync
+
+# check no vrf exists since it's only fdb mode
+AT_CHECK([ip a | grep -q ovnvrf100], [1])
+AT_CHECK([ip a | grep -q ovnbr100], [0])
+AT_CHECK([ip a | grep -q ovnvxlan100], [0])
+AT_CHECK([ip a | grep -q ovnlo100], [0])
+
+dstport=$((60000 + $vni))
+AT_CHECK([ip -d link show ovnvxlan$vni | grep -oP ' id \K[[0-9]]+'], [0], [dnl
+100
+], [ignore])
+
+AT_CHECK([ip -d link show ovnvxlan$vni | grep -oP 'dstport \K[[0-9]]+'],[0], 
[dnl
+60100
+], ignore)
+
+AT_CHECK([ip -d link show ovnvxlan$vni | grep -q 'local 169.0.0.1'], [0], 
[ignore], [ignore])
+
+AT_CHECK([ip -d link show ovnvxlan$vni | grep -q 'nolearning'], [0], [ignore], 
[ignore])
+
+AT_CHECK([ip link show ovnvxlan$vni | grep -oP 'master \K\S+'],[0], [dnl
+ovnbr100
+])
+AT_CHECK([ip link show ovnlo$vni | grep -oP 'master \K\S+'],[0], [dnl
+ovnbr100
+])
+
+
+AS_BOX([Bridge MAC via dynamic-routing-evpn-lrp])
+# Add LRP connected to ls-evpn; its MAC/IP will be applied to the bridge.
+check ovn-nbctl \
+    -- lr-add R-evpn \
+    -- lrp-add R-evpn rp-evpn 00:00:00:aa:bb:01 192.168.100.1/24 \
+    -- lsp-add ls-evpn ls-evpn-rp \
+    -- set Logical_Switch_Port ls-evpn-rp type=router \
+            options:router-port=rp-evpn \
+    -- lsp-set-addresses ls-evpn-rp router \
+    -- set Logical_Switch ls-evpn \
+            other_config:dynamic-routing-maintain-evpn-lrp=rp-evpn \
+            other_config:dynamic-routing-redistribute=fdb,ip
+check ovn-nbctl --wait=hv sync
+
+# check vrf exists since it's fdb + ip mode
+AT_CHECK([ip a | grep -q ovnvrf100], [0])
+AT_CHECK([ip a | grep -q ovnbr100], [0])
+AT_CHECK([ip a | grep -q ovnvxlan100], [0])
+AT_CHECK([ip a | grep -q ovnlo100], [0])
+
+AT_CHECK([ip link show ovnlo$vni | grep -oP 'master \K\S+'], [0], [dnl
+ovnbr100
+])
+AT_CHECK([ip link show ovnvxlan$vni | grep -oP 'master \K\S+'], [0], [dnl
+ovnbr100
+])
+AT_CHECK([ip link show ovnbr$vni | grep -oP 'master \K\S+'], [0], [dnl
+ovnvrf100
+])
+
+AT_CHECK([ip link show ovnbr$vni | grep -oP 'link/ether \K\S+'], [0], [dnl
+00:00:00:aa:bb:01
+])
+
+AS_BOX([Bridge MAC address changes])
+check ovn-nbctl set Logical_Router_Port rp-evpn \
+    mac='"00:00:00:00:00:08"'
+check ovn-nbctl --wait=hv sync
+
+AT_CHECK([ip link show ovnbr$vni | grep -oP 'link/ether \K\S+'],[0], [dnl
+00:00:00:00:00:08
+])
+
+AS_BOX([Redistribute mode change fdb,ip -> fdb: VRF destroyed, lo kept])
+check ovn-nbctl set Logical_Switch ls-evpn \
+    other_config:dynamic-routing-redistribute=fdb
+check ovn-nbctl --wait=hv sync
+
+# check vrf exists since it's fdb + ip mode
+AT_CHECK([ip a | grep -q ovnvrf100], [1])
+AT_CHECK([ip a | grep -q ovnbr100], [0])
+AT_CHECK([ip a | grep -q ovnvxlan100], [0])
+AT_CHECK([ip a | grep -q ovnlo100], [0])
+
+AT_CHECK([ip link show ovnlo$vni | grep -oP 'master \K\S+'], [0], [dnl
+ovnbr100
+])
+AT_CHECK([ip link show ovnvxlan$vni | grep -oP 'master \K\S+'], [0], [dnl
+ovnbr100
+])
+
+AS_BOX([Redistribute mode change fdb -> ip: lo destroyed, VRF recreated])
+check ovn-nbctl set Logical_Switch ls-evpn \
+    other_config:dynamic-routing-redistribute=ip
+check ovn-nbctl --wait=hv sync
+
+AT_CHECK([ip a | grep -q ovnvrf100], [0])
+AT_CHECK([ip a | grep -q ovnbr100], [0])
+AT_CHECK([ip a | grep -q ovnvxlan100], [0])
+AT_CHECK([ip a | grep -q ovnlo100], [1])
+AT_CHECK([ip link show ovnvxlan$vni | grep -oP 'master \K\S+'],[0], [dnl
+ovnbr100
+])
+AT_CHECK([ip link show ovnbr$vni | grep -oP 'master \K\S+'],[0], [dnl
+ovnvrf100
+])
+
+AS_BOX([Redistribute mode change ip -> fdb,ip: lo and VRF both present again])
+check ovn-nbctl set Logical_Switch ls-evpn \
+    other_config:dynamic-routing-redistribute=fdb,ip
+check ovn-nbctl --wait=hv sync
+
+AT_CHECK([ip a | grep -q ovnvrf100], [0])
+AT_CHECK([ip a | grep -q ovnbr100], [0])
+AT_CHECK([ip a | grep -q ovnvxlan100], [0])
+AT_CHECK([ip a | grep -q ovnlo100], [0])
+
+AT_CHECK([ip link show ovnlo$vni | grep -oP 'master \K\S+'], [0], [dnl
+ovnbr100
+])
+AT_CHECK([ip link show ovnvxlan$vni | grep -oP 'master \K\S+'], [0], [dnl
+ovnbr100
+])
+AT_CHECK([ip link show ovnbr$vni | grep -oP 'master \K\S+'],[0], [dnl
+ovnvrf100
+])
+
+AS_BOX([Remove maintain-evpn - full stack cleanup])
+check ovn-nbctl \
+    -- remove Logical_Switch ls-evpn other_config dynamic-routing-maintain-evpn
+check ovn-nbctl --wait=hv sync
+
+AT_CHECK([ip a | grep -q ovnvrf100], [1])
+AT_CHECK([ip a | grep -q ovnbr100], [1])
+AT_CHECK([ip a | grep -q ovnvxlan100], [1])
+AT_CHECK([ip a | grep -q ovnlo100], [1])
+
+OVN_CLEANUP_CONTROLLER([hv1])
+OVN_CLEANUP_NORTHD
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+/connection dropped.*/d"])
+AT_CLEANUP
+])
-- 
2.48.1

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to