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

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