[ovs-dev,PATCHv2,5/6] Add support for connection tracking helper/ALGs.
diff mbox

Message ID 1442531068-41423-6-git-send-email-joestringer@nicira.com
State Changes Requested
Headers show

Commit Message

Joe Stringer Sept. 17, 2015, 11:04 p.m. UTC
This patch adds support for specifying a "helper" or ALG to assist
connection tracking for protocols that consist of multiple streams.
Initially, only support for FTP is included.

Below is an example set of flows to allow FTP control connections from
port 1->2 to establish active data connections in the reverse direction:

    priority=1,action=drop
    priority=10,arp,action=normal
    in_port=1,tcp,action=ct(alg=ftp,commit),2
    in_port=2,tcp,ct_state=-trk,action=ct(table=1)
    table=1,in_port=2,tcp,ct_state=+trk+est,action=1
    table=1,in_port=2,tcp,ct_state=+trk+rel,action=ct(commit),1

Signed-off-by: Joe Stringer <joestringer@nicira.com>
Acked-by: Jarno Rajahalme <jrajahalme@nicira.com>
---
v2: Address feedback from v1
---
 datapath/linux/compat/include/linux/openvswitch.h |   3 +
 include/sparse/netinet/in.h                       |   2 +
 include/windows/netinet/in.h                      |   1 +
 lib/netlink.c                                     |  11 ++
 lib/netlink.h                                     |   2 +
 lib/odp-util.c                                    |  28 ++++-
 lib/ofp-actions.c                                 |  24 +++-
 lib/ofp-actions.h                                 |   1 +
 lib/ofp-parse.c                                   |  15 +++
 lib/ofp-parse.h                                   |   1 +
 ofproto/ofproto-dpif-xlate.c                      |  16 +++
 tests/atlocal.in                                  |   7 ++
 tests/odp.at                                      |   1 +
 tests/system-traffic.at                           | 145 ++++++++++++++++++++++
 utilities/ovs-ofctl.8.in                          |  11 ++
 15 files changed, 266 insertions(+), 2 deletions(-)

Comments

Ben Pfaff Sept. 18, 2015, 5:53 p.m. UTC | #1
On Thu, Sep 17, 2015 at 04:04:27PM -0700, Joe Stringer wrote:
> This patch adds support for specifying a "helper" or ALG to assist
> connection tracking for protocols that consist of multiple streams.
> Initially, only support for FTP is included.
> 
> Below is an example set of flows to allow FTP control connections from
> port 1->2 to establish active data connections in the reverse direction:
> 
>     priority=1,action=drop
>     priority=10,arp,action=normal
>     in_port=1,tcp,action=ct(alg=ftp,commit),2
>     in_port=2,tcp,ct_state=-trk,action=ct(table=1)
>     table=1,in_port=2,tcp,ct_state=+trk+est,action=1
>     table=1,in_port=2,tcp,ct_state=+trk+rel,action=ct(commit),1
> 
> Signed-off-by: Joe Stringer <joestringer@nicira.com>
> Acked-by: Jarno Rajahalme <jrajahalme@nicira.com>

Acked-by: Ben Pfaff <blp@nicira.com>

Patch
diff mbox

diff --git a/datapath/linux/compat/include/linux/openvswitch.h b/datapath/linux/compat/include/linux/openvswitch.h
index ddbeb32..a72d0cf 100644
--- a/datapath/linux/compat/include/linux/openvswitch.h
+++ b/datapath/linux/compat/include/linux/openvswitch.h
@@ -669,6 +669,7 @@  struct ovs_action_push_tnl {
  * @OVS_CT_ATTR_LABEL: %OVS_CT_LABEL_LEN value followed by %OVS_CT_LABEL_LEN
  * mask. For each bit set in the mask, the corresponding bit in the value is
  * copied to the connection tracking label field in the connection.
+ * @OVS_CT_ATTR_HELPER: variable length string defining conntrack ALG.
  */
 enum ovs_ct_attr {
 	OVS_CT_ATTR_UNSPEC,
@@ -676,6 +677,8 @@  enum ovs_ct_attr {
 	OVS_CT_ATTR_ZONE,       /* u16 zone id. */
 	OVS_CT_ATTR_MARK,       /* mark to associate with this connection. */
 	OVS_CT_ATTR_LABEL,      /* label to associate with this connection. */
+	OVS_CT_ATTR_HELPER,     /* netlink helper to assist detection of
+				   related connections. */
 	__OVS_CT_ATTR_MAX
 };
 
diff --git a/include/sparse/netinet/in.h b/include/sparse/netinet/in.h
index f66f205..1223553 100644
--- a/include/sparse/netinet/in.h
+++ b/include/sparse/netinet/in.h
@@ -74,6 +74,8 @@  struct sockaddr_in6 {
 #define IPPROTO_DSTOPTS 60
 #define IPPROTO_SCTP 132
 
+#define IPPORT_FTP 21
+
 /* All the IP options documented in Linux ip(7). */
 #define IP_ADD_MEMBERSHIP 0
 #define IP_DROP_MEMBERSHIP 1
diff --git a/include/windows/netinet/in.h b/include/windows/netinet/in.h
index 7143cf5..e416999 100644
--- a/include/windows/netinet/in.h
+++ b/include/windows/netinet/in.h
@@ -18,5 +18,6 @@ 
 #define __NETINET_IN_H 1
 
 #define IPPROTO_GRE 47
+#define IPPORT_FTP 21
 
 #endif /* netinet/in.h */
diff --git a/lib/netlink.c b/lib/netlink.c
index 09723b2..66b4927 100644
--- a/lib/netlink.c
+++ b/lib/netlink.c
@@ -316,6 +316,17 @@  nl_msg_put_odp_port(struct ofpbuf *msg, uint16_t type, odp_port_t value)
     nl_msg_put_u32(msg, type, odp_to_u32(value));
 }
 
+/* Appends a Netlink attribute of the given 'type' with the 'len' characters
+ * of 'value', followed by the null byte to 'msg'. */
+void
+nl_msg_put_string__(struct ofpbuf *msg, uint16_t type, const char *value,
+                    size_t len)
+{
+    char *data = nl_msg_put_unspec_uninit(msg, type, len + 1);
+
+    memcpy(data, value, len);
+    data[len] = '\0';
+}
 
 /* Appends a Netlink attribute of the given 'type' and the given
  * null-terminated string 'value' to 'msg'. */
diff --git a/lib/netlink.h b/lib/netlink.h
index 6068f5d..210cab5 100644
--- a/lib/netlink.h
+++ b/lib/netlink.h
@@ -70,6 +70,8 @@  void nl_msg_put_be16(struct ofpbuf *, uint16_t type, ovs_be16 value);
 void nl_msg_put_be32(struct ofpbuf *, uint16_t type, ovs_be32 value);
 void nl_msg_put_be64(struct ofpbuf *, uint16_t type, ovs_be64 value);
 void nl_msg_put_odp_port(struct ofpbuf *, uint16_t type, odp_port_t value);
+void nl_msg_put_string__(struct ofpbuf *, uint16_t type, const char *value,
+                         size_t len);
 void nl_msg_put_string(struct ofpbuf *, uint16_t type, const char *value);
 
 size_t nl_msg_start_nested(struct ofpbuf *, uint16_t type);
diff --git a/lib/odp-util.c b/lib/odp-util.c
index 2dd23b0..bd0d7e7 100644
--- a/lib/odp-util.c
+++ b/lib/odp-util.c
@@ -52,6 +52,7 @@  VLOG_DEFINE_THIS_MODULE(odp_util);
 /* The set of characters that may separate one action or one key attribute
  * from another. */
 static const char *delimiters = ", \t\r\n";
+static const char *delimiters_end = ", \t\r\n)";
 
 struct attr_len_tbl {
     int len;
@@ -546,6 +547,8 @@  static const struct nl_policy ovs_conntrack_policy[] = {
                            .min_len = sizeof(uint32_t) * 2 },
     [OVS_CT_ATTR_LABEL] = { .type = NL_A_UNSPEC, .optional = true,
                             .min_len = sizeof(ovs_u128) * 2 },
+    [OVS_CT_ATTR_HELPER] = { .type = NL_A_STRING, .optional = true,
+                             .min_len = 1, .max_len = 16 },
 };
 
 static void
@@ -554,6 +557,7 @@  format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
     struct nlattr *a[ARRAY_SIZE(ovs_conntrack_policy)];
     const ovs_u128 *label;
     const uint32_t *mark;
+    const char *helper;
     uint32_t flags;
     uint16_t zone;
 
@@ -566,9 +570,10 @@  format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
     zone = a[OVS_CT_ATTR_ZONE] ? nl_attr_get_u16(a[OVS_CT_ATTR_ZONE]) : 0;
     mark = a[OVS_CT_ATTR_MARK] ? nl_attr_get(a[OVS_CT_ATTR_MARK]) : NULL;
     label = a[OVS_CT_ATTR_LABEL] ? nl_attr_get(a[OVS_CT_ATTR_LABEL]): NULL;
+    helper = a[OVS_CT_ATTR_HELPER] ? nl_attr_get(a[OVS_CT_ATTR_HELPER]) : NULL;
 
     ds_put_format(ds, "ct");
-    if (flags || zone || mark || label) {
+    if (flags || zone || mark || label || helper) {
         ds_put_cstr(ds, "(");
         if (flags & OVS_CT_F_COMMIT) {
             ds_put_format(ds, "commit");
@@ -595,6 +600,12 @@  format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
             ds_put_char(ds, '/');
             ds_put_hex(ds, (label + 1), sizeof(*label));
         }
+        if (helper) {
+            if (ds_last(ds) != '(') {
+                ds_put_char(ds, ',');
+            }
+            ds_put_format(ds, "helper=%s", helper);
+        }
         ds_put_cstr(ds, ")");
     }
 }
@@ -1052,6 +1063,7 @@  parse_conntrack_action(const char *s_, struct ofpbuf *actions)
     const char *s = s_;
 
     if (ovs_scan(s, "ct")) {
+        const char *helper = NULL;
         uint32_t flags = 0;
         uint16_t zone = 0;
         struct {
@@ -1062,6 +1074,7 @@  parse_conntrack_action(const char *s_, struct ofpbuf *actions)
             ovs_u128 value;
             ovs_u128 mask;
         } ct_label;
+        size_t helper_len;
         size_t start;
         char *end;
 
@@ -1111,6 +1124,15 @@  parse_conntrack_action(const char *s_, struct ofpbuf *actions)
                     s = tail;
                     continue;
                 }
+                if (ovs_scan(s, "helper=%n", &n)) {
+                    s += n;
+                    helper_len = strcspn(s, delimiters_end);
+                    if (!helper_len || helper_len > 15) {
+                        return -EINVAL;
+                    }
+                    helper = s;
+                    s += helper_len;
+                }
 
                 if (n < 0) {
                     return -EINVAL;
@@ -1134,6 +1156,10 @@  parse_conntrack_action(const char *s_, struct ofpbuf *actions)
             nl_msg_put_unspec(actions, OVS_CT_ATTR_LABEL, &ct_label,
                               sizeof(ct_label));
         }
+        if (helper) {
+            nl_msg_put_string__(actions, OVS_CT_ATTR_HELPER, helper,
+                                helper_len);
+        }
         nl_msg_end_nested(actions, start);
     }
 
diff --git a/lib/ofp-actions.c b/lib/ofp-actions.c
index 36fa2c3..7632902 100644
--- a/lib/ofp-actions.c
+++ b/lib/ofp-actions.c
@@ -15,6 +15,8 @@ 
  */
 
 #include <config.h>
+#include <netinet/in.h>
+
 #include "ofp-actions.h"
 #include "bundle.h"
 #include "byte-order.h"
@@ -4564,7 +4566,9 @@  struct nx_action_conntrack {
     };
     uint8_t recirc_table;       /* Recirculate to a specific table, or
                                    NX_CT_RECIRC_NONE for no recirculation. */
-    uint8_t pad[5];             /* Zeroes */
+    uint8_t pad[3];             /* Zeroes */
+    ovs_be16 alg;               /* Well-known port number for the protocol.
+                                 * 0 indicates no ALG is required. */
     /* Followed by a sequence of ofpact elements, until the end of the action
      * is reached. */
 };
@@ -4613,6 +4617,7 @@  decode_NXAST_RAW_CT(const struct nx_action_conntrack *nac, struct ofpbuf *out)
         goto out;
     }
     conntrack->recirc_table = nac->recirc_table;
+    conntrack->alg = ntohs(nac->alg);
 
     ofpbuf_pull(out, sizeof(*conntrack));
 
@@ -4660,6 +4665,7 @@  encode_CT(const struct ofpact_conntrack *conntrack,
         nac->zone_imm = htons(conntrack->zone_imm);
     }
     nac->recirc_table = conntrack->recirc_table;
+    nac->alg = htons(conntrack->alg);
 
     len = ofpacts_put_openflow_actions(conntrack->actions,
                                        ofpact_ct_get_action_len(conntrack),
@@ -4701,6 +4707,8 @@  parse_CT(char *arg, struct ofpbuf *ofpacts,
                     return error;
                 }
             }
+        } else if (!strcmp(key, "alg")) {
+            error = str_to_connhelper(value, &oc->alg);
         } else if (!strcmp(key, "exec")) {
             /* Hide existing actions from ofpacts_parse_copy(), so the
              * nesting can be handled transparently. */
@@ -4734,6 +4742,18 @@  append_comma(struct ds *s, bool *first)
 }
 
 static void
+format_alg(int port, struct ds *s, bool *first)
+{
+    if (port == IPPORT_FTP) {
+        append_comma(s, first);
+        ds_put_format(s, "alg=ftp,");
+    } else if (port) {
+        append_comma(s, first);
+        ds_put_format(s, "alg=%d,", port);
+    }
+}
+
+static void
 format_CT(const struct ofpact_conntrack *a, struct ds *s)
 {
     bool first = true;
@@ -4747,6 +4767,7 @@  format_CT(const struct ofpact_conntrack *a, struct ds *s)
         append_comma(s, &first);
         ds_put_format(s, "table=%"PRIu8, a->recirc_table);
     }
+    format_alg(a->alg, s, &first);
     if (a->zone_src.field) {
         append_comma(s, &first);
         ds_put_format(s, "zone=");
@@ -4755,6 +4776,7 @@  format_CT(const struct ofpact_conntrack *a, struct ds *s)
         append_comma(s, &first);
         ds_put_format(s, "zone=%"PRIu16, a->zone_imm);
     }
+    format_alg(a->alg, s, &first);
     if (ofpact_ct_get_action_len(a)) {
         append_comma(s, &first);
         ds_put_cstr(s, "exec(");
diff --git a/lib/ofp-actions.h b/lib/ofp-actions.h
index 591d8a2..3951909 100644
--- a/lib/ofp-actions.h
+++ b/lib/ofp-actions.h
@@ -496,6 +496,7 @@  struct {                                \
     uint16_t flags;                     \
     uint16_t zone_imm;                  \
     struct mf_subfield zone_src;        \
+    uint16_t alg;                       \
     uint8_t recirc_table;               \
 }
 
diff --git a/lib/ofp-parse.c b/lib/ofp-parse.c
index 5950f06..8437656 100644
--- a/lib/ofp-parse.c
+++ b/lib/ofp-parse.c
@@ -21,6 +21,7 @@ 
 #include <ctype.h>
 #include <errno.h>
 #include <stdlib.h>
+#include <netinet/in.h>
 
 #include "byte-order.h"
 #include "dynamic-string.h"
@@ -168,6 +169,20 @@  str_to_ip(const char *str, ovs_be32 *ip)
     return NULL;
 }
 
+/* Parses 'str' as a conntrack helper into 'alg'.
+ *
+ * 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_connhelper(const char *str, uint16_t *alg)
+{
+    if (!strcmp(str, "ftp")) {
+        *alg = IPPORT_FTP;
+        return NULL;
+    }
+    return xasprintf("invalid conntrack helper \"%s\"", str);
+}
+
 struct protocol {
     const char *name;
     uint16_t dl_type;
diff --git a/lib/ofp-parse.h b/lib/ofp-parse.h
index b64a32e..36f9acc 100644
--- a/lib/ofp-parse.h
+++ b/lib/ofp-parse.h
@@ -99,5 +99,6 @@  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_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 49e1b77..320ce8b 100644
--- a/ofproto/ofproto-dpif-xlate.c
+++ b/ofproto/ofproto-dpif-xlate.c
@@ -4192,6 +4192,21 @@  put_ct_label(const struct flow *flow, struct flow *base_flow,
 }
 
 static void
+put_ct_helper(struct ofpbuf *odp_actions, struct ofpact_conntrack *ofc)
+{
+    if (ofc->alg) {
+        if (ofc->alg == IPPORT_FTP) {
+            const char *helper = "ftp";
+
+            nl_msg_put_string__(odp_actions, OVS_CT_ATTR_HELPER, helper,
+                                strlen(helper));
+        } else {
+            VLOG_WARN("Cannot serialize connhelper %d\n", ofc->alg);
+        }
+    }
+}
+
+static void
 compose_conntrack_action(struct xlate_ctx *ctx, struct ofpact_conntrack *ofc)
 {
     ovs_u128 old_ct_label = ctx->base_flow.ct_label;
@@ -4221,6 +4236,7 @@  compose_conntrack_action(struct xlate_ctx *ctx, struct ofpact_conntrack *ofc)
     nl_msg_put_u16(ctx->odp_actions, OVS_CT_ATTR_ZONE, zone);
     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);
     nl_msg_end_nested(ctx->odp_actions, ct_offset);
 
     /* Restore the original ct fields in the key. These should only be exposed
diff --git a/tests/atlocal.in b/tests/atlocal.in
index 8e9fd9b..095bc40 100644
--- a/tests/atlocal.in
+++ b/tests/atlocal.in
@@ -117,3 +117,10 @@  if test x`which conntrack` != x; then
 else
     HAVE_CONNTRACK="no"
 fi
+
+if test "$HAVE_PYTHON" = "yes" \
+   && test "x`$PYTHON $abs_top_srcdir/tests/test-l7.py --help | grep 'ftp'`" != x; then
+    HAVE_PYFTPDLIB="yes"
+else
+    HAVE_PYFTPDLIB="no"
+fi
diff --git a/tests/odp.at b/tests/odp.at
index 702d44c..e4e95a0 100644
--- a/tests/odp.at
+++ b/tests/odp.at
@@ -309,6 +309,7 @@  tnl_push(tnl_port(6),header(size=50,type=5,eth(dst=f8:bc:12:44:34:b6,src=f8:bc:1
 ct
 ct(commit)
 ct(commit,zone=5)
+ct(commit,helper=ftp)
 ])
 AT_CHECK_UNQUOTED([ovstest test-odp parse-actions < actions.txt], [0],
   [`cat actions.txt`
diff --git a/tests/system-traffic.at b/tests/system-traffic.at
index 03b9747..f873ff8 100644
--- a/tests/system-traffic.at
+++ b/tests/system-traffic.at
@@ -782,3 +782,148 @@  icmp,vlan_tci=0x0000,dl_src=c6:f9:4e:cb:72:db,dl_dst=e6:4c:47:35:28:c9,nw_src=17
 
 OVS_TRAFFIC_VSWITCHD_STOP
 AT_CLEANUP
+
+AT_SETUP([conntrack - FTP])
+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")
+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([flows1.txt], [dnl
+priority=1,action=drop
+priority=10,arp,action=normal
+priority=10,icmp,action=normal
+priority=100,in_port=1,tcp,action=ct(alg=ftp,commit),2
+priority=100,in_port=2,tcp,ct_state=-trk,action=ct(table=0)
+priority=100,in_port=2,tcp,ct_state=+trk+est,action=1
+priority=100,in_port=2,tcp,ct_state=+trk+rel,action=1
+])
+
+dnl Similar policy but without allowing all traffic from ns0->ns1.
+AT_DATA([flows2.txt], [dnl
+priority=1,action=drop
+priority=10,arp,action=normal
+priority=10,icmp,action=normal
+priority=100,in_port=1,tcp,ct_state=-trk,action=ct(table=0)
+priority=100,in_port=1,tcp,ct_state=+trk+new,action=ct(commit,alg=ftp),2
+priority=100,in_port=1,tcp,ct_state=+trk+est,action=2
+priority=100,in_port=2,tcp,ct_state=-trk,action=ct(table=0)
+priority=100,in_port=2,tcp,ct_state=+trk+new+rel,action=ct(commit),1
+priority=100,in_port=2,tcp,ct_state=+trk+est,action=1
+priority=100,in_port=2,tcp,ct_state=+trk-new+rel,action=1
+])
+
+AT_CHECK([ovs-ofctl add-flows br0 flows1.txt])
+
+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 p1->p0 should fail due to network failure.
+dnl Try 3 times, in 1 second intervals.
+NS_CHECK_EXEC([at_ns1], [wget ftp://10.1.1.1 --no-passive-ftp  -t 3 -T 1 -v -o wget1.log], [4])
+AT_CHECK([conntrack -L 2>&1 | FORMAT_CT(10.1.1.1)], [0], [dnl
+])
+
+dnl 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])
+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=1
+])
+
+dnl Try the second set of flows.
+conntrack -F
+AT_CHECK([ovs-ofctl del-flows br0])
+AT_CHECK([ovs-ofctl add-flows br0 flows2.txt])
+
+dnl FTP requests from p1->p0 should fail due to network failure.
+dnl Try 3 times, in 1 second intervals.
+NS_CHECK_EXEC([at_ns1], [wget ftp://10.1.1.1 --no-passive-ftp  -t 3 -T 1 -v -o wget1.log], [4])
+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])
+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
+])
+
+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])
+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
+])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
+
+AT_SETUP([conntrack - FTP with multiple expectations])
+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")
+ADD_VETH(p1, at_ns1, br0, "10.1.1.2/24")
+
+dnl Dual-firewall, allow all from ns1->ns2, allow established and ftp ns2->ns1.
+AT_DATA([flows.txt], [dnl
+priority=1,action=drop
+priority=10,arp,action=normal
+priority=10,icmp,action=normal
+priority=100,in_port=1,tcp,ct_state=-trk,action=ct(table=0,zone=1)
+priority=100,in_port=1,tcp,ct_zone=1,ct_state=+trk+new,action=ct(commit,alg=ftp,zone=1),ct(commit,alg=ftp,zone=2),2
+priority=100,in_port=1,tcp,ct_zone=1,ct_state=+trk+est,action=ct(table=0,zone=2)
+priority=100,in_port=1,tcp,ct_zone=2,ct_state=+trk+new,action=ct(commit,alg=ftp,zone=2)
+priority=100,in_port=1,tcp,ct_zone=2,ct_state=+trk+est,action=2
+priority=100,in_port=2,tcp,ct_state=-trk,action=ct(table=0,zone=2)
+priority=100,in_port=2,tcp,ct_zone=2,ct_state=+trk+rel,action=ct(commit,zone=2),ct(commit,zone=1),1
+priority=100,in_port=2,tcp,ct_zone=2,ct_state=+trk+est,action=ct(table=0,zone=1)
+priority=100,in_port=2,tcp,ct_zone=1,ct_state=+trk+rel,action=ct(commit,zone=2),ct(commit,zone=1),1
+priority=100,in_port=2,tcp,ct_zone=1,ct_state=+trk+est,action=1
+])
+
+AT_CHECK([ovs-ofctl add-flows br0 flows.txt])
+
+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 p1->p0 should fail due to network failure.
+dnl Try 3 times, in 1 second intervals.
+NS_CHECK_EXEC([at_ns1], [wget ftp://10.1.1.1 --no-passive-ftp  -t 3 -T 1 -v -o wget1.log], [4])
+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])
+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 zone=1 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 zone=2 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 zone=1 use=1
+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 zone=2 use=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])
+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 zone=1 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 zone=1 use=1
+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=2 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 zone=2 use=1
+])
+
+OVS_TRAFFIC_VSWITCHD_STOP
+AT_CLEANUP
diff --git a/utilities/ovs-ofctl.8.in b/utilities/ovs-ofctl.8.in
index 5058ab2..a378f52 100644
--- a/utilities/ovs-ofctl.8.in
+++ b/utilities/ovs-ofctl.8.in
@@ -1651,6 +1651,17 @@  Store a 128-bit metadata value with the connection. This will populate the
 action.
 .RE
 .
+.IP \fBalg=\fR\fIalg\fR
+Specify application layer gateway \fIalg\fR to track specific connection
+types. Supported types include:
+.
+.RS
+.IP \fBftp\fR
+Look for negotiation of FTP data connections. If a subsequent FTP data
+connection arrives which is related, the \fBct\fR action will mark such
+packets with the \fBrel\fR flag in the \fBct_state\fR field.
+.RE
+.
 .RE
 .IP
 Currently, connection tracking is only available on Linux kernels with the