diff --git a/ovn/lib/actions.c b/ovn/lib/actions.c
index 5f0bf19..4a486a0 100644
--- a/ovn/lib/actions.c
+++ b/ovn/lib/actions.c
@@ -442,6 +442,85 @@ emit_ct(struct action_context *ctx, bool recirc_next, bool commit)
     add_prerequisite(ctx, "ip");
 }
 
+static void
+parse_ct_nat(struct action_context *ctx, bool snat)
+{
+    const size_t ct_offset = ctx->ofpacts->size;
+    ofpbuf_pull(ctx->ofpacts, ct_offset);
+
+    struct ofpact_conntrack *ct = ofpact_put_CT(ctx->ofpacts);
+
+    if (ctx->ap->cur_ltable < ctx->ap->n_tables) {
+        ct->recirc_table = ctx->ap->first_ptable + ctx->ap->cur_ltable + 1;
+    } else {
+        action_error(ctx,
+                     "\"ct_[sd]nat\" action not allowed in last table.");
+        return;
+    }
+
+    if (snat) {
+        ct->zone_src.field = mf_from_id(MFF_LOG_SNAT_ZONE);
+    } else {
+        ct->zone_src.field = mf_from_id(MFF_LOG_DNAT_ZONE);
+    }
+    ct->zone_src.ofs = 0;
+    ct->zone_src.n_bits = 16;
+    ct->flags = 0;
+    ct->alg = 0;
+
+    add_prerequisite(ctx, "ip");
+
+    struct ofpact_nat *nat;
+    size_t nat_offset;
+    nat_offset = ctx->ofpacts->size;
+    ofpbuf_pull(ctx->ofpacts, nat_offset);
+
+    nat = ofpact_put_NAT(ctx->ofpacts);
+    nat->flags = 0;
+    nat->range_af = AF_UNSPEC;
+
+    int commit = 0;
+    if (lexer_match(ctx->lexer, LEX_T_LPAREN)) {
+        ovs_be32 ip;
+        if (ctx->lexer->token.type == LEX_T_INTEGER
+            && ctx->lexer->token.format == LEX_F_IPV4) {
+            ip = ctx->lexer->token.value.ipv4;
+        } else {
+            action_syntax_error(ctx, "invalid ip");
+            return;
+        }
+
+        nat->range_af = AF_INET;
+        nat->range.addr.ipv4.min = ip;
+        if (snat) {
+            nat->flags |= NX_NAT_F_SRC;
+        } else {
+            nat->flags |= NX_NAT_F_DST;
+        }
+        commit = NX_CT_F_COMMIT;
+        lexer_get(ctx->lexer);
+        if (!lexer_match(ctx->lexer, LEX_T_RPAREN)) {
+            action_syntax_error(ctx, "expecting `)'");
+            return;
+        }
+    }
+
+    ctx->ofpacts->header = ofpbuf_push_uninit(ctx->ofpacts, nat_offset);
+    ct = ctx->ofpacts->header;
+    ct->flags |= commit;
+
+    /* XXX: For performance reasons, we try to prevent additional
+     * recirculations.  So far, ct_snat which is used in a gateway router
+     * does not need a recirculation. ct_snat(IP) does need a recirculation.
+     * Should we consider a method to let the actions specify whether a action
+     * needs recirculation if there more use cases?. */
+    if (!commit && snat) {
+        ct->recirc_table = NX_CT_RECIRC_NONE;
+    }
+    ofpact_finish(ctx->ofpacts, &ct->ofpact);
+    ofpbuf_push_uninit(ctx->ofpacts, ct_offset);
+}
+
 static bool
 parse_action(struct action_context *ctx)
 {
@@ -469,6 +548,10 @@ parse_action(struct action_context *ctx)
         emit_ct(ctx, true, false);
     } else if (lexer_match_id(ctx->lexer, "ct_commit")) {
         emit_ct(ctx, false, true);
+    } else if (lexer_match_id(ctx->lexer, "ct_dnat")) {
+        parse_ct_nat(ctx, false);
+    } else if (lexer_match_id(ctx->lexer, "ct_snat")) {
+        parse_ct_nat(ctx, true);
     } else if (lexer_match_id(ctx->lexer, "arp")) {
         parse_arp_action(ctx);
     } else if (lexer_match_id(ctx->lexer, "get_arp")) {
diff --git a/ovn/northd/ovn-northd.8.xml b/ovn/northd/ovn-northd.8.xml
index 1983812..c237604 100644
--- a/ovn/northd/ovn-northd.8.xml
+++ b/ovn/northd/ovn-northd.8.xml
@@ -517,11 +517,40 @@ next;
 
       <li>
         <p>
-          Reply to ARP requests.  These flows reply to ARP requests for the
-          router's own IP address.  For each router port <var>P</var> that owns
-          IP address <var>A</var> and Ethernet address <var>E</var>, a
-          priority-90 flow matches <code>inport == <var>P</var> &amp;&amp;
-          arp.op == 1 &amp;&amp; arp.tpa == <var>A</var></code> (ARP request)
+          Reply to ARP requests.
+        </p>
+
+        <p>
+          These flows reply to ARP requests for the router's own IP address.
+          For each router port <var>P</var> that owns IP address <var>A</var>
+          and Ethernet address <var>E</var>, a priority-90 flow matches
+          <code>inport == <var>P</var> &amp;&amp; arp.op == 1 &amp;&amp;
+          arp.tpa == <var>A</var></code> (ARP request) with the following
+          actions:
+        </p>
+
+        <pre>
+eth.dst = eth.src;
+eth.src = <var>E</var>;
+arp.op = 2; /* ARP reply. */
+arp.tha = arp.sha;
+arp.sha = <var>E</var>;
+arp.tpa = arp.spa;
+arp.spa = <var>A</var>;
+outport = <var>P</var>;
+inport = ""; /* Allow sending out inport. */
+output;
+        </pre>
+      </li>
+
+      <li>
+        <p>
+          These flows reply to ARP requests for the virtual IP addresses
+          configured in the router for DNAT. For a configured DNAT IP address
+          <var>A</var>, for each router port <var>P</var> with Ethernet
+          address <var>E</var>, a priority-90 flow matches
+          <code>inport == <var>P</var> &amp;&amp; arp.op == 1 &amp;&amp;
+          arp.tpa == <var>A</var></code> (ARP request)
           with the following actions:
         </p>
 
@@ -663,7 +692,62 @@ icmp4 {
       </li>
     </ul>
 
-    <h3>Ingress Table 2: IP Routing</h3>
+    <h3>Ingress Table 2: UNSNAT</h3>
+
+    <p>
+      This is for already established connections' reverse traffic.
+      i.e., SNAT has already been done in egress pipeline and now the
+      packet has entered the ingress pipeline as part of a reply.  It is
+      unSNATted here.
+    </p>
+
+    <ul>
+      <li>
+        <p>
+          For each configuration in the OVN Northbound database, that asks
+          to change the source 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></code> with an action
+          <code>ct_snat; next;</code>.
+        </p>
+
+        <p>
+          A priority-0 logical flow with match <code>1</code> has actions
+          <code>next;</code>.
+        </p>
+      </li>
+    </ul>
+
+    <h3>Ingress Table 3: DNAT</h3>
+
+    <p>
+      Packets enter the pipeline with destination IP address that needs to
+      be DNATted from a virtual IP address to a real IP address.  Packets
+      in the reverse direction needs to be unDNATed.
+    </p>
+    <ul>
+      <li>
+        <p>
+          For each configuration in the OVN Northbound database, that asks
+          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>A</var></code> with an action <code>inport = "";
+          ct_dnat(<var>B</var>);</code>.
+        </p>
+
+        <p>
+          For all IP packets of a Gateway router, a priority-50 flow with an
+          action <code>inport = ""; ct_dnat;</code>.
+        </p>
+
+        <p>
+          A priority-0 logical flow with match <code>1</code> has actions
+          <code>next;</code>.
+        </p>
+      </li>
+    </ul>
+
+    <h3>Ingress Table 4: IP Routing</h3>
 
     <p>
       A packet that arrives at this table is an IP packet that should be routed
@@ -672,7 +756,7 @@ icmp4 {
       <code>ip4.dst</code>, the packet's final destination, unchanged) and
       advances to the next table for ARP resolution.  It also sets
       <code>reg1</code> to the IP address owned by the selected router port
-      (which is used later in table 4 as the IP source address for an ARP
+      (which is used later in table 6 as the IP source address for an ARP
       request, if needed).
     </p>
 
@@ -743,7 +827,7 @@ icmp4 {
       </li>
     </ul>
 
-    <h3>Ingress Table 3: ARP Resolution</h3>
+    <h3>Ingress Table 5: ARP Resolution</h3>
 
     <p>
       Any packet that reaches this table is an IP packet whose next-hop IP
@@ -798,7 +882,7 @@ icmp4 {
       </li>
     </ul>
 
-    <h3>Ingress Table 4: ARP Request</h3>
+    <h3>Ingress Table 6: ARP Request</h3>
 
     <p>
       In the common case where the Ethernet destination has been resolved, this
@@ -823,7 +907,7 @@ arp {
         </pre>
 
         <p>
-          (Ingress table 2 initialized <code>reg1</code> with the IP address
+          (Ingress table 4 initialized <code>reg1</code> with the IP address
           owned by <code>outport</code>.)
         </p>
 
@@ -838,7 +922,32 @@ arp {
       </li>
     </ul>
 
-    <h3>Egress Table 0: Delivery</h3>
+    <h3>Egress Table 0: SNAT</h3>
+
+    <p>
+      Packets that are configured to be SNATed get their source IP address
+      changed based on the configuration in the OVN Northbound database.
+    </p>
+    <ul>
+      <li>
+        <p>
+          For each configuration in the OVN Northbound database, that asks
+          to change the source IP address of a packet from an IP address of
+          <var>A</var> or to change the source IP address of a packet that
+          belongs to network <var>A</var> to <var>B</var>, a flow matches
+          <code>ip &amp;&amp; ip4.src == <var>A</var></code> with an action
+          <code>ct_snat(<var>B</var>);</code>.  The priority of the flow
+          is calculated based on the mask of <var>A</var>, with matches
+          having larger masks getting higher priorities.
+        </p>
+        <p>
+          A priority-0 logical flow with match <code>1</code> has actions
+          <code>next;</code>.
+        </p>
+      </li>
+    </ul>
+
+    <h3>Egress Table 1: Delivery</h3>
 
     <p>
       Packets that reach this table are ready for delivery.  It contains
diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index cac0148..4683780 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -105,12 +105,15 @@ enum ovn_stage {
     /* Logical router ingress stages. */                              \
     PIPELINE_STAGE(ROUTER, IN,  ADMISSION,   0, "lr_in_admission")    \
     PIPELINE_STAGE(ROUTER, IN,  IP_INPUT,    1, "lr_in_ip_input")     \
-    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,  2, "lr_in_ip_routing")   \
-    PIPELINE_STAGE(ROUTER, IN,  ARP_RESOLVE, 3, "lr_in_arp_resolve")  \
-    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST, 4, "lr_in_arp_request")  \
+    PIPELINE_STAGE(ROUTER, IN,  UNSNAT,      2, "lr_in_unsnat")       \
+    PIPELINE_STAGE(ROUTER, IN,  DNAT,        3, "lr_in_dnat")         \
+    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,  4, "lr_in_ip_routing")   \
+    PIPELINE_STAGE(ROUTER, IN,  ARP_RESOLVE, 5, "lr_in_arp_resolve")  \
+    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST, 6, "lr_in_arp_request")  \
                                                                       \
     /* Logical router egress stages. */                               \
-    PIPELINE_STAGE(ROUTER, OUT, DELIVERY,    0, "lr_out_delivery")
+    PIPELINE_STAGE(ROUTER, OUT, SNAT,      0, "lr_out_snat")          \
+    PIPELINE_STAGE(ROUTER, OUT, DELIVERY,  1, "lr_out_delivery")
 
 #define PIPELINE_STAGE(DP_TYPE, PIPELINE, STAGE, TABLE, NAME)   \
     S_##DP_TYPE##_##PIPELINE##_##STAGE                          \
@@ -1998,6 +2001,51 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
         free(match);
         free(actions);
 
+        /* ARP handling for external IP addresses.
+         *
+         * DNAT IP addresses are external IP addresses that need ARP
+         * handling. */
+        for (int i = 0; i < op->od->nbr->n_nat; i++) {
+            const struct nbrec_nat *nat;
+
+            nat = op->od->nbr->nat[i];
+
+            if(!strcmp(nat->type, "snat")) {
+                continue;
+            }
+
+            ovs_be32 ip;
+            if (!ip_parse(nat->external_ip, &ip) || !ip) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "bad ip address %s in dnat configuration "
+                             "for router %s", nat->external_ip, op->key);
+                continue;
+            }
+
+            match = xasprintf(
+                "inport == %s && arp.tpa == "IP_FMT" && arp.op == 1",
+                op->json_key, IP_ARGS(ip));
+            actions = xasprintf(
+                "eth.dst = eth.src; "
+                "eth.src = "ETH_ADDR_FMT"; "
+                "arp.op = 2; /* ARP reply */ "
+                "arp.tha = arp.sha; "
+                "arp.sha = "ETH_ADDR_FMT"; "
+                "arp.tpa = arp.spa; "
+                "arp.spa = "IP_FMT"; "
+                "outport = %s; "
+                "inport = \"\"; /* Allow sending out inport. */ "
+                "output;",
+                ETH_ADDR_ARGS(op->mac),
+                ETH_ADDR_ARGS(op->mac),
+                IP_ARGS(ip),
+                op->json_key);
+            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 90,
+                          match, actions);
+            free(match);
+            free(actions);
+        }
+
         /* Drop IP traffic to this router. */
         match = xasprintf("ip4.dst == "IP_FMT, IP_ARGS(op->ip));
         ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 60,
@@ -2005,6 +2053,135 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
         free(match);
     }
 
+    /* NAT in Gateway routers. */
+    HMAP_FOR_EACH (od, key_node, datapaths) {
+        if (!od->nbr) {
+            continue;
+        }
+
+        /* Packets are allowed by default. */
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_UNSNAT, 0, "1", "next;");
+        ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, 0, "1", "next;");
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_DNAT, 0, "1", "next;");
+
+        /* NAT rules are only valid on Gateway routers. */
+        if (!smap_get(&od->nbr->options, "chassis")) {
+            continue;
+        }
+
+        for (int i = 0; i < od->nbr->n_nat; i++) {
+            const struct nbrec_nat *nat;
+
+            nat = od->nbr->nat[i];
+
+            ovs_be32 ip, mask;
+
+            char *error = ip_parse_masked(nat->external_ip, &ip, &mask);
+            if (error || mask != OVS_BE32_MAX) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "bad external ip %s for nat",
+                             nat->external_ip);
+                free(error);
+                continue;
+            }
+
+            /* Check the validity of nat->logical_ip. 'logical_ip' can
+             * be a subnet when the type is "snat". */
+            error = ip_parse_masked(nat->logical_ip, &ip, &mask);
+            if (!strcmp(nat->type, "snat")) {
+                if (error) {
+                    static struct vlog_rate_limit rl =
+                        VLOG_RATE_LIMIT_INIT(5, 1);
+                    VLOG_WARN_RL(&rl, "bad ip network or ip %s for snat "
+                                 "in router "UUID_FMT"",
+                                 nat->logical_ip, UUID_ARGS(&od->key));
+                    free(error);
+                    continue;
+                }
+            } else {
+                if (error || mask != OVS_BE32_MAX) {
+                    static struct vlog_rate_limit rl =
+                        VLOG_RATE_LIMIT_INIT(5, 1);
+                    VLOG_WARN_RL(&rl, "bad ip %s for dnat in router "
+                        ""UUID_FMT"", nat->logical_ip, UUID_ARGS(&od->key));
+                    free(error);
+                    continue;
+                }
+            }
+
+
+            char *match, *actions;
+
+            /* Ingress UNSNAT table: It is for already established connections'
+             * reverse traffic. i.e., SNAT has already been done in egress
+             * pipeline and now the packet has entered the ingress pipeline as
+             * part of a reply. We undo the SNAT here.
+             *
+             * Undoing SNAT has to happen before DNAT processing.  This is
+             * because when the packet was DNATed in ingress pipeline, it did
+             * not know about the possibility of eventual additional SNAT in
+             * egress pipeline. */
+            if (!strcmp(nat->type, "snat")
+                || !strcmp(nat->type, "dnat_and_snat")) {
+                match = xasprintf("ip && ip4.dst == %s", nat->external_ip);
+                ovn_lflow_add(lflows, od, S_ROUTER_IN_UNSNAT, 100,
+                              match, "ct_snat; next;");
+                free(match);
+            }
+
+            /* Ingress DNAT table: Packets enter the pipeline with destination
+             * IP address that needs to be DNATted from a external IP address
+             * to a logical IP address. */
+            if (!strcmp(nat->type, "dnat")
+                || !strcmp(nat->type, "dnat_and_snat")) {
+                /* Packet when it goes from the initiator to destination.
+                 * We need to zero the inport because the router can
+                 * send the packet back through the same interface. */
+                match = xasprintf("ip && ip4.dst == %s", nat->external_ip);
+                actions = xasprintf("inport = \"\"; ct_dnat(%s);",
+                                    nat->logical_ip);
+                ovn_lflow_add(lflows, od, S_ROUTER_IN_DNAT, 100,
+                           match, actions);
+                free(match);
+                free(actions);
+            }
+
+            /* Egress SNAT table: Packets enter the egress pipeline with
+             * source ip address that needs to be SNATted to a external ip
+             * address. */
+            if (!strcmp(nat->type, "snat")
+                || !strcmp(nat->type, "dnat_and_snat")) {
+                match = xasprintf("ip && ip4.src == %s", nat->logical_ip);
+                actions = xasprintf("ct_snat(%s);", nat->external_ip);
+
+                /* The priority here is calculated such that the
+                 * nat->logical_ip with the longest mask gets a higher
+                 * priority. */
+                ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT,
+                              count_1bits(ntohl(mask)) + 1, match, actions);
+                free(match);
+                free(actions);
+            }
+        }
+
+        /* Re-circulate every packet through the DNAT zone.
+        * This helps with two things.
+        *
+        * 1. Any packet that needs to be unDNATed in the reverse
+        * direction gets unDNATed. Ideally this could be done in
+        * the egress pipeline. But since the gateway router
+        * does not have any feature that depends on the source
+        * ip address being external IP address for IP routing,
+        * we can do it here, saving a future re-circulation.
+        *
+        * 2. Any packet that was sent through SNAT zone in the
+        * previous table automatically gets re-circulated to get
+        * back the new destination IP address that is needed for
+        * routing in the openflow pipeline. */
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_DNAT, 50,
+                      "ip", "inport = \"\"; ct_dnat;");
+    }
+
     /* Logical router ingress table 2: IP Routing.
      *
      * A packet that arrives at this table is an IP packet that should be
@@ -2205,7 +2382,7 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
         ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_REQUEST, 0, "1", "output;");
     }
 
-    /* Logical router egress table 0: Delivery (priority 100).
+    /* Logical router egress table 1: Delivery (priority 100).
      *
      * Priority 100 rules deliver packets to enabled logical ports. */
     HMAP_FOR_EACH (op, key_node, ports) {
diff --git a/ovn/ovn-nb.ovsschema b/ovn/ovn-nb.ovsschema
index fa21b30..ac6ca14 100644
--- a/ovn/ovn-nb.ovsschema
+++ b/ovn/ovn-nb.ovsschema
@@ -1,7 +1,7 @@
 {
     "name": "OVN_Northbound",
-    "version": "2.1.2",
-    "cksum": "429668869 5325",
+    "version": "2.1.3",
+    "cksum": "3631923697 6121",
     "tables": {
         "Logical_Switch": {
             "columns": {
@@ -78,6 +78,11 @@
                                    "max": "unlimited"}},
                 "default_gw": {"type": {"key": "string", "min": 0, "max": 1}},
                 "enabled": {"type": {"key": "boolean", "min": 0, "max": 1}},
+                "nat": {"type": {"key": {"type": "uuid",
+                                         "refTable": "NAT",
+                                         "refType": "strong"},
+                                 "min": 0,
+                                 "max": "unlimited"}},
                 "options": {
                      "type": {"key": "string",
                               "value": "string",
@@ -104,6 +109,16 @@
                 "ip_prefix": {"type": "string"},
                 "nexthop": {"type": "string"},
                 "output_port": {"type": {"key": "string", "min": 0, "max": 1}}},
+            "isRoot": false},
+        "NAT": {
+            "columns": {
+                "external_ip": {"type": "string"},
+                "logical_ip": {"type": "string"},
+                "type": {"type": {"key": {"type": "string",
+                                           "enum": ["set", ["dnat",
+                                                             "snat",
+                                                             "dnat_and_snat"
+                                                               ]]}}}},
             "isRoot": false}
     }
 }
diff --git a/ovn/ovn-nb.xml b/ovn/ovn-nb.xml
index 41092f1..93ad305 100644
--- a/ovn/ovn-nb.xml
+++ b/ovn/ovn-nb.xml
@@ -631,18 +631,31 @@
       router has all ingress and egress traffic dropped.
     </column>
 
+    <column name="nat">
+      One or more NAT rules for the router. NAT rules only work on the
+      Gateway routers.
+    </column>
+
     <group title="Options">
       <p>
         Additional options for the logical router.
       </p>
 
       <column name="options" key="chassis">
-        If set, indicates that the logical router in question is
-        a Gateway router (which is centralized) and resides in the set
-        chassis.  The same value is also used by <code>ovn-controller</code>
-        to uniquely identify the chassis in the OVN deployment and
-        comes from <code>external_ids:system-id</code> in the
-        <code>Open_vSwitch</code> table of Open_vSwitch database.
+        <p>
+          If set, indicates that the logical router in question is a Gateway
+          router (which is centralized) and resides in the set chassis.  The
+          same value is also used by <code>ovn-controller</code> to
+          uniquely identify the chassis in the OVN deployment and
+          comes from <code>external_ids:system-id</code> in the
+          <code>Open_vSwitch</code> table of Open_vSwitch database.
+        </p>
+
+        <p>
+          The Gateway router can only be connected to a distributed router
+          via a switch if SNAT and DNAT are to be configured in the Gateway
+          router.
+        </p>
       </column>
     </group>
     
@@ -765,4 +778,44 @@
     </column>
   </table>
 
+  <table name="NAT" title="NAT rules for a Gateway router.">
+    <p>
+      Each record represents a NAT rule in a Gateway router.
+    </p>
+
+    <column name="type">
+      <p>Type of the NAT rule.</p>
+      <ul>
+        <li>
+          When <ref column="type"/> is <code>dnat</code>, the externally
+          visible IP address <ref column="external_ip"/> is DNATted to the IP
+          address <ref column="logical_ip"/> in the logical space.
+        </li>
+        <li>
+          When <ref column="type"/> is <code>snat</code>, IP packets
+          with their source IP address that either matches the IP address
+          in <ref column="logical_ip"/> or is in the network provided by
+          <ref column="logical_ip"/> is SNATed into the IP address in
+          <ref column="external_ip"/>.
+        </li>
+        <li>
+          When <ref column="type"/> is <code>dnat_and_snat</code>, the
+          externally visible IP address <ref column="external_ip"/> is
+          DNATted to the IP address <ref column="logical_ip"/> in the
+          logical space. In addition, IP packets with the source IP
+          address that matches <ref column="logical_ip"/> is SNATed into
+          the IP address in <ref column="external_ip"/>.
+        </li>
+      </ul>
+    </column>
+
+    <column name="external_ip">
+      An IPv4 address.
+    </column>
+
+    <column name="logical_ip">
+      An IPv4 network (e.g 192.168.1.0/24) or an IPv4 address.
+    </column>
+  </table>
+
 </database>
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index 1231b4e..5665871 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -951,6 +951,47 @@
           </p>
         </dd>
 
+        <dt><code>ct_dnat;</code></dt>
+        <dt><code>ct_dnat(<var>IP</var>);</code></dt>
+        <dd>
+          <p>
+            <code>ct_dnat</code> sends the packet through the DNAT zone in
+            connection tracking table to unDNAT any packet that was DNATed in
+            the opposite direction.  The packet is then automatically sent to
+            to the next tables as if followed by <code>next;</code> action.
+            The next tables will see the changes in the packet caused by
+            the connection tracker.
+          </p>
+          <p>
+            <code>ct_dnat(<var>IP</var>)</code> sends the packet through the
+            DNAT zone to change the destination IP address of the packet to
+            the one provided inside the parenthesis and commits the connection.
+            The packet is then automatically sent to the next tables as if
+            followed by <code>next;</code> action.  The next tables will see
+            the changes in the packet caused by the connection tracker.
+          </p>
+        </dd>
+
+        <dt><code>ct_snat;</code></dt>
+        <dt><code>ct_snat(<var>IP</var>);</code></dt>
+        <dd>
+          <p>
+            <code>ct_snat</code> sends the packet through the SNAT zone to
+            unSNAT any packet that was SNATed in the opposite direction.  If
+            the packet needs to be sent to the next tables, then it should be
+            followed by a <code>next;</code> action.  The next tables will not
+            see the changes in the packet caused by the connection tracker.
+          </p>
+          <p>
+            <code>ct_snat(<var>IP</var>)</code> sends the packet through the
+            SNAT zone to change the source IP address of the packet to
+            the one provided inside the parenthesis and commits the connection.
+            The packet is then automatically sent to the next tables as if
+            followed by <code>next;</code> action.  The next tables will see the
+            changes in the packet caused by the connection tracker.
+          </p>
+        </dd>
+
         <dt><code>arp { <var>action</var>; </code>...<code> };</code></dt>
         <dd>
           <p>
diff --git a/ovn/utilities/ovn-nbctl.c b/ovn/utilities/ovn-nbctl.c
index 321040e..b821307 100644
--- a/ovn/utilities/ovn-nbctl.c
+++ b/ovn/utilities/ovn-nbctl.c
@@ -1449,6 +1449,11 @@ static const struct ctl_table_class tables[] = {
        NULL},
       {NULL, NULL, NULL}}},
 
+    {&nbrec_table_nat,
+     {{&nbrec_table_nat, NULL,
+       NULL},
+      {NULL, NULL, NULL}}},
+
     {NULL, {{NULL, NULL, NULL}, {NULL, NULL, NULL}}}
 };
 
diff --git a/tests/ovn.at b/tests/ovn.at
index 633cf35..19d5c73 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -507,6 +507,23 @@ ip.ttl => Syntax error at end of input expecting `--'.
 ct_next; => actions=ct(table=27,zone=NXM_NX_REG5[0..15]), prereqs=ip
 ct_commit; => actions=ct(commit,zone=NXM_NX_REG5[0..15]), prereqs=ip
 
+# dnat
+ct_dnat; => actions=ct(table=27,zone=NXM_NX_REG3[0..15],nat), prereqs=ip
+ct_dnat(192.168.1.2); => actions=ct(commit,table=27,zone=NXM_NX_REG3[0..15],nat(dst=192.168.1.2)), prereqs=ip
+ct_dnat(192.168.1.2, 192.168.1.3); => Syntax error at `,' expecting `)'.
+ct_dnat(foo); => Syntax error at `foo' invalid ip.
+ct_dnat(foo, bar); => Syntax error at `foo' invalid ip.
+ct_dnat(); => Syntax error at `)' invalid ip.
+
+# snat
+ct_snat; => actions=ct(zone=NXM_NX_REG4[0..15],nat), prereqs=ip
+ct_snat(192.168.1.2); => actions=ct(commit,table=27,zone=NXM_NX_REG4[0..15],nat(src=192.168.1.2)), prereqs=ip
+ct_snat(192.168.1.2, 192.168.1.3); => Syntax error at `,' expecting `)'.
+ct_snat(foo); => Syntax error at `foo' invalid ip.
+ct_snat(foo, bar); => Syntax error at `foo' invalid ip.
+ct_snat(); => Syntax error at `)' invalid ip.
+
+
 # arp
 arp { eth.dst = ff:ff:ff:ff:ff:ff; output; }; => actions=controller(userdata=00.00.00.00.00.00.00.00.00.19.00.10.80.00.06.06.ff.ff.ff.ff.ff.ff.00.00.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.40.00.00.00), prereqs=ip4
 
