diff mbox

[ovs-dev,PATCHv3,09/11] Add support for connection tracking helper/ALGs.

Message ID 1443559234-7330-10-git-send-email-joestringer@nicira.com
State Superseded
Headers show

Commit Message

Joe Stringer Sept. 29, 2015, 8:40 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:

    table=0,priority=1,action=drop
    table=0,arp,action=normal
    table=0,in_port=1,tcp,action=ct(alg=ftp,commit),2
    table=0,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>
---
v3:
- Simplify action formatting using ds_chomp
- Improve documentation
- Add openflow wire format tests
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                                    |  26 +++-
 lib/ofp-actions.c                                 |  21 +++-
 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/ofp-actions.at                              |   3 +
 tests/system-traffic.at                           | 145 ++++++++++++++++++++++
 utilities/ovs-ofctl.8.in                          |  10 ++
 16 files changed, 263 insertions(+), 2 deletions(-)

Comments

Ben Pfaff Sept. 30, 2015, 3:58 p.m. UTC | #1
On Tue, Sep 29, 2015 at 01:40:32PM -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:
> 
>     table=0,priority=1,action=drop
>     table=0,arp,action=normal
>     table=0,in_port=1,tcp,action=ct(alg=ftp,commit),2
>     table=0,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>

I expected ALGs to be more complicated.  I guess the kernel does all the
heavy lifting for us!  Thanks for writing this.

In put_ct_helper(), this:
            const char *helper = "ftp";

            nl_msg_put_string__(odp_actions, OVS_CT_ATTR_HELPER, helper,
                                strlen(helper));
could be written as:
            nl_msg_put_string(odp_actions, OVS_CT_ATTR_HELPER, "ftp");

In ovs-ofctl.8.in here:
    .IP \fBalg=\fR\fIalg\fR
the \fR in the middle could be removed:
    .IP \fBalg=\fIalg\fR

Acked-by: Ben Pfaff <blp@nicira.com>
Joe Stringer Sept. 30, 2015, 4:56 p.m. UTC | #2
On 30 September 2015 at 08:58, Ben Pfaff <blp@nicira.com> wrote:
> On Tue, Sep 29, 2015 at 01:40:32PM -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:
>>
>>     table=0,priority=1,action=drop
>>     table=0,arp,action=normal
>>     table=0,in_port=1,tcp,action=ct(alg=ftp,commit),2
>>     table=0,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>
>
> I expected ALGs to be more complicated.  I guess the kernel does all the
> heavy lifting for us!  Thanks for writing this.

Sure thing. Indeed the complexity is in the kernel.

> In put_ct_helper(), this:
>             const char *helper = "ftp";
>
>             nl_msg_put_string__(odp_actions, OVS_CT_ATTR_HELPER, helper,
>                                 strlen(helper));
> could be written as:
>             nl_msg_put_string(odp_actions, OVS_CT_ATTR_HELPER, "ftp");
>
> In ovs-ofctl.8.in here:
>     .IP \fBalg=\fR\fIalg\fR
> the \fR in the middle could be removed:
>     .IP \fBalg=\fIalg\fR
>
> Acked-by: Ben Pfaff <blp@nicira.com>

Thanks, I'll fix these up.
diff mbox

Patch

diff --git a/datapath/linux/compat/include/linux/openvswitch.h b/datapath/linux/compat/include/linux/openvswitch.h
index 9881c77..3c5a697 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 2694bbf..f75fc34 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;
@@ -544,6 +545,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
@@ -552,6 +555,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;
 
@@ -564,9 +568,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,");
@@ -585,6 +590,9 @@  format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
             ds_put_hex(ds, (label + 1), sizeof(*label));
             ds_put_char(ds, ',');
         }
+        if (helper) {
+            ds_put_format(ds, "helper=%s,", helper);
+        }
         ds_chomp(ds, ',');
         ds_put_cstr(ds, ")");
     }
@@ -1044,6 +1052,8 @@  parse_conntrack_action(const char *s_, struct ofpbuf *actions)
     const char *s = s_;
 
     if (ovs_scan(s, "ct")) {
+        const char *helper = NULL;
+        size_t helper_len = 0;
         uint32_t flags = 0;
         uint16_t zone = 0;
         struct {
@@ -1103,6 +1113,16 @@  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;
+                    continue;
+                }
 
                 return -EINVAL;
             }
@@ -1124,6 +1144,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 dd0c7cc..97447fc 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"
@@ -4696,7 +4698,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. */
 };
@@ -4746,6 +4750,7 @@  decode_NXAST_RAW_CT(const struct nx_action_conntrack *nac,
         goto out;
     }
     conntrack->recirc_table = nac->recirc_table;
+    conntrack->alg = ntohs(nac->alg);
 
     ofpbuf_pull(out, sizeof(*conntrack));
 
@@ -4793,6 +4798,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),
@@ -4837,6 +4843,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. */
@@ -4860,6 +4868,16 @@  parse_CT(char *arg, struct ofpbuf *ofpacts,
 }
 
 static void
+format_alg(int port, struct ds *s)
+{
+    if (port == IPPORT_FTP) {
+        ds_put_format(s, "alg=ftp,");
+    } else if (port) {
+        ds_put_format(s, "alg=%d,", port);
+    }
+}
+
+static void
 format_CT(const struct ofpact_conntrack *a, struct ds *s)
 {
     ds_put_cstr(s, "ct(");
@@ -4881,6 +4899,7 @@  format_CT(const struct ofpact_conntrack *a, struct ds *s)
         ofpacts_format(a->actions, ofpact_ct_get_action_len(a), s);
         ds_put_format(s, "),");
     }
+    format_alg(a->alg, s);
     ds_chomp(s, ',');
     ds_put_char(s, ')');
 }
diff --git a/lib/ofp-actions.h b/lib/ofp-actions.h
index a0e270c..1311a55 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 5628b64..a7ac3e1 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 c09d24c..fe61886 100644
--- a/tests/odp.at
+++ b/tests/odp.at
@@ -311,6 +311,7 @@  ct(commit)
 ct(commit,zone=5)
 ct(commit,mark=0xa0a0a0a0/0xfefefefe)
 ct(commit,label=0x1234567890abcdef1234567890abcdef/0xf1f2f3f4f5f6f7f8f9f0fafbfcfdfeff)
+ct(commit,helper=ftp)
 ])
 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 fd48a8b..01e5b67 100644
--- a/tests/ofp-actions.at
+++ b/tests/ofp-actions.at
@@ -184,6 +184,9 @@  ffff 0018 00002320 0023 0000 00000000 0000 FF 000000 0000
 ffff 0030 00002320 0023 0001 00000000 0000 FF 000000 0000 dnl
 ffff 0018 00002320 0007 001f 00010004 000000000000f009
 
+# actions=ct(alg=ftp)
+ffff 0018 00002320 0023 0000 00000000 0000 FF 000000 0015
+
 ])
 sed '/^[[#&]]/d' < test-data > input.txt
 sed -n 's/^# //p; /^$/p' < test-data > expout
diff --git a/tests/system-traffic.at b/tests/system-traffic.at
index 71440cc..f6d0bdd 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 fcb47d8..3273885 100644
--- a/utilities/ovs-ofctl.8.in
+++ b/utilities/ovs-ofctl.8.in
@@ -1692,6 +1692,16 @@  connection tracker with the \fBtable\fR specified.
 .IP
 The \fBcommit\fR parameter must be specified to use \fBexec(...)\fR.
 .
+.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 set the
+\fBrel\fR flag in the \fBct_state\fR field for packets sent through \fBct\fR.
+.RE
+.
 .RE
 .IP
 The \fBct\fR action may be used as a primitive to construct stateful firewalls