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).
This commit is contained in:
Yves Rutschle 2013-07-10 23:14:15 +02:00
parent a9c9941988
commit ae008179f0
13 changed files with 1052 additions and 490 deletions

View File

@ -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!

View File

@ -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]

6
README
View File

@ -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.

View File

@ -13,42 +13,42 @@ with launchctl or simply reboot.
----BEGIN FILE----
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Disabled</key>
<false/>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>net.rutschle.sslh</string>
<key>ProgramArguments</key>
<array>
<string>/opt/local/sbin/sslh</string>
<string>-f</string>
<string>-v</string>
<string>-u</string>
<string>nobody</string>
<string>-p</string>
<string>0.0.0.0:443</string>
<string>-s</string>
<string>localhost:22</string>
<string>-l</string>
<string>localhost:443</string>
</array>
<key>QueueDirectories</key>
<array/>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/Library/Logs/sslh.log</string>
<key>StandardOutPath</key>
<string>/Library/Logs/sslh.log</string>
<key>WatchPaths</key>
<array/>
<key>Disabled</key>
<false/>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>net.rutschle.sslh</string>
<key>ProgramArguments</key>
<array>
<string>/opt/local/sbin/sslh</string>
<string>-f</string>
<string>-v</string>
<string>-u</string>
<string>nobody</string>
<string>-p</string>
<string>0.0.0.0:443</string>
<string>--ssh</string>
<string>localhost:22</string>
<string>--ssl</string>
<string>localhost:443</string>
</array>
<key>QueueDirectories</key>
<array/>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/Library/Logs/sslh.log</string>
<key>StandardOutPath</key>
<string>/Library/Logs/sslh.log</string>
<key>WatchPaths</key>
<array/>
</dict>
</plist>
----END FILE----

354
common.c
View File

@ -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 <timeout>] [-P <pidfile>] -u <username> -p <add> [-p <addr> ...] \n" \
"\t[--ssh <addr>] [--ssl <addr>] [--openvpn <addr>] [--tinc <addr>]\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 <tcpd.h>
@ -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));
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;
}

View File

@ -16,6 +16,7 @@
#include <syslog.h>
#include <libgen.h>
#include <time.h>
#include <getopt.h>
#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);

166
echosrv.c Executable file
View File

@ -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 <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdio.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <pwd.h>
#include <syslog.h>
#include <libgen.h>
#include <getopt.h>
#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 <address:port> [--prefix <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;
}

View File

@ -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

196
sslh-main.c Normal file
View File

@ -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 <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdio.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <pwd.h>
#include <syslog.h>
#include <libgen.h>
#include <getopt.h>
#include "common.h"
const char* USAGE_STRING =
"sslh " VERSION "\n" \
"usage:\n" \
"\tsslh [-v] [-i] [-V] [-f] [-n]\n"
"\t[-t <timeout>] [-P <pidfile>] -u <username> -p <add> [-p <addr> ...] \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 <addr>]\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;
}

View File

@ -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]);
}

View File

@ -6,15 +6,16 @@
=head1 SYNOPSIS
sslh [ B<-t> I<num> ] [B<-p> I<listening address> [B<-p> I<listening address> ...] [B<-l> I<target address for SSL>] [B<-s> I<target address for SSH>] [B<-o> I<target address for OpenVPN>] [B<-u> I<username>] [B<-P> I<pidfile>] [-v] [-i] [-V] [-f] [-n]
sslh [ B<-t> I<num> ] [B<-p> I<listening address> [B<-p> I<listening address> ...] [B<--ssl> I<target address for SSL>] [B<--ssh> I<target address for SSH>] [B<--openvpn> I<target address for OpenVPN>] [B<-u> I<username>] [B<-P> I<pidfile>] [-v] [-i] [-V] [-f] [-n]
=head1 DESCRIPTION
B<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 firewall, which almost never block port 443) while
still serving HTTPS on that port.
B<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.
The idea is to have B<sslh> 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<localhost:22>.
Interface and port on which to forward OpenVPN connections,
typically I<localhost:1194>.
=item B<--xmpp> I<target address>
Interface and port on which to forward XMPP connections,
typically I<localhost:5222>.
=item B<--tinc> I<target address>
Interface and port on which to forward tinc connections,

435
t
View File

@ -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";

128
t_load Executable file
View File

@ -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`;