tinc/src/net.c
2022-04-22 20:24:15 +02:00

525 lines
13 KiB
C

/*
net.c -- most of the network code
Copyright (C) 1998-2005 Ivo Timmermans,
2000-2021 Guus Sliepen <guus@tinc-vpn.org>
2006 Scott Lamb <slamb@slamb.org>
2011 Loïc Grenié <loic.grenie@gmail.com>
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 "system.h"
#include "autoconnect.h"
#include "conf.h"
#include "connection.h"
#include "device.h"
#include "graph.h"
#include "logger.h"
#include "meta.h"
#include "names.h"
#include "net.h"
#include "netutl.h"
#include "protocol.h"
#include "subnet.h"
#include "utils.h"
#include "xalloc.h"
int contradicting_add_edge = 0;
int contradicting_del_edge = 0;
static int sleeptime = 10;
time_t last_config_check = 0;
static timeout_t pingtimer;
static timeout_t periodictimer;
static struct timeval last_periodic_run_time;
/* Purge edges and subnets of unreachable nodes. Use carefully. */
void purge(void) {
logger(DEBUG_PROTOCOL, LOG_DEBUG, "Purging unreachable nodes");
/* Remove all edges and subnets owned by unreachable nodes. */
for splay_each(node_t, n, node_tree) {
if(!n->status.reachable) {
logger(DEBUG_SCARY_THINGS, LOG_DEBUG, "Purging node %s (%s)", n->name, n->hostname);
for splay_each(subnet_t, s, n->subnet_tree) {
send_del_subnet(everyone, s);
if(!strictsubnets) {
subnet_del(n, s);
}
}
for splay_each(edge_t, e, n->edge_tree) {
if(!tunnelserver) {
send_del_edge(everyone, e);
}
edge_del(e);
}
}
}
/* Check if anyone else claims to have an edge to an unreachable node. If not, delete node. */
for splay_each(node_t, n, node_tree) {
if(!n->status.reachable) {
for splay_each(edge_t, e, edge_weight_tree)
if(e->to == n) {
return;
}
if(!autoconnect && (!strictsubnets || !n->subnet_tree->head))
/* in strictsubnets mode do not delete nodes with subnets */
{
node_del(n);
}
}
}
}
/* Put a misbehaving connection in the tarpit */
void tarpit(int fd) {
static int pits[10] = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1};
static unsigned int next_pit = 0;
if(pits[next_pit] != -1) {
closesocket(pits[next_pit]);
}
pits[next_pit++] = fd;
if(next_pit >= sizeof pits / sizeof pits[0]) {
next_pit = 0;
}
}
/*
Terminate a connection:
- Mark it as inactive
- Remove the edge representing this connection
- Kill it with fire
- Check if we need to retry making an outgoing connection
*/
void terminate_connection(connection_t *c, bool report) {
logger(DEBUG_CONNECTIONS, LOG_NOTICE, "Closing connection with %s (%s)", c->name, c->hostname);
if(c->node) {
if(c->node->connection == c) {
c->node->connection = NULL;
}
if(c->edge) {
if(report && !tunnelserver) {
send_del_edge(everyone, c->edge);
}
edge_del(c->edge);
c->edge = NULL;
/* Run MST and SSSP algorithms */
graph();
/* If the node is not reachable anymore but we remember it had an edge to us, clean it up */
if(report && !c->node->status.reachable) {
edge_t *e;
e = lookup_edge(c->node, myself);
if(e) {
if(!tunnelserver) {
send_del_edge(everyone, e);
}
edge_del(e);
}
}
}
}
outgoing_t *outgoing = c->outgoing;
connection_del(c);
/* Check if this was our outgoing connection */
if(outgoing) {
do_outgoing_connection(outgoing);
}
#ifndef HAVE_MINGW
/* Clean up dead proxy processes */
while(waitpid(-1, NULL, WNOHANG) > 0);
#endif
}
/*
Check if the other end is active.
If we have sent packets, but didn't receive any,
then possibly the other end is dead. We send a
PING request over the meta connection. If the other
end does not reply in time, we consider them dead
and close the connection.
*/
static void timeout_handler(void *data) {
bool close_all_connections = false;
/*
timeout_handler will start after 30 seconds from start of tincd
hold information about the elapsed time since last time the handler
has been run
*/
long sleep_time = now.tv_sec - last_periodic_run_time.tv_sec;
/*
It seems that finding sane default value is harder than expected
Since we send every second a UDP packet to make holepunching work
And default UDP state expire on firewalls is between 15-30 seconds
we drop all connections after 60 Seconds - UDPDiscoveryTimeout=30
by default
*/
if(sleep_time > 2 * udp_discovery_timeout) {
logger(DEBUG_ALWAYS, LOG_ERR, "Awaking from dead after %ld seconds of sleep", sleep_time);
/*
Do not send any packets to tinc after we wake up.
The other node probably closed our connection but we still
are holding context information to them. This may happen on
laptops or any other hardware which can be suspended for some time.
Sending any data to node that wasn't expecting it will produce
annoying and misleading errors on the other side about failed signature
verification and or about missing sptps context
*/
close_all_connections = true;
}
last_periodic_run_time = now;
for list_each(connection_t, c, connection_list) {
// control connections (eg. tinc ctl) do not have any timeout
if(c->status.control) {
continue;
}
if(close_all_connections) {
logger(DEBUG_ALWAYS, LOG_ERR, "Forcing connection close after sleep time %s (%s)", c->name, c->hostname);
terminate_connection(c, c->edge);
continue;
}
// Bail out early if we haven't reached the ping timeout for this node yet
if(c->last_ping_time + pingtimeout > now.tv_sec) {
continue;
}
// timeout during connection establishing
if(!c->edge) {
if(c->status.connecting) {
logger(DEBUG_CONNECTIONS, LOG_WARNING, "Timeout while connecting to %s (%s)", c->name, c->hostname);
} else {
logger(DEBUG_CONNECTIONS, LOG_WARNING, "Timeout from %s (%s) during authentication", c->name, c->hostname);
c->status.tarpit = true;
}
terminate_connection(c, c->edge);
continue;
}
// helps in UDP holepunching
try_tx(c->node, false);
// timeout during ping
if(c->status.pinged) {
logger(DEBUG_CONNECTIONS, LOG_INFO, "%s (%s) didn't respond to PING in %ld seconds", c->name, c->hostname, (long)(now.tv_sec - c->last_ping_time));
terminate_connection(c, c->edge);
continue;
}
// check whether we need to send a new ping
if(c->last_ping_time + pinginterval <= now.tv_sec) {
send_ping(c);
}
}
timeout_set(data, &(struct timeval) {
1, rand() % 100000
});
}
static void periodic_handler(void *data) {
/* Check if there are too many contradicting ADD_EDGE and DEL_EDGE messages.
This usually only happens when another node has the same Name as this node.
If so, sleep for a short while to prevent a storm of contradicting messages.
*/
if(contradicting_del_edge > 100 && contradicting_add_edge > 100) {
logger(DEBUG_ALWAYS, LOG_WARNING, "Possible node with same Name as us! Sleeping %d seconds.", sleeptime);
nanosleep(&(struct timespec) {
sleeptime, 0
}, NULL);
sleeptime *= 2;
if(sleeptime < 0) {
sleeptime = 3600;
}
} else {
sleeptime /= 2;
if(sleeptime < 10) {
sleeptime = 10;
}
}
contradicting_add_edge = 0;
contradicting_del_edge = 0;
/* If AutoConnect is set, check if we need to make or break connections. */
if(autoconnect && node_tree->count > 1) {
do_autoconnect();
}
timeout_set(data, &(struct timeval) {
5, rand() % 100000
});
}
void handle_meta_connection_data(connection_t *c) {
if(!receive_meta(c)) {
if(!c->status.control) {
c->status.tarpit = true;
}
terminate_connection(c, c->edge);
return;
}
}
#ifndef HAVE_MINGW
static void sigterm_handler(void *data) {
logger(DEBUG_ALWAYS, LOG_NOTICE, "Got %s signal", strsignal(((signal_t *)data)->signum));
event_exit();
}
static void sighup_handler(void *data) {
logger(DEBUG_ALWAYS, LOG_NOTICE, "Got %s signal", strsignal(((signal_t *)data)->signum));
reopenlogger();
if(reload_configuration()) {
exit(1);
}
}
static void sigalrm_handler(void *data) {
logger(DEBUG_ALWAYS, LOG_NOTICE, "Got %s signal", strsignal(((signal_t *)data)->signum));
retry();
}
#endif
int reload_configuration(void) {
char fname[PATH_MAX];
/* Reread our own configuration file */
exit_configuration(&config_tree);
init_configuration(&config_tree);
if(!read_server_config()) {
logger(DEBUG_ALWAYS, LOG_ERR, "Unable to reread configuration file.");
return EINVAL;
}
read_config_options(config_tree, NULL);
snprintf(fname, sizeof(fname), "%s" SLASH "hosts" SLASH "%s", confbase, myself->name);
read_config_file(config_tree, fname, true);
/* Parse some options that are allowed to be changed while tinc is running */
setup_myself_reloadable();
/* If StrictSubnet is set, expire deleted Subnets and read new ones in */
if(strictsubnets) {
for splay_each(subnet_t, subnet, subnet_tree)
if(subnet->owner) {
subnet->expires = 1;
}
}
for splay_each(node_t, n, node_tree) {
n->status.has_address = false;
}
load_all_nodes();
if(strictsubnets) {
for splay_each(subnet_t, subnet, subnet_tree) {
if(!subnet->owner) {
continue;
}
if(subnet->expires == 1) {
send_del_subnet(everyone, subnet);
if(subnet->owner->status.reachable) {
subnet_update(subnet->owner, subnet, false);
}
subnet_del(subnet->owner, subnet);
} else if(subnet->expires == -1) {
subnet->expires = 0;
} else {
send_add_subnet(everyone, subnet);
if(subnet->owner->status.reachable) {
subnet_update(subnet->owner, subnet, true);
}
}
}
} else { /* Only read our own subnets back in */
for splay_each(subnet_t, subnet, myself->subnet_tree)
if(!subnet->expires) {
subnet->expires = 1;
}
config_t *cfg = lookup_config(config_tree, "Subnet");
while(cfg) {
subnet_t *subnet, *s2;
if(get_config_subnet(cfg, &subnet)) {
if((s2 = lookup_subnet(myself, subnet))) {
if(s2->expires == 1) {
s2->expires = 0;
}
free_subnet(subnet);
} else {
subnet_add(myself, subnet);
send_add_subnet(everyone, subnet);
subnet_update(myself, subnet, true);
}
}
cfg = lookup_config_next(config_tree, cfg);
}
for splay_each(subnet_t, subnet, myself->subnet_tree) {
if(subnet->expires == 1) {
send_del_subnet(everyone, subnet);
subnet_update(myself, subnet, false);
subnet_del(myself, subnet);
}
}
}
/* Try to make outgoing connections */
try_outgoing_connections();
/* Close connections to hosts that have a changed or deleted host config file */
for list_each(connection_t, c, connection_list) {
if(c->status.control) {
continue;
}
snprintf(fname, sizeof(fname), "%s" SLASH "hosts" SLASH "%s", confbase, c->name);
struct stat s;
if(stat(fname, &s) || s.st_mtime > last_config_check) {
logger(DEBUG_CONNECTIONS, LOG_INFO, "Host config file of %s has been changed", c->name);
terminate_connection(c, c->edge);
}
}
last_config_check = now.tv_sec;
return 0;
}
void retry(void) {
/* Reset the reconnection timers for all outgoing connections */
for list_each(outgoing_t, outgoing, outgoing_list) {
outgoing->timeout = 0;
if(outgoing->ev.cb)
timeout_set(&outgoing->ev, &(struct timeval) {
0, 0
});
}
/* Check for outgoing connections that are in progress, and reset their ping timers */
for list_each(connection_t, c, connection_list) {
if(c->outgoing && !c->node) {
c->last_ping_time = 0;
}
}
/* Kick the ping timeout handler */
timeout_set(&pingtimer, &(struct timeval) {
0, 0
});
}
/*
this is where it all happens...
*/
int main_loop(void) {
last_periodic_run_time = now;
timeout_add(&pingtimer, timeout_handler, &pingtimer, &(struct timeval) {
pingtimeout, rand() % 100000
});
timeout_add(&periodictimer, periodic_handler, &periodictimer, &(struct timeval) {
0, 0
});
#ifndef HAVE_MINGW
signal_t sighup = {0};
signal_t sigterm = {0};
signal_t sigquit = {0};
signal_t sigint = {0};
signal_t sigalrm = {0};
signal_add(&sighup, sighup_handler, &sighup, SIGHUP);
signal_add(&sigterm, sigterm_handler, &sigterm, SIGTERM);
signal_add(&sigquit, sigterm_handler, &sigquit, SIGQUIT);
signal_add(&sigint, sigterm_handler, &sigint, SIGINT);
signal_add(&sigalrm, sigalrm_handler, &sigalrm, SIGALRM);
#endif
if(!event_loop()) {
logger(DEBUG_ALWAYS, LOG_ERR, "Error while waiting for input: %s", sockstrerror(sockerrno));
return 1;
}
#ifndef HAVE_MINGW
signal_del(&sighup);
signal_del(&sigterm);
signal_del(&sigquit);
signal_del(&sigint);
signal_del(&sigalrm);
#endif
timeout_del(&periodictimer);
timeout_del(&pingtimer);
return 0;
}