From 952279942160655fd33308a27bee3ea4a7cab805 Mon Sep 17 00:00:00 2001 From: Yves Rutschle Date: Tue, 15 Apr 2025 22:16:03 +0200 Subject: [PATCH 1/7] preliminary receive proxyprotocol support --- echosrv-conf.c | 2 +- echosrv-conf.h | 2 +- probe.c | 3 --- probe.h | 1 + proxyprotocol.c | 20 ++++++++++++++++++++ proxyprotocol.h | 6 +++++- sslh-conf.c | 23 +++++++++++++++++++++-- sslh-conf.h | 3 ++- sslhconf.cfg | 3 ++- tcp-probe.c | 16 ++++++++++++++-- test.cfg | 6 +++--- version.h | 2 +- 12 files changed, 71 insertions(+), 16 deletions(-) diff --git a/echosrv-conf.c b/echosrv-conf.c index 8747a31..1276f21 100644 --- a/echosrv-conf.c +++ b/echosrv-conf.c @@ -1,5 +1,5 @@ /* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README) - * on Mon Feb 24 18:37:24 2025. + * on Tue Apr 8 22:35:50 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 1436146..7ccc111 100644 --- a/echosrv-conf.h +++ b/echosrv-conf.h @@ -1,5 +1,5 @@ /* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README) - * on Mon Feb 24 18:37:24 2025. + * on Tue Apr 8 22:35:50 2025. # conf2struct: generate libconf parsers that read to structs # Copyright (C) 2018-2024 Yves Rutschle diff --git a/probe.c b/probe.c index 00a6137..d54a340 100644 --- a/probe.c +++ b/probe.c @@ -436,9 +436,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"}; diff --git a/probe.h b/probe.h index d0b768b..59eb66f 100644 --- a/probe.h +++ b/probe.h @@ -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 */ diff --git a/proxyprotocol.c b/proxyprotocol.c index aab218c..93229e8 100644 --- a/proxyprotocol.c +++ b/proxyprotocol.c @@ -20,6 +20,10 @@ */ +#include "config.h" + +#if HAVE_PROXYPROTOCOL + #include #include "common.h" #include "log.h" @@ -112,3 +116,19 @@ int pp_write_header(int pp_version, struct connection* cnx) return 0; } + +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 */ diff --git a/proxyprotocol.h b/proxyprotocol.h index fab0157..39b843d 100644 --- a/proxyprotocol.h +++ b/proxyprotocol.h @@ -3,12 +3,16 @@ #if HAVE_PROXYPROTOCOL + + int pp_write_header(int pp_version, struct connection* cnx); +int pp_header_len(char* buffer, int len); #else /* HAVE_PROXYPROTOCOL */ -static inline int pp_write_header(int pp_version, struct connection* cnx) {} +static inline int pp_write_header(int pp_version, struct connection* cnx) { return 0; } +static inline int pp_header_len(char*, int) { return 0; } #endif /* HAVE_PROXYPROTOCOL */ diff --git a/sslh-conf.c b/sslh-conf.c index a7edc8f..bf74c22 100644 --- a/sslh-conf.c +++ b/sslh-conf.c @@ -1,5 +1,5 @@ /* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README) - * on Mon Feb 24 18:37:24 2025. + * on Tue Apr 8 22:35:50 2025. # conf2struct: generate libconf parsers that read to structs # Copyright (C) 2018-2024 Yves Rutschle @@ -793,7 +793,7 @@ static struct config_desc table_sslhcfg_protocols[] = { }, { 0 } }; - + static struct config_desc table_sslhcfg_listen[] = { @@ -876,6 +876,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 } }; @@ -2471,6 +2487,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( diff --git a/sslh-conf.h b/sslh-conf.h index 55c63e9..682a27b 100644 --- a/sslh-conf.h +++ b/sslh-conf.h @@ -1,5 +1,5 @@ /* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README) - * on Mon Feb 24 18:37:24 2025. + * on Tue Apr 8 22:35:50 2025. # conf2struct: generate libconf parsers that read to structs # Copyright (C) 2018-2024 Yves Rutschle @@ -46,6 +46,7 @@ struct sslhcfg_listen_item { int is_udp; int is_unix; int keepalive; + int proxyprotocol; }; struct sslhcfg_protocols_item { diff --git a/sslhconf.cfg b/sslhconf.cfg index 6b8444f..8d05fdc 100644 --- a/sslhconf.cfg +++ b/sslhconf.cfg @@ -99,7 +99,8 @@ config: { { name: "port"; type: "string"; var: 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; } ) }, diff --git a/tcp-probe.c b/tcp-probe.c index 6c9a379..7fc32bd 100644 --- a/tcp-probe.c +++ b/tcp-probe.c @@ -45,8 +45,20 @@ 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); + + + /* + TODO il ne faut appeler ca que si on supporte pp sur le lien + */ + + int 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 ); diff --git a/test.cfg b/test.cfg index 9aa7877..f02f779 100644 --- a/test.cfg +++ b/test.cfg @@ -21,7 +21,7 @@ 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: 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,7 @@ verbose-int-error: 1; # internal errors, the kind that should never happen # Options: listen: ( - { host: "localhost"; port: "8080"; keepalive: true; }, + { 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: ""; } @@ -45,7 +45,7 @@ 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"; 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"; }, diff --git a/version.h b/version.h index 39a8717..dcf9f0b 100644 --- a/version.h +++ b/version.h @@ -1,5 +1,5 @@ #ifndef VERSION_H #define VERSION_H -#define VERSION "v2.1.4-40-g416a82f-dirty" +#define VERSION "v2.1.4-42-g4978641-dirty" #endif From 9e2e02d58e9d16cded07855796ba4fd7438ef3c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yves=20R=C5=B1tschl=C3=A9?= Date: Thu, 21 Aug 2025 12:39:01 +0200 Subject: [PATCH 2/7] print settings include proxyprotocol setting --- sslh-main.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sslh-main.c b/sslh-main.c index ba3de13..f7eab1a 100644 --- a/sslh-main.c +++ b/sslh-main.c @@ -84,14 +84,15 @@ static void printsettings(void) } } print_message(msg_config, - "%s addr: %s. libwrap service: %s log_level: %d [%s] [%s] [%s]\n", + "%s addr: %s. libwrap service: %s log_level: %d [%s] [%s] [%s] pp%d\n", p->name, buf, p->service, 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, From 12f748fe8e8fd0a1d24793b3ae94470875dd89bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yves=20R=C5=B1tschl=C3=A9?= Date: Fri, 29 Aug 2025 12:22:30 +0200 Subject: [PATCH 3/7] include test for server-side proxyprotocol --- t/run | 45 +++++++++++++++++++++++++++++++++++++++++++-- t/test_pp1.cfg | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 t/test_pp1.cfg diff --git a/t/run b/t/run index 3343b48..4ac853c 100755 --- a/t/run +++ b/t/run @@ -53,7 +53,12 @@ sub my_is { push @results, [$cnt++, $desc, $res]; } -#my $pidfile = $conf->lookup_value("pidfile"); +sub my_like { + my ($a, $b, $desc) = @_; + + my $res = like($a, $b, $desc); + push @results, [$cnt++, $desc, $res]; +} sub verbose_exec { @@ -214,7 +219,9 @@ sub run_test_probe close $cnx; my_is($prefix, $expected, "$exe:$expected: probe"); - my_is($data, $test->{data}, "$exe:$expected: shoveled"); + my $data_expected = $test->{data_expected}; + $data_expected =~ s/INPUT_DATA/$test->{data}/; + my_like($data, qr/$data_expected/s, "$exe:$expected: shoveled"); } @@ -248,7 +255,21 @@ sub test_wrong_user ################################################################################ +# 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_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. +# + my @tests = ( + { desc => "No hostname in address", run => \&test_no_host, @@ -278,6 +299,26 @@ my @tests = ( 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+.*INPUT_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", + }, + ); diff --git a/t/test_pp1.cfg b/t/test_pp1.cfg new file mode 100644 index 0000000..eaa723e --- /dev/null +++ b/t/test_pp1.cfg @@ -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"; + From 2106bd6527af3ed096087885884d53dd82ea30d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yves=20R=C5=B1tschl=C3=A9?= Date: Fri, 29 Aug 2025 20:39:42 +0200 Subject: [PATCH 4/7] added test for client-side proxyprotocol --- t/run | 48 ++++++++++++++++++++++++++++++++++++++---------- t/test_pp2.cfg | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 t/test_pp2.cfg diff --git a/t/run b/t/run index 4ac853c..cab02d1 100755 --- a/t/run +++ b/t/run @@ -124,16 +124,18 @@ sub stop_echosrv } -# Runs sslh -my ($sslh_pid); +# 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 -u $user -F $conf"; + my $cmd = "../$exe -F $conf"; print "****CMDLINE [$cmd]\n"; #$valgrind = 1; #$cmd = "valgrind --leak-check=full $cmd"; @@ -150,6 +152,7 @@ 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; } } @@ -188,8 +191,9 @@ sub run_ending_test # 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, +# 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 @@ -202,7 +206,7 @@ sub run_test_probe my $expected = $test->{expected}; - my $sslh_port = $conf->value("listen")->[0]->{port}; + 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; @@ -218,10 +222,10 @@ sub run_test_probe print "Received $n bytes: protocol $prefix data [$data]\n"; close $cnx; - my_is($prefix, $expected, "$exe:$expected: probe"); - my $data_expected = $test->{data_expected}; + 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:$expected: shoveled"); + my_like($data, qr/$data_expected/s, "$exe:$test->{desc}:shoveled"); } @@ -262,10 +266,12 @@ sub test_wrong_user # # * `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. +# before matching. Use `data` if not specified. # my @tests = ( @@ -319,6 +325,28 @@ my @tests = ( 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\x0AINPUT_DATA", + }, + + # 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", + }, + + ); diff --git a/t/test_pp2.cfg b/t/test_pp2.cfg new file mode 100644 index 0000000..749e716 --- /dev/null +++ b/t/test_pp2.cfg @@ -0,0 +1,45 @@ +# 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: "anyprot"; host: "localhost"; port: "9099"; } +); + +on_timeout: "ssh"; + From eea50a97fceda40019d83e7b08fff4356f5a5a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yves=20R=C5=B1tschl=C3=A9?= Date: Wed, 10 Sep 2025 09:28:54 +0200 Subject: [PATCH 5/7] support for proxy_protocol on incoming (client-side) link --- t/run | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/run b/t/run index cab02d1..f2f77ef 100755 --- a/t/run +++ b/t/run @@ -313,7 +313,7 @@ my @tests = ( 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+.*INPUT_DATA", + data_expected => "^PROXY TCP4 127.0.0.1 127.0.0.1 \\d+ \\d+\x0D\x0AINPUT_DATA", }, # check socks5 does not @@ -332,7 +332,7 @@ my @tests = ( 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\x0AINPUT_DATA", + 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 From 6905498d60627ef189cd8cbfcace95117fc02b6f Mon Sep 17 00:00:00 2001 From: Yves Rutschle Date: Wed, 10 Sep 2025 12:14:41 +0200 Subject: [PATCH 6/7] proxyprotocol on client-side can be removed on server side, and its info is then logged --- common.c | 26 ++++++++++++++++++++++---- common.h | 1 + proxyprotocol.c | 19 +++++++++++++++++++ proxyprotocol.h | 2 ++ t/run | 11 +++++++++++ t/test_pp2.cfg | 2 ++ tcp-probe.c | 9 ++++----- version.h | 2 +- 8 files changed, 62 insertions(+), 10 deletions(-) diff --git a/common.c b/common.c index 6701678..bc086e5 100644 --- a/common.c +++ b/common.c @@ -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) { diff --git a/common.h b/common.h index 4166b84..e56bd1e 100644 --- a/common.h +++ b/common.h @@ -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); diff --git a/proxyprotocol.c b/proxyprotocol.c index 08cd847..d163ae4 100644 --- a/proxyprotocol.c +++ b/proxyprotocol.c @@ -133,6 +133,25 @@ 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; diff --git a/proxyprotocol.h b/proxyprotocol.h index 39b843d..19523b6 100644 --- a/proxyprotocol.h +++ b/proxyprotocol.h @@ -6,12 +6,14 @@ 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 */ diff --git a/t/run b/t/run index f2f77ef..e507b12 100755 --- a/t/run +++ b/t/run @@ -346,6 +346,17 @@ my @tests = ( 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", + }, ); diff --git a/t/test_pp2.cfg b/t/test_pp2.cfg index 749e716..b496367 100644 --- a/t/test_pp2.cfg +++ b/t/test_pp2.cfg @@ -38,6 +38,8 @@ 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"; } ); diff --git a/tcp-probe.c b/tcp-probe.c index 7fc32bd..811ac5b 100644 --- a/tcp-probe.c +++ b/tcp-probe.c @@ -50,12 +50,11 @@ int probe_client_protocol(struct connection *cnx) hexdump(msg_packets, cnx->q[1].begin_deferred_data, cnx->q[1].deferred_data_size); - /* - TODO il ne faut appeler ca que si on supporte pp sur le lien - */ - - int pp_len = pp_header_len(cnx->q[1].begin_deferred_data, + 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, diff --git a/version.h b/version.h index dcf9f0b..6cd6507 100644 --- a/version.h +++ b/version.h @@ -1,5 +1,5 @@ #ifndef VERSION_H #define VERSION_H -#define VERSION "v2.1.4-42-g4978641-dirty" +#define VERSION "v2.2.4-52-geea50a9-dirty" #endif From fcfc348aee4ede8a0cfc263c5c3135c989dd5ecc Mon Sep 17 00:00:00 2001 From: Yves Rutschle Date: Wed, 10 Sep 2025 15:29:04 +0200 Subject: [PATCH 7/7] added proxyprotocol documentation --- ChangeLog | 2 + README.md | 3 +- doc/proxyprotocol.md | 120 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 doc/proxyprotocol.md diff --git a/ChangeLog b/ChangeLog index 5739756..5871651 100644 --- a/ChangeLog +++ b/ChangeLog @@ -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. diff --git a/README.md b/README.md index 1fe3b09..49fd13d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/doc/proxyprotocol.md b/doc/proxyprotocol.md new file mode 100644 index 0000000..77e259d --- /dev/null +++ b/doc/proxyprotocol.md @@ -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: + | + | :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: + | 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). +