diff --git a/ChangeLog b/ChangeLog
index dfab89e..98d2b19 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,4 +1,26 @@
+v1.10:
+ Fixed calls referring to sockaddr length so they work
+ with FreeBSD.
+
+ Try target addresses in turn until one works if
+ there are several (e.g. "localhost:22" resolves to
+ an IPv6 address and an IPv4 address and sshd does
+ not listen on IPv6).
+
+ Fixed sslh-fork so killing the head process kills
+ the listener processes.
+
+ Heavily cleaned up test suite. Added stress test
+ t_load script. Added coverage (requires lcov).
+
+ Support for XMPP (Arnaud Gendre).
+
+ Updated README.MacOSX (Aaron Madlon-Kay).
+
v1.9: 02AUG2011
+ WARNING: This version does not work with FreeBSD and
+ derivatives!
+
WARNING: Options changed, you'll need to update your
start-up scripts! Log format changed, you'll need to
update log processing scripts!
diff --git a/Makefile b/Makefile
index 3ab7f64..a76c938 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,8 @@
# Configuration
-VERSION="v1.9"
+VERSION="v1.10"
USELIBWRAP= # Use libwrap?
+COV_TEST= # Perform test coverage?
PREFIX=/usr/local
MAN=sslh.8.gz # man page name
@@ -9,19 +10,23 @@ MAN=sslh.8.gz # man page name
# End of configuration -- the rest should take care of
# itself
+ifneq ($(strip $(COV_TEST)),)
+ CFLAGS_COV=-fprofile-arcs -ftest-coverage
+endif
+
CC = gcc
-CFLAGS=-Wall -g
+CFLAGS=-Wall -g $(CFLAGS_COV)
#LIBS=-lnet
LIBS=
-OBJS=common.o
+OBJS=common.o sslh-main.o
ifneq ($(strip $(USELIBWRAP)),)
LIBS:=$(LIBS) -lwrap
CFLAGS:=$(CFLAGS) -DLIBWRAP
endif
-all: sslh $(MAN)
+all: sslh $(MAN) echosrv
.c.o: *.h
$(CC) $(CFLAGS) -D'VERSION=$(VERSION)' -c $<
@@ -31,12 +36,14 @@ sslh: $(OBJS) sslh-fork sslh-select
sslh-fork: $(OBJS) sslh-fork.o Makefile common.h
$(CC) $(CFLAGS) -D'VERSION=$(VERSION)' -o sslh-fork sslh-fork.o $(OBJS) $(LIBS)
- strip sslh-fork
+ #strip sslh-fork
sslh-select: $(OBJS) sslh-select.o Makefile common.h
$(CC) $(CFLAGS) -D'VERSION=$(VERSION)' -o sslh-select sslh-select.o $(OBJS) $(LIBS)
- strip sslh-select
+ #strip sslh-select
+echosrv: $(OBJS) echosrv.o
+ $(CC) $(CFLAGS) -o echosrv echosrv.o common.o $(LIBS)
$(MAN): sslh.pod Makefile
pod2man --section=8 --release=$(VERSION) --center=" " sslh.pod | gzip -9 - > $(MAN)
@@ -58,7 +65,7 @@ uninstall:
update-rc.d sslh remove
clean:
- rm -f sslh-fork sslh-select $(MAN) *.o
+ rm -f sslh-fork sslh-select echosrv $(MAN) *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info
tags:
ctags -T *.[ch]
diff --git a/README b/README
index 9e385a5..45cfa39 100644
--- a/README
+++ b/README
@@ -1,8 +1,8 @@
===== sslh -- A ssl/ssh multiplexer. =====
-sslh accepts HTTPS, SSH and OpenVPN connections on the same
-port. This makes it possible to connect to an SSH server or
-an OpenVPN on port 443 (e.g. from inside a corporate
+sslh accepts HTTPS, SSH, OpenVPN, tinc and XMPP connections
+on the same port. This makes it possible to connect to any
+of these servers on port 443 (e.g. from inside a corporate
firewall, which almost never block port 443) while still
serving HTTPS on that port.
diff --git a/README.MacOSX b/README.MacOSX
index 7d9d177..2e70a7b 100644
--- a/README.MacOSX
+++ b/README.MacOSX
@@ -13,42 +13,42 @@ with launchctl or simply reboot.
----BEGIN FILE----
-
+
- Disabled
-
- KeepAlive
-
- Label
- net.rutschle.sslh
- ProgramArguments
-
- /opt/local/sbin/sslh
- -f
- -v
- -u
- nobody
- -p
- 0.0.0.0:443
- -s
- localhost:22
- -l
- localhost:443
-
- QueueDirectories
-
- RunAtLoad
-
- StandardErrorPath
- /Library/Logs/sslh.log
- StandardOutPath
- /Library/Logs/sslh.log
- WatchPaths
-
+ Disabled
+
+ KeepAlive
+
+ Label
+ net.rutschle.sslh
+ ProgramArguments
+
+ /opt/local/sbin/sslh
+ -f
+ -v
+ -u
+ nobody
+ -p
+ 0.0.0.0:443
+ --ssh
+ localhost:22
+ --ssl
+ localhost:443
+
+ QueueDirectories
+
+ RunAtLoad
+
+ StandardErrorPath
+ /Library/Logs/sslh.log
+ StandardOutPath
+ /Library/Logs/sslh.log
+ WatchPaths
+
+
----END FILE----
diff --git a/common.c b/common.c
index c57e06f..58ac7df 100755
--- a/common.c
+++ b/common.c
@@ -34,42 +34,33 @@
int is_ssh_protocol(const char *p, int len);
int is_openvpn_protocol(const char *p, int len);
int is_tinc_protocol(const char *p, int len);
+int is_xmpp_protocol(const char *p, int len);
int is_true(const char *p, int len) { return 1; }
+/* Table of all the protocols we know how to connect to.
+ *
+ * The first protocol in the table is where we connect in case of timeout
+ * (client didn't speak: typically this is SSH.)
+ *
+ * The last protocol in the table is where we connect if client spoke but we
+ * couldn't probe what it's saying.
+ */
struct proto protocols[] = {
/* affected description service saddr probe */
{ 0, "ssh", "sshd", {0}, is_ssh_protocol },
{ 0, "openvpn", NULL, {0}, is_openvpn_protocol },
{ 0, "tinc", NULL, {0}, is_tinc_protocol },
+ { 0, "xmpp", NULL, {0}, is_xmpp_protocol },
/* probe for SSL always successes: it's the default, and must be tried last
**/
{ 0, "ssl", NULL, {0}, is_true }
};
-
-
-const char* USAGE_STRING =
-"sslh " VERSION "\n" \
-"usage:\n" \
-"\tsslh [-v] [-i] [-V] [-f]\n"
-"\t[-t ] [-P ] -u -p [-p ...] \n" \
-"\t[--ssh ] [--ssl ] [--openvpn ] [--tinc ]\n\n" \
-"-v: verbose\n" \
-"-V: version\n" \
-"-f: foreground\n" \
-"-p: address and port to listen on. default: 0.0.0.0:443.\n Can be used several times to bind to several addresses.\n" \
-"--ssh: SSH address: where to connect an SSH connection.\n" \
-"--ssl: SSL address: where to connect an SSL connection.\n" \
-"--openvpn: OpenVPN address: where to connect an OpenVPN connection.\n" \
-"--tinc: tinc address: where to connect a tinc connection.\n" \
-"-P: PID file. Default: /var/run/sslh.pid.\n" \
-"-i: Run as a inetd service.\n" \
-"";
-
+int num_known_protocols = ARRAY_SIZE(protocols);
/*
- * Settings that depend on the command line.
- * They're set in main(), but also used in other places, and it'd be
- * heavy-handed to pass it all as parameters
+ * Settings that depend on the command line. They're set in main(), but also
+ * used in other places in common.c, and it'd be heavy-handed to pass it all as
+ * parameters
*/
int verbose = 0;
int probing_timeout = 2;
@@ -78,8 +69,7 @@ int foreground = 0;
int numeric = 0;
char *user_name, *pid_file;
-struct sockaddr_storage *addr_listen = NULL; /* what addresses do we listen to? */
-int num_addr_listen = 0; /* How many addresses do we listen to? */
+struct addrinfo *addr_listen = NULL; /* what addresses do we listen to? */
#ifdef LIBWRAP
#include
@@ -88,13 +78,13 @@ int allow_severity =0, deny_severity = 0;
/* check result and die, printing the offending address and error */
-void check_res_dumpdie(int res, struct sockaddr_storage *sock, char* syscall)
+void check_res_dumpdie(int res, struct addrinfo *addr, char* syscall)
{
- char buf[64];
+ char buf[NI_MAXHOST];
if (res == -1) {
fprintf(stderr, "%s:%s: %s\n",
- sprintaddr(buf, sizeof(buf), sock),
+ sprintaddr(buf, sizeof(buf), addr),
syscall,
strerror(errno));
exit(1);
@@ -103,31 +93,77 @@ void check_res_dumpdie(int res, struct sockaddr_storage *sock, char* syscall)
/* Starts listening sockets on specified addresses.
* IN: addr[], num_addr
- * OUT: sockfd[]
- * Bound file descriptors are returned in alread-allocated *sockfd pointer
- Returns file descriptor
+ * OUT: *sockfd[] pointer to newly-allocated array of file descriptors
+ * Returns number of addresses bound
+ * Bound file descriptors are returned in newly-allocated *sockfd pointer
*/
-void start_listen_sockets(int sockfd[], struct sockaddr_storage addr[], int num_addr)
+int start_listen_sockets(int *sockfd[], struct addrinfo *addr_list)
{
struct sockaddr_storage *saddr;
+ struct addrinfo *addr;
int i, res, reuse;
+ int num_addr = 0;
- for (i = 0; i < num_addr; i++) {
- saddr = &addr[i];
+ for (addr = addr_list; addr; addr = addr->ai_next)
+ num_addr++;
- sockfd[i] = socket(saddr->ss_family, SOCK_STREAM, 0);
- check_res_dumpdie(sockfd[i], saddr, "socket");
+ if (verbose)
+ fprintf(stderr, "listening to %d addresses\n", num_addr);
+
+ *sockfd = malloc(num_addr * sizeof(*sockfd[0]));
+
+ for (i = 0, addr = addr_list; i < num_addr && addr; i++, addr = addr->ai_next) {
+ if (!addr) {
+ fprintf(stderr, "FATAL: Inconsistent listen number. This should not happen.\n");
+ exit(1);
+ }
+ saddr = (struct sockaddr_storage*)addr->ai_addr;
+
+ (*sockfd)[i] = socket(saddr->ss_family, SOCK_STREAM, 0);
+ check_res_dumpdie((*sockfd)[i], addr, "socket");
reuse = 1;
- res = setsockopt(sockfd[i], SOL_SOCKET, SO_REUSEADDR, (char*)&reuse, sizeof(reuse));
- check_res_dumpdie(res, saddr, "setsockopt");
+ res = setsockopt((*sockfd)[i], SOL_SOCKET, SO_REUSEADDR, (char*)&reuse, sizeof(reuse));
+ check_res_dumpdie(res, addr, "setsockopt");
- res = bind (sockfd[i], (struct sockaddr*)saddr, sizeof(*saddr));
- check_res_dumpdie(res, saddr, "bind");
+ res = bind((*sockfd)[i], addr->ai_addr, addr->ai_addrlen);
+ check_res_dumpdie(res, addr, "bind");
+
+ res = listen ((*sockfd)[i], 50);
+ check_res_dumpdie(res, addr, "listen");
- res = listen (sockfd[i], 50);
- check_res_dumpdie(res, saddr, "listen");
}
+
+ return num_addr;
+}
+
+/* Connect to first address that works and returns a file descriptor, or -1 if
+ * none work. cnx_name points to the name of the service (for logging) */
+int connect_addr(struct addrinfo *addr, char* cnx_name)
+{
+ struct addrinfo *a;
+ char buf[NI_MAXHOST];
+ int fd, res;
+
+ for (a = addr; a; a = a->ai_next) {
+ if (verbose)
+ fprintf(stderr, "connecting to %s family %d len %d\n",
+ sprintaddr(buf, sizeof(buf), a),
+ a->ai_addr->sa_family, a->ai_addrlen);
+ fd = socket(a->ai_family, SOCK_STREAM, 0);
+ if (fd == -1) {
+ log_message(LOG_ERR, "forward to %s failed:socket: %s\n", cnx_name, strerror(errno));
+ } else {
+ res = connect(fd, a->ai_addr, a->ai_addrlen);
+ if (res == -1) {
+ log_message(LOG_ERR, "forward to %s failed:connect: %s\n",
+ cnx_name, strerror(errno));
+ } else {
+ return fd;
+ }
+ }
+ }
+ return -1;
}
/* Store some data to write to the queue later */
@@ -290,9 +326,19 @@ int is_openvpn_protocol (const char*p,int len)
* */
int is_tinc_protocol( const char *p, int len)
{
- return !strncmp(p, "0 ", len);
+ return !strncmp(p, "0 ", 2);
}
+/* Is the buffer the beginning of a jabber (XMPP) connections?
+ * (Protocol is documented (http://tools.ietf.org/html/rfc6120) but for lazy
+ * clients, just checking first frame containing "jabber" in xml entity)
+ * */
+int is_xmpp_protocol( const char *p, int len)
+{
+ return strstr(p, "jabber") ? 1 : 0;
+}
+
+
/*
* Read the beginning of data coming from the client connection and check if
* it's a known protocol. Then leave the data on the defered
@@ -335,23 +381,33 @@ T_PROTO_ID probe_client_protocol(struct connection *cnx)
}
/* returns a string that prints the IP and port of the sockaddr */
-char* sprintaddr(char* buf, size_t size, struct sockaddr_storage* s)
+char* sprintaddr(char* buf, size_t size, struct addrinfo *a)
{
char host[NI_MAXHOST], serv[NI_MAXSERV];
+ int res;
+
+ res = getnameinfo(a->ai_addr, a->ai_addrlen,
+ host, sizeof(host),
+ serv, sizeof(serv),
+ numeric ? NI_NUMERICHOST | NI_NUMERICSERV : 0 );
+
+ if (res) {
+ fprintf(stderr, "sprintaddr:getnameinfo: %s\n", gai_strerror(res));
+ exit(1);
+ }
- getnameinfo((struct sockaddr*)s, sizeof(*s), host, sizeof(host), serv, sizeof(serv), numeric ? NI_NUMERICHOST | NI_NUMERICSERV : 0 );
snprintf(buf, size, "%s:%s", host, serv);
return buf;
}
-/* turns a "hostname:port" string into a struct sockaddr;
-sock: socket address to which to copy the addr
+/* turns a "hostname:port" string into a list of struct addrinfo;
+out: list of newly allocated addrinfo (see getaddrinfo(3)); freeaddrinfo(3) when done
fullname: input string -- it gets clobbered
*/
-void resolve_name(struct sockaddr_storage *sock, char* fullname)
+void resolve_name(struct addrinfo **out, char* fullname)
{
- struct addrinfo *addr, hint;
+ struct addrinfo hint;
char *serv, *host;
int res;
@@ -371,17 +427,13 @@ void resolve_name(struct sockaddr_storage *sock, char* fullname)
hint.ai_family = PF_UNSPEC;
hint.ai_socktype = SOCK_STREAM;
- res = getaddrinfo(host, serv, &hint, &addr);
+ res = getaddrinfo(host, serv, &hint, out);
if (res) {
fprintf(stderr, "%s `%s'\n", gai_strerror(res), fullname);
if (res == EAI_SERVICE)
fprintf(stderr, "(Check you have specified all ports)\n");
- exit(1);
+ exit(4);
}
-
- memcpy(sock, addr->ai_addr, sizeof(*sock));
-
- freeaddrinfo(addr);
}
/* Log to syslog, and to stderr if foreground */
@@ -402,41 +454,40 @@ void log_message(int type, char* msg, ...)
/* syslogs who connected to where */
void log_connection(struct connection *cnx)
{
- struct sockaddr_storage peeraddr; /* Who's connecting to sshd */
- struct sockaddr_storage listenaddr; /* Where is it connecting to */
- struct sockaddr_storage forwardfromaddr; /* Where is it forwarded from */
- struct sockaddr_storage targetaddr; /* Where is it forwarded to */
- socklen_t size = sizeof(peeraddr);
+ struct addrinfo addr;
+ struct sockaddr_storage ss;
#define MAX_NAMELENGTH (NI_MAXHOST + NI_MAXSERV + 1)
- char buf[MAX_NAMELENGTH], buf2[MAX_NAMELENGTH], buf3[MAX_NAMELENGTH], buf4[MAX_NAMELENGTH];
+ char peer[MAX_NAMELENGTH], service[MAX_NAMELENGTH],
+ local[MAX_NAMELENGTH], target[MAX_NAMELENGTH];
int res;
- memset(&peeraddr, 0, sizeof(peeraddr));
- memset(&listenaddr, 0, sizeof(listenaddr));
- memset(&forwardfromaddr, 0, sizeof(forwardfromaddr));
- memset(&targetaddr, 0, sizeof(targetaddr));
+ addr.ai_addr = (struct sockaddr*)&ss;
+ addr.ai_addrlen = sizeof(ss);
- res = getpeername(cnx->q[0].fd, (struct sockaddr*)&peeraddr, &size);
+ res = getpeername(cnx->q[0].fd, addr.ai_addr, &addr.ai_addrlen);
if (res == -1) return; /* that should never happen, right? */
+ sprintaddr(peer, sizeof(peer), &addr);
- size = sizeof(listenaddr);
- res = getsockname(cnx->q[0].fd, (struct sockaddr*)&listenaddr, &size);
+ addr.ai_addrlen = sizeof(ss);
+ res = getsockname(cnx->q[0].fd, addr.ai_addr, &addr.ai_addrlen);
if (res == -1) return;
+ sprintaddr(service, sizeof(service), &addr);
- size = sizeof(targetaddr);
- res = getpeername(cnx->q[1].fd, (struct sockaddr*)&targetaddr, &size);
+ addr.ai_addrlen = sizeof(ss);
+ res = getpeername(cnx->q[1].fd, addr.ai_addr, &addr.ai_addrlen);
if (res == -1) return;
+ sprintaddr(target, sizeof(target), &addr);
- size = sizeof(forwardfromaddr);
- res = getsockname(cnx->q[1].fd, (struct sockaddr*)&forwardfromaddr, &size);
+ addr.ai_addrlen = sizeof(ss);
+ res = getsockname(cnx->q[1].fd, addr.ai_addr, &addr.ai_addrlen);
if (res == -1) return;
+ sprintaddr(local, sizeof(local), &addr);
- log_message(LOG_INFO, "connection from %s to %s forwarded from %s to %s\n",
- sprintaddr(buf, sizeof(buf), &peeraddr),
- sprintaddr(buf2, sizeof(buf2), &listenaddr),
- sprintaddr(buf3, sizeof(buf3), &forwardfromaddr),
- sprintaddr(buf4, sizeof(buf4), &targetaddr));
-
+ log_message(LOG_INFO, "connection from %s to %s forwarded from %s to %s\n",
+ peer,
+ service,
+ local,
+ target);
}
@@ -498,6 +549,14 @@ void setup_signals(void)
action.sa_flags = SA_NOCLDWAIT;
res = sigaction(SIGCHLD, &action, NULL);
CHECK_RES_DIE(res, "sigaction");
+
+
+ /* Set SIGTERM to exit. For some reason if it's not set explicitely,
+ * coverage information is lost when killing the process */
+ memset(&action, 0, sizeof(action));
+ action.sa_handler = exit;
+ res = sigaction(SIGTERM, &action, NULL);
+ CHECK_RES_DIE(res, "sigaction");
}
/* Open syslog connection with appropriate banner;
@@ -521,7 +580,7 @@ void drop_privileges(char* user_name)
struct passwd *pw = getpwnam(user_name);
if (!pw) {
fprintf(stderr, "%s: not found\n", user_name);
- exit(1);
+ exit(2);
}
if (verbose)
fprintf(stderr, "turning into %s\n", user_name);
@@ -540,7 +599,7 @@ void write_pid_file(char* pidfile)
f = fopen(pidfile, "w");
if (!f) {
perror(pidfile);
- exit(1);
+ exit(3);
}
fprintf(f, "%d\n", getpid());
@@ -549,20 +608,23 @@ void write_pid_file(char* pidfile)
void printsettings(void)
{
- char buf[64];
+ char buf[NI_MAXHOST];
+ struct addrinfo *a;
int i;
for (i = 0; i < ARRAY_SIZE(protocols); i++) {
if (protocols[i].affected)
fprintf(stderr,
- "%s addr: %s. libwrap service: %s\n",
+ "%s addr: %s. libwrap service: %s family %d %d\n",
protocols[i].description,
sprintaddr(buf, sizeof(buf), &protocols[i].saddr),
- protocols[i].service);
+ protocols[i].service,
+ protocols[i].saddr.ai_family,
+ protocols[i].saddr.ai_addr->sa_family);
}
fprintf(stderr, "listening on:\n");
- for (i = 0; i < num_addr_listen; i++) {
- fprintf(stderr, "\t%s\n", sprintaddr(buf, sizeof(buf), &addr_listen[i]));
+ for (a = addr_listen; a; a = a->ai_next) {
+ fprintf(stderr, "\t%s\n", sprintaddr(buf, sizeof(buf), a));
}
fprintf(stderr, "timeout to ssh: %d\n", probing_timeout);
}
@@ -574,7 +636,6 @@ void printsettings(void)
* prot: array of protocols
* n_prots: number of protocols in *prot
* */
-#define PROT_SHIFT 1000 /* protocol options will be 1000, 1001, etc */
void append_protocols(struct option *options, int n_opts, struct proto *prot, int n_prots)
{
int o, p;
@@ -587,126 +648,3 @@ void append_protocols(struct option *options, int n_opts, struct proto *prot, in
}
}
-void parse_cmdline(int argc, char* argv[])
-{
- int c, affected = 0;
- struct option const_options[] = {
- { "inetd", no_argument, &inetd, 1 },
- { "foreground", no_argument, &foreground, 1 },
- { "verbose", no_argument, &verbose, 1 },
- { "numeric", no_argument, &numeric, 1 },
- { "user", required_argument, 0, 'u' },
- { "pidfile", required_argument, 0, 'P' },
- { "timeout", required_argument, 0, 't' },
- { "listen", required_argument, 0, 'p' },
- };
- struct option all_options[ARRAY_SIZE(const_options) + ARRAY_SIZE(protocols) + 1];
-
- memset(all_options, 0, sizeof(all_options));
- memcpy(all_options, const_options, sizeof(const_options));
- append_protocols(all_options, ARRAY_SIZE(const_options), protocols, ARRAY_SIZE(protocols));
-
- while ((c = getopt_long_only(argc, argv, "t:l:s:o:T:p:VP:", all_options, NULL)) != -1) {
- if (c == 0) continue;
-
- if (c >= PROT_SHIFT) {
- affected++;
- protocols[c - PROT_SHIFT].affected = 1;
- resolve_name(&protocols[c - PROT_SHIFT].saddr, optarg);
- continue;
- }
-
- switch (c) {
-
- case 't':
- probing_timeout = atoi(optarg);
- break;
-
- case 'p':
- num_addr_listen++;
- addr_listen = realloc(addr_listen, num_addr_listen * sizeof(addr_listen[0]));
- resolve_name(&addr_listen[num_addr_listen - 1], optarg);
- break;
-
- case 'V':
- printf("%s %s\n", server_type, VERSION);
- exit(0);
-
- case 'u':
- user_name = optarg;
- break;
-
- case 'P':
- pid_file = optarg;
- break;
-
- default:
- fprintf(stderr, USAGE_STRING);
- exit(2);
- }
- }
-
- if (!affected) {
- fprintf(stderr, "At least one target protocol must be specified.\n");
- exit(2);
- }
-
- if (!num_addr_listen) {
- fprintf(stderr, "No listening address specified; use at least one -p option\n");
- exit(1);
- }
-
-}
-
-int main(int argc, char *argv[])
-{
-
- extern char *optarg;
- extern int optind;
- int res;
-
- int *listen_sockets;
-
- /* Init defaults */
- pid_file = "/var/run/sslh.pid";
- user_name = "nobody";
- foreground = 0;
-
- parse_cmdline(argc, argv);
-
- if (inetd)
- {
- verbose = 0;
- start_shoveler(0);
- exit(0);
- }
-
- if (verbose)
- printsettings();
-
- listen_sockets = malloc(num_addr_listen * sizeof(*listen_sockets));
- start_listen_sockets(listen_sockets, addr_listen, num_addr_listen);
- free(addr_listen);
-
- if (!foreground)
- if (fork() > 0) exit(0); /* Detach */
-
- setup_signals();
-
- write_pid_file(pid_file);
-
- drop_privileges(user_name);
-
- /* New session -- become group leader */
- if (getuid() == 0) {
- res = setsid();
- CHECK_RES_DIE(res, "setsid: already process leader");
- }
-
- /* Open syslog connection */
- setup_syslog(argv[0]);
-
- main_loop(listen_sockets, num_addr_listen);
-
- return 0;
-}
diff --git a/common.h b/common.h
index 70b9812..27dd3d3 100755
--- a/common.h
+++ b/common.h
@@ -16,6 +16,7 @@
#include
#include
#include
+#include
#ifndef VERSION
#define VERSION "v?"
@@ -46,27 +47,23 @@ enum connection_state {
ST_SHOVELING /* Connexion is established */
};
-
-/* Different types of protocols we support.
- * These must match the order of the protocols[] array in common.c */
-typedef enum protocol_type {
- PROT_SSH,
- PROT_OPENVPN,
- PROT_TINC,
- PROT_SSL,
-} T_PROTO_ID;
+typedef int T_PROTO_ID; /* Index into protocols[] array */
/* For each protocol we need: */
struct proto {
int affected; /* are we actually using it? */
char* description; /* a string that says what it is (for logging and command-line parsing) */
char* service; /* service name to do libwrap checks */
- struct sockaddr_storage saddr; /* where to switch that protocol */
+ struct addrinfo saddr; /* where to switch that protocol */
int (*probe)(const char*, int); /* function to probe that protocol */
};
/* A table in common.c contains all the known protocols */
extern struct proto protocols[];
+extern int num_known_protocols;
+
+/* this is used to pass protocols through the command-line parameter parsing */
+#define PROT_SHIFT 1000 /* protocol options will be 1000, 1001, etc */
/* A 'queue' is composed of a file descriptor (which can be read from or
* written to), and a queue for defered write data */
@@ -94,10 +91,10 @@ struct connection {
/* common.c */
void init_cnx(struct connection *cnx);
-void start_listen_sockets(int sockfd[], struct sockaddr_storage addr[], int num_addr);
+int connect_addr(struct addrinfo *addr, char* cnx_name);
int fd2fd(struct queue *target, struct queue *from);
-char* sprintaddr(char* buf, size_t size, struct sockaddr_storage* s);
-void resolve_name(struct sockaddr_storage *sock, char* fullname) ;
+char* sprintaddr(char* buf, size_t size, struct addrinfo *a);
+void resolve_name(struct addrinfo **out, char* fullname);
T_PROTO_ID probe_client_protocol(struct connection *cnx);
void log_connection(struct connection *cnx);
int check_access_rights(int in_socket, char* service);
@@ -110,12 +107,15 @@ void parse_cmdline(int argc, char* argv[]);
void log_message(int type, char* msg, ...);
void dump_connection(struct connection *cnx);
+void append_protocols(struct option *options, int n_opts, struct proto *prot, int n_prots);
+int start_listen_sockets(int *sockfd[], struct addrinfo *addr_list);
int defer_write(struct queue *q, void* data, int data_size);
int flush_defered(struct queue *q);
-extern int probing_timeout, verbose, inetd;
-extern struct sockaddr_storage *addr_listen, addr_ssl, addr_ssh, addr_openvpn;
+extern int probing_timeout, verbose, inetd, foreground, numeric;
+extern struct sockaddr_storage addr_ssl, addr_ssh, addr_openvpn;
+extern struct addrinfo *addr_listen;
extern const char* USAGE_STRING;
extern char* user_name, *pid_file;
extern const char* server_type;
@@ -124,5 +124,3 @@ extern const char* server_type;
void start_shoveler(int);
void main_loop(int *listen_sockets, int num_addr_listen);
-
-
diff --git a/echosrv.c b/echosrv.c
new file mode 100755
index 0000000..17969a0
--- /dev/null
+++ b/echosrv.c
@@ -0,0 +1,166 @@
+/* echosrv: a simple line echo server with optional prefix adding.
+ *
+ * echsrv --listen localhost6:1234 --prefix "ssl: "
+ *
+ * This will bind to 1234, and echo every line pre-pending "ssl: ". This is
+ * used for testing: we create several such servers with different prefixes,
+ * then we connect test clients that can then check they get the proper data
+ * back (thus testing that shoveling works both ways) with the correct prefix
+ * (thus testing it connected to the expected service).
+ * **/
+
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "common.h"
+
+/* Added to make the code compilable under CYGWIN
+ * */
+#ifndef SA_NOCLDWAIT
+#define SA_NOCLDWAIT 0
+#endif
+
+const char* USAGE_STRING =
+"echosrv\n" \
+"usage:\n" \
+"\techosrv [-v] --listen [--prefix ]\n"
+"-v: verbose\n" \
+"--listen: address to listen on. Can be specified multiple times.\n" \
+"--prefix: add specified prefix before every line echoed.\n"
+"";
+
+const char* server_type = "echsrv"; /* keep setup_syslog happy */
+
+/*
+ * Settings that depend on the command line.
+ */
+char* prefix = "";
+int port;
+
+void parse_cmdline(int argc, char* argv[])
+{
+ int c;
+ struct option options[] = {
+ { "verbose", no_argument, &verbose, 1 },
+ { "numeric", no_argument, &numeric, 1 },
+ { "listen", required_argument, 0, 'l' },
+ { "prefix", required_argument, 0, 'p' },
+ };
+ struct addrinfo **a;
+
+ while ((c = getopt_long_only(argc, argv, "l:p:", options, NULL)) != -1) {
+ if (c == 0) continue;
+
+ switch (c) {
+
+ case 'l':
+ /* find the end of the listen list */
+ for (a = &addr_listen; *a; a = &((*a)->ai_next));
+ /* append the specified addresses */
+ resolve_name(a, optarg);
+ break;
+
+ case 'p':
+ prefix = optarg;
+ break;
+
+ default:
+ fprintf(stderr, "%s", USAGE_STRING);
+ exit(2);
+ }
+ }
+
+ if (!addr_listen) {
+ fprintf(stderr, "No listening port specified\n");
+ exit(1);
+ }
+}
+
+void start_echo(int fd)
+{
+ fd_set fds;
+ int res;
+ char buffer[1 << 20];
+ char* ret;
+ FILE *socket;
+
+ FD_ZERO(&fds);
+
+ socket = fdopen(fd, "r+");
+ if (!socket) {
+ CHECK_RES_DIE(-1, "fdopen");
+ }
+
+ while (1) {
+ ret = fgets(buffer, sizeof(buffer), socket);
+ if (!ret) {
+ fprintf(stderr, "%s", strerror(ferror(socket)));
+ return;
+ }
+ res = fprintf(socket, "%s%s", prefix, buffer);
+ if (res < 0) {
+ fprintf(stderr, "%s", strerror(ferror(socket)));
+ return;
+ }
+
+ fflush(socket);
+ }
+}
+
+void main_loop(int listen_sockets[], int num_addr_listen)
+{
+ int in_socket, i;
+
+ for (i = 0; i < num_addr_listen; i++) {
+ if (!fork()) {
+ while (1)
+ {
+ in_socket = accept(listen_sockets[i], 0, 0);
+ if (verbose) fprintf(stderr, "accepted fd %d\n", in_socket);
+
+ if (!fork())
+ {
+ close(listen_sockets[i]);
+ start_echo(in_socket);
+ exit(0);
+ }
+ close(in_socket);
+ }
+ }
+ }
+ wait(NULL);
+}
+
+int main(int argc, char *argv[])
+{
+
+ extern char *optarg;
+ extern int optind;
+ int num_addr_listen;
+
+ int *listen_sockets;
+
+ parse_cmdline(argc, argv);
+
+ num_addr_listen = start_listen_sockets(&listen_sockets, addr_listen);
+
+ main_loop(listen_sockets, num_addr_listen);
+
+ return 0;
+}
diff --git a/sslh-fork.c b/sslh-fork.c
index ee9f3dd..d46743f 100644
--- a/sslh-fork.c
+++ b/sslh-fork.c
@@ -1,7 +1,7 @@
/*
Reimplementation of sslh in C
-# Copyright (C) 2007-2008 Yves Rutschle
+# Copyright (C) 2007-2011 Yves Rutschle
#
# This program is free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public
@@ -68,10 +68,9 @@ void start_shoveler(int in_socket)
{
fd_set fds;
struct timeval tv;
- struct sockaddr_storage *saddr;
+ struct addrinfo *saddr;
int res;
int out_socket;
- char *target;
struct connection cnx;
T_PROTO_ID prot;
@@ -92,22 +91,18 @@ void start_shoveler(int in_socket)
prot = probe_client_protocol(&cnx);
} else {
/* Timed out: it's necessarily SSH */
- prot = PROT_SSH;
+ prot = 0;
}
saddr = &protocols[prot].saddr;
- target = protocols[prot].description;
if (protocols[prot].service &&
check_access_rights(in_socket, protocols[prot].service)) {
exit(0);
}
/* Connect the target socket */
- out_socket = socket(saddr->ss_family, SOCK_STREAM, 0);
- res = connect(out_socket, (struct sockaddr*)saddr, sizeof(addr_ssl));
- CHECK_RES_DIE(res, "connect");
- if (verbose)
- fprintf(stderr, "connected to something\n");
+ out_socket = connect_addr(saddr, protocols[prot].description);
+ CHECK_RES_DIE(out_socket, "connect");
cnx.q[1].fd = out_socket;
@@ -126,12 +121,31 @@ void start_shoveler(int in_socket)
exit(0);
}
-void main_loop(int *listen_sockets, int num_addr_listen)
-{
- int in_socket, i;
+static int *listener_pid;
+static int listener_pid_number = 0;
+void stop_listeners(int sig)
+{
+ int i;
+
+ for (i = 0; i < listener_pid_number; i++) {
+ kill(listener_pid[i], sig);
+ }
+}
+
+void main_loop(int listen_sockets[], int num_addr_listen)
+{
+ int in_socket, i, res;
+ struct sigaction action;
+
+ listener_pid = malloc(listener_pid_number * sizeof(listener_pid[0]));
+
+ /* Start one process for each listening address */
for (i = 0; i < num_addr_listen; i++) {
- if (!fork()) {
+ if (!(listener_pid[i] = fork())) {
+
+ /* Listening process just accepts a connection, forks, and goes
+ * back to listening */
while (1)
{
in_socket = accept(listen_sockets[i], 0, 0);
@@ -147,6 +161,17 @@ void main_loop(int *listen_sockets, int num_addr_listen)
}
}
}
+
+ /* Set SIGTERM to "stop_listeners" which further kills all listener
+ * processes. Note this won't kill processes that listeners forked, which
+ * means active connections remain active. */
+ memset(&action, 0, sizeof(action));
+ action.sa_handler = stop_listeners;
+ res = sigaction(SIGTERM, &action, NULL);
+ CHECK_RES_DIE(res, "sigaction");
+
+ listener_pid_number = num_addr_listen;
+ wait(NULL);
}
/* The actual main is in common.c: it's the same for both version of
diff --git a/sslh-main.c b/sslh-main.c
new file mode 100644
index 0000000..c8febc2
--- /dev/null
+++ b/sslh-main.c
@@ -0,0 +1,196 @@
+/*
+# main: processing of command line options and start the main loop.
+#
+# Copyright (C) 2007-2011 Yves Rutschle
+#
+# 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.
+#
+# The full text for the General Public License is here:
+# http://www.gnu.org/licenses/gpl.html
+
+*/
+
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "common.h"
+
+const char* USAGE_STRING =
+"sslh " VERSION "\n" \
+"usage:\n" \
+"\tsslh [-v] [-i] [-V] [-f] [-n]\n"
+"\t[-t ] [-P ] -u -p [-p ...] \n" \
+"%s\n\n" \
+"-v: verbose\n" \
+"-V: version\n" \
+"-f: foreground\n" \
+"-n: numeric output\n" \
+"-t: timeout before connecting to SSH.\n" \
+"-p: address and port to listen on.\n Can be used several times to bind to several addresses.\n" \
+"--[ssh,ssl,...]: where to connect connections from corresponding protocol.\n" \
+"-P: PID file. Default: /var/run/sslh.pid.\n" \
+"-i: Run as a inetd service.\n" \
+"";
+
+void print_usage(void)
+{
+ int i;
+ char *prots = "";
+
+ for (i = 0; i < num_known_protocols; i++)
+ asprintf(&prots, "%s\t[--%s ]\n", prots, protocols[i].description);
+
+ fprintf(stderr, USAGE_STRING, prots);
+}
+
+void parse_cmdline(int argc, char* argv[])
+{
+ int c, affected = 0;
+ struct option const_options[] = {
+ { "inetd", no_argument, &inetd, 1 },
+ { "foreground", no_argument, &foreground, 1 },
+ { "verbose", no_argument, &verbose, 1 },
+ { "numeric", no_argument, &numeric, 1 },
+ { "user", required_argument, 0, 'u' },
+ { "pidfile", required_argument, 0, 'P' },
+ { "timeout", required_argument, 0, 't' },
+ { "listen", required_argument, 0, 'p' },
+ };
+ struct option all_options[ARRAY_SIZE(const_options) + num_known_protocols + 1];
+ struct addrinfo *addr, **a;
+
+ memset(all_options, 0, sizeof(all_options));
+ memcpy(all_options, const_options, sizeof(const_options));
+ append_protocols(all_options, ARRAY_SIZE(const_options), protocols, num_known_protocols);
+
+ while ((c = getopt_long_only(argc, argv, "t:T:p:VP:", all_options, NULL)) != -1) {
+ if (c == 0) continue;
+
+ if (c >= PROT_SHIFT) {
+ affected++;
+ protocols[c - PROT_SHIFT].affected = 1;
+ resolve_name(&addr, optarg);
+ protocols[c - PROT_SHIFT].saddr= *addr;
+ continue;
+ }
+
+ switch (c) {
+
+ case 't':
+ probing_timeout = atoi(optarg);
+ break;
+
+ case 'p':
+ /* find the end of the listen list */
+ for (a = &addr_listen; *a; a = &((*a)->ai_next));
+ /* append the specified addresses */
+ resolve_name(a, optarg);
+
+ break;
+
+ case 'V':
+ printf("%s %s\n", server_type, VERSION);
+ exit(0);
+
+ case 'u':
+ user_name = optarg;
+ break;
+
+ case 'P':
+ pid_file = optarg;
+ break;
+
+ default:
+ print_usage();
+ exit(2);
+ }
+ }
+
+ if (!affected) {
+ fprintf(stderr, "At least one target protocol must be specified.\n");
+ exit(2);
+ }
+
+ if (!addr_listen) {
+ fprintf(stderr, "No listening address specified; use at least one -p option\n");
+ exit(1);
+ }
+
+}
+
+int main(int argc, char *argv[])
+{
+
+ extern char *optarg;
+ extern int optind;
+ int res, num_addr_listen;
+
+ int *listen_sockets;
+
+ /* Init defaults */
+ pid_file = "/var/run/sslh.pid";
+ user_name = "nobody";
+ foreground = 0;
+
+ parse_cmdline(argc, argv);
+
+ if (inetd)
+ {
+ verbose = 0;
+ start_shoveler(0);
+ exit(0);
+ }
+
+ if (verbose)
+ printsettings();
+
+ num_addr_listen = start_listen_sockets(&listen_sockets, addr_listen);
+
+ if (!foreground)
+ if (fork() > 0) exit(0); /* Detach */
+
+ setup_signals();
+
+ drop_privileges(user_name);
+
+ /* New session -- become group leader */
+ if (getuid() == 0) {
+ res = setsid();
+ CHECK_RES_DIE(res, "setsid: already process leader");
+ }
+
+ write_pid_file(pid_file);
+
+ /* Open syslog connection */
+ setup_syslog(argv[0]);
+
+ main_loop(listen_sockets, num_addr_listen);
+
+ return 0;
+}
diff --git a/sslh-select.c b/sslh-select.c
index 63f97bd..36d9f70 100644
--- a/sslh-select.c
+++ b/sslh-select.c
@@ -30,7 +30,8 @@ const char* server_type = "sslh-select";
* and then every time we get too many simultaneous connections: e.g. start
* with 100 slots, then if we get more than 100 connections allocate another
* 100 slots, and so on). We never free up connection structures. We try to
- * allocate as many structures at once as will fit in one page.
+ * allocate as many structures at once as will fit in one page (which is 102
+ * in sslh 1.9 on Linux on x86)
*/
static long cnx_num_alloc;
@@ -55,14 +56,16 @@ int tidy_connection(struct connection *cnx, fd_set *fds, fd_set *fds2)
int i;
for (i = 0; i < 2; i++) {
- if (verbose)
- fprintf(stderr, "closing fd %d\n", cnx->q[i].fd);
+ if (cnx->q[i].fd != -1) {
+ if (verbose)
+ fprintf(stderr, "closing fd %d\n", cnx->q[i].fd);
- close(cnx->q[i].fd);
- FD_CLR(cnx->q[i].fd, fds);
- FD_CLR(cnx->q[i].fd, fds2);
- if (cnx->q[i].defered_data)
- free(cnx->q[i].defered_data);
+ close(cnx->q[i].fd);
+ FD_CLR(cnx->q[i].fd, fds);
+ FD_CLR(cnx->q[i].fd, fds2);
+ if (cnx->q[i].defered_data)
+ free(cnx->q[i].defered_data);
+ }
}
init_cnx(cnx);
return 0;
@@ -110,22 +113,17 @@ int accept_new_connection(int listen_socket, struct connection *cnx[], int* cnx_
return in_socket;
}
+
/* Connect queue 1 of connection to SSL; returns new file descriptor */
-int connect_queue(struct connection *cnx, struct sockaddr_storage *addr,
+int connect_queue(struct connection *cnx, struct addrinfo *addr,
char* cnx_name,
fd_set *fds_r, fd_set *fds_w)
{
struct queue *q = &cnx->q[1];
- int res;
- q->fd = socket(addr->ss_family, SOCK_STREAM, 0);
- res = connect(q->fd, (struct sockaddr*)addr, sizeof(*addr));
- log_connection(cnx);
- if (res == -1) {
- tidy_connection(cnx, fds_r, fds_w);
- log_message(LOG_ERR, "forward to %s failed\n", cnx_name);
- return -1;
- } else {
+ q->fd = connect_addr(addr, cnx_name);
+ if (q->fd != -1) {
+ log_connection(cnx);
set_nonblock(q->fd);
flush_defered(q);
if (q->defered_data) {
@@ -134,6 +132,9 @@ int connect_queue(struct connection *cnx, struct sockaddr_storage *addr,
FD_SET(q->fd, fds_r);
}
return q->fd;
+ } else {
+ tidy_connection(cnx, fds_r, fds_w);
+ return -1;
}
}
@@ -188,7 +189,7 @@ int is_fd_active(int fd, fd_set* set)
* That way, each pair of file descriptor (read from one, write to the other)
* is monitored either for read or for write, but never for both.
*/
-void main_loop(int *listen_sockets, int num_addr_listen)
+void main_loop(int listen_sockets[], int num_addr_listen)
{
fd_set fds_r, fds_w; /* reference fd sets (used to init the next 2) */
fd_set readfds, writefds; /* working read and write fd sets */
@@ -292,7 +293,7 @@ void main_loop(int *listen_sockets, int num_addr_listen)
/* If timed out it's SSH, otherwise the client sent
* data so probe the protocol */
if ((cnx[i].probe_timeout < time(NULL))) {
- prot = PROT_SSH;
+ prot = 0;
} else {
prot = probe_client_protocol(&cnx[i]);
}
diff --git a/sslh.pod b/sslh.pod
index e9ab29e..d2fadde 100644
--- a/sslh.pod
+++ b/sslh.pod
@@ -6,15 +6,16 @@
=head1 SYNOPSIS
-sslh [ B<-t> I ] [B<-p> I [B<-p> I ...] [B<-l> I] [B<-s> I] [B<-o> I] [B<-u> I] [B<-P> I] [-v] [-i] [-V] [-f] [-n]
+sslh [ B<-t> I ] [B<-p> I [B<-p> I ...] [B<--ssl> I] [B<--ssh> I] [B<--openvpn> I] [B<-u> I] [B<-P> I] [-v] [-i] [-V] [-f] [-n]
=head1 DESCRIPTION
-B accepts HTTPS, SSH and OpenVPN connections on the
-same port. This makes it possible to connect to an SSH
-server or an OpenVPN on port 443 (e.g. from inside a
-corporate firewall, which almost never block port 443) while
-still serving HTTPS on that port.
+B accepts HTTPS, SSH, OpenVPN, tinc and XMPP connections
+on the same port. This makes it possible to connect to any
+of these servers on port 443 (e.g. from inside a corporate
+firewall, which almost never block port 443) while still
+serving HTTPS on that port.
+
The idea is to have B listen to the external 443 port,
accept the incoming connections, work out what type of
@@ -27,7 +28,9 @@ The protocol detection is made based on the first bytes sent
by the client: SSH connections start by identifying each
other's versions using clear text "SSH-2.0" strings (or
equivalent version strings). This is defined in RFC4253,
-4.2. Meanwhile, OpenVPN clients start with 0x00 0x0D 0x38.
+4.2. Meanwhile, OpenVPN clients start with 0x00 0x0D 0x38,
+tinc clients start with "0 ", and XMPP client start with a
+packet containing "jabber".
Additionally, two kind of SSH clients exist: the client
waits for the server to send its version string ("Shy"
@@ -90,6 +93,11 @@ typically I.
Interface and port on which to forward OpenVPN connections,
typically I.
+=item B<--xmpp> I
+
+Interface and port on which to forward XMPP connections,
+typically I.
+
=item B<--tinc> I
Interface and port on which to forward tinc connections,
diff --git a/t b/t
index 2c2d164..6a95276 100755
--- a/t
+++ b/t
@@ -2,25 +2,17 @@
# Test script for sslh
-# The principle is to create two listening sockets which
-# will act as the ssh and ssl servers, and then perform a
-# number of connections in various combinations to check
-# that the server behaves properly.
-
use strict;
-use IO::Socket::INET;
+use IO::Socket::INET6;
use Test::More qw/no_plan/;
# We use ports 9000, 9001 and 9002 -- hope that won't clash
# with anything...
-my $ssh_port = 9000;
-my $ssl_port = 9001;
+my $ssh_address = "ip6-localhost:9000";
+my $ssl_address = "ip6-localhost:9001";
my $sslh_port = 9002;
-my $pidfile = "/tmp/sslh.pid";
-
-# How many connections will be created during the last test
-my $NUM_SSL_CNX = 20;
-my $NUM_SSH_CNX = 20;
+my $no_listen = 9003; # Port on which no-one listens
+my $pidfile = "/tmp/sslh_test.pid";
# Which tests do we run
my $SSL_CNX = 1;
@@ -29,201 +21,282 @@ my $SSH_BOLD_CNX = 1;
my $SSL_MIX_SSH = 1;
my $SSH_MIX_SSL = 1;
my $BIG_MSG = 1;
-my $MANY_CNX = 1;
+my $STALL_CNX = 1;
-# the Listen parameter needs to be bigger than the max number of connexions
-# we'll make during the last test (we open a bunch of SSH connexions, and
-# accept them all at once afterwards)
-my $ssh_listen = new IO::Socket::INET(LocalHost=> "localhost:$ssh_port", Blocking => 1, Reuse => 1, Listen => $NUM_SSH_CNX + 1);
-die "error1: $!\n" unless $ssh_listen;
+# Robustness tests. These are mostly to achieve full test
+# coverage, but do not necessarily result in an actual test
+# (e.g. some tests need to be run with valgrind to check all
+# memory management code).
+my $RB_CNX_NOSERVER = 1;
+my $RB_PARAM_NOHOST = 1;
+my $RB_WRONG_USERNAME = 1;
+my $RB_OPEN_PID_FILE = 1;
+my $RB_BIND_ADDRESS = 1;
+my $RB_RESOLVE_ADDRESS = 1;
-my $ssl_listen = new IO::Socket::INET(LocalHost=> "localhost:$ssl_port", Blocking => 1, Reuse => 1, Listen => $NUM_SSL_CNX + 1);
-die "error2: $!\n" unless $ssl_listen;
+`lcov --directory . --zerocounters`;
+
+
+my ($ssh_pid, $ssl_pid);
+
+if (!($ssh_pid = fork)) {
+ exec "./echosrv --listen $ssh_address --prefix 'ssh: '";
+}
+
+if (!($ssl_pid = fork)) {
+ exec "./echosrv --listen $ssl_address --prefix 'ssl: '";
+}
+
+my @binaries = ('sslh-select', 'sslh-fork');
+for my $binary (@binaries) {
+ warn "Testing $binary\n";
# Start sslh with the right plumbing
-my $sslh_pid;
-if (!($sslh_pid = fork)) {
- my $user = (getpwuid $<)[0]; # Run under current username
- exec "./sslh-fork -v -u $user -p localhost:$sslh_port -s localhost:$ssh_port -l localhost:$ssl_port -P $pidfile";
- #exec "./sslh-select -v -f -u $user -p localhost:$sslh_port -s localhost:$ssh_port -l localhost:$ssl_port -P $pidfile";
- #exec "valgrind --leak-check=full ./sslh-select -v -f -u $user -p localhost:$sslh_port -s localhost:$ssh_port -l localhost:$ssl_port -P $pidfile";
- exit 0;
-}
-warn "spawned $sslh_pid\n";
-sleep 1;
+ my $sslh_pid;
+ if (!($sslh_pid = fork)) {
+ my $user = (getpwuid $<)[0]; # Run under current username
+ exec "./$binary -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address --ssl $ssl_address -P $pidfile";
+ #exec "valgrind --leak-check=full ./sslh-select -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address -ssl $ssl_address -P $pidfile";
+ exit 0;
+ }
+ warn "spawned $sslh_pid\n";
+ sleep 1; # valgrind can be heavy -- wait 5 seconds
-my $test_data = "hello world\n";
+ my $test_data = "hello world\n";
# Test: SSL connection
-if ($SSL_CNX) {
- print "***Test: SSL connection\n";
- my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
- warn "$!\n" unless $cnx_l;
- if (defined $cnx_l) {
- print $cnx_l $test_data;
- my $ssl_data = $ssl_listen->accept;
- my $data = <$ssl_data>;
- is($data, $test_data, "SSL connection");
- }
-}
-
-# Test: Shy SSH connection
-if ($SSH_SHY_CNX) {
- print "***Test: Shy SSH connection\n";
- my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
- warn "$!\n" unless $cnx_h;
- if (defined $cnx_h) {
- sleep 3;
- my $ssh_data = $ssh_listen->accept;
- print $cnx_h $test_data;
- my $data = <$ssh_data>;
- is($data, $test_data, "Shy SSH connection");
- }
-}
-
-# Test: Bold SSH connection
-if ($SSH_BOLD_CNX) {
- print "***Test: Bold SSH connection\n";
- my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
- warn "$!\n" unless $cnx_h;
- if (defined $cnx_h) {
- my $td = "SSH-2.0 testsuite\n$test_data";
- print $cnx_h $td;
- my $ssh_data = $ssh_listen->accept;
- my $data = <$ssh_data>;
- $data .= <$ssh_data>;
- is($data, $td, "Bold SSH connection");
- }
-}
-
-# Test: One SSL half-started then one SSH
-if ($SSL_MIX_SSH) {
- print "***Test: One SSL half-started then one SSH\n";
- my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
- warn "$!\n" unless $cnx_l;
- if (defined $cnx_l) {
- print $cnx_l $test_data;
- my $cnx_h= new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
- warn "$!\n" unless $cnx_h;
- if (defined $cnx_h) {
- sleep 3;
- my $ssh_data = $ssh_listen->accept;
- print $cnx_h $test_data;
- my $data_h = <$ssh_data>;
- is($data_h, $test_data, "SSH during SSL being established");
- }
- my $ssl_data = $ssl_listen->accept;
- my $data = <$ssl_data>;
- is($data, $test_data, "SSL connection interrupted by SSH");
- }
-}
-
-# Test: One SSH half-started then one SSL
-if ($SSH_MIX_SSL) {
- print "***Test: One SSH half-started then one SSL\n";
- my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
- warn "$!\n" unless $cnx_h;
- if (defined $cnx_h) {
- sleep 3;
+ if ($SSL_CNX) {
+ print "***Test: SSL connection\n";
my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx_l;
if (defined $cnx_l) {
print $cnx_l $test_data;
- my $ssl_data = $ssl_listen->accept;
- my $data = <$ssl_data>;
- is($data, $test_data, "SSL during SSH being established");
+ my $data = <$cnx_l>;
+ is($data, "ssl: $test_data", "SSL connection");
}
- my $ssh_data = $ssh_listen->accept;
- print $cnx_h $test_data;
- my $data = <$ssh_data>;
- is($data, $test_data, "SSH connection interrupted by SSL");
}
-}
-
-# Test: Big messages
-if ($BIG_MSG) {
- print "***Test: big message\n";
- my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
- warn "$!\n" unless $cnx_l;
- my $test_data2 = "helloworld";
- my $rept = 10000;
- if (defined $cnx_l) {
- print $cnx_l ($test_data2 x $rept);
- print $cnx_l "\n";
- my $ssl_data = $ssl_listen->accept;
- my $data = <$ssl_data>;
- is($data, $test_data2 x $rept . "\n", "Big message");
- }
-}
-
-# Test: several connections active at once
-# We start 50 SSH connexions, then open 50 SSL connexion, then accept the 50
-# SSH connexions, then we randomize the order of connexions and write 1000
-# messages on each connexion and check we get it on the other end.
-if ($MANY_CNX) {
- print "***Test: several connexions active at once\n";
- my (@cnx_h, @ssh_data);
- for (1..$NUM_SSH_CNX) {
+# Test: Shy SSH connection
+ if ($SSH_SHY_CNX) {
+ print "***Test: Shy SSH connection\n";
my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
- warn "----> $!\n" unless defined $cnx_h;
+ warn "$!\n" unless $cnx_h;
if (defined $cnx_h) {
- push @cnx_h, $cnx_h;
+ sleep 3;
+ print $cnx_h $test_data;
+ my $data = <$cnx_h>;
+ is($data, "ssh: $test_data", "Shy SSH connection");
}
}
- my (@cnx_l, @ssl_data);
- for (1..$NUM_SSL_CNX) {
+
+# Test: Bold SSH connection
+ if ($SSH_BOLD_CNX) {
+ print "***Test: Bold SSH connection\n";
+ my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
+ warn "$!\n" unless $cnx_h;
+ if (defined $cnx_h) {
+ my $td = "SSH-2.0 testsuite\t$test_data";
+ print $cnx_h $td;
+ my $data = <$cnx_h>;
+ is($data, "ssh: $td", "Bold SSH connection");
+ }
+ }
+
+# Test: One SSL half-started then one SSH
+ if ($SSL_MIX_SSH) {
+ print "***Test: One SSL half-started then one SSH\n";
my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
- warn "----> $!\n" unless defined $cnx_l;
+ warn "$!\n" unless $cnx_l;
if (defined $cnx_l) {
- push @cnx_l, $cnx_l;
- print $cnx_l " ";
- push @ssl_data, ($ssl_listen->accept)[0];
- }
- }
- # give time to the connections to turn to SSH
- sleep 4;
- # and accept all SSH connections...
- for (1..$NUM_SSH_CNX) {
- push @ssh_data, $ssh_listen->accept;
- }
-
-# Make up a random order so we don't always hit the
-# connexions in the same order
-
-# fisher_yates_shuffle( \@array ) : generate a random permutation
-# of @array in place (from
-# http://docstore.mik.ua/orelly/perl/cookbook/ch04_18.htm,
-# modified to shuffle two arrays in the same way)
- sub fisher_yates_shuffle {
- my ($array1, $array2) = @_;
- my $i;
- for ($i = @$array1; --$i; ) {
- my $j = int rand ($i+1);
- next if $i == $j;
- @$array1[$i,$j] = @$array1[$j,$i];
- @$array2[$i,$j] = @$array2[$j,$i];
+ print $cnx_l $test_data;
+ my $cnx_h= new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
+ warn "$!\n" unless $cnx_h;
+ if (defined $cnx_h) {
+ sleep 3;
+ print $cnx_h $test_data;
+ my $data_h = <$cnx_h>;
+ is($data_h, "ssh: $test_data", "SSH during SSL being established");
+ }
+ my $data = <$cnx_l>;
+ is($data, "ssl: $test_data", "SSL connection interrupted by SSH");
}
}
- my @cnx = (@cnx_l, @cnx_l);
- my @rcv = (@ssl_data, @ssl_data);
-
- fisher_yates_shuffle(\@rcv, \@cnx);
-
-# Send a bunch of messages
- for my $cnt (1..1000) {
- foreach (@cnx) {
- print $_ "$cnt$test_data";
- }
- foreach (@rcv) {
- my $data = <$_>;
- like($data, qr/ ?$cnt$test_data/, "Multiple messages [$cnt]");
+# Test: One SSH half-started then one SSL
+ if ($SSH_MIX_SSL) {
+ print "***Test: One SSH half-started then one SSL\n";
+ my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
+ warn "$!\n" unless $cnx_h;
+ if (defined $cnx_h) {
+ sleep 3;
+ my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
+ warn "$!\n" unless $cnx_l;
+ if (defined $cnx_l) {
+ print $cnx_l $test_data;
+ my $data = <$cnx_l>;
+ is($data, "ssl: $test_data", "SSL during SSH being established");
+ }
+ print $cnx_h $test_data;
+ my $data = <$cnx_h>;
+ is($data, "ssh: $test_data", "SSH connection interrupted by SSL");
}
}
+
+
+# Test: Big messages (careful: don't go over echosrv's buffer limit (1M))
+ if ($BIG_MSG) {
+ print "***Test: big message\n";
+ my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
+ warn "$!\n" unless $cnx_l;
+ my $test_data2 = "helloworld";
+ my $rept = 10000;
+ if (defined $cnx_l) {
+ print $cnx_l ($test_data2 x $rept);
+ print $cnx_l "\n";
+ my $data = <$cnx_l>;
+ is($data, "ssl: ". ($test_data2 x $rept) . "\n", "Big message");
+ }
+ }
+
+# Test: Stalled connection
+# Create two connections, stall one, check the other one
+# works, unstall first and check it works fine
+ if ($STALL_CNX) {
+ print "***Test: Stalled connection\n";
+ my $cnx_1 = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
+ warn "$!\n" unless defined $cnx_1;
+ my $cnx_2 = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
+ warn "$!\n" unless defined $cnx_2;
+ my $test_data2 = "helloworld";
+ my $rept = 10000;
+ if (defined $cnx_1 and defined $cnx_2) {
+ print $cnx_1 ($test_data2 x $rept);
+ print $cnx_1 "\n";
+ print $cnx_2 ($test_data2 x $rept);
+ print $cnx_2 "\n";
+ my $data = <$cnx_2>;
+ is($data, "ssl: " . ($test_data2 x $rept) . "\n", "Stalled connection (1)");
+ print $cnx_2 ($test_data2 x $rept);
+ print $cnx_2 "\n";
+ $data = <$cnx_2>;
+ is($data, "ssl: " . ($test_data2 x $rept) . "\n", "Stalled connection (2)");
+ $data = <$cnx_1>;
+ is($data, "ssl: " . ($test_data2 x $rept) . "\n", "Stalled connection (3)");
+
+ }
+ }
+
+ my $pid = `cat $pidfile`;
+ warn "killing $pid\n";
+ kill TERM => $pid or warn "kill process: $!\n";
+ sleep 1;
+}
+
+# Robustness: Connecting to non-existant server
+if ($RB_CNX_NOSERVER) {
+ print "***Test: Connecting to non-existant server\n";
+ my $sslh_pid;
+ if (!($sslh_pid = fork)) {
+ my $user = (getpwuid $<)[0]; # Run under current username
+ exec "./sslh-select -v -f -u $user --listen localhost:$sslh_port --ssh localhost:$no_listen --ssl localhost:$no_listen -P $pidfile";
+ }
+ warn "spawned $sslh_pid\n";
+
+ sleep 1;
+
+ my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
+ warn "$!\n" unless $cnx_h;
+ if (defined $cnx_h) {
+ sleep 1;
+ my $test_data = "hello";
+ print $cnx_h $test_data;
+ }
+ # Ideally we should check a log is emitted.
+
+ kill TERM => `cat $pidfile` or warn "kill: $!\n";
+ sleep 1;
}
+# Robustness: No hostname in address
+if ($RB_PARAM_NOHOST) {
+ print "***Test: No hostname in address\n";
+ my $sslh_pid;
+ if (!($sslh_pid = fork)) {
+ my $user = (getpwuid $<)[0]; # Run under current username
+ exec "./sslh-select -v -f -u $user --listen $sslh_port --ssh $ssh_address --ssl $ssl_address -P $pidfile";
+ }
+ warn "spawned $sslh_pid\n";
+ waitpid $sslh_pid, 0;
+ my $code = $? >> 8;
+ warn "exited with $code\n";
+ is($code, 1, "Exit status on illegal option");
+}
+
+# Robustness: User does not exist
+if ($RB_WRONG_USERNAME) {
+ print "***Test: Changing to non-existant username\n";
+ my $sslh_pid;
+ if (!($sslh_pid = fork)) {
+ my $user = (getpwuid $<)[0]; # Run under current username
+ exec "./sslh-select -v -f -u ${user}_doesnt_exist --listen localhost:$sslh_port --ssh $ssh_address --ssl $ssl_address -P $pidfile";
+ }
+ warn "spawned $sslh_pid\n";
+ waitpid $sslh_pid, 0;
+ my $code = $? >> 8;
+ warn "exited with $code\n";
+ is($code, 2, "Exit status on non-existant username");
+}
+
+# Robustness: Can't open PID file
+if ($RB_OPEN_PID_FILE) {
+ print "***Test: Can't open PID file\n";
+ my $sslh_pid;
+ if (!($sslh_pid = fork)) {
+ my $user = (getpwuid $<)[0]; # Run under current username
+ exec "./sslh-select -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address --ssl $ssl_address -P /dont_exist/$pidfile";
+ # You don't have a /dont_exist/ directory, do you?!
+ }
+ warn "spawned $sslh_pid\n";
+ waitpid $sslh_pid, 0;
+ my $code = $? >> 8;
+ warn "exited with $code\n";
+ is($code, 3, "Exit status if can't open PID file");
+}
+
+# Robustness: Can't bind address
+if ($RB_BIND_ADDRESS) {
+ print "***Test: Can't bind address\n";
+ my $sslh_pid;
+ if (!($sslh_pid = fork)) {
+ my $user = (getpwuid $<)[0]; # Run under current username
+ exec "./sslh-select -v -f -u $user --listen 74.125.39.106:9000 --ssh $ssh_address --ssl $ssl_address -P $pidfile";
+ }
+ warn "spawned $sslh_pid\n";
+ waitpid $sslh_pid, 0;
+ my $code = $? >> 8;
+ warn "exited with $code\n";
+ is($code, 1, "Exit status if can't bind address");
+}
+
+# Robustness: Can't resolve address
+if ($RB_RESOLVE_ADDRESS) {
+ print "***Test: Can't resolve address\n";
+ my $sslh_pid;
+ if (!($sslh_pid = fork)) {
+ my $user = (getpwuid $<)[0]; # Run under current username
+ exec "./sslh-select -v -f -u $user --listen blahblah.dontexist:9000 --ssh $ssh_address --ssl $ssl_address -P $pidfile";
+ }
+ warn "spawned $sslh_pid\n";
+ waitpid $sslh_pid, 0;
+ my $code = $? >> 8;
+ warn "exited with $code\n";
+ is($code, 4, "Exit status if can't resolve address");
+}
+
+`lcov --directory . --capture --output-file sslh_cov.info`;
+`genhtml sslh_cov.info`;
+
+`killall echosrv`;
-kill 15, `cat $pidfile` or warn "kill: $!\n";
diff --git a/t_load b/t_load
new file mode 100755
index 0000000..d1bb683
--- /dev/null
+++ b/t_load
@@ -0,0 +1,128 @@
+#! /usr/bin/perl -w
+
+# Test script for sslh -- mass communication
+
+# This creates many clients that perform concurrent
+# connections, disconnect at any time, and try to generally
+# behave as badly as possible.
+
+# It can be used to test sslh behaves properly with many
+# clients, however its main use is to get an idea of how
+# much load it can take on your system before things start
+# to go wrong.
+
+use strict;
+use IO::Socket::INET6;
+use Data::Dumper;
+
+## BEGIN TEST CONFIG
+
+# Do we test sslh-select or sslh-fork?
+my $sslh_binary = "./sslh-select";
+
+# How many clients to we start for each protocol?
+my $NUM_CNX = 30;
+
+# Delay between starting new processes when starting up. If
+# you start 200 processes in under a second, things go wrong
+# and it's not sslh's fault (typically the echosrv won't be
+# forking fast enough).
+my $start_time_delay = 0.5;
+
+# If you test 4 protocols, you'll start $NUM_CNX * 4 clients
+# (e.g. 40), starting one every $start_time_delay seconds.
+
+# Max times we repeat the test string: allows to test for
+# large messages.
+my $block_rpt = 4096;
+
+# Probability to stop a client after a message (e.g. with
+# .01 a client will send an average of 100 messages before
+# disconnecting).
+my $stop_client_probability = .001;
+
+# What protocols we test, and on what ports
+# Just comment out protocols you don't want to use.
+my %protocols = (
+ "ssh" => { address => "localhost:9001", client => client("ssh") },
+ "ssl" => { address => "localhost:9002", client => client("ssl") },
+ "openvpn" => {address => "localhost:9003", client => client("openvpn") },
+ "tinc" => {address => "localhost:9004", client => client("tinc") },
+);
+
+##END CONFIG
+
+
+# We use ports 9000, 9001 and 9002 -- hope that won't clash
+# with anything...
+my $sslh_address = "localhost:9000";
+my $pidfile = "/tmp/sslh_test.pid";
+
+sub client {
+ my ($service) = @_;
+
+ return sub {
+ while (1) {
+ my $cnx = new IO::Socket::INET(PeerHost => $sslh_address);
+ my $test_data = "$service testing " x int(rand($block_rpt)+1) . "\n";
+
+ sleep 5 if $service eq "ssh";
+ if ($service eq "openvpn") {
+ syswrite $cnx, "\x00\x0F\x38\n";
+ my $msg;
+ sysread $cnx, $msg, 14; # length "openvpn: \x0\xF\x38\n" => 14
+ }
+ if ($service eq "tinc") {
+ syswrite $cnx, "0 \n";
+ my $msg;
+ sysread $cnx, $msg, 10; # length "tinc: 0 \n" => 10
+ }
+ while (1) {
+ print $cnx $test_data;
+ my $r = <$cnx>;
+ ($? = 1, die "$service got [$r]\n") if ($r ne "$service: $test_data");
+ last if rand(1) < $stop_client_probability;
+ }
+ }
+ exit 0;
+ }
+}
+
+foreach my $p (keys %protocols) {
+ if (!fork) {
+ exec "./echosrv --listen $protocols{$p}->{address} --prefix '$p: '";
+ }
+}
+
+# Start sslh with the right plumbing
+my $sslh_pid;
+if (!($sslh_pid = fork)) {
+ my $user = (getpwuid $<)[0]; # Run under current username
+ my $prots = join " ", map "--$_ $protocols{$_}->{address}", keys %protocols;
+ my $cmd = "$sslh_binary -f -t 3 -u $user --listen $sslh_address $prots -P $pidfile";
+ print "$cmd\n";
+ exec $cmd;
+ exit 0;
+}
+warn "spawned $sslh_pid\n";
+sleep 2; # valgrind can be heavy -- wait 5 seconds
+
+
+for (1 .. $NUM_CNX) {
+ foreach my $p (keys %protocols) {
+ if (!fork) {
+ warn "starting $p\n";
+ &{$protocols{$p}->{client}};
+ exit;
+ }
+ # Give a little time so we don't overrun the
+ # listen(2) backlog.
+ select undef, undef, undef, $start_time_delay;
+ }
+}
+
+wait;
+
+
+`killall echosrv`;
+