> On 12 Aug 2026, at 3:35 PM, Dumitru Ceara <[email protected]> wrote:
> 
> !-------------------------------------------------------------------|
>  CAUTION: External Email
> 
> |-------------------------------------------------------------------!
> 
> Hi Naveen,

Hi Dumitru,

> 
> On 8/11/26 5:10 PM, Naveen Yerramneni wrote:
>> 
>> 
>>> On 11 Aug 2026, at 8:27 PM, Naveen Yerramneni 
>>> <[email protected]> wrote:
>>> 
>>> 
>>> 
>>>> On 10 Aug 2026, at 7:02 PM, Dumitru Ceara <[email protected]> wrote:
>>>> 
>>>> !-------------------------------------------------------------------|
>>>> CAUTION: External Email
>>>> 
>>>> |-------------------------------------------------------------------!
>>>> 
>>>> Hi Naveen,
>>> 
>>> Hi Dumitru,
>>> 
>>>> 
>>>> Thanks for v5, please see some review comments below.  But before we go
>>>> to v6 I'd like to first agree on the right path forward about the new
>>>> flag this patch is adding, more details below inline.
>>> 
>>> Thanks for the review!
>>> 
>>>> 
>>>> On 8/4/26 4:31 AM, Naveen Yerramneni wrote:
>>>>> A Network Function (NF) in inline mode redirects matched traffic
>>>>> through a service VM.  When the redirected traffic is IP
>>>>> unknown-unicast (destination MAC not yet in the FDB), the packet
>>>>> coming back from the NF is re-flooded by the switch, because the
>>>>> destination MAC is still unknown.  That re-flood produces a copy on
>>>>> the same port the packet originally arrived on, causing MAC flaps and
>>>>> potential L2 loops.
>>>>> 
>>>>> Following are the example packet flows.
>>>>> 
>>>>> Example 1, VLAN switch (MAC flap):
>>>>> 
>>>>> Topology
>>>>>  VM1, VM2 and the NF are on the same logical switch (LS), which is
>>>>>  VLAN-backed (localnet port on every node).  VM1 is on N1, VM2 on
>>>>>  N2 (its port has "unknown" in addresses and a to-lport ACL that
>>>>>  redirects to the inline NF on N3).
>>>>> 
>>>>> Flow
>>>>>  1. VM1 sends pkt to dst MAC X (not in FDB).
>>>>>  2. The LS floods the pkt; on N1 the copy exits the localnet port
>>>>>     and the TOR floods it to N2.
>>>>>  3. On N2 the pkt ingresses on localnet; the LS floods it and the
>>>>>     copy reaches VM2 (unknown-addr).
>>>>>  4. The ACL redirects the pkt to the NF on N3; the NF returns it
>>>>>     to N2.
>>>>>  5. X is still not in the FDB, so the LS floods again; on N2 one
>>>>>     copy exits the localnet port.
>>>>> 
>>>>> Result
>>>>>  The TOR now sees VM1's source MAC on N2's port, but it had just
>>>>>  learned VM1's MAC on N1's port, so the MAC flaps on the TOR.
>>>>> 
>>>>> Example 2, VLAN switch with two protected VMs (loop):
>>>>> 
>>>>> Topology
>>>>>  As above, plus VM3 (on N3, same LS) also has "unknown" in
>>>>>  addresses and is also NF-protected.
>>>>> 
>>>>> Flow
>>>>>  1-5 as above; in parallel, the original flood also reaches N3 via
>>>>>      the TOR, where it is redirected to N3's NF, returns, and the
>>>>>      LS re-floods it out N3's localnet port.
>>>>>  6. N3's re-flood reaches N2 via the TOR; the LS floods it on N2,
>>>>>     the copy hits VM2 (unknown-addr), is redirected to the NF, and
>>>>>     re-floods out N2's localnet port.
>>>>>  7. That re-flood reaches N3 via the TOR; the LS floods it on N3,
>>>>>     the copy hits VM3 (also unknown-addr), is redirected to the
>>>>>     NF, re-floods out localnet, ...
>>>>> 
>>>>> Result
>>>>>  The pkt keeps bouncing between N2 and N3 via the TOR, i.e. an
>>>>>  L2 loop, on top of continuous MAC flaps.  The loop persists as
>>>>>  long as X stays unknown.
>>>>> 
>>>>> Example 3, overlay switch (copy reflected to inport):
>>>>> 
>>>>> Topology
>>>>>  VM1, VM2 and the NF are on the same LS and all on N1.  VM1 and
>>>>>  VM2 both have "unknown" in addresses; VM2's to-lport ACL
>>>>>  redirects to the inline NF.
>>>>> 
>>>>> Flow
>>>>>  1. VM1 sends pkt to dst MAC X (not in FDB).
>>>>>  2. The LS floods to MC_UNKNOWN members on N1; one copy goes to
>>>>>     VM2.
>>>>>  3. VM2's ACL redirects its copy to the NF; the NF returns it to
>>>>>     N1.
>>>>>  4. X is still not in the FDB, so the LS floods to MC_UNKNOWN
>>>>>     again; one copy is headed back out VM1's own port.
>>>>> 
>>>>> Result
>>>>>  VM1 receives a copy of the packet it just sent (reflected to the
>>>>>  inport).
>>>>> 
>>>>> Fix:
>>>>> 
>>>>> Use the nf_learn_orig_inport() / nf_lookup_orig_inport() actions
>>>>> from the previous commit to remember the original ingress port and
>>>>> drop the copy if it is about to be sent back out of that port.
>>>>> 
>>>>> - Introduce a logical flag flags.inport_in_mc_unknown (bit 26)
>>>>>  that marks packets entering on an MC_UNKNOWN-member port (i.e. a
>>>>>  port with "unknown" in addresses and receive_multicast not
>>>>>  disabled, the only ports that can both originate and receive
>>>>>  unknown-unicast floods).  ovn-controller sets it in the
>>>>>  physical-to-logical stage (OFTABLE_PHY_TO_LOG resubmits into a
>>>>>  new side table OFTABLE_INPORT_IN_MC_UNKNOWN_LOOKUP), which is
>>>>>  populated for the local members of the per-datapath _MC_unknown
>>>>>  multicast group in SB.  The flag is carried in MFF_LOG_FLAGS
>>>>>  into the egress pipeline.
>>>>> 
>>>>> - In the NF redirect stage (ls_in_nf for a from-lport ACL,
>>>>>  ls_out_nf for a to-lport ACL), on a switch with an inline NF
>>>>>  group on an ACL, learn the original ingress port
>>>>>  (nf_learn_orig_inport()) for unicast IP packets that entered on
>>>>>  an MC_UNKNOWN-member port (flags.inport_in_mc_unknown == 1).
>>>>>  Two flows per IP version:
>>>>>  * Priority 100: learn, then redirect to the NF, when the ACL
>>>>>    selected the packet for redirect.  The existing NF-port and
>>>>>    multicast skip flows move up to priority 110 so multicast is
>>>>>    skipped before this flow.
>>>>>  * Priority 50 (overlay switches only, in ls_in_nf): learn on
>>>>>    the inport's ingress node when the ACL did not redirect
>>>>>    the packet (REGBIT_NF_ENABLED == 0), so the learn stays
>>>>>    co-located with the lookup even when the redirecting port and
>>>>>    the inport live on different nodes.  VLAN-backed switches
>>>>>    do not need this: the post-NF packet is always sent back to
>>>>>    the inport's node.
>>>>> 
>>>>> - On the post-NF return path, run
>>>>>  REGBIT_NF_LOOKUP_HIT = nf_lookup_orig_inport() on both NF ports.
>>>>>  A priority-115 flow in ls_out_pre_acl runs the lookup on
>>>>>  input_port and skips already-traversed egress stages.  A
>>>>>  priority-2 flow in ls_out_nf runs the lookup on output_port for
>>>>>  packets redirected from the ingress pipeline (for example, a
>>>>>  from-lport request that is re-flooded after NF processing and
>>>>>  re-enters egress).
>>>>> 
>>>>> - A new priority-110 flow in ls_out_check_port_sec drops packets
>>>>>  with REGBIT_NF_LOOKUP_HIT == 1, i.e. the copies about to be
>>>>>  sent back out of the port they originally arrived on.
>>>>> 
>>>>> All new flows are gated on the switch having an inline NF group on
>>>>> an ACL.
>>>>> 
>>>>> Signed-off-by: Naveen Yerramneni <[email protected]>
>>>>> Acked-by: Aditya Mehakare <[email protected]>
>>>>> Fixes: 8e2d6fa14804 ("northd, tests: Network Function insertion logical 
>>>>> flow programming.")
>>>>> CC: Sragdhara Datta Chaudhuri <[email protected]>
>>>>> Assisted-by: Claude Opus 4.7, Cursor
>>>> 
>>>> Same nit about your signed-off-by that should be last.
>>> 
>>> Ack.
>>> 
>>>> 
>>>>> ---
>>>>> TODO.rst                     |  10 ++
>>>>> controller/lflow.h           |   1 +
>>>>> controller/physical.c        |  39 +++++
>>>>> include/ovn/logical-fields.h |   4 +
>>>>> lib/logical-fields.c         |   5 +
>>>>> northd/northd.c              | 151 +++++++++++++---
>>>>> ovn-nb.xml                   |  13 ++
>>>>> tests/ovn-macros.at          |   1 +
>>>>> tests/ovn-northd.at          | 326 +++++++++++++++++++++++++++++------
>>>>> tests/ovn.at                 | 113 ++++++++++++
>>>>> 10 files changed, 585 insertions(+), 78 deletions(-)
>>>>> 
>>>>> diff --git a/TODO.rst b/TODO.rst
>>>>> index beca38daf..9e7807b58 100644
>>>>> --- a/TODO.rst
>>>>> +++ b/TODO.rst
>>>>> @@ -177,6 +177,16 @@ OVN To-do List
>>>>> * Geneve tunnel is used for supporting this feature for VLAN network.
>>>>>   Extend the support over VxLAN tunnel as well.
>>>>> 
>>>>> +  * Inline-NF loop prevention has limitations in topologies where
>>>>> +    a post-NF flood copy may reach the same logical inport from a
>>>>> +    different node than the packet's original ingress node.  In that
>>>>> +    case, the lookup runs on a different node than where the inport
>>>>> +    was learned, so loopback copies are not identified.
>>>>> +    Known example: OVN-IC L2 stretch with the NF and IC gateway on
>>>>> +    different chassis.  With ``unknown``-addressed ports, the post-NF
>>>>> +    packet can be flooded across AZs and come back to the source AZ via
>>>>> +    another node, so the lookup does not hit the original learn entry.
>>>>> +
>>>>> * CI
>>>>> 
>>>>> * ovn-kubernetes: Only a subset of the ovn-kubernetes features is 
>>>>> currently
>>>>> diff --git a/controller/lflow.h b/controller/lflow.h
>>>>> index fa206392f..27104486f 100644
>>>>> --- a/controller/lflow.h
>>>>> +++ b/controller/lflow.h
>>>>> @@ -106,6 +106,7 @@ struct uuid;
>>>>> #define OFTABLE_LEARN_REMOTE_FDB          112
>>>>> #define OFTABLE_EVPN_ARP_LOOKUP           113
>>>>> #define OFTABLE_NF_ORIG_INPORT_LEARN    114
>>>>> +#define OFTABLE_INPORT_IN_MC_UNKNOWN_LOOKUP 115
>>>>> 
>>>>> /* Verify that table regions do not overlap. */
>>>>> BUILD_ASSERT_DECL(OFTABLE_LOG_INGRESS_PIPELINE + LOG_PIPELINE_INGRESS_LEN
>>>>> diff --git a/controller/physical.c b/controller/physical.c
>>>>> index 452256e7a..3cd7b2195 100644
>>>>> --- a/controller/physical.c
>>>>> +++ b/controller/physical.c
>>>>> @@ -1605,6 +1605,9 @@ load_logical_ingress_metadata(const struct 
>>>>> sbrec_port_binding *binding,
>>>>>       }
>>>>>       put_load(encap_id, MFF_LOG_ENCAP_ID, 16, 16, ofpacts_p);
>>>>>   }
>>>>> +
>>>>> +    /* Resubmit to OFTABLE_INPORT_IN_MC_UNKNOWN_LOOKUP table. */
>>>>> +    put_resubmit(OFTABLE_INPORT_IN_MC_UNKNOWN_LOOKUP, ofpacts_p);
>>>> 
>>>> So I've been thinking some more about this.  On v4 (I think) I asked
>>>> whether it's not an option to just execute nf_learn_orig_inport() on all
>>>> IP packets in the NF logical stages.
>>>> 
>>>> In the end it's just an optimization to run it only on packets received
>>>> from ports configured with "unknown" addresses.  It wouldn't hurt if we
>>>> just ran it on all IP packets, right?
>>>> 
>>>> That would remove the need for the new inport_in_mc_unknown flag all
>>>> together right?
>>>> 
>>>> And it should just work fine for well configured clusters.  It would
>>>> also solve the performance related concerns I have when looking at this
>>>> patch (I know I suggested the side table but):
>>>> - the resubmit will happen for all packets ingressing on all logical
>>>> switches
>>>> - the check in "consider_mc_group()" in physical.c is kind of ugly
>>>> because it singles out "unknown" multicast groups, it may also be
>>>> relatively costly as we need to manage these new flows as well
>>>> - the very specific flag name itself (inport_in_mc_unknown) is already a
>>>> sign that we're just working around a specific problem, ideally what
>>>> happens in the physical->logical stage should be generic enough, this is
>>>> not so much the case if we accept your patch.
>>> 
>>> 
>>> Yes, this is an optimization to learn entries only from ports configured
>>> with "unknown" addresses, so we add only the required entries to
>>> OFTABLE_NF_ORIG_INPORT_LEARN table.
>>> 
>>> I also considered learning on the NF node before redirect. I did not use
>>> that approach because it would add more flows (all redirected flows) to
>>> OFTABLE_NF_ORIG_INPORT_LEARN table on the NF node. We would likely
>>> also need a new stage at the end of, or after, ingress to detect and drop 
>>> loopback
>>> packets on the NF node before egress pipeline, since egress processing runs 
>>> on the
>>> destination node.
>>> 
>>> 
>> 
>> Hi Dumitru,
>> 
>> If you prefer to always learn the inport, I will update the code and send v6.
> 
> At this point it seems like the simplest/safest approach to me so I'd
> prefer always learning the inport if the packet is going to a NF.
> 
> If in the future we realize we need to restrict this further (e.g., due
> to too many flows being learned) we can revisit it and think of a better
> way.  As said above, one of the reasons I'm reluctant to accept the v5
> approach is because I'm not sure what impact it may have at scale on
> non-NF deployments.

Sure. I will send v6 with the changes.

> 
>> Also, I am thinking to set the learned flows idle timeout to 10 secs instead 
>> of 30secs
>> to match default idle timeout of OVS data path offloaded flows.
>> 
> 
> I'm not against it but I'm wondering what you're trying to achieve with
> that.

Idle flows will get evicted a bit early. It may not make significant difference.
Setting below OVS data path offloaded idle timeout value may cause
frequent up calls hence thought of setting it to 10 secs.

> 
>>>> 
>>>>> }
>>>>> 
>>>>> static bool
>>>>> @@ -2923,6 +2926,27 @@ mc_ofctrl_add_flow(const struct 
>>>>> sbrec_multicast_group *mc,
>>>>>   ofpbuf_clear(&ctx->ofpacts);
>>>>> }
>>>>> 
>>>>> +static void
>>>>> +add_inport_to_mc_unknown_table(const struct sbrec_multicast_group *mc,
>>>>> +                               const struct sbrec_port_binding *port,
>>>>> +                               struct ovn_desired_flow_table *flow_table)
>>>>> +{
>>>>> +    struct match match = MATCH_CATCHALL_INITIALIZER;
>>>>> +    match_inport_dp_and_port_keys(&match, mc->datapath->tunnel_key,
>>>>> +                                  port->tunnel_key);
>>>>> +
>>>>> +    uint64_t ofpacts_stub[16];
>>>>> +    struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
>>>>> +    put_load(1, MFF_LOG_FLAGS, MLF_INPORT_IN_MC_UNKNOWN_BIT, 1, 
>>>>> &ofpacts);
>>>>> +
>>>>> +    /* Install a flow in OFTABLE_INPORT_IN_MC_UNKNOWN_LOOKUP that sets
>>>>> +     * MLF_INPORT_IN_MC_UNKNOWN flag for packets ingressing on 'port'. */
>>>>> +    ofctrl_add_flow(flow_table, OFTABLE_INPORT_IN_MC_UNKNOWN_LOOKUP, 100,
>>>>> +                    mc->header_.uuid.parts[0], &match, &ofpacts,
>>>>> +                    &mc->header_.uuid);
>>>>> +    ofpbuf_uninit(&ofpacts);
>>>>> +}
>>>>> +
>>>>> static void
>>>>> consider_mc_group(const struct physical_ctx *ctx,
>>>>>                 const struct sbrec_multicast_group *mc,
>>>>> @@ -3002,6 +3026,21 @@ consider_mc_group(const struct physical_ctx *ctx,
>>>>>       const char *lport_name = (port->parent_port && *port->parent_port) ?
>>>>>                                 port->parent_port : port->logical_port;
>>>>> 
>>>>> +        /* For MC_UNKNOWN local members, add a flow in
>>>>> +         * OFTABLE_INPORT_IN_MC_UNKNOWN_LOOKUP table to set
>>>>> +         * MLF_INPORT_IN_MC_UNKNOWN flag. */
>>>>> +        if (mc->tunnel_key == OVN_MCAST_UNKNOWN_TUNNEL_KEY) {
>>>>> +            bool is_local =
>>>>> +                port == ldp->localnet_port
>>>>> +                || ((port->chassis == ctx->chassis
>>>>> +                     || is_additional_chassis(port, ctx->chassis))
>>>>> +                    && local_binding_get_primary_pb(ctx->local_bindings,
>>>>> +                                                    lport_name));
>>>>> +            if (is_local) {
>>>>> +                add_inport_to_mc_unknown_table(mc, port, flow_table);
>>>>> +            }
>>>>> +        }
>>>>> +
>>>>>       if (type == LP_PATCH) {
>>>>>           if (ldp->is_transit_switch) {
>>>>>               local_set_ct_zone_and_output_pb(port->tunnel_key, zone_id,
>>>>> diff --git a/include/ovn/logical-fields.h b/include/ovn/logical-fields.h
>>>>> index e95dee316..dcc477c4c 100644
>>>>> --- a/include/ovn/logical-fields.h
>>>>> +++ b/include/ovn/logical-fields.h
>>>>> @@ -141,6 +141,7 @@ enum mff_log_flags_bits {
>>>>>   MLF_RECIRC_BIT = 24,
>>>>>   MLF_EVPN_LOOKUP_BIT = 25,
>>>>>   MLF_POST_NF_LOOP_BACK_BIT = 26,
>>>>> +    MLF_INPORT_IN_MC_UNKNOWN_BIT = 27,
>>>>>   MLF_NETWORK_ID_START_BIT = 28,
>>>>>   MLF_NETWORK_ID_END_BIT = 31,
>>>>> };
>>>>> @@ -223,6 +224,9 @@ enum mff_log_flags {
>>>>>   /* 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),
>>>>> 
>>>>> +    /* Packet entered on an MC_UNKNOWN-member port. */
>>>>> +    MLF_INPORT_IN_MC_UNKNOWN = (1 << MLF_INPORT_IN_MC_UNKNOWN_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/logical-fields.c b/lib/logical-fields.c
>>>>> index 35d9a2e22..8295d71f4 100644
>>>>> --- a/lib/logical-fields.c
>>>>> +++ b/lib/logical-fields.c
>>>>> @@ -188,6 +188,11 @@ ovn_init_symtab(struct shash *symtab)
>>>>>   snprintf(flags_str, sizeof flags_str, "flags[%d]", MLF_PKT_SAMPLED_BIT);
>>>>>   expr_symtab_add_subfield(symtab, "flags.pkt_sampled", NULL, flags_str);
>>>>> 
>>>>> +    snprintf(flags_str, sizeof flags_str, "flags[%d]",
>>>>> +             MLF_INPORT_IN_MC_UNKNOWN_BIT);
>>>>> +    expr_symtab_add_subfield(symtab, "flags.inport_in_mc_unknown", NULL,
>>>>> +                             flags_str);
>>>>> +
>>>>>   /* Connection tracking state. */
>>>>>   expr_symtab_add_field_scoped(symtab, "ct_mark", MFF_CT_MARK, NULL, 
>>>>> false,
>>>>>                                WR_CT_COMMIT);
>>>>> diff --git a/northd/northd.c b/northd/northd.c
>>>>> index 1d1452791..55f368e61 100644
>>>>> --- a/northd/northd.c
>>>>> +++ b/northd/northd.c
>>>>> @@ -176,6 +176,9 @@ static bool vxlan_mode;
>>>>> #define REGBIT_NF_ENABLED         "reg8[21]"
>>>>> #define REGBIT_NF_ORIG_DIR        "reg8[22]"
>>>>> #define REGBIT_NF_EGRESS_LOOPBACK "reg8[23]"
>>>>> +/* Set on a post-NF packet flowing back out its original ingress port;
>>>>> + * such packets are dropped. */
>>>>> +#define REGBIT_NF_LOOKUP_HIT      "reg8[24]"
>>>>> /* Register to store the network function group id */
>>>>> #define REG_NF_GROUP_ID           "reg0[22..29]"
>>>>> /* REG_NF_ID overrides REG_NF_GROUP_ID in the pre_network_function stage. 
>>>>> */
>>>>> @@ -315,6 +318,8 @@ static const char *reg_ct_state[] = {
>>>>> * |    | REGBIT_NF_{ENABLED/ORIG_DIR/                 | G |               
>>>>>                     |
>>>>> * |    |            EGRESS_LOOPBACK}                  | 4 |               
>>>>>                     |
>>>>> * |    |       (>= ACL_EVAL* && <= NF*)               |   |               
>>>>>                     |
>>>>> + * |    | REGBIT_NF_LOOKUP_HIT                         |   |             
>>>>>                       |
>>>>> + * |    |     (>= OUT_PRE_ACL && <= OUT_CHECK_PORT_SEC)|   |             
>>>>>                       |
>>>>> * +----+----------------------------------------------+   
>>>>> +-----------------------------------+
>>>>> * | R9 |              OBS_POINT_ID_EST                |   |               
>>>>>                     |
>>>>> * |    |       (>= ACL_EVAL* && <= ACL_ACTION*)       |   |               
>>>>>                     |
>>>>> @@ -19391,6 +19396,68 @@ 
>>>>> network_function_configure_fail_open_flows(struct lflow_table *lflows,
>>>>>   ds_destroy(&match);
>>>>> }
>>>>> 
>>>>> +static void
>>>>> +build_nf_learn_orig_inport_flows(struct lflow_table *lflows,
>>>>> +                                   const struct ovn_datapath *od,
>>>>> +                                   const struct ovn_stage *stage,
>>>>> +                                   uint16_t priority,
>>>>> +                                   const char *match_suffix,
>>>>> +                                   const char *action_suffix,
>>>>> +                                   struct lflow_ref *lflow_ref)
>>>> 
>>>> Nit: indentation.
>>>> 
>>>>> +{
>>>>> +    struct ds match = DS_EMPTY_INITIALIZER;
>>>>> +    struct ds action = DS_EMPTY_INITIALIZER;
>>>>> +
>>>>> +    /* Emit IPv4 nf_learn_orig_inport() flows for flagged IP packets. */
>>>>> +    ds_put_cstr(&match, "ip4 && flags.inport_in_mc_unknown == 1");
>>>>> +    if (match_suffix) {
>>>>> +        ds_put_format(&match, " && %s", match_suffix);
>>>>> +    }
>>>>> +    ds_put_format(&action, "nf_learn_orig_inport(ipv6 = false); %s",
>>>>> +                  action_suffix);
>>>>> +    ovn_lflow_add(lflows, od, stage, priority, ds_cstr(&match),
>>>>> +                  ds_cstr(&action), lflow_ref);
>>>>> +
>>>>> +    ds_clear(&match);
>>>>> +    ds_clear(&action);
>>>>> +    /* Emit IPv6 nf_learn_orig_inport() flows for flagged IP packets. */
>>>>> +    ds_put_cstr(&match, "ip6 && flags.inport_in_mc_unknown == 1");
>>>>> +    if (match_suffix) {
>>>>> +        ds_put_format(&match, " && %s", match_suffix);
>>>>> +    }
>>>>> +    ds_put_format(&action, "nf_learn_orig_inport(ipv6 = true); %s",
>>>>> +                  action_suffix);
>>>>> +    ovn_lflow_add(lflows, od, stage, priority, ds_cstr(&match),
>>>>> +                  ds_cstr(&action), lflow_ref);
>>>>> +
>>>>> +    ds_destroy(&match);
>>>>> +    ds_destroy(&action);
>>>>> +}
>>>>> +
>>>>> +static void
>>>>> +build_nf_lookup_orig_inport_flow(struct lflow_table *lflows,
>>>>> +                                   const struct ovn_datapath *od,
>>>>> +                                   const struct ovn_stage *stage,
>>>>> +                                   uint16_t priority,
>>>>> +                                   const struct ovn_port *port,
>>>>> +                                   const char *action_suffix,
>>>>> +                                   struct lflow_ref *lflow_ref)
>>>>> +{
>>>>> +    struct ds match = DS_EMPTY_INITIALIZER;
>>>>> +    struct ds action = DS_EMPTY_INITIALIZER;
>>>>> +
>>>>> +    /* Prepend nf_lookup_orig_inport() action for packets entering on
>>>>> +     * 'port'. */
>>>>> +    ds_put_format(&match, "inport == %s", port->json_key);
>>>>> +    ds_put_format(&action,
>>>>> +                  REGBIT_NF_LOOKUP_HIT " = nf_lookup_orig_inport(); %s",
>>>>> +                  action_suffix);
>>>>> +    ovn_lflow_add(lflows, od, stage, priority, ds_cstr(&match),
>>>>> +                  ds_cstr(&action), lflow_ref);
>>>>> +    ds_destroy(&match);
>>>>> +    ds_destroy(&action);
>>>>> +}
>>>>> +
>>>>> static void
>>>>> consider_network_function_inline(struct lflow_table *lflows,
>>>>>                                const struct ovn_datapath *od,
>>>>> @@ -19505,6 +19572,13 @@ consider_network_function_inline(struct 
>>>>> lflow_table *lflows,
>>>>>                 (uint8_t) nf->id);
>>>>>   ovn_lflow_add(lflows, od, fwd_stage, 99, ds_cstr(&match),
>>>>>                 ds_cstr(&action), lflow_ref);
>>>>> +
>>>>> +    /* Priority 100 in fwd_stage: same as the priority-99 redirect above,
>>>>> +     * but learn the original inport first for flagged IP packets. */
>>>>> +    build_nf_learn_orig_inport_flows(lflows, od, fwd_stage, 100,
>>>>> +                                       ds_cstr(&match), ds_cstr(&action),
>>>>> +                                       lflow_ref);
>>>> 
>>>> Nit: indentation.
>>>> 
>>>> Also, this means that the priority-99 flow will never be hit right?  It
>>>> feels weird to have that flow there.  See below, maybe we should just
>>>> use learn directly here.
>>> 
>>> If we always learn inport then, we don’t need the additional flow here.
>>> 
>>>> 
>>>>> +
>>>>>   ds_clear(&match);
>>>>>   ds_clear(&action);
>>>>> 
>>>>> @@ -19542,53 +19616,57 @@ consider_network_function_inline(struct 
>>>>> lflow_table *lflows,
>>>>>   ds_clear(&match);
>>>>>   ds_clear(&action);
>>>>> 
>>>>> -    /* Priority 100 flow in in_nf:
>>>>> +    /* Priority 110 flow in in_nf:
>>>>>    * Allow packets to go through if coming from network-function port as
>>>>>    * we don't want the packets to be redirected again based on from-lport
>>>>>    * match.
>>>>>    */
>>>>>   ds_put_format(&match, "inport == %s", input_port->json_key);
>>>>>   ds_put_format(&action, REG_TUN_OFPORT" = ct_label.tun_if_id; next;");
>>>>> -    ovn_lflow_add(lflows, od, S_SWITCH_IN_NF, 100,
>>>>> +    ovn_lflow_add(lflows, od, S_SWITCH_IN_NF, 110,
>>>>>                 ds_cstr(&match), ds_cstr(&action), lflow_ref);
>>>>>   ds_clear(&match);
>>>>> 
>>>>>   ds_put_format(&match, "inport == %s", output_port->json_key);
>>>>> -    ovn_lflow_add(lflows, od, S_SWITCH_IN_NF, 100,
>>>>> +    ovn_lflow_add(lflows, od, S_SWITCH_IN_NF, 110,
>>>>>                 ds_cstr(&match), ds_cstr(&action), lflow_ref);
>>>>>   ds_clear(&match);
>>>>>   ds_clear(&action);
>>>>> 
>>>>> -    /* Priority 100 flow in out_nf:
>>>>> +    /* Priority 110 flow in out_nf:
>>>>>    * Allow packets to go through if outport is network-function port as
>>>>>    * we don't want the packets to be redirected again based on to-lport
>>>>>    * match.
>>>>>    */
>>>>>   ds_put_format(&match, "outport == %s", input_port->json_key);
>>>>>   ds_put_format(&action, "next;");
>>>>> -    ovn_lflow_add(lflows, od, S_SWITCH_OUT_NF, 100,
>>>>> +    ovn_lflow_add(lflows, od, S_SWITCH_OUT_NF, 110,
>>>>>                 ds_cstr(&match), ds_cstr(&action), lflow_ref);
>>>>>   ds_clear(&match);
>>>>> 
>>>>>   ds_put_format(&match, "outport == %s", output_port->json_key);
>>>>> -    ovn_lflow_add(lflows, od, S_SWITCH_OUT_NF, 100,
>>>>> +    ovn_lflow_add(lflows, od, S_SWITCH_OUT_NF, 110,
>>>>>                 ds_cstr(&match), ds_cstr(&action), lflow_ref);
>>>>>   ds_clear(&match);
>>>>>   ds_clear(&action);
>>>>> 
>>>>> -    /* For packets redirected from egress pipleline to the NF, when they 
>>>>> come
>>>>> -     * out from the other NF port, we don't want to process them again 
>>>>> through
>>>>> -     * egress stages they already went through, especially not again 
>>>>> through
>>>>> -     * conntrack as these packets are already accounted for there. Hence 
>>>>> we
>>>>> -     * need to skip the initial pipeline stages for such packets and 
>>>>> directly
>>>>> -     * start from the NF table. The packets that fall under this 
>>>>> category are
>>>>> -     * the response packets from NF for from-lport ACLs and request 
>>>>> packets
>>>>> -     * received from NF for to-lport ACLs. */
>>>>> -    ds_put_format(&match, "inport == %s", input_port->json_key);
>>>>> +    /* Post-NF Processing: Resumes pipeline after ls_out_nf to bypass
>>>>> +     * previously completed egress stages (e.g., conntrack). Uses 
>>>>> priority
>>>>> +     * 115 in out_pre_acl to take precedence over priority-110 conntrack
>>>>> +     * skips, and calls nf_lookup_orig_inport() to identify loopback
>>>>> +     * packets returning on their original inport. */
>>>>>   ds_put_format(&action, "next(pipeline=egress, table=%d);",
>>>>> -                  (ovn_stage_get_table(S_SWITCH_OUT_NF) + 1));
>>>>> -    ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 110, ds_cstr(&match),
>>>>> -                  ds_cstr(&action), lflow_ref);
>>>>> +                  ovn_stage_get_table(S_SWITCH_OUT_NF) + 1);
>>>>> +    build_nf_lookup_orig_inport_flow(lflows, od, S_SWITCH_OUT_PRE_ACL, 
>>>>> 115,
>>>>> +                                       input_port, ds_cstr(&action),
>>>>> +                                       lflow_ref);
>>>>> +    ds_clear(&action);
>>>>> +
>>>>> +    /* Priority 2 in out_nf (output_port): post-NF packet re-entering the
>>>>> +     * egress pipeline; run the lookup here so ls_out_check_port_sec 
>>>>> drops
>>>>> +     * a loopback copy. */
>>>>> +    build_nf_lookup_orig_inport_flow(lflows, od, S_SWITCH_OUT_NF, 2,
>>>>> +                                       output_port, "next;", lflow_ref);
>>>> 
>>>> I would just inline the two new logical flows (created by
>>>> build_nf_lookup_orig_inport_flow()) here, it makes it easier to read and
>>>> understand the pipeline if we have all the related flows in a single place.
>>> 
>>> Ack.
>>> 
>>>> 
>>>>> 
>>>>>   /* Priority 120 flows in out_stateful:
>>>>>    * If packet was received on a tunnel interface and being forwarded to a
>>>>> @@ -19807,6 +19885,7 @@ build_network_function(const struct ovn_datapath 
>>>>> *od,
>>>>> {
>>>>>   unsigned long *nfg_ingress_bitmap = 
>>>>> bitmap_allocate(MAX_OVN_NF_GROUP_IDS);
>>>>>   unsigned long *nfg_egress_bitmap = 
>>>>> bitmap_allocate(MAX_OVN_NF_GROUP_IDS);
>>>>> +    bool has_nfg = false;
>>>>> 
>>>>>   /* This flow matches packets injected from out_nf stage -
>>>>>    * after it sets the outport - back to in_l2_lkup stage. This rule must 
>>>>> be
>>>>> @@ -19834,14 +19913,15 @@ build_network_function(const struct 
>>>>> ovn_datapath *od,
>>>>>                 REGBIT_NF_ENABLED" == 1 && " REGBIT_NF_ORIG_DIR" == 1",
>>>>>                 REG_NF_ID" = 0; next;", lflow_ref);
>>>>> 
>>>>> -    /* Ingress and Egress NF Table (Priority 100): ACL stage determined 
>>>>> these
>>>>> -     * packets should be redirected, but these are multicast/broadcast
>>>>> -     * packets which can cause L2 loop if redirected to NF. */
>>>>> -    ovn_lflow_add(lflows, od, S_SWITCH_IN_NF, 100,
>>>>> -                  REGBIT_NF_ENABLED" == 1 && eth.mcast",
>>>>> +    /* Ingress and Egress NF Table (Priority 110): skip the NF redirect 
>>>>> for
>>>>> +     * multicast/broadcast packets marked by the ACL, which would 
>>>>> otherwise
>>>>> +     * cause an L2 loop.  Higher than the redirect/learn flows so they 
>>>>> skip
>>>>> +     * both. */
>>>>> +    ovn_lflow_add(lflows, od, S_SWITCH_IN_NF, 110,
>>>>> +                  "eth.mcast",
>>>>>                 "next;", lflow_ref);
>>>> 
>>>> We also have these flows for vtap mode:
>>>> 
>>>>  /* Priority 100 flow in in_network_function:
>>>>   * Drop packets coming from network-function in vtap mode.
>>>>   */
>>>>  ds_put_format(&match, "inport == %s", input_port->json_key);
>>>>  ds_put_format(&action, "drop;");
>>>>  ovn_lflow_add(lflows, od, S_SWITCH_IN_NF, 100,
>>>>                ds_cstr(&match), ds_cstr(&action), lflow_ref);
>>>> 
>>>> If you now add a priority-110 flow for multicast, we'll bypass those and
>>>> essentially allow multicast packets entering from a network-function
>>>> that's in vtap mode which seems like a regression.
>>> 
>>> Ack.
>>> 
>>>> 
>>>>> -    ovn_lflow_add(lflows, od, S_SWITCH_OUT_NF, 100,
>>>>> -                  REGBIT_NF_ENABLED" == 1 && eth.mcast",
>>>>> +    ovn_lflow_add(lflows, od, S_SWITCH_OUT_NF, 110,
>>>>> +                  "eth.mcast",
>>>>>                 "next;", lflow_ref);
>>>>> 
>>>>>   /* Ingress and Egress NF Table (Priority 0): Packets are forwarded to
>>>>> @@ -19875,6 +19955,7 @@ build_network_function(const struct ovn_datapath 
>>>>> *od,
>>>>>               continue;
>>>>>           }
>>>>>           nfg_bitmap = bitmap_set1(nfg_bitmap, nfg_id);
>>>>> +            has_nfg = true;
>>>>>           consider_network_function(lflows, od, 
>>>>> acl->network_function_group,
>>>>>                                     ingress, lflow_ref);
>>>>>       }
>>>>> @@ -19900,6 +19981,7 @@ build_network_function(const struct ovn_datapath 
>>>>> *od,
>>>>>                       continue;
>>>>>                   }
>>>>>                   nfg_bitmap = bitmap_set1(nfg_bitmap, nfg_id);
>>>>> +                    has_nfg = true;
>>>>>                   consider_network_function(lflows, od,
>>>>>                                             acl->network_function_group,
>>>>>                                             ingress, lflow_ref);
>>>>> @@ -19907,6 +19989,25 @@ build_network_function(const struct ovn_datapath 
>>>>> *od,
>>>>>           }
>>>>>       }
>>>>>   }
>>>>> +
>>>>> +    if (has_nfg) {
>>>>> +        /* Drop the loopback copy flagged by nf_lookup_orig_inport(). */
>>>>> +        ovn_lflow_add(lflows, od, S_SWITCH_OUT_CHECK_PORT_SEC, 110,
>>>>> +                      REGBIT_NF_LOOKUP_HIT " == 1", debug_drop_action(),
>>>>> +                      lflow_ref);
>>>>> +
>>>>> +        /* Overlay only: always learn the inport when the packet enters
>>>>> +         * on an MC_unknown-member port, so a post-NF loopback copy can
>>>>> +         * be detected and dropped when the destination port has NF
>>>>> +         * redirection enabled. */
>>>> 
>>>> I'm confused about why doing this in "overlay only" switches?  If we go
>>>> for my suggestion of unconditionally using the learn action then we can
>>>> just add it as part of the priority-99 flows installed in the NF_IN/OUT
>>>> stages right?
>>> 
>>> This is still needed for overlay subnets. In VLAN-backed cases, post-NF 
>>> return
>>> stays on the same node path where redirect-path learn already ran. In
>>> overlay, redirect may happen on the destination node, while post-NF flood
>>> may return toward the source node. Without the default-path learn, we
>>> cannot reliably catch loopback packets.
>>> 
> 
> So does this mean we'll always hit the learn flows for these kinds of
> switches (when they have NF applied) even if the packet is not going to
> be sent out to a network function?

Yes.

> 
> But I guess I can have a closer look once v6 is out for review.
> 
>>>> 
>>>>> +        if (!ls_has_localnet_port(od)) {
>>>>> +            build_nf_learn_orig_inport_flows(
>>>>> +                lflows, od, S_SWITCH_IN_NF, 50,
>>>>> +                REGBIT_NF_ENABLED" == 0",
>>>>> +                "next;", lflow_ref);
>>>>> +        }
>>>>> +    }
>>>>> +
>>>>>   bitmap_free(nfg_ingress_bitmap);
>>>>>   bitmap_free(nfg_egress_bitmap);
>>>>> }
>>>>> diff --git a/ovn-nb.xml b/ovn-nb.xml
>>>>> index 68d4237c0..001a36f8b 100644
>>>>> --- a/ovn-nb.xml
>>>>> +++ b/ovn-nb.xml
>>>>> @@ -6736,6 +6736,19 @@ or
>>>>>     as down, the fallback method is applied.
>>>>>   </p>
>>>>> 
>>>>> +    <p>
>>>>> +      <b>Known limitation (inline mode)</b>: in topologies where a
>>>>> +      post-NF flood copy may reach the same logical inport from a
>>>>> +      different node than the packet's original ingress node.  In that
>>>>> +      case, the lookup runs on a different node than where the inport
>>>>> +      was learned, so loopback copies are not identified.
>>>>> +      Known example: OVN-IC L2 stretch with the NF and IC gateway on
>>>>> +      different chassis.  With <code>unknown</code>-addressed ports, the
>>>>> +      post-NF packet can be flooded across AZs and come back to the
>>>>> +      source AZ via another node, so the lookup does not hit the
>>>>> +      original learn entry.
>>>>> +    </p>
>>>>> +
>>>>>   <column name="name">
>>>>>     Name of the <ref table="Network_Function_Group"/>. Name should be 
>>>>> unique.
>>>>>   </column>
>>>>> diff --git a/tests/ovn-macros.at b/tests/ovn-macros.at
>>>>> index 2a1faf677..530049821 100644
>>>>> --- a/tests/ovn-macros.at
>>>>> +++ b/tests/ovn-macros.at
>>>>> @@ -1656,5 +1656,6 @@ 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_INPORT_IN_MC_UNKNOWN_LOOKUP], [115])
>>>>> 
>>>>> m4_define([OFTABLE_SAVE_INPORT_HEX], [m4_eval(OFTABLE_SAVE_INPORT, 16)])
>>>>> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
>>>>> index c58f731e9..9c9d14400 100644
>>>>> --- a/tests/ovn-northd.at
>>>>> +++ b/tests/ovn-northd.at
>>>>> @@ -19966,15 +19966,20 @@ AT_CHECK(
>>>>> [grep -E 'ls_(in|out)_network_function' sw0flows | ovn_strip_lflows | 
>>>>> sort], [0], [dnl
>>>>> table=??(ls_in_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_in_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "sw0-nf-p1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "sw0-nf-p2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(reg8[[21]] == 
>>>>> 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=100  , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 101), action=(nf_learn_orig_inport(ipv6 = false); 
>>>>> outport = "sw0-nf-p1"; output;)
>>>>> +  table=??(ls_in_network_function), priority=100  , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 101), action=(nf_learn_orig_inport(ipv6 = true); 
>>>>> outport = "sw0-nf-p1"; output;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "sw0-nf-p1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "sw0-nf-p2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = false); next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = true); next;)
>>>>> table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 1 && reg0[[22..29]] == 101), action=(outport = 
>>>>> "sw0-nf-p1"; output;)
>>>>> table=??(ls_out_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_out_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "sw0-nf-p1"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "sw0-nf-p2"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(reg8[[21]] 
>>>>> == 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "sw0-nf-p1"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "sw0-nf-p2"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=2    , match=(inport == 
>>>>> "sw0-nf-p2"), action=(reg8[[24]] = nf_lookup_orig_inport(); next;)
>>>>> table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 0 && ct_label.nf_id == 101), action=(outport = 
>>>>> "sw0-nf-p2"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> ])
>>>>> 
>>>>> @@ -19983,7 +19988,7 @@ AT_CHECK([grep "ls_in_l2_lkup" sw0flows | 
>>>>> ovn_strip_lflows | grep 'priority=100'
>>>>> ])
>>>>> 
>>>>> AT_CHECK([grep "ls_out_pre_acl" sw0flows | ovn_strip_lflows | grep 
>>>>> 'sw0-nf-p1'], [0], [dnl
>>>>> -  table=??(ls_out_pre_acl     ), priority=110  , match=(inport == 
>>>>> "sw0-nf-p1"), action=(next(pipeline=egress, table=??);)
>>>>> +  table=??(ls_out_pre_acl     ), priority=115  , match=(inport == 
>>>>> "sw0-nf-p1"), action=(reg8[[24]] = nf_lookup_orig_inport(); 
>>>>> next(pipeline=egress, table=??);)
>>>>> ])
>>>>> 
>>>>> AT_CHECK(
>>>>> @@ -20074,20 +20079,28 @@ AT_CHECK(
>>>>> [grep -E 'ls_(in|out)_network_function' sw0flows | ovn_strip_lflows |  
>>>>> sort], [0], [dnl
>>>>> table=??(ls_in_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_in_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "sw0-nf-p1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "sw0-nf-p2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "sw0-nf-p3"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "sw0-nf-p4"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(reg8[[21]] == 
>>>>> 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=100  , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 101), action=(nf_learn_orig_inport(ipv6 = false); 
>>>>> outport = "sw0-nf-p1"; output;)
>>>>> +  table=??(ls_in_network_function), priority=100  , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 101), action=(nf_learn_orig_inport(ipv6 = true); 
>>>>> outport = "sw0-nf-p1"; output;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "sw0-nf-p1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "sw0-nf-p2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "sw0-nf-p3"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "sw0-nf-p4"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = false); next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = true); next;)
>>>>> table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 0 && ct_label.nf_id == 102), action=(outport = 
>>>>> "sw0-nf-p3"; output;)
>>>>> table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 1 && reg0[[22..29]] == 101), action=(outport = 
>>>>> "sw0-nf-p1"; output;)
>>>>> table=??(ls_out_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_out_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "sw0-nf-p1"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "sw0-nf-p2"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "sw0-nf-p3"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "sw0-nf-p4"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(reg8[[21]] 
>>>>> == 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=100  , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 102), action=(nf_learn_orig_inport(ipv6 = false); 
>>>>> outport = "sw0-nf-p4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> +  table=??(ls_out_network_function), priority=100  , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 102), action=(nf_learn_orig_inport(ipv6 = true); 
>>>>> outport = "sw0-nf-p4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "sw0-nf-p1"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "sw0-nf-p2"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "sw0-nf-p3"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "sw0-nf-p4"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=2    , match=(inport == 
>>>>> "sw0-nf-p2"), action=(reg8[[24]] = nf_lookup_orig_inport(); next;)
>>>>> +  table=??(ls_out_network_function), priority=2    , match=(inport == 
>>>>> "sw0-nf-p4"), action=(reg8[[24]] = nf_lookup_orig_inport(); next;)
>>>>> table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 0 && ct_label.nf_id == 101), action=(outport = 
>>>>> "sw0-nf-p2"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 1 && reg0[[22..29]] == 102), action=(outport = 
>>>>> "sw0-nf-p4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> ])
>>>>> @@ -20156,20 +20169,28 @@ AT_CHECK(
>>>>> [grep -E 'ls_(in|out)_network_function' sw1flows | ovn_strip_lflows | 
>>>>> sort], [0], [dnl
>>>>> table=??(ls_in_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_in_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "sw1-nf-p1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "sw1-nf-p2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "sw1-nf-p3"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "sw1-nf-p4"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(reg8[[21]] == 
>>>>> 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=100  , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 101), action=(nf_learn_orig_inport(ipv6 = false); 
>>>>> outport = "sw1-nf-p1"; output;)
>>>>> +  table=??(ls_in_network_function), priority=100  , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 101), action=(nf_learn_orig_inport(ipv6 = true); 
>>>>> outport = "sw1-nf-p1"; output;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "sw1-nf-p1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "sw1-nf-p2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "sw1-nf-p3"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "sw1-nf-p4"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = false); next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = true); next;)
>>>>> table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 0 && ct_label.nf_id == 102), action=(outport = 
>>>>> "sw1-nf-p3"; output;)
>>>>> table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 1 && reg0[[22..29]] == 101), action=(outport = 
>>>>> "sw1-nf-p1"; output;)
>>>>> table=??(ls_out_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_out_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "sw1-nf-p1"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "sw1-nf-p2"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "sw1-nf-p3"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "sw1-nf-p4"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(reg8[[21]] 
>>>>> == 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=100  , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 102), action=(nf_learn_orig_inport(ipv6 = false); 
>>>>> outport = "sw1-nf-p4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> +  table=??(ls_out_network_function), priority=100  , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 102), action=(nf_learn_orig_inport(ipv6 = true); 
>>>>> outport = "sw1-nf-p4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "sw1-nf-p1"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "sw1-nf-p2"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "sw1-nf-p3"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "sw1-nf-p4"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=2    , match=(inport == 
>>>>> "sw1-nf-p2"), action=(reg8[[24]] = nf_lookup_orig_inport(); next;)
>>>>> +  table=??(ls_out_network_function), priority=2    , match=(inport == 
>>>>> "sw1-nf-p4"), action=(reg8[[24]] = nf_lookup_orig_inport(); next;)
>>>>> table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 0 && ct_label.nf_id == 101), action=(outport = 
>>>>> "sw1-nf-p2"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 1 && reg0[[22..29]] == 102), action=(outport = 
>>>>> "sw1-nf-p4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> ])
>>>>> @@ -20204,6 +20225,176 @@ OVN_CLEANUP_NORTHD
>>>>> AT_CLEANUP
>>>>> ])
>>>>> 
>>>>> +OVN_FOR_EACH_NORTHD_NO_HV([
>>>>> +AT_SETUP([Network function -- inline post-NF re-flood loop prevention])
>>>>> +
>>>>> +dnl When an LS has ports with "unknown" addresses, a packet redirected
>>>>> +dnl to an inline NF can be re-flooded back out its original inport after 
>>>>> NF
>>>>> +dnl processing.  This is prevented by learning the original inport on 
>>>>> the way
>>>>> +dnl in, then on the post-NF return path looking it up and dropping any
>>>>> +dnl copy headed back out that same port.  This test verifies the 
>>>>> LEARN/LOOKUP/DROP
>>>>> +dnl flows northd installs for loop prevention.
>>>>> +
>>>>> +ovn_start
>>>>> +
>>>>> +check ovn-nbctl ls-add sw0
>>>>> +
>>>>> +dnl NF port-pair.
>>>>> +check ovn-nbctl lsp-add sw0 sw0-nf-p1
>>>>> +check ovn-nbctl lsp-add sw0 sw0-nf-p2
>>>>> +check ovn-nbctl set logical_switch_port sw0-nf-p1 \
>>>>> +    options:receive_multicast=false options:lsp_learn_mac=false \
>>>>> +    options:is-nf=true options:nf-linked-port=sw0-nf-p2
>>>>> +check ovn-nbctl set logical_switch_port sw0-nf-p2 \
>>>>> +    options:receive_multicast=false options:lsp_learn_mac=false \
>>>>> +    options:is-nf=true options:nf-linked-port=sw0-nf-p1
>>>>> +
>>>>> +dnl A VM port and a localnet port, both with "unknown" in addresses.
>>>>> +check ovn-nbctl lsp-add sw0 sw0-vm1 -- \
>>>>> +    lsp-set-addresses sw0-vm1 "00:00:00:00:00:01 10.0.0.10" unknown
>>>>> +check ovn-nbctl lsp-add sw0 sw0-ln -- \
>>>>> +    lsp-set-type sw0-ln localnet -- \
>>>>> +    lsp-set-options sw0-ln network_name=phys -- \
>>>>> +    lsp-set-addresses sw0-ln unknown
>>>>> +
>>>>> +dnl A second VM port without "unknown".
>>>>> +check ovn-nbctl lsp-add sw0 sw0-vm2 -- \
>>>>> +    lsp-set-addresses sw0-vm2 "00:00:00:00:00:02 10.0.0.11"
>>>>> +
>>>>> +check ovn-nbctl nf-add nf0 101 sw0-nf-p1 sw0-nf-p2
>>>>> +check ovn-nbctl nfg-add nfg0 201 inline nf0
>>>>> +check ovn-nbctl acl-add sw0 from-lport 1234 'ip' allow-related nfg0
>>>>> +
>>>>> +dnl Bind NF ports so nf_get_active() considers the NF healthy.
>>>>> +check ovn-sbctl chassis-add gw1 geneve 127.0.0.1
>>>>> +chassis_uuid=$(fetch_column Chassis _uuid name=gw1)
>>>>> +check ovn-sbctl set port_binding sw0-nf-p1 up=true chassis=$chassis_uuid
>>>>> +check ovn-sbctl set port_binding sw0-nf-p2 up=true chassis=$chassis_uuid
>>>>> +check ovn-nbctl --wait=sb sync
>>>>> +
>>>>> +ovn-sbctl dump-flows sw0 > sw0flows
>>>>> +AT_CAPTURE_FILE([sw0flows])
>>>>> +
>>>>> +dnl LEARN (redirect path): priority-100 LEARN+redirect flows in ls_in_nf
>>>>> +dnl for IPv4 and IPv6, gated on flags.inport_in_mc_unknown.
>>>>> +AT_CHECK([grep 'ls_in_network_function' sw0flows | ovn_strip_lflows | \
>>>>> +          grep nf_learn | grep 'priority=100' | sort], [0], [dnl
>>>>> +  table=??(ls_in_network_function), priority=100  , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 101), action=(nf_learn_orig_inport(ipv6 = false); 
>>>>> outport = "sw0-nf-p1"; output;)
>>>>> +  table=??(ls_in_network_function), priority=100  , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 101), action=(nf_learn_orig_inport(ipv6 = true); 
>>>>> outport = "sw0-nf-p1"; output;)
>>>>> +])
>>>>> +
>>>>> +dnl LEARN (default path): the priority-50 LEARN is only emitted on 
>>>>> overlay
>>>>> +dnl switches.  sw0 is VLAN-backed (has a localnet port), so it must NOT
>>>>> +dnl have any priority-50 nf_learn flow.
>>>>> +AT_CHECK([grep 'ls_in_network_function' sw0flows | ovn_strip_lflows | \
>>>>> +          grep nf_learn | grep 'priority=50' | wc -l], [0], [0
>>>>> +])
>>>>> +
>>>>> +dnl LOOKUP: nf_lookup_orig_inport() on both NF ports.  input_port runs
>>>>> +dnl in ls_out_pre_acl, output_port in ls_out_nf.
>>>>> +AT_CHECK([grep 'ls_out_pre_acl' sw0flows | ovn_strip_lflows | \
>>>>> +          grep 'sw0-nf-p1'], [0], [dnl
>>>>> +  table=??(ls_out_pre_acl     ), priority=115  , match=(inport == 
>>>>> "sw0-nf-p1"), action=(reg8[[24]] = nf_lookup_orig_inport(); 
>>>>> next(pipeline=egress, table=??);)
>>>>> +])
>>>>> +AT_CHECK([grep 'ls_out_network_function' sw0flows | ovn_strip_lflows | \
>>>>> +          grep nf_lookup], [0], [dnl
>>>>> +  table=??(ls_out_network_function), priority=2    , match=(inport == 
>>>>> "sw0-nf-p2"), action=(reg8[[24]] = nf_lookup_orig_inport(); next;)
>>>>> +])
>>>>> +
>>>>> +dnl DROP: ls_out_check_port_sec drops on REGBIT_NF_LOOKUP_HIT == 1.
>>>>> +AT_CHECK([grep 'ls_out_check_port_sec' sw0flows | ovn_strip_lflows | \
>>>>> +          grep 'reg8\[[24\]] == 1'], [0], [dnl
>>>>> +  table=??(ls_out_check_port_sec), priority=110  , match=(reg8[[24]] == 
>>>>> 1), action=(drop;)
>>>>> +])
>>>>> +
>>>>> +AS_BOX([An overlay switch gets the default-path LEARN])
>>>>> +
>>>>> +dnl On an overlay switch the NF redirect may happen on a different node
>>>>> +dnl than the inport.  To keep the LEARN co-located with the LOOKUP,
>>>>> +dnl northd emits a priority-50 default-path LEARN in ls_in_nf so the
>>>>> +dnl inport is recorded on the same node the packet enters on.
>>>>> +check ovn-nbctl ls-add sw1
>>>>> +check ovn-nbctl lsp-add sw1 sw1-nf-p1
>>>>> +check ovn-nbctl lsp-add sw1 sw1-nf-p2
>>>>> +check ovn-nbctl set logical_switch_port sw1-nf-p1 \
>>>>> +    options:receive_multicast=false options:lsp_learn_mac=false \
>>>>> +    options:is-nf=true options:nf-linked-port=sw1-nf-p2
>>>>> +check ovn-nbctl set logical_switch_port sw1-nf-p2 \
>>>>> +    options:receive_multicast=false options:lsp_learn_mac=false \
>>>>> +    options:is-nf=true options:nf-linked-port=sw1-nf-p1
>>>>> +check ovn-nbctl nf-add nf2 103 sw1-nf-p1 sw1-nf-p2
>>>>> +check ovn-nbctl nfg-add nfg2 203 inline nf2
>>>>> +check ovn-nbctl acl-add sw1 from-lport 1234 'ip' allow-related nfg2
>>>>> +
>>>>> +check ovn-sbctl set port_binding sw1-nf-p1 up=true chassis=$chassis_uuid
>>>>> +check ovn-sbctl set port_binding sw1-nf-p2 up=true chassis=$chassis_uuid
>>>>> +check ovn-nbctl --wait=sb sync
>>>>> +
>>>>> +ovn-sbctl dump-flows sw1 > sw1flows
>>>>> +AT_CAPTURE_FILE([sw1flows])
>>>>> +
>>>>> +AT_CHECK([grep 'ls_in_network_function' sw1flows | ovn_strip_lflows | \
>>>>> +          grep nf_learn | grep 'priority=50' | sort], [0], [dnl
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = false); next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = true); next;)
>>>>> +])
>>>>> +
>>>>> +AS_BOX([A second inline NFG on a to-lport ACL emits its own 
>>>>> LEARN+LOOKUP])
>>>>> +
>>>>> +dnl A second NF pair attached via a to-lport ACL adds:
>>>>> +dnl  - priority-100 LEARN+redirect flows in ls_out_nf for that NFG, and
>>>>> +dnl  - LOOKUP flows on both of that NF's ports.
>>>>> +check ovn-nbctl lsp-add sw0 sw0-nf-p3
>>>>> +check ovn-nbctl lsp-add sw0 sw0-nf-p4
>>>>> +check ovn-nbctl set logical_switch_port sw0-nf-p3 \
>>>>> +    options:receive_multicast=false options:lsp_learn_mac=false \
>>>>> +    options:is-nf=true options:nf-linked-port=sw0-nf-p4
>>>>> +check ovn-nbctl set logical_switch_port sw0-nf-p4 \
>>>>> +    options:receive_multicast=false options:lsp_learn_mac=false \
>>>>> +    options:is-nf=true options:nf-linked-port=sw0-nf-p3
>>>>> +check ovn-nbctl nf-add nf1 102 sw0-nf-p3 sw0-nf-p4
>>>>> +check ovn-nbctl nfg-add nfg1 202 inline nf1
>>>>> +check ovn-nbctl acl-add sw0 to-lport 1234 'ip' allow-related nfg1
>>>>> +
>>>>> +check ovn-sbctl set port_binding sw0-nf-p3 up=true chassis=$chassis_uuid
>>>>> +check ovn-sbctl set port_binding sw0-nf-p4 up=true chassis=$chassis_uuid
>>>>> +check ovn-nbctl --wait=sb sync
>>>>> +
>>>>> +ovn-sbctl dump-flows sw0 > sw0flows
>>>>> +
>>>>> +AT_CHECK([grep 'ls_out_pre_acl' sw0flows | ovn_strip_lflows | \
>>>>> +          grep -E 'sw0-nf-p[[13]]' | sort], [0], [dnl
>>>>> +  table=??(ls_out_pre_acl     ), priority=115  , match=(inport == 
>>>>> "sw0-nf-p1"), action=(reg8[[24]] = nf_lookup_orig_inport(); 
>>>>> next(pipeline=egress, table=??);)
>>>>> +  table=??(ls_out_pre_acl     ), priority=115  , match=(inport == 
>>>>> "sw0-nf-p3"), action=(reg8[[24]] = nf_lookup_orig_inport(); 
>>>>> next(pipeline=egress, table=??);)
>>>>> +])
>>>>> +AT_CHECK([grep 'ls_out_network_function' sw0flows | ovn_strip_lflows | \
>>>>> +          grep nf_lookup | sort], [0], [dnl
>>>>> +  table=??(ls_out_network_function), priority=2    , match=(inport == 
>>>>> "sw0-nf-p2"), action=(reg8[[24]] = nf_lookup_orig_inport(); next;)
>>>>> +  table=??(ls_out_network_function), priority=2    , match=(inport == 
>>>>> "sw0-nf-p4"), action=(reg8[[24]] = nf_lookup_orig_inport(); next;)
>>>>> +])
>>>>> +
>>>>> +AT_CHECK([grep 'ls_out_network_function' sw0flows | ovn_strip_lflows | \
>>>>> +          grep nf_learn | sort], [0], [dnl
>>>>> +  table=??(ls_out_network_function), priority=100  , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 102), action=(nf_learn_orig_inport(ipv6 = false); 
>>>>> outport = "sw0-nf-p4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> +  table=??(ls_out_network_function), priority=100  , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 102), action=(nf_learn_orig_inport(ipv6 = true); 
>>>>> outport = "sw0-nf-p4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> +])
>>>>> +
>>>>> +AS_BOX([Without any inline-mode NFG attached, no loop-prevention flows])
>>>>> +
>>>>> +check ovn-nbctl clear logical_switch sw0 acls
>>>>> +check as northd ovn-appctl -t ovn-northd inc-engine/recompute
>>>>> +check ovn-nbctl --wait=sb sync
>>>>> +ovn-sbctl dump-flows sw0 > sw0flows
>>>>> +
>>>>> +AT_CHECK([grep -E 'nf_(learn|lookup)_orig_inport' sw0flows | wc -l], 
>>>>> [0], [0
>>>>> +])
>>>>> +AT_CHECK([grep -E 'reg8\[[24\]]' sw0flows | wc -l], [0], [0
>>>>> +])
>>>>> +
>>>>> +OVN_CLEANUP_NORTHD
>>>>> +AT_CLEANUP
>>>>> +])
>>>>> +
>>>>> OVN_FOR_EACH_NORTHD([
>>>>> AT_SETUP([Network function health check])
>>>>> AT_KEYWORDS([ovn])
>>>>> @@ -20301,16 +20492,23 @@ AT_CHECK(
>>>>> [grep -E 'ls_(in|out)_network_function' lflows | ovn_strip_lflows | 
>>>>> sort], [0], [dnl
>>>>> table=??(ls_in_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_in_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "child-1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "child-2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(reg8[[21]] == 
>>>>> 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=100  , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 1), action=(nf_learn_orig_inport(ipv6 = false); outport 
>>>>> = "child-1"; output;)
>>>>> +  table=??(ls_in_network_function), priority=100  , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 1), action=(nf_learn_orig_inport(ipv6 = true); outport 
>>>>> = "child-1"; output;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "child-1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "child-2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = false); next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = true); next;)
>>>>> table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 0 && ct_label.nf_id == 1), action=(outport = "child-1"; 
>>>>> output;)
>>>>> table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(outport = "child-1"; 
>>>>> output;)
>>>>> table=??(ls_out_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_out_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "child-1"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "child-2"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(reg8[[21]] 
>>>>> == 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=100  , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 1), action=(nf_learn_orig_inport(ipv6 = false); outport 
>>>>> = "child-2"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> +  table=??(ls_out_network_function), priority=100  , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 1), action=(nf_learn_orig_inport(ipv6 = true); outport 
>>>>> = "child-2"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "child-1"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "child-2"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=2    , match=(inport == 
>>>>> "child-2"), action=(reg8[[24]] = nf_lookup_orig_inport(); next;)
>>>>> table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 0 && ct_label.nf_id == 1), action=(outport = "child-2"; 
>>>>> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(outport = "child-2"; 
>>>>> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> ])
>>>>> @@ -20342,16 +20540,23 @@ AT_CHECK(
>>>>> [grep -E 'ls_(in|out)_network_function' lflows | ovn_strip_lflows | 
>>>>> sort], [0], [dnl
>>>>> table=??(ls_in_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_in_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "child-1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "child-2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(reg8[[21]] == 
>>>>> 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=100  , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 1), action=(nf_learn_orig_inport(ipv6 = false); outport 
>>>>> = "child-1"; output;)
>>>>> +  table=??(ls_in_network_function), priority=100  , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 1), action=(nf_learn_orig_inport(ipv6 = true); outport 
>>>>> = "child-1"; output;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "child-1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "child-2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = false); next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = true); next;)
>>>>> table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 0 && ct_label.nf_id == 1), action=(outport = "child-1"; 
>>>>> output;)
>>>>> table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(outport = "child-1"; 
>>>>> output;)
>>>>> table=??(ls_out_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_out_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "child-1"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "child-2"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(reg8[[21]] 
>>>>> == 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=100  , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 1), action=(nf_learn_orig_inport(ipv6 = false); outport 
>>>>> = "child-2"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> +  table=??(ls_out_network_function), priority=100  , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 1), action=(nf_learn_orig_inport(ipv6 = true); outport 
>>>>> = "child-2"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "child-1"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "child-2"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=2    , match=(inport == 
>>>>> "child-2"), action=(reg8[[24]] = nf_lookup_orig_inport(); next;)
>>>>> table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 0 && ct_label.nf_id == 1), action=(outport = "child-2"; 
>>>>> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(outport = "child-2"; 
>>>>> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> ])
>>>>> @@ -20383,16 +20588,23 @@ AT_CHECK(
>>>>> [grep -E 'ls_(in|out)_network_function' lflows | ovn_strip_lflows | 
>>>>> sort], [0], [dnl
>>>>> table=??(ls_in_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_in_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "child-3"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "child-4"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(reg8[[21]] == 
>>>>> 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=100  , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 2), action=(nf_learn_orig_inport(ipv6 = false); outport 
>>>>> = "child-3"; output;)
>>>>> +  table=??(ls_in_network_function), priority=100  , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 2), action=(nf_learn_orig_inport(ipv6 = true); outport 
>>>>> = "child-3"; output;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "child-3"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(inport == 
>>>>> "child-4"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = false); next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = true); next;)
>>>>> table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 0 && ct_label.nf_id == 2), action=(outport = "child-3"; 
>>>>> output;)
>>>>> table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 1 && reg0[[22..29]] == 2), action=(outport = "child-3"; 
>>>>> output;)
>>>>> table=??(ls_out_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_out_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "child-3"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "child-4"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(reg8[[21]] 
>>>>> == 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=100  , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 2), action=(nf_learn_orig_inport(ipv6 = false); outport 
>>>>> = "child-4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> +  table=??(ls_out_network_function), priority=100  , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 && 
>>>>> reg0[[22..29]] == 2), action=(nf_learn_orig_inport(ipv6 = true); outport 
>>>>> = "child-4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "child-3"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(outport == 
>>>>> "child-4"), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=2    , match=(inport == 
>>>>> "child-4"), action=(reg8[[24]] = nf_lookup_orig_inport(); next;)
>>>>> table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 0 && ct_label.nf_id == 2), action=(outport = "child-4"; 
>>>>> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 1 && reg0[[22..29]] == 2), action=(outport = "child-4"; 
>>>>> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
>>>>> ])
>>>>> @@ -20422,10 +20634,12 @@ AT_CHECK(
>>>>> [grep -E 'ls_(in|out)_network_function' lflows | ovn_strip_lflows | 
>>>>> sort], [0], [dnl
>>>>> table=??(ls_in_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_in_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(reg8[[21]] == 
>>>>> 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = false); next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = true); next;)
>>>>> table=??(ls_out_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_out_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(reg8[[21]] 
>>>>> == 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> ])
>>>>> 
>>>>> AS_BOX([Configure NFG fallback method to fail-open])
>>>>> @@ -20453,10 +20667,12 @@ AT_CHECK(
>>>>> [grep -E 'ls_(in|out)_network_function' lflows | ovn_strip_lflows | 
>>>>> sort], [0], [dnl
>>>>> table=??(ls_in_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_in_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(reg8[[21]] == 
>>>>> 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = false); next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = true); next;)
>>>>> table=??(ls_out_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_out_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(reg8[[21]] 
>>>>> == 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> ])
>>>>> 
>>>>> OVN_CLEANUP_NORTHD
>>>>> @@ -21625,12 +21841,14 @@ AT_CHECK(
>>>>> table=??(ls_in_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_in_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "sw0-nf-p1"), action=(drop;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(reg8[[21]] == 
>>>>> 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = false); next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = true); next;)
>>>>> table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(clone {outport = 
>>>>> "sw0-nf-p1"; output;}; next;)
>>>>> table=??(ls_out_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_out_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "sw0-nf-p1"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(reg8[[21]] 
>>>>> == 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 0 && ct_label.nf_id == 1), action=(clone {outport = 
>>>>> "sw0-nf-p1"; reg8[[23]] = 1; next(pipeline=ingress, table=??);}; next;)
>>>>> ])
>>>>> 
>>>>> @@ -21723,14 +21941,16 @@ AT_CHECK(
>>>>> table=??(ls_in_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "sw0-nf-p1"), action=(drop;)
>>>>> table=??(ls_in_network_function), priority=100  , match=(inport == 
>>>>> "sw0-nf-p3"), action=(drop;)
>>>>> -  table=??(ls_in_network_function), priority=100  , match=(reg8[[21]] == 
>>>>> 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip4 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = false); next;)
>>>>> +  table=??(ls_in_network_function), priority=50   , match=(ip6 && 
>>>>> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 0), 
>>>>> action=(nf_learn_orig_inport(ipv6 = true); next;)
>>>>> table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 0 && ct_label.nf_id == 2), action=(clone {outport = 
>>>>> "sw0-nf-p3"; output;}; next;)
>>>>> table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(clone {outport = 
>>>>> "sw0-nf-p1"; output;}; next;)
>>>>> table=??(ls_out_network_function), priority=0    , match=(1), 
>>>>> action=(next;)
>>>>> table=??(ls_out_network_function), priority=1    , match=(reg8[[21]] == 
>>>>> 1), action=(drop;)
>>>>> table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "sw0-nf-p1"), action=(next;)
>>>>> table=??(ls_out_network_function), priority=100  , match=(outport == 
>>>>> "sw0-nf-p3"), action=(next;)
>>>>> -  table=??(ls_out_network_function), priority=100  , match=(reg8[[21]] 
>>>>> == 1 && eth.mcast), action=(next;)
>>>>> +  table=??(ls_out_network_function), priority=110  , match=(eth.mcast), 
>>>>> action=(next;)
>>>>> table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 0 && ct_label.nf_id == 1), action=(clone {outport = 
>>>>> "sw0-nf-p1"; reg8[[23]] = 1; next(pipeline=ingress, table=??);}; next;)
>>>>> table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
>>>>> && reg8[[22]] == 1 && reg0[[22..29]] == 2), action=(clone {outport = 
>>>>> "sw0-nf-p3"; reg8[[23]] = 1; next(pipeline=ingress, table=??);}; next;)
>>>>> ])
>>>>> diff --git a/tests/ovn.at b/tests/ovn.at
>>>>> index 47bf5f4ec..672dfb5f2 100644
>>>>> --- a/tests/ovn.at
>>>>> +++ b/tests/ovn.at
>>>>> @@ -45034,6 +45034,119 @@ OVN_CLEANUP_NORTHD
>>>>> AT_CLEANUP
>>>>> ])
>>>>> 
>>>>> +OVN_FOR_EACH_NORTHD([
>>>>> +AT_SETUP([Network function -- inline post-NF re-flood loop prevention])
>>>>> +AT_KEYWORDS([ovn])
>>>>> +ovn_start
>>>>> +
>>>>> +# When an LS has ports with "unknown" addresses, a packet redirected to
>>>>> +# an inline NF can be re-flooded back out its original inport after NF
>>>>> +# processing.  End-to-end check: sw0-p1 sends to an unknown MAC and the
>>>>> +# post-NF re-flood copy heading back out sw0-p1 is dropped, while sw0-p2
>>>>> +# still receives it.
>>>>> +
>>>>> +check ovn-nbctl ls-add sw0
>>>>> +check ovn-nbctl lsp-add sw0 sw0-p1 -- \
>>>>> +    lsp-set-addresses sw0-p1 "f0:00:00:00:00:01 192.168.0.11" "unknown"
>>>>> +check ovn-nbctl lsp-add sw0 sw0-p2 -- \
>>>>> +    lsp-set-addresses sw0-p2 "f0:00:00:00:00:02 192.168.0.12" "unknown"
>>>> 
>>>> These have FDB learning enabled.
>>>> 
>>>>> +for i in 1 2; do
>>>>> +    check ovn-nbctl lsp-add sw0 sw0-nf-p$i -- \
>>>>> +        lsp-set-addresses sw0-nf-p$i "f0:00:00:00:01:0$i"
>>>>> +done
>>>>> +check ovn-nbctl set logical_switch_port sw0-nf-p1 \
>>>>> +    options:receive_multicast=false options:lsp_learn_mac=false \
>>>>> +    options:is-nf=true options:nf-linked-port=sw0-nf-p2
>>>>> +check ovn-nbctl set logical_switch_port sw0-nf-p2 \
>>>>> +    options:receive_multicast=false options:lsp_learn_mac=false \
>>>>> +    options:is-nf=true options:nf-linked-port=sw0-nf-p1
>>>>> +check ovn-nbctl nf-add nf0 1 sw0-nf-p1 sw0-nf-p2
>>>>> +check ovn-nbctl nfg-add nfg0 1 inline nf0
>>>>> +check ovn-nbctl acl-add sw0 to-lport 1002 'outport == "sw0-p2" && ip4' \
>>>>> +    allow-related nfg0
>>>>> +
>>>>> +# VLAN-backed: add a localnet port.
>>>>> +check ovn-nbctl lsp-add-localnet-port sw0 ln0 phys
>>>>> +check ovn-nbctl set logical_switch_port ln0 tag_request=100
>>>>> +
>>>>> +net_add n
>>>>> +sim_add hv1
>>>>> +as hv1
>>>>> +ovs-vsctl add-br br-phys
>>>>> +ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys
>>>>> +ovn_attach n br-phys 192.168.1.1
>>>>> +
>>>>> +ovs-vsctl add-port br-int vif1 -- \
>>>>> +    set interface vif1 external-ids:iface-id=sw0-p1 \
>>>>> +    options:tx_pcap=hv1/vif1-tx.pcap options:rxq_pcap=hv1/vif1-rx.pcap
>>>>> +ovs-vsctl add-port br-int vif2 -- \
>>>>> +    set interface vif2 external-ids:iface-id=sw0-p2 \
>>>>> +    options:tx_pcap=hv1/vif2-tx.pcap options:rxq_pcap=hv1/vif2-rx.pcap
>>>>> +for i in 1 2; do
>>>>> +    ovs-vsctl add-port br-int vif-nf$i -- \
>>>>> +        set interface vif-nf$i external-ids:iface-id=sw0-nf-p$i \
>>>>> +        options:tx_pcap=hv1/vif-nf$i-tx.pcap \
>>>>> +        options:rxq_pcap=hv1/vif-nf$i-rx.pcap
>>>>> +done
>>>>> +
>>>>> +OVN_POPULATE_ARP
>>>>> +wait_for_ports_up
>>>>> +check ovn-nbctl --wait=hv sync
>>>>> +
>>>>> +# ovn-controller populates OFTABLE_INPORT_IN_MC_UNKNOWN_LOOKUP table 
>>>>> with one
>>>>> +# flow per local _MC_unknown member (sw0-p1, sw0-p2, and ln0).
>>>>> +# Each flow sets MLF_INPORT_IN_MC_UNKNOWN flag.
>>>>> +OVS_WAIT_UNTIL([
>>>>> +    flow_count=`as hv1 ovs-ofctl dump-flows br-int 
>>>>> table=OFTABLE_INPORT_IN_MC_UNKNOWN_LOOKUP | \
>>>>> +        grep -c 'load:0x1->NXM_NX_REG10\[[27\]]'`
>>>>> +    test $flow_count -eq 3
>>>>> +])
>>>>> +
>>>>> +# Match the test packet in the pcaps (src IP 192.168.0.11 followed by 
>>>>> dst IP
>>>>> +# 192.168.0.12).
>>>>> +icmp_pkt_filter=c0a8000bc0a8000c
>>>>> +
>>>>> +# sw0-p1 sends to an unknown destination MAC.  The flooded copy destined 
>>>>> to
>>>>> +# sw0-p2 is redirected to the NF output port (sw0-nf-p2);
>>>>> +packet="inport==\"sw0-p1\" && eth.src==f0:00:00:00:00:01 &&
>>>>> +        eth.dst==00:00:00:00:00:99 && ip.ttl==64 && 
>>>>> ip4.src==192.168.0.11 &&
>>>>> +        ip4.dst==192.168.0.12 && icmp4.type==8 && icmp4.code==0"
>>>>> +check as hv1 ovs-appctl -t ovn-controller inject-pkt "$packet"
>>>>> +OVS_WAIT_UNTIL([
>>>>> +    pkts=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" 
>>>>> hv1/vif-nf2-tx.pcap | grep ${icmp_pkt_filter} -c`
>>>>> +    test $pkts -ge 1
>>>>> +])
>>>>> +
>>>>> +# The NF returns the packet on sw0-nf-p1 with an unknown dst MAC, 
>>>>> triggering
>>>>> +# a re-flood. Loop prevention drops the copy returning to sw0-p1 
>>>>> (original
>>>>> +# inport), while sw0-p2 receives its copy.
>>>>> +packet="inport==\"sw0-nf-p1\" && eth.src==f0:00:00:00:00:01 &&
>>>>> +        eth.dst==00:00:00:00:00:99 && ip.ttl==64 && 
>>>>> ip4.src==192.168.0.11 &&
>>>>> +        ip4.dst==192.168.0.12 && icmp4.type==8 && icmp4.code==0"
>>>>> +check as hv1 ovs-appctl -t ovn-controller inject-pkt "$packet"
>>>>> +OVS_WAIT_UNTIL([
>>>>> +    pkts=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif2-tx.pcap | 
>>>>> grep ${icmp_pkt_filter} -c`
>>>>> +    test $pkts -ge 1
>>>>> +])
>>>>> +
>>>>> +# The original inport must not receive the looped-back copy.
>>>>> +AT_CHECK([
>>>>> +    pkts=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif1-tx.pcap | 
>>>>> grep ${icmp_pkt_filter} -c`
>>>>> +    test 0 -eq $pkts
>>>>> +])
>>>>> +
>>>>> +# The loop-prevention drop flow that has REGBIT_NF_LOOKUP_HIT 
>>>>> (reg8=0x1000000)
>>>>> +# bit set must have hit at least once.
>>>>> +OVS_WAIT_UNTIL([
>>>>> +    hits=`as hv1 ovs-ofctl dump-flows br-int | grep "reg8=0x1000000" | \
>>>>> +          grep -v n_packets=0 | wc -l`
>>>>> +    test $hits -ge 1
>>>>> +])
>>>>> +
>>>>> +OVN_CLEANUP([hv1])
>>>> 
>>>> There's a cleanup race here as a new FDB entry might be learned in the
>>>> meantime (between flows are collected and recompute is issued and flows
>>>> collected again).  Probably the easiest is to ignore FDB tables in this
>>>> case, i.e.:
>>>> 
>>>> OVN_CLEANUP([hv1
>>>> ignored_tables=OFTABLE_GET_FDB,OFTABLE_LOOKUP_FDB])
>>>> AT_CLEANUP
>>>> ])
>>> 
>>> Ack
>>> 
>>>> 
>>>>> +AT_CLEANUP
>>>>> +])
>>>>> +
>>>>> OVN_FOR_EACH_NORTHD([
>>>>> AT_SETUP([Unicast ARP when proxy ARP is configured])
>>>>> CHECK_SCAPY
>>>> 
>>>> Regards,
>>>> Dumitru
>>> 
>>> 
>>> Thanks,
>>> Naveen
>> 
>> 
> 
> Thanks,
> Dumitru

Thanks,
Naveen


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

Reply via email to