On Fri, Apr 24, 2026 at 05:35:58PM +0200, Dumitru Ceara via dev wrote:
> The ARP/ND responder stage (ls_in_arp_rsp) unconditionally
> bypassed all traffic arriving from localnet ports via a
> priority-100 "next;" flow.  This caused broadcast ARP/ND
> requests from the physical network to be flooded to every
> logical switch port instead of being handled by proxy
> ARP/ND.  On switches with ~200+ ports the resulting
> multicast replication exceeded the OVS 4K resubmit limit,
> dropping the packets and breaking connectivity.
> 
> Replace the bypass with a targeted mechanism:
> 
>   - In ls_in_lookup_fdb, set flags.localnet = 1 for
>     packets arriving from localnet ports (P50 fallback;
>     the existing P100 FDB-learning flow already sets this
>     flag when FDB learning is enabled).
> 
>   - In the P50 ARP/ND reply flows, append the condition
>     "((flags.localnet == 1 && is_chassis_resident(port))
>      || flags.localnet == 0)" on switches that have
>     localnet ports.
> 
> This ensures that ARP/ND requests from localnet are only
> answered on the chassis hosting the target VIF, preventing
> both the flood and duplicate replies from multiple
> hypervisors.  VIF-to-VIF proxy ARP/ND is unchanged because
> flags.localnet is 0 for non-localnet-sourced traffic.
> 
> Fixes: f763a3273b84 ("ovn: Avoid ARP responder for packets from localnet 
> port")
> Reported-at: https://redhat.atlassian.net/browse/FDP-3436
> Assisted-by: Claude Opus 4.6, Claude Code
> Signed-off-by: Dumitru Ceara <[email protected]>
> ---
>  northd/northd.c         |  44 +++++++---
>  northd/ovn-northd.8.xml |  76 +++++++++++-----
>  tests/ovn-northd.at     | 111 ++++++++++++++++++++++-
>  tests/ovn.at            | 189 ++++++++++++++++++++++++++++++++++++++++
>  4 files changed, 389 insertions(+), 31 deletions(-)
> 
> diff --git a/northd/northd.c b/northd/northd.c
> index 02c7e7e54e..8305e0428b 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -10402,25 +10402,43 @@ build_arp_nd_service_monitor_lflow(const char 
> *svc_monitor_mac,
>      }
>  }
>  
> -/* Ingress table 24: ARP/ND responder, skip requests coming from localnet
> - * ports. (priority 100); see ovn-northd.8.xml for the rationale. */
> -
> +/* 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. */
>  static void
> -build_lswitch_arp_nd_responder_skip_local(struct ovn_port *op,
> -                                          struct lflow_table *lflows,
> -                                          struct ds *match)
> +build_lswitch_from_localnet_op(struct ovn_port *op,
> +                               struct lflow_table *lflows,
> +                               struct ds *match)
>  {
>      ovs_assert(op->nbsp);
> -    if (!lsp_is_localnet(op->nbsp) || op->od->has_arp_proxy_port) {
> +    if (!lsp_is_localnet(op->nbsp)) {
>          return;
>      }
>      ds_clear(match);
>      ds_put_format(match, "inport == %s", op->json_key);
> -    ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100, 
> ds_cstr(match),
> -                  "next;", op->lflow_ref, WITH_IO_PORT(op->key),
> +    ovn_lflow_add(lflows, op->od, S_SWITCH_IN_LOOKUP_FDB, 50,
> +                  ds_cstr(match), "flags.localnet = 1; next;",
> +                  op->lflow_ref, WITH_IO_PORT(op->key),
>                    WITH_HINT(&op->nbsp->header_));
>  }
>  
> +/* On switches with localnet ports, restrict ARP/ND replies for
> + * localnet-sourced requests to the chassis hosting the target VIF
> + * (preventing duplicate replies from every hypervisor).  Non-localnet
> + * requests (VIF-to-VIF) are answered unconditionally as before. */
> +static void
> +build_lswitch_arp_nd_local_resp_match(struct ds *match,
> +                                      const struct ovn_port *op)
> +{
> +    if (!ls_has_localnet_port(op->od)) {
> +        return;
> +    }
> +
> +    ds_put_format(match,
> +        " && ((flags.localnet == 1 && is_chassis_resident(%s))"
> +            " || flags.localnet == 0)", op->json_key);
nit: spacing
> +}
> +
>  /* Ingress table 24: ARP/ND responder, reply for known IPs.
>   * (priority 50). */
>  static void
> @@ -10562,6 +10580,8 @@ build_lswitch_arp_nd_responder_known_ips(struct 
> ovn_port *op,
>                      ds_truncate(match, match_len);
>                  }
>                  ds_put_cstr(match, " && eth.dst == ff:ff:ff:ff:ff:ff");
> +                size_t match_arp_len = match->length;
> +                build_lswitch_arp_nd_local_resp_match(match, op);
>  
>                  ds_clear(actions);
>                  ds_put_format(actions,
> @@ -10593,6 +10613,7 @@ build_lswitch_arp_nd_responder_known_ips(struct 
> ovn_port *op,
>                   * address is intended to detect situations where the
>                   * network is not working as configured, so dropping the
>                   * request would frustrate that intent.) */
> +                ds_truncate(match, match_arp_len);
>                  ds_put_format(match, " && inport == %s", op->json_key);
>                  ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
>                                ds_cstr(match), "next;", op->lflow_ref,
> @@ -10632,6 +10653,8 @@ build_lswitch_arp_nd_responder_known_ips(struct 
> ovn_port *op,
>                      "nd_ns_mcast && ip6.dst == %s && nd.target == %s",
>                      op->lsp_addrs[i].ipv6_addrs[j].sn_addr_s,
>                      op->lsp_addrs[i].ipv6_addrs[j].addr_s);
> +                size_t match_nd_len = match->length;
> +                build_lswitch_arp_nd_local_resp_match(match, op);
>  
>                  ds_clear(actions);
>                  ds_put_format(actions,
> @@ -10658,6 +10681,7 @@ build_lswitch_arp_nd_responder_known_ips(struct 
> ovn_port *op,
>  
>                  /* Do not reply to a solicitation from the port that owns
>                   * the address (otherwise DAD detection will fail). */
> +                ds_truncate(match, match_nd_len);
>                  ds_put_format(match, " && inport == %s", op->json_key);
>                  ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
>                                ds_cstr(match), "next;", op->lflow_ref,
> @@ -19554,7 +19578,7 @@ 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_arp_nd_responder_skip_local(op, lflows, match);
> +    build_lswitch_from_localnet_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/ovn-northd.8.xml b/northd/ovn-northd.8.xml
> index 4d6370da6b..4ba4ab3cd1 100644
> --- a/northd/ovn-northd.8.xml
> +++ b/northd/ovn-northd.8.xml
> @@ -488,6 +488,21 @@
>          </ul>
>        </li>
>  
> +      <li>
> +        <p>
> +          For each localnet logical port <var>p</var>, a priority-50
> +          fallback flow is added with the match
> +          <code>inport == <var>p</var></code> and action
> +          <code>flags.localnet = 1; next;</code>.  This marks traffic
> +          arriving from localnet ports so that downstream stages (e.g.,
> +          ARP/ND responder) can condition their behavior.  When FDB
> +          learning is enabled on the localnet port, the priority-100
> +          flow described above already sets <code>flags.localnet</code>,
> +          so this priority-50 flow only takes effect when FDB learning
> +          is not configured.
> +        </p>
> +      </li>
> +
>        <li>
>          One priority-0 fallback flow that matches all packets and advances to
>          the next table.
> @@ -1734,12 +1749,16 @@
>      </p>
>  
>      <p>
> -      Note that ARP requests received from <code>localnet</code> logical
> -      inports can either go directly to VMs, in which case the VM responds or
> -      can hit an ARP responder for a logical router port if the packet is 
> used
> -      to resolve a logical router port next hop address.  In either case,
> -      logical switch ARP responder rules will not be hit.  It contains these
> -      logical flows:
> +      ARP/ND requests received from <code>localnet</code> logical inports
> +      do hit the ARP/ND responder, but the response is limited to the
> +      chassis that hosts the target VIF.  This is achieved by adding
> +      a <code>flags.localnet</code> check to the priority-50 reply flows
> +      (see below): when the request arrives from a localnet port
> +      (<code>flags.localnet == 1</code>), only the chassis on which the
> +      target port is resident will reply.  When the request arrives from
> +      a non-localnet port (<code>flags.localnet == 0</code>), the
> +      response is unconditional, preserving VIF-to-VIF proxy ARP/ND
> +      behavior.  It contains these logical flows:
>       </p>
>  
>      <ul>
> @@ -1750,18 +1769,10 @@
>          router ingress pipeline.
>        </li>
>        <li>
> -        If the logical switch has no router ports with options:arp_proxy
> -        configured add a priority-100 flows to skip the ARP responder if 
> inport
> -        is of type <code>localnet</code> advances directly to the next table.
> -        ARP requests sent to <code>localnet</code> ports can be received by
> -        multiple hypervisors.  Now, because the same mac binding rules are
> -        downloaded to all hypervisors, each of the multiple hypervisors will
> -        respond.  This will confuse L2 learning on the source of the ARP
> -        requests.  ARP requests received on an inport of type
> -        <code>router</code> are not expected to hit any logical switch ARP
> -        responder flows.  However, no skip flows are installed for these
> -        packets, as there would be some additional flow cost for this and the
> -        value appears limited.
> +        ARP/ND requests received on an inport of type <code>router</code> are
> +        not expected to hit any logical switch ARP responder flows.  However,
> +        no skip flows are installed for these packets, as there would be some
> +        additional flow cost for this and the value appears limited.
>        </li>
>  
>        <li>
> @@ -1816,6 +1827,18 @@ flags.loopback = 1;
>  output;
>          </pre>
>  
> +        <p>
> +          On logical switches that have a localnet port, the match for
> +          these flows includes an additional condition:
> +          <code>((flags.localnet == 1 &amp;&amp;
> +          is_chassis_resident(<var>port</var>)) ||
> +          flags.localnet == 0)</code>.
> +          This ensures that when an ARP request arrives from a localnet
> +          port, only the chassis hosting the target VIF responds.  When
> +          the request arrives from a non-localnet port, the response is
> +          unconditional, preserving VIF-to-VIF proxy ARP behavior.
> +        </p>
> +
>          <p>
>            These flows are omitted for logical ports (other than router ports 
> or
>            <code>localport</code> ports) that are down (unless <code>
> @@ -1877,6 +1900,19 @@ nd_na_router {
>  };
>          </pre>
>  
> +        <p>
> +          On logical switches that have a localnet port, the match for
> +          these flows includes an additional condition:
> +          <code>((flags.localnet == 1 &amp;&amp;
> +          is_chassis_resident(<var>port</var>)) ||
> +          flags.localnet == 0)</code>.
> +          This ensures that when an ND solicitation arrives from a
> +          localnet port, only the chassis hosting the target VIF
> +          responds.  When the solicitation arrives from a non-localnet
> +          port, the response is unconditional, preserving VIF-to-VIF
> +          proxy ND behavior.
> +        </p>
> +
>          <p>
>            These flows are omitted for logical ports (other than router ports 
> or
>            <code>localport</code> ports) that are down (unless <code>
> @@ -1896,8 +1932,8 @@ nd_na_router {
>  
>        <li>
>          <p>
> -          Priority-100 flows with match criteria like the ARP and ND flows
> -          above, except that they only match packets from the
> +          Priority-100 flows with match criteria similar to the ARP and ND
> +          flows above, except that they only match packets from the
>            <code>inport</code> that owns the IP addresses in question, with
>            action <code>next;</code>.  These flows prevent OVN from replying 
> to,
>            for example, an ARP request emitted by a VM for its own IP address.
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index 1d7bd6c288..df7bac1529 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -7730,7 +7730,9 @@ AT_CHECK([grep -e "ls_in_.*_fdb.*S1-vm1" S1flows | 
> ovn_strip_lflows], [0], [dnl
>  ])
>  
>  #Verify the flows for a non-default port type (localnet port)
> -AT_CHECK([grep -e "ls_in_.*_fdb.*S1-localnet" S1flows], [1], [])
> +AT_CHECK([grep -e "ls_in_.*_fdb.*S1-localnet" S1flows | ovn_strip_lflows], 
> [0], [dnl
> +  table=??(ls_in_lookup_fdb   ), priority=50   , match=(inport == 
> "S1-localnet"), action=(flags.localnet = 1; next;)
> +])
>  
>  OVN_CLEANUP_NORTHD
>  AT_CLEANUP
> @@ -10039,6 +10041,7 @@ AT_CHECK([ovn-nbctl --wait=sb sync])
>  # Check MAC learning flows with 'localnet_learn_fdb' default (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_put_fdb      ), priority=0    , match=(1), action=(next;)
>  ])
>  
> @@ -10047,6 +10050,7 @@ 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_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;)
>  ])
> @@ -10055,6 +10059,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_put_fdb      ), priority=0    , match=(1), action=(next;)
>  ])
>  
> @@ -10404,6 +10409,110 @@ OVN_CLEANUP_NORTHD
>  AT_CLEANUP
>  ])
>  
> +OVN_FOR_EACH_NORTHD_NO_HV([
> +AT_SETUP([ARP/ND responder for localnet-sourced requests])
> +ovn_start
> +
> +dnl Switch with localnet port.
> +check ovn-nbctl ls-add ls1
> +check ovn-nbctl lsp-add-localnet-port ls1 ln1 physnet1
> +check ovn-nbctl lsp-add ls1 vm1 \
> +    -- lsp-set-addresses vm1 "00:00:00:00:00:01 10.0.0.1 fd01::1"
> +check ovn-nbctl lsp-add ls1 vm2 \
> +    -- lsp-set-addresses vm2 "00:00:00:00:00:02 10.0.0.2 fd01::2"
> +
> +dnl Switch without localnet port.
> +check ovn-nbctl ls-add ls2
> +check ovn-nbctl --wait=sb lsp-add ls2 vm3 \
> +    -- lsp-set-addresses vm3 "00:00:00:00:00:03 10.0.0.3 fd01::3"
> +
> +AS_BOX([FDB learning disabled])
> +
> +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;)
> +])
> +
> +dnl ls1: ls_in_arp_rsp should include flags.localnet condition for
> +dnl priority 50 ARP/ND reply flows but NOT for priority 100 self-reply
> +dnl flows (since those match on inport == VIF, flags.localnet is always 0).
> +AT_CHECK([ovn-sbctl dump-flows ls1 | grep -e 'ls_in_arp_rsp' | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(ls_in_arp_rsp      ), priority=0    , match=(1), action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=100  , match=(arp.tpa == 10.0.0.1 
> && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && inport == "vm1"), 
> action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=100  , match=(arp.tpa == 10.0.0.2 
> && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && inport == "vm2"), 
> action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && 
> ip6.dst == ff02::1:ff00:1 && nd.target == fd01::1 && inport == "vm1"), 
> action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && 
> ip6.dst == ff02::1:ff00:2 && nd.target == fd01::2 && inport == "vm2"), 
> action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 10.0.0.1 
> && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && ((flags.localnet == 1 && 
> is_chassis_resident("vm1")) || flags.localnet == 0)), action=(eth.dst = 
> eth.src; eth.src = 00:00:00:00:00:01; arp.op = 2; /* ARP reply */ arp.tha = 
> arp.sha; arp.sha = 00:00:00:00:00:01; arp.tpa = arp.spa; arp.spa = 10.0.0.1; 
> outport = inport; flags.loopback = 1; output;)
> +  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 10.0.0.2 
> && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && ((flags.localnet == 1 && 
> is_chassis_resident("vm2")) || flags.localnet == 0)), action=(eth.dst = 
> eth.src; eth.src = 00:00:00:00:00:02; arp.op = 2; /* ARP reply */ arp.tha = 
> arp.sha; arp.sha = 00:00:00:00:00:02; arp.tpa = arp.spa; arp.spa = 10.0.0.2; 
> outport = inport; flags.loopback = 1; output;)
> +  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && 
> ip6.dst == ff02::1:ff00:1 && nd.target == fd01::1 && ((flags.localnet == 1 && 
> is_chassis_resident("vm1")) || flags.localnet == 0)), action=(nd_na { eth.src 
> = 00:00:00:00:00:01; ip6.src = fd01::1; nd.target = fd01::1; nd.tll = 
> 00:00:00:00:00:01; outport = inport; flags.loopback = 1; output; };)
> +  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && 
> ip6.dst == ff02::1:ff00:2 && nd.target == fd01::2 && ((flags.localnet == 1 && 
> is_chassis_resident("vm2")) || flags.localnet == 0)), action=(nd_na { eth.src 
> = 00:00:00:00:00:02; ip6.src = fd01::2; nd.target = fd01::2; nd.tll = 
> 00:00:00:00:00:02; outport = inport; flags.loopback = 1; output; };)
> +])
> +
> +dnl ls2: ls_in_arp_rsp should NOT include flags.localnet condition.
> +AT_CHECK([ovn-sbctl dump-flows ls2 | grep -e 'ls_in_arp_rsp' | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(ls_in_arp_rsp      ), priority=0    , match=(1), action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=100  , match=(arp.tpa == 10.0.0.3 
> && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && inport == "vm3"), 
> action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && 
> ip6.dst == ff02::1:ff00:3 && nd.target == fd01::3 && inport == "vm3"), 
> action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 10.0.0.3 
> && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff), action=(eth.dst = eth.src; 
> eth.src = 00:00:00:00:00:03; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; 
> arp.sha = 00:00:00:00:00:03; arp.tpa = arp.spa; arp.spa = 10.0.0.3; outport = 
> inport; flags.loopback = 1; output;)
> +  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && 
> ip6.dst == ff02::1:ff00:3 && nd.target == fd01::3), action=(nd_na { eth.src = 
> 00:00:00:00:00:03; ip6.src = fd01::3; nd.target = fd01::3; nd.tll = 
> 00:00:00:00:00:03; outport = inport; flags.loopback = 1; output; };)
> +])
> +
> +dnl ls2: ls_in_lookup_fdb should only have priority 0 default,
> +dnl no priority 50 flags.localnet.
> +AT_CHECK([ovn-sbctl dump-flows ls2 | grep -e 'ls_in_lookup_fdb' | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(ls_in_lookup_fdb   ), priority=0    , match=(1), action=(next;)
> +])
> +
> +AS_BOX([Enable FDB learning on ln1])
> +check ovn-nbctl --wait=sb lsp-set-options ln1 localnet_learn_fdb=true
> +
> +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;)
> +])
> +
> +dnl ls1: ls_in_arp_rsp should be unchanged.
> +AT_CHECK([ovn-sbctl dump-flows ls1 | grep -e 'ls_in_arp_rsp' | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(ls_in_arp_rsp      ), priority=0    , match=(1), action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=100  , match=(arp.tpa == 10.0.0.1 
> && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && inport == "vm1"), 
> action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=100  , match=(arp.tpa == 10.0.0.2 
> && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && inport == "vm2"), 
> action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && 
> ip6.dst == ff02::1:ff00:1 && nd.target == fd01::1 && inport == "vm1"), 
> action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && 
> ip6.dst == ff02::1:ff00:2 && nd.target == fd01::2 && inport == "vm2"), 
> action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 10.0.0.1 
> && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && ((flags.localnet == 1 && 
> is_chassis_resident("vm1")) || flags.localnet == 0)), action=(eth.dst = 
> eth.src; eth.src = 00:00:00:00:00:01; arp.op = 2; /* ARP reply */ arp.tha = 
> arp.sha; arp.sha = 00:00:00:00:00:01; arp.tpa = arp.spa; arp.spa = 10.0.0.1; 
> outport = inport; flags.loopback = 1; output;)
> +  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 10.0.0.2 
> && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && ((flags.localnet == 1 && 
> is_chassis_resident("vm2")) || flags.localnet == 0)), action=(eth.dst = 
> eth.src; eth.src = 00:00:00:00:00:02; arp.op = 2; /* ARP reply */ arp.tha = 
> arp.sha; arp.sha = 00:00:00:00:00:02; arp.tpa = arp.spa; arp.spa = 10.0.0.2; 
> outport = inport; flags.loopback = 1; output;)
> +  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && 
> ip6.dst == ff02::1:ff00:1 && nd.target == fd01::1 && ((flags.localnet == 1 && 
> is_chassis_resident("vm1")) || flags.localnet == 0)), action=(nd_na { eth.src 
> = 00:00:00:00:00:01; ip6.src = fd01::1; nd.target = fd01::1; nd.tll = 
> 00:00:00:00:00:01; outport = inport; flags.loopback = 1; output; };)
> +  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && 
> ip6.dst == ff02::1:ff00:2 && nd.target == fd01::2 && ((flags.localnet == 1 && 
> is_chassis_resident("vm2")) || flags.localnet == 0)), action=(nd_na { eth.src 
> = 00:00:00:00:00:02; ip6.src = fd01::2; nd.target = fd01::2; nd.tll = 
> 00:00:00:00:00:02; outport = inport; flags.loopback = 1; output; };)
> +])
> +
> +AS_BOX([Disable FDB learning])
> +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;)
> +])
> +
> +AT_CHECK([ovn-sbctl dump-flows ls1 | grep -e 'ls_in_arp_rsp' | 
> ovn_strip_lflows], [0], [dnl
> +  table=??(ls_in_arp_rsp      ), priority=0    , match=(1), action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=100  , match=(arp.tpa == 10.0.0.1 
> && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && inport == "vm1"), 
> action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=100  , match=(arp.tpa == 10.0.0.2 
> && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && inport == "vm2"), 
> action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && 
> ip6.dst == ff02::1:ff00:1 && nd.target == fd01::1 && inport == "vm1"), 
> action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=100  , match=(nd_ns_mcast && 
> ip6.dst == ff02::1:ff00:2 && nd.target == fd01::2 && inport == "vm2"), 
> action=(next;)
> +  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 10.0.0.1 
> && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && ((flags.localnet == 1 && 
> is_chassis_resident("vm1")) || flags.localnet == 0)), action=(eth.dst = 
> eth.src; eth.src = 00:00:00:00:00:01; arp.op = 2; /* ARP reply */ arp.tha = 
> arp.sha; arp.sha = 00:00:00:00:00:01; arp.tpa = arp.spa; arp.spa = 10.0.0.1; 
> outport = inport; flags.loopback = 1; output;)
> +  table=??(ls_in_arp_rsp      ), priority=50   , match=(arp.tpa == 10.0.0.2 
> && arp.op == 1 && eth.dst == ff:ff:ff:ff:ff:ff && ((flags.localnet == 1 && 
> is_chassis_resident("vm2")) || flags.localnet == 0)), action=(eth.dst = 
> eth.src; eth.src = 00:00:00:00:00:02; arp.op = 2; /* ARP reply */ arp.tha = 
> arp.sha; arp.sha = 00:00:00:00:00:02; arp.tpa = arp.spa; arp.spa = 10.0.0.2; 
> outport = inport; flags.loopback = 1; output;)
> +  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && 
> ip6.dst == ff02::1:ff00:1 && nd.target == fd01::1 && ((flags.localnet == 1 && 
> is_chassis_resident("vm1")) || flags.localnet == 0)), action=(nd_na { eth.src 
> = 00:00:00:00:00:01; ip6.src = fd01::1; nd.target = fd01::1; nd.tll = 
> 00:00:00:00:00:01; outport = inport; flags.loopback = 1; output; };)
> +  table=??(ls_in_arp_rsp      ), priority=50   , match=(nd_ns_mcast && 
> ip6.dst == ff02::1:ff00:2 && nd.target == fd01::2 && ((flags.localnet == 1 && 
> is_chassis_resident("vm2")) || flags.localnet == 0)), action=(nd_na { eth.src 
> = 00:00:00:00:00:02; ip6.src = fd01::2; nd.target = fd01::2; nd.tll = 
> 00:00:00:00:00:02; outport = inport; flags.loopback = 1; output; };)
> +])
> +
> +OVN_CLEANUP_NORTHD
> +AT_CLEANUP
> +])
> +
>  OVN_FOR_EACH_NORTHD_NO_HV([
>  AT_SETUP([Address set incremental processing])
>  ovn_start
> diff --git a/tests/ovn.at b/tests/ovn.at
> index c0ae611bc9..fbaa63d99c 100644
> --- a/tests/ovn.at
> +++ b/tests/ovn.at
> @@ -10190,6 +10190,195 @@ OVN_CLEANUP([hv1])
>  AT_CLEANUP
>  ])
>  
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([ARP/ND from localnet -- proxy reply on resident chassis only])
> +AT_SKIP_IF([test $HAVE_SCAPY = no])
> +ovn_start
> +
> +dnl Create logical switch with localnet port.
> +check ovn-nbctl ls-add ls1
> +check ovn-nbctl lsp-add-localnet-port ls1 ln1 physnet1
> +check ovn-nbctl lsp-add ls1 vm1 \
> +    -- lsp-set-addresses vm1 "f0:00:00:00:00:01 10.0.0.1 fd01::1"
> +check ovn-nbctl lsp-add ls1 vm2 \
> +    -- lsp-set-addresses vm2 "f0:00:00:00:00:02 10.0.0.2 fd01::2"
> +
> +dnl Two hypervisors with bridge-mappings.
> +net_add n1
> +
> +sim_add hv1
> +as hv1
> +ovs-vsctl \
> +    -- add-br br-phys \
> +    -- add-br br-eth0
> +ovn_attach n1 br-phys 192.168.0.1
> +check ovs-vsctl set Open_vSwitch . 
> external-ids:ovn-bridge-mappings=physnet1:br-eth0
> +check ovs-vsctl add-port br-eth0 snoopvif1 \
> +    -- set Interface snoopvif1 options:tx_pcap=hv1/snoopvif-tx.pcap \
> +                                options:rxq_pcap=hv1/snoopvif-rx.pcap
> +check ovs-vsctl add-port br-int vm1 \
> +    -- set Interface vm1 external-ids:iface-id=vm1 \
> +                         options:tx_pcap=hv1/vm1-tx.pcap \
> +                         options:rxq_pcap=hv1/vm1-rx.pcap
> +
> +sim_add hv2
> +as hv2
> +ovs-vsctl \
> +    -- add-br br-phys \
> +    -- add-br br-eth0
> +ovn_attach n1 br-phys 192.168.0.2
> +check ovs-vsctl set Open_vSwitch . 
> external-ids:ovn-bridge-mappings=physnet1:br-eth0
> +check ovs-vsctl add-port br-eth0 snoopvif2 \
> +    -- set Interface snoopvif2 options:tx_pcap=hv2/snoopvif-tx.pcap \
> +                                options:rxq_pcap=hv2/snoopvif-rx.pcap
> +check ovs-vsctl add-port br-int vm2 \
> +    -- set Interface vm2 external-ids:iface-id=vm2 \
> +                         options:tx_pcap=hv2/vm2-tx.pcap \
> +                         options:rxq_pcap=hv2/vm2-rx.pcap
> +
> +wait_for_ports_up vm1 vm2
> +OVN_POPULATE_ARP
> +check ovn-nbctl --wait=hv sync
> +
> +dnl Helper: construct ARP request.
> +build_arp_request() {
> +    local sha=$1 spa=$2 tpa=$3
> +    fmt_pkt "Ether(dst='ff:ff:ff:ff:ff:ff', src='${sha}')/ \
> +             ARP(hwsrc='${sha}', hwdst='ff:ff:ff:ff:ff:ff', \
> +                 psrc='${spa}', pdst='${tpa}')"
> +}
> +
> +dnl Helper: construct expected ARP reply.
> +build_arp_reply() {
> +    local req_sha=$1 req_spa=$2 reply_sha=$3 reply_spa=$4
> +    fmt_pkt "Ether(dst='${req_sha}', src='${reply_sha}')/ \
> +             ARP(op=2, hwsrc='${reply_sha}', hwdst='${req_sha}', \
> +                 psrc='${reply_spa}', pdst='${req_spa}')"
> +}
> +
> +dnl Helper: construct ND solicitation.
> +build_nd_ns() {
> +    local sha=$1 spa=$2 tpa=$3 sol_mcast=$4
> +    fmt_pkt "Ether(dst='33:33:ff:00:00:0${tpa##*:}', src='${sha}')/ \
> +             IPv6(src='${spa}', dst='${sol_mcast}')/ \
> +             ICMPv6ND_NS(tgt='${tpa}')/ \
> +             ICMPv6NDOptSrcLLAddr(lladdr='${sha}')"
> +}
> +
> +dnl Helper: construct expected ND advertisement.
> +build_nd_na() {
> +    local req_sha=$1 req_spa=$2 reply_sha=$3 reply_tgt=$4
> +    fmt_pkt "Ether(dst='${req_sha}', src='${reply_sha}')/ \
> +             IPv6(src='${reply_tgt}', dst='${req_spa}')/ \
> +             ICMPv6ND_NA(tgt='${reply_tgt}', R=0, S=1, O=1)/ \
> +             ICMPv6NDOptDstLLAddr(lladdr='${reply_sha}')"
> +}
> +
> +test_arp_nd_localnet() {
> +    AS_BOX([ARP from localnet on hv1 for vm1 - expect reply])
> +    as hv1 reset_pcap_file snoopvif1 hv1/snoopvif
> +    as hv2 reset_pcap_file snoopvif2 hv2/snoopvif
> +    as hv1 reset_pcap_file vm1 hv1/vm1
> +    as hv2 reset_pcap_file vm2 hv2/vm2
> +
> +    dnl ARP request from br-eth0 on hv1 for vm1 (10.0.0.1).
> +    dnl vm1 is resident on hv1, so hv1 should reply.
> +    local arp_req=$(build_arp_request "f0:00:00:00:00:99" "10.0.0.99" 
> "10.0.0.1")
> +    as hv1 ovs-appctl netdev-dummy/receive snoopvif1 $arp_req
> +    local arp_rep=$(build_arp_reply "f0:00:00:00:00:99" "10.0.0.99" \
> +                                    "f0:00:00:00:00:01" "10.0.0.1")
> +    echo $arp_rep > expected_arp_reply
> +    OVN_CHECK_PACKETS_CONTAIN([hv1/snoopvif-tx.pcap], [expected_arp_reply])
> +
> +    AS_BOX([ARP from localnet on hv2 for vm1 - expect no reply])
> +    as hv2 reset_pcap_file snoopvif2 hv2/snoopvif
> +
> +    dnl ARP request from br-eth0 on hv2 for vm1 (10.0.0.1).
> +    dnl vm1 is NOT resident on hv2, so hv2 should NOT reply.
> +    dnl To avoid relying on sleep, we also send an ARP request for vm2
> +    dnl (which IS resident on hv2) and wait for that reply.  This proves
> +    dnl the pipeline is running and any reply for vm1 would have appeared.
> +    as hv2 ovs-appctl netdev-dummy/receive snoopvif2 $arp_req
> +
> +    local arp_req_vm2=$(build_arp_request "f0:00:00:00:00:99" "10.0.0.99" 
> "10.0.0.2")
> +    as hv2 ovs-appctl netdev-dummy/receive snoopvif2 $arp_req_vm2
> +    local arp_rep_vm2=$(build_arp_reply "f0:00:00:00:00:99" "10.0.0.99" \
> +                                        "f0:00:00:00:00:02" "10.0.0.2")
> +    echo $arp_rep_vm2 > expected_arp_vm2
> +    OVN_CHECK_PACKETS_CONTAIN([hv2/snoopvif-tx.pcap], [expected_arp_vm2])
> +
> +    dnl Now verify that no ARP reply for vm1 was generated on hv2.
> +    AT_CHECK([$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" 
> hv2/snoopvif-tx.pcap | \
> +              grep -c "$arp_rep"], [1], [dnl
> +0
> +])
> +
> +    AS_BOX([ARP from vm2 VIF for vm1 - expect proxy reply])
> +    as hv2 reset_pcap_file vm2 hv2/vm2
> +    local arp_req2=$(build_arp_request "f0:00:00:00:00:02" "10.0.0.2" 
> "10.0.0.1")
> +    as hv2 ovs-appctl netdev-dummy/receive vm2 $arp_req2
> +    local arp_rep2=$(build_arp_reply "f0:00:00:00:00:02" "10.0.0.2" \
> +                                     "f0:00:00:00:00:01" "10.0.0.1")
> +    echo $arp_rep2 > expected_arp_proxy
> +    OVN_CHECK_PACKETS_CONTAIN([hv2/vm2-tx.pcap], [expected_arp_proxy])
> +
> +    AS_BOX([ND from localnet on hv1 for vm1 - expect reply])
> +    as hv1 reset_pcap_file snoopvif1 hv1/snoopvif
> +    as hv2 reset_pcap_file snoopvif2 hv2/snoopvif
> +
> +    dnl ND solicitation from br-eth0 on hv1 for vm1 IPv6 (fd01::1).
> +    dnl vm1 is resident on hv1, so hv1 should reply.
> +    local nd_ns=$(build_nd_ns "f0:00:00:00:00:99" "fd01::99" "fd01::1" 
> "ff02::1:ff00:1")
> +    as hv1 ovs-appctl netdev-dummy/receive snoopvif1 $nd_ns
> +    local nd_na=$(build_nd_na "f0:00:00:00:00:99" "fd01::99" \
> +                               "f0:00:00:00:00:01" "fd01::1")
> +    echo $nd_na > expected_nd_reply
> +    OVN_CHECK_PACKETS_CONTAIN([hv1/snoopvif-tx.pcap], [expected_nd_reply])
> +
> +    AS_BOX([ND from localnet on hv2 for vm1 - expect no reply])
> +    as hv2 reset_pcap_file snoopvif2 hv2/snoopvif
> +
> +    dnl ND solicitation from br-eth0 on hv2 for vm1 IPv6 (fd01::1).
> +    dnl vm1 is NOT resident on hv2, so hv2 should NOT reply.
> +    dnl Same technique: send ND for vm2 (resident) and wait for that reply.
> +    as hv2 ovs-appctl netdev-dummy/receive snoopvif2 $nd_ns
> +
> +    local nd_ns_vm2=$(build_nd_ns "f0:00:00:00:00:99" "fd01::99" "fd01::2" 
> "ff02::1:ff00:2")
> +    as hv2 ovs-appctl netdev-dummy/receive snoopvif2 $nd_ns_vm2
> +    local nd_na_vm2=$(build_nd_na "f0:00:00:00:00:99" "fd01::99" \
> +                                   "f0:00:00:00:00:02" "fd01::2")
> +    echo $nd_na_vm2 > expected_nd_vm2
> +    OVN_CHECK_PACKETS_CONTAIN([hv2/snoopvif-tx.pcap], [expected_nd_vm2])
> +
> +    dnl Now verify that no ND advertisement for vm1 was generated on hv2.
> +    AT_CHECK([$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" 
> hv2/snoopvif-tx.pcap | \
> +              grep -c "$nd_na"], [1], [dnl
> +0
> +])
> +
> +    AS_BOX([ND from vm2 VIF for vm1 - expect proxy reply])
> +    as hv2 reset_pcap_file vm2 hv2/vm2
> +    local nd_ns2=$(build_nd_ns "f0:00:00:00:00:02" "fd01::2" "fd01::1" 
> "ff02::1:ff00:1")
> +    as hv2 ovs-appctl netdev-dummy/receive vm2 $nd_ns2
> +    local nd_na2=$(build_nd_na "f0:00:00:00:00:02" "fd01::2" \
> +                                "f0:00:00:00:00:01" "fd01::1")
> +    echo $nd_na2 > expected_nd_proxy
> +    OVN_CHECK_PACKETS_CONTAIN([hv2/vm2-tx.pcap], [expected_nd_proxy])
> +}
> +
> +AS_BOX([FDB learning disabled])
> +test_arp_nd_localnet
> +
> +AS_BOX([FDB learning enabled])
> +dnl Use 'set' instead of 'lsp-set-options' to preserve network_name.
> +check ovn-nbctl --wait=hv set Logical_Switch_Port ln1 \
> +    options:localnet_learn_fdb=true
> +test_arp_nd_localnet
> +
> +OVN_CLEANUP([hv1],[hv2])
> +AT_CLEANUP
> +])
> +
>  OVN_FOR_EACH_NORTHD([
>  AT_SETUP([send reverse arp for router without ipv4 address])
>  ovn_start
> -- 
> 2.53.0
> 
> _______________________________________________
> dev mailing list
> [email protected]
> https://mail.openvswitch.org/mailman/listinfo/ovs-dev
> 
LGTM. Just one small nit.

Acked-by: Mairtin O'Loingsigh <[email protected]>

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

Reply via email to