On 6/16/26 11:54 AM, Ales Musil via dev wrote:
> 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]>
> ---
Hi Ales,
Thanks for the patch!
> 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(-)
>
...
> +
> +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 '('. * */
Nit: stray '*'.
But this should actually be:
lexer_force_match(ctx->lexer, LEX_T_ID); /* Skip "chk_evpn_arp". */
lexer_force_match(ctx->lexer, LEX_T_LPAREN); /* 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);
Regards,
Dumitru
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev