diff -Nru sslh-1.18/basic.cfg sslh-1.20/basic.cfg --- sslh-1.18/basic.cfg 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/basic.cfg 2018-11-20 21:58:41.000000000 +0000 @@ -9,6 +9,7 @@ timeout: 2; user: "nobody"; pidfile: "/var/run/sslh.pid"; +chroot: "/var/empty"; # Change hostname with your external address name. @@ -19,7 +20,7 @@ protocols: ( - { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; }, + { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; fork: true; }, { name: "openvpn"; host: "localhost"; port: "1194"; }, { name: "xmpp"; host: "localhost"; port: "5222"; }, { name: "http"; host: "localhost"; port: "80"; }, diff -Nru sslh-1.18/ChangeLog sslh-1.20/ChangeLog --- sslh-1.18/ChangeLog 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/ChangeLog 2018-11-20 21:58:41.000000000 +0000 @@ -1,3 +1,50 @@ +v1.20: 20NOV2018 + Added support for socks5 protocol (Eugene Protozanov) + + New probing method: + Before, probes were tried in order, repeating on the + same probe as long it returned PROBE_AGAIN before + moving to the next one. This means a probe which + requires a lot of data (i.e. returne PROBE_AGAIN for + a long time) could prevent sucessful matches from + subsequent probes. The configuration file needed to + take that into account. + + Now, all probes are tried each time new data is + found. If any probe matches, use it. If at least one + probe requires more data, wait for more. If all + probes failed, connect to the last one. So the only + thing to know when writing the configuration file is + that 'anyprot' needs to be last. + + Test suite heavily refactored; `t` uses `test.cfg` + to decide which probes to test and all setup is + automatic; probes get tested with 'fast' (entire + first message in one packet) and 'slow' (one byte at + a time); when SNI/ALPN are defined, all combinations + are tested. + + Old 'tls' probe removed, 'sni_alpn' probe renamed as 'tls'. + You'll need to change 'sni_alpn' to 'tls' in + your configuration file, if ever you used it. + +v1.19: 20JAN2018 + Added 'syslog_facility' configuration option to + specify where to log. + + TLS now supports SNI and ALPN (Travis Burtrum), + including support for Let's Encrypt challenges + (Jonathan McCrohan) + + ADB probe. (Mike Frysinger) + + Added per-protocol 'fork' option. (Oleg Oshmyan) + + Added chroot option. (Mike Frysinger) + + A truckload of bug fixes and documentation + improvements (Various contributors) + v1.18: 29MAR2016 Added USELIBPCRE to make use of regex engine optional. diff -Nru sslh-1.18/common.c sslh-1.20/common.c --- sslh-1.18/common.c 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/common.c 2018-11-20 21:58:41.000000000 +0000 @@ -4,10 +4,16 @@ * No code here should assume whether sockets are blocking or not. **/ +#define SYSLOG_NAMES #define _GNU_SOURCE +#include #include #include +#include +#include +#include + #include "common.h" #include "probe.h" @@ -35,7 +41,7 @@ int background = 0; int transparent = 0; int numeric = 0; -const char *user_name, *pid_file; +const char *user_name, *pid_file, *chroot_path, *facility = "auth"; struct addrinfo *addr_listen = NULL; /* what addresses do we listen to? */ @@ -44,8 +50,13 @@ int allow_severity =0, deny_severity = 0; #endif +typedef enum { + CR_DIE, + CR_WARN +} CR_ACTION; + /* check result and die, printing the offending address and error */ -void check_res_dumpdie(int res, struct addrinfo *addr, char* syscall) +void check_res_dump(CR_ACTION act, int res, struct addrinfo *addr, char* syscall) { char buf[NI_MAXHOST]; @@ -54,7 +65,9 @@ sprintaddr(buf, sizeof(buf), addr), syscall, strerror(errno)); - exit(1); + + if (act == CR_DIE) + exit(1); } } @@ -69,8 +82,10 @@ exit(1); } if (sd > 0) { + int i; *sockfd = malloc(sd * sizeof(*sockfd[0])); - for (int i = 0; i < sd; i++) { + CHECK_ALLOC(*sockfd, "malloc"); + for (i = 0; i < sd; i++) { (*sockfd)[i] = SD_LISTEN_FDS_START + i; } } @@ -102,10 +117,16 @@ for (addr = addr_list; addr; addr = addr->ai_next) num_addr++; + if (num_addr == 0) { + fprintf(stderr, "FATAL: No available addresses.\n"); + exit(1); + } + if (verbose) fprintf(stderr, "listening to %d addresses\n", num_addr); *sockfd = malloc(num_addr * sizeof(*sockfd[0])); + CHECK_ALLOC(*sockfd, "malloc"); for (i = 0, addr = addr_list; i < num_addr && addr; i++, addr = addr->ai_next) { if (!addr) { @@ -115,34 +136,80 @@ saddr = (struct sockaddr_storage*)addr->ai_addr; (*sockfd)[i] = socket(saddr->ss_family, SOCK_STREAM, 0); - check_res_dumpdie((*sockfd)[i], addr, "socket"); + check_res_dump(CR_DIE, (*sockfd)[i], addr, "socket"); one = 1; res = setsockopt((*sockfd)[i], SOL_SOCKET, SO_REUSEADDR, (char*)&one, sizeof(one)); - check_res_dumpdie(res, addr, "setsockopt(SO_REUSEADDR)"); + check_res_dump(CR_DIE, res, addr, "setsockopt(SO_REUSEADDR)"); if (addr->ai_flags & SO_KEEPALIVE) { res = setsockopt((*sockfd)[i], SOL_SOCKET, SO_KEEPALIVE, (char*)&one, sizeof(one)); - check_res_dumpdie(res, addr, "setsockopt(SO_KEEPALIVE)"); - printf("set up keepalive\n"); + check_res_dump(CR_DIE, res, addr, "setsockopt(SO_KEEPALIVE)"); } if (IP_FREEBIND) { res = setsockopt((*sockfd)[i], IPPROTO_IP, IP_FREEBIND, (char*)&one, sizeof(one)); - check_res_dumpdie(res, addr, "setsockopt(IP_FREEBIND)"); + check_res_dump(CR_WARN, res, addr, "setsockopt(IP_FREEBIND)"); + } + + if (addr->ai_addr->sa_family == AF_INET6) { + res = setsockopt((*sockfd)[i], IPPROTO_IPV6, IPV6_V6ONLY, (char*)&one, sizeof(one)); + check_res_dump(CR_WARN, res, addr, "setsockopt(IPV6_V6ONLY)"); } res = bind((*sockfd)[i], addr->ai_addr, addr->ai_addrlen); - check_res_dumpdie(res, addr, "bind"); + check_res_dump(CR_DIE, res, addr, "bind"); res = listen ((*sockfd)[i], 50); - check_res_dumpdie(res, addr, "listen"); + check_res_dump(CR_DIE, res, addr, "listen"); } return num_addr; } + +/* returns 1 if given address is on the local machine: iterate through all + * network interfaces and check their addresses */ +int is_same_machine(struct addrinfo* from) +{ + struct ifaddrs *ifaddrs_p = NULL, *ifa; + int match = 0; + + getifaddrs(&ifaddrs_p); + + for (ifa = ifaddrs_p; ifa != NULL; ifa = ifa->ifa_next) + { + if (!ifa->ifa_addr) + continue; + if (from->ai_addr->sa_family == ifa->ifa_addr->sa_family) + { + int family = ifa->ifa_addr->sa_family; + if (family == AF_INET) + { + struct sockaddr_in *from_addr = (struct sockaddr_in*)from->ai_addr; + struct sockaddr_in *ifa_addr = (struct sockaddr_in*)ifa->ifa_addr; + if (from_addr->sin_addr.s_addr == ifa_addr->sin_addr.s_addr) { + match = 1; + break; + } + } + else if (family == AF_INET6) + { + struct sockaddr_in6 *from_addr = (struct sockaddr_in6*)from->ai_addr; + struct sockaddr_in6 *ifa_addr = (struct sockaddr_in6*)ifa->ifa_addr; + if (!memcmp(from_addr->sin6_addr.s6_addr, ifa_addr->sin6_addr.s6_addr, 16)) { + match = 1; + break; + } + } + } + } + freeifaddrs(ifaddrs_p); + return match; +} + + /* Transparent proxying: bind the peer address of fd to the peer address of * fd_from */ #define IP_TRANSPARENT 19 @@ -160,6 +227,11 @@ * got here */ res = getpeername(fd_from, from.ai_addr, &from.ai_addrlen); CHECK_RES_RETURN(res, "getpeername"); + + /* if the destination is the same machine, there's no need to do bind */ + if (is_same_machine(&from)) + return 0; + #ifndef IP_BINDANY /* use IP_TRANSPARENT */ res = setsockopt(fd, IPPROTO_IP, IP_TRANSPARENT, &trans, sizeof(trans)); CHECK_RES_DIE(res, "setsockopt"); @@ -227,7 +299,6 @@ one = 1; res = setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, (char*)&one, sizeof(one)); CHECK_RES_RETURN(res, "setsockopt(SO_KEEPALIVE)"); - printf("set up keepalive\n"); } return fd; } @@ -240,17 +311,16 @@ int defer_write(struct queue *q, void* data, int data_size) { char *p; + ptrdiff_t data_offset = q->deferred_data - q->begin_deferred_data; if (verbose) fprintf(stderr, "**** writing deferred on fd %d\n", q->fd); - p = realloc(q->begin_deferred_data, q->deferred_data_size + data_size); - if (!p) { - perror("realloc"); - exit(1); - } + p = realloc(q->begin_deferred_data, data_offset + q->deferred_data_size + data_size); + CHECK_ALLOC(p, "realloc"); - q->deferred_data = q->begin_deferred_data = p; - p += q->deferred_data_size; + q->begin_deferred_data = p; + q->deferred_data = p + data_offset; + p += data_offset + q->deferred_data_size; q->deferred_data_size += data_size; memcpy(p, data, data_size); @@ -398,19 +468,43 @@ /* Turns a hostname and port (or service) into a list of struct addrinfo * returns 0 on success, -1 otherwise and logs error - **/ -int resolve_split_name(struct addrinfo **out, const char* host, const char* serv) + */ +int resolve_split_name(struct addrinfo **out, const char* ct_host, const char* serv) { struct addrinfo hint; + char *end; int res; + char* host, *host_base; memset(&hint, 0, sizeof(hint)); hint.ai_family = PF_UNSPEC; hint.ai_socktype = SOCK_STREAM; + /* Copy parameter so not to clobber data in libconfig */ + res = asprintf(&host_base, "%s", ct_host); + if (res == -1) { + log_message(LOG_ERR, "asprintf: cannot allocate memory"); + return -1; + } + + host = host_base; + + /* If it is a RFC-Compliant IPv6 address ("[1234::12]:443"), remove brackets + * around IP address */ + if (host[0] == '[') { + end = strrchr(host, ']'); + if (!end) { + fprintf(stderr, "%s: no closing bracket in IPv6 address?\n", host); + return -1; + } + host++; /* skip first bracket */ + *end = 0; /* remove last bracket */ + } + res = getaddrinfo(host, serv, &hint, out); if (res) log_message(LOG_ERR, "%s `%s:%s'\n", gai_strerror(res), host, serv); + free(host_base); return res; } @@ -420,7 +514,7 @@ */ void resolve_name(struct addrinfo **out, char* fullname) { - char *serv, *host, *end; + char *serv, *host; int res; /* Find port */ @@ -434,17 +528,6 @@ host = fullname; - /* If it is a RFC-Compliant IPv6 address ("[1234::12]:443"), remove brackets - * around IP address */ - if (host[0] == '[') { - end = strrchr(host, ']'); - if (!end) { - fprintf(stderr, "%s: no closing bracket in IPv6 address?\n", host); - } - host++; /* skip first bracket */ - *end = 0; /* remove last bracket */ - } - res = resolve_split_name(out, host, serv); if (res) { fprintf(stderr, "%s `%s'\n", gai_strerror(res), fullname); @@ -591,12 +674,21 @@ * banner is made up of basename(bin_name)+"[pid]" */ void setup_syslog(const char* bin_name) { char *name1, *name2; - int res; + int res, fn; name1 = strdup(bin_name); res = asprintf(&name2, "%s[%d]", basename(name1), getpid()); CHECK_RES_DIE(res, "asprintf"); - openlog(name2, LOG_CONS, LOG_AUTH); + + for (fn = 0; facilitynames[fn].c_val != -1; fn++) + if (strcmp(facilitynames[fn].c_name, facility) == 0) + break; + if (facilitynames[fn].c_val == -1) { + fprintf(stderr, "Unknown facility %s\n", facility); + exit(1); + } + + openlog(name2, LOG_CONS, facilitynames[fn].c_val); free(name1); /* Don't free name2, as openlog(3) uses it (at least in glibc) */ @@ -654,33 +746,47 @@ } /* We don't want to run as root -- drop privileges if required */ -void drop_privileges(const char* user_name) +void drop_privileges(const char* user_name, const char* chroot_path) { int res; - struct passwd *pw = getpwnam(user_name); - if (!pw) { - fprintf(stderr, "%s: not found\n", user_name); - exit(2); + struct passwd *pw = NULL; + + if (user_name) { + pw = getpwnam(user_name); + if (!pw) { + fprintf(stderr, "%s: not found\n", user_name); + exit(2); + } + if (verbose) + fprintf(stderr, "turning into %s\n", user_name); } - if (verbose) - fprintf(stderr, "turning into %s\n", user_name); - set_keepcaps(1); + if (chroot_path) { + if (verbose) + fprintf(stderr, "chrooting into %s\n", chroot_path); + + res = chroot(chroot_path); + CHECK_RES_DIE(res, "chroot"); + } - /* remove extraneous groups in case we belong to several extra groups that - * may have unwanted rights. If non-root when calling setgroups(), it - * fails, which is fine because... we have no unwanted rights - * (see POS36-C for security context) - * */ - setgroups(0, NULL); - - res = setgid(pw->pw_gid); - CHECK_RES_DIE(res, "setgid"); - res = setuid(pw->pw_uid); - CHECK_RES_DIE(res, "setuid"); + if (user_name) { + set_keepcaps(1); - set_capabilities(); - set_keepcaps(0); + /* remove extraneous groups in case we belong to several extra groups + * that may have unwanted rights. If non-root when calling setgroups(), + * it fails, which is fine because... we have no unwanted rights + * (see POS36-C for security context) + * */ + setgroups(0, NULL); + + res = setgid(pw->pw_gid); + CHECK_RES_DIE(res, "setgid"); + res = setuid(pw->pw_uid); + CHECK_RES_DIE(res, "setuid"); + + set_capabilities(); + set_keepcaps(0); + } } /* Writes my PID */ diff -Nru sslh-1.18/common.h sslh-1.20/common.h --- sslh-1.18/common.h 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/common.h 2018-11-20 21:58:41.000000000 +0000 @@ -37,16 +37,24 @@ #define CHECK_RES_DIE(res, str) \ if (res == -1) { \ + fprintf(stderr, "%s:%d:", __FILE__, __LINE__); \ perror(str); \ exit(1); \ } #define CHECK_RES_RETURN(res, str) \ if (res == -1) { \ - log_message(LOG_CRIT, "%s:%d:%s\n", str, errno, strerror(errno)); \ + log_message(LOG_CRIT, "%s:%d:%s:%d:%s\n", __FILE__, __LINE__, str, errno, strerror(errno)); \ return res; \ } +#define CHECK_ALLOC(a, str) \ + if (!a) { \ + fprintf(stderr, "%s:%d:", __FILE__, __LINE__); \ + perror(str); \ + exit(1); \ + } + #define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0])) #if 1 @@ -102,7 +110,7 @@ int check_access_rights(int in_socket, const char* service); void setup_signals(void); void setup_syslog(const char* bin_name); -void drop_privileges(const char* user_name); +void drop_privileges(const char* user_name, const char* chroot_path); void write_pid_file(const char* pidfile); void log_message(int type, char* msg, ...); void dump_connection(struct connection *cnx); @@ -118,7 +126,7 @@ extern struct sockaddr_storage addr_ssl, addr_ssh, addr_openvpn; extern struct addrinfo *addr_listen; extern const char* USAGE_STRING; -extern const char* user_name, *pid_file; +extern const char* user_name, *pid_file, *chroot_path, *facility; extern const char* server_type; /* sslh-fork.c */ diff -Nru sslh-1.18/debian/changelog sslh-1.20/debian/changelog --- sslh-1.18/debian/changelog 2016-05-17 19:20:19.000000000 +0000 +++ sslh-1.20/debian/changelog 2019-09-05 17:04:54.000000000 +0000 @@ -1,3 +1,21 @@ +sslh (1.20-1) unstable; urgency=medium + + [ Guillaume Delacour ] + * Remove debian/patches/transparent_mode_local.diff (merged upstream) + * Replace extra priority by optional + * Use debhelper 12 to avoid build depends on dh-systemd + * Update obsolete homepage (Closes: #889575) + + [ Don Armstrong ] + * New upstream release (Closes: #889576) + * Refresh static port patch and remove localhost connection patch (for + testing only) + * Add patch which avoids requiring Conf::Libconfig, use dynamic ports + * Bump to Standards-Version 4.4.0; primary change is + --no-enable/--no-start instead of RUN + + -- Don Armstrong Thu, 05 Sep 2019 10:04:54 -0700 + sslh (1.18-1) unstable; urgency=medium * New upstream release, include transparent mode local patch from upstream diff -Nru sslh-1.18/debian/compat sslh-1.20/debian/compat --- sslh-1.18/debian/compat 2016-04-02 08:04:27.000000000 +0000 +++ sslh-1.20/debian/compat 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -9 diff -Nru sslh-1.18/debian/control sslh-1.20/debian/control --- sslh-1.18/debian/control 2016-05-17 19:03:05.000000000 +0000 +++ sslh-1.20/debian/control 2019-09-05 17:04:54.000000000 +0000 @@ -1,14 +1,15 @@ Source: sslh Section: net -Priority: extra -Maintainer: Guillaume Delacour -Build-Depends: debhelper (>= 9.0.0), libwrap0-dev, binutils, po-debconf, +Priority: optional +Maintainer: Don Armstrong +Build-Depends: debhelper-compat (= 12), libwrap0-dev, binutils, po-debconf, libio-socket-inet6-perl, libconfig-dev, libcap-dev [linux-any], - psmisc, lcov, dh-systemd (>= 1.5) -Standards-Version: 3.9.8 -Homepage: http://www.rutschle.net/tech/sslh.shtml -Vcs-Browser: https://anonscm.debian.org/cgit/collab-maint/sslh.git -Vcs-Git: https://anonscm.debian.org/git/collab-maint/sslh.git + psmisc, lcov, libpcre3-dev, + init-system-helpers (>= 1.51) +Standards-Version: 4.4.0 +Homepage: http://www.rutschle.net/tech/sslh/README.html +Vcs-Browser: https://salsa.debian.org/debian/sslh.git +Vcs-Git: https://salsa.debian.org/debian/sslh.git Package: sslh Architecture: any diff -Nru sslh-1.18/debian/default sslh-1.20/debian/default --- sslh-1.18/debian/default 2016-04-02 08:04:27.000000000 +0000 +++ sslh-1.20/debian/default 2019-09-05 17:04:54.000000000 +0000 @@ -1,16 +1,6 @@ # Default options for sslh initscript # sourced by /etc/init.d/sslh -# Disabled by default, to force yourself -# to read the configuration: -# - /usr/share/doc/sslh/README.Debian (quick start) -# - /usr/share/doc/sslh/README, at "Configuration" section -# - sslh(8) via "man sslh" for more configuration details. -# Once configuration ready, you *must* set RUN to yes here -# and try to start sslh (standalone mode only) - -RUN=no - # binary to use: forked (sslh) or single-thread (sslh-select) version # systemd users: don't forget to modify /lib/systemd/system/sslh.service DAEMON=/usr/sbin/sslh diff -Nru sslh-1.18/debian/init sslh-1.20/debian/init --- sslh-1.18/debian/init 2016-04-02 08:04:27.000000000 +0000 +++ sslh-1.20/debian/init 2019-09-05 17:04:54.000000000 +0000 @@ -24,6 +24,7 @@ DAEMON_OPTS="" PIDFILE=/var/run/sslh/$NAME.pid SCRIPTNAME=/etc/init.d/$NAME +RUN=yes # Read configuration variable file if it is present [ -r /etc/default/$NAME ] && . /etc/default/$NAME @@ -48,16 +49,16 @@ # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started - - # Use this if you want the user to explicitly set 'RUN' in - # /etc/default/ - if [ "$RUN" != "yes" ] - then - echo "$NAME disabled, please adjust the configuration to your needs " - log_failure_msg "and then set RUN to 'yes' in /etc/default/$NAME to enable it." - return 2 - fi - + + # Use this if you want the user to explicitly set 'RUN' in + # /etc/default/ + if [ "$RUN" != "yes" ] + then + echo "$NAME disabled, please adjust the configuration to your needs " + log_failure_msg "and then set RUN to 'yes' in /etc/default/$NAME to enable it." + return 2 + fi + # sslh write the pid as sslh user if [ ! -d /var/run/sslh/ ] then diff -Nru sslh-1.18/debian/patches/disable_valgrind_launch.diff sslh-1.20/debian/patches/disable_valgrind_launch.diff --- sslh-1.18/debian/patches/disable_valgrind_launch.diff 2016-04-02 08:06:58.000000000 +0000 +++ sslh-1.20/debian/patches/disable_valgrind_launch.diff 1970-01-01 00:00:00.000000000 +0000 @@ -1,18 +0,0 @@ -From: Guillaume Delacour -Subject: Don't call valgrind in tests -Last-Update: 2014-07-15 - -Index: sslh/t -=================================================================== ---- sslh.orig/t -+++ sslh/t -@@ -58,8 +58,7 @@ for my $binary (@binaries) { - 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"; - warn "$cmd\n"; -- #exec $cmd; -- exec "valgrind --leak-check=full ./$binary -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address -ssl $ssl_address -P $pidfile"; -+ exec $cmd; - exit 0; - } - warn "spawned $sslh_pid\n"; diff -Nru sslh-1.18/debian/patches/fix-ftbfs-static-port.patch sslh-1.20/debian/patches/fix-ftbfs-static-port.patch --- sslh-1.18/debian/patches/fix-ftbfs-static-port.patch 1970-01-01 00:00:00.000000000 +0000 +++ sslh-1.20/debian/patches/fix-ftbfs-static-port.patch 2019-09-05 17:04:54.000000000 +0000 @@ -0,0 +1,32 @@ +--- a/t ++++ b/t +@@ -8,6 +8,7 @@ + + use strict; + use IO::Socket::INET6; ++use IO::Socket::INET; + use Test::More qw/no_plan/; + use Conf::Libconfig; + +@@ -15,9 +16,19 @@ + $conf->read_file("test.cfg"); + + +-my $no_listen = 8083; # Port on which no-one listens ++sub get_unused_port { ++ my $sock = IO::Socket::INET->new( ++ Listen => 1, ++ LocalAddr => 'localhost', ++ ReuseAddr => 1, ++ ); ++ my $port = $sock->sockport(); ++ $sock->shutdown(2); ++ return $port; ++} ++my $no_listen = get_unused_port(); # Port on which no-one listens + my $pidfile = $conf->lookup_value("pidfile"); +-my $sslh_port = $conf->fetch_array("listen")->[0]->{port}; ++my $sslh_port = get_unused_port(); + my $user = (getpwuid $<)[0]; # Run under current username + + # Which tests do we run diff -Nru sslh-1.18/debian/patches/fix_sslh_compiler_warnings sslh-1.20/debian/patches/fix_sslh_compiler_warnings --- sslh-1.18/debian/patches/fix_sslh_compiler_warnings 1970-01-01 00:00:00.000000000 +0000 +++ sslh-1.20/debian/patches/fix_sslh_compiler_warnings 2019-09-05 17:04:54.000000000 +0000 @@ -0,0 +1,14 @@ +Description: Silence compiler warnings because ssl_err_msg isn't a format string +Author: Don Armstrong +Forwarded: no +--- a/sslh-main.c ++++ b/sslh-main.c +@@ -167,7 +167,7 @@ + strcpy(argv[i], "--tls"); + /* foreground option not parsed yet, syslog not open, just print on + * stderr and hope for the best */ +- fprintf(stderr, ssl_err_msg); ++ fprintf(stderr, "%s",ssl_err_msg); + } + } + } diff -Nru sslh-1.18/debian/patches/ftbfs_localhost.diff sslh-1.20/debian/patches/ftbfs_localhost.diff --- sslh-1.18/debian/patches/ftbfs_localhost.diff 2016-04-02 08:04:27.000000000 +0000 +++ sslh-1.20/debian/patches/ftbfs_localhost.diff 1970-01-01 00:00:00.000000000 +0000 @@ -1,17 +0,0 @@ -From: Guillaume Delacour -Subject: Fix FTBFS if ip6-localhost don't resolv to ::1 -Last-Update: 2012-08-26 - ---- a/t -+++ b/t -@@ -8,8 +8,8 @@ - - # We use ports 9000, 9001 and 9002 -- hope that won't clash - # with anything... --my $ssh_address = "ip6-localhost:9000"; --my $ssl_address = "ip6-localhost:9001"; -+my $ssh_address = "::1:9000"; -+my $ssl_address = "::1:9001"; - my $sslh_port = 9002; - my $no_listen = 9003; # Port on which no-one listens - my $pidfile = "/tmp/sslh_test.pid"; diff -Nru sslh-1.18/debian/patches/log_message_const sslh-1.20/debian/patches/log_message_const --- sslh-1.18/debian/patches/log_message_const 1970-01-01 00:00:00.000000000 +0000 +++ sslh-1.20/debian/patches/log_message_const 2019-09-05 17:04:54.000000000 +0000 @@ -0,0 +1,25 @@ +Description: log_message should be const char*, not just char* +Author: Don Armstrong +Forwarded: no +--- a/common.c ++++ b/common.c +@@ -538,7 +538,7 @@ + } + + /* Log to syslog or stderr if foreground */ +-void log_message(int type, char* msg, ...) ++void log_message(int type, const char* msg, ...) + { + va_list ap; + +--- a/common.h ++++ b/common.h +@@ -112,7 +112,7 @@ + void setup_syslog(const char* bin_name); + void drop_privileges(const char* user_name, const char* chroot_path); + void write_pid_file(const char* pidfile); +-void log_message(int type, char* msg, ...); ++void log_message(int type, const char* msg, ...); + void dump_connection(struct connection *cnx); + int resolve_split_name(struct addrinfo **out, const char* hostname, const char* port); + diff -Nru sslh-1.18/debian/patches/series sslh-1.20/debian/patches/series --- sslh-1.18/debian/patches/series 2016-05-16 14:04:27.000000000 +0000 +++ sslh-1.20/debian/patches/series 2019-09-05 17:04:54.000000000 +0000 @@ -1,4 +1,5 @@ +log_message_const +fix_sslh_compiler_warnings fixed_version.diff -disable_valgrind_launch.diff -ftbfs_localhost.diff -transparent_mode_local.diff +fix-ftbfs-static-port.patch +t_no_libconfig diff -Nru sslh-1.18/debian/patches/t_no_libconfig sslh-1.20/debian/patches/t_no_libconfig --- sslh-1.18/debian/patches/t_no_libconfig 1970-01-01 00:00:00.000000000 +0000 +++ sslh-1.20/debian/patches/t_no_libconfig 2019-09-05 17:04:54.000000000 +0000 @@ -0,0 +1,138 @@ +--- a/t ++++ b/t +@@ -10,11 +10,10 @@ + use IO::Socket::INET6; + use IO::Socket::INET; + use Test::More qw/no_plan/; +-use Conf::Libconfig; +- +-my $conf = new Conf::Libconfig; +-$conf->read_file("test.cfg"); +- ++use File::Temp qw(tempdir); ++# Because nothing else in Debian uses Conf::Libconfig, and I ++# (don@debian.org) don't want to package it, we have hard coded ++# test.cfg + + sub get_unused_port { + my $sock = IO::Socket::INET->new( +@@ -26,8 +25,26 @@ + $sock->shutdown(2); + return $port; + } ++ ++my $conf = ++ {protocols => ++ [{ name => "ssh", host => "localhost", fork => 1}, ++ { name => "socks5", host => "localhost", }, ++ { name => "http", host => "localhost", , }, ++ { name => "tinc", host => "localhost", , }, ++ { name => "openvpn", host => "localhost",, }, ++ { name => "xmpp", host => "localhost", }, ++ { name => "adb", host => "localhost",, }, ++ { name => "tls", host => "localhost", , alpn_protocols => [ "alpn1", "alpn2" ], sni_hostnames => [ "sni1" ], }, ++ { name => "ssl", host => "localhost", alpn_protocols => [ "alpn1", "alpn2" ], sni_hostnames => [ "sni2", "sni3" ], }, ++ { name => "tls", host => "localhost", alpn_protocols => [ "alpn3" ], }, ++ { name => "tls", host => "localhost", sni_hostnames => [ "sni3" ], }, ++ { name => "ssl", host => "localhost", }, ++ { name => "anyprot", host => "localhost", }], ++ }; ++ + my $no_listen = get_unused_port(); # Port on which no-one listens +-my $pidfile = $conf->lookup_value("pidfile"); ++my $pidfile = tempdir(CLEANUP=>1).'/sslh.pid'; + my $sslh_port = get_unused_port(); + my $user = (getpwuid $<)[0]; # Run under current username + +@@ -117,7 +134,7 @@ + sub test_probes { + my (%opts) = @_; + +- my @probes = @{$conf->fetch_array("protocols")}; ++ my @probes = @{$conf->{"protocols"}}; + foreach my $p (@probes) { + my %protocols = ( + 'ssh' => { data => "SSH-2.0 tester" }, +@@ -185,11 +202,13 @@ + + + # Start an echoserver for each service +-foreach my $s (@{$conf->fetch_array("protocols")}) { ++foreach my $s (@{$conf->{"protocols"}}) { + my $prefix = $s->{name}; + + $prefix =~ s/^ssl/tls/; # To remove in 1.21 + ++ $s->{port} = get_unused_port(); ++ + if ($s->{sni_hostnames} or $s->{alpn_protocols}) { + $prefix = make_sni_alpn_name($s); + } +@@ -197,6 +216,53 @@ + verbose_exec "./echosrv --listen $s->{host}:$s->{port} --prefix '$prefix: '"; + } + ++# Write out test.cfg with new unused ports ++open(my $test_cfg,'>test.cfg') or ++ die "Unable to open test.cfg for writing"; ++print {$test_cfg} <<"EOF"; ++# Configuration file for testing (use both by sslh under ++# test and the test script `t`) ++ ++verbose: 2; ++foreground: true; ++inetd: false; ++numeric: false; ++transparent: false; ++timeout: 10; # Probe test writes slowly ++pidfile: "$pidfile"; ++ ++syslog_facility: "auth"; ++ ++ ++# List of interfaces on which we should listen ++# Options: ++listen: ++( ++ { host: "localhost"; port: "$sslh_port"; keepalive: true; }, ++ { host: "localhost"; port: "8081"; keepalive: true; } ++); ++ ++ ++protocols: ++( ++ { name: "ssh"; host: "localhost"; port: "$conf->{protocols}[0]{port}"; fork: true; }, ++ { name: "socks5"; host: "localhost"; port: "$conf->{protocols}[1]{port}"; }, ++ { name: "http"; host: "localhost"; port: "$conf->{protocols}[2]{port}"; }, ++ { name: "tinc"; host: "localhost"; port: "$conf->{protocols}[3]{port}"; }, ++ { name: "openvpn"; host: "localhost"; port: "$conf->{protocols}[4]{port}"; }, ++ { name: "xmpp"; host: "localhost"; port: "$conf->{protocols}[5]{port}"; }, ++ { name: "adb"; host: "localhost"; port: "$conf->{protocols}[6]{port}"; }, ++ { name: "tls"; host: "localhost"; port: "$conf->{protocols}[7]{port}"; alpn_protocols: [ "alpn1", "alpn2" ]; sni_hostnames: [ "sni1" ]; }, ++ { name: "ssl"; host: "localhost"; port: "$conf->{protocols}[8]{port}"; alpn_protocols: [ "alpn1", "alpn2" ]; sni_hostnames: [ "sni2", "sni3" ]; }, ++ { name: "tls"; host: "localhost"; port: "$conf->{protocols}[9]{port}"; alpn_protocols: [ "alpn3" ]; }, ++ { name: "tls"; host: "localhost"; port: "$conf->{protocols}[10]{port}"; sni_hostnames: [ "sni3" ]; }, ++ { name: "ssl"; host: "localhost"; port: "$conf->{protocols}[11]{port}"; }, ++ { name: "anyprot"; host: "localhost"; port: "$conf->{protocols}[12]{port}"; } ++); ++ ++on-timeout: "ssh"; ++EOF ++close ($test_cfg); + + my @binaries = ('sslh-select', 'sslh-fork'); + for my $binary (@binaries) { +@@ -314,11 +380,11 @@ + } + + +-my $ssh_conf = (grep { $_->{name} eq "ssh" } @{$conf->fetch_array("protocols")})[0]; ++my $ssh_conf = (grep { $_->{name} eq "ssh" } @{$conf->{"protocols"}})[0]; + my $ssh_address = $ssh_conf->{host} . ":" . $ssh_conf->{port}; + + # Use the last TLS echoserv (no SNI/ALPN) +-my $ssl_conf = (grep { $_->{name} eq "tls" } @{$conf->fetch_array("protocols")})[-1]; ++my $ssl_conf = (grep { $_->{name} eq "tls" } @{$conf->{"protocols"}})[-1]; + my $ssl_address = $ssl_conf->{host} . ":" . $ssl_conf->{port}; + + diff -Nru sslh-1.18/debian/patches/transparent_mode_local.diff sslh-1.20/debian/patches/transparent_mode_local.diff --- sslh-1.18/debian/patches/transparent_mode_local.diff 2016-05-16 14:05:12.000000000 +0000 +++ sslh-1.20/debian/patches/transparent_mode_local.diff 1970-01-01 00:00:00.000000000 +0000 @@ -1,68 +0,0 @@ -From: ViKing -Date: Wed, 7 Oct 2015 00:09:33 +0800 -Subject: [PATCH] Fix the connection problem in transparent mode. - -When the source and destination are the same, the bind_peer() will -fail, thus end the connection. Therefore a check of all the interface -IPs are checked to skip bind() if they are the same. -Bug-Debian: https://bugs.debian.org/823229 -Origin: upstream, https://github.com/yrutschle/sslh/pull/69 ---- - common.c | 37 +++++++++++++++++++++++++++++++++++++ - 1 file changed, 37 insertions(+) - -Index: sslh/common.c -=================================================================== ---- sslh.orig/common.c -+++ sslh/common.c -@@ -8,6 +8,10 @@ - #include - #include - -+#include -+#include -+#include -+ - #include "common.h" - #include "probe.h" - -@@ -160,6 +164,39 @@ int bind_peer(int fd, int fd_from) - * got here */ - res = getpeername(fd_from, from.ai_addr, &from.ai_addrlen); - CHECK_RES_RETURN(res, "getpeername"); -+ -+ // if the destination is the same machine, there's no need to do bind -+ struct ifaddrs *ifaddrs_p = NULL, *ifa; -+ -+ getifaddrs(&ifaddrs_p); -+ -+ for (ifa = ifaddrs_p; ifa != NULL; ifa = ifa->ifa_next) -+ { -+ if (!ifa->ifa_addr) -+ continue; -+ int match = 0; -+ if (from.ai_addr->sa_family == ifa->ifa_addr->sa_family) -+ { -+ int family = ifa->ifa_addr->sa_family; -+ if (family == AF_INET) -+ { -+ struct sockaddr_in *from_addr = from.ai_addr; -+ struct sockaddr_in *ifa_addr = ifa->ifa_addr; -+ if (from_addr->sin_addr.s_addr == ifa_addr->sin_addr.s_addr) -+ match = 1; -+ } -+ else if (family == AF_INET6) -+ { -+ struct sockaddr_in6 *from_addr = from.ai_addr; -+ struct sockaddr_in6 *ifa_addr = ifa->ifa_addr; -+ if (!memcmp(from_addr->sin6_addr.s6_addr, ifa_addr->sin6_addr.s6_addr, 16)) -+ match = 1; -+ } -+ } -+ if (match) // the destination is the same as the source, should not create a transparent bind -+ return 0; -+ } -+ - #ifndef IP_BINDANY /* use IP_TRANSPARENT */ - res = setsockopt(fd, IPPROTO_IP, IP_TRANSPARENT, &trans, sizeof(trans)); - CHECK_RES_DIE(res, "setsockopt"); diff -Nru sslh-1.18/debian/rules sslh-1.20/debian/rules --- sslh-1.18/debian/rules 2016-04-02 08:19:06.000000000 +0000 +++ sslh-1.20/debian/rules 2019-09-05 17:04:54.000000000 +0000 @@ -20,7 +20,7 @@ # do not start daemon by default: force user to configure override_dh_installinit: - dh_installinit --no-start + dh_installinit --no-start --no-enable %: - dh $@ --with systemd + dh $@ diff -Nru sslh-1.18/debian/watch sslh-1.20/debian/watch --- sslh-1.18/debian/watch 2016-04-02 08:04:27.000000000 +0000 +++ sslh-1.20/debian/watch 2019-09-05 16:58:56.000000000 +0000 @@ -1,4 +1,3 @@ version=3 -opts=uversionmangle=s/_/./g;s/^(\S*\.cyg-(?:i686|x86.64)-libconfig)$/0.0.$1/ \ -http://www.rutschle.net/tech/sslh.shtml \ -(?:|.*/)sslh(?:[_\-]v?|)(\d\S*)\.(?:tar\.xz|txz|tar\.bz2|tbz2|tar\.gz|tgz) +opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/sslh-$1\.tar\.gz/ \ + https://github.com/yrutschle/sslh/tags .*/v?(\d\S+)\.tar\.gz diff -Nru sslh-1.18/example.cfg sslh-1.20/example.cfg --- sslh-1.18/example.cfg 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/example.cfg 2018-11-20 21:58:41.000000000 +0000 @@ -11,7 +11,13 @@ timeout: 2; user: "nobody"; pidfile: "/var/run/sslh.pid"; +chroot: "/var/empty"; +# Specify which syslog facility to use (names for your +# system are usually defined in /usr/include/*/sys/syslog.h +# or equivalent) +# Default is "auth" +syslog_facility: "auth"; # List of interfaces on which we should listen # Options: @@ -33,27 +39,34 @@ # 1 to log each incoming connection # keepalive: Should TCP keepalive be on or off for that # connection (default is off) +# fork: Should a new process be forked for this protocol? +# (only useful for sslh-select) # # Probe-specific options: +# (sslh will try each probe in order they are declared, and +# connect to the first that matches.) +# # tls: # sni_hostnames: list of FQDN for that target # alpn_protocols: list of ALPN protocols for that target, see: # https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids # # if both sni_hostnames AND alpn_protocols are specified, both must match +# # if neither are set, it is just checked whether this is the TLS protocol or not +# +# Obviously set the most specific probes +# first, and if you use TLS with no ALPN/SNI +# set it as the last TLS probe # regex: # regex_patterns: list of patterns to match for # that target. # -# sslh will try each probe in order they are declared, and -# connect to the first that matches. -# # You can specify several of 'regex' and 'tls'. protocols: ( - { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; keepalive: true; }, + { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; keepalive: true; fork: true; }, { name: "http"; host: "localhost"; port: "80"; }, # match BOTH ALPN/SNI @@ -67,14 +80,18 @@ { name: "tls"; host: "localhost"; port: "993"; sni_hostnames: [ "mail.rutschle.net", "mail.englishintoulouse.com" ]; log_level: 0; }, { name: "tls"; host: "localhost"; port: "xmpp-client"; sni_hostnames: [ "im.rutschle.net", "im.englishintoulouse.com" ]; log_level: 0;}, +# Let's Encrypt (tls-sni-* challenges) + { name: "tls"; host: "localhost"; port: "letsencrypt-client"; sni_hostnames: [ "*.*.acme.invalid" ]; log_level: 0;}, + # catch anything else TLS { name: "tls"; host: "localhost"; port: "443"; }, +# Regex examples -- better use the built-in probes for real-world use! # OpenVPN { name: "regex"; host: "localhost"; port: "1194"; regex_patterns: [ "^\x00[\x0D-\xFF]$", "^\x00[\x0D-\xFF]\x38" ]; }, # Jabber { name: "regex"; host: "localhost"; port: "5222"; regex_patterns: [ "jabber" ]; }, - + # Catch-all { name: "regex"; host: "localhost"; port: "443"; regex_patterns: [ "" ]; }, diff -Nru sslh-1.18/Makefile sslh-1.20/Makefile --- sslh-1.18/Makefile 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/Makefile 2018-11-20 21:58:41.000000000 +0000 @@ -3,7 +3,7 @@ VERSION=$(shell ./genver.sh -r) ENABLE_REGEX=1 # Enable regex probes USELIBCONFIG=1 # Use libconfig? (necessary to use configuration files) -USELIBPCRE= # Use libpcre? (needed for regex on musl) +USELIBPCRE=1 # Use libpcre? (needed for regex on musl) USELIBWRAP?= # Use libwrap? USELIBCAP= # Use libcap? USESYSTEMD= # Make use of systemd socket activation @@ -27,6 +27,8 @@ LIBS= OBJS=common.o sslh-main.o probe.o tls.o +CONDITIONAL_TARGETS= + ifneq ($(strip $(USELIBWRAP)),) LIBS:=$(LIBS) -lwrap CPPFLAGS+=-DLIBWRAP @@ -38,7 +40,7 @@ ifneq ($(strip $(USELIBPCRE)),) CPPFLAGS+=-DLIBPCRE - LIBS:=$(LIBS) -lpcre + LIBS:=$(LIBS) -lpcreposix endif ifneq ($(strip $(USELIBCONFIG)),) @@ -54,12 +56,13 @@ ifneq ($(strip $(USESYSTEMD)),) LIBS:=$(LIBS) -lsystemd CPPFLAGS+=-DSYSTEMD + CONDITIONAL_TARGETS+=systemd-sslh-generator endif -all: sslh $(MAN) echosrv +all: sslh $(MAN) echosrv $(CONDITIONAL_TARGETS) -.c.o: *.h +.c.o: *.h version.h $(CC) $(CFLAGS) $(CPPFLAGS) -c $< version.h: @@ -67,6 +70,8 @@ sslh: sslh-fork sslh-select +$(OBJS): version.h + sslh-fork: version.h $(OBJS) sslh-fork.o Makefile common.h $(CC) $(CFLAGS) $(LDFLAGS) -o sslh-fork sslh-fork.o $(OBJS) $(LIBS) #strip sslh-fork @@ -78,7 +83,7 @@ systemd-sslh-generator: systemd-sslh-generator.o $(CC) $(CFLAGS) $(LDFLAGS) -o systemd-sslh-generator systemd-sslh-generator.o -lconfig -echosrv: $(OBJS) echosrv.o +echosrv: version.h $(OBJS) echosrv.o $(CC) $(CFLAGS) $(LDFLAGS) -o echosrv echosrv.o probe.o common.o tls.o $(LIBS) $(MAN): sslh.pod Makefile diff -Nru sslh-1.18/probe.c sslh-1.20/probe.c --- sslh-1.18/probe.c 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/probe.c 2018-11-20 21:58:41.000000000 +0000 @@ -40,21 +40,22 @@ 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_adb_protocol(const char *p, int len, struct proto*); +static int is_socks5_protocol(const char *p, int len, struct proto*); static int is_true(const char *p, int len, struct proto* proto) { return 1; } /* Table of protocols that have a built-in probe */ static struct proto builtins[] = { - /* description service saddr log_level keepalive probe */ - { "ssh", "sshd", NULL, 1, 0, is_ssh_protocol}, - { "openvpn", NULL, NULL, 1, 0, is_openvpn_protocol }, - { "tinc", NULL, NULL, 1, 0, is_tinc_protocol }, - { "xmpp", NULL, NULL, 1, 0, is_xmpp_protocol }, - { "http", NULL, NULL, 1, 0, is_http_protocol }, - { "ssl", NULL, NULL, 1, 0, is_tls_protocol }, - { "tls", NULL, NULL, 1, 0, is_tls_protocol }, - { "adb", NULL, NULL, 1, 0, is_adb_protocol }, - { "anyprot", NULL, NULL, 1, 0, is_true } + /* description service saddr log_level keepalive fork probe */ + { "ssh", "sshd", NULL, 1, 0, 1, is_ssh_protocol}, + { "openvpn", NULL, NULL, 1, 0, 1, is_openvpn_protocol }, + { "tinc", NULL, NULL, 1, 0, 1, is_tinc_protocol }, + { "xmpp", NULL, NULL, 1, 0, 0, is_xmpp_protocol }, + { "http", NULL, NULL, 1, 0, 0, is_http_protocol }, + { "tls", NULL, NULL, 1, 0, 0, is_tls_protocol }, + { "adb", NULL, NULL, 1, 0, 0, is_adb_protocol }, + { "socks5", NULL, NULL, 1, 0, 0, is_socks5_protocol }, + { "anyprot", NULL, NULL, 1, 0, 0, is_true } }; static struct proto *protocols; @@ -107,25 +108,25 @@ { /* print offset */ if(i % HEXDUMP_COLS == 0) - printf("0x%06x: ", i); + fprintf(stderr, "0x%06x: ", i); /* print hex data */ if(i < len) - printf("%02x ", 0xFF & mem[i]); + fprintf(stderr, "%02x ", 0xFF & mem[i]); else /* end of block, just aligning for ASCII dump */ - printf(" "); + fprintf(stderr, " "); /* 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(' '); + fputc(' ', stderr); else if(isprint(mem[j])) /* printable char */ - putchar(0xFF & mem[j]); + fputc(0xFF & mem[j], stderr); else /* other char */ - putchar('.'); + fputc('.', stderr); } - putchar('\n'); + fputc('\n', stderr); } } } @@ -178,13 +179,16 @@ * */ static int is_xmpp_protocol( const char *p, int len, struct proto *proto) { + if (memmem(p, len, "jabber", 6)) + return PROBE_MATCH; + /* sometimes the word 'jabber' shows up late in the initial string, sometimes after a newline. this makes sure we snarf the entire preamble and detect it. (fixed for adium/pidgin) */ if (len < 50) return PROBE_AGAIN; - return memmem(p, len, "jabber", 6) ? 1 : 0; + return PROBE_NEXT; } static int probe_http_method(const char *p, int len, const char *opt) @@ -192,7 +196,7 @@ if (len < strlen(opt)) return PROBE_AGAIN; - return !strncmp(p, opt, len); + return !strncmp(p, opt, strlen(opt)); } /* Is the buffer the beginning of an HTTP connection? */ @@ -221,45 +225,97 @@ return PROBE_NEXT; } -static int is_sni_alpn_protocol(const char *p, int len, struct proto *proto) +/* Says if it's TLS, optionally with SNI and ALPN lists in proto->data */ +static int is_tls_protocol(const char *p, int len, struct proto *proto) { - int valid_tls; - - valid_tls = parse_tls_header(proto->data, p, len); - - if(valid_tls < 0) - return -1 == valid_tls ? PROBE_AGAIN : PROBE_NEXT; + switch (parse_tls_header(proto->data, p, len)) { + case TLS_MATCH: return PROBE_MATCH; + case TLS_NOMATCH: return PROBE_NEXT; + case TLS_ELENGTH: return PROBE_AGAIN; + default: return PROBE_NEXT; + } +} - /* There *was* a valid match */ - return PROBE_MATCH; +static int probe_adb_cnxn_message(const char *p) +{ + /* The initial ADB host->device packet has a command type of CNXN, and a + * data payload starting with "host:". Note that current versions of the + * client hardcode "host::" (with empty serialno and banner fields) but + * other clients may populate those fields. + */ + return !memcmp(&p[0], "CNXN", 4) && !memcmp(&p[24], "host:", 5); } -static int is_tls_protocol(const char *p, int len, struct proto *proto) +static int is_adb_protocol(const char *p, int len, struct proto *proto) { - if (len < 3) + /* amessage.data_length is not being checked, under the assumption that + * a packet >= 30 bytes will have "something" in the payload field. + * + * 24 bytes for the message header and 5 bytes for the "host:" tag. + * + * ADB protocol: + * https://android.googlesource.com/platform/system/adb/+/master/protocol.txt + */ + static const unsigned int min_data_packet_size = 30; + + if (len < min_data_packet_size) return PROBE_AGAIN; - /* 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) + if (probe_adb_cnxn_message(&p[0]) == PROBE_MATCH) + return PROBE_MATCH; + + /* In ADB v26.0.0 rc1-4321094, the initial host->device packet sends an + * empty message before sending the CNXN command type. This was an + * unintended side effect introduced in + * https://android-review.googlesource.com/c/342653, and will be reverted for + * a future release. */ - return p[0] == 0x16 && p[1] == 0x03 && ( p[2] >= 0 && p[2] <= 0x03); + static const unsigned char empty_message[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff + }; + + if (len < min_data_packet_size + sizeof(empty_message)) + return PROBE_AGAIN; + + if (memcmp(&p[0], empty_message, sizeof(empty_message))) + return PROBE_NEXT; + + return probe_adb_cnxn_message(&p[sizeof(empty_message)]); } -static int is_adb_protocol(const char *p, int len, struct proto *proto) +static int is_socks5_protocol(const char *p_in, int len, struct proto *proto) { - if (len < 30) + unsigned char* p = (unsigned char*)p_in; + int i; + + if (len < 2) return PROBE_AGAIN; - /* The initial ADB host->device packet has a command type of CNXN, and a - * data payload starting with "host:". Note that current versions of the - * client hardcode "host::" (with empty serialno and banner fields) but - * other clients may populate those fields. - * - * We aren't checking amessage.data_length, under the assumption that - * a packet >= 30 bytes long will have "something" in the payload field. + /* First byte should be socks protocol version */ + if (p[0] != 5) + return PROBE_NEXT; + + /* Second byte should be number of supported + * authentication methods, assuming maximum of 10, + * as defined in https://www.iana.org/assignments/socks-methods/socks-methods.xhtml */ - return !memcmp(&p[0], "CNXN", 4) && !memcmp(&p[24], "host:", 5); + char m_count = p[1]; + if (m_count < 1 || m_count > 10) + return PROBE_NEXT; + + if (len < 2 + m_count) + return PROBE_AGAIN; + + /* Each authentication method number should be in range 0..9 + * (https://www.iana.org/assignments/socks-methods/socks-methods.xhtml) + */ + for (i = 0; i < m_count; i++) { + if (p[2 + i] > 9) + return PROBE_NEXT; + } + return PROBE_MATCH; } static int regex_probe(const char *p, int len, struct proto *proto) @@ -288,8 +344,8 @@ int probe_client_protocol(struct connection *cnx) { char buffer[BUFSIZ]; - struct proto *p; - int n; + struct proto *p, *last_p = cnx->proto; + int n, res, again = 0; n = read(cnx->q[0].fd, buffer, sizeof(buffer)); /* It's possible that read() returns an error, e.g. if the client @@ -297,30 +353,41 @@ * 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) { - int res = PROBE_NEXT; + if (n > 0) { + if (verbose > 1) { + fprintf(stderr, "hexdump of incoming packet:\n"); + hexdump(buffer, n); + } defer_write(&cnx->q[1], buffer, n); + } + + for (p = cnx->proto; p; p = p->next) { + char* probe_str[3] = {"PROBE_NEXT", "PROBE_MATCH", "PROBE_AGAIN"}; + if (! p->probe) continue; + + /* Don't probe last protocol if it is anyprot (and store last protocol) */ + if (! p->next) { + last_p = p; + if (!strcmp(p->description, "anyprot")) + break; + } - for (p = cnx->proto; p && res == PROBE_NEXT; p = p->next) { - if (! p->probe) continue; - if (verbose) fprintf(stderr, "probing for %s\n", p->description); + res = p->probe(cnx->q[1].begin_deferred_data, cnx->q[1].deferred_data_size, p); + if (verbose) fprintf(stderr, "probing for %s: %s\n", p->description, probe_str[res]); + if (res == PROBE_MATCH) { cnx->proto = p; - res = p->probe(cnx->q[1].begin_deferred_data, cnx->q[1].deferred_data_size, p); + return PROBE_MATCH; } - if (res != PROBE_NEXT) - return res; + if (res == PROBE_AGAIN) + again++; } + if ((again && (n > 0)) || ((n == -1) && (errno == EAGAIN))) + return PROBE_AGAIN; - 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 - * arbitrary) */ - cnx->proto = protocols; + /* Everything failed: match the last one */ + cnx->proto = last_p; return PROBE_MATCH; } @@ -352,10 +419,6 @@ if (!strcmp(description, "regex")) return regex_probe; - /* Special case of "sni/alpn" probe for same reason as above*/ - if (!strcmp(description, "sni_alpn")) - return is_sni_alpn_protocol; - /* Special case of "timeout" is allowed as a probe name in the * configuration file even though it's not really a probe */ if (!strcmp(description, "timeout")) diff -Nru sslh-1.18/probe.h sslh-1.20/probe.h --- sslh-1.18/probe.h 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/probe.h 2018-11-20 21:58:41.000000000 +0000 @@ -24,6 +24,7 @@ * 1: Log incoming connection */ int keepalive; /* 0: No keepalive ; 1: Set Keepalive for this connection */ + int fork; /* 0: Connection can run within shared process ; 1: Separate process required for this connection */ /* function to probe that protocol; parameters are buffer and length * containing the data to probe, and a pointer to the protocol structure */ diff -Nru sslh-1.18/README.md sslh-1.20/README.md --- sslh-1.18/README.md 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/README.md 2018-11-20 21:58:41.000000000 +0000 @@ -135,7 +135,7 @@ Libwrap support --------------- -Sslh can optionnaly perform `libwrap` checks for the sshd +Sslh can optionally perform `libwrap` checks for the sshd service: because the connection to `sshd` will be coming locally from `sslh`, `sshd` cannot determine the IP of the client. @@ -145,10 +145,10 @@ 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 +the time the TCP connection 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 +default settings timing out and assuming an SSH connection. +To support OpenVPN connections reliably, it is necessary to increase `sslh`'s timeout to 5 seconds. Instead of using OpenVPN's port sharing, it is more reliable @@ -205,11 +205,11 @@ You can use the `setcap(8)` utility to give these capabilities to the executable: - # setcap cap_net_bind_service,cap_net_admin+pe sslh-select + sudo setcap cap_net_bind_service,cap_net_admin+pe sslh-select Then you can run sslh-select as an unpriviledged user, e.g.: - $ sslh-select -p myname:443 --ssh localhost:22 --ssl localhost:443 + sslh-select -p myname:443 --ssh localhost:22 --ssl localhost:443 Caveat: `CAP_NET_ADMIN` does give sslh too many rights, e.g. configuring the interface. If you're not going to use @@ -231,35 +231,70 @@ give it `CAP_NET_ADMIN` capabilities (see appropriate chapter) or run it as root (but don't do that). -The firewalling tables also need to be adjusted as follow. -The example connects to HTTPS on 4443 -- adapt to your needs ; -I don't think it is possible to have `httpd` listen to 443 in +The firewalling tables also need to be adjusted as follows. +I don't think it is possible to have `httpd` and `sslh` both listen to 443 in this scheme -- let me know if you manage that: - # iptables -t mangle -N SSLH - # iptables -t mangle -A OUTPUT --protocol tcp --out-interface eth0 --sport 22 --jump SSLH - # iptables -t mangle -A OUTPUT --protocol tcp --out-interface eth0 --sport 4443 --jump SSLH - # iptables -t mangle -A SSLH --jump MARK --set-mark 0x1 - # iptables -t mangle -A SSLH --jump ACCEPT - # ip rule add fwmark 0x1 lookup 100 - # ip route add local 0.0.0.0/0 dev lo table 100 + # Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination + sysctl -w net.ipv4.conf.default.route_localnet=1 + sysctl -w net.ipv4.conf.all.route_localnet=1 + + # DROP martian packets as they would have been if route_localnet was zero + # Note: packets not leaving the server aren't affected by this, thus sslh will still work + iptables -t raw -A PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP + iptables -t mangle -A POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP + + # Mark all connections made by ssl for special treatment (here sslh is run as user "sslh") + iptables -t nat -A OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f + + # Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark) + iptables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f + + # Configure routing for those marked packets + ip rule add fwmark 0x1 lookup 100 + ip route add local 0.0.0.0/0 dev lo table 100 Tranparent proxying with IPv6 is similarly set up as follows: - # ip6tables -t mangle -N SSLH - # ip6tables -t mangle -A OUTPUT --protocol tcp --out-interface eth0 --sport 22 --jump SSLH - # ip6tables -t mangle -A OUTPUT --protocol tcp --out-interface eth0 --sport 4443 --jump SSLH - # ip6tables -t mangle -A SSLH --jump MARK --set-mark 0x1 - # ip6tables -t mangle -A SSLH --jump ACCEPT - # ip -6 rule add fwmark 0x1 lookup 100 - # ip -6 route add local ::/0 dev lo table 100 - -Note that these rules will prevent from connecting directly -to ssh on the port 22, as packets coming out of sshd will be -tagged. If you need to retain direct access to ssh on port -22 as well as through sslh, you can make sshd listen to -22 AND another port (e.g. 2222), and change the above rules -accordingly. + # Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination + # Not sure if this is needed for ipv6 though + sysctl -w net.ipv4.conf.default.route_localnet=1 + sysctl -w net.ipv4.conf.all.route_localnet=1 + + # DROP martian packets as they would have been if route_localnet was zero + # Note: packets not leaving the server aren't affected by this, thus sslh will still work + ip6tables -t raw -A PREROUTING ! -i lo -d ::1/128 -j DROP + ip6tables -t mangle -A POSTROUTING ! -o lo -s ::1/128 -j DROP + + # Mark all connections made by ssl for special treatment (here sslh is run as user "sslh") + ip6tables -t nat -A OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f + + # Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark) + ip6tables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f + + # Configure routing for those marked packets + ip -6 rule add fwmark 0x1 lookup 100 + ip -6 route add local ::/0 dev lo table 100 + +Explanation: +To be able to use `localhost` as destination in your sslh config along with transparent proxying +you have to allow routing of loopback addresses as done above. +This is something you usually should not do (see [this stackoverflow post](https://serverfault.com/questions/656279/how-to-force-linux-to-accept-packet-with-loopback-ip/656484#656484)) +The two `DROP` iptables rules emulate the behaviour of `route_localnet` set to off (with one small difference: +allowing the reroute-check to happen after the fwmark is set on packets destined for sslh). +See [this diagram](https://upload.wikimedia.org/wikipedia/commons/3/37/Netfilter-packet-flow.svg) for a good visualisation +showing how packets will traverse the iptables chains. + +Note: +You have to run `sslh` as dedicated user (in this example the user is also named `sslh`), to not mess up with your normal networking. +These rules will allow you to connect directly to ssh on port +22 (or to any other service behind sslh) as well as through sslh on port 443. + +Also remember that iptables configuration and ip routes and +rules won't be necessarily persisted after you reboot. Make +sure to save them properly. For example in CentOS7, you would +do `iptables-save > /etc/sysconfig/iptables`, and add both +`ip` commands to your `/etc/rc.local`. FreeBSD: @@ -390,32 +425,14 @@ call systemctl daemon-reload after any changes to /etc/sslh.cfg to generate the new dynamic socket unit. -Transparent proxying means the target server sees the real -origin address, so it means if the client connects using -IPv6, the server must also support IPv6. It is easy to -support both IPv4 and IPv6 by configuring the server -accordingly, and setting `sslh` to connect to a name that -resolves to both IPv4 and IPv6, e.g.: - - sslh --transparent --listen :443 --ssh insideaddr:22 - - /etc/hosts: - 192.168.0.1 insideaddr - 201::::2 insideaddr - -Upon incoming IPv6 connection, `sslh` will first try to -connect to the IPv4 address (which will fail), then connect -to the IPv6 address. - - Fail2ban -------- If using transparent proxying, just use the standard ssh rules. If you can't or don't want to use transparent proxying, you can set `fail2ban` rules to block repeated ssh -connections from a same IP address (obviously this depends -on the site, there might be legimite reasons you would get +connections from an IP address (obviously this depends +on the site, there might be legitimate reasons you would get many connections to ssh from the same IP address...) See example files in scripts/fail2ban. diff -Nru sslh-1.18/scripts/systemd.sslh.service sslh-1.20/scripts/systemd.sslh.service --- sslh-1.18/scripts/systemd.sslh.service 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/scripts/systemd.sslh.service 2018-11-20 21:58:41.000000000 +0000 @@ -4,8 +4,24 @@ [Service] EnvironmentFile=/etc/conf.d/sslh -ExecStart=/usr/bin/sslh --foreground $DAEMON_OPTS +ExecStart=/usr/sbin/sslh --foreground $DAEMON_OPTS KillMode=process +#Hardening +PrivateTmp=true +CapabilityBoundingSet=CAP_SETGID CAP_SETUID CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE +SecureBits=noroot-locked +ProtectSystem=strict +ProtectHome=true +ProtectKernelModules=true +ProtectKernelTunables=true +ProtectControlGroups=true +MountFlags=private +NoNewPrivileges=true +PrivateDevices=true +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +MemoryDenyWriteExecute=true +DynamicUser=true [Install] WantedBy=multi-user.target diff -Nru sslh-1.18/sslh-fork.c sslh-1.20/sslh-fork.c --- sslh-1.18/sslh-fork.c 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/sslh-fork.c 2018-11-20 21:58:41.000000000 +0000 @@ -94,6 +94,8 @@ } else { /* Timed out: it's necessarily SSH */ cnx.proto = timeout_protocol(); + if (verbose) + log_message(LOG_INFO, "timed out, connect to %s\n", cnx.proto->description); break; } } @@ -143,11 +145,17 @@ listener_pid_number = num_addr_listen; listener_pid = malloc(listener_pid_number * sizeof(listener_pid[0])); + CHECK_ALLOC(listener_pid, "malloc"); /* Start one process for each listening address */ for (i = 0; i < num_addr_listen; i++) { - if (!(listener_pid[i] = fork())) { - + listener_pid[i] = fork(); + switch(listener_pid[i]) { + // Log if fork() fails for some reason + case -1: log_message(LOG_ERR, "fork failed: err %d: %s\n", errno, strerror(errno)); + break; + // We're in the child, we have work to do + case 0: /* Listening process just accepts a connection, forks, and goes * back to listening */ while (1) @@ -155,15 +163,26 @@ in_socket = accept(listen_sockets[i], 0, 0); if (verbose) fprintf(stderr, "accepted fd %d\n", in_socket); - if (!fork()) - { + switch(fork()) { + case -1: log_message(LOG_ERR, "fork failed: err %d: %s\n", errno, strerror(errno)); + break; + + /* In child process */ + case 0: for (i = 0; i < num_addr_listen; ++i) close(listen_sockets[i]); start_shoveler(in_socket); exit(0); + + /* In parent process */ + default: break; } close(in_socket); } + break; + // We're in the parent, we don't need to do anything + default: + break; } } diff -Nru sslh-1.18/sslh-main.c sslh-1.20/sslh-main.c --- sslh-1.18/sslh-main.c 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/sslh-main.c 2018-11-20 21:58:41.000000000 +0000 @@ -39,8 +39,8 @@ const char* USAGE_STRING = "sslh " VERSION "\n" \ "usage:\n" \ -"\tsslh [-v] [-i] [-V] [-f] [-n] [--transparent] [-F ]\n" -"\t[-t ] [-P ] -u -p [-p ...] \n" \ +"\tsslh [-v] [-i] [-V] [-f] [-n] [--transparent] [-F]\n" +"\t[-t ] [-P ] [-u ] [-C ] -p [-p ...] \n" \ "%s\n\n" /* Dynamically built list of builtin protocols */ \ "\t[--on-timeout ]\n" \ "-v: verbose\n" \ @@ -48,8 +48,9 @@ "-f: foreground\n" \ "-n: numeric output\n" \ "-u: specify under which user to run\n" \ +"-C: specify under which chroot path to run\n" \ "--transparent: behave as a transparent proxy\n" \ -"-F: use configuration file\n" \ +"-F: use configuration file (warning: no space between -F and file name!)\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" \ @@ -71,6 +72,7 @@ { "user", required_argument, 0, 'u' }, { "config", optional_argument, 0, 'F' }, { "pidfile", required_argument, 0, 'P' }, + { "chroot", required_argument, 0, 'C' }, { "timeout", required_argument, 0, 't' }, { "on-timeout", required_argument, 0, OPT_ONTIMEOUT }, { "listen", required_argument, 0, 'p' }, @@ -78,7 +80,7 @@ }; static struct option* all_options; static struct proto* builtins; -static const char *optstr = "vt:T:p:VP:F::"; +static const char *optstr = "vt:T:p:VP:C:F::"; @@ -123,14 +125,15 @@ for (p = get_first_protocol(); p; p = p->next) { fprintf(stderr, - "%s addr: %s. libwrap service: %s log_level: %d family %d %d [%s]\n", + "%s addr: %s. libwrap service: %s log_level: %d family %d %d [%s] [%s]\n", p->description, sprintaddr(buf, sizeof(buf), p->saddr), p->service, p->log_level, p->saddr->ai_family, p->saddr->ai_addr->sa_family, - p->keepalive ? "keepalive" : ""); + p->keepalive ? "keepalive" : "", + p->fork ? "fork" : ""); } fprintf(stderr, "listening on:\n"); for (a = addr_listen; a; a = a->ai_next) { @@ -144,6 +147,32 @@ } +/* To removed in v1.21 */ +const char* ssl_err_msg = "Usage of 'ssl' setting is deprecated and will be removed in v1.21. Please use 'tls' instead\n"; +void ssl_to_tls(char* setting) +{ + if (!strcmp(setting, "ssl")) { + strcpy(setting, "tls"); /* legacy configuration */ + log_message(LOG_INFO, ssl_err_msg); + } +} + + +/* Turn 'ssl' command line option to 'tls'. To removed in v1.21 */ +void cmd_ssl_to_tls(int argc, char* argv[]) +{ + int i; + for (i = 0; i < argc; i++) { + if (!strcmp(argv[i], "--ssl")) { + strcpy(argv[i], "--tls"); + /* foreground option not parsed yet, syslog not open, just print on + * stderr and hope for the best */ + fprintf(stderr, ssl_err_msg); + } + } +} + + /* Extract configuration on addresses and ports on which to listen. * out: newly allocated list of addrinfo to listen to */ @@ -207,14 +236,21 @@ p->probe = get_probe("regex"); probe_list = calloc(num_probes + 1, sizeof(*probe_list)); + CHECK_ALLOC(probe_list, "calloc"); p->data = (void*)probe_list; for (i = 0; i < num_probes; i++) { probe_list[i] = malloc(sizeof(*(probe_list[i]))); + CHECK_ALLOC(probe_list[i], "malloc"); expr = config_setting_get_string_elem(probes, i); - res = regcomp(probe_list[i], expr, 0); + if (expr == NULL) { + fprintf(stderr, "%s: invalid probe specified\n", p->description); + exit(1); + } + res = regcomp(probe_list[i], expr, REG_EXTENDED); if (res) { err = malloc(errsize = regerror(res, probe_list[i], NULL, 0)); + CHECK_ALLOC(err, "malloc"); regerror(res, probe_list[i], err, errsize); fprintf(stderr, "%s:%s\n", expr, err); free(err); @@ -232,13 +268,9 @@ static void setup_sni_alpn_list(struct proto *p, config_setting_t* config_items, const char* name, int alpn) { int num_probes, i, max_server_name_len, server_name_len; - const char * config_item; + const char * config_item, *server_name; char** sni_hostname_list; - if(!config_items || !config_setting_is_array(config_items)) { - fprintf(stderr, "%s: no %s specified\n", p->description, name); - return; - } num_probes = config_setting_length(config_items); if (!num_probes) { fprintf(stderr, "%s: no %s specified\n", p->description, name); @@ -247,16 +279,27 @@ max_server_name_len = 0; for (i = 0; i < num_probes; i++) { - server_name_len = strlen(config_setting_get_string_elem(config_items, i)); + server_name = config_setting_get_string_elem(config_items, i); + if (server_name == NULL) { + fprintf(stderr, "%s: invalid %s specified\n", p->description, name); + exit(1); + } + server_name_len = strlen(server_name); if(server_name_len > max_server_name_len) max_server_name_len = server_name_len; } sni_hostname_list = calloc(num_probes + 1, ++max_server_name_len); + CHECK_ALLOC(sni_hostname_list, "calloc"); for (i = 0; i < num_probes; i++) { config_item = config_setting_get_string_elem(config_items, i); + if (config_item == NULL) { + fprintf(stderr, "%s: invalid %s specified\n", p->description, name); + exit(1); + } sni_hostname_list[i] = malloc(max_server_name_len); + CHECK_ALLOC(sni_hostname_list[i], "malloc"); strcpy (sni_hostname_list[i], config_item); if(verbose) fprintf(stderr, "%s: %s[%d]: %s\n", p->description, name, i, sni_hostname_list[i]); } @@ -264,12 +307,20 @@ p->data = (void*)tls_data_set_list(p->data, alpn, sni_hostname_list); } -static void setup_sni_alpn(struct proto *p, config_setting_t* sni_hostnames, config_setting_t* alpn_protocols) +static void setup_sni_alpn(struct proto *p, config_setting_t* prot) { + config_setting_t *sni_hostnames, *alpn_protocols; + p->data = (void*)new_tls_data(); - p->probe = get_probe("sni_alpn"); - setup_sni_alpn_list(p, sni_hostnames, "sni_hostnames", 0); - setup_sni_alpn_list(p, alpn_protocols, "alpn_protocols", 1); + sni_hostnames = config_setting_get_member(prot, "sni_hostnames"); + alpn_protocols = config_setting_get_member(prot, "alpn_protocols"); + + if(sni_hostnames && config_setting_is_array(sni_hostnames)) { + setup_sni_alpn_list(p, sni_hostnames, "sni_hostnames", 0); + } + if(alpn_protocols && config_setting_is_array(alpn_protocols)) { + setup_sni_alpn_list(p, alpn_protocols, "alpn_protocols", 1); + } } #endif @@ -279,8 +330,9 @@ #ifdef LIBCONFIG static int config_protocols(config_t *config, struct proto **prots) { - config_setting_t *setting, *prot, *patterns, *sni_hostnames, *alpn_protocols; - const char *hostname, *port, *name; + config_setting_t *setting, *prot, *patterns; + const char *hostname, *port, *cfg_name; + char* name; int i, num_prots; struct proto *p, *prev = NULL; @@ -289,27 +341,36 @@ num_prots = config_setting_length(setting); for (i = 0; i < num_prots; i++) { p = calloc(1, sizeof(*p)); + CHECK_ALLOC(p, "calloc"); if (i == 0) *prots = p; if (prev) prev->next = p; prev = p; prot = config_setting_get_elem(setting, i); - if ((config_setting_lookup_string(prot, "name", &name) && + if ((config_setting_lookup_string(prot, "name", &cfg_name) && config_setting_lookup_string(prot, "host", &hostname) && config_setting_lookup_string(prot, "port", &port) )) { + /* To removed in v1.21 */ + name = strdup(cfg_name); + ssl_to_tls(name); + /* /remove */ p->description = name; config_setting_lookup_string(prot, "service", &(p->service)); config_setting_lookup_bool(prot, "keepalive", &p->keepalive); + config_setting_lookup_bool(prot, "fork", &p->fork); if (config_setting_lookup_int(prot, "log_level", &p->log_level) == CONFIG_FALSE) { p->log_level = 1; } - resolve_split_name(&(p->saddr), hostname, port); + if (resolve_split_name(&(p->saddr), hostname, port)) { + fprintf(stderr, "line %d: cannot resolve %s:%s\n", config_setting_source_line(prot), hostname, port); + exit(1); + } p->probe = get_probe(name); - if (!p->probe || !strcmp(name, "sni_alpn")) { + if (!p->probe) { fprintf(stderr, "line %d: %s: probe unknown\n", config_setting_source_line(prot), name); exit(1); } @@ -324,14 +385,12 @@ /* Probe-specific options: SNI/ALPN */ if (!strcmp(name, "tls")) { - sni_hostnames = config_setting_get_member(prot, "sni_hostnames"); - alpn_protocols = config_setting_get_member(prot, "alpn_protocols"); - - if((sni_hostnames && config_setting_is_array(sni_hostnames)) || (alpn_protocols && config_setting_is_array(alpn_protocols))) { - setup_sni_alpn(p, sni_hostnames, alpn_protocols); - } + setup_sni_alpn(p, prot); } + } else { + fprintf(stderr, "line %d: Illegal protocol description (missing name, host or port)\n", config_setting_source_line(prot)); + exit(1); } } } @@ -371,7 +430,10 @@ return 1; } - config_lookup_bool(&config, "verbose", &verbose); + if(config_lookup_bool(&config, "verbose", &verbose) == CONFIG_FALSE) { + config_lookup_int(&config, "verbose", &verbose); + } + config_lookup_bool(&config, "inetd", &inetd); config_lookup_bool(&config, "foreground", &foreground); config_lookup_bool(&config, "numeric", &numeric); @@ -387,6 +449,9 @@ config_lookup_string(&config, "user", &user_name); config_lookup_string(&config, "pidfile", &pid_file); + config_lookup_string(&config, "chroot", &chroot_path); + + config_lookup_string(&config, "syslog_facility", &facility); config_listen(&config, listen); config_protocols(&config, prots); @@ -421,6 +486,7 @@ /* Create all_options, composed of const_options followed by one option per * known protocol */ all_options = calloc(ARRAY_SIZE(const_options) + get_num_builtins(), sizeof(struct option)); + CHECK_ALLOC(all_options, "calloc"); memcpy(all_options, const_options, sizeof(const_options)); append_protocols(all_options, ARRAY_SIZE(const_options) - 1, builtins, get_num_builtins()); } @@ -438,6 +504,8 @@ char *config_filename; #endif + cmd_ssl_to_tls(argc, argv); /* To remove in v1.21 */ + make_alloptions(); #ifdef LIBCONFIG @@ -499,8 +567,10 @@ if (!prots) { /* No protocols yet -- create the list */ p = prots = calloc(1, sizeof(*p)); + CHECK_ALLOC(p, "calloc"); } else { p->next = calloc(1, sizeof(*p)); + CHECK_ALLOC(p->next, "calloc"); p = p->next; } memcpy(p, &builtins[c-PROT_SHIFT], sizeof(*p)); @@ -547,6 +617,10 @@ pid_file = optarg; break; + case 'C': + chroot_path = optarg; + break; + case 'v': verbose++; break; @@ -591,6 +665,7 @@ /* Init defaults */ pid_file = NULL; user_name = NULL; + chroot_path = NULL; cmdline_config(argc, argv, &protocols); parse_cmdline(argc, argv, protocols); @@ -629,13 +704,12 @@ if (pid_file) write_pid_file(pid_file); - if (user_name) - drop_privileges(user_name); - - - /* Open syslog connection */ + /* Open syslog connection before we drop privs/chroot */ setup_syslog(argv[0]); + if (user_name || chroot_path) + drop_privileges(user_name, chroot_path); + if (verbose) printcaps(); diff -Nru sslh-1.18/sslh.pod sslh-1.20/sslh.pod --- sslh-1.18/sslh.pod 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/sslh.pod 2018-11-20 21:58:41.000000000 +0000 @@ -6,7 +6,7 @@ =head1 SYNOPSIS -sslh [B<-F> I] [ B<-t> I ] [B<--transparent>] [B<-p> I [B<-p> I ...] [B<--ssl> I] [B<--ssh> I] [B<--openvpn> I] [B<--http> I] [B<--anyprot> I] [B<--on-timeout> I] [B<-u> I] [B<-P> I] [-v] [-i] [-V] [-f] [-n] +sslh [B<-F>I] [B<-t> I] [B<--transparent>] [B<-p> I [B<-p> I ...] [B<--ssl> I] [B<--tls> I] [B<--ssh> I] [B<--openvpn> I] [B<--http> I] [B<--xmpp> I] [B<--tinc> I] [B<--anyprot> I] [B<--on-timeout> I] [B<-u> I] [B<-C> I] [B<-P> I] [-v] [-i] [-V] [-f] [-n] =head1 DESCRIPTION @@ -58,7 +58,7 @@ =head2 Probing protocols When receiving an incoming connection, B will read the -first bytes sent be the connecting client. It will then +first bytes sent by 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 @@ -78,12 +78,15 @@ =over 4 -=item B<-F> I, B<--config> I +=item B<-F>I, B<--config> I -Uses I has configuration file. If other +Uses I as configuration file. If other command-line options are specified, they will override the configuration file's settings. +When using the shorthand version, make sure there should be +no space between B<-F> and the I. + =item B<-t> I, B<--timeout> I Timeout before forwarding the connection to the timeout @@ -92,7 +95,11 @@ =item B<--on-timeout> I Name of the protocol to connect to after the timeout period -is over. Default is 'ssh'. +is over. Default is to forward to the first specified +protocol. It usually makes sense to specify 'ssh' as the +timeout protocol, as the SSH specification does not tell +who is supposed to speak first and a large number of SSH +clients wait for the server to send its banner. =item B<--transparent> @@ -123,7 +130,7 @@ Also, B 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 +compliant with RFC6176 which prohibits the usage of SSLv2. If you wish to accept SSLv2, use B<--default> instead. =item B<--ssh> I @@ -181,6 +188,10 @@ Requires to run under the specified username. +=item B<-C> I, B<--chroot> I + +Requires to run under the specified chroot. + =item B<-P> I, B<--pidfile> I Specifies a file in which to write the PID of the main @@ -226,10 +237,10 @@ =head1 SEE ALSO -Last version available from +The latest version is available from L, and can be tracked from L. =head1 AUTHOR -Written by Yves Rutschle +Written by Yves Rutschle. diff -Nru sslh-1.18/sslh-select.c sslh-1.20/sslh-select.c --- sslh-1.18/sslh-select.c 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/sslh-select.c 2018-11-20 21:58:41.000000000 +0000 @@ -27,6 +27,8 @@ const char* server_type = "sslh-select"; +#define MAX(a, b) (((a) > (b)) ? (a) : (b)) + /* 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 @@ -61,9 +63,9 @@ 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); + close(cnx->q[i].fd); if (cnx->q[i].deferred_data) free(cnx->q[i].deferred_data); } @@ -93,11 +95,16 @@ in_socket = accept(listen_socket, 0, 0); CHECK_RES_RETURN(in_socket, "accept"); - if (!fd_is_in_range(in_socket)) + if (!fd_is_in_range(in_socket)) { + close(in_socket); return -1; + } res = set_nonblock(in_socket); - if (res == -1) return -1; + if (res == -1) { + close(in_socket); + return -1; + } /* Find an empty slot */ for (free = 0; (free < *cnx_size) && ((*cnx)[free].q[0].fd != -1); free++) { @@ -109,6 +116,7 @@ new = realloc(*cnx, (*cnx_size + cnx_num_alloc) * sizeof((*cnx)[0])); if (!new) { log_message(LOG_ERR, "unable to realloc -- dropping connection\n"); + close(in_socket); return -1; } *cnx = new; @@ -140,9 +148,9 @@ flush_deferred(q); if (q->deferred_data) { FD_SET(q->fd, fds_w); - } else { - FD_SET(q->fd, fds_r); + FD_CLR(cnx->q[0].fd, fds_r); } + FD_SET(q->fd, fds_r); return q->fd; } else { tidy_connection(cnx, fds_r, fds_w); @@ -180,6 +188,93 @@ } } +/* shovels data from one fd to the other and vice-versa + returns after one socket closed + */ +void shovel_single(struct connection *cnx) +{ + fd_set fds_r, fds_w; + int res, i; + int max_fd = MAX(cnx->q[0].fd, cnx->q[1].fd) + 1; + + FD_ZERO(&fds_r); + FD_ZERO(&fds_w); + while (1) { + for (i = 0; i < 2; i++) { + if (cnx->q[i].deferred_data_size) { + FD_SET(cnx->q[i].fd, &fds_w); + FD_CLR(cnx->q[1-i].fd, &fds_r); + } else { + FD_CLR(cnx->q[i].fd, &fds_w); + FD_SET(cnx->q[1-i].fd, &fds_r); + } + } + + res = select( + max_fd, + &fds_r, + &fds_w, + NULL, + NULL + ); + CHECK_RES_DIE(res, "select"); + + for (i = 0; i < 2; i++) { + if (FD_ISSET(cnx->q[i].fd, &fds_w)) { + res = flush_deferred(&cnx->q[i]); + if ((res == -1) && ((errno == EPIPE) || (errno == ECONNRESET))) { + if (verbose) + fprintf(stderr, "%s socket closed\n", i ? "server" : "client"); + return; + } + } + if (FD_ISSET(cnx->q[i].fd, &fds_r)) { + res = fd2fd(&cnx->q[1-i], &cnx->q[i]); + if (!res) { + if (verbose) + fprintf(stderr, "socket closed\n"); + return; + } + } + } + } +} + +/* Child process that makes internal connection and proxies + */ +void connect_proxy(struct connection *cnx) +{ + int in_socket; + int out_socket; + + /* Minimize the file descriptor value to help select() */ + in_socket = dup(cnx->q[0].fd); + if (in_socket == -1) { + in_socket = cnx->q[0].fd; + } else { + close(cnx->q[0].fd); + cnx->q[0].fd = in_socket; + } + + /* Connect the target socket */ + out_socket = connect_addr(cnx, in_socket); + CHECK_RES_DIE(out_socket, "connect"); + + cnx->q[1].fd = out_socket; + + log_connection(cnx); + + shovel_single(cnx); + + close(in_socket); + close(out_socket); + + if (verbose) + fprintf(stderr, "connection closed down\n"); + + exit(0); +} + /* returns true if specified fd is initialised and present in fd_set */ int is_fd_active(int fd, fd_set* set) { @@ -206,7 +301,8 @@ 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; + int max_fd, i, j, res; + int in_socket = 0; struct connection *cnx; int num_cnx; /* Number of connections in *cnx */ int num_probing = 0; /* Number of connections currently probing @@ -226,6 +322,7 @@ num_cnx = cnx_num_alloc; /* Start with a set pool of slots */ cnx = malloc(num_cnx * sizeof(struct connection)); + CHECK_ALLOC(cnx, "malloc"); for (i = 0; i < num_cnx; i++) init_cnx(&cnx[i]); @@ -248,15 +345,12 @@ for (i = 0; i < num_addr_listen; i++) { if (FD_ISSET(listen_sockets[i], &readfds)) { in_socket = accept_new_connection(listen_sockets[i], &cnx, &num_cnx); - if (in_socket != -1) - num_probing++; - if (in_socket > 0) { + num_probing++; FD_SET(in_socket, &fds_r); if (in_socket >= max_fd) max_fd = in_socket + 1;; } - FD_CLR(listen_sockets[i], &readfds); } } @@ -305,6 +399,10 @@ * data so probe the protocol */ if ((cnx[i].probe_timeout < time(NULL))) { cnx[i].proto = timeout_protocol(); + if (verbose) + log_message(LOG_INFO, + "timed out, connect to %s\n", + cnx[i].proto->description); } else { res = probe_client_protocol(&cnx[i]); if (res == PROBE_AGAIN) @@ -314,14 +412,36 @@ num_probing--; cnx[i].state = ST_SHOVELING; - /* libwrap check if required for this protocol */ - if (cnx[i].proto->service && - check_access_rights(in_socket, cnx[i].proto->service)) { - tidy_connection(&cnx[i], &fds_r, &fds_w); - res = -1; - } else { - res = connect_queue(&cnx[i], &fds_r, &fds_w); - } + /* libwrap check if required for this protocol */ + if (cnx[i].proto->service && + check_access_rights(in_socket, cnx[i].proto->service)) { + tidy_connection(&cnx[i], &fds_r, &fds_w); + res = -1; + } else if (cnx[i].proto->fork) { + struct connection *pcnx_i = &cnx[i]; + struct connection cnx_i = *pcnx_i; + switch (fork()) { + case 0: /* child */ + for (i = 0; i < num_addr_listen; i++) + close(listen_sockets[i]); + for (i = 0; i < num_cnx; i++) + if (&cnx[i] != pcnx_i) + for (j = 0; j < 2; j++) + if (cnx[i].q[j].fd != -1) + close(cnx[i].q[j].fd); + free(cnx); + connect_proxy(&cnx_i); + exit(0); + case -1: log_message(LOG_ERR, "fork failed: err %d: %s\n", errno, strerror(errno)); + break; + default: /* parent */ + break; + } + tidy_connection(&cnx[i], &fds_r, &fds_w); + res = -1; + } else { + res = connect_queue(&cnx[i], &fds_r, &fds_w); + } if (res >= max_fd) max_fd = res + 1;; diff -Nru sslh-1.18/systemd-sslh-generator.c sslh-1.20/systemd-sslh-generator.c --- sslh-1.18/systemd-sslh-generator.c 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/systemd-sslh-generator.c 2018-11-20 21:58:41.000000000 +0000 @@ -2,13 +2,14 @@ #include #include #include +#include "common.h" static char* resolve_listen(const char *hostname, const char *port) { - -/* Need room in the strcat for \0 and : - * the format in the socket unit file is hostname:port */ - char *conn = (char*)malloc(strlen(hostname)+strlen(port)+2); + /* Need room in the strcat for \0 and : + * the format in the socket unit file is hostname:port */ + char *conn = malloc(strlen(hostname)+strlen(port)+2); + CHECK_ALLOC(conn, "malloc"); strcpy(conn, hostname); strcat(conn, ":"); strcat(conn, port); @@ -18,136 +19,135 @@ } -static int get_listen_from_conf(const char *filename, char **listen) { - - config_t config; - config_setting_t *setting, *addr; - const char *hostname, *port; - int len = 0; - -/* look up the listen stanzas in the config file so these - * can be used in the socket file generated */ - - config_init(&config); - if (config_read_file(&config, filename) == CONFIG_FALSE) { - /* we don't care if file is missing, skip it */ - if (config_error_line(&config) != 0) { - fprintf(stderr, "%s:%d:%s\n", - filename, - config_error_line(&config), - config_error_text(&config)); - return -1; - } - } else { - setting = config_lookup(&config, "listen"); - if (setting) { - len = config_setting_length(setting); - for (int i = 0; i < len; i++) { - addr = config_setting_get_elem(setting, i); - if (! (config_setting_lookup_string(addr, "host", &hostname) && - config_setting_lookup_string(addr, "port", &port))) { - fprintf(stderr, - "line %d:Incomplete specification (hostname and port required)\n", - config_setting_source_line(addr)); +static int get_listen_from_conf(const char *filename, char **listen[]) { + config_t config; + config_setting_t *setting, *addr; + const char *hostname, *port; + int len = 0; + + /* look up the listen stanzas in the config file so these + * can be used in the socket file generated */ + config_init(&config); + if (config_read_file(&config, filename) == CONFIG_FALSE) { + /* we don't care if file is missing, skip it */ + if (config_error_line(&config) != 0) { + fprintf(stderr, "%s:%d:%s\n", + filename, + config_error_line(&config), + config_error_text(&config)); return -1; - } else { - - listen[i] = malloc(strlen(resolve_listen(hostname, port))); - strcpy(listen[i], resolve_listen(hostname, port)); } - } + } else { + setting = config_lookup(&config, "listen"); + if (setting) { + int i; + len = config_setting_length(setting); + *listen = malloc(len * sizeof(**listen)); + CHECK_ALLOC(*listen, "malloc"); + for (i = 0; i < len; i++) { + addr = config_setting_get_elem(setting, i); + if (! (config_setting_lookup_string(addr, "host", &hostname) && + config_setting_lookup_string(addr, "port", &port))) { + fprintf(stderr, + "line %d:Incomplete specification (hostname and port required)\n", + config_setting_source_line(addr)); + return -1; + } else { + (*listen)[i] = malloc(strlen(resolve_listen(hostname, port))); + CHECK_ALLOC((*listen)[i], "malloc"); + strcpy((*listen)[i], resolve_listen(hostname, port)); + } + } + } } - } - return len; + return len; } -static int write_socket_unit(FILE *socket, char **listen, int num_addr, const char *source) { +static int write_socket_unit(FILE *socket, char *listen[], int num_addr, const char *source) { + int i; - fprintf(socket, - "# Automatically generated by systemd-sslh-generator\n\n" - "[Unit]\n" - "Before=sslh.service\n" - "SourcePath=%s\n" - "Documentation=man:sslh(8) man:systemd-sslh-generator(8)\n\n" - "[Socket]\n" - "FreeBind=true\n", - source); + fprintf(socket, + "# Automatically generated by systemd-sslh-generator\n\n" + "[Unit]\n" + "Before=sslh.service\n" + "SourcePath=%s\n" + "Documentation=man:sslh(8) man:systemd-sslh-generator(8)\n\n" + "[Socket]\n" + "FreeBind=true\n", + source); - for (int i = 0; i < num_addr; i++) { - fprintf(socket, "ListenStream=%s\n", listen[i]); - } + for (i = 0; i < num_addr; i++) { + fprintf(socket, "ListenStream=%s\n", listen[i]); + } -return 0; + return 0; } static int gen_sslh_config(char *runtime_unit_dir) { - - char *sslh_conf; - int num_addr; - FILE *config; - char **listen; - FILE *runtime_conf_fd = stdout; - const char *unit_file; - -/* There are two default locations so check both with first given preference */ - sslh_conf = "/etc/sslh.cfg"; - - config = fopen(sslh_conf, "r"); - if (config == NULL) { - sslh_conf="/etc/sslh/sslh.cfg"; - config = fopen(sslh_conf, "r"); - if (config == NULL) { - return -1; + char *sslh_conf; + int num_addr; + FILE *config; + char **listen; + FILE *runtime_conf_fd = stdout; + const char *unit_file; + + /* There are two default locations so check both with first given preference */ + sslh_conf = "/etc/sslh.cfg"; + + config = fopen(sslh_conf, "r"); + if (config == NULL) { + sslh_conf="/etc/sslh/sslh.cfg"; + config = fopen(sslh_conf, "r"); + if (config == NULL) { + return -1; + } } - } - - fclose(config); + fclose(config); - num_addr = get_listen_from_conf(sslh_conf, listen); - if (num_addr < 0) - return -1; - -/* If this is run by systemd directly write to the location told to - * otherwise write to standard out so that it's trivial to check what - * will be written */ - if (runtime_unit_dir != "") { - unit_file = "/sslh.socket"; - size_t uf_len = strlen(unit_file); - size_t runtime_len = strlen(runtime_unit_dir) + uf_len + 1; - char *runtime_conf = malloc(runtime_len); - strcpy(runtime_conf, runtime_unit_dir); - strcat(runtime_conf, unit_file); - runtime_conf_fd = fopen(runtime_conf, "w"); - } + num_addr = get_listen_from_conf(sslh_conf, &listen); + if (num_addr < 0) + return -1; + /* If this is run by systemd directly write to the location told to + * otherwise write to standard out so that it's trivial to check what + * will be written */ + if (runtime_unit_dir && *runtime_unit_dir) { + unit_file = "/sslh.socket"; + size_t uf_len = strlen(unit_file); + size_t runtime_len = strlen(runtime_unit_dir) + uf_len + 1; + char *runtime_conf = malloc(runtime_len); + CHECK_ALLOC(runtime_conf, "malloc"); + strcpy(runtime_conf, runtime_unit_dir); + strcat(runtime_conf, unit_file); + runtime_conf_fd = fopen(runtime_conf, "w"); + } - return write_socket_unit(runtime_conf_fd, listen, num_addr, sslh_conf); + return write_socket_unit(runtime_conf_fd, listen, num_addr, sslh_conf); } -int main(int argc, char *argv[]){ - int r = 0; - int k; - char *runtime_unit_dest = ""; - - if (argc > 1 && (argc != 4) ) { - printf("This program takes three or no arguments.\n"); - return -1; - } +int main(int argc, char *argv[]){ + int r = 0; + int k; + char *runtime_unit_dest = ""; - if (argc > 1) - runtime_unit_dest = argv[1]; + if (argc > 1 && (argc != 4) ) { + printf("This program takes three or no arguments.\n"); + return -1; + } - k = gen_sslh_config(runtime_unit_dest); - if (k < 0) - r = k; + if (argc > 1) + runtime_unit_dest = argv[1]; - return r < 0 ? -1 : 0; + k = gen_sslh_config(runtime_unit_dest); + if (k < 0) + r = k; + return r < 0 ? -1 : 0; } diff -Nru sslh-1.18/t sslh-1.20/t --- sslh-1.18/t 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/t 2018-11-20 21:58:41.000000000 +0000 @@ -2,27 +2,30 @@ # Test script for sslh +# Uses Conf::Libconfig to read sslh config file: install +# with: +# cpan Conf::Libconfig + use strict; use IO::Socket::INET6; use Test::More qw/no_plan/; +use Conf::Libconfig; + +my $conf = new Conf::Libconfig; +$conf->read_file("test.cfg"); -# We use ports 9000, 9001 and 9002 -- hope that won't clash -# with anything... -my $ssh_address = "ip6-localhost:9000"; -my $ssl_address = "ip6-localhost:9001"; -my $sslh_port = 9002; -my $no_listen = 9003; # Port on which no-one listens -my $pidfile = "/tmp/sslh_test.pid"; + +my $no_listen = 8083; # Port on which no-one listens +my $pidfile = $conf->lookup_value("pidfile"); +my $sslh_port = $conf->fetch_array("listen")->[0]->{port}; +my $user = (getpwuid $<)[0]; # Run under current username # Which tests do we run -my $SSL_CNX = 1; my $SSH_SHY_CNX = 1; -my $SSH_BOLD_CNX = 1; -my $SSH_PROBE_AGAIN = 1; +my $PROBES_NOFRAG = 1; +my $PROBES_AGAIN = 1; my $SSL_MIX_SSH = 1; my $SSH_MIX_SSL = 1; -my $BIG_MSG = 0; # This test is unreliable -my $STALL_CNX = 0; # This test needs fixing # Robustness tests. These are mostly to achieve full test # coverage, but do not necessarily result in an actual test @@ -32,22 +35,158 @@ my $RB_PARAM_NOHOST = 1; my $RB_WRONG_USERNAME = 1; my $RB_OPEN_PID_FILE = 1; -my $RB_BIND_ADDRESS = 1; my $RB_RESOLVE_ADDRESS = 1; `lcov --directory . --zerocounters`; +sub verbose_exec +{ + my ($cmd) = @_; + + warn "$cmd\n"; + if (!fork) { + exec $cmd; + } +} + + + +# For SNI/ALPN, build a protocol name as such: +# tls:sni1,sni2,...;alpn1,alpn2,... +# input: a protocol entry from Libconfig +sub make_sni_alpn_name { + my ($prot) = @_; + + return "tls:" . (join ",", @{$prot->{sni_hostnames} // []}) + . ";" . (join ",", @{$prot->{alpn_protocols} // [] }); +} + + +# Tests one probe: given input data, connect, verify we get +# the expected server, verify shoveling works +# Named options: +# data: what to write +# expected: expected protocol prefix +# no_frag: don't print byte-per-byte +sub test_probe { + my (%opts) = @_; + + my $cnx = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); + warn "$!\n" unless $cnx; + return unless $cnx; + + my $pattern = $opts{data}; + if ($opts{no_frag}) { + syswrite $cnx, $pattern; + } else { + while (length $pattern) { + syswrite $cnx, (substr $pattern, 0, 1, ''); + select undef, undef, undef, .01; + } + } + + my $data; + my $n = sysread $cnx, $data, 1024; + $data =~ /^(.*?): /; + my $prefix = $1; + $data =~ s/$prefix: //g; + print "Received: protocol $prefix data [$data]\n"; + close $cnx; + + $opts{expected} =~ s/^ssl/tls/; # to remove in 1.21 + is($prefix, $opts{expected}, "probe $opts{expected} connected correctly"); + is($data, $opts{data}, "data shoveled correctly"); +} -my ($ssh_pid, $ssl_pid); +# Test all probes, with or without fragmentation +# options: +# no_frag: write test patterns all at once (also +# available per-protocol as some probes don't support +# fragmentation) +sub test_probes { + my (%opts) = @_; + + my @probes = @{$conf->fetch_array("protocols")}; + foreach my $p (@probes) { + my %protocols = ( + 'ssh' => { data => "SSH-2.0 tester" }, + 'socks5' => { data => "\x05\x04\x01\x02\x03\x04" }, + 'http' => { + data => "GET index.html HTTP/1.1", + no_frag => 1 }, + 'ssl' => { + data => "\x16\x03\x01\x00\xab\x01\x00\x00\xa7\x03\x03\x89\x22\x33\x95\x43\x7a\xc3\x89\x45\x51\x12\x3c\x28\x24\x1b\x6a\x78\xbf\xbe\x95\xd8\x90\x58\xd7\x65\xf7\xbb\x2d\xb2\x8d\xa0\x75\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x46\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x16\x00\x00\x00\x17\x00\x00hello ssl alone" + }, + 'tls' => { + # Packet with SNI and ALPN (`openssl s_client -connect localhost:443 -alpn alpn1 -servername sni1`) + data_sni_alpn => "\x16\x03\x01\x00\xc4\x01\x00\x00\xc0\x03\x03\x03\x19\x01\x00\x40\x14\x13\xcc\x1b\x94\xad\x20\x5d\x13\x1a\x8d\xd2\x65\x23\x70\xde\xd1\x3c\x5d\x05\x19\xcb\x27\x0d\x7c\x2c\x89\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x5f\x00\x00\x00\x09\x00\x07\x00\x00\x04\$sni\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x10\x00\x08\x00\x06\x05\$alpn\x00\x16\x00\x00\x00\x17\x00\x00hello sni/alpn", + # Packet with SNI alone + data_sni => "\x16\x03\x01\x00\xb8\x01\x00\x00\xb4\x03\x03\x97\xe4\xe9\xad\x86\xe1\x21\xfd\xc4\x5b\x27\x0e\xad\x4b\x55\xc2\x50\xe4\x1c\x86\x2f\x37\x25\xde\xe8\x9c\x59\xfc\x1b\xa9\x37\x32\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x53\x00\x00\x00\x09\x00\x07\x00\x00\x04\$sni\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x16\x00\x00\x00\x17\x00\x00hello sni", + # packet with ALPN alone + data_alpn => "\x16\x03\x01\x00\xb7\x01\x00\x00\xb3\x03\x03\xe2\x90\xa2\x29\x03\x31\xad\x98\x44\x51\x54\x90\x5b\xd9\x51\x0e\x66\xb5\x3f\xe8\x8b\x09\xc9\xe4\x2b\x97\x24\xef\xad\x56\x06\xc9\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x52\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x10\x00\x08\x00\x06\x05\$alpn\x00\x16\x00\x00\x00\x17\x00\x00hello alpn", + # packet with no SNI, no ALPN + data => "\x16\x03\x01\x00\xab\x01\x00\x00\xa7\x03\x03\x89\x22\x33\x95\x43\x7a\xc3\x89\x45\x51\x12\x3c\x28\x24\x1b\x6a\x78\xbf\xbe\x95\xd8\x90\x58\xd7\x65\xf7\xbb\x2d\xb2\x8d\xa0\x75\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x46\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x16\x00\x00\x00\x17\x00\x00hello tls alone" + }, + 'openvpn' => { data => "\x00\x00" }, + 'tinc' => { data => "0 hello" }, + 'xmpp' => {data => "I should get a real jabber connection initialisation here" }, + 'adb' => { data => "CNXN....................host:..." }, + 'anyprot' => {data => "hello anyprot this needs to be longer than xmpp and adb which expect about 50 characters, which I all have to write before the timeout!" }, + ); + + my $pattern = $protocols{$p->{name}}->{data}; + + $opts{no_frag} = 1 if $protocols{$p->{name}}->{no_frag}; + + if ($p->{sni_hostnames} or $p->{alpn_protocols}) { + my $pname = make_sni_alpn_name($p); + + my @sni = @{$p->{sni_hostnames} // [""] }; + my @alpn = @{$p->{alpn_protocols} // [""] }; + + foreach my $sni ( @sni ) { + foreach my $alpn ( @alpn ) { + print "sni: $sni\nalpn: $alpn\n"; + $pattern = $protocols{tls}->{ + "data". ($sni ? "_sni" : "") . + ($alpn ? "_alpn": "") + }; + $pattern =~ s/(\$\w+)/$1/eeg; + + test_probe( + data => $pattern, + expected => $pname, + %opts + ); + } + } + } else { + test_probe( + data => $pattern, + expected => $p->{name}, + %opts + ); -if (!($ssh_pid = fork)) { - exec "./echosrv --listen $ssh_address --prefix 'ssh: '"; + } + } } -if (!($ssl_pid = fork)) { - exec "./echosrv --listen $ssl_address --prefix 'ssl: '"; + + +# Start an echoserver for each service +foreach my $s (@{$conf->fetch_array("protocols")}) { + my $prefix = $s->{name}; + + $prefix =~ s/^ssl/tls/; # To remove in 1.21 + + if ($s->{sni_hostnames} or $s->{alpn_protocols}) { + $prefix = make_sni_alpn_name($s); + } + + verbose_exec "./echosrv --listen $s->{host}:$s->{port} --prefix '$prefix: '"; } + my @binaries = ('sslh-select', 'sslh-fork'); for my $binary (@binaries) { warn "Testing $binary\n"; @@ -56,10 +195,10 @@ my $sslh_pid; if (!($sslh_pid = fork)) { 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"; - warn "$cmd\n"; - #exec $cmd; - exec "valgrind --leak-check=full ./$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"; + my $cmd = "./$binary -v -f -u $user -Ftest.cfg"; + verbose_exec $cmd; + #exec "valgrind --leak-check=full ./$binary -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address -ssl $ssl_address -P $pidfile"; exit 0; } warn "spawned $sslh_pid\n"; @@ -67,21 +206,7 @@ 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 - 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 $ssl_test_data; - my $data; - my $n = sysread $cnx_l, $data, 1024; - is($data, "ssl: $ssl_test_data", "SSL connection"); - } - } + my $ssl_test_data = "\x16\x03\x01\x00\xab\x01\x00\x00\xa7\x03\x03\x89\x22\x33\x95\x43\x7a\xc3\x89\x45\x51\x12\x3c\x28\x24\x1b\x6a\x78\xbf\xbe\x95\xd8\x90\x58\xd7\x65\xf7\xbb\x2d\xb2\x8d\xa0\x75\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x46\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x16\x00\x00\x00\x17\x00\x00hello tls alone"; # Test: Shy SSH connection if ($SSH_SHY_CNX) { @@ -96,35 +221,6 @@ } } -# Test: Bold SSH connection - if ($SSH_BOLD_CNX) { - print "***Test: Bold SSH connection\n"; - my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); - warn "$!\n" unless $cnx_h; - if (defined $cnx_h) { - my $td = "SSH-2.0 testsuite\t$test_data"; - print $cnx_h $td; - my $data = <$cnx_h>; - is($data, "ssh: $td", "Bold SSH connection"); - } - } - -# Test: PROBE_AGAIN, incomplete first frame - if ($SSH_PROBE_AGAIN) { - print "***Test: incomplete SSH first frame\n"; - my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); - warn "$!\n" unless $cnx_h; - if (defined $cnx_h) { - my $td = "SSH-2.0 testsuite\t$test_data"; - print $cnx_h substr $td, 0, 2; - sleep 1; - print $cnx_h substr $td, 2; - my $data = <$cnx_h>; - is($data, "ssh: $td", "Incomplete first SSH frame"); - } - } - - # Test: One SSL half-started then one SSH if ($SSL_MIX_SSH) { print "***Test: One SSL half-started then one SSH\n"; @@ -142,7 +238,7 @@ } my $data; my $n = sysread $cnx_l, $data, 1024; - is($data, "ssl: $ssl_test_data", "SSL connection interrupted by SSH"); + is($data, "tls: $ssl_test_data", "SSL connection interrupted by SSH"); } } @@ -159,7 +255,7 @@ print $cnx_l $ssl_test_data; my $data; my $n = sysread $cnx_l, $data, 1024; - is($data, "ssl: $ssl_test_data", "SSL during SSH being established"); + is($data, "tls: $ssl_test_data", "SSL during SSH being established"); } print $cnx_h $test_data; my $data = <$cnx_h>; @@ -168,53 +264,12 @@ } -# Test: Big messages (careful: don't go over echosrv's buffer limit (1M)) - if ($BIG_MSG) { - print "***Test: big message\n"; - my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); - warn "$!\n" unless $cnx_l; - my $rept = 1000; - my $test_data2 = $ssl_test_data . ("helloworld"x$rept); - if (defined $cnx_l) { - my $n = syswrite $cnx_l, $test_data2; - my ($data); - $n = sysread $cnx_l, $data, 1 << 20; - is($data, "ssl: ". $test_data2, "Big message"); - } + if ($PROBES_NOFRAG) { + test_probes(no_frag => 1); } -# Test: Stalled connection -# Create two connections, stall one, check the other one -# 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) { - print "***Test: Stalled connection\n"; - my $cnx_1 = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); - warn "$!\n" unless defined $cnx_1; - my $cnx_2 = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); - warn "$!\n" unless defined $cnx_2; - my $test_data2 = "helloworld"; - sleep 4; - my $rept = 1000; - if (defined $cnx_1 and defined $cnx_2) { - print $cnx_1 ($test_data2 x $rept); - print $cnx_1 "\n"; - print $cnx_2 ($test_data2 x $rept); - print $cnx_2 "\n"; - my $data = <$cnx_2>; - is($data, "ssh: " . ($test_data2 x $rept) . "\n", "Stalled connection (1)"); - print $cnx_2 ($test_data2 x $rept); - print $cnx_2 "\n"; - $data = <$cnx_2>; - is($data, "ssh: " . ($test_data2 x $rept) . "\n", "Stalled connection (2)"); - $data = <$cnx_1>; - is($data, "ssh: " . ($test_data2 x $rept) . "\n", "Stalled connection (3)"); - - } + if ($PROBES_AGAIN) { + test_probes; } my $pid = `cat $pidfile`; @@ -228,7 +283,6 @@ print "***Test: Connecting to non-existant server\n"; my $sslh_pid; if (!($sslh_pid = fork)) { - my $user = (getpwuid $<)[0]; # Run under current username exec "./sslh-select -v -f -u $user --listen localhost:$sslh_port --ssh localhost:$no_listen --ssl localhost:$no_listen -P $pidfile"; } warn "spawned $sslh_pid\n"; @@ -249,12 +303,19 @@ } +my $ssh_conf = (grep { $_->{name} eq "ssh" } @{$conf->fetch_array("protocols")})[0]; +my $ssh_address = $ssh_conf->{host} . ":" . $ssh_conf->{port}; + +# Use the last TLS echoserv (no SNI/ALPN) +my $ssl_conf = (grep { $_->{name} eq "tls" } @{$conf->fetch_array("protocols")})[-1]; +my $ssl_address = $ssl_conf->{host} . ":" . $ssl_conf->{port}; + + # Robustness: No hostname in address if ($RB_PARAM_NOHOST) { print "***Test: No hostname in address\n"; my $sslh_pid; if (!($sslh_pid = fork)) { - my $user = (getpwuid $<)[0]; # Run under current username exec "./sslh-select -v -f -u $user --listen $sslh_port --ssh $ssh_address --ssl $ssl_address -P $pidfile"; } warn "spawned $sslh_pid\n"; @@ -269,7 +330,6 @@ print "***Test: Changing to non-existant username\n"; my $sslh_pid; if (!($sslh_pid = fork)) { - my $user = (getpwuid $<)[0]; # Run under current username exec "./sslh-select -v -f -u ${user}_doesnt_exist --listen localhost:$sslh_port --ssh $ssh_address --ssl $ssl_address -P $pidfile"; } warn "spawned $sslh_pid\n"; @@ -284,7 +344,6 @@ print "***Test: Can't open PID file\n"; my $sslh_pid; if (!($sslh_pid = fork)) { - my $user = (getpwuid $<)[0]; # Run under current username exec "./sslh-select -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address --ssl $ssl_address -P /dont_exist/$pidfile"; # You don't have a /dont_exist/ directory, do you?! } diff -Nru sslh-1.18/test.cfg sslh-1.20/test.cfg --- sslh-1.18/test.cfg 1970-01-01 00:00:00.000000000 +0000 +++ sslh-1.20/test.cfg 2018-11-20 21:58:41.000000000 +0000 @@ -0,0 +1,42 @@ +# Configuration file for testing (use both by sslh under +# test and the test script `t`) + +verbose: 2; +foreground: true; +inetd: false; +numeric: false; +transparent: false; +timeout: 10; # Probe test writes slowly +pidfile: "/tmp/sslh_test.pid"; + +syslog_facility: "auth"; + + +# List of interfaces on which we should listen +# Options: +listen: +( + { host: "localhost"; port: "8080"; keepalive: true; }, + { host: "localhost"; port: "8081"; keepalive: true; } +); + + +protocols: +( + { name: "ssh"; host: "localhost"; port: "9000"; fork: true; }, + { name: "socks5"; host: "localhost"; port: "9001"; }, + { name: "http"; host: "localhost"; port: "9002"; }, + { name: "tinc"; host: "localhost"; port: "9003"; }, + { name: "openvpn"; host: "localhost"; port: "9004"; }, + { name: "xmpp"; host: "localhost"; port: "9009"; }, + { name: "adb"; host: "localhost"; port: "9010"; }, + { name: "tls"; host: "localhost"; port: "9021"; alpn_protocols: [ "alpn1", "alpn2" ]; sni_hostnames: [ "sni1" ]; }, + { name: "ssl"; host: "localhost"; port: "9022"; alpn_protocols: [ "alpn1", "alpn2" ]; sni_hostnames: [ "sni2", "sni3" ]; }, + { name: "tls"; host: "localhost"; port: "9023"; alpn_protocols: [ "alpn3" ]; }, + { name: "tls"; host: "localhost"; port: "9024"; sni_hostnames: [ "sni3" ]; }, + { name: "ssl"; host: "localhost"; port: "9025"; }, + { name: "anyprot"; host: "localhost"; port: "9099"; } +); + +on-timeout: "ssh"; + diff -Nru sslh-1.18/tls.c sslh-1.20/tls.c --- sslh-1.18/tls.c 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/tls.c 2018-11-20 21:58:41.000000000 +0000 @@ -30,6 +30,7 @@ */ #include #include /* malloc() */ +#include /* fnmatch() */ #include "tls.h" #define TLS_HEADER_LEN 5 @@ -40,9 +41,13 @@ #define MIN(X, Y) ((X) < (Y) ? (X) : (Y)) #endif +typedef struct { + int tls_match_sni : 1; + int tls_match_alpn : 1; +} TLS_MATCHMODE; struct TLSProtocol { - int use_alpn; + TLS_MATCHMODE match_mode; char** sni_hostname_list; char** alpn_protocol_list; }; @@ -56,12 +61,9 @@ * hello handshake, returning a status code * * Returns: - * >=0 - length of the hostname and updates *hostname - * caller is responsible for freeing *hostname - * -1 - Incomplete request - * -2 - No Host header included in this request - * -3 - Invalid hostname pointer - * < -4 - Invalid TLS client hello + * 0: no match + * 1: match + * < 0: error code (see tls.h) */ int parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_t data_len) { @@ -73,12 +75,12 @@ /* Check that our TCP payload is at least large enough for a TLS header */ if (data_len < TLS_HEADER_LEN) - return -1; + return TLS_ELENGTH; tls_content_type = data[0]; if (tls_content_type != TLS_HANDSHAKE_CONTENT_TYPE) { if (verbose) fprintf(stderr, "Request did not begin with TLS handshake.\n"); - return -5; + return TLS_EPROTOCOL; } tls_version_major = data[1]; @@ -87,7 +89,7 @@ if (verbose) fprintf(stderr, "Received SSL %d.%d handshake which cannot be parsed.\n", tls_version_major, tls_version_minor); - return -2; + return TLS_EVERSION; } /* TLS record length */ @@ -97,18 +99,18 @@ /* Check we received entire TLS record length */ if (data_len < len) - return -1; + return TLS_ELENGTH; /* * Handshake */ if (pos + 1 > data_len) { - return -5; + return TLS_EPROTOCOL; } if (data[pos] != TLS_HANDSHAKE_TYPE_CLIENT_HELLO) { if (verbose) fprintf(stderr, "Not a client hello\n"); - return -5; + return TLS_EPROTOCOL; } /* Skip past fixed length records: @@ -122,46 +124,54 @@ /* Session ID */ if (pos + 1 > data_len) - return -5; + return TLS_EPROTOCOL; len = (unsigned char)data[pos]; pos += 1 + len; /* Cipher Suites */ if (pos + 2 > data_len) - return -5; + return TLS_EPROTOCOL; len = ((unsigned char)data[pos] << 8) + (unsigned char)data[pos + 1]; pos += 2 + len; /* Compression Methods */ if (pos + 1 > data_len) - return -5; + return TLS_EPROTOCOL; len = (unsigned char)data[pos]; pos += 1 + len; if (pos == data_len && tls_version_major == 3 && tls_version_minor == 0) { if (verbose) fprintf(stderr, "Received SSL 3.0 handshake without extensions\n"); - return -2; + return TLS_EVERSION; } /* Extensions */ if (pos + 2 > data_len) - return -5; + return TLS_EPROTOCOL; len = ((unsigned char)data[pos] << 8) + (unsigned char)data[pos + 1]; pos += 2; if (pos + len > data_len) - return -5; - return parse_extensions(tls_data, data + pos, len); + return TLS_EPROTOCOL; + + /* By now we know it's TLS. if SNI or ALPN is set, parse extensions to see if + * they match. Otherwise, it's a match already */ + if (tls_data && + (tls_data->match_mode.tls_match_alpn || tls_data->match_mode.tls_match_sni)) { + return parse_extensions(tls_data, data + pos, len); + } else { + return TLS_MATCH; + } } static int parse_extensions(const struct TLSProtocol *tls_data, const char *data, size_t data_len) { size_t pos = 0; size_t len; - int last_matched = 0; + int sni_match = 0, alpn_match = 0; if (tls_data == NULL) - return -3; + return TLS_EINVAL; /* Parse each 4 bytes for the extension header */ while (pos + 4 <= data_len) { @@ -170,51 +180,17 @@ (unsigned char) data[pos + 3]; if (pos + 4 + len > data_len) - return -5; + return TLS_EPROTOCOL; size_t extension_type = ((unsigned char) data[pos] << 8) + (unsigned char) data[pos + 1]; - - /* Check if it's a server name extension */ - /* There can be only one extension of each type, so we break - our state and move pos to beginning of the extension here */ - if (tls_data->use_alpn == 2) { - /* we want BOTH alpn and sni to match */ - if (extension_type == 0x00) { /* Server Name */ - if (parse_server_name_extension(tls_data, data + pos + 4, len)) { - /* SNI matched */ - if(last_matched) { - /* this is only true if ALPN matched, so return true */ - return last_matched; - } else { - /* otherwise store that SNI matched */ - last_matched = 1; - } - } else { - // both can't match - return -2; - } - } else if (extension_type == 0x10) { /* ALPN */ - if (parse_alpn_extension(tls_data, data + pos + 4, len)) { - /* ALPN matched */ - if(last_matched) { - /* this is only true if SNI matched, so return true */ - return last_matched; - } else { - /* otherwise store that ALPN matched */ - last_matched = 1; - } - } else { - // both can't match - return -2; - } - } - - } else if (extension_type == 0x00 && tls_data->use_alpn == 0) { /* Server Name */ - return parse_server_name_extension(tls_data, data + pos + 4, len); - } else if (extension_type == 0x10 && tls_data->use_alpn == 1) { /* ALPN */ - return parse_alpn_extension(tls_data, data + pos + 4, len); + if (extension_type == 0x00 && tls_data->match_mode.tls_match_sni) { /* Server Name */ + sni_match = parse_server_name_extension(tls_data, data + pos + 4, len); + if (sni_match < 0) return sni_match; + } else if (extension_type == 0x10 && tls_data->match_mode.tls_match_alpn) { /* ALPN */ + alpn_match = parse_alpn_extension(tls_data, data + pos + 4, len); + if (alpn_match < 0) return alpn_match; } pos += 4 + len; /* Advance to the next extension header */ @@ -222,9 +198,11 @@ /* Check we ended where we expected to */ if (pos != data_len) - return -5; + return TLS_EPROTOCOL; - return -2; + return (sni_match && alpn_match) + || (!tls_data->match_mode.tls_match_sni && alpn_match) + || (!tls_data->match_mode.tls_match_alpn && sni_match); } static int @@ -237,14 +215,14 @@ (unsigned char)data[pos + 2]; if (pos + 3 + len > data_len) - return -5; + return TLS_EPROTOCOL; switch (data[pos]) { /* name type */ case 0x00: /* host_name */ if(has_match(tls_data->sni_hostname_list, data + pos + 3, len)) { return len; } else { - return -2; + return TLS_ENOEXT; } default: if (verbose) fprintf(stderr, "Unknown server name extension name type: %d\n", @@ -254,9 +232,9 @@ } /* Check we ended where we expected to */ if (pos != data_len) - return -5; + return TLS_EPROTOCOL; - return -2; + return TLS_ENOEXT; } static int @@ -268,7 +246,7 @@ len = (unsigned char)data[pos]; if (pos + 1 + len > data_len) - return -5; + return TLS_EPROTOCOL; if (len > 0 && has_match(tls_data->alpn_protocol_list, data + pos + 1, len)) { return len; @@ -279,30 +257,36 @@ } /* Check we ended where we expected to */ if (pos != data_len) - return -5; + return TLS_EPROTOCOL; - return -2; + return TLS_ENOEXT; } static int has_match(char** list, const char* name, size_t name_len) { char **item; + char *name_nullterminated = malloc(name_len+1); + CHECK_ALLOC(name_nullterminated, "malloc"); + memcpy(name_nullterminated, name, name_len); + name_nullterminated[name_len]='\0'; for (item = list; *item; item++) { if (verbose) fprintf(stderr, "matching [%.*s] with [%s]\n", (int)name_len, name, *item); - if(!strncmp(*item, name, name_len)) { + if(!fnmatch(*item, name_nullterminated, 0)) { + free(name_nullterminated); return 1; } } + free(name_nullterminated); return 0; } struct TLSProtocol * new_tls_data() { struct TLSProtocol *tls_data = malloc(sizeof(struct TLSProtocol)); - if (tls_data != NULL) { - tls_data->use_alpn = -1; - } + CHECK_ALLOC(tls_data, "malloc"); + + memset(tls_data, 0, sizeof(*tls_data)); return tls_data; } @@ -311,16 +295,10 @@ tls_data_set_list(struct TLSProtocol *tls_data, int alpn, char** list) { if (alpn) { tls_data->alpn_protocol_list = list; - if(tls_data->use_alpn == 0) - tls_data->use_alpn = 2; - else - tls_data->use_alpn = 1; + tls_data->match_mode.tls_match_alpn = 1; } else { tls_data->sni_hostname_list = list; - if(tls_data->use_alpn == 1) - tls_data->use_alpn = 2; - else - tls_data->use_alpn = 0; + tls_data->match_mode.tls_match_sni = 1; } return tls_data; diff -Nru sslh-1.18/tls.h sslh-1.20/tls.h --- sslh-1.18/tls.h 2016-03-29 19:19:05.000000000 +0000 +++ sslh-1.20/tls.h 2018-11-20 21:58:41.000000000 +0000 @@ -35,4 +35,14 @@ struct TLSProtocol *new_tls_data(); struct TLSProtocol *tls_data_set_list(struct TLSProtocol *, int, char**); +#define TLS_MATCH 1 +#define TLS_NOMATCH 0 + +#define TLS_EINVAL -1 /* Invalid parameter (NULL data pointer) */ +#define TLS_ELENGTH -2 /* Incomplete request */ +#define TLS_EVERSION -3 /* TLS version that cannot be parsed */ +#define TLS_ENOEXT -4 /* No ALPN or SNI extension found */ +#define TLS_EPROTOCOL -5 /* Protocol error */ + + #endif