Extend physical_consider_evpn_arp() to also install flows in
the dedicated EVPN ARP side table (OFTABLE_EVPN_ARP_LOOKUP,
table 113) for the switch datapath.  These flows are matched
by the chk_evpn_arp() action added in the previous commit.

Each flow matches on metadata (switch dp_key) and IP address
(reg0 for IPv4, xxreg0 for IPv6).  On a hit, it loads the
resolved MAC into eth.dst and sets MLF_EVPN_LOOKUP_BIT.

The flows use the same UUID as the router-side neighbor flows,
so ofctrl_remove_flows() in physical_handle_evpn_arp_changes()
automatically removes both router-side and switch-side flows
when an EVPN ARP entry is deleted.

Assisted-by: Claude Opus 4.6, Claude Code
Signed-off-by: Ales Musil <[email protected]>
---
 controller/physical.c | 44 +++++++++++++++++++++++++++++--
 tests/system-ovn.at   | 60 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 102 insertions(+), 2 deletions(-)

diff --git a/controller/physical.c b/controller/physical.c
index 7584d6065..89ff2785e 100644
--- a/controller/physical.c
+++ b/controller/physical.c
@@ -3689,6 +3689,8 @@ physical_consider_evpn_fdb(const struct evpn_fdb *fdb,
 static void
 physical_consider_evpn_arp(const struct hmap *local_datapaths,
                            const struct evpn_arp *arp,
+                           struct ofpbuf *ofpacts,
+                           struct match *match,
                            struct ovn_desired_flow_table *flow_table)
 {
     /* Walk connected OVN routers and install neighbor flows for the ARPs
@@ -3706,6 +3708,38 @@ physical_consider_evpn_arp(const struct hmap 
*local_datapaths,
         consider_neighbor_flow(remote_pb, &arp->flow_uuid, &arp->ip, arp->mac,
                                flow_table, arp->priority, false);
     }
+
+    /* Install EVPN ARP lookup flows in the dedicated side table for the
+     * switch datapath.  These flows are matched by the chk_evpn_arp()
+     * action.  On a hit they load the resolved MAC into eth.dst
+     * and set MLF_EVPN_LOOKUP_BIT. */
+    const struct in6_addr *ip = &arp->ip;
+    struct eth_addr mac = arp->mac;
+    const struct local_datapath *ldp = arp->ldp;
+
+    match_init_catchall(match);
+    match_set_metadata(match, htonll(ldp->datapath->tunnel_key));
+
+    if (IN6_IS_ADDR_V4MAPPED(ip)) {
+        ovs_be32 ip4 = in6_addr_get_mapped_ipv4(ip);
+        match_set_reg(match, 0, ntohl(ip4));
+    } else {
+        ovs_be128 value;
+        memcpy(&value, ip, sizeof(value));
+        match_set_xxreg(match, 0, ntoh128(value));
+    }
+
+    ofpbuf_clear(ofpacts);
+
+    /* Load resolved MAC into eth.dst. */
+    put_load_bytes(mac.ea, sizeof mac.ea,
+                   MFF_ETH_DST, 0, 48, ofpacts);
+    /* Set the EVPN lookup result bit. */
+    put_load(1, MFF_LOG_FLAGS, MLF_EVPN_LOOKUP_BIT, 1, ofpacts);
+
+    ofctrl_add_flow(flow_table, OFTABLE_EVPN_ARP_LOOKUP, arp->priority,
+                    arp->flow_uuid.parts[0], match, ofpacts,
+                    &arp->flow_uuid);
 }
 
 static void
@@ -3753,7 +3787,8 @@ physical_eval_evpn_flows(const struct physical_ctx *ctx,
 
     const struct evpn_arp *arp;
     HMAP_FOR_EACH (arp, hmap_node, ctx->evpn_arps) {
-        physical_consider_evpn_arp(ctx->local_datapaths, arp, flow_table);
+        physical_consider_evpn_arp(ctx->local_datapaths, arp, ofpacts,
+                                   &match, flow_table);
     }
 }
 
@@ -3967,14 +4002,19 @@ physical_handle_evpn_arp_changes(const struct hmap 
*local_datapaths,
                                  const struct hmapx *updated_arps,
                                  const struct uuidset *removed_arps)
 {
+    struct ofpbuf ofpacts;
+    ofpbuf_init(&ofpacts, 0);
+    struct match match = MATCH_CATCHALL_INITIALIZER;
 
     const struct hmapx_node *node;
     HMAPX_FOR_EACH (node, updated_arps) {
         const struct evpn_arp *arp = node->data;
 
         ofctrl_remove_flows(flow_table, &arp->flow_uuid);
-        physical_consider_evpn_arp(local_datapaths, arp, flow_table);
+        physical_consider_evpn_arp(local_datapaths, arp, &ofpacts,
+                                   &match, flow_table);
     }
+    ofpbuf_uninit(&ofpacts);
 
     const struct uuidset_node *uuidset_node;
     UUIDSET_FOR_EACH (uuidset_node, removed_arps) {
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index 35df0ec2f..1f0f17cb7 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -18528,6 +18528,66 @@ ovn-appctl evpn/vtep-arp-list > arp_after
 
 check diff -q arp_before arp_after
 
+AS_BOX([EVPN ARP/ND side table flows])
+dnl Verify that EVPN ARP entries also produce flows in the dedicated
+dnl side table (OFTABLE_EVPN_ARP_LOOKUP) for the switch datapath.
+dnl These flows match on metadata+IP and load the resolved MAC into
+dnl eth.dst while setting the EVPN lookup bit (reg10[25]).
+dnl Verify we have 6 flows: 3 IPv4 + 3 IPv6.
+AT_CHECK([test "$(ovs-ofctl dump-flows br-int table=OFTABLE_EVPN_ARP_LOOKUP | \
+                  grep -c priority)" = "6"])
+
+dnl Verify each IPv4 EVPN entry has a side table flow.
+AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_EVPN_ARP_LOOKUP | \
+          grep "reg0=0xac100132" | grep -q "metadata=0x$dp_key"])
+AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_EVPN_ARP_LOOKUP | \
+          grep "reg0=0xac10013c" | grep -q "metadata=0x$dp_key"])
+AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_EVPN_ARP_LOOKUP | \
+          grep "reg0=0xac100146" | grep -q "metadata=0x$dp_key"])
+
+dnl Verify each IPv6 EVPN entry has a side table flow.
+AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_EVPN_ARP_LOOKUP | \
+          grep "reg0=0x1720016" | grep "reg3=0x50" | grep -q 
"metadata=0x$dp_key"])
+AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_EVPN_ARP_LOOKUP | \
+          grep "reg0=0x1720016" | grep "reg3=0x60" | grep -q 
"metadata=0x$dp_key"])
+AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_EVPN_ARP_LOOKUP | \
+          grep "reg0=0x1720016" | grep "reg3=0x70" | grep -q 
"metadata=0x$dp_key"])
+
+dnl Verify recompute stability for side table flows.
+ovs-ofctl dump-flows br-int table=OFTABLE_EVPN_ARP_LOOKUP | \
+    grep priority | awk '{print $[7], $[8]}' | sort > evpn_side_before
+
+check ovn-appctl inc-engine/recompute
+check ovn-nbctl --wait=hv sync
+
+ovs-ofctl dump-flows br-int table=OFTABLE_EVPN_ARP_LOOKUP | \
+    grep priority | awk '{print $[7], $[8]}' | sort > evpn_side_after
+AT_CHECK([diff -q evpn_side_before evpn_side_after])
+
+dnl Verify side table flows are removed when EVPN entries are removed.
+check ip neigh del dev $BR_NAME 172.16.1.50
+check ip neigh del dev $BR_NAME 172.16.1.60
+check ip neigh del dev $BR_NAME 172.16.1.70
+
+check ip -6 neigh del dev $BR_NAME 172:16::50
+check ip -6 neigh del dev $BR_NAME 172:16::60
+check ip -6 neigh del dev $BR_NAME 172:16::70
+
+OVS_WAIT_UNTIL([test "$(ovs-ofctl dump-flows br-int 
table=OFTABLE_EVPN_ARP_LOOKUP | \
+                         grep -c priority)" = "0"])
+
+dnl Re-add the EVPN entries for subsequent tests.
+check ip neigh add dev $BR_NAME 172.16.1.50 lladdr f0:00:0f:16:10:50 nud noarp 
extern_learn
+check ip neigh add dev $BR_NAME 172.16.1.60 lladdr f0:00:0f:16:10:60 nud noarp 
extern_learn
+check ip neigh add dev $BR_NAME 172.16.1.70 lladdr f0:00:0f:16:10:70 nud noarp 
extern_learn
+
+check ip -6 neigh add dev $BR_NAME 172:16::50 lladdr f0:00:0f:16:10:50 nud 
noarp extern_learn
+check ip -6 neigh add dev $BR_NAME 172:16::60 lladdr f0:00:0f:16:10:60 nud 
noarp extern_learn
+check ip -6 neigh add dev $BR_NAME 172:16::70 lladdr f0:00:0f:16:10:70 nud 
noarp extern_learn
+
+OVS_WAIT_UNTIL([test "$(ovs-ofctl dump-flows br-int 
table=OFTABLE_EVPN_ARP_LOOKUP | \
+                         grep -c priority)" = "6"])
+
 # Remove remote workload ARP entries and check ovn-controller's state.
 check ip neigh del dev $BR_NAME 172.16.1.50
 check ip neigh del dev $BR_NAME 172.16.1.60
-- 
2.54.0

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

Reply via email to