From b0e33784b9f76329856abdad1b412aa4303bb8f8 Mon Sep 17 00:00:00 2001 From: TJ Saunders Date: Sun, 19 Jun 2022 15:46:39 -0700 Subject: [PATCH] Issue #158: Adding more regression test coverage for proxying FTP data transfers in various situations. We have the following independent axes to attempt to handle, automatically if we can: * active _vs_ passive FTP data transfers * public _vs_ private IP addresses * IPv4 _vs_ IPv6 addresses And in some cases, all we can do is log a message indicating that the admin needs to explicitly configure an appropriate `ProxySourceAddress` for `mod_proxy` to use. --- .github/workflows/regressions.yml | 1 + lib/proxy/conn.c | 50 +- lib/proxy/ftp/conn.c | 12 +- mod_proxy.c | 54 +- .../Tests/Modules/mod_proxy/reverse/ipv6.pm | 2020 ++++++++++++++++- 5 files changed, 2052 insertions(+), 85 deletions(-) diff --git a/.github/workflows/regressions.yml b/.github/workflows/regressions.yml index 5ebbef1..b1ce373 100644 --- a/.github/workflows/regressions.yml +++ b/.github/workflows/regressions.yml @@ -81,6 +81,7 @@ jobs: libfile-spec-native-perl \ libmime-base32-perl \ libnet-address-ip-local-perl \ + libnet-inet6glue-perl \ libnet-ssh2-perl \ libnet-ssleay-perl \ libnet-telnet-perl \ diff --git a/lib/proxy/conn.c b/lib/proxy/conn.c index 4201376..1625aed 100644 --- a/lib/proxy/conn.c +++ b/lib/proxy/conn.c @@ -717,6 +717,21 @@ conn_t *proxy_conn_get_server_conn(pool *p, struct proxy_session *proxy_sess, pr_trace_msg(trace_channel, 4, "error converting IPv6 local address %s to IPv4 address: %s", pr_netaddr_get_ipstr(session.c->local_addr), strerror(errno)); + + if (proxy_sess->src_addr == NULL) { + (void) pr_log_writefile(proxy_logfd, MOD_PROXY_VERSION, + "local address '%s' is an IPv6 address, and remote address " + "'%s' is an IPv4 address; consider using ProxySourceAddress " + "directive to configure an IPv4 address", + pr_netaddr_get_ipstr(session.c->local_addr), + pr_netaddr_get_ipstr(remote_addr)); + } + + } else { + pr_trace_msg(trace_channel, 9, + "converted IPv6 local address %s to IPv4 address %s", + pr_netaddr_get_ipstr(session.c->local_addr), + pr_netaddr_get_ipstr(local_addr)); } } @@ -755,7 +770,7 @@ conn_t *proxy_conn_get_server_conn(pool *p, struct proxy_session *proxy_sess, if (local_family != remote_family) { pr_netaddr_t *new_addr = NULL; -#ifdef PR_USE_IPV6 +#if defined(PR_USE_IPV6) if (local_family == AF_INET) { new_addr = pr_netaddr_v4tov6(p, new_local_addr); @@ -808,22 +823,25 @@ conn_t *proxy_conn_get_server_conn(pool *p, struct proxy_session *proxy_sess, * for those particular errors, and if so, log a suggestion to explicitly * configure an appropriate ProxySourceAddress (Issue #213). */ - if (netaddr_is_private(bind_addr) == TRUE) { - if (netaddr_is_private(remote_addr) != TRUE) { - (void) pr_log_writefile(proxy_logfd, MOD_PROXY_VERSION, - "local address '%s' is a private network address, and remote address " - "'%s' is a public address; consider using ProxySourceAddress " - "directive to configure a public local address", - pr_netaddr_get_ipstr(bind_addr), remote_ipstr); - } - } else { - if (netaddr_is_private(remote_addr) == TRUE) { - (void) pr_log_writefile(proxy_logfd, MOD_PROXY_VERSION, - "local address '%s' is a public address, and remote address '%s' is " - "a private network address; consider using ProxySourceAddress " - "directive to configure a private local address", - pr_netaddr_get_ipstr(bind_addr), remote_ipstr); + if (pr_netaddr_get_family(bind_addr) == pr_netaddr_get_family(remote_addr)) { + if (netaddr_is_private(bind_addr) == TRUE) { + if (netaddr_is_private(remote_addr) != TRUE) { + (void) pr_log_writefile(proxy_logfd, MOD_PROXY_VERSION, + "local address '%s' is a private network address, and remote " + "address '%s' is a public address; consider using " + "ProxySourceAddress directive to configure a public local address", + pr_netaddr_get_ipstr(bind_addr), remote_ipstr); + } + + } else { + if (netaddr_is_private(remote_addr) == TRUE) { + (void) pr_log_writefile(proxy_logfd, MOD_PROXY_VERSION, + "local address '%s' is a public address, and remote address '%s' " + "is a private network address; consider using ProxySourceAddress " + "directive to configure a private local address", + pr_netaddr_get_ipstr(bind_addr), remote_ipstr); + } } } diff --git a/lib/proxy/ftp/conn.c b/lib/proxy/ftp/conn.c index a144dc5..35d6b9e 100644 --- a/lib/proxy/ftp/conn.c +++ b/lib/proxy/ftp/conn.c @@ -1,6 +1,6 @@ /* * ProFTPD - mod_proxy FTP connection routines - * 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 @@ -166,7 +166,7 @@ conn_t *proxy_ftp_conn_connect(pool *p, const pr_netaddr_t *bind_addr, pr_netaddr_get_ipstr(remote_addr), ntohs(pr_netaddr_get_port(remote_addr)), pr_netaddr_get_ipstr(bind_addr), ntohs(pr_netaddr_get_port(bind_addr))); - if (frontend_data) { + if (frontend_data == TRUE) { res = pr_inet_connect(p, conn, remote_addr, ntohs(pr_netaddr_get_port(remote_addr))); @@ -182,7 +182,7 @@ conn_t *proxy_ftp_conn_connect(pool *p, const pr_netaddr_t *bind_addr, "unable to connect to %s#%u: %s\n", pr_netaddr_get_ipstr(remote_addr), ntohs(pr_netaddr_get_port(remote_addr)), strerror(xerrno)); - if (!frontend_data) { + if (frontend_data == FALSE) { proxy_inet_close(session.pool, conn); } pr_inet_close(session.pool, conn); @@ -193,7 +193,7 @@ conn_t *proxy_ftp_conn_connect(pool *p, const pr_netaddr_t *bind_addr, /* XXX Will it always be STRM_DATA? */ - if (frontend_data) { + if (frontend_data == TRUE) { opened = pr_inet_openrw(session.pool, conn, NULL, PR_NETIO_STRM_DATA, conn->listen_fd, -1, -1, TRUE); @@ -207,7 +207,7 @@ conn_t *proxy_ftp_conn_connect(pool *p, const pr_netaddr_t *bind_addr, if (opened == NULL) { int xerrno = errno; - if (!frontend_data) { + if (frontend_data == FALSE) { proxy_inet_close(session.pool, conn); } pr_inet_close(session.pool, conn); @@ -219,7 +219,7 @@ conn_t *proxy_ftp_conn_connect(pool *p, const pr_netaddr_t *bind_addr, /* The conn returned by pr_inet_openrw() is a copy of the input conn; * we no longer need the input conn at this point. */ - if (frontend_data) { + if (frontend_data == TRUE) { pr_inet_close(session.pool, conn); pr_pool_tag(opened->pool, "proxy frontend data connect conn pool"); diff --git a/mod_proxy.c b/mod_proxy.c index 7e08a6c..9497079 100644 --- a/mod_proxy.c +++ b/mod_proxy.c @@ -2389,24 +2389,70 @@ static int proxy_data_prepare_conns(struct proxy_session *proxy_sess, /* XXX Should handle EPSV_ALL here, too. */ if (proxy_sess->backend_sess_flags & SF_PASSIVE) { - const pr_netaddr_t *bind_addr = NULL; + const pr_netaddr_t *bind_addr = NULL, *local_addr = NULL; /* Connect to the backend server now. We won't receive the initial * response until we connect to the backend data address/port. */ + /* Check the family of the remote address vs what we'll be using to connect. + * If there's a mismatch, we need to get an addr with the matching family. + */ + if (pr_netaddr_get_family(bind_addr) != pr_netaddr_get_family(proxy_sess->backend_data_addr)) { + /* In this scenario, the proxy has an IPv6 socket, but the remote/backend + * server has an IPv4 (or IPv4-mapped IPv6) address. OR it's the proxy + * which has an IPv4 socket, and the remote/backend server has an IPv6 + * address. + */ + if (pr_netaddr_get_family(session.c->local_addr) == AF_INET) { + char *ip_str; + + /* Convert the local address from an IPv4 to an IPv6 addr. */ + ip_str = pcalloc(cmd->tmp_pool, INET6_ADDRSTRLEN + 1); + snprintf(ip_str, INET6_ADDRSTRLEN, "::ffff:%s", + pr_netaddr_get_ipstr(session.c->local_addr)); + local_addr = pr_netaddr_get_addr(cmd->tmp_pool, ip_str, NULL); + + } else { + local_addr = pr_netaddr_v6tov4(cmd->tmp_pool, session.c->local_addr); + if (local_addr == NULL) { + pr_trace_msg(trace_channel, 4, + "error converting IPv6 local address %s to IPv4 address: %s", + pr_netaddr_get_ipstr(session.c->local_addr), strerror(errno)); + + if (proxy_sess->src_addr == NULL) { + (void) pr_log_writefile(proxy_logfd, MOD_PROXY_VERSION, + "local address '%s' is an IPv6 address, and remote address " + "'%s' is an IPv4 address; consider using ProxySourceAddress " + "directive to configure an IPv4 address", + pr_netaddr_get_ipstr(session.c->local_addr), + pr_netaddr_get_ipstr(proxy_sess->backend_data_addr)); + } + } + } + + if (local_addr != NULL) { + pr_trace_msg(trace_channel, 7, + "converted local address %s to %s for passive backend transfer", + pr_netaddr_get_ipstr(session.c->local_addr), + pr_netaddr_get_ipstr(local_addr)); + + } else { + local_addr = session.c->local_addr; + } + } + /* Specify the specific address/interface to use as the source address for * connections to the destination server. */ bind_addr = proxy_sess->src_addr; if (bind_addr == NULL) { - bind_addr = session.c->local_addr; + bind_addr = local_addr; } if (pr_netaddr_is_loopback(bind_addr) == TRUE && pr_netaddr_is_loopback(proxy_sess->backend_ctrl_conn->remote_addr) != TRUE) { const char *local_name; - const pr_netaddr_t *local_addr; local_name = pr_netaddr_get_localaddr_str(cmd->pool); local_addr = pr_netaddr_get_addr(cmd->pool, local_name, NULL); @@ -2423,7 +2469,7 @@ static int proxy_data_prepare_conns(struct proxy_session *proxy_sess, if (local_family != remote_family) { pr_netaddr_t *new_addr = NULL; -#ifdef PR_USE_IPV6 +#if defined(PR_USE_IPV6) if (local_family == AF_INET) { new_addr = pr_netaddr_v4tov6(cmd->pool, local_addr); diff --git a/t/lib/ProFTPD/Tests/Modules/mod_proxy/reverse/ipv6.pm b/t/lib/ProFTPD/Tests/Modules/mod_proxy/reverse/ipv6.pm index 84a63a8..eea9c99 100644 --- a/t/lib/ProFTPD/Tests/Modules/mod_proxy/reverse/ipv6.pm +++ b/t/lib/ProFTPD/Tests/Modules/mod_proxy/reverse/ipv6.pm @@ -10,6 +10,7 @@ use File::Path qw(mkpath); use File::Spec; use IO::Handle; use IO::Socket::INET; +use IO::Socket::INET6; use Net::Address::IP::Local; use ProFTPD::TestSuite::FTP; @@ -44,6 +45,62 @@ my $TESTS = { order => ++$order, test_class => [qw(feature_ipv6 forking reverse)], }, + + proxy_reverse_ipv4mappedipv6_eprt_ipv4_backend_issue158 => { + order => ++$order, + test_class => [qw(bug feature_ipv6 forking reverse)], + }, + + proxy_reverse_ipv4mappedipv6_epsv_ipv4_backend_issue158 => { + order => ++$order, + test_class => [qw(bug feature_ipv6 forking reverse)], + }, + + proxy_reverse_ipv4mappedipv6_active_list_ipv4_backend_issue158 => { + order => ++$order, + test_class => [qw(bug feature_ipv6 forking reverse)], + }, + + proxy_reverse_ipv4mappedipv6_port_list_ipv4_backend_issue158 => { + order => ++$order, + test_class => [qw(bug feature_ipv6 forking reverse)], + }, + + proxy_reverse_ipv6only_eprt_ipv4_backend_issue158 => { + order => ++$order, + test_class => [qw(bug feature_ipv6 forking reverse)], + }, + + proxy_reverse_ipv6only_epsv_ipv4_backend_issue158 => { + order => ++$order, + test_class => [qw(bug feature_ipv6 forking reverse)], + }, + + proxy_reverse_ipv6only_active_list_ipv4_backend_issue158 => { + order => ++$order, + test_class => [qw(bug feature_ipv6 forking reverse)], + }, + + proxy_reverse_ipv6only_port_list_ipv4_backend_issue158 => { + order => ++$order, + test_class => [qw(bug feature_ipv6 forking reverse)], + }, + + proxy_reverse_ipv4_active_list_ipv6only_backend_issue158 => { + order => ++$order, + test_class => [qw(bug feature_ipv6 forking reverse)], + }, + + proxy_reverse_ipv4_port_list_ipv6only_backend_issue158 => { + order => ++$order, + test_class => [qw(bug feature_ipv6 forking reverse)], + }, + + proxy_reverse_ipv4_passive_list_ipv6only_backend_issue158 => { + order => ++$order, + test_class => [qw(bug feature_ipv6 forking reverse)], + }, + }; sub new { @@ -54,7 +111,25 @@ sub list_tests { return testsuite_get_runnable_tests($TESTS); } -sub get_reverse_proxy_config { +sub get_reverse_proxy_config_ipv4 { + my $tmpdir = shift; + my $log_file = shift; + my $vhost_port = shift; + + my $table_dir = File::Spec->rel2abs("$tmpdir/var/proxy"); + + my $config = { + ProxyEngine => 'on', + ProxyLog => $log_file, + ProxyReverseServers => "ftp://127.0.0.1:$vhost_port", + ProxyRole => 'reverse', + ProxyTables => $table_dir, + }; + + return $config; +} + +sub get_reverse_proxy_config_ipv6 { my $tmpdir = shift; my $log_file = shift; my $vhost_port = shift; @@ -112,7 +187,8 @@ sub proxy_reverse_ipv6_list_pasv { my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); $vhost_port += 12; - my $proxy_config = get_reverse_proxy_config($tmpdir, $log_file, $vhost_port); + my $proxy_config = get_reverse_proxy_config_ipv6($tmpdir, $log_file, + $vhost_port); my $timeout_idle = 10; @@ -286,7 +362,8 @@ sub proxy_reverse_ipv6_list_port { my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); $vhost_port += 12; - my $proxy_config = get_reverse_proxy_config($tmpdir, $log_file, $vhost_port); + my $proxy_config = get_reverse_proxy_config_ipv6($tmpdir, $log_file, + $vhost_port); my $timeout_idle = 10; @@ -461,7 +538,8 @@ sub proxy_reverse_ipv6_epsv { my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); $vhost_port += 12; - my $proxy_config = get_reverse_proxy_config($tmpdir, $log_file, $vhost_port); + my $proxy_config = get_reverse_proxy_config_ipv6($tmpdir, $log_file, + $vhost_port); my $timeout_idle = 10; @@ -622,7 +700,8 @@ sub proxy_reverse_ipv6_eprt_ipv4 { my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); $vhost_port += 12; - my $proxy_config = get_reverse_proxy_config($tmpdir, $log_file, $vhost_port); + my $proxy_config = get_reverse_proxy_config_ipv6($tmpdir, $log_file, + $vhost_port); my $timeout_idle = 10; @@ -751,55 +830,155 @@ EOC sub proxy_reverse_ipv6_eprt_ipv6 { my $self = shift; my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy'); - my $config_file = "$tmpdir/proxy.conf"; - my $pid_file = File::Spec->rel2abs("$tmpdir/proxy.pid"); - my $scoreboard_file = File::Spec->rel2abs("$tmpdir/proxy.scoreboard"); + my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); + $vhost_port += 12; - my $log_file = test_get_logfile(); + my $proxy_config = get_reverse_proxy_config_ipv6($tmpdir, $setup->{log_file}, + $vhost_port); - my $auth_user_file = File::Spec->rel2abs("$tmpdir/proxy.passwd"); - my $auth_group_file = File::Spec->rel2abs("$tmpdir/proxy.group"); + my $timeout_idle = 10; - my $user = 'proftpd'; - my $passwd = 'test'; - my $group = 'ftpd'; - my $home_dir = File::Spec->rel2abs($tmpdir); - my $uid = 500; - my $gid = 500; + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'DEFAULT:10 event:0 lock:0 scoreboard:0 signal:0 proxy:20 proxy.ftp.conn:20 proxy.ftp.ctrl:20 proxy.ftp.data:20 proxy.ftp.msg:20 proxy.ftp.xfer:20', - # Make sure that, if we're running as root, that the home directory has - # permissions/privs set for the account we create - if ($< == 0) { - unless (chmod(0755, $home_dir)) { - die("Can't set perms on $home_dir to 0755: $!"); + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + SocketBindTight => 'on', + TimeoutIdle => $timeout_idle, + UseIPv6 => 'on', + + IfModules => { + 'mod_proxy.c' => $proxy_config, + + 'mod_delay.c' => { + DelayEngine => 'off', + }, + }, + + Limit => { + LOGIN => { + DenyUser => $setup->{user}, + }, + }, + + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + if (open(my $fh, ">> $setup->{config_file}")) { + print $fh < + Port $vhost_port + ServerName "Real Server" + + AuthUserFile $setup->{auth_user_file} + AuthGroupFile $setup->{auth_group_file} + AuthOrder mod_auth_file.c + + AllowOverride off + TimeoutIdle $timeout_idle + + TransferLog none + WtmpLog off + +EOC + unless (close($fh)) { + die("Can't write $setup->{config_file}: $!"); } - unless (chown($uid, $gid, $home_dir)) { - die("Can't set owner of $home_dir to $uid/$gid: $!"); + } else { + die("Can't open $setup->{config_file}: $!"); + } + + # 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(1); + my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1); + $client->login($setup->{user}, $setup->{passwd}); + + my ($resp_code, $resp_msg) = $client->eprt('|2|::ffff:127.0.0.1|4856|'); + + my $expected = 200; + $self->assert($expected == $resp_code, + test_msg("Expected response code $expected, got $resp_code")); + + $expected = "EPRT command successful"; + $self->assert($expected eq $resp_msg, + test_msg("Expected response message '$expected', got '$resp_msg'")); + + $client->quit(); + }; + if ($@) { + $ex = $@; } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; } - auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, - '/bin/bash'); - auth_group_write($auth_group_file, $group, $gid, $user); + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + test_cleanup($setup->{log_file}, $ex); +} + +sub proxy_reverse_ipv4mappedipv6_eprt_ipv4_backend_issue158 { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy'); my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); $vhost_port += 12; - my $proxy_config = get_reverse_proxy_config($tmpdir, $log_file, $vhost_port); + my $proxy_config = get_reverse_proxy_config_ipv4($tmpdir, $setup->{log_file}, + $vhost_port); my $timeout_idle = 10; my $config = { - PidFile => $pid_file, - ScoreboardFile => $scoreboard_file, - SystemLog => $log_file, - TraceLog => $log_file, - Trace => 'DEFAULT:10 event:0 lock:0 scoreboard:0 signal:0 proxy:20 proxy.ftp.conn:20 proxy.ftp.ctrl:20 proxy.ftp.data:20 proxy.ftp.msg:20 proxy.ftp.xfer:20', + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'DEFAULT:10 event:0 lock:0 scoreboard:0 signal:0 proxy:20 proxy.conn:30 proxy.ftp.conn:20 proxy.ftp.ctrl:20 proxy.ftp.data:20 proxy.ftp.msg:20 proxy.ftp.xfer:20', - AuthUserFile => $auth_user_file, - AuthGroupFile => $auth_group_file, + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + DefaultAddress => '::ffff:127.0.0.1', SocketBindTight => 'on', TimeoutIdle => $timeout_idle, UseIPv6 => 'on', @@ -814,22 +993,23 @@ sub proxy_reverse_ipv6_eprt_ipv6 { Limit => { LOGIN => { - DenyUser => $user, + DenyUser => $setup->{user}, }, }, }; - my ($port, $config_user, $config_group) = config_write($config_file, $config); + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); - if (open(my $fh, ">> $config_file")) { + if (open(my $fh, ">> $setup->{config_file}")) { print $fh < + Port $vhost_port ServerName "Real Server" - AuthUserFile $auth_user_file - AuthGroupFile $auth_group_file + AuthUserFile $setup->{auth_user_file} + AuthGroupFile $setup->{auth_group_file} AuthOrder mod_auth_file.c AllowOverride off @@ -840,11 +1020,11 @@ sub proxy_reverse_ipv6_eprt_ipv6 { EOC unless (close($fh)) { - die("Can't write $config_file: $!"); + die("Can't write $setup->{config_file}: $!"); } } else { - die("Can't open $config_file: $!"); + die("Can't open $setup->{config_file}: $!"); } # Open pipes, for use between the parent and child processes. Specifically, @@ -863,24 +1043,22 @@ EOC if ($pid) { eval { sleep(1); - my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1); - $client->login($user, $passwd); - my ($resp_code, $resp_msg) = $client->eprt('|2|::ffff:127.0.0.1|4856|'); + my $client = ProFTPD::TestSuite::FTP->new('::ffff:127.0.0.1', $port); + $client->login($setup->{user}, $setup->{passwd}); - my $expected; + my ($resp_code, $resp_msg) = $client->eprt('|2|::ffff:127.0.0.1|4856|'); - $expected = 200; + my $expected = 200; $self->assert($expected == $resp_code, - test_msg("Expected $expected, got $resp_code")); + test_msg("Expected response code $expected, got $resp_code")); $expected = "EPRT command successful"; $self->assert($expected eq $resp_msg, - test_msg("Expected '$expected', got '$resp_msg'")); + test_msg("Expected response message '$expected', got '$resp_msg'")); $client->quit(); }; - if ($@) { $ex = $@; } @@ -889,7 +1067,7 @@ EOC $wfh->flush(); } else { - eval { server_wait($config_file, $rfh, $timeout_idle + 2) }; + eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) }; if ($@) { warn($@); exit 1; @@ -899,18 +1077,1742 @@ EOC } # Stop server - server_stop($pid_file); - + server_stop($setup->{pid_file}); $self->assert_child_ok($pid); - if ($ex) { - test_append_logfile($log_file, $ex); - unlink($log_file); - + if (defined($ex)) { + test_cleanup($setup->{log_file}, $ex); die($ex); } - unlink($log_file); + eval { + if (open(my $fh, "< $setup->{log_file}")) { + my $ok = 0; + + while (my $line = <$fh>) { + chomp($line); + + if ($ENV{TEST_VERBOSE}) { + print STDERR "# $line\n"; + } + + if ($line =~ /proxied command 'EPRT \|1\|127\.0\.0\.1\|/) { + $ok = 1; + last; + } + } + + close($fh); + $self->assert($ok, test_msg("Did not see expected backend log message")); + + } else { + die("Can't read $setup->{log_file}: $!"); + } + }; + if ($@) { + $ex = $@; + } + + test_cleanup($setup->{log_file}, $ex); +} + +sub proxy_reverse_ipv4mappedipv6_epsv_ipv4_backend_issue158 { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy'); + + my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); + $vhost_port += 12; + + my $proxy_config = get_reverse_proxy_config_ipv4($tmpdir, $setup->{log_file}, + $vhost_port); + + my $timeout_idle = 10; + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'DEFAULT:10 event:0 lock:0 scoreboard:0 signal:0 proxy:20 proxy.conn:30 proxy.ftp.conn:20 proxy.ftp.ctrl:20 proxy.ftp.data:20 proxy.ftp.msg:20 proxy.ftp.xfer:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + DefaultAddress => '::ffff:127.0.0.1', + SocketBindTight => 'on', + TimeoutIdle => $timeout_idle, + UseIPv6 => 'on', + + IfModules => { + 'mod_proxy.c' => $proxy_config, + + 'mod_delay.c' => { + DelayEngine => 'off', + }, + }, + + Limit => { + LOGIN => { + DenyUser => $setup->{user}, + }, + }, + + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + if (open(my $fh, ">> $setup->{config_file}")) { + print $fh < + Port $vhost_port + ServerName "Real Server" + + AuthUserFile $setup->{auth_user_file} + AuthGroupFile $setup->{auth_group_file} + AuthOrder mod_auth_file.c + + AllowOverride off + TimeoutIdle $timeout_idle + + TransferLog none + WtmpLog off + +EOC + unless (close($fh)) { + die("Can't write $setup->{config_file}: $!"); + } + + } else { + die("Can't open $setup->{config_file}: $!"); + } + + # 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(1); + + my $client = ProFTPD::TestSuite::FTP->new('::ffff:127.0.0.1', $port); + $client->login($setup->{user}, $setup->{passwd}); + + my ($resp_code, $resp_msg) = $client->epsv(); + + my $expected = 229; + $self->assert($expected == $resp_code, + test_msg("Expected response code $expected, got $resp_code")); + + $expected = 'Entering Extended Passive Mode'; + $self->assert(qr/$expected/, $resp_msg, + test_msg("Expected response message '$expected', got '$resp_msg'")); + + $client->quit(); + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + if (defined($ex)) { + test_cleanup($setup->{log_file}, $ex); + die($ex); + } + + eval { + if (open(my $fh, "< $setup->{log_file}")) { + my $ok = 0; + + while (my $line = <$fh>) { + chomp($line); + + if ($ENV{TEST_VERBOSE}) { + print STDERR "# $line\n"; + } + + if ($line =~ /proxied EPSV command/) { + $ok = 1; + last; + } + } + + close($fh); + $self->assert($ok, test_msg("Did not see expected proxy TraceLog message")); + + } else { + die("Can't read $setup->{log_file}: $!"); + } + }; + if ($@) { + $ex = $@; + } + + test_cleanup($setup->{log_file}, $ex); +} + +sub proxy_reverse_ipv4mappedipv6_active_list_ipv4_backend_issue158 { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy'); + + my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); + $vhost_port += 12; + + my $proxy_config = get_reverse_proxy_config_ipv4($tmpdir, $setup->{log_file}, + $vhost_port); + $proxy_config->{ProxyDataTransferPolicy} = 'active'; + + my $timeout_idle = 10; + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'DEFAULT:10 directory:0 event:0 lock:0 scoreboard:0 signal:0 proxy:20 proxy.conn:30 proxy.ftp.conn:20 proxy.ftp.ctrl:20 proxy.ftp.data:20 proxy.ftp.msg:20 proxy.ftp.xfer:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + DefaultAddress => '::ffff:127.0.0.1', + SocketBindTight => 'on', + TimeoutIdle => $timeout_idle, + UseIPv6 => 'on', + + IfModules => { + 'mod_proxy.c' => $proxy_config, + + 'mod_delay.c' => { + DelayEngine => 'off', + }, + }, + + Limit => { + LOGIN => { + DenyUser => $setup->{user}, + }, + }, + + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + if (open(my $fh, ">> $setup->{config_file}")) { + print $fh < + Port $vhost_port + ServerName "Real Server" + + AuthUserFile $setup->{auth_user_file} + AuthGroupFile $setup->{auth_group_file} + AuthOrder mod_auth_file.c + + AllowOverride off + TimeoutIdle $timeout_idle + + TransferLog none + WtmpLog off + +EOC + unless (close($fh)) { + die("Can't write $setup->{config_file}: $!"); + } + + } else { + die("Can't open $setup->{config_file}: $!"); + } + + # 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(1); + + # We'll use passive transfers from the client to the frontend, but + # use ProxyDataTransferPolicy to force an active transfer between + # proxy and backend. + my $client = ProFTPD::TestSuite::FTP->new('::ffff:127.0.0.1', $port, 0); + $client->login($setup->{user}, $setup->{passwd}); + + my $conn = $client->list_raw(); + unless ($conn) { + die("LIST failed: " . $client->response_code() . ' ' . + $client->response_msg()); + } + + my $buf; + $conn->read($buf, 8192, 25); + eval { $conn->close() }; + + my $resp_code = $client->response_code(); + my $resp_msg = $client->response_msg(); + $client->quit(); + + $self->assert_transfer_ok($resp_code, $resp_msg); + + # We have to be careful of the fact that readdir returns directory + # entries in an unordered fashion. + my $res = {}; + my $lines = [split(/\n/, $buf)]; + foreach my $line (@$lines) { + if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { + $res->{$1} = 1; + } + } + + my $expected = { + 'proxy.conf' => 1, + 'proxy.group' => 1, + 'proxy.passwd' => 1, + 'proxy.pid' => 1, + 'proxy.scoreboard' => 1, + 'proxy.scoreboard.lck' => 1, + 'var' => 1, + }; + + my $ok = 1; + my $mismatch; + foreach my $name (keys(%$res)) { + unless (defined($expected->{$name})) { + $mismatch = $name; + $ok = 0; + last; + } + } + + unless ($ok) { + die("Unexpected name '$mismatch' appeared in LIST data") + } + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) }; + 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_reverse_ipv4mappedipv6_port_list_ipv4_backend_issue158 { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy'); + + my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); + $vhost_port += 12; + + my $proxy_config = get_reverse_proxy_config_ipv4($tmpdir, $setup->{log_file}, + $vhost_port); + $proxy_config->{ProxyDataTransferPolicy} = 'port'; + + my $timeout_idle = 10; + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'DEFAULT:10 directory:0 event:0 lock:0 scoreboard:0 signal:0 proxy:20 proxy.conn:30 proxy.ftp.conn:20 proxy.ftp.ctrl:20 proxy.ftp.data:20 proxy.ftp.msg:20 proxy.ftp.xfer:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + DefaultAddress => '::ffff:127.0.0.1', + SocketBindTight => 'on', + TimeoutIdle => $timeout_idle, + UseIPv6 => 'on', + + IfModules => { + 'mod_proxy.c' => $proxy_config, + + 'mod_delay.c' => { + DelayEngine => 'off', + }, + }, + + Limit => { + LOGIN => { + DenyUser => $setup->{user}, + }, + }, + + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + if (open(my $fh, ">> $setup->{config_file}")) { + print $fh < + Port $vhost_port + ServerName "Real Server" + + AuthUserFile $setup->{auth_user_file} + AuthGroupFile $setup->{auth_group_file} + AuthOrder mod_auth_file.c + + AllowOverride off + TimeoutIdle $timeout_idle + + TransferLog none + WtmpLog off + +EOC + unless (close($fh)) { + die("Can't write $setup->{config_file}: $!"); + } + + } else { + die("Can't open $setup->{config_file}: $!"); + } + + # 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(1); + + # We'll use passive transfers from the client to the frontend, but + # use ProxyDataTransferPolicy to force an active transfer between + # proxy and backend. + my $client = ProFTPD::TestSuite::FTP->new('::ffff:127.0.0.1', $port, 0); + $client->login($setup->{user}, $setup->{passwd}); + + my $conn = $client->list_raw(); + unless ($conn) { + die("LIST failed: " . $client->response_code() . ' ' . + $client->response_msg()); + } + + my $buf; + $conn->read($buf, 8192, 25); + eval { $conn->close() }; + + my $resp_code = $client->response_code(); + my $resp_msg = $client->response_msg(); + $client->quit(); + + $self->assert_transfer_ok($resp_code, $resp_msg); + + # We have to be careful of the fact that readdir returns directory + # entries in an unordered fashion. + my $res = {}; + my $lines = [split(/\n/, $buf)]; + foreach my $line (@$lines) { + if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { + $res->{$1} = 1; + } + } + + my $expected = { + 'proxy.conf' => 1, + 'proxy.group' => 1, + 'proxy.passwd' => 1, + 'proxy.pid' => 1, + 'proxy.scoreboard' => 1, + 'proxy.scoreboard.lck' => 1, + 'var' => 1, + }; + + my $ok = 1; + my $mismatch; + foreach my $name (keys(%$res)) { + unless (defined($expected->{$name})) { + $mismatch = $name; + $ok = 0; + last; + } + } + + unless ($ok) { + die("Unexpected name '$mismatch' appeared in LIST data") + } + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) }; + 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_reverse_ipv6only_eprt_ipv4_backend_issue158 { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy'); + + my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); + $vhost_port += 12; + + my $proxy_config = get_reverse_proxy_config_ipv4($tmpdir, $setup->{log_file}, + $vhost_port); + + # Without this IPv4 ProxySourceAddress, the backend connections (ctrl/data) + # fail. + $proxy_config->{ProxySourceAddress} = '127.0.0.1'; + + my $timeout_idle = 10; + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'DEFAULT:10 event:0 lock:0 scoreboard:0 signal:0 proxy:20 proxy.conn:30 proxy.ftp.conn:20 proxy.ftp.ctrl:20 proxy.ftp.data:20 proxy.ftp.msg:20 proxy.ftp.xfer:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + DefaultAddress => '::1', + SocketBindTight => 'on', + TimeoutIdle => $timeout_idle, + UseIPv6 => 'on', + + IfModules => { + 'mod_proxy.c' => $proxy_config, + + 'mod_delay.c' => { + DelayEngine => 'off', + }, + }, + + Limit => { + LOGIN => { + DenyUser => $setup->{user}, + }, + }, + + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + if (open(my $fh, ">> $setup->{config_file}")) { + print $fh < + Port $vhost_port + ServerName "Real Server" + + AuthUserFile $setup->{auth_user_file} + AuthGroupFile $setup->{auth_group_file} + AuthOrder mod_auth_file.c + + AllowOverride off + TimeoutIdle $timeout_idle + + TransferLog none + WtmpLog off + +EOC + unless (close($fh)) { + die("Can't write $setup->{config_file}: $!"); + } + + } else { + die("Can't open $setup->{config_file}: $!"); + } + + # 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(1); + + my $client = ProFTPD::TestSuite::FTP->new('::1', $port); + $client->login($setup->{user}, $setup->{passwd}); + + my ($resp_code, $resp_msg) = $client->eprt('|2|::1|4856|'); + + my $expected = 200; + $self->assert($expected == $resp_code, + test_msg("Expected response code $expected, got $resp_code")); + + $expected = "EPRT command successful"; + $self->assert($expected eq $resp_msg, + test_msg("Expected response message '$expected', got '$resp_msg'")); + + $client->quit(); + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + if (defined($ex)) { + test_cleanup($setup->{log_file}, $ex); + die($ex); + } + + eval { + if (open(my $fh, "< $setup->{log_file}")) { + my $ok = 0; + + while (my $line = <$fh>) { + chomp($line); + + if ($ENV{TEST_VERBOSE}) { + print STDERR "# $line\n"; + } + + if ($line =~ /proxied command 'EPRT \|1\|127\.0\.0\.1\|/) { + $ok = 1; + last; + } + } + + close($fh); + $self->assert($ok, test_msg("Did not see expected backend log message")); + + } else { + die("Can't read $setup->{log_file}: $!"); + } + }; + if ($@) { + $ex = $@; + } + + test_cleanup($setup->{log_file}, $ex); +} + +sub proxy_reverse_ipv6only_epsv_ipv4_backend_issue158 { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy'); + + my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); + $vhost_port += 12; + + my $proxy_config = get_reverse_proxy_config_ipv4($tmpdir, $setup->{log_file}, + $vhost_port); + + # Without this IPv4 ProxySourceAddress, the backend connections (ctrl/data) + # fail. + $proxy_config->{ProxySourceAddress} = '127.0.0.1'; + + my $timeout_idle = 10; + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'DEFAULT:10 event:0 lock:0 scoreboard:0 signal:0 proxy:20 proxy.conn:30 proxy.ftp.conn:20 proxy.ftp.ctrl:20 proxy.ftp.data:20 proxy.ftp.msg:20 proxy.ftp.xfer:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + DefaultAddress => '::1', + SocketBindTight => 'on', + TimeoutIdle => $timeout_idle, + UseIPv6 => 'on', + + IfModules => { + 'mod_proxy.c' => $proxy_config, + + 'mod_delay.c' => { + DelayEngine => 'off', + }, + }, + + Limit => { + LOGIN => { + DenyUser => $setup->{user}, + }, + }, + + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + if (open(my $fh, ">> $setup->{config_file}")) { + print $fh < + Port $vhost_port + ServerName "Real Server" + + AuthUserFile $setup->{auth_user_file} + AuthGroupFile $setup->{auth_group_file} + AuthOrder mod_auth_file.c + + AllowOverride off + TimeoutIdle $timeout_idle + + TransferLog none + WtmpLog off + +EOC + unless (close($fh)) { + die("Can't write $setup->{config_file}: $!"); + } + + } else { + die("Can't open $setup->{config_file}: $!"); + } + + # 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(1); + + my $client = ProFTPD::TestSuite::FTP->new('::1', $port); + $client->login($setup->{user}, $setup->{passwd}); + + my ($resp_code, $resp_msg) = $client->epsv(); + + my $expected = 229; + $self->assert($expected == $resp_code, + test_msg("Expected response code $expected, got $resp_code")); + + $expected = 'Entering Extended Passive Mode'; + $self->assert(qr/$expected/, $resp_msg, + test_msg("Expected response message '$expected', got '$resp_msg'")); + + $client->quit(); + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + if (defined($ex)) { + test_cleanup($setup->{log_file}, $ex); + die($ex); + } + + eval { + if (open(my $fh, "< $setup->{log_file}")) { + my $ok = 0; + + while (my $line = <$fh>) { + chomp($line); + + if ($ENV{TEST_VERBOSE}) { + print STDERR "# $line\n"; + } + + if ($line =~ /proxied EPSV command/) { + $ok = 1; + last; + } + } + + close($fh); + $self->assert($ok, test_msg("Did not see expected proxy TraceLog message")); + + } else { + die("Can't read $setup->{log_file}: $!"); + } + }; + if ($@) { + $ex = $@; + } + + test_cleanup($setup->{log_file}, $ex); +} + +sub proxy_reverse_ipv6only_active_list_ipv4_backend_issue158 { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy'); + + my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); + $vhost_port += 12; + + my $proxy_config = get_reverse_proxy_config_ipv4($tmpdir, $setup->{log_file}, + $vhost_port); + $proxy_config->{ProxyDataTransferPolicy} = 'active'; + + # Without this IPv4 ProxySourceAddress, the backend connections (ctrl/data) + # fail. + $proxy_config->{ProxySourceAddress} = '127.0.0.1'; + + my $timeout_idle = 10; + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'DEFAULT:10 directory:0 event:0 lock:0 scoreboard:0 signal:0 proxy:20 proxy.conn:30 proxy.ftp.conn:20 proxy.ftp.ctrl:20 proxy.ftp.data:20 proxy.ftp.msg:20 proxy.ftp.xfer:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + DefaultAddress => '::1', + SocketBindTight => 'on', + TimeoutIdle => $timeout_idle, + UseIPv6 => 'on', + + IfModules => { + 'mod_proxy.c' => $proxy_config, + + 'mod_delay.c' => { + DelayEngine => 'off', + }, + }, + + Limit => { + LOGIN => { + DenyUser => $setup->{user}, + }, + }, + + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + if (open(my $fh, ">> $setup->{config_file}")) { + print $fh < + Port $vhost_port + ServerName "Real Server" + + AuthUserFile $setup->{auth_user_file} + AuthGroupFile $setup->{auth_group_file} + AuthOrder mod_auth_file.c + + AllowOverride off + TimeoutIdle $timeout_idle + + TransferLog none + WtmpLog off + +EOC + unless (close($fh)) { + die("Can't write $setup->{config_file}: $!"); + } + + } else { + die("Can't open $setup->{config_file}: $!"); + } + + # 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(1); + + # We'll use passive transfers from the client to the frontend, but + # use ProxyDataTransferPolicy to force an active transfer between + # proxy and backend. + my $client = ProFTPD::TestSuite::FTP->new('::1', $port, 0); + $client->login($setup->{user}, $setup->{passwd}); + + my $conn = $client->list_raw(); + unless ($conn) { + die("LIST failed: " . $client->response_code() . ' ' . + $client->response_msg()); + } + + my $buf; + $conn->read($buf, 8192, 25); + eval { $conn->close() }; + + my $resp_code = $client->response_code(); + my $resp_msg = $client->response_msg(); + $client->quit(); + + $self->assert_transfer_ok($resp_code, $resp_msg); + + # We have to be careful of the fact that readdir returns directory + # entries in an unordered fashion. + my $res = {}; + my $lines = [split(/\n/, $buf)]; + foreach my $line (@$lines) { + if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { + $res->{$1} = 1; + } + } + + my $expected = { + 'proxy.conf' => 1, + 'proxy.group' => 1, + 'proxy.passwd' => 1, + 'proxy.pid' => 1, + 'proxy.scoreboard' => 1, + 'proxy.scoreboard.lck' => 1, + 'var' => 1, + }; + + my $ok = 1; + my $mismatch; + foreach my $name (keys(%$res)) { + unless (defined($expected->{$name})) { + $mismatch = $name; + $ok = 0; + last; + } + } + + unless ($ok) { + die("Unexpected name '$mismatch' appeared in LIST data") + } + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) }; + 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_reverse_ipv6only_port_list_ipv4_backend_issue158 { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy'); + + my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); + $vhost_port += 12; + + my $proxy_config = get_reverse_proxy_config_ipv4($tmpdir, $setup->{log_file}, + $vhost_port); + $proxy_config->{ProxyDataTransferPolicy} = 'port'; + + # Without this IPv4 ProxySourceAddress, the backend connections (ctrl/data) + # fail. + $proxy_config->{ProxySourceAddress} = '127.0.0.1'; + + my $timeout_idle = 10; + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'DEFAULT:10 directory:0 event:0 lock:0 scoreboard:0 signal:0 proxy:20 proxy.conn:30 proxy.ftp.conn:20 proxy.ftp.ctrl:20 proxy.ftp.data:20 proxy.ftp.msg:20 proxy.ftp.xfer:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + DefaultAddress => '::1', + SocketBindTight => 'on', + TimeoutIdle => $timeout_idle, + UseIPv6 => 'on', + + IfModules => { + 'mod_proxy.c' => $proxy_config, + + 'mod_delay.c' => { + DelayEngine => 'off', + }, + }, + + Limit => { + LOGIN => { + DenyUser => $setup->{user}, + }, + }, + + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + if (open(my $fh, ">> $setup->{config_file}")) { + print $fh < + Port $vhost_port + ServerName "Real Server" + + AuthUserFile $setup->{auth_user_file} + AuthGroupFile $setup->{auth_group_file} + AuthOrder mod_auth_file.c + + AllowOverride off + TimeoutIdle $timeout_idle + + TransferLog none + WtmpLog off + +EOC + unless (close($fh)) { + die("Can't write $setup->{config_file}: $!"); + } + + } else { + die("Can't open $setup->{config_file}: $!"); + } + + # 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(1); + + # We'll use passive transfers from the client to the frontend, but + # use ProxyDataTransferPolicy to force an active transfer between + # proxy and backend. + my $client = ProFTPD::TestSuite::FTP->new('::1', $port, 0); + $client->login($setup->{user}, $setup->{passwd}); + + my $conn = $client->list_raw(); + unless ($conn) { + die("LIST failed: " . $client->response_code() . ' ' . + $client->response_msg()); + } + + my $buf; + $conn->read($buf, 8192, 25); + eval { $conn->close() }; + + my $resp_code = $client->response_code(); + my $resp_msg = $client->response_msg(); + $client->quit(); + + $self->assert_transfer_ok($resp_code, $resp_msg); + + # We have to be careful of the fact that readdir returns directory + # entries in an unordered fashion. + my $res = {}; + my $lines = [split(/\n/, $buf)]; + foreach my $line (@$lines) { + if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { + $res->{$1} = 1; + } + } + + my $expected = { + 'proxy.conf' => 1, + 'proxy.group' => 1, + 'proxy.passwd' => 1, + 'proxy.pid' => 1, + 'proxy.scoreboard' => 1, + 'proxy.scoreboard.lck' => 1, + 'var' => 1, + }; + + my $ok = 1; + my $mismatch; + foreach my $name (keys(%$res)) { + unless (defined($expected->{$name})) { + $mismatch = $name; + $ok = 0; + last; + } + } + + unless ($ok) { + die("Unexpected name '$mismatch' appeared in LIST data") + } + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) }; + 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_reverse_ipv4_active_list_ipv6only_backend_issue158 { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy'); + + my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); + $vhost_port += 12; + + my $proxy_config = get_reverse_proxy_config_ipv6($tmpdir, $setup->{log_file}, + $vhost_port); + $proxy_config->{ProxyDataTransferPolicy} = 'active'; + + my $timeout_idle = 10; + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'DEFAULT:10 directory:0 event:0 lock:0 scoreboard:0 signal:0 proxy:20 proxy.conn:30 proxy.ftp.conn:20 proxy.ftp.ctrl:20 proxy.ftp.data:20 proxy.ftp.msg:20 proxy.ftp.xfer:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + DefaultAddress => '127.0.0.1', + SocketBindTight => 'on', + TimeoutIdle => $timeout_idle, + UseIPv6 => 'on', + + IfModules => { + 'mod_proxy.c' => $proxy_config, + + 'mod_delay.c' => { + DelayEngine => 'off', + }, + }, + + Limit => { + LOGIN => { + DenyUser => $setup->{user}, + }, + }, + + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + if (open(my $fh, ">> $setup->{config_file}")) { + print $fh < + Port $vhost_port + ServerName "Real Server" + + AuthUserFile $setup->{auth_user_file} + AuthGroupFile $setup->{auth_group_file} + AuthOrder mod_auth_file.c + + AllowOverride off + TimeoutIdle $timeout_idle + + TransferLog none + WtmpLog off + +EOC + unless (close($fh)) { + die("Can't write $setup->{config_file}: $!"); + } + + } else { + die("Can't open $setup->{config_file}: $!"); + } + + # 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(1); + + # We'll use passive transfers from the client to the frontend, but + # use ProxyDataTransferPolicy to force an active transfer between + # proxy and backend. + my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0); + $client->login($setup->{user}, $setup->{passwd}); + + my $conn = $client->list_raw(); + unless ($conn) { + die("LIST failed: " . $client->response_code() . ' ' . + $client->response_msg()); + } + + my $buf; + $conn->read($buf, 8192, 25); + eval { $conn->close() }; + + my $resp_code = $client->response_code(); + my $resp_msg = $client->response_msg(); + $client->quit(); + + $self->assert_transfer_ok($resp_code, $resp_msg); + + # We have to be careful of the fact that readdir returns directory + # entries in an unordered fashion. + my $res = {}; + my $lines = [split(/\n/, $buf)]; + foreach my $line (@$lines) { + if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { + $res->{$1} = 1; + } + } + + my $expected = { + 'proxy.conf' => 1, + 'proxy.group' => 1, + 'proxy.passwd' => 1, + 'proxy.pid' => 1, + 'proxy.scoreboard' => 1, + 'proxy.scoreboard.lck' => 1, + 'var' => 1, + }; + + my $ok = 1; + my $mismatch; + foreach my $name (keys(%$res)) { + unless (defined($expected->{$name})) { + $mismatch = $name; + $ok = 0; + last; + } + } + + unless ($ok) { + die("Unexpected name '$mismatch' appeared in LIST data") + } + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) }; + 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_reverse_ipv4_port_list_ipv6only_backend_issue158 { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy'); + + my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); + $vhost_port += 12; + + my $proxy_config = get_reverse_proxy_config_ipv6($tmpdir, $setup->{log_file}, + $vhost_port); + $proxy_config->{ProxyDataTransferPolicy} = 'port'; + + my $timeout_idle = 10; + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'DEFAULT:10 directory:0 event:0 lock:0 scoreboard:0 signal:0 proxy:20 proxy.conn:30 proxy.ftp.conn:20 proxy.ftp.ctrl:20 proxy.ftp.data:20 proxy.ftp.msg:20 proxy.ftp.xfer:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + DefaultAddress => '127.0.0.1', + SocketBindTight => 'on', + TimeoutIdle => $timeout_idle, + UseIPv6 => 'on', + + IfModules => { + 'mod_proxy.c' => $proxy_config, + + 'mod_delay.c' => { + DelayEngine => 'off', + }, + }, + + Limit => { + LOGIN => { + DenyUser => $setup->{user}, + }, + }, + + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + if (open(my $fh, ">> $setup->{config_file}")) { + print $fh < + Port $vhost_port + ServerName "Real Server" + + AuthUserFile $setup->{auth_user_file} + AuthGroupFile $setup->{auth_group_file} + AuthOrder mod_auth_file.c + + AllowOverride off + TimeoutIdle $timeout_idle + + TransferLog none + WtmpLog off + +EOC + unless (close($fh)) { + die("Can't write $setup->{config_file}: $!"); + } + + } else { + die("Can't open $setup->{config_file}: $!"); + } + + # 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(1); + + # We'll use passive transfers from the client to the frontend, but + # use ProxyDataTransferPolicy to force an active transfer between + # proxy and backend. + my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0); + $client->login($setup->{user}, $setup->{passwd}); + + my $conn = $client->list_raw(); + unless ($conn) { + die("LIST failed: " . $client->response_code() . ' ' . + $client->response_msg()); + } + + my $buf; + $conn->read($buf, 8192, 25); + eval { $conn->close() }; + + my $resp_code = $client->response_code(); + my $resp_msg = $client->response_msg(); + $client->quit(); + + $self->assert_transfer_ok($resp_code, $resp_msg); + + # We have to be careful of the fact that readdir returns directory + # entries in an unordered fashion. + my $res = {}; + my $lines = [split(/\n/, $buf)]; + foreach my $line (@$lines) { + if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { + $res->{$1} = 1; + } + } + + my $expected = { + 'proxy.conf' => 1, + 'proxy.group' => 1, + 'proxy.passwd' => 1, + 'proxy.pid' => 1, + 'proxy.scoreboard' => 1, + 'proxy.scoreboard.lck' => 1, + 'var' => 1, + }; + + my $ok = 1; + my $mismatch; + foreach my $name (keys(%$res)) { + unless (defined($expected->{$name})) { + $mismatch = $name; + $ok = 0; + last; + } + } + + unless ($ok) { + die("Unexpected name '$mismatch' appeared in LIST data") + } + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) }; + 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_reverse_ipv4_passive_list_ipv6only_backend_issue158 { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy'); + + my $vhost_port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); + $vhost_port += 12; + + my $proxy_config = get_reverse_proxy_config_ipv6($tmpdir, $setup->{log_file}, + $vhost_port); + $proxy_config->{ProxyDataTransferPolicy} = 'passive'; + + my $timeout_idle = 10; + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'DEFAULT:10 directory:0 event:0 lock:0 scoreboard:0 signal:0 proxy:20 proxy.conn:30 proxy.ftp.conn:20 proxy.ftp.ctrl:20 proxy.ftp.data:20 proxy.ftp.msg:20 proxy.ftp.xfer:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + DefaultAddress => '127.0.0.1', + SocketBindTight => 'on', + TimeoutIdle => $timeout_idle, + UseIPv6 => 'on', + + IfModules => { + 'mod_proxy.c' => $proxy_config, + + 'mod_delay.c' => { + DelayEngine => 'off', + }, + }, + + Limit => { + LOGIN => { + DenyUser => $setup->{user}, + }, + }, + + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + if (open(my $fh, ">> $setup->{config_file}")) { + print $fh < + Port $vhost_port + ServerName "Real Server" + + AuthUserFile $setup->{auth_user_file} + AuthGroupFile $setup->{auth_group_file} + AuthOrder mod_auth_file.c + + AllowOverride off + TimeoutIdle $timeout_idle + + TransferLog none + WtmpLog off + +EOC + unless (close($fh)) { + die("Can't write $setup->{config_file}: $!"); + } + + } else { + die("Can't open $setup->{config_file}: $!"); + } + + # 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(1); + + # We'll use active transfers from the client to the frontend, but + # use ProxyDataTransferPolicy to force a passive transfer between + # proxy and backend. + my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1); + $client->login($setup->{user}, $setup->{passwd}); + + my $conn = $client->list_raw(); + unless ($conn) { + die("LIST failed: " . $client->response_code() . ' ' . + $client->response_msg()); + } + + my $buf; + $conn->read($buf, 8192, 25); + eval { $conn->close() }; + + my $resp_code = $client->response_code(); + my $resp_msg = $client->response_msg(); + $client->quit(); + + $self->assert_transfer_ok($resp_code, $resp_msg); + + # We have to be careful of the fact that readdir returns directory + # entries in an unordered fashion. + my $res = {}; + my $lines = [split(/\n/, $buf)]; + foreach my $line (@$lines) { + if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { + $res->{$1} = 1; + } + } + + my $expected = { + 'proxy.conf' => 1, + 'proxy.group' => 1, + 'proxy.passwd' => 1, + 'proxy.pid' => 1, + 'proxy.scoreboard' => 1, + 'proxy.scoreboard.lck' => 1, + 'var' => 1, + }; + + my $ok = 1; + my $mismatch; + foreach my $name (keys(%$res)) { + unless (defined($expected->{$name})) { + $mismatch = $name; + $ok = 0; + last; + } + } + + unless ($ok) { + die("Unexpected name '$mismatch' appeared in LIST data") + } + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + test_cleanup($setup->{log_file}, $ex); } 1;