diff --git a/.gitignore b/.gitignore index 6b9d908..493642b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ sslh-ev systemd-sslh-generator sslh.8.gz tags +version.h /config.status /config.log /config.h diff --git a/ChangeLog b/ChangeLog index f096723..e4bee24 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,11 +1,42 @@ vNEXT: + Fix proxyprotocol target field (Thanks to Github user 404-Not-Found) + +v2.2.4: + Fix CVE-2025-46806 for "Misaligned Memory Accesses in `is_openvpn_protocol()`" + + Fix CVE-2025-46807 for "File Descriptor Exhaustion in sslh-select and sslh-ev" + + Fix potential parsing of undefined data in syslog + probe (no CVE assigned) + + Thanks to Matthias Gerstner of the SUSE security + team for the security review that found these + defects! + +v2.2.3: + Reverse older commit: version.h cannot be included + without breaking the build (everything recompiles + every time) and the release archive creation (which + relies on git tags). + +v2.2.2: + Fix potential vulnerability similar to CVE-2020-28935 + +v2.2.1: Added a boolean setting "is_unix" for listen and protocol entries. This will use the 'host' setting as a path name to a socket file, and connections (listening or connecting) will be performed on Unix socket instead of Internet sockets. - Support proxyprotocol on the backend server side. + Support HAProxy's proxyprotocol on the backend + server side. + + Lots of documentation about a new, simpler way to + perform transparent proxying. + + New "verbose" option that overrides all other + verbose settings. v2.1.3: Fix Landlock access to /etc/hosts.deny and diff --git a/Makefile.in b/Makefile.in index f614984..adc3470 100644 --- a/Makefile.in +++ b/Makefile.in @@ -22,7 +22,7 @@ MAN=sslh.8.gz # man page name # itself ifneq ($(strip $(ENABLE_SANITIZER)),) - CFLAGS_SAN=-fsanitize=address -fsanitize=leak -fsanitize=undefined + CFLAGS_SAN=-fsanitize=address -fsanitize=leak -fsanitize=undefined -fsanitize=alignment endif ifneq ($(strip $(COV_TEST)),) @@ -70,9 +70,8 @@ all: sslh-fork sslh-select $(MAN) echosrv $(CONDITIONAL_TARGETS) $(OBJS_A): $(OBJS) $(AR) rcs $(OBJS_A) $(OBJS) -version.h: .FORCE +version.h: ./genver.sh >version.h -.FORCE: $(OBJS) $(FORK_OBJS) $(SELECT_OBJS) $(EV_OBJS): argtable3.h collection.h common.h gap.h hash.h log.h probe.h processes.h sslh-conf.h tcp-listener.h tcp-probe.h tls.h udp-listener.h version.h @@ -145,7 +144,6 @@ distclean: clean clean: rm -f sslh-fork sslh-select $(CONDITIONAL_TARGETS) echosrv version.h $(MAN) systemd-sslh-generator *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info - echo "// this is a placeholder for version.h, to make code-checking editors happy" > version.h tags: ctags --globals -T *.[ch] diff --git a/README.md b/README.md index 1fcaeaa..58e2e85 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ interfaces. The principle and basic setup is described [here](doc/simple_transparent_proxy.md), with further scenarios described [there](doc/scenarios-for-simple-transparent-proxy.md). +There is also a guide to use [podman](doc/podman.md). Another method uses iptable packet marking features, and is highly dependent on your network environment and diff --git a/common.c b/common.c index 4545560..db97490 100644 --- a/common.c +++ b/common.c @@ -986,26 +986,48 @@ void drop_privileges(const char* user_name, const char* chroot_path) } } + +#ifndef O_NOFOLLOW +#define O_NOFOLLOW 0 +#endif + /* Writes my PID */ void write_pid_file(const char* pidfile) { - FILE *f; - int res; + int fd; + char pidbuf[32]; + size_t len, written = 0; + ssize_t res; - f = fopen(pidfile, "w"); - if (!f) { + /* Format PID as string */ + len = snprintf(pidbuf, sizeof(pidbuf), "%d\n", getpid()); + if (len >= sizeof(pidbuf)) { + print_message(msg_system_error, "write_pid_file: PID string too long\n"); + return; + } + + /* Open file with O_NOFOLLOW to prevent symlink attacks (Similar to CVE-2020-28935) */ + fd = open(pidfile, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW ,0644); + + if (fd == -1) { print_message(msg_system_error, "write_pid_file: %s: %s\n", pidfile, strerror(errno)); return; } - res = fprintf(f, "%d\n", getpid()); - if (res < 0) { - print_message(msg_system_error, "write_pid_file: fprintf: %s\n", strerror(errno)); + /* Write PID to file with proper error handling */ + while (written < len) { + res = write(fd, pidbuf + written, len - written); + if (res == -1) { + if (errno == EINTR || errno == EAGAIN) + continue; + print_message(msg_system_error, "write_pid_file: write: %s\n", strerror(errno)); + break; + } + written += res; } - res = fclose(f); - if (res == EOF) { - print_message(msg_system_error, "write_pid_file: fclose: %s\n", strerror(errno)); - return; + /* Close file */ + if (close(fd) == -1) { + print_message(msg_system_error, "write_pid_file: close: %s\n", strerror(errno)); } } diff --git a/common.h b/common.h index 3503a95..7243104 100644 --- a/common.h +++ b/common.h @@ -34,6 +34,10 @@ #include #endif +#ifdef __APPLE__ +#include +#endif + #include "config.h" #include "version.h" @@ -182,6 +186,10 @@ extern struct sslhcfg_item cfg; extern struct addrinfo *addr_listen; extern const char* server_type; +#if defined(__APPLE__) && (MAC_OS_X_VERSION_MIN_REQUIRED < 1080) +extern int hosts_ctl(); +#endif + /* sslh-fork.c */ void start_shoveler(int); diff --git a/doc/podman.md b/doc/podman.md new file mode 100644 index 0000000..7e268ff --- /dev/null +++ b/doc/podman.md @@ -0,0 +1,120 @@ +Here is an example of deployment of sslh in transparent mode +using ansible and podman, [submitted by Github user +olegstepura](https://github.com/yrutschle/sslh/issues/448) + +```yaml +# ansible podman task +- name: "Create sslh-co pod" + containers.podman.podman_pod: + name: sslh-co # read as "sslh and company" + state: started + ports: + - "80:80" + - "443:444" + # ... (more ports if needed) + network: + - '{{ containers.config.network }}' # other services from this network can access containers in this network for example prometheus can read caddy metrics at sslh-co:2020, also caddy itself can connect to other services to act as a reverse proxy + +- name: "Create the sslh container" + containers.podman.podman_container: + name: sslh + image: "yrutschle/sslh:latest" + pod: sslh-co + capabilities: + - NET_RAW + - NET_BIND_SERVICE + - NET_ADMIN + sysctl: + net.ipv4.conf.default.route_localnet: 1 + net.ipv4.conf.all.route_localnet: 1 + expose: + - 444 + volume: + # ... (make sure to mount config as you like) + command: --transparent -F/etc/sslh/sslh.cfg # parameter --transparent here is needed to trigger configure_iptables in init script + state: started + +- name: "Create the caddy container" + containers.podman.podman_container: + name: caddy + image: "lucaslorentz/caddy-docker-proxy:alpine" # regular caddy or nginx image will also work + pod: sslh-co + expose: + - 80 + - 443 + - 2020 # metrics, since caddy-docker-proxy uses :2019 internally + volume: + # ... (mount your configs and other stuff here) + - "/var/run/podman/podman.sock:/var/run/docker.sock" + state: started + notify: podman restart sslh + +- name: "Create the SSH proxy to host container" + containers.podman.podman_container: + name: ssh-proxy + image: "alpine/socat:latest" + pod: sslh-co + expose: + - 222 + command: TCP-LISTEN:222,fork TCP:host.containers.internal:22 + state: started +``` + +```ini +# sslh config +foreground: true; +inetd: false; +numeric: true; +transparent: true; +timeout: 5; + +listen: +( + { host: "0.0.0.0"; port: "444"; keepalive: true; }, +); + +protocols: +( + { + name: "ssh"; + service: "ssh"; + host: "localhost"; + port: "222"; + fork: true; + }, + { + name: "http"; + host: "localhost"; + port: "80"; + }, + { + name: "tls"; + host: "localhost"; + port: "443"; + }, +); +``` + +I omitted caddy configs here as it's not important. Some unrelated container configs were also dropped. +In the example above `sslh`, `caddy` and `ssh-proxy` are 3 containers in the same pod, all listening on `localhost`. SSLH has to listen on `444` because caddy already listens on `443` and it's more complex to reconfigure caddy port because of let's encrypt (caddy itself "thinks" it is bound to your host interface). + +Scheme of port mapping is (all containers share same `localhost`): +``` +host 443 → pod 444 (sslh) → pod 443 (caddy) +``` +- podman connects host `443` port to pod's `444` port +- `sslh` listens `444` on `localhost`, reroutes tls to `caddy` on `localhost:443` +- `caddy` listens `443` on `localhost` (reverse-proxies to other apps on private network) +Reverse-proxied services get correct IP in `X-Forwarded-For` header. + +Takeaways: +- `net.ipv4.conf.default.route_localnet` is setup only in container, not on host, which is nice. +- same with iptables and route rules applied by `init` script in sslh container +- `sslh` proxy to other services in the private network will not work (because transparent mode is enabled) even if that other service does not need real IP + - with transparent mode all services that `sslh` will connect to should be attached to this pod making it listen on `localhost` of the pod + - ports of containers should not clash + - ports should be published on pod level, not on containers + - containers should not be configured to connect to custom networks +- because of the above proxying ssh to host will also not work out of the box, `sslh` should connect to `localhost` and not to a random IP. While adding another proxy as a container to the same pod sounds like an overkill I don't see any other solution, so `socat` is used as an additional proxy. +- `reverse_proxy` by caddy to other containers in the same custom network (as pod is attached to) works (e.g. caddy can connect to other IPs from custom network). `socat` can also connect to host and/or other containers in custom network. + diff --git a/echosrv-conf.c b/echosrv-conf.c index 1276f21..fe11180 100644 --- a/echosrv-conf.c +++ b/echosrv-conf.c @@ -1,5 +1,5 @@ /* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README) - * on Tue Apr 8 22:35:50 2025. + * on Sun Jun 1 08:49:54 2025. # conf2struct: generate libconf parsers that read to structs # Copyright (C) 2018-2024 Yves Rutschle diff --git a/echosrv-conf.h b/echosrv-conf.h index 7ccc111..0ff5230 100644 --- a/echosrv-conf.h +++ b/echosrv-conf.h @@ -1,5 +1,5 @@ /* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README) - * on Tue Apr 8 22:35:50 2025. + * on Sun Jun 1 08:49:54 2025. # conf2struct: generate libconf parsers that read to structs # Copyright (C) 2018-2024 Yves Rutschle diff --git a/echosrv.c b/echosrv.c index 4fa8e4e..54719ca 100644 --- a/echosrv.c +++ b/echosrv.c @@ -26,7 +26,6 @@ #include #include #include -#include #include #define cfg sslhcfg @@ -323,9 +322,6 @@ int start_listen_sockets(struct listen_endpoint *sockfd[]) int main(int argc, char *argv[]) { - - extern char *optarg; - extern int optind; int num_addr_listen; struct listen_endpoint *listen_sockets; diff --git a/probe.c b/probe.c index d54a340..ac4f26c 100644 --- a/probe.c +++ b/probe.c @@ -25,6 +25,7 @@ #define PCRE2_CODE_UNIT_WIDTH 8 #include #endif +#include #include #include "probe.h" #include "log.h" @@ -146,6 +147,7 @@ static int is_ssh_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_ #define OVPN_OPCODE_MASK 0xF8 #define OVPN_CONTROL_HARD_RESET_CLIENT_V1 (0x01 << 3) #define OVPN_CONTROL_HARD_RESET_CLIENT_V2 (0x07 << 3) +#define OVPN_CONTROL_HARD_RESET_CLIENT_V3 (0x0A << 3) #define OVPN_HMAC_128 16 #define OVPN_HMAC_160 20 #define OVPN_HARD_RESET_PACKET_ID_OFFSET(hmac_size) (9 + hmac_size) @@ -164,8 +166,12 @@ static int is_openvpn_protocol (const char*p,ssize_t len, struct sslhcfg_protoco if (len < 1) return PROBE_NEXT; + printf("opcode: %d\n", (p[0] & OVPN_OPCODE_MASK) >> 3); + if ((p[0] & OVPN_OPCODE_MASK) != OVPN_CONTROL_HARD_RESET_CLIENT_V1 && - (p[0] & OVPN_OPCODE_MASK) != OVPN_CONTROL_HARD_RESET_CLIENT_V2) + (p[0] & OVPN_OPCODE_MASK) != OVPN_CONTROL_HARD_RESET_CLIENT_V2 && + (p[0] & OVPN_OPCODE_MASK) != OVPN_CONTROL_HARD_RESET_CLIENT_V3 + ) return PROBE_NEXT; /* The detection pattern above may not be reliable enough. @@ -176,12 +182,18 @@ static int is_openvpn_protocol (const char*p,ssize_t len, struct sslhcfg_protoco if (len <= OVPN_HARD_RESET_PACKET_ID_OFFSET(OVPN_HMAC_128) + sizeof(uint32_t)) return PROBE_NEXT; - if (ntohl(*(uint32_t*)(p + OVPN_HARD_RESET_PACKET_ID_OFFSET(OVPN_HMAC_128))) <= 5u) + uint32_t i; + /* OVPN_HMAC_128 is unaligned, which requires special care e.g. on ARM */ + memcpy(&i, (p + OVPN_HARD_RESET_PACKET_ID_OFFSET(OVPN_HMAC_128)), sizeof(i)); + i = ntohl(i); + if (i <= 5u) return PROBE_MATCH; if (len <= OVPN_HARD_RESET_PACKET_ID_OFFSET(OVPN_HMAC_160) + sizeof(uint32_t)) return PROBE_NEXT; + memcpy(&i, (p + OVPN_HARD_RESET_PACKET_ID_OFFSET(OVPN_HMAC_160)), sizeof(i)); + i = ntohl(i); if (ntohl(*(uint32_t*)(p + OVPN_HARD_RESET_PACKET_ID_OFFSET(OVPN_HMAC_160))) <= 5u) return PROBE_MATCH; @@ -362,17 +374,38 @@ static int is_socks5_protocol(const char *p_in, ssize_t len, struct sslhcfg_prot return PROBE_MATCH; } +/* ****************** + * is_syslog_protocol + * */ +static regex_t syslog_preg; +static int configured_syslog_regex = 0; + +static void config_syslog_regex(void) +{ + /* two patterns for syslog messages: + * <12> My message + * 15 <12> My message + * 12 is 'priority', 1 to 3 digits (RFC4234) + * 15 is 'message length', a TCP-only option (RFC6587) + */ + int res = regcomp(&syslog_preg, "^([0-9]{1,3} )?<[0-9]{1,3}>", REG_EXTENDED); + if (res) { + print_message(msg_system_error, "regcomp"); + exit(1); + } + configured_syslog_regex = 1; +} + static int is_syslog_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item* proto) { - int res, i, j; + char buf[len+1]; - res = sscanf(p, "<%d>", &i); - if (res == 1) return 1; + if (!configured_syslog_regex) config_syslog_regex(); - res = sscanf(p, "%d <%d>", &i, &j); - if (res == 2) return 1; + strncpy(buf, p, len); + buf[len] = 0; - return 0; + return (regexec(&syslog_preg, buf, (size_t)0, NULL, 0) == 0); } static int is_teamspeak_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item* proto) diff --git a/proxyprotocol.c b/proxyprotocol.c index 93229e8..08cd847 100644 --- a/proxyprotocol.c +++ b/proxyprotocol.c @@ -22,7 +22,7 @@ #include "config.h" -#if HAVE_PROXYPROTOCOL +#ifdef HAVE_PROXYPROTOCOL #include #include "common.h" @@ -50,22 +50,38 @@ static int family_to_pp(int af_family) typedef char libpp_addr[108]; /* This is hardcoded in libproxyprotocol/proxy_protocol.h */ + +typedef enum { + PEER, + SOCK +} sockpeer_t; + /* Fills *addr, *host and *serv with the connection information corresponding - * to fd. *host is the IP address as string and *serv is the service (port) + * to fd. sockpeer indicates if if is filled with the remote, or local, part of + * the socket. + * *host is the IP address as string and *serv is the service (port) * */ -static int get_info(int fd, struct addrinfo* addr, libpp_addr* host, uint16_t* serv) +static int get_info(int fd, sockpeer_t sockpeer, struct addrinfo* addr, libpp_addr* host, uint16_t* serv) { char serv_str[NI_MAXSERV]; int res; - res = getpeername(fd, addr->ai_addr, &addr->ai_addrlen); - CHECK_RES_RETURN(res, "getpeername", -1); + if (sockpeer == PEER) { + res = getpeername(fd, addr->ai_addr, &addr->ai_addrlen); + CHECK_RES_RETURN(res, "getpeername", -1); + } else { + res = getsockname(fd, addr->ai_addr, &addr->ai_addrlen); + CHECK_RES_RETURN(res, "getsockname", -1); + } res = getnameinfo(addr->ai_addr, addr->ai_addrlen, (char*)host, sizeof(*host), serv_str, sizeof(serv_str), NI_NUMERICHOST | NI_NUMERICSERV ); - CHECK_RES_RETURN(res, "getnameinfo", -1); + if (res != 0) { + print_message(msg_system_error, "getnameinfo: %s\n", gai_strerror(res)); + return -1; + } *serv = atoi(serv_str); @@ -89,14 +105,14 @@ int pp_write_header(int pp_version, struct connection* cnx) addr.ai_addr = (struct sockaddr*)&ss; addr.ai_addrlen = sizeof(ss); - res = get_info(cnx->q[0].fd, + res = get_info(cnx->q[0].fd, PEER, &addr, &pp_info_in_v1.src_addr, &pp_info_in_v1.src_port); if (res == -1) return -1; pp_info_in_v1.address_family = family_to_pp(addr.ai_addr->sa_family); - res = get_info(cnx->q[1].fd, + res = get_info(cnx->q[1].fd, SOCK, &addr, &pp_info_in_v1.dst_addr, &pp_info_in_v1.dst_port diff --git a/sslh-conf.c b/sslh-conf.c index bf74c22..62a861b 100644 --- a/sslh-conf.c +++ b/sslh-conf.c @@ -1,5 +1,5 @@ /* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README) - * on Tue Apr 8 22:35:50 2025. + * on Sun Jun 1 08:49:54 2025. # conf2struct: generate libconf parsers that read to structs # Copyright (C) 2018-2024 Yves Rutschle diff --git a/sslh-conf.h b/sslh-conf.h index 682a27b..4d6f828 100644 --- a/sslh-conf.h +++ b/sslh-conf.h @@ -1,5 +1,5 @@ /* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README) - * on Tue Apr 8 22:35:50 2025. + * on Sun Jun 1 08:49:54 2025. # conf2struct: generate libconf parsers that read to structs # Copyright (C) 2018-2024 Yves Rutschle diff --git a/sslh-main.c b/sslh-main.c index 160d9fc..ba3de13 100644 --- a/sslh-main.c +++ b/sslh-main.c @@ -280,9 +280,6 @@ void close_std(void) int main(int argc, char *argv[], char* envp[]) { - - extern char *optarg; - extern int optind; int res, num_addr_listen; struct listen_endpoint *listen_sockets; diff --git a/sslh-select.c b/sslh-select.c index acac6eb..5ee679e 100644 --- a/sslh-select.c +++ b/sslh-select.c @@ -156,13 +156,15 @@ void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen) /* Check main socket for new connections */ for (i = 0; i < num_addr_listen; i++) { if (FD_ISSET(listen_sockets[i].socketfd, &readfds)) { - struct connection* new_cnx = cnx_accept_process(&fd_info, &listen_sockets[i]); - - if (fd_out_of_range(new_cnx->q[0].fd)) - tidy_connection(new_cnx, &fd_info); - /* don't also process it as a read socket */ FD_CLR(listen_sockets[i].socketfd, &readfds); + + struct connection* new_cnx; + while ((new_cnx = cnx_accept_process(&fd_info, &listen_sockets[i]))) { + if (fd_out_of_range(new_cnx->q[0].fd)) + tidy_connection(new_cnx, &fd_info); + } + } } diff --git a/t/run b/t/run new file mode 100755 index 0000000..3343b48 --- /dev/null +++ b/t/run @@ -0,0 +1,335 @@ +#! /usr/bin/perl -w + +# Test suite for sslh + +# Uses Conf::Libconfig to read sslh config file: install +# with: +# cpan Conf::Libconfig + +# Usage: +# ./run # run all tests +# ./run -l # list all tests +# ./run 1 3 5 # run specified tests + +use strict; +use IO::Socket::INET6; +use Test::More qw/no_plan/; +use Conf::Libconfig 1.0.3; +use Getopt::Long; + +my ($coverage, $list_tests); +GetOptions( + 'cover' => \$coverage, + 'list' => \$list_tests, +); + + +use Data::Dumper; + +################################################################################ +# Global setup +################################################################################ +my $no_listen = 8083; # Port on which no-one listens +my $user = (getpwuid $<)[0]; # Run under current username + +################################################################################ +# Helper functions +################################################################################ +sub get_conf { + my ($filename) = @_; + my $conf = new Conf::Libconfig; + $conf->read_file($filename) or die "$filename: $!"; + return $conf; +} + +# We want to keep track of tests to print a report at the +# end, so we centralise all calls to Test::More::is here +my $cnt = 1; # test counter +my @results; +sub my_is { + my ($a, $b, $desc) = @_; + + my $res = is($a, $b, $desc); + push @results, [$cnt++, $desc, $res]; +} + +#my $pidfile = $conf->lookup_value("pidfile"); + +sub verbose_exec +{ + my ($cmd) = @_; + + warn "$cmd\n"; + if (!fork) { + exec $cmd; + } +} + +# Returns a 'host:port' address targetting ssh from the configuration file +sub ssh_address { + my ($conf) = @_; + my $ssh_conf = (grep { $_->{name} eq "ssh" } @{$conf->value("protocols")})[0]; + return $ssh_conf->{host} . ":" . $ssh_conf->{port}; +} + + +# Returns the last TLS target from the configuration file +sub ssl_address { + my ($conf) = @_; + my $ssl_conf = (grep { $_->{name} eq "tls" } @{$conf->value ("protocols")})[-1]; + return $ssl_conf->{host} . ":" . $ssl_conf->{port}; +} + +# 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} // [] }); +} + +# Start an echoserver for each service +my ($started_echosrvs); +sub start_echosrv +{ + my ($conf) = @_; + + return if defined $started_echosrvs; + + foreach my $s (@{$conf->value("protocols")}) { + my $prefix = $s->{name}; + + $prefix =~ s/^ssl/tls/; + + if ($s->{sni_hostnames} or $s->{alpn_protocols}) { + $prefix = make_sni_alpn_name($s); + } + + verbose_exec "../echosrv --listen $s->{host}:$s->{port} --prefix '$prefix: '"; + } + $started_echosrvs = 1; +} + +sub stop_echosrv +{ + `killall echosrv` if $started_echosrvs; + undef $started_echosrvs; +} + + +# Runs sslh +my ($sslh_pid); +sub start_sslh +{ + my ($exe, $conf) = @_; + return if defined $sslh_pid; + my ($valgrind); + if (!($sslh_pid = fork)) { + my $user = (getpwuid $<)[0]; # Run under current username + my $cmd = "../$exe -u $user -F $conf"; + print "****CMDLINE [$cmd]\n"; + #$valgrind = 1; + #$cmd = "valgrind --leak-check=full $cmd"; + verbose_exec $cmd; + exit 0; + } + sleep 1; + warn "spawned $sslh_pid\n"; + sleep 5 if $valgrind; # valgrind can be heavy -- wait 5 seconds +} + +#stop sslh +sub stop_sslh +{ + if (defined $sslh_pid) { + kill TERM => $sslh_pid or warn "kill process: $!\n"; + undef $sslh_pid; + } +} + +################################################################################ +# Test methods +################################################################################ + +# runs a test that ends in sslh exiting early +# $cmdline: which command line to run +# $test: test object +# Returns: sslh exit code +sub run_ending_test +{ + my ($exe, $params, $test) = @_; + my $conf = get_conf($test->{cfg}); + my $ssh_address = ssh_address($conf); + my $ssl_address = ssl_address($conf); + my $pidfile = $conf->lookup_value("pidfile"); + my $sslh_pid; + if (!($sslh_pid = fork)) { + my $cmdline = "../$exe $params"; + warn "eval `$cmdline`\n"; + $cmdline = eval "\"$cmdline\""; + warn "running `$cmdline`\n"; + exec "$cmdline"; + } + warn "spawned $sslh_pid\n"; + waitpid $sslh_pid, 0; + my $code = $? >> 8; + warn "exited with $code\n"; + my_is($code, $test->{exit}, "$exe: $test->{desc}"); + return $code; +} + + + +# Runs one test for one probe. Start echosrv's if required. +# Extract sslh's listening port from the config file, run +# sslh, connect, write the test pattern, read the result, +# check it connected to the right echosrv, check the data +# was transfered ok. +sub run_test_probe +{ + my ($exe, $test) = @_; + + my $conf = get_conf($test->{cfg}); + start_echosrv($conf); + start_sslh($exe, $test->{cfg}); + + my $expected = $test->{expected}; + + my $sslh_port = $conf->value("listen")->[0]->{port}; + print "test_probe [$expected] $sslh_port\n"; + my $cnx = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); + warn "t: $!\n" unless $cnx; + return unless $cnx; + + syswrite $cnx, $test->{data}; + + my $data; + my $n = sysread $cnx, $data, 1024; + $data =~ /^(.*?): /; + my $prefix = $1; + $data =~ s/$prefix: //g; + print "Received $n bytes: protocol $prefix data [$data]\n"; + close $cnx; + + my_is($prefix, $expected, "$exe:$expected: probe"); + my_is($data, $test->{data}, "$exe:$expected: shoveled"); +} + + +################################################################################ +# Test bodies +# Functions here each perform one test, and will be called with: +# - exe: path the executable to test +# - test: test object +################################################################################ + +sub test_no_host +{ + my ($exe, $test) = @_; + run_ending_test( + $exe, + "-v 3 -f -u $user --listen $no_listen --ssh \$ssh_address --tls \$ssl_address -P \$pidfile", + $test, + ); +} + +sub test_wrong_user +{ + my ($exe, $test) = @_; + run_ending_test( + $exe, + "-F $test->{cfg} -u ${user}_doesnt_exist --listen localhost:$no_listen --ssh \$ssh_address --tls \$ssl_address -P \$pidfile", + $test, + ); +} + + +################################################################################ + +my @tests = ( + { + desc => "No hostname in address", + run => \&test_no_host, + cfg => "test1.cfg", + exit => 6, + }, + { + desc => "Changing to non-existant username", + run => \&test_wrong_user, + cfg => "test1.cfg", + exit => 2, + }, + + { + desc => "ssh probe", + run => \&run_test_probe, + cfg => "test1.cfg", + data => "SSH-2.0 Tester banner\n", # Sent to echosrv + expected => "ssh", # prefix expected from echosrv + }, + + { + desc => "socks5 probe", + run => \&run_test_probe, + cfg => "test1.cfg", + data => "\x05\x04\x01\x02\x03\x04", + expected => "socks5", # prefix expected from echosrv + }, + +); + + +# Number tests +my $i = 1; +map { $_->{'index'} = $i++ } @tests; + +# Only keep selected tests +@tests = @tests[ map { $_ - 1 } @ARGV ] if @ARGV; + +################################################################################ +#####List all tests +format test_list_top = +ID | Description +----+------------------------------------------------------------------------- +. +format test_list = +@>> | @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +$_->{'index'}, $_->{'desc'}; +. +if ($list_tests) { + format_name STDOUT "test_list"; + format_top_name STDOUT "test_list_top"; + map { write; } @tests; + exit 0; +} +#####/list + +################################################################################ +# Run selected tests +foreach my $test (@tests) { + warn "Running test: $test->{desc}\n"; + my $binary = 'sslh-ev'; + my $code = ($test->{run})->($binary, $test); +} + +stop_echosrv(); +stop_sslh(); + +done_testing(); + +format test_results_top = +ID | Description | Status +----+-------------------------------------------------------------------+------- +. + +format test_results = +@>> | @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< | @>> +$_->[0], $_->[1], $_->[2] ? "OK" : "NOK" +. + +format_name STDOUT "test_results"; +format_top_name STDOUT "test_results_top"; +map { write; } @results; + diff --git a/t/test1.cfg b/t/test1.cfg new file mode 100644 index 0000000..f02f779 --- /dev/null +++ b/t/test1.cfg @@ -0,0 +1,79 @@ +# Configuration file for testing (use both by sslh under +# test and the test script `t`) + +foreground: true; +inetd: false; +numeric: true; +transparent: false; +timeout: 10; # Probe test writes slowly +pidfile: "/tmp/sslh_test.pid"; + +syslog_facility: "auth"; + +# Logging configuration +# Value: 1: stdout; 2: syslog; 3: both +# Defaults should be sensible. Generally, you want *-error +# to be always enabled, to know if something is going wrong. +verbose-config: 1; # print configuration at startup +verbose-config-error: 1; # print configuration errors +verbose-connections: 1; # trace established incoming address to forward address +verbose-connections-error: 1; # connection errors +verbose-connections-try: 1; # connection attempts towards targets +verbose-fd: 0; # file descriptor activity, open/close/whatnot +verbose-packets: 1; # hexdump packets on which probing is done +verbose-probe-info: 1; # what's happening during the probe process +verbose-probe-error: 1; # failures and problems during probing +verbose-system-error: 1; # system call problem, i.e. malloc, fork, failing +verbose-int-error: 1; # internal errors, the kind that should never happen + +# List of interfaces on which we should listen +# Options: +listen: +( + { host: "localhost"; port: "8080"; keepalive: true; proxyprotocol: true; }, + { host: "localhost"; port: "8081"; keepalive: true; }, + { host: "ip4-localhost"; is_udp: true; port: "8086"; }, + { host: "/tmp/sslh.sock"; is_unix: true; port: ""; } +); + + +# Tester beware: when using fork, the forked process loses +# track of buffers of other, concurrent connections. Memory +# leak tools thus complain each time a forked process stops. + +protocols: +( + { name: "ssh"; host: "localhost"; port: "9000"; fork: true; transparent: true; resolve_on_forward: true; }, + { name: "socks5"; host: "localhost"; port: "9001"; }, + { name: "http"; host: "localhost"; port: "80"; }, + { 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: "syslog"; host: "localhost"; port: "9013"; }, + { name: "regex"; host: "ip4-localhost"; is_udp: true; port: "9020"; + udp_timeout: 30; + regex_patterns: [ "^foo" ]; + resolve_on_forward: true; + }, + { name: "regex"; host: "localhost"; port: "9011"; + regex_patterns: [ "^foo", "^bar" ]; + minlength: 4; + test_patterns: ( # this is used by the test script, not by sslh + { pattern: "foo"; result: "ssh"; }, # After timeout + { pattern: "fooo"; result: "regex"; }, + { pattern: "bar"; result: "ssh"; }, + { pattern: "barr"; result: "regex"; }, + { pattern: "barrrr"; result: "regex"; } + ); + }, + { name: "tls"; host: "localhost"; port: "9021"; alpn_protocols: [ "alpn1", "alpn2" ]; sni_hostnames: [ "sni1" ]; }, + { name: "tls"; 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: "tls"; host: "localhost"; port: "9025"; }, + { name: "anyprot"; host: "localhost"; port: "9099"; } +); + +on_timeout: "ssh"; + diff --git a/t b/t_old similarity index 97% rename from t rename to t_old index 63137cb..c54b1f8 100755 --- a/t +++ b/t_old @@ -9,7 +9,7 @@ use strict; use IO::Socket::INET6; use Test::More qw/no_plan/; -use Conf::Libconfig; +use Conf::Libconfig 1.0.3; my $conf = new Conf::Libconfig; $conf->read_file("test.cfg"); @@ -17,7 +17,7 @@ $conf->read_file("test.cfg"); 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 $sslh_port = $conf->value("listen")->[0]->{port}; my $user = (getpwuid $<)[0]; # Run under current username # Which tests do we run @@ -84,8 +84,9 @@ sub make_sni_alpn_name { sub test_probe { my (%opts) = @_; + print "test_probe [$opts{expected}] $sslh_port\n"; my $cnx = new IO::Socket::INET(PeerHost => "localhost:$sslh_port"); - warn "$!\n" unless $cnx; + warn "t: $!\n" unless $cnx; return unless $cnx; my $pattern = $opts{data}; @@ -119,7 +120,7 @@ sub test_probe { sub test_probes { my (%in_opts) = @_; - my @probes = @{$conf->fetch_array("protocols")}; + my @probes = @{$conf->value("protocols")}; foreach my $p (@probes) { my %protocols = ( 'ssh' => { data => "SSH-2.0 tester" }, @@ -194,7 +195,7 @@ sub test_probes { # Start an echoserver for each service -foreach my $s (@{$conf->fetch_array("protocols")}) { +foreach my $s (@{$conf->value("protocols")}) { my $prefix = $s->{name}; $prefix =~ s/^ssl/tls/; # To remove in 1.21 @@ -217,7 +218,7 @@ for my $binary (@binaries) { my ($sslh_pid, $valgrind); if (!($sslh_pid = fork)) { my $user = (getpwuid $<)[0]; # Run under current username - my $cmd = "./$binary -v 4 -f -u $user -F test.cfg"; + my $cmd = "./$binary -u $user -F test.cfg"; #$valgrind = 1; #$cmd = "valgrind --leak-check=full $cmd"; verbose_exec $cmd; @@ -339,11 +340,11 @@ if ($RB_CNX_NOSERVER) { } -my $ssh_conf = (grep { $_->{name} eq "ssh" } @{$conf->fetch_array("protocols")})[0]; +my $ssh_conf = (grep { $_->{name} eq "ssh" } @{$conf->value("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->value ("protocols")})[-1]; my $ssl_address = $ssl_conf->{host} . ":" . $ssl_conf->{port};