From: Etienne Dechamps Date: Sun, 15 Nov 2015 13:40:07 +0000 (+0000) Subject: Add UPnP support to tincd. X-Git-Tag: release-1.1pre12~86 X-Git-Url: https://tinc-vpn.org/git/browse?a=commitdiff_plain;h=513bffe1fee07bcbcb50691e221874adc1507857;p=tinc Add UPnP support to tincd. This commit makes tincd capable of discovering UPnP-IGD devices on the local network, and add mappings (port redirects) for its TCP and/or UDP port. The goal is to improve reliability and performance of tinc with nodes sitting behind home routers that support UPnP, by making it less reliant on UDP Hole Punching, which is prone to failure when "hostile" NATs are involved. The way this is implemented is by leveraging the libminiupnpc library, which we have just added a new dependency on. We use pthread to run the UPnP client code in a dedicated thread; we can't use the tinc event loop because libminiupnpc doesn't have a non-blocking API. --- diff --git a/bash_completion.d/tinc b/bash_completion.d/tinc index 48512dd5..d21aef32 100644 --- a/bash_completion.d/tinc +++ b/bash_completion.d/tinc @@ -4,7 +4,7 @@ _tinc() { cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" opts="-c -d -D -K -n -o -L -R -U --config --no-detach --debug --net --option --mlock --logfile --pidfile --chroot --user --help --version" - confvars="Address AddressFamily BindToAddress BindToInterface Broadcast BroadcastSubnet Cipher ClampMSS Compression ConnectTo DecrementTTL Device DeviceStandby DeviceType Digest DirectOnly Ed25519PrivateKeyFile Ed25519PublicKey Ed25519PublicKeyFile ExperimentalProtocol Forwarding GraphDumpFile Hostnames IffOneQueue IndirectData Interface KeyExpire ListenAddress LocalDiscovery MACExpire MACLength MaxOutputBufferSize MaxTimeout Mode MTUInfoInterval Name PMTU PMTUDiscovery PingInterval PingTimeout Port PriorityInheritance PrivateKeyFile ProcessPriority Proxy PublicKeyFile ReplayWindow StrictSubnets Subnet TCPOnly TunnelServer UDPDiscovery UDPDiscoveryKeepaliveInterval UDPDiscoveryInterval UDPDiscoveryTimeout UDPInfoInterval UDPRcvBuf UDPSndBuf VDEGroup VDEPort Weight" + confvars="Address AddressFamily BindToAddress BindToInterface Broadcast BroadcastSubnet Cipher ClampMSS Compression ConnectTo DecrementTTL Device DeviceStandby DeviceType Digest DirectOnly Ed25519PrivateKeyFile Ed25519PublicKey Ed25519PublicKeyFile ExperimentalProtocol Forwarding GraphDumpFile Hostnames IffOneQueue IndirectData Interface KeyExpire ListenAddress LocalDiscovery MACExpire MACLength MaxOutputBufferSize MaxTimeout Mode MTUInfoInterval Name PMTU PMTUDiscovery PingInterval PingTimeout Port PriorityInheritance PrivateKeyFile ProcessPriority Proxy PublicKeyFile ReplayWindow StrictSubnets Subnet TCPOnly TunnelServer UDPDiscovery UDPDiscoveryKeepaliveInterval UDPDiscoveryInterval UDPDiscoveryTimeout UDPInfoInterval UDPRcvBuf UDPSndBuf UPnP UPnPDiscoverWait UPnPRefreshPeriod VDEGroup VDEPort Weight" commands="add connect debug del disconnect dump edit export export-all generate-ed25519-keys generate-keys generate-rsa-keys get help import info init invite join list log network pcap pid purge reload restart retry set start stop top version" case ${prev} in diff --git a/doc/tinc.conf.5.in b/doc/tinc.conf.5.in index 71b5ec6f..30af25df 100644 --- a/doc/tinc.conf.5.in +++ b/doc/tinc.conf.5.in @@ -510,6 +510,17 @@ Note: this setting can have a significant impact on performance, especially raw Sets the socket send buffer size for the UDP socket, in bytes. If set to zero, the default buffer size will be used by the operating system. Note: this setting can have a significant impact on performance, especially raw throughput. +.It Va UPnP Li = yes | udponly | no Po no Pc +If this option is enabled then tinc will search for UPnP-IGD devices on the local network. +It will then create and maintain port mappings for tinc's listening TCP and UDP ports. +If set to "udponly", tinc will only create a mapping for its UDP (data) port, not for its TCP (metaconnection) port. +Note that tinc must have been built with miniupnpc support for this feature to be available. +Furthermore, be advised that enabling this can have security implications, because the miniupnpc library that +tinc uses might not be well-hardened with regard to malicious UPnP replies. +.It Va UPnPDiscoverWait Li = Ar seconds Pq 5 +The amount of time to wait for replies when probing the local network for UPnP devices. +.It Va UPnPRefreshPeriod Li = Ar seconds Pq 60 +How often tinc will re-add the port mapping, in case it gets reset on the UPnP device. This also controls the duration of the port mapping itself, which will be set to twice that duration. .El .Sh HOST CONFIGURATION FILES The host configuration files contain all information needed diff --git a/doc/tinc.texi b/doc/tinc.texi index e939784a..ed554469 100644 --- a/doc/tinc.texi +++ b/doc/tinc.texi @@ -1269,6 +1269,24 @@ Sets the socket send buffer size for the UDP socket, in bytes. If set to zero, the default buffer size will be used by the operating system. Note: this setting can have a significant impact on performance, especially raw throughput. +@cindex UPnP +@item UPnP = (no) +If this option is enabled then tinc will search for UPnP-IGD devices on the local network. +It will then create and maintain port mappings for tinc's listening TCP and UDP ports. +If set to "udponly", tinc will only create a mapping for its UDP (data) port, not for its TCP (metaconnection) port. +Note that tinc must have been built with miniupnpc support for this feature to be available. +Furthermore, be advised that enabling this can have security implications, because the miniupnpc library that +tinc uses might not be well-hardened with regard to malicious UPnP replies. + +@cindex UPnPDiscoverWait +@item UPnPDiscoverWait = (5) +The amount of time to wait for replies when probing the local network for UPnP devices. + +@cindex UPnPRefreshPeriod +@item UPnPRefreshPeriod = (5) +How often tinc will re-add the port mapping, in case it gets reset on the UPnP device. +This also controls the duration of the port mapping itself, which will be set to twice that duration. + @end table diff --git a/src/Makefile.am b/src/Makefile.am index 63af709c..c3e746f6 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -251,6 +251,12 @@ sptps_speed_SOURCES += \ endif endif +if MINIUPNPC +tincd_SOURCES += upnp.c +tincd_LDADD = $(MINIUPNPC_LIBS) +tincd_LDFLAGS = -pthread +endif + tinc_LDADD = $(READLINE_LIBS) $(CURSES_LIBS) sptps_speed_LDADD = -lrt diff --git a/src/net_setup.c b/src/net_setup.c index 23dd2521..38fed521 100644 --- a/src/net_setup.c +++ b/src/net_setup.c @@ -43,6 +43,10 @@ #include "utils.h" #include "xalloc.h" +#ifdef HAVE_MINIUPNPC +#include "upnp.h" +#endif + char *myport; static char *myname; static io_t device_io; @@ -1059,6 +1063,25 @@ static bool setup_myself(void) { xasprintf(&myself->hostname, "MYSELF port %s", myport); myself->connection->hostname = xstrdup(myself->hostname); + char *upnp = NULL; + get_config_string(lookup_config(config_tree, "UPnP"), &upnp); + bool upnp_tcp = false; + bool upnp_udp = false; + if (upnp) { + if (!strcasecmp(upnp, "yes")) + upnp_tcp = upnp_udp = true; + else if (!strcasecmp(upnp, "udponly")) + upnp_udp = true; + free(upnp); + } + if (upnp_tcp || upnp_udp) { +#ifdef HAVE_MINIUPNPC + upnp_init(upnp_tcp, upnp_udp); +#else + logger(DEBUG_ALWAYS, LOG_WARNING, "UPnP was requested, but tinc isn't built with miniupnpc support!"); +#endif + } + /* Done. */ last_config_check = now.tv_sec; diff --git a/src/tincctl.c b/src/tincctl.c index abc5c095..6c54eff9 100644 --- a/src/tincctl.c +++ b/src/tincctl.c @@ -1481,6 +1481,9 @@ const var_t variables[] = { {"UDPInfoInterval", VAR_SERVER}, {"UDPRcvBuf", VAR_SERVER}, {"UDPSndBuf", VAR_SERVER}, + {"UPnP", VAR_SERVER}, + {"UPnPDiscoverWait", VAR_SERVER}, + {"UPnPRefreshPeriod", VAR_SERVER}, {"VDEGroup", VAR_SERVER}, {"VDEPort", VAR_SERVER}, /* Host configuration */ diff --git a/src/upnp.c b/src/upnp.c new file mode 100644 index 00000000..a03143a1 --- /dev/null +++ b/src/upnp.c @@ -0,0 +1,131 @@ +/* + upnp.c -- UPnP-IGD client + Copyright (C) 2015 Guus Sliepen , + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "upnp.h" + +#include + +#include "miniupnpc/miniupnpc.h" +#include "miniupnpc/upnpcommands.h" +#include "miniupnpc/upnperrors.h" + +#include "system.h" +#include "logger.h" +#include "names.h" +#include "net.h" +#include "netutl.h" +#include "utils.h" + +static bool upnp_tcp; +static bool upnp_udp; +static int upnp_discover_wait = 5; +static int upnp_refresh_period = 60; + +static void upnp_add_mapping(struct UPNPUrls *urls, struct IGDdatas *data, const char *myaddr, int socket, const char *proto) { + // Extract the port from the listening socket. + // Note that we can't simply use listen_socket[].sa because this won't have the port + // if we're running with Port=0 (dynamically assigned port). + sockaddr_t sa; + socklen_t salen = sizeof sa; + if (getsockname(socket, &sa.sa, &salen)) { + logger(DEBUG_PROTOCOL, LOG_ERR, "[upnp] Unable to get socket address: [%d] %s", sockerrno, sockstrerror(sockerrno)); + return; + } + char *port; + sockaddr2str(&sa, NULL, &port); + if (!port) { + logger(DEBUG_PROTOCOL, LOG_ERR, "[upnp] Unable to get socket port"); + return; + } + + // Use a lease twice as long as the refresh period so that the mapping won't expire before we refresh. + char lease_duration[16]; + snprintf(lease_duration, sizeof lease_duration, "%d", upnp_refresh_period * 2); + + int error = UPNP_AddPortMapping(urls->controlURL, data->first.servicetype, port, port, myaddr, identname, proto, NULL, lease_duration); + if (error == 0) { + logger(DEBUG_PROTOCOL, LOG_INFO, "[upnp] Successfully set port mapping (%s:%s %s for %s seconds)", myaddr, port, proto, lease_duration); + } else { + logger(DEBUG_PROTOCOL, LOG_ERR, "[upnp] Failed to set port mapping (%s:%s %s for %s seconds): [%d] %s", myaddr, port, proto, lease_duration, error, strupnperror(error)); + } + + free(port); +} + +static void upnp_refresh() { + logger(DEBUG_PROTOCOL, LOG_INFO, "[upnp] Discovering IGD devices"); + + int error; + struct UPNPDev *devices = upnpDiscover(upnp_discover_wait * 1000, NULL, NULL, false, false, &error); + if (!devices) { + logger(DEBUG_PROTOCOL, LOG_WARNING, "[upnp] Unable to find IGD devices: [%d] %s", error, strupnperror(error)); + freeUPNPDevlist(devices); + return; + } + + struct UPNPUrls urls; + struct IGDdatas data; + char myaddr[64]; + int result = UPNP_GetValidIGD(devices, &urls, &data, myaddr, sizeof myaddr); + if (result <= 0) { + logger(DEBUG_PROTOCOL, LOG_WARNING, "[upnp] No IGD found"); + freeUPNPDevlist(devices); + return; + } + logger(DEBUG_PROTOCOL, LOG_INFO, "[upnp] IGD found: [%d] %s (local address: %s, service type: %s)", result, urls.controlURL, myaddr, data.first.servicetype); + + for (int i = 0; i < listen_sockets; i++) { + if (upnp_tcp) upnp_add_mapping(&urls, &data, myaddr, listen_socket[i].tcp.fd, "TCP"); + if (upnp_udp) upnp_add_mapping(&urls, &data, myaddr, listen_socket[i].udp.fd, "UDP"); + } + + FreeUPNPUrls(&urls); + freeUPNPDevlist(devices); +} + +static void *upnp_thread(void *data) { + while (true) { + time_t start = time(NULL); + upnp_refresh(); + + // Make sure we'll stick to the refresh period no matter how long upnp_refresh() takes. + time_t refresh_time = start + upnp_refresh_period; + time_t now = time(NULL); + if (now < refresh_time) sleep(refresh_time - now); + } + + // TODO: we don't have a clean thread shutdown procedure, so we can't remove the mapping. + // this is probably not a concern as long as the UPnP device honors the lease duration, + // but considering how bug-riddled these devices often are, that's a big "if". + return NULL; +} + +void upnp_init(bool tcp, bool udp) { + upnp_tcp = tcp; + upnp_udp = udp; + + get_config_int(lookup_config(config_tree, "UPnPDiscoverWait"), &upnp_discover_wait); + get_config_int(lookup_config(config_tree, "UPnPRefreshPeriod"), &upnp_refresh_period); + + pthread_t thread; + int error = pthread_create(&thread, NULL, upnp_thread, NULL); + if (error) { + logger(DEBUG_ALWAYS, LOG_ERR, "Unable to start UPnP-IGD client thread: [%d] %s", error, strerror(error)); + } +} diff --git a/src/upnp.h b/src/upnp.h new file mode 100644 index 00000000..dd0531bb --- /dev/null +++ b/src/upnp.h @@ -0,0 +1,27 @@ +/* + upnp.h -- UPnP-IGD client + Copyright (C) 2015 Guus Sliepen + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#ifndef __UPNP_H__ +#define __UPNP_H__ + +#include "system.h" + +extern void upnp_init(bool, bool); + +#endif