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.
This commit is contained in:
Etienne Dechamps 2015-11-15 13:40:07 +00:00
parent 2bb567c6a3
commit 513bffe1fe
8 changed files with 220 additions and 1 deletions

View 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

View 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

View 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

View 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

View file

@ -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;

View 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 */

131
src/upnp.c Normal file
View file

@ -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));
}
}

27
src/upnp.h Normal file
View file

@ -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