> On 20 Jul 2026, at 7:34 PM, Dumitru Ceara <[email protected]> wrote:
>
> !-------------------------------------------------------------------|
> CAUTION: External Email
>
> |-------------------------------------------------------------------!
>
> 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.
Hi Dumitru,
Thanks for the review.
Apologies for the late reply.
I think we can remove the remote port check.
I was trying to avoid learning excessive entries in the
OFTABLE_NF_ORIG_INPORT_LEARN table, which made the patch a bit
more complex.
As per your suggestion, we can set MLF_INPORT_IN_MC_UNKNOWN in the
physical pipeline. To keep this driven by northd, we can have
northd set options:mc_unknown_ingress on the SB Port_Binding for
eligible LSPs, and load MLF_INPORT_IN_MC_UNKNOWN in the
OFTABLE_PHY_TO_LOG stage when that option is set. NF stages keep
the existing "flags.inport_in_mc_unknown == 1" guard on
learn/lookup.
If this approach looks fine, I'll address the rest of your comments
and send v5.
On a separate note, while thinking through the L2 stretch case with
overlay subnets over OVN IC (spine/leaf, VMs on leaf LSes connected
via a Transit_Switch with type=remote ports), I noticed a gap when
there are ports with "unknown" addresses on both AZs and the NF is
not co-located with the IC gateway that decapsulates the packet:
the post-NF flood copy can still leak back out towards the source
side and cause a MAC flap on the Transit_Switch FDB.
For now I'd like to call out the L2 stretch case as a known
limitation and request to consider this patch as the immediate fix
for non-stretch deployments.
For a longer-term solution I'm still exploring options. One
direction I'm considering is to extend the Geneve header so that
post-NF packets carry the original source inport, and use it on
the receiving chassis to drop copies whose outport matches the
carried inport. This keeps the "same context of how the packet
was redirected" available on the return path without needing a
learn/lookup table. Let me know your thoughts, or if there are
better alternatives.
Thanks,
Naveen
>
>> + 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