On 19.08.2026 14:34, Dumitru Ceara wrote:
> On 7/20/26 6:59 PM, Alexandra Rukomoinikova via dev wrote:
>> When a 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, when FDB redistribution is enabled,
>> a dummy interface (ovnlo<vni>) used to advertise MACs.
>>
>> For L3 VNI support (IP redistribution), dynamic-routing-maintain-evpn-lrp
>> must name a Logical Router Port; its MAC is applied to the bridge
>> interface so BGP can use it as next-hop. Interfaces are recreated
>> whenever a change (local IP, VXLAN port, has_addr or redistribute
>> mode) can't be applied in place, and are torn down on shutdown or
>> when the VNI is no longer configured.
>>
>> The low-level netlink calls to create/delete devices and set their
>> master/MAC, previously duplicated between route-exchange and
>> neighbor-exchange, are consolidated into a new
>> controller/netlink-utils.c. evpn_local_ip_map also moves from
>> controller/physical.c to lib/ovn-util.c so it can be shared with the
>> new code.
>>
>> Add system-ovn.at coverage and document the new options in
>> ovn-nb.xml
>>
>> Signed-off-by: Alexandra Rukomoinikova <[email protected]>
>> ---
> Hi Alexandra,
>
> Thanks for the new feature patch! I didn't get a chance to review it
> yet and unfortunately it will have to wait for 27.03 as it didn't get
> any reviews before soft freeze.
HI!
I think it's great that you didn't get around to reviewing this yet;
I’ve reworked the patch a bit on my end, found some issues, and will
send a new version.
> On the other hand that gives us more time to think about the user facing
> configuration.
>
> Some thoughts:
> - dynamic-routing-maintain-evpn-lrp=LRP is actually used in
> ovn-controller where we can only access SB data so Port_Bindings. But,
> at least theoretically, those can have multiple mac addresses. Which
> one should ovn-controller choose? Is it maybe better to make the MAC
> address a different EVPN LS configuration?
Right now the logic picks the first MAC address, and yes, that does look
odd. But honestly, I like the idea of options for mac rather for router
port name even less. First, I consider the northbound database the
source of truth: if the port already has a MAC address, why would I have
to specify it a second time? Tying it to the port also seems more robust
— if I change the address there in NB, the controller picks it up on its
own, whereas with an option the CMS has to go and update it separately
as well. And "take the first MAC by default" seems to cover almost every
case. That said, I'm not sure where the right compromise is here — if
you think an explicit MAC option is better, that's fine, I can do that.
>
> - what about SVD (single vxlan device) mode? Can we find a reasonable
> way of having ovn autoconfigure that too so we don't have this new
> feature that only covers half of the deployment modes? What would be
> needed? I guess we'd need VNI <-> VLAN mappings passed somehow.
> Anything else?
It seems like that's all, and yes, I can finish the SVD part, I don't
see any problems with it.
I have two questions.
1. Right now, anywhere in the controller code, any netlink error
triggers a recompute. That approach feels concerning to me: a
netlink error doesn't invalidate any engine input — the desired
state hasn't changed, so the recompute derives the same result and
re-issues the same calls. It's really a retry expressed as
invalidation. What do you think, is this worth doing something
about? What bothers me is how this looks in production: if the error
somehow isn't caused by OVN's own code but by an external factor, we
end up with a controller spinning at 100% CPU until someone fixes it
by hand.
2. In the current patch, the VNI for the fake vxlan port is derived as
an offset: if the user specifies, say, 67, what's actually used is
60000 + 67. This limits the range of usable VNIs. What do you think
— leave it as is, or add a separate option after all? Though I don't
much like the idea of yet another option here either. or create an
option with some pool of free ports?
>
> Regards,
> Dumitru
>
>
>> v1 --> v2: minor cleanups, no logic changed, rebased
>> ---
>> NEWS | 7 +
>> controller/automake.mk | 2 +
>> controller/host-if-monitor.c | 15 ++
>> controller/host-if-monitor.h | 2 +-
>> controller/neighbor-exchange-netlink.c | 103 +++++++-
>> controller/neighbor-exchange-netlink.h | 11 +
>> controller/neighbor-exchange-stub.c | 10 +
>> controller/neighbor-exchange.c | 324 +++++++++++++++++++++++++
>> controller/neighbor-exchange.h | 8 +
>> controller/neighbor.c | 182 +++++++++++---
>> controller/neighbor.h | 34 +++
>> controller/netlink-utils.c | 148 +++++++++++
>> controller/netlink-utils.h | 41 ++++
>> controller/ovn-controller.c | 20 ++
>> controller/physical.c | 157 ------------
>> controller/route-exchange-netlink.c | 45 +---
>> controller/route-exchange-netlink.h | 5 -
>> controller/route-exchange.c | 1 +
>> lib/ovn-util.c | 157 ++++++++++++
>> lib/ovn-util.h | 25 ++
>> northd/en-datapath-logical-switch.c | 14 ++
>> ovn-nb.xml | 88 ++++++-
>> tests/automake.mk | 2 +
>> tests/system-ovn.at | 186 ++++++++++++++
>> 24 files changed, 1337 insertions(+), 250 deletions(-)
>> create mode 100644 controller/netlink-utils.c
>> create mode 100644 controller/netlink-utils.h
>>
>> diff --git a/NEWS b/NEWS
>> index 82bf1a272..43c3e5af1 100644
>> --- a/NEWS
>> +++ b/NEWS
>> @@ -74,6 +74,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
>> + Switche. When set to "mvd", ovn-controller creates and maintains
>> + Linux kernel interfaces required for EVPN/VXLAN itself: VRF, bridge,
>> + VXLAN device per VNI, 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..8261f6cf6 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,94 @@ 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)) {
>> + return nl_create_vrf(ifname, table_id);
>> + }
>> +
>> + VLOG_WARN_RL(&rl, "attempt to create VRF using invalid table id
>> %"PRIu32,
>> + table_id);
>> +
>> + return EINVAL;
>> +}
>> +
>> +
>> 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..b1f6402a5 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"
>> @@ -47,6 +51,8 @@ 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 +71,328 @@ 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;
>> +}
>> +
>> +/* Deletes the interfaces for 'entry'. Returns true if all of them were
>> + * deleted (or were already absent), false if any netlink call failed. */
>> +static bool
>> +evpn_delete_evpn_devices(struct neighbor_ovn_maintain_entry *entry)
>> +{
>> + int error;
>> + bool ok = true;
>> +
>> + 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);
>> + ok = false;
>> + }
>> +
>> + 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);
>> + ok = false;
>> + }
>> +
>> + 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);
>> + ok = false;
>> + }
>> +
>> + 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);
>> + ok = false;
>> + }
>> +
>> + /* 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);
>> +
>> + return ok;
>> +}
>> +
>> +static bool
>> +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);
>> + return false;
>> + }
>> + }
>> +
>> + return true;
>> +}
>> +
>> +/* Creates the interfaces for 'entry'. Returns true if the whole stack was
>> + * created successfully (or already existed), false if any netlink call
>> + * failed. */
>> +static bool
>> +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;
>> + bool ok = true;
>> +
>> + if (!link_ifindex) {
>> + err = ENODEV;
>> + VLOG_WARN_RL(&rl,
>> + "Unable to find OVS system vxlan interface");
>> + SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
>> + ok = false;
>> + }
>> +
>> + 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);
>> + ok = false;
>> + }
>> + }
>> +
>> + 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);
>> + ok = false;
>> + }
>> +
>> + 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);
>> + ok = false;
>> + }
>> +
>> + if (!set_bridge_evpn_device_addr(entry)) {
>> + ok = false;
>> + }
>> +
>> + 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);
>> + ok = false;
>> + }
>> + }
>> +
>> + 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);
>> + ok = false;
>> + }
>> + 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);
>> + ok = false;
>> + }
>> + }
>> +
>> + 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);
>> + ok = false;
>> + }
>> +
>> + /* Refresh the host-if-monitor ifindex cache so a lookup later in this
>> + * same engine iteration picks up the newly (re)created interfaces
>> + * instead of a stale ifindex left over from before they were
>> + * deleted/recreated. */
>> + host_if_monitor_invalidate(entry->br_if_name);
>> + host_if_monitor_invalidate(entry->vxlan_if_name);
>> + host_if_monitor_invalidate(entry->lo_if_name);
>> +
>> + return ok;
>> +}
>> +
>> +/* 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 bool
>> +maintain_evpn_devices(struct neighbor_ovn_maintain_entry *entry,
>> + struct neighbor_ovn_maintain_entry *old_entry)
>> +{
>> + if (!old_entry) {
>> + return evpn_create_devices(entry);
>> + }
>> +
>> + if (should_destroy_interfaces(entry, old_entry)) {
>> + if (!evpn_delete_evpn_devices(entry)) {
>> + return false;
>> + }
>> + return evpn_create_devices(entry);
>> + }
>> +
>> + return true;
>> +}
>> +
>> +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);
>> +
>> + bool ok = maintain_evpn_devices(entry, old_me ? &old_me->entry :
>> NULL);
>> +
>> + if (!ok && !old_me) {
>> + /* First-time creation failed: don't start tracking this VNI as
>> + * maintained. The next run will find no old entry for it and
>> + * retry evpn_create_devices() from scratch. */
>> + continue;
>> + }
>> +
>> + 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);
>> + }
>> + if (ok) {
>> + 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) {
>> + if (evpn_delete_evpn_devices(&stale_me->entry)) {
>> + free(stale_me);
>> + } else {
>> + /* Deletion failed: keep tracking it so it's retried as stale
>> + * again on the next run (it's no longer in 'maintain_evpn'). */
>> + hmap_insert(&maintained_evpn_entries, &stale_me->node,
>> + hash_int(stale_me->entry.vni, 0));
>> + }
>> + }
>> +
>> + 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);
>> +}
>> +
>> 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..509655788 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 interfaces 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 ada9254e0..2c42ec541 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",
>> @@ -88,13 +89,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)
>> @@ -116,24 +215,50 @@ 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)) {
>> @@ -146,32 +271,31 @@ 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,
>> @@ -182,7 +306,7 @@ neighbor_run(struct neighbor_ctx_in *n_ctx_in,
>> * meant to be advertised. With 'fdb' set, also program them as
>> FDB
>> * entries. */
>> neighbor_collect_advertised_mac_bindings(
>> - n_ctx_in, mode, lo ? &lo->announced_neighbors : NULL,
>> + n_ctx_in, redistribute_mode, lo ? &lo->announced_neighbors :
>> NULL,
>> br_v4 ? &br_v4->announced_neighbors : NULL,
>> br_v6 ? &br_v6->announced_neighbors : NULL,
>> 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..95c817e33
>> --- /dev/null
>> +++ b/controller/netlink-utils.h
>> @@ -0,0 +1,41 @@
>> +/*
>> + * 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_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 */
>> diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
>> index 5f63c5fec..dc6089388 100644
>> --- a/controller/ovn-controller.c
>> +++ b/controller/ovn-controller.c
>> @@ -6081,6 +6081,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 *
>> @@ -6093,6 +6096,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;
>> }
>> @@ -6104,6 +6109,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);
>> }
>>
>> @@ -6138,22 +6144,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;
>> }
>> @@ -6486,6 +6502,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,
>> @@ -8465,6 +8482,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();
>> @@ -8501,6 +8520,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 c137b5119..b6718f3c7 100644
>> --- a/controller/route-exchange-netlink.h
>> +++ b/controller/route-exchange-netlink.h
>> @@ -29,11 +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_LOCAL &&
>> \
>> - table_id != RT_TABLE_MAX)
>> -
>> struct in6_addr;
>> struct hmap;
>> struct vector;
>> 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 d9b78fbb6..314575d00 100644
>> --- a/lib/ovn-util.c
>> +++ b/lib/ovn-util.c
>> @@ -1897,3 +1897,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 29f6bb739..65aa53e75 100644
>> --- a/lib/ovn-util.h
>> +++ b/lib/ovn-util.h
>> @@ -831,4 +831,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/ovn-nb.xml b/ovn-nb.xml
>> index b0bb8c264..03d6536bb 100644
>> --- a/ovn-nb.xml
>> +++ b/ovn-nb.xml
>> @@ -948,15 +948,30 @@
>> </p>
>>
>> <p>
>> - All of the following configuration options must also be provided
>> - in order for the configuration to be valid:
>> - <ref column="other_config" key="dynamic-routing-bridge-ifname"
>> - table="Logical_switch"/>,
>> - <ref column="other_config" key="dynamic-routing-vxlan-ifname"
>> - table="Logical_switch"/>,
>> - <ref column="other_config" key="dynamic-routing-advertise-ifname"
>> - table="Logical_switch"/>.
>> + The EVPN interfaces (VXLAN, bridge and advertise devices) can
>> + either be named explicitly by the CMS, or auto-created and
>> + maintained by <code>ovn-controller</code>:
>> </p>
>> +
>> + <ul>
>> + <li>
>> + To name the interfaces explicitly, all of the following
>> + configuration options must also be provided in order for the
>> + configuration to be valid:
>> + <ref column="other_config" key="dynamic-routing-bridge-ifname"
>> + table="Logical_switch"/>,
>> + <ref column="other_config" key="dynamic-routing-vxlan-ifname"
>> + table="Logical_switch"/>,
>> + <ref column="other_config"
>> key="dynamic-routing-advertise-ifname"
>> + table="Logical_switch"/>.
>> + </li>
>> + <li>
>> + To have <code>ovn-controller</code> create and maintain the
>> + interfaces automatically instead, set
>> + <ref column="other_config" key="dynamic-routing-maintain-evpn"
>> + table="Logical_switch"/>.
>> + </li>
>> + </ul>
>> </column>
>>
>> <column name="other_config" key="dynamic-routing-bridge-ifname">
>> @@ -979,6 +994,63 @@
>> table="Logical_switch"/> is set to valid VNI.
>> </column>
>>
>> + <column name="other_config" key="dynamic-routing-maintain-evpn"
>> + type='{"type": "string", "enum": ["set", ["mvd"]]}'>
>> + <p>
>> + Enables automatic management of the EVPN interface stack by
>> + <code>ovn-controller</code> for the VNI specified by
>> + <ref column="other_config" key="dynamic-routing-vni"
>> + table="Logical_switch"/>.
>> + Instead of requiring the CMS to create and configure the
>> interfaces,
>> + <code>ovn-controller</code> creates and maintains them on every
>> + chassis where the logical switch is local.
>> + </p>
>> +
>> + <p>
>> + The only supported value is <code>mvd</code> (VXLAN device per VNI).
>> + </p>
>> +
>> + <p>
>> + In this mode, <code>ovn-controller</code> creates the following
>> + interfaces, each suffixed with the VNI:
>> + <code>ovnbr<vni></code>,
>> + <code>ovnvxlan<vni></code>, and
>> + <code>ovnlo<vni></code>
>> + (for example, <code>ovnvxlan100</code>).
>> + </p>
>> +
>> + <p>
>> + If <ref column="other_config" key="dynamic-routing-redistribute"
>> + table="Logical_switch"/> includes <code>ip</code> and
>> + <ref column="other_config" key="dynamic-routing-maintain-evpn-lrp"
>> + table="Logical_switch"/> is configured,
>> + <code>ovn-controller</code> also creates a VRF named
>> + <code>ovnvrf<vni></code> and enslaves the bridge interface
>> + to it. This allows the logical router IP addresses to be
>> advertised
>> + through EVPN.
>> + </p>
>> + </column>
>> +
>> + <column name="other_config" key="dynamic-routing-maintain-evpn-lrp">
>> + <p>
>> + Specifies the
>> + <ref table="Logical_Router_Port"/> whose MAC address is assigned
>> + to the auto-created bridge interface managed by
>> + <ref column="other_config" key="dynamic-routing-maintain-evpn"
>> + table="Logical_switch"/>.
>> + This ensures that L3/IRB traffic terminating on the EVPN VNI uses
>> + the logical router's MAC address.
>> + </p>
>> +
>> + <p>
>> + This option is required only when
>> + <ref column="other_config" key="dynamic-routing-maintain-evpn"
>> + table="Logical_switch"/> is enabled and
>> + <ref column="other_config" key="dynamic-routing-redistribute"
>> + table="Logical_switch"/> includes <code>ip</code>.
>> + </p>
>> + </column>
>> +
>> <column name="other_config" key="dynamic-routing-fdb-prefer-local"
>> type='{"type": "boolean"}'>
>> <p>
>> 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 17b4dcb3d..87e0e58a2 100644
>> --- a/tests/system-ovn.at
>> +++ b/tests/system-ovn.at
>> @@ -21998,3 +21998,189 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port
>> patch-.*/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])
>> +
>> +# Create a fake VXLAN interface, since there's no real peer chassis in
>> +# this test to make OVS create vxlan_sys_<port> automatically.
>> +ip link add vxlan_sys_4789 type vxlan dstport 4789 external
>> +ip link set vxlan_sys_4789 up
>> +
>> +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])
>> +
>> +ip link del vxlan_sys_4789
>> +
>> +OVN_CLEANUP_CONTROLLER([hv1])
>> +OVN_CLEANUP_NORTHD
>> +as
>> +OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
>> +/connection dropped.*/d
>> +/misses dynamic-routing.*/d"])
>> +AT_CLEANUP
>> +])
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev