diff --git a/mod_proxy_protocol.c b/mod_proxy_protocol.c index cc17872..d2e7762 100644 --- a/mod_proxy_protocol.c +++ b/mod_proxy_protocol.c @@ -1,6 +1,6 @@ /* * ProFTPD - mod_proxy_protocol - * Copyright (c) 2013-2021 TJ Saunders + * Copyright (c) 2013-2022 TJ Saunders * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,7 +22,6 @@ * source distribution. */ - #include "conf.h" #include "privs.h" @@ -30,7 +29,7 @@ # include #endif /* HAVE_SYS_UIO_H */ -#define MOD_PROXY_PROTOCOL_VERSION "mod_proxy_protocol/0.4" +#define MOD_PROXY_PROTOCOL_VERSION "mod_proxy_protocol/0.5" /* Make sure the version of proftpd is as necessary. */ #if PROFTPD_VERSION_NUMBER < 0x0001030507 @@ -277,6 +276,31 @@ static int readv_sock(int sockfd, const struct iovec *iov, int count) { return res; } +static int is_tls_handshake(const unsigned char *buf, size_t buflen) { + /* We can't tell if it's a TLS handshake record without at least 3 bytes of + * data. + */ + if (buflen < 3) { + return -1; + } + + if (buf[0] == 22 && + buf[1] == 3 && + (buf[2] == 0 || buf[2] == 1)) { + /* SSLv3, TLSv1+ */ + return 0; + } + + if (buf[0] == 128 && + buf[1] == 43 && + buf[2] == 1) { + /* SSLv2 */ + return 0; + } + + return -1; +} + static unsigned int strtou(const char **str, const char *last) { const char *ptr = *str; unsigned int i = 0, j, k; @@ -356,6 +380,15 @@ static int read_haproxy_v1(pool *p, conn_t *conn, */ if (i == 6) { if (strncmp(ptr, "PROXY ", 6) != 0) { + /* Check for a common error, that of TLS handshake bytes instead of + * PROXY bytes, to provide for a better diagnostic/log message. + */ + if (is_tls_handshake((const unsigned char *) ptr, i) == 0) { + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": received unexpected TLS handshake bytes from %s", + pr_netaddr_get_ipstr(conn->remote_addr)); + } + goto bad_proto; } @@ -919,6 +952,16 @@ static int read_haproxy_v2(pool *p, conn_t *conn, if (memcmp(v2_sig, haproxy_v2_sig, sizeof(haproxy_v2_sig)) != 0) { pr_trace_msg(trace_channel, 3, "invalid proxy protocol V2 signature, rejecting"); + + /* Check for a common error, that of TLS handshake bytes instead of + * PROXY bytes, to provide for a better diagnostic/log message. + */ + if (is_tls_handshake(v2_sig, sizeof(v2_sig)) == 0) { + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": received unexpected TLS handshake bytes from %s", + pr_netaddr_get_ipstr(conn->remote_addr)); + } + errno = EINVAL; return -1; } diff --git a/t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol/tls.pm b/t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol/tls.pm index 13adb0c..5d5e8e9 100644 --- a/t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol/tls.pm +++ b/t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol/tls.pm @@ -22,11 +22,33 @@ my $TESTS = { test_class => [qw(forking mod_proxy_protocol mod_tls)], }, + # NOTE: Beware of module load order; this requires that mod_proxy_protocol + # be loaded AFTER mod_tls, so that the mod_proxy_protocol init handlers + # run BEFORE mod_tls. proxy_protocol_tls_login_with_proxy_useimplicitssl => { order => ++$order, - test_class => [qw(forking mod_proxy_protocol mod_tls)], + test_class => [qw(forking inprogress mod_proxy_protocol mod_tls)], + }, + + proxy_protocol_tls_without_proxy_v1_bytes => { + order => ++$order, + test_class => [qw(bug forking mod_proxy_protocol)], + }, + + proxy_protocol_tls_without_proxy_v2_bytes => { + order => ++$order, + test_class => [qw(bug forking mod_proxy_protocol)], + }, + + proxy_protocol_starttls_without_proxy_v1_bytes => { + order => ++$order, + test_class => [qw(bug forking mod_proxy_protocol)], }, + proxy_protocol_starttls_without_proxy_v2_bytes => { + order => ++$order, + test_class => [qw(bug forking mod_proxy_protocol)], + }, }; sub new { @@ -57,10 +79,7 @@ sub list_tests { } } -# return testsuite_get_runnable_tests($TESTS); - return qw( - proxy_protocol_tls_login_with_proxy_useimplicitssl - ); + return testsuite_get_runnable_tests($TESTS); } sub proxy_protocol_tls_login_with_proxy { @@ -80,6 +99,7 @@ sub proxy_protocol_tls_login_with_proxy { AuthUserFile => $setup->{auth_user_file}, AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', IfModules => { 'mod_delay.c' => { @@ -132,8 +152,13 @@ sub proxy_protocol_tls_login_with_proxy { my $ssl_opts = { SSL_version => 'SSLv23', + SSL_ca_file => $ca_file, }; + if ($ENV{TEST_VERBOSE}) { + $ssl_opts->{Debug} = 2; + } + my $ssl_client = IO::Socket::SSL->start_SSL($client, %$ssl_opts); unless ($ssl_client) { die("TLS handshake failed: " . IO::Socket::SSL::errstr()); @@ -156,7 +181,6 @@ sub proxy_protocol_tls_login_with_proxy { die($client->message); } }; - if ($@) { $ex = $@; } @@ -176,7 +200,6 @@ sub proxy_protocol_tls_login_with_proxy { # Stop server server_stop($setup->{pid_file}); - $self->assert_child_ok($pid); test_cleanup($setup->{log_file}, $ex); @@ -194,9 +217,12 @@ sub proxy_protocol_tls_login_with_proxy_useimplicitssl { PidFile => $setup->{pid_file}, ScoreboardFile => $setup->{scoreboard_file}, SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'proxy_protocol:20 tls:30', AuthUserFile => $setup->{auth_user_file}, AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', IfModules => { 'mod_delay.c' => { @@ -210,11 +236,10 @@ sub proxy_protocol_tls_login_with_proxy_useimplicitssl { 'mod_tls.c' => { TLSEngine => 'on', TLSLog => $setup->{log_file}, - TLSProtocol => 'SSLv3 TLSv1', TLSRequired => 'on', TLSRSACertificateFile => $server_cert_file, TLSCACertificateFile => $ca_file, - TLSOptions => 'UseImplicitSSL', + TLSOptions => 'UseImplicitSSL EnableDiags', }, }, }; @@ -246,8 +271,13 @@ sub proxy_protocol_tls_login_with_proxy_useimplicitssl { my $ssl_opts = { SSL_version => 'SSLv23', + SSL_ca_file => $ca_file, }; + if ($ENV{TEST_VERBOSE}) { + $ssl_opts->{Debug} = 2; + } + my $ssl_client = IO::Socket::SSL->start_SSL($client, %$ssl_opts); unless ($ssl_client) { die("TLS handshake failed: " . IO::Socket::SSL::errstr()); @@ -275,7 +305,258 @@ sub proxy_protocol_tls_login_with_proxy_useimplicitssl { die($client->message); } }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, 10) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + test_cleanup($setup->{log_file}, $ex); +} + +sub proxy_protocol_tls_without_proxy_v1_bytes { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy_protocol'); + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'proxy_protocol:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + ProxyProtocolVersion => 'haproxyV1', + }, + }, + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + # Open pipes, for use between the parent and child processes. Specifically, + # the child will indicate when it's done with its test by writing a message + # to the parent. + my ($rfh, $wfh); + unless (pipe($rfh, $wfh)) { + die("Can't open pipe: $!"); + } + + my $ex; + + require IO::Socket::SSL; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + my $ssl_client = IO::Socket::SSL->new( + PeerHost => '127.0.0.1', + PeerPort => $port, + ); + if ($ssl_client) { + die("TLS handshake succeeded unexpectedly"); + } + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, 10) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + test_cleanup($setup->{log_file}, $ex); +} + +sub proxy_protocol_tls_without_proxy_v2_bytes { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy_protocol'); + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'proxy_protocol:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + ProxyProtocolVersion => 'haproxyV2', + }, + }, + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + # Open pipes, for use between the parent and child processes. Specifically, + # the child will indicate when it's done with its test by writing a message + # to the parent. + my ($rfh, $wfh); + unless (pipe($rfh, $wfh)) { + die("Can't open pipe: $!"); + } + + my $ex; + + require IO::Socket::SSL; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + my $ssl_client = IO::Socket::SSL->new( + PeerHost => '127.0.0.1', + PeerPort => $port, + ); + if ($ssl_client) { + die("TLS handshake succeeded unexpectedly"); + } + }; + if ($@) { + $ex = $@; + } + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, 10) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + test_cleanup($setup->{log_file}, $ex); +} + +sub proxy_protocol_starttls_without_proxy_v1_bytes { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy_protocol'); + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'proxy_protocol:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + ProxyProtocolVersion => 'haproxyV1', + }, + }, + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + # Open pipes, for use between the parent and child processes. Specifically, + # the child will indicate when it's done with its test by writing a message + # to the parent. + my ($rfh, $wfh); + unless (pipe($rfh, $wfh)) { + die("Can't open pipe: $!"); + } + + my $ex; + + require Net::FTPSSL; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + my $ssl_opts = { + Encryption => 'E', + Port => $port, + }; + + if ($ENV{TEST_VERBOSE}) { + $ssl_opts->{Debug} = 2; + } + + my $client = Net::FTPSSL->new('127.0.0.1', %$ssl_opts); + if ($client) { + die("STARTTLS FTP handshake succeeded unexpectedly"); + } + }; if ($@) { $ex = $@; } @@ -295,7 +576,94 @@ sub proxy_protocol_tls_login_with_proxy_useimplicitssl { # Stop server server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + test_cleanup($setup->{log_file}, $ex); +} + +sub proxy_protocol_starttls_without_proxy_v2_bytes { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy_protocol'); + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'proxy_protocol:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + ProxyProtocolVersion => 'haproxyV2', + }, + }, + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + # Open pipes, for use between the parent and child processes. Specifically, + # the child will indicate when it's done with its test by writing a message + # to the parent. + my ($rfh, $wfh); + unless (pipe($rfh, $wfh)) { + die("Can't open pipe: $!"); + } + + my $ex; + + require Net::FTPSSL; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + my $ssl_opts = { + Encryption => 'E', + Port => $port, + }; + + if ($ENV{TEST_VERBOSE}) { + $ssl_opts->{Debug} = 2; + } + + my $client = Net::FTPSSL->new('127.0.0.1', %$ssl_opts); + if ($client) { + die("STARTTLS FTP handshake succeeded unexpectedly"); + } + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, 10) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); $self->assert_child_ok($pid); test_cleanup($setup->{log_file}, $ex);