Add UPnP support to tincd.
authorEtienne Dechamps <etienne@edechamps.fr>
Sun, 15 Nov 2015 13:40:07 +0000 (13:40 +0000)
committerEtienne Dechamps <etienne@edechamps.fr>
Sat, 21 Nov 2015 16:17:59 +0000 (16:17 +0000)
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.

bash_completion.d/tinc
doc/tinc.conf.5.in
doc/tinc.texi
src/Makefile.am
src/net_setup.c
src/tincctl.c
src/upnp.c [new file with mode: 0644]
src/upnp.h [new file with mode: 0644]

index 48512dd5447167e4cadaad5fbf6b60c88bbee8af..d21aef325a714770d1e0496078d193653c2367f8 100644 (file)
@@ -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
index 71b5ec6f36db4d193803d4a2b3c47425d0382cc8..30af25df8d1533708751af644cc93206909f68ad 100644 (file)
@@ -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
index e939784a3d1660959aebf8beb2efeda03eb6a4e9..ed5544692d3115b78ead3ba6f292b087ad66e647 100644 (file)
@@ -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 = <yes|udponly|no> (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 = <seconds> (5)
+The amount of time to wait for replies when probing the local network for UPnP devices.
+
+@cindex UPnPRefreshPeriod
+@item UPnPRefreshPeriod = <seconds> (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
 
 
index 63af709cdb77024897f8d47ea69631080984eeed..c3e746f62535df2a5bebd295fbd60cbf630de2ef 100644 (file)
@@ -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
 
index 23dd2521c74b465cdc64405ea79b3f821e3040aa..38fed521a64dae1ece4831c27c28285e268d17f0 100644 (file)
 #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;
index abc5c095a82c5d07a977c4c3ddbc0e5be81955ee..6c54eff90ac20fa7953ab9ae002e0910174a78b6 100644 (file)
@@ -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 (file)
index 0000000..a03143a
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+    upnp.c -- UPnP-IGD client
+    Copyright (C) 2015 Guus Sliepen <guus@tinc-vpn.org>,
+
+    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 <pthread.h>
+
+#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 (file)
index 0000000..dd0531b
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+    upnp.h -- UPnP-IGD client
+    Copyright (C) 2015 Guus Sliepen <guus@tinc-vpn.org>
+
+    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