0f6d34dc1b
Unfortunately, libminiupnpc has a somewhat... "peculiar" approach to backwards compatibility for their API, where they reserve the right to make breaking changes when they feel like it, forcing users to resort to #ifdefs to ensure they use the correct API. Sigh. Previously, tinc would only build against API versions <= 13, because I was doing my initial development using miniupnpc-1.9.20140610 which is the version that ships with Debian. The changes in this commit are required for tinc to build against more recent versions, from 1.9.20150730 to the latest one at the time of this commit, 1.9.20151026.
164 lines
5.7 KiB
C
164 lines
5.7 KiB
C
/*
|
|
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;
|
|
|
|
// Unfortunately, libminiupnpc devs don't seem to care about API compatibility,
|
|
// and there are slight changes to function signatures between library versions.
|
|
// Well, at least they publish a "MINIUPNPC_API_VERSION" constant, so we got that going for us, which is nice.
|
|
// Differences between API versions are documented in "apiversions.txt" in the libminiupnpc distribution.
|
|
|
|
#ifndef MINIUPNPC_API_VERSION
|
|
#define MINIUPNPC_API_VERSION 0
|
|
#endif
|
|
|
|
static struct UPNPDev *upnp_discover(int delay, int *error) {
|
|
#if MINIUPNPC_API_VERSION <= 13
|
|
|
|
#if MINIUPNPC_API_VERSION < 8
|
|
#warning "The version of libminiupnpc you're building against seems to be too old. Expect trouble."
|
|
#endif
|
|
|
|
return upnpDiscover(delay, NULL, NULL, false, false, error);
|
|
|
|
#elif MINIUPNPC_API_VERSION <= 14
|
|
|
|
return upnpDiscover(delay, NULL NULL, false, false, 2, error);
|
|
|
|
#else
|
|
|
|
#if MINIUPNPC_API_VERSION > 15
|
|
#warning "The version of libminiupnpc you're building against seems to be too recent. Expect trouble."
|
|
#endif
|
|
|
|
return upnpDiscover(delay, NULL, NULL, UPNP_LOCAL_PORT_ANY, false, 2, error);
|
|
|
|
#endif
|
|
}
|
|
|
|
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 = upnp_discover(upnp_discover_wait * 1000, &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));
|
|
}
|
|
}
|