diff mbox series

[ovs-dev,v2] northd: Determine gateway port for NAT when not specified

Message ID 20220524103105.120129-1-sangana.abhiram@nutanix.com
State Accepted
Headers show
Series [ovs-dev,v2] northd: Determine gateway port for NAT when not specified | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_Build_and_Test success github build: passed
ovsrobot/github-robot-_ovn-kubernetes success github build: passed

Commit Message

Abhiram Sangana May 24, 2022, 10:31 a.m. UTC
If multiple distributed gateway ports (DGP) are configured on a
logical router, "gateway_port" column needs to be set for NAT entries
of the logical router for the rules to be applied, as part of commit
2d942be (northd: Add support for NAT with multiple DGP).

This patch updates the behavior by inferring the DGP where the NAT
rule needs to be applied based on the "external_ip" column of the
NAT rule when "gateway_port" column is not set.

Signed-off-by: Abhiram Sangana <sangana.abhiram@nutanix.com>
---
 lib/ovn-util.c            |  56 ++++++++++++++++++++
 lib/ovn-util.h            |   2 +
 northd/northd.c           | 109 ++++++++++++++++----------------------
 northd/ovn-northd.8.xml   |  19 ++++---
 ovn-nb.xml                |  19 ++++---
 tests/ovn-nbctl.at        |  47 ++++++++--------
 tests/ovn-northd.at       | 102 ++++++++++++++++++++++-------------
 utilities/ovn-nbctl.8.xml |   5 +-
 utilities/ovn-nbctl.c     |  57 +++++++++++++-------
 9 files changed, 260 insertions(+), 156 deletions(-)

Comments

Han Zhou June 9, 2022, 4:03 p.m. UTC | #1
On Tue, May 24, 2022 at 3:31 AM Abhiram Sangana <sangana.abhiram@nutanix.com>
wrote:
>
> If multiple distributed gateway ports (DGP) are configured on a
> logical router, "gateway_port" column needs to be set for NAT entries
> of the logical router for the rules to be applied, as part of commit
> 2d942be (northd: Add support for NAT with multiple DGP).
>
> This patch updates the behavior by inferring the DGP where the NAT
> rule needs to be applied based on the "external_ip" column of the
> NAT rule when "gateway_port" column is not set.
>
> Signed-off-by: Abhiram Sangana <sangana.abhiram@nutanix.com>

Thanks Abhiram!
Acked-by: Han Zhou <hzhou@ovn.org>

Since Mark commented on v1, I will let him confirm.

Thanks,
Han
Mark Michelson June 9, 2022, 6:35 p.m. UTC | #2
Thanks, Abhiram and Han.

I added my own ack and pushed this change to main.

On 6/9/22 12:03, Han Zhou wrote:
> 
> 
> On Tue, May 24, 2022 at 3:31 AM Abhiram Sangana 
> <sangana.abhiram@nutanix.com <mailto:sangana.abhiram@nutanix.com>> wrote:
>  >
>  > If multiple distributed gateway ports (DGP) are configured on a
>  > logical router, "gateway_port" column needs to be set for NAT entries
>  > of the logical router for the rules to be applied, as part of commit
>  > 2d942be (northd: Add support for NAT with multiple DGP).
>  >
>  > This patch updates the behavior by inferring the DGP where the NAT
>  > rule needs to be applied based on the "external_ip" column of the
>  > NAT rule when "gateway_port" column is not set.
>  >
>  > Signed-off-by: Abhiram Sangana <sangana.abhiram@nutanix.com 
> <mailto:sangana.abhiram@nutanix.com>>
> 
> Thanks Abhiram!
> Acked-by: Han Zhou <hzhou@ovn.org <mailto:hzhou@ovn.org>>
> 
> Since Mark commented on v1, I will let him confirm.
> 
> Thanks,
> Han
Abhiram Sangana June 10, 2022, 5:23 a.m. UTC | #3
Thank you for reviewing the patch, Mark and Han.
diff mbox series

Patch

diff --git a/lib/ovn-util.c b/lib/ovn-util.c
index 81f18d6df..616999eab 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -370,6 +370,62 @@  destroy_lport_addresses(struct lport_addresses *laddrs)
     free(laddrs->ipv6_addrs);
 }
 
+/* Returns a string of the IP address of 'laddrs' that overlaps with 'ip_s'.
+ * If one is not found, returns NULL.
+ *
+ * The caller must not free the returned string. */
+const char *
+find_lport_address(const struct lport_addresses *laddrs, const char *ip_s)
+{
+    bool is_ipv4 = strchr(ip_s, '.') ? true : false;
+
+    if (is_ipv4) {
+        ovs_be32 ip;
+
+        if (!ip_parse(ip_s, &ip)) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad ip address %s", ip_s);
+            return NULL;
+        }
+
+        for (int i = 0; i < laddrs->n_ipv4_addrs; i++) {
+            const struct ipv4_netaddr *na = &laddrs->ipv4_addrs[i];
+
+            if (!((na->network ^ ip) & na->mask)) {
+                /* There should be only 1 interface that matches the
+                 * supplied IP.  Otherwise, it's a configuration error,
+                 * because subnets of a router's interfaces should NOT
+                 * overlap. */
+                return na->addr_s;
+            }
+        }
+    } else {
+        struct in6_addr ip6;
+
+        if (!ipv6_parse(ip_s, &ip6)) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad ipv6 address %s", ip_s);
+            return NULL;
+        }
+
+        for (int i = 0; i < laddrs->n_ipv6_addrs; i++) {
+            const struct ipv6_netaddr *na = &laddrs->ipv6_addrs[i];
+            struct in6_addr xor_addr = ipv6_addr_bitxor(&na->network, &ip6);
+            struct in6_addr and_addr = ipv6_addr_bitand(&xor_addr, &na->mask);
+
+            if (ipv6_is_zero(&and_addr)) {
+                /* There should be only 1 interface that matches the
+                 * supplied IP.  Otherwise, it's a configuration error,
+                 * because subnets of a router's interfaces should NOT
+                 * overlap. */
+                return na->addr_s;
+            }
+        }
+    }
+
+    return NULL;
+}
+
 /* Go through 'addresses' and add found IPv4 addresses to 'ipv4_addrs' and
  * IPv6 addresses to 'ipv6_addrs'. */
 void
diff --git a/lib/ovn-util.h b/lib/ovn-util.h
index 024b86b89..b3905ef7b 100644
--- a/lib/ovn-util.h
+++ b/lib/ovn-util.h
@@ -86,6 +86,8 @@  bool extract_lrp_networks__(char *mac, char **networks, size_t n_networks,
 
 bool lport_addresses_is_empty(struct lport_addresses *);
 void destroy_lport_addresses(struct lport_addresses *);
+const char *find_lport_address(const struct lport_addresses *laddrs,
+                               const char *ip_s);
 
 void split_addresses(const char *addresses, struct svec *ipv4_addrs,
                      struct svec *ipv6_addrs);
diff --git a/northd/northd.c b/northd/northd.c
index 51dec36b3..81473d93a 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -1965,6 +1965,23 @@  ipam_add_port_addresses(struct ovn_datapath *od, struct ovn_port *op)
     }
 }
 
+static const char *find_lrp_member_ip(const struct ovn_port *op,
+                                      const char *ip_s);
+
+/* Returns true if the given router port 'op' (assumed to be a distributed
+ * gateway port) is the relevant DGP where the NAT rule of the router needs to
+ * be applied. */
+static bool
+is_nat_gateway_port(const struct nbrec_nat *nat, const struct ovn_port *op)
+{
+    if (op->od->n_l3dgw_ports > 1
+        && ((!nat->gateway_port && !find_lrp_member_ip(op, nat->external_ip))
+            || (nat->gateway_port && nat->gateway_port != op->nbrp))) {
+        return false;
+    }
+    return true;
+}
+
 enum dynamic_update_type {
     NONE,    /* No change to the address */
     REMOVE,  /* Address is no longer dynamic */
@@ -2802,9 +2819,9 @@  join_logical_ports(struct northd_input *input_data,
  * port, followed by 'is_chassis_resident("LPORT_NAME")', where the
  * LPORT_NAME is the name of the L3 redirect port or the name of the
  * logical_port specified in a NAT rule. These strings include the
- * external IP addresses of NAT rules defined on that router which have
- * gateway_port not set or have gateway_port as the router port 'op', and all
- * of the IP addresses used in load balancer VIPs defined on that router.
+ * external IP addresses of NAT rules defined on that router whose
+ * gateway_port is router port 'op', and all of the IP addresses used in
+ * load balancer VIPs defined on that router.
  *
  * The caller must free each of the n returned strings with free(),
  * and must free the returned array when it is no longer needed. */
@@ -2847,7 +2864,7 @@  get_nat_addresses(const struct ovn_port *op, size_t *n, bool routable_only,
 
         /* Not including external IP of NAT rules whose gateway_port is
          * not 'op'. */
-        if (nat->gateway_port && nat->gateway_port != op->nbrp) {
+        if (!is_nat_gateway_port(nat, op)) {
             continue;
         }
 
@@ -3522,8 +3539,12 @@  ovn_port_update_sbrec(struct northd_input *input_data,
                     struct ds nat_addr = DS_EMPTY_INITIALIZER;
                     ds_put_format(&nat_addr, "%s", nat_addresses);
                     if (l3dgw_ports) {
+                        const struct ovn_port *l3dgw_port = (
+                            is_l3dgw_port(op->peer)
+                            ? op->peer
+                            : op->peer->od->l3dgw_ports[0]);
                         ds_put_format(&nat_addr, " is_chassis_resident(%s)",
-                            op->peer->od->l3dgw_ports[0]->cr_port->json_key);
+                            l3dgw_port->cr_port->json_key);
                     }
                     nats[0] = xstrdup(ds_cstr(&nat_addr));
                     ds_destroy(&nat_addr);
@@ -8604,53 +8625,7 @@  build_bfd_table(struct lflow_input *input_data,
 static const char *
 find_lrp_member_ip(const struct ovn_port *op, const char *ip_s)
 {
-    bool is_ipv4 = strchr(ip_s, '.') ? true : false;
-
-    if (is_ipv4) {
-        ovs_be32 ip;
-
-        if (!ip_parse(ip_s, &ip)) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-            VLOG_WARN_RL(&rl, "bad ip address %s", ip_s);
-            return NULL;
-        }
-
-        for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
-            const struct ipv4_netaddr *na = &op->lrp_networks.ipv4_addrs[i];
-
-            if (!((na->network ^ ip) & na->mask)) {
-                /* There should be only 1 interface that matches the
-                 * supplied IP.  Otherwise, it's a configuration error,
-                 * because subnets of a router's interfaces should NOT
-                 * overlap. */
-                return na->addr_s;
-            }
-        }
-    } else {
-        struct in6_addr ip6;
-
-        if (!ipv6_parse(ip_s, &ip6)) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-            VLOG_WARN_RL(&rl, "bad ipv6 address %s", ip_s);
-            return NULL;
-        }
-
-        for (int i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
-            const struct ipv6_netaddr *na = &op->lrp_networks.ipv6_addrs[i];
-            struct in6_addr xor_addr = ipv6_addr_bitxor(&na->network, &ip6);
-            struct in6_addr and_addr = ipv6_addr_bitand(&xor_addr, &na->mask);
-
-            if (ipv6_is_zero(&and_addr)) {
-                /* There should be only 1 interface that matches the
-                 * supplied IP.  Otherwise, it's a configuration error,
-                 * because subnets of a router's interfaces should NOT
-                 * overlap. */
-                return na->addr_s;
-            }
-        }
-    }
-
-    return NULL;
+    return find_lport_address(&op->lrp_networks, ip_s);
 }
 
 static struct ovn_port*
@@ -10385,9 +10360,9 @@  build_lrouter_port_nat_arp_nd_flow(struct ovn_port *op,
     const struct nbrec_nat *nat = nat_entry->nb;
     struct ds match = DS_EMPTY_INITIALIZER;
 
-    /* ARP/ND should be sent from distributed gateway port specified in
-     * the NAT rule. */
-    if (nat->gateway_port && nat->gateway_port != op->nbrp) {
+    /* ARP/ND should be sent from distributed gateway port where the NAT rule
+     * will be applied. */
+    if (!is_nat_gateway_port(nat, op)) {
         return;
     }
 
@@ -13206,14 +13181,24 @@  lrouter_check_nat_entry(struct ovn_datapath *od, const struct nbrec_nat *nat,
     /* Validate gateway_port of NAT rule. */
     *nat_l3dgw_port = NULL;
     if (nat->gateway_port == NULL) {
-        if (od->n_l3dgw_ports > 1) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-            VLOG_WARN_RL(&rl, "NAT configured on logical router: %s with"
-                         "multiple distributed gateway ports needs to specify"
-                         "valid gateway_port.", od->nbr->name);
-            return -EINVAL;
-        } else if (od->n_l3dgw_ports) {
+        if (od->n_l3dgw_ports == 1) {
             *nat_l3dgw_port = od->l3dgw_ports[0];
+        } else if (od->n_l3dgw_ports > 1) {
+            /* Find the DGP reachable for the NAT external IP. */
+            for (size_t i = 0; i < od->n_l3dgw_ports; i++) {
+               if (find_lrp_member_ip(od->l3dgw_ports[i], nat->external_ip)) {
+                   *nat_l3dgw_port = od->l3dgw_ports[i];
+                   break;
+               }
+            }
+            if (*nat_l3dgw_port == NULL) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "Unable to determine gateway_port for NAT "
+                             "with external_ip: %s configured on logical "
+                             "router: %s with multiple distributed gateway "
+                             "ports", nat->external_ip, od->nbr->name);
+                return -EINVAL;
+            }
         }
     } else {
         *nat_l3dgw_port = ovn_port_find(ports, nat->gateway_port->name);
diff --git a/northd/ovn-northd.8.xml b/northd/ovn-northd.8.xml
index 1f7022490..a3187b401 100644
--- a/northd/ovn-northd.8.xml
+++ b/northd/ovn-northd.8.xml
@@ -2135,7 +2135,8 @@  output;
           router that specifies an external Ethernet address <var>E</var>,
           a priority-50 flow that matches <code>inport == <var>GW</var>
           &amp;&amp; eth.dst == <var>E</var></code>, where <var>GW</var>
-          is the logical router gateway port, with action
+          is the logical router distributed gateway port corresponding to the
+          NAT rule (specified or inferred), with action
           <code>xreg0[0..47]=E; next;</code>.
         </p>
 
@@ -2451,11 +2452,12 @@  icmp6_error {
       <li>
         <p>
           For each NAT entry of a distributed logical router  (with
-          distributed gateway router port) of type <code>snat</code>,
+          distributed gateway router port(s)) of type <code>snat</code>,
           a priority-120 flow with the match <code>inport == <var>P</var>
           &amp;&amp; ip4.src == <var>A</var></code> advances the packet to
           the next pipeline, where <var>P</var> is the distributed logical
-          router port and <var>A</var> is the <code>external_ip</code> set
+          router port corresponding to the NAT entry (specified or inferred)
+          and <var>A</var> is the <code>external_ip</code> set
           in the NAT entry. If <var>A</var> is an IPv6 address, then
           <code>ip6.src</code> is used for the match.
         </p>
@@ -3055,7 +3057,7 @@  icmp6 {
               ip6.dst == <var>B</var> &amp;&amp; inport == <var>GW</var>
               &amp;&amp; flags.loopback == 0</code>
               where <var>GW</var> is the distributed gateway port
-              specified in the NAT rule, with an
+              corresponding to the NAT rule (specified or inferred), with an
               action <code>ct_snat_in_czone;</code> to unSNAT in the common
               zone.  If the NAT rule is of type dnat_and_snat and has
               <code>stateless=true</code> in the options, then the action
@@ -3081,7 +3083,7 @@  icmp6 {
               &amp;&amp; flags.loopback == 0 &amp;&amp;
               flags.use_snat_zone == 1</code>
               where <var>GW</var> is the distributed gateway port
-              specified in the NAT rule, with an
+              corresponding to the NAT rule (specified or inferred), with an
               action <code>ct_snat;</code> to unSNAT in the snat zone. If the
               NAT rule is of type dnat_and_snat and has
               <code>stateless=true</code> in the options, then the action
@@ -3362,8 +3364,8 @@  icmp6 {
           to change the destination IP address of a packet from <var>A</var> to
           <var>B</var>, a priority-100 flow matches <code>ip &amp;&amp;
           ip4.dst == <var>B</var> &amp;&amp; inport == <var>GW</var></code>,
-          where <var>GW</var> is the logical router gateway port configured
-          for the NAT rule, with an action
+          where <var>GW</var> is the logical router gateway port corresponding
+          to the NAT rule (specified or inferred), with an action
           <code>ct_dnat(<var>B</var>);</code>.  The match will include
           <code>ip6.dst == <var>B</var></code> in the IPv6 case.
           If the NAT rule is of type dnat_and_snat and has
@@ -4681,7 +4683,8 @@  nd_ns {
           outport == <var>GW</var> &amp;&amp;
           is_chassis_resident(<var>P</var>)</code>, where <var>E</var> is the
           external IP address specified in the NAT rule, <var>GW</var>
-          is the distributed gateway port specified in the NAT rule.
+          is the distributed gateway port corresponding to the NAT rule
+          (specified or inferred).
           For dnat_and_snat NAT rule, <var>P</var> is the logical port
           specified in the NAT rule.
           If <ref column="logical_port"
diff --git a/ovn-nb.xml b/ovn-nb.xml
index 3e3e142b3..c2475105a 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -3380,14 +3380,6 @@ 
         table where the NAT rule needs to be applied.
       </p>
 
-      <p>
-        This column needs to be set when multiple distributed gateway ports
-        are configured on a <ref table="Logical_Router"/> for the NAT rule to
-        be applied. If logical router has a single distributed gateway port,
-        NAT rule is applied at the distributed gateway port even if this
-        column is not set.
-      </p>
-
       <p>
         When multiple distributed gateway ports are configured on a
         <ref table="Logical_Router"/>, applying a NAT rule at each of the
@@ -3405,6 +3397,17 @@ 
         <ref column="networks" table="Logical_Router_Port"/>
         <code>50.0.0.10/24</code>.
       </p>
+
+      <p>
+        When a logical router has multiple distributed gateway ports and this
+        column is not set for a NAT rule, then the rule will be applied at the
+        distributed gateway port which is in the same network as the
+        <ref column="external_ip"/> of the NAT rule, if such a router port
+        exists. If logical router has a single distributed gateway port and
+        this column is not set for a NAT rule, the rule will be applied at the
+        distributed gateway port even if the router port is not in the same
+        network as the <ref column="external_ip"/> of the NAT rule.
+      </p>
     </column>
 
     <column name="options" key="stateless">
diff --git a/tests/ovn-nbctl.at b/tests/ovn-nbctl.at
index 061e95ff8..726efa6f4 100644
--- a/tests/ovn-nbctl.at
+++ b/tests/ovn-nbctl.at
@@ -521,28 +521,28 @@  snat                                   30.0.0.1                            192.1
 snat                                   fd01::1                             fd11::/64
 ])
 AT_CHECK([ovn-nbctl lr-nat-add lr0 snat 30.0.0.1 192.168.1.0/24], [1], [],
-[ovn-nbctl: 30.0.0.1, 192.168.1.0/24, : a NAT with this external_ip, logical_ip and gateway_port already exists
+[ovn-nbctl: 30.0.0.1, 192.168.1.0/24: a NAT with this external_ip and logical_ip already exists
 ])
 AT_CHECK([ovn-nbctl lr-nat-add lr0 snat 30.0.0.1 192.168.1.10/24], [1], [],
-[ovn-nbctl: 30.0.0.1, 192.168.1.0/24, : a NAT with this external_ip, logical_ip and gateway_port already exists
+[ovn-nbctl: 30.0.0.1, 192.168.1.0/24: a NAT with this external_ip and logical_ip already exists
 ])
 AT_CHECK([ovn-nbctl --may-exist lr-nat-add lr0 snat 30.0.0.1 192.168.1.0/24])
 AT_CHECK([ovn-nbctl lr-nat-add lr0 snat 30.0.0.2 192.168.1.0/24], [1], [],
-[ovn-nbctl: a NAT with this type (snat), logical_ip (192.168.1.0/24) and gateway_port () already exists
+[ovn-nbctl: a NAT with this type (snat), logical_ip (192.168.1.0/24) already exists
 ])
 AT_CHECK([ovn-nbctl lr-nat-add lr0 dnat 30.0.0.1 192.168.1.2], [1], [],
-[ovn-nbctl: 30.0.0.1, 192.168.1.2, : a NAT with this external_ip, logical_ip and gateway_port already exists
+[ovn-nbctl: 30.0.0.1, 192.168.1.2: a NAT with this external_ip and logical_ip already exists
 ])
 AT_CHECK([ovn-nbctl --may-exist lr-nat-add lr0 dnat 30.0.0.1 192.168.1.2])
 AT_CHECK([ovn-nbctl lr-nat-add lr0 dnat 30.0.0.1 192.168.1.3], [1], [],
-[ovn-nbctl: a NAT with this type (dnat), external_ip (30.0.0.1) and gateway_port () already exists
+[ovn-nbctl: a NAT with this type (dnat), external_ip (30.0.0.1) already exists
 ])
 AT_CHECK([ovn-nbctl lr-nat-add lr0 dnat_and_snat 30.0.0.1 192.168.1.2], [1], [],
-[ovn-nbctl: 30.0.0.1, 192.168.1.2, : a NAT with this external_ip, logical_ip and gateway_port already exists
+[ovn-nbctl: 30.0.0.1, 192.168.1.2: a NAT with this external_ip and logical_ip already exists
 ])
 AT_CHECK([ovn-nbctl --may-exist lr-nat-add lr0 dnat_and_snat 30.0.0.1 192.168.1.2])
 AT_CHECK([ovn-nbctl lr-nat-add lr0 dnat_and_snat 30.0.0.1 192.168.1.3], [1], [],
-[ovn-nbctl: a NAT with this type (dnat_and_snat), external_ip (30.0.0.1) and gateway_port () already exists
+[ovn-nbctl: a NAT with this type (dnat_and_snat), external_ip (30.0.0.1) already exists
 ])
 AT_CHECK([ovn-nbctl --may-exist lr-nat-add lr0 dnat_and_snat 30.0.0.2 192.168.1.3 lp0 00:00:00:04:05:06])
 AT_CHECK([ovn-nbctl lr-nat-list lr0], [0], [dnl
@@ -756,7 +756,6 @@  AT_CHECK([ovn-nbctl lrp-set-gateway-chassis lrp00 chassis1])
 AT_CHECK([ovn-nbctl lr-add lr1])
 AT_CHECK([ovn-nbctl lrp-add lr1 lrp10 00:00:00:01:02:05 172.64.2.10/24])
 
-AT_CHECK([ovn-nbctl --gateway-port=lrp00 lr-nat-add lr0 snat 172.64.0.10 20.0.0.10])
 AT_CHECK([ovn-nbctl --gateway-port=lrp01 lr-nat-add lr0 dnat 172.64.1.10 20.0.0.10], [1], [],
 [ovn-nbctl: lrp01 is not a distributed gateway router port.
 ])
@@ -766,50 +765,54 @@  AT_CHECK([ovn-nbctl --gateway-port=lrp10 lr-nat-add lr0 dnat_and_snat 172.64.2.1
 
 AT_CHECK([ovn-nbctl lrp-set-gateway-chassis lrp01 chassis2])
 
-AT_CHECK([ovn-nbctl lr-nat-add lr0 dnat 172.64.1.10 20.0.0.10], [1], [],
-[ovn-nbctl: logical router: lr0 has multiple distributed gateway ports. NAT rule needs to specify gateway_port.
+AT_CHECK([ovn-nbctl --gateway-port=lrp00 lr-nat-add lr0 snat 172.64.0.10 20.0.0.10])
+AT_CHECK([ovn-nbctl lr-nat-add lr0 snat 172.64.1.10 20.0.0.10])
+AT_CHECK([ovn-nbctl lr-nat-add lr0 dnat 30.0.0.10 20.0.0.10], [1], [],
+[ovn-nbctl: logical router: lr0 has multiple distributed gateway ports and gateway_port can not be determined from external IP of NAT rule.
 ])
 AT_CHECK([ovn-nbctl --gateway-port=lrp00 lr-nat-add lr0 dnat 30.0.0.10 20.0.0.10])
-AT_CHECK([ovn-nbctl --gateway-port=lrp01 lr-nat-add lr0 dnat 30.0.0.10 20.0.0.10])
-AT_CHECK([ovn-nbctl --gateway-port=lrp01 lr-nat-add lr0 dnat 30.0.0.10 20.0.0.20], [1], [],
-[ovn-nbctl: a NAT with this type (dnat), external_ip (30.0.0.10) and gateway_port (lrp01) already exists
+AT_CHECK([ovn-nbctl --gateway-port=lrp01 lr-nat-add lr0 snat 172.64.1.10 20.0.0.10], [1], [],
+[ovn-nbctl: 172.64.1.10, 20.0.0.10: a NAT with this external_ip and logical_ip already exists
 ])
+AT_CHECK([ovn-nbctl --gateway-port=lrp00 lr-nat-add lr0 dnat 30.0.0.10 20.0.0.11], [1], [],
+[ovn-nbctl: a NAT with this type (dnat), external_ip (30.0.0.10) already exists
+])
+AT_CHECK([ovn-nbctl --gateway-port=lrp01 lr-nat-add lr0 dnat 30.0.0.10 20.0.0.10])
 
 AT_CHECK([ovn-nbctl lr-nat-list lr0], [0], [dnl
 TYPE             GATEWAY_PORT          EXTERNAL_IP        EXTERNAL_PORT    LOGICAL_IP          EXTERNAL_MAC         LOGICAL_PORT
 dnat             lrp00                 30.0.0.10                           20.0.0.10
 dnat             lrp01                 30.0.0.10                           20.0.0.10
+snat                                   172.64.1.10                         20.0.0.10
 snat             lrp00                 172.64.0.10                         20.0.0.10
 ])
 
 AT_CHECK([ovn-nbctl lr-nat-del lr0 dnat 30.0.0.10])
 AT_CHECK([ovn-nbctl lr-nat-list lr0], [0], [dnl
 TYPE             GATEWAY_PORT          EXTERNAL_IP        EXTERNAL_PORT    LOGICAL_IP          EXTERNAL_MAC         LOGICAL_PORT
+snat                                   172.64.1.10                         20.0.0.10
 snat             lrp00                 172.64.0.10                         20.0.0.10
 ])
 
 AT_CHECK([ovn-nbctl --gateway-port=lrp00 lr-nat-add lr0 snat 30.0.0.10 20.0.0.20])
-AT_CHECK([ovn-nbctl --gateway-port=lrp01 lr-nat-add lr0 snat 30.0.0.10 20.0.0.20])
 AT_CHECK([ovn-nbctl lr-nat-list lr0], [0], [dnl
 TYPE             GATEWAY_PORT          EXTERNAL_IP        EXTERNAL_PORT    LOGICAL_IP          EXTERNAL_MAC         LOGICAL_PORT
+snat                                   172.64.1.10                         20.0.0.10
 snat             lrp00                 172.64.0.10                         20.0.0.10
 snat             lrp00                 30.0.0.10                           20.0.0.20
-snat             lrp01                 30.0.0.10                           20.0.0.20
 ])
 AT_CHECK([ovn-nbctl lr-nat-del lr0 dnat lrp11], [1], [],
 [ovn-nbctl: lrp11: port name not found
 ])
-AT_CHECK([ovn-nbctl lr-nat-del lr0 dnat lrp00])
+AT_CHECK([ovn-nbctl lr-nat-del lr0 snat lrp00])
 AT_CHECK([ovn-nbctl lr-nat-list lr0], [0], [dnl
 TYPE             GATEWAY_PORT          EXTERNAL_IP        EXTERNAL_PORT    LOGICAL_IP          EXTERNAL_MAC         LOGICAL_PORT
-snat             lrp00                 172.64.0.10                         20.0.0.10
-snat             lrp00                 30.0.0.10                           20.0.0.20
-snat             lrp01                 30.0.0.10                           20.0.0.20
+snat                                   172.64.1.10                         20.0.0.10
 ])
-AT_CHECK([ovn-nbctl lr-nat-del lr0 snat lrp00])
+AT_CHECK([ovn-nbctl lr-nat-del lr0 snat lrp01])
 AT_CHECK([ovn-nbctl lr-nat-list lr0], [0], [dnl
 TYPE             GATEWAY_PORT          EXTERNAL_IP        EXTERNAL_PORT    LOGICAL_IP          EXTERNAL_MAC         LOGICAL_PORT
-snat             lrp01                 30.0.0.10                           20.0.0.20
+snat                                   172.64.1.10                         20.0.0.10
 ])
 
 AT_CHECK([ovn-nbctl lr-nat-del lr0 snat 20.0.0 lrp01], [1], [],
@@ -819,7 +822,7 @@  AT_CHECK([ovn-nbctl lr-nat-del lr0 snat 20.0.0.10 lrp01], [1], [],
 [ovn-nbctl: no matching NAT with the type (snat), logical_ip (20.0.0.10) and gateway_port (lrp01)
 ])
 AT_CHECK([ovn-nbctl --if-exists lr-nat-del lr0 snat 20.0.0.10 lrp01])
-AT_CHECK([ovn-nbctl lr-nat-del lr0 snat 20.0.0.20 lrp01])
+AT_CHECK([ovn-nbctl lr-nat-del lr0 snat])
 ])
 
 dnl ---------------------------------------------------------------------
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index 5bd0935e7..f3670d1bb 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -6564,18 +6564,17 @@  check ovn-nbctl lrp-set-gateway-chassis DR-S1 gw1
 check ovn-nbctl lrp-set-gateway-chassis DR-S2 gw2
 check ovn-nbctl lrp-set-gateway-chassis DR-S3 gw3
 
-# Configure SNAT
-check ovn-nbctl --gateway-port=DR-S1 lr-nat-add DR snat  172.16.1.10    20.0.0.10
+# Configure SNAT with and without setting "gateway_port" column
+check ovn-nbctl                      lr-nat-add DR snat  172.16.1.10    20.0.0.10
 check ovn-nbctl --gateway-port=DR-S2 lr-nat-add DR snat  10.0.0.10      20.0.0.10
-check ovn-nbctl --gateway-port=DR-S3 lr-nat-add DR snat  192.168.0.10   20.0.0.10
+check ovn-nbctl                      lr-nat-add DR snat  192.168.0.10   20.0.0.10
 
 check ovn-nbctl --wait=sb sync
 
 ovn-sbctl dump-flows DR > lrflows
 AT_CAPTURE_FILE([lrflows])
 
-check_lr_in_arp_nat_flows() {
-    AT_CHECK([grep lr_in_ip_input lrflows | grep arp | grep -e 172.16.1.10 -e 10.0.0.10 -e 192.168.0.10 | sed 's/table=../table=??/' | sort], [0], [dnl
+AT_CHECK([grep lr_in_ip_input lrflows | grep arp | grep -e 172.16.1.10 -e 10.0.0.10 -e 192.168.0.10 | sed 's/table=../table=??/' | sort], [0], [dnl
   table=??(lr_in_ip_input     ), priority=90   , match=(arp.op == 1 && arp.tpa == 10.0.0.10), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=??(lr_in_ip_input     ), priority=90   , match=(arp.op == 1 && arp.tpa == 172.16.1.10), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=??(lr_in_ip_input     ), priority=90   , match=(arp.op == 1 && arp.tpa == 192.168.0.10), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
@@ -6586,10 +6585,8 @@  check_lr_in_arp_nat_flows() {
   table=??(lr_in_ip_input     ), priority=92   , match=(inport == "DR-S2" && arp.op == 1 && arp.tpa == 10.0.0.10 && is_chassis_resident("cr-DR-S2")), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=??(lr_in_ip_input     ), priority=92   , match=(inport == "DR-S3" && arp.op == 1 && arp.tpa == 192.168.0.10 && is_chassis_resident("cr-DR-S3")), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
 ])
-}
 
-check_lr_in_unsnat_flows() {
-    AT_CHECK([grep lr_in_unsnat lrflows | grep ct_snat | sed 's/table=../table=??/' | sort], [0], [dnl
+AT_CHECK([grep lr_in_unsnat lrflows | grep ct_snat | sed 's/table=../table=??/' | sort], [0], [dnl
   table=??(lr_in_unsnat       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10 && inport == "DR-S2" && flags.loopback == 0 && is_chassis_resident("cr-DR-S2")), action=(ct_snat_in_czone;)
   table=??(lr_in_unsnat       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10 && inport == "DR-S2" && flags.loopback == 1 && flags.use_snat_zone == 1 && is_chassis_resident("cr-DR-S2")), action=(ct_snat;)
   table=??(lr_in_unsnat       ), priority=100  , match=(ip && ip4.dst == 172.16.1.10 && inport == "DR-S1" && flags.loopback == 0 && is_chassis_resident("cr-DR-S1")), action=(ct_snat_in_czone;)
@@ -6597,10 +6594,8 @@  check_lr_in_unsnat_flows() {
   table=??(lr_in_unsnat       ), priority=100  , match=(ip && ip4.dst == 192.168.0.10 && inport == "DR-S3" && flags.loopback == 0 && is_chassis_resident("cr-DR-S3")), action=(ct_snat_in_czone;)
   table=??(lr_in_unsnat       ), priority=100  , match=(ip && ip4.dst == 192.168.0.10 && inport == "DR-S3" && flags.loopback == 1 && flags.use_snat_zone == 1 && is_chassis_resident("cr-DR-S3")), action=(ct_snat;)
 ])
-}
 
-check_lr_out_snat_flows() {
-    AT_CHECK([grep lr_out_snat lrflows | grep ct_snat | sed 's/table=../table=??/' | sort], [0], [dnl
+AT_CHECK([grep lr_out_snat lrflows | grep ct_snat | sed 's/table=../table=??/' | sort], [0], [dnl
   table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1")), action=(ct_snat_in_czone(172.16.1.10);)
   table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S2" && is_chassis_resident("cr-DR-S2")), action=(ct_snat_in_czone(10.0.0.10);)
   table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S3" && is_chassis_resident("cr-DR-S3")), action=(ct_snat_in_czone(192.168.0.10);)
@@ -6608,45 +6603,44 @@  check_lr_out_snat_flows() {
   table=??(lr_out_snat        ), priority=162  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S2" && is_chassis_resident("cr-DR-S2") && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(10.0.0.10);)
   table=??(lr_out_snat        ), priority=162  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S3" && is_chassis_resident("cr-DR-S3") && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(192.168.0.10);)
 ])
-}
-
-check_lr_in_unsnat_flows
-check_lr_out_snat_flows
-check_lr_in_arp_nat_flows
 
 check ovn-nbctl --wait=sb lr-nat-del DR snat 20.0.0.10
 AT_CHECK([ovn-sbctl dump-flows DR | grep -e lr_in_unsnat -e lr_out_snat | grep ct_snat | wc -l], [0], [0
 ])
 
-# Configure DNAT
-check ovn-nbctl --gateway-port=DR-S1 lr-nat-add DR dnat  172.16.1.10    20.0.0.10
+# Configure DNAT - 2 gateway_ports configured for same external IP
+check ovn-nbctl                      lr-nat-add DR dnat  172.16.1.10    20.0.0.10
 check ovn-nbctl --gateway-port=DR-S2 lr-nat-add DR dnat  10.0.0.10      20.0.0.10
-check ovn-nbctl --gateway-port=DR-S3 lr-nat-add DR dnat  192.168.0.10   20.0.0.10
+check ovn-nbctl --gateway-port=DR-S3 lr-nat-add DR dnat  172.16.1.10    20.0.0.10
 
 check ovn-nbctl --wait=sb sync
 
 ovn-sbctl dump-flows DR > lrflows
 AT_CAPTURE_FILE([lrflows])
 
-check_lr_in_dnat_flows() {
-    AT_CHECK([grep lr_in_dnat lrflows | grep ct_dnat | sed 's/table=../table=??/' | sort], [0], [dnl
+AT_CHECK([grep lr_in_ip_input lrflows | grep arp | grep -e 172.16.1.10 -e 10.0.0.10 | sed 's/table=../table=??/' | sort], [0], [dnl
+  table=??(lr_in_ip_input     ), priority=90   , match=(arp.op == 1 && arp.tpa == 10.0.0.10), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=90   , match=(arp.op == 1 && arp.tpa == 172.16.1.10), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=90   , match=(arp.op == 1 && arp.tpa == 172.16.1.10), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "DR-S1" && arp.op == 1 && arp.tpa == 172.16.1.10), action=(drop;)
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "DR-S2" && arp.op == 1 && arp.tpa == 10.0.0.10), action=(drop;)
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "DR-S3" && arp.op == 1 && arp.tpa == 172.16.1.10), action=(drop;)
+  table=??(lr_in_ip_input     ), priority=92   , match=(inport == "DR-S1" && arp.op == 1 && arp.tpa == 172.16.1.10 && is_chassis_resident("cr-DR-S1")), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=92   , match=(inport == "DR-S2" && arp.op == 1 && arp.tpa == 10.0.0.10 && is_chassis_resident("cr-DR-S2")), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=92   , match=(inport == "DR-S3" && arp.op == 1 && arp.tpa == 172.16.1.10 && is_chassis_resident("cr-DR-S3")), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+])
+
+AT_CHECK([grep lr_in_dnat lrflows | grep ct_dnat | sed 's/table=../table=??/' | sort], [0], [dnl
   table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 10.0.0.10 && inport == "DR-S2" && is_chassis_resident("cr-DR-S2")), action=(ct_dnat_in_czone(20.0.0.10);)
   table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.16.1.10 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1")), action=(ct_dnat_in_czone(20.0.0.10);)
-  table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 192.168.0.10 && inport == "DR-S3" && is_chassis_resident("cr-DR-S3")), action=(ct_dnat_in_czone(20.0.0.10);)
+  table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.16.1.10 && inport == "DR-S3" && is_chassis_resident("cr-DR-S3")), action=(ct_dnat_in_czone(20.0.0.10);)
 ])
-}
 
-check_lr_out_undnat_flows() {
-    AT_CHECK([grep lr_out_undnat lrflows | grep ct_dnat | sed 's/table=../table=??/' | sort], [0], [dnl
+AT_CHECK([grep lr_out_undnat lrflows | grep ct_dnat | sed 's/table=../table=??/' | sort], [0], [dnl
   table=??(lr_out_undnat      ), priority=100  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1")), action=(ct_dnat_in_czone;)
   table=??(lr_out_undnat      ), priority=100  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S2" && is_chassis_resident("cr-DR-S2")), action=(ct_dnat_in_czone;)
   table=??(lr_out_undnat      ), priority=100  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S3" && is_chassis_resident("cr-DR-S3")), action=(ct_dnat_in_czone;)
 ])
-}
-
-check_lr_in_dnat_flows
-check_lr_out_undnat_flows
-check_lr_in_arp_nat_flows
 
 check ovn-nbctl --wait=sb lr-nat-del DR dnat
 
@@ -6655,7 +6649,7 @@  AT_CHECK([ovn-sbctl dump-flows DR | grep -e lr_in_dnat -e lr_out_undnat | grep c
 
 # Configure DNAT_AND_SNAT
 check ovn-nbctl --gateway-port=DR-S1 lr-nat-add DR dnat_and_snat  172.16.1.10    20.0.0.10
-check ovn-nbctl --gateway-port=DR-S2 lr-nat-add DR dnat_and_snat  10.0.0.10      20.0.0.10
+check ovn-nbctl                      lr-nat-add DR dnat_and_snat  10.0.0.10      20.0.0.10
 check ovn-nbctl --gateway-port=DR-S3 lr-nat-add DR dnat_and_snat  192.168.0.10   20.0.0.10
 
 check ovn-nbctl --wait=sb sync
@@ -6663,11 +6657,47 @@  check ovn-nbctl --wait=sb sync
 ovn-sbctl dump-flows DR > lrflows
 AT_CAPTURE_FILE([lrflows])
 
-check_lr_in_unsnat_flows
-check_lr_out_snat_flows
-check_lr_in_dnat_flows
-check_lr_out_undnat_flows
-check_lr_in_arp_nat_flows
+AT_CHECK([grep lr_in_ip_input lrflows | grep arp | grep -e 172.16.1.10 -e 10.0.0.10 -e 192.168.0.10 | sed 's/table=../table=??/' | sort], [0], [dnl
+  table=??(lr_in_ip_input     ), priority=90   , match=(arp.op == 1 && arp.tpa == 10.0.0.10), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=90   , match=(arp.op == 1 && arp.tpa == 172.16.1.10), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=90   , match=(arp.op == 1 && arp.tpa == 192.168.0.10), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "DR-S1" && arp.op == 1 && arp.tpa == 172.16.1.10), action=(drop;)
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "DR-S2" && arp.op == 1 && arp.tpa == 10.0.0.10), action=(drop;)
+  table=??(lr_in_ip_input     ), priority=91   , match=(inport == "DR-S3" && arp.op == 1 && arp.tpa == 192.168.0.10), action=(drop;)
+  table=??(lr_in_ip_input     ), priority=92   , match=(inport == "DR-S1" && arp.op == 1 && arp.tpa == 172.16.1.10 && is_chassis_resident("cr-DR-S1")), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=92   , match=(inport == "DR-S2" && arp.op == 1 && arp.tpa == 10.0.0.10 && is_chassis_resident("cr-DR-S2")), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=??(lr_in_ip_input     ), priority=92   , match=(inport == "DR-S3" && arp.op == 1 && arp.tpa == 192.168.0.10 && is_chassis_resident("cr-DR-S3")), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+])
+
+AT_CHECK([grep lr_in_unsnat lrflows | grep ct_snat | sed 's/table=../table=??/' | sort], [0], [dnl
+  table=??(lr_in_unsnat       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10 && inport == "DR-S2" && flags.loopback == 0 && is_chassis_resident("cr-DR-S2")), action=(ct_snat_in_czone;)
+  table=??(lr_in_unsnat       ), priority=100  , match=(ip && ip4.dst == 10.0.0.10 && inport == "DR-S2" && flags.loopback == 1 && flags.use_snat_zone == 1 && is_chassis_resident("cr-DR-S2")), action=(ct_snat;)
+  table=??(lr_in_unsnat       ), priority=100  , match=(ip && ip4.dst == 172.16.1.10 && inport == "DR-S1" && flags.loopback == 0 && is_chassis_resident("cr-DR-S1")), action=(ct_snat_in_czone;)
+  table=??(lr_in_unsnat       ), priority=100  , match=(ip && ip4.dst == 172.16.1.10 && inport == "DR-S1" && flags.loopback == 1 && flags.use_snat_zone == 1 && is_chassis_resident("cr-DR-S1")), action=(ct_snat;)
+  table=??(lr_in_unsnat       ), priority=100  , match=(ip && ip4.dst == 192.168.0.10 && inport == "DR-S3" && flags.loopback == 0 && is_chassis_resident("cr-DR-S3")), action=(ct_snat_in_czone;)
+  table=??(lr_in_unsnat       ), priority=100  , match=(ip && ip4.dst == 192.168.0.10 && inport == "DR-S3" && flags.loopback == 1 && flags.use_snat_zone == 1 && is_chassis_resident("cr-DR-S3")), action=(ct_snat;)
+])
+
+AT_CHECK([grep lr_out_snat lrflows | grep ct_snat | sed 's/table=../table=??/' | sort], [0], [dnl
+  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1")), action=(ct_snat_in_czone(172.16.1.10);)
+  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S2" && is_chassis_resident("cr-DR-S2")), action=(ct_snat_in_czone(10.0.0.10);)
+  table=??(lr_out_snat        ), priority=161  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S3" && is_chassis_resident("cr-DR-S3")), action=(ct_snat_in_czone(192.168.0.10);)
+  table=??(lr_out_snat        ), priority=162  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1") && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(172.16.1.10);)
+  table=??(lr_out_snat        ), priority=162  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S2" && is_chassis_resident("cr-DR-S2") && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(10.0.0.10);)
+  table=??(lr_out_snat        ), priority=162  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S3" && is_chassis_resident("cr-DR-S3") && reg9[[4]] == 1), action=(reg9[[4]] = 0; ct_snat(192.168.0.10);)
+])
+
+AT_CHECK([grep lr_in_dnat lrflows | grep ct_dnat | sed 's/table=../table=??/' | sort], [0], [dnl
+  table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 10.0.0.10 && inport == "DR-S2" && is_chassis_resident("cr-DR-S2")), action=(ct_dnat_in_czone(20.0.0.10);)
+  table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 172.16.1.10 && inport == "DR-S1" && is_chassis_resident("cr-DR-S1")), action=(ct_dnat_in_czone(20.0.0.10);)
+  table=??(lr_in_dnat         ), priority=100  , match=(ip && ip4.dst == 192.168.0.10 && inport == "DR-S3" && is_chassis_resident("cr-DR-S3")), action=(ct_dnat_in_czone(20.0.0.10);)
+])
+
+AT_CHECK([grep lr_out_undnat lrflows | grep ct_dnat | sed 's/table=../table=??/' | sort], [0], [dnl
+  table=??(lr_out_undnat      ), priority=100  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S1" && is_chassis_resident("cr-DR-S1")), action=(ct_dnat_in_czone;)
+  table=??(lr_out_undnat      ), priority=100  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S2" && is_chassis_resident("cr-DR-S2")), action=(ct_dnat_in_czone;)
+  table=??(lr_out_undnat      ), priority=100  , match=(ip && ip4.src == 20.0.0.10 && outport == "DR-S3" && is_chassis_resident("cr-DR-S3")), action=(ct_dnat_in_czone;)
+])
 
 check ovn-nbctl --wait=sb lr-nat-del DR dnat_and_snat
 
diff --git a/utilities/ovn-nbctl.8.xml b/utilities/ovn-nbctl.8.xml
index 3e9176fa0..040d05227 100644
--- a/utilities/ovn-nbctl.8.xml
+++ b/utilities/ovn-nbctl.8.xml
@@ -1174,8 +1174,9 @@ 
           applied. <var>GATEWAY_PORT</var> should reference a
           <ref table="Logical_Router_Port"/> row that is a distributed gateway
           port of <var>router</var>. When <var>router</var> has multiple
-          distributed gateway ports, it is an error to not specify the
-          <var>GATEWAY_PORT</var>.
+          distributed gateway ports and the gateway port for this NAT can't
+          be inferred from the <var>external_ip</var>, it is an error to not
+          specify the <var>GATEWAY_PORT</var>.
         </p>
 
         <p>
diff --git a/utilities/ovn-nbctl.c b/utilities/ovn-nbctl.c
index 6a8ba6330..0d5647b2f 100644
--- a/utilities/ovn-nbctl.c
+++ b/utilities/ovn-nbctl.c
@@ -4623,6 +4623,17 @@  done:
     return ret;
 }
 
+static bool
+ip_in_lrp_networks(const struct nbrec_logical_router_port *lrp,
+                   const char *ip_s) {
+    struct lport_addresses lrp_networks;
+    extract_lrp_networks(lrp, &lrp_networks);
+
+    bool retval = find_lport_address(&lrp_networks, ip_s) ? true : false;
+    destroy_lport_addresses(&lrp_networks);
+    return retval;
+}
+
 static void
 nbctl_pre_lr_nat_add(struct ctl_context *ctx)
 {
@@ -4641,6 +4652,8 @@  nbctl_pre_lr_nat_add(struct ctl_context *ctx)
     ovsdb_idl_add_column(ctx->idl, &nbrec_nat_col_options);
 
     ovsdb_idl_add_column(ctx->idl, &nbrec_logical_router_port_col_name);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_logical_router_port_col_mac);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_logical_router_port_col_networks);
     ovsdb_idl_add_column(ctx->idl,
                          &nbrec_logical_router_port_col_gateway_chassis);
     ovsdb_idl_add_column(ctx->idl,
@@ -4783,6 +4796,8 @@  nbctl_lr_nat_add(struct ctl_context *ctx)
     const char *dgw_port_name = shash_find_data(&ctx->options,
                                                 "--gateway-port");
     const struct nbrec_logical_router_port *dgw_port = NULL;
+    size_t num_l3dgw_ports = 0;
+
     if (dgw_port_name) {
         error = lrp_by_name_or_uuid(ctx, dgw_port_name,
                                     true, &dgw_port);
@@ -4791,14 +4806,17 @@  nbctl_lr_nat_add(struct ctl_context *ctx)
             goto cleanup;
         }
 
-        bool nat_lr_port = false;
+        bool is_lr_port = false;
         for (size_t i = 0; i < lr->n_ports; i++) {
             const struct nbrec_logical_router_port *lrp = lr->ports[i];
+            if (lrp->ha_chassis_group || lrp->n_gateway_chassis) {
+                num_l3dgw_ports++;
+            }
             if (lrp == dgw_port) {
-                nat_lr_port = true;
+                is_lr_port = true;
             }
         }
-        if (!nat_lr_port) {
+        if (!is_lr_port) {
             ctl_error(ctx, "%s is not a router port of logical router: %s.",
                       dgw_port_name, ctx->argv[1]);
             goto cleanup;
@@ -4810,17 +4828,19 @@  nbctl_lr_nat_add(struct ctl_context *ctx)
             goto cleanup;
         }
     } else {
-        size_t num_l3dgw_ports = 0;
         for (size_t i = 0; i < lr->n_ports; i++) {
             const struct nbrec_logical_router_port *lrp = lr->ports[i];
             if (lrp->ha_chassis_group || lrp->n_gateway_chassis) {
                 num_l3dgw_ports++;
+                if (ip_in_lrp_networks(lrp, new_external_ip)) {
+                    dgw_port = lrp;
+                }
             }
         }
-        if (num_l3dgw_ports > 1) {
+        if (num_l3dgw_ports > 1 && !dgw_port) {
             ctl_error(ctx, "logical router: %s has multiple distributed "
-                      "gateway ports. NAT rule needs to specify "
-                      "gateway_port.", ctx->argv[1]);
+                      "gateway ports and gateway_port can not be determined "
+                      "from external IP of NAT rule.", ctx->argv[1]);
             goto cleanup;
         }
     }
@@ -4841,8 +4861,11 @@  nbctl_lr_nat_add(struct ctl_context *ctx)
             continue;
         }
 
-        if (!strcmp(nat_type, nat->type) &&
-            dgw_port == nat->gateway_port) {
+        if (!strcmp(nat_type, nat->type)
+            && (num_l3dgw_ports <= 1
+                || (nat->gateway_port && nat->gateway_port == dgw_port)
+                || (!nat->gateway_port
+                    && ip_in_lrp_networks(dgw_port, old_external_ip)))) {
             if (!strcmp(is_snat ? new_logical_ip : new_external_ip,
                         is_snat ? old_logical_ip : old_external_ip)) {
                 if (!strcmp(is_snat ? new_external_ip : new_logical_ip,
@@ -4854,20 +4877,18 @@  nbctl_lr_nat_add(struct ctl_context *ctx)
                             nbrec_nat_set_external_mac(nat, external_mac);
                             should_return = true;
                         } else {
-                            ctl_error(ctx, "%s, %s, %s: a NAT with this "
-                                      "external_ip, logical_ip and "
-                                      "gateway_port already exists",
-                                      new_external_ip, new_logical_ip,
-                                      dgw_port ? dgw_port->name : "");
+                            ctl_error(ctx, "%s, %s: a NAT with this "
+                                      "external_ip and logical_ip already "
+                                      "exists", new_external_ip,
+                                      new_logical_ip);
                             should_return = true;
                         }
                 } else {
                     ctl_error(ctx, "a NAT with this type (%s), %s (%s) "
-                              "and gateway_port (%s) already exists",
+                              "already exists",
                               nat_type,
                               is_snat ? "logical_ip" : "external_ip",
-                              is_snat ? new_logical_ip : new_external_ip,
-                              dgw_port ? dgw_port->name : "");
+                              is_snat ? new_logical_ip : new_external_ip);
                     should_return = true;
                 }
             }
@@ -4915,7 +4936,7 @@  nbctl_lr_nat_add(struct ctl_context *ctx)
         smap_add(&nat_options, "add_route", "true");
     }
 
-    if (dgw_port) {
+    if (dgw_port_name) {
         nbrec_nat_update_gateway_port_addvalue(nat, dgw_port);
     }
     nbrec_nat_set_options(nat, &nat_options);