diff mbox series

[ovs-dev,v8,6/6] northd: Flood ARPs to routers for "unreachable" addresses.

Message ID 20210603184931.1425441-7-mmichels@redhat.com
State New
Headers show
Series ARP and Floating IP Fixes | expand

Commit Message

Mark Michelson June 3, 2021, 6:49 p.m. UTC
Previously, ARP TPAs were filtered down only to "reachable" addresses.
Reachable addresses are all router interface addresses, as well as NAT
external addresses and load balancer VIPs that are within the subnet
handled by a router's port.

However, it is possible that in some configurations, CMSes purposely
configure NAT or load balancer addresses on a router that are outside
the router's subnets, and they expect the router to respond to ARPs for
those addresses.

This commit adds a higher priority flow to logical switches that makes
it so ARPs targeted at "unreachable" addresses are flooded to all ports.
This way, the ARPs can reach the router appropriately and receive a
response.

Reported at: https://bugzilla.redhat.com/show_bug.cgi?id=1929901

Signed-off-by: Mark Michelson <mmichels@redhat.com>
---
 northd/ovn-northd.8.xml |   8 +++
 northd/ovn-northd.c     | 153 +++++++++++++++++++++++++++-------------
 northd/ovn_northd.dl    | 101 ++++++++++++++++++++------
 tests/ovn-northd.at     |  99 ++++++++++++++++++++++++++
 tests/system-ovn.at     | 102 +++++++++++++++++++++++++++
 5 files changed, 391 insertions(+), 72 deletions(-)
diff mbox series

Patch

diff --git a/northd/ovn-northd.8.xml b/northd/ovn-northd.8.xml
index bb77689de..8bb77bf6c 100644
--- a/northd/ovn-northd.8.xml
+++ b/northd/ovn-northd.8.xml
@@ -1549,6 +1549,14 @@  output;
         logical ports.
       </li>
 
+      <li>
+        Priority-80 flows for each IP address/VIP/NAT address configured
+        outside its owning router port's subnet. These flows match ARP
+        requests and ND packets for the specific IP addresses.  Matched packets
+        are forwarded to the <code>MC_FLOOD</code> multicast group which
+        contains all connected logical ports.
+      </li>
+
       <li>
         Priority-75 flows for each port connected to a logical router
         matching self originated ARP request/ND packets.  These packets
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index 414bf9c48..eacbab96a 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -6539,44 +6539,48 @@  build_lswitch_rport_arp_req_self_orig_flow(struct ovn_port *op,
     ds_destroy(&match);
 }
 
-/*
- * Ingress table 19: Flows that forward ARP/ND requests only to the routers
- * that own the addresses. Other ARP/ND packets are still flooded in the
- * switching domain as regular broadcast.
- */
 static void
-build_lswitch_rport_arp_req_flow_for_ip(struct sset *ips,
-                                        int addr_family,
-                                        struct ovn_port *patch_op,
-                                        struct ovn_datapath *od,
-                                        uint32_t priority,
-                                        struct hmap *lflows,
-                                        const struct ovsdb_idl_row *stage_hint)
+arp_nd_ns_match(struct sset *ips, int addr_family, struct ds *match)
 {
-    struct ds match   = DS_EMPTY_INITIALIZER;
-    struct ds actions = DS_EMPTY_INITIALIZER;
 
     /* Packets received from VXLAN tunnels have already been through the
      * router pipeline so we should skip them. Normally this is done by the
      * multicast_group implementation (VXLAN packets skip table 32 which
      * delivers to patch ports) but we're bypassing multicast_groups.
      */
-    ds_put_cstr(&match, FLAGBIT_NOT_VXLAN " && ");
+    ds_put_cstr(match, FLAGBIT_NOT_VXLAN " && ");
 
     if (addr_family == AF_INET) {
-        ds_put_cstr(&match, "arp.op == 1 && arp.tpa == { ");
+        ds_put_cstr(match, "arp.op == 1 && arp.tpa == {");
     } else {
-        ds_put_cstr(&match, "nd_ns && nd.target == { ");
+        ds_put_cstr(match, "nd_ns && nd.target == {");
     }
 
     const char *ip_address;
     SSET_FOR_EACH (ip_address, ips) {
-        ds_put_format(&match, "%s, ", ip_address);
+        ds_put_format(match, "%s, ", ip_address);
     }
 
-    ds_chomp(&match, ' ');
-    ds_chomp(&match, ',');
-    ds_put_cstr(&match, "}");
+    ds_chomp(match, ' ');
+    ds_chomp(match, ',');
+    ds_put_cstr(match, "}");
+}
+
+/*
+ * Ingress table 19: Flows that forward ARP/ND requests only to the routers
+ * that own the addresses. Other ARP/ND packets are still flooded in the
+ * switching domain as regular broadcast.
+ */
+static void
+build_lswitch_rport_arp_req_flow_for_reachable_ip(struct sset *ips,
+    int addr_family, struct ovn_port *patch_op, struct ovn_datapath *od,
+    uint32_t priority, struct hmap *lflows,
+    const struct ovsdb_idl_row *stage_hint)
+{
+    struct ds match   = DS_EMPTY_INITIALIZER;
+    struct ds actions = DS_EMPTY_INITIALIZER;
+
+    arp_nd_ns_match(ips, addr_family, &match);
 
     /* Send a the packet to the router pipeline.  If the switch has non-router
      * ports then flood it there as well.
@@ -6599,6 +6603,30 @@  build_lswitch_rport_arp_req_flow_for_ip(struct sset *ips,
     ds_destroy(&actions);
 }
 
+/*
+ * Ingress table 19: Flows that forward ARP/ND requests for "unreachable" IPs
+ * (NAT or load balancer IPs configured on a router that are outside the
+ * router's configured subnets).
+ * These ARP/ND packets are flooded in the switching domain as regular
+ * broadcast.
+ */
+static void
+build_lswitch_rport_arp_req_flow_for_unreachable_ip(struct sset *ips,
+    int addr_family, struct ovn_datapath *od, uint32_t priority,
+    struct hmap *lflows, const struct ovsdb_idl_row *stage_hint)
+{
+    struct ds match = DS_EMPTY_INITIALIZER;
+
+    arp_nd_ns_match(ips, addr_family, &match);
+
+    ovn_lflow_add_unique_with_hint(lflows, od, S_SWITCH_IN_L2_LKUP,
+                                   priority, ds_cstr(&match),
+                                   "outport = \""MC_FLOOD"\"; output;",
+                                   stage_hint);
+
+    ds_destroy(&match);
+}
+
 /*
  * Ingress table 19: Flows that forward ARP/ND requests only to the routers
  * that own the addresses.
@@ -6625,39 +6653,48 @@  build_lswitch_rport_arp_req_flows(struct ovn_port *op,
      * router port.
      * Priority: 80.
      */
-    struct sset all_ips_v4 = SSET_INITIALIZER(&all_ips_v4);
-    struct sset all_ips_v6 = SSET_INITIALIZER(&all_ips_v6);
+    struct sset lb_ips_v4 = SSET_INITIALIZER(&lb_ips_v4);
+    struct sset lb_ips_v6 = SSET_INITIALIZER(&lb_ips_v6);
 
-    get_router_load_balancer_ips(op->od, false, &all_ips_v4, &all_ips_v6);
+    get_router_load_balancer_ips(op->od, false, &lb_ips_v4, &lb_ips_v6);
+
+    struct sset reachable_ips_v4 = SSET_INITIALIZER(&reachable_ips_v4);
+    struct sset reachable_ips_v6 = SSET_INITIALIZER(&reachable_ips_v6);
+    struct sset unreachable_ips_v4 = SSET_INITIALIZER(&unreachable_ips_v4);
+    struct sset unreachable_ips_v6 = SSET_INITIALIZER(&unreachable_ips_v6);
 
     const char *ip_addr;
     const char *ip_addr_next;
-    SSET_FOR_EACH_SAFE (ip_addr, ip_addr_next, &all_ips_v4) {
+    SSET_FOR_EACH_SAFE (ip_addr, ip_addr_next, &lb_ips_v4) {
         ovs_be32 ipv4_addr;
 
         /* Check if the ovn port has a network configured on which we could
          * expect ARP requests for the LB VIP.
          */
-        if (ip_parse(ip_addr, &ipv4_addr) &&
-                lrouter_port_ipv4_reachable(op, ipv4_addr)) {
-            continue;
+        if (ip_parse(ip_addr, &ipv4_addr)) {
+            if (lrouter_port_ipv4_reachable(op, ipv4_addr)) {
+                sset_add(&reachable_ips_v4, ip_addr);
+            } else {
+                sset_add(&unreachable_ips_v4, ip_addr);
+            }
         }
-
-        sset_delete(&all_ips_v4, SSET_NODE_FROM_NAME(ip_addr));
     }
-    SSET_FOR_EACH_SAFE (ip_addr, ip_addr_next, &all_ips_v6) {
+    SSET_FOR_EACH_SAFE (ip_addr, ip_addr_next, &lb_ips_v6) {
         struct in6_addr ipv6_addr;
 
         /* Check if the ovn port has a network configured on which we could
          * expect NS requests for the LB VIP.
          */
-        if (ipv6_parse(ip_addr, &ipv6_addr) &&
-                lrouter_port_ipv6_reachable(op, &ipv6_addr)) {
-            continue;
+        if (ipv6_parse(ip_addr, &ipv6_addr)) {
+            if (lrouter_port_ipv6_reachable(op, &ipv6_addr)) {
+                sset_add(&reachable_ips_v6, ip_addr);
+            } else {
+                sset_add(&unreachable_ips_v6, ip_addr);
+            }
         }
-
-        sset_delete(&all_ips_v6, SSET_NODE_FROM_NAME(ip_addr));
     }
+    sset_destroy(&lb_ips_v4);
+    sset_destroy(&lb_ips_v6);
 
     for (size_t i = 0; i < op->od->nbr->n_nat; i++) {
         struct ovn_nat *nat_entry = &op->od->nat_entries[i];
@@ -6678,37 +6715,53 @@  build_lswitch_rport_arp_req_flows(struct ovn_port *op,
             struct in6_addr *addr = &nat_entry->ext_addrs.ipv6_addrs[0].addr;
 
             if (lrouter_port_ipv6_reachable(op, addr)) {
-                sset_add(&all_ips_v6, nat->external_ip);
+                sset_add(&reachable_ips_v6, nat->external_ip);
+            } else {
+                sset_add(&unreachable_ips_v6, nat->external_ip);
             }
         } else {
             ovs_be32 addr = nat_entry->ext_addrs.ipv4_addrs[0].addr;
 
             if (lrouter_port_ipv4_reachable(op, addr)) {
-                sset_add(&all_ips_v4, nat->external_ip);
+                sset_add(&reachable_ips_v4, nat->external_ip);
+            } else {
+                sset_add(&unreachable_ips_v4, nat->external_ip);
             }
         }
     }
 
     for (size_t i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
-        sset_add(&all_ips_v4, op->lrp_networks.ipv4_addrs[i].addr_s);
+        sset_add(&reachable_ips_v4, op->lrp_networks.ipv4_addrs[i].addr_s);
     }
     for (size_t i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
-        sset_add(&all_ips_v6, op->lrp_networks.ipv6_addrs[i].addr_s);
+        sset_add(&reachable_ips_v6, op->lrp_networks.ipv6_addrs[i].addr_s);
     }
 
-    if (!sset_is_empty(&all_ips_v4)) {
-        build_lswitch_rport_arp_req_flow_for_ip(&all_ips_v4, AF_INET, sw_op,
-                                                sw_od, 80, lflows,
-                                                stage_hint);
+    if (!sset_is_empty(&reachable_ips_v4)) {
+        build_lswitch_rport_arp_req_flow_for_reachable_ip(&reachable_ips_v4,
+                                                          AF_INET, sw_op,
+                                                          sw_od, 80, lflows,
+                                                          stage_hint);
+    }
+    if (!sset_is_empty(&reachable_ips_v6)) {
+        build_lswitch_rport_arp_req_flow_for_reachable_ip(&reachable_ips_v6,
+                                                          AF_INET6, sw_op,
+                                                          sw_od, 80, lflows,
+                                                          stage_hint);
     }
-    if (!sset_is_empty(&all_ips_v6)) {
-        build_lswitch_rport_arp_req_flow_for_ip(&all_ips_v6, AF_INET6, sw_op,
-                                                sw_od, 80, lflows,
-                                                stage_hint);
+    if (!sset_is_empty(&unreachable_ips_v4)) {
+        build_lswitch_rport_arp_req_flow_for_unreachable_ip(
+            &unreachable_ips_v4, AF_INET, sw_od, 90, lflows, stage_hint);
+    }
+    if (!sset_is_empty(&unreachable_ips_v6)) {
+        build_lswitch_rport_arp_req_flow_for_unreachable_ip(
+            &unreachable_ips_v6, AF_INET6, sw_od, 90, lflows, stage_hint);
     }
 
-    sset_destroy(&all_ips_v4);
-    sset_destroy(&all_ips_v6);
+    sset_destroy(&reachable_ips_v4);
+    sset_destroy(&reachable_ips_v6);
+    sset_destroy(&unreachable_ips_v4);
+    sset_destroy(&unreachable_ips_v6);
 
     /* Self originated ARP requests/ND need to be flooded as usual.
      *
diff --git a/northd/ovn_northd.dl b/northd/ovn_northd.dl
index 794b86ee1..1061fae8b 100644
--- a/northd/ovn_northd.dl
+++ b/northd/ovn_northd.dl
@@ -4103,9 +4103,13 @@  UniqueFlow[Flow{.logical_datapath = sw._uuid,
  * router port.
  * Priority: 80.
  */
-function get_arp_forward_ips(rp: Intern<RouterPort>): (Set<string>, Set<string>) = {
-    var all_ips_v4 = set_empty();
-    var all_ips_v6 = set_empty();
+function get_arp_forward_ips(rp: Intern<RouterPort>):
+    (Set<string>, Set<string>, Set<string>, Set<string>) =
+{
+    var reachable_ips_v4 = set_empty();
+    var reachable_ips_v6 = set_empty();
+    var unreachable_ips_v4 = set_empty();
+    var unreachable_ips_v6 = set_empty();
 
     (var lb_ips_v4, var lb_ips_v6)
         = get_router_load_balancer_ips(rp.router, false);
@@ -4115,7 +4119,9 @@  function get_arp_forward_ips(rp: Intern<RouterPort>): (Set<string>, Set<string>)
          */
         match (ip_parse(a)) {
             Some{ipv4} -> if (lrouter_port_ip_reachable(rp, IPv4{ipv4})) {
-                all_ips_v4.insert(a)
+                reachable_ips_v4.insert(a)
+            } else {
+                unreachable_ips_v4.insert(a)
             },
             _ -> ()
         }
@@ -4126,7 +4132,9 @@  function get_arp_forward_ips(rp: Intern<RouterPort>): (Set<string>, Set<string>)
          */
         match (ipv6_parse(a)) {
             Some{ipv6} -> if (lrouter_port_ip_reachable(rp, IPv6{ipv6})) {
-                all_ips_v6.insert(a)
+                reachable_ips_v6.insert(a)
+            } else {
+                unreachable_ips_v6.insert(a)
             },
             _ -> ()
         }
@@ -4139,22 +4147,45 @@  function get_arp_forward_ips(rp: Intern<RouterPort>): (Set<string>, Set<string>)
              */
             if (lrouter_port_ip_reachable(rp, nat.external_ip)) {
                 match (nat.external_ip) {
-                    IPv4{_} -> all_ips_v4.insert(nat.nat.external_ip),
-                    IPv6{_} -> all_ips_v6.insert(nat.nat.external_ip)
+                    IPv4{_} -> reachable_ips_v4.insert(nat.nat.external_ip),
+                    IPv6{_} -> reachable_ips_v6.insert(nat.nat.external_ip)
+                }
+            } else {
+                match (nat.external_ip) {
+                    IPv4{_} -> unreachable_ips_v4.insert(nat.nat.external_ip),
+                    IPv6{_} -> unreachable_ips_v6.insert(nat.nat.external_ip),
                 }
             }
         }
     };
 
     for (a in rp.networks.ipv4_addrs) {
-        all_ips_v4.insert("${a.addr}")
+        reachable_ips_v4.insert("${a.addr}")
     };
     for (a in rp.networks.ipv6_addrs) {
-        all_ips_v6.insert("${a.addr}")
+        reachable_ips_v6.insert("${a.addr}")
     };
 
-    (all_ips_v4, all_ips_v6)
+    (reachable_ips_v4, reachable_ips_v6, unreachable_ips_v4, unreachable_ips_v6)
 }
+
+relation &SwitchPortARPForwards(
+    port: Intern<SwitchPort>,
+    reachable_ips_v4: Set<string>,
+    reachable_ips_v6: Set<string>,
+    unreachable_ips_v4: Set<string>,
+    unreachable_ips_v6: Set<string>
+)
+
+&SwitchPortARPForwards(.port = port,
+                       .reachable_ips_v4 = reachable_ips_v4,
+                       .reachable_ips_v6 = reachable_ips_v6,
+                       .unreachable_ips_v4 = unreachable_ips_v4,
+                       .unreachable_ips_v6 = unreachable_ips_v6) :-
+    port in &SwitchPort(.peer = Some{rp}),
+    rp.is_enabled(),
+    (var reachable_ips_v4, var reachable_ips_v6, var unreachable_ips_v4, var unreachable_ips_v6) = get_arp_forward_ips(rp).
+
 /* Packets received from VXLAN tunnels have already been through the
  * router pipeline so we should skip them. Normally this is done by the
  * multicast_group implementation (VXLAN packets skip table 32 which
@@ -4165,8 +4196,8 @@  AnnotatedFlow(.f = Flow{.logical_datapath = sw._uuid,
                         .stage            = s_SWITCH_IN_L2_LKUP(),
                         .priority         = 80,
                         .__match          = fLAGBIT_NOT_VXLAN() ++
-                                            " && arp.op == 1 && arp.tpa == { " ++
-                                            all_ips_v4.to_vec().join(", ") ++ "}",
+                                            " && arp.op == 1 && arp.tpa == {" ++
+                                            ipv4.to_vec().join(", ") ++ "}",
                         .actions          = if (sw.has_non_router_port) {
                                                 "clone {outport = ${sp.json_name}; output; }; "
                                                 "outport = ${mc_flood_l2}; output;"
@@ -4175,17 +4206,16 @@  AnnotatedFlow(.f = Flow{.logical_datapath = sw._uuid,
                                             },
                         .external_ids     = stage_hint(sp.lsp._uuid)},
               .shared = not sw.has_non_router_port) :-
-    sp in &SwitchPort(.sw = sw, .peer = Some{rp}),
-    rp.is_enabled(),
-    (var all_ips_v4, _) = get_arp_forward_ips(rp),
-    not all_ips_v4.is_empty(),
+    sp in &SwitchPort(.sw = sw),
+    &SwitchPortARPForwards(.port = sp, .reachable_ips_v4 = ipv4),
+    not ipv4.is_empty(),
     var mc_flood_l2 = json_string_escape(mC_FLOOD_L2().0).
 AnnotatedFlow(.f = Flow{.logical_datapath = sw._uuid,
                         .stage            = s_SWITCH_IN_L2_LKUP(),
                         .priority         = 80,
                         .__match          = fLAGBIT_NOT_VXLAN() ++
-                                            " && nd_ns && nd.target == { " ++
-                                            all_ips_v6.to_vec().join(", ") ++ "}",
+                                            " && nd_ns && nd.target == {" ++
+                                            ipv6.to_vec().join(", ") ++ "}",
                         .actions          = if (sw.has_non_router_port) {
                                                 "clone {outport = ${sp.json_name}; output; }; "
                                                 "outport = ${mc_flood_l2}; output;"
@@ -4194,12 +4224,39 @@  AnnotatedFlow(.f = Flow{.logical_datapath = sw._uuid,
                                             },
                         .external_ids     = stage_hint(sp.lsp._uuid)},
               .shared = not sw.has_non_router_port) :-
-    sp in &SwitchPort(.sw = sw, .peer = Some{rp}),
-    rp.is_enabled(),
-    (_, var all_ips_v6) = get_arp_forward_ips(rp),
-    not all_ips_v6.is_empty(),
+    sp in &SwitchPort(.sw = sw),
+    &SwitchPortARPForwards(.port = sp, .reachable_ips_v6 = ipv6),
+    not ipv6.is_empty(),
     var mc_flood_l2 = json_string_escape(mC_FLOOD_L2().0).
 
+AnnotatedFlow(.f = Flow{.logical_datapath = sw._uuid,
+                        .stage            = s_SWITCH_IN_L2_LKUP(),
+                        .priority         = 90,
+                        .__match          = fLAGBIT_NOT_VXLAN() ++
+                                            " && arp.op == 1 && arp.tpa == {" ++
+                                            ipv4.to_vec().join(", ") ++ "}",
+                        .actions          = "outport = ${flood}; output;",
+                        .external_ids     = stage_hint(sp.lsp._uuid)},
+              .shared = not sw.has_non_router_port) :-
+    sp in &SwitchPort(.sw = sw),
+    &SwitchPortARPForwards(.port = sp, .unreachable_ips_v4 = ipv4),
+    not ipv4.is_empty(),
+    var flood = json_string_escape(mC_FLOOD().0).
+
+AnnotatedFlow(.f = Flow{.logical_datapath = sw._uuid,
+                        .stage            = s_SWITCH_IN_L2_LKUP(),
+                        .priority         = 90,
+                        .__match          = fLAGBIT_NOT_VXLAN() ++
+                                            " && nd_ns && nd.target == {" ++
+                                            ipv6.to_vec().join(", ") ++ "}",
+                        .actions          = "outport = ${flood}; output;",
+                        .external_ids     = stage_hint(sp.lsp._uuid)},
+              .shared = not sw.has_non_router_port) :-
+    sp in &SwitchPort(.sw = sw),
+    &SwitchPortARPForwards(.port = sp, .unreachable_ips_v6 = ipv6),
+    not ipv6.is_empty(),
+    var flood = json_string_escape(mC_FLOOD().0).
+
 for (SwitchPortNewDynamicAddress(.port = &SwitchPort{.lsp = lsp, .json_name = json_name, .sw = sw},
                                  .address = Some{addrs})
      if lsp.__type != "external") {
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index 9e4f2ca62..13ead49ba 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -3840,3 +3840,102 @@  check ovn-nbctl --wait=sb clear logical_router_port ro2-sw ha_chassis_group
 check_lflows 0
 
 AT_CLEANUP
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ovn -- ARP flood for unreachable addresses])
+ovn_start
+
+AS_BOX([Setting up the logical network])
+
+# This network is the same as the one from "Router Address Propagation"
+check ovn-nbctl ls-add sw
+
+check ovn-nbctl lr-add ro1
+check ovn-nbctl lrp-add ro1 ro1-sw 00:00:00:00:00:01 10.0.0.1/24
+check ovn-nbctl lsp-add sw sw-ro1
+check ovn-nbctl lsp-set-type sw-ro1 router
+check ovn-nbctl lsp-set-addresses sw-ro1 router
+check ovn-nbctl lsp-set-options sw-ro1 router-port=ro1-sw
+
+check ovn-nbctl lr-add ro2
+check ovn-nbctl lrp-add ro2 ro2-sw 00:00:00:00:00:02 20.0.0.1/24
+check ovn-nbctl lsp-add sw sw-ro2
+check ovn-nbctl lsp-set-type sw-ro2 router
+check ovn-nbctl lsp-set-addresses sw-ro2 router
+check ovn-nbctl --wait=sb lsp-set-options sw-ro2 router-port=ro2-sw
+
+check ovn-nbctl ls-add ls1
+check ovn-nbctl lsp-add ls1 vm1
+check ovn-nbctl lsp-set-addresses vm1 "00:00:00:00:01:02 192.168.1.2"
+check ovn-nbctl lrp-add ro1 ro1-ls1 00:00:00:00:01:01 192.168.1.1/24
+check ovn-nbctl lsp-add ls1 ls1-ro1
+check ovn-nbctl lsp-set-type ls1-ro1 router
+check ovn-nbctl lsp-set-addresses ls1-ro1 router
+check ovn-nbctl lsp-set-options ls1-ro1 router-port=ro1-ls1
+
+check ovn-nbctl ls-add ls2
+check ovn-nbctl lsp-add ls2 vm2
+check ovn-nbctl lsp-set-addresses vm2 "00:00:00:00:02:02 192.168.2.2"
+check ovn-nbctl lrp-add ro2 ro2-ls2 00:00:00:00:02:01 192.168.2.1/24
+check ovn-nbctl lsp-add ls2 ls2-ro2
+check ovn-nbctl lsp-set-type ls2-ro2 router
+check ovn-nbctl lsp-set-addresses ls2-ro2 router
+check ovn-nbctl lsp-set-options ls2-ro2 router-port=ro2-ls2
+
+AS_BOX([Ensure that unreachable flood flows are not installed, since no addresses are unreachable])
+
+AT_CHECK([ovn-sbctl lflow-list sw | grep "ls_in_l2_lkup" | grep "priority=90" -c], [1], [dnl
+0
+])
+
+AS_BOX([Adding some reachable NAT addresses])
+
+check ovn-nbctl lr-nat-add ro1 dnat 10.0.0.100 192.168.1.100
+check ovn-nbctl lr-nat-add ro1 snat 10.0.0.200 192.168.1.200/30
+
+check ovn-nbctl lr-nat-add ro2 dnat 20.0.0.100 192.168.2.100
+check ovn-nbctl --wait=sb lr-nat-add ro2 snat 20.0.0.200 192.168.2.200/30
+
+AS_BOX([Ensure that unreachable flood flows are not installed, since all addresses are reachable])
+
+AT_CHECK([ovn-sbctl lflow-list sw | grep "ls_in_l2_lkup" | grep "priority=90" -c], [1], [dnl
+0
+])
+
+AS_BOX([Adding some unreachable NAT addresses])
+
+check ovn-nbctl lr-nat-add ro1 dnat 30.0.0.100 192.168.1.130
+check ovn-nbctl lr-nat-add ro1 snat 30.0.0.200 192.168.1.148/30
+
+check ovn-nbctl lr-nat-add ro2 dnat 40.0.0.100 192.168.2.130
+check ovn-nbctl --wait=sb lr-nat-add ro2 snat 40.0.0.200 192.168.2.148/30
+
+AS_BOX([Ensure that unreachable flood flows are installed, since there are unreachable addresses])
+
+ovn-sbctl lflow-list
+
+# We expect two flows to be installed, one per connected router port on sw
+AT_CHECK([ovn-sbctl lflow-list sw | grep ls_in_l2_lkup | grep priority=90 -c], [0], [dnl
+2
+])
+
+# We expect that the installed flows will match the unreachable DNAT addresses only.
+AT_CHECK([ovn-sbctl lflow-list sw | grep ls_in_l2_lkup | grep priority=90 | grep "arp.tpa == {30.0.0.100}" -c], [0], [dnl
+1
+])
+
+AT_CHECK([ovn-sbctl lflow-list sw | grep ls_in_l2_lkup | grep priority=90 | grep "arp.tpa == {40.0.0.100}" -c], [0], [dnl
+1
+])
+
+# Ensure that we do not create flows for SNAT addresses
+AT_CHECK([ovn-sbctl lflow-list sw | grep ls_in_l2_lkup | grep priority=90 | grep "arp.tpa == {30.0.0.200}" -c], [1], [dnl
+0
+])
+
+AT_CHECK([ovn-sbctl lflow-list sw | grep ls_in_l2_lkup | grep priority=90 | grep "arp.tpa == {40.0.0.200}" -c], [1], [dnl
+0
+])
+
+AT_CLEANUP
+])
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index 3824788c4..6539a66cb 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -6293,3 +6293,105 @@  OVS_TRAFFIC_VSWITCHD_STOP(["/.*error receiving.*/d
 
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ovn -- Floating IP outside router subnet IPv4])
+AT_KEYWORDS(NAT)
+
+ovn_start
+
+OVS_TRAFFIC_VSWITCHD_START()
+ADD_BR([br-int])
+
+# Set external-ids in br-int needed for ovn-controller
+ovs-vsctl \
+        -- set Open_vSwitch . external-ids:system-id=hv1 \
+        -- set Open_vSwitch . external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+
+start_daemon ovn-controller
+
+# Logical network:
+# Two VMs
+#   * VM1 with IP address 192.168.100.5
+#   * VM2 with IP address 192.168.200.5
+#
+# VM1 connects to logical switch ls1. ls1 connects to logical router lr1.
+# VM2 connects to logical switch ls2. ls2 connects to logical router lr2.
+# lr1 and lr2 both connect to logical switch ls-pub.
+# * lr1's interface that connects to ls-pub has IP address 172.18.2.110/24
+# * lr2's interface that connects to ls-pub has IP address 172.18.1.173/24
+#
+# lr1 has the following attributes:
+#   * It has a DNAT rule that translates 172.18.2.11 to 192.168.100.5 (VM1)
+#
+# lr2 has the following attributes:
+#   * It has a DNAT rule that translates 172.18.2.12 to 192.168.200.5 (VM2)
+#
+# In this test, we want to ensure that a ping from VM1 to IP address 172.18.2.12 reaches VM2.
+# When the NAT rules are set up, there should be MAC_Bindings created that allow for traffic
+# to exit lr1, go through ls-pub, and reach the NAT external IP configured on lr2.
+
+check ovn-nbctl ls-add ls1
+check ovn-nbctl lsp-add ls1 vm1 -- lsp-set-addresses vm1 "00:00:00:00:01:05 192.168.100.5"
+
+check ovn-nbctl ls-add ls2
+check ovn-nbctl lsp-add ls2 vm2 -- lsp-set-addresses vm2 "00:00:00:00:02:05 192.168.200.5"
+
+check ovn-nbctl ls-add ls-pub
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-ls1 00:00:00:00:01:01 192.168.100.1/24
+check ovn-nbctl lsp-add ls1 ls1-lr1                      \
+    -- lsp-set-type ls1-lr1 router                 \
+    -- lsp-set-addresses ls1-lr1 router            \
+    -- lsp-set-options ls1-lr1 router-port=lr1-ls1
+
+check ovn-nbctl lr-add lr2
+check ovn-nbctl lrp-add lr2 lr2-ls2 00:00:00:00:02:01 192.168.200.1/24
+check ovn-nbctl lsp-add ls2 ls2-lr2                      \
+    -- lsp-set-type ls2-lr2 router                 \
+    -- lsp-set-addresses ls2-lr2 router            \
+    -- lsp-set-options ls2-lr2 router-port=lr2-ls2
+
+check ovn-nbctl lrp-add lr1 lr1-ls-pub 00:00:00:00:03:01 172.18.2.110/24
+check ovn-nbctl lrp-set-gateway-chassis lr1-ls-pub hv1
+check ovn-nbctl lsp-add ls-pub ls-pub-lr1                      \
+    -- lsp-set-type ls-pub-lr1 router                    \
+    -- lsp-set-addresses ls-pub-lr1 router               \
+    -- lsp-set-options ls-pub-lr1 router-port=lr1-ls-pub
+
+check ovn-nbctl lrp-add lr2 lr2-ls-pub 00:00:00:00:03:02 172.18.1.173/24
+check ovn-nbctl lrp-set-gateway-chassis lr2-ls-pub hv1
+check ovn-nbctl lsp-add ls-pub ls-pub-lr2                      \
+    -- lsp-set-type ls-pub-lr2 router                    \
+    -- lsp-set-addresses ls-pub-lr2 router               \
+    -- lsp-set-options ls-pub-lr2 router-port=lr2-ls-pub
+
+# Putting --add-route on these NAT rules means there is no need to
+# add any static routes.
+check ovn-nbctl --add-route lr-nat-add lr1 dnat_and_snat 172.18.2.11 192.168.100.5 vm1 00:00:00:00:03:01
+check ovn-nbctl --add-route lr-nat-add lr2 dnat_and_snat 172.18.2.12 192.168.200.5 vm2 00:00:00:00:03:02
+
+ADD_NAMESPACES(vm1)
+ADD_VETH(vm1, vm1, br-int, "192.168.100.5/24", "00:00:00:00:01:05", \
+         "192.168.100.1")
+
+ADD_NAMESPACES(vm2)
+ADD_VETH(vm2, vm2, br-int, "192.168.200.5/24", "00:00:00:00:02:05", \
+         "192.168.200.1")
+
+OVN_POPULATE_ARP
+check ovn-nbctl --wait=hv sync
+
+AS_BOX([Testing a ping])
+
+NS_CHECK_EXEC([vm1], [ping -q -c 3 -i 0.3 -w 2 172.18.2.12 | FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+AT_CLEANUP
+])