On 7/13/26 6:06 AM, Naveen Yerramneni wrote:
> Implement vtap mode in northd where traffic is cloned to the NF port
> while the original packet continues to its destination.
> 
> - Generate mirror flows that clone packets to NF port
> - Determine NF health from port binding status (no health probes)
> - Validate that health_check requires both inport and outport
> 
> Note:
> ----
> For inline NF health status, updated the code to consider
> port binding state along with service monitor health.
> 
> Signed-off-by: Naveen Yerramneni <[email protected]>
> Acked-by: Sragdhara Datta Chaudhuri <[email protected]>
> Acked-by: Aditya Mehakare <[email protected]>
> Assisted-by: Claude Opus 4.7, Cursor
> ---

Hi Naveen,

Thanks for the new revision!

>  Documentation/ref/ovn-logical-flows.7.rst | 294 ++++++++-------
>  NEWS                                      |   5 +
>  northd/northd.c                           | 421 +++++++++++++++++++---
>  tests/ovn-northd.at                       | 212 ++++++++++-
>  tests/ovn.at                              | 250 ++++++++++++-
>  tests/system-ovn.at                       | 285 ++++++++++++++-
>  6 files changed, 1268 insertions(+), 199 deletions(-)
> 
> diff --git a/Documentation/ref/ovn-logical-flows.7.rst 
> b/Documentation/ref/ovn-logical-flows.7.rst
> index 95f7e0d7e..2778b3633 100644
> --- a/Documentation/ref/ovn-logical-flows.7.rst
> +++ b/Documentation/ref/ovn-logical-flows.7.rst
> @@ -866,28 +866,34 @@ from the group and overwrites ``reg0[22..29]`` with the 
> specific ``id`` of a
>  this NF ID to redirect packets to the appropriate network function port. In 
> the
>  future, this stage will be extended to support network function load 
> balancing.
>  
> -- For each network_function_group *id* with an active network function, a
> -  priority-99 flow matches ``reg8[21] == 1 && reg8[22] == 1 && reg0[22..29] 
> ==
> -  id`` and sets ``reg0[22..29] = nf_id; next;`` where *nf_id* is the ID of 
> the
> -  active network function. This prepares request packets that matched a 
> ``from-
> -  lport`` ACL with network_function_group for redirection in the subsequent
> -  Network Function table.
> -
> -- For each network function group with *id* that has ``fallback`` set to 
> ``fail-
> -  open``, a priority-10 flow matches ``reg8[21] == 1 && reg8[22] == 1 &&
> -  reg0[22..29] == id`` and sets ``reg8[21] = 0; reg0[22..29] = 0; next;``. 
> This
> -  clears both the NF enabled bit and the NF group ID, allowing packets to
> -  continue processing through the pipeline without network function 
> redirection
> -  when no active network function is available (fail-open behavior).
> -
> -- A priority-1 flow matches ``reg8[21] == 1 && reg8[22] == 1`` and sets
> -  ``reg0[22..29] = 0; next;``. This is a catch-all flow for network function
> -  groups with ``fallback`` set to ``fail-close`` (or default) when no active
> -  network function is available. It clears only the NF group ID, leaving the 
> NF
> -  enabled bit set. These packets will be dropped by the priority-1 drop rule 
> in
> -  the subsequent Network Function table (fail-close behavior).
> +- In inline, vtap mode: For each network_function_group *id* with an active
> +  network function, a priority-99 flow matches ``reg8[21] == 1 && reg8[22] 
> == 1
> +  && reg0[22..29] == id`` and sets ``reg0[22..29] = nf_id; next;`` where
> +  *nf_id* is the ID of the active network function. This prepares request
> +  packets that matched a ``from-lport`` ACL with network_function_group for
> +  redirection (inline) or mirroring (vtap) in the subsequent Network Function
> +  table.
>  
> -- A priority-0 flow that simply moves traffic to the next table.
> +- In inline mode: For each network function group with *id* that has
> +  ``fallback`` set to ``fail-open``, a priority-10 flow matches ``reg8[21] 
> == 1
> +  && reg8[22] == 1 && reg0[22..29] == id`` and sets ``reg8[21] = 0;
> +  reg0[22..29] = 0; next;``.
> +  In vtap mode: A priority-10 flow with the same match and action is always
> +  added (vtap does not support fail-close). This clears both the NF enabled 
> bit
> +  and the NF group ID, allowing packets to continue when no active network
> +  function is available (fail-open behavior).
> +
> +- In inline, vtap mode: A priority-1 flow matches ``reg8[21] == 1 && reg8[22]
> +  == 1`` and sets ``reg0[22..29] = 0; next;``. This is a catch-all for when 
> no
> +  active network function is available and no higher-priority flow matched. 
> For
> +  inline groups with ``fallback`` set to ``fail-close`` (or default) this
> +  leaves the NF enabled bit set so the packet is dropped by the priority-1
> +  drop rule in the subsequent Network Function table (fail-close behavior).
> +  For vtap groups this flow is superseded by the priority-10 fail-open flow
> +  above and is not reached when an NFG is configured; it acts as a safety 
> net.
> +
> +- In inline, vtap mode: A priority-0 flow that simply moves traffic to the 
> next
> +  table.
>  
>  .. _ls-in-24:
>  
> @@ -904,13 +910,13 @@ Ingress Table 24: Stateful
>    connection tracker using ``ct_commit; next;`` action based on a hint 
> provided
>    by the previous tables (with a match for ``reg0[1] == 1 && reg0[13] == 
> 0``).
>  
> -- Corresponding to each of the two priority 100 flows above, a priority 110 
> flow
> -  is added, which has the following extra match and action, but otherwise
> -  identical to the priority 100 flow. Match: ``reg8[21] == 1`` (packet 
> matched
> -  an ACL with ``network_function_group`` set) Action: ``ct_label.nf = 1;
> -  ct_label.nf_id = reg0[22..29];`` This is to commit the network_function
> -  information in conntrack so that the response and related packets can be
> -  redirected to it as well.
> +- In inline, vtap mode: Corresponding to each of the two priority 100 flows
> +  above, a priority 110 flow is added, which has the following extra match 
> and
> +  action, but otherwise identical to the priority 100 flow. Match: ``reg8[21]
> +  == 1`` (packet matched an ACL with ``network_function_group`` set). Action:
> +  ``ct_label.nf = 1; ct_label.nf_id = reg0[22..29];`` This commits the
> +  network_function information in conntrack so that response and related
> +  packets can be redirected or mirrored to it as well.
>  
>  - A priority-0 flow that simply moves traffic to the next table.
>  
> @@ -940,44 +946,54 @@ If the network function ports are not present on this 
> logical switch, their
>  child ports (if any) are used. In the statements below, network function 
> ports
>  refer to either the parent or child ports as applicable to this logical 
> switch.
>  
> -- For each network_function port *P*, a priority-100 flow is added that 
> matches
> -  ``inport == P`` and advances packets to the next table. Thus packets coming
> -  from network function are not subject to redirection. This flow also sets
> -  ``reg5[16..31] = ct_label.tun_if_id``. This is used for tunneling packet to
> -  originating host in case of cross host traffic redirection for VLAN subnet.
> -  This ct_label field stores the openflow tunnel interface id of the 
> originating
> -  host for this connection and gets populated in egress :ref:`Stateful
> -  <ls-out-12>` table.
> -
> -- For each active network function with *id* that is referenced in a network
> -  function group, a priority-99 flow matches ``reg8[21] == 1 && reg8[22] == 
> 1 &&
> -  reg0[22..29] == id`` and sets ``outport=P; output;`` where *P* is the
> -  ``inport`` of that network function. This redirects request packets for 
> flows
> -  matching ``from-lport`` ACLs with network_function_group to the specific
> -  network function selected by the Pre Network Function stage.
> -
> -- For each active network function with *id* that is referenced in a network
> -  function group, a priority-99 rule matches ``reg8[21] == 1 && reg8[22] == 
> 0 &&
> -  ct_label.nf_id == id`` and takes identical action as above. This redirects
> -  response and related packets for ``to-lport`` ACLs to the same network
> -  function that handled the request, using the NF ID stored in the connection
> -  tracking label.
> -
> -- In each of the above cases, when the same packet comes out unchanged 
> through
> -  the other port of the network_function, it would match the priority 100 
> flow
> -  and be forwarded to the next table.
> -
> -- One priority-100 rule to skip redirection of multicast packets that hit a
> -  network_function ACL. Match on ``reg8[21] == 1 && eth.mcast`` and action 
> is to
> -  advance to the next table.
> +- In inline: For each network_function port *P*, a priority-100 flow matches
> +  ``inport == P`` and advances packets to the next table (packets from the
> +  network function are not subject to redirection). This flow also sets
> +  ``reg5[16..31] = ct_label.tun_if_id`` for cross host traffic redirection 
> for
> +  VLAN subnet; the tunnel id is populated in egress :ref:`Stateful 
> <ls-out-12>`
> +  table.
>  
> -- One priority-1 rule that checks ``reg8[21] == 1``, and drops such packets.
> -  This is to address the case where a packet hit an ACL with network function
> -  but the network function does not have ports or child ports on this logical
> -  switch.
> +- In inline: For each active network function with *id* that is referenced 
> in a
> +  network function group, a priority-99 flow matches ``reg8[21] == 1 &&
> +  reg8[22] == 1 && reg0[22..29] == id`` and sets ``outport=P; output;`` where
> +  *P* is the ``inport`` of that network function. This redirects request
> +  packets for flows matching ``from-lport`` ACLs with network_function_group
> +  to the specific network function selected by the Pre Network Function 
> stage.
>  
> -- One priority-0 fallback flow that matches all packets and advances to the 
> next
> -  table.
> +- In vtap mode: For each active network function with *id*, a priority-99
> +  forward flow matches ``reg8[21] == 1 && reg8[22] == 1 && reg0[22..29] == 
> id``
> +  and sets ``clone { outport = P; output; }; next;`` where *P* is the
> +  ``inport`` of that network function. A copy is sent to the NF port while
> +  the original packet continues (mirroring; only inport is used, outport is
> +  not supported).
> +
> +- In inline: For each active network function with *id* that is referenced 
> in a
> +  network function group, a priority-99 rule matches ``reg8[21] == 1 &&
> +  reg8[22] == 0 && ct_label.nf_id == id`` and takes identical action as 
> above.
> +  This redirects response and related packets for ``to-lport`` ACLs to the
> +  same network function that handled the request.
> +
> +- In vtap mode: A priority-99 reverse flow matches ``reg8[21] == 1 && 
> reg8[22]
> +  == 0 && ct_label.nf_id == id`` and sets ``clone { outport = P; output; };
> +  next;`` to mirror response/related packets to the same NF.
> +
> +- In inline: In each of the above cases, when the same packet comes out
> +  unchanged through the other port of the network_function, it would match 
> the
> +  priority 100 flow and be forwarded to the next table.
> +
> +- In vtap mode: A priority-100 flow matches ``inport == P`` (packets from the
> +  NF port) and drops them.
> +
> +- In inline, vtap mode: One priority-100 rule to skip redirection/mirroring 
> of
> +  multicast packets that hit a network_function ACL. Match on ``reg8[21] == 1
> +  && eth.mcast`` and action is to advance to the next table.
> +
> +- In inline: One priority-1 rule that checks ``reg8[21] == 1``, and drops 
> such
> +  packets when the network function does not have ports or child ports on 
> this
> +  logical switch.
> +
> +- In inline, vtap mode: One priority-0 fallback flow that matches all packets
> +  and advances to the next table.
>  
>  .. _ls-in-26:
>  
> @@ -1812,28 +1828,32 @@ The subsequent Network Function table uses this NF ID 
> to redirect packets to the
>  appropriate network function port. In the future, this stage will be 
> extended to
>  support network function load balancing.
>  
> -- For each network function group with *id* that has an active network 
> function,
> -  a priority-99 flow matches ``reg8[21] == 1 && reg8[22] == 1 && 
> reg0[22..29] ==
> -  id`` and sets ``reg0[22..29] = nf_id; next;`` where *nf_id* is the ``id`` 
> of
> -  the active ``Network_Function`` selected from the group. This prepares 
> request
> -  packets that matched a ``to-lport`` ACL with network_function_group for
> -  redirection in the subsequent Network Function table.
> -
> -- For each network function group with *id* that has ``fallback`` set to 
> ``fail-
> -  open``, a priority-10 flow matches ``reg8[21] == 1 && reg8[22] == 1 &&
> -  reg0[22..29] == id`` and sets ``reg8[21] = 0; reg0[22..29] = 0; next;``. 
> This
> -  clears both the NF enabled bit and the NF group ID, allowing packets to
> -  continue processing through the pipeline without network function 
> redirection
> -  when no active network function is available (fail-open behavior).
> -
> -- A priority-1 flow matches ``reg8[21] == 1 && reg8[22] == 1`` and sets
> -  ``reg0[22..29] = 0; next;``. This is a catch-all flow for network function
> -  groups with ``fallback`` set to ``fail-close`` (or default) when no active
> -  network function is available. It clears only the NF group ID, leaving the 
> NF
> -  enabled bit set. These packets will be dropped by the priority-1 drop rule 
> in
> -  the subsequent Network Function table (fail-close behavior).
> -
> -- A priority-0 flow that simply moves traffic to the next table.
> +- In inline, vtap mode: For each network function group with *id* that has an
> +  active network function, a priority-99 flow matches ``reg8[21] == 1 &&
> +  reg8[22] == 1 && reg0[22..29] == id`` and sets ``reg0[22..29] = nf_id;
> +  next;`` where *nf_id* is the ``id`` of the active ``Network_Function``
> +  selected from the group. This prepares request packets that matched a
> +  ``to-lport`` ACL with network_function_group for redirection (inline) or
> +  mirroring (vtap) in the subsequent Network Function table.
> +
> +- In inline: For each network function group with *id* that has ``fallback``
> +  set to ``fail-open``, a priority-10 flow matches ``reg8[21] == 1 && 
> reg8[22]
> +  == 1 && reg0[22..29] == id`` and sets ``reg8[21] = 0; reg0[22..29] = 0;
> +  next;``. In vtap mode: A priority-10 flow with the same match and action is
> +  always added. This clears both the NF enabled bit and the NF group ID when
> +  no active network function is available (fail-open behavior).
> +
> +- In inline, vtap mode: A priority-1 flow matches ``reg8[21] == 1 && reg8[22]
> +  == 1`` and sets ``reg0[22..29] = 0; next;``. This is a catch-all for when 
> no
> +  active network function is available and no higher-priority flow matched. 
> For
> +  inline groups with ``fallback`` set to ``fail-close`` (or default) this
> +  leaves the NF enabled bit set so the packet is dropped by the priority-1
> +  drop rule in the subsequent Network Function table (fail-close behavior).
> +  For vtap groups this flow is superseded by the priority-10 fail-open flow
> +  above and is not reached when an NFG is configured; it acts as a safety 
> net.
> +
> +- In inline, vtap mode: A priority-0 flow that simply moves traffic to the 
> next
> +  table.
>  
>  .. _ls-out-12:
>  
> @@ -1845,17 +1865,15 @@ are no rules added for load balancing new 
> connections. When
>  ``enable-stateless-acl-with-lb`` is enabled, new stateless connections bypass
>  connection tracking.
>  
> -- A priority 120 flow is added for each network function port *P* that is
> -  identical to the priority 100 flow except for additional match ``outport ==
> -  P`` and additional action  ``ct_label.tun_if_id = reg5[16..31]``. In case
> -  packets redirected by network function logic gets tunneled from host1 to 
> host2
> -  where the network function port resides, host2's physical table 0 populates
> -  reg5[16..31] with the openflow tunnel interface id on which the packet was
> -  received. This priority 120 flow commits the tunnel id to the ct_label. 
> That
> -  way, when the same packet comes out of the other port of the network 
> function
> -  it can retrieve this information from the peer port's CT entry and tunnel 
> the
> -  packet back to host1. This is required to make cross host traffic 
> redirection
> -  work for VLAN subnet.
> +- In inline: A priority 120 flow is added for each network function port *P*
> +  that is identical to the priority 100 flow except for additional match
> +  ``outport == P`` and additional action ``ct_label.tun_if_id = 
> reg5[16..31]``.
> +  In case packets redirected by network function logic get tunneled from 
> host1
> +  to host2 where the network function port resides, host2's physical table 0
> +  populates reg5[16..31] with the openflow tunnel interface id. This flow
> +  commits the tunnel id to ct_label so the packet can be tunneled back to 
> host1
> +  when it comes out of the other port of the network function (required for
> +  cross host traffic redirection for VLAN subnet).
>  
>  .. _ls-out-13:
>  
> @@ -1870,38 +1888,60 @@ packets are handled in the ingress pipeline, but 
> corresponding response/related
>  packets for those flows are redirected here using the network function ID 
> stored
>  in ``ct_label.nf_id`` during request processing.
>  
> -- Similar to ingress :ref:`Network Function <ls-in-25>` a priority-100 flow 
> is
> -  added for each network_function port, that matches the inport with the 
> network
> -  function port and advances the packet to the next table.
> +- In inline: Similar to ingress :ref:`Network Function <ls-in-25>`, a
> +  priority-100 flow is added for each network_function port that matches the
> +  outport with the network function port and advances the packet to the next
> +  table.
>  
> -- For each active network function with *id* that is referenced in a network
> -  function group, a priority-99 flow matches ``reg8[21] == 1 && reg8[22] == 
> 1 &&
> -  reg0[22..29] == id`` and sets ``outport=P; reg8[23] = 1;
> -  next(pipeline=ingress, table=T)`` where *P* is the ``outport`` of that 
> network
> -  function and *T* is the ingress table :ref:`Destination Lookup <ls-in-33>`.
> -  This redirects request packets matching ``to-lport`` ACLs with
> +- In inline: For each active network function with *id* that is referenced 
> in a
> +  network function group, a priority-99 flow matches ``reg8[21] == 1 &&
> +  reg8[22] == 1 && reg0[22..29] == id`` and sets ``outport=P; reg8[23] = 1;
> +  next(pipeline=ingress, table=T)`` where *P* is the ``outport`` of
> +  that network function and *T* is the ingress table :ref:`Destination Lookup
> +  <ls-in-33>`. This redirects request packets matching ``to-lport`` ACLs with
>    network_function_group to the specific network function selected by the Pre
> -  Network Function stage. The packets are injected back to the ingress 
> pipeline
> -  from where they get sent out, skipping any further lookup because of
> -  ``reg8[23]``.
> -
> -- For each active network function with *id* that is referenced in a network
> -  function group, a priority-99 rule matches ``reg8[21] == 1 && reg8[22] == 
> 0 &&
> -  ct_label.nf_id == id`` and takes identical action as above. This redirects
> -  response and related packets for ``from-lport`` ACLs to the same network
> -  function that handled the request, using the NF ID stored in the connection
> -  tracking label.
> -
> -- In each of the above cases, when the same packet comes out unchanged 
> through
> -  the other port of the network_function, it would match the priority 100 
> flow
> -  and be forwarded to the next table.
> -
> -- One priority-100 multicast match flow same as ingress :ref:`Network 
> Function
> -  <ls-in-25>`.
> -
> -- One priority-1 flow same as ingress :ref:`Network Function <ls-in-25>`.
> -
> -- One priority-0 flow same as ingress :ref:`Network Function <ls-in-25>`.
> +  Network Function stage.
> +
> +- In vtap mode: For each active network function with *id*, a priority-99
> +  forward flow matches ``reg8[21] == 1 && reg8[22] == 1 && reg0[22..29] == 
> id``
> +  and sets ``clone { outport = P; reg8[23] = 1; next(pipeline=ingress,
> +  table=T); }; next;`` where *P* is the ``inport`` of that network function
> +  and *T* is the ingress table :ref:`Destination Lookup <ls-in-33>`
> +  (mirroring; only inport is used).
> +
> +- In inline: For each active network function with *id* that is referenced 
> in a
> +  network function group, a priority-99 rule matches ``reg8[21] == 1 &&
> +  reg8[22] == 0 && ct_label.nf_id == id`` and takes identical action as 
> above.
> +  This redirects response and related packets for ``from-lport`` ACLs to the
> +  same network function that handled the request.
> +
> +- In vtap mode: A priority-99 reverse flow matches ``reg8[21] == 1 && 
> reg8[22]
> +  == 0 && ct_label.nf_id == id`` and sets ``clone { outport = P; reg8[23] = 
> 1;
> +  next(pipeline=ingress, table=T); }; next;`` where *T* is the ingress table
> +  :ref:`Destination Lookup <ls-in-33>`. This loops the mirrored response/
> +  related packets back to the ingress pipeline so they reach the NF port even
> +  if it resides on a remote chassis.
> +
> +- In inline: In each of the above cases, when the same packet comes out
> +  unchanged through the other port of the network_function, it would match 
> the
> +  priority 100 flow and be forwarded to the next table.
> +
> +- In vtap mode: A priority-100 flow matches ``outport == P`` (packets to the 
> NF
> +  port) and advances to the next table so packets to the NF are not mirrored
> +  again.
> +
> +- In vtap mode: In egress Pre ACL table, a priority-110 flow matches ``ip &&
> +  outport == P`` with action ``ct_clear; next;`` for the vtap NF port so
> +  packets toward the NF are not committed to conntrack.
> +
> +- In inline, vtap mode: One priority-100 multicast match flow same as ingress
> +  :ref:`Network Function <ls-in-25>`.
> +
> +- In inline, vtap mode: One priority-1 flow same as ingress :ref:`Network
> +  Function <ls-in-25>`.
> +
> +- In inline, vtap mode: One priority-0 flow same as ingress :ref:`Network
> +  Function <ls-in-25>`.
>  
>  .. _ls-out-14:
>  
> diff --git a/NEWS b/NEWS
> index 9fc75dfef..c51e85c68 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -68,6 +68,11 @@ Post v26.03.0
>       (type 11) and Parameter Problem (type 12) - generated by an external
>       router are un-NATed correctly.  This makes Path MTU discovery and
>       traceroute work through stateless NAT.
> +   - Add vtap mode support for Network Function. In vtap mode, traffic 
> matching
> +     ACLs is mirrored to the network function while continuing to flow to the
> +     original destination. This enables passive monitoring use cases where
> +     network functions can observe traffic without being inline in
> +     the data path.
>  
>  OVN v26.03.0 - xxx xx xxxx
>  --------------------------
> diff --git a/northd/northd.c b/northd/northd.c
> index e58154b85..2315b99b3 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -3292,6 +3292,58 @@ create_or_get_service_mon(struct ovsdb_idl_txn 
> *ovnsb_txn,
>      return mon_info;
>  }
>  
> +enum nf_port_binding_state {
> +    NF_PORT_STATE_UNKNOWN,
> +    NF_PORT_STATE_CHASSIS_INVALID,
> +    NF_PORT_STATE_DOWN,
> +    NF_PORT_STATE_UP
> +};
> +
> +static enum nf_port_binding_state
> +network_function_port_binding_state(const char **ports, size_t n_ports,
> +                                    struct hmap *ls_ports,
> +                                    const char **chassis_name_pptr)
> +{
> +    const char *chassis_name = NULL;
> +    size_t n_port_up = 0;
> +
> +    for (size_t i = 0; i < n_ports; i++) {
> +        const char *port = ports[i];
> +        struct ovn_port *op = ovn_port_find(ls_ports, port);
> +        if (op == NULL) {
> +            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
> +            VLOG_ERR_RL(&rl, "NetworkFunction: skip health check, port:%s "
> +                        "not found",  port);
> +            return NF_PORT_STATE_UNKNOWN;
> +        }
> +        if (op->sb && op->sb->chassis) {
> +            if (chassis_name == NULL) {
> +                chassis_name = op->sb->chassis->name;
> +            } else if (strcmp(chassis_name, op->sb->chassis->name)) {
> +                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 
> 1);
> +                VLOG_ERR_RL(&rl, "NetworkFunction: chassis mismatch "
> +                            "for port:%s chassis:%s peer_port_chassis:%s",
> +                            port, op->sb->chassis->name, chassis_name);
> +                return NF_PORT_STATE_CHASSIS_INVALID;
> +            }
> +            if (op->sb->n_up && op->sb->up[0]) {
> +                n_port_up++;
> +            }
> +        } else {
> +            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
> +            VLOG_ERR_RL(&rl, "NetworkFunction: chassis not set for port:%s",
> +                        port);
> +            return NF_PORT_STATE_CHASSIS_INVALID;
> +        }
> +    }
> +
> +    if (chassis_name_pptr) {
> +        *chassis_name_pptr = chassis_name;
> +    }
> +
> +    return n_port_up == n_ports ? NF_PORT_STATE_UP : NF_PORT_STATE_DOWN;
> +}
> +
>  static void
>  ovn_nf_svc_create(struct ovsdb_idl_txn *ovnsb_txn,
>                    const struct nbrec_network_function *nbrec_nf,
> @@ -3310,29 +3362,19 @@ ovn_nf_svc_create(struct ovsdb_idl_txn *ovnsb_txn,
>      }
>  
>      const char *ports[] = {nbrec_nf->outport->name, nbrec_nf->inport->name};
> +    size_t n_ports = ARRAY_SIZE(ports);
>      const char *chassis_name = NULL;
> -    bool port_up = true;
>  
> -    for (size_t i = 0; i < ARRAY_SIZE(ports); i++) {
> +    for (size_t i = 0; i < n_ports; i++) {
>          const char *port = ports[i];
>          sset_add(svc_monitor_lsps, port);
> -        struct ovn_port *op = ovn_port_find(ls_ports, port);
> -        if (op == NULL) {
> -            VLOG_ERR_RL(&rl, "NetworkFunction: skip health check, port:%s "
> -                             "not found",  port);
> -            return;
> -        }
> +    }
>  
> -        if (op->sb->chassis) {
> -            if (chassis_name == NULL) {
> -                chassis_name = op->sb->chassis->name;
> -            } else if (strcmp(chassis_name, op->sb->chassis->name)) {
> -                 VLOG_ERR_RL(&rl, "NetworkFunction: chassis mismatch "
> -                                  "chassis:%s port:%s\n",
> -                             op->sb->chassis->name, port);
> -            }
> -        }
> -        port_up = port_up && (op->sb->n_up && op->sb->up[0]);
> +    enum nf_port_binding_state port_state =
> +        network_function_port_binding_state(ports, n_ports, ls_ports,
> +                                            &chassis_name);
> +    if (port_state == NF_PORT_STATE_UNKNOWN) {
> +        return;
>      }
>  
>      struct service_monitor_info *mon_info =
> @@ -3372,7 +3414,7 @@ ovn_nf_svc_create(struct ovsdb_idl_txn *ovnsb_txn,
>                                       svc_global_addresses->ip_dst);
>      }
>  
> -    if (!port_up && mon_info->sbrec_mon->status
> +    if (port_state != NF_PORT_STATE_UP && mon_info->sbrec_mon->status
>          && !strcmp(mon_info->sbrec_mon->status, "online")) {
>          sbrec_service_monitor_set_status(mon_info->sbrec_mon, "offline");
>      }
> @@ -3855,6 +3897,16 @@ build_svc_monitors_data(
>      NBREC_NETWORK_FUNCTION_TABLE_FOR_EACH (nbrec_nf,
>                              nbrec_network_function_table) {
>          if (nbrec_nf->health_check) {
> +            /* For Network Function, health check requires both
> +             * inport and outport to be set.
> +             */
> +            if (!nbrec_nf->inport || !nbrec_nf->outport) {
> +                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 
> 1);
> +                VLOG_WARN_RL(&rl, "NetworkFunction: health_check requires "
> +                             "both inport and outport, skipping health_check 
> "
> +                             "for network_function:%s", nbrec_nf->name);
> +                continue;
> +            }
>              ovn_nf_svc_create(ovnsb_txn,
>                                nbrec_nf,
>                                svc_global_addresses,
> @@ -19151,10 +19203,23 @@ build_lswitch_stateful_nf(struct ovn_port *op,
>                    ds_cstr(match), ds_cstr(actions), lflow_ref);
>  }
>  
> +static bool
> +network_function_group_is_vtap_mode(
> +    const struct nbrec_network_function_group *nfg)
> +{
> +    const char *mode = nfg->mode ? nfg->mode : "inline";
> +    return !strcmp(mode, "vtap");
> +}
> +
>  static const char*
>  network_function_group_get_fallback(
>      const struct nbrec_network_function_group *nfg)
>  {
> +    /* For vtap mode, fallback is always defaulted to fail-open */
> +    if (network_function_group_is_vtap_mode(nfg)) {
> +        return "fail-open";
> +    }
> +
>      if (nfg->fallback) {
>          return nfg->fallback;
>      }
> @@ -19166,10 +19231,7 @@ network_function_group_is_fallback_fail_open(
>      const struct nbrec_network_function_group *nfg)
>  {
>      const char *fallback = network_function_group_get_fallback(nfg);
> -    if (!strcasecmp(fallback, "fail-open")) {
> -        return true;
> -    }
> -    return false;
> +    return !strcmp(fallback, "fail-open");
>  }
>  
>  static struct nbrec_network_function *
> @@ -19182,7 +19244,8 @@ static void
>  network_function_update_active(const struct nbrec_network_function_group 
> *nfg,
>                                 struct hmap *local_svc_monitors_map,
>                                 struct hmap *ic_learned_svc_monitors_map,
> -                               const char *svc_monitor_ip_dst)
> +                               const char *svc_monitor_ip_dst,
> +                               struct hmap *ls_ports)
>  {
>      if (!nfg->n_network_function) {
>          static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
> @@ -19194,10 +19257,13 @@ network_function_update_active(const struct 
> nbrec_network_function_group *nfg,
>          }
>          return;
>      }
> +
>      /* Array to store healthy network functions */
>      struct nbrec_network_function **healthy_nfs =
>          xmalloc(sizeof *healthy_nfs * nfg->n_network_function);
>      struct nbrec_network_function *nf_active_prev = NULL;
> +    bool is_nfg_vtap = network_function_group_is_vtap_mode(nfg);
> +
>      if (nfg->network_function_active) {
>          nf_active_prev = nfg->network_function_active;
>      }
> @@ -19206,32 +19272,65 @@ network_function_update_active(const struct 
> nbrec_network_function_group *nfg,
>      /* Determine the set of healthy network functions */
>      for (size_t i = 0; i < nfg->n_network_function; i++) {
>          struct nbrec_network_function *nf = nfg->network_function[i];
> -        bool is_healthy = false;
> +        const char *ports[] = {nf->inport->name,
> +                               !is_nfg_vtap && nf->outport
> +                               ? nf->outport->name : NULL};
> +        size_t n_ports = is_nfg_vtap ? 1 : 2;
>  
> -        if (nf->health_check == NULL) {
> -            VLOG_DBG("NetworkFunction: Health check is not configured for "
> -                     "network_function %s, considering it healthy", 
> nf->name);
> -            is_healthy = true;
> +        if (is_nfg_vtap) {
> +            if (nf->outport) {
> +                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 
> 1);
> +                VLOG_ERR_RL(&rl, "NetworkFunction: outport should not be set 
> "
> +                            "for vtap mode, network_function:%s", nf->name);
> +                continue;
> +            }
> +
> +            /* For vtap mode, consider network_function healthy based on
> +             * port binding status. */
> +            if (network_function_port_binding_state(
> +                    ports, n_ports, ls_ports, NULL) != NF_PORT_STATE_UP) {
> +                continue;
> +            }
>          } else {
> -            struct service_monitor_info *mon_info =
> -                get_service_mon(local_svc_monitors_map,
> -                                ic_learned_svc_monitors_map,
> -                                svc_monitor_ip_dst,
> -                                nf->outport->name, 0, "icmp");
> -            if (mon_info == NULL) {
> -                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 
> 1);
> -                VLOG_ERR_RL(&rl, "NetworkFunction: Service_monitor is not "
> -                            "found for network_function:%s", nf->name);
> -                is_healthy = false;
> -            } else if (mon_info->sbrec_mon->status
> -                       && !strcmp(mon_info->sbrec_mon->status, "online")) {
> -                is_healthy = true;
> +            /* For inline mode, inport and outport must be specified.
> +             * inport is mandatory in schema, check for outport. */
> +            if (nf->outport == NULL) {
> +                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 
> 1);
> +                VLOG_ERR_RL(&rl, "NetworkFunction: outport must be set "
> +                            "for inline mode, network_function:%s", 
> nf->name);
> +                continue;
> +            }
> +
> +            /* Always check port binding state first. */
> +            if (network_function_port_binding_state(
> +                    ports, n_ports, ls_ports, NULL) != NF_PORT_STATE_UP) {
> +                continue;
>              }
> -        }
>  
> -        if (is_healthy) {
> -            healthy_nfs[n_healthy++] = nf;
> +            /* Consider network_function healthy based on port binding
> +             * status if health_check is not configured. */
> +            if (nf->health_check) {
> +                struct service_monitor_info *mon_info =
> +                    get_service_mon(local_svc_monitors_map,
> +                                    ic_learned_svc_monitors_map,
> +                                    svc_monitor_ip_dst,
> +                                    nf->outport->name, 0, "icmp");
> +                if (mon_info == NULL) {
> +                    static struct vlog_rate_limit rl =
> +                        VLOG_RATE_LIMIT_INIT(5, 1);
> +                    VLOG_ERR_RL(&rl, "NetworkFunction: Service_monitor is 
> not "
> +                                "found for network_function:%s", nf->name);
> +                    continue;
> +                }
> +
> +                if (!mon_info->sbrec_mon->status
> +                    || strcmp(mon_info->sbrec_mon->status, "online")) {
> +                    continue;
> +                }
> +            }
>          }
> +
> +        healthy_nfs[n_healthy++] = nf;
>      }
>  
>      struct nbrec_network_function *nf_active = NULL;
> @@ -19274,15 +19373,15 @@ static void build_network_function_active(
>      const struct nbrec_network_function_group_table *nbrec_nfg_table,
>      struct hmap *local_svc_monitors_map,
>      struct hmap *ic_learned_svc_monitors_map,
> -    const char *svc_monitor_ip_dst)
> +    const char *svc_monitor_ip_dst,
> +    struct hmap *ls_ports)
>  {
>      const struct nbrec_network_function_group *nbrec_nfg;
>      NBREC_NETWORK_FUNCTION_GROUP_TABLE_FOR_EACH (nbrec_nfg,
>                              nbrec_nfg_table) {
> -        network_function_update_active(nbrec_nfg,
> -                                       local_svc_monitors_map,
> -                                       ic_learned_svc_monitors_map,
> -                                       svc_monitor_ip_dst);
> +        network_function_update_active(nbrec_nfg, local_svc_monitors_map,
> +                                      ic_learned_svc_monitors_map,
> +                                      svc_monitor_ip_dst, ls_ports);
>      }
>  }
>  
> @@ -19315,10 +19414,10 @@ network_function_configure_fail_open_flows(struct 
> lflow_table *lflows,
>  }
>  
>  static void
> -consider_network_function(struct lflow_table *lflows,
> -                          const struct ovn_datapath *od,
> -                          struct nbrec_network_function_group *nfg,
> -                          bool ingress, struct lflow_ref *lflow_ref)
> +consider_network_function_inline(struct lflow_table *lflows,
> +                                 const struct ovn_datapath *od,
> +                                 struct nbrec_network_function_group *nfg,
> +                                 bool ingress, struct lflow_ref *lflow_ref)
>  {
>      struct ds match = DS_EMPTY_INITIALIZER;
>      struct ds action = DS_EMPTY_INITIALIZER;
> @@ -19343,6 +19442,15 @@ consider_network_function(struct lflow_table *lflows,
>          return;
>      }
>  
> +    if (nf->outport == NULL) {
> +        VLOG_ERR_RL(&rl, "No outport configured for inline mode "
> +                    "network function:%s", nf->name);
> +        return;
> +    }
> +
> +    VLOG_DBG("network_function %s: inport %s outport %s",
> +              nf->name, nf->inport->name, nf->outport->name);
> +
>      /* If NF ports are present on this LS, use those; otherwise look for 
> child
>       * ports. */
>      struct ovn_port *input_port =
> @@ -19515,6 +19623,204 @@ consider_network_function(struct lflow_table 
> *lflows,
>      ds_destroy(&action);
>  }
>  
> +static void
> +consider_network_function_vtap(struct lflow_table *lflows,
> +                               const struct ovn_datapath *od,
> +                               struct nbrec_network_function_group *nfg,
> +                               bool ingress, struct lflow_ref *lflow_ref)
> +{
> +    struct nbrec_network_function *nf;
> +    struct ds match = DS_EMPTY_INITIALIZER;
> +    struct ds action = DS_EMPTY_INITIALIZER;
> +    const struct ovn_stage *fwd_stage, *rev_stage;
> +    struct ovn_port *input_port = NULL;
> +    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
> +
> +    if (nfg->fallback && !strcmp(nfg->fallback, "fail-close")) {
> +        VLOG_WARN_RL(&rl, "NF vtap mode: fallback is set to fail-close but "
> +                     "will be overridden to fail-open for nfg:%s", 
> nfg->name);
> +    }
> +
> +    /* Configure flows with higher priority than default drop rule to allow
> +     * the traffic when there is no active NF available.
> +     */
> +    network_function_configure_fail_open_flows(lflows, od, lflow_ref,
> +                                               nfg->id, ingress);
> +    /* Currently we support only one active port-pair in a group.
> +     * If there are multiple active pairs, take the first one.
> +     * Load balancing would be added in future. */
> +    nf = nf_get_active(nfg);
> +    if (nf == NULL) {
> +        VLOG_ERR_RL(&rl, "No active network function available, nfg:%s",

This can happen in valid operation cases, if there's no active NF.  This
should be a VLOG_INFO_RL().

> +                    nfg->name);
> +        return;
> +    }
> +
> +    if (nf->outport) {
> +        VLOG_ERR_RL(&rl, "Outport is not supported for vtap mode "
> +                    "network function:%s", nf->name);
> +        return;
> +    }
> +
> +    VLOG_DBG("network_function %s: inport %s",
> +              nf->name, nf->inport->name);
> +
> +    /* If NF ports are present on this LS, use those; otherwise look for 
> child
> +     * ports. */
> +    input_port = ovn_port_find_port_or_child(od, nf->inport->name);
> +    if (input_port == NULL) {
> +        VLOG_ERR_RL(&rl, "Port not found for network_function %s", nf->name);
> +        return;
> +    }
> +
> +    if (ingress) {
> +        fwd_stage = S_SWITCH_IN_NF;
> +        rev_stage = S_SWITCH_OUT_NF;
> +    } else {
> +        fwd_stage = S_SWITCH_OUT_NF;
> +        rev_stage = S_SWITCH_IN_NF;
> +    }
> +
> +    /* Pre NF Table (Priority 99):
> +     *
> +     * Currently, this stage simply writes the active network function ID 
> into
> +     * the nf_id register.
> +     *
> +     * In the future, this stage will be extended to support network function
> +     * load balancing.
> +     */
> +    ds_put_format(&match, REGBIT_NF_ENABLED" == 1 && "
> +                          REGBIT_NF_ORIG_DIR" == 1 && "
> +                          REG_NF_GROUP_ID " == %"PRIu8,
> +                  (uint8_t) nfg->id);
> +    ds_put_format(&action, REG_NF_ID" = %"PRIu8"; next;", (uint8_t) nf->id);
> +    ovn_lflow_add(lflows, od, ingress ? S_SWITCH_IN_PRE_NF
> +                                      : S_SWITCH_OUT_PRE_NF,
> +                  99, ds_cstr(&match), ds_cstr(&action), lflow_ref);
> +    ds_clear(&match);
> +    ds_clear(&action);
> +
> +    /* Add forward flows for mirroring:
> +     * Flows to handle request packets for new or existing connections.
> +     *
> +     * from-lport ACL in_network_function priority 99:
> +     * in_acl_eval has already categorized it and populated nf_enabled,
> +     * direction and nfg_id registers. in_pre_nf sets the active network
> +     * function id in nf_id register. Here this rule sets the outport to the
> +     * NF port for the mirrored packet and does output action to skip the 
> rest
> +     * of the ingress pipeline. Original packet continues with ingress
> +     * pipeline.
> +     *
> +     * to-lport ACL out_network_function priority 99:
> +     * out_acl_eval, and out_pre_nf set the nf related registers. Then the
> +     * out_network_function stage sets the outport to NF port for the 
> mirrored
> +     * packet and submits the packet back to ingress pipeline l2_lkup table.
> +     * The l2_lkup would skip mac based lookup as the
> +     * NETWORK_FUNCTION_EGRESS_LOOPBACK is set. Original packet continues 
> with
> +     * the egress pipeline processing.
> +     */
> +    if (ingress) {
> +        ds_put_format(&action, "clone {outport = %s; output;}; next;",
> +                      input_port->json_key);
> +    } else {
> +        ds_put_format(&action, "clone {outport = %s; "
> +                      REGBIT_NF_EGRESS_LOOPBACK" = 1; "
> +                      "next(pipeline=ingress, table=%d);}; next;",
> +                      input_port->json_key,
> +                      ovn_stage_get_table(S_SWITCH_IN_L2_LKUP));
> +    }
> +    ds_put_format(&match, REGBIT_NF_ENABLED" == 1 && "
> +                  REGBIT_NF_ORIG_DIR" == 1 && "
> +                  REG_NF_ID " == %"PRIu8, (uint8_t) nf->id);
> +    ovn_lflow_add(lflows, od, fwd_stage, 99, ds_cstr(&match),
> +                  ds_cstr(&action), lflow_ref);
> +    ds_clear(&match);
> +    ds_clear(&action);
> +
> +    /* Add reverse flows for mirroring:
> +     * Flows to handle response packets for existing connections.
> +     *
> +     * from-lport ACL out_network_function priority 99:
> +     * out_acl stage sets the nf_enabled register based on CT label.
> +     * Here this rule sets the outport to the NF port for the mirrored packet
> +     * based on nf_id fetched from the CT label. Then it submits the packet
> +     * back to ingress pipeline l2_lkup table. The l2_lkup would skip mac
> +     * lookup as the NETWORK_FUNCTION_EGRESS_LOOPBACK is set. Original packet
> +     * continues with the egress pipeline.
> +     *
> +     * to-lport ACL in_network_function priority 99:
> +     * in_acl stage sets the nf_enabled register based on CT label.
> +     * Here this rule sets the outport to the NF port for the mirrored packet
> +     * based on nf_id fetched from the CT label and does output action to 
> skip
> +     * the rest of the ingress pipeline. Original packet continues with the
> +     * ingress pipeline.
> +     */
> +    if (ingress) {
> +        ds_put_format(&action, "clone {outport = %s; "
> +                      REGBIT_NF_EGRESS_LOOPBACK" = 1; "
> +                      "next(pipeline=ingress, table=%d);}; next;",
> +                      input_port->json_key,
> +                      ovn_stage_get_table(S_SWITCH_IN_L2_LKUP));
> +    } else {
> +        ds_put_format(&action, "clone {outport = %s; output;}; next;",
> +                      input_port->json_key);
> +    }
> +    ds_put_format(&match, REGBIT_NF_ENABLED" == 1 && "
> +                 REGBIT_NF_ORIG_DIR" == 0 && "
> +                 "ct_label.nf_id == %"PRIu8, (uint8_t) nf->id);
> +    ovn_lflow_add(lflows, od, rev_stage, 99, ds_cstr(&match), 
> ds_cstr(&action),
> +                  lflow_ref);
> +    ds_clear(&match);
> +    ds_clear(&action);
> +
> +    /* Priority 100 flow in in_network_function:
> +     * Drop packets coming from network-function in vtap mode.
> +     */
> +    ds_put_format(&match, "inport == %s", input_port->json_key);
> +    ds_put_format(&action, "drop;");
> +    ovn_lflow_add(lflows, od, S_SWITCH_IN_NF, 100,
> +                  ds_cstr(&match), ds_cstr(&action), lflow_ref);
> +    ds_clear(&match);
> +    ds_clear(&action);
> +
> +    /* Priority 100 flow in out_network_function:
> +     * Allow packets to go through if outport is network-function port as
> +     * we don't want the packets to be mirrored again based on to-lport
> +     * match.
> +     */
> +    ds_put_format(&match, "outport == %s", input_port->json_key);
> +    ds_put_format(&action, "next;");
> +    ovn_lflow_add(lflows, od, S_SWITCH_OUT_NF, 100,
> +                  ds_cstr(&match), ds_cstr(&action), lflow_ref);
> +    ds_clear(&match);
> +    ds_clear(&action);
> +
> +    /* Priority 110 flow in out_pre_acl:
> +     * Avoid ct for packets going to network-function port in vtap mode since
> +     * these packets gets consumed at VNF.

Typo: s/gets/get/

> +     */
> +    ds_put_format(&match, "ip && outport == %s", input_port->json_key);
> +    ds_put_format(&action, "ct_clear; next;");
> +    ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 110, ds_cstr(&match),
> +                  ds_cstr(&action), lflow_ref);
> +
> +    ds_destroy(&match);
> +    ds_destroy(&action);
> +}
> +
> +static void
> +consider_network_function(struct lflow_table *lflows,
> +                          const struct ovn_datapath *od,
> +                          struct nbrec_network_function_group *nfg,
> +                          bool ingress, struct lflow_ref *lflow_ref)
> +{
> +    if (network_function_group_is_vtap_mode(nfg)) {
> +        consider_network_function_vtap(lflows, od, nfg, ingress, lflow_ref);
> +    } else {
> +        consider_network_function_inline(lflows, od, nfg, ingress, 
> lflow_ref);
> +    }
> +}
> +
>  static void
>  build_network_function(const struct ovn_datapath *od,
>                         struct lflow_table *lflows,
> @@ -19541,8 +19847,8 @@ build_network_function(const struct ovn_datapath *od,
>      /* Ingress and Egress PRE NF Table (Priority 1): ACL stage determined 
> these
>       * packets should be redirected, but there is no active NF in NFG.
>       * Reset the nf_id register to 0. This will drop the packet by the
> -     * default drop rule in the subsequent NF table.
> -     */
> +     * default drop rule in the subsequent NF table if NF is in fail-close
> +     * mode. */
>      ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_NF, 1,
>                    REGBIT_NF_ENABLED" == 1 && " REGBIT_NF_ORIG_DIR" == 1",
>                    REG_NF_ID" = 0; next;", lflow_ref);
> @@ -21395,7 +21701,8 @@ ovnnb_db_run(struct northd_input *input_data,
>          input_data->nbrec_network_function_group_table,
>          &data->local_svc_monitors_map,
>          input_data->ic_learned_svc_monitors_map,
> -        input_data->svc_global_addresses->ip_dst);
> +        input_data->svc_global_addresses->ip_dst,
> +        &data->ls_ports);
>      build_ipam(&data->ls_datapaths.datapaths);
>      build_lrouter_groups(&data->lr_ports, &data->lr_datapaths);
>      build_ip_mcast(ovnsb_txn, input_data->sbrec_ip_multicast_table,
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index 8dc3a2a08..555d01c36 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -19938,7 +19938,7 @@ AT_CLEANUP
>  ])
>  
>  OVN_FOR_EACH_NORTHD_NO_HV([
> -AT_SETUP([Check network function])
> +AT_SETUP([Check network-function in inline mode])
>  ovn_start
>  
>  AS_BOX([Create a NF and add it to a from-lport ACL])
> @@ -19963,11 +19963,12 @@ check ovn-nbctl lsp-add sw0 sw0-p3 -- 
> lsp-set-addresses sw0-p3 "00:00:00:00:00:0
>  check ovn-nbctl pg-add pg0 sw0-p1
>  check ovn-nbctl acl-add pg0 from-lport 1002 "inport == @pg0 && ip4.dst == 
> 10.0.0.3" allow-related nfg0
>  
> -# Add hypervisor and bind NF ports
> -check ovn-sbctl chassis-add hv1 geneve 127.0.0.1
> -check ovn-sbctl lsp-bind sw0-nf-p1 hv1
> -check ovn-sbctl lsp-bind sw0-nf-p2 hv1
> -
> +check ovn-sbctl chassis-add gw1 geneve 127.0.0.1 \
> +    -- set chassis gw1 other_config:ovn-ct-lb-related=true \
> +    -- set chassis gw1 other_config:ct-no-masked-label=true
> +chassis_uuid=$(fetch_column Chassis _uuid name=gw1)
> +check ovn-sbctl set port_binding sw0-nf-p1 up=true chassis=$chassis_uuid
> +check ovn-sbctl set port_binding sw0-nf-p2 up=true chassis=$chassis_uuid
>  check ovn-nbctl --wait=sb sync
>  
>  ovn-sbctl dump-flows sw0 > sw0flows
> @@ -20078,8 +20079,8 @@ check ovn-nbctl set logical_switch_port sw0-nf-p4 \
>  check ovn-nbctl nf-add nf1 102 sw0-nf-p3 sw0-nf-p4
>  check ovn-nbctl nfg-add nfg1 202 inline nf1
>  check ovn-nbctl acl-add pg0 to-lport 1003 "outport == @pg0 && ip4.src == 
> 10.0.0.4" allow-related nfg1
> -check ovn-sbctl lsp-bind sw0-nf-p3 hv1
> -check ovn-sbctl lsp-bind sw0-nf-p4 hv1
> +check ovn-sbctl set port_binding sw0-nf-p3 up=true chassis=$chassis_uuid
> +check ovn-sbctl set port_binding sw0-nf-p4 up=true chassis=$chassis_uuid
>  check ovn-nbctl --wait=sb sync
>  
>  ovn-sbctl dump-flows sw0 > sw0flows
> @@ -20254,11 +20255,17 @@ done
>  
>  nfsw="nf-sw"
>  check ovn-nbctl ls-add $nfsw
> +
> +check ovn-sbctl chassis-add gw1 geneve 127.0.0.1 \
> +    -- set chassis gw1 other_config:ovn-ct-lb-related=true \
> +    -- set chassis gw1 other_config:ct-no-masked-label=true
> +chassis_uuid=$(fetch_column Chassis _uuid name=gw1)
> +
>  for i in {1..4}; do
>      port=$nfsw-p$i
>      check ovn-nbctl lsp-add $nfsw $port
>      check ovn-nbctl --wait=sb sync
> -    check ovn-sbctl set port_binding $port up=true
> +    check ovn-sbctl set port_binding $port up=true chassis=$chassis_uuid
>      check ovn-nbctl lsp-add $sw child-$i $port 100
>  done
>  check ovn-nbctl set logical_switch_port $nfsw-p1 \
> @@ -21444,6 +21451,193 @@ AT_CHECK([test "$sr_lflow_uuid" == ""])
>  
>  CHECK_NO_CHANGE_AFTER_RECOMPUTE
>  
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +
> +AT_SETUP([Check network-function in vtap mode])
> +ovn_start
> +
> +AS_BOX([Create a NF and add it to a from-lport ACL])
> +
> +# Create a NF and add it to a from-lport ACL.
> +check ovn-nbctl ls-add sw0
> +check ovn-nbctl lsp-add sw0 sw0-nf-p1
> +check ovn-nbctl set logical_switch_port sw0-nf-p1 
> options:receive_multicast=false options:lsp_learn_fdb=false options:is-nf=true
> +check ovn-nbctl nf-add nf0 1 sw0-nf-p1
> +check ovn-nbctl nfg-add nfg0 1 vtap nf0
> +
> +check ovn-nbctl lsp-add sw0 sw0-p1 -- lsp-set-addresses sw0-p1 
> "00:00:00:00:00:01 10.0.0.2"
> +check ovn-nbctl lsp-add sw0 sw0-p2 -- lsp-set-addresses sw0-p2 
> "00:00:00:00:00:02 10.0.0.3"
> +check ovn-nbctl lsp-add sw0 sw0-p3 -- lsp-set-addresses sw0-p3 
> "00:00:00:00:00:03 10.0.0.4"
> +
> +check ovn-nbctl pg-add pg0 sw0-p1
> +check ovn-nbctl acl-add pg0 from-lport 1002 "inport == @pg0 && ip4.dst == 
> 10.0.0.3" allow-related nfg0
> +
> +check ovn-sbctl chassis-add gw1 geneve 127.0.0.1 \
> +    -- set chassis gw1 other_config:ovn-ct-lb-related=true \
> +    -- set chassis gw1 other_config:ct-no-masked-label=true
> +chassis_uuid=$(fetch_column Chassis _uuid name=gw1)
> +check ovn-sbctl set port_binding sw0-nf-p1 up=true chassis=$chassis_uuid
> +check ovn-nbctl --wait=sb sync
> +
> +ovn-sbctl dump-flows sw0 > sw0flows
> +AT_CAPTURE_FILE([sw0flows])
> +
> +AT_CHECK(
> +  [grep -E 'ls_(in|out)_acl_eval' sw0flows | ovn_strip_lflows | grep pg0 | 
> sort], [0], [dnl
> +  table=??(ls_in_acl_eval     ), priority=2002 , match=(reg0[[7]] == 1 && 
> (inport == @pg0 && ip4.dst == 10.0.0.3)), action=(reg8[[16]] = 1; reg8[[21]] 
> = 1; reg8[[22]] = 1; reg0[[22..29]] = 1; next;)
> +  table=??(ls_in_acl_eval     ), priority=2002 , match=(reg0[[8]] == 1 && 
> (inport == @pg0 && ip4.dst == 10.0.0.3)), action=(reg8[[16]] = 1; reg0[[1]] = 
> 1; reg8[[21]] = 1; reg8[[22]] = 1; reg0[[22..29]] = 1; next;)
> +])
> +
> +# Vtap uses Pre NF (priority 99) to set REG_NF_ID for CT commit, aligned 
> with inline mode.
> +# First box has only from-lport ACL (nfg0), so vtap Pre NF flows are only in 
> IN path.
> +AT_CHECK(
> +  [grep -E 'ls_(in|out)_pre_network_function' sw0flows | ovn_strip_lflows | 
> sort], [0], [dnl
> +  table=??(ls_in_pre_network_function), priority=0    , match=(1), 
> action=(next;)
> +  table=??(ls_in_pre_network_function), priority=1    , match=(reg8[[21]] == 
> 1 && reg8[[22]] == 1), action=(reg0[[22..29]] = 0; next;)
> +  table=??(ls_in_pre_network_function), priority=10   , match=(reg8[[21]] == 
> 1 && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(reg8[[21]] = 0; 
> reg0[[22..29]] = 0; next;)
> +  table=??(ls_in_pre_network_function), priority=99   , match=(reg8[[21]] == 
> 1 && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(reg0[[22..29]] = 1; 
> next;)
> +  table=??(ls_out_pre_network_function), priority=0    , match=(1), 
> action=(next;)
> +  table=??(ls_out_pre_network_function), priority=1    , match=(reg8[[21]] 
> == 1 && reg8[[22]] == 1), action=(reg0[[22..29]] = 0; next;)
> +])
> +
> +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=(drop;)
> +  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=(clone {outport = 
> "sw0-nf-p1"; output;}; next;)
> +  table=??(ls_out_network_function), priority=0    , match=(1), 
> action=(next;)
> +  table=??(ls_out_network_function), priority=1    , match=(reg8[[21]] == 
> 1), action=(drop;)
> +  table=??(ls_out_network_function), priority=100  , match=(outport == 
> "sw0-nf-p1"), action=(next;)
> +  table=??(ls_out_network_function), priority=100  , match=(reg8[[21]] == 1 
> && eth.mcast), action=(next;)
> +  table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
> && reg8[[22]] == 0 && ct_label.nf_id == 1), action=(clone {outport = 
> "sw0-nf-p1"; reg8[[23]] = 1; next(pipeline=ingress, table=??);}; next;)
> +])
> +
> +AT_CHECK([grep "ls_in_l2_lkup" sw0flows | ovn_strip_lflows | grep 
> 'priority=100'], [0], [dnl
> +  table=??(ls_in_l2_lkup      ), priority=100  , match=(reg8[[23]] == 1), 
> action=(output;)
> +])
> +
> +# Vtap: egress pre-acl ct_clear for packets going to vtap NF port so they are
> +# not committed to conntrack.
> +AT_CHECK(
> +  [grep 'ls_out_pre_acl' sw0flows | ovn_strip_lflows | grep 'sw0-nf-p1'], 
> [0], [dnl
> +  table=??(ls_out_pre_acl     ), priority=110  , match=(ip && outport == 
> "sw0-nf-p1"), action=(ct_clear; next;)
> +])
> +
> +AT_CHECK(
> +  [grep -E 'ls_(in|out)_acl_eval' sw0flows | ovn_strip_lflows | grep 
> 'ct_label.nf' | sort], [0], [dnl
> +  table=??(ls_in_acl_eval     ), priority=65532, match=(!ct.est && ct.rel && 
> !ct.new && ct_mark.blocked == 0), action=(reg0[[17]] = 1; reg8[[21]] = 
> ct_label.nf; reg8[[16]] = 1; ct_commit_nat;)
> +  table=??(ls_in_acl_eval     ), priority=65532, match=(ct.est && !ct.rel && 
> ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; 
> reg0[[17]] = 1; reg8[[21]] = ct_label.nf; reg8[[16]] = 1; next;)
> +  table=??(ls_in_acl_eval     ), priority=65532, match=(ct.est && 
> ct_mark.allow_established == 1), action=(reg0[[21]] = 1; reg8[[21]] = 
> ct_label.nf; reg8[[16]] = 1; next;)
> +  table=??(ls_out_acl_eval    ), priority=65532, match=(!ct.est && ct.rel && 
> !ct.new && ct_mark.blocked == 0), action=(reg8[[21]] = ct_label.nf; 
> reg8[[16]] = 1; ct_commit_nat;)
> +  table=??(ls_out_acl_eval    ), priority=65532, match=(ct.est && !ct.rel && 
> ct.rpl && ct_mark.blocked == 0), action=(reg8[[21]] = ct_label.nf; reg8[[16]] 
> = 1; next;)
> +  table=??(ls_out_acl_eval    ), priority=65532, match=(ct.est && 
> ct_mark.allow_established == 1), action=(reg8[[21]] = ct_label.nf; reg8[[16]] 
> = 1; next;)
> +])
> +
> +# ICMP packets from sw0-p1 should be mirrored to sw0-nf-p1 but traffic 
> originated
> +# in opposite direction should not get mirrored.
> +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'
> +flow_icmp='icmp4.type == 8'
> +flow_from_p1="inport == \"sw0-p1\" && ${flow_eth_from_p1} && 
> ${flow_ip_from_p1} && ${flow_icmp}"
> +AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --minimal sw0 
> "${flow_from_p1}"], [0], [dnl
> +ct_next(ct_state=new|trk) {
> +    clone {
> +        output("sw0-nf-p1");
> +    };
> +    ct_next(ct_state=new|trk) {
> +        output("sw0-p2");
> +    };
> +};
> +])
> +flow_eth_rev='eth.src == 00:00:00:00:00:02 && eth.dst == 00:00:00:00:00:01'
> +flow_ip_rev='ip.ttl==64 && ip4.src == 10.0.0.3 && ip4.dst == 10.0.0.2'
> +flow_rev="inport == \"sw0-p2\" && ${flow_eth_rev} && ${flow_ip_rev} && 
> ${flow_icmp}"
> +AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --minimal sw0 "${flow_rev}"], 
> [0], [dnl
> +ct_next(ct_state=new|trk) {
> +    ct_next(ct_state=new|trk) {
> +        output("sw0-p1");
> +    };
> +};
> +])
> +
> +AS_BOX([Create another NF and add it to a to-lport ACL.])
> +
> +# Create another NF and add it to a to-lport ACL.
> +check ovn-nbctl lsp-add sw0 sw0-nf-p3
> +check ovn-nbctl set logical_switch_port sw0-nf-p3 
> options:receive_multicast=false options:lsp_learn_fdb=false options:is-nf=true
> +check ovn-nbctl nf-add nf1 2 sw0-nf-p3
> +check ovn-nbctl nfg-add nfg1 2 vtap nf1
> +check ovn-sbctl set port_binding sw0-nf-p3 up=true chassis=$chassis_uuid
> +check ovn-nbctl --wait=sb sync
> +check ovn-nbctl acl-add pg0 to-lport 1003 "outport == @pg0 && ip4.src == 
> 10.0.0.4" allow-related nfg1
> +
> +ovn-sbctl dump-flows sw0 > sw0flows
> +AT_CAPTURE_FILE([sw0flows])
> +
> +AT_CHECK(
> +  [grep -E 'ls_(in|out)_acl_eval' sw0flows | ovn_strip_lflows | grep pg0 | 
> sort], [0], [dnl
> +  table=??(ls_in_acl_eval     ), priority=2002 , match=(reg0[[7]] == 1 && 
> (inport == @pg0 && ip4.dst == 10.0.0.3)), action=(reg8[[16]] = 1; reg8[[21]] 
> = 1; reg8[[22]] = 1; reg0[[22..29]] = 1; next;)
> +  table=??(ls_in_acl_eval     ), priority=2002 , match=(reg0[[8]] == 1 && 
> (inport == @pg0 && ip4.dst == 10.0.0.3)), action=(reg8[[16]] = 1; reg0[[1]] = 
> 1; reg8[[21]] = 1; reg8[[22]] = 1; reg0[[22..29]] = 1; next;)
> +  table=??(ls_out_acl_eval    ), priority=2003 , match=(reg0[[7]] == 1 && 
> (outport == @pg0 && ip4.src == 10.0.0.4)), action=(reg8[[16]] = 1; reg8[[21]] 
> = 1; reg8[[22]] = 1; reg0[[22..29]] = 2; next;)
> +  table=??(ls_out_acl_eval    ), priority=2003 , match=(reg0[[8]] == 1 && 
> (outport == @pg0 && ip4.src == 10.0.0.4)), action=(reg8[[16]] = 1; reg0[[1]] 
> = 1; reg8[[21]] = 1; reg8[[22]] = 1; reg0[[22..29]] = 2; next;)
> +])
> +
> +# Pre NF: nfg0 (id=1) is from-lport so IN_PRE_NF only; nfg1 (id=2) is 
> to-lport so OUT_PRE_NF only.
> +AT_CHECK(
> +  [grep -E 'ls_(in|out)_pre_network_function' sw0flows | ovn_strip_lflows | 
> sort], [0], [dnl
> +  table=??(ls_in_pre_network_function), priority=0    , match=(1), 
> action=(next;)
> +  table=??(ls_in_pre_network_function), priority=1    , match=(reg8[[21]] == 
> 1 && reg8[[22]] == 1), action=(reg0[[22..29]] = 0; next;)
> +  table=??(ls_in_pre_network_function), priority=10   , match=(reg8[[21]] == 
> 1 && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(reg8[[21]] = 0; 
> reg0[[22..29]] = 0; next;)
> +  table=??(ls_in_pre_network_function), priority=99   , match=(reg8[[21]] == 
> 1 && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(reg0[[22..29]] = 1; 
> next;)
> +  table=??(ls_out_pre_network_function), priority=0    , match=(1), 
> action=(next;)
> +  table=??(ls_out_pre_network_function), priority=1    , match=(reg8[[21]] 
> == 1 && reg8[[22]] == 1), action=(reg0[[22..29]] = 0; next;)
> +  table=??(ls_out_pre_network_function), priority=10   , match=(reg8[[21]] 
> == 1 && reg8[[22]] == 1 && reg0[[22..29]] == 2), action=(reg8[[21]] = 0; 
> reg0[[22..29]] = 0; next;)
> +  table=??(ls_out_pre_network_function), priority=99   , match=(reg8[[21]] 
> == 1 && reg8[[22]] == 1 && reg0[[22..29]] == 2), action=(reg0[[22..29]] = 2; 
> next;)
> +])
> +
> +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=(drop;)
> +  table=??(ls_in_network_function), priority=100  , match=(inport == 
> "sw0-nf-p3"), action=(drop;)
> +  table=??(ls_in_network_function), priority=100  , match=(reg8[[21]] == 1 
> && eth.mcast), action=(next;)
> +  table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
> && reg8[[22]] == 0 && ct_label.nf_id == 2), action=(clone {outport = 
> "sw0-nf-p3"; output;}; next;)
> +  table=??(ls_in_network_function), priority=99   , match=(reg8[[21]] == 1 
> && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(clone {outport = 
> "sw0-nf-p1"; output;}; next;)
> +  table=??(ls_out_network_function), priority=0    , match=(1), 
> action=(next;)
> +  table=??(ls_out_network_function), priority=1    , match=(reg8[[21]] == 
> 1), action=(drop;)
> +  table=??(ls_out_network_function), priority=100  , match=(outport == 
> "sw0-nf-p1"), action=(next;)
> +  table=??(ls_out_network_function), priority=100  , match=(outport == 
> "sw0-nf-p3"), action=(next;)
> +  table=??(ls_out_network_function), priority=100  , match=(reg8[[21]] == 1 
> && eth.mcast), action=(next;)
> +  table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
> && reg8[[22]] == 0 && ct_label.nf_id == 1), action=(clone {outport = 
> "sw0-nf-p1"; reg8[[23]] = 1; next(pipeline=ingress, table=??);}; next;)
> +  table=??(ls_out_network_function), priority=99   , match=(reg8[[21]] == 1 
> && reg8[[22]] == 1 && reg0[[22..29]] == 2), action=(clone {outport = 
> "sw0-nf-p3"; reg8[[23]] = 1; next(pipeline=ingress, table=??);}; next;)
> +])
> +
> +# Vtap: egress pre-acl ct_clear for all vtap NF ports so that they are not 
> committed to conntrack.
> +AT_CHECK(
> +  [grep 'ls_out_pre_acl' sw0flows | ovn_strip_lflows | grep 'nf-p' | sort], 
> [0], [dnl
> +  table=??(ls_out_pre_acl     ), priority=110  , match=(ip && outport == 
> "sw0-nf-p1"), action=(ct_clear; next;)
> +  table=??(ls_out_pre_acl     ), priority=110  , match=(ip && outport == 
> "sw0-nf-p3"), action=(ct_clear; next;)
> +])
> +
> +# ICMP packets to sw0-p1 should be mirrored to sw0-nf-p3.
> +flow_eth_to_p1='eth.src == 00:00:00:00:00:03 && eth.dst == 00:00:00:00:00:01'
> +flow_ip_to_p1='ip.ttl==64 && ip4.src == 10.0.0.4 && ip4.dst == 10.0.0.2'
> +flow_to_p1="inport == \"sw0-p3\" && ${flow_eth_to_p1} && ${flow_ip_to_p1} && 
> ${flow_icmp}"
> +AT_CHECK_UNQUOTED([ovn_trace --ct new --ct new --minimal sw0 
> "${flow_to_p1}"], [0], [dnl
> +ct_next(ct_state=new|trk) {
> +    ct_next(ct_state=new|trk) {
> +        clone {
> +            output("sw0-nf-p3");
> +        };
> +        output("sw0-p1");
> +    };
> +};
> +])
> +
>  OVN_CLEANUP_NORTHD
>  AT_CLEANUP
>  ])
> diff --git a/tests/ovn.at b/tests/ovn.at
> index 2967ffbbf..042f4e397 100644
> --- a/tests/ovn.at
> +++ b/tests/ovn.at
> @@ -44470,10 +44470,24 @@ nf_cleanup_port_binding() {
>      check ovn-nbctl --wait=hv sync
>      nf_teardown_hypervisors
>  }
> +
> +nf_test_icmp_mirrored() {
> +    local inport=${1} src_mac=${2} dst_mac=${3} src_ip=${4} dst_ip=${5} 
> icmp_type=${6}
> +    local nf_outport=${7} dst_outport=${8} in_hv=${9} nf_hv=${10} 
> dst_hv=${11}
> +    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"
> +    as $in_hv ovs-appctl -t ovn-controller inject-pkt "$packet"
> +    echo "INJECTED PACKET $packet"
> +    # Expect packet at both NF (mirrored) and destination ports.
> +    echo $packet | ovstest test-ovn expr-to-packets >> 
> $nf_hv-$nf_outport.expected
> +    echo $packet | ovstest test-ovn expr-to-packets >> 
> $dst_hv-$dst_outport.expected
> +}
>  ])
>  
>  OVN_FOR_EACH_NORTHD([
> -AT_SETUP([Network function packet flow - outbound])
> +AT_SETUP([Network function inline packet flow - outbound])
>  AT_KEYWORDS([ovn])
>  ovn_start
>  
> @@ -44606,7 +44620,7 @@ AT_CLEANUP
>  ])
>  
>  OVN_FOR_EACH_NORTHD([
> -AT_SETUP([Network function packet flow - inbound])
> +AT_SETUP([Network function inline packet flow - inbound])
>  AT_KEYWORDS([ovn])
>  ovn_start
>  
> @@ -44740,6 +44754,238 @@ OVN_CLEANUP_NORTHD
>  AT_CLEANUP
>  ])
>  
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([Network function vtap packet flow - outbound])
> +AT_KEYWORDS([ovn])
> +ovn_start
> +
> +NF_HELPERS
> +
> +# Create logical topology. One LS sw0 with 3 ports.
> +# from-lport ACL rule mirrors request packets from sw0-p1 to sw0-p2 via vtap 
> NF port sw0-nf-vtap.
> +# In vtap mode, traffic is mirrored (copied) to NF, original packets still 
> reach destination.
> +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"
> +    done
> +    check ovn-nbctl lsp-add $sw $sw-nf-vtap -- lsp-set-addresses $sw-nf-vtap 
> "f0:00:00:00:01:01"
> +    check ovn-nbctl set logical_switch_port $sw-nf-vtap \
> +        options:receive_multicast=false options:lsp_learn_fdb=false \
> +        options:is-nf=true
> +    check ovn-nbctl nf-add nf0 1 $sw-nf-vtap
> +    check ovn-nbctl nfg-add nfg0 1 vtap 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
> +}
> +
> +create_logical_topology sw0
> +
> +packet_mirroring_test() {
> +    local hvp1=$1 hvp2=$2 hvnf=$3
> +
> +    # Test 1: Inject ICMP request from sw0-p1 to sw0-p2

It's better to not number tests, in case we need to insert steps in the
middle in the future.

> +    # In vtap mode: single packet should be mirrored to NF AND reach sw0-p2
> +    nf_test_icmp_mirrored sw0-p1 "f0:00:00:00:00:01" "f0:00:00:00:00:02" 
> "192.168.0.11" "192.168.0.12" 8 \
> +        vif-nf vif2 $hvp1 $hvnf $hvp2
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvnf/vif-nf-tx.pcap], 
> [$hvnf-vif-nf.expected])
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvp2/vif2-tx.pcap], 
> [$hvp2-vif2.expected])
> +
> +    # Test 2: Reverse direction - ICMP request from sw0-p2 to sw0-p1
> +    # No mirroring expected (ACL only matches from-lport on pg0 which 
> contains sw0-p1)
> +    nf_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])
> +}
> +
> +test_nf_vtap_with_multinodes_outbound() {
> +    mode=$1
> +    nf_ports="vif-nf:sw0-nf-vtap"
> +    # Test 1: Bind all 3 ports to one node
> +    echo "$mode: Network function vtap outbound with single node"
> +    nf_create_port_binding hv1 hv1 hv1 "$nf_ports"
> +
> +    packet_mirroring_test hv1 hv1 hv1 sw0

The function only receives 3 arguments.  This applies to all occurences,
"sw0" should be removed from the arguments.

> +
> +    nf_cleanup_port_binding hv1 hv1 hv1 "$nf_ports"
> +
> +    # Test 2: src & dst ports on one node, NF on another node
> +    echo "$mode: Network function vtap outbound with two nodes - nf separate"
> +    nf_create_port_binding hv1 hv1 hv2 "$nf_ports"
> +
> +    packet_mirroring_test hv1 hv1 hv2 sw0
> +
> +    nf_cleanup_port_binding hv1 hv1 hv2 "$nf_ports"
> +
> +    # Test 3: src and nf on one node, dst on a second node
> +    echo "$mode: Network function vtap outbound with two nodes - nf with src"
> +    nf_create_port_binding hv1 hv2 hv1 "$nf_ports"
> +
> +    packet_mirroring_test hv1 hv2 hv1 sw0
> +
> +    nf_cleanup_port_binding hv1 hv2 hv1 "$nf_ports"
> +
> +    # Test 4: src on one node, nf & dst on a second node
> +    echo "$mode: Network function vtap outbound with two nodes - nf with dst"
> +    nf_create_port_binding hv1 hv2 hv2 "$nf_ports"
> +
> +    packet_mirroring_test hv1 hv2 hv2 sw0
> +
> +    nf_cleanup_port_binding hv1 hv2 hv2 "$nf_ports"
> +
> +    # Test 5: src on one node, dst on another, NF on a 3rd one
> +    echo "$mode: Network function vtap outbound with three nodes"
> +    nf_create_port_binding hv1 hv2 hv3 "$nf_ports"
> +
> +    packet_mirroring_test hv1 hv2 hv3 sw0
> +
> +    nf_cleanup_port_binding hv1 hv2 hv3 "$nf_ports"
> +}
> +
> +test_nf_vtap_with_multinodes_outbound overlay
> +
> +# Tests for VLAN network
> +# Add localnet port to make it a VLAN backed LS
> +check ovn-nbctl lsp-add-localnet-port sw0 ln0 phys
> +check ovn-nbctl set logical_switch_port ln0 tag_request=100
> +
> +test_nf_vtap_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-vtap options
> +for i in 1 2; do
> +    check ovn-nbctl lsp-del sw0-p$i
> +done
> +check ovn-nbctl lsp-del sw0-nf-vtap
> +check ovn-nbctl ls-del sw0
> +check ovn-nbctl --wait=hv sync
> +
> +# hv1/hv2/hv3 + main are already down (last nf_cleanup_port_binding).
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +])
> +
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([Network function vtap packet flow - inbound])
> +AT_KEYWORDS([ovn])
> +ovn_start
> +
> +NF_HELPERS
> +
> +# Create logical topology. One LS sw0 with 3 ports.
> +# to-lport ACL rule mirrors request packets from sw0-p2 to sw0-p1 via vtap 
> NF port sw0-nf-vtap.
> +# In vtap mode, traffic is mirrored (copied) to NF, original packets still 
> reach destination.
> +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"
> +    done
> +    check ovn-nbctl lsp-add $sw $sw-nf-vtap -- lsp-set-addresses $sw-nf-vtap 
> "f0:00:00:00:01:01"
> +    check ovn-nbctl set logical_switch_port $sw-nf-vtap \
> +        options:receive_multicast=false options:lsp_learn_fdb=false \
> +        options:is-nf=true
> +    check ovn-nbctl nf-add nf0 1 $sw-nf-vtap
> +    check ovn-nbctl nfg-add nfg0 1 vtap 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
> +}
> +
> +create_logical_topology sw0
> +
> +packet_mirroring_test() {
> +    local hvp1=$1 hvp2=$2 hvnf=$3
> +
> +    # Test 1: Inject ICMP request from sw0-p2 to sw0-p1
> +    # In vtap mode: single packet should be mirrored to NF AND reach sw0-p1
> +    nf_test_icmp_mirrored sw0-p2 "f0:00:00:00:00:02" "f0:00:00:00:00:01" 
> "192.168.0.12" "192.168.0.11" 8 \
> +        vif-nf vif1 $hvp2 $hvnf $hvp1
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvnf/vif-nf-tx.pcap], 
> [$hvnf-vif-nf.expected])
> +    OVN_CHECK_PACKETS_REMOVE_BROADCAST([$hvp1/vif1-tx.pcap], 
> [$hvp1-vif1.expected])
> +
> +    # Test 2: Reverse direction - ICMP request from sw0-p1 to sw0-p2
> +    # No mirroring expected (ACL only matches to-lport on pg0 which contains 
> sw0-p1)
> +    nf_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])
> +}
> +
> +test_nf_vtap_with_multinodes_inbound() {
> +    mode=$1
> +    nf_ports="vif-nf:sw0-nf-vtap"
> +    # Test 1: Bind all 3 ports to one node
> +    echo "$mode: Network function vtap inbound with single node"
> +    nf_create_port_binding hv1 hv1 hv1 "$nf_ports"
> +
> +    packet_mirroring_test hv1 hv1 hv1 sw0
> +
> +    nf_cleanup_port_binding hv1 hv1 hv1 "$nf_ports"
> +
> +    # Test 2: src & dst ports on one node, NF on another node
> +    echo "$mode: Network function vtap inbound with two nodes - nf separate"
> +    nf_create_port_binding hv1 hv1 hv2 "$nf_ports"
> +
> +    packet_mirroring_test hv1 hv1 hv2 sw0
> +
> +    nf_cleanup_port_binding hv1 hv1 hv2 "$nf_ports"
> +
> +    # Test 3: dst and nf on one node, src on a second node
> +    echo "$mode: Network function vtap inbound with two nodes - nf with dst"
> +    nf_create_port_binding hv1 hv2 hv1 "$nf_ports"
> +
> +    packet_mirroring_test hv1 hv2 hv1 sw0
> +
> +    nf_cleanup_port_binding hv1 hv2 hv1 "$nf_ports"
> +
> +    # Test 4: dst on one node, nf & src on a second node
> +    echo "$mode: Network function vtap inbound with two nodes - nf with src"
> +    nf_create_port_binding hv1 hv2 hv2 "$nf_ports"
> +
> +    packet_mirroring_test hv1 hv2 hv2 sw0
> +
> +    nf_cleanup_port_binding hv1 hv2 hv2 "$nf_ports"
> +
> +    # Test 5: src on one node, dst on another, NF on a 3rd one
> +    echo "$mode: Network function vtap inbound with three nodes"
> +    nf_create_port_binding hv1 hv2 hv3 "$nf_ports"
> +
> +    packet_mirroring_test hv1 hv2 hv3 sw0
> +
> +    nf_cleanup_port_binding hv1 hv2 hv3 "$nf_ports"
> +}
> +
> +test_nf_vtap_with_multinodes_inbound overlay
> +
> +# Tests for VLAN network
> +# Add localnet port to make it a VLAN backed LS
> +check ovn-nbctl lsp-add-localnet-port sw0 ln0 phys
> +check ovn-nbctl set logical_switch_port ln0 tag_request=100
> +
> +test_nf_vtap_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-vtap options
> +for i in 1 2; do
> +    check ovn-nbctl lsp-del sw0-p$i
> +done
> +check ovn-nbctl lsp-del sw0-nf-vtap
> +check ovn-nbctl ls-del sw0
> +check ovn-nbctl --wait=hv sync
> +
> +# hv1/hv2/hv3 + main are already down (last nf_cleanup_port_binding).
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +])
> +
>  OVN_FOR_EACH_NORTHD([
>  AT_SETUP([Unicast ARP when proxy ARP is configured])
>  CHECK_SCAPY
> diff --git a/tests/system-ovn.at b/tests/system-ovn.at
> index 17b4dcb3d..13bba22a3 100644
> --- a/tests/system-ovn.at
> +++ b/tests/system-ovn.at
> @@ -20036,7 +20036,7 @@ AT_CLEANUP
>  ])
>  
>  OVN_FOR_EACH_NORTHD([
> -AT_SETUP([Network Function])
> +AT_SETUP([Network Function - inline mode])
>  AT_SKIP_IF([test $HAVE_TCPDUMP = no])
>  ovn_start
>  OVS_TRAFFIC_VSWITCHD_START()
> @@ -20320,9 +20320,10 @@ AS_BOX([IPv6 Testing - Setup])
>  ip netns exec client ip addr del 192.168.1.10/24 dev client
>  ip netns exec server ip addr del 192.168.1.20/24 dev server
>  
> -# Add IPv6 addresses to client and server
> -ip netns exec client ip -6 addr add fd00:192:168:1::10/64 dev client
> -ip netns exec server ip -6 addr add fd00:192:168:1::20/64 dev server
> +# Add IPv6 addresses to client and server. Use 'nodad' to skip Duplicate
> +# Address Detection so the addresses are immediately usable.
> +ip netns exec client ip -6 addr add fd00:192:168:1::10/64 dev client nodad
> +ip netns exec server ip -6 addr add fd00:192:168:1::20/64 dev server nodad
>  
>  # Update service monitor IPs to IPv6
>  check ovn-nbctl set nb_global . options:svc_monitor_ip=fd00:169:254:100::10
> @@ -20355,6 +20356,282 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/.*error receiving.*/d
>  AT_CLEANUP
>  ])
>  
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([Network Function - vtap mode])
> +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.
> +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_daemon ovn-controller
> +
> +# Create namespaces: client, server, and nf (for vtap)
> +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-vtap, nf, br-int, "0", "f0:00:00:01:02:30")
> +ADD_VETH(nf-vtap2, nf, br-int, "0", "f0:00:00:01:02:40")
> +
> +# Create logical switch and ports
> +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 lsp-add sw0 nf-vtap
> +check ovn-nbctl set logical_switch_port nf-vtap 
> options:receive_multicast=false \
> +                                                 options:lsp_learn_fdb=false 
> \
> +                                                 options:is-nf=true
> +check ovn-nbctl lsp-add sw0 nf-vtap2
> +check ovn-nbctl set logical_switch_port nf-vtap2 
> options:receive_multicast=false \
> +                                                  
> options:lsp_learn_fdb=false \
> +                                                  options:is-nf=true
> +
> +AS_BOX([Setup: Create 2 NFs in vtap mode with health check])
> +
> +# Create NF0 with only inport (vtap mode)
> +check ovn-nbctl nf-add nf0 1 nf-vtap
> +nf0_uuid=$(fetch_column nb:network_function _uuid name=nf0)
> +
> +# Create NF1 with only inport (vtap mode)
> +check ovn-nbctl nf-add nf1 2 nf-vtap2
> +nf1_uuid=$(fetch_column nb:network_function _uuid name=nf1)
> +
> +# Create NFG with both NFs
> +check ovn-nbctl nfg-add nfg0 1 vtap nf0
> +nfg_uuid=$(fetch_column nb:network_function_group _uuid name=nfg0)
> +check ovn-nbctl nfg-add-nf nfg0 nf1
> +
> +# Set monitor IPs for health check
> +check ovn-nbctl set nb_global . options:svc_monitor_ip=169.254.100.10
> +check ovn-nbctl set nb_global . options:svc_monitor_ip_dst=169.254.100.11
> +
> +# Create health check configuration and assign to both NFs
> +AT_CHECK(
> +  [ovn-nbctl --wait=sb \
> +          -- --id=@hc create network_function_health_check 
> name=nf_health_cfg \
> +             options:interval=1 options:timeout=1 options:success_count=2 
> options:failure_count=2 \
> +          -- add network_function $nf0_uuid health_check @hc | uuidfilt], 
> [0], [<0>
> +])
> +nf_health_uuid=$(fetch_column nb:network_function_health_check _uuid 
> name=nf_health_cfg)
> +check ovn-nbctl set network_function $nf1_uuid health_check=$nf_health_uuid
> +
> +# Create port group and ACLs for both from-lport and to-lport traffic 
> mirroring
> +check ovn-nbctl pg-add pg0 client
> +check ovn-nbctl acl-add pg0 from-lport 1001 "inport == @pg0 && ip4.dst == 
> 192.168.1.20" allow-related nfg0
> +check ovn-nbctl acl-add pg0 to-lport 1002 "outport == @pg0 && ip4.src == 
> 192.168.1.20" allow-related nfg0
> +
> +check ovn-nbctl --wait=hv sync
> +
> +# Bring up NF ports
> +NS_CHECK_EXEC([nf], [ip link set dev nf-vtap up])
> +NS_CHECK_EXEC([nf], [ip link set dev nf-vtap2 up])
> +
> +# Helper function to simulate NF down by removing iface-id
> +nf_down() {
> +    local port=$1
> +    ovs-vsctl remove interface ovs-$port external-ids iface-id
> +}
> +
> +# Helper function to simulate NF up by restoring iface-id
> +nf_up() {
> +    local port=$1
> +    ovs-vsctl set interface ovs-$port external-ids:iface-id="$port"
> +}
> +
> +# Returns 0 if the given address contains ':', i.e. is an IPv6 address.
> +is_v6_addr() {
> +    test "${1#*:}" != "$1"
> +}
> +
> +validate_nf_vtap_with_traffic() {
> +    client_ns=$1; server_ns=$2; sip=$3; direction=$4
> +
> +    # Filter by IP to ignore unrelated ICMP/ND traffic.
> +    local proto="-4"
> +    local pcap_filter="icmp and ip host $sip"
> +    if is_v6_addr "$sip"; then
> +        proto="-6"
> +        pcap_filter="icmp6 and ip6 host $sip"
> +    fi
> +
> +    AS_BOX([$direction: Verify traffic mirroring to nf0 when nf0 is active])
> +
> +    # Ensure nf0 is up, nf1 is down
> +    nf_up nf-vtap
> +    nf_down nf-vtap2
> +    check ovn-nbctl set network_function_group $nfg_uuid fallback=fail-close
> +    check ovn-nbctl --wait=hv sync
> +
> +    # Wait for the health check to mark nf0 as the active NF.
> +    wait_column "$nf0_uuid" nb:network_function_group \
> +        network_function_active name=nfg0
> +
> +    NETNS_START_TCPDUMP([nf], [-nvvv -i nf-vtap $pcap_filter], 
> [tcpdump-nf-vtap])
> +
> +    # Send 5 ICMP packets - in vtap mode, traffic should be mirrored AND 
> reach destination
> +    # NF should see 10 packets: 5 echo requests (from-lport) + 5 echo 
> replies (to-lport)
> +    NS_CHECK_EXEC([$client_ns], [ping $proto -c 5 -i 0.3 $sip], [0], 
> [ignore])
> +
> +    # Verify all mirrored packets were captured (5 requests + 5 replies = 10 
> packets)
> +    OVS_WAIT_UNTIL([
> +        n=$(cat tcpdump-nf-vtap.tcpdump | wc -l)
> +        test "$n" -ge 10
> +    ])
> +
> +    kill $(cat tcpdump-nf-vtap.pid) 2>/dev/null || true
> +
> +    AS_BOX([$direction: Verify failover - traffic mirroring to nf1 when nf0 
> is down])
> +
> +    # Bring nf0 down, nf1 up (failover)
> +    nf_down nf-vtap
> +    nf_up nf-vtap2
> +    check ovn-nbctl --wait=hv sync
> +
> +    # Wait for the health check to fail over to nf1.
> +    wait_column "$nf1_uuid" nb:network_function_group \
> +        network_function_active name=nfg0
> +
> +    NETNS_START_TCPDUMP([nf], [-nvvv -i nf-vtap2 $pcap_filter], 
> [tcpdump-nf-vtap])
> +
> +    # Send 5 ICMP packets - should now be mirrored to nf1
> +    # NF should see 10 packets: 5 echo requests + 5 echo replies
> +    NS_CHECK_EXEC([$client_ns], [ping $proto -c 5 -i 0.3 $sip], [0], 
> [ignore])
> +
> +    # Verify all mirrored packets were captured (5 requests + 5 replies = 10 
> packets)
> +    OVS_WAIT_UNTIL([
> +        n=$(cat tcpdump-nf-vtap.tcpdump | wc -l)
> +        test "$n" -ge 10
> +    ])
> +
> +    kill $(cat tcpdump-nf-vtap.pid) 2>/dev/null || true
> +
> +    AS_BOX([$direction: Verify fallback - traffic mirroring back to nf0 when 
> nf0 recovers])
> +
> +    # Bring nf0 back up and nf1 down (fallback to nf0)
> +    nf_up nf-vtap
> +    nf_down nf-vtap2
> +    check ovn-nbctl --wait=hv sync
> +
> +    # Wait for the health check to fall back to nf0.
> +    wait_column "$nf0_uuid" nb:network_function_group \
> +        network_function_active name=nfg0
> +
> +    NETNS_START_TCPDUMP([nf], [-nvvv -i nf-vtap $pcap_filter], 
> [tcpdump-nf-vtap])
> +
> +    # Send 5 ICMP packets - should be mirrored back to nf0
> +    # NF should see 10 packets: 5 echo requests + 5 echo replies
> +    NS_CHECK_EXEC([$client_ns], [ping $proto -c 5 -i 0.3 $sip], [0], 
> [ignore])
> +
> +    # Verify all mirrored packets were captured (5 requests + 5 replies = 10 
> packets)
> +    OVS_WAIT_UNTIL([
> +        n=$(cat tcpdump-nf-vtap.tcpdump | wc -l)
> +        test "$n" -ge 10
> +    ])
> +
> +    kill $(cat tcpdump-nf-vtap.pid) 2>/dev/null || true
> +
> +    AS_BOX([$direction: Verify fail-close - traffic flows but no mirroring 
> when both NFs are down])
> +
> +    # Bring both NFs down with fail-close
> +    nf_down nf-vtap
> +    nf_down nf-vtap2
> +    check ovn-nbctl set network_function_group $nfg_uuid fallback=fail-close
> +    check ovn-nbctl --wait=hv sync
> +
> +    # Wait for the health check to mark the NFG as having no active NF.
> +    wait_column "" nb:network_function_group \
> +        network_function_active name=nfg0
> +
> +    NETNS_START_TCPDUMP([nf], [-nvvv -i nf-vtap $pcap_filter], 
> [tcpdump-nf-vtap])
> +
> +    # Send ICMP packets - in vtap mode, traffic still flows (mirroring is 
> separate from forwarding)
> +    # but no packets should be mirrored to NF with fail-close
> +    NS_CHECK_EXEC([$client_ns], [ping $proto -c 3 -i 0.3 $sip], [0], 
> [ignore])
> +
> +    # Verify no packets were mirrored (tcpdump should capture nothing).
> +    # tcpdump is started with '-l' (line-buffered) by NETNS_START_TCPDUMP, so
> +    # any captured packet would already be in the file by the time ping
> +    # finishes.
> +    AT_CHECK([cat tcpdump-nf-vtap.tcpdump | wc -l], [0], [0
> +])
> +
> +    kill $(cat tcpdump-nf-vtap.pid) 2>/dev/null || true
> +
> +    AS_BOX([$direction: Verify fail-open - traffic flows with no mirroring 
> when both NFs are down])
> +
> +    # Set fail-open mode - in vtap mode, this behaves same as fail-close for 
> traffic flow
> +    # (traffic always flows), difference is in ACL behavior
> +    check ovn-nbctl set network_function_group $nfg_uuid fallback=fail-open
> +    check ovn-nbctl --wait=hv sync
> +
> +    # Send ICMP packets - traffic should flow
> +    NS_CHECK_EXEC([$client_ns], [ping $proto -c 3 -i 0.3 $sip], [0], 
> [ignore])
> +}
> +
> +AS_BOX([IPv4 Testing - Inbound traffic])
> +validate_nf_vtap_with_traffic "client" "server" "192.168.1.20" "Inbound"
> +
> +AS_BOX([IPv4 Testing - Outbound traffic])
> +validate_nf_vtap_with_traffic "server" "client" "192.168.1.10" "Outbound"
> +
> +AS_BOX([IPv6 Testing - Setup])
> +
> +# Remove IPv4 addresses from namespaces
> +ip netns exec client ip addr del 192.168.1.10/24 dev client
> +ip netns exec server ip addr del 192.168.1.20/24 dev server
> +
> +# Add IPv6 addresses to client and server. Use 'nodad' to skip Duplicate
> +# Address Detection so the addresses are immediately usable.
> +ip netns exec client ip -6 addr add fd00:192:168:1::10/64 dev client nodad
> +ip netns exec server ip -6 addr add fd00:192:168:1::20/64 dev server nodad
> +
> +# Update service monitor IPs to IPv6 for health check
> +check ovn-nbctl set nb_global . options:svc_monitor_ip=fd00:169:254:100::10
> +check ovn-nbctl set nb_global . 
> options:svc_monitor_ip_dst=fd00:169:254:100::11
> +
> +# Configure IPv6-only addresses on logical ports
> +check ovn-nbctl lsp-set-addresses client "f0:00:00:01:02:10 
> fd00:192:168:1::10"
> +check ovn-nbctl lsp-set-addresses server "f0:00:00:01:02:20 
> fd00:192:168:1::20"
> +
> +# Add IPv6 ACLs
> +check ovn-nbctl acl-add pg0 from-lport 1003 "inport == @pg0 && ip6.dst == 
> fd00:192:168:1::20" allow-related nfg0
> +check ovn-nbctl acl-add pg0 to-lport 1004 "outport == @pg0 && ip6.src == 
> fd00:192:168:1::20" allow-related nfg0
> +
> +check ovn-nbctl --wait=hv sync
> +
> +AS_BOX([IPv6 Testing - Inbound traffic])
> +validate_nf_vtap_with_traffic "client" "server" "fd00:192:168:1::20" "IPv6 
> Inbound"
> +
> +AS_BOX([IPv6 Testing - Outbound traffic])
> +validate_nf_vtap_with_traffic "server" "client" "fd00:192:168:1::10" "IPv6 
> Outbound"
> +
> +# Restore NF iface-ids before cleanup
> +nf_up nf-vtap
> +nf_up nf-vtap2
> +check ovn-nbctl --wait=hv sync
> +
> +OVN_CLEANUP_CONTROLLER([hv1])
> +OVN_CLEANUP_NORTHD
> +
> +as
> +OVS_TRAFFIC_VSWITCHD_STOP(["/.*error receiving.*/d
> +/failed to query port patch-.*/d
> +/.*terminating with signal 15.*/d"])
> +AT_CLEANUP
> +])
> +
>  OVN_FOR_EACH_NORTHD([
>  AT_SETUP([dynamic-routing - BGP learned routes])
>  

I took care of the minor things I found above and applied the patch to main.

Regards,
Dumitru

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

Reply via email to