diff --git a/ChangeLog b/ChangeLog index 23fa368..150384e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,33 @@ +v1.8: + Changed log format to make it possible to link + connections to subsequent logs from other services. + + Updated CentOS init.d script (Andre Krajnik). + + Fixed zombie issue with OpenBSD (The SA_NOCLDWAIT flag is not + propagated to the child process, so we set up signals after + the fork.) (François FRITZ) + + Added -o "OpenVPN" and OpenVPN probing and support. + + Added single-threaded, select(2)-based version. + + Added support for "Bold" SSH clients (clients that speak first) + Thanks to Guillaume Ricaud for spotting a regression + bug. + + Added -f "foreground" option. + + Added test suite. (only tests connexions. No test for libwrap, + setsid, setuid and so on) and corresponding 'make + test' target. + + Added README.MacOSX (thanks Aaron Madlon-Kay) + + Documented use with proxytunnel and corkscrew in + README. + + v1.7: 01FEB2010 Added CentOS init.d script (Andre Krajnik). diff --git a/Makefile b/Makefile index 3ccb2a7..f3dc943 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Configuration -VERSION="v1.7a" -USELIBWRAP=1 # Use libwrap? +VERSION="v1.8" +USELIBWRAP= # Use libwrap? PREFIX=/usr/local MAN=sslh.8.gz # man page name @@ -10,10 +10,11 @@ MAN=sslh.8.gz # man page name # itself CC = gcc -CFLAGS=-Wall +CFLAGS=-Wall -g #LIBS=-lnet LIBS= +OBJS=common.o ifneq ($(strip $(USELIBWRAP)),) LIBS:=$(LIBS) -lwrap @@ -22,16 +23,27 @@ endif all: sslh $(MAN) -sslh: sslh.c Makefile - $(CC) $(CFLAGS) -D'VERSION=$(VERSION)' -o sslh sslh.c $(LIBS) - strip sslh +.c.o: *.h + $(CC) $(CFLAGS) -D'VERSION=$(VERSION)' -c $< + + +sslh: $(OBJS) sslh-fork sslh-select + +sslh-fork: $(OBJS) sslh-fork.o Makefile + $(CC) $(CFLAGS) -D'VERSION=$(VERSION)' -o sslh-fork sslh-fork.o $(OBJS) $(LIBS) + strip sslh-fork + +sslh-select: $(OBJS) sslh-select.o Makefile + $(CC) $(CFLAGS) -D'VERSION=$(VERSION)' -o sslh-select sslh-select.o $(OBJS) $(LIBS) + strip sslh-select + $(MAN): sslh.pod Makefile pod2man --section=8 --release=$(VERSION) --center=" " sslh.pod | gzip -9 - > $(MAN) # generic install: install binary and man page install: sslh $(MAN) - install -D sslh $(PREFIX)/sbin/sslh + install -D sslh-fork $(PREFIX)/sbin/sslh install -D -m 0644 $(MAN) $(PREFIX)/share/man/man8/$(MAN) # "extended" install for Debian: install startup script @@ -46,4 +58,11 @@ uninstall: update-rc.d sslh remove clean: - rm -f sslh $(MAN) + rm -f sslh-fork sslh-select $(MAN) *.o + +tags: + ctags -T *.[ch] + +test: + ./t + diff --git a/README b/README index cd23eb7..180a50b 100644 --- a/README +++ b/README @@ -1,9 +1,10 @@ ===== sslh -- A ssl/ssh multiplexer. ===== -sslh lets one accept both HTTPS and SSH connections on the -same port. It makes it possible to connect to an SSH server -on port 443 (e.g. from inside a corporate firewall) while -still serving HTTPS on that port. +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. ==== Compile and install ==== @@ -11,32 +12,31 @@ If you're lucky, the Makefile will work for you: make install -(see below for configuration hints) +The Makefile produces two different executables: sslh-fork +and sslh-select. +sslh-fork forks a new process for each incoming connection. +It is well-tested and very reliable, but incurs the overhead +of many processes. sslh-select uses only one thread, which +monitors all connections at once. It is more recent and less +tested, but only incurs a 16 byte overhead per connection. +Also, if it stops, you'll lose all connections, which means +you can't upgrade it remotely. -Otherwise: +If you are going to use sslh for a "small" setup (less than +a dozen ssh connections and a low-traffic https server) then +sslh-fork is probably more suited for you. If you are going +to use sslh on a "medium" setup (a few thousand ssh +connections, and another few thousand sslh connections), +sslh-select will be better. If you have a very large site +(tens of thousands of connections), you'll need a vapourware +version that would use libevent or something like that. -Compilation instructions (the binary produced won't contain -the version number, which is stored only in the Makefile) - -Solaris: - cc -o sslh sslh.c -lresolv -lsocket -lnsl - -LynxOS: - gcc -o tcproxy tcproxy.c -lnetinet - -Linux: - cc -o sslh sslh.c -lnet -or: - cc -o sslh sslh.c - -To compile with libwrap support: - cc -o sslh -DLIBWRAP sslh.c -lwrap To install: make -cp sslh /usr/local/sbin +cp sslh-fork /usr/local/sbin/sslh cp scripts/etc.default.sslh /etc/default/sslh For Debian: @@ -79,13 +79,48 @@ client. ==== OpenVPN support ==== -OpenVPN clients reportedly take more than one second between +OpenVPN clients connecting to OpenVPN running with +-port-share reportedly take more than one second between the time the TCP connexion is established and the time they send the first data packet. This results in sslh with default settings timing out and assuming an SSH connexion. To support OpenVPN connexions reliably, it is necessary to increase sslh's timeout to 5 seconds. +Instead of using OpenVPN's port sharing, it is more reliable +to use sslh's -o option to get sslh to do the port sharing. + +==== Using proxytunnel with sslh ==== + +If you are connecting through a proxy that checks that the +outgoing connection really is SSL and rejects SSH, you can +encapsulate all your traffic in SSL using proxytunnel (this +should work with corkscrew as well). On the server side you +receive the traffic with stunnel to decapsulate SSL, then +pipe through sslh to switch HTTP on one side and SSL on the +other. + +In that case, you end up with something like this: + +ssh -> proxytunnel -e --------ssh/ssl------> stunnel ---ssh---> sslh --> sshd + +navigateur --------http/ssl------> stunnel ---http---> sslh --> http:80 + +Configuration goes like this: + +On the server side, using stunnel3: +stunnel -f -p mycert.pem -d thelonious:443 -l /usr/local/sbin/sslh -- sslh -i -l localhost:80 -s localhost:22 + +stunnel options: -f for foreground/debugging, -p specifies +the key + certificate, -d specifies which interface and port +we're listening to for incoming connexions, -l summons sslh +in inetd mode. + +sslh options: -i for inetd mode, -l to forward SSL +connexions (in fact normal HTTP at that stage) to port 80, +and SSH connexions to port 22. This works because sslh +considers that anything that is not SSH is SSL. + ==== IP_TPROXY support ==== There is a netfilter patch that adds an option to the Linux @@ -112,4 +147,12 @@ when/if the feature finds its way into the main kernel and it becomes usuable by non-root processes. -Comments? questions? sslh@rutschle.net +==== Comments? Questions? ==== + +You can subscribe to the sslh mailing list here: +http://rutschle.net/cgi-bin/mailman/listinfo/sslh + +This mailing list should be used for discussion, feature +requests, and will be the prefered channel for +announcements. + diff --git a/README.MacOSX b/README.MacOSX new file mode 100644 index 0000000..7d9d177 --- /dev/null +++ b/README.MacOSX @@ -0,0 +1,54 @@ + +sslh is available for Mac OS X via MacPorts. If you have +MacPorts installed on your system you can install sslh by +executing the following in the Terminal: + +port install sslh + +Also, the following is a helpful launchd configuration that +covers the most common use case of sslh. Save the following +into a text file, e.g. +/Library/LaunchDaemons/net.rutschle.sslh.plist, then load it +with launchctl or simply reboot. + +----BEGIN FILE---- + + + + + Disabled + + KeepAlive + + Label + net.rutschle.sslh + ProgramArguments + + /opt/local/sbin/sslh + -f + -v + -u + nobody + -p + 0.0.0.0:443 + -s + localhost:22 + -l + localhost:443 + + QueueDirectories + + RunAtLoad + + StandardErrorPath + /Library/Logs/sslh.log + StandardOutPath + /Library/Logs/sslh.log + WatchPaths + + + +----END FILE---- + + diff --git a/common.c b/common.c new file mode 100755 index 0000000..5ef1d75 --- /dev/null +++ b/common.c @@ -0,0 +1,614 @@ +/* Code and variables that is common to both fork and select-based + * servers. + * + * No code here should assume whether sockets are blocking or not. + **/ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" + +/* Added to make the code compilable under CYGWIN + * */ +#ifndef SA_NOCLDWAIT +#define SA_NOCLDWAIT 0 +#endif + +int is_ssh_protocol(const char *p, int len); +int is_openvpn_protocol(const char *p, int len); +int is_true(const char *p, int len) { return 1; } + +struct proto protocols[] = { + /* affected description service saddr probe */ + { 0, "SSH", "sshd", {0}, is_ssh_protocol }, + { 0, "OpenVPN", NULL, {0}, is_openvpn_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]" +"[-t ] -u -p [listenaddr:] \n" \ +"\t\t-s [sshhost:]port -l [sslhost:]port [-P pidfile]\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" \ +"-s: SSH address: where to connect an SSH connection. default: localhost:22\n" \ +"-l: SSL address: where to connect an SSL connection.\n" \ +"-o: OpenVPN address: where to connect an OpenVPN connection.\n" \ +"-P: PID file. Default: /var/run/sslh.pid.\n" \ +"-i: Run as a inetd service.\n" \ +""; + + +/* + * 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 + */ +int verbose = 0; +int probing_timeout = 2; +int inetd = 0; +int foreground = 0; +struct sockaddr addr_listen; +char *user_name, *pid_file; + +#ifdef LIBWRAP +#include +int allow_severity =0, deny_severity = 0; +#endif + + + +/* Starts a listening socket on specified address. + Returns file descriptor + */ +int start_listen_socket(struct sockaddr *addr) +{ + struct sockaddr_in *saddr = (struct sockaddr_in*)addr; + int sockfd, res, reuse; + + sockfd = socket(AF_INET, SOCK_STREAM, 0); + CHECK_RES_DIE(sockfd, "socket"); + + reuse = 1; + res = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char*)&reuse, sizeof(reuse)); + CHECK_RES_DIE(res, "setsockopt"); + + res = bind (sockfd, (struct sockaddr*)saddr, sizeof(*saddr)); + CHECK_RES_DIE(res, "bind"); + + res = listen (sockfd, 50); + CHECK_RES_DIE(res, "listen"); + + return sockfd; +} + +/* Store some data to write to the queue later */ +int defer_write(struct queue *q, void* data, int data_size) +{ + if (verbose) + fprintf(stderr, "**** writing defered on fd %d\n", q->fd); + q->defered_data = malloc(data_size); + q->begin_defered_data = q->defered_data; + q->defered_data_size = data_size; + memcpy(q->defered_data, data, data_size); + + return 0; +} + +/* tries to flush some of the data for specified queue + * Upon success, the number of bytes written is returned. + * Upon failure, -1 returned (e.g. connexion closed) + * */ +int flush_defered(struct queue *q) +{ + int n; + + if (verbose) + fprintf(stderr, "flushing defered data to fd %d\n", q->fd); + + n = write(q->fd, q->defered_data, q->defered_data_size); + if (n == -1) + return n; + + if (n == q->defered_data_size) { + /* All has been written -- release the memory */ + free(q->begin_defered_data); + q->begin_defered_data = NULL; + q->defered_data = NULL; + q->defered_data_size = 0; + } else { + /* There is data left */ + q->defered_data += n; + q->defered_data_size -= n; + } + + + return n; +} + + +void init_cnx(struct connection *cnx) +{ + memset(cnx, 0, sizeof(*cnx)); + cnx->q[0].fd = -1; + cnx->q[1].fd = -1; +} + +void dump_connection(struct connection *cnx) +{ + printf("state: %d\n", cnx->state); + printf("fd %d, %d defered\n", cnx->q[0].fd, cnx->q[0].defered_data_size); + printf("fd %d, %d defered\n", cnx->q[1].fd, cnx->q[1].defered_data_size); +} + + +/* + * moves data from one fd to other + * + * retuns number of bytes copied if success + * returns 0 (FD_CNXCLOSED) if incoming socket closed + * returns FD_NODATA if no data was available + * returns FD_STALLED if data was read, could not be written, and has been + * 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) +{ + char buffer[BUFSIZ]; + int target, from, size_r, size_w; + + target = target_q->fd; + from = from_q->fd; + + size_r = read(from, buffer, sizeof(buffer)); + if (size_r == -1) { + switch (errno) { + case EAGAIN: + if (verbose) + fprintf(stderr, "reading 0 from %d\n", from); + return FD_NODATA; + + case ECONNRESET: + case EPIPE: + return FD_CNXCLOSED; + } + } + + CHECK_RES_RETURN(size_r, "read"); + + if (size_r == 0) + return FD_CNXCLOSED; + + size_w = write(target, buffer, size_r); + /* process -1 when we know how to deal with it */ + if ((size_w == -1)) { + switch (errno) { + case EAGAIN: + /* write blocked: Defer data */ + defer_write(target_q, buffer, size_r); + return FD_STALLED; + + case ECONNRESET: + case EPIPE: + /* remove end closed -- drop the connection */ + return FD_CNXCLOSED; + } + } else if (size_w < size_r) { + /* incomplete write -- defer the rest of the data */ + defer_write(target_q, buffer + size_w, size_r - size_w); + return FD_STALLED; + } + + CHECK_RES_RETURN(size_w, "write"); + + return size_w; +} + +/* If the client wrote something first, read it and check if it's a SSH banner. + * Data is left in appropriate defered write buffer. + */ +int is_ssh_protocol(const char *p, int len) +{ + if (!strncmp(p, "SSH-", 4)) { + return 1; + } + return 0; +} + +/* Is the buffer the beginning of an OpenVPN connection? + * (code lifted from OpenVPN port-share option) + */ +int is_openvpn_protocol (const char*p,int len) +{ +#define P_OPCODE_SHIFT 3 +#define P_CONTROL_HARD_RESET_CLIENT_V2 7 + if (len >= 3) + { + return p[0] == 0 + && p[1] >= 14 + && p[2] == (P_CONTROL_HARD_RESET_CLIENT_V2<= 2) + { + return p[0] == 0 && p[1] >= 14; + } + else + return 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 + * write buffer of the connection and returns the protocol index in the + * protocols[] array * + */ +T_PROTO_ID probe_client_protocol(struct connection *cnx) +{ + char buffer[BUFSIZ]; + int n, i; + + n = read(cnx->q[0].fd, buffer, sizeof(buffer)); + /* It's possible that read() returns an error, e.g. if the client + * disconnected between the previous call to select() and now. If that + * happens, we just connect to the default protocol so the caller of this + * function does not have to deal with a specific failure condition (the + * connection will just fail later normally). */ + if (n > 0) { + defer_write(&cnx->q[1], buffer, n); + + for (i = 0; i < ARRAY_SIZE(protocols); i++) { + if (protocols[i].affected) { + if (protocols[i].probe(buffer, n)) { + return i; + } + } + } + } + + /* If none worked, return the last one */ + return ARRAY_SIZE(protocols) - 1; +} + +/* returns a string that prints the IP and port of the sockaddr */ +char* sprintaddr(char* buf, size_t size, struct sockaddr* s) +{ + char addr_str[1024]; + + inet_ntop(AF_INET, &((struct sockaddr_in*)s)->sin_addr, addr_str, sizeof(addr_str)); + snprintf(buf, size, "%s:%d", addr_str, ntohs(((struct sockaddr_in*)s)->sin_port)); + return buf; +} + +/* turns a "hostname:port" string into a struct sockaddr; +sock: socket address to which to copy the addr +fullname: input string -- it gets clobbered +*/ +void resolve_name(struct sockaddr *sock, char* fullname) +{ + struct addrinfo *addr, hint; + char *serv, *host; + int res; + + char *sep = strchr(fullname, ':'); + + if (!sep) /* No separator: parameter is just a port */ + { + serv = fullname; + fprintf(stderr, "names must be fully specified as hostname:port\n"); + exit(1); + } + else { + host = fullname; + serv = sep+1; + *sep = 0; + } + + memset(&hint, 0, sizeof(hint)); + hint.ai_family = PF_INET; + hint.ai_socktype = SOCK_STREAM; + + res = getaddrinfo(host, serv, &hint, &addr); + 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); + } + + memcpy(sock, addr->ai_addr, sizeof(*sock)); + + freeaddrinfo(addr); +} + +/* Log to syslog, and to stderr if foreground */ +void log_message(int type, char* msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vsyslog(type, msg, ap); + va_end(ap); + + va_start(ap, msg); + if (foreground) + vfprintf(stderr, msg, ap); + va_end(ap); +} + +/* syslogs who connected to where */ +void log_connection(struct connection *cnx) +{ + struct sockaddr peeraddr, localaddr; + socklen_t size = sizeof(peeraddr); + char buf[64], buf2[64]; + int res; + + res = getpeername(cnx->q[0].fd, &peeraddr, &size); + if (res == -1) return; /* that should never happen, right? */ + + res = getpeername(cnx->q[1].fd, &localaddr, &size); + if (res == -1) return; /* that should never happen, right? */ + + log_message(LOG_INFO, "connection from %s forwarded to %s\n", + sprintaddr(buf, sizeof(buf), &peeraddr), sprintaddr(buf2, sizeof(buf2), &localaddr)); + +} + + +/* libwrap (tcpd): check the connection is legal. This is necessary because + * the actual server will only see a connection coming from localhost and can't + * apply the rules itself. + * + * Returns -1 if access is denied, 0 otherwise + */ +int check_access_rights(int in_socket, const char* service) +{ +#ifdef LIBWRAP + struct sockaddr peeraddr; + socklen_t size = sizeof(peeraddr); + char addr_str[1024]; + struct hostent *host; + struct in_addr addr; + int res; + + res = getpeername(in_socket, &peeraddr, &size); + CHECK_RES_DIE(res, "getpeername"); + inet_ntop(AF_INET, &((struct sockaddr_in*)&peeraddr)->sin_addr, addr_str, sizeof(addr_str)); + + addr.s_addr = inet_addr(addr_str); + host = gethostbyaddr((char *)&addr, sizeof(addr), AF_INET); + + if (!hosts_ctl(service, (host ? host->h_name : STRING_UNKNOWN), addr_str, STRING_UNKNOWN)) { + if (verbose) + fprintf(stderr, "access denied\n"); + log_connection(in_socket, "access denied"); + close(in_socket); + return -1; + } +#endif + return 0; +} + + +void setup_signals(void) +{ + int res; + struct sigaction action; + + /* Request no SIGCHLD is sent upon termination of + * the children */ + memset(&action, 0, sizeof(action)); + action.sa_handler = NULL; + action.sa_flags = SA_NOCLDWAIT; + res = sigaction(SIGCHLD, &action, NULL); + CHECK_RES_DIE(res, "sigaction"); +} + +/* Open syslog connection with appropriate banner; + * banner is made up of basename(bin_name)+"[pid]" */ +void setup_syslog(char* bin_name) { + char *name1, *name2; + + name1 = strdup(bin_name); + asprintf(&name2, "%s[%d]", basename(name1), getpid()); + openlog(name2, LOG_CONS, LOG_AUTH); + free(name1); + /* Don't free name2, as openlog(3) uses it (at least in glibc) */ + + log_message(LOG_INFO, "%s %s started\n", server_type, VERSION); +} + +/* We don't want to run as root -- drop priviledges if required */ +void drop_privileges(char* user_name) +{ + int res; + struct passwd *pw = getpwnam(user_name); + if (!pw) { + fprintf(stderr, "%s: not found\n", user_name); + exit(1); + } + if (verbose) + fprintf(stderr, "turning into %s\n", user_name); + + res = setgid(pw->pw_gid); + CHECK_RES_DIE(res, "setgid"); + setuid(pw->pw_uid); + CHECK_RES_DIE(res, "setuid"); +} + +/* Writes my PID */ +void write_pid_file(char* pidfile) +{ + FILE *f; + + f = fopen(pidfile, "w"); + if (!f) { + perror(pidfile); + exit(1); + } + + fprintf(f, "%d\n", getpid()); + fclose(f); +} + +void printsettings(void) +{ + char buf[64]; + int i; + + for (i = 0; i < ARRAY_SIZE(protocols); i++) { + if (protocols[i].affected) + fprintf(stderr, + "%s addr: %s. libwrap service: %s\n", + protocols[i].description, + sprintaddr(buf, sizeof(buf), &protocols[i].saddr), + protocols[i].service); + } + fprintf(stderr, "listening on %s\n", sprintaddr(buf, sizeof(buf), &addr_listen)); +} + +void parse_cmdline(int argc, char* argv[]) +{ + int c; + + while ((c = getopt(argc, argv, "t:l:s:o:p:P:ivfVu:")) != EOF) { + switch (c) { + + case 't': + probing_timeout = atoi(optarg); + break; + + case 'p': + resolve_name(&addr_listen, optarg); + break; + + case 'l': + protocols[PROT_SSL].affected = 1; + resolve_name(&protocols[PROT_SSL].saddr, optarg); + break; + + case 's': + protocols[PROT_SSH].affected = 1; + resolve_name(&protocols[PROT_SSH].saddr, optarg); + break; + + case 'o': + protocols[PROT_OPENVPN].affected = 1; + resolve_name(&protocols[PROT_OPENVPN].saddr, optarg); + break; + + case 'i': + inetd = 1; + break; + + case 'f': + foreground = 1; + break; + + case 'v': + verbose += 1; + 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); + } + } +} + +int main(int argc, char *argv[]) +{ + + extern char *optarg; + extern int optind; + int res; + + int listen_socket; + + /* Init defaults */ + char listen_str[] = "0.0.0.0:443"; + char ssl_str[] = "localhost:443"; + char ssh_str[] = "localhost:22"; + pid_file = "/var/run/sslh.pid"; + user_name = "nobody"; + foreground = 0; + + resolve_name(&addr_listen, listen_str); + protocols[PROT_SSL].affected = 1; + resolve_name(&protocols[PROT_SSL].saddr, ssl_str); + protocols[PROT_SSH].affected = 1; + resolve_name(&protocols[PROT_SSH].saddr, ssh_str); + + parse_cmdline(argc, argv); + + if (inetd) + { + verbose = 0; + start_shoveler(0); + exit(0); + } + + if (verbose) + printsettings(); + + listen_socket = start_listen_socket(&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_socket); + + return 0; +} diff --git a/common.h b/common.h new file mode 100755 index 0000000..7748b77 --- /dev/null +++ b/common.h @@ -0,0 +1,127 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef VERSION +#define VERSION "v?" +#endif + +#define CHECK_RES_DIE(res, str) \ + if (res == -1) { \ + perror(str); \ + exit(1); \ + } + +#define CHECK_RES_RETURN(res, str) \ + if (res == -1) { \ + log_message(LOG_CRIT, "%s: %d\n", str, errno); \ + return res; \ + } + +#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0])) + +#if 1 +#define TRACE fprintf(stderr, "%s:%d\n", __FILE__, __LINE__); +#else +#define TRACE +#endif + +enum connection_state { + ST_PROBING=1, /* Waiting for timeout to find where to forward */ + 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_SSL, +} T_PROTO_ID; + +/* 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) */ + char* service; /* service name to do libwrap checks */ + struct sockaddr 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[]; + +/* A 'queue' is composed of a file descriptor (which can be read from or + * written to), and a queue for defered write data */ +struct queue { + int fd; + void *begin_defered_data; + void *defered_data; + int defered_data_size; +}; + +struct connection { + enum connection_state state; + time_t probe_timeout; + + /* q[0]: queue for external connection (client); + * q[1]: queue for internal connection (httpd or sshd); + * */ + struct queue q[2]; +}; + +#define FD_CNXCLOSED 0 +#define FD_NODATA -1 +#define FD_STALLED -2 + + +/* common.c */ +void init_cnx(struct connection *cnx); +int start_listen_socket(struct sockaddr *addr); +int fd2fd(struct queue *target, struct queue *from); +T_PROTO_ID probe_client_protocol(struct connection *cnx); +char* sprintaddr(char* buf, size_t size, struct sockaddr* s); +void resolve_name(struct sockaddr *sock, char* fullname) ; +void log_connection(struct connection *cnx); +int check_access_rights(int in_socket, const char* service); +void setup_signals(void); +void setup_syslog(char* bin_name); +void drop_privileges(char* user_name); +void write_pid_file(char* pidfile); +void printsettings(void); +void parse_cmdline(int argc, char* argv[]); +void log_message(int type, char* msg, ...); +void dump_connection(struct connection *cnx); + + +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 addr_listen, addr_ssl, addr_ssh, addr_openvpn; +extern const char* USAGE_STRING; +extern char* user_name, *pid_file; +extern const char* server_type; + +/* sslh-fork.c */ +void start_shoveler(int); + +void main_loop(int); + + diff --git a/scripts/etc.rc.d.init.d.sslh.centos b/scripts/etc.rc.d.init.d.sslh.centos index c96d967..2007927 100755 --- a/scripts/etc.rc.d.init.d.sslh.centos +++ b/scripts/etc.rc.d.init.d.sslh.centos @@ -5,8 +5,11 @@ # sslh - a daemon switching incoming connection between SSH and SSL/HTTPS servers # # Author: Andre Krajnik akrajnik@gmail.com +# 2010-03-20 +# # # chkconfig: 2345 13 87 +# # description: sslh - a daemon switching incoming connection between SSH and SSL/HTTPS servers # Source function library. @@ -14,28 +17,22 @@ # ./sslh -p 0.0.0.0:8443 -l 127.0.0.1:443 -s 127.0.0.1:22 -SSLH='/usr/local/sbin/sslh' -PIDFILE='/var/run/sslh' +SSLH="/usr/local/sbin/sslh" +PIDFILE="/var/run/sslh" -OPTIONS='-p 0.0.0.0:8443 -l 127.0.0.1:443 -s 127.0.0.1:22 -P $PIDFILE' +OPTIONS="-p 0.0.0.0:8443 -l 127.0.0.1:443 -s 127.0.0.1:22" if [ -f /etc/sysconfig/sslh ]; then . /etc/sysconfig/sslh fi - start() { echo -n "Starting SSL-SSH-Switch: " if [ -f $PIDFILE ]; then PID=`cat $PIDFILE` echo sslh already running: $PID exit 2; - elif [ -f $PIDFILE ]; then - PID=`cat $PIDFILE` - echo sslh already running: $PID - exit 2; else - cd $SLAPD_DIR daemon $SSLH $OPTIONS RETVAL=$? echo @@ -75,3 +72,5 @@ case "$1" in esac exit $? + + diff --git a/sslh-fork.c b/sslh-fork.c new file mode 100644 index 0000000..893c345 --- /dev/null +++ b/sslh-fork.c @@ -0,0 +1,151 @@ +/* + Reimplementation of sslh in C + +# Copyright (C) 2007-2008 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 + +*/ + +#include "common.h" + +const char* server_type = "sslh-fork"; + +#define MAX(a, b) (((a) > (b)) ? (a) : (b)) + +/* shovels data from one fd to the other and vice-versa + returns after one socket closed + */ +int shovel(struct connection *cnx) +{ + fd_set fds; + int res, i; + int max_fd = MAX(cnx->q[0].fd, cnx->q[1].fd) + 1; + + FD_ZERO(&fds); + while (1) { + FD_SET(cnx->q[0].fd, &fds); + FD_SET(cnx->q[1].fd, &fds); + + res = select( + max_fd, + &fds, + NULL, + NULL, + NULL + ); + CHECK_RES_DIE(res, "select"); + + for (i = 0; i < 2; i++) { + if (FD_ISSET(cnx->q[i].fd, &fds)) { + res = fd2fd(&cnx->q[1-i], &cnx->q[i]); + if (!res) { + if (verbose) + fprintf(stderr, "%s %s", i ? "client" : "server", "socket closed\n"); + return res; + } + } + } + } +} + +/* Child process that finds out what to connect to and proxies + */ +void start_shoveler(int in_socket) +{ + fd_set fds; + struct timeval tv; + struct sockaddr *saddr; + int res; + int out_socket; + char *target; + struct connection cnx; + T_PROTO_ID prot; + + init_cnx(&cnx); + + FD_ZERO(&fds); + FD_SET(in_socket, &fds); + memset(&tv, 0, sizeof(tv)); + tv.tv_sec = probing_timeout; + res = select(in_socket + 1, &fds, NULL, NULL, &tv); + if (res == -1) + perror("select"); + + cnx.q[0].fd = in_socket; + + if (FD_ISSET(in_socket, &fds)) { + /* Received data: figure out what protocol it is */ + prot = probe_client_protocol(&cnx); + } else { + /* Timed out: it's necessarily SSH */ + prot = PROT_SSH; + } + + 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(AF_INET, SOCK_STREAM, 0); + res = connect(out_socket, saddr, sizeof(addr_ssl)); + CHECK_RES_DIE(res, "connect"); + if (verbose) + fprintf(stderr, "connected to something\n"); + + cnx.q[1].fd = out_socket; + + log_connection(&cnx); + + flush_defered(&cnx.q[1]); + + shovel(&cnx); + + close(in_socket); + close(out_socket); + + if (verbose) + fprintf(stderr, "connection closed down\n"); + + exit(0); +} + +void main_loop(int listen_socket) +{ + int in_socket; + + while (1) + { + in_socket = accept(listen_socket, 0, 0); + if (verbose) fprintf(stderr, "accepted fd %d\n", in_socket); + + if (!fork()) + { + close(listen_socket); + start_shoveler(in_socket); + exit(0); + } + close(in_socket); + } +} + +/* The actual main is in common.c: it's the same for both version of + * the server + */ + diff --git a/sslh-select.c b/sslh-select.c new file mode 100644 index 0000000..7c1592e --- /dev/null +++ b/sslh-select.c @@ -0,0 +1,337 @@ +/* + sslh: a SSL/SSH multiplexer + +# Copyright (C) 2007-2010 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 __LINUX__ + +#include "common.h" + +const char* server_type = "sslh-select"; + +/* cnx_num_alloc is the number of connection to allocate at once (at start-up, + * 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. + */ +static long cnx_num_alloc; + +/* Make the file descriptor non-block */ +int set_nonblock(int fd) +{ + int flags; + + flags = fcntl(fd, F_GETFL); + CHECK_RES_RETURN(flags, "fcntl"); + + flags |= O_NONBLOCK; + + flags = fcntl(fd, F_SETFL, flags); + CHECK_RES_RETURN(flags, "fcntl"); + + return flags; +} + +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); + + 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; +} + +/* Accepts a connection from the main socket and assigns it to an empty slot. + * If no slots are available, allocate another few. If that fails, drop the + * connexion */ +int accept_new_connection(int listen_socket, struct connection *cnx[], int* cnx_size) +{ + int in_socket, free, i, res; + struct connection *new; + + in_socket = accept(listen_socket, 0, 0); + CHECK_RES_RETURN(in_socket, "accept"); + + res = set_nonblock(in_socket); + if (res == -1) return -1; + + /* Find an empty slot */ + for (free = 0; (free < *cnx_size) && ((*cnx)[free].q[0].fd != -1); free++) { + /* nothing */ + } + if (free >= *cnx_size) { + if (verbose) + fprintf(stderr, "buying more slots from the slot machine.\n"); + new = realloc(*cnx, (*cnx_size + cnx_num_alloc) * sizeof((*cnx)[0])); + if (!new) { + log_message(LOG_ERR, "unable to realloc -- dropping connection\n"); + return -1; + } + *cnx = new; + *cnx_size += cnx_num_alloc; + for (i = free; i < *cnx_size; i++) { + init_cnx(&(*cnx)[i]); + } + } + (*cnx)[free].q[0].fd = in_socket; + (*cnx)[free].state = ST_PROBING; + (*cnx)[free].probe_timeout = time(NULL) + probing_timeout; + + if (verbose) + fprintf(stderr, "accepted fd %d on slot %d\n", in_socket, free); + + return in_socket; +} + +/* Connect queue 1 of connection to SSL; returns new file descriptor */ +int connect_queue(struct connection *cnx, struct sockaddr *addr, + char* cnx_name, + fd_set *fds_r, fd_set *fds_w) +{ + struct queue *q = &cnx->q[1]; + int res; + + q->fd = socket(AF_INET, SOCK_STREAM, 0); + res = connect(q->fd, 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 { + set_nonblock(q->fd); + flush_defered(q); + if (q->defered_data) { + FD_SET(q->fd, fds_w); + } else { + FD_SET(q->fd, fds_r); + } + return q->fd; + } +} + +/* shovels data from active fd to the other + returns after one socket closed or operation would block + */ +void shovel(struct connection *cnx, int active_fd, + fd_set *fds_r, fd_set *fds_w) +{ + struct queue *read_q, *write_q; + + read_q = &cnx->q[active_fd]; + write_q = &cnx->q[1-active_fd]; + + if (verbose) + fprintf(stderr, "activity on fd%d\n", read_q->fd); + + switch(fd2fd(write_q, read_q)) { + case -1: + case FD_CNXCLOSED: + tidy_connection(cnx, fds_r, fds_w); + break; + + case FD_STALLED: + FD_SET(write_q->fd, fds_w); + FD_CLR(read_q->fd, fds_r); + break; + + default: /* Nothing */ + break; + } +} + +/* returns true if specified fd is initialised and present in fd_set */ +int is_fd_active(int fd, fd_set* set) +{ + if (fd == -1) return 0; + return FD_ISSET(fd, set); +} + +/* Main loop: the idea is as follow: + * - fds_r and fds_w contain the file descritors to monitor in read and write + * - When a file descriptor goes off, process it: read from it, write the data + * to its corresponding pair. + * - When a file descriptor blocks when writing, remove the read fd from fds_r, + * move the data to a defered buffer, and add the write fd to fds_w. Defered + * buffer is allocated dynamically. + * - When we can write to a file descriptor that has defered data, we try to + * write as much as we can. Once all data is written, remove the fd from fds_w + * and add its corresponding pair to fds_r, free the buffer. + * + * 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_socket) +{ + 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 */ + struct timeval tv; + int max_fd, in_socket, i, j, res; + struct connection *cnx; + T_PROTO_ID prot; + int num_cnx; /* Number of connections in *cnx */ + int num_probing = 0; /* Number of connections currently probing + * We use this to know if we need to time out of + * select() */ + + FD_ZERO(&fds_r); + FD_ZERO(&fds_w); + FD_SET(listen_socket, &fds_r); + max_fd = listen_socket + 1; + + set_nonblock(listen_socket); + + cnx_num_alloc = getpagesize() / sizeof(struct connection); + + num_cnx = cnx_num_alloc; /* Start with a set pool of slots */ + cnx = malloc(num_cnx * sizeof(struct connection)); + for (i = 0; i < num_cnx; i++) + init_cnx(&cnx[i]); + + while (1) + { + memset(&tv, 0, sizeof(tv)); + tv.tv_sec = probing_timeout; + + memcpy(&readfds, &fds_r, sizeof(readfds)); + memcpy(&writefds, &fds_w, sizeof(writefds)); + + if (verbose) + fprintf(stderr, "selecting... max_fd=%d num_probing=%d\n", max_fd, num_probing); + res = select(max_fd, &readfds, &writefds, NULL, num_probing ? &tv : NULL); + if (res < 0) + perror("select"); + + + /* Check main socket for new connections */ + if (FD_ISSET(listen_socket, &readfds)) { + in_socket = accept_new_connection(listen_socket, &cnx, &num_cnx); + num_probing++; + + if (in_socket > 0) { + FD_SET(in_socket, &fds_r); + if (in_socket >= max_fd) + max_fd = in_socket + 1;; + } + FD_CLR(listen_socket, &readfds); + } + + /* Check all sockets for write activity */ + for (i = 0; i < num_cnx; i++) { + if (cnx[i].q[0].fd != -1) { + for (j = 0; j < 2; j++) { + if (is_fd_active(cnx[i].q[j].fd, &writefds)) { + res = flush_defered(&cnx[i].q[j]); + if ((res == -1) && ((errno == EPIPE) || (errno == ECONNRESET))) { + if (cnx[i].state == ST_PROBING) num_probing--; + tidy_connection(&cnx[i], &fds_r, &fds_w); + if (verbose) + fprintf(stderr, "closed slot %d\n", i); + } + /* If no defered data is left, stop monitoring the fd + * for write, and restart monitoring the other one for reads*/ + if (!cnx[i].q[j].defered_data_size) { + FD_CLR(cnx[i].q[j].fd, &fds_w); + FD_SET(cnx[i].q[1-j].fd, &fds_r); + } + } + } + } + } + + /* Check all sockets for read activity */ + for (i = 0; i < num_cnx; i++) { + for (j = 0; j < 2; j++) { + if (is_fd_active(cnx[i].q[j].fd, &readfds) || + ((cnx[i].state == ST_PROBING) && (cnx[i].probe_timeout < time(NULL)))) { + if (verbose) + fprintf(stderr, "processing fd%d slot %d\n", j, i); + + switch (cnx[i].state) { + + case ST_PROBING: + if (j == 1) { + fprintf(stderr, "Activity on fd2 while probing, impossible\n"); + dump_connection(&cnx[i]); + exit(1); + } + num_probing--; + cnx[i].state = ST_SHOVELING; + + /* 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; + } else { + prot = probe_client_protocol(&cnx[i]); + } + + /* libwrap check if required for this protocol */ + if (protocols[prot].service && + check_access_rights(in_socket, protocols[prot].service)) { + tidy_connection(&cnx[i], &fds_r, &fds_w); + res = -1; + } else { + res = connect_queue(&cnx[i], + &protocols[prot].saddr, + protocols[prot].description, + &fds_r, &fds_w); + } + + if (res >= max_fd) + max_fd = res + 1;; + break; + + case ST_SHOVELING: + shovel(&cnx[i], j, &fds_r, &fds_w); + break; + + default: /* illegal */ + log_message(LOG_ERR, "Illegal connection state %d\n", cnx[i].state); + exit(1); + } + } + } + } + } +} + + +void start_shoveler(int listen_socket) { + fprintf(stderr, "inetd mode is not supported in select mode\n"); + exit(1); +} + + +/* The actual main is in common.c: it's the same for both version of + * the server + */ + + diff --git a/sslh.c b/sslh.c deleted file mode 100644 index 3bf205f..0000000 --- a/sslh.c +++ /dev/null @@ -1,498 +0,0 @@ -/* - Reimplementation of sslh in C - -# Copyright (C) 2007-2008 Yves Rutschle -# -# This program is free software; you can redistribute it -# and/or modify it under the terms of the GNU General Public -# License as published by the Free Software Foundation; either -# version 2 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be -# useful, but WITHOUT ANY WARRANTY; without even the implied -# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR -# PURPOSE. See the GNU General Public License for more -# details. -# -# The full text for the General Public License is here: -# http://www.gnu.org/licenses/gpl.html - -*/ - -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef LIBWRAP -#include -int allow_severity =0, deny_severity = 0; -#endif - -#ifndef VERSION -#define VERSION "v?" -#endif - -#define CHECK_RES_DIE(res, str) \ -if (res == -1) { \ - perror(str); \ - exit(1); \ -} - -#define USAGE_STRING \ -"sslh " VERSION "\n" \ -"usage:\n" \ -"\tsslh [-t ] -u -p [listenaddr:] \n" \ -"\t\t-s [sshhost:]port -l [sslhost:]port [-P pidfile] [-v] [-i] [-V]\n\n" \ -"-v: verbose\n" \ -"-V: version\n" \ -"-p: address and port to listen on. default: 0.0.0.0:443\n" \ -"-s: SSH address: where to connect an SSH connection. default: localhost:22\n" \ -"-l: SSL address: where to connect an SSL connection.\n" \ -"-P: PID file. Default: /var/run/sslh.pid.\n" \ -"-i: Run as a inetd service.\n" \ -"" - -int verbose = 0; /* That's really quite global */ - -/* Starts a listening socket on specified address. - Returns file descriptor - */ -int start_listen_socket(struct sockaddr *addr) -{ - struct sockaddr_in *saddr = (struct sockaddr_in*)addr; - int sockfd, res, reuse; - - sockfd = socket(AF_INET, SOCK_STREAM, 0); - CHECK_RES_DIE(sockfd, "socket"); - - reuse = 1; - res = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char*)&reuse, sizeof(reuse)); - CHECK_RES_DIE(res, "setsockopt"); - - res = bind (sockfd, (struct sockaddr*)saddr, sizeof(*saddr)); - CHECK_RES_DIE(res, "bind"); - - res = listen (sockfd, 5); - CHECK_RES_DIE(res, "listen"); - - return sockfd; -} - - -/* - * moves data from one fd to other - * returns 0 if incoming socket closed, size moved otherwise - */ -int fd2fd(int target, int from) -{ - char buffer[BUFSIZ]; - int size; - - size = read(from, buffer, sizeof(buffer)); - CHECK_RES_DIE(size, "read"); - - if (size == 0) - return 0; - - size = write(target, buffer, size); - CHECK_RES_DIE(size, "write"); - - return size; -} - -/* shovels data from one fd to the other and vice-versa - returns after one socket closed - */ -int shovel(int fd1, int fd2) -{ - fd_set fds; - int res; - - FD_ZERO(&fds); - while (1) { - FD_SET(fd1, &fds); - FD_SET(fd2, &fds); - - res = select( - (fd1 > fd2 ? fd1 : fd2 ) + 1, - &fds, - NULL, - NULL, - NULL - ); - CHECK_RES_DIE(res, "select"); - - if (FD_ISSET(fd1, &fds)) { - res = fd2fd(fd2, fd1); - if (!res) { - if (verbose) fprintf(stderr, "client socket closed\n"); - return res; - } - } - - if (FD_ISSET(fd2, &fds)) { - res = fd2fd(fd1, fd2); - if (!res) { - if (verbose) fprintf(stderr, "server socket closed\n"); - return res; - } - } - } -} - -/* returns a string that prints the IP and port of the sockaddr */ -char* sprintaddr(char* buf, size_t size, struct sockaddr* s) -{ - char addr_str[1024]; - - inet_ntop(AF_INET, &((struct sockaddr_in*)s)->sin_addr, addr_str, sizeof(addr_str)); - snprintf(buf, size, "%s:%d", addr_str, ntohs(((struct sockaddr_in*)s)->sin_port)); - return buf; -} - -/* turns a "hostname:port" string into a struct sockaddr; -sock: socket address to which to copy the addr -fullname: input string -- it gets clobbered -*/ -void resolve_name(struct sockaddr *sock, char* fullname) { - struct addrinfo *addr, hint; - char *serv, *host; - int res; - - char *sep = strchr(fullname, ':'); - - if (!sep) /* No separator: parameter is just a port */ - { - serv = fullname; - fprintf(stderr, "names must be fully specified as hostname:port\n"); - exit(1); - } - else { - host = fullname; - serv = sep+1; - *sep = 0; - } - - memset(&hint, 0, sizeof(hint)); - hint.ai_family = PF_INET; - hint.ai_socktype = SOCK_STREAM; - - res = getaddrinfo(host, serv, &hint, &addr); - 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); - } - - memcpy(sock, addr->ai_addr, sizeof(*sock)); -} - -/* syslogs who connected to where */ -void log_connection(int socket, char* target) -{ - struct sockaddr peeraddr; - socklen_t size = sizeof(peeraddr); - char buf[64]; - int res; - - res = getpeername(socket, &peeraddr, &size); - CHECK_RES_DIE(res, "getpeername"); - - syslog(LOG_INFO, "connection from %s forwarded to %s\n", - sprintaddr(buf, sizeof(buf), &peeraddr), target); - -} - -/* - * Settings that depend on the command line. That's less global than verbose * :-) - * They're set in main(), but also used in start_shoveler(), and it'd be heavy-handed - * to pass it all as parameters - */ -int timeout = 2; -struct sockaddr addr_listen; -struct sockaddr addr_ssl, addr_ssh; - -/* libwrap (tcpd): check the ssh connection is legal. This is necessary because - * the actual sshd will only see a connection coming from localhost and can't - * apply the rules itself. - */ -void check_access_rights(int in_socket) -{ -#ifdef LIBWRAP - struct sockaddr peeraddr; - socklen_t size = sizeof(peeraddr); - char addr_str[1024]; - struct hostent *host; - struct in_addr addr; - int res; - - res = getpeername(in_socket, &peeraddr, &size); - CHECK_RES_DIE(res, "getpeername"); - inet_ntop(AF_INET, &((struct sockaddr_in*)&peeraddr)->sin_addr, addr_str, sizeof(addr_str)); - - addr.s_addr = inet_addr(addr_str); - host = gethostbyaddr((char *)&addr, sizeof(addr), AF_INET); - - if (!hosts_ctl("sshd", (host ? host->h_name : STRING_UNKNOWN), addr_str, STRING_UNKNOWN)) { - if (verbose) - fprintf(stderr, "access denied\n"); - log_connection(in_socket, "access denied"); - close(in_socket); - exit(0); - } -#endif -} - -/* Child process that finds out what to connect to and proxies - */ -void start_shoveler(int in_socket) -{ - fd_set fds; - struct timeval tv; - struct sockaddr *saddr; - int res; - int out_socket; - char *target; - - FD_ZERO(&fds); - FD_SET(in_socket, &fds); - memset(&tv, 0, sizeof(tv)); - tv.tv_sec = timeout; - res = select(in_socket + 1, &fds, NULL, NULL, &tv); - if (res == -1) - perror("select"); - - /* Pick the target address depending on whether we timed out or not */ - if (FD_ISSET(in_socket, &fds)) { - /* The client wrote something to the socket: it's an SSL connection */ - saddr = &addr_ssl; - target = "SSL"; - } else { - /* The client hasn't written anything and we timed out: connect to SSH */ - saddr = &addr_ssh; - target = "SSH"; - - /* do hosts_access check if built with libwrap support */ - check_access_rights(in_socket); - } - - log_connection(in_socket, target); - - /* Connect the target socket */ - out_socket = socket(AF_INET, SOCK_STREAM, 0); - res = connect(out_socket, saddr, sizeof(addr_ssl)); - CHECK_RES_DIE(res, "connect"); - if (verbose) - fprintf(stderr, "connected to something\n"); - - shovel(in_socket, out_socket); - - close(in_socket); - close(out_socket); - - if (verbose) - fprintf(stderr, "connection closed down\n"); - - exit(0); -} - -void setup_signals(void) -{ - int res; - struct sigaction action; - - /* Request no SIGCHLD is sent upon termination of - * the children */ - memset(&action, 0, sizeof(action)); - action.sa_handler = NULL; - action.sa_flags = SA_NOCLDWAIT; - res = sigaction(SIGCHLD, &action, NULL); - CHECK_RES_DIE(res, "sigaction"); -} - -/* Open syslog connection with appropriate banner; - * banner is made up of basename(bin_name)+"[pid]" */ -void setup_syslog(char* bin_name) { - char *name1, *name2; - - name1 = strdup(bin_name); - asprintf(&name2, "%s[%d]", basename(name1), getpid()); - openlog(name2, LOG_CONS, LOG_AUTH); - free(name1); - /* Don't free name2, as openlog(3) uses it (at least in glibc) */ -} - -/* We don't want to run as root -- drop priviledges if required */ -void drop_privileges(char* user_name) -{ - int res; - struct passwd *pw = getpwnam(user_name); - if (!pw) { - fprintf(stderr, "%s: not found\n", user_name); - exit(1); - } - if (verbose) - fprintf(stderr, "turning into %s\n", user_name); - - res = setgid(pw->pw_gid); - CHECK_RES_DIE(res, "setgid"); - setuid(pw->pw_uid); - CHECK_RES_DIE(res, "setuid"); -} - -/* Writes my PID */ -void write_pid_file(char* pidfile) -{ - FILE *f; - - f = fopen(pidfile, "w"); - if (!f) { - perror(pidfile); - exit(1); - } - - fprintf(f, "%d\n", getpid()); - fclose(f); -} - -void printsettings(void) -{ - char buf[64]; - - fprintf( - stderr, - "SSL addr: %s (after timeout %ds)\n", - sprintaddr(buf, sizeof(buf), &addr_ssl), - timeout - ); - fprintf(stderr, "SSH addr: %s\n", sprintaddr(buf, sizeof(buf), &addr_ssh)); - fprintf(stderr, "listening on %s\n", sprintaddr(buf, sizeof(buf), &addr_listen)); -} - -int main(int argc, char *argv[]) -{ - - extern char *optarg; - extern int optind; - int c, res; - - int in_socket, listen_socket; - - /* Init defaults */ - char *user_name = "nobody"; - char listen_str[] = "0.0.0.0:443"; - char ssl_str[] = "localhost:443"; - char ssh_str[] = "localhost:22"; - char *pid_file = "/var/run/sslh.pid"; - char inetd = 0; - - resolve_name(&addr_listen, listen_str); - resolve_name(&addr_ssl, ssl_str); - resolve_name(&addr_ssh, ssh_str); - - while ((c = getopt(argc, argv, "t:l:s:p:P:ivVu:")) != EOF) { - switch (c) { - - case 't': - timeout = atoi(optarg); - break; - - case 'p': - resolve_name(&addr_listen, optarg); - break; - - case 'l': - resolve_name(&addr_ssl, optarg); - break; - - case 's': - resolve_name(&addr_ssh, optarg); - break; - - case 'i': - inetd = 1; - break; - - case 'v': - verbose += 1; - break; - - case 'V': - printf("sslh %s\n", VERSION); - exit(0); - - case 'u': - user_name = optarg; - break; - - case 'P': - pid_file = optarg; - break; - - default: - fprintf(stderr, USAGE_STRING); - exit(2); - } - } - - if(inetd) - { - verbose = 0; - start_shoveler(0); - exit(0); - } - - if (verbose) - printsettings(); - - setup_signals(); - - listen_socket = start_listen_socket(&addr_listen); - - if (fork() > 0) exit(0); /* Detach */ - - write_pid_file(pid_file); - - drop_privileges(user_name); - - /* New session -- become group leader */ - res = setsid(); - CHECK_RES_DIE(res, "setsid: already process leader"); - - /* Open syslog connection */ - setup_syslog(argv[0]); - - /* Main server loop: accept connections, find what they are, fork shovelers */ - while (1) - { - in_socket = accept(listen_socket, 0, 0); - if (verbose) fprintf(stderr, "accepted fd %d\n", in_socket); - - if (!fork()) - { - close(listen_socket); - start_shoveler(in_socket); - exit(0); - } - close(in_socket); - } - - return 0; -} - - diff --git a/sslh.pod b/sslh.pod index 6d7b513..8f488ca 100644 --- a/sslh.pod +++ b/sslh.pod @@ -6,15 +6,15 @@ =head1 SYNOPSIS -sslh [ B<-t> I ] [B<-p> I] [B<-l> I] [B<-s> I] [B<-u> I] [B<-P> I] [-v] [-i] [-V] +sslh [ B<-t> I ] [B<-p> I] [B<-l> I] [B<-s> I] [B<-o> I] [B<-u> I] [B<-P> I] [-v] [-i] [-V] [-f] =head1 DESCRIPTION -B lets one accept both HTTPS and SSH connections on -the same port. It makes it possible to connect to an SSH -server on port 443 (e.g. from inside a corporate firewall, -which almost never block port 443) while still serving HTTPS -on that port. +B accepts HTTPS, SSH 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. The idea is to have B listen to the external 443 port, accept the incoming connections, work out what type of @@ -23,14 +23,24 @@ server. =head2 Protocol detection -The protocol detection is made based on a small difference -between SSL and SSH: an SSL client connecting to a server -speaks first, whereas an SSH client expects the SSH server -to speak first (announcing itself with a banner). B -waits for some time for the incoming connection to send data. -If it does before the timeout occurs, it is supposed to be -an SSL connection. Otherwise, it is supposed to be an SSH -connection. +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. + +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). + +B waits for some time for the incoming connection to +send data. If it stays quiet after the timeout period, it is +assumed to be a shy SSH client, and is connected to the SSH +server. Otherwise, B reads the first packet the client +provides, and connects it to the SSH server if it starts +with "SSH-", or connects it to the SSL server otherwise. =head2 Libwrap support @@ -78,6 +88,13 @@ inside your network to just connect directly to B. Interface and port on which to forward SSH connection, defaults to I. +=item B<-o> I + +Interface and port on which to forward OpenVPN connections. +This parameter is optional, and has no default. If not +specified, incoming OpenVPN connections will not be detected +as such and treated the same as SSL. + =item B<-v> Increase verboseness. @@ -100,7 +117,13 @@ server. Defaults to I. =item B<-i> Runs as an I server. Options B<-P> (PID file), B<-p> -(listen address), B<-U> (user) are ignored. +(listen address), B<-u> (user) are ignored. + +=item B<-f> + +Runs in foreground. The server will not fork and will remain connected +to the terminal. Messages normally sent to B will also be sent +to I. =back diff --git a/t b/t new file mode 100755 index 0000000..2c2d164 --- /dev/null +++ b/t @@ -0,0 +1,229 @@ +#! /usr/bin/perl -w + +# 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 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 $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; + +# Which tests do we run +my $SSL_CNX = 1; +my $SSH_SHY_CNX = 1; +my $SSH_BOLD_CNX = 1; +my $SSL_MIX_SSH = 1; +my $SSH_MIX_SSL = 1; +my $BIG_MSG = 1; +my $MANY_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; + +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; + +# 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 $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; + 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 $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) { + my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); + warn "----> $!\n" unless defined $cnx_h; + if (defined $cnx_h) { + push @cnx_h, $cnx_h; + } + } + my (@cnx_l, @ssl_data); + for (1..$NUM_SSL_CNX) { + my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); + warn "----> $!\n" unless defined $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]; + } + } + + 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]"); + } + } +} + + + +kill 15, `cat $pidfile` or warn "kill: $!\n";