| From 0a9cfa953309f8e65e7f341625624be5fae49eae Mon Sep 17 00:00:00 2001 |
| From: Kevin Cernekee <cernekee@chromium.org> |
| Date: Wed, 31 Aug 2016 21:01:06 -0700 |
| Subject: [PATCH] conntrackd: cthelper: ssdp: Track UPnP eventing |
| |
| 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. |
| |
| Add the necessary code to add 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 | 481 ++++++++++++++++++++++++++++++++++++++++++++- |
| 2 files changed, 484 insertions(+), 7 deletions(-) |
| |
| 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..0354e9f71b1a 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,26 @@ |
| * 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 |
| @@ -19,8 +39,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 +58,99 @@ |
| #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) |
| + 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; |
| @@ -109,12 +222,353 @@ 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 = { |
| +/** |
| + * 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, |
| @@ -122,7 +576,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 +598,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); |
| } |
| -- |
| 2.11.0.483.g087da7b7c-goog |
| |