[ovs-dev] [RFCv4 08/10] userspace: Add support for conntrack ALGs.

Joe Stringer joestringer at nicira.com
Thu Apr 30 19:38:42 UTC 2015


Add the ability to specify ALG ("helper") modules to assist connection tracking
for protocols which use multiple connections. The alg only needs to be
specified when committing the initial connection. It will be reused each time
packets for this connection are subsequently passed through conntrack. Note
that to track related connections, they must be committed separately from the
initial connection.

Here's a simple example flow table to allow outbound TCP traffic from port 1;
Use the FTP helper module to determine related FTP connections; and allow both
FTP control and data connections from port 2:

priority=10,arp,action=normal
in_port=1,tcp,action=ct(alg=ftp,commit),2
in_port=2,tcp,conn_state=-trk,action=ct(recirc)
in_port=2,tcp,conn_state=+trk-new+est,action=1
in_port=2,tcp,conn_state=+trk+new+rel,action=ct(commit),1
in_port=2,tcp,conn_state=+trk-new+est+rel,action=1

Only FTP is supported at this stage.

Signed-off-by: Joe Stringer <joestringer at nicira.com>
---
XXX: Probe for supported ALGs first.
---
 datapath/linux/compat/include/linux/openvswitch.h |    2 +
 lib/netlink.c                                     |   11 +++
 lib/netlink.h                                     |    2 +
 lib/odp-util.c                                    |   23 +++++-
 lib/ofp-actions.c                                 |   16 +++--
 lib/ofp-actions.h                                 |    1 +
 lib/ofp-parse.c                                   |   14 ++++
 lib/ofp-parse.h                                   |    1 +
 ofproto/ofproto-dpif-xlate.c                      |   17 +++++
 tests/kmod-traffic.at                             |   79 ++++++++++++++++++++-
 tests/test-conntrack.py                           |   22 +++++-
 11 files changed, 180 insertions(+), 8 deletions(-)

diff --git a/datapath/linux/compat/include/linux/openvswitch.h b/datapath/linux/compat/include/linux/openvswitch.h
index 37526f9..59a4078 100644
--- a/datapath/linux/compat/include/linux/openvswitch.h
+++ b/datapath/linux/compat/include/linux/openvswitch.h
@@ -661,11 +661,13 @@ struct ovs_action_push_tnl {
  * enum ovs_ct_attr - Attributes for %OVS_ACTION_ATTR_CT action.
  * @OVS_CT_ATTR_FLAGS: u32 connection tracking flags.
  * @OVS_CT_ATTR_ZONE: u16 connection tracking zone.
+ * XXX @OVS_CT_ATTR_HELPER
  */
 enum ovs_ct_attr {
 	OVS_CT_ATTR_UNSPEC,
 	OVS_CT_ATTR_FLAGS,      /* u8 of OVS_CT_F_*. */
 	OVS_CT_ATTR_ZONE,       /* u16 zone id. */
+	OVS_CT_ATTR_HELPER,
 	__OVS_CT_ATTR_MAX
 };
 
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 9b5ac59..42d47af 100644
--- a/lib/odp-util.c
+++ b/lib/odp-util.c
@@ -621,8 +621,10 @@ format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
     static const struct nl_policy ovs_conntrack_policy[] = {
         [OVS_CT_ATTR_FLAGS] = { .type = NL_A_U32, .optional = true },
         [OVS_CT_ATTR_ZONE] = { .type = NL_A_U16, .optional = true },
+        [OVS_CT_ATTR_HELPER] = { .type = NL_A_STRING, .optional = true },
     };
     struct nlattr *a[ARRAY_SIZE(ovs_conntrack_policy)];
+    const char *helper;
     uint32_t flags;
 
     if (!nl_parse_nested(attr, ovs_conntrack_policy, a, ARRAY_SIZE(a))) {
@@ -631,10 +633,15 @@ format_odp_conntrack_action(struct ds *ds, const struct nlattr *attr)
     }
 
     flags = nl_attr_get_u32(a[OVS_CT_ATTR_FLAGS]);
+    helper = a[OVS_CT_ATTR_HELPER] ? nl_attr_get(a[OVS_CT_ATTR_HELPER]) : NULL;
 
-    ds_put_format(ds, "ct(%szone=%"PRIu16")",
+    ds_put_format(ds, "ct(%szone=%"PRIu16,
                   flags & OVS_CT_F_COMMIT ? "commit," : "",
                   nl_attr_get_u16(a[OVS_CT_ATTR_ZONE]));
+    if (helper) {
+        ds_put_format(ds, ",helper=%s", helper);
+    }
+    ds_put_cstr(ds, ")");
 }
 
 static void
@@ -1037,6 +1044,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;
         size_t start;
@@ -1059,6 +1067,14 @@ parse_conntrack_action(const char *s_, struct ofpbuf *actions)
             if (ovs_scan(s, "zone=%"SCNu16"%n", &zone, &n)) {
                 continue;
             }
+            if (ovs_scan(s, "helper=%n", &n)) {
+                s += n;
+                n = strspn(s, delimiters);
+                if (n > 15) { /* XXX */
+                    return -EINVAL;
+                }
+                helper = s;
+            }
 
             if (n < 0) {
                 return -EINVAL;
@@ -1074,6 +1090,11 @@ parse_conntrack_action(const char *s_, struct ofpbuf *actions)
         if (zone) {
             nl_msg_put_u16(actions, OVS_CT_ATTR_ZONE, zone);
         }
+        if (helper) {
+            int n = strspn(helper, delimiters);
+
+            nl_msg_put_string__(actions, OVS_CT_ATTR_HELPER, helper, n);
+        }
         nl_msg_end_nested(actions, start);
     }
 
diff --git a/lib/ofp-actions.c b/lib/ofp-actions.c
index a6ad547..b5ade57 100644
--- a/lib/ofp-actions.c
+++ b/lib/ofp-actions.c
@@ -4388,7 +4388,8 @@ struct nx_action_conntrack {
     ovs_be16 flags;             /* Zero or more NX_CT_F_* flags.
                                  * Unspecified flag bits must be zero. */
     ovs_be16 zone;              /* Connection tracking context. */
-    uint8_t  pad[2];
+    ovs_be16 alg;               /* IANA-assigned port for the protocol.
+                                 * 0 indicates no ALG is required. */
 };
 OFP_ASSERT(sizeof(struct nx_action_conntrack) == 16);
 
@@ -4400,6 +4401,7 @@ decode_NXAST_RAW_CT(const struct nx_action_conntrack *nac, struct ofpbuf *out)
     conntrack = ofpact_put_CT(out);
     conntrack->flags = ntohs(nac->flags);
     conntrack->zone = ntohs(nac->zone);
+    conntrack->alg = ntohs(nac->alg);
 
     return 0;
 }
@@ -4413,6 +4415,7 @@ encode_CT(const struct ofpact_conntrack *conntrack,
     nac = put_NXAST_CT(out);
     nac->flags = htons(conntrack->flags);
     nac->zone = htons(conntrack->zone);
+    nac->alg = htons(conntrack->alg);
 }
 
 /* Parses 'arg' as the argument to a "ct" action, and appends such an
@@ -4437,6 +4440,8 @@ parse_CT(char *arg, struct ofpbuf *ofpacts,
             oc->flags |= NX_CT_F_RECIRC;
         } else if (!strcmp(key, "zone")) {
             error = str_to_u16(value, "zone", &oc->zone);
+        } else if (!strcmp(key, "alg")) {
+            error = str_to_connhelper(value, &oc->alg);
         } else {
             error = xasprintf("invalid key \"%s\" in \"ct\" argument",
                               key);
@@ -4451,10 +4456,13 @@ parse_CT(char *arg, struct ofpbuf *ofpacts,
 static void
 format_CT(const struct ofpact_conntrack *a, struct ds *s)
 {
-    ds_put_format(s, "ct(%s%szone=%"PRIu16")",
+    ds_put_format(s, "ct(%s%s",
                   a->flags & NX_CT_F_COMMIT ? "commit," : "",
-                  a->flags & NX_CT_F_RECIRC ? "recirc," : "",
-                  a->zone);
+                  a->flags & NX_CT_F_RECIRC ? "recirc," : "");
+    if (a->alg) {
+        ds_put_format(s, "alg=%d,", a->alg);
+    }
+    ds_put_format(s, "zone=%"PRIu16")", a->zone);
 }
 
 /* Meter instruction. */
diff --git a/lib/ofp-actions.h b/lib/ofp-actions.h
index 6d1bad8..01958c2 100644
--- a/lib/ofp-actions.h
+++ b/lib/ofp-actions.h
@@ -485,6 +485,7 @@ struct ofpact_conntrack {
     struct ofpact ofpact;
     uint16_t flags;
     uint16_t zone;
+    uint16_t alg;
 };
 
 static inline size_t
diff --git a/lib/ofp-parse.c b/lib/ofp-parse.c
index 856044d..849b5f8 100644
--- a/lib/ofp-parse.c
+++ b/lib/ofp-parse.c
@@ -168,6 +168,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 OVS_UNUSED)
+{
+    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 db30f43..a85020f 100644
--- a/lib/ofp-parse.h
+++ b/lib/ofp-parse.h
@@ -93,5 +93,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, uint8_t mac[ETH_ADDR_LEN]) 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 bec1c64..5561410 100644
--- a/ofproto/ofproto-dpif-xlate.c
+++ b/ofproto/ofproto-dpif-xlate.c
@@ -4081,6 +4081,22 @@ recirc_unroll_actions(const struct ofpact *ofpacts, size_t ofpacts_len,
     }
 
 static void
+put_connhelper(struct ofpbuf *odp_actions, struct ofpact_conntrack *ofc)
+{
+    if (ofc->alg) {
+        /* XXX: Is this always in netinet/in.h? */
+        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)
 {
     struct ofpbuf *odp_actions = ctx->xout->odp_actions;
@@ -4094,6 +4110,7 @@ compose_conntrack_action(struct xlate_ctx *ctx, struct ofpact_conntrack *ofc)
     ct_offset = nl_msg_start_nested(odp_actions, OVS_ACTION_ATTR_CT);
     nl_msg_put_u32(odp_actions, OVS_CT_ATTR_FLAGS, flags);
     nl_msg_put_u16(odp_actions, OVS_CT_ATTR_ZONE, ofc->zone);
+    put_connhelper(odp_actions, ofc);
     nl_msg_end_nested(odp_actions, ct_offset);
 
     if (ofc->flags & NX_CT_F_RECIRC) {
diff --git a/tests/kmod-traffic.at b/tests/kmod-traffic.at
index 6c1737c..5d41b95 100644
--- a/tests/kmod-traffic.at
+++ b/tests/kmod-traffic.at
@@ -210,8 +210,6 @@ in_port=4,conn_state=+trk+inv,tcp,action=3
 in_port=4,conn_state=+trk+new,tcp,action=3
 ])
 
-ovs-appctl vlog/set dpif:dbg
-
 AT_CHECK([ovs-ofctl add-flows br0 flows.txt])
 
 dnl We set up our rules to allow the request without committing. The return
@@ -399,3 +397,80 @@ AT_CHECK([ip netns exec at_ns2 wget 10.1.1.4 -t 3 -T 1 -v -o wget1.log], [4])
 
 OVS_KMOD_VSWITCHD_STOP
 AT_CLEANUP
+
+AT_SETUP([conntrack - FTP])
+AT_SKIP_IF([test $HAVE_PYTHON = no])
+OVS_KMOD_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
+in_port=1,tcp,action=ct(alg=ftp,commit),2
+in_port=2,tcp,conn_state=-trk,action=ct(recirc)
+in_port=2,tcp,conn_state=+trk-new+est,action=1
+in_port=2,tcp,conn_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
+in_port=1,tcp,conn_state=-trk,action=ct(recirc)
+in_port=1,tcp,conn_state=+trk+new,action=ct(commit,alg=ftp),2
+in_port=1,tcp,conn_state=+trk+est,action=2
+in_port=2,tcp,conn_state=-trk,action=ct(recirc)
+in_port=2,tcp,conn_state=+trk+new+rel,action=ct(commit),1
+in_port=2,tcp,conn_state=+trk-new+est,action=1
+in_port=2,tcp,conn_state=+trk-new+rel,action=1
+])
+
+AT_CHECK([ovs-ofctl add-flows br0 flows1.txt])
+
+NETNS_DAEMONIZE([at_ns0], [[$PYTHON $srcdir/test-conntrack.py ftp]], [test-conntrack1.pid])
+NETNS_DAEMONIZE([at_ns1], [[$PYTHON $srcdir/test-conntrack.py ftp]], [test-conntrack0.pid])
+
+dnl FTP requests from p1->p0 should fail due to network failure.
+dnl Try 3 times, in 1 second intervals.
+AT_CHECK([ip netns 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.
+AT_CHECK([ip netns exec at_ns0 wget ftp://10.1.1.2 --no-passive-ftp -t 3 -T 1 --retry-connrefused -v -o wget0.log])
+
+dnl I think that the first of the below connections is because we don't wait long enough before the above command...
+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 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.
+AT_CHECK([ip netns 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.
+AT_CHECK([ip netns exec at_ns0 wget ftp://10.1.1.2 --no-passive-ftp -t 3 -T 1 --retry-connrefused -v -o wget0.log])
+
+dnl I think that the first of the below connections is because we don't wait long enough before the above command...
+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 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
+])
+
+OVS_KMOD_VSWITCHD_STOP
+AT_CLEANUP
diff --git a/tests/test-conntrack.py b/tests/test-conntrack.py
index 8c8b7b8..a753669 100755
--- a/tests/test-conntrack.py
+++ b/tests/test-conntrack.py
@@ -24,16 +24,36 @@ class TCPServerV6(HTTPServer):
     address_family = socket.AF_INET6
 
 
+def get_ftpd():
+    try:
+        from pyftpdlib.authorizers import DummyAuthorizer
+        from pyftpdlib.handlers import FTPHandler
+        from pyftpdlib.servers import FTPServer
+
+        class OVSFTPHandler(FTPHandler):
+            authorizer = DummyAuthorizer()
+            authorizer.add_anonymous("/tmp")
+        server = [FTPServer, OVSFTPHandler, 21]
+    except ImportError:
+        server = None
+        pass
+    return server
+
+
 def main():
     SERVERS = {
         'http':  [TCPServer,   SimpleHTTPRequestHandler, 80],
         'http6': [TCPServerV6, SimpleHTTPRequestHandler, 80],
     }
 
+    ftpd = get_ftpd()
+    if ftpd is not None:
+        SERVERS['ftp'] = ftpd
+
     parser = argparse.ArgumentParser(
             description='Run basic application servers.')
     parser.add_argument('proto', default='http', nargs='?',
-            help='protocol to serve (http, http6)')
+            help='protocol to serve (http, http6, ftp)')
     args = parser.parse_args()
 
     if args.proto not in SERVERS:
-- 
1.7.10.4




More information about the dev mailing list