new test framework, with test classes for robustness and probe tests. Not all tests are ported yet.

This commit is contained in:
Yves Rutschle committed 2025-06-15 16:02:02 +02:00
commit 071e7500a2
19 files changed
+696 -56

No files matched your search

-475
View File
@@ -1,475 +0,0 @@
#! /usr/bin/perl -w
# Test script for sslh
# Uses Conf::Libconfig to read sslh config file: install
# with:
# cpan Conf::Libconfig
use strict;
use IO::Socket::INET6;
use Test::More qw/no_plan/;
use Conf::Libconfig;
my $conf = new Conf::Libconfig;
$conf->read_file("test.cfg");
my $no_listen = 8083; # Port on which no-one listens
my $pidfile = $conf->lookup_value("pidfile");
my $sslh_port = $conf->fetch_array("listen")->[0]->{port};
my $user = (getpwuid $<)[0]; # Run under current username
# Which tests do we run
my $SSH_SHY_CNX = 0;
my $PROBES_NOFRAG = 1;
my $PROBES_AGAIN = 1;
my $SSL_MIX_SSH = 1;
my $SSH_MIX_SSL = 1;
my $DROP_CNX = 1;
# Robustness tests. These are mostly to achieve full test
# coverage, but do not necessarily result in an actual test
# (e.g. some tests need to be run with valgrind to check all
# memory management code).
my $RB_CNX_NOSERVER = 0;
my $RB_PARAM_NOHOST = 0;
my $RB_WRONG_USERNAME = 0;
my $RB_OPEN_PID_FILE = 0;
my $RB_RESOLVE_ADDRESS = 0;
my $RB_CL_PARAMS = 0;
`lcov --directory . --zerocounters`;
sub verbose_exec
{
my ($cmd) = @_;
warn "$cmd\n";
if (!fork) {
exec $cmd;
}
}
# 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];
}
# For SNI/ALPN, build a protocol name as such:
# tls:sni1,sni2,...;alpn1,alpn2,...
# input: a protocol entry from Libconfig
sub make_sni_alpn_name {
my ($prot) = @_;
return "tls:" . (join ",", @{$prot->{sni_hostnames} // []})
. ";" . (join ",", @{$prot->{alpn_protocols} // [] });
}
# Tests one probe: given input data, connect, verify we get
# the expected server, verify shoveling works
# Named options:
# data: what to write
# expected: expected protocol prefix
# no_frag: don't print byte-per-byte
sub test_probe {
my (%opts) = @_;
my $cnx = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx;
return unless $cnx;
my $pattern = $opts{data};
if ($opts{no_frag}) {
syswrite $cnx, $pattern;
} else {
while (length $pattern) {
syswrite $cnx, (substr $pattern, 0, 1, '');
select undef, undef, undef, .01;
}
}
my $data;
my $n = sysread $cnx, $data, 1024;
$data =~ /^(.*?): /;
my $prefix = $1;
$data =~ s/$prefix: //g;
print "Received $n bytes: protocol $prefix data [$data]\n";
close $cnx;
$opts{expected} =~ s/^ssl/tls/; # to remove in 1.21
my_is($prefix, $opts{expected}, "$opts{binary}:$opts{expected}: probe connected correctly");
my_is($data, $opts{data}, "$opts{binary}:$opts{expected}: data shoveled correctly");
}
# Test all probes, with or without fragmentation
# options:
# no_frag: write test patterns all at once (also
# available per-protocol as some probes don't support
# fragmentation)
sub test_probes {
my (%in_opts) = @_;
my @probes = @{$conf->fetch_array("protocols")};
foreach my $p (@probes) {
my %protocols = (
'ssh' => { data => "SSH-2.0 tester" },
'socks5' => { data => "\x05\x04\x01\x02\x03\x04" },
'http' => {
data => "GET index.html HTTP/1.1",
no_frag => 1 },
'tls' => {
# Packet with SNI and ALPN (`openssl s_client -connect localhost:443 -alpn alpn1 -servername sni1`)
data_sni_alpn => "\x16\x03\x01\x00\xc4\x01\x00\x00\xc0\x03\x03\x03\x19\x01\x00\x40\x14\x13\xcc\x1b\x94\xad\x20\x5d\x13\x1a\x8d\xd2\x65\x23\x70\xde\xd1\x3c\x5d\x05\x19\xcb\x27\x0d\x7c\x2c\x89\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x5f\x00\x00\x00\x09\x00\x07\x00\x00\x04\$sni\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x10\x00\x08\x00\x06\x05\$alpn\x00\x16\x00\x00\x00\x17\x00\x00hello sni/alpn",
# Packet with SNI alone
data_sni => "\x16\x03\x01\x00\xb8\x01\x00\x00\xb4\x03\x03\x97\xe4\xe9\xad\x86\xe1\x21\xfd\xc4\x5b\x27\x0e\xad\x4b\x55\xc2\x50\xe4\x1c\x86\x2f\x37\x25\xde\xe8\x9c\x59\xfc\x1b\xa9\x37\x32\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x53\x00\x00\x00\x09\x00\x07\x00\x00\x04\$sni\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x16\x00\x00\x00\x17\x00\x00hello sni",
# packet with ALPN alone
data_alpn => "\x16\x03\x01\x00\xb7\x01\x00\x00\xb3\x03\x03\xe2\x90\xa2\x29\x03\x31\xad\x98\x44\x51\x54\x90\x5b\xd9\x51\x0e\x66\xb5\x3f\xe8\x8b\x09\xc9\xe4\x2b\x97\x24\xef\xad\x56\x06\xc9\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x52\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x10\x00\x08\x00\x06\x05\$alpn\x00\x16\x00\x00\x00\x17\x00\x00hello alpn",
# packet with no SNI, no ALPN
data => "\x16\x03\x01\x00\xab\x01\x00\x00\xa7\x03\x03\x89\x22\x33\x95\x43\x7a\xc3\x89\x45\x51\x12\x3c\x28\x24\x1b\x6a\x78\xbf\xbe\x95\xd8\x90\x58\xd7\x65\xf7\xbb\x2d\xb2\x8d\xa0\x75\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x46\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x16\x00\x00\x00\x17\x00\x00hello tls alone"
},
'openvpn' => { data => "\x00\x00" },
'syslog' => { data => "<42> My syslog message" },
'tinc' => { data => "0 hello" },
'xmpp' => {data => "I should get a real jabber connection initialisation here" },
'adb' => { data => "CNXN....................host:..." },
'anyprot' => {data => "hello anyprot this needs to be longer than xmpp and adb which expect about 50 characters, which I all have to write before the timeout!" },
);
my $pattern = $protocols{$p->{name}}->{data};
my %opts = %in_opts;
$opts{no_frag} = 1 if $protocols{$p->{name}}->{no_frag};
if ($p->{sni_hostnames} or $p->{alpn_protocols}) {
my $pname = make_sni_alpn_name($p);
my @sni = @{$p->{sni_hostnames} // [""] };
my @alpn = @{$p->{alpn_protocols} // [""] };
foreach my $sni ( @sni ) {
foreach my $alpn ( @alpn ) {
print "sni: $sni\nalpn: $alpn\n";
$pattern = $protocols{tls}->{
"data". ($sni ? "_sni" : "") .
($alpn ? "_alpn": "")
};
$pattern =~ s/(\$\w+)/$1/eeg;
test_probe(
data => $pattern,
expected => $pname,
%opts
);
}
}
} elsif ($p->{name} eq 'regex') {
foreach my $test (@{$p->{test_patterns}}) {
test_probe(
data => $test->{pattern},
expected => $test->{result},
%opts
);
}
} else {
test_probe(
data => $pattern,
expected => $p->{name},
%opts
);
}
}
}
# Start an echoserver for each service
foreach my $s (@{$conf->fetch_array("protocols")}) {
my $prefix = $s->{name};
$prefix =~ s/^ssl/tls/; # To remove in 1.21
if ($s->{sni_hostnames} or $s->{alpn_protocols}) {
$prefix = make_sni_alpn_name($s);
}
verbose_exec "./echosrv --listen $s->{host}:$s->{port} --prefix '$prefix: '";
}
#my @binaries = ('sslh-select', 'sslh-fork');
my @binaries = ('sslh-select');
for my $binary (@binaries) {
warn "Testing $binary\n";
# Start sslh with the right plumbing
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";
#$valgrind = 1;
#$cmd = "valgrind --leak-check=full $cmd";
verbose_exec $cmd;
exit 0;
}
warn "spawned $sslh_pid\n";
sleep 1; # Give everyone some time to start
sleep 5 if $valgrind; # valgrind can be heavy -- wait 5 seconds
my $test_data = "hello world\n";
my $ssl_test_data = "\x16\x03\x01\x00\xab\x01\x00\x00\xa7\x03\x03\x89\x22\x33\x95\x43\x7a\xc3\x89\x45\x51\x12\x3c\x28\x24\x1b\x6a\x78\xbf\xbe\x95\xd8\x90\x58\xd7\x65\xf7\xbb\x2d\xb2\x8d\xa0\x75\x00\x00\x38\xc0\x2c\xc0\x30\x00\x9f\xcc\xa9\xcc\xa8\xcc\xaa\xc0\x2b\xc0\x2f\x00\x9e\xc0\x24\xc0\x28\x00\x6b\xc0\x23\xc0\x27\x00\x67\xc0\x0a\xc0\x14\x00\x39\xc0\x09\xc0\x13\x00\x33\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\xff\x01\x00\x00\x46\x00\x0b\x00\x04\x03\x00\x01\x02\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x19\x00\x18\x00\x23\x00\x00\x00\x0d\x00\x20\x00\x1e\x06\x01\x06\x02\x06\x03\x05\x01\x05\x02\x05\x03\x04\x01\x04\x02\x04\x03\x03\x01\x03\x02\x03\x03\x02\x01\x02\x02\x02\x03\x00\x16\x00\x00\x00\x17\x00\x00hello tls alone";
# Test: Shy SSH connection
if ($SSH_SHY_CNX) {
print "***Test: Shy SSH connection\n";
my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx_h;
if (defined $cnx_h) {
sleep 13;
print $cnx_h $test_data;
my $data = <$cnx_h>;
my_is($data, "ssh: $test_data", "$binary: Shy SSH connection");
}
}
# Test: One SSL half-started then one SSH
if ($SSL_MIX_SSH) {
print "***Test: One SSL half-started then one SSH\n";
my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx_l;
if (defined $cnx_l) {
print $cnx_l $ssl_test_data;
my $cnx_h= new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx_h;
if (defined $cnx_h) {
sleep 3;
print $cnx_h $test_data;
my $data_h = <$cnx_h>;
my_is($data_h, "ssh: $test_data", "$binary: SSH during SSL being established");
}
my $data;
my $n = sysread $cnx_l, $data, 1024;
my_is($data, "tls: $ssl_test_data", "$binary: SSL connection interrupted by SSH");
}
}
# Test: One SSH half-started then one SSL
if ($SSH_MIX_SSL) {
print "***Test: One SSH half-started then one SSL\n";
my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx_h;
if (defined $cnx_h) {
sleep 3;
my $cnx_l = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx_l;
if (defined $cnx_l) {
print $cnx_l $ssl_test_data;
my $data;
my $n = sysread $cnx_l, $data, 1024;
my_is($data, "tls: $ssl_test_data", "$binary: SSL during SSH being established");
}
print $cnx_h $test_data;
my $data = <$cnx_h>;
my_is($data, "ssh: $test_data", "$binary: SSH connection interrupted by SSL");
}
}
# Test: Drop connection without writing anything
if ($DROP_CNX) {
print "***Test: Connect but don't write anything\n";
my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx_h;
if ($cnx_h) {
close $cnx_h;
my_is(1, "$binary: Connect and write nothing");
# The goal of the test is to check sslh doesn't
# crash
}
}
if ($PROBES_NOFRAG) {
test_probes(no_frag => 1, binary => $binary);
}
if ($PROBES_AGAIN) {
test_probes(binary => $binary);
}
my $pid = `cat $pidfile`;
warn "killing $pid\n";
kill TERM => $pid or warn "kill process: $!\n";
sleep 1;
}
# Robustness: Connecting to non-existant server
if ($RB_CNX_NOSERVER) {
print "***Test: Connecting to non-existant server\n";
my $sslh_pid;
if (!($sslh_pid = fork)) {
exec "./sslh-select -v 3 -f -u $user --listen localhost:$sslh_port --ssh localhost:$no_listen --tls localhost:$no_listen -P $pidfile";
}
warn "spawned $sslh_pid\n";
sleep 1;
my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx_h;
if (defined $cnx_h) {
sleep 1;
my $test_data = "hello";
print $cnx_h $test_data;
}
# Ideally we should check a log is emitted.
kill TERM => `cat $pidfile` or warn "kill: $!\n";
sleep 1;
}
my $ssh_conf = (grep { $_->{name} eq "ssh" } @{$conf->fetch_array("protocols")})[0];
my $ssh_address = $ssh_conf->{host} . ":" . $ssh_conf->{port};
# Use the last TLS echoserv (no SNI/ALPN)
my $ssl_conf = (grep { $_->{name} eq "tls" } @{$conf->fetch_array("protocols")})[-1];
my $ssl_address = $ssl_conf->{host} . ":" . $ssl_conf->{port};
# Robustness: No hostname in address
if ($RB_PARAM_NOHOST) {
print "***Test: No hostname in address\n";
my $sslh_pid;
if (!($sslh_pid = fork)) {
exec "./sslh-select -v 3 -f -u $user --listen $sslh_port --ssh $ssh_address --tls $ssl_address -P $pidfile";
}
warn "spawned $sslh_pid\n";
waitpid $sslh_pid, 0;
my $code = $? >> 8;
warn "exited with $code\n";
my_is($code, 6, "Exit status on illegal option");
}
# Robustness: User does not exist
if ($RB_WRONG_USERNAME) {
print "***Test: Changing to non-existant username\n";
my $sslh_pid;
if (!($sslh_pid = fork)) {
exec "./sslh-select -v 3 -f -u ${user}_doesnt_exist --listen localhost:$no_listen --ssh $ssh_address --tls $ssl_address -P $pidfile";
}
warn "spawned $sslh_pid\n";
waitpid $sslh_pid, 0;
my $code = $? >> 8;
warn "exited with $code\n";
my_is($code, 2, "Exit status on non-existant username");
}
# Robustness: Can't open PID file
if ($RB_OPEN_PID_FILE) {
print "***Test: Can't open PID file\n";
my $sslh_pid;
if (!($sslh_pid = fork)) {
exec "./sslh-select -v 3 -f -u $user --listen localhost:$no_listen --ssh $ssh_address --tls $ssl_address -P /dont_exist/$pidfile";
# You don't have a /dont_exist/ directory, do you?!
}
warn "spawned $sslh_pid\n";
waitpid $sslh_pid, 0;
my $code = $? >> 8;
warn "exited with $code\n";
my_is($code, 3, "Exit status if can't open PID file");
}
# Robustness: Can't resolve address
if ($RB_RESOLVE_ADDRESS) {
print "***Test: Can't resolve address\n";
my $sslh_pid;
if (!($sslh_pid = fork)) {
my $user = (getpwuid $<)[0]; # Run under current username
exec "./sslh-select -v 3 -f -u $user --listen blahblah.nonexistent:9000 --ssh $ssh_address --tls $ssl_address -P $pidfile";
}
warn "spawned $sslh_pid\n";
waitpid $sslh_pid, 0;
my $code = $? >> 8;
warn "exited with $code\n";
my_is($code, 4, "Exit status if can't resolve address");
}
# Robustness: verify all command line options work
if ($RB_CL_PARAMS) {
print "***Test: Command line parameters\n";
my $sslh_pid;
if (!($sslh_pid = fork)) {
my $user = (getpwuid $<)[0]; # Run under current username
# This doesn't test --inetd
exec "./sslh-select -v 3 -f -u $user -P $pidfile".
" -n --transparent --timeout 10 -C /tmp".
" --syslog-facility auth --on-timeout ssh".
" --listen localhost:$no_listen --ssh $ssh_address --tls $ssl_address".
" --openvpn localhost:$no_listen".
" --tinc localhost:$no_listen".
" --xmpp localhost:$no_listen".
" --http localhost:$no_listen".
" --adb localhost:$no_listen".
" --socks5 localhost:$no_listen".
" --anyprot localhost:$no_listen";
exit 0;
}
warn "spawned $sslh_pid\n";
# It will die soon because $user cannot chroot (you
# don't test as root, do you?)
waitpid $sslh_pid, 0;
my $code = $? >> 8;
warn "exited with $code\n";
my_is($code, 1, "Command line arguments");
print "***Test: Bad command line parameters\n";
my $sslh_pid;
if (!($sslh_pid = fork)) {
my $user = (getpwuid $<)[0]; # Run under current username
# This doesn't test --inetd
exec "./sslh-select -v 3 -f -u $user -P $pidfile".
" -n --transparent --timeout 10 -C /tmp".
" --fakeoption".
" --anyprot localhost:$no_listen";
exit 0;
}
warn "spawned $sslh_pid\n";
waitpid $sslh_pid, 0;
my $code = $? >> 8;
warn "exited with $code\n";
my_is($code, 6, "Bad command line parameters");
}
`lcov --directory . --capture --output-file sslh_cov.info`;
`genhtml sslh_cov.info`;
`killall echosrv`;
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;
Executable
+335
View File
@@ -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;
+79
View File
@@ -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";