mirror of
https://github.com/yrutschle/sslh.git
synced 2026-10-22 18:12:10 +03:00
Merge branch 'proxyprotocol'
This commit is contained in:
commit
6f3dcfb365
23 files changed
+824
-21
No files matched your search
@@ -6,6 +6,8 @@ vNEXT:
|
||||
|
||||
accept() is now disabled for some time when ulimit -n is reached.
|
||||
|
||||
Support proxyprotocol on incoming connections.
|
||||
|
||||
Fix proxyprotocol target field (Thanks to Github user 404-Not-Found)
|
||||
|
||||
Fix memory leak in regex probe.
|
||||
|
||||
@@ -60,7 +60,8 @@ The same result can be achieved more easily by using
|
||||
`proxyprotocol` if the backend server supports it. This is a
|
||||
simple setting to add to the `sslh` protocol configuration,
|
||||
usually with an equivalently simple setting to add in
|
||||
the backend server configuration, so try that first.
|
||||
the backend server configuration, so try that first. This is
|
||||
explained in a [separate document](doc/proxyprotocol.md).
|
||||
|
||||
This means services behind `sslh` (Apache, `sshd` and so on)
|
||||
will see the external IP and ports as if the external world
|
||||
|
||||
@@ -514,10 +514,21 @@ void connect_addr(struct connection *cnx, int fd_from, connect_blocking blocking
|
||||
cnx->q[1].fd = fd;
|
||||
|
||||
if (cnx->proto->proxyprotocol_is_present) {
|
||||
pp_write_header(cnx->proto->proxyprotocol, cnx);
|
||||
/* If pp_write_header() fails, it already logs a message and there is
|
||||
* nothing much we can do. The server side will probably close the
|
||||
* connection */
|
||||
if (cnx->proto->proxyprotocol) {
|
||||
pp_write_header(cnx->proto->proxyprotocol, cnx);
|
||||
/* If pp_write_header() fails, it already logs a message and there is
|
||||
* nothing much we can do. The server side will probably close the
|
||||
* connection */
|
||||
} else {
|
||||
/* proxyprotocol == 0 means we remove a proxyprotocol header from
|
||||
* the client-side, and log PP information (as it will be lost
|
||||
* after sslh) */
|
||||
int pp_len = pp_log_connection(cnx->q[1].begin_deferred_data,
|
||||
cnx->q[1].deferred_data_size);
|
||||
if (pp_len > 0) {
|
||||
defer_skip(&cnx->q[1], pp_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,6 +551,13 @@ int defer_write(struct queue *q, void* data, ssize_t data_size)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* skip (remove) some data from the head of the queue */
|
||||
void defer_skip(struct queue *q, ssize_t data_size)
|
||||
{
|
||||
q->deferred_data += data_size;
|
||||
q->deferred_data_size -= data_size;
|
||||
}
|
||||
|
||||
/* Store some data to write *before* what's already in the queue */
|
||||
int defer_write_before(struct queue *q, void* data, ssize_t data_size)
|
||||
{
|
||||
|
||||
@@ -184,6 +184,7 @@ int resolve_split_name(struct addrinfo **out, char* hostname, char* port);
|
||||
int start_listen_sockets(struct listen_endpoint *sockfd[]);
|
||||
|
||||
int defer_write(struct queue *q, void* data, ssize_t data_size);
|
||||
void defer_skip(struct queue *q, ssize_t data_size);
|
||||
int defer_write_before(struct queue *q, void* data, ssize_t data_size);
|
||||
int flush_deferred(struct queue *q);
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
Using Proxyprotocol with sslh
|
||||
=============================
|
||||
|
||||
HAProxy defined the [Proxy protocol](https://www.haproxy.com/documentation/haproxy-configuration-tutorials/proxying-essentials/client-ip-preservation/enable-proxy-protocol/), where a proxy adds external connection information directly inside the TCP connection, for the backend server to get. This achieves the same goal as transparent proxying, while requiring no networking knowledge and no administration rights.
|
||||
|
||||
Instead, the protocol needs to be supported in the servers.
|
||||
Many do, e.g. Apache (with the `RemoteIPProxyProtocol` setting).
|
||||
|
||||
`sslh` supports this protocol both on the client and the
|
||||
server side.
|
||||
|
||||
Proxyprotocol on the server side
|
||||
--------------------------------
|
||||
|
||||
Presumably that's the most common setup, with `sslh` being
|
||||
the client-facing proxy in front of the backend server.
|
||||
Support is enabled by adding the `proxyprotocol: [1|2]`
|
||||
setting in the target protocol:
|
||||
|
||||
```
|
||||
listen:
|
||||
(
|
||||
{ host: "localhost"; port: "443"; }
|
||||
);
|
||||
|
||||
protocols:
|
||||
(
|
||||
{ name: "ssh"; host: "localhost"; port: "2222"; },
|
||||
{ name: "tls"; host: "localhost"; port: "8080"; proxyprotocol: 2; }
|
||||
);
|
||||
);
|
||||
```
|
||||
|
||||
Here `sslh` listens on port 443. It forwards `ssh`
|
||||
connections to port 2222 without touching them (which means sshd does not see the external data) (Apparently openssh does not as of september 2025 support Proxyprotocol).
|
||||
It forwards `tls` to port 8080, adding a proxy-protocol v2
|
||||
header which allows Apache (configured with RemoteIPProxyProtocol) to log the external IP and port.
|
||||
|
||||
|
||||
```
|
||||
203.0.113.4:443 :2222
|
||||
Client -----------------> sslh -------------> sshd
|
||||
198.51.100.42:2342 | logs: 127.0.0.1:<local port>
|
||||
|
|
||||
| :8080
|
||||
\--pp v2-------> apache
|
||||
logs: 198.51.100.42:2342
|
||||
|
||||
```
|
||||
|
||||
The `proxyprotocol` setting can be set to 1 or 2, which
|
||||
selects the appropriate protocol version. Version 2 being
|
||||
more efficient, this is what you should use unless you have
|
||||
good reasons to use version 1 (e.g. your backend server only
|
||||
supports v1, an unlikely scenario).
|
||||
|
||||
|
||||
Proxyprotocol on the client side
|
||||
--------------------------------
|
||||
|
||||
`sslh` also supports Proxy-protocol on incoming connections.
|
||||
These are so-called "client side", but you should not enable
|
||||
proxy-protocol on an Internet-facing server. Instead, this
|
||||
can be useful if `sslh` is located behind another proxy that
|
||||
adds proxy-protocol headers, e.g. HAProxy.
|
||||
|
||||
This is enabled by adding `proxyprotocol: true` to a
|
||||
`listen` statement:
|
||||
|
||||
```
|
||||
listen:
|
||||
(
|
||||
{ host: "localhost"; port: "443"; proxyprotocol: true; }
|
||||
);
|
||||
|
||||
protocols:
|
||||
(
|
||||
{ name: "ssh"; host: "localhost"; port: "2222"; proxyprotocol: 0; },
|
||||
{ name: "tls"; host: "localhost"; port: "8080"; }
|
||||
);
|
||||
);
|
||||
```
|
||||
|
||||
In that case, `sslh` expects to find a proxyprotocol header
|
||||
on all incoming connections. It ignores the header to
|
||||
perform its usual protocol detection.
|
||||
|
||||
If the target protocol
|
||||
specified nothing, the connection is passed untouched, i.e.
|
||||
the proxyprotocol header is forwarded as usual: here, Apache
|
||||
on 8080 will receive the proxyprotocol header.
|
||||
|
||||
If the target protocol specifies `proxyprotocol: 0`, then
|
||||
`sslh` removes the proxyprotocol header, and logs an
|
||||
additional message with the connection information extracted
|
||||
from the header. Here, `sslh` logs the external information
|
||||
for ssh connections, removes the header, and forwards the
|
||||
connection to sshd. `sshd` does not support proxyprotocol,
|
||||
and logs a connection coming from the local IP address.
|
||||
|
||||
|
||||
```
|
||||
203.0.113.4:443 :2222
|
||||
Client ---------> HaProxy ----pp v2---------> sslh -------------> sshd
|
||||
198.51.100.42:2342 | logs: 127.0.0.1:<local port>
|
||||
| sslh logs: 198.51.100.42:2342 to localhost:2222
|
||||
|
|
||||
| :8080
|
||||
\--pp v2-------> apache
|
||||
logs: 198.51.100.42:2342
|
||||
sslh logs: local connection
|
||||
```
|
||||
|
||||
`sslh` always logs the 'real' network information, here the
|
||||
local addresses, and only logs the proxyprotocol information
|
||||
when it removes the header. It is not necessary to log the
|
||||
proxyprotocol information when the target server will
|
||||
receive it (as it is then the target server's job to log the
|
||||
external IP address).
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
|
||||
* on Wed Jul 16 17:59:23 2025.
|
||||
* on Fri Aug 29 20:43:29 2025.
|
||||
|
||||
# conf2struct: generate libconf parsers that read to structs
|
||||
# Copyright (C) 2018-2024 Yves Rutschle
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
|
||||
* on Wed Jul 16 17:59:23 2025.
|
||||
* on Fri Aug 29 20:43:29 2025.
|
||||
|
||||
# conf2struct: generate libconf parsers that read to structs
|
||||
# Copyright (C) 2018-2024 Yves Rutschle
|
||||
|
||||
@@ -465,9 +465,6 @@ int probe_buffer(char* buf, int len,
|
||||
struct sslhcfg_protocols_item* p;
|
||||
int i, res, again = 0;
|
||||
|
||||
print_message(msg_packets, "hexdump of incoming packet:\n");
|
||||
hexdump(msg_packets, buf, len);
|
||||
|
||||
*proto_out = NULL;
|
||||
for (i = 0; i < proto_len; i++) {
|
||||
char* probe_str[3] = {"PROBE_NEXT", "PROBE_MATCH", "PROBE_AGAIN"};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "common.h"
|
||||
#include "tls.h"
|
||||
#include "log.h"
|
||||
#include "proxyprotocol.h"
|
||||
|
||||
typedef enum {
|
||||
PROBE_NEXT, /* Enough data, probe failed -- it's some other protocol */
|
||||
|
||||
@@ -133,4 +133,37 @@ int pp_write_header(int pp_version, struct connection* cnx)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Logs the connection data in the pp header and return header length */
|
||||
int pp_log_connection(char* buffer, ssize_t buffer_len)
|
||||
{
|
||||
pp_info_t pp_info;
|
||||
|
||||
int header_len = pp_parse_hdr((uint8_t*)buffer, buffer_len, &pp_info);
|
||||
|
||||
if (header_len > 0) {
|
||||
print_message(msg_connections, "client had proxyprotocol info: %s:%d to %s:%d not forwarded to server\n",
|
||||
pp_info.src_addr, pp_info.src_port,
|
||||
pp_info.dst_addr, pp_info.dst_port);
|
||||
} else {
|
||||
print_message(msg_connections, "no proxyprotocol data found on client side, forwarding as is\n");
|
||||
header_len = 0;
|
||||
}
|
||||
|
||||
return header_len;
|
||||
}
|
||||
|
||||
int pp_header_len(char* buffer, int buffer_len)
|
||||
{
|
||||
pp_info_t pp_info;
|
||||
|
||||
int header_len = pp_parse_hdr((uint8_t*)buffer, buffer_len, &pp_info);
|
||||
|
||||
print_message(msg_probe_info, "proxyprotocol header %d bytes found\n", header_len);
|
||||
|
||||
if (header_len < 0) header_len = 0;
|
||||
|
||||
return header_len;
|
||||
}
|
||||
|
||||
|
||||
#endif /* HAVE_PROXYPROTOCOL */
|
||||
@@ -3,12 +3,18 @@
|
||||
|
||||
|
||||
#if HAVE_PROXYPROTOCOL
|
||||
|
||||
|
||||
int pp_write_header(int pp_version, struct connection* cnx);
|
||||
int pp_log_connection(char* buffer, ssize_t buffer_len);
|
||||
int pp_header_len(char* buffer, int len);
|
||||
|
||||
|
||||
#else /* HAVE_PROXYPROTOCOL */
|
||||
|
||||
static inline int pp_write_header(int pp_version, struct connection* cnx) { return 0; }
|
||||
static int pp_log_connection(char* buffer, ssize_t buffer_len) { return 0; }
|
||||
static inline int pp_header_len(char*, int) { return 0; }
|
||||
|
||||
#endif /* HAVE_PROXYPROTOCOL */
|
||||
|
||||
|
||||
+21
-2
@@ -1,5 +1,5 @@
|
||||
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
|
||||
* on Mon Aug 4 18:25:44 2025.
|
||||
* on Fri Aug 29 20:43:29 2025.
|
||||
|
||||
# conf2struct: generate libconf parsers that read to structs
|
||||
# Copyright (C) 2018-2024 Yves Rutschle
|
||||
@@ -809,7 +809,7 @@ static struct config_desc table_sslhcfg_protocols[] = {
|
||||
},
|
||||
{ 0 }
|
||||
};
|
||||
|
||||
|
||||
static struct config_desc table_sslhcfg_listen[] = {
|
||||
|
||||
|
||||
@@ -908,6 +908,22 @@ static struct config_desc table_sslhcfg_listen[] = {
|
||||
/* optional */ 0,
|
||||
/* default_val*/ .default_val.def_bool = 0
|
||||
},
|
||||
|
||||
{
|
||||
/* name */ "proxyprotocol",
|
||||
/* type */ CFG_BOOL,
|
||||
/* sub_group*/ NULL,
|
||||
/* arg_cl */ NULL,
|
||||
/* base_addr */ NULL,
|
||||
/* offset */ offsetof(struct sslhcfg_listen_item, proxyprotocol),
|
||||
/* offset_len */ 0,
|
||||
/* offset_present */ 0,
|
||||
/* size */ sizeof(int),
|
||||
/* array_type */ -1,
|
||||
/* mandatory */ 0,
|
||||
/* optional */ 0,
|
||||
/* default_val*/ .default_val.def_bool = 0
|
||||
},
|
||||
{ 0 }
|
||||
};
|
||||
|
||||
@@ -2513,6 +2529,9 @@ static void sslhcfg_listen_fprint(
|
||||
indent(out, depth);
|
||||
fprintf(out, "keepalive: %d", sslhcfg_listen->keepalive);
|
||||
fprintf(out, "\n");
|
||||
indent(out, depth);
|
||||
fprintf(out, "proxyprotocol: %d", sslhcfg_listen->proxyprotocol);
|
||||
fprintf(out, "\n");
|
||||
}
|
||||
|
||||
void sslhcfg_fprint(
|
||||
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
|
||||
* on Mon Aug 4 18:25:44 2025.
|
||||
* on Fri Aug 29 20:43:29 2025.
|
||||
|
||||
# conf2struct: generate libconf parsers that read to structs
|
||||
# Copyright (C) 2018-2024 Yves Rutschle
|
||||
@@ -48,6 +48,7 @@ struct sslhcfg_listen_item {
|
||||
int is_udp;
|
||||
int is_unix;
|
||||
int keepalive;
|
||||
int proxyprotocol;
|
||||
};
|
||||
|
||||
struct sslhcfg_protocols_item {
|
||||
|
||||
+3
-2
@@ -87,7 +87,7 @@ static void printsettings(void)
|
||||
}
|
||||
}
|
||||
print_message(msg_config,
|
||||
"%s addr: %s. max_cnx: %d libwrap service: %s log_level: %d [%s] [%s] [%s]\n",
|
||||
"%s addr: %s. max_cnx: %d libwrap service: %s log_level: %d [%s] [%s] [%s] pp:%d\n",
|
||||
p->name,
|
||||
buf,
|
||||
p->max_connections,
|
||||
@@ -95,7 +95,8 @@ static void printsettings(void)
|
||||
p->log_level,
|
||||
p->keepalive ? "keepalive" : "",
|
||||
p->fork ? "fork" : "",
|
||||
p->transparent ? "transparent" : ""
|
||||
p->transparent ? "transparent" : "",
|
||||
p->proxyprotocol_is_present ? p->proxyprotocol : -1
|
||||
);
|
||||
}
|
||||
print_message(msg_config,
|
||||
|
||||
+2
-1
@@ -100,7 +100,8 @@ config: {
|
||||
{ name: "max_connections"; type: "int"; optional: true; },
|
||||
{ name: "is_udp"; type: "bool"; default: false },
|
||||
{ name: "is_unix"; type: "bool"; default: false },
|
||||
{ name: "keepalive"; type: "bool"; default: false; }
|
||||
{ name: "keepalive"; type: "bool"; default: false; },
|
||||
{ name: "proxyprotocol"; type: "bool"; default: false; }
|
||||
)
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
#! /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];
|
||||
}
|
||||
|
||||
sub my_like {
|
||||
my ($a, $b, $desc) = @_;
|
||||
|
||||
my $res = like($a, $b, $desc);
|
||||
push @results, [$cnt++, $desc, $res];
|
||||
}
|
||||
|
||||
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, restart it if configuration has changed
|
||||
my ($sslh_pid, $sslh_conf);
|
||||
sub start_sslh
|
||||
{
|
||||
my ($exe, $conf) = @_;
|
||||
stop_sslh() if (defined $sslh_conf and ($conf ne $sslh_conf));
|
||||
return if defined $sslh_pid;
|
||||
$sslh_conf = $conf;
|
||||
my ($valgrind);
|
||||
if (!($sslh_pid = fork)) {
|
||||
my $user = (getpwuid $<)[0]; # Run under current username
|
||||
my $cmd = "../$exe -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";
|
||||
`killall sslh-ev sslh-select sslh-fork`;
|
||||
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.
|
||||
# Connect to port specified in the test, otherwise to the
|
||||
# first port in the sslh configuration.
|
||||
# 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 = $test->{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:$test->{desc}:probe");
|
||||
my $data_expected = $test->{data_expected} // "^$test->{data}\$";
|
||||
$data_expected =~ s/INPUT_DATA/$test->{data}/;
|
||||
my_like($data, qr/$data_expected/s, "$exe:$test->{desc}: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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
################################################################################
|
||||
|
||||
# Each test is decribed with an entry containing the
|
||||
# following fields:
|
||||
#
|
||||
# * `run`: which test method to use
|
||||
#
|
||||
# * `expected`: protocol prompt added by the echosrv (should be the protocol name)
|
||||
#
|
||||
# * `data`: what is sent to sslh, as a client
|
||||
#
|
||||
# * `data_expected` is a regex for the expected return (which can be
|
||||
# different when using Proxyprotocol). INPUT_DATA gets
|
||||
# replaced with $_->{data}, i.e. data sent originally,
|
||||
# before matching. Use `data` if not specified.
|
||||
#
|
||||
|
||||
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
|
||||
},
|
||||
|
||||
# test_pp1.cfg has proxyprotocol on ssh but not socks5.
|
||||
# Check ssh has pp
|
||||
{
|
||||
desc => "Server-side ProxyProtocol active",
|
||||
run => \&run_test_probe,
|
||||
cfg => "test_pp1.cfg",
|
||||
data => "SSH-2.0 hello",
|
||||
expected => "ssh",
|
||||
data_expected => "^PROXY TCP4 127.0.0.1 127.0.0.1 \\d+ \\d+\x0D\x0AINPUT_DATA",
|
||||
},
|
||||
|
||||
# check socks5 does not
|
||||
{
|
||||
desc => "Server-side ProxyProtocol not active",
|
||||
run => \&run_test_probe,
|
||||
cfg => "test_pp1.cfg",
|
||||
data => "\x05\x04\x01\x02\x03\x04",
|
||||
expected => "socks5",
|
||||
},
|
||||
|
||||
# test_pp2.cfg has proxyprotocol on port 8080
|
||||
{
|
||||
desc => "Client-side ProxyProtocol active",
|
||||
run => \&run_test_probe,
|
||||
cfg => "test_pp2.cfg",
|
||||
data => "PROXY TCP4 127.0.0.1 127.0.0.1 8080 8081\x0D\x0ASSH-2.0 hello",
|
||||
expected => "ssh",
|
||||
data_expected => "^PROXY TCP4 127.0.0.1 127.0.0.1 \\d+ \\d+\x0D\x0ASSH-2.0 hello",
|
||||
},
|
||||
|
||||
# test_pp2.cfg has no proxyprotocol on port 8081, so ssh
|
||||
# should pass untouched
|
||||
{
|
||||
desc => "Client-side ProxyProtocol not active",
|
||||
run => \&run_test_probe,
|
||||
cfg => "test_pp2.cfg",
|
||||
port => 8081,
|
||||
data => "SSH-2.0 hello",
|
||||
expected => "ssh",
|
||||
},
|
||||
|
||||
# test_pp2.cfg has proxyprotocol on port 8080 but
|
||||
# removes it towards "regex_remove_pp" tarbet
|
||||
# TODO: Log the addresses!
|
||||
{
|
||||
desc => "Client-side ProxyProtocol active but removed",
|
||||
run => \&run_test_probe,
|
||||
cfg => "test_pp2.cfg",
|
||||
data => "PROXY TCP4 127.0.0.1 127.0.0.1 8080 8081\x0D\x0Aregex_remove_pp hello",
|
||||
expected => "regex",
|
||||
data_expected => "^regex_remove_pp hello",
|
||||
},
|
||||
|
||||
);
|
||||
|
||||
|
||||
# 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
@@ -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";
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Testing for server-side ProxyProtocol
|
||||
|
||||
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; }
|
||||
);
|
||||
|
||||
|
||||
protocols:
|
||||
(
|
||||
{ name: "ssh"; host: "localhost"; port: "9000"; proxyprotocol: 1; },
|
||||
{ name: "socks5"; host: "localhost"; port: "9001"; },
|
||||
{ name: "anyprot"; host: "localhost"; port: "9099"; }
|
||||
);
|
||||
|
||||
on_timeout: "ssh";
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Testing for client-side ProxyProtocol
|
||||
|
||||
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; proxyprotocol: false; }
|
||||
);
|
||||
|
||||
|
||||
protocols:
|
||||
(
|
||||
{ name: "ssh"; host: "localhost"; port: "9000"; },
|
||||
{ name: "socks5"; host: "localhost"; port: "9001"; },
|
||||
{ name: "regex"; host: "localhost"; port: "9002";
|
||||
regex_patterns: [ "^regex_remove_pp" ]; proxyprotocol: 0; },
|
||||
{ name: "anyprot"; host: "localhost"; port: "9099"; }
|
||||
);
|
||||
|
||||
on_timeout: "ssh";
|
||||
|
||||
+13
-2
@@ -45,8 +45,19 @@ int probe_client_protocol(struct connection *cnx)
|
||||
|
||||
if (n > 0) {
|
||||
defer_write(&cnx->q[1], buffer, n);
|
||||
return probe_buffer(cnx->q[1].begin_deferred_data,
|
||||
cnx->q[1].deferred_data_size,
|
||||
|
||||
print_message(msg_packets, "hexdump of incoming packet:\n");
|
||||
hexdump(msg_packets, cnx->q[1].begin_deferred_data, cnx->q[1].deferred_data_size);
|
||||
|
||||
|
||||
size_t pp_len = 0;
|
||||
if (cnx->endpoint->endpoint_cfg->proxyprotocol) {
|
||||
pp_len = pp_header_len(cnx->q[1].begin_deferred_data,
|
||||
cnx->q[1].deferred_data_size);
|
||||
}
|
||||
|
||||
return probe_buffer(cnx->q[1].begin_deferred_data + pp_len,
|
||||
cnx->q[1].deferred_data_size - pp_len,
|
||||
tcp_protocols, tcp_protocols_len,
|
||||
&cnx->proto
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@ verbose-connections-error: 1; # connection errors
|
||||
verbose-connections-try: 1; # connection attempts towards targets
|
||||
verbose-fd: 1; # file descriptor activity, open/close/whatnot
|
||||
verbose-packets: 1; # hexdump packets on which probing is done
|
||||
verbose-probe-info: 0; # what's happening during the probe process
|
||||
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
|
||||
@@ -30,7 +30,8 @@ verbose-int-error: 1; # internal errors, the kind that should never happen
|
||||
# Options:
|
||||
listen:
|
||||
(
|
||||
{ host: "localhost"; port: "8080"; keepalive: true; max_connections: 2; },
|
||||
{ host: "localhost"; port: "8080"; keepalive: true;
|
||||
max_connections: 2; proxyprotocol: true; },
|
||||
{ host: "localhost"; port: "8081"; keepalive: true; },
|
||||
# { host: "ip4-localhost"; is_udp: true; port: "8086"; },
|
||||
{ host: "/tmp/sslh.sock"; is_unix: true; port: ""; }
|
||||
@@ -45,7 +46,7 @@ protocols:
|
||||
(
|
||||
{ name: "ssh"; host: "localhost"; port: "9000"; fork: true; transparent: true; resolve_on_forward: true; max_connections: 4; },
|
||||
{ name: "socks5"; host: "localhost"; port: "9001"; },
|
||||
{ name: "http"; host: "localhost"; port: "80"; proxyprotocol: 2; },
|
||||
{ name: "http"; host: "localhost"; port: "80"; },
|
||||
{ name: "tinc"; host: "localhost"; port: "9003"; },
|
||||
{ name: "openvpn"; host: "localhost"; port: "9004"; },
|
||||
{ name: "xmpp"; host: "localhost"; port: "9009"; },
|
||||
|
||||
Reference in new issue
Block a user