summaryrefslogtreecommitdiffstats
path: root/src/helpers/ssdp.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/helpers/ssdp.c')
-rw-r--r--src/helpers/ssdp.c477
1 files changed, 471 insertions, 6 deletions
diff --git a/src/helpers/ssdp.c b/src/helpers/ssdp.c
index bc41087..1f7f76f 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);
}