132 lines
4.6 KiB
C
132 lines
4.6 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;
|
||
|
|
||
|
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));
|
||
|
}
|
||
|
}
|