diff --git a/ChangeLog b/ChangeLog index 422445b..847bd41 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,23 @@ -vNEXT: +v1.18: 29MAR2016 + Added USELIBPCRE to make use of regex engine + optional. + + Added support for RFC4366 SNI and RFC7301 ALPN + (Travis Burtrum) + + Changed connection log to include the name of the probe that + triggered. + + Changed configuration file format: 'probe' field is + no longer required, 'name' field can now contain + 'tls' or 'regex', with corresponding options (see + example.cfg) + Added 'log_level' option to each protocol, which + allows to turn off generation of log at each + connection. + Added 'keepalive' option. + +v1.17: 09MAR2015 Support RFC5952-style IPv6 addresses, e.g. [::]:443. Transparant proxy support for FreeBSD. diff --git a/Makefile b/Makefile index ef77957..b1cf9ce 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,16 @@ # Configuration 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) USELIBWRAP?= # Use libwrap? USELIBCAP= # Use libcap? +USESYSTEMD= # Make use of systemd socket activation COV_TEST= # Perform test coverage? -PREFIX=/usr/local +PREFIX?=/usr +BINDIR?=$(PREFIX)/sbin +MANDIR?=$(PREFIX)/share/man/man8 MAN=sslh.8.gz # man page name @@ -20,13 +25,22 @@ CC ?= gcc CFLAGS ?=-Wall -g $(CFLAGS_COV) LIBS= -OBJS=common.o sslh-main.o probe.o +OBJS=common.o sslh-main.o probe.o tls.o ifneq ($(strip $(USELIBWRAP)),) LIBS:=$(LIBS) -lwrap CPPFLAGS+=-DLIBWRAP endif +ifneq ($(strip $(ENABLE_REGEX)),) + CPPFLAGS+=-DENABLE_REGEX +endif + +ifneq ($(strip $(USELIBPCRE)),) + CPPFLAGS+=-DLIBPCRE + LIBS:=$(LIBS) -lpcre +endif + ifneq ($(strip $(USELIBCONFIG)),) LIBS:=$(LIBS) -lconfig CPPFLAGS+=-DLIBCONFIG @@ -37,6 +51,12 @@ ifneq ($(strip $(USELIBCAP)),) CPPFLAGS+=-DLIBCAP endif +ifneq ($(strip $(USESYSTEMD)),) + LIBS:=$(LIBS) -lsystemd + CPPFLAGS+=-DSYSTEMD +endif + + all: sslh $(MAN) echosrv .c.o: *.h @@ -55,8 +75,11 @@ sslh-select: version.h $(OBJS) sslh-select.o Makefile common.h $(CC) $(CFLAGS) $(LDFLAGS) -o sslh-select sslh-select.o $(OBJS) $(LIBS) #strip sslh-select +systemd-sslh-generator: systemd-sslh-generator.o + $(CC) $(CFLAGS) $(LDFLAGS) -o systemd-sslh-generator systemd-sslh-generator.o -lconfig + echosrv: $(OBJS) echosrv.o - $(CC) $(CFLAGS) $(LDFLAGS) -o echosrv echosrv.o probe.o common.o $(LIBS) + $(CC) $(CFLAGS) $(LDFLAGS) -o echosrv echosrv.o probe.o common.o tls.o $(LIBS) $(MAN): sslh.pod Makefile pod2man --section=8 --release=$(VERSION) --center=" " sslh.pod | gzip -9 - > $(MAN) @@ -68,8 +91,10 @@ release: # generic install: install binary and man page install: sslh $(MAN) - install -pD sslh-fork $(DESTDIR)$(PREFIX)/sbin/sslh - install -pD -m 0644 $(MAN) $(DESTDIR)$(PREFIX)/share/man/man8/$(MAN) + mkdir -p $(DESTDIR)/$(BINDIR) + mkdir -p $(DESTDIR)/$(MANDIR) + install -p sslh-fork $(DESTDIR)/$(BINDIR)/sslh + install -p -m 0644 $(MAN) $(DESTDIR)/$(MANDIR)/$(MAN) # "extended" install for Debian: install startup script install-debian: install sslh $(MAN) @@ -78,14 +103,14 @@ install-debian: install sslh $(MAN) update-rc.d sslh defaults uninstall: - rm -f $(DESTDIR)$(PREFIX)/sbin/sslh $(DESTDIR)$(PREFIX)/share/man/man8/$(MAN) $(DESTDIR)/etc/init.d/sslh $(DESTDIR)/etc/default/sslh + rm -f $(DESTDIR)$(BINDIR)/sslh $(DESTDIR)$(MANDIR)/$(MAN) $(DESTDIR)/etc/init.d/sslh $(DESTDIR)/etc/default/sslh update-rc.d sslh remove distclean: clean rm -f tags cscope.* clean: - rm -f sslh-fork sslh-select echosrv version.h $(MAN) *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info + rm -f sslh-fork sslh-select echosrv version.h $(MAN) systemd-sslh-generator *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info tags: ctags --globals -T *.[ch] @@ -96,4 +121,3 @@ cscope: test: ./t - diff --git a/README.md b/README.md index 7000160..ceb781a 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,8 @@ of the Makefile: file. You will need `libconfig` headers to compile (`libconfig8-dev` in Debian). +* `USESYSTEMD` compiles support for using systemd socket activation. + You will need `systemd` headers to compile (`systemd-devel` in Fedora). Binaries -------- @@ -102,7 +104,7 @@ Installation * For CentOS: - cp scripts/etc.rc.d.init.d.sslh /etc/rc.d/init.d/sslh + cp scripts/etc.rc.d.init.d.sslh.centos /etc/rc.d/init.d/sslh You might need to create links in /etc/rc.d so that the server @@ -217,7 +219,7 @@ Transparent proxy support ------------------------- On Linux and FreeBSD you can use the `--transparent` option to -request transparent proying. This means services behind `sslh` +request transparent proxying. This means services behind `sslh` (Apache, `sshd` and so on) will see the external IP and ports as if the external world connected directly to them. This simplifies IP-based access control (or makes it possible at @@ -303,6 +305,90 @@ explicit IP addresses (or names): This will not work: sslh --listen 192.168.0.1:443 --ssh 127.0.0.1:22 --ssl 127.0.0.1:4443 + +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. + +Systemd Socket Activation +------------------------- +If compiled with `USESYSTEMD` then it is possible to activate +the service on demand and avoid running any code as root. + +In this mode any listen configuration options are ignored and +the sockets are passed by systemd to the service. + +Example socket unit: + + [Unit] + Before=sslh.service + + [Socket] + ListenStream=1.2.3.4:443 + ListenStream=5.6.7.8:444 + ListenStream=9.10.11.12:445 + FreeBind=true + + [Install] + WantedBy=sockets.target + +Example service unit: + + [Unit] + PartOf=sslh.socket + + [Service] + ExecStart=/usr/sbin/sslh -v -f --ssh 127.0.0.1:22 --ssl 127.0.0.1:443 + KillMode=process + CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_SETGID CAP_SETUID + PrivateTmp=true + PrivateDevices=true + ProtectSystem=full + ProtectHome=true + User=sslh + + +With this setup only the socket needs to be enabled. The sslh service +will be started on demand and does not need to run as root to bind the +sockets as systemd has already bound and passed them over. If the sslh +service is started on its own without the sockets being passed by systemd +then it will look to use those defined on the command line or config +file as usual. Any number of ListenStreams can be defined in the socket +file and systemd will pass them all over to sslh to use as usual. + +To avoid inconsistency between starting via socket and starting directly +via the service Requires=sslh.socket can be added to the service unit to +mandate the use of the socket configuration. + +Rather than overwriting the entire socket file drop in values can be placed +in /etc/systemd/system/sslh.socket.d/.conf with additional ListenStream +values that will be merged. + +In addition to the above with manual .socket file configuration there is an +optional systemd generator which can be compiled - systemd-sslh-generator + +This parses the /etc/sslh.cfg (or /etc/sslh/sslh.cfg file if that exists +instead) configuration file and dynamically generates a socket file to use. + +This will also merge with any sslh.socket.d drop in configuration but will be +overriden by a /etc/systemd/system/sslh.socket file. + +To use the generator place it in /usr/lib/systemd/system-generators and then +call systemctl daemon-reload after any changes to /etc/sslh.cfg to generate +the new dynamic socket unit. Fail2ban -------- diff --git a/basic.cfg b/basic.cfg index 526ffbf..54a799c 100644 --- a/basic.cfg +++ b/basic.cfg @@ -6,7 +6,7 @@ foreground: false; inetd: false; numeric: false; transparent: false; -timeout: "2"; +timeout: 2; user: "nobody"; pidfile: "/var/run/sslh.pid"; @@ -19,11 +19,11 @@ listen: protocols: ( - { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; probe: "builtin"; }, - { name: "openvpn"; host: "localhost"; port: "1194"; probe: "builtin"; }, - { name: "xmpp"; host: "localhost"; port: "5222"; probe: "builtin"; }, - { name: "http"; host: "localhost"; port: "80"; probe: "builtin"; }, - { name: "ssl"; host: "localhost"; port: "443"; probe: "builtin"; }, - { name: "anyprot"; host: "localhost"; port: "443"; probe: "builtin"; } + { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; }, + { name: "openvpn"; host: "localhost"; port: "1194"; }, + { name: "xmpp"; host: "localhost"; port: "5222"; }, + { name: "http"; host: "localhost"; port: "80"; }, + { name: "ssl"; host: "localhost"; port: "443"; log_level: 0; }, + { name: "anyprot"; host: "localhost"; port: "443"; } ); diff --git a/common.c b/common.c index 100753a..8187b4d 100644 --- a/common.c +++ b/common.c @@ -8,16 +8,26 @@ #include #include +#include +#include +#include + #include "common.h" #include "probe.h" -/* Added to make the code compilable under CYGWIN +/* Added to make the code compilable under CYGWIN * */ #ifndef SA_NOCLDWAIT #define SA_NOCLDWAIT 0 #endif -/* +/* Make use of systemd socket activation + * */ +#ifdef SYSTEMD +#include +#endif + +/* * Settings that depend on the command line. They're set in main(), but also * used in other places in common.c, and it'd be heavy-handed to pass it all as * parameters @@ -27,7 +37,6 @@ int probing_timeout = 2; int inetd = 0; int foreground = 0; int background = 0; -int transparent = 0; int numeric = 0; const char *user_name, *pid_file; @@ -44,14 +53,35 @@ void check_res_dumpdie(int res, struct addrinfo *addr, char* syscall) char buf[NI_MAXHOST]; if (res == -1) { - fprintf(stderr, "%s:%s: %s\n", - sprintaddr(buf, sizeof(buf), addr), - syscall, + fprintf(stderr, "%s:%s: %s\n", + sprintaddr(buf, sizeof(buf), addr), + syscall, strerror(errno)); exit(1); } } +int get_fd_sockets(int *sockfd[]) +{ + int sd = 0; + +#ifdef SYSTEMD + sd = sd_listen_fds(0); + if (sd < 0) { + fprintf(stderr, "sd_listen_fds(): %s\n", strerror(-sd)); + exit(1); + } + if (sd > 0) { + *sockfd = malloc(sd * sizeof(*sockfd[0])); + for (int i = 0; i < sd; i++) { + (*sockfd)[i] = SD_LISTEN_FDS_START + i; + } + } +#endif + + return sd; +} + /* Starts listening sockets on specified addresses. * IN: addr[], num_addr * OUT: *sockfd[] pointer to newly-allocated array of file descriptors @@ -64,6 +94,13 @@ int start_listen_sockets(int *sockfd[], struct addrinfo *addr_list) struct addrinfo *addr; int i, res, one; int num_addr = 0; + int sd_socks = 0; + + sd_socks = get_fd_sockets(sockfd); + + if (sd_socks > 0) { + return sd_socks; + } for (addr = addr_list; addr; addr = addr->ai_next) num_addr++; @@ -87,6 +124,12 @@ int start_listen_sockets(int *sockfd[], struct addrinfo *addr_list) res = setsockopt((*sockfd)[i], SOL_SOCKET, SO_REUSEADDR, (char*)&one, sizeof(one)); check_res_dumpdie(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"); + } + if (IP_FREEBIND) { res = setsockopt((*sockfd)[i], IPPROTO_IP, IP_FREEBIND, (char*)&one, sizeof(one)); check_res_dumpdie(res, addr, "setsockopt(IP_FREEBIND)"); @@ -120,6 +163,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 = (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; + } + 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; + } + } + 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"); @@ -141,7 +217,7 @@ int bind_peer(int fd, int fd_from) } /* Connect to first address that works and returns a file descriptor, or -1 if - * none work. + * none work. * If transparent proxying is on, use fd_from peer address on external address * of new file descriptor. */ int connect_addr(struct connection *cnx, int fd_from) @@ -149,7 +225,7 @@ int connect_addr(struct connection *cnx, int fd_from) struct addrinfo *a, from; struct sockaddr_storage ss; char buf[NI_MAXHOST]; - int fd, res; + int fd, res, one; memset(&from, 0, sizeof(from)); from.ai_addr = (struct sockaddr*)&ss; @@ -160,10 +236,10 @@ int connect_addr(struct connection *cnx, int fd_from) for (a = cnx->proto->saddr; a; a = a->ai_next) { /* When transparent, make sure both connections use the same address family */ - if (transparent && a->ai_family != from.ai_addr->sa_family) + if (cnx->proto->transparent && a->ai_family != from.ai_addr->sa_family) continue; - if (verbose) - fprintf(stderr, "connecting to %s family %d len %d\n", + if (verbose) + fprintf(stderr, "connecting to %s family %d len %d\n", sprintaddr(buf, sizeof(buf), a), a->ai_addr->sa_family, a->ai_addrlen); @@ -173,16 +249,22 @@ int connect_addr(struct connection *cnx, int fd_from) log_message(LOG_ERR, "forward to %s failed:socket: %s\n", cnx->proto->description, strerror(errno)); } else { - if (transparent) { + if (cnx->proto->transparent) { res = bind_peer(fd, fd_from); CHECK_RES_RETURN(res, "bind_peer"); } res = connect(fd, a->ai_addr, a->ai_addrlen); if (res == -1) { - log_message(LOG_ERR, "forward to %s failed:connect: %s\n", + log_message(LOG_ERR, "forward to %s failed:connect: %s\n", cnx->proto->description, strerror(errno)); close(fd); } else { + if (cnx->proto->keepalive) { + 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; } } @@ -191,10 +273,10 @@ int connect_addr(struct connection *cnx, int fd_from) } /* Store some data to write to the queue later */ -int defer_write(struct queue *q, void* data, int data_size) +int defer_write(struct queue *q, void* data, int data_size) { char *p; - if (verbose) + if (verbose) fprintf(stderr, "**** writing deferred on fd %d\n", q->fd); p = realloc(q->begin_deferred_data, q->deferred_data_size + data_size); @@ -258,7 +340,7 @@ void dump_connection(struct connection *cnx) } -/* +/* * moves data from one fd to other * * returns number of bytes copied if success @@ -326,16 +408,16 @@ char* sprintaddr(char* buf, size_t size, struct addrinfo *a) int res; res = getnameinfo(a->ai_addr, a->ai_addrlen, - host, sizeof(host), - serv, sizeof(serv), + host, sizeof(host), + serv, sizeof(serv), numeric ? NI_NUMERICHOST | NI_NUMERICSERV : 0 ); if (res) { log_message(LOG_ERR, "sprintaddr:getnameinfo: %s\n", gai_strerror(res)); /* Name resolution failed: do it numerically instead */ res = getnameinfo(a->ai_addr, a->ai_addrlen, - host, sizeof(host), - serv, sizeof(serv), + host, sizeof(host), + serv, sizeof(serv), NI_NUMERICHOST | NI_NUMERICSERV); /* should not fail but... */ if (res) { @@ -350,7 +432,7 @@ char* sprintaddr(char* buf, size_t size, struct addrinfo *a) return buf; } -/* Turns a hostname and port (or service) into a list of struct addrinfo +/* 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) @@ -431,11 +513,14 @@ void log_connection(struct connection *cnx) local[MAX_NAMELENGTH], target[MAX_NAMELENGTH]; int res; + if (cnx->proto->log_level < 1) + return; + addr.ai_addr = (struct sockaddr*)&ss; addr.ai_addrlen = sizeof(ss); res = getpeername(cnx->q[0].fd, addr.ai_addr, &addr.ai_addrlen); - if (res == -1) return; /* Can happen if connection drops before we get here. + if (res == -1) return; /* Can happen if connection drops before we get here. In that case, don't log anything (there is no connection) */ sprintaddr(peer, sizeof(peer), &addr); @@ -454,7 +539,8 @@ void log_connection(struct connection *cnx) if (res == -1) return; sprintaddr(local, sizeof(local), &addr); - log_message(LOG_INFO, "connection from %s to %s forwarded from %s to %s\n", + log_message(LOG_INFO, "%s:connection from %s to %s forwarded from %s to %s\n", + cnx->proto->description, peer, service, local, @@ -471,16 +557,19 @@ void log_connection(struct connection *cnx) int check_access_rights(int in_socket, const char* service) { #ifdef LIBWRAP - struct sockaddr peeraddr; - socklen_t size = sizeof(peeraddr); + union { + struct sockaddr saddr; + struct sockaddr_storage ss; + } peer; + socklen_t size = sizeof(peer); char addr_str[NI_MAXHOST], host[NI_MAXHOST]; int res; - res = getpeername(in_socket, &peeraddr, &size); + res = getpeername(in_socket, &peer.saddr, &size); CHECK_RES_RETURN(res, "getpeername"); /* extract peer address */ - res = getnameinfo(&peeraddr, size, addr_str, sizeof(addr_str), NULL, 0, NI_NUMERICHOST); + res = getnameinfo(&peer.saddr, size, addr_str, sizeof(addr_str), NULL, 0, NI_NUMERICHOST); if (res) { if (verbose) fprintf(stderr, "getnameinfo(NI_NUMERICHOST):%s\n", gai_strerror(res)); @@ -489,7 +578,7 @@ int check_access_rights(int in_socket, const char* service) /* extract peer name */ strcpy(host, STRING_UNKNOWN); if (!numeric) { - res = getnameinfo(&peeraddr, size, host, sizeof(host), NULL, 0, NI_NAMEREQD); + res = getnameinfo(&peer.saddr, size, host, sizeof(host), NULL, 0, NI_NAMEREQD); if (res) { if (verbose) fprintf(stderr, "getnameinfo(NI_NAMEREQD):%s\n", gai_strerror(res)); @@ -534,7 +623,7 @@ void setup_signals(void) } -/* Open syslog connection with appropriate banner; +/* Open syslog connection with appropriate banner; * banner is made up of basename(bin_name)+"[pid]" */ void setup_syslog(const char* bin_name) { char *name1, *name2; @@ -544,7 +633,7 @@ void setup_syslog(const char* bin_name) { res = asprintf(&name2, "%s[%d]", basename(name1), getpid()); CHECK_RES_DIE(res, "asprintf"); openlog(name2, LOG_CONS, LOG_AUTH); - free(name1); + free(name1); /* Don't free name2, as openlog(3) uses it (at least in glibc) */ log_message(LOG_INFO, "%s %s started\n", server_type, VERSION); @@ -616,7 +705,7 @@ void drop_privileges(const char* user_name) /* 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 + * fails, which is fine because... we have no unwanted rights * (see POS36-C for security context) * */ setgroups(0, NULL); diff --git a/common.h b/common.h index db0ccc5..1002d23 100644 --- a/common.h +++ b/common.h @@ -1,5 +1,5 @@ -#ifndef __COMMON_H_ -#define __COMMON_H_ +#ifndef COMMON_H +#define COMMON_H /* FD_SETSIZE is 64 on Cygwin, which is really low. Just redefining it is * enough for the macros to adapt (http://support.microsoft.com/kb/111855) @@ -113,8 +113,7 @@ int start_listen_sockets(int *sockfd[], struct addrinfo *addr_list); int defer_write(struct queue *q, void* data, int data_size); int flush_deferred(struct queue *q); -extern int probing_timeout, verbose, inetd, foreground, - background, transparent, numeric; +extern int probing_timeout, verbose, inetd, foreground, background, numeric; extern struct sockaddr_storage addr_ssl, addr_ssh, addr_openvpn; extern struct addrinfo *addr_listen; extern const char* USAGE_STRING; diff --git a/example.cfg b/example.cfg index 20299c9..371bcf0 100644 --- a/example.cfg +++ b/example.cfg @@ -8,45 +8,84 @@ foreground: true; inetd: false; numeric: false; transparent: false; -timeout: "2"; +timeout: 2; user: "nobody"; pidfile: "/var/run/sslh.pid"; # List of interfaces on which we should listen +# Options: listen: ( { host: "thelonious"; port: "443"; }, - { host: "thelonious"; port: "8080"; } + { host: "thelonious"; port: "8080"; keepalive: true; } ); # List of protocols # # Each protocol entry consists of: -# name: name of the protocol +# name: name of the probe. These are listed on the command +# line (ssh -?), plus 'regex' and 'timeout'. + # service: (optional) libwrap service name (see hosts_access(5)) -# host: host name to connect that protocol -# port: port number to connect that protocol -# probe: "builtin" or a list of regular expressions -# (can be left out, e.g. to use with on-timeout) +# host, port: where to connect when this probe succeeds +# log_level: 0 to turn off logging +# 1 to log each incoming connection +# keepalive: Should TCP keepalive be on or off for that +# connection (default is off) +# +# Probe-specific options: +# 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 +# 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"; probe: "builtin"; }, - { name: "openvpn"; host: "localhost"; port: "1194"; probe: [ "^\x00[\x0D-\xFF]$", "^\x00[\x0D-\xFF]\x38" ]; }, - { name: "xmpp"; host: "localhost"; port: "5222"; probe: [ "jabber" ]; }, - { name: "http"; host: "localhost"; port: "80"; probe: "builtin"; }, - { name: "ssl"; host: "localhost"; port: "443"; probe: [ "" ]; }, + { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; keepalive: true; }, + { name: "http"; host: "localhost"; port: "80"; }, + +# match BOTH ALPN/SNI + { name: "tls"; host: "localhost"; port: "5223"; alpn_protocols: [ "xmpp-client" ]; sni_hostnames: [ "im.somethingelse.net" ]; log_level: 0;}, + +# just match ALPN + { name: "tls"; host: "localhost"; port: "443"; alpn_protocols: [ "h2", "http/1.1", "spdy/1", "spdy/2", "spdy/3" ]; log_level: 0; }, + { name: "tls"; host: "localhost"; port: "xmpp-client"; alpn_protocols: [ "xmpp-client" ]; log_level: 0;}, + +# just match SNI + { 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;}, + +# catch anything else TLS + { name: "tls"; host: "localhost"; port: "443"; }, + +# 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: [ "" ]; }, + +# Where to connect in case of timeout (defaults to ssh) { name: "timeout"; service: "daytime"; host: "localhost"; port: "daytime"; } ); # Optionally, specify to which protocol to connect in case # of timeout (defaults to "ssh"). -# You can timeout to any arbitrary address by setting a -# protocol with no probe, as is the case with this example. +# You can timeout to any arbitrary address by setting an +# entry in 'protocols' named "timeout". # This enables you to set a tcpd service name for this # protocol too. on-timeout: "timeout"; diff --git a/genver.sh b/genver.sh index 4d6e76a..79fd0a0 100755 --- a/genver.sh +++ b/genver.sh @@ -40,8 +40,8 @@ fi if [ $QUIET -ne 1 ]; then - printf "#ifndef _VERSION_H_ \n" - printf "#define _VERSION_H_ \n\n" + printf "#ifndef VERSION_H \n" + printf "#define VERSION_H \n\n" printf "#define VERSION \"$release\"\n" printf "#endif\n" else diff --git a/probe.c b/probe.c index 3a6306a..0bf9a6e 100644 --- a/probe.c +++ b/probe.c @@ -1,27 +1,33 @@ /* # probe.c: Code for probing protocols # -# Copyright (C) 2007-2012 Yves Rutschle -# +# Copyright (C) 2007-2015 Yves Rutschle +# # This program is free software; you can redistribute it # and/or modify it under the terms of the GNU General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later # version. -# +# # This program is distributed in the hope that it will be # useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more # details. -# +# # The full text for the General Public License is here: # http://www.gnu.org/licenses/gpl.html */ #define _GNU_SOURCE #include +#ifdef ENABLE_REGEX +#ifdef LIBPCRE +#include +#else #include +#endif +#endif #include #include "probe.h" @@ -33,20 +39,22 @@ static int is_tinc_protocol(const char *p, int len, struct proto*); static int is_xmpp_protocol(const char *p, int len, struct proto*); static int is_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_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 probe */ - { "ssh", "sshd", NULL, is_ssh_protocol}, - { "openvpn", NULL, NULL, is_openvpn_protocol }, - { "tinc", NULL, NULL, is_tinc_protocol }, - { "xmpp", NULL, NULL, is_xmpp_protocol }, - { "http", NULL, NULL, is_http_protocol }, - { "ssl", NULL, NULL, is_tls_protocol }, - { "tls", NULL, NULL, is_tls_protocol }, - { "anyprot", NULL, NULL, is_true } + /* description service saddr log_level keepalive transparent probe */ + { "ssh", "sshd", NULL, 1, 0, 0, is_ssh_protocol}, + { "openvpn", NULL, NULL, 1, 0, 0, is_openvpn_protocol }, + { "tinc", NULL, NULL, 1, 0, 0, is_tinc_protocol }, + { "xmpp", NULL, NULL, 1, 0, 0, is_xmpp_protocol }, + { "http", NULL, NULL, 1, 0, 0, is_http_protocol }, + { "ssl", NULL, NULL, 1, 0, 0, is_tls_protocol }, + { "tls", NULL, NULL, 1, 0, 0, is_tls_protocol }, + { "adb", NULL, NULL, 1, 0, 0, is_adb_protocol }, + { "anyprot", NULL, NULL, 1, 0, 0, is_true } }; static struct proto *protocols; @@ -67,10 +75,10 @@ void set_ontimeout(const char* name) CHECK_RES_DIE(res, "asprintf"); } -/* Returns the protocol to connect to in case of timeout; - * if not found, return the first protocol specified +/* Returns the protocol to connect to in case of timeout; + * if not found, return the first protocol specified */ -struct proto* timeout_protocol(void) +struct proto* timeout_protocol(void) { struct proto* p = get_first_protocol(); for (; p && strcmp(p->description, on_timeout); p = p->next); @@ -113,7 +121,7 @@ void hexdump(const char *mem, unsigned int len) if(j >= len) /* end of block, not really printing */ putchar(' '); else if(isprint(mem[j])) /* printable char */ - putchar(0xFF & mem[j]); + putchar(0xFF & mem[j]); else /* other char */ putchar('.'); } @@ -153,7 +161,8 @@ static int is_openvpn_protocol (const char*p,int len, struct proto *proto) } /* Is the buffer the beginning of a tinc connections? - * (protocol is undocumented, but starts with "0 " in 1.0.15) + * Protocol is documented here: http://www.tinc-vpn.org/documentation/tinc.pdf + * First connection starts with "0 " in 1.0.15) * */ static int is_tinc_protocol( const char *p, int len, struct proto *proto) { @@ -169,7 +178,10 @@ static int is_tinc_protocol( const char *p, int len, struct proto *proto) * */ static int is_xmpp_protocol( const char *p, int len, struct proto *proto) { - if (len < 6) + /* 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; @@ -209,6 +221,19 @@ static int is_http_protocol(const char *p, int len, struct proto *proto) return PROBE_NEXT; } +static int is_sni_alpn_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; + + /* There *was* a valid match */ + return PROBE_MATCH; +} + static int is_tls_protocol(const char *p, int len, struct proto *proto) { if (len < 3) @@ -221,8 +246,25 @@ static int is_tls_protocol(const char *p, int len, struct proto *proto) return p[0] == 0x16 && p[1] == 0x03 && ( p[2] >= 0 && p[2] <= 0x03); } +static int is_adb_protocol(const char *p, int len, struct proto *proto) +{ + if (len < 30) + 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. + */ + return !memcmp(&p[0], "CNXN", 4) && !memcmp(&p[24], "host:", 5); +} + static int regex_probe(const char *p, int len, struct proto *proto) { +#ifdef ENABLE_REGEX char *str; regex_t **probe = proto->data; regmatch_t pos = { 0, len }; @@ -240,11 +282,16 @@ static int regex_probe(const char *p, int len, struct proto *proto) /* try them all */; free(str); return (*probe != NULL); +#else + /* Should never happen as we check when loading config file */ + fprintf(stderr, "FATAL: regex probe called but not built in\n"); + exit(5); +#endif } -/* +/* * Read the beginning of data coming from the client connection and check if - * it's a known protocol. + * it's a known protocol. * Return PROBE_AGAIN if not enough data, or PROBE_MATCH if it succeeded in * which case cnx->proto is set to the appropriate protocol. */ @@ -276,9 +323,9 @@ int probe_client_protocol(struct connection *cnx) return res; } - if (verbose) - fprintf(stderr, - "all probes failed, connecting to first protocol: %s\n", + 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 @@ -301,7 +348,7 @@ static struct proto* get_protocol(const char* description) } /* Returns the probe for specified protocol: - * parameter is the description in builtins[], or "regex" + * parameter is the description in builtins[], or "regex" * */ T_PROBE* get_probe(const char* description) { struct proto* p = get_protocol(description); @@ -315,7 +362,14 @@ T_PROBE* get_probe(const char* description) { 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")) + return is_true; + return NULL; } - - diff --git a/probe.h b/probe.h index d79b795..492e42f 100644 --- a/probe.h +++ b/probe.h @@ -1,9 +1,10 @@ /* API for probe.c */ -#ifndef __PROBE_H_ -#define __PROBE_H_ +#ifndef PROBE_H +#define PROBE_H #include "common.h" +#include "tls.h" typedef enum { PROBE_NEXT, /* Enough data, probe failed -- it's some other protocol */ @@ -19,11 +20,17 @@ struct proto { const char* description; /* a string that says what it is (for logging and command-line parsing) */ const char* service; /* service name to do libwrap checks */ struct addrinfo *saddr; /* list of addresses to try and switch that protocol */ + int log_level; /* 0: No logging of connection + * 1: Log incoming connection + */ + int keepalive; /* 0: No keepalive ; 1: Set Keepalive for this connection */ + int transparent; /* 0: opaque proxy ; 1: transparent proxy */ /* function to probe that protocol; parameters are buffer and length * containing the data to probe, and a pointer to the protocol structure */ T_PROBE* probe; - void* data; /* opaque pointer ; used to pass list of regex to regex probe */ + /* opaque pointer ; used to pass list of regex to regex probe, or TLSProtocol struct to sni/alpn probe */ + void* data; struct proto *next; /* pointer to next protocol in list, NULL if last */ }; diff --git a/scripts/etc.init.d.sslh b/scripts/etc.init.d.sslh index 54aa0d1..790f066 100755 --- a/scripts/etc.init.d.sslh +++ b/scripts/etc.init.d.sslh @@ -27,7 +27,7 @@ DAEMON=$PREFIX/sbin/sslh start() { echo "Start services: sslh" - $DAEMON -F /etc/sslh.cfg + $DAEMON -F/etc/sslh.cfg logger -t ${tag} -p ${facility} -i 'Started sslh' } diff --git a/sslh-main.c b/sslh-main.c index 9cc8a06..b72a2c0 100644 --- a/sslh-main.c +++ b/sslh-main.c @@ -2,7 +2,7 @@ # main: processing of config file, command line options and start the main # loop. # -# Copyright (C) 2007-2014 Yves Rutschle +# Copyright (C) 2007-2016 Yves Rutschle # # This program is free software; you can redistribute it # and/or modify it under the terms of the GNU General Public @@ -25,7 +25,13 @@ #ifdef LIBCONFIG #include #endif +#ifdef ENABLE_REGEX +#ifdef LIBPCRE +#include +#else #include +#endif +#endif #include "common.h" #include "probe.h" @@ -33,7 +39,7 @@ const char* USAGE_STRING = "sslh " VERSION "\n" \ "usage:\n" \ -"\tsslh [-v] [-i] [-V] [-f] [-n] [--transparent] [-F ]\n" +"\tsslh [-v] [-i] [-V] [-f] [-n] [--transparent] [-F]\n" "\t[-t ] [-P ] -u -p [-p ...] \n" \ "%s\n\n" /* Dynamically built list of builtin protocols */ \ "\t[--on-timeout ]\n" \ @@ -43,7 +49,7 @@ const char* USAGE_STRING = "-n: numeric output\n" \ "-u: specify under which user 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" \ @@ -55,11 +61,14 @@ const char* USAGE_STRING = /* Constants for options that have no one-character shorthand */ #define OPT_ONTIMEOUT 257 +/* Global setting for transparent proxying */ +int g_transparent = 0; + static struct option const_options[] = { { "inetd", no_argument, &inetd, 1 }, { "foreground", no_argument, &foreground, 1 }, { "background", no_argument, &background, 1 }, - { "transparent", no_argument, &transparent, 1 }, + { "transparent", no_argument, &g_transparent, 1 }, { "numeric", no_argument, &numeric, 1 }, { "verbose", no_argument, &verbose, 1 }, { "user", required_argument, 0, 'u' }, @@ -117,16 +126,23 @@ static void printsettings(void) for (p = get_first_protocol(); p; p = p->next) { fprintf(stderr, - "%s addr: %s. libwrap service: %s family %d %d\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->saddr->ai_addr->sa_family, + p->keepalive ? "keepalive " : "", + p->transparent ? "transparent" : "" + ); } fprintf(stderr, "listening on:\n"); for (a = addr_listen; a; a = a->ai_next) { - fprintf(stderr, "\t%s\n", sprintaddr(buf, sizeof(buf), a)); + fprintf(stderr, + "\t%s\t[%s]\n", + sprintaddr(buf, sizeof(buf), a), + a->ai_flags & SO_KEEPALIVE ? "keepalive" : ""); } fprintf(stderr, "timeout: %d\non-timeout: %s\n", probing_timeout, timeout_protocol()->description); @@ -140,7 +156,7 @@ static void printsettings(void) static int config_listen(config_t *config, struct addrinfo **listen) { config_setting_t *setting, *addr; - int len, i; + int len, i, keepalive; const char *hostname, *port; setting = config_lookup(config, "listen"); @@ -156,12 +172,20 @@ static int config_listen(config_t *config, struct addrinfo **listen) return -1; } + keepalive = 0; + config_setting_lookup_bool(addr, "keepalive", &keepalive); + resolve_split_name(listen, hostname, port); /* getaddrinfo returned a list of addresses corresponding to the * specification; move the pointer to the end of that list before - * processing the next specification */ - for (; *listen; listen = &((*listen)->ai_next)); + * processing the next specification, while setting flags for + * start_listen_sockets() through ai_flags (which is not meant for + * that, but is only used as hint in getaddrinfo, so it's OK) */ + for (; *listen; listen = &((*listen)->ai_next)) { + if (keepalive) + (*listen)->ai_flags = SO_KEEPALIVE; + } } } @@ -174,6 +198,7 @@ static int config_listen(config_t *config, struct addrinfo **listen) #ifdef LIBCONFIG static void setup_regex_probe(struct proto *p, config_setting_t* probes) { +#ifdef ENABLE_REGEX int num_probes, errsize, i, res; char *err; const char * expr; @@ -201,6 +226,61 @@ static void setup_regex_probe(struct proto *p, config_setting_t* probes) exit(1); } } +#else + fprintf(stderr, "line %d: regex probe specified but not compiled in\n", config_setting_source_line(probes)); + exit(5); +#endif +} +#endif + +#ifdef LIBCONFIG +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; + char** sni_hostname_list; + + num_probes = config_setting_length(config_items); + if (!num_probes) { + fprintf(stderr, "%s: no %s specified\n", p->description, name); + return; + } + + max_server_name_len = 0; + for (i = 0; i < num_probes; i++) { + server_name_len = strlen(config_setting_get_string_elem(config_items, i)); + 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); + + for (i = 0; i < num_probes; i++) { + config_item = config_setting_get_string_elem(config_items, i); + sni_hostname_list[i] = malloc(max_server_name_len); + strcpy (sni_hostname_list[i], config_item); + if(verbose) fprintf(stderr, "%s: %s[%d]: %s\n", p->description, name, i, sni_hostname_list[i]); + } + + p->data = (void*)tls_data_set_list(p->data, alpn, sni_hostname_list); +} + +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(); + 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)) { + p->probe = get_probe("sni_alpn"); + setup_sni_alpn_list(p, sni_hostnames, "sni_hostnames", 0); + } + if(alpn_protocols && config_setting_is_array(alpn_protocols)) { + p->probe = get_probe("sni_alpn"); + setup_sni_alpn_list(p, alpn_protocols, "alpn_protocols", 1); + } } #endif @@ -210,7 +290,7 @@ static void setup_regex_probe(struct proto *p, config_setting_t* probes) #ifdef LIBCONFIG static int config_protocols(config_t *config, struct proto **prots) { - config_setting_t *setting, *prot, *probes; + config_setting_t *setting, *prot, *patterns; const char *hostname, *port, *name; int i, num_prots; struct proto *p, *prev = NULL; @@ -231,33 +311,34 @@ static int config_protocols(config_t *config, struct proto **prots) )) { p->description = name; config_setting_lookup_string(prot, "service", &(p->service)); + config_setting_lookup_bool(prot, "keepalive", &p->keepalive); + config_setting_lookup_bool(prot, "transparent", &p->transparent); + + if (config_setting_lookup_int(prot, "log_level", &p->log_level) == CONFIG_FALSE) { + p->log_level = 1; + } resolve_split_name(&(p->saddr), hostname, port); + p->probe = get_probe(name); + if (!p->probe || !strcmp(name, "sni_alpn")) { + fprintf(stderr, "line %d: %s: probe unknown\n", config_setting_source_line(prot), name); + exit(1); + } - probes = config_setting_get_member(prot, "probe"); - if (probes) { - if (config_setting_is_array(probes)) { - /* If 'probe' is an array, setup a regex probe using the - * array of strings as pattern */ - - setup_regex_probe(p, probes); - - } else { - /* if 'probe' is 'builtin', set the probe to the - * appropriate builtin protocol */ - if (!strcmp(config_setting_get_string(probes), "builtin")) { - p->probe = get_probe(name); - if (!p->probe) { - fprintf(stderr, "%s: no builtin probe for this protocol\n", name); - exit(1); - } - } else { - fprintf(stderr, "%s: illegal probe name\n", name); - exit(1); - } + /* Probe-specific options: regex patterns */ + if (!strcmp(name, "regex")) { + patterns = config_setting_get_member(prot, "regex_patterns"); + if (patterns && config_setting_is_array(patterns)) { + setup_regex_probe(p, patterns); } } + + /* Probe-specific options: SNI/ALPN */ + if (!strcmp(name, "tls")) { + setup_sni_alpn(p, prot); + } + } } } @@ -270,6 +351,7 @@ static int config_protocols(config_t *config, struct proto **prots) * in: *filename * out: *listen, a newly-allocated linked list of listen addrinfo * *prots, a newly-allocated linked list of protocols + * 1 on error, 0 on success */ #ifdef LIBCONFIG static int config_parse(char *filename, struct addrinfo **listen, struct proto **prots) @@ -280,13 +362,19 @@ static int config_parse(char *filename, struct addrinfo **listen, struct proto * config_init(&config); if (config_read_file(&config, filename) == CONFIG_FALSE) { - if (config_error_type(&config) == CONFIG_ERR_PARSE) { + /* If it's a parse error then there will be a line number for the failure + * an I/O error (such as non-existent file) will have the error line as 0 + */ + if (config_error_line(&config) != 0) { fprintf(stderr, "%s:%d:%s\n", filename, config_error_line(&config), config_error_text(&config)); exit(1); } + fprintf(stderr, "%s:%s\n", + filename, + config_error_text(&config)); return 1; } @@ -294,7 +382,7 @@ static int config_parse(char *filename, struct addrinfo **listen, struct proto * config_lookup_bool(&config, "inetd", &inetd); config_lookup_bool(&config, "foreground", &foreground); config_lookup_bool(&config, "numeric", &numeric); - config_lookup_bool(&config, "transparent", &transparent); + config_lookup_bool(&config, "transparent", &g_transparent); if (config_lookup_int(&config, "timeout", (int *)&timeout) == CONFIG_TRUE) { probing_timeout = timeout; @@ -363,10 +451,12 @@ static void cmdline_config(int argc, char* argv[], struct proto** prots) optind = 1; opterr = 0; /* we're missing protocol options at this stage so don't output errors */ while ((c = getopt_long_only(argc, argv, optstr, all_options, NULL)) != -1) { + if (c == 'v') { + verbose++; + } if (c == 'F') { config_filename = optarg; if (config_filename) { - fprintf(stderr, "config: %s\n", config_filename); res = config_parse(config_filename, &addr_listen, prots); } else { /* No configuration file specified -- try default file locations */ @@ -481,10 +571,13 @@ next_arg: set_protocol_list(prots); +/* If compiling with systemd socket support no need to require listen address */ +#ifndef SYSTEMD if (!addr_listen && !inetd) { fprintf(stderr, "No listening address specified; use at least one -p option\n"); exit(1); } +#endif /* Did command-line override foreground setting? */ if (background) @@ -521,6 +614,13 @@ int main(int argc, char *argv[]) num_addr_listen = start_listen_sockets(&listen_sockets, addr_listen); +#ifdef SYSTEMD + if (num_addr_listen < 1) { + fprintf(stderr, "No listening sockets found, restart sockets or specify addresses in config\n"); + exit(1); + } +#endif + if (!foreground) { if (fork() > 0) exit(0); /* Detach */ diff --git a/sslh.pod b/sslh.pod index c0ce606..95246f7 100644 --- a/sslh.pod +++ b/sslh.pod @@ -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<-P> I] [-v] [-i] [-V] [-f] [-n] =head1 DESCRIPTION @@ -51,14 +51,10 @@ and the list of protocols). The configuration file makes it possible to specify protocols using regular expressions: a list of regular -expressions is given as the I parameter, and if the +expressions is given as the I parameter, and if the first packet received from the client matches any of these expressions, B connects to that protocol. -Alternatively, the I parameter can be set to -"builtin", to use the compiled probes which are much faster -than regular expressions. - =head2 Probing protocols When receiving an incoming connection, B will read the @@ -82,12 +78,15 @@ connections and LOG_ERR for failures. =over 4 -=item B<-F> I, B<--config> I +=item B<-F>I, B<--config> I Uses I has 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 diff --git a/systemd-sslh-generator.c b/systemd-sslh-generator.c new file mode 100644 index 0000000..5909a5b --- /dev/null +++ b/systemd-sslh-generator.c @@ -0,0 +1,153 @@ +#include +#include +#include +#include + + +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); + strcpy(conn, hostname); + strcat(conn, ":"); + strcat(conn, port); + + return conn; + +} + + +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)); + return -1; + } else { + + listen[i] = malloc(strlen(resolve_listen(hostname, port))); + strcpy(listen[i], resolve_listen(hostname, port)); + } + } + } + } + + return len; + +} + +static int write_socket_unit(FILE *socket, char **listen, int num_addr, const char *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]); + } + +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; + } + } + + 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"); + } + + + 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; + } + + if (argc > 1) + runtime_unit_dest = argv[1]; + + k = gen_sslh_config(runtime_unit_dest); + if (k < 0) + r = k; + + return r < 0 ? -1 : 0; + +} + + diff --git a/tls.c b/tls.c new file mode 100644 index 0000000..8640dec --- /dev/null +++ b/tls.c @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2011 and 2012, Dustin Lundquist + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This is a minimal TLS implementation intended only to parse the server name + * extension. This was created based primarily on Wireshark dissection of a + * TLS handshake and RFC4366. + */ +#include +#include /* malloc() */ +#include "tls.h" + +#define TLS_HEADER_LEN 5 +#define TLS_HANDSHAKE_CONTENT_TYPE 0x16 +#define TLS_HANDSHAKE_TYPE_CLIENT_HELLO 0x01 + +#ifndef MIN +#define MIN(X, Y) ((X) < (Y) ? (X) : (Y)) +#endif + + +struct TLSProtocol { + int use_alpn; + char** sni_hostname_list; + char** alpn_protocol_list; +}; + +static int parse_extensions(const struct TLSProtocol *, const char *, size_t); +static int parse_server_name_extension(const struct TLSProtocol *, const char *, size_t); +static int parse_alpn_extension(const struct TLSProtocol *, const char *, size_t); +static int has_match(char**, const char*, size_t); + +/* Parse a TLS packet for the Server Name Indication and ALPN extension in the client + * 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 + */ +int +parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_t data_len) { + char tls_content_type; + char tls_version_major; + char tls_version_minor; + size_t pos = TLS_HEADER_LEN; + size_t len; + + /* Check that our TCP payload is at least large enough for a TLS header */ + if (data_len < TLS_HEADER_LEN) + return -1; + + 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; + } + + tls_version_major = data[1]; + tls_version_minor = data[2]; + if (tls_version_major < 3) { + if (verbose) fprintf(stderr, "Received SSL %d.%d handshake which cannot be parsed.\n", + tls_version_major, tls_version_minor); + + return -2; + } + + /* TLS record length */ + len = ((unsigned char)data[3] << 8) + + (unsigned char)data[4] + TLS_HEADER_LEN; + data_len = MIN(data_len, len); + + /* Check we received entire TLS record length */ + if (data_len < len) + return -1; + + /* + * Handshake + */ + if (pos + 1 > data_len) { + return -5; + } + if (data[pos] != TLS_HANDSHAKE_TYPE_CLIENT_HELLO) { + if (verbose) fprintf(stderr, "Not a client hello\n"); + + return -5; + } + + /* Skip past fixed length records: + 1 Handshake Type + 3 Length + 2 Version (again) + 32 Random + to Session ID Length + */ + pos += 38; + + /* Session ID */ + if (pos + 1 > data_len) + return -5; + len = (unsigned char)data[pos]; + pos += 1 + len; + + /* Cipher Suites */ + if (pos + 2 > data_len) + return -5; + len = ((unsigned char)data[pos] << 8) + (unsigned char)data[pos + 1]; + pos += 2 + len; + + /* Compression Methods */ + if (pos + 1 > data_len) + return -5; + 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; + } + + /* Extensions */ + if (pos + 2 > data_len) + return -5; + 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); +} + +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; + + if (tls_data == NULL) + return -3; + + /* Parse each 4 bytes for the extension header */ + while (pos + 4 <= data_len) { + /* Extension Length */ + len = ((unsigned char) data[pos + 2] << 8) + + (unsigned char) data[pos + 3]; + + if (pos + 4 + len > data_len) + return -5; + + 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); + } + + pos += 4 + len; /* Advance to the next extension header */ + } + + /* Check we ended where we expected to */ + if (pos != data_len) + return -5; + + return -2; +} + +static int +parse_server_name_extension(const struct TLSProtocol *tls_data, const char *data, size_t data_len) { + size_t pos = 2; /* skip server name list length */ + size_t len; + + while (pos + 3 < data_len) { + len = ((unsigned char)data[pos + 1] << 8) + + (unsigned char)data[pos + 2]; + + if (pos + 3 + len > data_len) + return -5; + + 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; + } + default: + if (verbose) fprintf(stderr, "Unknown server name extension name type: %d\n", + data[pos]); + } + pos += 3 + len; + } + /* Check we ended where we expected to */ + if (pos != data_len) + return -5; + + return -2; +} + +static int +parse_alpn_extension(const struct TLSProtocol *tls_data, const char *data, size_t data_len) { + size_t pos = 2; + size_t len; + + while (pos + 1 < data_len) { + len = (unsigned char)data[pos]; + + if (pos + 1 + len > data_len) + return -5; + + if (len > 0 && has_match(tls_data->alpn_protocol_list, data + pos + 1, len)) { + return len; + } else if (len > 0) { + if (verbose) fprintf(stderr, "Unknown ALPN name: %.*s\n", (int)len, data + pos + 1); + } + pos += 1 + len; + } + /* Check we ended where we expected to */ + if (pos != data_len) + return -5; + + return -2; +} + +static int +has_match(char** list, const char* name, size_t name_len) { + char **item; + + 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)) { + return 1; + } + } + return 0; +} + +struct TLSProtocol * +new_tls_data() { + struct TLSProtocol *tls_data = malloc(sizeof(struct TLSProtocol)); + if (tls_data != NULL) { + tls_data->use_alpn = -1; + } + + return tls_data; +} + +struct TLSProtocol * +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; + } else { + tls_data->sni_hostname_list = list; + if(tls_data->use_alpn == 1) + tls_data->use_alpn = 2; + else + tls_data->use_alpn = 0; + } + + return tls_data; +} diff --git a/tls.h b/tls.h new file mode 100644 index 0000000..0e79e0c --- /dev/null +++ b/tls.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2011 and 2012, Dustin Lundquist + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +#ifndef TLS_H +#define TLS_H + +#include "common.h" + +struct TLSProtocol; + +int parse_tls_header(const struct TLSProtocol *tls_data, const char *data, size_t data_len); + +struct TLSProtocol *new_tls_data(); +struct TLSProtocol *tls_data_set_list(struct TLSProtocol *, int, char**); + +#endif