tinc/src/net.c
Guus Sliepen 3308d13e7e Handle UDP packets from different and ports than advertised.
Previously, tinc used a fixed address and port for each node for UDP packet
exchange.  The port was the one advertised by that node as its listening port.
However, due to NAT the port might be different.  Now, tinc sends a different
session key to each node. This way, the sending node can be determined from
incoming packets by checking the MAC against all session keys. If a match is
found, the address and port for that node are updated.
2009-04-03 01:05:23 +02:00

491 lines
11 KiB
C

/*
net.c -- most of the network code
Copyright (C) 1998-2005 Ivo Timmermans,
2000-2009 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., 675 Mass Ave, Cambridge, MA 02139, USA.
$Id$
*/
#include "system.h"
#include <openssl/rand.h>
#include "utils.h"
#include "avl_tree.h"
#include "conf.h"
#include "connection.h"
#include "device.h"
#include "event.h"
#include "graph.h"
#include "logger.h"
#include "meta.h"
#include "net.h"
#include "netutl.h"
#include "process.h"
#include "protocol.h"
#include "route.h"
#include "subnet.h"
#include "xalloc.h"
bool do_purge = false;
volatile bool running = false;
time_t now = 0;
/* Purge edges and subnets of unreachable nodes. Use carefully. */
static void purge(void)
{
avl_node_t *nnode, *nnext, *enode, *enext, *snode, *snext;
node_t *n;
edge_t *e;
subnet_t *s;
cp();
ifdebug(PROTOCOL) logger(LOG_DEBUG, _("Purging unreachable nodes"));
/* Remove all edges and subnets owned by unreachable nodes. */
for(nnode = node_tree->head; nnode; nnode = nnext) {
nnext = nnode->next;
n = nnode->data;
if(!n->status.reachable) {
ifdebug(SCARY_THINGS) logger(LOG_DEBUG, _("Purging node %s (%s)"), n->name,
n->hostname);
for(snode = n->subnet_tree->head; snode; snode = snext) {
snext = snode->next;
s = snode->data;
if(!tunnelserver)
send_del_subnet(broadcast, s);
subnet_del(n, s);
}
for(enode = n->edge_tree->head; enode; enode = enext) {
enext = enode->next;
e = enode->data;
if(!tunnelserver)
send_del_edge(broadcast, e);
edge_del(e);
}
}
}
/* Check if anyone else claims to have an edge to an unreachable node. If not, delete node. */
for(nnode = node_tree->head; nnode; nnode = nnext) {
nnext = nnode->next;
n = nnode->data;
if(!n->status.reachable) {
for(enode = edge_weight_tree->head; enode; enode = enext) {
enext = enode->next;
e = enode->data;
if(e->to == n)
break;
}
if(!enode)
node_del(n);
}
}
}
/*
put all file descriptors in an fd_set array
While we're at it, purge stuff that needs to be removed.
*/
static int build_fdset(fd_set *readset, fd_set *writeset)
{
avl_node_t *node, *next;
connection_t *c;
int i, max = 0;
cp();
FD_ZERO(readset);
FD_ZERO(writeset);
for(node = connection_tree->head; node; node = next) {
next = node->next;
c = node->data;
if(c->status.remove) {
connection_del(c);
if(!connection_tree->head)
purge();
} else {
FD_SET(c->socket, readset);
if(c->outbuflen > 0)
FD_SET(c->socket, writeset);
if(c->socket > max)
max = c->socket;
}
}
for(i = 0; i < listen_sockets; i++) {
FD_SET(listen_socket[i].tcp, readset);
if(listen_socket[i].tcp > max)
max = listen_socket[i].tcp;
FD_SET(listen_socket[i].udp, readset);
if(listen_socket[i].udp > max)
max = listen_socket[i].udp;
}
FD_SET(device_fd, readset);
if(device_fd > max)
max = device_fd;
return max;
}
/*
Terminate a connection:
- Close the socket
- Remove associated edge and tell other connections about it if report = true
- Check if we need to retry making an outgoing connection
- Deactivate the host
*/
void terminate_connection(connection_t *c, bool report)
{
cp();
if(c->status.remove)
return;
ifdebug(CONNECTIONS) logger(LOG_NOTICE, _("Closing connection with %s (%s)"),
c->name, c->hostname);
c->status.remove = true;
c->status.active = false;
if(c->node)
c->node->connection = NULL;
if(c->socket)
closesocket(c->socket);
if(c->edge) {
if(report && !tunnelserver)
send_del_edge(broadcast, c->edge);
edge_del(c->edge);
/* 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(broadcast, e);
edge_del(e);
}
}
}
/* Check if this was our outgoing connection */
if(c->outgoing) {
retry_outgoing(c->outgoing);
c->outgoing = NULL;
}
free(c->outbuf);
c->outbuf = NULL;
c->outbuflen = 0;
c->outbufsize = 0;
c->outbufstart = 0;
}
/*
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 check_dead_connections(void)
{
avl_node_t *node, *next;
connection_t *c;
cp();
for(node = connection_tree->head; node; node = next) {
next = node->next;
c = node->data;
if(c->last_ping_time + pingtimeout < now) {
if(c->status.active) {
if(c->status.pinged) {
ifdebug(CONNECTIONS) logger(LOG_INFO, _("%s (%s) didn't respond to PING in %ld seconds"),
c->name, c->hostname, now - c->last_ping_time);
c->status.timeout = true;
terminate_connection(c, true);
} else if(c->last_ping_time + pinginterval < now) {
send_ping(c);
}
} else {
if(c->status.remove) {
logger(LOG_WARNING, _("Old connection_t for %s (%s) status %04x still lingering, deleting..."),
c->name, c->hostname, c->status.value);
connection_del(c);
continue;
}
ifdebug(CONNECTIONS) logger(LOG_WARNING, _("Timeout from %s (%s) during authentication"),
c->name, c->hostname);
if(c->status.connecting) {
c->status.connecting = false;
closesocket(c->socket);
do_outgoing_connection(c);
} else {
terminate_connection(c, false);
}
}
}
if(c->outbuflen > 0 && c->last_flushed_time + pingtimeout < now) {
if(c->status.active) {
ifdebug(CONNECTIONS) logger(LOG_INFO,
_("%s (%s) could not flush for %ld seconds (%d bytes remaining)"),
c->name, c->hostname, now - c->last_flushed_time, c->outbuflen);
c->status.timeout = true;
terminate_connection(c, true);
}
}
}
}
/*
check all connections to see if anything
happened on their sockets
*/
static void check_network_activity(fd_set * readset, fd_set * writeset)
{
connection_t *c;
avl_node_t *node;
int result, i;
socklen_t len = sizeof(result);
vpn_packet_t packet;
cp();
/* check input from kernel */
if(FD_ISSET(device_fd, readset)) {
if(read_packet(&packet)) {
packet.priority = 0;
route(myself, &packet);
}
}
/* check meta connections */
for(node = connection_tree->head; node; node = node->next) {
c = node->data;
if(c->status.remove)
continue;
if(FD_ISSET(c->socket, readset)) {
if(c->status.connecting) {
c->status.connecting = false;
getsockopt(c->socket, SOL_SOCKET, SO_ERROR, &result, &len);
if(!result)
finish_connecting(c);
else {
ifdebug(CONNECTIONS) logger(LOG_DEBUG,
_("Error while connecting to %s (%s): %s"),
c->name, c->hostname, strerror(result));
closesocket(c->socket);
do_outgoing_connection(c);
continue;
}
}
if(!receive_meta(c)) {
terminate_connection(c, c->status.active);
continue;
}
}
if(FD_ISSET(c->socket, writeset)) {
if(!flush_meta(c)) {
terminate_connection(c, c->status.active);
continue;
}
}
}
for(i = 0; i < listen_sockets; i++) {
if(FD_ISSET(listen_socket[i].udp, readset))
handle_incoming_vpn_data(listen_socket[i].udp);
if(FD_ISSET(listen_socket[i].tcp, readset))
handle_new_meta_connection(listen_socket[i].tcp);
}
}
/*
this is where it all happens...
*/
int main_loop(void)
{
fd_set readset, writeset;
struct timeval tv;
int r, maxfd;
time_t last_ping_check, last_config_check, last_graph_dump;
event_t *event;
cp();
last_ping_check = now;
last_config_check = now;
last_graph_dump = now;
srand(now);
srand48(now);
running = true;
while(running) {
now = time(NULL);
// tv.tv_sec = 1 + (rand() & 7); /* Approx. 5 seconds, randomized to prevent global synchronisation effects */
tv.tv_sec = 1;
tv.tv_usec = 0;
maxfd = build_fdset(&readset, &writeset);
r = select(maxfd + 1, &readset, &writeset, NULL, &tv);
if(r < 0) {
if(errno != EINTR && errno != EAGAIN) {
logger(LOG_ERR, _("Error while waiting for input: %s"),
strerror(errno));
cp_trace();
dump_connections();
return 1;
}
continue;
}
check_network_activity(&readset, &writeset);
if(do_purge) {
purge();
do_purge = false;
}
/* Let's check if everybody is still alive */
if(last_ping_check + pingtimeout < now) {
check_dead_connections();
last_ping_check = now;
if(routing_mode == RMODE_SWITCH)
age_subnets();
age_past_requests();
/* Should we regenerate our key? */
if(keyexpires < now) {
avl_node_t *node;
node_t *n;
ifdebug(STATUS) logger(LOG_INFO, _("Expiring symmetric keys"));
for(node = node_tree->head; node; node = node->next) {
n = node->data;
if(n->inkey) {
free(n->inkey);
n->inkey = NULL;
}
}
send_key_changed(broadcast, myself);
keyexpires = now + keylifetime;
}
}
if(sigalrm) {
logger(LOG_INFO, _("Flushing event queue"));
expire_events();
sigalrm = false;
}
while((event = get_expired_event())) {
event->handler(event->data);
free_event(event);
}
if(sighup) {
connection_t *c;
avl_node_t *node;
char *fname;
struct stat s;
sighup = false;
/* Reread our own configuration file */
exit_configuration(&config_tree);
init_configuration(&config_tree);
if(!read_server_config()) {
logger(LOG_ERR, _("Unable to reread configuration file, exitting."));
return 1;
}
/* Close connections to hosts that have a changed or deleted host config file */
for(node = connection_tree->head; node; node = node->next) {
c = node->data;
asprintf(&fname, "%s/hosts/%s", confbase, c->name);
if(stat(fname, &s) || s.st_mtime > last_config_check)
terminate_connection(c, c->status.active);
free(fname);
}
last_config_check = now;
/* Try to make outgoing connections */
try_outgoing_connections();
}
/* Dump graph if wanted every 60 seconds*/
if(last_graph_dump + 60 < now) {
dump_graph();
last_graph_dump = now;
}
}
return 0;
}