1
0
mirror of https://github.com/moparisthebest/sslh synced 2024-11-23 17:42:22 -05:00

v1.14: 21DEC2012

Corrected OpenVPN probe to support pre-shared secret
	mode (OpenVPN port-sharing code is... wrong). Thanks
	to Kai Ellinger for help in investigating and
	testing.

	Added an actual TLS/SSL probe.

	Added configurable --on-timeout protocol
	specification.

	Added a --anyprot protocol probe (equivalent to what
	--ssl was).

	Makefile respects the user's compiler and CFLAG
	choices (falling back to the current values if
	undefined), as well as LDFLAGS.
	(Michael Palimaka)

	Added "After" and "KillMode" to systemd.sslh.service
	(Thomas Weischuh).

	Added LSB tags to etc.init.d.sslh
	(Thomas Varis).
This commit is contained in:
Yves Rutschle 2013-07-10 23:19:33 +02:00
parent 5cd1fa1875
commit f842e2e081
15 changed files with 355 additions and 158 deletions

View File

@ -1,3 +1,28 @@
v1.14: 21DEC2012
Corrected OpenVPN probe to support pre-shared secret
mode (OpenVPN port-sharing code is... wrong). Thanks
to Kai Ellinger for help in investigating and
testing.
Added an actual TLS/SSL probe.
Added configurable --on-timeout protocol
specification.
Added a --anyprot protocol probe (equivalent to what
--ssl was).
Makefile respects the user's compiler and CFLAG
choices (falling back to the current values if
undefined), as well as LDFLAGS.
(Michael Palimaka)
Added "After" and "KillMode" to systemd.sslh.service
(Thomas Weißschuh).
Added LSB tags to etc.init.d.sslh
(Thomas Varis).
v1.13: 18MAY2012 v1.13: 18MAY2012
Write PID file before dropping privileges. Write PID file before dropping privileges.

View File

@ -1,6 +1,6 @@
# Configuration # Configuration
VERSION="v1.13b" VERSION="1.14"
USELIBCONFIG=1 # Use libconfig? (necessary to use configuration files) USELIBCONFIG=1 # Use libconfig? (necessary to use configuration files)
USELIBWRAP= # Use libwrap? USELIBWRAP= # Use libwrap?
COV_TEST= # Perform test coverage? COV_TEST= # Perform test coverage?
@ -15,10 +15,10 @@ ifneq ($(strip $(COV_TEST)),)
CFLAGS_COV=-fprofile-arcs -ftest-coverage CFLAGS_COV=-fprofile-arcs -ftest-coverage
endif endif
CC = gcc CC ?= gcc
CFLAGS=-Wall -g $(CFLAGS_COV) CFLAGS ?=-Wall -g $(CFLAGS_COV)
LIBS= LIBS=$(LDFLAGS)
OBJS=common.o sslh-main.o probe.o OBJS=common.o sslh-main.o probe.o
ifneq ($(strip $(USELIBWRAP)),) ifneq ($(strip $(USELIBWRAP)),)
@ -48,11 +48,18 @@ sslh-select: $(OBJS) sslh-select.o Makefile common.h
#strip sslh-select #strip sslh-select
echosrv: $(OBJS) echosrv.o echosrv: $(OBJS) echosrv.o
$(CC) $(CFLAGS) -o echosrv echosrv.o common.o $(LIBS) $(CC) $(CFLAGS) -o echosrv echosrv.o probe.o common.o $(LIBS)
$(MAN): sslh.pod Makefile $(MAN): sslh.pod Makefile
pod2man --section=8 --release=$(VERSION) --center=" " sslh.pod | gzip -9 - > $(MAN) pod2man --section=8 --release=$(VERSION) --center=" " sslh.pod | gzip -9 - > $(MAN)
# Create release: export clean tree and tag current
# configuration
release:
svn export . /tmp/sslh-$(VERSION)
( cd /tmp; tar zcvf /tmp/sslh-$(VERSION).tar.gz sslh-$(VERSION) )
( cd .. ; svn copy trunk tags/sslh-$(VERSION) ; cd tags/sslh-$(VERSION) ; make clean )
# generic install: install binary and man page # generic install: install binary and man page
install: sslh $(MAN) install: sslh $(MAN)
install -D sslh-fork $(PREFIX)/sbin/sslh install -D sslh-fork $(PREFIX)/sbin/sslh

30
README
View File

@ -1,12 +1,20 @@
===== sslh -- A ssl/ssh multiplexer. ===== ===== sslh -- A ssl/ssh multiplexer. =====
sslh accepts connections in HTTP, HTTPS, SSH, OpenVPN, Sslh accepts connections on specified ports, and forwards
tinc, XMPP, or any other protocol that can be tested using a them further based on tests performed on the first data
regular expression, on the same port. This makes it possible packet sent by the remote client.
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.
Probes for HTTP, SSL, SSH, OpenVPN, tinc, XMPP are
implemented, and any other protocol that can be tested using
a regular expression, can be recognised. A typical use case
is to allow serving several services on port 443 (e.g. to
connect to ssh from inside a corporate firewall, which
almost never block port 443) while still serving HTTPS on
that port.
Hence sslh acts as a protocol demultiplexer, or a
switchboard. Its name comes from its original function to
serve SSH and HTTPS on the same port.
==== Compile and install ==== ==== Compile and install ====
@ -57,7 +65,7 @@ If you are going to use sslh for a "small" setup (less than
a dozen ssh connections and a low-traffic https server) then a dozen ssh connections and a low-traffic https server) then
sslh-fork is probably more suited for you. If you are going sslh-fork is probably more suited for you. If you are going
to use sslh on a "medium" setup (a few thousand ssh to use sslh on a "medium" setup (a few thousand ssh
connections, and another few thousand sslh connections), connections, and another few thousand ssl connections),
sslh-select will be better. If you have a very large site sslh-select will be better. If you have a very large site
(tens of thousands of connections), you'll need a vapourware (tens of thousands of connections), you'll need a vapourware
version that would use libevent or something like that. version that would use libevent or something like that.
@ -139,17 +147,15 @@ Web browser --------http/ssl------> stunnel ---http---> sslh --> http:80
Configuration goes like this: Configuration goes like this:
On the server side, using stunnel3: On the server side, using stunnel3:
stunnel -f -p mycert.pem -d thelonious:443 -l /usr/local/sbin/sslh -- sslh -i --ssl localhost:80 --ssh localhost:22 stunnel -f -p mycert.pem -d thelonious:443 -l /usr/local/sbin/sslh -- sslh -i --http localhost:80 --ssh localhost:22
stunnel options: -f for foreground/debugging, -p specifies stunnel options: -f for foreground/debugging, -p specifies
the key + certificate, -d specifies which interface and port the key + certificate, -d specifies which interface and port
we're listening to for incoming connexions, -l summons sslh we're listening to for incoming connexions, -l summons sslh
in inetd mode. in inetd mode.
sslh options: -i for inetd mode, --ssl to forward SSL sslh options: -i for inetd mode, --http to forward http
connexions (in fact normal HTTP at that stage) to port 80, connexions to port 80, and SSH connexions to port 22.
and SSH connexions to port 22. This works because sslh
considers that any protocol it doesn't recognise is SSL.
==== IP_TPROXY support ==== ==== IP_TPROXY support ====

28
basic.cfg Normal file
View File

@ -0,0 +1,28 @@
# This is a basic configuration file that should provide
# sensible values for "standard" setup.
verbose: false;
foreground: false;
inetd: false;
numeric: false;
timeout: 2;
user: "nobody";
pidfile: "/var/run/sslh.pid";
# Change hostname with your external address name.
listen:
(
{ host: "thelonious"; port: "443"; }
);
protocols:
(
{ name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; probe: "builtin"; },
{ name: "openvpn"; host: "localhost"; port: "1194"; probe: "builtin"; },
{ name: "xmpp"; host: "localhost"; port: "5222"; probe: "builtin"; },
{ name: "http"; host: "localhost"; port: "80"; probe: "builtin"; },
{ name: "ssl"; host: "localhost"; port: "443"; probe: "builtin"; },
{ name: "anyprot"; host: "localhost"; port: "443"; probe: "builtin"; }
);

View File

@ -26,7 +26,7 @@ int inetd = 0;
int foreground = 0; int foreground = 0;
int background = 0; int background = 0;
int numeric = 0; int numeric = 0;
const char *user_name, *pid_file, *rule_filename; const char *user_name, *pid_file;
struct addrinfo *addr_listen = NULL; /* what addresses do we listen to? */ struct addrinfo *addr_listen = NULL; /* what addresses do we listen to? */
@ -165,7 +165,6 @@ int flush_defered(struct queue *q)
q->defered_data_size -= n; q->defered_data_size -= n;
} }
return n; return n;
} }
@ -193,8 +192,6 @@ void dump_connection(struct connection *cnx)
* returns FD_NODATA if no data was available * returns FD_NODATA if no data was available
* returns FD_STALLED if data was read, could not be written, and has been * returns FD_STALLED if data was read, could not be written, and has been
* stored in temporary buffer. * stored in temporary buffer.
*
* slot for debug only and may go away at some point
*/ */
int fd2fd(struct queue *target_q, struct queue *from_q) int fd2fd(struct queue *target_q, struct queue *from_q)
{ {

View File

@ -104,7 +104,7 @@ extern int probing_timeout, verbose, inetd, foreground, background, numeric;
extern struct sockaddr_storage addr_ssl, addr_ssh, addr_openvpn; extern struct sockaddr_storage addr_ssl, addr_ssh, addr_openvpn;
extern struct addrinfo *addr_listen; extern struct addrinfo *addr_listen;
extern const char* USAGE_STRING; extern const char* USAGE_STRING;
extern const char* user_name, *pid_file, *rule_filename; extern const char* user_name, *pid_file;
extern const char* server_type; extern const char* server_type;
/* sslh-fork.c */ /* sslh-fork.c */

View File

@ -94,32 +94,26 @@ void parse_cmdline(int argc, char* argv[])
void start_echo(int fd) void start_echo(int fd)
{ {
fd_set fds;
int res; int res;
char buffer[1 << 20]; char buffer[1 << 20];
char* ret; int ret, prefix_len;
FILE *socket;
FD_ZERO(&fds); prefix_len = strlen(prefix);
socket = fdopen(fd, "r+"); memset(buffer, 0, sizeof(buffer));
if (!socket) { strcpy(buffer, prefix);
CHECK_RES_DIE(-1, "fdopen");
}
while (1) { while (1) {
ret = fgets(buffer, sizeof(buffer), socket); ret = read(fd, buffer + prefix_len, sizeof(buffer));
if (!ret) { if (ret == -1) {
fprintf(stderr, "%s", strerror(ferror(socket))); fprintf(stderr, "%s", strerror(errno));
return; return;
} }
res = fprintf(socket, "%s%s", prefix, buffer); res = write(fd, buffer, ret + prefix_len);
if (res < 0) { if (res < 0) {
fprintf(stderr, "%s", strerror(ferror(socket))); fprintf(stderr, "%s", strerror(errno));
return; return;
} }
fflush(socket);
} }
} }

View File

@ -1,4 +1,9 @@
verbose: false; # This file is provided as documentation to show what is
# possible. It should not be used as-is, and probably should
# not be used as a starting point for a working
# configuration. Instead use basic.cfg.
verbose: true;
foreground: true; foreground: true;
inetd: false; inetd: false;
numeric: false; numeric: false;
@ -10,8 +15,8 @@ pidfile: "/var/run/sslh.pid";
# List of interfaces on which we should listen # List of interfaces on which we should listen
listen: listen:
( (
{ host: "thelonious"; port: "443"; } { host: "thelonious"; port: "443"; },
# , { host: "thelonious"; port: "8080"; } { host: "thelonious"; port: "8080"; }
); );
# List of protocols # List of protocols
@ -22,10 +27,8 @@ listen:
# host: host name to connect that protocol # host: host name to connect that protocol
# port: port number to connect that protocol # port: port number to connect that protocol
# probe: "builtin" or a list of regular expressions # probe: "builtin" or a list of regular expressions
# (can be left out, e.g. to use with on-timeout)
# #
# In case of timeout sslh will connect to the first
# protocol: this should be SSH.
# SSL should have a "always true" probe, and come last.
# sslh will try each probe in order they are declared, and # sslh will try each probe in order they are declared, and
# connect to the first that matches. # connect to the first that matches.
@ -35,6 +38,15 @@ protocols:
{ name: "openvpn"; host: "localhost"; port: "1194"; probe: [ "^\x00[\x0D-\xFF]$", "^\x00[\x0D-\xFF]\x38" ]; }, { name: "openvpn"; host: "localhost"; port: "1194"; probe: [ "^\x00[\x0D-\xFF]$", "^\x00[\x0D-\xFF]\x38" ]; },
{ name: "xmpp"; host: "localhost"; port: "5222"; probe: [ "jabber" ]; }, { name: "xmpp"; host: "localhost"; port: "5222"; probe: [ "jabber" ]; },
{ name: "http"; host: "localhost"; port: "80"; probe: "builtin"; }, { name: "http"; host: "localhost"; port: "80"; probe: "builtin"; },
{ name: "ssl"; host: "localhost"; port: "443"; probe: [ "" ]; } { name: "ssl"; host: "localhost"; port: "443"; probe: [ "" ]; },
{ name: "timeout"; service: "daytime"; host: "localhost"; port: "daytime"; }
); );
# Optionally, specify to which protocol to connect in case
# of timeout (defaults to "ssh").
# You can timeout to any arbitrary address by setting a
# protocol with no probe, as is the case with this example.
# This enables you to set a tcpd service name for this
# protocol too.
on-timeout: "timeout";

124
probe.c
View File

@ -19,7 +19,10 @@
# http://www.gnu.org/licenses/gpl.html # http://www.gnu.org/licenses/gpl.html
*/ */
#define _GNU_SOURCE
#include <stdio.h>
#include <regex.h> #include <regex.h>
#include <ctype.h>
#include "probe.h" #include "probe.h"
@ -29,6 +32,7 @@ static int is_openvpn_protocol(const char *p, int len, struct proto*);
static int is_tinc_protocol(const char *p, int len, struct proto*); static int is_tinc_protocol(const char *p, int len, struct proto*);
static int is_xmpp_protocol(const char *p, int len, struct proto*); static int is_xmpp_protocol(const char *p, int len, struct proto*);
static int is_http_protocol(const char *p, int len, struct proto*); static int is_http_protocol(const char *p, int len, struct proto*);
static int is_tls_protocol(const char *p, int len, struct proto*);
static int is_true(const char *p, int len, struct proto* proto) { return 1; } static int is_true(const char *p, int len, struct proto* proto) { return 1; }
/* Table of protocols that have a built-in probe /* Table of protocols that have a built-in probe
@ -40,10 +44,13 @@ static struct proto builtins[] = {
{ "tinc", NULL, NULL, is_tinc_protocol }, { "tinc", NULL, NULL, is_tinc_protocol },
{ "xmpp", NULL, NULL, is_xmpp_protocol }, { "xmpp", NULL, NULL, is_xmpp_protocol },
{ "http", NULL, NULL, is_http_protocol }, { "http", NULL, NULL, is_http_protocol },
{ "ssl", NULL, NULL, is_true } { "ssl", NULL, NULL, is_tls_protocol },
{ "tls", NULL, NULL, is_tls_protocol },
{ "anyprot", NULL, NULL, is_true }
}; };
static struct proto *protocols; static struct proto *protocols;
static char* on_timeout = "ssh";
struct proto* get_builtins(void) { struct proto* get_builtins(void) {
return builtins; return builtins;
@ -53,12 +60,21 @@ int get_num_builtins(void) {
return ARRAY_SIZE(builtins); return ARRAY_SIZE(builtins);
} }
/* Returns the protocol to connect to in case of timeout; conventionaly this is /* Sets the protocol name to connect to in case of timeout */
* the first protocol specified (but maybe we'll make it more explicit some void set_ontimeout(const char* name)
* day) {
asprintf(&on_timeout, "%s", name);
}
/* Returns the protocol to connect to in case of timeout;
* if not found, return the first protocol specified
*/ */
struct proto* timeout_protocol(void) { struct proto* timeout_protocol(void)
return protocols; {
struct proto* p = get_first_protocol();
for (; p && strcmp(p->description, on_timeout); p = p->next);
if (p) return p;
return get_first_protocol();
} }
/* returns the first protocol (caller can then follow the *next pointers) */ /* returns the first protocol (caller can then follow the *next pointers) */
@ -72,6 +88,39 @@ void set_protocol_list(struct proto* prots)
protocols = prots; protocols = prots;
} }
/* From http://grapsus.net/blog/post/Hexadecimal-dump-in-C */
#define HEXDUMP_COLS 16
void hexdump(const char *mem, unsigned int len)
{
unsigned int i, j;
for(i = 0; i < len + ((len % HEXDUMP_COLS) ? (HEXDUMP_COLS - len % HEXDUMP_COLS) : 0); i++)
{
/* print offset */
if(i % HEXDUMP_COLS == 0)
printf("0x%06x: ", i);
/* print hex data */
if(i < len)
printf("%02x ", 0xFF & mem[i]);
else /* end of block, just aligning for ASCII dump */
printf(" ");
/* print ASCII dump */
if(i % HEXDUMP_COLS == (HEXDUMP_COLS - 1)) {
for(j = i - (HEXDUMP_COLS - 1); j <= i; j++) {
if(j >= len) /* end of block, not really printing */
putchar(' ');
else if(isprint(mem[j])) /* printable char */
putchar(0xFF & mem[j]);
else /* other char */
putchar('.');
}
putchar('\n');
}
}
}
/* Is the buffer the beginning of an SSH connection? */ /* Is the buffer the beginning of an SSH connection? */
static int is_ssh_protocol(const char *p, int len, struct proto *proto) static int is_ssh_protocol(const char *p, int len, struct proto *proto)
{ {
@ -82,24 +131,20 @@ static int is_ssh_protocol(const char *p, int len, struct proto *proto)
} }
/* Is the buffer the beginning of an OpenVPN connection? /* Is the buffer the beginning of an OpenVPN connection?
* (code lifted from OpenVPN port-share option) *
* Code inspired from OpenVPN port-share option; however, OpenVPN code is
* wrong: users using pre-shared secrets have non-initialised key_id fields so
* p[3] & 7 should not be looked at, and also the key_method can be specified
* to 1 which changes the opcode to P_CONTROL_HARD_RESET_CLIENT_V1.
* See:
* http://www.fengnet.com/book/vpns%20illustrated%20tunnels%20%20vpnsand%20ipsec/ch08lev1sec5.html
* and OpenVPN ssl.c, ssl.h and options.c
*/ */
static int is_openvpn_protocol (const char*p,int len, struct proto *proto) static int is_openvpn_protocol (const char*p,int len, struct proto *proto)
{ {
#define P_OPCODE_SHIFT 3 int packet_len = ntohs(*(uint16_t*)p);
#define P_CONTROL_HARD_RESET_CLIENT_V2 7
if (len >= 3) return packet_len == len - 2;
{
return p[0] == 0
&& p[1] >= 14
&& p[2] == (P_CONTROL_HARD_RESET_CLIENT_V2<<P_OPCODE_SHIFT);
}
else if (len >= 2)
{
return p[0] == 0 && p[1] >= 14;
}
else
return 0;
} }
/* Is the buffer the beginning of a tinc connections? /* Is the buffer the beginning of a tinc connections?
@ -145,6 +190,14 @@ static int is_http_protocol(const char *p, int len, struct proto *proto)
return 0; return 0;
} }
static int is_tls_protocol(const char *p, int len, struct proto *proto)
{
/* TLS packet starts with a record "Hello" (0x16), followed by version
* (0x03 0x00-0x03) (RFC6101 A.1)
* This means we reject SSLv2 and lower, which is actually a good thing (RFC6176)
*/
return p[0] == 0x16 && p[1] == 0x03 && ( p[2] >= 0 && p[2] <= 0x03);
}
static int regex_probe(const char *p, int len, struct proto *proto) static int regex_probe(const char *p, int len, struct proto *proto)
{ {
@ -182,28 +235,47 @@ struct proto* probe_client_protocol(struct connection *cnx)
defer_write(&cnx->q[1], buffer, n); defer_write(&cnx->q[1], buffer, n);
for (p = protocols; p; p = p->next) { for (p = protocols; p; p = p->next) {
if (! p->probe) continue;
if (verbose) fprintf(stderr, "probing for %s\n", p->description);
if (p->probe(buffer, n, p)) { if (p->probe(buffer, n, p)) {
if (verbose) fprintf(stderr, "probe %s successful\n", p->description);
return p; return p;
} }
} }
} }
if (verbose)
fprintf(stderr,
"all probes failed, connecting to first protocol: %s\n",
protocols->description);
/* If none worked, return the first one affected (that's completely /* If none worked, return the first one affected (that's completely
* arbitrary) */ * arbitrary) */
return protocols; return protocols;
} }
/* Returns the probe for specified protocol: /* Returns the structure for specified protocol or NULL if not found */
* parameter is the description in builtins[], or "regex" static struct proto* get_protocol(const char* description)
* */ {
T_PROBE* get_probe(const char* description) {
int i; int i;
for (i = 0; i < ARRAY_SIZE(builtins); i++) { for (i = 0; i < ARRAY_SIZE(builtins); i++) {
if (!strcmp(builtins[i].description, description)) { if (!strcmp(builtins[i].description, description)) {
return builtins[i].probe; return &builtins[i];
} }
} }
return NULL;
}
/* Returns the probe for specified protocol:
* parameter is the description in builtins[], or "regex"
* */
T_PROBE* get_probe(const char* description) {
struct proto* p = get_protocol(description);
if (p)
return p->probe;
/* Special case of "regex" probe (we don't want to set it in builtins /* Special case of "regex" probe (we don't want to set it in builtins
* because builtins is also used to build the command-line options and * because builtins is also used to build the command-line options and
* regexp is not legal on the command line)*/ * regexp is not legal on the command line)*/

View File

@ -45,10 +45,15 @@ void set_protocol_list(struct proto*);
*/ */
struct proto* probe_client_protocol(struct connection *cnx); struct proto* probe_client_protocol(struct connection *cnx);
/* set the protocol to connect to in case of timeout */
void set_ontimeout(const char* name);
/* timeout_protocol /* timeout_protocol
* *
* Returns the protocol to connect to in case of timeout * Returns the protocol to connect to in case of timeout
*/ */
struct proto* timeout_protocol(void); struct proto* timeout_protocol(void);
void hexdump(const char*, unsigned int);
#endif #endif

View File

@ -2,6 +2,8 @@
### BEGIN INIT INFO ### BEGIN INIT INFO
# Provides: sslh # Provides: sslh
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5 # Default-Start: 2 3 4 5
# Default-Stop: 1 # Default-Stop: 1
# Short-Description: sslh proxy ssl & ssh connections # Short-Description: sslh proxy ssl & ssh connections

View File

@ -1,9 +1,11 @@
[Unit] [Unit]
Description=SSL/SSH multiplexer Description=SSL/SSH multiplexer
After=network.target
[Service] [Service]
EnvironmentFile=/etc/conf.d/sslh EnvironmentFile=/etc/conf.d/sslh
ExecStart=/usr/bin/sslh --foreground $DAEMON_OPTS ExecStart=/usr/bin/sslh --foreground $DAEMON_OPTS
KillMode=process
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -36,12 +36,14 @@ const char* USAGE_STRING =
"\tsslh [-v] [-i] [-V] [-f] [-n] [-F <file>]\n" "\tsslh [-v] [-i] [-V] [-f] [-n] [-F <file>]\n"
"\t[-t <timeout>] [-P <pidfile>] -u <username> -p <add> [-p <addr> ...] \n" \ "\t[-t <timeout>] [-P <pidfile>] -u <username> -p <add> [-p <addr> ...] \n" \
"%s\n\n" /* Dynamically built list of builtin protocols */ \ "%s\n\n" /* Dynamically built list of builtin protocols */ \
"\t[--on-timeout <addr>]\n" \
"-v: verbose\n" \ "-v: verbose\n" \
"-V: version\n" \ "-V: version\n" \
"-f: foreground\n" \ "-f: foreground\n" \
"-n: numeric output\n" \ "-n: numeric output\n" \
"-F: use configuration file\n" \ "-F: use configuration file\n" \
"-t: timeout before connecting to SSH.\n" \ "--on-timeout: connect to specified address upon timeout (default: ssh address)\n" \
"-t: seconds to wait before connecting to --on-timeout address.\n" \
"-p: address and port to listen on.\n Can be used several times to bind to several addresses.\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" \ "--[ssh,ssl,...]: where to connect connections from corresponding protocol.\n" \
"-F: specify a configuration file\n" \ "-F: specify a configuration file\n" \
@ -49,6 +51,9 @@ const char* USAGE_STRING =
"-i: Run as a inetd service.\n" \ "-i: Run as a inetd service.\n" \
""; "";
/* Constants for options that have no one-character shorthand */
#define OPT_ONTIMEOUT 257
static struct option const_options[] = { static struct option const_options[] = {
{ "inetd", no_argument, &inetd, 1 }, { "inetd", no_argument, &inetd, 1 },
{ "foreground", no_argument, &foreground, 1 }, { "foreground", no_argument, &foreground, 1 },
@ -59,6 +64,7 @@ static struct option const_options[] = {
{ "config", required_argument, 0, 'F' }, { "config", required_argument, 0, 'F' },
{ "pidfile", required_argument, 0, 'P' }, { "pidfile", required_argument, 0, 'P' },
{ "timeout", required_argument, 0, 't' }, { "timeout", required_argument, 0, 't' },
{ "on-timeout", required_argument, 0, OPT_ONTIMEOUT },
{ "listen", required_argument, 0, 'p' }, { "listen", required_argument, 0, 'p' },
{} {}
}; };
@ -71,10 +77,12 @@ static const char *optstr = "vt:T:p:VP:F:";
static void print_usage(void) static void print_usage(void)
{ {
struct proto *p; struct proto *p;
int i;
char *prots = ""; char *prots = "";
for (p = get_first_protocol(); p; p = p->next) p = get_builtins();
asprintf(&prots, "%s\t[--%s <addr>]\n", prots, p->description); for (i = 0; i < get_num_builtins(); i++)
asprintf(&prots, "%s\t[--%s <addr>]\n", prots, p[i].description);
fprintf(stderr, USAGE_STRING, prots); fprintf(stderr, USAGE_STRING, prots);
} }
@ -98,7 +106,8 @@ static void printsettings(void)
for (a = addr_listen; a; a = a->ai_next) { for (a = addr_listen; a; a = a->ai_next) {
fprintf(stderr, "\t%s\n", sprintaddr(buf, sizeof(buf), a)); fprintf(stderr, "\t%s\n", sprintaddr(buf, sizeof(buf), a));
} }
fprintf(stderr, "timeout to ssh: %d\n", probing_timeout); fprintf(stderr, "timeout: %d\non-timeout: %s\n", probing_timeout,
timeout_protocol()->description);
} }
@ -195,8 +204,8 @@ static int config_protocols(config_t *config, struct proto **prots)
prot = config_setting_get_elem(setting, i); prot = config_setting_get_elem(setting, i);
if ((config_setting_lookup_string(prot, "name", &name) && if ((config_setting_lookup_string(prot, "name", &name) &&
config_setting_lookup_string(prot, "host", &hostname) && config_setting_lookup_string(prot, "host", &hostname) &&
config_setting_lookup_string(prot, "port", &port) config_setting_lookup_string(prot, "port", &port)
)) { )) {
p->description = name; p->description = name;
config_setting_lookup_string(prot, "service", &(p->service)); config_setting_lookup_string(prot, "service", &(p->service));
@ -205,24 +214,26 @@ static int config_protocols(config_t *config, struct proto **prots)
probes = config_setting_get_member(prot, "probe"); probes = config_setting_get_member(prot, "probe");
if (config_setting_is_array(probes)) { if (probes) {
/* If 'probe' is an array, setup a regex probe using the if (config_setting_is_array(probes)) {
* array of strings as pattern */ /* If 'probe' is an array, setup a regex probe using the
* array of strings as pattern */
setup_regex_probe(p, probes); setup_regex_probe(p, probes);
} else { } else {
/* if 'probe' is 'builtin', set the probe to the /* if 'probe' is 'builtin', set the probe to the
* appropriate builtin protocol */ * appropriate builtin protocol */
if (!strcmp(config_setting_get_string(probes), "builtin")) { if (!strcmp(config_setting_get_string(probes), "builtin")) {
p->probe = get_probe(name); p->probe = get_probe(name);
if (!p->probe) { if (!p->probe) {
fprintf(stderr, "%s: no builtin probe for this protocol\n", name); fprintf(stderr, "%s: no builtin probe for this protocol\n", name);
exit(1);
}
} else {
fprintf(stderr, "%s: illegal probe name\n", name);
exit(1); exit(1);
} }
} else {
fprintf(stderr, "%s: illegal probe name\n", name);
exit(1);
} }
} }
} }
@ -243,6 +254,7 @@ static int config_parse(char *filename, struct addrinfo **listen, struct proto *
{ {
config_t config; config_t config;
long int timeout; long int timeout;
const char* str;
config_init(&config); config_init(&config);
if (config_read_file(&config, filename) == CONFIG_FALSE) { if (config_read_file(&config, filename) == CONFIG_FALSE) {
@ -262,6 +274,10 @@ static int config_parse(char *filename, struct addrinfo **listen, struct proto *
probing_timeout = timeout; probing_timeout = timeout;
} }
if (config_lookup_string(&config, "on-timeout", &str)) {
set_ontimeout(str);
}
config_lookup_string(&config, "user", &user_name); config_lookup_string(&config, "user", &user_name);
config_lookup_string(&config, "pidfile", &pid_file); config_lookup_string(&config, "pidfile", &pid_file);
@ -388,6 +404,10 @@ next_arg:
probing_timeout = atoi(optarg); probing_timeout = atoi(optarg);
break; break;
case OPT_ONTIMEOUT:
set_ontimeout(optarg);
break;
case 'p': case 'p':
/* find the end of the listen list */ /* find the end of the listen list */
for (a = &addr_listen; *a; a = &((*a)->ai_next)); for (a = &addr_listen; *a; a = &((*a)->ai_next));

106
sslh.pod
View File

@ -2,59 +2,40 @@
=head1 NAME =head1 NAME
sslh - ssl/ssh multiplexer sslh - protocol demultiplexer
=head1 SYNOPSIS =head1 SYNOPSIS
sslh [B<-F> I<config file>] [ 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<--http> I<target address for HTTP>] [B<-u> I<username>] [B<-P> I<pidfile>] [-v] [-i] [-V] [-f] [-n] sslh [B<-F> I<config file>] [ 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<--http> I<target address for HTTP>] [B<--anyprot> I<default target address>] [B<--on-timeout> I<protocol name>] [B<-u> I<username>] [B<-P> I<pidfile>] [-v] [-i] [-V] [-f] [-n]
=head1 DESCRIPTION =head1 DESCRIPTION
B<sslh> accepts connections in HTTP, HTTPS, SSH, OpenVPN, B<sslh> accepts connections on specified ports, and forwards
tinc, XMPP, or any other protocol that can be tested using a them further based on tests performed on the first data
regular expression, on the same port. This makes it possible packet sent by the remote client.
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.
Probes for HTTP, SSL, SSH, OpenVPN, tinc, XMPP are
implemented, and any other protocol that can be tested using
a regular expression, can be recognised. A typical use case
is to allow serving several services on port 443 (e.g. to
connect to ssh 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, Hence B<sslh> acts as a protocol demultiplexer, or a
accept the incoming connections, work out what type of switchboard. Its name comes from its original function to
connection it is, and then fordward to the appropriate serve SSH and HTTPS on the same port.
server.
=head2 Protocol detection
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,
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"
client, which is the case of OpenSSH and Putty), or the
client sends its version first ("Bold" client, which is the
case of Bitvise Tunnelier and ConnectBot).
If the client stays quiet after the timeout period, B<sslh>
will connect to the first protocol defined (in the
configuration file, or on the command line), so SSH should
be defined first in B<sslh> configuration to accomodate for
shy SSH clients.
=head2 Libwrap support =head2 Libwrap support
One drawback of B<sslh> is that the B<ssh> and B<httpd> One drawback of B<sslh> is that the servers do not see the
servers do not see the original IP address of the client original IP address of the client anymore, as the connection
anymore, as the connection is forwarded through B<sslh>. is forwarded through B<sslh>.
B<sslh> provides enough logging to circumvent that problem.
However it is common to limit access to B<ssh> using For this reason, B<sslh> can be compiled with B<libwrap> to
B<libwrap> or B<tcpd>. For this reason, B<sslh> can be check accesses defined in F</etc/hosts.allow> and
compiled to check SSH accesses against SSH access lists as F</etc/hosts.deny>. Libwrap services can be defined using
defined in F</etc/hosts.allow> and F</etc/hosts.deny>. the configuration file.
=head2 Configuration file =head2 Configuration file
@ -78,6 +59,18 @@ Alternatively, the I<probe> parameter can be set to
"builtin", to use the compiled probes which are much faster "builtin", to use the compiled probes which are much faster
than regular expressions. than regular expressions.
=head2 Probing protocols
When receiving an incoming connection, B<sslh> will read the
first bytes sent be the connecting client. It will then
probe for the protocol in the order specified on the command
line (or the configuration file). Therefore B<--anyprot>
should alway be used last, as it always succeeds and further
protocols will never be tried.
If no data is sent by the client, B<sslh> will eventually
time out and connect to the protocol specified with
B<--on-timeout>, or I<ssh> if none is specified.
=head1 OPTIONS =head1 OPTIONS
@ -85,9 +78,13 @@ than regular expressions.
=item B<-t> I<num>, B<--timeout> I<num> =item B<-t> I<num>, B<--timeout> I<num>
Timeout before forwarding the connection to the first Timeout before forwarding the connection to the timeout
configured protocol (which should usually be SSH). Default protocol (which should usually be SSH). Default is 2s.
is 2s.
=item B<--on-timeout> I<protocol name>
Name of the protocol to connect to after the timeout period
is over. Default is 'ssh'.
=item B<-p> I<listening address>, B<--listen> I<listening address> =item B<-p> I<listening address>, B<--listen> I<listening address>
@ -99,6 +96,7 @@ This can be specified several times to bind B<sslh> to
several addresses. several addresses.
=item B<--ssl> I<target address> =item B<--ssl> I<target address>
=item B<--tls> I<target address>
Interface and port on which to forward SSL connection, Interface and port on which to forward SSL connection,
typically I<localhost:443>. typically I<localhost:443>.
@ -107,6 +105,11 @@ Note that you can set B<sslh> to listen on I<ext_ip:443> and
B<httpd> to listen on I<localhost:443>: this allows clients B<httpd> to listen on I<localhost:443>: this allows clients
inside your network to just connect directly to B<httpd>. inside your network to just connect directly to B<httpd>.
Also, B<sslh> probes for SSLv3 (or TLSv1) handshake and will
reject connections from clients requesting SSLv2. This is
compliant to RFC6176 which prohibits the usage of SSLv2. If
you wish to accept SSLv2, use B<--default> instead.
=item B<--ssh> I<target address> =item B<--ssh> I<target address>
Interface and port on which to forward SSH connections, Interface and port on which to forward SSH connections,
@ -122,6 +125,11 @@ typically I<localhost:1194>.
Interface and port on which to forward XMPP connections, Interface and port on which to forward XMPP connections,
typically I<localhost:5222>. typically I<localhost:5222>.
=item B<--http> I<target address>
Interface and port on which to forward HTTP connections,
typically I<localhost:80>.
=item B<--tinc> I<target address> =item B<--tinc> I<target address>
Interface and port on which to forward tinc connections, Interface and port on which to forward tinc connections,
@ -130,6 +138,14 @@ typically I<localhost:655>.
This is experimental. If you use this feature, please report This is experimental. If you use this feature, please report
the results (even if it works!) the results (even if it works!)
=item B<--anyprot> I<target address>
Interface and port on which to forward if no other protocol
has been found. Because B<sslh> tries protocols in the order
specified on the command line, this should be specified
last. If no default is specified, B<sslh> will forward
unknown protocols to the first protocol specified.
=item B<-v>, B<--verbose> =item B<-v>, B<--verbose>
Increase verboseness. Increase verboseness.

57
t
View File

@ -20,8 +20,8 @@ my $SSH_SHY_CNX = 1;
my $SSH_BOLD_CNX = 1; my $SSH_BOLD_CNX = 1;
my $SSL_MIX_SSH = 1; my $SSL_MIX_SSH = 1;
my $SSH_MIX_SSL = 1; my $SSH_MIX_SSL = 1;
my $BIG_MSG = 1; my $BIG_MSG = 0; # This test is unreliable
my $STALL_CNX = 1; my $STALL_CNX = 0; # This test needs fixing
# Robustness tests. These are mostly to achieve full test # Robustness tests. These are mostly to achieve full test
# coverage, but do not necessarily result in an actual test # coverage, but do not necessarily result in an actual test
@ -57,8 +57,8 @@ for my $binary (@binaries) {
my $user = (getpwuid $<)[0]; # Run under current username my $user = (getpwuid $<)[0]; # Run under current username
my $cmd = "./$binary -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address --ssl $ssl_address -P $pidfile"; my $cmd = "./$binary -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address --ssl $ssl_address -P $pidfile";
warn "$cmd\n"; warn "$cmd\n";
exec $cmd; #exec $cmd;
#exec "valgrind --leak-check=full ./sslh-select -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; exit 0;
} }
warn "spawned $sslh_pid\n"; warn "spawned $sslh_pid\n";
@ -66,6 +66,8 @@ for my $binary (@binaries) {
my $test_data = "hello world\n"; my $test_data = "hello world\n";
# my $ssl_test_data = (pack 'n', ((length $test_data) + 2)) . $test_data;
my $ssl_test_data = "\x16\x03\x03$test_data\n";
# Test: SSL connection # Test: SSL connection
if ($SSL_CNX) { if ($SSL_CNX) {
@ -73,9 +75,10 @@ for my $binary (@binaries) {
my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx_l; warn "$!\n" unless $cnx_l;
if (defined $cnx_l) { if (defined $cnx_l) {
print $cnx_l $test_data; print $cnx_l $ssl_test_data;
my $data = <$cnx_l>; my $data;
is($data, "ssl: $test_data", "SSL connection"); my $n = sysread $cnx_l, $data, 1024;
is($data, "ssl: $ssl_test_data", "SSL connection");
} }
} }
@ -111,7 +114,7 @@ for my $binary (@binaries) {
my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx_l; warn "$!\n" unless $cnx_l;
if (defined $cnx_l) { if (defined $cnx_l) {
print $cnx_l $test_data; print $cnx_l $ssl_test_data;
my $cnx_h= new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); my $cnx_h= new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx_h; warn "$!\n" unless $cnx_h;
if (defined $cnx_h) { if (defined $cnx_h) {
@ -120,8 +123,9 @@ for my $binary (@binaries) {
my $data_h = <$cnx_h>; my $data_h = <$cnx_h>;
is($data_h, "ssh: $test_data", "SSH during SSL being established"); is($data_h, "ssh: $test_data", "SSH during SSL being established");
} }
my $data = <$cnx_l>; my $data;
is($data, "ssl: $test_data", "SSL connection interrupted by SSH"); my $n = sysread $cnx_l, $data, 1024;
is($data, "ssl: $ssl_test_data", "SSL connection interrupted by SSH");
} }
} }
@ -135,9 +139,10 @@ for my $binary (@binaries) {
my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx_l; warn "$!\n" unless $cnx_l;
if (defined $cnx_l) { if (defined $cnx_l) {
print $cnx_l $test_data; print $cnx_l $ssl_test_data;
my $data = <$cnx_l>; my $data;
is($data, "ssl: $test_data", "SSL during SSH being established"); my $n = sysread $cnx_l, $data, 1024;
is($data, "ssl: $ssl_test_data", "SSL during SSH being established");
} }
print $cnx_h $test_data; print $cnx_h $test_data;
my $data = <$cnx_h>; my $data = <$cnx_h>;
@ -151,19 +156,24 @@ for my $binary (@binaries) {
print "***Test: big message\n"; print "***Test: big message\n";
my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx_l; warn "$!\n" unless $cnx_l;
my $test_data2 = "helloworld"; my $rept = 1000;
my $rept = 10000; my $test_data2 = $ssl_test_data . ("helloworld"x$rept);
if (defined $cnx_l) { if (defined $cnx_l) {
print $cnx_l ($test_data2 x $rept); my $n = syswrite $cnx_l, $test_data2;
print $cnx_l "\n"; my ($data);
my $data = <$cnx_l>; $n = sysread $cnx_l, $data, 1 << 20;
is($data, "ssl: ". ($test_data2 x $rept) . "\n", "Big message"); is($data, "ssl: ". $test_data2, "Big message");
} }
} }
# Test: Stalled connection # Test: Stalled connection
# Create two connections, stall one, check the other one # Create two connections, stall one, check the other one
# works, unstall first and check it works fine # works, unstall first and check it works fine
# This test needs fixing.
# Now that echosrv no longer works on "lines" (finishing
# with '\n'), it may cut blocks randomly with prefixes.
# The whole thing needs to be re-thought as it'll only
# work by chance.
if ($STALL_CNX) { if ($STALL_CNX) {
print "***Test: Stalled connection\n"; print "***Test: Stalled connection\n";
my $cnx_1 = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); my $cnx_1 = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
@ -171,20 +181,21 @@ for my $binary (@binaries) {
my $cnx_2 = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); my $cnx_2 = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless defined $cnx_2; warn "$!\n" unless defined $cnx_2;
my $test_data2 = "helloworld"; my $test_data2 = "helloworld";
my $rept = 10000; sleep 4;
my $rept = 1000;
if (defined $cnx_1 and defined $cnx_2) { if (defined $cnx_1 and defined $cnx_2) {
print $cnx_1 ($test_data2 x $rept); print $cnx_1 ($test_data2 x $rept);
print $cnx_1 "\n"; print $cnx_1 "\n";
print $cnx_2 ($test_data2 x $rept); print $cnx_2 ($test_data2 x $rept);
print $cnx_2 "\n"; print $cnx_2 "\n";
my $data = <$cnx_2>; my $data = <$cnx_2>;
is($data, "ssl: " . ($test_data2 x $rept) . "\n", "Stalled connection (1)"); is($data, "ssh: " . ($test_data2 x $rept) . "\n", "Stalled connection (1)");
print $cnx_2 ($test_data2 x $rept); print $cnx_2 ($test_data2 x $rept);
print $cnx_2 "\n"; print $cnx_2 "\n";
$data = <$cnx_2>; $data = <$cnx_2>;
is($data, "ssl: " . ($test_data2 x $rept) . "\n", "Stalled connection (2)"); is($data, "ssh: " . ($test_data2 x $rept) . "\n", "Stalled connection (2)");
$data = <$cnx_1>; $data = <$cnx_1>;
is($data, "ssl: " . ($test_data2 x $rept) . "\n", "Stalled connection (3)"); is($data, "ssh: " . ($test_data2 x $rept) . "\n", "Stalled connection (3)");
} }
} }