> 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]>

Fixing the nits spotted by Dumitru:
Acked-by: Lorenzo Bianconi <[email protected]>

Regards,
Lorenzo

> ---
>  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
> 
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to