On 8/20/26 6:09 PM, Naveen Yerramneni wrote:
> This commit introduces two NF-related logical actions, nf_learn_orig_inport()
> and nf_lookup_orig_inport(), used to store and retrieve a packet's original
> ingress port. These actions will be used by northd to identify and drop
> post-NF loopback copies in logical pipeline stages.
>
> - "nf_learn_orig_inport(ipv6 = true|false)" installs a learned flow in
> a new OFTABLE_NF_ORIG_INPORT_LEARN table. The learned flow is keyed
> on the logical datapath and the IP source and destination addresses
> and matches future packets whose logical output port equals the
> logical input port of the packet that created it, i.e. packets that
> are about to be sent back out of the port they entered on. Matching
> packets get MLF_POST_NF_LOOP_BACK set. The learned flow has a 60s idle
> timeout.
>
> - "R = nf_lookup_orig_inport()" resubmits to the learn table and stores
> the result of the lookup (MLF_POST_NF_LOOP_BACK) into the 1-bit field R.
>
> Acked-by: Aditya Mehakare <[email protected]>
> CC: Sragdhara Datta Chaudhuri <[email protected]>
> Assisted-by: Claude Opus 4.7, Cursor
> Signed-off-by: Naveen Yerramneni <[email protected]>
> ---
Hi Naveen,
Thanks for the new revision. Applied to main.
Regards,
Dumitru
> controller/lflow.h | 1 +
> include/ovn/actions.h | 14 +++
> include/ovn/logical-fields.h | 4 +
> lib/actions.c | 190 +++++++++++++++++++++++++++++++++++
> lib/ovn-util.c | 4 +-
> ovn-sb.xml | 57 +++++++++++
> tests/ovn-macros.at | 1 +
> tests/ovn.at | 19 ++++
> utilities/ovn-trace.c | 18 ++++
> 9 files changed, 306 insertions(+), 2 deletions(-)
>
> diff --git a/controller/lflow.h b/controller/lflow.h
> index c211fe195..33b8deeba 100644
> --- a/controller/lflow.h
> +++ b/controller/lflow.h
> @@ -105,6 +105,7 @@ struct uuid;
> #define OFTABLE_GET_REMOTE_FDB 111
> #define OFTABLE_LEARN_REMOTE_FDB 112
> #define OFTABLE_EVPN_ARP_LOOKUP 113
> +#define OFTABLE_NF_ORIG_INPORT_LEARN 114
>
> /* 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 2b81f3f05..7d0ed63bc 100644
> --- a/include/ovn/actions.h
> +++ b/include/ovn/actions.h
> @@ -140,6 +140,8 @@ struct collector_set_ids;
> OVNACT(CT_STATE_SAVE, ovnact_result) \
> OVNACT(MIRROR, ovnact_mirror) \
> OVNACT(CHK_EVPN_ARP, ovnact_chk_evpn_arp) \
> + OVNACT(NF_LEARN_ORIG_INPORT, ovnact_nf_learn) \
> + OVNACT(NF_LOOKUP_ORIG_INPORT, ovnact_nf_lookup) \
>
> /* enum ovnact_type, with a member OVNACT_<ENUM> for each action. */
> enum OVS_PACKED_ENUM ovnact_type {
> @@ -513,6 +515,18 @@ struct ovnact_lookup_fdb {
> struct expr_field dst; /* 1-bit destination field. */
> };
>
> +/* OVNACT_NF_LEARN_ORIG_INPORT. */
> +struct ovnact_nf_learn {
> + struct ovnact ovnact;
> + bool ipv6;
> +};
> +
> +/* OVNACT_NF_LOOKUP_ORIG_INPORT. */
> +struct ovnact_nf_lookup {
> + struct ovnact ovnact;
> + struct expr_field dst; /* 1-bit destination field. */
> +};
> +
> /* OVNACT_SAMPLE */
> struct ovnact_sample {
> struct ovnact ovnact;
> diff --git a/include/ovn/logical-fields.h b/include/ovn/logical-fields.h
> index 9694d105f..e95dee316 100644
> --- a/include/ovn/logical-fields.h
> +++ b/include/ovn/logical-fields.h
> @@ -140,6 +140,7 @@ enum mff_log_flags_bits {
> MLF_PKT_SAMPLED_BIT = 23,
> MLF_RECIRC_BIT = 24,
> MLF_EVPN_LOOKUP_BIT = 25,
> + MLF_POST_NF_LOOP_BACK_BIT = 26,
> MLF_NETWORK_ID_START_BIT = 28,
> MLF_NETWORK_ID_END_BIT = 31,
> };
> @@ -219,6 +220,9 @@ enum mff_log_flags {
> /* Indicate that the lookup in the EVPN ARP table was successful. */
> MLF_EVPN_LOOKUP = (1 << MLF_EVPN_LOOKUP_BIT),
>
> + /* Set on a post-NF packet going back to the original ingress port. */
> + MLF_POST_NF_LOOP_BACK = (1 << MLF_POST_NF_LOOP_BACK_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 4b5fda856..77b053361 100644
> --- a/lib/actions.c
> +++ b/lib/actions.c
> @@ -4718,6 +4718,189 @@ ovnact_lookup_fdb_free(struct ovnact_lookup_fdb
> *get_fdb OVS_UNUSED)
> {
> }
>
> +static void
> +format_NF_LEARN_ORIG_INPORT(const struct ovnact_nf_learn *nf_learn,
> + struct ds *s)
> +{
> + ds_put_format(s, "nf_learn_orig_inport(ipv6 = %s);",
> + nf_learn->ipv6 ? "true" : "false");
> +}
> +
> +/* Idle timeout, in seconds, of the flow installed by
> + * nf_learn_orig_inport(). */
> +#define NF_LEARN_ORIG_INPORT_IDLE_TIMEOUT_S 60
> +
> +/* Adds a NXAST_LEARN spec matching the value of field 'id' (as it is in the
> + * packet that triggers the learn) on the same field of future packets. */
> +static void
> +nf_learn_put_match_field(struct ofpbuf *ofpacts, enum mf_field_id id)
> +{
> + struct ofpact_learn_spec *ol_spec =
> + ofpbuf_put_zeros(ofpacts, sizeof *ol_spec);
> + ol_spec->dst.field = mf_from_id(id);
> + ol_spec->dst.ofs = 0;
> + ol_spec->dst.n_bits = ol_spec->dst.field->n_bits;
> + ol_spec->n_bits = ol_spec->dst.n_bits;
> + ol_spec->dst_type = NX_LEARN_DST_MATCH;
> + ol_spec->src_type = NX_LEARN_SRC_FIELD;
> + ol_spec->src.field = mf_from_id(id);
> +}
> +
> +static void
> +encode_NF_LEARN_ORIG_INPORT(const struct ovnact_nf_learn *nf_learn,
> + const struct ovnact_encode_params *ep,
> + struct ofpbuf *ofpacts)
> +{
> + size_t ol_offset = ofpacts->size;
> + struct ofpact_learn *ol = ofpact_put_LEARN(ofpacts);
> + struct match match = MATCH_CATCHALL_INITIALIZER;
> + struct ofpact_learn_spec *ol_spec;
> + unsigned int imm_bytes;
> + uint8_t *src_imm;
> +
> + ol->flags = NX_LEARN_F_DELETE_LEARNED;
> + ol->idle_timeout = NF_LEARN_ORIG_INPORT_IDLE_TIMEOUT_S;
> + ol->hard_timeout = OFP_FLOW_PERMANENT;
> + ol->priority = OFP_DEFAULT_PRIORITY;
> + ol->table_id = OFTABLE_NF_ORIG_INPORT_LEARN;
> + ol->cookie = htonll(ep->lflow_uuid.parts[0]);
> +
> + /* Match on the logical datapath. */
> + nf_learn_put_match_field(ofpacts, MFF_METADATA);
> +
> + /* Match on the same ETH type as the packet that created the flow. */
> + ol_spec = ofpbuf_put_zeros(ofpacts, sizeof *ol_spec);
> + ol_spec->dst.field = mf_from_id(MFF_ETH_TYPE);
> + ol_spec->dst.ofs = 0;
> + ol_spec->dst.n_bits = ol_spec->dst.field->n_bits;
> + ol_spec->n_bits = ol_spec->dst.n_bits;
> + ol_spec->dst_type = NX_LEARN_DST_MATCH;
> + ol_spec->src_type = NX_LEARN_SRC_IMMEDIATE;
> + union mf_value imm_eth_type = {
> + .be16 = nf_learn->ipv6 ? htons(ETH_TYPE_IPV6) : htons(ETH_TYPE_IP)
> + };
> + mf_write_subfield_value(&ol_spec->dst, &imm_eth_type, &match);
> + /* Push value last, as this may reallocate 'ol_spec'. */
> + imm_bytes = DIV_ROUND_UP(ol_spec->dst.n_bits, 8);
> + src_imm = ofpbuf_put_zeros(ofpacts, OFPACT_ALIGN(imm_bytes));
> + memcpy(src_imm, &imm_eth_type, imm_bytes);
> +
> + /* Match on the IP source and destination addresses. */
> + nf_learn_put_match_field(ofpacts,
> + nf_learn->ipv6 ? MFF_IPV6_SRC : MFF_IPV4_SRC);
> + nf_learn_put_match_field(ofpacts,
> + nf_learn->ipv6 ? MFF_IPV6_DST : MFF_IPV4_DST);
> +
> + /* Match future packets whose logical output port equals the logical
> + * input port of the packet that created this flow. */
> + ol_spec = ofpbuf_put_zeros(ofpacts, sizeof *ol_spec);
> + ol_spec->dst.field = mf_from_id(MFF_LOG_OUTPORT);
> + ol_spec->dst.ofs = 0;
> + ol_spec->dst.n_bits = ol_spec->dst.field->n_bits;
> + ol_spec->n_bits = ol_spec->dst.n_bits;
> + ol_spec->dst_type = NX_LEARN_DST_MATCH;
> + ol_spec->src_type = NX_LEARN_SRC_FIELD;
> + ol_spec->src.field = mf_from_id(MFF_LOG_INPORT);
> +
> + /* Set MLF_POST_NF_LOOP_BACK on matching packets. */
> + ol_spec = ofpbuf_put_zeros(ofpacts, sizeof *ol_spec);
> + ol_spec->dst.field = mf_from_id(MFF_LOG_FLAGS);
> + ol_spec->dst.ofs = MLF_POST_NF_LOOP_BACK_BIT;
> + ol_spec->dst.n_bits = 1;
> + ol_spec->n_bits = ol_spec->dst.n_bits;
> + ol_spec->dst_type = NX_LEARN_DST_LOAD;
> + ol_spec->src_type = NX_LEARN_SRC_IMMEDIATE;
> + union mf_value imm_hit = { .u8 = 1 };
> + mf_write_subfield_value(&ol_spec->dst, &imm_hit, &match);
> + /* Push value last, as this may reallocate 'ol_spec'. */
> + imm_bytes = DIV_ROUND_UP(ol_spec->dst.n_bits, 8);
> + src_imm = ofpbuf_put_zeros(ofpacts, OFPACT_ALIGN(imm_bytes));
> + memcpy(src_imm, &imm_hit, imm_bytes);
> +
> + ol = ofpbuf_at_assert(ofpacts, ol_offset, sizeof *ol);
> + ofpact_finish_LEARN(ofpacts, &ol);
> +}
> +
> +static void
> +parse_nf_learn_orig_inport(struct action_context *ctx,
> + struct ovnact_nf_learn *nf_learn)
> +{
> + lexer_force_match(ctx->lexer, LEX_T_LPAREN);
> + if (!lexer_match_id(ctx->lexer, "ipv6")) {
> + lexer_syntax_error(ctx->lexer, "invalid parameter");
> + return;
> + }
> + if (!lexer_force_match(ctx->lexer, LEX_T_EQUALS)) {
> + return;
> + }
> + if (lexer_match_id(ctx->lexer, "true")) {
> + nf_learn->ipv6 = true;
> + } else if (lexer_match_id(ctx->lexer, "false")) {
> + nf_learn->ipv6 = false;
> + } else {
> + lexer_syntax_error(ctx->lexer, "expecting true or false");
> + return;
> + }
> + lexer_force_match(ctx->lexer, LEX_T_RPAREN);
> +}
> +
> +static void
> +ovnact_nf_learn_free(struct ovnact_nf_learn *nf_learn OVS_UNUSED)
> +{
> +}
> +
> +static void
> +format_NF_LOOKUP_ORIG_INPORT(const struct ovnact_nf_lookup *nf_lookup,
> + struct ds *s)
> +{
> + expr_field_format(&nf_lookup->dst, s);
> + ds_put_cstr(s, " = nf_lookup_orig_inport();");
> +}
> +
> +static void
> +encode_NF_LOOKUP_ORIG_INPORT(
> + const struct ovnact_nf_lookup *nf_lookup,
> + const struct ovnact_encode_params *ep OVS_UNUSED,
> + struct ofpbuf *ofpacts)
> +{
> + struct mf_subfield dst = expr_resolve_field(&nf_lookup->dst);
> + ovs_assert(dst.field);
> +
> + put_load(0, MFF_LOG_FLAGS, MLF_POST_NF_LOOP_BACK_BIT, 1, ofpacts);
> + emit_resubmit(ofpacts, OFTABLE_NF_ORIG_INPORT_LEARN);
> +
> + 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_POST_NF_LOOP_BACK_BIT;
> + orm->src.n_bits = 1;
> +}
> +
> +static void
> +parse_nf_lookup_orig_inport(struct action_context *ctx,
> + struct expr_field *dst,
> + struct ovnact_nf_lookup *nf_lookup)
> +{
> + lexer_get(ctx->lexer); /* Skip nf_lookup_orig_inport. */
> + lexer_force_match(ctx->lexer, LEX_T_LPAREN);
> +
> + /* Validate that the 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;
> + }
> + nf_lookup->dst = *dst;
> +
> + lexer_force_match(ctx->lexer, LEX_T_RPAREN);
> +}
> +
> +static void
> +ovnact_nf_lookup_free(struct ovnact_nf_lookup *nf_lookup OVS_UNUSED)
> +{
> +}
> +
> static void
> parse_check_in_port_sec(struct action_context *ctx,
> const struct expr_field *dst,
> @@ -5977,6 +6160,10 @@ parse_set_action(struct action_context *ctx)
> && lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
> parse_lookup_fdb(
> ctx, &lhs, ovnact_put_LOOKUP_FDB(ctx->ovnacts));
> + } else if (!strcmp(ctx->lexer->token.s, "nf_lookup_orig_inport")
> + && lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
> + parse_nf_lookup_orig_inport(
> + ctx, &lhs, ovnact_put_NF_LOOKUP_ORIG_INPORT(ctx->ovnacts));
> } else if (!strcmp(ctx->lexer->token.s, "check_in_port_sec")
> && lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
> parse_check_in_port_sec(
> @@ -6148,6 +6335,9 @@ parse_action(struct action_context *ctx)
> ovnact_put_FLOOD_REMOTE(ctx->ovnacts);
> } else if (lexer_match_id(ctx->lexer, "mirror")) {
> parse_MIRROR_action(ctx);
> + } else if (lexer_match_id(ctx->lexer, "nf_learn_orig_inport")) {
> + parse_nf_learn_orig_inport(
> + ctx, ovnact_put_NF_LEARN_ORIG_INPORT(ctx->ovnacts));
> } else {
> lexer_syntax_error(ctx->lexer, "expecting action");
> }
> diff --git a/lib/ovn-util.c b/lib/ovn-util.c
> index d9b78fbb6..9842afb3c 100644
> --- a/lib/ovn-util.c
> +++ b/lib/ovn-util.c
> @@ -1007,8 +1007,8 @@ 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 "118971668 11318"
> -#define OVN_INTERNAL_MINOR_VER 15
> +#define OVN_NORTHD_PIPELINE_CSUM "1167787232 11430"
> +#define OVN_INTERNAL_MINOR_VER 16
>
> /* Returns the OVN version. The caller must free the returned value. */
> char *
> diff --git a/ovn-sb.xml b/ovn-sb.xml
> index d5c5210dc..357f3d7c9 100644
> --- a/ovn-sb.xml
> +++ b/ovn-sb.xml
> @@ -1832,6 +1832,63 @@
> </p>
> </dd>
>
> + <dt>
> + <code>nf_learn_orig_inport(ipv6 = <var>B</var>);</code>
> + </dt>
> +
> + <dd>
> + <p>
> + Records the logical input port that the current IP packet
> + entered the logical switch on by learning a flow. The learned
> + flow matches a later packet of the same flow (same logical
> + datapath and IP source and destination addresses) when its
> + logical output port equals that input port, that is, when the
> + packet is about to be sent back out of the port it originally
> + arrived on.
> + </p>
> +
> + <p>
> + <var>B</var> must be <code>true</code> to learn an IPv6 flow or
> + <code>false</code> to learn an IPv4 flow. The learned flow has a
> + 60 second idle timeout. This action is used together with
> + <code>nf_lookup_orig_inport()</code> to drop the duplicate
> + copies that a network function (NF) redirection can produce when
> + the destination MAC address is still unknown, which would
> + otherwise cause MAC address flaps or L2 loops.
> + </p>
> +
> + <p>
> + <b>Example:</b>
> + <code>nf_learn_orig_inport(ipv6 = false);</code>
> + </p>
> + </dd>
> +
> + <dt>
> + <code><var>R</var> = nf_lookup_orig_inport();</code>
> + </dt>
> +
> + <dd>
> + <p>
> + <b>Result</b>: stored to a 1-bit subfield <var>R</var>.
> + </p>
> +
> + <p>
> + Looks up the flow learned by
> + <code>nf_learn_orig_inport()</code>. Stores <code>1</code> in
> + <var>R</var> if the current packet matches a learned flow, that
> is,
> + if it is an IP packet that, after network function redirection,
> is
> + about to be sent back out of the logical input port it originally
> + arrived on. Otherwise stores <code>0</code>.
> + </p>
> +
> + <p>
> + <b>Example:</b>
> + <code>
> + reg0[0] = nf_lookup_orig_inport();
> + </code>
> + </p>
> + </dd>
> +
> <dt><code>nd_ns { <var>action</var>; </code>...<code> };</code></dt>
> <dd>
> <p>
> diff --git a/tests/ovn-macros.at b/tests/ovn-macros.at
> index ab14b65c6..2a1faf677 100644
> --- a/tests/ovn-macros.at
> +++ b/tests/ovn-macros.at
> @@ -1655,5 +1655,6 @@ 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_NF_ORIG_INPORT_LEARN], [114])
>
> m4_define([OFTABLE_SAVE_INPORT_HEX], [m4_eval(OFTABLE_SAVE_INPORT, 16)])
> diff --git a/tests/ovn.at b/tests/ovn.at
> index 9c4698d76..52fb75286 100644
> --- a/tests/ovn.at
> +++ b/tests/ovn.at
> @@ -2129,6 +2129,25 @@ reg1[[1]] = lookup_fdb(outport, ip4.src);
> reg1[[1]] = lookup_fdb(ip4.src, eth.src);
> Cannot use numeric field ip4.src where string field is required.
>
> +# nf_learn_orig_inport / nf_lookup_orig_inport
> +nf_learn_orig_inport(ipv6 = false);
> + encodes as
> learn(table=OFTABLE_NF_ORIG_INPORT_LEARN,idle_timeout=60,delete_learned,cookie=0xaaaaaaaa,OXM_OF_METADATA[[]],eth_type=0x800,NXM_OF_IP_SRC[[]],NXM_OF_IP_DST[[]],NXM_NX_REG15[[]]=NXM_NX_REG14[[0..-1]],load:0x1->NXM_NX_REG10[[26]])
> +
> +nf_learn_orig_inport(ipv6 = true);
> + encodes as
> learn(table=OFTABLE_NF_ORIG_INPORT_LEARN,idle_timeout=60,delete_learned,cookie=0xaaaaaaaa,OXM_OF_METADATA[[]],eth_type=0x86dd,NXM_NX_IPV6_SRC[[]],NXM_NX_IPV6_DST[[]],NXM_NX_REG15[[]]=NXM_NX_REG14[[0..-1]],load:0x1->NXM_NX_REG10[[26]])
> +
> +nf_learn_orig_inport();
> + Syntax error at `)' invalid parameter.
> +
> +nf_learn_orig_inport(ipv6 = maybe);
> + Syntax error at `maybe' expecting true or false.
> +
> +reg0[[0]] = nf_lookup_orig_inport();
> + encodes as
> set_field:0/0x4000000->reg10,resubmit(,OFTABLE_NF_ORIG_INPORT_LEARN),move:NXM_NX_REG10[[26]]->NXM_NX_XXREG0[[96]]
> +
> +reg0 = nf_lookup_orig_inport();
> + Cannot use 32-bit field reg0[[0..31]] where 1-bit field is required.
> +
> # check_in_port_sec
> reg0[[0]] = check_in_port_sec();
> encodes as
> set_field:0/0x1000->reg10,resubmit(,OFTABLE_CHK_IN_PORT_SEC),move:NXM_NX_REG10[[12]]->NXM_NX_XXREG0[[96]]
> diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
> index 0dcee89ac..1fcebf584 100644
> --- a/utilities/ovn-trace.c
> +++ b/utilities/ovn-trace.c
> @@ -3216,6 +3216,17 @@ execute_ct_save_state(const struct ovnact_result *dl,
> struct flow *uflow,
> ds_destroy(&s);
> }
>
> +static void
> +execute_nf_lookup_orig_inport(const struct ovnact_nf_lookup *nf_lookup,
> + struct flow *uflow, struct ovs_list *super)
> +{
> + struct mf_subfield dst = expr_resolve_field(&nf_lookup->dst);
> + union mf_subvalue sv = { .u8_val = 0 };
> + mf_write_subfield_flow(&dst, &sv, uflow);
> + ovntrace_node_append(super, OVNTRACE_NODE_ACTION,
> + "/* Assumed nf_lookup_orig_inport miss. */");
> +}
> +
> static void
> execute_ct_orig_tp_dst(const struct ovnact_result *res, struct flow *uflow,
> struct ovs_list *super)
> @@ -3641,6 +3652,13 @@ trace_actions(const struct ovnact *ovnacts, size_t
> ovnacts_len,
> execute_chk_evpn_arp(ovnact_get_CHK_EVPN_ARP(a), dp, uflow,
> super);
> break;
> + case OVNACT_NF_LEARN_ORIG_INPORT:
> + /* Nothing to do for tracing. */
> + break;
> + case OVNACT_NF_LOOKUP_ORIG_INPORT:
> + execute_nf_lookup_orig_inport(
> + ovnact_get_NF_LOOKUP_ORIG_INPORT(a), uflow, super);
> + break;
> }
> }
> ofpbuf_uninit(&stack);
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev