| /* |
| * SSDP/UPnP connection tracking helper |
| * (SSDP = Simple Service Discovery Protocol) |
| * For documentation about SSDP see |
| * http://en.wikipedia.org/wiki/Simple_Service_Discovery_Protocol |
| * |
| * Copyright (C) 2014 Ashley Hughes <ashley.hughes@blueyonder.co.uk> |
| * 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. The timeout |
| * period on the master conntrack is determined from the TCP timeouts, |
| * which can be changed through sysctl: |
| * |
| * sysctl -w net.netfilter.nf_conntrack_tcp_timeout_close=300 |
| * sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=300 |
| * |
| * 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 |
| * published by the Free Software Foundation. |
| */ |
| |
| #include "conntrackd.h" |
| #include "helper.h" |
| #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> |
| #include <libnetfilter_queue/libnetfilter_queue.h> |
| #include <libnetfilter_queue/libnetfilter_queue_tcp.h> |
| #include <libnetfilter_queue/pktbuff.h> |
| #include <linux/netfilter.h> |
| |
| #define SSDP_MCAST_ADDR "239.255.255.250" |
| #define UPNP_MCAST_LL_ADDR "FF02::C" /* link-local */ |
| #define UPNP_MCAST_SL_ADDR "FF05::C" /* site-local */ |
| |
| #define SSDP_M_SEARCH "M-SEARCH" |
| #define SSDP_M_SEARCH_SIZE (sizeof SSDP_M_SEARCH - 1) |
| |
| /* 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) |
| goto bad_destroy; |
| |
| 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; |
| } |
| } |
| nfct_destroy(nat_tuple); |
| |
| if (port == 0) |
| goto bad_destroy; |
| |
| /* Only the SUBSCRIBE request contains an IP string that needs to be |
| mangled. */ |
| if (matchoff) { |
| 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 bad_del_destroy; |
| |
| if (!nfq_tcp_mangle_ipv4(pkt, matchoff, matchlen, buffer, |
| buflen)) |
| goto bad_del_destroy; |
| } |
| |
| nfexp_destroy(exp); |
| return NF_ACCEPT; |
| |
| bad_del_destroy: |
| cthelper_del_expect(exp); |
| bad_destroy: |
| nfexp_destroy(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; |
| struct iphdr *net_hdr = (struct iphdr *)pktb_network_header(pkt); |
| int good_packet = 0; |
| struct nf_expect *exp; |
| uint16_t port; |
| unsigned int dataoff; |
| void *sb_ptr; |
| |
| cthelper_get_addr_dst(myct->ct, MYCT_DIR_ORIG, &daddr); |
| switch (nfct_get_attr_u8(myct->ct, ATTR_L3PROTO)) { |
| case AF_INET: |
| inet_pton(AF_INET, SSDP_MCAST_ADDR, &(taddr.ip)); |
| if (daddr.ip == taddr.ip) |
| good_packet = 1; |
| break; |
| case AF_INET6: |
| inet_pton(AF_INET6, UPNP_MCAST_LL_ADDR, &(taddr.ip6)); |
| if (daddr.ip6[0] == taddr.ip6[0] && |
| daddr.ip6[1] == taddr.ip6[1] && |
| daddr.ip6[2] == taddr.ip6[2] && |
| daddr.ip6[3] == taddr.ip6[3]) { |
| good_packet = 1; |
| break; |
| } |
| inet_pton(AF_INET6, UPNP_MCAST_SL_ADDR, &(taddr.ip6)); |
| if (daddr.ip6[0] == taddr.ip6[0] && |
| daddr.ip6[1] == taddr.ip6[1] && |
| daddr.ip6[2] == taddr.ip6[2] && |
| daddr.ip6[3] == taddr.ip6[3]) { |
| good_packet = 1; |
| break; |
| } |
| break; |
| default: |
| break; |
| } |
| |
| if (!good_packet) { |
| pr_debug("ssdp_help: destination address not multicast; ignoring\n"); |
| return NF_ACCEPT; |
| } |
| |
| /* No data? Ignore */ |
| dataoff = net_hdr->ihl*4 + sizeof(struct udphdr); |
| if (dataoff >= pktb_len(pkt)) { |
| pr_debug("ssdp_help: UDP payload too small for M-SEARCH; ignoring\n"); |
| return NF_ACCEPT; |
| } |
| |
| sb_ptr = pktb_network_header(pkt) + dataoff; |
| |
| if (memcmp(sb_ptr, SSDP_M_SEARCH, SSDP_M_SEARCH_SIZE) != 0) { |
| pr_debug("ssdp_help: UDP payload does not begin with 'M-SEARCH'; ignoring\n"); |
| return NF_ACCEPT; |
| } |
| |
| cthelper_get_addr_src(myct->ct, MYCT_DIR_ORIG, &saddr); |
| cthelper_get_port_src(myct->ct, MYCT_DIR_ORIG, &port); |
| |
| exp = nfexp_new(); |
| if (exp == NULL) |
| return NF_DROP; |
| |
| if (cthelper_expect_init(exp, myct->ct, 0, NULL, &saddr, |
| IPPROTO_UDP, NULL, &port, |
| NF_CT_EXPECT_PERMANENT)) { |
| 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; |
| } |
| |
| /** |
| * find_hdr - scans a packet for an HTTP header and copies out the value |
| * |
| * @name: the header string to scan for (e.g. "LOCATION:") |
| * @data: the packet data to scan |
| * @data_len: the length of the packet data |
| * @val: the buffer into which the value (if found) should be copied |
| * @val_len: the length of the output buffer |
| * @pos: if not NULL, a pointer to the first byte of the value in DATA |
| * will be stored in *POS |
| */ |
| 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); |
| |
| /* 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; |
| } |
| |
| static struct ctd_helper ssdp_helper_udp = { |
| .name = "ssdp", |
| .l4proto = IPPROTO_UDP, |
| .priv_data_len = 0, |
| .cb = ssdp_helper_cb, |
| .policy = { |
| [0] = { |
| .name = "ssdp", |
| .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, |
| }, |
| }, |
| }; |
| |
| static void __attribute__ ((constructor)) ssdp_init(void) |
| { |
| helper_register(&ssdp_helper_udp); |
| helper_register(&ssdp_helper_tcp); |
| } |