On 9/15/25 9:34 PM, Sragdhara Datta Chaudhuri wrote:
> For overlay subnets, all cross-host traffic exchanges are tunneled. For VLAN
> subnets, we need to selectively tunnel traffic sent to or coming from the NF
> ports. Consider a from-lport ACL applied to port p1 on host1. The NF ports 
> nfp1
> and nfp2 are on host2. A new option in LSP allows the NF ports to be linked.
> The “nf-linked-port” in nfp1 is to be set to nfp2 and vice versa.
> The ingress pipeline on host1 sets the outport to nfp1 and the packet is then
> processed by table REMOTE_OUTPUT.
> 
> On host1
> --------
> REMOTE_OUTPUT (table 43):
> It tunnels traffic destined to all non-local overlay ports to their associated
> hosts. The Same rule is now also added for traffic to non-local NF ports. Thus
> the packets from p1 get tunneled to host 2.
> 
> Upon reaching host2
> -------------------
> PHY_TO_LOG (table 0):
> Existing priority 100 rule: for each geneve tunnel interface on the chassis,
> copy info from header to inport, outport, metadata registers. Now same rule
> also stores the tun intf id in a register (reg5[16..31]).
> 
> CHECK_LOOPBACK (table 46)
> This table has a rule that clears all the registers. The change is to skip the
> clearing of reg5[16..31].
> 
> Logical egress pipeline:
> ls_out_stateful priority 120: If the outport is NF port, copy reg5[16..31]
> (table0 had set it) to ct_label.tun_if_id.
> 
> LOCAL_OUTPUT (table 45)
> When the packet comes out of the other NF port (nfp2), following two rules 
> send
> it back to the host that it originally came from:
> Priority 110: For each NF port local to this host, following rule processes 
> the
> packet through CT of linked port:
>   match: inport==nfp2 && RECIRC_BIT==0
>   action: RECIRC_BIT = 1, ct(zone=nfp1’s zone, table=LOCAL), resubmit table 43
> 
> Priority 109: For each local {tunnel_id, nf port}, send the recirculated 
> packet
> using tun_if_id in ct zone:
>   match: inport==nfp1 && RECIRC_BIT==1 && && ct_label.tun_if_id==<tun-id>
>   action: tunnel packet using tun-id
> 
> Case where NF responds back on nfp1, instead of forwarding to nfp2
> ------------------------------------------------------------------
> For example, a SYN packet from p1 got redirected to nfp1. Then the NF, which 
> is
> a firewall VM, drops the SYN and sends RST back on port nfp1. In this case,
> looking up in linked port (nfp2) ct zone will not give anything. The following
> rule uses ct.inv to identify such scenario and uses nfp1’s CT zone to send the
> packet back. To achieve this, following 2 rules are installed:
> 
> in_network_function:
> Priority 100 rule that allows packets incoming from NF type ports, is enhanced
> with additional action to store the tun_if_id from ct_label into reg5[16..31].
> 
> LOCAL_OUTPUT (table 45)
> Priority 110 rule: for recirculated packets, if ct (of the linked port) is
> invalid, use the tun id from MFF_LOG_TUN_OFPORT to tunnel the packet back (as
> CT zone info has been overwritten in the above 110 priority rule).
>   match: inport==nfp1 && RECIRC_BIT==1 && ct.inv && reg5[16..31]==<tun-id>
>   action: tunnel packet using tun-id
> 
> Signed-off-by: Sragdhara Datta Chaudhuri <[email protected]>
> Acked-by: Naveen Yerramneni <[email protected]>
> ---

Hi Sragdhara, Naveen, Numan,

Thanks for this new revision and reviews!

I have a few minor comments on this patch.

>  NEWS                         |   5 +
>  TODO.rst                     |   5 +
>  controller/physical.c        | 313 ++++++++++++++++++++++++-
>  include/ovn/logical-fields.h |   9 +
>  lib/logical-fields.c         |  10 +
>  northd/northd.c              |  82 +++++--
>  ovn-nb.xml                   |  16 ++
>  tests/ovn-controller.at      |   4 +-
>  tests/ovn-nbctl.at           |   8 +-
>  tests/ovn-northd.at          |  32 ++-
>  tests/ovn.at                 | 442 +++++++++++++++++++++++++++--------
>  tests/system-ovn.at          | 164 +++++++++++++
>  12 files changed, 962 insertions(+), 128 deletions(-)
> 
> diff --git a/NEWS b/NEWS
> index 66eb9e0b1..619d8f1e2 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -22,6 +22,11 @@ Post v25.09.0
>       NOTE:
>       * Network functions must not modify packet headers.
>       * The feature is not supported in conjunction with Load Balancer.
> +     * The feature is supported for both VLAN and overlay networks.
> +       When network function is used in a VLAN network, geneve tunneling is 
> used
> +       for cross host traffic (between the chassis hosting network function 
> and
> +       the chassis hosting the port where the ACL is being enforced). Proper
> +       MTU needs to be configured to accomodate this encapsulation.
>     - Added disable_garp_rarp option to logical_router table in order to 
> disable
>       GARP/RARP announcements by all the peer ports of this logical router.
>  
> diff --git a/TODO.rst b/TODO.rst
> index cda5f0d99..522389919 100644
> --- a/TODO.rst
> +++ b/TODO.rst
> @@ -171,6 +171,11 @@ OVN To-do List
>      allow for the eventual removal of the ovn\_datapath structure from the
>      codebase.
>  
> +* Network function insertion
> +
> +  * Geneve tunnel is used for supporting this feature for VLAN network.
> +    Extend the support over VxLAN tunnel as well.
> +
>  * CI
>  
>    * ovn-kubernetes: Only a subset of the ovn-kubernetes features is currently
> diff --git a/controller/physical.c b/controller/physical.c
> index 9ca535a6c..daae1e7c5 100644
> --- a/controller/physical.c
> +++ b/controller/physical.c
> @@ -175,6 +175,8 @@ put_decapsulation(enum mf_field_id mff_ovn_geneve,
>          put_move(MFF_TUN_ID, 0,  MFF_LOG_DATAPATH, 0, 24, ofpacts);
>          put_move(mff_ovn_geneve, 16, MFF_LOG_INPORT, 0, 15, ofpacts);
>          put_move(mff_ovn_geneve, 0, MFF_LOG_OUTPORT, 0, 16, ofpacts);
> +        put_load(ofp_to_u16(tun->ofport), MFF_LOG_TUN_OFPORT,
> +                 16, 16, ofpacts);
>      } else if (tun->type == VXLAN) {
>          /* Add flows for non-VTEP tunnels. Split VNI into two 12-bit
>           * sections and use them for datapath and outport IDs. */
> @@ -387,6 +389,15 @@ match_outport_dp_and_port_keys(struct match *match,
>      match_set_reg(match, MFF_LOG_OUTPORT - MFF_REG0, port_key);
>  }
>  
> +static void
> +match_inport_dp_and_port_keys(struct match *match,
> +                              uint32_t dp_key, uint32_t port_key)
> +{
> +    match_init_catchall(match);
> +    match_set_metadata(match, htonll(dp_key));
> +    match_set_reg(match, MFF_LOG_INPORT - MFF_REG0, port_key);
> +}
> +
>  static struct sbrec_encap *
>  find_additional_encap_for_chassis(const struct sbrec_port_binding *pb,
>                                    const struct sbrec_chassis *chassis_rec)
> @@ -452,7 +463,8 @@ put_remote_port_redirect_overlay(const struct 
> sbrec_port_binding *binding,
>                                   uint32_t port_key,
>                                   struct match *match,
>                                   struct ofpbuf *ofpacts_p,
> -                                 struct ovn_desired_flow_table *flow_table)
> +                                 struct ovn_desired_flow_table *flow_table,
> +                                 bool allow_hairpin)
>  {
>      /* Setup encapsulation */
>      for (size_t i = 0; i < ctx->n_encap_ips; i++) {
> @@ -471,6 +483,14 @@ put_remote_port_redirect_overlay(const struct 
> sbrec_port_binding *binding,
>                           ofpacts_clone);
>              }
>  
> +            /* Clear the MFF_INPORT if the same packet may need to go out 
> from
> +             * the same tunnel inport. */
> +            if (allow_hairpin) {
> +                put_stack(MFF_IN_PORT, ofpact_put_STACK_PUSH(ofpacts_clone));
> +                put_load(ofp_to_u16(OFPP_NONE), MFF_IN_PORT, 0, 16,
> +                         ofpacts_clone);
> +            }
> +
>              const struct chassis_tunnel *tun;
>              VECTOR_FOR_EACH (&tuns, tun) {
>                  put_encapsulation(ctx->mff_ovn_geneve, tun, 
> binding->datapath,
> @@ -478,6 +498,11 @@ put_remote_port_redirect_overlay(const struct 
> sbrec_port_binding *binding,
>                  ofpact_put_OUTPUT(ofpacts_clone)->port = tun->ofport;
>              }
>              put_resubmit(OFTABLE_REMOTE_VTEP_OUTPUT, ofpacts_clone);
> +
> +            if (allow_hairpin) {
> +                put_stack(MFF_IN_PORT, ofpact_put_STACK_POP(ofpacts_clone));
> +            }
> +
>              ofctrl_add_flow(flow_table, OFTABLE_REMOTE_OUTPUT, 100,
>                              binding->header_.uuid.parts[0], match,
>                              ofpacts_clone, &binding->header_.uuid);
> @@ -487,6 +512,229 @@ put_remote_port_redirect_overlay(const struct 
> sbrec_port_binding *binding,
>      }
>  }
>  
> +static const struct sbrec_port_binding *
> +get_binding_network_function_linked_port(
> +                    struct ovsdb_idl_index *sbrec_port_binding_by_name,
> +                    const struct sbrec_port_binding *binding)
> +{
> +    const char *nf_linked_name = smap_get(&binding->options,
> +                                          "nf-linked-port");
> +    if (!nf_linked_name) {
> +        return NULL;
> +    }
> +    VLOG_DBG("get NF linked port_binding %s:%s",
> +             binding->logical_port, nf_linked_name);
> +    const struct sbrec_port_binding *nf_linked_port = lport_lookup_by_name(
> +        sbrec_port_binding_by_name, nf_linked_name);
> +    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
> +    if (!nf_linked_port) {
> +        VLOG_INFO_RL(&rl, "Binding not found for nf-linked-port"
> +                  " %s", nf_linked_name);
> +        return NULL;
> +    }
> +    if (strcmp(nf_linked_port->type, binding->type)) {
> +        VLOG_ERR_RL(&rl, "Binding type mismatch between %s and "
> +                  "nf-linked-port %s",
> +                  binding->logical_port,  nf_linked_name);
> +        return NULL;
> +    }
> +    const char *nf_linked_linked_name = smap_get(
> +        &nf_linked_port->options, "nf-linked-port");
> +    if (!nf_linked_linked_name || strcmp(nf_linked_linked_name,
> +                                         binding->logical_port)) {
> +        VLOG_INFO_RL(&rl, "LSP name %s does not match linked_linked_name",
> +                  binding->logical_port);
> +        return NULL;
> +    }
> +
> +    return nf_linked_port;
> +}
> +
> +static void
> +send_traffic_by_tunnel(const struct sbrec_port_binding *binding,
> +                       struct match *match,
> +                       struct ofpbuf *ofpacts_p,
> +                       uint32_t dp_key,
> +                       uint32_t port_key,
> +                       struct chassis_tunnel *tun,
> +                       enum mf_field_id mff_ovn_geneve,
> +                       struct ovn_desired_flow_table *flow_table)
> +{
> +    match_init_catchall(match);
> +    ofpbuf_clear(ofpacts_p);
> +
> +    match_inport_dp_and_port_keys(match, dp_key, port_key);
> +    match_set_reg_masked(match, MFF_LOG_FLAGS - MFF_REG0, MLF_RECIRC,
> +                         MLF_RECIRC);
> +    ovs_u128 of_tun_ct_label_id_val = {
> +        .u64.hi = ((uint32_t) ofp_to_u16(tun->ofport)) << 16,
> +    };
> +    ovs_u128 of_tun_ct_label_id_mask = {
> +        .u64.hi = 0x00000000ffff0000,
> +    };
> +
> +    match_set_ct_label_masked(match, of_tun_ct_label_id_val,
> +                              of_tun_ct_label_id_mask);
> +
> +    put_load(binding->datapath->tunnel_key, MFF_TUN_ID, 0, 24, ofpacts_p);
> +    put_move(MFF_LOG_OUTPORT, 0, mff_ovn_geneve, 0, 32, ofpacts_p);
> +    put_load(port_key, mff_ovn_geneve, 16, 15, ofpacts_p);
> +
> +    ofpact_put_OUTPUT(ofpacts_p)->port = tun->ofport;
> +    ofctrl_add_flow(flow_table, OFTABLE_LOCAL_OUTPUT, 109,
> +                    binding->header_.uuid.parts[0], match,
> +                    ofpacts_p, &binding->header_.uuid);
> +}
> +
> +static void
> +put_redirect_overlay_to_source(const struct sbrec_port_binding *binding,
> +                               int linked_ct,

Nit: "int linked_ct_zone" would be more descriptive.

> +                               const struct hmap *chassis_tunnels,
> +                               enum mf_field_id mff_ovn_geneve,
> +                               struct match *match,
> +                               struct ofpbuf *ofpacts_p,
> +                               struct ovn_desired_flow_table *flow_table)
> +{
> +    uint32_t dp_key = binding->datapath->tunnel_key;
> +    uint32_t port_key = binding->tunnel_key;
> +
> +    /* Say, a network function has ports nf1 and nf2. The source port p1 is 
> on
> +     * a different host. The packet redirected from p1 was tunneled to the NF
> +     * host. In PHY_TO_LOG table the tunnel interface id is stored in
> +     * MFF_LOG_TUN_OFPORT. The egress pipeline then commits it into ct_label
> +     * tun_if_id in nf1's zone (out_stateful priority 120 rule). When the 
> same
> +     * packet comes out from nf2, two rules process it:
> +     * first rule sets recirc bit to 1 and processes the packet through nf1's
> +     * ct zone and resubmits to same table. When the recirculated packet 
> comes
> +     * back, the second rule (which checks recirc bit == 1) uses the 
> tun_if_id
> +     * from ct_label to send the packet back to p1's host.
> +     */
> +
> +    /* Table 45 (LOCAL_OUTPUT), priority 110
> +     * =====================================
> +     *
> +     * Each flow matches a logical inport to a nf port and checks if
> +     * recirc bit is 0 (i.e. packet first time being processed by this 
> table).
> +     * The action processes the packet through ct zone of the linked nf port
> +     * and resubmits to the same table after setting recirc bit to 1.
> +     * match: inport == svc-port[i] && MLF_RECIRC_BIT = 0
> +     * action: MLF_RECIRC_BIT = 1, ct(zone=linked-zone[i], table=LOCAL)
> +     */
> +    match_init_catchall(match);
> +    ofpbuf_clear(ofpacts_p);
> +    match_inport_dp_and_port_keys(match, dp_key, port_key);
> +    match_set_dl_type(match, htons(ETH_TYPE_IP));
> +    match_set_reg_masked(match, MFF_LOG_FLAGS - MFF_REG0, 0, MLF_RECIRC);
> +
> +    put_load(1, MFF_LOG_FLAGS, MLF_RECIRC_BIT, 1, ofpacts_p);
> +    put_load(linked_ct, MFF_LOG_CT_ZONE, 0, 16, ofpacts_p);
> +
> +    struct ofpact_conntrack *ct = ofpact_put_CT(ofpacts_p);
> +    ct->recirc_table = OFTABLE_LOCAL_OUTPUT;
> +    ct->zone_src.field = mf_from_id(MFF_LOG_CT_ZONE);
> +    ct->zone_src.ofs = 0;
> +    ct->zone_src.n_bits = 16;
> +    ct->flags = 0;
> +    ct->alg = 0;
> +    ofpact_finish(ofpacts_p, &ct->ofpact);
> +
> +    ofctrl_add_flow(flow_table, OFTABLE_LOCAL_OUTPUT, 110,
> +                    binding->header_.uuid.parts[0], match,
> +                    ofpacts_p, &binding->header_.uuid);
> +
> +    /* Table 45 (LOCAL_OUTPUT), priority 110
> +     * In case NF is sending back a response on the port it received the
> +     * packet on, instead of forwarding out of the other port (e.g. NF 
> sending
> +     * RST to the SYN received), the ct lookup in linked port's zone would
> +     * fail. Based on ct.inv check the packet is then tunneled back using
> +     * the tunnel id from this port's zone itself. The above rule has
> +     * overwritten the zone info by now, so we recover it from the register
> +     * that was populated by in_network_function stage with the tunnel id.
> +     * match: inport == svc-port[i] && MLF_RECIRC_BIT = 1
> +     *        && ct.inv && MFF_LOG_TUN_OFPORT == <tun-id>
> +     * action: tunnel back using above tun-id
> +     */
> +    struct chassis_tunnel *tun;
> +    HMAP_FOR_EACH (tun, hmap_node, chassis_tunnels) {
> +        match_init_catchall(match);
> +        ofpbuf_clear(ofpacts_p);
> +        match_inport_dp_and_port_keys(match, dp_key, port_key);
> +        match_set_reg_masked(match, MFF_LOG_FLAGS - MFF_REG0, MLF_RECIRC,
> +                             MLF_RECIRC);
> +        match_set_ct_state_masked(match, OVS_CS_F_INVALID, OVS_CS_F_INVALID);
> +        match_set_reg_masked(match, MFF_LOG_TUN_OFPORT - MFF_REG0,
> +                             ((uint32_t) ofp_to_u16(tun->ofport)) << 16,
> +                             ((uint32_t) 0xffff) << 16);
> +        put_load(binding->datapath->tunnel_key, MFF_TUN_ID, 0, 24, 
> ofpacts_p);
> +        put_move(MFF_LOG_OUTPORT, 0, mff_ovn_geneve, 0, 32, ofpacts_p);
> +        put_load(port_key, mff_ovn_geneve, 16, 15, ofpacts_p);
> +
> +        ofpact_put_OUTPUT(ofpacts_p)->port = tun->ofport;
> +        ofctrl_add_flow(flow_table, OFTABLE_LOCAL_OUTPUT, 110,
> +                        binding->header_.uuid.parts[0], match,
> +                        ofpacts_p, &binding->header_.uuid);
> +    }
> +
> +    /* Table 45 (LOCAL_OUTPUT), priority 109
> +     * =====================================
> +     *
> +     * A flow is installed For each {remote tunnel_id, nf port} combination. 
> It
> +     * matches the inport with the nf port and the ct_label.tun_if_id with 
> the
> +     * tunnel_id. Also checks if the recirc bit is 1 (i.e. packet being
> +     * processed by this table second time). The action is to send the packet
> +     * out using the tunnel interface.
> +     * match: inport == svc-port[i] && MLF_RECIRC_BIT = 1
> +     *        && ct_label.tun_if_id == <tun-id>
> +     * action: tunnel back using tun-id
> +     */
> +    HMAP_FOR_EACH (tun, hmap_node, chassis_tunnels) {
> +        send_traffic_by_tunnel(binding, match, ofpacts_p, dp_key, port_key,
> +                               tun, mff_ovn_geneve, flow_table);
> +    }
> +    ofpbuf_clear(ofpacts_p);
> +}
> +
> +static void
> +put_redirect_overlay_to_source_from_nf_port(
> +        const struct sbrec_port_binding *binding,
> +        struct ovsdb_idl_index *sbrec_port_binding_by_name,
> +        const struct hmap *chassis_tunnels,
> +        const struct shash *ct_zones,
> +        enum mf_field_id mff_ovn_geneve,
> +        struct match *match,
> +        struct ofpbuf *ofpacts_p,
> +        struct ovn_desired_flow_table *flow_table)
> +{
> +    const struct sbrec_port_binding *linked_pb;
> +    linked_pb = get_binding_network_function_linked_port(
> +        sbrec_port_binding_by_name, binding);
> +    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
> +    if (!linked_pb) {
> +        VLOG_INFO_RL(&rl, "Linked port not found for %s",
> +                     binding->logical_port);
> +        return;
> +    }
> +    struct zone_ids zone = get_zone_ids(binding, ct_zones);
> +    if (!zone.ct) {
> +        VLOG_INFO_RL(&rl, "Port zone not found for %s", 
> binding->logical_port);
> +        return;
> +    }
> +    struct zone_ids linked_zone = get_zone_ids(linked_pb, ct_zones);
> +    if (!linked_zone.ct) {
> +        VLOG_INFO_RL(&rl, "Linked port zone not found for %s",
> +                     binding->logical_port);
> +        return;
> +    }
> +    VLOG_DBG_RL(&rl, "Both port zones found for NF port %s",
> +                binding->logical_port);

To be more useful this should be VLOG_DBG().

> +    put_redirect_overlay_to_source(binding, linked_zone.ct, chassis_tunnels,
> +                                   mff_ovn_geneve, match, ofpacts_p,
> +                                   flow_table);
> +    put_redirect_overlay_to_source(linked_pb, zone.ct, chassis_tunnels,
> +                                   mff_ovn_geneve,  match, ofpacts_p,
> +                                   flow_table);
> +}
> +
>  static void
>  put_remote_port_redirect_overlay_ha_remote(
>      const struct sbrec_port_binding *binding,
> @@ -962,6 +1210,29 @@ add_default_drop_flow(const struct physical_ctx *p_ctx,
>      ofpbuf_uninit(&ofpacts);
>  }
>  
> +/* Clear logical registers for network function datapaths.
> + * Resets all logical registers to zero except MFF_LOG_TUN_OFPORT, which is
> + * partially cleared. Bits 16-31 store the geneve tunnel interface ID of
> + * received packets and are preserved for the egress pipeline.
> + * Bits 0-15 are cleared.
> + */
> +static void
> +clear_registers_for_nf_datapath(struct ofpbuf *ofpacts_p)
> +{
> +    /* Clear all logical registers except MFF_LOG_TUN_OFPORT */
> +    for (int i = 0; i < MFF_N_LOG_REGS; i++) {

for (size_t i = 0

> +        if ((MFF_REG0 + i) != MFF_LOG_TUN_OFPORT) {
> +            /* Clear entire 32-bit register */
> +            put_load(0, MFF_REG0 + i, 0, 32, ofpacts_p);
> +        }
> +    }
> +
> +    /* Partially clear MFF_LOG_TUN_OFPORT register:
> +     * - Bits 16-31: Preserve geneve tunnel ID for egress pipeline
> +     * - Bits 0-15: Clear to zero for clean state */
> +    put_load(0, MFF_LOG_TUN_OFPORT, 0, 16, ofpacts_p);
> +}
> +
>  static void
>  put_local_common_flows(uint32_t dp_key,
>                         const struct sbrec_port_binding *pb,
> @@ -1013,6 +1284,24 @@ put_local_common_flows(uint32_t dp_key,
>                      pb->header_.uuid.parts[0], &match, ofpacts_p,
>                      &pb->header_.uuid);
>  
> +    /* Table 46, Priority 1.
> +     * =======================
> +     * For datapath with network function ports, add a flow to clear only the
> +     * required logical registers.
> +     * In the default case, priority 0 rule clears all the registers.
> +     */
> +    bool nf_port = smap_get_bool(&pb->options, "is-nf", false);
> +    if (nf_port) {
> +        match_init_catchall(&match);
> +        ofpbuf_clear(ofpacts_p);
> +        match_set_metadata(&match, htonll(dp_key));
> +        clear_registers_for_nf_datapath(ofpacts_p);
> +        put_resubmit(OFTABLE_LOG_EGRESS_PIPELINE, ofpacts_p);
> +        ofctrl_add_flow(flow_table, OFTABLE_CHECK_LOOPBACK, 1,
> +                        pb->datapath->header_.uuid.parts[0], &match,
> +                        ofpacts_p, &pb->datapath->header_.uuid);
> +    }
> +
>      /* Table 64, Priority 100.
>       * =======================
>       *
> @@ -1907,10 +2196,11 @@ consider_port_binding(const struct physical_ctx *ctx,
>  
>      /* Determine how the port is accessed. */
>      enum access_type access_type = PORT_LOCAL;
> +    bool is_nf = smap_get_bool(&binding->options, "is-nf", false);
>      if (!ofport) {
>          /* Enforce tunneling while we clone packets to additional chassis b/c
>           * otherwise upstream switch won't flood the packet to both chassis. 
> */
> -        if (localnet_port && !binding->additional_chassis) {
> +        if (localnet_port && !binding->additional_chassis && !is_nf) {
>              ofport = u16_to_ofp(simap_get(ctx->patch_ofports,
>                                            localnet_port->logical_port));
>              if (!ofport) {
> @@ -2140,6 +2430,20 @@ consider_port_binding(const struct physical_ctx *ctx,
>                              binding->header_.uuid.parts[0], &match,
>                              ofpacts_p, &binding->header_.uuid);
>          }
> +
> +        /* Packets egressing from network function ports need to be sent to 
> the
> +         * source. */
> +        if (is_nf && localnet_port) {
> +            put_redirect_overlay_to_source_from_nf_port(
> +                                 binding,
> +                                 ctx->sbrec_port_binding_by_name,
> +                                 ctx->chassis_tunnels,
> +                                 ctx->ct_zones,
> +                                 ctx->mff_ovn_geneve,
> +                                 &match,
> +                                 ofpacts_p,
> +                                 flow_table);
> +        }
>      } else if (access_type == PORT_LOCALNET && !ctx->always_tunnel) {
>          /* Remote port connected by localnet port */
>          /* Table 45, priority 100.
> @@ -2199,7 +2503,8 @@ consider_port_binding(const struct physical_ctx *ctx,
>              &match, ofpacts_p, ctx->chassis_tunnels, flow_table);
>      } else {
>          put_remote_port_redirect_overlay(
> -            binding, type, ctx, port_key, &match, ofpacts_p, flow_table);
> +            binding, type, ctx, port_key, &match, ofpacts_p, flow_table,
> +            is_nf);
>      }
>  out:
>      if (ha_ch_ordered) {
> @@ -3194,7 +3499,7 @@ physical_run(struct physical_ctx *p_ctx,
>       *
>       * Handles packets received from a VXLAN tunnel which get resubmitted to
>       * OFTABLE_LOG_INGRESS_PIPELINE due to lack of needed metadata in VXLAN,
> -     * explicitly skip sending back out any tunnels and resubmit to table 40
> +     * explicitly skip sending back out any tunnels and resubmit to table 43
>       * for local delivery, except packets which have MLF_ALLOW_LOOPBACK bit
>       * set.
>       */
> diff --git a/include/ovn/logical-fields.h b/include/ovn/logical-fields.h
> index 76925eac7..d2ba45240 100644
> --- a/include/ovn/logical-fields.h
> +++ b/include/ovn/logical-fields.h
> @@ -42,6 +42,7 @@ enum ovn_controller_event {
>                                         * (16..31 of the 32 bits). */
>  #define MFF_LOG_INPORT     MFF_REG14  /* Logical input port (32 bits). */
>  #define MFF_LOG_OUTPORT    MFF_REG15  /* Logical output port (32 bits). */
> +#define MFF_LOG_TUN_OFPORT MFF_REG5   /* 16..31 of the 32 bits */
>  
>  /* Logical registers.
>   *
> @@ -104,6 +105,7 @@ enum mff_log_flags_bits {
>      MLF_UNSNAT_NEW_BIT = 20,
>      MLF_UNSNAT_NOT_TRACKED_BIT = 21,
>      MLF_IGMP_IGMP_SNOOP_INJECT_BIT = 22,
> +    MLF_RECIRC_BIT = 23,
>      MLF_NETWORK_ID_START_BIT = 28,
>      MLF_NETWORK_ID_END_BIT = 31,
>  };
> @@ -173,6 +175,9 @@ enum mff_log_flags {
>      /* Indicate that this is an IGMP packet reinjected by ovn-controller. */
>      MLF_IGMP_IGMP_SNOOP = (1 << MLF_IGMP_IGMP_SNOOP_INJECT_BIT),
>  
> +    /* Indicate the packet has been processed by LOCAL table once before. */
> +    MLF_RECIRC = (1 << MLF_RECIRC_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),
> @@ -240,15 +245,19 @@ const struct ovn_field *ovn_field_from_name(const char 
> *name);
>  #define OVN_CT_OBS_STAGE_END_BIT 5
>  #define OVN_CT_ALLOW_ESTABLISHED_BIT 6
>  #define OVN_CT_NETWORK_FUNCTION_GROUP_BIT 7
> +#define OVN_CT_TUN_IF_BIT 8
>  
>  #define OVN_CT_BLOCKED 1
>  #define OVN_CT_NATTED  2
>  #define OVN_CT_LB_SKIP_SNAT 4
>  #define OVN_CT_LB_FORCE_SNAT 8
>  #define OVN_CT_NETWORK_FUNCTION_GROUP 128
> +#define OVN_CT_TUN_IF 256
>  
>  #define OVN_CT_NETWORK_FUNCTION_GROUP_ID_1ST_BIT 17
>  #define OVN_CT_NETWORK_FUNCTION_GROUP_ID_END_BIT 24
> +#define OVN_CT_TUN_IF_1ST_BIT 80
> +#define OVN_CT_TUN_IF_END_BIT 95
>  
>  #define OVN_CT_ECMP_ETH_1ST_BIT 32
>  #define OVN_CT_ECMP_ETH_END_BIT 79
> diff --git a/lib/logical-fields.c b/lib/logical-fields.c
> index 809ae39af..e19e6a757 100644
> --- a/lib/logical-fields.c
> +++ b/lib/logical-fields.c
> @@ -233,6 +233,16 @@ ovn_init_symtab(struct shash *symtab)
>                                      OVN_CT_NETWORK_FUNCTION_GROUP_ID_END_BIT)
>                                      "]",
>                                      WR_CT_COMMIT);
> +    expr_symtab_add_subfield_scoped(symtab, "ct_label.tun_if", NULL,
> +                                    "ct_label["
> +                                    OVN_CT_STR(OVN_CT_TUN_IF_BIT)
> +                                    "]",
> +                                    WR_CT_COMMIT);
> +    expr_symtab_add_subfield_scoped(symtab, "ct_label.tun_if_id", NULL,
> +                                    "ct_label["
> +                                    OVN_CT_STR(OVN_CT_TUN_IF_1ST_BIT) ".."
> +                                    OVN_CT_STR(OVN_CT_TUN_IF_END_BIT) "]",
> +                                    WR_CT_COMMIT);
>  
>      expr_symtab_add_field(symtab, "ct_state", MFF_CT_STATE, NULL, false);
>  
> diff --git a/northd/northd.c b/northd/northd.c
> index 672fffcab..1e177b8d2 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -240,6 +240,10 @@ static const char *reg_ct_state[] = {
>  #undef CS_STATE
>  };
>  
> +/* Register used for storing tunnel openflow interface id, in a Logical 
> Switch.
> + * Must match the MFF_LOG_TUN_OFPORT in logical-fields.h */
> +#define REG_TUN_OFPORT "reg5[16..31]"
> +
>  /* Register used for temporarily store ECMP eth.src to avoid masked ct_label
>   * access. It doesn't really occupy registers because the content of the
>   * register is saved to stack and then restored in the same flow.
> @@ -283,7 +287,7 @@ static const char *reg_ct_state[] = {
>   * | R4 |                 REG_LB_IPV4                  |   |                 
>                   |
>   * | R4 |    (>= IN_PRE_STATEFUL && <= IN_HAIRPIN)     | X |                 
>                   |
>   * +----+----------------------------------------------+ X |           
> REG_LB_IPV6             |
> - * | R5 |                   UNUSED                     | R |      (>= 
> IN_PRE_STATEFUL &&       |
> + * | R5 |           REG_TUN_OFPORT (16..31)            | R |      (>= 
> IN_PRE_STATEFUL &&       |
>   * +----+----------------------------------------------+ E |       <= 
> IN_HAIRPIN)              |
>   * | R6 |                   UNUSED                     | G |                 
>                   |
>   * +----+----------------------------------------------+ 1 |                 
>                   |
> @@ -17789,11 +17793,53 @@ network_function_get_active(const struct 
> nbrec_network_function_group *nfg)
>      return nfg->n_network_function ? nfg->network_function[0] : NULL;
>  }
>  
> +/* For packets received on tunnel and egressing towards a network-function 
> port
> + * commit the tunnel interface id in CT. This will be utilized when the 
> packet
> + * comes out of the other network-function interface of the service VM. The
> + * packet then will be tunneled back to the source host. */
> +static void
> +build_lswitch_stateful_nf(struct ovn_port *op,
> +                          struct ds *actions, struct ds *match,
> +                          struct lflow_table *lflows,
> +                          struct lflow_ref *lflow_ref)
> +{
> +    ds_clear(actions);
> +    ds_clear(match);
> +
> +    ds_put_cstr(actions,
> +                 "ct_commit { "
> +                    "ct_mark.blocked = 0; "
> +                    "ct_mark.allow_established = " REGBIT_ACL_PERSIST_ID "; "
> +                    "ct_label.acl_id = " REG_ACL_ID "; "
> +                    "ct_label.tun_if_id = " REG_TUN_OFPORT "; }; next;");
> +    ds_put_format(match,
> +                  "outport == %s && " REGBIT_ACL_LABEL" == 0", op->json_key);
> +    ovn_lflow_add(lflows, op->od, S_SWITCH_OUT_STATEFUL, 120,
> +                  ds_cstr(match), ds_cstr(actions), lflow_ref);
> +
> +    ds_clear(actions);
> +    ds_clear(match);
> +    ds_put_format(match,
> +                  "outport == %s && " REGBIT_ACL_LABEL" == 1",
> +                  op->json_key);
> +    ds_put_cstr(actions,
> +                 "ct_commit { "
> +                    "ct_mark.blocked = 0; "
> +                    "ct_mark.allow_established = " REGBIT_ACL_PERSIST_ID "; "
> +                    "ct_label.acl_id = " REG_ACL_ID "; "
> +                    "ct_mark.obs_stage = " REGBIT_ACL_OBS_STAGE "; "
> +                    "ct_mark.obs_collector_id = " REG_OBS_COLLECTOR_ID_EST 
> "; "
> +                    "ct_label.obs_point_id = " REG_OBS_POINT_ID_EST "; "
> +                    "ct_label.tun_if_id = " REG_TUN_OFPORT "; }; next;");
> +    ovn_lflow_add(lflows, op->od, S_SWITCH_OUT_STATEFUL, 120,
> +                  ds_cstr(match), ds_cstr(actions), lflow_ref);
> +}
> +
>  static void
> -consider_network_function(struct lflow_table *lflows,
> -                          const struct ovn_datapath *od,
> +consider_network_function(const struct ovn_datapath *od,
>                            struct nbrec_network_function_group *nfg,
> -                          struct lflow_ref *lflow_ref, bool ingress)
> +                          bool ingress, struct lflow_table *lflows,
> +                          struct lflow_ref *lflow_ref)
>  {
>      struct ds match = DS_EMPTY_INITIALIZER;
>      struct ds action = DS_EMPTY_INITIALIZER;
> @@ -17906,7 +17952,7 @@ consider_network_function(struct lflow_table *lflows,
>       * match.
>       */
>      ds_put_format(&match, "inport == %s", input_port->json_key);
> -    ds_put_format(&action, "next;");
> +    ds_put_format(&action, REG_TUN_OFPORT" = ct_label.tun_if_id; next;");
>      ovn_lflow_add(lflows, od, S_SWITCH_IN_NETWORK_FUNCTION, 100,
>                    ds_cstr(&match), ds_cstr(&action), lflow_ref);
>      ds_clear(&match);
> @@ -17948,14 +17994,21 @@ consider_network_function(struct lflow_table 
> *lflows,
>      ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 110, ds_cstr(&match),
>                    ds_cstr(&action), lflow_ref);
>  
> +    /* Priority 120 flows in out_stateful:
> +     * If packet was received on a tunnel interface and being forwarded to a
> +     * NF port, commit openflow tunnel interface id in ct_label.
> +     */
> +    build_lswitch_stateful_nf(output_port, &action, &match, lflows, 
> lflow_ref);
> +    build_lswitch_stateful_nf(input_port, &action, &match, lflows, 
> lflow_ref);
> +
>      ds_destroy(&match);
>      ds_destroy(&action);
>  }
>  
>  static void
>  build_network_function(const struct ovn_datapath *od,
> -                       struct lflow_table *lflows,
>                         const struct ls_port_group_table *ls_pgs,
> +                       struct lflow_table *lflows,

I'm not sure why the parameter order had to change.  I assume it was
unintentional.  I reverted this part.

>                         struct lflow_ref *lflow_ref)
>  {
>      unsigned long *nfg_ingress_bitmap
> @@ -18016,8 +18069,8 @@ build_network_function(const struct ovn_datapath *od,
>                  continue;
>              }
>              nfg_bitmap = bitmap_set1(nfg_bitmap, nfg_id);
> -            consider_network_function(lflows, od, 
> acl->network_function_group,
> -                                      lflow_ref, ingress);
> +            consider_network_function(od, acl->network_function_group,
> +                                      ingress, lflows, lflow_ref);
>          }
>      }
>  
> @@ -18041,9 +18094,8 @@ build_network_function(const struct ovn_datapath *od,
>                          continue;
>                      }
>                      nfg_bitmap = bitmap_set1(nfg_bitmap, nfg_id);
> -                    consider_network_function(lflows, od,
> -                                              acl->network_function_group,
> -                                              lflow_ref, ingress);
> +                    consider_network_function(od, 
> acl->network_function_group,
> +                                              ingress, lflows, lflow_ref);
>                  }
>              }
>          }
> @@ -18093,8 +18145,7 @@ build_lswitch_and_lrouter_iterate_by_ls(struct 
> ovn_datapath *od,
>      build_mirror_default_lflow(od, lsi->lflows);
>      build_lswitch_lflows_pre_acl_and_acl(od, lsi->lflows,
>                                           lsi->meter_groups, NULL);
> -    build_network_function(od, lsi->lflows, lsi->ls_port_groups,
> -                           NULL);
> +    build_network_function(od, lsi->ls_port_groups, lsi->lflows, NULL);
>      build_fwd_group_lflows(od, lsi->lflows, NULL);
>      build_lswitch_lflows_admission_control(od, lsi->lflows, NULL);
>      build_lswitch_learn_fdb_od(od, lsi->lflows, NULL);
> @@ -19119,9 +19170,8 @@ lflow_handle_ls_stateful_changes(struct ovsdb_idl_txn 
> *ovnsb_txn,
>                                  lflow_input->features,
>                                  lflows,
>                                  lflow_input->sbrec_acl_id_table);
> -        build_network_function(od, lflows,
> -                               lflow_input->ls_port_groups,
> -                               ls_stateful_rec->lflow_ref);
> +        build_network_function(od, lflow_input->ls_port_groups,
> +                               lflows, ls_stateful_rec->lflow_ref);
>  
>          /* Sync the new flows to SB. */
>          bool handled = lflow_ref_sync_lflows(
> diff --git a/ovn-nb.xml b/ovn-nb.xml
> index 7f7e70fab..dcb4ac635 100644
> --- a/ovn-nb.xml
> +++ b/ovn-nb.xml
> @@ -1557,6 +1557,22 @@
>            this port. The default value is <code>true</code>.
>          </column>
>  
> +        <column name="options" key="is-nf"
> +                type='{"type": "boolean"}'>
> +          Needs to be set to <code>true</code> for Network Function ports.
> +          These are the ports used as <code>inport</code> or
> +          <code>outport</code> in Network_Function table.
> +          The default value is <code>false</code>.
> +        </column>
> +
> +        <column name="options" key="nf-linked-port"
> +                type='{"type": "string"}'>
> +          Each row in Network_Function table refers to two logical switch 
> ports
> +          under the columns <code>inport</code> and <code>outport</code>. The
> +          port identified as <code>inport</code> needs to have this option 
> set
> +          to the port identified as <code>outport</code>, and vice-versa.
> +        </column>
> +
>          <group title="VIF Plugging Options">
>            <column name="options" key="vif-plug-type">
>              If set, OVN will attempt to perform plugging of this VIF.  In 
> order
> diff --git a/tests/ovn-controller.at b/tests/ovn-controller.at
> index 0b00906ae..b0af455e4 100644
> --- a/tests/ovn-controller.at
> +++ b/tests/ovn-controller.at
> @@ -3717,8 +3717,8 @@ AT_CHECK([grep -c "reg10=0/0x10000" flood_flows], [0], 
> [dnl
>  # Geneve
>  hv2_cookie="$(chassis_cookie hv2)"
>  AT_CHECK_UNQUOTED([grep "cookie=$hv2_cookie," phy_to_log_flows], [0], [dnl
> - cookie=$hv2_cookie, 
> priority=120,arp,tun_metadata0=0,in_port="ovn-hv2-0",arp_op=2 
> actions=load:0x1->NXM_NX_REG10[[16]],move:NXM_NX_TUN_ID[[0..23]]->OXM_OF_METADATA[[0..23]],move:NXM_NX_TUN_METADATA0[[16..30]]->NXM_NX_REG14[[0..14]],move:NXM_NX_TUN_METADATA0[[0..15]]->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_LOG_INGRESS_PIPELINE)
> - cookie=$hv2_cookie, 
> priority=120,icmp6,tun_metadata0=0,in_port="ovn-hv2-0",icmp_type=136,icmp_code=0
>  
> actions=load:0x1->NXM_NX_REG10[[16]],move:NXM_NX_TUN_ID[[0..23]]->OXM_OF_METADATA[[0..23]],move:NXM_NX_TUN_METADATA0[[16..30]]->NXM_NX_REG14[[0..14]],move:NXM_NX_TUN_METADATA0[[0..15]]->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_LOG_INGRESS_PIPELINE)
> + cookie=$hv2_cookie, 
> priority=120,arp,tun_metadata0=0,in_port="ovn-hv2-0",arp_op=2 
> actions=load:0x1->NXM_NX_REG10[[16]],move:NXM_NX_TUN_ID[[0..23]]->OXM_OF_METADATA[[0..23]],move:NXM_NX_TUN_METADATA0[[16..30]]->NXM_NX_REG14[[0..14]],move:NXM_NX_TUN_METADATA0[[0..15]]->NXM_NX_REG15[[0..15]],load:0x1->NXM_NX_REG5[[16..31]],resubmit(,OFTABLE_LOG_INGRESS_PIPELINE)
> + cookie=$hv2_cookie, 
> priority=120,icmp6,tun_metadata0=0,in_port="ovn-hv2-0",icmp_type=136,icmp_code=0
>  
> actions=load:0x1->NXM_NX_REG10[[16]],move:NXM_NX_TUN_ID[[0..23]]->OXM_OF_METADATA[[0..23]],move:NXM_NX_TUN_METADATA0[[16..30]]->NXM_NX_REG14[[0..14]],move:NXM_NX_TUN_METADATA0[[0..15]]->NXM_NX_REG15[[0..15]],load:0x1->NXM_NX_REG5[[16..31]],resubmit(,OFTABLE_LOG_INGRESS_PIPELINE)
>  ])
>  
>  # VXLAN
> diff --git a/tests/ovn-nbctl.at b/tests/ovn-nbctl.at
> index 0e8d78c98..5636b2969 100644
> --- a/tests/ovn-nbctl.at
> +++ b/tests/ovn-nbctl.at
> @@ -3218,10 +3218,10 @@ AT_CHECK([check ovn-nbctl lsp-add ls0 svc-port0])
>  AT_CHECK([check ovn-nbctl lsp-add ls0 svc-port1])
>  AT_CHECK([check ovn-nbctl set logical_switch_port svc-port0 \
>      options:receive_multicast=false options:lsp_learn_fdb=false \
> -    options:network-function=true 
> options:network-function-linked-port=svc-port1])
> +    options:is-nf=true options:nf-linked-port=svc-port1])
>  AT_CHECK([check ovn-nbctl set logical_switch_port svc-port1 \
>      options:receive_multicast=false options:lsp_learn_fdb=false \
> -    options:network-function=true 
> options:network-function-linked-port=svc-port0])
> +    options:is-nf=true options:nf-linked-port=svc-port0])
>  
>  # Create network-function.
>  AT_CHECK([ovn-nbctl nf-add nf0 svc-port0 svc-port1])
> @@ -3238,10 +3238,10 @@ AT_CHECK([check ovn-nbctl lsp-add ls0 svc-port4])
>  AT_CHECK([check ovn-nbctl lsp-add ls0 svc-port5])
>  AT_CHECK([check ovn-nbctl set logical_switch_port svc-port4 \
>      options:receive_multicast=false options:lsp_learn_fdb=false \
> -    options:network-function=true 
> options:network-function-linked-port=svc-port5])
> +    options:is-nf=true options:nf-linked-port=svc-port5])
>  AT_CHECK([check ovn-nbctl set logical_switch_port svc-port5 \
>      options:receive_multicast=false options:lsp_learn_fdb=false \
> -    options:network-function=true 
> options:network-function-linked-port=svc-port4])
> +    options:is-nf=true options:nf-linked-port=svc-port4])
>  AT_CHECK([ovn-nbctl --may-exist nf-add nf0 svc-port4 svc-port5])
>  AT_CHECK([ovn-nbctl nf-list | uuidfilt], [0], [dnl
>  <0> (nf0) in:svc-port4 out:svc-port5
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index 53dc6f409..a3f258173 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -18227,8 +18227,8 @@ 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=(next;)
> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
> "sw0-nf-p2"), action=(next;)
> +  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=99   , match=(reg8[[21]] == 1 
> && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(outport = "sw0-nf-p1"; 
> output;)
>    table=??(ls_out_network_function), priority=0    , match=(1), 
> action=(next;)
> @@ -18257,6 +18257,18 @@ AT_CHECK(
>    table=??(ls_out_acl_eval    ), priority=65532, match=(ct.est && 
> ct_mark.allow_established == 1), action=(reg8[[21]] = 
> ct_label.network_function_group; reg8[[16]] = 1; next;)
>  ])
>  
> +    AT_CHECK([grep "ls_out_stateful" sw0flows | ovn_strip_lflows], [0], [dnl
> +  table=??(ls_out_stateful    ), priority=0    , match=(1), action=(next;)
> +  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && 
> reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; 
> ct_mark.allow_established = reg0[[20]]; ct_label.acl_id = reg2[[16..31]]; 
> ct_label.network_function_group = 0; ct_label.network_function_group_id = 0; 
> }; next;)
> +  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && 
> reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; 
> ct_mark.allow_established = reg0[[20]]; ct_mark.obs_stage = reg8[[19..20]]; 
> ct_mark.obs_collector_id = reg8[[8..15]]; ct_label.obs_point_id = reg9; 
> ct_label.acl_id = reg2[[16..31]]; ct_label.network_function_group = 0; 
> ct_label.network_function_group_id = 0; }; next;)
> +  table=??(ls_out_stateful    ), priority=110  , match=(reg0[[1]] == 1 && 
> reg0[[13]] == 0 && reg8[[21]] == 1), action=(ct_commit { ct_mark.blocked = 0; 
> ct_mark.allow_established = reg0[[20]]; ct_label.acl_id = reg2[[16..31]]; 
> ct_label.network_function_group = 1; ct_label.network_function_group_id = 
> reg0[[22..29]]; }; next;)
> +  table=??(ls_out_stateful    ), priority=110  , match=(reg0[[1]] == 1 && 
> reg0[[13]] == 1 && reg8[[21]] == 1), action=(ct_commit { ct_mark.blocked = 0; 
> ct_mark.allow_established = reg0[[20]]; ct_mark.obs_stage = reg8[[19..20]]; 
> ct_mark.obs_collector_id = reg8[[8..15]]; ct_label.obs_point_id = reg9; 
> ct_label.acl_id = reg2[[16..31]]; ct_label.network_function_group = 1; 
> ct_label.network_function_group_id = reg0[[22..29]]; }; next;)
> +  table=??(ls_out_stateful    ), priority=120  , match=(outport == 
> "sw0-nf-p1" && reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; 
> ct_mark.allow_established = reg0[[20]]; ct_label.acl_id = reg2[[16..31]]; 
> ct_label.tun_if_id = reg5[[16..31]]; }; next;)
> +  table=??(ls_out_stateful    ), priority=120  , match=(outport == 
> "sw0-nf-p1" && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; 
> ct_mark.allow_established = reg0[[20]]; ct_label.acl_id = reg2[[16..31]]; 
> ct_mark.obs_stage = reg8[[19..20]]; ct_mark.obs_collector_id = reg8[[8..15]]; 
> ct_label.obs_point_id = reg9; ct_label.tun_if_id = reg5[[16..31]]; }; next;)
> +  table=??(ls_out_stateful    ), priority=120  , match=(outport == 
> "sw0-nf-p2" && reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; 
> ct_mark.allow_established = reg0[[20]]; ct_label.acl_id = reg2[[16..31]]; 
> ct_label.tun_if_id = reg5[[16..31]]; }; next;)
> +  table=??(ls_out_stateful    ), priority=120  , match=(outport == 
> "sw0-nf-p2" && reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; 
> ct_mark.allow_established = reg0[[20]]; ct_label.acl_id = reg2[[16..31]]; 
> ct_mark.obs_stage = reg8[[19..20]]; ct_mark.obs_collector_id = reg8[[8..15]]; 
> ct_label.obs_point_id = reg9; ct_label.tun_if_id = reg5[[16..31]]; }; next;)
> +])
> +
>  # ICMP packets from sw0-p1 should be redirected to sw0-nf-p1, but in 
> revervse direction should not.
>  flow_eth_from_p1='eth.src == 00:00:00:00:00:01 && eth.dst == 
> 00:00:00:00:00:02'
>  flow_ip_from_p1='ip.ttl==64 && ip4.src == 10.0.0.2 && ip4.dst == 10.0.0.3'
> @@ -18309,10 +18321,10 @@ 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=(next;)
> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
> "sw0-nf-p2"), action=(next;)
> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
> "sw0-nf-p3"), action=(next;)
> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
> "sw0-nf-p4"), action=(next;)
> +  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=99   , match=(reg8[[21]] == 1 
> && reg8[[22]] == 0 && ct_label.network_function_group_id == 2), 
> action=(outport = "sw0-nf-p3"; output;)
>    table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
> && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(outport = "sw0-nf-p1"; 
> output;)
> @@ -18380,10 +18392,10 @@ 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=(next;)
> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
> "sw1-nf-p2"), action=(next;)
> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
> "sw1-nf-p3"), action=(next;)
> -  table=??(ls_in_network_function), priority=100  , match=(inport == 
> "sw1-nf-p4"), action=(next;)
> +  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=99   , match=(reg8[[21]] == 1 
> && reg8[[22]] == 0 && ct_label.network_function_group_id == 2), 
> action=(outport = "sw1-nf-p3"; output;)
>    table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
> && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(outport = "sw1-nf-p1"; 
> output;)
> diff --git a/tests/ovn.at b/tests/ovn.at
> index 31aa511f8..de69abb1c 100644
> --- a/tests/ovn.at
> +++ b/tests/ovn.at
> @@ -147,6 +147,8 @@ ct_label.network_function_group = ct_label[7]
>  ct_label.network_function_group_id = ct_label[17..24]
>  ct_label.obs_point_id = ct_label[96..127]
>  ct_label.obs_unused = ct_label[0..95]
> +ct_label.tun_if = ct_label[8]
> +ct_label.tun_if_id = ct_label[80..95]
>  ct_mark = NXM_NX_CT_MARK
>  ct_mark.allow_established = ct_mark[6]
>  ct_mark.blocked = ct_mark[0]
> @@ -44010,133 +44012,389 @@ AT_CLEANUP
>  ])
>  
>  OVN_FOR_EACH_NORTHD([
> -AT_SETUP([Network function packet flow])
> +AT_SETUP([Network function packet flow - outbound])
>  AT_KEYWORDS([ovn])
>  ovn_start
>  
> -check ovn-nbctl ls-add sw0
> +# Create logical topology. One LS sw0 with 4 ports.
> +# From-lport ACL rule directs request packets from sw0-p1 to sw0-p2 via NF 
> port {sw0-nf-p1, sw0-nf-p2}
> +# Response packets from sw0-p2 to sw0-p1 redirected via NF ports in reverse 
> order.
> +create_logical_topology() {
> +    sw=$1
> +    check ovn-nbctl ls-add $sw
> +    for i in 1 2; do
> +        check ovn-nbctl lsp-add $sw $sw-p$i -- lsp-set-addresses $sw-p$i 
> "f0:00:00:00:00:0$i 192.168.0.1$i"
> +        check ovn-nbctl lsp-add $sw $sw-nf-p$i -- lsp-set-addresses 
> $sw-nf-p$i "f0:00:00:00:01:0$i"
> +    done
> +    check ovn-nbctl set logical_switch_port $sw-nf-p1 \
> +        options:receive_multicast=false options:lsp_learn_mac=false \
> +        options:is-nf=true options:nf-linked-port=$sw-nf-p2
> +    check ovn-nbctl set logical_switch_port $sw-nf-p2 \
> +        options:receive_multicast=false options:lsp_learn_mac=false \
> +        options:is-nf=true options:nf-linked-port=$sw-nf-p1
> +    check ovn-nbctl nf-add nf0 $sw-nf-p1 $sw-nf-p2
> +    check ovn-nbctl nfg-add nfg0 1 inline nf0
> +    check ovn-nbctl pg-add pg0 $sw-p1
> +    check ovn-nbctl acl-add pg0 from-lport 1002 "inport == @pg0 && ip4.dst 
> == 192.168.0.12" allow-related nfg0
> +}
>  
> -net_add n1
> -sim_add hv1
> -as hv1
> -ovs-vsctl add-br br-phys
> -ovn_attach n1 br-phys 192.168.0.1
> +create_logical_topology sw0
> +
> +# Create three hypervisors
> +net_add n
>  for i in 1 2 3; do
> -    ovs-vsctl add-port br-int vif$i -- \
> -        set interface vif$i external-ids:iface-id=sw0-p$i \
> -        options:tx_pcap=hv1/vif$i-tx.pcap \
> -        options:rxq_pcap=hv1/vif$i-rx.pcap
> -    check ovn-nbctl lsp-add sw0 sw0-p$i -- lsp-set-addresses sw0-p$i 
> "f0:00:00:00:00:0$i 192.168.0.1$i"
> +    sim_add hv$i
> +    as hv$i
> +    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.$i
>  done
>  
> -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
> -    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:network-function=true options:network-function-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:network-function=true options:network-function-linked-port=sw0-nf-p1
> -check ovn-nbctl nf-add nf0 sw0-nf-p1 sw0-nf-p2
> -check ovn-nbctl nfg-add nfg0 1 inline nf0
> -check ovn-nbctl pg-add pg0 sw0-p1
> -check ovn-nbctl acl-add pg0 from-lport 1002 "inport == @pg0 && ip4.dst == 
> 192.168.0.12" allow-related nfg0
> -
> -OVN_POPULATE_ARP
> -
> -wait_for_ports_up
> -check ovn-nbctl --wait=hv sync
> -
>  test_icmp() {
> -    local inport=$1 src_mac=$2 dst_mac=$3 src_ip=$4 dst_ip=$5 icmp_type=$6 
> outport=$7
> +    local inport=$1 src_mac=$2 dst_mac=$3 src_ip=$4 dst_ip=$5 icmp_type=$6 
> outport=$7 in_hv=$8 out_hv=$9
>      local packet="inport==\"$inport\" && eth.src==$src_mac &&
>                    eth.dst==$dst_mac && ip.ttl==64 && ip4.src==$src_ip
>                    && ip4.dst==$dst_ip && icmp4.type==$icmp_type &&
>                    icmp4.code==0"
>      shift; shift; shift; shift; shift; shift
> -    OVS_WAIT_UNTIL([as hv1 ovs-appctl -t ovn-controller inject-pkt 
> "$packet"])
> +    OVS_WAIT_UNTIL([as $in_hv ovs-appctl -t ovn-controller inject-pkt 
> "$packet"])
>      echo "INJECTED PACKET $packet"
> -    echo $packet | ovstest test-ovn expr-to-packets >> $outport.expected
> +    echo $packet | ovstest test-ovn expr-to-packets >> 
> $out_hv-$outport.expected
> +}
> +
> +packet_redirection_test() {
> +    local hvp1=$1 hvp2=$2 hvnf=$3
> +    # Inject ICMP request packet from sw0-p1 and make sure it is being 
> redirected to the nf ingress port.
> +    test_icmp sw0-p1 "f0:00:00:00:00:01" "f0:00:00:00:00:02" "192.168.0.11" 
> "192.168.0.12" 8 vif-nf1 $hvp1 $hvnf
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvnf/vif-nf1-tx.pcap], 
> [$hvnf-vif-nf1.expected])
> +
> +    # Forward same packet from nf egress port and make sure it is reaching 
> sw0-p2.
> +    test_icmp sw0-nf-p2 "f0:00:00:00:00:01" "f0:00:00:00:00:02" 
> "192.168.0.11" "192.168.0.12" 8 vif2 $hvnf $hvp2
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvp2/vif2-tx.pcap], 
> [$hvp2-vif2.expected])
> +
> +    # Send response from sw0-p2 and check that it is being redirected to nf 
> egress port.
> +    test_icmp sw0-p2 "f0:00:00:00:00:02" "f0:00:00:00:00:01" "192.168.0.12" 
> "192.168.0.11" 0 vif-nf2 $hvp2 $hvnf
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvnf/vif-nf2-tx.pcap], 
> [$hvnf-vif-nf2.expected])
> +
> +    # Forward same response packet from nf ingress port and make sure it is 
> reaching sw0-p1.
> +    test_icmp sw0-nf-p1 "f0:00:00:00:00:02" "f0:00:00:00:00:01" 
> "192.168.0.12" "192.168.0.11" 0 vif1 $hvnf $hvp1
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvp1/vif1-tx.pcap], 
> [$hvp1-vif1.expected])
> +
> +    # Reverse direction packet should flow normally without redirection.
> +    # Send ICMP request from sw0-p2 destined to sw0-p1.
> +    test_icmp sw0-p2 "f0:00:00:00:00:02" "f0:00:00:00:00:01" "192.168.0.12" 
> "192.168.0.11" 8 vif1 $hvp2 $hvp1
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvp1/vif1-tx.pcap], 
> [$hvp1-vif1.expected])
> +    # Send ICMP response from sw0-p1 destined to sw0-p2.
> +    test_icmp sw0-p1 "f0:00:00:00:00:01" "f0:00:00:00:00:02" "192.168.0.11" 
> "192.168.0.12" 0 vif2 $hvp1 $hvp2
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvp2/vif2-tx.pcap], 
> [$hvp2-vif2.expected])
> +}
> +
> +create_port_binding() {
> +    hvp1=$1 hvp2=$2 hvnf=$3
> +    as $hvp1
> +    ovs-vsctl add-port br-int vif1 -- \
> +        set interface vif1 external-ids:iface-id=sw0-p1 \
> +        options:tx_pcap=$hvp1/vif1-tx.pcap \
> +        options:rxq_pcap=$hvp1/vif1-rx.pcap
> +    as $hvp2
> +    ovs-vsctl add-port br-int vif2 -- \
> +        set interface vif2 external-ids:iface-id=sw0-p2 \
> +        options:tx_pcap=$hvp2/vif2-tx.pcap \
> +        options:rxq_pcap=$hvp2/vif2-rx.pcap
> +    as $hvnf
> +    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=$hvnf/vif-nf$i-tx.pcap \
> +            options:rxq_pcap=$hvnf/vif-nf$i-rx.pcap
> +    done
> +
> +    OVN_POPULATE_ARP
> +    wait_for_ports_up
> +    check ovn-nbctl --wait=hv sync
> +    sleep 1
>  }
> -# Inject ICMP request packet from sw0-p1 and make sure it is being 
> redirected to the nf ingress port.
> -test_icmp sw0-p1 "f0:00:00:00:00:01" "f0:00:00:00:00:02" "192.168.0.11" 
> "192.168.0.12" 8 vif-nf1
> -OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif-nf1-tx.pcap], [vif-nf1.expected])
>  
> -# Forward same packet from nf egress port and make sure it is reaching 
> sw0-p2.
> -test_icmp sw0-nf-p2 "f0:00:00:00:00:01" "f0:00:00:00:00:02" "192.168.0.11" 
> "192.168.0.12" 8 vif2
> -OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif2-tx.pcap], [vif2.expected])
> +cleanup_port_binding() {
> +    hvp1=$1 hvp2=$2 hvnf=$3
> +    as $hvp1
> +    ovs-vsctl del-port br-int vif1
> +    as $hvp2
> +    ovs-vsctl del-port br-int vif2
> +    as $hvnf
> +    for i in 1 2; do
> +        ovs-vsctl del-port br-int vif-nf$i
> +    done
> +    sleep 1
> +}
>  
> -# Send response from sw0-p2 and check that it is being redirected to nf 
> egress port.
> -test_icmp sw0-p2 "f0:00:00:00:00:02" "f0:00:00:00:00:01" "192.168.0.12" 
> "192.168.0.11" 0 vif-nf2
> -OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif-nf2-tx.pcap], [vif-nf2.expected])
> +test_nf_with_multinodes_outbound() {
> +    mode=$1
> +    # Test 1: Bind all 4 ports to one node
> +    echo "$mode: Network function outbound with single node"
> +    create_port_binding hv1 hv1 hv1
>  
> -# Forward same response packet from nf ingress port and make sure it is 
> reaching sw0-p1.
> -test_icmp sw0-nf-p1 "f0:00:00:00:00:02" "f0:00:00:00:00:01" "192.168.0.12" 
> "192.168.0.11" 0 vif1
> -OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif1-tx.pcap], [vif1.expected])
> +    packet_redirection_test hv1 hv1 hv1 sw0
>  
> -# Reverse direction packet should flow normally without redirection.
> -# Send ICMP request from sw0-p2 destined to sw0-p1.
> -test_icmp sw0-p2 "f0:00:00:00:00:02" "f0:00:00:00:00:01" "192.168.0.12" 
> "192.168.0.11" 8 vif1
> -OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif1-tx.pcap], [vif1.expected])
> -# Send ICMP response from sw0-p1 destined to sw0-p2.
> -test_icmp sw0-p1 "f0:00:00:00:00:01" "f0:00:00:00:00:02" "192.168.0.11" 
> "192.168.0.12" 0 vif2
> -OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif2-tx.pcap], [vif2.expected])
> +    cleanup_port_binding hv1 hv1 hv1
>  
> +    # Test 2: src & dst ports on one node, NF on another node
> +    # sw0-p1, sw0-p2 on hv1, NF ports on hv2
> +    echo "$mode: Network function outbound with two nodes - nf separate"
> +    create_port_binding hv1 hv1 hv2
>  
> -OVN_CLEANUP([hv1])
> +    packet_redirection_test hv1 hv1 hv2 sw0
> +
> +    cleanup_port_binding hv1 hv1 hv2
> +
> +    # Test 3: src and nf on one node, dst on a second node
> +    echo "$mode: Network function outbound with two nodes - nf with src"
> +    create_port_binding hv1 hv2 hv1
> +
> +    packet_redirection_test hv1 hv2 hv1 sw0
> +
> +    cleanup_port_binding hv1 hv2 hv1
> +
> +    # Test 4: src on one node, nf & dst on a second node
> +    echo "$mode: Network function outbound with two nodes - nf with dst"
> +    create_port_binding hv1 hv2 hv2
> +
> +    packet_redirection_test hv1 hv2 hv2 sw0
> +
> +    cleanup_port_binding hv1 hv2 hv2
> +
> +    # Test 5: src on one node, dst on another, NF on a 3rd one
> +    # sw0-p1 on hv1, sw0-p2 on hv2, NF ports on hv3
> +    echo "$mode: Network function outbound with three nodes"
> +    create_port_binding hv1 hv2 hv3
> +
> +    packet_redirection_test hv1 hv2 hv3 sw0
> +
> +    cleanup_port_binding hv1 hv2 hv3
> +}
> +
> +test_nf_with_multinodes_outbound overlay
> +
> +# Tests for VLAN network
> +# Add localnet port to make it a VLAN backed LS
> +check ovn-nbctl lsp-add sw0 ln0 "" 100
> +check ovn-nbctl lsp-set-addresses ln0 unknown
> +check ovn-nbctl lsp-set-type ln0 localnet
> +check ovn-nbctl lsp-set-options ln0 network_name=phys
> +
> +test_nf_with_multinodes_outbound VLAN
> +
> +# Cleanup logical topology
> +check ovn-nbctl lsp-del ln0
> +check ovn-nbctl acl-del pg0 from-lport 1002 "inport == @pg0 && ip4.dst == 
> 192.168.0.12"
> +check ovn-nbctl pg-del pg0
> +check ovn-nbctl nfg-del nfg0
> +check ovn-nbctl nf-del nf0
> +check ovn-nbctl clear logical_switch_port sw0-nf-p1 options
> +check ovn-nbctl clear logical_switch_port sw0-nf-p2 options
> +for i in 1 2; do
> +    check ovn-nbctl lsp-del sw0-p$i
> +    check ovn-nbctl lsp-del sw0-nf-p$i
> +done
> +check ovn-nbctl ls-del sw0
> +check ovn-nbctl --wait=hv sync
> +
> +OVN_CLEANUP([hv1],[hv2],[hv3])
>  AT_CLEANUP
>  ])
>  
>  OVN_FOR_EACH_NORTHD([
> -AT_SETUP([Network function TCP packet flow])
> +AT_SETUP([Network function packet flow - inbound])
>  AT_KEYWORDS([ovn])
>  ovn_start
>  
> -check ovn-nbctl ls-add sw0
> +# Create logical topology. One LS sw0 with 4 ports.
> +# to-lport ACL rule directs request packets from sw0-p2 to sw0-p1 via NF 
> port {sw0-nf-p1, sw0-nf-p2}
> +# Response packets from sw0-p1 to sw0-p2 redirected via NF ports in reverse 
> order.
> +create_logical_topology() {
> +    sw=$1
> +    check ovn-nbctl ls-add $sw
> +    for i in 1 2; do
> +        check ovn-nbctl lsp-add $sw $sw-p$i -- lsp-set-addresses $sw-p$i 
> "f0:00:00:00:00:0$i 192.168.0.1$i"
> +        check ovn-nbctl lsp-add $sw $sw-nf-p$i -- lsp-set-addresses 
> $sw-nf-p$i "f0:00:00:00:01:0$i"
> +    done
> +    check ovn-nbctl set logical_switch_port $sw-nf-p1 \
> +        options:receive_multicast=false options:lsp_learn_mac=false \
> +        options:is-nf=true options:nf-linked-port=$sw-nf-p2
> +    check ovn-nbctl set logical_switch_port $sw-nf-p2 \
> +        options:receive_multicast=false options:lsp_learn_mac=false \
> +        options:is-nf=true options:nf-linked-port=$sw-nf-p1
> +    check ovn-nbctl nf-add nf0 $sw-nf-p1 $sw-nf-p2
> +    check ovn-nbctl nfg-add nfg0 1 inline nf0
> +    check ovn-nbctl pg-add pg0 $sw-p1
> +    check ovn-nbctl acl-add pg0 to-lport 1002 "outport == @pg0 && ip4.src == 
> 192.168.0.12" allow-related nfg0
> +}
>  
> -net_add n1
> -sim_add hv1
> -as hv1
> -ovs-vsctl add-br br-phys
> -ovn_attach n1 br-phys 192.168.0.1
> +create_logical_topology sw0
> +
> +# Create three hypervisors
> +net_add n
>  for i in 1 2 3; do
> -    ovs-vsctl add-port br-int vif$i -- \
> -        set interface vif$i external-ids:iface-id=sw0-p$i \
> -        options:tx_pcap=hv1/vif$i-tx.pcap \
> -        options:rxq_pcap=hv1/vif$i-rx.pcap
> -    check ovn-nbctl lsp-add sw0 sw0-p$i -- lsp-set-addresses sw0-p$i 
> "f0:00:00:00:00:0$i 192.168.0.1$i"
> +    sim_add hv$i
> +    as hv$i
> +    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.$i
>  done
>  
> -check ovn-nbctl pg-add pg0 sw0-p1
> -check ovn-nbctl acl-add pg0 from-lport 1002 "inport == @pg0 && ip4.dst == 
> 192.168.0.12" allow-related
> -check ovn-nbctl acl-add pg0 to-lport 1002 "outport == @pg0 && ip4.src == 
> 192.168.0.12" drop
> +test_icmp() {
> +    local inport=$1 src_mac=$2 dst_mac=$3 src_ip=$4 dst_ip=$5 icmp_type=$6 
> outport=$7 in_hv=$8 out_hv=$9
> +    local packet="inport==\"$inport\" && eth.src==$src_mac &&
> +                  eth.dst==$dst_mac && ip.ttl==64 && ip4.src==$src_ip
> +                  && ip4.dst==$dst_ip && icmp4.type==$icmp_type &&
> +                  icmp4.code==0"
> +    shift; shift; shift; shift; shift; shift
> +    OVS_WAIT_UNTIL([as $in_hv ovs-appctl -t ovn-controller inject-pkt 
> "$packet"])
> +    echo "INJECTED PACKET $packet"
> +    echo $packet | ovstest test-ovn expr-to-packets >> 
> $out_hv-$outport.expected
> +}
> +
> +packet_redirection_test() {
> +    local hvp1=$1 hvp2=$2 hvnf=$3
> +    # Inject ICMP request packet from sw0-p2 and make sure it is being 
> redirected to the nf outport.
> +    test_icmp sw0-p2 "f0:00:00:00:00:02" "f0:00:00:00:00:01" "192.168.0.12" 
> "192.168.0.11" 8 vif-nf2 $hvp2 $hvnf
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvnf/vif-nf2-tx.pcap], 
> [$hvnf-vif-nf2.expected])
> +
> +    # Forward same packet from nf inport and make sure it is reaching sw0-p1.
> +    test_icmp sw0-nf-p1 "f0:00:00:00:00:02" "f0:00:00:00:00:01" 
> "192.168.0.12" "192.168.0.11" 8 vif1 $hvnf $hvp1
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvp1/vif1-tx.pcap], 
> [$hvp1-vif1.expected])
> +
> +    # Send response from sw0-p1 and check that it is being redirected to nf 
> inport.
> +    test_icmp sw0-p1 "f0:00:00:00:00:01" "f0:00:00:00:00:02" "192.168.0.11" 
> "192.168.0.12" 0 vif-nf1 $hvp1 $hvnf
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvnf/vif-nf1-tx.pcap], 
> [$hvnf-vif-nf1.expected])
> +
> +    # Forward same response packet from nf outport and make sure it is 
> reaching sw0-p2.
> +    test_icmp sw0-nf-p1 "f0:00:00:00:00:01" "f0:00:00:00:00:02" 
> "192.168.0.11" "192.168.0.12" 0 vif2 $hvnf $hvp2
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvp2/vif2-tx.pcap], 
> [$hvp2-vif2.expected])
> +
> +    # Reverse direction packet should flow normally without redirection.
> +    # Send ICMP request from sw0-p1 destined to sw0-p2.
> +    test_icmp sw0-p1 "f0:00:00:00:00:01" "f0:00:00:00:00:02" "192.168.0.11" 
> "192.168.0.12" 8 vif2 $hvp1 $hvp2
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvp2/vif2-tx.pcap], 
> [$hvp2-vif2.expected])
> +    # Send ICMP response from sw0-p2 destined to sw0-p1.
> +    test_icmp sw0-p2 "f0:00:00:00:00:02" "f0:00:00:00:00:01" "192.168.0.12" 
> "192.168.0.11" 0 vif1 $hvp2 $hvp1
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvp1/vif1-tx.pcap], 
> [$hvp1-vif1.expected])
> +}
> +
> +create_port_binding() {
> +    hvp1=$1 hvp2=$2 hvnf=$3
> +    as $hvp1
> +    ovs-vsctl add-port br-int vif1 -- \
> +        set interface vif1 external-ids:iface-id=sw0-p1 \
> +        options:tx_pcap=$hvp1/vif1-tx.pcap \
> +        options:rxq_pcap=$hvp1/vif1-rx.pcap
> +    as $hvp2
> +    ovs-vsctl add-port br-int vif2 -- \
> +        set interface vif2 external-ids:iface-id=sw0-p2 \
> +        options:tx_pcap=$hvp2/vif2-tx.pcap \
> +        options:rxq_pcap=$hvp2/vif2-rx.pcap
> +    as $hvnf
> +    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=$hvnf/vif-nf$i-tx.pcap \
> +            options:rxq_pcap=$hvnf/vif-nf$i-rx.pcap
> +    done
>  
> -OVN_POPULATE_ARP
> +    OVN_POPULATE_ARP
> +    wait_for_ports_up
> +    check ovn-nbctl --wait=hv sync
> +    sleep 1
> +}
>  
> -wait_for_ports_up
> -check ovn-nbctl --wait=hv sync
> +cleanup_port_binding() {
> +    hvp1=$1 hvp2=$2 hvnf=$3
> +    as $hvp1
> +    ovs-vsctl del-port br-int vif1
> +    as $hvp2
> +    ovs-vsctl del-port br-int vif2
> +    as $hvnf
> +    for i in 1 2; do
> +        ovs-vsctl del-port br-int vif-nf$i
> +    done
> +    check ovn-nbctl --wait=hv sync
> +    sleep 1
> +}
>  
> -test_tcp() {
> -    local inport=$1 outport=$2 src_mac=$3 dst_mac=$4 src_ip=$5 dst_ip=$6 
> src_port=$7 dst_port=$8 flags=$9
> -    local packet="inport==\"$inport\" && eth.src==$src_mac &&
> -                  eth.dst==$dst_mac && ip.ttl==64 && ip4.src==$src_ip
> -                  && ip4.dst==$dst_ip && tcp && tcp.src==$src_port && 
> tcp.dst==$dst_port
> -                  && tcp.dst==$dst_port && tcp.flags==$flags"
> -    shift; shift; shift; shift; shift; shift; shift; shift; shift
> -    OVS_WAIT_UNTIL([as hv1 ovs-appctl -t ovn-controller inject-pkt 
> "$packet"])
> -    echo "INJECTED TCP PACKET $packet"
> -    echo $packet | ovstest test-ovn expr-to-packets >> $outport.expected
> -}
> -# Send TCP SYN from p1 to p2.
> -echo "Sending TCP SYN from p1 to p2"
> -test_tcp sw0-p1 vif2 "f0:00:00:00:00:01" "f0:00:00:00:00:02" "192.168.0.11" 
> "192.168.0.12" 1000 80 2
> -OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif2-tx.pcap], [vif2.expected])
> -# Send TCP SYN-ACK from p2 to p1.
> -echo "Sending TCP SYN-ACK from p2 to p1"
> -test_tcp sw0-p2 vif1 "f0:00:00:00:00:02" "f0:00:00:00:00:01" "192.168.0.12" 
> "192.168.0.11" 80 1000 18
> -OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif1-tx.pcap], [vif1.expected])
> +test_nf_with_multinodes_inbound() {
> +    mode=$1
>  
> -OVN_CLEANUP([hv1])
> +    # Test 1: Bind all 4 ports to one node
> +    echo "$mode: Network function inbound with single node"
> +    create_port_binding hv1 hv1 hv1
> +
> +    packet_redirection_test hv1 hv1 hv1 sw0
> +
> +    cleanup_port_binding hv1 hv1 hv1
> +
> +    # Test 2: src & dst ports on one node, NF on another node
> +    # sw0-p1, sw0-p2 on hv1, NF ports on hv2
> +    echo "$mode: Network function inbound with two nodes - nf separate"
> +    create_port_binding hv1 hv1 hv2
> +
> +    packet_redirection_test hv1 hv1 hv2 sw0
> +
> +    cleanup_port_binding hv1 hv1 hv2
> +
> +    # Test 3: dst and nf on one node, src on a second node
> +    # sw0-p1 & NF ports on hv1, sw0-p2 on hv2
> +    echo "$mode: Network function inbound with two nodes - nf with dst"
> +    create_port_binding hv1 hv2 hv1
> +
> +    packet_redirection_test hv1 hv2 hv1 sw0
> +
> +    cleanup_port_binding hv1 hv2 hv1
> +
> +    # Test 4: dst on one node, nf & src on a second node
> +    # sw0-p1 on hv1, sw0-p2 & NF ports on hv2
> +    echo "$mode: Network function inbound with two nodes - nf with src"
> +    create_port_binding hv1 hv2 hv2
> +
> +    packet_redirection_test hv1 hv2 hv2 sw0
> +
> +    cleanup_port_binding hv1 hv2 hv2
> +
> +    # Test 5: src on one node, dst on another, NF on a 3rd one
> +    # sw0-p1 on hv1, sw0-p2 on hv2, NF ports on hv3
> +    echo "$mode: Network function inbound with three nodes"
> +    create_port_binding hv1 hv2 hv3
> +
> +    packet_redirection_test hv1 hv2 hv3 sw0
> +
> +    cleanup_port_binding hv1 hv2 hv3
> +}
> +
> +test_nf_with_multinodes_inbound overlay
> +
> +# Tests for VLAN network
> +# Add localnet port to make it a VLAN backed LS
> +check ovn-nbctl lsp-add sw0 ln0 "" 100
> +check ovn-nbctl lsp-set-addresses ln0 unknown
> +check ovn-nbctl lsp-set-type ln0 localnet
> +check ovn-nbctl lsp-set-options ln0 network_name=phys
> +
> +test_nf_with_multinodes_inbound VLAN
> +
> +# Cleanup logical topology
> +check ovn-nbctl lsp-del ln0
> +check ovn-nbctl acl-del pg0 to-lport 1002 "outport == @pg0 && ip4.src == 
> 192.168.0.12"
> +check ovn-nbctl pg-del pg0
> +check ovn-nbctl nfg-del nfg0
> +check ovn-nbctl nf-del nf0
> +check ovn-nbctl clear logical_switch_port sw0-nf-p1 options
> +check ovn-nbctl clear logical_switch_port sw0-nf-p2 options
> +for i in 1 2; do
> +    check ovn-nbctl lsp-del sw0-p$i
> +    check ovn-nbctl lsp-del sw0-nf-p$i
> +done
> +check ovn-nbctl ls-del sw0
> +check ovn-nbctl --wait=hv sync
> +
> +OVN_CLEANUP([hv1],[hv2],[hv3])
>  AT_CLEANUP
>  ])
> diff --git a/tests/system-ovn.at b/tests/system-ovn.at
> index 8e356df6f..6aef8ec80 100644
> --- a/tests/system-ovn.at
> +++ b/tests/system-ovn.at
> @@ -18484,3 +18484,167 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port 
> patch-.*/d
>  /connection dropped.*/d"])
>  AT_CLEANUP
>  ])
> +
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([Network Function])
> +AT_SKIP_IF([test $HAVE_TCPDUMP = no])
> +ovn_start
> +OVS_TRAFFIC_VSWITCHD_START()
> +
> +ADD_BR([br-int])
> +
> +# Set external-ids in br-int needed for ovn-controller
> +ovs-vsctl \

Missing check.

> +        -- set Open_vSwitch . external-ids:system-id=hv1 \
> +        -- set Open_vSwitch . 
> external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
> +        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
> +        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
> +        -- set bridge br-int fail-mode=secure 
> other-config:disable-in-band=true
> +
> +# Start ovn-controller
> +start_daemon ovn-controller
> +
> +ADD_NAMESPACES(client)
> +ADD_VETH(client, client, br-int, "192.168.1.10/24", "f0:00:00:01:02:10")
> +ADD_NAMESPACES(server)
> +ADD_VETH(server, server, br-int, "192.168.1.20/24", "f0:00:00:01:02:20")
> +ADD_NAMESPACES(nf)
> +ADD_VETH(nf-p1, nf, br-int, "0", "f0:00:00:01:02:30")
> +ADD_VETH(nf-p2, nf, br-int, "0", "f0:00:00:01:02:40")
> +ADD_VETH(nf-p3, nf, br-int, "0", "f0:00:00:01:02:50")
> +ADD_VETH(nf-p4, nf, br-int, "0", "f0:00:00:01:02:60")
> +
> +check ovn-nbctl ls-add sw0
> +check ovn-nbctl lsp-add sw0 client \
> +    -- lsp-set-addresses client "f0:00:00:01:02:10 192.168.1.10/24"
> +check ovn-nbctl lsp-add sw0 server \
> +    -- lsp-set-addresses server "f0:00:00:01:02:20 192.168.1.20/24"
> +check ovn-nbctl ls-add nf
> +check ovn-nbctl lsp-add nf nf-p1
> +check ovn-nbctl lsp-add nf nf-p2
> +check ovn-nbctl lsp-add nf nf-p3
> +check ovn-nbctl lsp-add nf nf-p4
> +check ovn-nbctl set logical_switch_port nf-p1 
> options:receive_multicast=false options:lsp_learn_fdb=false \
> +                                              options:is-nf=true 
> options:nf-linked-port=nf-p2
> +check ovn-nbctl set logical_switch_port nf-p2 
> options:receive_multicast=false options:lsp_learn_fdb=false \
> +                                              options:is-nf=true 
> options:nf-linked-port=nf-p1
> +check ovn-nbctl set logical_switch_port nf-p3 
> options:receive_multicast=false options:lsp_learn_fdb=false \
> +                                              options:is-nf=true 
> options:nf-linked-port=nf-p4
> +check ovn-nbctl set logical_switch_port nf-p4 
> options:receive_multicast=false options:lsp_learn_fdb=false \
> +                                              options:is-nf=true 
> options:nf-linked-port=nf-p3
> +check ovn-nbctl lsp-add sw0 child-1 nf-p1 100
> +check ovn-nbctl lsp-add sw0 child-2 nf-p2 100
> +check ovn-nbctl lsp-add sw0 child-3 nf-p3 100
> +check ovn-nbctl lsp-add sw0 child-4 nf-p4 100
> +check ovn-nbctl set logical_switch_port child-1 
> options:receive_multicast=false options:lsp_learn_fdb=false \
> +                                                options:is-nf=true 
> options:nf-linked-port=child-2
> +check ovn-nbctl set logical_switch_port child-2 
> options:receive_multicast=false options:lsp_learn_fdb=false \
> +                                                options:is-nf=true 
> options:nf-linked-port=child-1
> +check ovn-nbctl set logical_switch_port child-3 
> options:receive_multicast=false options:lsp_learn_fdb=false \
> +                                                options:is-nf=true 
> options:nf-linked-port=child-4
> +check ovn-nbctl set logical_switch_port child-4 
> options:receive_multicast=false options:lsp_learn_fdb=false \
> +                                                options:is-nf=true 
> options:nf-linked-port=child-3
> +
> +AS_BOX([Test-1: Single NF without health check])
> +
> +check ovn-nbctl nf-add nf0 nf-p1 nf-p2
> +nf0_uuid=$(fetch_column nb:network_function _uuid name=nf0)
> +check ovn-nbctl nfg-add nfg0 1 inline nf0
> +
> +check ovn-nbctl pg-add pg0 server
> +check ovn-nbctl acl-add pg0 from-lport 1001 "inport == @pg0 && ip4.dst == 
> 192.168.1.10" allow-related nfg0
> +check ovn-nbctl acl-add pg0 to-lport 1002 "outport == @pg0 && ip4.src == 
> 192.168.1.10" allow-related nfg0
> +
> +check ovn-nbctl --wait=hv sync
> +
> +# configure bridge inside nf namespace for nf0 to simulate NF behaviour
> +NS_CHECK_EXEC([nf], [ip link add name br0 type bridge])
> +NS_CHECK_EXEC([nf], [ip link set dev nf-p1 master br0])
> +NS_CHECK_EXEC([nf], [ip link set dev nf-p2 master br0])
> +NS_CHECK_EXEC([nf], [ip link set dev br0 up])
> +
> +validate_traffic() {
> +    send_data=$1; recv_data=$2; pkt_cnt=$3;
> +    AT_CHECK([printf "$send_data\n" > /tmp/nffifo], [0], [dnl
> +])
> +
> +    if [[ -n "$recv_data" ]]; then
> +        OVS_WAIT_FOR_OUTPUT_UNQUOTED([cat output.txt], [0], [dnl
> +$recv_data
> +])
> +    else
> +        OVS_WAIT_FOR_OUTPUT([cat output.txt], [0], [dnl
> +])
> +    fi
> +
> +    : > output.txt
> +
> +    OVS_WAIT_UNTIL([
> +        total_pkts=$(cat pkt.pcap | wc -l)
> +        test ${total_pkts} -ge ${pkt_cnt}
> +    ])
> +}
> +
> +validate_single_nf_no_health_check() {
> +    client_ns=$1; server_ns=$2; sip=$3; direction=$4
> +
> +    # Start a TCP server
> +    NETNS_DAEMONIZE($server_ns, [server.py -i $sip -p 10000], [server.pid])
> +    on_exit 'kill $(cat server.pid)'
> +
> +    # Ensure TCP server is ready for connections
> +    OVS_WAIT_FOR_OUTPUT([cat output.txt], [0], [dnl
> +Server Ready
> +])
> +    : > output.txt
> +
> +    # Make a FIFO and send its output to a server
> +    mkfifo /tmp/nffifo
> +    on_exit 'rm -rf /tmp/nffifo'
> +
> +    NETNS_DAEMONIZE($client_ns, [client.py -f "/tmp/nffifo" -i $sip -p 
> 10000], [client.pid])
> +    on_exit 'kill $(cat client.pid)'
> +
> +    AS_BOX([$direction: Verify traffic forwarding through single NF without 
> health check])
> +
> +    # Capture traffic on nf0
> +    NS_CHECK_EXEC([nf], [tcpdump -l -nvv -i nf-p1 tcp > pkt.pcap 
> 2>tcpdump_err &])
> +    OVS_WAIT_UNTIL([grep "listening" tcpdump_err])
> +    on_exit 'kill $(pidof tcpdump)'
> +
> +    # Verify no service monitors exist when health check is not configured
> +    #AT_CHECK([ovn-sbctl list service_monitor | grep -v "^$"], [1])
> +    AT_CHECK([ovn-sbctl list service_monitor | wc -l], [0], [dnl
> +0
> +])
> +
> +    validate_traffic "test" "test" 5
> +
> +    kill $(pidof tcpdump)
> +    kill $(cat client.pid)
> +    kill $(cat server.pid)
> +    rm -f client.pid server.pid /tmp/nffifo
> +}
> +
> +AS_BOX([Verify inbound traffic forwarding through NF without health check])
> +validate_single_nf_no_health_check "client" "server" "192.168.1.20" "Inbound"
> +AS_BOX([Verify outbound traffic forwarding through NF without health check])
> +validate_single_nf_no_health_check "server" "client" "192.168.1.10" 
> "Outbound"
> +
> +OVN_CLEANUP_CONTROLLER([hv1])
> +
> +as ovn-sb
> +OVS_APP_EXIT_AND_WAIT([ovsdb-server])
> +
> +as ovn-nb
> +OVS_APP_EXIT_AND_WAIT([ovsdb-server])
> +
> +as northd
> +OVS_APP_EXIT_AND_WAIT([ovn-northd])
> +
> +as
> +OVS_TRAFFIC_VSWITCHD_STOP(["/.*error receiving.*/d
> +/failed to query port patch-.*/d
> +/.*terminating with signal 15.*/d"])
> +AT_CLEANUP
> +])

I also fixed up indentation in a few places.  I squashed in the following
incremental change to address all the small things above and then applied
the patch to main.

Regards,
Dumitru

diff --git a/controller/physical.c b/controller/physical.c
index e36e29c7d4..26f7124e53 100644
--- a/controller/physical.c
+++ b/controller/physical.c
@@ -516,18 +516,20 @@ put_remote_port_redirect_overlay(const struct 
sbrec_port_binding *binding,
 
 static const struct sbrec_port_binding *
 get_binding_network_function_linked_port(
-                    struct ovsdb_idl_index *sbrec_port_binding_by_name,
-                    const struct sbrec_port_binding *binding)
+    struct ovsdb_idl_index *sbrec_port_binding_by_name,
+    const struct sbrec_port_binding *binding)
 {
-    const char *nf_linked_name = smap_get(&binding->options,
-                                          "nf-linked-port");
+    const char *nf_linked_name = smap_get(&binding->options, "nf-linked-port");
     if (!nf_linked_name) {
         return NULL;
     }
+
     VLOG_DBG("get NF linked port_binding %s:%s",
              binding->logical_port, nf_linked_name);
-    const struct sbrec_port_binding *nf_linked_port = lport_lookup_by_name(
-        sbrec_port_binding_by_name, nf_linked_name);
+
+    const struct sbrec_port_binding *nf_linked_port =
+        lport_lookup_by_name(sbrec_port_binding_by_name, nf_linked_name);
+
     static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
     if (!nf_linked_port) {
         VLOG_INFO_RL(&rl, "Binding not found for nf-linked-port"
@@ -540,8 +542,9 @@ get_binding_network_function_linked_port(
                   binding->logical_port,  nf_linked_name);
         return NULL;
     }
-    const char *nf_linked_linked_name = smap_get(
-        &nf_linked_port->options, "nf-linked-port");
+
+    const char *nf_linked_linked_name = smap_get(&nf_linked_port->options,
+                                                 "nf-linked-port");
     if (!nf_linked_linked_name || strcmp(nf_linked_linked_name,
                                          binding->logical_port)) {
         VLOG_INFO_RL(&rl, "LSP name %s does not match linked_linked_name",
@@ -590,7 +593,7 @@ send_traffic_by_tunnel(const struct sbrec_port_binding 
*binding,
 
 static void
 put_redirect_overlay_to_source(const struct sbrec_port_binding *binding,
-                               int linked_ct,
+                               int linked_ct_zone,
                                const struct hmap *chassis_tunnels,
                                enum mf_field_id mff_ovn_geneve,
                                struct match *match,
@@ -629,7 +632,7 @@ put_redirect_overlay_to_source(const struct 
sbrec_port_binding *binding,
     match_set_reg_masked(match, MFF_LOG_FLAGS - MFF_REG0, 0, MLF_RECIRC);
 
     put_load(1, MFF_LOG_FLAGS, MLF_RECIRC_BIT, 1, ofpacts_p);
-    put_load(linked_ct, MFF_LOG_CT_ZONE, 0, 16, ofpacts_p);
+    put_load(linked_ct_zone, MFF_LOG_CT_ZONE, 0, 16, ofpacts_p);
 
     struct ofpact_conntrack *ct = ofpact_put_CT(ofpacts_p);
     ct->recirc_table = OFTABLE_LOCAL_OUTPUT;
@@ -698,19 +701,20 @@ put_redirect_overlay_to_source(const struct 
sbrec_port_binding *binding,
 
 static void
 put_redirect_overlay_to_source_from_nf_port(
-        const struct sbrec_port_binding *binding,
-        struct ovsdb_idl_index *sbrec_port_binding_by_name,
-        const struct hmap *chassis_tunnels,
-        const struct shash *ct_zones,
-        enum mf_field_id mff_ovn_geneve,
-        struct match *match,
-        struct ofpbuf *ofpacts_p,
-        struct ovn_desired_flow_table *flow_table)
-{
-    const struct sbrec_port_binding *linked_pb;
-    linked_pb = get_binding_network_function_linked_port(
-        sbrec_port_binding_by_name, binding);
+    const struct sbrec_port_binding *binding,
+    struct ovsdb_idl_index *sbrec_port_binding_by_name,
+    const struct hmap *chassis_tunnels,
+    const struct shash *ct_zones,
+    enum mf_field_id mff_ovn_geneve,
+    struct match *match,
+    struct ofpbuf *ofpacts_p,
+    struct ovn_desired_flow_table *flow_table)
+{
     static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+
+    const struct sbrec_port_binding *linked_pb =
+        get_binding_network_function_linked_port(sbrec_port_binding_by_name,
+                                                 binding);
     if (!linked_pb) {
         VLOG_INFO_RL(&rl, "Linked port not found for %s",
                      binding->logical_port);
@@ -727,8 +731,8 @@ put_redirect_overlay_to_source_from_nf_port(
                      binding->logical_port);
         return;
     }
-    VLOG_DBG_RL(&rl, "Both port zones found for NF port %s",
-                binding->logical_port);
+
+    VLOG_DBG("Both port zones found for NF port %s", binding->logical_port);
     put_redirect_overlay_to_source(binding, linked_zone.ct, chassis_tunnels,
                                    mff_ovn_geneve, match, ofpacts_p,
                                    flow_table);
@@ -1222,7 +1226,7 @@ static void
 clear_registers_for_nf_datapath(struct ofpbuf *ofpacts_p)
 {
     /* Clear all logical registers except MFF_LOG_TUN_OFPORT */
-    for (int i = 0; i < MFF_N_LOG_REGS; i++) {
+    for (size_t i = 0; i < MFF_N_LOG_REGS; i++) {
         if ((MFF_REG0 + i) != MFF_LOG_TUN_OFPORT) {
             /* Clear entire 32-bit register */
             put_load(0, MFF_REG0 + i, 0, 32, ofpacts_p);
@@ -2196,9 +2200,10 @@ consider_port_binding(const struct physical_ctx *ctx,
     struct ha_chassis_ordered *ha_ch_ordered;
     ha_ch_ordered = ha_chassis_get_ordered(binding->ha_chassis_group);
 
+    bool is_nf = smap_get_bool(&binding->options, "is-nf", false);
+
     /* Determine how the port is accessed. */
     enum access_type access_type = PORT_LOCAL;
-    bool is_nf = smap_get_bool(&binding->options, "is-nf", false);
     if (!ofport) {
         /* Enforce tunneling while we clone packets to additional chassis b/c
          * otherwise upstream switch won't flood the packet to both chassis. */
diff --git a/lib/logical-fields.c b/lib/logical-fields.c
index 5b4cac835c..c8bddcdc5f 100644
--- a/lib/logical-fields.c
+++ b/lib/logical-fields.c
@@ -235,13 +235,15 @@ ovn_init_symtab(struct shash *symtab)
                                     WR_CT_COMMIT);
     expr_symtab_add_subfield_scoped(symtab, "ct_label.tun_if", NULL,
                                     "ct_label["
-                                    OVN_CT_STR(OVN_CT_TUN_IF_BIT)
+                                        OVN_CT_STR(OVN_CT_TUN_IF_BIT)
                                     "]",
                                     WR_CT_COMMIT);
     expr_symtab_add_subfield_scoped(symtab, "ct_label.tun_if_id", NULL,
                                     "ct_label["
-                                    OVN_CT_STR(OVN_CT_TUN_IF_1ST_BIT) ".."
-                                    OVN_CT_STR(OVN_CT_TUN_IF_END_BIT) "]",
+                                        OVN_CT_STR(OVN_CT_TUN_IF_1ST_BIT)
+                                        ".."
+                                        OVN_CT_STR(OVN_CT_TUN_IF_END_BIT)
+                                    "]",
                                     WR_CT_COMMIT);
 
     expr_symtab_add_field(symtab, "ct_state", MFF_CT_STATE, NULL, false);
diff --git a/northd/northd.c b/northd/northd.c
index a3d1cb0557..e42d03c989 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -17956,30 +17956,31 @@ build_lswitch_stateful_nf(struct ovn_port *op,
     ds_clear(match);
 
     ds_put_cstr(actions,
-                 "ct_commit { "
-                    "ct_mark.blocked = 0; "
-                    "ct_mark.allow_established = " REGBIT_ACL_PERSIST_ID "; "
-                    "ct_label.acl_id = " REG_ACL_ID "; "
-                    "ct_label.tun_if_id = " REG_TUN_OFPORT "; }; next;");
-    ds_put_format(match,
-                  "outport == %s && " REGBIT_ACL_LABEL" == 0", op->json_key);
+                "ct_commit { "
+                   "ct_mark.blocked = 0; "
+                   "ct_mark.allow_established = " REGBIT_ACL_PERSIST_ID "; "
+                   "ct_label.acl_id = " REG_ACL_ID "; "
+                   "ct_label.tun_if_id = " REG_TUN_OFPORT "; "
+                "}; next;");
+    ds_put_format(match, "outport == %s && " REGBIT_ACL_LABEL" == 0",
+                  op->json_key);
     ovn_lflow_add(lflows, op->od, S_SWITCH_OUT_STATEFUL, 120,
                   ds_cstr(match), ds_cstr(actions), lflow_ref);
 
     ds_clear(actions);
     ds_clear(match);
-    ds_put_format(match,
-                  "outport == %s && " REGBIT_ACL_LABEL" == 1",
+    ds_put_format(match, "outport == %s && " REGBIT_ACL_LABEL" == 1",
                   op->json_key);
     ds_put_cstr(actions,
-                 "ct_commit { "
-                    "ct_mark.blocked = 0; "
-                    "ct_mark.allow_established = " REGBIT_ACL_PERSIST_ID "; "
-                    "ct_label.acl_id = " REG_ACL_ID "; "
-                    "ct_mark.obs_stage = " REGBIT_ACL_OBS_STAGE "; "
-                    "ct_mark.obs_collector_id = " REG_OBS_COLLECTOR_ID_EST "; "
-                    "ct_label.obs_point_id = " REG_OBS_POINT_ID_EST "; "
-                    "ct_label.tun_if_id = " REG_TUN_OFPORT "; }; next;");
+                "ct_commit { "
+                   "ct_mark.blocked = 0; "
+                   "ct_mark.allow_established = " REGBIT_ACL_PERSIST_ID "; "
+                   "ct_label.acl_id = " REG_ACL_ID "; "
+                   "ct_mark.obs_stage = " REGBIT_ACL_OBS_STAGE "; "
+                   "ct_mark.obs_collector_id = " REG_OBS_COLLECTOR_ID_EST "; "
+                   "ct_label.obs_point_id = " REG_OBS_POINT_ID_EST "; "
+                   "ct_label.tun_if_id = " REG_TUN_OFPORT "; "
+                "}; next;");
     ovn_lflow_add(lflows, op->od, S_SWITCH_OUT_STATEFUL, 120,
                   ds_cstr(match), ds_cstr(actions), lflow_ref);
 }
@@ -18158,8 +18159,8 @@ consider_network_function(struct lflow_table *lflows,
 
 static void
 build_network_function(const struct ovn_datapath *od,
-                       const struct ls_port_group_table *ls_pgs,
                        struct lflow_table *lflows,
+                       const struct ls_port_group_table *ls_pgs,
                        struct lflow_ref *lflow_ref)
 {
     unsigned long *nfg_ingress_bitmap = bitmap_allocate(MAX_OVN_NF_GROUP_IDS);
@@ -18291,7 +18292,7 @@ build_lswitch_and_lrouter_iterate_by_ls(struct 
ovn_datapath *od,
     build_mirror_default_lflow(od, lsi->lflows);
     build_lswitch_lflows_pre_acl_and_acl(od, lsi->lflows,
                                          lsi->meter_groups, NULL);
-    build_network_function(od, lsi->ls_port_groups, lsi->lflows, NULL);
+    build_network_function(od, lsi->lflows, lsi->ls_port_groups, NULL);
     build_fwd_group_lflows(od, lsi->lflows, NULL);
     build_lswitch_lflows_admission_control(od, lsi->lflows, NULL);
     build_lswitch_learn_fdb_od(od, lsi->lflows, NULL);
@@ -19376,8 +19377,9 @@ lflow_handle_ls_stateful_changes(struct ovsdb_idl_txn 
*ovnsb_txn,
                                 lflow_input->features,
                                 lflows,
                                 lflow_input->sbrec_acl_id_table);
-        build_network_function(od, lflow_input->ls_port_groups,
-                               lflows, ls_stateful_rec->lflow_ref);
+        build_network_function(od, lflows,
+                               lflow_input->ls_port_groups,
+                               ls_stateful_rec->lflow_ref);
 
         /* Sync the new flows to SB. */
         bool handled = lflow_ref_sync_lflows(
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index 0b937c5646..1496b9ff04 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -18336,10 +18336,10 @@ AT_CHECK(
 
     AT_CHECK([grep "ls_out_stateful" sw0flows | ovn_strip_lflows], [0], [dnl
   table=??(ls_out_stateful    ), priority=0    , match=(1), action=(next;)
-  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && 
reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; 
ct_mark.allow_established = reg0[[20]]; ct_label.acl_id = reg2[[16..31]]; 
ct_label.network_function_group = 0; ct_label.network_function_group_id = 0; }; 
next;)
-  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && 
reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; 
ct_mark.allow_established = reg0[[20]]; ct_mark.obs_stage = reg8[[19..20]]; 
ct_mark.obs_collector_id = reg8[[8..15]]; ct_label.obs_point_id = reg9; 
ct_label.acl_id = reg2[[16..31]]; ct_label.network_function_group = 0; 
ct_label.network_function_group_id = 0; }; next;)
-  table=??(ls_out_stateful    ), priority=110  , match=(reg0[[1]] == 1 && 
reg0[[13]] == 0 && reg8[[21]] == 1), action=(ct_commit { ct_mark.blocked = 0; 
ct_mark.allow_established = reg0[[20]]; ct_label.acl_id = reg2[[16..31]]; 
ct_label.network_function_group = 1; ct_label.network_function_group_id = 
reg0[[22..29]]; }; next;)
-  table=??(ls_out_stateful    ), priority=110  , match=(reg0[[1]] == 1 && 
reg0[[13]] == 1 && reg8[[21]] == 1), action=(ct_commit { ct_mark.blocked = 0; 
ct_mark.allow_established = reg0[[20]]; ct_mark.obs_stage = reg8[[19..20]]; 
ct_mark.obs_collector_id = reg8[[8..15]]; ct_label.obs_point_id = reg9; 
ct_label.acl_id = reg2[[16..31]]; ct_label.network_function_group = 1; 
ct_label.network_function_group_id = reg0[[22..29]]; }; next;)
+  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && 
reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; 
ct_mark.allow_established = reg0[[20]]; ct_label.acl_id = reg2[[16..31]]; 
ct_label.nf_group = 0; ct_label.nf_group_id = 0; }; next;)
+  table=??(ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1 && 
reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; 
ct_mark.allow_established = reg0[[20]]; ct_mark.obs_stage = reg8[[19..20]]; 
ct_mark.obs_collector_id = reg8[[8..15]]; ct_label.obs_point_id = reg9; 
ct_label.acl_id = reg2[[16..31]]; ct_label.nf_group = 0; ct_label.nf_group_id = 
0; }; next;)
+  table=??(ls_out_stateful    ), priority=110  , match=(reg0[[1]] == 1 && 
reg0[[13]] == 0 && reg8[[21]] == 1), action=(ct_commit { ct_mark.blocked = 0; 
ct_mark.allow_established = reg0[[20]]; ct_label.acl_id = reg2[[16..31]]; 
ct_label.nf_group = 1; ct_label.nf_group_id = reg0[[22..29]]; }; next;)
+  table=??(ls_out_stateful    ), priority=110  , match=(reg0[[1]] == 1 && 
reg0[[13]] == 1 && reg8[[21]] == 1), action=(ct_commit { ct_mark.blocked = 0; 
ct_mark.allow_established = reg0[[20]]; ct_mark.obs_stage = reg8[[19..20]]; 
ct_mark.obs_collector_id = reg8[[8..15]]; ct_label.obs_point_id = reg9; 
ct_label.acl_id = reg2[[16..31]]; ct_label.nf_group = 1; ct_label.nf_group_id = 
reg0[[22..29]]; }; next;)
   table=??(ls_out_stateful    ), priority=120  , match=(outport == "sw0-nf-p1" 
&& reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; 
ct_mark.allow_established = reg0[[20]]; ct_label.acl_id = reg2[[16..31]]; 
ct_label.tun_if_id = reg5[[16..31]]; }; next;)
   table=??(ls_out_stateful    ), priority=120  , match=(outport == "sw0-nf-p1" 
&& reg0[[13]] == 1), action=(ct_commit { ct_mark.blocked = 0; 
ct_mark.allow_established = reg0[[20]]; ct_label.acl_id = reg2[[16..31]]; 
ct_mark.obs_stage = reg8[[19..20]]; ct_mark.obs_collector_id = reg8[[8..15]]; 
ct_label.obs_point_id = reg9; ct_label.tun_if_id = reg5[[16..31]]; }; next;)
   table=??(ls_out_stateful    ), priority=120  , match=(outport == "sw0-nf-p2" 
&& reg0[[13]] == 0), action=(ct_commit { ct_mark.blocked = 0; 
ct_mark.allow_established = reg0[[20]]; ct_label.acl_id = reg2[[16..31]]; 
ct_label.tun_if_id = reg5[[16..31]]; }; next;)
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index 0cfe727838..6a11c43207 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -18796,15 +18796,14 @@ OVS_TRAFFIC_VSWITCHD_START()
 
 ADD_BR([br-int])
 
-# Set external-ids in br-int needed for ovn-controller
-ovs-vsctl \
-        -- set Open_vSwitch . external-ids:system-id=hv1 \
-        -- set Open_vSwitch . 
external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
-        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
-        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
-        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+# Set external-ids in br-int needed for ovn-controller.
+check ovs-vsctl \
+    -- set Open_vSwitch . external-ids:system-id=hv1 \
+    -- set Open_vSwitch . 
external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+    -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+    -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+    -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
 
-# Start ovn-controller
 start_daemon ovn-controller
 
 ADD_NAMESPACES(client)
@@ -18931,6 +18930,7 @@ Server Ready
 
 AS_BOX([Verify inbound traffic forwarding through NF without health check])
 validate_single_nf_no_health_check "client" "server" "192.168.1.20" "Inbound"
+
 AS_BOX([Verify outbound traffic forwarding through NF without health check])
 validate_single_nf_no_health_check "server" "client" "192.168.1.10" "Outbound"
 


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

Reply via email to