diff --git a/datapath/linux/compat/include/linux/openvswitch.h b/datapath/linux/compat/include/linux/openvswitch.h
index dae2e5b..2c5b87d 100644
--- a/datapath/linux/compat/include/linux/openvswitch.h
+++ b/datapath/linux/compat/include/linux/openvswitch.h
@@ -686,12 +686,49 @@ enum ovs_ct_attr {
 	OVS_CT_ATTR_LABELS,     /* label to associate with this connection. */
 	OVS_CT_ATTR_HELPER,     /* netlink helper to assist detection of
 				   related connections. */
+	OVS_CT_ATTR_NAT,        /* Nested OVS_NAT_ATTR_* */
 	__OVS_CT_ATTR_MAX
 };
 
 #define OVS_CT_ATTR_MAX (__OVS_CT_ATTR_MAX - 1)
 
 /**
+ * enum ovs_nat_attr - Attributes for %OVS_CT_ATTR_NAT.
+ * @OVS_NAT_ATTR_SRC: Flag for Source NAT (mangle source address/port).
+ * @OVS_NAT_ATTR_DST: Flag for Destination NAT (mangle destination
+ * address/port).  Only one of (@OVS_NAT_ATTR_SRC, @OVS_NAT_ATTR_DST) may be
+ * specified.  Effective only for packets for ct_state NEW connections.
+ * Committed connections are mangled by the NAT action according to the
+ * committed NAT type regardless of the flags specified.  As a corollary, a NAT
+ * action without a NAT type flag will only mangle packets of committed
+ * connections.  The following NAT attributes only apply for NEW connections,
+ * and they may be included only when the CT action has the @OVS_CT_ATTR_COMMIT
+ * flag and either @OVS_NAT_ATTR_SRC, @OVS_NAT_ATTR_DST is also included.
+ * @OVS_NAT_ATTR_IP_MIN: struct in_addr or struct in6_addr
+ * @OVS_NAT_ATTR_IP_MAX: struct in_addr or struct in6_addr
+ * @OVS_NAT_ATTR_PROTO_MIN: u16 L4 protocol specific lower boundary (port)
+ * @OVS_NAT_ATTR_PROTO_MAX: u16 L4 protocol specific upper boundary (port)
+ * @OVS_NAT_ATTR_PERSISTENT: Flag for persistent IP mapping across reboots
+ * @OVS_NAT_ATTR_PROTO_HASH: Flag for pseudo random L4 port mapping (MD5)
+ * @OVS_NAT_ATTR_PROTO_RANDOM: Flag for fully randomized L4 port mapping
+ */
+enum ovs_nat_attr {
+	OVS_NAT_ATTR_UNSPEC,
+	OVS_NAT_ATTR_SRC,
+	OVS_NAT_ATTR_DST,
+	OVS_NAT_ATTR_IP_MIN,
+	OVS_NAT_ATTR_IP_MAX,
+	OVS_NAT_ATTR_PROTO_MIN,
+	OVS_NAT_ATTR_PROTO_MAX,
+	OVS_NAT_ATTR_PERSISTENT,
+	OVS_NAT_ATTR_PROTO_HASH,
+	OVS_NAT_ATTR_PROTO_RANDOM,
+	__OVS_NAT_ATTR_MAX,
+};
+
+#define OVS_NAT_ATTR_MAX (__OVS_NAT_ATTR_MAX - 1)
+
+/**
  * enum ovs_action_attr - Action types.
  *
  * @OVS_ACTION_ATTR_OUTPUT: Output packet to port.
diff --git a/lib/odp-util.c b/lib/odp-util.c
index 8f0f39a..185dd5c 100644
--- a/lib/odp-util.c
+++ b/lib/odp-util.c
@@ -542,6 +542,150 @@ format_odp_tnl_push_action(struct ds *ds, const struct nlattr *attr)
     ds_put_format(ds, ",out_port(%"PRIu32"))", data->out_port);
 }
 
+static const struct nl_policy ovs_nat_policy[] = {
+    [OVS_NAT_ATTR_SRC] = { .type = NL_A_FLAG, .optional = true, },
+    [OVS_NAT_ATTR_DST] = { .type = NL_A_FLAG, .optional = true, },
+    [OVS_NAT_ATTR_IP_MIN] = { .type = NL_A_UNSPEC, .optional = true,
+                              .min_len = sizeof(struct in_addr),
+                              .max_len = sizeof(struct in6_addr)},
+    [OVS_NAT_ATTR_IP_MAX] = { .type = NL_A_UNSPEC, .optional = true,
+                              .min_len = sizeof(struct in_addr),
+                              .max_len = sizeof(struct in6_addr)},
+    [OVS_NAT_ATTR_PROTO_MIN] = { .type = NL_A_U16, .optional = true, },
+    [OVS_NAT_ATTR_PROTO_MAX] = { .type = NL_A_U16, .optional = true, },
+    [OVS_NAT_ATTR_PERSISTENT] = { .type = NL_A_FLAG, .optional = true, },
+    [OVS_NAT_ATTR_PROTO_HASH] = { .type = NL_A_FLAG, .optional = true, },
+    [OVS_NAT_ATTR_PROTO_RANDOM] = { .type = NL_A_FLAG, .optional = true, },
+};
+
+static void
+format_odp_ct_nat(struct ds *ds, const struct nlattr *attr)
+{
+    struct nlattr *a[ARRAY_SIZE(ovs_nat_policy)];
+    size_t addr_len;
+    ovs_be32 ip_min, ip_max;
+    struct in6_addr ip6_min, ip6_max;
+    ovs_be16 proto_min, proto_max;
+
+    if (!nl_parse_nested(attr, ovs_nat_policy, a, ARRAY_SIZE(a))) {
+        ds_put_cstr(ds, "nat(error: nl_parse_nested() failed.)");
+        return;
+    }
+    /* If no type, then nothing else either. */
+    if (!(a[OVS_NAT_ATTR_SRC] || a[OVS_NAT_ATTR_DST])
+        && (a[OVS_NAT_ATTR_IP_MIN] || a[OVS_NAT_ATTR_IP_MAX]
+            || a[OVS_NAT_ATTR_PROTO_MIN] || a[OVS_NAT_ATTR_PROTO_MAX]
+            || a[OVS_NAT_ATTR_PERSISTENT] || a[OVS_NAT_ATTR_PROTO_HASH]
+            || a[OVS_NAT_ATTR_PROTO_RANDOM])) {
+        ds_put_cstr(ds, "nat(error: options allowed only with \"src\" or \"dst\")");
+        return;
+    }
+    /* Both SNAT & DNAT may not be specified. */
+    if (a[OVS_NAT_ATTR_SRC] && a[OVS_NAT_ATTR_DST]) {
+        ds_put_cstr(ds, "nat(error: Only one of \"src\" or \"dst\" may be present.)");
+        return;
+    }
+    /* proto may not appear without ip. */
+    if (!a[OVS_NAT_ATTR_IP_MIN] && a[OVS_NAT_ATTR_PROTO_MIN]) {
+        ds_put_cstr(ds, "nat(error: proto but no IP.)");
+        return;
+    }
+    /* MAX may not appear without MIN. */
+    if ((!a[OVS_NAT_ATTR_IP_MIN] && a[OVS_NAT_ATTR_IP_MAX])
+        || (!a[OVS_NAT_ATTR_PROTO_MIN] && a[OVS_NAT_ATTR_PROTO_MAX])) {
+        ds_put_cstr(ds, "nat(error: range max without min.)");
+        return;
+    }
+    /* Address sizes must match. */
+    if ((a[OVS_NAT_ATTR_IP_MIN]
+         && (nl_attr_get_size(a[OVS_NAT_ATTR_IP_MIN]) != sizeof(ovs_be32) &&
+             nl_attr_get_size(a[OVS_NAT_ATTR_IP_MIN]) != sizeof(struct in6_addr)))
+        || (a[OVS_NAT_ATTR_IP_MIN] && a[OVS_NAT_ATTR_IP_MAX]
+            && (nl_attr_get_size(a[OVS_NAT_ATTR_IP_MIN])
+                != nl_attr_get_size(a[OVS_NAT_ATTR_IP_MAX])))) {
+        ds_put_cstr(ds, "nat(error: IP address sizes do not match)");
+        return;
+    }
+
+    addr_len = a[OVS_NAT_ATTR_IP_MIN]
+        ? nl_attr_get_size(a[OVS_NAT_ATTR_IP_MIN]) : 0;
+    if (addr_len == sizeof(ovs_be32)) {
+        ip_min = a[OVS_NAT_ATTR_IP_MIN]
+            ? nl_attr_get_be32(a[OVS_NAT_ATTR_IP_MIN]) : 0;
+        ip_max = a[OVS_NAT_ATTR_IP_MAX]
+            ? nl_attr_get_be32(a[OVS_NAT_ATTR_IP_MAX]) : 0;
+    } else {
+        memcpy(&ip6_min, a[OVS_NAT_ATTR_IP_MIN]
+               ? nl_attr_get(a[OVS_NAT_ATTR_IP_MIN]) : &in6addr_any,
+               sizeof ip6_min);
+        memcpy(&ip6_max, a[OVS_NAT_ATTR_IP_MAX]
+               ? nl_attr_get(a[OVS_NAT_ATTR_IP_MAX]) : &in6addr_any,
+               sizeof ip6_max);
+    }
+    proto_min = a[OVS_NAT_ATTR_PROTO_MIN]
+        ? nl_attr_get_be16(a[OVS_NAT_ATTR_PROTO_MIN]) : 0;
+    proto_max = a[OVS_NAT_ATTR_PROTO_MAX]
+        ? nl_attr_get_be16(a[OVS_NAT_ATTR_PROTO_MAX]) : 0;
+
+    if ((addr_len == sizeof(ovs_be32)
+         && ip_max && ntohl(ip_min) > ntohl(ip_max))
+        || (addr_len == sizeof(struct in6_addr)
+            && memcmp(&ip6_min, &ip_max, sizeof ip6_min) > 0)
+        || (proto_max && proto_min > proto_max)) {
+        ds_put_cstr(ds, "nat(range error)");
+        return;
+    }
+
+    ds_put_cstr(ds, "nat");
+    if (a[OVS_NAT_ATTR_SRC] || a[OVS_NAT_ATTR_DST]) {
+        ds_put_char(ds, '(');
+        if (a[OVS_NAT_ATTR_SRC]) {
+            ds_put_cstr(ds, "src");
+        } else if (a[OVS_NAT_ATTR_DST]) {
+            ds_put_cstr(ds, "dst");
+        }
+
+        if (addr_len > 0) {
+            ds_put_cstr(ds, "=");
+
+            if (addr_len == sizeof ip_min) {
+                ds_put_format(ds, IP_FMT, IP_ARGS(ip_min));
+
+                if (ip_max && ip_max != ip_min) {
+                    ds_put_format(ds, "-"IP_FMT, IP_ARGS(ip_max));
+                }
+            } else if (addr_len == sizeof ip6_min) {
+                print_ipv6_addr(ds, &ip6_min);
+
+                if (ipv6_mask_is_any(&ip6_max) &&
+                    memcmp(&ip6_max, &ip6_min, sizeof ip6_max) != 0) {
+                    ds_put_char(ds, '-');
+                    print_ipv6_addr(ds, &ip6_max);
+                }
+            }
+            if (proto_min) {
+                ds_put_format(ds, ":%"PRIu16, proto_min);
+
+                if (proto_max && proto_max != proto_min) {
+                    ds_put_format(ds, "-%"PRIu16, proto_max);
+                }
+            }
+        }
+        ds_put_char(ds, ',');
+        if (a[OVS_NAT_ATTR_PERSISTENT]) {
+            ds_put_cstr(ds, "persistent,");
+        }
+        if (a[OVS_NAT_ATTR_PROTO_HASH]) {
+            ds_put_cstr(ds, "hash,");
+        }
+        if (a[OVS_NAT_ATTR_PROTO_RANDOM]) {
+            ds_put_cstr(ds, "random,");
+        }
+        ds_chomp(ds, ',');
+        ds_put_char(ds, ')');
+    }
+}
+
 static const struct nl_policy ovs_conntrack_policy[] = {
     [OVS_CT_ATTR_COMMIT] = { .type = NL_A_FLAG, .optional = true, },
     [OVS_CT_ATTR_ZONE] = { .type = NL_A_U16, .optional = true, },
@@ -551,6 +695,8 @@ static const struct nl_policy ovs_conntrack_policy[] = {
                              .min_len = sizeof(struct ovs_key_ct_labels) * 2 },
     [OVS_CT_ATTR_HELPER] = { .type = NL_A_STRING, .optional = true,
                              .min_len = 1, .max_len = 16 },
+    [OVS_CT_ATTR_NAT] = { .type = NL_A_UNSPEC, .optional = true,
+                          .min_len = 0, .max_len = 96 },
 };
 
 static void
@@ -562,6 +708,7 @@ format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
     const char *helper;
     uint16_t zone;
     bool commit;
+    const struct nlattr *nat;
 
     if (!nl_parse_nested(attr, ovs_conntrack_policy, a, ARRAY_SIZE(a))) {
         ds_put_cstr(ds, "ct(error)");
@@ -573,9 +720,10 @@ format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
     mark = a[OVS_CT_ATTR_MARK] ? nl_attr_get(a[OVS_CT_ATTR_MARK]) : NULL;
     label = a[OVS_CT_ATTR_LABELS] ? nl_attr_get(a[OVS_CT_ATTR_LABELS]): NULL;
     helper = a[OVS_CT_ATTR_HELPER] ? nl_attr_get(a[OVS_CT_ATTR_HELPER]) : NULL;
+    nat = a[OVS_CT_ATTR_NAT];
 
     ds_put_format(ds, "ct");
-    if (commit || zone || mark || label || helper) {
+    if (commit || zone || mark || label || helper || nat) {
         ds_put_cstr(ds, "(");
         if (commit) {
             ds_put_format(ds, "commit,");
@@ -595,6 +743,9 @@ format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
         if (helper) {
             ds_put_format(ds, "helper=%s,", helper);
         }
+        if (nat) {
+            format_odp_ct_nat(ds, nat);
+        }
         ds_chomp(ds, ',');
         ds_put_cstr(ds, ")");
     }
@@ -876,15 +1027,15 @@ ovs_parse_tnl_push(const char *s, struct ovs_action_push_tnl *data)
     l4 = ((uint8_t *) l3 + sizeof (struct ip_header));
     ip = (struct ip_header *) l3;
     if (!ovs_scan_len(s, &n, "header(size=%"SCNi32",type=%"SCNi32","
-                         "eth(dst="ETH_ADDR_SCAN_FMT",",
-                         &data->header_len,
-                         &data->tnl_type,
-                         ETH_ADDR_SCAN_ARGS(eth->eth_dst))) {
+                      "eth(dst="ETH_ADDR_SCAN_FMT",",
+                      &data->header_len,
+                      &data->tnl_type,
+                      ETH_ADDR_SCAN_ARGS(eth->eth_dst))) {
         return -EINVAL;
     }
 
     if (!ovs_scan_len(s, &n, "src="ETH_ADDR_SCAN_FMT",",
-                  ETH_ADDR_SCAN_ARGS(eth->eth_src))) {
+                      ETH_ADDR_SCAN_ARGS(eth->eth_src))) {
         return -EINVAL;
     }
     if (!ovs_scan_len(s, &n, "dl_type=0x%"SCNx16"),", &dl_type)) {
@@ -894,11 +1045,11 @@ ovs_parse_tnl_push(const char *s, struct ovs_action_push_tnl *data)
 
     /* IPv4 */
     if (!ovs_scan_len(s, &n, "ipv4(src="IP_SCAN_FMT",dst="IP_SCAN_FMT",proto=%"SCNi8
-                         ",tos=%"SCNi8",ttl=%"SCNi8",frag=0x%"SCNx16"),",
-                         IP_SCAN_ARGS(&sip),
-                         IP_SCAN_ARGS(&dip),
-                         &ip->ip_proto, &ip->ip_tos,
-                         &ip->ip_ttl, &ip->ip_frag_off)) {
+                      ",tos=%"SCNi8",ttl=%"SCNi8",frag=0x%"SCNx16"),",
+                      IP_SCAN_ARGS(&sip),
+                      IP_SCAN_ARGS(&dip),
+                      &ip->ip_proto, &ip->ip_tos,
+                      &ip->ip_ttl, &ip->ip_frag_off)) {
         return -EINVAL;
     }
     put_16aligned_be32(&ip->ip_src, sip);
@@ -908,7 +1059,7 @@ ovs_parse_tnl_push(const char *s, struct ovs_action_push_tnl *data)
     udp = (struct udp_header *) l4;
     greh = (struct gre_base_hdr *) l4;
     if (ovs_scan_len(s, &n, "udp(src=%"SCNi16",dst=%"SCNi16",csum=0x%"SCNx16"),",
-                         &udp_src, &udp_dst, &csum)) {
+                     &udp_src, &udp_dst, &csum)) {
         uint32_t vx_flags, vni;
 
         udp->udp_src = htons(udp_src);
@@ -917,7 +1068,7 @@ ovs_parse_tnl_push(const char *s, struct ovs_action_push_tnl *data)
         udp->udp_csum = htons(csum);
 
         if (ovs_scan_len(s, &n, "vxlan(flags=0x%"SCNx32",vni=0x%"SCNx32"))",
-                            &vx_flags, &vni)) {
+                         &vx_flags, &vni)) {
             struct vxlanhdr *vxh = (struct vxlanhdr *) (udp + 1);
 
             put_16aligned_be32(&vxh->vx_flags, htonl(vx_flags));
@@ -968,7 +1119,7 @@ ovs_parse_tnl_push(const char *s, struct ovs_action_push_tnl *data)
             return -EINVAL;
         }
     } else if (ovs_scan_len(s, &n, "gre((flags=0x%"SCNx16",proto=0x%"SCNx16")",
-                         &gre_flags, &gre_proto)){
+                            &gre_flags, &gre_proto)){
 
         tnl_type = OVS_VPORT_TYPE_GRE;
         greh->flags = htons(gre_flags);
@@ -1030,6 +1181,181 @@ ovs_parse_tnl_push(const char *s, struct ovs_action_push_tnl *data)
     return n;
 }
 
+struct ct_nat_params {
+    bool snat;
+    bool dnat;
+    size_t addr_len;
+    union {
+        ovs_be32 ip;
+        struct in6_addr ip6;
+    } addr_min;
+    union {
+        ovs_be32 ip;
+        struct in6_addr ip6;
+    } addr_max;
+    uint16_t proto_min;
+    uint16_t proto_max;
+    bool persistent;
+    bool proto_hash;
+    bool proto_random;
+};
+
+static int
+scan_ct_nat_range(const char *s, int *n, struct ct_nat_params *p)
+{
+    if (ovs_scan_len(s, n, "=")) {
+        char ipv6_s[IPV6_SCAN_LEN + 1];
+        struct in6_addr ipv6;
+
+        if (ovs_scan_len(s, n, IP_SCAN_FMT, IP_SCAN_ARGS(&p->addr_min.ip))) {
+            p->addr_len = sizeof p->addr_min.ip;
+            if (ovs_scan_len(s, n, "-")) {
+                if (!ovs_scan_len(s, n, IP_SCAN_FMT,
+                                  IP_SCAN_ARGS(&p->addr_max.ip))) {
+                    return -EINVAL;
+                }
+            }
+            if (ovs_scan_len(s, n, ":%"SCNu16, &p->proto_min)) {
+                if (ovs_scan_len(s, n, "-")) {
+                    if (!ovs_scan_len(s, n, "%"SCNu16, &p->proto_max)) {
+                        return -EINVAL;
+                    }
+                }
+            }
+        } else if (ovs_scan_len(s, n, IPV6_SCAN_FMT, ipv6_s) &&
+                   inet_pton(AF_INET6, ipv6_s, &ipv6) == 1) {
+            p->addr_len = sizeof p->addr_min.ip6;
+            p->addr_min.ip6 = ipv6;
+            if (ovs_scan_len(s, n, "-")) {
+                if (ovs_scan_len(s, n, IPV6_SCAN_FMT, ipv6_s) &&
+                    inet_pton(AF_INET6, ipv6_s, &ipv6) == 1) {
+                    p->addr_max.ip6 = ipv6;
+                } else {
+                    return -EINVAL;
+                }
+            }
+            if (ovs_scan_len(s, n, "+%"SCNu16, &p->proto_min)) {
+                if (ovs_scan_len(s, n, "-")) {
+                    if (!ovs_scan_len(s, n, "%"SCNu16, &p->proto_max)) {
+                        return -EINVAL;
+                    }
+                }
+            }
+        } else {
+            return -EINVAL;
+        }
+    }
+    return 0;
+}
+
+static int
+scan_ct_nat(const char *s, struct ct_nat_params *p)
+{
+    int n = 0;
+
+    if (ovs_scan_len(s, &n, "nat")) {
+        memset(p, 0, sizeof *p);
+
+        if (ovs_scan_len(s, &n, "(")) {
+            char *end;
+            int end_n;
+
+            end = strchr(s + n, ')');
+            if (!end) {
+                return -EINVAL;
+            }
+            end_n = end - s;
+
+            while (n < end_n) {
+                n += strspn(s + n, delimiters);
+                if (ovs_scan_len(s, &n, "src")) {
+                    int err = scan_ct_nat_range(s, &n, p);
+                    if (err) {
+                        return err;
+                    }
+                    p->snat = true;
+                    continue;
+                }
+                if (ovs_scan_len(s, &n, "dst")) {
+                    int err = scan_ct_nat_range(s, &n, p);
+                    if (err) {
+                        return err;
+                    }
+                    p->dnat = true;
+                    continue;
+                }
+                if (ovs_scan_len(s, &n, "persistent")) {
+                    p->persistent = true;
+                    continue;
+                }
+                if (ovs_scan_len(s, &n, "hash")) {
+                    p->proto_hash = true;
+                    continue;
+                }
+                if (ovs_scan_len(s, &n, "random")) {
+                    p->proto_random = true;
+                    continue;
+                }
+                return -EINVAL;
+            }
+
+            if (p->snat && p->dnat) {
+                return -EINVAL;
+            }
+            if ((p->addr_len != 0 &&
+                 memcmp(&p->addr_max, &in6addr_any, p->addr_len) &&
+                 memcmp(&p->addr_max, &p->addr_min, p->addr_len) < 0) ||
+                (p->proto_max && p->proto_max < p->proto_min)) {
+                return -EINVAL;
+            }
+            if (p->proto_hash && p->proto_random) {
+                return -EINVAL;
+            }
+            n++;
+        }
+    }
+    return n;
+}
+
+static void
+nl_msg_put_ct_nat(struct ct_nat_params *p, struct ofpbuf *actions)
+{
+    size_t start = nl_msg_start_nested(actions, OVS_CT_ATTR_NAT);
+
+    if (p->snat) {
+        nl_msg_put_flag(actions, OVS_NAT_ATTR_SRC);
+    } else if (p->dnat) {
+        nl_msg_put_flag(actions, OVS_NAT_ATTR_DST);
+    } else {
+        goto out;
+    }
+    if (p->addr_len != 0) {
+        nl_msg_put_unspec(actions, OVS_NAT_ATTR_IP_MIN, &p->addr_min,
+                          p->addr_len);
+        if (memcmp(&p->addr_max, &p->addr_min, p->addr_len) > 0) {
+            nl_msg_put_unspec(actions, OVS_NAT_ATTR_IP_MAX, &p->addr_max,
+                              p->addr_len);
+        }
+        if (p->proto_min) {
+            nl_msg_put_u16(actions, OVS_NAT_ATTR_PROTO_MIN, p->proto_min);
+            if (p->proto_max && p->proto_max > p->proto_min) {
+                nl_msg_put_u16(actions, OVS_NAT_ATTR_PROTO_MAX, p->proto_max);
+            }
+        }
+        if (p->persistent) {
+            nl_msg_put_flag(actions, OVS_NAT_ATTR_PERSISTENT);
+        }
+        if (p->proto_hash) {
+            nl_msg_put_flag(actions, OVS_NAT_ATTR_PROTO_HASH);
+        }
+        if (p->proto_random) {
+            nl_msg_put_flag(actions, OVS_NAT_ATTR_PROTO_RANDOM);
+        }
+    }
+out:
+    nl_msg_end_nested(actions, start);
+}
+
 static int
 parse_conntrack_action(const char *s_, struct ofpbuf *actions)
 {
@@ -1048,6 +1374,8 @@ parse_conntrack_action(const char *s_, struct ofpbuf *actions)
             ovs_u128 value;
             ovs_u128 mask;
         } ct_label;
+        struct ct_nat_params nat_params;
+        bool have_nat = false;
         size_t start;
         char *end;
 
@@ -1056,13 +1384,14 @@ parse_conntrack_action(const char *s_, struct ofpbuf *actions)
         s += 2;
         if (ovs_scan(s, "(")) {
             s++;
+find_end:
             end = strchr(s, ')');
             if (!end) {
                 return -EINVAL;
             }
 
             while (s != end) {
-                int n = -1;
+                int n;
 
                 s += strspn(s, delimiters);
                 if (ovs_scan(s, "commit%n", &n)) {
@@ -1106,6 +1435,16 @@ parse_conntrack_action(const char *s_, struct ofpbuf *actions)
                     continue;
                 }
 
+                n = scan_ct_nat(s, &nat_params);
+                if (n > 0) {
+                    s += n;
+                    have_nat = true;
+
+                    /* end points to the end of the nested, nat action.
+                     * find the real end. */
+                    goto find_end;
+                }
+                /* Nothing matched. */
                 return -EINVAL;
             }
             s++;
@@ -1130,6 +1469,9 @@ parse_conntrack_action(const char *s_, struct ofpbuf *actions)
             nl_msg_put_string__(actions, OVS_CT_ATTR_HELPER, helper,
                                 helper_len);
         }
+        if (have_nat) {
+            nl_msg_put_ct_nat(&nat_params, actions);
+        }
         nl_msg_end_nested(actions, start);
     }
 
diff --git a/lib/ofp-actions.c b/lib/ofp-actions.c
index 5f72fda..49caa3d 100644
--- a/lib/ofp-actions.c
+++ b/lib/ofp-actions.c
@@ -28,6 +28,7 @@
 #include "meta-flow.h"
 #include "multipath.h"
 #include "nx-match.h"
+#include "odp-netlink.h"
 #include "ofp-parse.h"
 #include "ofp-util.h"
 #include "ofpbuf.h"
@@ -291,6 +292,9 @@ enum ofp_raw_action_type {
     /* NX1.0+(35): struct nx_action_conntrack, ... */
     NXAST_RAW_CT,
 
+    /* NX1.0+(36): struct nx_action_nat, ... */
+    NXAST_RAW_NAT,
+
 /* ## ------------------ ## */
 /* ## Debugging actions. ## */
 /* ## ------------------ ## */
@@ -4316,21 +4320,12 @@ encode_NOTE(const struct ofpact_note *note,
 {
     size_t start_ofs = out->size;
     struct nx_action_note *nan;
-    unsigned int remainder;
-    unsigned int len;
 
     put_NXAST_NOTE(out);
     out->size = out->size - sizeof nan->note;
 
     ofpbuf_put(out, note->data, note->length);
-
-    len = out->size - start_ofs;
-    remainder = len % OFP_ACTION_ALIGN;
-    if (remainder) {
-        ofpbuf_put_zeros(out, OFP_ACTION_ALIGN - remainder);
-    }
-    nan = ofpbuf_at(out, start_ofs, sizeof *nan);
-    nan->len = htons(out->size - start_ofs);
+    pad_ofpat(out, start_ofs);
 }
 
 static char * OVS_WARN_UNUSED_RESULT
@@ -4773,9 +4768,18 @@ decode_NXAST_RAW_CT(const struct nx_action_conntrack *nac,
 
     if (conntrack->ofpact.len > sizeof(*conntrack)
         && !(conntrack->flags & NX_CT_F_COMMIT)) {
-        VLOG_WARN_RL(&rl, "CT action requires commit flag if actions are "
-                     "specified.");
-        error = OFPERR_OFPBAC_BAD_ARGUMENT;
+        const struct ofpact *a;
+        size_t ofpacts_len = conntrack->ofpact.len - sizeof(*conntrack);
+
+        OFPACT_FOR_EACH (a, conntrack->actions, ofpacts_len) {
+            if (a->type != OFPACT_NAT || ((struct ofpact_nat *)a)->flags
+                || ((struct ofpact_nat *)a)->range_af != AF_UNSPEC) {
+                VLOG_WARN_RL(&rl, "CT action requires commit flag if actions "
+                             "other than NAT without arguments are specified.");
+                error = OFPERR_OFPBAC_BAD_ARGUMENT;
+                goto out;
+            }
+        }
     }
 
 out:
@@ -4812,6 +4816,9 @@ encode_CT(const struct ofpact_conntrack *conntrack,
     nac->len = htons(len);
 }
 
+static char * OVS_WARN_UNUSED_RESULT parse_NAT(char *arg, struct ofpbuf *,
+                                               enum ofputil_protocol * OVS_UNUSED);
+
 /* Parses 'arg' as the argument to a "ct" action, and appends such an
  * action to 'ofpacts'.
  *
@@ -4849,14 +4856,22 @@ parse_CT(char *arg, struct ofpbuf *ofpacts,
             }
         } else if (!strcmp(key, "alg")) {
             error = str_to_connhelper(value, &oc->alg);
+        } else if (!strcmp(key, "nat")) {
+            const size_t nat_offset = ofpacts_pull(ofpacts);
+
+            error = parse_NAT(value, ofpacts, usable_protocols);
+            ofpact_pad(ofpacts);
+            /* Update CT action pointer and length. */
+            ofpacts->header = ofpbuf_push_uninit(ofpacts, nat_offset);
+            oc = ofpacts->header;
         } else if (!strcmp(key, "exec")) {
             /* Hide existing actions from ofpacts_parse_copy(), so the
              * nesting can be handled transparently. */
-            ofpbuf_pull(ofpacts, sizeof(*oc));
+            const size_t exec_offset = ofpacts_pull(ofpacts);
             error = ofpacts_parse_copy(value, ofpacts, usable_protocols, false,
                                        OFPACT_CT);
             ofpact_pad(ofpacts);
-            ofpacts->header = ofpbuf_push_uninit(ofpacts, sizeof(*oc));
+            ofpacts->header = ofpbuf_push_uninit(ofpacts, exec_offset);
             oc = ofpacts->header;
         } else {
             error = xasprintf("invalid argument to \"ct\" action: `%s'", key);
@@ -4881,6 +4896,8 @@ format_alg(int port, struct ds *s)
     }
 }
 
+static void format_NAT(const struct ofpact_nat *a, struct ds *ds);
+
 static void
 format_CT(const struct ofpact_conntrack *a, struct ds *s)
 {
@@ -4898,16 +4915,371 @@ format_CT(const struct ofpact_conntrack *a, struct ds *s)
     } else if (a->zone_imm) {
         ds_put_format(s, "zone=%"PRIu16",", a->zone_imm);
     }
-    if (ofpact_ct_get_action_len(a)) {
+    /* If the first action is a NAT action, format it outside of the 'exec'
+     * envelope. */
+    const struct ofpact *action = a->actions;
+    size_t actions_len = ofpact_ct_get_action_len(a);
+    if (actions_len && action->type == OFPACT_NAT) {
+        format_NAT((const struct ofpact_nat *)action, s);
+        ds_put_char(s, ',');
+        actions_len -= OFPACT_ALIGN(action->len);
+        action = ofpact_next(action);
+    }
+    if (actions_len) {
         ds_put_cstr(s, "exec(");
-        ofpacts_format(a->actions, ofpact_ct_get_action_len(a), s);
-        ds_put_format(s, "),");
+        ofpacts_format(action, actions_len, s);
+        ds_put_cstr(s, "),");
     }
     format_alg(a->alg, s);
     ds_chomp(s, ',');
     ds_put_char(s, ')');
 }
 
+/* NAT action. */
+
+/* Which optional fields are present? */
+enum nx_nat_range {
+    NX_NAT_RANGE_IPV4_MIN  = 1 << 0, /* ovs_be32 */
+    NX_NAT_RANGE_IPV4_MAX  = 1 << 1, /* ovs_be32 */
+    NX_NAT_RANGE_IPV6_MIN  = 1 << 2, /* struct in6_addr */
+    NX_NAT_RANGE_IPV6_MAX  = 1 << 3, /* struct in6_addr */
+    NX_NAT_RANGE_PROTO_MIN = 1 << 4, /* ovs_be16 */
+    NX_NAT_RANGE_PROTO_MAX = 1 << 5, /* ovs_be16 */
+};
+
+/* Action structure for NXAST_NAT. */
+struct nx_action_nat {
+    ovs_be16 type;              /* OFPAT_VENDOR. */
+    ovs_be16 len;               /* At least 16. */
+    ovs_be32 vendor;            /* NX_VENDOR_ID. */
+    ovs_be16 subtype;           /* NXAST_NAT. */
+    uint8_t  pad[2];            /* Must be zero. */
+    ovs_be16 flags;             /* Zero or more NX_NAT_F_* flags.
+                                 * Unspecified flag bits must be zero. */
+    ovs_be16 range_present;     /* NX_NAT_RANGE_* */
+    /* Followed by optional parameters as specified by 'range_present' */
+};
+OFP_ASSERT(sizeof(struct nx_action_nat) == 16);
+
+static void
+encode_NAT(const struct ofpact_nat *nat,
+           enum ofp_version ofp_version OVS_UNUSED,
+           struct ofpbuf *out)
+{
+    struct nx_action_nat *nan;
+    const size_t ofs = out->size;
+    uint16_t range_present = 0;
+
+    nan = put_NXAST_NAT(out);
+    nan->flags = htons(nat->flags);
+    if (nat->range_af == AF_INET) {
+        if (nat->range.addr.ipv4.min) {
+            ovs_be32 *min = ofpbuf_put_uninit(out, sizeof *min);
+            *min = nat->range.addr.ipv4.min;
+            range_present |= NX_NAT_RANGE_IPV4_MIN;
+        }
+        if (nat->range.addr.ipv4.max) {
+            ovs_be32 *max = ofpbuf_put_uninit(out, sizeof *max);
+            *max = nat->range.addr.ipv4.max;
+            range_present |= NX_NAT_RANGE_IPV4_MAX;
+        }
+    }
+    else if (nat->range_af == AF_INET6) {
+        if (!ipv6_mask_is_any(&nat->range.addr.ipv6.min)) {
+            struct in6_addr *min = ofpbuf_put_uninit(out, sizeof *min);
+            *min = nat->range.addr.ipv6.min;
+            range_present |= NX_NAT_RANGE_IPV6_MIN;
+        }
+        if (!ipv6_mask_is_any(&nat->range.addr.ipv6.max)) {
+            struct in6_addr *max = ofpbuf_put_uninit(out, sizeof *max);
+            *max = nat->range.addr.ipv6.max;
+            range_present |= NX_NAT_RANGE_IPV6_MAX;
+        }
+    }
+    if (nat->range_af != AF_UNSPEC) {
+        if (nat->range.proto.min) {
+            ovs_be16 *min = ofpbuf_put_uninit(out, sizeof *min);
+            *min = htons(nat->range.proto.min);
+            range_present |= NX_NAT_RANGE_PROTO_MIN;
+        }
+        if (nat->range.proto.max) {
+            ovs_be16 *max = ofpbuf_put_uninit(out, sizeof *max);
+            *max = htons(nat->range.proto.max);
+            range_present |= NX_NAT_RANGE_PROTO_MAX;
+        }
+    }
+    pad_ofpat(out, ofs);
+    nan = ofpbuf_at(out, ofs, sizeof *nan);
+    nan->range_present = htons(range_present);
+}
+
+static enum ofperr
+decode_NXAST_RAW_NAT(const struct nx_action_nat *nan,
+                     enum ofp_version ofp_version OVS_UNUSED,
+                     struct ofpbuf *out)
+{
+    struct ofpact_nat *nat;
+    uint16_t range_present = ntohs(nan->range_present);
+    const char *opts = (char *)(nan + 1);
+    uint16_t len = ntohs(nan->len) - sizeof *nan;
+
+    nat = ofpact_put_NAT(out);
+    nat->flags = ntohs(nan->flags);
+
+#define NX_NAT_GET_OPT(DST, SRC, LEN, TYPE)                     \
+    (LEN >= sizeof(TYPE)                                        \
+     ? (memcpy(DST, SRC, sizeof(TYPE)), LEN -= sizeof(TYPE),    \
+        SRC += sizeof(TYPE))                                    \
+     : NULL)
+
+    nat->range_af = AF_UNSPEC;
+    if (range_present & NX_NAT_RANGE_IPV4_MIN) {
+        if (range_present & (NX_NAT_RANGE_IPV6_MIN | NX_NAT_RANGE_IPV6_MAX)) {
+            return OFPERR_OFPBAC_BAD_ARGUMENT;
+        }
+
+        if (!NX_NAT_GET_OPT(&nat->range.addr.ipv4.min, opts, len, ovs_be32)
+            || !nat->range.addr.ipv4.min) {
+            return OFPERR_OFPBAC_BAD_ARGUMENT;
+        }
+
+        nat->range_af = AF_INET;
+
+        if (range_present & NX_NAT_RANGE_IPV4_MAX) {
+            if (!NX_NAT_GET_OPT(&nat->range.addr.ipv4.max, opts, len,
+                                ovs_be32)) {
+                return OFPERR_OFPBAC_BAD_ARGUMENT;
+            }
+            if (ntohl(nat->range.addr.ipv4.max)
+                < ntohl(nat->range.addr.ipv4.min)) {
+                return OFPERR_OFPBAC_BAD_ARGUMENT;
+            }
+        }
+    } else if (range_present & NX_NAT_RANGE_IPV4_MAX) {
+        return OFPERR_OFPBAC_BAD_ARGUMENT;
+    }
+    else if (range_present & NX_NAT_RANGE_IPV6_MIN) {
+        if (!NX_NAT_GET_OPT(&nat->range.addr.ipv6.min, opts, len,
+                            struct in6_addr)
+            || ipv6_mask_is_any(&nat->range.addr.ipv6.min)) {
+            return OFPERR_OFPBAC_BAD_ARGUMENT;
+        }
+
+        nat->range_af = AF_INET6;
+
+        if (range_present & NX_NAT_RANGE_IPV6_MAX) {
+            if (!NX_NAT_GET_OPT(&nat->range.addr.ipv6.max, opts, len,
+                                struct in6_addr)) {
+                return OFPERR_OFPBAC_BAD_ARGUMENT;
+            }
+            if (memcmp(&nat->range.addr.ipv6.max, &nat->range.addr.ipv6.min,
+                       sizeof(struct in6_addr)) < 0) {
+                return OFPERR_OFPBAC_BAD_ARGUMENT;
+            }
+        }
+    } else if (range_present & NX_NAT_RANGE_IPV6_MAX) {
+        return OFPERR_OFPBAC_BAD_ARGUMENT;
+    }
+
+    if (range_present & NX_NAT_RANGE_PROTO_MIN) {
+        ovs_be16 proto;
+
+        if (nat->range_af == AF_UNSPEC) {
+            return OFPERR_OFPBAC_BAD_ARGUMENT;
+        }
+        if (!NX_NAT_GET_OPT(&proto, opts, len, ovs_be16) || proto == 0) {
+            return OFPERR_OFPBAC_BAD_ARGUMENT;
+        }
+        nat->range.proto.min = ntohs(proto);
+        if (range_present & NX_NAT_RANGE_PROTO_MAX) {
+            if (!NX_NAT_GET_OPT(&proto, opts, len, ovs_be16)) {
+                return OFPERR_OFPBAC_BAD_ARGUMENT;
+            }
+            nat->range.proto.max = ntohs(proto);
+            if (nat->range.proto.max < nat->range.proto.min) {
+                return OFPERR_OFPBAC_BAD_ARGUMENT;
+            }
+        }
+    } else if (range_present & NX_NAT_RANGE_PROTO_MAX) {
+        return OFPERR_OFPBAC_BAD_ARGUMENT;
+    }
+
+    return 0;
+}
+
+static void
+format_NAT(const struct ofpact_nat *a, struct ds *ds)
+{
+    ds_put_cstr(ds, "nat");
+
+    if (a->flags & (NX_NAT_F_SRC | NX_NAT_F_DST)) {
+        ds_put_char(ds, '(');
+        ds_put_cstr(ds, a->flags & NX_NAT_F_SRC ? "src" : "dst");
+
+        if (a->range_af != AF_UNSPEC) {
+            char port_delim = ':';
+
+            ds_put_cstr(ds, "=");
+
+            if (a->range_af == AF_INET) {
+                ds_put_format(ds, IP_FMT, IP_ARGS(a->range.addr.ipv4.min));
+
+                if (a->range.addr.ipv4.max
+                    && a->range.addr.ipv4.max != a->range.addr.ipv4.min) {
+                    ds_put_format(ds, "-"IP_FMT,
+                                  IP_ARGS(a->range.addr.ipv4.max));
+                }
+            } else if (a->range_af == AF_INET6) {
+                print_ipv6_addr(ds, &a->range.addr.ipv6.min);
+
+                if (!ipv6_mask_is_any(&a->range.addr.ipv6.max)
+                    && memcmp(&a->range.addr.ipv6.max, &a->range.addr.ipv6.min,
+                              sizeof(struct in6_addr)) != 0) {
+                    ds_put_char(ds, '-');
+                    print_ipv6_addr(ds, &a->range.addr.ipv6.max);
+                }
+                port_delim = '+';
+            }
+            if (a->range.proto.min) {
+                ds_put_char(ds, port_delim);
+                ds_put_format(ds, "%"PRIu16, a->range.proto.min);
+
+                if (a->range.proto.max
+                    && a->range.proto.max != a->range.proto.min) {
+                    ds_put_format(ds, "-%"PRIu16, a->range.proto.max);
+                }
+            }
+            ds_put_char(ds, ',');
+
+            if (a->flags & NX_NAT_F_PERSISTENT) {
+                ds_put_cstr(ds, "persistent,");
+            }
+            if (a->flags & NX_NAT_F_PROTO_HASH) {
+                ds_put_cstr(ds, "hash,");
+            }
+            if (a->flags & NX_NAT_F_PROTO_RANDOM) {
+                ds_put_cstr(ds, "random,");
+            }
+        }
+        ds_chomp(ds, ',');
+        ds_put_char(ds, ')');
+    }
+}
+
+static char * OVS_WARN_UNUSED_RESULT
+str_to_nat_range(const char *s, struct ofpact_nat *on)
+{
+    char ipv6_s[IPV6_SCAN_LEN + 1];
+    char port_delim = ':';
+    int n = 0;
+
+    on->range_af = AF_UNSPEC;
+    if (ovs_scan_len(s, &n, IP_SCAN_FMT,
+                     IP_SCAN_ARGS(&on->range.addr.ipv4.min))) {
+        on->range_af = AF_INET;
+
+        if (s[n] == '-') {
+            n++;
+            if (!ovs_scan_len(s, &n, IP_SCAN_FMT,
+                              IP_SCAN_ARGS(&on->range.addr.ipv4.max))
+                || (ntohl(on->range.addr.ipv4.max)
+                    < ntohl(on->range.addr.ipv4.min))) {
+                goto error;
+            }
+        }
+    } else if (ovs_scan_len(s, &n, IPV6_SCAN_FMT, ipv6_s)
+               && inet_pton(AF_INET6, ipv6_s, &on->range.addr.ipv6.min) == 1) {
+        on->range_af = AF_INET6;
+
+        if (s[n] == '-') {
+            n++;
+            if (!ovs_scan_len(s, &n, IPV6_SCAN_FMT, ipv6_s)
+                || inet_pton(AF_INET6, ipv6_s, &on->range.addr.ipv6.max) != 1
+                || memcmp(&on->range.addr.ipv6.max, &on->range.addr.ipv6.min,
+                          sizeof on->range.addr.ipv6.max) < 0) {
+                goto error;
+            }
+        }
+        /* XXX: ':' as a delimiter seems difficult as it can be mixed with the
+         * delimiters in an IPv6 address. */
+        port_delim = '+';
+    }
+    if (on->range_af != AF_UNSPEC && s[n] == port_delim) {
+        n++;
+        if (!ovs_scan_len(s, &n, "%"SCNu16, &on->range.proto.min)) {
+            goto error;
+        }
+        if (s[n] == '-') {
+            n++;
+            if (!ovs_scan_len(s, &n, "%"SCNu16, &on->range.proto.max)
+                || on->range.proto.max < on->range.proto.min) {
+                goto error;
+            }
+        }
+    }
+    if (strlen(s) != n) {
+        return xasprintf("garbage (%s) after nat range \"%s\" (pos: %d)",
+                         &s[n], s, n);
+    }
+    return NULL;
+error:
+    return xasprintf("invalid nat range \"%s\"", s);
+}
+
+
+/* Parses 'arg' as the argument to a "nat" action, and appends such an
+ * action to 'ofpacts'.
+ *
+ * Returns NULL if successful, otherwise a malloc()'d string describing the
+ * error.  The caller is responsible for freeing the returned string. */
+static char * OVS_WARN_UNUSED_RESULT
+parse_NAT(char *arg, struct ofpbuf *ofpacts,
+          enum ofputil_protocol *usable_protocols OVS_UNUSED)
+{
+    struct ofpact_nat *on = ofpact_put_NAT(ofpacts);
+    char *key, *value;
+
+    on->flags = 0;
+    on->range_af = AF_UNSPEC;
+
+    while (ofputil_parse_key_value(&arg, &key, &value)) {
+        char *error = NULL;
+
+        if (!strcmp(key, "src")) {
+            on->flags |= NX_NAT_F_SRC;
+            error = str_to_nat_range(value, on);
+        } else if (!strcmp(key, "dst")) {
+            on->flags |= NX_NAT_F_DST;
+            error = str_to_nat_range(value, on);
+        } else if (!strcmp(key, "persistent")) {
+            on->flags |= NX_NAT_F_PERSISTENT;
+        } else if (!strcmp(key, "hash")) {
+            on->flags |= NX_NAT_F_PROTO_HASH;
+        } else if (!strcmp(key, "random")) {
+            on->flags |= NX_NAT_F_PROTO_RANDOM;
+        } else {
+            error = xasprintf("invalid key \"%s\" in \"nat\" argument",
+                              key);
+        }
+        if (error) {
+            return error;
+        }
+    }
+    if (on->flags & NX_NAT_F_SRC && on->flags & NX_NAT_F_DST) {
+        return xasprintf("May only specify one of \"snat\" or \"dnat\".");
+    }
+    if (!(on->flags & NX_NAT_F_SRC || on->flags & NX_NAT_F_DST)) {
+        if (on->flags) {
+            return xasprintf("Flags allowed only with \"snat\" or \"dnat\".");
+        }
+        if (on->range_af != AF_UNSPEC) {
+            return xasprintf("Range allowed only with \"snat\" or \"dnat\".");
+        }
+    }
+    return NULL;
+}
+
+
 /* Meter instruction. */
 
 static void
@@ -5288,6 +5660,7 @@ ofpact_is_set_or_move_action(const struct ofpact *a)
     case OFPACT_BUNDLE:
     case OFPACT_CLEAR_ACTIONS:
     case OFPACT_CT:
+    case OFPACT_NAT:
     case OFPACT_CONTROLLER:
     case OFPACT_DEC_MPLS_TTL:
     case OFPACT_DEC_TTL:
@@ -5363,6 +5736,7 @@ ofpact_is_allowed_in_actions_set(const struct ofpact *a)
     case OFPACT_BUNDLE:
     case OFPACT_CONTROLLER:
     case OFPACT_CT:
+    case OFPACT_NAT:
     case OFPACT_ENQUEUE:
     case OFPACT_EXIT:
     case OFPACT_UNROLL_XLATE:
@@ -5593,6 +5967,7 @@ ovs_instruction_type_from_ofpact_type(enum ofpact_type type)
     case OFPACT_SAMPLE:
     case OFPACT_DEBUG_RECIRC:
     case OFPACT_CT:
+    case OFPACT_NAT:
     default:
         return OVSINST_OFPIT11_APPLY_ACTIONS;
     }
@@ -6166,6 +6541,19 @@ ofpact_check__(enum ofputil_protocol *usable_protocols, struct ofpact *a,
                              flow, max_ports, table_id, n_tables, &p);
     }
 
+    case OFPACT_NAT: {
+        struct ofpact_nat *on = ofpact_get_NAT(a);
+
+        /* XXX: Check if any other criteria applies. */
+        if (!dl_type_is_ip_any(flow->dl_type) ||
+            (on->range_af == AF_INET && flow->dl_type != htons(ETH_TYPE_IP)) ||
+            (on->range_af == AF_INET6
+             && flow->dl_type != htons(ETH_TYPE_IPV6))) {
+            inconsistent_match(usable_protocols);
+        }
+        return 0;
+    }
+
     case OFPACT_CLEAR_ACTIONS:
         return 0;
 
@@ -6310,6 +6698,13 @@ ofpacts_verify_nested(const struct ofpact *a, enum ofpact_type outer_action)
         VLOG_WARN("cannot set CT fields outside of ct action");
         return OFPERR_OFPBAC_BAD_SET_ARGUMENT;
     }
+    if (a->type == OFPACT_NAT) {
+        if (outer_action != OFPACT_CT) {
+            VLOG_WARN("Cannot have NAT action outside of \"ct\" action");
+            return OFPERR_OFPBAC_BAD_SET_ARGUMENT;
+        }
+        return 0;
+    }
 
     if (outer_action) {
         ovs_assert(outer_action == OFPACT_WRITE_ACTIONS
@@ -6681,6 +7076,7 @@ ofpact_outputs_to_port(const struct ofpact *ofpact, ofp_port_t port)
     case OFPACT_GROUP:
     case OFPACT_DEBUG_RECIRC:
     case OFPACT_CT:
+    case OFPACT_NAT:
     default:
         return false;
     }
@@ -7347,7 +7743,8 @@ pad_ofpat(struct ofpbuf *openflow, size_t start_ofs)
 {
     struct ofp_action_header *oah;
 
-    ofpbuf_put_zeros(openflow, PAD_SIZE(openflow->size - start_ofs, 8));
+    ofpbuf_put_zeros(openflow, PAD_SIZE(openflow->size - start_ofs,
+                                        OFP_ACTION_ALIGN));
 
     oah = ofpbuf_at_assert(openflow, start_ofs, sizeof *oah);
     oah->len = htons(openflow->size - start_ofs);
diff --git a/lib/ofp-actions.h b/lib/ofp-actions.h
index 773b617..d180767 100644
--- a/lib/ofp-actions.h
+++ b/lib/ofp-actions.h
@@ -107,6 +107,7 @@
     OFPACT(SAMPLE,          ofpact_sample,      ofpact, "sample")       \
     OFPACT(UNROLL_XLATE,    ofpact_unroll_xlate, ofpact, "unroll_xlate") \
     OFPACT(CT,              ofpact_conntrack,   ofpact, "ct")           \
+    OFPACT(NAT,             ofpact_nat,         ofpact, "nat")          \
                                                                         \
     /* Debugging actions.                                               \
      *                                                                  \
@@ -529,6 +530,42 @@ ofpact_nest_get_action_len(const struct ofpact_nest *on)
 void ofpacts_execute_action_set(struct ofpbuf *action_list,
                                 const struct ofpbuf *action_set);
 
+/* Bits for 'flags' in struct nx_action_nat.
+ */
+enum nx_nat_flags {
+    NX_NAT_F_SRC          = 1 << 0,
+    NX_NAT_F_DST          = 1 << 1,
+    NX_NAT_F_PERSISTENT   = 1 << 2,
+    NX_NAT_F_PROTO_HASH   = 1 << 3,
+    NX_NAT_F_PROTO_RANDOM = 1 << 4,
+};
+
+/* OFPACT_NAT.
+ *
+ * Used for NXAST_NAT. */
+struct ofpact_nat {
+    struct ofpact ofpact;
+    uint8_t range_af; /* AF_UNSPEC, AF_INET, or AF_INET6 */
+    uint16_t flags;  /* NX_NAT_F_* */
+    struct {
+        struct {
+            uint16_t min;
+            uint16_t max;
+        } proto;
+        union {
+            struct {
+                ovs_be32 min;
+                ovs_be32 max;
+            } ipv4;
+            struct {
+                struct in6_addr min;
+                struct in6_addr max;
+            } ipv6;
+        } addr;
+    } range;
+};
+
+
 /* OFPACT_RESUBMIT.
  *
  * Used for NXAST_RESUBMIT, NXAST_RESUBMIT_TABLE. */
@@ -844,7 +881,7 @@ void *ofpact_put(struct ofpbuf *, enum ofpact_type, size_t len);
  *
  *     Appends a new 'ofpact', of length OFPACT_<ENUM>_RAW_SIZE, to 'ofpacts',
  *     initializes it with ofpact_init_<ENUM>(), and returns it.  Also sets
- *     'ofpacts->l2' to the returned action.
+ *     'ofpacts->header' to the returned action.
  *
  *     After using this function to add a variable-length action, add the
  *     elements of the flexible array (e.g. with ofpbuf_put()), then use
diff --git a/lib/ofp-parse.c b/lib/ofp-parse.c
index 8437656..b6e8d77 100644
--- a/lib/ofp-parse.c
+++ b/lib/ofp-parse.c
@@ -169,6 +169,19 @@ str_to_ip(const char *str, ovs_be32 *ip)
     return NULL;
 }
 
+/* Parses 'str' as an IP address into '*ip'.
+ *
+ * Returns NULL if successful, otherwise a malloc()'d string describing the
+ * error.  The caller is responsible for freeing the returned string. */
+char *  OVS_WARN_UNUSED_RESULT
+str_to_ipv6(const char *str, struct in6_addr *ip)
+{
+    if (lookup_ipv6(str, ip)) {
+        return xasprintf("%s: could not convert to IPv6 address", str);
+    }
+    return NULL;
+}
+
 /* Parses 'str' as a conntrack helper into 'alg'.
  *
  * Returns NULL if successful, otherwise a malloc()'d string describing the
diff --git a/lib/ofp-parse.h b/lib/ofp-parse.h
index 36f9acc..9a803f8 100644
--- a/lib/ofp-parse.h
+++ b/lib/ofp-parse.h
@@ -99,6 +99,7 @@ char *str_to_u64(const char *str, uint64_t *valuep) OVS_WARN_UNUSED_RESULT;
 char *str_to_be64(const char *str, ovs_be64 *valuep) OVS_WARN_UNUSED_RESULT;
 char *str_to_mac(const char *str, struct eth_addr *mac) OVS_WARN_UNUSED_RESULT;
 char *str_to_ip(const char *str, ovs_be32 *ip) OVS_WARN_UNUSED_RESULT;
+char *str_to_ipv6(const char *str, struct in6_addr *ip) OVS_WARN_UNUSED_RESULT;
 char *str_to_connhelper(const char *str, uint16_t *alg) OVS_WARN_UNUSED_RESULT;
 
 #endif /* ofp-parse.h */
diff --git a/ofproto/ofproto-dpif-xlate.c b/ofproto/ofproto-dpif-xlate.c
index a4007e3..9810c7b 100644
--- a/ofproto/ofproto-dpif-xlate.c
+++ b/ofproto/ofproto-dpif-xlate.c
@@ -305,6 +305,9 @@ struct xlate_ctx {
      * state from the datapath should be honored after recirculation. */
     bool conntracked;
 
+    /* Pointer to an embedded NAT action in a conntrack action, or NULL. */
+    struct ofpact_nat *ct_nat_action;
+
     /* OpenFlow 1.1+ action set.
      *
      * 'action_set' accumulates "struct ofpact"s added by OFPACT_WRITE_ACTIONS.
@@ -4125,6 +4128,7 @@ recirc_unroll_actions(const struct ofpact *ofpacts, size_t ofpacts_len,
         case OFPACT_SAMPLE:
         case OFPACT_DEBUG_RECIRC:
         case OFPACT_CT:
+        case OFPACT_NAT:
             break;
 
             /* These need not be copied for restoration. */
@@ -4197,6 +4201,61 @@ put_ct_helper(struct ofpbuf *odp_actions, struct ofpact_conntrack *ofc)
 }
 
 static void
+put_ct_nat(struct xlate_ctx *ctx)
+{
+    struct ofpact_nat *ofn = ctx->ct_nat_action;
+    size_t nat_offset;
+
+    if (!ofn) {
+        return;
+    }
+
+    nat_offset = nl_msg_start_nested(ctx->odp_actions, OVS_CT_ATTR_NAT);
+    if (ofn->flags & NX_NAT_F_SRC || ofn->flags & NX_NAT_F_DST) {
+        nl_msg_put_flag(ctx->odp_actions, ofn->flags & NX_NAT_F_SRC
+                        ? OVS_NAT_ATTR_SRC : OVS_NAT_ATTR_DST);
+        if (ofn->flags & NX_NAT_F_PERSISTENT) {
+            nl_msg_put_flag(ctx->odp_actions, OVS_NAT_ATTR_PERSISTENT);
+        }
+        if (ofn->flags & NX_NAT_F_PROTO_HASH) {
+            nl_msg_put_flag(ctx->odp_actions, OVS_NAT_ATTR_PROTO_HASH);
+        } else if (ofn->flags & NX_NAT_F_PROTO_RANDOM) {
+            nl_msg_put_flag(ctx->odp_actions, OVS_NAT_ATTR_PROTO_RANDOM);
+        }
+        if (ofn->range_af == AF_INET) {
+            nl_msg_put_u32(ctx->odp_actions, OVS_NAT_ATTR_IP_MIN,
+                           ofn->range.addr.ipv4.min);
+            if (ofn->range.addr.ipv4.max &&
+                ofn->range.addr.ipv4.max > ofn->range.addr.ipv4.min) {
+                nl_msg_put_u32(ctx->odp_actions, OVS_NAT_ATTR_IP_MAX,
+                               ofn->range.addr.ipv4.max);
+            }
+        } else if (ofn->range_af == AF_INET6) {
+            nl_msg_put_unspec(ctx->odp_actions, OVS_NAT_ATTR_IP_MIN,
+                              &ofn->range.addr.ipv6.min,
+                              sizeof ofn->range.addr.ipv6.min);
+            if (!ipv6_mask_is_any(&ofn->range.addr.ipv6.max) &&
+                memcmp(&ofn->range.addr.ipv6.max, &ofn->range.addr.ipv6.min,
+                       sizeof ofn->range.addr.ipv6.max) > 0) {
+                nl_msg_put_unspec(ctx->odp_actions, OVS_NAT_ATTR_IP_MAX,
+                                  &ofn->range.addr.ipv6.max,
+                                  sizeof ofn->range.addr.ipv6.max);
+            }
+        }
+        if (ofn->range_af != AF_UNSPEC && ofn->range.proto.min) {
+            nl_msg_put_u16(ctx->odp_actions, OVS_NAT_ATTR_PROTO_MIN,
+                           ofn->range.proto.min);
+            if (ofn->range.proto.max &&
+                ofn->range.proto.max > ofn->range.proto.min) {
+                nl_msg_put_u16(ctx->odp_actions, OVS_NAT_ATTR_PROTO_MAX,
+                               ofn->range.proto.max);
+            }
+        }
+    }
+    nl_msg_end_nested(ctx->odp_actions, nat_offset);
+}
+
+static void
 compose_conntrack_action(struct xlate_ctx *ctx, struct ofpact_conntrack *ofc)
 {
     ovs_u128 old_ct_label = ctx->base_flow.ct_label;
@@ -4209,6 +4268,7 @@ compose_conntrack_action(struct xlate_ctx *ctx, struct ofpact_conntrack *ofc)
     xlate_commit_actions(ctx);
 
     /* Process nested actions first, to populate the key. */
+    ctx->ct_nat_action = NULL;
     do_xlate_actions(ofc->actions, ofpact_ct_get_action_len(ofc), ctx);
 
     if (ofc->zone_src.field) {
@@ -4225,6 +4285,8 @@ compose_conntrack_action(struct xlate_ctx *ctx, struct ofpact_conntrack *ofc)
     put_ct_mark(&ctx->xin->flow, &ctx->base_flow, ctx->odp_actions, ctx->wc);
     put_ct_label(&ctx->xin->flow, &ctx->base_flow, ctx->odp_actions, ctx->wc);
     put_ct_helper(ctx->odp_actions, ofc);
+    put_ct_nat(ctx);
+    ctx->ct_nat_action = NULL;
     nl_msg_end_nested(ctx->odp_actions, ct_offset);
 
     /* Restore the original ct fields in the key. These should only be exposed
@@ -4614,6 +4676,11 @@ do_xlate_actions(const struct ofpact *ofpacts, size_t ofpacts_len,
             compose_conntrack_action(ctx, ofpact_get_CT(a));
             break;
 
+        case OFPACT_NAT:
+            /* This will be processed by compose_conntrack_action(). */
+            ctx->ct_nat_action = ofpact_get_NAT(a);
+            break;
+
         case OFPACT_DEBUG_RECIRC:
             ctx_trigger_recirculation(ctx);
             a = ofpact_next(a);
@@ -4922,6 +4989,8 @@ xlate_actions(struct xlate_in *xin, struct xlate_out *xout)
         .was_mpls = false,
         .conntracked = false,
 
+        .ct_nat_action = NULL,
+
         .action_set_has_group = false,
         .action_set = OFPBUF_STUB_INITIALIZER(action_set_stub),
     };
diff --git a/tests/odp.at b/tests/odp.at
index eaba059..7353de3 100644
--- a/tests/odp.at
+++ b/tests/odp.at
@@ -312,6 +312,13 @@ ct(commit,zone=5)
 ct(commit,mark=0xa0a0a0a0/0xfefefefe)
 ct(commit,label=0x1234567890abcdef1234567890abcdef/0xf1f2f3f4f5f6f7f8f9f0fafbfcfdfeff)
 ct(commit,helper=ftp)
+ct(nat)
+ct(commit,nat(src))
+ct(commit,nat(dst))
+ct(commit,nat(src=10.0.0.240,random))
+ct(commit,nat(src=10.0.0.240:32768-65535,random))
+ct(commit,nat(dst=10.0.0.128-10.0.0.254,hash))
+ct(commit,nat(src=10.0.0.240-10.0.0.254:32768-65535,persistent))
 ])
 AT_CHECK_UNQUOTED([ovstest test-odp parse-actions < actions.txt], [0],
   [`cat actions.txt`
diff --git a/tests/ofp-actions.at b/tests/ofp-actions.at
index 01e5b67..8ebd928 100644
--- a/tests/ofp-actions.at
+++ b/tests/ofp-actions.at
@@ -187,6 +187,53 @@ ffff 0018 00002320 0007 001f 00010004 000000000000f009
 # actions=ct(alg=ftp)
 ffff 0018 00002320 0023 0000 00000000 0000 FF 000000 0015
 
+# actions=ct(commit,nat(src))
+ffff 0028 00002320 0023 0001 00000000 0000 FF 000000 0000 dnl
+ffff 0010 00002320 0024 00 00 0001 0000
+
+# actions=ct(commit,nat(dst))
+ffff 0028 00002320 0023 0001 00000000 0000 FF 000000 0000 dnl
+ffff 0010 00002320 0024 00 00 0002 0000
+
+# actions=ct(nat)
+ffff 0028 00002320 0023 0000 00000000 0000 FF 000000 0000 dnl
+ffff 0010 00002320 0024 00 00 0000 0000
+
+# actions=ct(commit,nat(src=10.0.0.240,random))
+ffff 0030 00002320 0023 0001 00000000 0000 FF 000000 0000 dnl
+ffff 0018 00002320 0024 00 00 0011 0001 0a0000f0 00000000
+
+# actions=ct(commit,nat(src=10.0.0.240:32768-65535,random))
+ffff 0030 00002320 0023 0001 00000000 0000 FF 000000 0000 dnl
+ffff 0018 00002320 0024 00 00 0011 0031 0a0000f0 8000ffff
+
+# actions=ct(commit,nat(dst=10.0.0.128-10.0.0.254,hash))
+ffff 0030 00002320 0023 0001 00000000 0000 FF 000000 0000 dnl
+ffff 0018 00002320 0024 00 00 000a 0003 0a000080 0a0000fe
+
+# actions=ct(commit,nat(src=10.0.0.240-10.0.0.254:32768-65535,persistent))
+ffff 0038 00002320 0023 0001 00000000 0000 FF 000000 0000 dnl
+ffff 0020 00002320 0024 00 00 0005 0033 0a0000f0 0a0000fe 8000ffff 00000000
+
+# actions=ct(commit,nat(src=fe80::20c:29ff:fe88:a18b,random))
+ffff 0038 00002320 0023 0001 00000000 0000 FF 000000 0000 dnl
+ffff 0020 00002320 0024 00 00 0011 0004 fe800000 00000000 020c 29ff fe88 a18b
+
+# actions=ct(commit,nat(src=fe80::20c:29ff:fe88:1-fe80::20c:29ff:fe88:a18b,random))
+ffff 0048 00002320 0023 0001 00000000 0000 FF 000000 0000 dnl
+ffff 0030 00002320 0024 00 00 0011 000c fe800000 00000000 020c 29ff fe88 0001 fe800000 00000000 020c 29ff fe88 a18b
+
+# actions=ct(commit,nat(src=fe80::20c:29ff:fe88:1-fe80::20c:29ff:fe88:a18b+255-4096,random))
+ffff 0050 00002320 0023 0001 00000000 0000 FF 000000 0000 dnl
+ffff 0038 00002320 0024 00 00 0011 003c dnl
+fe800000 00000000 020c 29ff fe88 0001 dnl
+fe800000 00000000 020c 29ff fe88 a18b dnl
+00ff1000 00000000
+
+# bad OpenFlow10 actions: OFPBAC_BAD_ARGUMENT
+ffff 0048 00002320 0023 0001 00000000 0000 FF 000000 0000 dnl
+ffff 0030 00002320 0024 00 00 0011 000c fe800000 00000000 020c 29ff fe88 a18b fe800000 00000000 020c 29ff fe88 0001
+
 ])
 sed '/^[[#&]]/d' < test-data > input.txt
 sed -n 's/^# //p; /^$/p' < test-data > expout
diff --git a/tests/ovs-ofctl.at b/tests/ovs-ofctl.at
index 7375cad..48e6106 100644
--- a/tests/ovs-ofctl.at
+++ b/tests/ovs-ofctl.at
@@ -160,12 +160,23 @@ sctp actions=drop
 sctp actions=drop
 in_port=0 actions=resubmit:0
 actions=sample(probability=12345,collector_set_id=23456,obs_domain_id=34567,obs_point_id=45678)
+actions=ct(nat)
+actions=ct(commit,nat(dst))
+actions=ct(commit,nat(src))
+actions=ct(commit,nat(src=10.0.0.240,random))
+actions=ct(commit,nat(src=10.0.0.240:32768-65535,random))
+actions=ct(commit,nat(dst=10.0.0.128-10.0.0.254,hash))
+actions=ct(commit,nat(src=10.0.0.240-10.0.0.254:32768-65535,persistent))
+actions=ct(commit,nat(src=fe80::20c:29ff:fe88:a18b,random))
+actions=ct(commit,nat(src=fe80::20c:29ff:fe88:1-fe80::20c:29ff:fe88:a18b,random))
+actions=ct(commit,nat(src=fe80::20c:29ff:fe88:1-fe80::20c:29ff:fe88:a18b+255-4096,random))
+actions=ct(commit,nat(src=10.1.1.240-10.1.1.255),alg=ftp)
 ]])
 
 AT_CHECK([ovs-ofctl parse-flows flows.txt
 ], [0], [stdout])
 AT_CHECK([[sed 's/ (xid=0x[0-9a-fA-F]*)//' stdout]], [0],
-[[usable protocols: any
+[[usable protocols: OpenFlow10,NXM
 chosen protocol: OpenFlow10-table_id
 OFPT_FLOW_MOD: ADD tcp,tp_src=123 actions=FLOOD
 OFPT_FLOW_MOD: ADD in_port=LOCAL,dl_vlan=9,dl_src=00:0a:e4:25:6b:b0 actions=drop
@@ -179,6 +190,17 @@ OFPT_FLOW_MOD: ADD sctp actions=drop
 OFPT_FLOW_MOD: ADD sctp actions=drop
 OFPT_FLOW_MOD: ADD in_port=0 actions=resubmit:0
 OFPT_FLOW_MOD: ADD actions=sample(probability=12345,collector_set_id=23456,obs_domain_id=34567,obs_point_id=45678)
+OFPT_FLOW_MOD: ADD actions=ct(nat)
+OFPT_FLOW_MOD: ADD actions=ct(commit,nat(dst))
+OFPT_FLOW_MOD: ADD actions=ct(commit,nat(src))
+OFPT_FLOW_MOD: ADD actions=ct(commit,nat(src=10.0.0.240,random))
+OFPT_FLOW_MOD: ADD actions=ct(commit,nat(src=10.0.0.240:32768-65535,random))
+OFPT_FLOW_MOD: ADD actions=ct(commit,nat(dst=10.0.0.128-10.0.0.254,hash))
+OFPT_FLOW_MOD: ADD actions=ct(commit,nat(src=10.0.0.240-10.0.0.254:32768-65535,persistent))
+OFPT_FLOW_MOD: ADD actions=ct(commit,nat(src=fe80::20c:29ff:fe88:a18b,random))
+OFPT_FLOW_MOD: ADD actions=ct(commit,nat(src=fe80::20c:29ff:fe88:1-fe80::20c:29ff:fe88:a18b,random))
+OFPT_FLOW_MOD: ADD actions=ct(commit,nat(src=fe80::20c:29ff:fe88:1-fe80::20c:29ff:fe88:a18b+255-4096,random))
+OFPT_FLOW_MOD: ADD actions=ct(commit,nat(src=10.1.1.240-10.1.1.255),alg=ftp)
 ]])
 AT_CLEANUP
 
diff --git a/tests/system-traffic.at b/tests/system-traffic.at
index 3b2de83..5b27100 100644
--- a/tests/system-traffic.at
+++ b/tests/system-traffic.at
@@ -966,7 +966,7 @@ AT_CHECK([conntrack -L 2>&1 | FORMAT_CT(10.1.1.1)], [0], [dnl
 ])
 
 dnl Active FTP requests from p0->p1 should work fine.
-NS_CHECK_EXEC([at_ns0], [wget ftp://10.1.1.2 --no-passive-ftp -t 3 -T 1 --retry-connrefused -v -o wget0.log])
+NS_CHECK_EXEC([at_ns0], [wget ftp://10.1.1.2 --no-passive-ftp -t 3 -T 1 --retry-connrefused -v -o wget0-1.log])
 AT_CHECK([conntrack -L 2>&1 | FORMAT_CT(10.1.1.2) | grep -v "FIN"], [0], [dnl
 TIME_WAIT src=10.1.1.1 dst=10.1.1.2 sport=<cleared> dport=<cleared> src=10.1.1.2 dst=10.1.1.1 sport=<cleared> dport=<cleared> [[ASSURED]] mark=0 helper=ftp use=2
 TIME_WAIT src=10.1.1.2 dst=10.1.1.1 sport=<cleared> dport=<cleared> src=10.1.1.1 dst=10.1.1.2 sport=<cleared> dport=<cleared> [[ASSURED]] mark=0 use=1
@@ -975,7 +975,7 @@ TIME_WAIT src=10.1.1.2 dst=10.1.1.1 sport=<cleared> dport=<cleared> src=10.1.1.1
 AT_CHECK([conntrack -F 2>/dev/null])
 
 dnl Passive FTP requests from p0->p1 should work fine.
-NS_CHECK_EXEC([at_ns0], [wget ftp://10.1.1.2 -t 3 -T 1 --retry-connrefused -v -o wget0.log])
+NS_CHECK_EXEC([at_ns0], [wget ftp://10.1.1.2 -t 3 -T 1 --retry-connrefused -v -o wget0-2.log])
 AT_CHECK([conntrack -L 2>&1 | FORMAT_CT(10.1.1.2) | grep -v "FIN"], [0], [dnl
 TIME_WAIT src=10.1.1.1 dst=10.1.1.2 sport=<cleared> dport=<cleared> src=10.1.1.2 dst=10.1.1.1 sport=<cleared> dport=<cleared> [[ASSURED]] mark=0 helper=ftp use=2
 TIME_WAIT src=10.1.1.1 dst=10.1.1.2 sport=<cleared> dport=<cleared> src=10.1.1.2 dst=10.1.1.1 sport=<cleared> dport=<cleared> [[ASSURED]] mark=0 use=1
@@ -1266,3 +1266,459 @@ NS_CHECK_EXEC([at_ns0], [ping -s 3200 -q -c 3 -i 0.3 -w 2 10.1.1.100 | FORMAT_PI
 
 OVS_TRAFFIC_VSWITCHD_STOP
 AT_CLEANUP
+
+AT_SETUP([conntrack - simple SNAT])
+CHECK_CONNTRACK()
+OVS_TRAFFIC_VSWITCHD_START(
+   [set-fail-mode br0 standalone -- ])
+
+ADD_NAMESPACES(at_ns0, at_ns1)
+
+ADD_VETH(p0, at_ns0, br0, "10.1.1.1/24")
+NS_CHECK_EXEC([at_ns0], [ip link set dev p0 address 80:88:88:88:88:88])
+ADD_VETH(p1, at_ns1, br0, "10.1.1.2/24")
+
+dnl Allow any traffic from ns0->ns1. Only allow nd, return traffic from ns1->ns0.
+AT_DATA([flows.txt], [dnl
+in_port=1,ip,action=ct(commit,zone=1,nat(src=10.1.1.240-10.1.1.255)),2
+in_port=2,ct_state=-trk,ip,action=ct(table=0,zone=1,nat)
+in_port=2,ct_state=+trk,ct_zone=1,ip,action=1
+dnl
+dnl ARP
+priority=100 arp arp_op=1 action=move:OXM_OF_ARP_TPA[[]]->NXM_NX_REG2[[]],resubmit(,8),goto_table:10
+priority=10 arp action=normal
+priority=0,action=drop
+dnl
+dnl MAC resolution table for IP in reg2, stores mac in OXM_OF_PKT_REG0
+table=8,reg2=0x0a0101f0/0xfffffff0,action=load:0x808888888888->OXM_OF_PKT_REG0[[]]
+table=8,priority=0,action=load:0->OXM_OF_PKT_REG0[[]]
+dnl ARP responder mac filled in at OXM_OF_PKT_REG0, or 0 for normal action.
+dnl TPA IP in reg2.
+dnl Swaps the fields of the ARP message to turn a query to a response.
+table=10 priority=100 arp xreg0=0 action=normal
+table=10 priority=10,arp,arp_op=1,action=load:2->OXM_OF_ARP_OP[[]],move:OXM_OF_ARP_SHA[[]]->OXM_OF_ARP_THA[[]],move:OXM_OF_PKT_REG0[[0..47]]->OXM_OF_ARP_SHA[[]],move:OXM_OF_ARP_SPA[[]]->OXM_OF_ARP_TPA[[]],move:NXM_NX_REG2[[]]->OXM_OF_ARP_SPA[[]],move:NXM_OF_ETH_SRC[[]]->NXM_OF_ETH_DST[[]],move:OXM_OF_PKT_REG0[[0..47]]->NXM_OF_ETH_SRC[[]],move:NXM_OF_IN_PORT[[]]->NXM_NX_REG3[[0..15]],load:0->NXM_OF_IN_PORT[[]],output:NXM_NX_REG3[[0..15]]
+table=10 priority=0 action=drop
+])
+
+AT_CHECK([ovs-ofctl -O OpenFlow15 add-flows br0 flows.txt])
+
+on_exit 'conntrack -L --output extended --any-nat'
+on_exit 'ovs-ofctl -O OpenFlow15 dump-flows br0'
+on_exit 'ovs-ofctl dump-ports br0'
+
+dnl HTTP requests from p0->p1 should work fine.
+NETNS_DAEMONIZE([at_ns1], [[$PYTHON $srcdir/test-l7.py]], [http0.pid])
+NS_CHECK_EXEC([at_ns0], [wget 10.1.1.2 -t 5 -T 1 --retry-connrefused -v -o wget0.log])
+
+AT_CHECK([conntrack -L 2>&1 | FORMAT_CT(10.1.1.2) | sed -e 's/dst=10.1.1.2[[45]][[0-9]]/dst=10.1.1.2XX/'], [0], [dnl
+TIME_WAIT src=10.1.1.1 dst=10.1.1.2 sport=<cleared> dport=<cleared> src=10.1.1.2 dst=10.1.1.2XX sport=<cleared> dport=<cleared> [[ASSURED]] mark=0 zone=1 use=1
+])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([conntrack - more complex SNAT])
+CHECK_CONNTRACK()
+OVS_TRAFFIC_VSWITCHD_START(
+   [set-fail-mode br0 standalone -- ])
+
+ADD_NAMESPACES(at_ns0, at_ns1)
+
+ADD_VETH(p0, at_ns0, br0, "10.1.1.1/24")
+NS_CHECK_EXEC([at_ns0], [ip link set dev p0 address 80:88:88:88:88:88])
+ADD_VETH(p1, at_ns1, br0, "10.1.1.2/24")
+
+AT_DATA([flows.txt], [dnl
+dnl Track all IP traffic, NAT existing connections.
+priority=100 ip action=ct(table=1,zone=1,nat)
+dnl
+dnl Allow ARP, but generate responses for NATed addresses
+priority=100 arp arp_op=1 action=move:OXM_OF_ARP_TPA[[]]->NXM_NX_REG2[[]],resubmit(,8),goto_table:10
+priority=10 arp action=normal
+priority=0 action=drop
+dnl
+dnl Allow any traffic from ns0->ns1. SNAT ns0 to 10.1.1.240-10.1.1.255
+table=1 priority=100 in_port=1 ip ct_state=+trk+new-est action=ct(commit,zone=1,nat(src=10.1.1.240-10.1.1.255)),2
+table=1 priority=100 in_port=1 ip ct_state=+trk-new+est action=2
+dnl Only allow established traffic from ns1->ns0.
+table=1 priority=100 in_port=2 ip ct_state=+trk-new+est action=1
+table=1 priority=0 action=drop
+dnl
+dnl MAC resolution table for IP in reg2, stores mac in OXM_OF_PKT_REG0
+table=8 priority=100 reg2=0x0a0101f0/0xfffffff0 action=load:0x808888888888->OXM_OF_PKT_REG0[[]]
+dnl Zero result means not found.
+table=8 priority=0 action=load:0->OXM_OF_PKT_REG0[[]]
+dnl ARP responder mac filled in at OXM_OF_PKT_REG0, or 0 for normal action.
+dnl ARP TPA IP in reg2.
+table=10 priority=100 arp xreg0=0 action=normal
+dnl Swaps the fields of the ARP message to turn a query to a response.
+table=10 priority=10 arp arp_op=1 action=load:2->OXM_OF_ARP_OP[[]],move:OXM_OF_ARP_SHA[[]]->OXM_OF_ARP_THA[[]],move:OXM_OF_PKT_REG0[[0..47]]->OXM_OF_ARP_SHA[[]],move:OXM_OF_ARP_SPA[[]]->OXM_OF_ARP_TPA[[]],move:NXM_NX_REG2[[]]->OXM_OF_ARP_SPA[[]],move:NXM_OF_ETH_SRC[[]]->NXM_OF_ETH_DST[[]],move:OXM_OF_PKT_REG0[[0..47]]->NXM_OF_ETH_SRC[[]],move:NXM_OF_IN_PORT[[]]->NXM_NX_REG3[[0..15]],load:0->NXM_OF_IN_PORT[[]],output:NXM_NX_REG3[[0..15]]
+table=10 priority=0 action=drop
+])
+
+AT_CHECK([ovs-ofctl -O OpenFlow15 add-flows br0 flows.txt])
+
+on_exit 'conntrack -L --output extended'
+on_exit 'ovs-ofctl -O OpenFlow15 dump-flows br0'
+on_exit 'ovs-ofctl dump-ports br0'
+on_exit 'ovs-appctl dpctl/dump-flows'
+
+dnl HTTP requests from p0->p1 should work fine.
+NETNS_DAEMONIZE([at_ns1], [[$PYTHON $srcdir/test-l7.py]], [http0.pid])
+NS_CHECK_EXEC([at_ns0], [wget 10.1.1.2 -t 5 -T 1 --retry-connrefused -v -o wget0.log])
+
+AT_CHECK([conntrack -L 2>&1 | FORMAT_CT(10.1.1.2) | sed -e 's/dst=10.1.1.2[[45]][[0-9]]/dst=10.1.1.2XX/'], [0], [dnl
+TIME_WAIT src=10.1.1.1 dst=10.1.1.2 sport=<cleared> dport=<cleared> src=10.1.1.2 dst=10.1.1.2XX sport=<cleared> dport=<cleared> [[ASSURED]] mark=0 zone=1 use=1
+])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([conntrack - simple DNAT])
+CHECK_CONNTRACK()
+OVS_TRAFFIC_VSWITCHD_START(
+   [set-fail-mode br0 standalone -- ])
+
+ADD_NAMESPACES(at_ns0, at_ns1)
+
+ADD_VETH(p0, at_ns0, br0, "10.1.1.1/24")
+ADD_VETH(p1, at_ns1, br0, "10.1.1.2/24")
+NS_CHECK_EXEC([at_ns1], [ip link set dev p1 address 80:88:88:88:88:88])
+
+dnl Allow any traffic from ns0->ns1. Only allow nd, return traffic from ns1->ns0.
+AT_DATA([flows.txt], [dnl
+priority=100 in_port=1,ip,nw_dst=10.1.1.64,action=ct(zone=1,nat(dst=10.1.1.2),commit),2
+priority=10 in_port=1,ip,action=ct(commit,zone=1),2
+priority=100 in_port=2,ct_state=-trk,ip,action=ct(table=0,nat,zone=1)
+priority=100 in_port=2,ct_state=+trk+est,ct_zone=1,ip,action=1
+dnl
+dnl ARP
+priority=100 arp arp_op=1 action=move:OXM_OF_ARP_TPA[[]]->NXM_NX_REG2[[]],resubmit(,8),goto_table:10
+priority=10 arp action=normal
+priority=0,action=drop
+dnl
+dnl MAC resolution table for IP in reg2, stores mac in OXM_OF_PKT_REG0
+table=8,reg2=0x0a010140,action=load:0x808888888888->OXM_OF_PKT_REG0[[]]
+dnl Zero result means not found.
+table=8,priority=0,action=load:0->OXM_OF_PKT_REG0[[]]
+dnl ARP responder mac filled in at OXM_OF_PKT_REG0, or 0 for normal action.
+dnl TPA IP in reg2.
+table=10 priority=100 arp xreg0=0 action=normal
+dnl Swaps the fields of the ARP message to turn a query to a response.
+table=10 priority=10,arp,arp_op=1,action=load:2->OXM_OF_ARP_OP[[]],move:OXM_OF_ARP_SHA[[]]->OXM_OF_ARP_THA[[]],move:OXM_OF_PKT_REG0[[0..47]]->OXM_OF_ARP_SHA[[]],move:OXM_OF_ARP_SPA[[]]->OXM_OF_ARP_TPA[[]],move:NXM_NX_REG2[[]]->OXM_OF_ARP_SPA[[]],move:NXM_OF_ETH_SRC[[]]->NXM_OF_ETH_DST[[]],move:OXM_OF_PKT_REG0[[0..47]]->NXM_OF_ETH_SRC[[]],move:NXM_OF_IN_PORT[[]]->NXM_NX_REG3[[0..15]],load:0->NXM_OF_IN_PORT[[]],output:NXM_NX_REG3[[0..15]]
+table=10 priority=0 action=drop
+])
+
+AT_CHECK([ovs-ofctl -O OpenFlow15 add-flows br0 flows.txt])
+
+on_exit 'conntrack -L --output extended'
+on_exit 'ovs-ofctl -O OpenFlow15 dump-flows br0'
+on_exit 'ovs-ofctl dump-ports br0'
+on_exit 'ovs-appctl dpctl/dump-flows'
+
+dnl Should work with the virtual IP address through NAT
+NETNS_DAEMONIZE([at_ns1], [[$PYTHON $srcdir/test-l7.py]], [http0.pid])
+NS_CHECK_EXEC([at_ns0], [wget 10.1.1.64 -t 5 -T 1 --retry-connrefused -v -o wget0.log])
+
+AT_CHECK([conntrack -L 2>&1 | FORMAT_CT(10.1.1.64) ], [0], [dnl
+TIME_WAIT src=10.1.1.1 dst=10.1.1.64 sport=<cleared> dport=<cleared> src=10.1.1.2 dst=10.1.1.1 sport=<cleared> dport=<cleared> [[ASSURED]] mark=0 zone=1 use=1
+])
+
+dnl Should work with the assigned IP address as well
+NS_CHECK_EXEC([at_ns0], [wget 10.1.1.2 -t 3 -T 1 --retry-connrefused -v -o wget0.log])
+
+AT_CHECK([conntrack -L 2>&1 | FORMAT_CT(10.1.1.2) ], [0], [dnl
+TIME_WAIT src=10.1.1.1 dst=10.1.1.2 sport=<cleared> dport=<cleared> src=10.1.1.2 dst=10.1.1.1 sport=<cleared> dport=<cleared> [[ASSURED]] mark=0 zone=1 use=1
+])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([conntrack - more complex DNAT])
+CHECK_CONNTRACK()
+OVS_TRAFFIC_VSWITCHD_START(
+   [set-fail-mode br0 standalone -- ])
+
+ADD_NAMESPACES(at_ns0, at_ns1)
+
+ADD_VETH(p0, at_ns0, br0, "10.1.1.1/24")
+ADD_VETH(p1, at_ns1, br0, "10.1.1.2/24")
+NS_CHECK_EXEC([at_ns1], [ip link set dev p1 address 80:88:88:88:88:88])
+
+dnl Allow any traffic from ns0->ns1. Only allow nd, return traffic from ns1->ns0.
+AT_DATA([flows.txt], [dnl
+dnl Track all IP traffic
+table=0 priority=100 ip action=ct(table=1,zone=1,nat)
+dnl
+dnl Allow ARP, but generate responses for NATed addresses
+table=0 priority=100 arp arp_op=1 action=move:OXM_OF_ARP_TPA[[]]->NXM_NX_REG2[[]],resubmit(,8),goto_table:10
+table=0 priority=10 arp action=normal
+table=0 priority=0 action=drop
+dnl
+dnl Allow any IP traffic from ns0->ns1. DNAT ns0 from 10.1.1.64 to 10.1.1.2
+table=1 priority=100 in_port=1 ct_state=+new ip nw_dst=10.1.1.64 action=ct(zone=1,nat(dst=10.1.1.2),commit),2
+table=1 priority=10 in_port=1 ct_state=+new ip action=ct(commit,zone=1),2
+table=1 priority=100 in_port=1 ct_state=+est ct_zone=1 action=2
+dnl Only allow established traffic from ns1->ns0.
+table=1 priority=100 in_port=2 ct_state=+est ct_zone=1 action=1
+table=1 priority=0 action=drop
+dnl
+dnl MAC resolution table for IP in reg2, stores mac in OXM_OF_PKT_REG0
+table=8,reg2=0x0a010140,action=load:0x808888888888->OXM_OF_PKT_REG0[[]]
+dnl Zero result means not found.
+table=8,priority=0,action=load:0->OXM_OF_PKT_REG0[[]]
+dnl ARP responder mac filled in at OXM_OF_PKT_REG0, or 0 for normal action.
+dnl TPA IP in reg2.
+table=10 priority=100 arp xreg0=0 action=normal
+dnl Swaps the fields of the ARP message to turn a query to a response.
+table=10 priority=10,arp,arp_op=1,action=load:2->OXM_OF_ARP_OP[[]],move:OXM_OF_ARP_SHA[[]]->OXM_OF_ARP_THA[[]],move:OXM_OF_PKT_REG0[[0..47]]->OXM_OF_ARP_SHA[[]],move:OXM_OF_ARP_SPA[[]]->OXM_OF_ARP_TPA[[]],move:NXM_NX_REG2[[]]->OXM_OF_ARP_SPA[[]],move:NXM_OF_ETH_SRC[[]]->NXM_OF_ETH_DST[[]],move:OXM_OF_PKT_REG0[[0..47]]->NXM_OF_ETH_SRC[[]],move:NXM_OF_IN_PORT[[]]->NXM_NX_REG3[[0..15]],load:0->NXM_OF_IN_PORT[[]],output:NXM_NX_REG3[[0..15]]
+table=10 priority=0 action=drop
+])
+
+AT_CHECK([ovs-ofctl -O OpenFlow15 add-flows br0 flows.txt])
+
+on_exit 'conntrack -L --output extended --any-nat'
+on_exit 'ovs-ofctl -O OpenFlow15 dump-flows br0'
+on_exit 'ovs-ofctl dump-ports br0'
+
+dnl Should work with the virtual IP address through NAT
+NETNS_DAEMONIZE([at_ns1], [[$PYTHON $srcdir/test-l7.py]], [http0.pid])
+NS_CHECK_EXEC([at_ns0], [wget 10.1.1.64 -t 5 -T 1 --retry-connrefused -v -o wget0.log])
+
+AT_CHECK([conntrack -L 2>&1 | FORMAT_CT(10.1.1.64) ], [0], [dnl
+TIME_WAIT src=10.1.1.1 dst=10.1.1.64 sport=<cleared> dport=<cleared> src=10.1.1.2 dst=10.1.1.1 sport=<cleared> dport=<cleared> [[ASSURED]] mark=0 zone=1 use=1
+])
+
+dnl Should work with the assigned IP address as well
+NS_CHECK_EXEC([at_ns0], [wget 10.1.1.2 -t 3 -T 1 --retry-connrefused -v -o wget0.log])
+
+AT_CHECK([conntrack -L 2>&1 | FORMAT_CT(10.1.1.2) ], [0], [dnl
+TIME_WAIT src=10.1.1.1 dst=10.1.1.2 sport=<cleared> dport=<cleared> src=10.1.1.2 dst=10.1.1.1 sport=<cleared> dport=<cleared> [[ASSURED]] mark=0 zone=1 use=1
+])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([conntrack - ICMP related with NAT])
+CHECK_CONNTRACK()
+OVS_TRAFFIC_VSWITCHD_START(
+   [set-fail-mode br0 standalone -- ])
+
+ADD_NAMESPACES(at_ns0, at_ns1)
+
+ADD_VETH(p0, at_ns0, br0, "10.1.1.1/24")
+NS_CHECK_EXEC([at_ns0], [ip link set dev p0 address 80:88:88:88:88:88])
+ADD_VETH(p1, at_ns1, br0, "10.1.1.2/24")
+
+dnl Allow UDP traffic from ns0->ns1. Only allow related ICMP responses back.
+dnl Make sure ICMP responses are reverse-NATted.
+AT_DATA([flows.txt], [dnl
+in_port=1,udp,action=ct(commit,nat(src=10.1.1.240-10.1.1.255),exec(set_field:1->ct_mark)),2
+in_port=2,icmp,ct_state=-trk,action=ct(table=0,nat)
+in_port=2,icmp,nw_dst=10.1.1.1,ct_state=+trk+rel,ct_mark=1,action=1
+dnl
+dnl ARP
+priority=100 arp arp_op=1 action=move:OXM_OF_ARP_TPA[[]]->NXM_NX_REG2[[]],resubmit(,8),goto_table:10
+priority=10 arp action=normal
+priority=0,action=drop
+dnl
+dnl MAC resolution table for IP in reg2, stores mac in OXM_OF_PKT_REG0
+table=8,reg2=0x0a0101f0/0xfffffff0,action=load:0x808888888888->OXM_OF_PKT_REG0[[]]
+table=8,priority=0,action=load:0->OXM_OF_PKT_REG0[[]]
+dnl ARP responder mac filled in at OXM_OF_PKT_REG0, or 0 for normal action.
+dnl TPA IP in reg2.
+dnl Swaps the fields of the ARP message to turn a query to a response.
+table=10 priority=100 arp xreg0=0 action=normal
+table=10 priority=10,arp,arp_op=1,action=load:2->OXM_OF_ARP_OP[[]],move:OXM_OF_ARP_SHA[[]]->OXM_OF_ARP_THA[[]],move:OXM_OF_PKT_REG0[[0..47]]->OXM_OF_ARP_SHA[[]],move:OXM_OF_ARP_SPA[[]]->OXM_OF_ARP_TPA[[]],move:NXM_NX_REG2[[]]->OXM_OF_ARP_SPA[[]],move:NXM_OF_ETH_SRC[[]]->NXM_OF_ETH_DST[[]],move:OXM_OF_PKT_REG0[[0..47]]->NXM_OF_ETH_SRC[[]],move:NXM_OF_IN_PORT[[]]->NXM_NX_REG3[[0..15]],load:0->NXM_OF_IN_PORT[[]],output:NXM_NX_REG3[[0..15]]
+table=10 priority=0 action=drop
+])
+
+AT_CHECK([ovs-ofctl -O OpenFlow15 add-flows br0 flows.txt])
+
+on_exit 'conntrack -L --output extended --any-nat'
+on_exit 'ovs-ofctl -O OpenFlow15 dump-flows br0'
+on_exit 'ovs-ofctl dump-ports br0'
+on_exit 'ovs-appctl dpctl/dump-flows'
+
+dnl UDP packets from ns0->ns1 should solicit "destination unreachable" response.
+dnl We pass "-q 1" here to handle openbsd-style nc that can't quit immediately.
+NS_CHECK_EXEC([at_ns0], [bash -c "echo a | nc -q 1 -u 10.1.1.2 10000"])
+
+AT_CHECK([ovs-appctl revalidator/purge], [0])
+AT_CHECK([ovs-ofctl -O OpenFlow15 dump-flows br0 | ofctl_strip | sort | grep -v drop], [0], [dnl
+ n_packets=1, n_bytes=42, priority=10,arp actions=NORMAL
+ n_packets=1, n_bytes=44, udp,in_port=1 actions=ct(commit,nat(src=10.1.1.240-10.1.1.255),exec(set_field:0x1->ct_mark)),output:2
+ n_packets=1, n_bytes=72, ct_state=+rel+trk,ct_mark=0x1,icmp,in_port=2,nw_dst=10.1.1.1 actions=output:1
+ n_packets=1, n_bytes=72, ct_state=-trk,icmp,in_port=2 actions=ct(table=0,nat)
+ n_packets=2, n_bytes=84, priority=100,arp,arp_op=1 actions=move:NXM_OF_ARP_TPA[[]]->NXM_NX_REG2[[]],resubmit(,8),goto_table:10
+ table=10, n_packets=1, n_bytes=42, priority=10,arp,arp_op=1 actions=set_field:2->arp_op,move:NXM_NX_ARP_SHA[[]]->NXM_NX_ARP_THA[[]],move:OXM_OF_PKT_REG0[[0..47]]->NXM_NX_ARP_SHA[[]],move:NXM_OF_ARP_SPA[[]]->NXM_OF_ARP_TPA[[]],move:NXM_NX_REG2[[]]->NXM_OF_ARP_SPA[[]],move:NXM_OF_ETH_SRC[[]]->NXM_OF_ETH_DST[[]],move:OXM_OF_PKT_REG0[[0..47]]->NXM_OF_ETH_SRC[[]],move:NXM_OF_IN_PORT[[]]->NXM_NX_REG3[[0..15]],set_field:0->in_port,output:NXM_NX_REG3[[0..15]]
+ table=10, n_packets=1, n_bytes=42, priority=100,arp,reg0=0,reg1=0 actions=NORMAL
+ table=8, n_packets=1, n_bytes=42, priority=0 actions=set_field:0->xreg0
+ table=8, n_packets=1, n_bytes=42, reg2=0xa0101f0/0xfffffff0 actions=set_field:0x808888888888->xreg0
+OFPST_FLOW reply (OF1.5):
+])
+
+AT_CHECK([conntrack -L 2>&1 | FORMAT_CT(10.1.1.2) | sed -e 's/dst=10.1.1.2[[45]][[0-9]]/dst=10.1.1.2XX/'], [0], [dnl
+src=10.1.1.1 dst=10.1.1.2 sport=<cleared> dport=<cleared> [[UNREPLIED]] src=10.1.1.2 dst=10.1.1.2XX sport=<cleared> dport=<cleared> mark=1 use=1
+])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
+
+
+AT_SETUP([conntrack - FTP with NAT])
+AT_SKIP_IF([test $HAVE_PYFTPDLIB = no])
+CHECK_CONNTRACK()
+OVS_TRAFFIC_VSWITCHD_START(
+   [set-fail-mode br0 standalone -- ])
+
+ADD_NAMESPACES(at_ns0, at_ns1)
+
+ADD_VETH(p0, at_ns0, br0, "10.1.1.1/24")
+NS_CHECK_EXEC([at_ns0], [ip link set dev p0 address 80:88:88:88:88:88])
+ADD_VETH(p1, at_ns1, br0, "10.1.1.2/24")
+
+dnl Allow any traffic from ns0->ns1. Only allow nd, return traffic from ns1->ns0.
+
+AT_DATA([flows.txt], [dnl
+dnl track all IP traffic, de-mangle non-NEW connections
+table=0 in_port=1, ip, action=ct(table=1,nat)
+table=0 in_port=2, ip, action=ct(table=2,nat)
+dnl
+dnl ARP
+dnl
+table=0 priority=100 arp arp_op=1 action=move:OXM_OF_ARP_TPA[[]]->NXM_NX_REG2[[]],resubmit(,8),goto_table:10
+table=0 priority=10 arp action=normal
+table=0 priority=0 action=drop
+dnl
+dnl Table 1: port 1 -> 2
+dnl
+dnl Allow new FTP connections. These need to be commited.
+table=1 ct_state=+new, tcp, tp_dst=21, action=ct(alg=ftp,commit,nat(src=10.1.1.240)),2
+dnl Allow established TCP connections
+table=1 ct_state=+est, tcp,     action=2
+dnl
+dnl Table 1: droppers
+dnl
+table=1 priority=10, tcp, action=drop
+table=1 priority=0,action=drop
+dnl
+dnl Table 2: port 2 -> 1
+dnl
+dnl Allow established TCP connections
+table=2 ct_state=+est, tcp, action=1
+dnl Allow (new) related (data) connections.  These need to be commited.
+table=2 ct_state=+new+rel, tcp, action=ct(commit,nat),1
+dnl Allow related ICMP packets, make sure they are reverse NATted
+table=2 ct_state=+rel, icmp, nw_dst=10.1.1.1, action=1
+dnl
+dnl Table 2: droppers
+dnl
+table=2 priority=10, tcp, action=drop
+table=2 priority=0, action=drop
+dnl
+dnl MAC resolution table for IP in reg2, stores mac in OXM_OF_PKT_REG0
+dnl
+table=8,reg2=0x0a0101f0/0xfffffff0,action=load:0x808888888888->OXM_OF_PKT_REG0[[]]
+table=8,priority=0,action=load:0->OXM_OF_PKT_REG0[[]]
+dnl ARP responder mac filled in at OXM_OF_PKT_REG0, or 0 for normal action.
+dnl TPA IP in reg2.
+dnl Swaps the fields of the ARP message to turn a query to a response.
+table=10 priority=100 arp xreg0=0 action=normal
+table=10 priority=10,arp,arp_op=1,action=load:2->OXM_OF_ARP_OP[[]],move:OXM_OF_ARP_SHA[[]]->OXM_OF_ARP_THA[[]],move:OXM_OF_PKT_REG0[[0..47]]->OXM_OF_ARP_SHA[[]],move:OXM_OF_ARP_SPA[[]]->OXM_OF_ARP_TPA[[]],move:NXM_NX_REG2[[]]->OXM_OF_ARP_SPA[[]],move:NXM_OF_ETH_SRC[[]]->NXM_OF_ETH_DST[[]],move:OXM_OF_PKT_REG0[[0..47]]->NXM_OF_ETH_SRC[[]],move:NXM_OF_IN_PORT[[]]->NXM_NX_REG3[[0..15]],load:0->NXM_OF_IN_PORT[[]],output:NXM_NX_REG3[[0..15]]
+table=10 priority=0 action=drop
+])
+
+AT_CHECK([ovs-ofctl -O OpenFlow15 add-flows br0 flows.txt])
+
+on_exit 'conntrack -L --output extended --any-nat'
+on_exit 'ovs-ofctl -O OpenFlow15 dump-flows br0'
+on_exit 'ovs-ofctl dump-ports br0'
+on_exit 'ovs-appctl dpctl/dump-flows'
+
+dnl NETNS_DAEMONIZE([at_ns0], [[$PYTHON $srcdir/test-l7.py ftp]], [ftp1.pid])
+NETNS_DAEMONIZE([at_ns1], [[$PYTHON $srcdir/test-l7.py ftp]], [ftp0.pid])
+
+dnl FTP requests from p0->p1 should work fine.
+NS_CHECK_EXEC([at_ns0], [wget ftp://10.1.1.2 -4 --no-passive-ftp -t 3 -T 1 --retry-connrefused -v --server-response --no-proxy --no-remove-listing -o wget0.log -d])
+
+AT_CHECK([conntrack -L 2>&1 | FORMAT_CT(10.1.1.2) | grep -v "FIN"], [0], [dnl
+TIME_WAIT src=10.1.1.1 dst=10.1.1.2 sport=<cleared> dport=<cleared> src=10.1.1.2 dst=10.1.1.240 sport=<cleared> dport=<cleared> [[ASSURED]] mark=0 helper=ftp use=2
+TIME_WAIT src=10.1.1.2 dst=10.1.1.240 sport=<cleared> dport=<cleared> src=10.1.1.1 dst=10.1.1.2 sport=<cleared> dport=<cleared> [[ASSURED]] mark=0 use=1
+])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
+
+
+AT_SETUP([conntrack - FTP with NAT 2])
+AT_SKIP_IF([test $HAVE_PYFTPDLIB = no])
+CHECK_CONNTRACK()
+OVS_TRAFFIC_VSWITCHD_START(
+   [set-fail-mode br0 standalone -- ])
+
+ADD_NAMESPACES(at_ns0, at_ns1)
+
+ADD_VETH(p0, at_ns0, br0, "10.1.1.1/24")
+NS_CHECK_EXEC([at_ns0], [ip link set dev p0 address 80:88:88:88:88:88])
+ADD_VETH(p1, at_ns1, br0, "10.1.1.2/24")
+
+dnl Allow any traffic from ns0->ns1.
+dnl Only allow nd, return traffic from ns1->ns0.
+AT_DATA([flows.txt], [dnl
+dnl track all IP traffic (this includes a helper call to non-NEW packets.)
+table=0 ip, action=ct(table=1)
+dnl
+dnl ARP
+dnl
+table=0 priority=100 arp arp_op=1 action=move:OXM_OF_ARP_TPA[[]]->NXM_NX_REG2[[]],resubmit(,8),goto_table:10
+table=0 priority=10 arp action=normal
+table=0 priority=0 action=drop
+dnl
+dnl Table 1
+dnl
+dnl Allow new FTP connections. These need to be commited.
+dnl This does helper for new packets.
+table=1 in_port=1 ct_state=+new, tcp, tp_dst=21, action=ct(alg=ftp,commit,nat(src=10.1.1.240)),2
+dnl Allow and NAT established TCP connections
+table=1 in_port=1 ct_state=+est, tcp,     action=ct(nat),2
+table=1 in_port=2 ct_state=+est, tcp,     action=ct(nat),1
+dnl Allow and NAT (new) related active (data) connections.
+dnl These need to be commited.
+table=1 in_port=2 ct_state=+new+rel, tcp, action=ct(commit,nat),1
+dnl Allow related ICMP packets.
+table=1 in_port=2 ct_state=+rel, icmp,    action=ct(nat),1
+dnl Drop everything else.
+table=1 priority=0, action=drop
+dnl
+dnl MAC resolution table for IP in reg2, stores mac in OXM_OF_PKT_REG0
+dnl
+table=8,reg2=0x0a0101f0/0xfffffff0,action=load:0x808888888888->OXM_OF_PKT_REG0[[]]
+table=8,priority=0,action=load:0->OXM_OF_PKT_REG0[[]]
+dnl ARP responder mac filled in at OXM_OF_PKT_REG0, or 0 for normal action.
+dnl TPA IP in reg2.
+dnl Swaps the fields of the ARP message to turn a query to a response.
+table=10 priority=100 arp xreg0=0 action=normal
+table=10 priority=10,arp,arp_op=1,action=load:2->OXM_OF_ARP_OP[[]],move:OXM_OF_ARP_SHA[[]]->OXM_OF_ARP_THA[[]],move:OXM_OF_PKT_REG0[[0..47]]->OXM_OF_ARP_SHA[[]],move:OXM_OF_ARP_SPA[[]]->OXM_OF_ARP_TPA[[]],move:NXM_NX_REG2[[]]->OXM_OF_ARP_SPA[[]],move:NXM_OF_ETH_SRC[[]]->NXM_OF_ETH_DST[[]],move:OXM_OF_PKT_REG0[[0..47]]->NXM_OF_ETH_SRC[[]],move:NXM_OF_IN_PORT[[]]->NXM_NX_REG3[[0..15]],load:0->NXM_OF_IN_PORT[[]],output:NXM_NX_REG3[[0..15]]
+table=10 priority=0 action=drop
+])
+
+AT_CHECK([ovs-ofctl -O OpenFlow15 add-flows br0 flows.txt])
+
+on_exit 'conntrack -L --output extended --any-nat'
+on_exit 'ovs-ofctl -O OpenFlow15 dump-flows br0'
+on_exit 'ovs-ofctl dump-ports br0'
+on_exit 'ovs-appctl dpctl/dump-flows'
+
+NETNS_DAEMONIZE([at_ns1], [[$PYTHON $srcdir/test-l7.py ftp]], [ftp0.pid])
+
+dnl FTP requests from p0->p1 should work fine.
+NS_CHECK_EXEC([at_ns0], [wget ftp://10.1.1.2 -4 --no-passive-ftp -t 3 -T 1 --retry-connrefused -v --server-response --no-proxy --no-remove-listing -o wget0.log -d])
+
+AT_CHECK([conntrack -L 2>&1 | FORMAT_CT(10.1.1.2) | grep -v "FIN" | grep -v "CLOSE"], [0], [dnl
+TIME_WAIT src=10.1.1.1 dst=10.1.1.2 sport=<cleared> dport=<cleared> src=10.1.1.2 dst=10.1.1.240 sport=<cleared> dport=<cleared> [[ASSURED]] mark=0 helper=ftp use=2
+TIME_WAIT src=10.1.1.2 dst=10.1.1.240 sport=<cleared> dport=<cleared> src=10.1.1.1 dst=10.1.1.2 sport=<cleared> dport=<cleared> [[ASSURED]] mark=0 use=1
+])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
