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

>> 
>>> }
>>> 
>>> 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.
> 
>> 
>>> +        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


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

Reply via email to