@@ -671,6 +671,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,
@@ -678,6 +679,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
};
@@ -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
@@ -18,5 +18,6 @@
#define __NETINET_IN_H 1
#define IPPROTO_GRE 47
+#define IPPORT_FTP 21
#endif /* netinet/in.h */
@@ -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'. */
@@ -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);
@@ -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;
@@ -548,6 +549,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
@@ -556,6 +559,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;
uint16_t zone;
bool commit;
@@ -568,9 +572,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 (commit || zone || mark || label) {
+ if (commit || zone || mark || label || helper) {
ds_put_cstr(ds, "(");
if (commit) {
ds_put_format(ds, "commit,");
@@ -586,6 +591,9 @@ format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
ds_put_format(ds, "label=");
format_u128(ds, label, label + 1, true);
}
+ if (helper) {
+ ds_put_format(ds, "helper=%s,", helper);
+ }
ds_chomp(ds, ',');
ds_put_cstr(ds, ")");
}
@@ -1027,6 +1035,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;
bool commit = false;
uint16_t zone = 0;
struct {
@@ -1084,6 +1094,16 @@ parse_conntrack_action(const char *s_, struct ofpbuf *actions)
s += retval;
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;
}
@@ -1105,6 +1125,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);
}
@@ -15,6 +15,8 @@
*/
#include <config.h>
+#include <netinet/in.h>
+
#include "ofp-actions.h"
#include "bundle.h"
#include "byte-order.h"
@@ -4680,7 +4682,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. */
};
@@ -4730,6 +4734,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));
@@ -4777,6 +4782,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),
@@ -4821,6 +4827,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. */
@@ -4844,6 +4852,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(");
@@ -4865,6 +4883,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, ')');
}
@@ -496,6 +496,7 @@ struct { \
uint16_t flags; \
uint16_t zone_imm; \
struct mf_subfield zone_src; \
+ uint16_t alg; \
uint8_t recirc_table; \
}
@@ -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;
@@ -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 */
@@ -4184,6 +4184,18 @@ 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) {
+ nl_msg_put_string(odp_actions, OVS_CT_ATTR_HELPER, "ftp");
+ } else {
+ VLOG_WARN("Cannot serialize ct_helper %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;
@@ -4211,6 +4223,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
@@ -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
@@ -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`
@@ -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
@@ -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
@@ -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=\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