diff --git a/ChangeLog b/ChangeLog index 6784926..fbafaaa 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,8 +1,6 @@ vNEXT: - UDP support now works. It only works with sslh-fork, - which will create a single process that handles all - UDP connections with select(). Probes specified in - the `protocols` configuration entry are tried on + UDP support now works. It only works with sslh-select. + Probes specified in the `protocols` configuration entry are tried on incoming packets, TCP or UDP, and forwarded based on the input protocol (an incoming TCP connection will be forwarded as TCP, and same with UDP). @@ -11,6 +9,14 @@ vNEXT: assumed to be a DNS request and forwarded accordingly. Note this could cause problems if combined with incoming TLS with SNI. + UDP clients and servers need to agree on the + IPv4/IPv6 they use: use the same protocol on all + sides! Often, this means explicitely using + 'ip4-localhost'. + Currently there is a hard limit of MAX_UDP_SRC + (1024) UDP connections tracked at once, which get + forgotten after a hardcoded timeout of UDP_TIMEOUT + (60s), all defined in udp-listener.c. sslh-select refactored to remove linear searches. diff --git a/collection.c b/collection.c index 97ea9a1..e0d556d 100644 --- a/collection.c +++ b/collection.c @@ -68,6 +68,7 @@ struct connection* collection_alloc_cnx_from_fd(struct cnx_collection* collectio if (!cnx) return NULL; init_cnx(cnx); + cnx->type = SOCK_STREAM; cnx->q[0].fd = fd; cnx->state = ST_PROBING; cnx->probe_timeout = time(NULL) + cfg.timeout; diff --git a/common.h b/common.h index 02611df..e900dc4 100644 --- a/common.h +++ b/common.h @@ -95,7 +95,12 @@ struct queue { int deferred_data_size; }; +struct known_udp_source; + struct connection { + int type; /* SOCK_DGRAM | SOCK_STREAM */ + + /* SOCK_STREAM */ enum connection_state state; time_t probe_timeout; struct sslhcfg_protocols_item* proto; @@ -104,8 +109,12 @@ struct connection { * q[1]: queue for internal connection (httpd or sshd); * */ struct queue q[2]; + + /* SOCK_DGRAM */ + struct known_udp_source* udp_source; }; + struct listen_endpoint { int socketfd; /* file descriptor of listening socket */ int type; /* SOCK_DGRAM | SOCK_STREAM */ diff --git a/echosrv.c b/echosrv.c index 9f83d70..4a7ce88 100644 --- a/echosrv.c +++ b/echosrv.c @@ -135,7 +135,7 @@ void print_udp_xchange(int sockfd, struct sockaddr* addr, socklen_t addrlen) void udp_echo(struct listen_endpoint* listen_socket) { char data[65536]; - struct sockaddr src_addr, sock_name; + struct sockaddr src_addr; socklen_t addrlen; memset(data, 0, sizeof(data)); diff --git a/sslh-fork.c b/sslh-fork.c index bbf2fc1..128c144 100644 --- a/sslh-fork.c +++ b/sslh-fork.c @@ -220,7 +220,7 @@ void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen) case 0: set_listen_procname(&listen_sockets[i]); if (listen_sockets[i].type == SOCK_DGRAM) - udp_listener(listen_sockets, num_addr_listen, i); + log_message(LOG_ERR, "UDP not (yet?) supported in sslh-fork\n"); else tcp_listener(listen_sockets, num_addr_listen, i); break; diff --git a/sslh-select.c b/sslh-select.c index acd318a..af76df1 100644 --- a/sslh-select.c +++ b/sslh-select.c @@ -32,6 +32,7 @@ #include "common.h" #include "probe.h" +#include "udp-listener.h" #include "collection.h" #include "gap.h" @@ -339,7 +340,7 @@ int active_queue(struct connection* cnx, int fd) } /* Process a connection that is active in read */ -static void cnx_read_process(struct select_info* fd_info, +static void tcp_read_process(struct select_info* fd_info, int fd) { if (debug) fprintf(stderr, "cnx_read_process fd %d\n", fd); @@ -374,6 +375,26 @@ static void cnx_read_process(struct select_info* fd_info, } } +static void cnx_read_process(struct select_info* fd_info, int fd) +{ + cnx_collection* collection = fd_info->collection; + struct connection* cnx = collection_get_cnx_from_fd(collection, fd); + switch (cnx->type) { + case SOCK_STREAM: + tcp_read_process(fd_info, fd); + break; + + case SOCK_DGRAM: + udp_s2c_forward(cnx->udp_source); + break; + + default: + log_message(LOG_ERR, "cnx_read_process: Illegal connection type %d\n", cnx->type); + dump_connection(cnx); + exit(1); + } + +} /* Process a connection that is active in write */ static void cnx_write_process(struct select_info* fd_info, int fd) @@ -398,20 +419,45 @@ static void cnx_write_process(struct select_info* fd_info, int fd) } } -/* Process a connection that accepts a socket */ -void cnx_accept_process(struct select_info* fd_info, int fd) +/* Process a connection that accepts a socket + * (For UDP, this means all traffic coming from remote clients) + * */ +void cnx_accept_process(struct select_info* fd_info, struct listen_endpoint* listen_socket) { + int fd = listen_socket->socketfd; + int type = listen_socket->type; + struct connection* cnx; + int new_fd; + if (debug) fprintf(stderr, "cnx_accept_process fd %d\n", fd); - struct connection* cnx = accept_new_connection(fd, fd_info->collection); + switch (type) { + case SOCK_STREAM: + cnx = accept_new_connection(fd, fd_info->collection); - if (cnx) { - add_probing_cnx(fd_info, cnx); - int new_socket = cnx->q[0].fd; - FD_SET(new_socket, &fd_info->fds_r); - if (new_socket >= fd_info->max_fd) - fd_info->max_fd = new_socket + 1; + if (cnx) { + add_probing_cnx(fd_info, cnx); + new_fd = cnx->q[0].fd; + } + break; + + case SOCK_DGRAM: + new_fd = udp_c2s_forward(fd, fd_info->collection); + fprintf(stderr, "new_fd %d\n", new_fd); + if (new_fd == -1) + return; + break; + + default: + log_message(LOG_ERR, "Inconsistent cnx type: %d\n", type); + exit(1); + return; } + + FD_SET(new_fd, &fd_info->fds_r); + if (new_fd >= fd_info->max_fd) + fd_info->max_fd = new_fd + 1; + } @@ -465,10 +511,25 @@ void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen) if (res < 0) perror("select"); + + /* UDP timeouts: clear out connections after some idle time */ + for (i = 0; i < fd_info.max_fd; i++) { + /* if it's either in read or write set, there is a connection + * behind that file descriptor */ + if (FD_ISSET(i, &fd_info.fds_r) || FD_ISSET(i, &fd_info.fds_w)) { + struct connection* cnx = collection_get_cnx_from_fd(fd_info.collection, i); + if (cnx && udp_timedout(cnx)) { + FD_CLR(i, &fd_info.fds_r); + FD_CLR(i, &fd_info.fds_w); + collection_remove_cnx(fd_info.collection, cnx); + } + } + } + /* Check main socket for new connections */ for (i = 0; i < num_addr_listen; i++) { if (FD_ISSET(listen_sockets[i].socketfd, &readfds)) { - cnx_accept_process(&fd_info, listen_sockets[i].socketfd); + cnx_accept_process(&fd_info, &listen_sockets[i]); /* don't also process it as a read socket */ FD_CLR(listen_sockets[i].socketfd, &readfds); @@ -507,7 +568,6 @@ void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen) cnx_read_process(&fd_info, i); } } - } } diff --git a/t_load b/t_load index b229043..d7102fe 100755 --- a/t_load +++ b/t_load @@ -18,12 +18,9 @@ use Conf::Libconfig; ## BEGIN TEST CONFIG -# Do we test sslh-select or sslh-fork? -my $sslh_binary = "./sslh-select"; - # How many total clients to we start? Each client will pick # a new protocol among what's in test.cfg. -my $NUM_CNX = 20; +my $NUM_CNX = 50; # Delay between starting new processes when starting up. If # you start 200 processes in under a second, things go wrong @@ -33,7 +30,7 @@ my $start_time_delay = .5; # Max times we repeat the test string: allows to test for # large messages. -my $block_rpt = 4096; +my $block_rpt = 5; # Probability to stop a client after a message (e.g. with # .01 a client will send an average of 100 messages before @@ -53,28 +50,33 @@ my ($sslh_tcp_address, $sslh_udp_address); foreach my $l (@listen) { if ($l->{is_udp}) { $sslh_udp_address //= "$l->{host}:$l->{port}"; - last if defined $sslh_tcp_address; } else { $sslh_tcp_address //= "$l->{host}:$l->{port}"; - last if defined $sslh_udp_address; } + last if defined $sslh_tcp_address and defined $sslh_udp_address; } # code snippets to connect to each protocol my %connect_params = ( - ssh => { + regex => { + is_udp => 1, + sleep => 0, + test_data => "foo bar", + resp_len => 12, + }, + ssh => { sleep => 20, # So it times out 50% of connections test_data => "SSH-2.0 hello", resp_len => 18, # length "ssh: SSH-2.0 hello" => 18 }, tinc => { - sleep => 10, + sleep => 0, test_data => "0 ", resp_len => 8, # length "tinc: 0 " => 10 }, openvpn => { - sleep => 10, + sleep => 0, test_data => "\x00\x00", resp_len => 11, # length "openvpn: \x0\x0" => 11 }, @@ -103,13 +105,13 @@ sub client { while (1) { my $r; - warn "$client_id: connect $sslh_tcp_address\n"; + #warn "$client_id: connect $sslh_tcp_address\n"; my $cnx = new IO::Socket::INET(PeerHost => $sslh_tcp_address); die "$@\n" if (!$cnx); my $cnt = 0; - warn "$client_id: connecting $service\n"; + #warn "$client_id: connecting $service\n"; if (not connect_service($cnx, $service)) { print $fd_out "$client_id\t0\tC\n"; @@ -117,7 +119,7 @@ sub client { exit; } - warn "$client_id: shoveling $service\n"; + #warn "$client_id: shoveling $service\n"; while (1) { my $test_data = "$service $cnt" x int(rand($block_rpt)+1) . "\n"; @@ -141,26 +143,60 @@ sub client { exit 0; } +# For now, a simple regex client +sub udp_client { + my ($protocol, $client_id, $fd_out) = @_; + + warn "UDP client starts\n"; + + while (1) { + my $cnx = new IO::Socket::INET(Proto => 'udp', PeerHost => $sslh_udp_address); + # my $cnx; socket $cnx, PF_INET, SOCK_DGRAM, 0 or die "socket: $!\n"; + die "$@\n" if (!$cnx); + my $cnt = 0; + + while (1) { + my $test_data = "foo udp $cnt"x int(rand($block_rpt)+1). "\n"; + + my $ipaddr = inet_aton("localhost"); + my $portaddr = sockaddr_in(8086, $ipaddr); + my $res = send($cnx, $test_data, 0, $portaddr); + if ($res != length($test_data)) { + die "cannot sendto: $!"; + } + + my $expected= "$protocol->{name}: $test_data"; + + my $r; + defined(recv($cnx, $r, length $expected, 0)) or die "recv: $!\n"; + + my $r_l = length $r; + my $e_l = length $expected; + $fd_out->autoflush; + my $error = ""; + $error = "M" if $r ne $expected; + print $fd_out ("$client_id\t$r_l\t$error\n"); + ($? = 1, die "udp got [$r] expected [$expected]\n") if ($r ne $expected); + if (rand(1) < $stop_client_probability) { + print $fd_out ("$client_id\t$r_l\tD\n"); + last; + } + $cnt++; + } + } +} + foreach my $p (@{$conf->fetch_array("protocols")}) { if (!fork) { my $udp = $p->{is_udp} ? "--udp" : ""; - my $cmd = "./echosrv $udp -p $p->{host}:$p->{port} --prefix '$p->{name}: '"; + my $cmd = "./echosrv $udp -p $p->{host}:$p->{port} --prefix '$p->{name}: ' 2> /dev/null"; warn "$cmd\n"; exec $cmd; exit; } } -# Start sslh with the right plumbing -my $sslh_pid; -if (0) { -if (!($sslh_pid = fork)) { - my $cmd = "$sslh_binary -F test.cfg"; - warn "$cmd\n"; - exec $cmd; -} -warn "spawned $sslh_pid\n"; -} +warn "Don't forget to run sslh -F test.cfg!\n"; sleep 2; # Let echosrv's and sslh start @@ -178,6 +214,7 @@ if (!fork) { my @p = grep { $_->{name} eq $p_name } @protocols; my $p = shift @p; if ($p->{is_udp}) { + udp_client($p, "$p->{name}$client_num", $c_out); } else { client($p, "$p->{name}$client_num", $c_out); } @@ -194,22 +231,31 @@ if (!fork) { # The condition here selects between pretty output or # raw output if (1) { + my $CLEAR_LINE = "\033[2K"; + my $CURSOR_HOME = "\033[1;1H"; + my $CLEAR_SCREEN = "\033[2J"; + # Process that retrieves client output to pretty print - print "\033[2J"; + print $CLEAR_SCREEN; # Clear screen while (<$c_in>) { chop; my ($client_id, $r_l, $error, @rest) = split /\t/, $_; - my ($curr_rcv) = ${$data{$client_id}}[0]; - my ($curr_error) = ${$data{$client_id}}[1] // ""; + $data{$client_id} = [ 0, ""] if not exists $data{$client_id}; + my ($curr_rcv) = ${$data{$client_id}}[0] + $r_l;; $error //= ""; + my ($curr_error) = "${$data{$client_id}}[1]$error"; + $data{$client_id} = [ $r_l + $curr_rcv, "$curr_error$error" ]; - print "\033[0;0H"; - foreach my $i (sort keys %data) { - ($r_l, $error) = @{$data{$i}}; - print "\033[2K$i\t$r_l\t$error\n"; - } + + $client_id =~ /(\d+)/; + my $i = $1; + # print $CURSOR_HOME; + print "\033[$i;1H$CLEAR_LINE$client_id\t$curr_rcv\t$curr_error\n"; + #foreach my $i (sort keys %data) { + # ($r_l, $error) = @{$data{$i}}; + # print "$CLEAR_LINE$i\t$r_l\t$error\n"; } } else { # Just print the client outputs diff --git a/test.cfg b/test.cfg index 34a5aee..328a80e 100644 --- a/test.cfg +++ b/test.cfg @@ -4,7 +4,7 @@ verbose: 3; foreground: true; inetd: false; -numeric: false; +numeric: true; transparent: false; timeout: 10; # Probe test writes slowly pidfile: "/tmp/sslh_test.pid"; @@ -17,8 +17,8 @@ syslog_facility: "auth"; listen: ( { host: "localhost"; port: "8080"; keepalive: true; }, - { host: "localhost"; port: "8081"; keepalive: true; } -# { host: "localhost"; is_udp: true; port: "4443"; } + { host: "localhost"; port: "8081"; keepalive: true; }, + { host: "ip4-localhost"; is_udp: true; port: "8086"; } ); @@ -31,7 +31,9 @@ protocols: { name: "openvpn"; host: "localhost"; port: "9004"; }, { name: "xmpp"; host: "localhost"; port: "9009"; }, { name: "adb"; host: "localhost"; port: "9010"; }, - { name: "regex"; host: "localhost"; is_udp: true; port: "2020"; }, + { name: "regex"; host: "ip4-localhost"; is_udp: true; port: "9020"; + regex_patterns: [ "^foo" ]; + }, { name: "regex"; host: "localhost"; port: "9011"; regex_patterns: [ "^foo", "^bar" ]; minlength: 4; diff --git a/udp-listener.c b/udp-listener.c index 1a7b067..51508f6 100644 --- a/udp-listener.c +++ b/udp-listener.c @@ -23,13 +23,17 @@ #include "common.h" #include "probe.h" #include "sslh-conf.h" +#include "udp-listener.h" /* UDP support types and stuff */ struct known_udp_source { int allocated; - struct sockaddr client_addr; + struct sockaddr client_addr; /* Contains the remote client address */ socklen_t addrlen; + + int local_endpoint; /* Contains the local address */ + time_t last_active; struct sslhcfg_protocols_item* proto; /* Where to connect it to */ @@ -73,13 +77,13 @@ static int get_empty_source(struct known_udp_source* ks, int ks_len) /* Array to keep the UDP sources we have seen before */ struct known_udp_source udp_known_sources[MAX_UDP_SRC]; - /* Process UDP coming from outside (client towards server) * If it's a new source, probe; otherwise, forward to previous target * Returns: >= 0 sockfd of newly allocated socket, for new connections * -1 otherwise * */ -static int udp_c2s_forward(int sockfd) { +int udp_c2s_forward(int sockfd, cnx_collection* collection) +{ char addr_str[NI_MAXHOST+1+NI_MAXSERV+1]; struct sockaddr src_addr; struct addrinfo addrinfo; @@ -91,7 +95,6 @@ static int udp_c2s_forward(int sockfd) { This will do. Dynamic allocation is possible with the MSG_PEEK flag in recvfrom(2), but that'd imply malloc/free overhead for each packet, when really 64K is not that much */ - fprintf(stderr, "recvfrom(%d)\n", getpid()); addrlen = sizeof(src_addr); len = recvfrom(sockfd, data, sizeof(data), 0, &src_addr, &addrlen); if (len < 0) { @@ -104,107 +107,85 @@ static int udp_c2s_forward(int sockfd) { addrinfo.ai_addrlen = addrlen; if (cfg.verbose) fprintf(stderr, "received %ld UDP from %d:%s\n", len, target, sprintaddr(addr_str, sizeof(addr_str), &addrinfo)); + if (target == -1) { target = get_empty_source(udp_known_sources, ARRAY_SIZE(udp_known_sources)); fprintf(stderr, "source target index %d\n", target); - if (target == -1) exit(0); /* TODO handle this properly */ - /* A probe worked: save this as an active connection */ + if (target == -1) { + fprintf(stderr, "Out of UDP structs\n"); + exit(0); /* TODO handle this properly */ + } + + /* save this as an active connection */ src = &udp_known_sources[target]; src->allocated = 1; src->client_addr = src_addr; src->addrlen = addrlen; - src->last_active = time(NULL); + src->local_endpoint = sockfd; res = probe_buffer(data, len, &src->proto); /* First version: if we can't work out the protocol from the first * packet, drop it. Conceivably, we could store several packets to * run probes on packet sets */ if (cfg.verbose) fprintf(stderr, "UDP probed: %d\n", res); - if (res != PROBE_MATCH) return -1; + if (res != PROBE_MATCH) { + src->allocated = 0; + return -1; + } src->target_sock = socket(src->proto->saddr->ai_family, SOCK_DGRAM, 0); out = src->target_sock; + + struct connection* cnx = collection_alloc_cnx_from_fd(collection, out); + cnx->type = SOCK_DGRAM; + cnx->udp_source = &udp_known_sources[target]; } src = &udp_known_sources[target]; + /* at this point src is the UDP connection */ res = sendto(src->target_sock, data, len, 0, src->proto->saddr->ai_addr, src->proto->saddr->ai_addrlen); src->last_active = time(NULL); - fprintf(stderr, "sending %d to %s", + fprintf(stderr, "sending %d to %s\n", res, sprintaddr(data, sizeof(data), src->proto->saddr)); return out; } +void udp_s2c_forward(struct known_udp_source* src) +{ + int sockfd = src->target_sock; + char data[65536]; + int res; + + res = recvfrom(sockfd, data, sizeof(data), 0, NULL, NULL); + fprintf(stderr, "recvfrom %d\n", res); + CHECK_RES_DIE(res, "udp_listener/recvfrom"); + res = sendto(src->local_endpoint, data, res, 0, + &src->client_addr, src->addrlen); + src->last_active = time(NULL); + fprintf(stderr, "sendto %d to\n", res); +} + + /* Clears old connections from udp_known_sources, and from passed fd_set */ #define UDP_TIMEOUT 60 /* Timeout before forgetting the connection, in seconds */ -static void reap_timeouts(struct known_udp_source* sources, int n_src, fd_set* fd) +int udp_timedout(struct connection* cnx) { int i; time_t now = time(NULL); - struct known_udp_source* src; + struct known_udp_source* src = cnx->udp_source; - for (i = 0; i < n_src; i++) { - src = &sources[i]; - if (src->allocated && (now - src->last_active > UDP_TIMEOUT)) { - close(src->target_sock); - FD_CLR(src->target_sock, fd); - memset(&sources[i], 0, sizeof(sources[i])); - if (cfg.verbose > 3) - fprintf(stderr, "disconnect %d\n", i); - } + if (!cnx->udp_source) return 0; /* Not a UDP connection */ + + if (src->allocated && (now - src->last_active > UDP_TIMEOUT)) { + close(src->target_sock); + memset(src, 0, sizeof(*src)); + if (cfg.verbose > 3) + fprintf(stderr, "disconnect timed out UDP %d\n", i); + return 1; } + return 0; } - -/* UDP listener: upon incoming packet, find where it should go - * This is run in its own process and never returns. - */ -void udp_listener(struct listen_endpoint* endpoint, int num_endpoints, int active_endpoint) -{ - fd_set fds_r, fds_r_tmp; - char data[65536]; /* TODO what? */ - int max_fd, res, sockfd, i; - struct known_udp_source* src; - struct timeval tv; - - FD_ZERO(&fds_r); - FD_SET(endpoint[active_endpoint].socketfd, &fds_r); - max_fd = endpoint[active_endpoint].socketfd + 1; - - while (1) { - fds_r_tmp = fds_r; - tv.tv_sec = 1; - tv.tv_usec = 0; - res = select(max_fd + 1, &fds_r_tmp, NULL, NULL, &tv); - CHECK_RES_DIE(res, "select"); - - if (res) { - if (FD_ISSET(endpoint[active_endpoint].socketfd, &fds_r_tmp)) { - sockfd = udp_c2s_forward(endpoint[active_endpoint].socketfd); - if (sockfd >= 0) { - FD_SET(sockfd, &fds_r); - max_fd = MAX(max_fd, sockfd); - } - } else { - for (i = 0; i < ARRAY_SIZE(udp_known_sources); i++) { - src = &udp_known_sources[i]; - if (src->allocated) { - sockfd = src->target_sock; - if (FD_ISSET(sockfd, &fds_r_tmp)) { - res = recvfrom(sockfd, data, sizeof(data), 0, NULL, NULL); - fprintf(stderr, "recvfrom %d\n", res); - CHECK_RES_DIE(res, "udp_listener/recvfrom"); - res = sendto(endpoint[active_endpoint].socketfd, data, res, 0, - &src->client_addr, src->addrlen); - src->last_active = time(NULL); - fprintf(stderr, "sendto %d to\n", res); - } - } - } - } - } - reap_timeouts(udp_known_sources, ARRAY_SIZE(udp_known_sources), &fds_r); - } -} diff --git a/udp-listener.h b/udp-listener.h index 06536e6..f5d72b7 100644 --- a/udp-listener.h +++ b/udp-listener.h @@ -1,11 +1,26 @@ #ifndef UDPLISTENER_H #define UDPLISTENER_H +#include "collection.h" + /* UDP listener: upon incoming packet, find where it should go * This is run in its own process and never returns. */ void udp_listener(struct listen_endpoint* endpoint, int num_endpoints, int active_endpoint); +/* Process UDP coming from outside (client towards server) + * If it's a new source, probe; otherwise, forward to previous target + * Returns: >= 0 sockfd of newly allocated socket, for new connections + * -1 otherwise + * */ +int udp_c2s_forward(int sockfd, cnx_collection* collection); + +/* Process UDP coming from inside (server towards client) */ +void udp_s2c_forward(struct known_udp_source* src); + + +/* Checks if a connection timed out, in which case clear it. */ +int udp_timedout(struct connection* cnx); #endif /* UDPLISTENER_H */