Skip to content

Commit 3baa974

Browse files
committed
controller: Distribute EVPN-learned MAC bindings via SB.
EVPN-learned MAC bindings (Type-2 routes from FRR via the kernel neighbor table) were only installed as local OpenFlow flows on the chassis running FRR. When a VM on a different chassis routed traffic through a logical router that needed to resolve an EVPN-learned next-hop, the MAC binding lookup failed because the flow only existed on the FRR chassis. Add a new engine node, en_evpn_mac_binding_sync, that writes EVPN-learned MAC bindings to the SB MAC_Binding table. Once in SB, every chassis consumes them through the existing lflow MAC_Binding pipeline at the same priority as regular ARP/ND-learned entries. The sync node tracks which (logical_port, ip, mac) tuples it has written so it only deletes its own entries without interfering with pinctrl-written dynamic bindings, and skips SB writes when the MAC has not changed. A companion waker node periodically triggers timestamp refreshes on synced entries to prevent northd aging from removing entries that are still present in the kernel neighbor table but not actively carrying traffic. The router-side consider_neighbor_flow() call is removed from physical_consider_evpn_arp() since those flows now come from the SB path. The switch-side EVPN ARP lookup flows (table 113, used for ARP/ND suppression) remain local-only. Note: the dynamic-routing-arp-prefer-local option no longer affects the router MAC_Binding table priority for EVPN entries (they are now always at the same priority as dynamic entries). It still controls the EVPN ARP lookup side table (table 113) priority. Removal of the option is planned as a follow-up. Fixes: 93ae639 ("controller, northd: Add support for learning IP neighbors through EVPN.") Reported-at: https://redhat.atlassian.net/browse/FDP-3587 Reported-at: https://redhat.atlassian.net/browse/FDP-4001 Assisted-by: Claude Opus 4.6, OpenCode Acked-by: Dumitru Ceara <dceara@redhat.com> Signed-off-by: Ales Musil <amusil@redhat.com> (cherry picked from commit ee02a78)
1 parent 721fbde commit 3baa974

14 files changed

Lines changed: 693 additions & 213 deletions

TODO.rst

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,6 @@ OVN To-do List
156156
Otherwise we could try to add duplicated Learned_Routes and the ovnsb
157157
commit would fail.
158158

159-
* Add support for EVPN L3, that involves MAC Binding learning and
160-
advertisement.
161-
162159
* Datapath sync nodes
163160

164161
* Migrate data stored in the ovn\_datapath structure to

controller/automake.mk

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ controller_ovn_controller_SOURCES = \
1616
controller/evpn-binding.h \
1717
controller/evpn-fdb.c \
1818
controller/evpn-fdb.h \
19+
controller/evpn-mac-binding-sync.c \
20+
controller/evpn-mac-binding-sync.h \
1921
controller/ha-chassis.c \
2022
controller/ha-chassis.h \
2123
controller/if-status.c \

controller/evpn-mac-binding-sync.c

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/* Copyright (c) 2026, Red Hat, Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at:
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
#include <config.h>
17+
18+
#include "openvswitch/poll-loop.h"
19+
#include "packets.h"
20+
#include "timeval.h"
21+
22+
#include "evpn-arp.h"
23+
#include "evpn-mac-binding-sync.h"
24+
#include "lib/mac-binding-index.h"
25+
#include "local_data.h"
26+
#include "mac-cache.h"
27+
#include "ovn-sb-idl.h"
28+
#include "ovn-util.h"
29+
#include "vec.h"
30+
31+
/* Hash a (logical_port, ip) pair. */
32+
static uint32_t
33+
evpn_mb_hash(const char *logical_port, const char *ip)
34+
{
35+
uint32_t hash = hash_string(logical_port, 0);
36+
return hash_string(ip, hash);
37+
}
38+
39+
static struct evpn_mb_synced_entry *
40+
evpn_mb_synced_find(const struct hmap *synced, const char *logical_port,
41+
const char *ip)
42+
{
43+
uint32_t hash = evpn_mb_hash(logical_port, ip);
44+
45+
struct evpn_mb_synced_entry *entry;
46+
HMAP_FOR_EACH_WITH_HASH (entry, hmap_node, hash, synced) {
47+
if (!strcmp(entry->logical_port, logical_port) &&
48+
!strcmp(entry->ip, ip)) {
49+
return entry;
50+
}
51+
}
52+
53+
return NULL;
54+
}
55+
56+
static struct evpn_mb_synced_entry *
57+
evpn_mb_synced_add(struct hmap *synced, const char *logical_port,
58+
const char *ip)
59+
{
60+
struct evpn_mb_synced_entry *entry = xmalloc(sizeof *entry);
61+
*entry = (struct evpn_mb_synced_entry) {
62+
.logical_port = xstrdup(logical_port),
63+
.ip = xstrdup(ip),
64+
};
65+
hmap_insert(synced, &entry->hmap_node, evpn_mb_hash(logical_port, ip));
66+
return entry;
67+
}
68+
69+
static void
70+
evpn_mb_synced_remove(struct hmap *synced, struct evpn_mb_synced_entry *entry)
71+
{
72+
hmap_remove(synced, &entry->hmap_node);
73+
free(entry->logical_port);
74+
free(entry->ip);
75+
free(entry);
76+
}
77+
78+
/* Schedule the waker to fire after 'delay_ms' milliseconds. */
79+
static void
80+
evpn_mb_sync_waker_schedule(struct evpn_mb_sync_waker *waker,
81+
int64_t delay_ms)
82+
{
83+
if (delay_ms < INT64_MAX) {
84+
waker->should_schedule = true;
85+
waker->next_wake_msec = time_msec() + delay_ms;
86+
poll_timer_wait_until(waker->next_wake_msec);
87+
}
88+
}
89+
90+
/* Sync a single EVPN ARP entry to the SB MAC_Binding table for one
91+
* router port. Caller must ensure 'ovnsb_idl_txn' is valid.
92+
* Returns the remaining time (in ms) until the next timestamp
93+
* refresh is needed, or INT64_MAX if none. */
94+
static int64_t
95+
sync_evpn_mb_for_router_port(
96+
struct ovsdb_idl_txn *ovnsb_idl_txn,
97+
struct ovsdb_idl_index *sbrec_mac_binding_by_lport_ip,
98+
const struct sbrec_mac_binding_table *mb_table,
99+
const struct sbrec_port_binding *router_pb,
100+
const struct evpn_arp *arp,
101+
struct mac_cache_data *mac_cache_data,
102+
struct ed_type_evpn_mac_binding_sync *data,
103+
long long timewall_now)
104+
{
105+
char *ip_s = normalize_v46(&arp->ip);
106+
struct evpn_mb_synced_entry *existing =
107+
evpn_mb_synced_find(&data->synced_entries,
108+
router_pb->logical_port, ip_s);
109+
110+
const struct sbrec_mac_binding *sb_mb = existing
111+
? sbrec_mac_binding_table_get_for_uuid(mb_table, &existing->mb_uuid)
112+
: NULL;
113+
114+
/* Insert or update the SB MAC_Binding row. */
115+
const struct sbrec_mac_binding *b =
116+
mac_binding_add_to_sb(ovnsb_idl_txn,
117+
sbrec_mac_binding_by_lport_ip,
118+
router_pb->logical_port,
119+
router_pb->datapath,
120+
arp->mac, ip_s, false,
121+
sb_mb);
122+
123+
if (!existing) {
124+
existing = evpn_mb_synced_add(&data->synced_entries,
125+
router_pb->logical_port, ip_s);
126+
}
127+
128+
free(ip_s);
129+
existing->mb_uuid = b->header_.uuid;
130+
existing->stale = false;
131+
132+
/* Refresh timestamp to prevent aging. */
133+
struct mac_cache_threshold *threshold =
134+
mac_cache_threshold_find(mac_cache_data,
135+
router_pb->datapath->tunnel_key);
136+
if (!threshold) {
137+
return INT64_MAX;
138+
}
139+
140+
uint64_t since_updated = timewall_now - b->timestamp;
141+
if (since_updated >= threshold->cooldown_period) {
142+
sbrec_mac_binding_set_timestamp(b, timewall_now);
143+
return threshold->cooldown_period;
144+
}
145+
146+
return threshold->cooldown_period - since_updated;
147+
}
148+
149+
void
150+
evpn_mac_binding_sync_run(
151+
struct ovsdb_idl_txn *ovnsb_idl_txn,
152+
struct ovsdb_idl_index *sbrec_mac_binding_by_lport_ip,
153+
const struct sbrec_mac_binding_table *mb_table,
154+
const struct hmap *local_datapaths,
155+
const struct hmap *evpn_arps,
156+
struct mac_cache_data *mac_cache_data,
157+
struct ed_type_evpn_mac_binding_sync *data,
158+
struct evpn_mb_sync_waker *waker)
159+
{
160+
if (!ovnsb_idl_txn) {
161+
data->sb_changes_pending = true;
162+
return;
163+
}
164+
165+
long long timewall_now = time_wall_msec();
166+
int64_t min_next_refresh_ms = INT64_MAX;
167+
168+
/* Mark all synced entries as stale. SB row pointers are resolved
169+
* lazily by UUID only where needed (insert/update, timestamp
170+
* refresh, delete), avoiding a per-entry index lookup here. */
171+
struct evpn_mb_synced_entry *synced_entry;
172+
HMAP_FOR_EACH (synced_entry, hmap_node, &data->synced_entries) {
173+
synced_entry->stale = true;
174+
}
175+
176+
/* Walk current EVPN ARPs and sync to SB. */
177+
const struct evpn_arp *arp;
178+
HMAP_FOR_EACH (arp, hmap_node, evpn_arps) {
179+
const struct peer_ports *peers;
180+
VECTOR_FOR_EACH_PTR (&arp->ldp->peer_ports, peers) {
181+
const struct sbrec_port_binding *remote_pb = peers->remote;
182+
struct local_datapath *peer_ld =
183+
get_local_datapath(local_datapaths,
184+
remote_pb->datapath->tunnel_key);
185+
if (!peer_ld || peer_ld->is_switch) {
186+
continue;
187+
}
188+
189+
int64_t remaining =
190+
sync_evpn_mb_for_router_port(ovnsb_idl_txn,
191+
sbrec_mac_binding_by_lport_ip,
192+
mb_table,
193+
remote_pb, arp,
194+
mac_cache_data, data,
195+
timewall_now);
196+
if (remaining < min_next_refresh_ms) {
197+
min_next_refresh_ms = remaining;
198+
}
199+
}
200+
}
201+
202+
/* Delete stale entries from SB. */
203+
HMAP_FOR_EACH_SAFE (synced_entry, hmap_node, &data->synced_entries) {
204+
if (!synced_entry->stale) {
205+
continue;
206+
}
207+
208+
const struct sbrec_mac_binding *sb_mb =
209+
sbrec_mac_binding_table_get_for_uuid(
210+
mb_table, &synced_entry->mb_uuid);
211+
if (sb_mb) {
212+
sbrec_mac_binding_delete(sb_mb);
213+
}
214+
215+
evpn_mb_synced_remove(&data->synced_entries, synced_entry);
216+
}
217+
218+
/* Schedule the waker for the next timestamp refresh. */
219+
if (min_next_refresh_ms < INT64_MAX) {
220+
evpn_mb_sync_waker_schedule(waker, min_next_refresh_ms);
221+
}
222+
}
223+
224+
void
225+
evpn_mac_binding_sync_init(struct ed_type_evpn_mac_binding_sync *data)
226+
{
227+
hmap_init(&data->synced_entries);
228+
data->sb_changes_pending = false;
229+
}
230+
231+
void
232+
evpn_mac_binding_sync_cleanup(struct ed_type_evpn_mac_binding_sync *data)
233+
{
234+
struct evpn_mb_synced_entry *entry;
235+
HMAP_FOR_EACH_POP (entry, hmap_node, &data->synced_entries) {
236+
free(entry->logical_port);
237+
free(entry->ip);
238+
free(entry);
239+
}
240+
hmap_destroy(&data->synced_entries);
241+
}

controller/evpn-mac-binding-sync.h

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* Copyright (c) 2026, Red Hat, Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at:
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
#ifndef EVPN_MAC_BINDING_SYNC_H
17+
#define EVPN_MAC_BINDING_SYNC_H 1
18+
19+
#include "openvswitch/hmap.h"
20+
#include "openvswitch/uuid.h"
21+
22+
struct mac_cache_data;
23+
struct ovsdb_idl_index;
24+
struct ovsdb_idl_txn;
25+
struct sbrec_mac_binding_table;
26+
27+
/* Tracks a MAC_Binding row that was written to SB by the EVPN sync. */
28+
struct evpn_mb_synced_entry {
29+
struct hmap_node hmap_node;
30+
char *logical_port; /* Router port name. */
31+
char *ip; /* Normalized IP string. */
32+
struct uuid mb_uuid; /* UUID of the SB MAC_Binding row.
33+
* All-zeros when not yet synced. */
34+
bool stale; /* Marked true at start of sync,
35+
* cleared when still desired. */
36+
};
37+
38+
/* Persistent state for the en_evpn_mac_binding_sync engine node. */
39+
struct ed_type_evpn_mac_binding_sync {
40+
/* Contains 'struct evpn_mb_synced_entry'. Tracks which
41+
* (logical_port, ip) pairs we have written to SB. */
42+
struct hmap synced_entries;
43+
44+
/* True when an SB write was skipped because ovnsb_idl_txn was NULL. */
45+
bool sb_changes_pending;
46+
};
47+
48+
/* Timer that periodically wakes the sync node so it can refresh
49+
* timestamps on SB MAC_Binding rows it owns. */
50+
struct evpn_mb_sync_waker {
51+
bool should_schedule; /* Whether a wake is pending. */
52+
long long next_wake_msec; /* Absolute time of next wake. */
53+
};
54+
55+
void evpn_mac_binding_sync_run(
56+
struct ovsdb_idl_txn *ovnsb_idl_txn,
57+
struct ovsdb_idl_index *sbrec_mac_binding_by_lport_ip,
58+
const struct sbrec_mac_binding_table *mb_table,
59+
const struct hmap *local_datapaths,
60+
const struct hmap *evpn_arps,
61+
struct mac_cache_data *mac_cache_data,
62+
struct ed_type_evpn_mac_binding_sync *data,
63+
struct evpn_mb_sync_waker *waker);
64+
65+
void evpn_mac_binding_sync_init(struct ed_type_evpn_mac_binding_sync *);
66+
void evpn_mac_binding_sync_cleanup(struct ed_type_evpn_mac_binding_sync *);
67+
68+
#endif /* EVPN_MAC_BINDING_SYNC_H */

controller/garp_rarp.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ send_garp_locally(const struct garp_rarp_ctx_in *r_ctx_in,
365365
mac_binding_add_to_sb(r_ctx_in->ovnsb_idl_txn,
366366
r_ctx_in->sbrec_mac_binding_by_lport_ip,
367367
remote->logical_port, remote->datapath,
368-
ea, ds_cstr(&ip_s), update_only);
368+
ea, ds_cstr(&ip_s), update_only, NULL);
369369
ds_destroy(&ip_s);
370370
}
371371
}

0 commit comments

Comments
 (0)