On 7/14/26 10:21 AM, Naveen Yerramneni wrote:
> A Network Function (NF) in inline mode redirects matched traffic
> through a service VM. When the redirected traffic is IP
> unknown-unicast (destination MAC not yet in the FDB), the packet
> coming back from the NF is re-flooded by the switch, because the
> destination MAC is still unknown. That re-flood produces a copy on
> the same port the packet originally arrived on, causing MAC flaps and
> potential L2 loops.
>
> Following are the example packet flows.
>
> Example 1, VLAN switch (MAC flap):
>
> Topology
> VM1, VM2 and the NF are on the same logical switch (LS), which is
> VLAN-backed (localnet port on every node). VM1 is on N1, VM2 on
> N2 (its port has "unknown" in addresses and a to-lport ACL that
> redirects to the inline NF on N3).
>
> Flow
> 1. VM1 sends pkt to dst MAC X (not in FDB).
> 2. The LS floods the pkt; on N1 the copy exits the localnet port
> and the TOR floods it to N2.
> 3. On N2 the pkt ingresses on localnet; the LS floods it and the
> copy reaches VM2 (unknown-addr).
> 4. The ACL redirects the pkt to the NF on N3; the NF returns it
> to N2.
> 5. X is still not in the FDB, so the LS floods again; on N2 one
> copy exits the localnet port.
>
> Result
> The TOR now sees VM1's source MAC on N2's port, but it had just
> learned VM1's MAC on N1's port, so the MAC flaps on the TOR.
>
> Example 2, VLAN switch with two protected VMs (loop):
>
> Topology
> As above, plus VM3 (on N3, same LS) also has "unknown" in
> addresses and is also NF-protected.
>
> Flow
> 1-5 as above; in parallel, the original flood also reaches N3 via
> the TOR, where it is redirected to N3's NF, returns, and the
> LS re-floods it out N3's localnet port.
> 6. N3's re-flood reaches N2 via the TOR; the LS floods it on N2,
> the copy hits VM2 (unknown-addr), is redirected to the NF, and
> re-floods out N2's localnet port.
> 7. That re-flood reaches N3 via the TOR; the LS floods it on N3,
> the copy hits VM3 (also unknown-addr), is redirected to the
> NF, re-floods out localnet, ...
>
> Result
> The pkt keeps bouncing between N2 and N3 via the TOR, i.e. an
> L2 loop, on top of continuous MAC flaps. The loop persists as
> long as X stays unknown.
>
> Example 3, overlay switch (copy reflected to source port):
>
> Topology
> VM1, VM2 and the NF are on the same LS and all on N1. VM1 and
> VM2 both have "unknown" in addresses; VM2's to-lport ACL
> redirects to the inline NF.
>
> Flow
> 1. VM1 sends pkt to dst MAC X (not in FDB).
> 2. The LS floods to MC_UNKNOWN members on N1; one copy goes to
> VM2.
> 3. VM2's ACL redirects its copy to the NF; the NF returns it to
> N1.
> 4. X is still not in the FDB, so the LS floods to MC_UNKNOWN
> again; one copy is headed back out VM1's own port.
>
> Result
> VM1 receives a copy of the packet it just sent (reflected to the
> source port).
>
> Fix:
>
> Use the nf_learn_orig_src_port() / nf_lookup_orig_src_port() actions
> from the previous commit to remember the original ingress port and
> drop the copy if it is about to be sent back out of that port.
>
> - Add a logical flag flags.inport_in_mc_unknown (bit 26) that
> marks packets entering on an MC_UNKNOWN-member port (i.e. one
> with "unknown" in addresses and receive_multicast not disabled,
> the only ports that can both originate and receive
> unknown-unicast floods). northd sets it in ls_in_lookup_fdb,
> piggybacking on the existing FDB-learn or flags.localnet flow
> where possible, and emits a dedicated priority-50 flow for the
> remaining MC_UNKNOWN members (ex: ports where
> lsp_learn_fdb=false).
>
> - In the NF redirect stage (ls_in_nf for a from-lport ACL,
> ls_out_nf for a to-lport ACL), on a switch with an inline NF group
> on an ACL, learn the original ingress port
> (nf_learn_orig_src_port()) for unicast IP packets that entered on
> an MC_UNKNOWN-member port (flags.inport_in_mc_unknown set). The
> flag is set in ls_in_lookup_fdb and carried in MFF_LOG_FLAGS into
> the egress pipeline, so it is still available in ls_out_nf. Two
> flows per IP version:
> * Priority 100: learn, then redirect to the NF, when the ACL
> selected the packet for redirect. The existing NF-port and
> multicast skip flows move up to priority 110 so multicast is
> skipped before this flow, which therefore needs no !eth.mcast
> match.
> * Priority 50 (overlay switches only, in ls_in_nf): learn, then
> continue to the next stage, when the ACL did not redirect the
> packet (REGBIT_NF_ENABLED == 0). On an overlay switch the
> redirecting port and the source port can be on different nodes,
> so the priority-100 learn would land on a different node than
> the lookup; learning here, on the source port's own ingress
> node, keeps them co-located. VLAN-backed switches do not need
> this: the re-flood returns to the redirecting node, where the
> priority-100 learn already ran.
>
> - On the post-NF return path run
> REGBIT_NF_LOOKUP_HIT = nf_lookup_orig_src_port() on both NF
> ports. The existing ls_out_pre_acl skip-stages flow does it for
> the input_port (priority bumped 110->115 to win over the
> priority-110 conntrack skip flow); a new priority-2 flow in
> ls_out_nf does it for the output_port (packets redirected from
> the ingress pipeline, e.g. a from-lport request re-flooded after
> the NF, doing a fresh egress after conntrack).
>
> - A new priority-110 flow in ls_out_check_port_sec drops packets
> with REGBIT_NF_LOOKUP_HIT == 1, i.e. the copies about to be
> sent back out of the port they originally arrived on.
>
> On VLAN-backed switches the re-flood returns to the redirecting node
> and egresses the shared localnet port, so the learn and the lookup
> always run on that node: dropping the copy headed back out that port
> removes both the MAC flap and the underlay L2 loop.
>
> On overlay switches the copy headed back to the source port is always
> dropped, as in example 3: the learn runs on the source port's ingress
> and the lookup on its egress, both on the source port's node.
>
> All new flows are gated on the switch having an inline NF group on
> an ACL.
>
> Signed-off-by: Naveen Yerramneni <[email protected]>
> Acked-by: Aditya Mehakare <[email protected]>
> Fixes: 8e2d6fa14804 ("northd, tests: Network Function insertion logical flow
> programming.")
> CC: Sragdhara Datta Chaudhuri <[email protected]>
> Assisted-by: Claude Opus 4.7, Cursor
> ---
Hi Naveen,
> NEWS | 3 +
> include/ovn/logical-fields.h | 9 +
> lib/logical-fields.c | 5 +
> northd/northd.c | 257 ++++++++++++++++++-----
> northd/northd.h | 19 ++
> ovn-sb.xml | 8 +
> tests/ovn-northd.at | 393 +++++++++++++++++++++++++++++------
> tests/ovn.at | 114 +++++++++-
> 8 files changed, 690 insertions(+), 118 deletions(-)
>
> diff --git a/NEWS b/NEWS
> index 384e30820..9a7f03f19 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -68,6 +68,9 @@ 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.
> + - Fixed MAC flaps and L2 loops caused by inline Network_Function
> + redirection of unknown-unicast IP traffic, where the copy returning
> + from the NF could be re-flooded out of the original ingress port.
>
This is arguably a bug fix, it doesn't really need a NEWS item. OTOH,
if we ever need to backport this we need to ensure that northd doesn't
use the new actions if not all ovn-controller are running an updated
version. I presume your goal is to just fix this behavior on versions
>= 26.09.0.
> OVN v26.03.0 - xxx xx xxxx
> --------------------------
> diff --git a/include/ovn/logical-fields.h b/include/ovn/logical-fields.h
> index b2f1af64a..e3dddd0c4 100644
> --- a/include/ovn/logical-fields.h
> +++ b/include/ovn/logical-fields.h
> @@ -141,6 +141,7 @@ enum mff_log_flags_bits {
> MLF_RECIRC_BIT = 24,
> MLF_EVPN_LOOKUP_BIT = 25,
> MLF_NF_LOOKUP_HIT_BIT = 26,
> + MLF_INPORT_IN_MC_UNKNOWN_BIT = 27,
> MLF_NETWORK_ID_START_BIT = 28,
> MLF_NETWORK_ID_END_BIT = 31,
> };
> @@ -226,6 +227,14 @@ enum mff_log_flags {
> * MAC flaps / L2 loops after network function redirection. */
> MLF_NF_LOOKUP_HIT = (1 << MLF_NF_LOOKUP_HIT_BIT),
>
> + /* Indicate that the packet entered the logical switch on a port that
> + * is a member of MC_UNKNOWN (i.e. the port has "unknown" in its
> + * addresses and "receive_multicast" is not disabled, so it can both
> + * receive and originate unknown-unicast floods). Used by the inline
> + * network function loop prevention to learn the original source port
> + * before redirection. */
> + MLF_INPORT_IN_MC_UNKNOWN = (1 << MLF_INPORT_IN_MC_UNKNOWN_BIT),
> +
> /* Assign network ID to packet to choose correct network for snat when
> * lb_force_snat_ip=router_ip. */
> MLF_NETWORK_ID = (OVN_MAX_NETWORK_ID << MLF_NETWORK_ID_START_BIT),
> diff --git a/lib/logical-fields.c b/lib/logical-fields.c
> index 35d9a2e22..8295d71f4 100644
> --- a/lib/logical-fields.c
> +++ b/lib/logical-fields.c
> @@ -188,6 +188,11 @@ ovn_init_symtab(struct shash *symtab)
> snprintf(flags_str, sizeof flags_str, "flags[%d]", MLF_PKT_SAMPLED_BIT);
> expr_symtab_add_subfield(symtab, "flags.pkt_sampled", NULL, flags_str);
>
> + snprintf(flags_str, sizeof flags_str, "flags[%d]",
> + MLF_INPORT_IN_MC_UNKNOWN_BIT);
> + expr_symtab_add_subfield(symtab, "flags.inport_in_mc_unknown", NULL,
> + flags_str);
> +
> /* Connection tracking state. */
> expr_symtab_add_field_scoped(symtab, "ct_mark", MFF_CT_MARK, NULL, false,
> WR_CT_COMMIT);
> diff --git a/northd/northd.c b/northd/northd.c
> index 3a4afa063..65ed1edaf 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -176,6 +176,10 @@ static bool vxlan_mode;
> #define REGBIT_NF_ENABLED "reg8[21]"
> #define REGBIT_NF_ORIG_DIR "reg8[22]"
> #define REGBIT_NF_EGRESS_LOOPBACK "reg8[23]"
> +/* Set when a packet returning from an inline network function is about to
> + * be sent back out of the port it originally arrived on; such loopback
> + * copies are dropped. */
> +#define REGBIT_NF_LOOKUP_HIT "reg8[24]"
> /* Register to store the network function group id */
> #define REG_NF_GROUP_ID "reg0[22..29]"
> /* REG_NF_ID overrides REG_NF_GROUP_ID in the pre_network_function stage. */
> @@ -315,6 +319,8 @@ static const char *reg_ct_state[] = {
> * | | REGBIT_NF_{ENABLED/ORIG_DIR/ | G |
> |
> * | | EGRESS_LOOPBACK} | 4 |
> |
> * | | (>= ACL_EVAL* && <= NF*) | |
> |
> + * | | REGBIT_NF_LOOKUP_HIT | |
> |
> + * | | (>= OUT_PRE_ACL && <= OUT_CHECK_PORT_SEC)| |
> |
> * +----+----------------------------------------------+
> +-----------------------------------+
> * | R9 | OBS_POINT_ID_EST | |
> |
> * | | (>= ACL_EVAL* && <= ACL_ACTION*) | |
> |
> @@ -6386,6 +6392,19 @@ build_lswitch_port_sec_op(struct ovn_port *op, struct
> lflow_table *lflows,
> }
> }
>
> +/* True if 'op' emits the FDB-learn (LOOKUP_FDB / PUT_FDB) flows. */
> +static bool
> +lsp_emits_fdb_learn_lflow(const struct ovn_port *op)
> +{
> + if (op->lsp_has_port_sec || !op->has_unknown) {
> + return false;
> + }
> + return lsp_is_remote(op->nbsp)
> + || (!strcmp(op->nbsp->type, "") && lsp_can_learn_mac(op->nbsp))
> + || lsp_is_switch(op->nbsp)
> + || (lsp_is_localnet(op->nbsp) && localnet_can_learn_mac(op->nbsp));
> +}
> +
> static void
> build_lswitch_learn_fdb_op(
> struct ovn_port *op, struct lflow_table *lflows,
> @@ -6393,36 +6412,35 @@ build_lswitch_learn_fdb_op(
> {
> ovs_assert(op->nbsp);
>
> - if (op->lsp_has_port_sec || !op->has_unknown) {
> + if (!lsp_emits_fdb_learn_lflow(op)) {
> return;
> }
>
> bool remote = lsp_is_remote(op->nbsp);
>
> - if (remote || (!strcmp(op->nbsp->type, "") &&
> lsp_can_learn_mac(op->nbsp))
> - || lsp_is_switch(op->nbsp)
> - || (lsp_is_localnet(op->nbsp) && localnet_can_learn_mac(op->nbsp))) {
> - ds_clear(match);
> - ds_clear(actions);
> - ds_put_format(match, "inport == %s", op->json_key);
> - if (lsp_is_localnet(op->nbsp)) {
> - ds_put_cstr(actions, "flags.localnet = 1; ");
> - }
> - ds_put_format(actions, REGBIT_LKUP_FDB
> - " = lookup_fdb(inport, eth.src); next;");
> - ovn_lflow_add(lflows, op->od, remote ? S_SWITCH_OUT_LOOKUP_FDB
> - : S_SWITCH_IN_LOOKUP_FDB,
> - 100, ds_cstr(match), ds_cstr(actions), op->lflow_ref,
> - WITH_IO_PORT(op->key), WITH_HINT(&op->nbsp->header_));
> -
> - ds_put_cstr(match, " && "REGBIT_LKUP_FDB" == 0");
> - ds_clear(actions);
> - ds_put_cstr(actions, "put_fdb(inport, eth.src); next;");
> - ovn_lflow_add(lflows, op->od, remote ? S_SWITCH_OUT_PUT_FDB
> - : S_SWITCH_IN_PUT_FDB,
> - 100, ds_cstr(match), ds_cstr(actions), op->lflow_ref,
> - WITH_IO_PORT(op->key), WITH_HINT(&op->nbsp->header_));
> + ds_clear(match);
> + ds_clear(actions);
> + ds_put_format(match, "inport == %s", op->json_key);
> + if (lsp_is_localnet(op->nbsp)) {
> + ds_put_cstr(actions, "flags.localnet = 1; ");
> }
> + if (!remote && lsp_is_mc_unknown_ingress_member(op)) {
lsp_is_mc_unknown_ingress_member() already checks that the port is not
remote.
Also why skip remote ports?
But the larger question is: we're messing up the code quite a bit with
this "is traffic coming from a LSP with <unknown> addresses set but that
isn't a remote port" condition everywhere. What's the downside to using
the learn action all the time?
If we _really_ need this condition, why not just implement it through
physical.c flows like we do for MLF_RX_FROM_TUNNEL_BIT for example?
All these new logical flow additions in various places and with various
combinations of conditions are very hard to maintain on the long term.
> + ds_put_cstr(actions, "flags.inport_in_mc_unknown = 1; ");
> + }
> + ds_put_format(actions, REGBIT_LKUP_FDB
> + " = lookup_fdb(inport, eth.src); next;");
> + ovn_lflow_add(lflows, op->od, remote ? S_SWITCH_OUT_LOOKUP_FDB
> + : S_SWITCH_IN_LOOKUP_FDB,
> + 100, ds_cstr(match), ds_cstr(actions), op->lflow_ref,
> + WITH_IO_PORT(op->key), WITH_HINT(&op->nbsp->header_));
> +
> + ds_put_cstr(match, " && "REGBIT_LKUP_FDB" == 0");
> + ds_clear(actions);
> + ds_put_cstr(actions, "put_fdb(inport, eth.src); next;");
> + ovn_lflow_add(lflows, op->od, remote ? S_SWITCH_OUT_PUT_FDB
> + : S_SWITCH_IN_PUT_FDB,
> + 100, ds_cstr(match), ds_cstr(actions), op->lflow_ref,
> + WITH_IO_PORT(op->key), WITH_HINT(&op->nbsp->header_));
> }
>
> static void
> @@ -10445,20 +10463,52 @@ build_arp_nd_service_monitor_lflow(const char
> *svc_monitor_mac,
>
> /* Ingress table: Lookup FDB. Set flags.localnet for packets arriving from
> * localnet ports so that downstream stages (e.g., ARP/ND responder) can
> - * condition their behavior on whether the packet came from localnet. */
> + * condition their behavior on whether the packet came from localnet. Also
> + * set flags.inport_in_mc_unknown for MC_UNKNOWN-member localnet ports. */
> static void
> build_lswitch_from_localnet_op(struct ovn_port *op,
> struct lflow_table *lflows,
> - struct ds *match)
> + struct ds *actions, struct ds *match)
> {
> ovs_assert(op->nbsp);
> if (!lsp_is_localnet(op->nbsp)) {
> return;
> }
> ds_clear(match);
> + ds_clear(actions);
> ds_put_format(match, "inport == %s", op->json_key);
> + ds_put_cstr(actions, "flags.localnet = 1; ");
> + if (lsp_is_mc_unknown_ingress_member(op)) {
> + ds_put_cstr(actions, "flags.inport_in_mc_unknown = 1; ");
> + }
> + ds_put_cstr(actions, "next;");
> ovn_lflow_add(lflows, op->od, S_SWITCH_IN_LOOKUP_FDB, 50,
> - ds_cstr(match), "flags.localnet = 1; next;",
> + ds_cstr(match), ds_cstr(actions),
> + op->lflow_ref, WITH_IO_PORT(op->key),
> + WITH_HINT(&op->nbsp->header_));
> +}
> +
> +/* Ingress table: Lookup FDB. Set flags.inport_in_mc_unknown for
> + * MC_UNKNOWN-member ports not already covered by the FDB-learn or
> + * localnet flows. */
> +static void
> +build_lswitch_set_inport_in_mc_unknown_op(struct ovn_port *op,
> + struct lflow_table *lflows,
> + struct ds *match)
> +{
> + ovs_assert(op->nbsp);
> + if (!lsp_is_mc_unknown_ingress_member(op)) {
> + return;
> + }
> + if (lsp_emits_fdb_learn_lflow(op) || lsp_is_localnet(op->nbsp)) {
> + return;
> + }
Like this.. this one is very hard to maintain on the long term.
> +
> + ds_clear(match);
> + ds_put_format(match, "inport == %s", op->json_key);
> + ovn_lflow_add(lflows, op->od, S_SWITCH_IN_LOOKUP_FDB, 50,
> + ds_cstr(match),
> + "flags.inport_in_mc_unknown = 1; next;",
> op->lflow_ref, WITH_IO_PORT(op->key),
> WITH_HINT(&op->nbsp->header_));
> }
> @@ -19310,6 +19360,61 @@ network_function_configure_fail_open_flows(struct
> lflow_table *lflows,
> ds_destroy(&match);
> }
>
> +/* Emit IPv4 and IPv6 nf_learn_orig_src_port() flows for flagged IP packets.
> + * 'extra_match' is ANDed onto the match if non-NULL; 'action_suffix' runs
> + * after the learn. */
> +static void
> +build_nf_learn_orig_src_port_flows(struct lflow_table *lflows,
> + const struct ovn_datapath *od,
> + const struct ovn_stage *stage,
> + uint16_t priority,
> + const char *extra_match,
> + const char *action_suffix,
> + struct lflow_ref *lflow_ref)
> +{
> + for (int ipv6 = 0; ipv6 <= 1; ipv6++) {
IMO this is not so nice.
I'd just duplicate the code instead of obfuscating it like this.
> + struct ds match = DS_EMPTY_INITIALIZER;
> + struct ds action = DS_EMPTY_INITIALIZER;
> +
> + ds_put_cstr(&match, ipv6 ? "ip6" : "ip4");
> + ds_put_cstr(&match, " && flags.inport_in_mc_unknown == 1");
> + if (extra_match) {
> + ds_put_format(&match, " && %s", extra_match);
> + }
> + ds_put_format(&action, "nf_learn_orig_src_port(ipv6 = %s); %s",
> + ipv6 ? "true" : "false", action_suffix);
> + ovn_lflow_add(lflows, od, stage, priority, ds_cstr(&match),
> + ds_cstr(&action), lflow_ref);
> + ds_destroy(&match);
> + ds_destroy(&action);
> + }
> +}
> +
> +/* Emit a post-NF nf_lookup_orig_src_port() flow for packets re-entering on
> + * 'port'. A hit sets REGBIT_NF_LOOKUP_HIT, which ls_out_check_port_sec
> + * drops. 'action_suffix' runs after the lookup. */
> +static void
> +build_nf_lookup_orig_src_port_flow(struct lflow_table *lflows,
> + const struct ovn_datapath *od,
> + const struct ovn_stage *stage,
> + uint16_t priority,
> + const struct ovn_port *port,
> + const char *action_suffix,
> + struct lflow_ref *lflow_ref)
> +{
> + struct ds match = DS_EMPTY_INITIALIZER;
> + struct ds action = DS_EMPTY_INITIALIZER;
> +
> + ds_put_format(&match, "inport == %s", port->json_key);
> + ds_put_format(&action,
> + REGBIT_NF_LOOKUP_HIT " = nf_lookup_orig_src_port(); %s",
> + action_suffix);
> + ovn_lflow_add(lflows, od, stage, priority, ds_cstr(&match),
> + ds_cstr(&action), lflow_ref);
> + ds_destroy(&match);
> + ds_destroy(&action);
> +}
> +
> static void
> consider_network_function(struct lflow_table *lflows,
> const struct ovn_datapath *od,
> @@ -19415,6 +19520,15 @@ consider_network_function(struct lflow_table *lflows,
> (uint8_t) nf->id);
> ovn_lflow_add(lflows, od, fwd_stage, 99, ds_cstr(&match),
> ds_cstr(&action), lflow_ref);
> +
> + /* Priority 100 flows in fwd_stage:
> + * Same as the priority-99 redirect above, but learn the original source
> + * port first (for flagged IP packets). Multicast is already skipped by
> + * the higher-priority (110) flow below, so it is not matched here. */
> + build_nf_learn_orig_src_port_flows(lflows, od, fwd_stage, 100,
> + ds_cstr(&match), ds_cstr(&action),
> + lflow_ref);
> +
> ds_clear(&match);
> ds_clear(&action);
>
> @@ -19452,53 +19566,62 @@ consider_network_function(struct lflow_table
> *lflows,
> ds_clear(&match);
> ds_clear(&action);
>
> - /* Priority 100 flow in in_nf:
> + /* Priority 110 flow in in_nf:
> * Allow packets to go through if coming from network-function port as
> * we don't want the packets to be redirected again based on from-lport
> * match.
> */
> ds_put_format(&match, "inport == %s", input_port->json_key);
> ds_put_format(&action, REG_TUN_OFPORT" = ct_label.tun_if_id; next;");
> - ovn_lflow_add(lflows, od, S_SWITCH_IN_NF, 100,
> + ovn_lflow_add(lflows, od, S_SWITCH_IN_NF, 110,
> ds_cstr(&match), ds_cstr(&action), lflow_ref);
> ds_clear(&match);
>
> ds_put_format(&match, "inport == %s", output_port->json_key);
> - ovn_lflow_add(lflows, od, S_SWITCH_IN_NF, 100,
> + ovn_lflow_add(lflows, od, S_SWITCH_IN_NF, 110,
> ds_cstr(&match), ds_cstr(&action), lflow_ref);
> ds_clear(&match);
> ds_clear(&action);
>
> - /* Priority 100 flow in out_nf:
> + /* Priority 110 flow in out_nf:
> * Allow packets to go through if outport is network-function port as
> * we don't want the packets to be redirected again based on to-lport
> * match.
> */
> ds_put_format(&match, "outport == %s", input_port->json_key);
> ds_put_format(&action, "next;");
> - ovn_lflow_add(lflows, od, S_SWITCH_OUT_NF, 100,
> + ovn_lflow_add(lflows, od, S_SWITCH_OUT_NF, 110,
> ds_cstr(&match), ds_cstr(&action), lflow_ref);
> ds_clear(&match);
>
> ds_put_format(&match, "outport == %s", output_port->json_key);
> - ovn_lflow_add(lflows, od, S_SWITCH_OUT_NF, 100,
> + ovn_lflow_add(lflows, od, S_SWITCH_OUT_NF, 110,
> ds_cstr(&match), ds_cstr(&action), lflow_ref);
> ds_clear(&match);
> ds_clear(&action);
>
> - /* For packets redirected from egress pipleline to the NF, when they come
> - * out from the other NF port, we don't want to process them again
> through
> - * egress stages they already went through, especially not again through
> - * conntrack as these packets are already accounted for there. Hence we
> - * need to skip the initial pipeline stages for such packets and directly
> - * start from the NF table. The packets that fall under this category are
> - * the response packets from NF for from-lport ACLs and request packets
> - * received from NF for to-lport ACLs. */
> - ds_put_format(&match, "inport == %s", input_port->json_key);
> + /* Priority 115 flow in out_pre_acl (input_port):
> + * Extend the existing input_port skip-stages flow (skip the NF
> + * table to avoid re-running conntrack) so the same flow also runs
> + * nf_lookup_orig_src_port(). On hit, ls_out_check_port_sec drops
> + * the copy heading back to its source port.
> + *
> + * Priority 115 to take precedence over the priority-110 conntrack
> + * skip flow. */
> ds_put_format(&action, "next(pipeline=egress, table=%d);",
> - (ovn_stage_get_table(S_SWITCH_OUT_NF) + 1));
> - ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 110, ds_cstr(&match),
> - ds_cstr(&action), lflow_ref);
> + ovn_stage_get_table(S_SWITCH_OUT_NF) + 1);
> + build_nf_lookup_orig_src_port_flow(lflows, od, S_SWITCH_OUT_PRE_ACL, 115,
> + input_port, ds_cstr(&action),
> + lflow_ref);
> + ds_clear(&action);
> +
> + /* Priority 2 flow in out_nf (output_port):
> + * A post-NF packet re-entering here is doing a fresh egress and has not
> + * run the egress stages, so run the lookup here. A hit means it is
> + * heading back out the port it arrived on, and ls_out_check_port_sec
> + * drops it. */
> + build_nf_lookup_orig_src_port_flow(lflows, od, S_SWITCH_OUT_NF, 2,
> + output_port, "next;", lflow_ref);
>
> /* Priority 120 flows in out_stateful:
> * If packet was received on a tunnel interface and being forwarded to a
> @@ -19519,6 +19642,7 @@ build_network_function(const struct ovn_datapath *od,
> {
> unsigned long *nfg_ingress_bitmap =
> bitmap_allocate(MAX_OVN_NF_GROUP_IDS);
> unsigned long *nfg_egress_bitmap = bitmap_allocate(MAX_OVN_NF_GROUP_IDS);
> + bool has_nfg = false;
>
> /* This flow matches packets injected from out_nf stage -
> * after it sets the outport - back to in_l2_lkup stage. This rule must
> be
> @@ -19546,13 +19670,14 @@ build_network_function(const struct ovn_datapath
> *od,
> REGBIT_NF_ENABLED" == 1 && " REGBIT_NF_ORIG_DIR" == 1",
> REG_NF_ID" = 0; next;", lflow_ref);
>
> - /* Ingress and Egress NF Table (Priority 100): ACL stage determined these
> + /* Ingress and Egress NF Table (Priority 110): ACL stage determined these
> * packets should be redirected, but these are multicast/broadcast
> - * packets which can cause L2 loop if redirected to NF. */
> - ovn_lflow_add(lflows, od, S_SWITCH_IN_NF, 100,
> + * packets which can cause L2 loop if redirected to NF. Higher priority
> + * than the redirect/learn flows so they skip both. */
> + ovn_lflow_add(lflows, od, S_SWITCH_IN_NF, 110,
> REGBIT_NF_ENABLED" == 1 && eth.mcast",
> "next;", lflow_ref);
> - ovn_lflow_add(lflows, od, S_SWITCH_OUT_NF, 100,
> + ovn_lflow_add(lflows, od, S_SWITCH_OUT_NF, 110,
> REGBIT_NF_ENABLED" == 1 && eth.mcast",
> "next;", lflow_ref);
>
> @@ -19587,6 +19712,7 @@ build_network_function(const struct ovn_datapath *od,
> continue;
> }
> nfg_bitmap = bitmap_set1(nfg_bitmap, nfg_id);
> + has_nfg = true;
> consider_network_function(lflows, od,
> acl->network_function_group,
> ingress, lflow_ref);
> }
> @@ -19612,6 +19738,7 @@ build_network_function(const struct ovn_datapath *od,
> continue;
> }
> nfg_bitmap = bitmap_set1(nfg_bitmap, nfg_id);
> + has_nfg = true;
> consider_network_function(lflows, od,
> acl->network_function_group,
> ingress, lflow_ref);
> @@ -19619,6 +19746,35 @@ build_network_function(const struct ovn_datapath *od,
> }
> }
> }
> +
> + if (has_nfg) {
> + /* Drop flow for loopback duplicates that the post-NF lookup
> + * marked via REGBIT_NF_LOOKUP_HIT. The lookup runs on both NF
> + * return ports (ls_out_pre_acl for input_port, ls_out_nf for
> + * output_port) and is installed by consider_network_function(). */
> + ovn_lflow_add(lflows, od, S_SWITCH_OUT_CHECK_PORT_SEC, 110,
> + REGBIT_NF_LOOKUP_HIT " == 1", debug_drop_action(),
> + lflow_ref);
> +
> + /* Priority 50 flows in in_nf, overlay switches only:
> + * Learn the ingress port of a flagged unicast IP packet this ACL did
> + * not redirect to an NF (REGBIT_NF_ENABLED == 0), so a post-NF copy
> + * reflected back to that port can be dropped. On overlay switches
> + * the redirect may run on a different node than the source port, so
> + * the priority-100 redirect-path learn would land on the wrong node;
> + * learning here keeps it on the source port's node, co-located with
> + * the lookup. VLAN-backed switches don't need this: the re-flood
> + * returns to the redirecting node, where the 100 learn already ran.
> + * Unlike the redirect-path learn, no higher-priority flow skips
> + * multicast here, so exclude it in the match. */
> + if (!ls_has_localnet_port(od)) {
> + build_nf_learn_orig_src_port_flows(
> + lflows, od, S_SWITCH_IN_NF, 50,
> + "!eth.mcast && " REGBIT_NF_ENABLED" == 0",
> + "next;", lflow_ref);
> + }
> + }
> +
> bitmap_free(nfg_ingress_bitmap);
> bitmap_free(nfg_egress_bitmap);
> }
> @@ -19766,7 +19922,8 @@ build_lswitch_and_lrouter_iterate_by_lsp(struct
> ovn_port *op,
> build_mirror_lflows(op, ls_ports, lflows);
> build_lswitch_port_sec_op(op, lflows, actions, match);
> build_lswitch_learn_fdb_op(op, lflows, actions, match);
> - build_lswitch_from_localnet_op(op, lflows, match);
> + build_lswitch_from_localnet_op(op, lflows, actions, match);
> + build_lswitch_set_inport_in_mc_unknown_op(op, lflows, match);
> build_lswitch_arp_nd_responder_known_ips(op, lflows, ls_ports,
> meter_groups, actions, match);
> build_lswitch_dhcp_options_and_response(op, lflows, meter_groups);
> diff --git a/northd/northd.h b/northd/northd.h
> index d27f519d6..976405571 100644
> --- a/northd/northd.h
> +++ b/northd/northd.h
> @@ -1155,6 +1155,25 @@ lsp_can_learn_mac(const struct
> nbrec_logical_switch_port *nbsp)
> return smap_get_bool( ->options, "lsp_learn_fdb", true);
> }
>
> +/* True if 'op' can receive unknown-unicast floods, i.e. it has 'unknown'
> + * among its addresses and "receive_multicast" is not disabled. */
> +static inline bool
> +lsp_can_receive_unknown_flood(const struct ovn_port *op)
> +{
> + return op->nbsp
> + && op->has_unknown
> + && lsp_can_receive_multicast(op->nbsp);
> +}
> +
> +/* True if 'op' is a local MC_UNKNOWN-member ingress port (excludes
> + * remote ports). Used to set flags.inport_in_mc_unknown. */
> +static inline bool
> +lsp_is_mc_unknown_ingress_member(const struct ovn_port *op)
> +{
> + return lsp_can_receive_unknown_flood(op)
> + && strcmp(op->nbsp->type, "remote");
> +}
> +
> const char *lrp_find_member_ip(const struct ovn_port *op, const char *ip_s);
>
> /* This function returns true if 'op' is a gateway router port.
> diff --git a/ovn-sb.xml b/ovn-sb.xml
> index 434c8e43e..746373891 100644
> --- a/ovn-sb.xml
> +++ b/ovn-sb.xml
> @@ -1856,6 +1856,14 @@
> otherwise cause MAC address flaps or L2 loops.
> </p>
>
> + <p>
> + northd emits the learn action only for packets entering on a port
> + that is a member of the <code>MC_UNKNOWN</code> multicast group
> + (gated by <code>flags.inport_in_mc_unknown</code>, set in the
> + <code>ls_in_lookup_fdb</code> stage), since only such packets can
> + be re-flooded by the NF.
> + </p>
> +
> <p>
> <b>Example:</b>
> <code>nf_learn_orig_src_port(ipv6 = false);</code>
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index 86cab3d5b..d35affe54 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -8003,7 +8003,7 @@ AT_CAPTURE_FILE([S1flows])
> #Verify the flows for default port type (VM port)
> AT_CHECK([grep -e "ls_in_l2_lkup.*S1-vm1" S1flows | grep -e "match=(eth.dst
> == 50:54:00:00:00:01)"], [1], [])
> AT_CHECK([grep -e "ls_in_.*_fdb.*S1-vm1" S1flows | ovn_strip_lflows], [0],
> [dnl
> - table=??(ls_in_lookup_fdb ), priority=100 , match=(inport == "S1-vm1"),
> action=(reg0[[11]] = lookup_fdb(inport, eth.src); next;)
> + table=??(ls_in_lookup_fdb ), priority=100 , match=(inport == "S1-vm1"),
> action=(flags.inport_in_mc_unknown = 1; reg0[[11]] = lookup_fdb(inport,
> eth.src); next;)
> table=??(ls_in_put_fdb ), priority=100 , match=(inport == "S1-vm1"
> && reg0[[11]] == 0), action=(put_fdb(inport, eth.src); next;)
> ])
>
> @@ -8079,15 +8079,19 @@ AT_CAPTURE_FILE([S1flows])
>
> dnl Check that S2-vm address is not known on S1 and the forwarding to
> dnl _MC_unknown group is configured.
> -AT_CHECK([grep -E "ls_in_l2_lkup.*S1-|unknown" S1flows | ovn_strip_lflows],
> [0], [dnl
> +AT_CHECK([grep -E "ls_in_l2_lkup.*S1-|ls_in_l2_unknown" S1flows |
> ovn_strip_lflows], [0], [dnl
> table=??(ls_in_l2_lkup ), priority=50 , match=(eth.dst ==
> 50:54:00:00:00:01), action=(outport = "S1-vm"; output;)
> table=??(ls_in_l2_unknown ), priority=0 , match=(1), action=(output;)
> table=??(ls_in_l2_unknown ), priority=50 , match=(outport == "none"),
> action=(outport = "_MC_unknown"; output;)
> ])
>
> dnl Check that FDB learning is enabled for the switch port.
> +dnl The port type "switch" implicitly carries unknown addresses and thus
> +dnl can receive unknown-unicast floods, so flags.inport_in_mc_unknown is
> +dnl also set (consumed by inline NF redirection in ls_in_nf; harmless
> +dnl otherwise).
> AT_CHECK([grep -E "ls_.*fdb.*S1-" S1flows | ovn_strip_lflows], [0], [dnl
> - table=??(ls_in_lookup_fdb ), priority=100 , match=(inport == "S1-S2"),
> action=(reg0[[11]] = lookup_fdb(inport, eth.src); next;)
> + table=??(ls_in_lookup_fdb ), priority=100 , match=(inport == "S1-S2"),
> action=(flags.inport_in_mc_unknown = 1; reg0[[11]] = lookup_fdb(inport,
> eth.src); next;)
> table=??(ls_in_put_fdb ), priority=100 , match=(inport == "S1-S2" &&
> reg0[[11]] == 0), action=(put_fdb(inport, eth.src); next;)
> ])
>
> @@ -8096,7 +8100,7 @@ AT_CAPTURE_FILE([S2flows])
>
> dnl Check that S1-vm address is not known on S2 and the forwarding to
> dnl _MC_unknown group is configured.
> -AT_CHECK([grep -E "ls_in_l2_lkup.*S2-|unknown" S2flows | ovn_strip_lflows],
> [0], [dnl
> +AT_CHECK([grep -E "ls_in_l2_lkup.*S2-|ls_in_l2_unknown" S2flows |
> ovn_strip_lflows], [0], [dnl
> table=??(ls_in_l2_lkup ), priority=50 , match=(eth.dst ==
> 50:54:00:00:00:02), action=(outport = "S2-vm"; output;)
> table=??(ls_in_l2_unknown ), priority=0 , match=(1), action=(output;)
> table=??(ls_in_l2_unknown ), priority=50 , match=(outport == "none"),
> action=(outport = "_MC_unknown"; output;)
> @@ -8104,7 +8108,7 @@ AT_CHECK([grep -E "ls_in_l2_lkup.*S2-|unknown" S2flows
> | ovn_strip_lflows], [0],
>
> dnl Check that FDB learning is enabled for the switch port.
> AT_CHECK([grep -E "ls_.*fdb.*S2-" S2flows | ovn_strip_lflows], [0], [dnl
> - table=??(ls_in_lookup_fdb ), priority=100 , match=(inport == "S2-S1"),
> action=(reg0[[11]] = lookup_fdb(inport, eth.src); next;)
> + table=??(ls_in_lookup_fdb ), priority=100 , match=(inport == "S2-S1"),
> action=(flags.inport_in_mc_unknown = 1; reg0[[11]] = lookup_fdb(inport,
> eth.src); next;)
> table=??(ls_in_put_fdb ), priority=100 , match=(inport == "S2-S1" &&
> reg0[[11]] == 0), action=(put_fdb(inport, eth.src); next;)
> ])
>
> @@ -8116,7 +8120,7 @@ AT_CAPTURE_FILE([S1flows2])
>
> dnl Check that that address is now known on S1, but forwarding to _MC_unknown
> dnl group is still configured for other potential addresses.
> -AT_CHECK([grep -E "ls_in_l2_lkup.*S1-|unknown" S1flows2 | ovn_strip_lflows],
> [0], [dnl
> +AT_CHECK([grep -E "ls_in_l2_lkup.*S1-|ls_in_l2_unknown" S1flows2 |
> ovn_strip_lflows], [0], [dnl
> table=??(ls_in_l2_lkup ), priority=50 , match=(eth.dst ==
> 50:54:00:00:00:01), action=(outport = "S1-vm"; output;)
> table=??(ls_in_l2_lkup ), priority=50 , match=(eth.dst ==
> 50:54:00:00:00:02), action=(outport = "S1-S2"; output;)
> table=??(ls_in_l2_unknown ), priority=0 , match=(1), action=(output;)
> @@ -8125,7 +8129,7 @@ AT_CHECK([grep -E "ls_in_l2_lkup.*S1-|unknown" S1flows2
> | ovn_strip_lflows], [0]
>
> dnl Check that FDB learning is still enabled for the switch port.
> AT_CHECK([grep -E "ls_.*fdb.*S1-" S1flows | ovn_strip_lflows], [0], [dnl
> - table=??(ls_in_lookup_fdb ), priority=100 , match=(inport == "S1-S2"),
> action=(reg0[[11]] = lookup_fdb(inport, eth.src); next;)
> + table=??(ls_in_lookup_fdb ), priority=100 , match=(inport == "S1-S2"),
> action=(flags.inport_in_mc_unknown = 1; reg0[[11]] = lookup_fdb(inport,
> eth.src); next;)
> table=??(ls_in_put_fdb ), priority=100 , match=(inport == "S1-S2" &&
> reg0[[11]] == 0), action=(put_fdb(inport, eth.src); next;)
> ])
>
> @@ -10368,10 +10372,14 @@ AT_CHECK([ovn-nbctl ls-add ls0])
> AT_CHECK([ovn-nbctl lsp-add-localnet-port ls0 ln_port phys])
> AT_CHECK([ovn-nbctl --wait=sb sync])
>
> -# Check MAC learning flows with 'localnet_learn_fdb' default (false)
> +# Check MAC learning flows with 'localnet_learn_fdb' default (false).
> +# Because lsp-add-localnet-port defaults addresses to "unknown", the
> +# localnet port can receive unknown-unicast floods and gets
> +# flags.inport_in_mc_unknown set (consumed by inline NF redirection
> +# in ls_in_nf; harmless otherwise).
> AT_CHECK([ovn-sbctl dump-flows ls0 | grep -e 'ls_in_\(put\|lookup\)_fdb' |
> ovn_strip_lflows], [0], [dnl
> table=??(ls_in_lookup_fdb ), priority=0 , match=(1), action=(next;)
> - table=??(ls_in_lookup_fdb ), priority=50 , match=(inport ==
> "ln_port"), action=(flags.localnet = 1; next;)
> + table=??(ls_in_lookup_fdb ), priority=50 , match=(inport ==
> "ln_port"), action=(flags.localnet = 1; flags.inport_in_mc_unknown = 1; next;)
> table=??(ls_in_put_fdb ), priority=0 , match=(1), action=(next;)
> ])
>
> @@ -10379,8 +10387,8 @@ AT_CHECK([ovn-sbctl dump-flows ls0 | grep -e
> 'ls_in_\(put\|lookup\)_fdb' | ovn_s
> AT_CHECK([ovn-nbctl --wait=sb lsp-set-options ln_port
> localnet_learn_fdb=true])
> AT_CHECK([ovn-sbctl dump-flows ls0 | grep -e 'ls_in_\(put\|lookup\)_fdb' |
> ovn_strip_lflows], [0], [dnl
> table=??(ls_in_lookup_fdb ), priority=0 , match=(1), action=(next;)
> - table=??(ls_in_lookup_fdb ), priority=100 , match=(inport ==
> "ln_port"), action=(flags.localnet = 1; reg0[[11]] = lookup_fdb(inport,
> eth.src); next;)
> - table=??(ls_in_lookup_fdb ), priority=50 , match=(inport ==
> "ln_port"), action=(flags.localnet = 1; next;)
> + table=??(ls_in_lookup_fdb ), priority=100 , match=(inport ==
> "ln_port"), action=(flags.localnet = 1; flags.inport_in_mc_unknown = 1;
> reg0[[11]] = lookup_fdb(inport, eth.src); next;)
> + table=??(ls_in_lookup_fdb ), priority=50 , match=(inport ==
> "ln_port"), action=(flags.localnet = 1; flags.inport_in_mc_unknown = 1; next;)
> table=??(ls_in_put_fdb ), priority=0 , match=(1), action=(next;)
> table=??(ls_in_put_fdb ), priority=100 , match=(inport == "ln_port"
> && reg0[[11]] == 0), action=(put_fdb(inport, eth.src); next;)
> ])
> @@ -10389,7 +10397,7 @@ AT_CHECK([ovn-sbctl dump-flows ls0 | grep -e
> 'ls_in_\(put\|lookup\)_fdb' | ovn_s
> AT_CHECK([ovn-nbctl --wait=sb lsp-set-options ln_port
> localnet_learn_fdb=false])
> AT_CHECK([ovn-sbctl dump-flows ls0 | grep -e 'ls_in_\(put\|lookup\)_fdb' |
> ovn_strip_lflows], [0], [dnl
> table=??(ls_in_lookup_fdb ), priority=0 , match=(1), action=(next;)
> - table=??(ls_in_lookup_fdb ), priority=50 , match=(inport ==
> "ln_port"), action=(flags.localnet = 1; next;)
> + table=??(ls_in_lookup_fdb ), priority=50 , match=(inport ==
> "ln_port"), action=(flags.localnet = 1; flags.inport_in_mc_unknown = 1; next;)
> table=??(ls_in_put_fdb ), priority=0 , match=(1), action=(next;)
> ])
>
> @@ -10762,7 +10770,7 @@ dnl ls1: ls_in_lookup_fdb should have priority 0
> default +
> dnl priority 50 flags.localnet.
> AT_CHECK([ovn-sbctl dump-flows ls1 | grep -e 'ls_in_lookup_fdb' |
> ovn_strip_lflows], [0], [dnl
> table=??(ls_in_lookup_fdb ), priority=0 , match=(1), action=(next;)
> - table=??(ls_in_lookup_fdb ), priority=50 , match=(inport == "ln1"),
> action=(flags.localnet = 1; next;)
> + table=??(ls_in_lookup_fdb ), priority=50 , match=(inport == "ln1"),
> action=(flags.localnet = 1; flags.inport_in_mc_unknown = 1; next;)
> ])
>
> dnl ls1: ls_in_arp_rsp should include flags.localnet condition for
> @@ -10802,8 +10810,8 @@ dnl ls1: ls_in_lookup_fdb should have priority 100
> FDB +
> dnl priority 50 fallback.
> AT_CHECK([ovn-sbctl dump-flows ls1 | grep -e 'ls_in_lookup_fdb' |
> ovn_strip_lflows], [0], [dnl
> table=??(ls_in_lookup_fdb ), priority=0 , match=(1), action=(next;)
> - table=??(ls_in_lookup_fdb ), priority=100 , match=(inport == "ln1"),
> action=(flags.localnet = 1; reg0[[11]] = lookup_fdb(inport, eth.src); next;)
> - table=??(ls_in_lookup_fdb ), priority=50 , match=(inport == "ln1"),
> action=(flags.localnet = 1; next;)
> + table=??(ls_in_lookup_fdb ), priority=100 , match=(inport == "ln1"),
> action=(flags.localnet = 1; flags.inport_in_mc_unknown = 1; reg0[[11]] =
> lookup_fdb(inport, eth.src); next;)
> + table=??(ls_in_lookup_fdb ), priority=50 , match=(inport == "ln1"),
> action=(flags.localnet = 1; flags.inport_in_mc_unknown = 1; next;)
> ])
>
> dnl ls1: ls_in_arp_rsp should be unchanged.
> @@ -10824,7 +10832,7 @@ check ovn-nbctl --wait=sb lsp-set-options ln1
> localnet_learn_fdb=false
>
> AT_CHECK([ovn-sbctl dump-flows ls1 | grep -e 'ls_in_lookup_fdb' |
> ovn_strip_lflows], [0], [dnl
> table=??(ls_in_lookup_fdb ), priority=0 , match=(1), action=(next;)
> - table=??(ls_in_lookup_fdb ), priority=50 , match=(inport == "ln1"),
> action=(flags.localnet = 1; next;)
> + table=??(ls_in_lookup_fdb ), priority=50 , match=(inport == "ln1"),
> action=(flags.localnet = 1; flags.inport_in_mc_unknown = 1; next;)
> ])
>
> AT_CHECK([ovn-sbctl dump-flows ls1 | grep -e 'ls_in_arp_rsp' |
> ovn_strip_lflows], [0], [dnl
> @@ -19962,15 +19970,20 @@ AT_CHECK(
> [grep -E 'ls_(in|out)_network_function' sw0flows | ovn_strip_lflows |
> sort], [0], [dnl
> table=??(ls_in_network_function), priority=0 , match=(1), action=(next;)
> table=??(ls_in_network_function), priority=1 , match=(reg8[[21]] == 1),
> action=(drop;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "sw0-nf-p1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "sw0-nf-p2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=100 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 101), action=(nf_learn_orig_src_port(ipv6 = false); outport
> = "sw0-nf-p1"; output;)
> + table=??(ls_in_network_function), priority=100 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 101), action=(nf_learn_orig_src_port(ipv6 = true); outport
> = "sw0-nf-p1"; output;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "sw0-nf-p1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "sw0-nf-p2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = false); next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = true); next;)
> table=??(ls_in_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 1 && reg0[[22..29]] == 101), action=(outport = "sw0-nf-p1";
> output;)
> table=??(ls_out_network_function), priority=0 , match=(1),
> action=(next;)
> table=??(ls_out_network_function), priority=1 , match=(reg8[[21]] ==
> 1), action=(drop;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "sw0-nf-p1"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "sw0-nf-p2"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "sw0-nf-p1"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "sw0-nf-p2"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_out_network_function), priority=2 , match=(inport ==
> "sw0-nf-p2"), action=(reg8[[24]] = nf_lookup_orig_src_port(); next;)
> table=??(ls_out_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 0 && ct_label.nf_id == 101), action=(outport = "sw0-nf-p2";
> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> ])
>
> @@ -19978,8 +19991,11 @@ AT_CHECK([grep "ls_in_l2_lkup" sw0flows |
> ovn_strip_lflows | grep 'priority=100'
> table=??(ls_in_l2_lkup ), priority=100 , match=(reg8[[23]] == 1),
> action=(output;)
> ])
>
> +dnl LOOKUP runs on both NF return ports: input_port in ls_out_pre_acl (also
> +dnl skipping the already-traversed egress stages) and output_port in
> +dnl ls_out_nf (fresh egress, after 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=(inport ==
> "sw0-nf-p1"), action=(next(pipeline=egress, table=??);)
> + table=??(ls_out_pre_acl ), priority=115 , match=(inport ==
> "sw0-nf-p1"), action=(reg8[[24]] = nf_lookup_orig_src_port();
> next(pipeline=egress, table=??);)
> ])
>
> AT_CHECK(
> @@ -20070,20 +20086,28 @@ AT_CHECK(
> [grep -E 'ls_(in|out)_network_function' sw0flows | ovn_strip_lflows |
> sort], [0], [dnl
> table=??(ls_in_network_function), priority=0 , match=(1), action=(next;)
> table=??(ls_in_network_function), priority=1 , match=(reg8[[21]] == 1),
> action=(drop;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "sw0-nf-p1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "sw0-nf-p2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "sw0-nf-p3"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "sw0-nf-p4"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=100 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 101), action=(nf_learn_orig_src_port(ipv6 = false); outport
> = "sw0-nf-p1"; output;)
> + table=??(ls_in_network_function), priority=100 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 101), action=(nf_learn_orig_src_port(ipv6 = true); outport
> = "sw0-nf-p1"; output;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "sw0-nf-p1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "sw0-nf-p2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "sw0-nf-p3"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "sw0-nf-p4"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = false); next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = true); next;)
> table=??(ls_in_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 0 && ct_label.nf_id == 102), action=(outport = "sw0-nf-p3";
> output;)
> table=??(ls_in_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 1 && reg0[[22..29]] == 101), action=(outport = "sw0-nf-p1";
> output;)
> table=??(ls_out_network_function), priority=0 , match=(1),
> action=(next;)
> table=??(ls_out_network_function), priority=1 , match=(reg8[[21]] ==
> 1), action=(drop;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "sw0-nf-p1"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "sw0-nf-p2"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "sw0-nf-p3"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "sw0-nf-p4"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_out_network_function), priority=100 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 102), action=(nf_learn_orig_src_port(ipv6 = false); outport
> = "sw0-nf-p4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> + table=??(ls_out_network_function), priority=100 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 102), action=(nf_learn_orig_src_port(ipv6 = true); outport
> = "sw0-nf-p4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "sw0-nf-p1"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "sw0-nf-p2"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "sw0-nf-p3"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "sw0-nf-p4"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_out_network_function), priority=2 , match=(inport ==
> "sw0-nf-p2"), action=(reg8[[24]] = nf_lookup_orig_src_port(); next;)
> + table=??(ls_out_network_function), priority=2 , match=(inport ==
> "sw0-nf-p4"), action=(reg8[[24]] = nf_lookup_orig_src_port(); next;)
> table=??(ls_out_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 0 && ct_label.nf_id == 101), action=(outport = "sw0-nf-p2";
> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> table=??(ls_out_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 1 && reg0[[22..29]] == 102), action=(outport = "sw0-nf-p4";
> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> ])
> @@ -20152,20 +20176,28 @@ AT_CHECK(
> [grep -E 'ls_(in|out)_network_function' sw1flows | ovn_strip_lflows |
> sort], [0], [dnl
> table=??(ls_in_network_function), priority=0 , match=(1), action=(next;)
> table=??(ls_in_network_function), priority=1 , match=(reg8[[21]] == 1),
> action=(drop;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "sw1-nf-p1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "sw1-nf-p2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "sw1-nf-p3"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "sw1-nf-p4"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=100 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 101), action=(nf_learn_orig_src_port(ipv6 = false); outport
> = "sw1-nf-p1"; output;)
> + table=??(ls_in_network_function), priority=100 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 101), action=(nf_learn_orig_src_port(ipv6 = true); outport
> = "sw1-nf-p1"; output;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "sw1-nf-p1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "sw1-nf-p2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "sw1-nf-p3"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "sw1-nf-p4"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = false); next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = true); next;)
> table=??(ls_in_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 0 && ct_label.nf_id == 102), action=(outport = "sw1-nf-p3";
> output;)
> table=??(ls_in_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 1 && reg0[[22..29]] == 101), action=(outport = "sw1-nf-p1";
> output;)
> table=??(ls_out_network_function), priority=0 , match=(1),
> action=(next;)
> table=??(ls_out_network_function), priority=1 , match=(reg8[[21]] ==
> 1), action=(drop;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "sw1-nf-p1"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "sw1-nf-p2"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "sw1-nf-p3"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "sw1-nf-p4"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_out_network_function), priority=100 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 102), action=(nf_learn_orig_src_port(ipv6 = false); outport
> = "sw1-nf-p4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> + table=??(ls_out_network_function), priority=100 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 102), action=(nf_learn_orig_src_port(ipv6 = true); outport
> = "sw1-nf-p4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "sw1-nf-p1"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "sw1-nf-p2"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "sw1-nf-p3"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "sw1-nf-p4"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_out_network_function), priority=2 , match=(inport ==
> "sw1-nf-p2"), action=(reg8[[24]] = nf_lookup_orig_src_port(); next;)
> + table=??(ls_out_network_function), priority=2 , match=(inport ==
> "sw1-nf-p4"), action=(reg8[[24]] = nf_lookup_orig_src_port(); next;)
> table=??(ls_out_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 0 && ct_label.nf_id == 101), action=(outport = "sw1-nf-p2";
> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> table=??(ls_out_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 1 && reg0[[22..29]] == 102), action=(outport = "sw1-nf-p4";
> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> ])
> @@ -20200,6 +20232,210 @@ OVN_CLEANUP_NORTHD
> AT_CLEANUP
> ])
>
> +OVN_FOR_EACH_NORTHD_NO_HV([
> +AT_SETUP([Network function -- inline post-NF re-flood loop prevention])
> +
> +dnl Verifies that with an inline-mode Network_Function_Group attached to an
> +dnl ACL, northd installs:
> +dnl - flags.inport_in_mc_unknown = 1 in ls_in_lookup_fdb for every port
> +dnl that can receive an unknown-unicast flood,
> +dnl - per-NFG nf_learn_orig_src_port() redirect-path flows (priority 101)
> +dnl in ls_in_nf (or ls_out_nf for to-lport ACLs) gated on that flag, and,
Priority 100?
> +dnl on overlay switches only, a per-LS default-path flow (priority 50) in
> +dnl ls_in_nf,
> +dnl - nf_lookup_orig_src_port() flows on both NF ports: input_port in
> +dnl ls_out_pre_acl (also skipping the already-traversed egress stages)
> +dnl and output_port in ls_out_nf (fresh egress, after conntrack), and
> +dnl - the ls_out_check_port_sec drop on REGBIT_NF_LOOKUP_HIT.
> +
> +ovn_start
> +
> +check ovn-nbctl ls-add sw0
> +
> +dnl NF port-pair.
> +check ovn-nbctl lsp-add sw0 sw0-nf-p1
> +check ovn-nbctl lsp-add sw0 sw0-nf-p2
> +check ovn-nbctl set logical_switch_port sw0-nf-p1 \
> + options:receive_multicast=false options:lsp_learn_mac=false \
> + options:is-nf=true options:nf-linked-port=sw0-nf-p2
> +check ovn-nbctl set logical_switch_port sw0-nf-p2 \
> + options:receive_multicast=false options:lsp_learn_mac=false \
> + options:is-nf=true options:nf-linked-port=sw0-nf-p1
> +
> +dnl A VM port and a localnet port, both with "unknown" in addresses: each
> +dnl can receive an unknown-unicast flood, so each must get the flag.
> +check ovn-nbctl lsp-add sw0 sw0-vm1 -- \
> + lsp-set-addresses sw0-vm1 "00:00:00:00:00:01 10.0.0.10" unknown
> +check ovn-nbctl lsp-add sw0 sw0-ln -- \
> + lsp-set-type sw0-ln localnet -- \
> + lsp-set-options sw0-ln network_name=phys -- \
> + lsp-set-addresses sw0-ln unknown
> +
> +dnl A second VM port without "unknown" must NOT get the flag set.
> +check ovn-nbctl lsp-add sw0 sw0-vm2 -- \
> + lsp-set-addresses sw0-vm2 "00:00:00:00:00:02 10.0.0.11"
> +
> +dnl A third port with "unknown" but with receive_multicast=false must NOT
> +dnl get the flag set (it would never be a flood target).
> +check ovn-nbctl lsp-add sw0 sw0-vm3 -- \
> + lsp-set-addresses sw0-vm3 "00:00:00:00:00:03 10.0.0.12" unknown -- \
> + lsp-set-options sw0-vm3 receive_multicast=false
> +
> +check ovn-nbctl nf-add nf0 101 sw0-nf-p1 sw0-nf-p2
> +check ovn-nbctl nfg-add nfg0 201 inline nf0
> +check ovn-nbctl acl-add sw0 from-lport 1234 'ip' allow-related nfg0
> +check ovn-nbctl --wait=sb sync
> +
> +ovn-sbctl dump-flows sw0 > sw0flows
> +AT_CAPTURE_FILE([sw0flows])
> +
> +dnl FLAG: every port that can receive an unknown-unicast flood gets
> +dnl flags.inport_in_mc_unknown = 1 set in ls_in_lookup_fdb. The VIF with
> +dnl "unknown" gets the action appended to its priority-100 FDB-learn flow;
> +dnl the localnet (without localnet_learn_fdb) gets it appended to its
> +dnl priority-50 flags.localnet flow.
> +AT_CHECK([grep 'ls_in_lookup_fdb' sw0flows | ovn_strip_lflows | \
> + grep inport_in_mc_unknown | sort], [0], [dnl
> + table=??(ls_in_lookup_fdb ), priority=100 , match=(inport ==
> "sw0-vm1"), action=(flags.inport_in_mc_unknown = 1; reg0[[11]] =
> lookup_fdb(inport, eth.src); next;)
> + table=??(ls_in_lookup_fdb ), priority=50 , match=(inport == "sw0-ln"),
> action=(flags.localnet = 1; flags.inport_in_mc_unknown = 1; next;)
> +])
> +
> +dnl FLAG must not be set for NF ports (receive_multicast=false), plain VM
> +dnl ports without "unknown", or ports with receive_multicast=false.
> +AT_CHECK([grep 'ls_in_lookup_fdb' sw0flows | ovn_strip_lflows | \
> + grep inport_in_mc_unknown | \
> + grep -E 'sw0-nf-p[[12]]|sw0-vm2|sw0-vm3' | wc -l], [0], [0
> +])
> +
> +dnl LEARN (redirect path): for the inline-mode NFG attached via from-lport
> +dnl ACL, ls_in_nf has priority-100 LEARN+redirect flows for IPv4 and IPv6,
> +dnl gated on the flag. Multicast is skipped by the higher-priority (110)
> +dnl flow, so it is not matched here. The existing priority-99 redirect flow
> +dnl remains as a catch-all.
> +AT_CHECK([grep 'ls_in_network_function' sw0flows | ovn_strip_lflows | \
> + grep nf_learn | grep 'priority=100' | sort], [0], [dnl
> + table=??(ls_in_network_function), priority=100 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 101), action=(nf_learn_orig_src_port(ipv6 = false); outport
> = "sw0-nf-p1"; output;)
> + table=??(ls_in_network_function), priority=100 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 101), action=(nf_learn_orig_src_port(ipv6 = true); outport
> = "sw0-nf-p1"; output;)
> +])
> +
> +dnl LEARN (default path): the per-LS priority-50 default-path LEARN is only
> +dnl emitted on overlay switches. sw0 has a localnet port (VLAN-backed),
> where
> +dnl the redirect-path learn already co-locates with the lookup, so it must
> NOT
> +dnl have any priority-50 nf_learn flow. (Overlay coverage is below.)
> +AT_CHECK([grep 'ls_in_network_function' sw0flows | ovn_strip_lflows | \
> + grep nf_learn | grep 'priority=50' | wc -l], [0], [0
> +])
> +
> +dnl LOOKUP: both NF ports run nf_lookup_orig_src_port() and stash the result
> +dnl in REGBIT_NF_LOOKUP_HIT. input_port runs in ls_out_pre_acl and also
> +dnl skips the already-traversed egress stages; output_port (fresh egress,
> +dnl e.g. a re-flooded request) runs in ls_out_nf, after conntrack.
> +AT_CHECK([grep 'ls_out_pre_acl' sw0flows | ovn_strip_lflows | \
> + grep 'sw0-nf-p1'], [0], [dnl
> + table=??(ls_out_pre_acl ), priority=115 , match=(inport ==
> "sw0-nf-p1"), action=(reg8[[24]] = nf_lookup_orig_src_port();
> next(pipeline=egress, table=??);)
> +])
> +AT_CHECK([grep 'ls_out_network_function' sw0flows | ovn_strip_lflows | \
> + grep nf_lookup], [0], [dnl
> + table=??(ls_out_network_function), priority=2 , match=(inport ==
> "sw0-nf-p2"), action=(reg8[[24]] = nf_lookup_orig_src_port(); next;)
> +])
> +
> +dnl DROP: ls_out_check_port_sec drops on REGBIT_NF_LOOKUP_HIT == 1.
> +AT_CHECK([grep 'ls_out_check_port_sec' sw0flows | ovn_strip_lflows | \
> + grep 'reg8\[[24\]] == 1'], [0], [dnl
> + table=??(ls_out_check_port_sec), priority=110 , match=(reg8[[24]] == 1),
> action=(drop;)
> +])
> +
> +AS_BOX([An overlay switch (no localnet) gets the default-path LEARN])
> +
> +dnl On an overlay switch the redirect can run on a different node than the
> +dnl source port, so the redirect-path learn would land on the wrong node.
> +dnl northd therefore also emits the per-LS priority-50 default-path LEARN in
> +dnl ls_in_nf, recording the source port on its own ingress node.
> +check ovn-nbctl ls-add sw1
> +check ovn-nbctl lsp-add sw1 sw1-nf-p1
> +check ovn-nbctl lsp-add sw1 sw1-nf-p2
> +check ovn-nbctl set logical_switch_port sw1-nf-p1 \
> + options:receive_multicast=false options:lsp_learn_mac=false \
> + options:is-nf=true options:nf-linked-port=sw1-nf-p2
> +check ovn-nbctl set logical_switch_port sw1-nf-p2 \
> + options:receive_multicast=false options:lsp_learn_mac=false \
> + options:is-nf=true options:nf-linked-port=sw1-nf-p1
> +check ovn-nbctl nf-add nf2 103 sw1-nf-p1 sw1-nf-p2
> +check ovn-nbctl nfg-add nfg2 203 inline nf2
> +check ovn-nbctl acl-add sw1 from-lport 1234 'ip' allow-related nfg2
> +check ovn-nbctl --wait=sb sync
> +
> +ovn-sbctl dump-flows sw1 > sw1flows
> +AT_CAPTURE_FILE([sw1flows])
> +
> +AT_CHECK([grep 'ls_in_network_function' sw1flows | ovn_strip_lflows | \
> + grep nf_learn | grep 'priority=50' | sort], [0], [dnl
> + table=??(ls_in_network_function), priority=50 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = false); next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = true); next;)
> +])
> +
> +AS_BOX([A second inline NFG on a to-lport ACL emits its own LEARN+LOOKUP])
> +
> +dnl Adding a second NF pair and attaching it via a to-lport ACL must yield:
> +dnl - additional priority-100 LEARN+redirect flows in ls_out_nf for that
> +dnl NFG (gated on the flag),
> +dnl - LOOKUP flows keyed on both of that NF's ports too.
> +dnl Flag-set flows in ls_in_lookup_fdb are LS-wide and are not duplicated by
> +dnl the second NFG.
> +check ovn-nbctl lsp-add sw0 sw0-nf-p3
> +check ovn-nbctl lsp-add sw0 sw0-nf-p4
> +check ovn-nbctl set logical_switch_port sw0-nf-p3 \
> + options:receive_multicast=false options:lsp_learn_mac=false \
> + options:is-nf=true options:nf-linked-port=sw0-nf-p4
> +check ovn-nbctl set logical_switch_port sw0-nf-p4 \
> + options:receive_multicast=false options:lsp_learn_mac=false \
> + options:is-nf=true options:nf-linked-port=sw0-nf-p3
> +check ovn-nbctl nf-add nf1 102 sw0-nf-p3 sw0-nf-p4
> +check ovn-nbctl nfg-add nfg1 202 inline nf1
> +check ovn-nbctl acl-add sw0 to-lport 1234 'ip' allow-related nfg1
> +check ovn-nbctl --wait=sb sync
> +
> +ovn-sbctl dump-flows sw0 > sw0flows
> +
> +AT_CHECK([grep 'ls_out_pre_acl' sw0flows | ovn_strip_lflows | \
> + grep -E 'sw0-nf-p[[13]]' | sort], [0], [dnl
> + table=??(ls_out_pre_acl ), priority=115 , match=(inport ==
> "sw0-nf-p1"), action=(reg8[[24]] = nf_lookup_orig_src_port();
> next(pipeline=egress, table=??);)
> + table=??(ls_out_pre_acl ), priority=115 , match=(inport ==
> "sw0-nf-p3"), action=(reg8[[24]] = nf_lookup_orig_src_port();
> next(pipeline=egress, table=??);)
> +])
> +AT_CHECK([grep 'ls_out_network_function' sw0flows | ovn_strip_lflows | \
> + grep nf_lookup | sort], [0], [dnl
> + table=??(ls_out_network_function), priority=2 , match=(inport ==
> "sw0-nf-p2"), action=(reg8[[24]] = nf_lookup_orig_src_port(); next;)
> + table=??(ls_out_network_function), priority=2 , match=(inport ==
> "sw0-nf-p4"), action=(reg8[[24]] = nf_lookup_orig_src_port(); next;)
> +])
> +
> +AT_CHECK([grep 'ls_out_network_function' sw0flows | ovn_strip_lflows | \
> + grep nf_learn | sort], [0], [dnl
> + table=??(ls_out_network_function), priority=100 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 102), action=(nf_learn_orig_src_port(ipv6 = false); outport
> = "sw0-nf-p4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> + table=??(ls_out_network_function), priority=100 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 102), action=(nf_learn_orig_src_port(ipv6 = true); outport
> = "sw0-nf-p4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> +])
> +
> +AS_BOX([Without any inline-mode NFG attached, no loop-prevention flows])
> +
> +check ovn-nbctl acl-del sw0
> +check ovn-nbctl --wait=sb sync
> +ovn-sbctl dump-flows sw0 > sw0flows
> +
> +AT_CHECK([grep -E 'nf_(learn|lookup)_orig_src_port' sw0flows | wc -l], [0],
> [0
> +])
> +AT_CHECK([grep -E 'reg8\[[24\]]' sw0flows | wc -l], [0], [0
> +])
> +
> +dnl The ls_in_lookup_fdb flag-set flows persist even with no inline NFG
> +dnl attached: they are per-port and harmless (nothing consumes the flag
> +dnl without an inline NFG).
> +AT_CHECK([grep 'ls_in_lookup_fdb' sw0flows | ovn_strip_lflows | \
> + grep inport_in_mc_unknown | wc -l], [0], [2
> +])
> +
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +])
> +
> OVN_FOR_EACH_NORTHD([
> AT_SETUP([Network function health check])
> AT_KEYWORDS([ovn])
> @@ -20291,16 +20527,23 @@ AT_CHECK(
> [grep -E 'ls_(in|out)_network_function' lflows | ovn_strip_lflows | sort],
> [0], [dnl
> table=??(ls_in_network_function), priority=0 , match=(1), action=(next;)
> table=??(ls_in_network_function), priority=1 , match=(reg8[[21]] == 1),
> action=(drop;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "child-1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "child-2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=100 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 1), action=(nf_learn_orig_src_port(ipv6 = false); outport =
> "child-1"; output;)
> + table=??(ls_in_network_function), priority=100 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 1), action=(nf_learn_orig_src_port(ipv6 = true); outport =
> "child-1"; output;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "child-1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "child-2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = false); next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = true); next;)
> table=??(ls_in_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 0 && ct_label.nf_id == 1), action=(outport = "child-1";
> output;)
> table=??(ls_in_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(outport = "child-1";
> output;)
> table=??(ls_out_network_function), priority=0 , match=(1),
> action=(next;)
> table=??(ls_out_network_function), priority=1 , match=(reg8[[21]] ==
> 1), action=(drop;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "child-1"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "child-2"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_out_network_function), priority=100 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 1), action=(nf_learn_orig_src_port(ipv6 = false); outport =
> "child-2"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> + table=??(ls_out_network_function), priority=100 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 1), action=(nf_learn_orig_src_port(ipv6 = true); outport =
> "child-2"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "child-1"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "child-2"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_out_network_function), priority=2 , match=(inport ==
> "child-2"), action=(reg8[[24]] = nf_lookup_orig_src_port(); next;)
> table=??(ls_out_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 0 && ct_label.nf_id == 1), action=(outport = "child-2";
> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> table=??(ls_out_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(outport = "child-2";
> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> ])
> @@ -20332,16 +20575,23 @@ AT_CHECK(
> [grep -E 'ls_(in|out)_network_function' lflows | ovn_strip_lflows | sort],
> [0], [dnl
> table=??(ls_in_network_function), priority=0 , match=(1), action=(next;)
> table=??(ls_in_network_function), priority=1 , match=(reg8[[21]] == 1),
> action=(drop;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "child-1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "child-2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=100 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 1), action=(nf_learn_orig_src_port(ipv6 = false); outport =
> "child-1"; output;)
> + table=??(ls_in_network_function), priority=100 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 1), action=(nf_learn_orig_src_port(ipv6 = true); outport =
> "child-1"; output;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "child-1"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "child-2"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = false); next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = true); next;)
> table=??(ls_in_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 0 && ct_label.nf_id == 1), action=(outport = "child-1";
> output;)
> table=??(ls_in_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(outport = "child-1";
> output;)
> table=??(ls_out_network_function), priority=0 , match=(1),
> action=(next;)
> table=??(ls_out_network_function), priority=1 , match=(reg8[[21]] ==
> 1), action=(drop;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "child-1"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "child-2"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_out_network_function), priority=100 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 1), action=(nf_learn_orig_src_port(ipv6 = false); outport =
> "child-2"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> + table=??(ls_out_network_function), priority=100 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 1), action=(nf_learn_orig_src_port(ipv6 = true); outport =
> "child-2"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "child-1"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "child-2"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_out_network_function), priority=2 , match=(inport ==
> "child-2"), action=(reg8[[24]] = nf_lookup_orig_src_port(); next;)
> table=??(ls_out_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 0 && ct_label.nf_id == 1), action=(outport = "child-2";
> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> table=??(ls_out_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 1 && reg0[[22..29]] == 1), action=(outport = "child-2";
> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> ])
> @@ -20373,16 +20623,23 @@ AT_CHECK(
> [grep -E 'ls_(in|out)_network_function' lflows | ovn_strip_lflows | sort],
> [0], [dnl
> table=??(ls_in_network_function), priority=0 , match=(1), action=(next;)
> table=??(ls_in_network_function), priority=1 , match=(reg8[[21]] == 1),
> action=(drop;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "child-3"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(inport ==
> "child-4"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> - table=??(ls_in_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=100 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 2), action=(nf_learn_orig_src_port(ipv6 = false); outport =
> "child-3"; output;)
> + table=??(ls_in_network_function), priority=100 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 2), action=(nf_learn_orig_src_port(ipv6 = true); outport =
> "child-3"; output;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "child-3"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(inport ==
> "child-4"), action=(reg5[[16..31]] = ct_label.tun_if_id; next;)
> + table=??(ls_in_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = false); next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = true); next;)
> table=??(ls_in_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 0 && ct_label.nf_id == 2), action=(outport = "child-3";
> output;)
> table=??(ls_in_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 1 && reg0[[22..29]] == 2), action=(outport = "child-3";
> output;)
> table=??(ls_out_network_function), priority=0 , match=(1),
> action=(next;)
> table=??(ls_out_network_function), priority=1 , match=(reg8[[21]] ==
> 1), action=(drop;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "child-3"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(outport ==
> "child-4"), action=(next;)
> - table=??(ls_out_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_out_network_function), priority=100 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 2), action=(nf_learn_orig_src_port(ipv6 = false); outport =
> "child-4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> + table=??(ls_out_network_function), priority=100 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && reg8[[21]] == 1 && reg8[[22]] == 1 &&
> reg0[[22..29]] == 2), action=(nf_learn_orig_src_port(ipv6 = true); outport =
> "child-4"; reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "child-3"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(outport ==
> "child-4"), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_out_network_function), priority=2 , match=(inport ==
> "child-4"), action=(reg8[[24]] = nf_lookup_orig_src_port(); next;)
> table=??(ls_out_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 0 && ct_label.nf_id == 2), action=(outport = "child-4";
> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> table=??(ls_out_network_function), priority=99 , match=(reg8[[21]] == 1
> && reg8[[22]] == 1 && reg0[[22..29]] == 2), action=(outport = "child-4";
> reg8[[23]] = 1; next(pipeline=ingress, table=??);)
> ])
> @@ -20412,10 +20669,12 @@ AT_CHECK(
> [grep -E 'ls_(in|out)_network_function' lflows | ovn_strip_lflows | sort],
> [0], [dnl
> table=??(ls_in_network_function), priority=0 , match=(1), action=(next;)
> table=??(ls_in_network_function), priority=1 , match=(reg8[[21]] == 1),
> action=(drop;)
> - table=??(ls_in_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = false); next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = true); next;)
> table=??(ls_out_network_function), priority=0 , match=(1),
> action=(next;)
> table=??(ls_out_network_function), priority=1 , match=(reg8[[21]] ==
> 1), action=(drop;)
> - table=??(ls_out_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> ])
>
> AS_BOX([Configure NFG fallback method to fail-open])
> @@ -20443,10 +20702,12 @@ AT_CHECK(
> [grep -E 'ls_(in|out)_network_function' lflows | ovn_strip_lflows | sort],
> [0], [dnl
> table=??(ls_in_network_function), priority=0 , match=(1), action=(next;)
> table=??(ls_in_network_function), priority=1 , match=(reg8[[21]] == 1),
> action=(drop;)
> - table=??(ls_in_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip4 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = false); next;)
> + table=??(ls_in_network_function), priority=50 , match=(ip6 &&
> flags.inport_in_mc_unknown == 1 && !eth.mcast && reg8[[21]] == 0),
> action=(nf_learn_orig_src_port(ipv6 = true); next;)
> table=??(ls_out_network_function), priority=0 , match=(1),
> action=(next;)
> table=??(ls_out_network_function), priority=1 , match=(reg8[[21]] ==
> 1), action=(drop;)
> - table=??(ls_out_network_function), priority=100 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> + table=??(ls_out_network_function), priority=110 , match=(reg8[[21]] == 1
> && eth.mcast), action=(next;)
> ])
>
> OVN_CLEANUP_NORTHD
> @@ -20944,7 +21205,7 @@ AT_CHECK([ovn-sbctl dump-flows ls1 > ls1flows])
> AT_CAPTURE_FILE([ls1flows])
>
> AT_CHECK([grep "ls1-to-spine" ls1flows | ovn_strip_lflows | sort], [0], [dnl
> - table=??(ls_in_lookup_fdb ), priority=100 , match=(inport ==
> "ls1-to-spine"), action=(reg0[[11]] = lookup_fdb(inport, eth.src); next;)
> + table=??(ls_in_lookup_fdb ), priority=100 , match=(inport ==
> "ls1-to-spine"), action=(flags.inport_in_mc_unknown = 1; reg0[[11]] =
> lookup_fdb(inport, eth.src); next;)
> table=??(ls_in_pre_acl ), priority=110 , match=(ip && inport ==
> "ls1-to-spine"), action=(next;)
> table=??(ls_in_pre_lb ), priority=110 , match=(ip && inport ==
> "ls1-to-spine"), action=(next;)
> table=??(ls_in_put_fdb ), priority=100 , match=(inport ==
> "ls1-to-spine" && reg0[[11]] == 0), action=(put_fdb(inport, eth.src); next;)
> diff --git a/tests/ovn.at b/tests/ovn.at
> index 29ea90215..f7aeb8c4f 100644
> --- a/tests/ovn.at
> +++ b/tests/ovn.at
> @@ -33530,9 +33530,9 @@ AT_CHECK([grep "ls_in_lookup_fdb" sw0flows |
> ovn_strip_lflows], [0], [dnl
> table=??(ls_in_lookup_fdb ), priority=0 , dnl
> match=(1), action=(next;)
> table=??(ls_in_lookup_fdb ), priority=100 , dnl
> -match=(inport == "sw0-p1"), action=(reg0[[11]] = lookup_fdb(inport,
> eth.src); next;)
> +match=(inport == "sw0-p1"), action=(flags.inport_in_mc_unknown = 1;
> reg0[[11]] = lookup_fdb(inport, eth.src); next;)
> table=??(ls_in_lookup_fdb ), priority=100 , dnl
> -match=(inport == "sw0-p3"), action=(reg0[[11]] = lookup_fdb(inport,
> eth.src); next;)
> +match=(inport == "sw0-p3"), action=(flags.inport_in_mc_unknown = 1;
> reg0[[11]] = lookup_fdb(inport, eth.src); next;)
> ])
>
> AT_CHECK([grep "ls_in_put_fdb" sw0flows | ovn_strip_lflows], [0], [dnl
> @@ -44775,6 +44775,116 @@ OVN_CLEANUP([hv1],[hv2],[hv3])
> AT_CLEANUP
> ])
>
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([Network function -- post-NF re-flood copy to source port is
> dropped])
> +AT_KEYWORDS([ovn])
> +ovn_start
> +
> +# Single-node, VLAN-backed (localnet) switch. sw0-p1 and sw0-p2 carry
> +# "unknown" addresses, so they flood unknown-unicast; a to-lport ACL
> redirects
> +# traffic destined to sw0-p2 to the inline NF.
> +#
> +# sw0-p1 sends to an unknown destination MAC. The flood copy to sw0-p2 is
> +# redirected to the NF, where sw0-p1 is learned as the source port. When the
> +# NF returns the packet, the still-unknown MAC makes the switch re-flood; the
> +# copy headed back out sw0-p1 is dropped by the loop-prevention, while sw0-p2
> +# still receives it.
> +
> +check ovn-nbctl ls-add sw0
> +check ovn-nbctl lsp-add sw0 sw0-p1 -- \
> + lsp-set-addresses sw0-p1 "f0:00:00:00:00:01 192.168.0.11" "unknown"
> +check ovn-nbctl lsp-add sw0 sw0-p2 -- \
> + lsp-set-addresses sw0-p2 "f0:00:00:00:00:02 192.168.0.12" "unknown"
> +for i in 1 2; do
> + check ovn-nbctl lsp-add sw0 sw0-nf-p$i -- \
> + lsp-set-addresses sw0-nf-p$i "f0:00:00:00:01:0$i"
> +done
> +check ovn-nbctl set logical_switch_port sw0-nf-p1 \
> + options:receive_multicast=false options:lsp_learn_mac=false \
> + options:is-nf=true options:nf-linked-port=sw0-nf-p2
> +check ovn-nbctl set logical_switch_port sw0-nf-p2 \
> + options:receive_multicast=false options:lsp_learn_mac=false \
> + options:is-nf=true options:nf-linked-port=sw0-nf-p1
> +check ovn-nbctl nf-add nf0 1 sw0-nf-p1 sw0-nf-p2
> +check ovn-nbctl nfg-add nfg0 1 inline nf0
> +check ovn-nbctl acl-add sw0 to-lport 1002 'outport == "sw0-p2" && ip4' \
> + allow-related nfg0
> +
> +# VLAN-backed: add a localnet port.
> +check ovn-nbctl lsp-add-localnet-port sw0 ln0 phys
> +check ovn-nbctl set logical_switch_port ln0 tag_request=100
> +
> +net_add n
> +sim_add hv1
> +as hv1
> +ovs-vsctl add-br br-phys
> +ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys
> +ovn_attach n br-phys 192.168.1.1
> +
> +ovs-vsctl add-port br-int vif1 -- \
> + set interface vif1 external-ids:iface-id=sw0-p1 \
> + options:tx_pcap=hv1/vif1-tx.pcap options:rxq_pcap=hv1/vif1-rx.pcap
> +ovs-vsctl add-port br-int vif2 -- \
> + set interface vif2 external-ids:iface-id=sw0-p2 \
> + options:tx_pcap=hv1/vif2-tx.pcap options:rxq_pcap=hv1/vif2-rx.pcap
> +for i in 1 2; do
> + ovs-vsctl add-port br-int vif-nf$i -- \
> + set interface vif-nf$i external-ids:iface-id=sw0-nf-p$i \
> + options:tx_pcap=hv1/vif-nf$i-tx.pcap \
> + options:rxq_pcap=hv1/vif-nf$i-rx.pcap
> +done
> +
> +OVN_POPULATE_ARP
> +wait_for_ports_up
> +check ovn-nbctl --wait=hv sync
> +
> +# Match the test packet in the pcaps (src IP 192.168.0.11 followed by dst IP
> +# 192.168.0.12, consecutive only in an IP header).
> +icmp_pkt_filter=c0a8000bc0a8000c
> +
> +# 1. sw0-p1 sends to an unknown destination MAC. The flooded copy destined
> to
> +# sw0-p2 is redirected to the NF output port (sw0-nf-p2); the learn of the
> +# original source port (sw0-p1) happens on this redirect.
> +packet="inport==\"sw0-p1\" && eth.src==f0:00:00:00:00:01 &&
> + eth.dst==00:00:00:00:00:99 && ip.ttl==64 && ip4.src==192.168.0.11 &&
> + ip4.dst==192.168.0.12 && icmp4.type==8 && icmp4.code==0"
> +check as hv1 ovs-appctl -t ovn-controller inject-pkt "$packet"
> +OVS_WAIT_UNTIL([
> + pkts=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif-nf2-tx.pcap |
> grep ${icmp_pkt_filter} -c`
> + test $pkts -ge 1
> +])
> +
> +# 2. The NF returns the packet on its input port (sw0-nf-p1). The
> destination
> +# MAC is still unknown, so the switch re-floods. The copy headed back out
> +# sw0-p1 (the original ingress port) must be dropped by the
> loop-prevention;
> +# the sibling copy to sw0-p2 is delivered, confirming the re-flood
> happened.
> +packet="inport==\"sw0-nf-p1\" && eth.src==f0:00:00:00:00:01 &&
> + eth.dst==00:00:00:00:00:99 && ip.ttl==64 && ip4.src==192.168.0.11 &&
> + ip4.dst==192.168.0.12 && icmp4.type==8 && icmp4.code==0"
> +check as hv1 ovs-appctl -t ovn-controller inject-pkt "$packet"
> +OVS_WAIT_UNTIL([
> + pkts=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif2-tx.pcap |
> grep ${icmp_pkt_filter} -c`
> + test $pkts -ge 1
> +])
> +
> +# The loop-prevention drop flow (ls_out_check_port_sec, REGBIT_NF_LOOKUP_HIT,
> +# rendered as reg8=0x1000000 -> drop) must have dropped exactly the one
> +# looped-back copy.
> +OVS_WAIT_UNTIL([
> + hits=`as hv1 ovs-ofctl dump-flows br-int | grep "reg8=0x1000000" | sed
> -n 's/.*n_packets=\([[0-9]]*\).*/\1/p'`
> + test 1 -eq $hits
Instead of the rather complex sed here you could just "grep -v
n_packets=0", I think.
> +])
> +
> +# The source port must not receive the looped-back copy.
> +AT_CHECK([
> + pkts=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif1-tx.pcap |
> grep ${icmp_pkt_filter} -c`
> + test 0 -eq $pkts
> +])
> +
> +OVN_CLEANUP([hv1])
> +AT_CLEANUP
> +])
> +
> OVN_FOR_EACH_NORTHD([
> AT_SETUP([Unicast ARP when proxy ARP is configured])
> CHECK_SCAPY
Regards,
Dumitru
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev