Add the chk_evpn_arp(ip) action that performs an EVPN ARP/ND
lookup in a dedicated side table (OFTABLE_EVPN_ARP_LOOKUP,
table 113).  The action takes one IP field argument (32-bit
for IPv4 or 128-bit for IPv6) and sets a 1-bit destination
regbit to indicate whether a match was found.

On a hit, the side table loads the resolved MAC address into
eth.dst and sets the MLF_EVPN_LOOKUP_BIT (bit 25 of
MFF_LOG_FLAGS).  The caller reads the MAC from eth.dst
directly in response flows.

For IPv4, the IP argument is stored in MFF_REG0.  For IPv6
it is stored in MFF_XXREG0.

This commit adds the infrastructure only: OVNACT definition,
parse/format/encode/free functions, OFTABLE definition, and
ovn-trace stub.  No flows use the action yet.

Assisted-by: Claude Opus 4.6, Claude Code
Signed-off-by: Ales Musil <[email protected]>
---
 controller/lflow.c           |  1 +
 controller/lflow.h           |  1 +
 include/ovn/actions.h        | 10 +++++
 include/ovn/logical-fields.h |  4 ++
 lib/actions.c                | 78 ++++++++++++++++++++++++++++++++++++
 lib/ovn-util.c               |  2 +-
 ovn-sb.xml                   | 41 +++++++++++++++++++
 tests/ovn-macros.at          |  1 +
 tests/ovn.at                 | 32 +++++++++++++++
 tests/test-ovn.c             |  1 +
 utilities/ovn-trace.c        | 37 +++++++++++++++++
 11 files changed, 207 insertions(+), 1 deletion(-)

diff --git a/controller/lflow.c b/controller/lflow.c
index 382c2aecb..89ad6831f 100644
--- a/controller/lflow.c
+++ b/controller/lflow.c
@@ -950,6 +950,7 @@ add_matches_to_flow_table(const struct sbrec_logical_flow 
*lflow,
         .ct_proto_load_table = OFTABLE_CT_ORIG_PROTO_LOAD,
         .flood_remote_table = OFTABLE_FLOOD_REMOTE_CHASSIS,
         .ct_state_save_table = OFTABLE_CT_STATE_SAVE,
+        .evpn_arp_ptable = OFTABLE_EVPN_ARP_LOOKUP,
         .ctrl_meter_id = ctrl_meter_id,
         .common_nat_ct_zone = get_common_nat_zone(ldp),
     };
diff --git a/controller/lflow.h b/controller/lflow.h
index 786cf974e..0b7e53a10 100644
--- a/controller/lflow.h
+++ b/controller/lflow.h
@@ -104,6 +104,7 @@ struct uuid;
 #define OFTABLE_CT_ORIG_PROTO_LOAD        110
 #define OFTABLE_GET_REMOTE_FDB            111
 #define OFTABLE_LEARN_REMOTE_FDB          112
+#define OFTABLE_EVPN_ARP_LOOKUP           113
 
 /* Verify that table regions do not overlap. */
 BUILD_ASSERT_DECL(OFTABLE_LOG_INGRESS_PIPELINE + LOG_PIPELINE_INGRESS_LEN
diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index 2ca8dac8f..4473e3297 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -140,6 +140,7 @@ struct collector_set_ids;
     OVNACT(FLOOD_REMOTE,      ovnact_null)            \
     OVNACT(CT_STATE_SAVE,     ovnact_result)          \
     OVNACT(MIRROR,            ovnact_mirror)          \
+    OVNACT(CHK_EVPN_ARP,      ovnact_chk_evpn_arp)    \
 
 /* enum ovnact_type, with a member OVNACT_<ENUM> for each action. */
 enum OVS_PACKED_ENUM ovnact_type {
@@ -549,6 +550,13 @@ struct ovnact_commit_lb_aff {
     uint16_t timeout;
 };
 
+/* OVNACT_CHK_EVPN_ARP. */
+struct ovnact_chk_evpn_arp {
+    struct ovnact ovnact;
+    struct expr_field dst;      /* 1-bit destination field. */
+    struct expr_field ip;       /* 32-bit or 128-bit IP address. */
+};
+
 /* OVNACT_MIRROR. */
 struct ovnact_mirror {
     struct ovnact ovnact;
@@ -972,6 +980,8 @@ struct ovnact_encode_params {
                                 * 'get_remote_fdb' to resubmit. */
     uint8_t fdb_lookup_ptable; /* OpenFlow table for
                                 * 'lookup_fdb' to resubmit. */
+    uint8_t evpn_arp_ptable; /* OpenFlow table for
+                              * 'chk_evpn_arp' to resubmit. */
     uint8_t in_port_sec_ptable; /* OpenFlow table for
                                 * 'check_in_port_sec' to resubmit. */
     uint8_t out_port_sec_ptable; /* OpenFlow table for
diff --git a/include/ovn/logical-fields.h b/include/ovn/logical-fields.h
index 3c0fb22e7..8b4876530 100644
--- a/include/ovn/logical-fields.h
+++ b/include/ovn/logical-fields.h
@@ -149,6 +149,7 @@ enum mff_log_flags_bits {
     MLF_IGMP_IGMP_SNOOP_INJECT_BIT = 22,
     MLF_PKT_SAMPLED_BIT = 23,
     MLF_RECIRC_BIT = 24,
+    MLF_EVPN_LOOKUP_BIT = 25,
     MLF_NETWORK_ID_START_BIT = 28,
     MLF_NETWORK_ID_END_BIT = 31,
 };
@@ -225,6 +226,9 @@ enum mff_log_flags {
     /* Indicate the packet has been processed by LOCAL table once before. */
     MLF_RECIRC = (1 << MLF_RECIRC_BIT),
 
+    /* Indicate that the lookup in the EVPN ARP table was successful. */
+    MLF_EVPN_LOOKUP = (1 << MLF_EVPN_LOOKUP_BIT),
+
     /* Assign network ID to packet to choose correct network for snat when
      * lb_force_snat_ip=router_ip. */
     MLF_NETWORK_ID = (OVN_MAX_NETWORK_ID << MLF_NETWORK_ID_START_BIT),
diff --git a/lib/actions.c b/lib/actions.c
index 3fbaed7af..3b58bca0c 100644
--- a/lib/actions.c
+++ b/lib/actions.c
@@ -2600,6 +2600,80 @@ ovnact_lookup_mac_bind_ip_free(
 
 }
 
+/* chk_evpn_arp action. */
+
+static void
+format_CHK_EVPN_ARP(const struct ovnact_chk_evpn_arp *chk, struct ds *s)
+{
+    expr_field_format(&chk->dst, s);
+    ds_put_cstr(s, " = chk_evpn_arp(");
+    expr_field_format(&chk->ip, s);
+    ds_put_cstr(s, ");");
+}
+
+static void
+encode_CHK_EVPN_ARP(const struct ovnact_chk_evpn_arp *chk,
+                     const struct ovnact_encode_params *ep,
+                     struct ofpbuf *ofpacts)
+{
+    struct mf_subfield ip_sf = expr_resolve_field(&chk->ip);
+    const struct arg args[] = {
+        { ip_sf, ip_sf.n_bits == 32 ? MFF_REG0 : MFF_XXREG0 },
+    };
+    encode_setup_args(args, ARRAY_SIZE(args), ofpacts);
+
+    put_load(0, MFF_LOG_FLAGS, MLF_EVPN_LOOKUP_BIT, 1, ofpacts);
+    emit_resubmit(ofpacts, ep->evpn_arp_ptable);
+
+    struct mf_subfield dst = expr_resolve_field(&chk->dst);
+    struct ofpact_reg_move *orm = ofpact_put_REG_MOVE(ofpacts);
+    orm->dst = dst;
+    orm->src.field = mf_from_id(MFF_LOG_FLAGS);
+    orm->src.ofs = MLF_EVPN_LOOKUP_BIT;
+    orm->src.n_bits = 1;
+
+    encode_restore_args(args, ARRAY_SIZE(args), ofpacts);
+}
+
+static void
+parse_chk_evpn_arp(struct action_context *ctx,
+                   const struct expr_field *dst,
+                   struct ovnact_chk_evpn_arp *chk)
+{
+    /* Validate destination is a 1-bit modifiable field. */
+    char *error = expr_type_check(dst, 1, true, ctx->scope);
+    if (error) {
+        lexer_error(ctx->lexer, "%s", error);
+        free(error);
+        return;
+    }
+    chk->dst = *dst;
+
+    lexer_get(ctx->lexer);  /* Skip "chk_evpn_arp". */
+    lexer_get(ctx->lexer); /* Skip '('. * */
+
+    if (!expr_field_parse(ctx->lexer, ctx->pp->symtab,
+                          &chk->ip, &ctx->prereqs)) {
+        return;
+    }
+
+    /* Validate IP width: must be 32 (IPv4) or 128 (IPv6). */
+    struct mf_subfield ip_sf = expr_resolve_field(&chk->ip);
+    if (ip_sf.n_bits != 32 && ip_sf.n_bits != 128) {
+        lexer_error(ctx->lexer,
+                    "chk_evpn_arp requires a 32-bit or "
+                    "128-bit IP field");
+        return;
+    }
+
+    lexer_force_match(ctx->lexer, LEX_T_RPAREN);
+}
+
+static void
+ovnact_chk_evpn_arp_free(struct ovnact_chk_evpn_arp *chk OVS_UNUSED)
+{
+}
+
 
 static void
 parse_gen_opt(struct action_context *ctx, struct ovnact_gen_option *o,
@@ -5877,6 +5951,10 @@ parse_set_action(struct action_context *ctx)
                 && lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
             parse_lookup_mac_bind_ip(ctx, &lhs, 128,
                                      ovnact_put_LOOKUP_ND_IP(ctx->ovnacts));
+        } else if (!strcmp(ctx->lexer->token.s, "chk_evpn_arp")
+                   && lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
+            parse_chk_evpn_arp(ctx, &lhs,
+                               ovnact_put_CHK_EVPN_ARP(ctx->ovnacts));
         } else if (!strcmp(ctx->lexer->token.s, "chk_lb_hairpin")
                    && lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
             parse_chk_lb_hairpin(ctx, &lhs,
diff --git a/lib/ovn-util.c b/lib/ovn-util.c
index e6143d7a9..cc5431a11 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -1007,7 +1007,7 @@ ip_address_and_port_from_lb_key(const char *key, char 
**ip_address,
  *
  * NOTE: If OVN_NORTHD_PIPELINE_CSUM is updated make sure to double check
  * whether an update of OVN_INTERNAL_MINOR_VER is required. */
-#define OVN_NORTHD_PIPELINE_CSUM "3760014456 11249"
+#define OVN_NORTHD_PIPELINE_CSUM "951247664 11305"
 #define OVN_INTERNAL_MINOR_VER 14
 
 /* Returns the OVN version. The caller must free the returned value. */
diff --git a/ovn-sb.xml b/ovn-sb.xml
index e45b63d73..9b453a372 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -1696,6 +1696,47 @@
           </p>
         </dd>
 
+        <dt>
+          <code><var>R</var> = chk_evpn_arp(<var>A</var>);</code>
+        </dt>
+
+        <dd>
+          <p>
+            <b>Parameters</b>: 32-bit or 128-bit IP address field
+            <var>A</var>.
+          </p>
+
+          <p>
+            <b>Result</b>: stored to a 1-bit subfield <var>R</var>.
+          </p>
+
+          <p>
+            Looks up <var>A</var> in the EVPN ARP side table for the
+            current logical datapath.  If a matching entry is found,
+            stores <code>1</code> in the 1-bit subfield <var>R</var>
+            and loads the resolved MAC address into
+            <code>eth.dst</code>.  Otherwise, stores
+            <code>0</code> in <var>R</var> and <code>eth.dst</code>
+            is left unchanged.
+          </p>
+
+          <p>
+            This action is used for EVPN ARP/ND suppression on
+            logical switches.  When an ARP request or ND solicitation
+            targets an IP address that was learned via EVPN, the
+            switch can proxy-reply using the MAC from
+            <code>eth.dst</code> instead of flooding the request
+            to remote VTEPs.
+          </p>
+
+          <p>
+            <b>Example:</b>
+            <code>
+              reg9[5] = chk_evpn_arp(arp.tpa);
+            </code>
+          </p>
+        </dd>
+
         <dt><code><var>P</var> = get_fdb(<var>A</var>);</code></dt>
 
         <dd>
diff --git a/tests/ovn-macros.at b/tests/ovn-macros.at
index c4f80642d..d96e8e61f 100644
--- a/tests/ovn-macros.at
+++ b/tests/ovn-macros.at
@@ -1649,5 +1649,6 @@ m4_define([OFTABLE_CT_STATE_SAVE], [109])
 m4_define([OFTABLE_CT_ORIG_PROTO_LOAD], [110])
 m4_define([OFTABLE_GET_REMOTE_FDB], [111])
 m4_define([OFTABLE_LEARN_REMOTE_FDB], [112])
+m4_define([OFTABLE_EVPN_ARP_LOOKUP], [113])
 
 m4_define([OFTABLE_SAVE_INPORT_HEX], [m4_eval(OFTABLE_SAVE_INPORT, 16)])
diff --git a/tests/ovn.at b/tests/ovn.at
index 272346dd7..2d2dcb385 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -2217,6 +2217,38 @@ commit_lb_aff(vip = "[[::1]]:8080", backend = 
"[[::2]]:8080", proto = tcp, timeo
 reg9[[6]] = chk_lb_aff();
     encodes as 
set_field:0/0x4000->reg10,resubmit(,OFTABLE_CHK_LB_AFFINITY),move:NXM_NX_REG10[[14]]->OXM_OF_PKT_REG4[[6]]
 
+# chk_evpn_arp
+reg0[[0]] = chk_evpn_arp(arp.tpa);
+    encodes as 
push:NXM_NX_REG0[[]],push:NXM_OF_ARP_TPA[[]],pop:NXM_NX_REG0[[]],set_field:0/0x2000000->reg10,resubmit(,OFTABLE_EVPN_ARP_LOOKUP),move:NXM_NX_REG10[[25]]->NXM_NX_XXREG0[[96]],pop:NXM_NX_REG0[[]]
+    has prereqs eth.type == 0x806
+
+reg2[[2]] = chk_evpn_arp(arp.tpa);
+    encodes as 
push:NXM_NX_REG0[[]],push:NXM_OF_ARP_TPA[[]],pop:NXM_NX_REG0[[]],set_field:0/0x2000000->reg10,resubmit(,OFTABLE_EVPN_ARP_LOOKUP),move:NXM_NX_REG10[[25]]->NXM_NX_XXREG0[[34]],pop:NXM_NX_REG0[[]]
+    has prereqs eth.type == 0x806
+
+reg0[[0]] = chk_evpn_arp(ip6.dst);
+    encodes as 
push:NXM_NX_XXREG0[[]],push:NXM_NX_IPV6_DST[[]],pop:NXM_NX_XXREG0[[]],set_field:0/0x2000000->reg10,resubmit(,OFTABLE_EVPN_ARP_LOOKUP),move:NXM_NX_REG10[[25]]->NXM_NX_XXREG0[[96]],pop:NXM_NX_XXREG0[[]]
+    has prereqs eth.type == 0x86dd
+
+reg0[[0]] = chk_evpn_arp(nd.target);
+    encodes as 
push:NXM_NX_XXREG0[[]],push:NXM_NX_ND_TARGET[[]],pop:NXM_NX_XXREG0[[]],set_field:0/0x2000000->reg10,resubmit(,OFTABLE_EVPN_ARP_LOOKUP),move:NXM_NX_REG10[[25]]->NXM_NX_XXREG0[[96]],pop:NXM_NX_XXREG0[[]]
+    has prereqs (icmp6.type == 0x87 || icmp6.type == 0x88) && eth.type == 
0x86dd && ip.proto == 0x3a && (eth.type == 0x800 || eth.type == 0x86dd) && 
icmp6.code == 0 && eth.type == 0x86dd && ip.proto == 0x3a && (eth.type == 0x800 
|| eth.type == 0x86dd) && ip.ttl == 0xff && (eth.type == 0x800 || eth.type == 
0x86dd)
+
+reg0 = chk_evpn_arp(arp.tpa);
+    Cannot use 32-bit field reg0[[0..31]] where 1-bit field is required.
+
+reg0[[0]] = chk_evpn_arp(eth.src);
+    chk_evpn_arp requires a 32-bit or 128-bit IP field
+
+reg0[[0]] = chk_evpn_arp();
+    Syntax error at `)' expecting field name.
+
+reg0[[0]] = chk_evpn_arp(arp.tpa, ip4.src);
+    Syntax error at `,' expecting `)'.
+
+chk_evpn_arp;
+    Syntax error at `chk_evpn_arp' expecting action.
+
 # push/pop
 
push(xxreg0);push(xxreg1[[10..20]]);push(eth.src);pop(xxreg0[[0..47]]);pop(xxreg0[[48..57]]);pop(xxreg1);
     formats as push(xxreg0); push(xxreg1[[10..20]]); push(eth.src); 
pop(xxreg0[[0..47]]); pop(xxreg0[[48..57]]); pop(xxreg1);
diff --git a/tests/test-ovn.c b/tests/test-ovn.c
index 114e60d65..f88d44d3d 100644
--- a/tests/test-ovn.c
+++ b/tests/test-ovn.c
@@ -1392,6 +1392,7 @@ test_parse_actions(struct ovs_cmdl_context *ctx 
OVS_UNUSED)
                 .ct_proto_load_table = OFTABLE_CT_ORIG_PROTO_LOAD,
                 .flood_remote_table = OFTABLE_FLOOD_REMOTE_CHASSIS,
                 .ct_state_save_table = OFTABLE_CT_STATE_SAVE,
+                .evpn_arp_ptable = OFTABLE_EVPN_ARP_LOOKUP,
                 .lflow_uuid.parts =
                     { 0xaaaaaaaa, 0xbbbbbbbb, 0xcccccccc, 0xdddddddd},
                 .dp_key = 0xabcdef,
diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
index c09a9041f..c0e04262c 100644
--- a/utilities/ovn-trace.c
+++ b/utilities/ovn-trace.c
@@ -2242,6 +2242,39 @@ execute_lookup_mac_bind_ip(const struct 
ovnact_lookup_mac_bind_ip *bind,
     mf_write_subfield_flow(&dst, &sv, uflow);
 }
 
+static void
+execute_chk_evpn_arp(const struct ovnact_chk_evpn_arp *chk,
+                     const struct ovntrace_datapath *dp OVS_UNUSED,
+                     struct flow *uflow,
+                     struct ovs_list *super)
+{
+    /* Get IP address. */
+    struct mf_subfield ip_sf = expr_resolve_field(&chk->ip);
+    ovs_assert(ip_sf.n_bits == 32 || ip_sf.n_bits == 128);
+    union mf_subvalue ip_sv;
+    mf_read_subfield(&ip_sf, uflow, &ip_sv);
+    struct in6_addr ip = (ip_sf.n_bits == 32
+                          ? in6_addr_mapped_ipv4(ip_sv.ipv4)
+                          : ip_sv.ipv6);
+
+    struct ds ip_s = DS_EMPTY_INITIALIZER;
+    ipv6_format_mapped(&ip, &ip_s);
+
+    /* EVPN entries only exist in ovn-controller's OpenFlow tables
+     * (OFTABLE_EVPN_ARP_LOOKUP), not in the SB database, so
+     * ovn-trace cannot resolve them.  Report the miss and set
+     * the result to 0. */
+    ovntrace_node_append(super, OVNTRACE_NODE_ACTION,
+                         "/* EVPN ARP lookup for %s: "
+                         "not available in trace. */", ds_cstr(&ip_s));
+    ds_destroy(&ip_s);
+
+    /* Set the result bit to 0 (miss). */
+    struct mf_subfield dst = expr_resolve_field(&chk->dst);
+    union mf_subvalue sv = { .u8_val = 0 };
+    mf_write_subfield_flow(&dst, &sv, uflow);
+}
+
 static void
 execute_lookup_fdb(const struct ovnact_lookup_fdb *lookup_fdb,
                    const struct ovntrace_datapath *dp,
@@ -3603,6 +3636,10 @@ trace_actions(const struct ovnact *ovnacts, size_t 
ovnacts_len,
         case OVNACT_CT_STATE_SAVE:
             execute_ct_save_state(ovnact_get_CT_STATE_SAVE(a), uflow, super);
             break;
+        case OVNACT_CHK_EVPN_ARP:
+            execute_chk_evpn_arp(ovnact_get_CHK_EVPN_ARP(a), dp, uflow,
+                                 super);
+            break;
         }
     }
     ofpbuf_uninit(&stack);
-- 
2.54.0

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

Reply via email to