diff mbox

[V2,conntrack-tools] conntrackd: cthelper: ssdp: Track UPnP eventing

Message ID 20170105214220.35549-1-cernekee@chromium.org
State Accepted
Delegated to: Pablo Neira
Headers show

Commit Message

Kevin Cernekee Jan. 5, 2017, 9:42 p.m. UTC
The UPnP Device Architecture spec provides a way for devices to connect
back to control points, called "Eventing" (chapter 4).  This sequence can
look something like:

1) Outbound multicast M-SEARCH packet (dst: 1900/udp)
 - Create expectation for unicast reply from <any host> to source port

2) Inbound unicast reply (there may be several of these from different devices)
 - Find the device's URL, e.g.
   LOCATION: http://192.168.1.123:1400/xml/device_description.xml
 - Create expectation to track connections to this host:port (tcp)

3) Outbound connection to device's web server (there will be several of these)
 - Watch for a SUBSCRIBE request
 - Find the control point's callback URL, e.g.
   CALLBACK: <http://192.168.1.124:3500/notify>
 - Create expectation to open up inbound connections to this host:port

4) Inbound connections to control point's web server
 - The device will send NOTIFY HTTP requests to inform the control point
   of new events.  These can continue indefinitely.  Each NOTIFY
   request arrives on a new TCP connection and may have a different
   source port.

Add the necessary code to create expectations for each of these
connections and rewrite the IP in the CALLBACK URL.  Tested with and
without NAT.

Signed-off-by: Kevin Cernekee <cernekee@chromium.org>
---
 doc/helper/conntrackd.conf |  10 +-
 src/helpers/ssdp.c         | 477 ++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 480 insertions(+), 7 deletions(-)

Comments

Kevin Cernekee Jan. 5, 2017, 10:14 p.m. UTC | #1
On Thu, Jan 5, 2017 at 1:42 PM, Kevin Cernekee <cernekee@chromium.org> wrote:
> + *     nfct timeout add long-timewait inet tcp \
> + *         established 1000 close 10 time_wait 10 last_ack 10
> + *     nfct timeout add long-timewait inet tcp time_wait 3600
> + *     iptables -t raw -A OUTPUT -p udp --dport 1900 -j CT --helper ssdp \
> + *         --timeout long-timewait

Oops, looks like this will not work (and it has a C&P error).
Instead, I'll tweak the systemwide timeouts for now.

Is there a way that a conntrack user helper can change the timeout
policy on a per-flow basis using NF_CT_NETLINK_TIMEOUT?

(It would be even better if the NOTIFY expectation can stay active
after its parent conntrack disappears, but I wasn't able to find a way
to do that.)
--
To unsubscribe from this list: send the line "unsubscribe netfilter-devel" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
Pablo Neira Ayuso Feb. 12, 2017, 2:05 p.m. UTC | #2
On Thu, Jan 05, 2017 at 01:42:20PM -0800, Kevin Cernekee wrote:
> The UPnP Device Architecture spec provides a way for devices to connect
> back to control points, called "Eventing" (chapter 4).  This sequence can
> look something like:
> 
> 1) Outbound multicast M-SEARCH packet (dst: 1900/udp)
>  - Create expectation for unicast reply from <any host> to source port
> 
> 2) Inbound unicast reply (there may be several of these from different devices)
>  - Find the device's URL, e.g.
>    LOCATION: http://192.168.1.123:1400/xml/device_description.xml
>  - Create expectation to track connections to this host:port (tcp)
> 
> 3) Outbound connection to device's web server (there will be several of these)
>  - Watch for a SUBSCRIBE request
>  - Find the control point's callback URL, e.g.
>    CALLBACK: <http://192.168.1.124:3500/notify>
>  - Create expectation to open up inbound connections to this host:port
> 
> 4) Inbound connections to control point's web server
>  - The device will send NOTIFY HTTP requests to inform the control point
>    of new events.  These can continue indefinitely.  Each NOTIFY
>    request arrives on a new TCP connection and may have a different
>    source port.
> 
> Add the necessary code to create expectations for each of these
> connections and rewrite the IP in the CALLBACK URL.  Tested with and
> without NAT.

I'm going to place this in the tree. Just follow up with fixes if
required.

Thanks Kevin.
--
To unsubscribe from this list: send the line "unsubscribe netfilter-devel" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
diff mbox

Patch

diff --git a/doc/helper/conntrackd.conf b/doc/helper/conntrackd.conf
index a827b93461a6..7eae8bc8a17a 100644
--- a/doc/helper/conntrackd.conf
+++ b/doc/helper/conntrackd.conf
@@ -84,7 +84,15 @@  Helper {
 		QueueNum 5
 		QueueLen 10240
 		Policy ssdp {
-			ExpectMax 1
+			ExpectMax 8
+			ExpectTimeout 300
+		}
+	}
+	Type ssdp inet tcp {
+		QueueNum 5
+		QueueLen 10240
+		Policy ssdp {
+			ExpectMax 8
 			ExpectTimeout 300
 		}
 	}
diff --git a/src/helpers/ssdp.c b/src/helpers/ssdp.c
index bc410875c2b8..1f7f76f67d60 100644
--- a/src/helpers/ssdp.c
+++ b/src/helpers/ssdp.c
@@ -1,5 +1,5 @@ 
 /*
- * SSDP connection tracking helper
+ * SSDP/UPnP connection tracking helper
  * (SSDP = Simple Service Discovery Protocol)
  * For documentation about SSDP see
  * http://en.wikipedia.org/wiki/Simple_Service_Discovery_Protocol
@@ -8,6 +8,33 @@ 
  * Based on the SSDP conntrack helper (nf_conntrack_ssdp.c),
  * :http://marc.info/?t=132945775100001&r=1&w=2
  *  (C) 2012 Ian Pilcher <arequipeno@gmail.com>
+ * Copyright (C) 2017 Google Inc.
+ *
+ * This requires Linux 3.12 or higher.  Basic usage:
+ *
+ *     nfct add helper ssdp inet udp
+ *     nfct add helper ssdp inet tcp
+ *     iptables -t raw -A OUTPUT -p udp --dport 1900 -j CT --helper ssdp
+ *     iptables -t raw -A PREROUTING -p udp --dport 1900 -j CT --helper ssdp
+ *
+ * This helper supports SNAT when used in conjunction with a daemon that
+ * forwards SSDP broadcasts/replies between interfaces, e.g.
+ * https://chromium.googlesource.com/chromiumos/platform2/+/master/arc-networkd/multicast_forwarder.h
+ *
+ * If UPnP eventing is used, callbacks should be triggered at regular
+ * intervals to prevent the expectation from expiring.  It will expire
+ * after min(nf_conntrack_tcp_timeout_time_wait, ExpectTimeout) which
+ * is min(120, 300) = 120 by default.  The latter option can be changed
+ * in the conntrackd configuration file; the former option can be changed
+ * via procfs or through policy:
+ *
+ *     nfct timeout add long-timewait inet tcp \
+ *         established 1000 close 10 time_wait 10 last_ack 10
+ *     nfct timeout add long-timewait inet tcp time_wait 3600
+ *     iptables -t raw -A OUTPUT -p udp --dport 1900 -j CT --helper ssdp \
+ *         --timeout long-timewait
+ *     iptables -t raw -A PREROUTING -p udp --dport 1900 -j CT --helper ssdp \
+ *         --timeout long-timewait
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License version 2 as
@@ -19,8 +46,10 @@ 
 #include "myct.h"
 #include "log.h"
 #include <errno.h>
+#include <stdlib.h>
 #include <arpa/inet.h>
 #include <netinet/ip.h>
+#include <netinet/tcp.h>
 #include <netinet/udp.h>
 #include <libmnl/libmnl.h>
 #include <libnetfilter_conntrack/libnetfilter_conntrack.h>
@@ -36,8 +65,95 @@ 
 #define SSDP_M_SEARCH		"M-SEARCH"
 #define SSDP_M_SEARCH_SIZE	(sizeof SSDP_M_SEARCH - 1)
 
-static int ssdp_helper_cb(struct pkt_buff *pkt, uint32_t protoff,
-			  struct myct *myct, uint32_t ctinfo)
+/* So, this packet has hit the connection tracking matching code.
+   Mangle it, and change the expectation to match the new version. */
+static unsigned int nf_nat_ssdp(struct pkt_buff *pkt,
+				int ctinfo,
+				unsigned int matchoff,
+				unsigned int matchlen,
+				struct nf_conntrack *ct,
+				struct nf_expect *exp)
+{
+	union nfct_attr_grp_addr newip;
+	uint16_t port;
+	int dir = CTINFO2DIR(ctinfo);
+	char buffer[sizeof("255.255.255.255:65535")];
+	unsigned int buflen;
+	const struct nf_conntrack *expected;
+	struct nf_conntrack *nat_tuple;
+	uint16_t initial_port;
+
+	/* Connection will come from wherever this packet goes, hence !dir */
+	cthelper_get_addr_dst(ct, !dir, &newip);
+
+	expected = nfexp_get_attr(exp, ATTR_EXP_EXPECTED);
+
+	nat_tuple = nfct_new();
+	if (nat_tuple == NULL)
+		return NF_ACCEPT;
+
+	initial_port = nfct_get_attr_u16(expected, ATTR_PORT_DST);
+
+	/* pkt is NULL for NOTIFY (renewal, same dir), non-NULL otherwise */
+	nfexp_set_attr_u32(exp, ATTR_EXP_NAT_DIR, pkt ? !dir : dir);
+
+	/* libnetfilter_conntrack needs this */
+	nfct_set_attr_u8(nat_tuple, ATTR_L3PROTO, AF_INET);
+	nfct_set_attr_u32(nat_tuple, ATTR_IPV4_SRC, 0);
+	nfct_set_attr_u32(nat_tuple, ATTR_IPV4_DST, 0);
+	nfct_set_attr_u8(nat_tuple, ATTR_L4PROTO,
+			 nfct_get_attr_u8(ct, ATTR_L4PROTO));
+	nfct_set_attr_u16(nat_tuple, ATTR_PORT_DST, 0);
+
+	/* When you see the packet, we need to NAT it the same as the
+	   this one. */
+	nfexp_set_attr(exp, ATTR_EXP_FN, "nat-follow-master");
+
+	/* Try to get same port: if not, try to change it. */
+	for (port = ntohs(initial_port); port != 0; port++) {
+		int ret;
+
+		nfct_set_attr_u16(nat_tuple, ATTR_PORT_SRC, htons(port));
+		nfexp_set_attr(exp, ATTR_EXP_NAT_TUPLE, nat_tuple);
+
+		ret = cthelper_add_expect(exp);
+		if (ret == 0)
+			break;
+		else if (ret != -EBUSY) {
+			port = 0;
+			break;
+		}
+	}
+
+	if (port == 0)
+		return NF_DROP;
+
+	/* Only the SUBSCRIBE request contains an IP string that needs to be
+	   mangled. */
+	if (!matchoff)
+		return NF_ACCEPT;
+
+	buflen = snprintf(buffer, sizeof(buffer),
+				"%u.%u.%u.%u:%u",
+                                ((unsigned char *)&newip.ip)[0],
+                                ((unsigned char *)&newip.ip)[1],
+                                ((unsigned char *)&newip.ip)[2],
+                                ((unsigned char *)&newip.ip)[3], port);
+	if (!buflen)
+		goto out;
+
+	if (!nfq_tcp_mangle_ipv4(pkt, matchoff, matchlen, buffer, buflen))
+		goto out;
+
+	return NF_ACCEPT;
+
+out:
+	cthelper_del_expect(exp);
+	return NF_DROP;
+}
+
+static int handle_ssdp_new(struct pkt_buff *pkt, uint32_t protoff,
+			   struct myct *myct, uint32_t ctinfo)
 {
 	int ret = NF_ACCEPT;
 	union nfct_attr_grp_addr daddr, saddr, taddr;
@@ -109,12 +225,346 @@  static int ssdp_helper_cb(struct pkt_buff *pkt, uint32_t protoff,
 		nfexp_destroy(exp);
 		return NF_DROP;
 	}
+	nfexp_set_attr(exp, ATTR_EXP_HELPER_NAME, "ssdp");
+	if (nfct_get_attr_u32(myct->ct, ATTR_STATUS) & IPS_SRC_NAT)
+		return nf_nat_ssdp(pkt, ctinfo, 0, 0, myct->ct, exp);
+
 	myct->exp = exp;
 
 	return ret;
 }
 
-static struct ctd_helper ssdp_helper = {
+static int find_hdr(const char *name, const uint8_t *data, int data_len,
+		    char *val, int val_len, const uint8_t **pos)
+{
+	int name_len = strlen(name);
+	int i;
+
+	while (1) {
+		if (data_len < name_len + 2)
+			return -1;
+
+		if (strncasecmp(name, (char *)data, name_len) == 0)
+			break;
+
+		for (i = 0; ; i++) {
+			if (i >= data_len - 1)
+				return -1;
+			if (data[i] == '\r' && data[i+1] == '\n')
+				break;
+		}
+
+		data_len -= i+2;
+		data += i+2;
+	}
+
+	data_len -= name_len;
+	data += name_len;
+	if (pos)
+		*pos = data;
+
+	for (i = 0; ; i++, val_len--) {
+		if (!val_len)
+			return -1;
+		if (*data == '\r') {
+			*val = 0;
+			return 0;
+		}
+		*(val++) = *(data++);
+	}
+}
+
+static int parse_url(const char *url,
+		     uint8_t l3proto,
+		     union nfct_attr_grp_addr *addr,
+		     uint16_t *port,
+		     size_t *match_offset,
+		     size_t *match_len)
+{
+	const char *start = url, *end;
+	size_t ip_len;
+
+	if (strncasecmp(url, "http://[", 8) == 0) {
+		char buf[64] = {0};
+
+		if (l3proto != AF_INET6) {
+			pr_debug("conntrack_ssdp: IPv6 URL in IPv4 SSDP reply\n");
+			return -1;
+		}
+
+		url += 8;
+
+		end = strchr(url, ']');
+		if (!end) {
+			pr_debug("conntrack_ssdp: unterminated IPv6 address: '%s'\n", url);
+			return -1;
+		}
+
+		ip_len = end - url;
+		if (ip_len > sizeof(buf) - 1) {
+			pr_debug("conntrack_ssdp: IPv6 address too long: '%s'\n", url);
+			return -1;
+		}
+		strncpy(buf, url, ip_len);
+
+		if (inet_pton(AF_INET6, buf, addr) != 1) {
+			pr_debug("conntrack_ssdp: Error parsing IPv6 address: '%s'\n", buf);
+			return -1;
+		}
+	} else if (strncasecmp(url, "http://", 7) == 0) {
+		char buf[64] = {0};
+
+		if (l3proto != AF_INET) {
+			pr_debug("conntrack_ssdp: IPv4 URL in IPv6 SSDP reply\n");
+			return -1;
+		}
+
+		url += 7;
+		for (end = url; ; end++) {
+			if (*end != '.' && *end != '\0' &&
+			    (*end < '0' || *end > '9'))
+				break;
+		}
+
+		ip_len = end - url;
+		if (ip_len > sizeof(buf) - 1) {
+			pr_debug("conntrack_ssdp: IPv4 address too long: '%s'\n", url);
+			return -1;
+		}
+		strncpy(buf, url, ip_len);
+
+		if (inet_pton(AF_INET, buf, addr) != 1) {
+			pr_debug("conntrack_ssdp: Error parsing IPv4 address: '%s'\n", buf);
+			return -1;
+		}
+	} else {
+		pr_debug("conntrack_ssdp: header does not start with http://\n");
+		return -1;
+	}
+
+	if (match_offset)
+		*match_offset = url - start;
+
+	if (*end != ':') {
+		*port = htons(80);
+		if (match_len)
+			*match_len = ip_len;
+	} else {
+		char *endptr = NULL;
+		*port = htons(strtol(end + 1, &endptr, 10));
+		if (match_len)
+			*match_len = ip_len + endptr - end;;
+	}
+
+	return 0;
+}
+
+static int handle_ssdp_reply(struct pkt_buff *pkt, uint32_t protoff,
+			     struct myct *myct, uint32_t ctinfo)
+{
+	uint8_t *data = pktb_network_header(pkt);
+	size_t bytes_left = pktb_len(pkt);
+	char hdr_val[256];
+	union nfct_attr_grp_addr addr;
+	uint16_t port;
+	struct nf_expect *exp = NULL;
+
+	if (bytes_left < protoff + sizeof(struct udphdr)) {
+		pr_debug("conntrack_ssdp: Short packet\n");
+		return NF_ACCEPT;
+	}
+	bytes_left -= protoff + sizeof(struct udphdr);
+	data += protoff + sizeof(struct udphdr);
+
+	if (find_hdr("LOCATION: ", data, bytes_left,
+		     hdr_val, sizeof(hdr_val), NULL) < 0) {
+		pr_debug("conntrack_ssdp: No LOCATION header found\n");
+		return NF_ACCEPT;
+	}
+	pr_debug("conntrack_ssdp: found location URL `%s'\n", hdr_val);
+
+	if (parse_url(hdr_val, nfct_get_attr_u8(myct->ct, ATTR_L3PROTO),
+		      &addr, &port, NULL, NULL) < 0) {
+		pr_debug("conntrack_ssdp: Error parsing URL\n");
+		return NF_ACCEPT;
+	}
+
+	exp = nfexp_new();
+	if (cthelper_expect_init(exp,
+				 myct->ct,
+				 0 /* class */,
+				 NULL /* saddr */,
+				 &addr /* daddr */,
+				 IPPROTO_TCP,
+				 NULL /* sport */,
+				 &port /* dport */,
+				 NF_CT_EXPECT_PERMANENT /* flags */) < 0) {
+		pr_debug("conntrack_ssdp: Failed to init expectation\n");
+		nfexp_destroy(exp);
+		return NF_ACCEPT;
+	}
+
+	nfexp_set_attr(exp, ATTR_EXP_HELPER_NAME, "ssdp");
+	if (nfct_get_attr_u32(myct->ct, ATTR_STATUS) & IPS_SRC_NAT)
+		return nf_nat_ssdp(pkt, ctinfo, 0, 0, myct->ct, exp);
+
+	myct->exp = exp;
+	return NF_ACCEPT;
+}
+
+static int renew_exp(struct myct *myct, uint32_t ctinfo)
+{
+	int dir = CTINFO2DIR(ctinfo);
+	union nfct_attr_grp_addr saddr = {0}, daddr = {0};
+	uint16_t sport, dport;
+	struct nf_expect *exp = nfexp_new();
+
+	pr_debug("conntrack_ssdp: Renewing NOTIFY expectation\n");
+
+	cthelper_get_addr_src(myct->ct, dir, &saddr);
+	cthelper_get_addr_dst(myct->ct, dir, &daddr);
+	cthelper_get_port_src(myct->ct, dir, &sport);
+	cthelper_get_port_dst(myct->ct, dir, &dport);
+
+	if (cthelper_expect_init(exp,
+				 myct->ct,
+				 0 /* class */,
+				 &saddr /* saddr */,
+				 &daddr /* daddr */,
+				 IPPROTO_TCP,
+				 NULL /* sport */,
+				 &dport /* dport */,
+				 0 /* flags */) < 0) {
+		pr_debug("conntrack_ssdp: Failed to init expectation\n");
+		nfexp_destroy(exp);
+		return NF_ACCEPT;
+	}
+
+	nfexp_set_attr(exp, ATTR_EXP_HELPER_NAME, "ssdp");
+	if (nfct_get_attr_u32(myct->ct, ATTR_STATUS) & IPS_DST_NAT)
+		return nf_nat_ssdp(NULL, ctinfo, 0, 0, myct->ct, exp);
+
+	myct->exp = exp;
+	return NF_ACCEPT;
+}
+
+static int handle_http_request(struct pkt_buff *pkt, uint32_t protoff,
+			       struct myct *myct, uint32_t ctinfo)
+{
+	struct tcphdr *th;
+	unsigned int dataoff, datalen;
+	const uint8_t *data;
+	char hdr_val[256];
+	union nfct_attr_grp_addr cbaddr = {0}, daddr = {0}, saddr = {0};
+	uint16_t cbport;
+	struct nf_expect *exp = NULL;
+	const uint8_t *hdr_pos;
+	size_t ip_offset, ip_len;
+	int dir = CTINFO2DIR(ctinfo);
+
+	th = (struct tcphdr *) (pktb_network_header(pkt) + protoff);
+	dataoff = protoff + th->doff * 4;
+	datalen = pktb_len(pkt) - dataoff;
+	data = pktb_network_header(pkt) + dataoff;
+
+	if (datalen >= 7 && strncmp((char *)data, "NOTIFY ", 7) == 0)
+		return renew_exp(myct, ctinfo);
+
+	if (datalen < 10 || strncmp((char *)data, "SUBSCRIBE ", 10) != 0)
+		return NF_ACCEPT;
+
+	if (find_hdr("CALLBACK: <", data, datalen,
+		     hdr_val, sizeof(hdr_val), &hdr_pos) < 0) {
+		pr_debug("conntrack_ssdp: No CALLBACK header found\n");
+		return NF_ACCEPT;
+	}
+	pr_debug("conntrack_ssdp: found callback URL `%s'\n", hdr_val);
+
+	if (parse_url(hdr_val, nfct_get_attr_u8(myct->ct, ATTR_L3PROTO),
+		      &cbaddr, &cbport, &ip_offset, &ip_len) < 0) {
+		pr_debug("conntrack_ssdp: Error parsing URL\n");
+		return NF_ACCEPT;
+	}
+
+	cthelper_get_addr_dst(myct->ct, !dir, &daddr);
+	cthelper_get_addr_src(myct->ct, dir, &saddr);
+
+	if (memcmp(&saddr, &cbaddr, sizeof(cbaddr)) != 0) {
+		pr_debug("conntrack_ssdp: Callback address belongs to another host\n");
+		return NF_ACCEPT;
+	}
+
+	cthelper_get_addr_src(myct->ct, !dir, &saddr);
+
+	exp = nfexp_new();
+	if (cthelper_expect_init(exp,
+				 myct->ct,
+				 0 /* class */,
+				 &saddr /* saddr */,
+				 &daddr /* daddr */,
+				 IPPROTO_TCP,
+				 NULL /* sport */,
+				 &cbport /* dport */,
+				 0 /* flags */) < 0) {
+		pr_debug("conntrack_ssdp: Failed to init expectation\n");
+		nfexp_destroy(exp);
+		return NF_ACCEPT;
+	}
+
+	nfexp_set_attr(exp, ATTR_EXP_HELPER_NAME, "ssdp");
+	if (nfct_get_attr_u32(myct->ct, ATTR_STATUS) & IPS_SRC_NAT) {
+		return nf_nat_ssdp(pkt, ctinfo,
+				   (hdr_pos - data) + ip_offset,
+				   ip_len, myct->ct, exp);
+	}
+
+	myct->exp = exp;
+	return NF_ACCEPT;
+}
+
+static int ssdp_helper_cb(struct pkt_buff *pkt, uint32_t protoff,
+			  struct myct *myct, uint32_t ctinfo)
+{
+	uint8_t proto;
+
+	/* All new UDP conntracks are M-SEARCH queries. */
+	if (ctinfo == IP_CT_NEW)
+		return handle_ssdp_new(pkt, protoff, myct, ctinfo);
+
+	proto = nfct_get_attr_u16(myct->ct, ATTR_ORIG_L4PROTO);
+
+	/* All existing UDP conntracks are replies to an M-SEARCH query.
+	   M-SEARCH queries often generate replies from multiple devices
+	   on the LAN. */
+	if (proto == IPPROTO_UDP)
+		return handle_ssdp_reply(pkt, protoff, myct, ctinfo);
+	else {
+		/* TCP conntracks can represent:
+		 *
+		 *  - SUBSCRIBE requests (control point -> device) containing a
+		 *    callback URL.  These create an expectation that allows
+		 *    the NOTIFY callbacks to pass.
+		 *  - NOTIFY callbacks (device -> control point), which
+		 *    "auto-renew" the expectation
+		 *  - Some other HTTP request (don't care)
+		 *
+		 * Currently all TCP conntracks are scanned for SUBSCRIBE
+		 * and NOTIFY requests.  This is not ideal, because we do
+		 * not want callbacks to be able to create new expectations
+		 * on a different port.  Fixing this will require convincing
+		 * the kernel to pass private state data for related
+		 * conntracks. */
+		if (ctinfo == IP_CT_ESTABLISHED)
+			return handle_http_request(pkt, protoff, myct, ctinfo);
+		else
+			return NF_ACCEPT;
+	}
+
+	/* Not reached. */
+	return NF_DROP;
+}
+
+static struct ctd_helper ssdp_helper_udp = {
 	.name		= "ssdp",
 	.l4proto	= IPPROTO_UDP,
 	.priv_data_len	= 0,
@@ -122,7 +572,21 @@  static struct ctd_helper ssdp_helper = {
 	.policy		= {
 		[0] = {
 			.name		= "ssdp",
-			.expect_max	= 1,
+			.expect_max	= 8,
+			.expect_timeout	= 5 * 60,
+		},
+	},
+};
+
+static struct ctd_helper ssdp_helper_tcp = {
+	.name		= "ssdp",
+	.l4proto	= IPPROTO_TCP,
+	.priv_data_len	= 0,
+	.cb		= ssdp_helper_cb,
+	.policy		= {
+		[0] = {
+			.name		= "ssdp",
+			.expect_max	= 8,
 			.expect_timeout	= 5 * 60,
 		},
 	},
@@ -130,5 +594,6 @@  static struct ctd_helper ssdp_helper = {
 
 static void __attribute__ ((constructor)) ssdp_init(void)
 {
-	helper_register(&ssdp_helper);
+	helper_register(&ssdp_helper_udp);
+	helper_register(&ssdp_helper_tcp);
 }