From 2b457cf5f6f31d0b8db7e79ea0da7c467b154823 Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Tue, 10 Mar 2020 14:19:49 +0100 Subject: [PATCH] Imported Upstream version 0.1 --- .gitattributes | 2 + .gitignore | 1 + .travis.yml | 30 + README.md | 15 + mod_proxy_protocol.c | 1181 +++++++++ mod_proxy_protocol.html | 237 ++ t/config/maxhostsperuser.t | 11 + t/lib/ProFTPD/TestSuite/ProxiedFTP.pm | 66 + t/lib/ProFTPD/Tests/Config/MaxHostsPerUser.pm | 229 ++ .../Tests/Modules/mod_proxy_protocol.pm | 2233 +++++++++++++++++ .../Tests/Modules/mod_proxy_protocol/sftp.pm | 150 ++ .../Tests/Modules/mod_proxy_protocol/tls.pm | 304 +++ .../Tests/Modules/mod_proxy_protocol/wrap2.pm | 139 + t/modules/mod_proxy_protocol.t | 11 + t/modules/mod_proxy_protocol/sftp.t | 11 + t/modules/mod_proxy_protocol/tls.t | 11 + t/modules/mod_proxy_protocol/wrap2.t | 11 + 17 files changed, 4642 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 README.md create mode 100644 mod_proxy_protocol.c create mode 100644 mod_proxy_protocol.html create mode 100644 t/config/maxhostsperuser.t create mode 100644 t/lib/ProFTPD/TestSuite/ProxiedFTP.pm create mode 100644 t/lib/ProFTPD/Tests/Config/MaxHostsPerUser.pm create mode 100644 t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol.pm create mode 100644 t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol/sftp.pm create mode 100644 t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol/tls.pm create mode 100644 t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol/wrap2.pm create mode 100644 t/modules/mod_proxy_protocol.t create mode 100644 t/modules/mod_proxy_protocol/sftp.t create mode 100644 t/modules/mod_proxy_protocol/tls.t create mode 100644 t/modules/mod_proxy_protocol/wrap2.t diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fa18026 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.pl linguist-language=C +*.pm linguist-language=C diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4ae464c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,30 @@ +language: c + +compiler: + - gcc + - clang + +install: + # for unit tests + - sudo apt-get install -y check + # for static code analysis + - sudo apt-get install -y cppcheck rats + # for test code coverage + - sudo apt-get install -y lcov + - gem install coveralls-lcov + +before_script: + - cd ${TRAVIS_BUILD_DIR} + - lcov --directory . --zerocounters + +script: + # - find . -type f -name "*.c" -print | grep -v t\/ | xargs cppcheck 2>&1 + # - find . -type f -name "*.c" -print | grep -v t\/ | xargs rats --language=c + - git clone --depth 10 https://github.com/proftpd/proftpd.git + - cp mod_proxy_protocol.c proftpd/contrib/ + - cd proftpd + - ./configure LIBS="-lm -lrt -pthread" --enable-devel=coverage --enable-tests --with-module=mod_proxy_protocol + - make + - make clean + - ./configure LIBS="-lm -lrt -pthread" --enable-devel=coverage --enable-dso --enable-tests --with-shared=mod_proxy_protocol + - make diff --git a/README.md b/README.md new file mode 100644 index 0000000..8409202 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +proftpd-mod_proxy_protocol +========================== + +Status +------ +[![Build Status](https://travis-ci.org/Castaglia/proftpd-mod_proxy_protocol.svg?branch=master)](https://travis-ci.org/Castaglia/proftpd-mod_proxy_protocol) +[![License](https://img.shields.io/badge/license-GPL-brightgreen.svg)](https://img.shields.io/badge/license-GPL-brightgreen.svg) + +Synopsis +-------- + +The `mod_proxy_protocol` module for ProFTPD handles protocols that are used +for conveying connection information via proxies such as haproxy. + +See the [mod_proxy_protocol.html](https://htmlpreview.github.io/?https://github.com/Castaglia/proftpd-mod_proxy_protocol/blob/master/mod_proxy_protocol.html) documentation for more details. diff --git a/mod_proxy_protocol.c b/mod_proxy_protocol.c new file mode 100644 index 0000000..28bd7fe --- /dev/null +++ b/mod_proxy_protocol.c @@ -0,0 +1,1181 @@ +/* + * ProFTPD - mod_proxy_protocol + * Copyright (c) 2013-2017 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA. + * + * As a special exemption, TJ Saunders and other respective copyright holders + * give permission to link this program with OpenSSL, and distribute the + * resulting executable, without including the source code for OpenSSL in the + * source distribution. + */ + + +#include "conf.h" +#include "privs.h" + +#define MOD_PROXY_PROTOCOL_VERSION "mod_proxy_protocol/0.1" + +/* Make sure the version of proftpd is as necessary. */ +#if PROFTPD_VERSION_NUMBER < 0x0001030504 +# error "ProFTPD 1.3.5rc4 or later required" +#endif + +module proxy_protocol_module; + +#define PROXY_PROTOCOL_BUFSZ 128 + +#define PROXY_PROTOCOL_TIMEOUT_DEFAULT 3 +static int proxy_protocol_timeout = PROXY_PROTOCOL_TIMEOUT_DEFAULT; + +#define PROXY_PROTOCOL_VERSION_HAPROXY_V1 1 +#define PROXY_PROTOCOL_VERSION_HAPROXY_V2 2 +static unsigned int proxy_protocol_version = PROXY_PROTOCOL_VERSION_HAPROXY_V1; + +static const char *trace_channel = "proxy_protocol"; + +static int poll_sock(int sockfd) { + fd_set rfds; + int res = 0; + struct timeval tv; + + memset(&tv, 0, sizeof(tv)); + tv.tv_sec = proxy_protocol_timeout; + tv.tv_usec = 0; + + pr_trace_msg(trace_channel, 19, + "waiting for max of %lu secs while polling socket %d using select(2)", + (unsigned long) tv.tv_sec, sockfd); + + while (TRUE) { + pr_signals_handle(); + + FD_ZERO(&rfds); + FD_SET(sockfd, &rfds); + + res = select(sockfd + 1, &rfds, NULL, NULL, &tv); + if (res < 0) { + int xerrno = errno; + + if (xerrno == EINTR) { + pr_signals_handle(); + continue; + } + + pr_trace_msg(trace_channel, 18, "error calling select(2) on fd %d: %s", + sockfd, strerror(xerrno)); + + errno = xerrno; + return -1; + + } else if (res == 0) { + memset(&tv, 0, sizeof(tv)); + tv.tv_sec = proxy_protocol_timeout; + tv.tv_usec = 0; + + pr_trace_msg(trace_channel, 18, + "polling on socket %d timed out after %lu sec, trying again", sockfd, + (unsigned long) tv.tv_sec); + continue; + } + + break; + } + + return 0; +} + +static int read_sock(int sockfd, void *buf, size_t reqlen) { + void *ptr = NULL; + size_t remainlen = 0; + + if (reqlen == 0) { + return 0; + } + + errno = 0; + ptr = buf; + remainlen = reqlen; + + while (remainlen > 0) { + int res, xerrno = 0; + + if (poll_sock(sockfd) < 0) { + return -1; + } + + res = read(sockfd, ptr, remainlen); + xerrno = errno; + + while (res <= 0) { + if (res < 0) { + if (xerrno == EINTR) { + pr_signals_handle(); + continue; + } + + pr_trace_msg(trace_channel, 16, + "error reading from client (fd %d): %s", sockfd, strerror(xerrno)); + pr_log_debug(DEBUG5, MOD_PROXY_PROTOCOL_VERSION + ": error reading from client (fd %d): %s", sockfd, strerror(xerrno)); + + /* We explicitly disconnect the client here because the errors below + * all indicate a problem with the TCP connection. + */ + if (xerrno == ECONNRESET || + xerrno == ECONNABORTED || +#ifdef ETIMEDOUT + xerrno == ETIMEDOUT || +#endif /* ETIMEDOUT */ +#ifdef ENOTCONN + xerrno == ENOTCONN || +#endif /* ENOTCONN */ +#ifdef ESHUTDOWN + xerrno == ESHUTDOWN || +#endif /* ESHUTDOWNN */ + xerrno == EPIPE) { + errno = xerrno; + + pr_trace_msg(trace_channel, 16, + "disconnecting client (%s)", strerror(xerrno)); + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": disconnecting client (%s)", strerror(xerrno)); + pr_session_disconnect(&proxy_protocol_module, + PR_SESS_DISCONNECT_CLIENT_EOF, strerror(xerrno)); + } + + return -1; + + } else { + /* If we read zero bytes here, treat it as an EOF and hang up on + * the uncommunicative client. + */ + + pr_trace_msg(trace_channel, 16, "%s", + "disconnecting client (received EOF)"); + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": disconnecting client (received EOF)"); + pr_session_disconnect(&proxy_protocol_module, + PR_SESS_DISCONNECT_CLIENT_EOF, NULL); + } + } + + /* Generate an event for any interested listeners. */ + pr_event_generate("core.ctrl-read", buf); + + session.total_raw_in += reqlen; + + if (res == remainlen) { + break; + } + + pr_trace_msg(trace_channel, 20, "read %lu bytes, expected %lu bytes; " + "reading more", (unsigned long) res, (unsigned long) remainlen); + ptr = ((char *) ptr + res); + remainlen -= res; + } + + return reqlen; +} + +static int readv_sock(int sockfd, const struct iovec *iov, int count) { + int res, xerrno = 0; + + if (poll_sock(sockfd) < 0) { + return -1; + } + + res = readv(sockfd, iov, count); + xerrno = errno; + + while (res <= 0) { + if (res < 0) { + if (xerrno == EINTR) { + pr_signals_handle(); + + if (poll_sock(sockfd) < 0) { + return -1; + } + + res = readv(sockfd, iov, count); + xerrno = errno; + continue; + } + + pr_trace_msg(trace_channel, 16, + "error reading from client (fd %d): %s", sockfd, strerror(xerrno)); + pr_log_debug(DEBUG5, MOD_PROXY_PROTOCOL_VERSION + ": error reading from client (fd %d): %s", sockfd, strerror(xerrno)); + + /* We explicitly disconnect the client here because the errors below + * all indicate a problem with the TCP connection. + */ + if (xerrno == ECONNRESET || + xerrno == ECONNABORTED || +#ifdef ETIMEDOUT + xerrno == ETIMEDOUT || +#endif /* ETIMEDOUT */ +#ifdef ENOTCONN + xerrno == ENOTCONN || +#endif /* ENOTCONN */ +#ifdef ESHUTDOWN + xerrno == ESHUTDOWN || +#endif /* ESHUTDOWNN */ + xerrno == EPIPE) { + + pr_trace_msg(trace_channel, 16, + "disconnecting client (%s)", strerror(xerrno)); + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": disconnecting client (%s)", strerror(xerrno)); + pr_session_disconnect(&proxy_protocol_module, + PR_SESS_DISCONNECT_CLIENT_EOF, strerror(xerrno)); + + return -1; + } + + /* If we read zero bytes here, treat it as an EOF and hang up on + * the uncommunicative client. + */ + + pr_trace_msg(trace_channel, 16, "%s", + "disconnecting client (received EOF)"); + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": disconnecting client (received EOF)"); + pr_session_disconnect(&proxy_protocol_module, + PR_SESS_DISCONNECT_CLIENT_EOF, NULL); + + errno = ENOENT; + return -1; + } + } + + session.total_raw_in += res; + return res; +} + +static unsigned int strtou(const char **str, const char *last) { + const char *ptr = *str; + unsigned int i = 0, j, k; + + while (ptr < last) { + pr_signals_handle(); + + j = *ptr - '0'; + k = i * 10; + + if (j > 9) { + break; + } + + i = k + j; + ptr++; + } + + *str = ptr; + return i; +} + +/* This function waits a PROXY protocol header at the beginning of the + * raw data stream. The header looks like this : + * + * "PROXY" PROTO SRC3 DST3 SRC4 "\r\n" + * + * There must be exactly one space between each field. Fields are : + * + * - PROTO: layer 4 protocol, which must be "TCP4" or "TCP6". + * - SRC3: layer 3 (e.g. IP) source address in standard text form + * - DST3: layer 3 (e.g. IP) destination address in standard text form + * - SRC4: layer 4 (e.g. TCP port) source address in standard text form + * - DST4: layer 4 (e.g. TCP port) destination address in standard text form + */ +static int read_haproxy_v1(pool *p, conn_t *conn, + const pr_netaddr_t **proxied_addr, unsigned int *proxied_port) { + register unsigned int i; + char buf[PROXY_PROTOCOL_BUFSZ], *last = NULL, *ptr = NULL; + int have_cr = FALSE, have_nl = FALSE, have_tcp4 = FALSE, have_tcp6 = FALSE; + size_t buflen = 0; + + /* Read until we find the expected PROXY string. */ + + memset(buf, '\0', sizeof(buf)); + ptr = buf; + + for (i = 0; i < sizeof(buf)-1; i++) { + int res, xerrno; + + pr_signals_handle(); + + res = read_sock(conn->rfd, &buf[i], 1); + xerrno = errno; + + while (res <= 0) { + if (xerrno == EINTR) { + pr_signals_handle(); + + res = read_sock(conn->rfd, &buf[i], 1); + xerrno = errno; + + continue; + } + + if (res < 0) { + pr_log_debug(DEBUG5, MOD_PROXY_PROTOCOL_VERSION + ": error reading from client socket: %s", strerror(xerrno)); + errno = xerrno; + return -1; + } + } + + /* Decode a possible PROXY request as early as we can, and fail + * early if it does not match. + */ + if (i == 6) { + if (strncmp(ptr, "PROXY ", 6) != 0) { + goto bad_proto; + } + + ptr += 6; + } + + /* We continue reading until the client has sent the terminating + * CRLF sequence. + */ + if (buf[i] == '\r') { + have_cr = TRUE; + buf[i] = '\0'; + continue; + } + + if (have_cr == TRUE && + buf[i] == '\n') { + have_nl = TRUE; + buf[i] = '\0'; + break; + } + + buflen++; + } + + buf[sizeof(buf)-1] = '\0'; + + pr_trace_msg(trace_channel, 7, + "read %lu bytes of proxy data (minus CRLF): '%.100s'", + (unsigned long) buflen, buf); + + if (have_nl == FALSE) { + pr_log_debug(DEBUG5, MOD_PROXY_PROTOCOL_VERSION + ": missing expected CRLF termination"); + goto bad_proto; + } + + if (buflen == 0) { + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": missing expected PROXY protocol data"); + goto bad_proto; + } + + last = buf + buflen; + + /* Check the PROTO field: "TCP4" or "TCP6" are supported. */ + if (strncmp(ptr, "TCP4 ", 5) == 0) { + have_tcp4 = TRUE; + +#ifdef PR_USE_IPV6 + } else if (strncmp(ptr, "TCP6 ", 5) == 0) { + if (pr_netaddr_use_ipv6()) { + have_tcp6 = TRUE; + } + +#endif /* PR_USE_IPV6 */ + } + + if (have_tcp4 || have_tcp6) { + const pr_netaddr_t *src_addr = NULL, *dst_addr = NULL; + char *ptr2 = NULL; + unsigned int src_port, dst_port; + int flags = PR_NETADDR_GET_ADDR_FL_EXCL_DNS; + + ptr += 5; + + ptr2 = strchr(ptr, ' '); + if (ptr2 == NULL) { + goto bad_proto; + } + + *ptr2 = '\0'; + pr_trace_msg(trace_channel, 9, + "resolving source address field '%s'", ptr); + src_addr = pr_netaddr_get_addr2(p, ptr, NULL, flags); + + if (src_addr == NULL) { + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": unable to resolve source address '%s': %s", ptr, strerror(errno)); + *ptr2 = ' '; + goto bad_proto; + + } else { + *ptr2 = ' '; + pr_trace_msg(trace_channel, 9, "resolve source address '%s': %s", + ptr, pr_netaddr_get_ipstr(src_addr)); + } + + ptr = ptr2 + 1; + ptr2 = strchr(ptr, ' '); + if (ptr2 == NULL) { + goto bad_proto; + } + + *ptr2 = '\0'; + pr_trace_msg(trace_channel, 9, + "resolving destination address field '%s'", ptr); + dst_addr = pr_netaddr_get_addr2(p, ptr, NULL, flags); + + if (dst_addr == NULL) { + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": unable to resolve destination address '%s': %s", ptr, + strerror(errno)); + *ptr2 = ' '; + goto bad_proto; + + } else { + *ptr2 = ' '; + pr_trace_msg(trace_channel, 9, "resolve destination address '%s': %s", + ptr, pr_netaddr_get_ipstr(dst_addr)); + } + + /* Check the address family against what the PROTO field says it should + * be. This is to pedantically guard against IPv6 addresses in a + * "TCP4" PROXY line, or IPv4 addresses in a "TCP6" line. + */ + + /* TODO: Technically, it's possible that the remote client sent us DNS + * names, rather than IP addresses, and we resolved them. To pedantically + * check for this, scan the given address fields for illegal (e.g. + * alphabetic) characters, keeping in mind that IPv6 addresses can use + * hex. + */ + + if (have_tcp4) { + if (pr_netaddr_get_family(src_addr) != AF_INET) { + pr_log_debug(DEBUG8, MOD_PROXY_PROTOCOL_VERSION + ": expected IPv4 source address for '%s', got IPv6", + pr_netaddr_get_ipstr(src_addr)); + errno = EINVAL; + return -1; + } + + if (pr_netaddr_get_family(dst_addr) != AF_INET) { + pr_log_debug(DEBUG8, MOD_PROXY_PROTOCOL_VERSION + ": expected IPv4 destination address for '%s', got IPv6", + pr_netaddr_get_ipstr(dst_addr)); + errno = EINVAL; + return -1; + } + +#ifdef PR_USE_IPV6 + } else { + if (pr_netaddr_get_family(src_addr) != AF_INET6) { + pr_log_debug(DEBUG8, MOD_PROXY_PROTOCOL_VERSION + ": expected IPv6 source address for '%s', got IPv4", + pr_netaddr_get_ipstr(src_addr)); + errno = EINVAL; + return -1; + } + + if (pr_netaddr_get_family(dst_addr) != AF_INET6) { + pr_log_debug(DEBUG8, MOD_PROXY_PROTOCOL_VERSION + ": expected IPv6 destination address for '%s', got IPv4", + pr_netaddr_get_ipstr(dst_addr)); + errno = EINVAL; + return -1; + } + + /* Handle IPv4-mapped IPv6 addresses as IPv4 addresses. */ + if (pr_netaddr_is_v4mappedv6(src_addr) == TRUE) { + src_addr = pr_netaddr_v6tov4(p, src_addr); + } + + if (pr_netaddr_is_v4mappedv6(dst_addr) == TRUE) { + dst_addr = pr_netaddr_v6tov4(p, dst_addr); + } +#endif /* PR_USE_IPV6 */ + } + + ptr = ptr2 + 1; + ptr2 = strchr(ptr, ' '); + if (ptr2 == NULL) { + goto bad_proto; + } + + *ptr2 = '\0'; + pr_trace_msg(trace_channel, 9, + "resolving source port field '%s'", ptr); + src_port = strtou((const char **) &ptr, last); + + if (src_port == 0) { + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": invalid source port '%s' provided", ptr); + *ptr2 = ' '; + goto bad_proto; + + } else { + *ptr2 = ' '; + pr_trace_msg(trace_channel, 9, "resolved source port: %u", src_port); + } + + if (src_port > 65535) { + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": out-of-range source port provided: %u", src_port); + goto bad_proto; + } + + ptr = ptr2 + 1; + pr_trace_msg(trace_channel, 9, + "resolving destination port field '%s'", ptr); + dst_port = strtou((const char **) &ptr, last); + + if (dst_port == 0) { + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": invalid destination port '%s' provided", ptr); + *ptr2 = ' '; + goto bad_proto; + + } else { + *ptr2 = ' '; + pr_trace_msg(trace_channel, 9, "resolved destination port: %u", dst_port); + } + + if (dst_port > 65535) { + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": out-of-range destination port provided: %u", dst_port); + goto bad_proto; + } + + if (ptr > last) { + goto bad_proto; + } + + /* Paranoidly check the given source address/port against the + * destination address/port. If the two tuples match, then the remote + * client is lying to us (it's not possible to have a TCP connection + * FROM an address/port tuple which is identical to the destination + * address/port tuple). + */ + if (pr_netaddr_cmp(src_addr, dst_addr) == 0 && + src_port == dst_port) { + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": source/destination address/port are IDENTICAL: %s#%u", + pr_netaddr_get_ipstr(src_addr), src_port); + goto bad_proto; + } + + /* Set the source port for the source address. */ + pr_netaddr_set_port((pr_netaddr_t *) src_addr, htons(src_port)); + + *proxied_addr = src_addr; + *proxied_port = src_port; + + } else if (strncmp(ptr, "UNKNOWN", 7) == 0) { + pr_log_debug(DEBUG5, MOD_PROXY_PROTOCOL_VERSION + ": client cannot provide proxied address: '%.100s'", buf); + errno = ENOENT; + return 0; + + } else { + pr_log_debug(DEBUG5, MOD_PROXY_PROTOCOL_VERSION + ": unknown/unsupported PROTO field"); + goto bad_proto; + } + + return 1; + +bad_proto: + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": Bad/unsupported proxy protocol data '%.100s' from %s", buf, + pr_netaddr_get_ipstr(conn->remote_addr)); + + errno = EINVAL; + return -1; +} + +static const char haproxy_v2_sig[12] = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"; + +static int read_haproxy_v2(pool *p, conn_t *conn, + const pr_netaddr_t **proxied_addr, unsigned int *proxied_port) { + int res = 0; + uint8_t v2_sig[12], ver_cmd, trans_fam; + uint16_t v2_len; + struct iovec v2_hdr[4]; + const pr_netaddr_t *src_addr = NULL, *dst_addr = NULL; + + v2_hdr[0].iov_base = (void *) v2_sig; + v2_hdr[0].iov_len = sizeof(v2_sig); + v2_hdr[1].iov_base = (void *) &ver_cmd; + v2_hdr[1].iov_len = sizeof(ver_cmd); + v2_hdr[2].iov_base = (void *) &trans_fam; + v2_hdr[2].iov_len = sizeof(trans_fam); + v2_hdr[3].iov_base = (void *) &v2_len; + v2_hdr[3].iov_len = sizeof(v2_len); + + res = readv_sock(conn->rfd, v2_hdr, 4); + if (res < 0) { + return -1; + } + + if (res != 16) { + pr_trace_msg(trace_channel, 20, "read %lu V2 bytes, expected %lu bytes", + (unsigned long) res, (unsigned long) 16); + errno = EPERM; + return -1; + } + + /* Validate the obtained data. */ + if (memcmp(v2_sig, haproxy_v2_sig, sizeof(haproxy_v2_sig)) != 0) { + pr_trace_msg(trace_channel, 3, + "invalid proxy protocol V2 signature, rejecting"); + errno = EINVAL; + return -1; + } + + if ((ver_cmd & 0xF0) != 0x20) { + errno = EINVAL; + return -1; + } + + switch (ver_cmd & 0xF) { + /* PROXY command */ + case 0x01: + switch (trans_fam) { + /* TCP, IPv4 */ + case 0x11: { + uint32_t src_ipv4, dst_ipv4; + uint16_t src_port, dst_port; + struct iovec ipv4[4]; + struct sockaddr_in *saddr; + + pr_trace_msg(trace_channel, 17, + "received proxy protocol V2 TCP/IPv4 transport family (%lu bytes)", + (unsigned long) ntohs(v2_len)); + + if (ntohs(v2_len) != 12) { + pr_trace_msg(trace_channel, 3, + "proxy protocol V2 TCP/IPv4 transport family sent %lu bytes, " + "expected %lu bytes", (unsigned long) ntohs(v2_len), + (unsigned long) 12); + errno = EINVAL; + return -1; + } + + ipv4[0].iov_base = (void *) &src_ipv4; + ipv4[0].iov_len = sizeof(src_ipv4); + ipv4[1].iov_base = (void *) &dst_ipv4; + ipv4[1].iov_len = sizeof(dst_ipv4); + ipv4[2].iov_base = (void *) &src_port; + ipv4[2].iov_len = sizeof(src_port); + ipv4[3].iov_base = (void *) &dst_port; + ipv4[3].iov_len = sizeof(dst_port); + + res = readv_sock(conn->rfd, ipv4, 4); + if (res < 0) { + return -1; + } + + src_addr = pr_netaddr_alloc(p); + pr_netaddr_set_family((pr_netaddr_t *) src_addr, AF_INET); + saddr = (struct sockaddr_in *) pr_netaddr_get_sockaddr(src_addr); + saddr->sin_family = AF_INET; + saddr->sin_addr.s_addr = src_ipv4; + saddr->sin_port = src_port; + pr_netaddr_set_port((pr_netaddr_t *) src_addr, src_port); + + dst_addr = pr_netaddr_alloc(p); + pr_netaddr_set_family((pr_netaddr_t *) dst_addr, AF_INET); + saddr = (struct sockaddr_in *) pr_netaddr_get_sockaddr(dst_addr); + saddr->sin_family = AF_INET; + saddr->sin_addr.s_addr = dst_ipv4; + saddr->sin_port = dst_port; + pr_netaddr_set_port((pr_netaddr_t *) dst_addr, dst_port); + + pr_trace_msg(trace_channel, 17, + "received proxy protocol V2 TCP/IPv4 transport family: " + "source address %s#%d, destination address %s#%d", + pr_netaddr_get_ipstr(src_addr), + ntohs(pr_netaddr_get_port(src_addr)), + pr_netaddr_get_ipstr(dst_addr), + ntohs(pr_netaddr_get_port(dst_addr))); + + break; + } + + /* TCP, IPv6 */ + case 0x21: { + uint8_t src_ipv6[16], dst_ipv6[16]; + uint16_t src_port, dst_port; + struct iovec ipv6[4]; + struct sockaddr_in6 *saddr; + + pr_trace_msg(trace_channel, 17, + "received proxy protocol V2 TCP/IPv6 transport family (%lu bytes)", + (unsigned long) ntohs(v2_len)); + + if (ntohs(v2_len) != 36) { + pr_trace_msg(trace_channel, 3, + "proxy protocol V2 TCP/IPv6 transport family sent %lu bytes, " + "expected %lu bytes", (unsigned long) ntohs(v2_len), + (unsigned long) 36); + errno = EINVAL; + return -1; + } + +#ifdef PR_USE_IPV6 + ipv6[0].iov_base = (void *) &src_ipv6; + ipv6[0].iov_len = sizeof(src_ipv6); + ipv6[1].iov_base = (void *) &dst_ipv6; + ipv6[1].iov_len = sizeof(dst_ipv6); + ipv6[2].iov_base = (void *) &src_port; + ipv6[2].iov_len = sizeof(src_port); + ipv6[3].iov_base = (void *) &dst_port; + ipv6[3].iov_len = sizeof(dst_port); + + res = readv_sock(conn->rfd, ipv6, 4); + if (res < 0) { + return -1; + } + + src_addr = pr_netaddr_alloc(p); + pr_netaddr_set_family((pr_netaddr_t *) src_addr, AF_INET6); + saddr = (struct sockaddr_in6 *) pr_netaddr_get_sockaddr(src_addr); + saddr->sin6_family = AF_INET6; + memcpy(&(saddr->sin6_addr), src_ipv6, sizeof(src_ipv6)); + saddr->sin6_port = src_port; + pr_netaddr_set_port((pr_netaddr_t *) src_addr, src_port); + + dst_addr = pr_netaddr_alloc(p); + pr_netaddr_set_family((pr_netaddr_t *) dst_addr, AF_INET6); + saddr = (struct sockaddr_in6 *) pr_netaddr_get_sockaddr(dst_addr); + saddr->sin6_family = AF_INET6; + memcpy(&(saddr->sin6_addr), dst_ipv6, sizeof(dst_ipv6)); + saddr->sin6_port = dst_port; + pr_netaddr_set_port((pr_netaddr_t *) dst_addr, dst_port); + + /* Handle IPv4-mapped IPv6 addresses as IPv4 addresses. */ + if (pr_netaddr_is_v4mappedv6(src_addr) == TRUE) { + src_addr = pr_netaddr_v6tov4(p, src_addr); + } + + if (pr_netaddr_is_v4mappedv6(dst_addr) == TRUE) { + dst_addr = pr_netaddr_v6tov4(p, dst_addr); + } + + pr_trace_msg(trace_channel, 17, + "received proxy protocol V2 TCP/IPv6 transport family: " + "source address %s#%d, destination address %s#%d", + pr_netaddr_get_ipstr(src_addr), + ntohs(pr_netaddr_get_port(src_addr)), + pr_netaddr_get_ipstr(dst_addr), + ntohs(pr_netaddr_get_port(dst_addr))); +#else + /* Avoid compiler warnings about unused variables. */ + (void) src_ipv6; + (void) dst_ipv6; + (void) src_port; + (void) dst_port; + (void) ipv6; + (void) saddr; + + pr_trace_msg(trace_channel, 3, + "IPv6 support disabled, ignoring proxy protocol V2 data"); +#endif /* PR_USE_IPV6 */ + break; + } + + /* Unix */ + case 0x31: { + unsigned char src_path[108]; + unsigned char dst_path[108]; + + pr_trace_msg(trace_channel, 17, + "received proxy protocol V2 Unix transport family " + "(%lu bytes), ignoring", (unsigned long) ntohs(v2_len)); + + if (ntohs(v2_len) != 216) { + pr_trace_msg(trace_channel, 3, + "proxy protocol V2 Unix transport family sent %lu bytes, " + "expected %lu bytes", (unsigned long) ntohs(v2_len), + (unsigned long) 216); + errno = EINVAL; + return -1; + } + + res = read_sock(conn->rfd, src_path, sizeof(src_path)); + if (res > 0) { + pr_trace_msg(trace_channel, 15, + "received proxy protocol V2 Unix source path: '%s'", src_path); + } + + res = read_sock(conn->rfd, dst_path, sizeof(dst_path)); + if (res > 0) { + pr_trace_msg(trace_channel, 15, + "received proxy protocol V2 Unix destination path: '%s'", + dst_path); + } + + break; + } + + /* Unspecified */ + case 0x00: { + pool *tmp_pool; + unsigned char *buf; + size_t buflen; + + buflen = ntohs(v2_len); + + pr_trace_msg(trace_channel, 17, + "received proxy protocol V2 unspecified transport family " + "(%lu bytes), ignoring", (unsigned long) buflen); + + tmp_pool = make_sub_pool(p); + buf = palloc(tmp_pool, buflen); + (void) read_sock(conn->rfd, buf, buflen); + destroy_pool(tmp_pool); + break; + } + + default: + pr_trace_msg(trace_channel, 3, + "unsupported proxy protocol V2 transport family: %u", trans_fam); + errno = EINVAL; + return -1; + } + break; + + /* LOCAL command */ + case 0x00: + /* Keep local connection address for LOCAL commands. */ + pr_trace_msg(trace_channel, 17, + "received proxy protocol V2 LOCAL command, ignoring"); + return 0; + + default: + pr_trace_msg(trace_channel, 3, + "unsupported proxy protocol V2 command: %u", ver_cmd); + errno = EINVAL; + return -1; + } + + if (src_addr == NULL && + dst_addr == NULL) { + return 0; + } + + if (ntohs(pr_netaddr_get_port(dst_addr)) > 65535) { + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": out-of-range destination port provided: %u", + ntohs(pr_netaddr_get_port(dst_addr))); + errno = EINVAL; + return -1; + } + + /* Paranoidly check the given source address/port against the + * destination address/port. If the two tuples match, then the remote + * client is lying to us (it's not possible to have a TCP connection + * FROM an address/port tuple which is identical to the destination + * address/port tuple). + */ + if (pr_netaddr_cmp(src_addr, dst_addr) == 0 && + pr_netaddr_get_port(src_addr) == pr_netaddr_get_port(dst_addr)) { + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": source/destination address/port are IDENTICAL: %s#%u", + pr_netaddr_get_ipstr(src_addr), ntohs(pr_netaddr_get_port(src_addr))); + errno = EPERM; + return -1; + } + + *proxied_addr = src_addr; + *proxied_port = ntohs(pr_netaddr_get_port(src_addr)); + + return 0; +} + +static int read_proxied_addr(pool *p, conn_t *conn, + const pr_netaddr_t **proxied_addr, unsigned int *proxied_port) { + int res; + + /* Note that in theory, we could auto-detect the protocol version. */ + + switch (proxy_protocol_version) { + case PROXY_PROTOCOL_VERSION_HAPROXY_V1: + res = read_haproxy_v1(p, conn, proxied_addr, proxied_port); + break; + + case PROXY_PROTOCOL_VERSION_HAPROXY_V2: + res = read_haproxy_v2(p, conn, proxied_addr, proxied_port); + break; + + default: + errno = ENOSYS; + res = -1; + } + + return res; +} + +static int proxy_protocol_timeout_cb(CALLBACK_FRAME) { + pr_event_generate("proxy_protocol.timeout", NULL); + + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": proxy protocol timeout (%d %s) reached, disconnecting client", + proxy_protocol_timeout, proxy_protocol_timeout != 1 ? "secs" : "sec"); + pr_session_disconnect(&proxy_protocol_module, PR_SESS_DISCONNECT_TIMEOUT, + "ProxyProtocolTimeout"); + + return 0; +} + +/* Configuration handlers + */ + +/* usage: ProxyProtocolEngine on|off */ +MODRET set_proxyprotocolengine(cmd_rec *cmd) { + int engine = 0; + config_rec *c; + + CHECK_ARGS(cmd, 1); + CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); + + engine = get_boolean(cmd, 1); + if (engine == -1) { + CONF_ERROR(cmd, "expected Boolean parameter"); + } + + c = add_config_param(cmd->argv[0], 1, NULL); + c->argv[0] = pcalloc(c->pool, sizeof(int)); + *((int *) c->argv[0]) = engine; + + return PR_HANDLED(cmd); +} + +/* usage: ProxyProtocolTimeout nsecs */ +MODRET set_proxyprotocoltimeout(cmd_rec *cmd) { + int timeout = -1; + config_rec *c = NULL; + + CHECK_ARGS(cmd, 1); + CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); + + if (pr_str_get_duration(cmd->argv[1], &timeout) < 0) { + CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error parsing timeout value '", + cmd->argv[1], "': ", strerror(errno), NULL)); + } + + c = add_config_param(cmd->argv[0], 1, NULL); + c->argv[0] = pcalloc(c->pool, sizeof(int)); + *((int *) c->argv[0]) = timeout; + + return PR_HANDLED(cmd); +} + +/* usage: ProxyProtocolVersion protocol */ +MODRET set_proxyprotocolversion(cmd_rec *cmd) { + int proto_version = -1; + config_rec *c = NULL; + + CHECK_ARGS(cmd, 1); + CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); + + if (strcasecmp(cmd->argv[1], "haproxyV1") == 0) { + proto_version = PROXY_PROTOCOL_VERSION_HAPROXY_V1; + + } else if (strcasecmp(cmd->argv[1], "haproxyV2") == 0) { + proto_version = PROXY_PROTOCOL_VERSION_HAPROXY_V2; + + } else { + CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown protocol/version: ", + cmd->argv[1], NULL)); + } + + c = add_config_param(cmd->argv[0], 1, NULL); + c->argv[0] = pcalloc(c->pool, sizeof(int)); + *((int *) c->argv[0]) = proto_version; + + return PR_HANDLED(cmd); +} + +/* Initialization routines + */ + +static int proxy_protocol_sess_init(void) { + config_rec *c; + int engine = 0, res = 0, timerno = -1, xerrno; + const pr_netaddr_t *proxied_addr = NULL; + unsigned int proxied_port = 0; + const char *remote_ip = NULL, *remote_name = NULL; + pr_netio_t *tls_netio = NULL; + + c = find_config(main_server->conf, CONF_PARAM, "ProxyProtocolEngine", FALSE); + if (c != NULL) { + engine = *((int *) c->argv[0]); + } + + if (engine == FALSE) { + return 0; + } + + c = find_config(main_server->conf, CONF_PARAM, "ProxyProtocolTimeout", FALSE); + if (c != NULL) { + proxy_protocol_timeout = *((int *) c->argv[0]); + } + + c = find_config(main_server->conf, CONF_PARAM, "ProxyProtocolVersion", FALSE); + if (c != NULL) { + proxy_protocol_version = *((int *) c->argv[0]); + } + + if (proxy_protocol_timeout > 0) { + timerno = pr_timer_add(proxy_protocol_timeout, -1, + &proxy_protocol_module, proxy_protocol_timeout_cb, + "ProxyProtocolTimeout"); + } + + /* If the mod_tls module is in effect, then we need to work around its + * use of the NetIO API. Otherwise, trying to read the proxied address + * on the control connection will cause problems, e.g. for FTPS clients + * using implicit TLS. + */ + tls_netio = pr_get_netio(PR_NETIO_STRM_CTRL); + if (tls_netio == NULL || + tls_netio->owner_name == NULL || + strncmp(tls_netio->owner_name, "tls", 4) != 0) { + + /* Not a mod_tls netio; ignore it. */ + tls_netio = NULL; + + } else { + /* Unregister it; we'll put it back after reading the proxied address. */ + pr_unregister_netio(PR_NETIO_STRM_CTRL); + } + + res = read_proxied_addr(session.pool, session.c, &proxied_addr, + &proxied_port); + xerrno = errno; + + if (tls_netio != NULL) { + if (pr_register_netio(tls_netio, PR_NETIO_STRM_CTRL) < 0) { + pr_log_debug(DEBUG1, MOD_PROXY_PROTOCOL_VERSION + ": unable to re-register TLS control NetIO: %s", strerror(errno)); + } + } + + if (proxy_protocol_timeout > 0) { + pr_timer_remove(timerno, &proxy_protocol_module); + } + + if (res < 0) { + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": error reading proxy info: %s", strerror(xerrno)); + + errno = EPERM; + return -1; + } + + if (proxied_addr != NULL) { + remote_ip = pstrdup(session.pool, + pr_netaddr_get_ipstr(pr_netaddr_get_sess_remote_addr())); + remote_name = pstrdup(session.pool, + pr_netaddr_get_sess_remote_name()); + + pr_log_debug(DEBUG9, MOD_PROXY_PROTOCOL_VERSION + ": using proxied source address: %s", pr_netaddr_get_ipstr(proxied_addr)); + + session.c->remote_addr = proxied_addr; + session.c->remote_port = proxied_port; + + /* Now perform reverse DNS lookups. */ + if (ServerUseReverseDNS) { + int reverse_dns; + + reverse_dns = pr_netaddr_set_reverse_dns(ServerUseReverseDNS); + session.c->remote_name = pr_netaddr_get_dnsstr(session.c->remote_addr); + + pr_netaddr_set_reverse_dns(reverse_dns); + + } else { + session.c->remote_name = pr_netaddr_get_ipstr(session.c->remote_addr); + } + + pr_netaddr_set_sess_addrs(); + + pr_log_debug(DEBUG0, MOD_PROXY_PROTOCOL_VERSION + ": UPDATED client remote address/name: %s/%s (WAS %s/%s)", + pr_netaddr_get_ipstr(pr_netaddr_get_sess_remote_addr()), + pr_netaddr_get_sess_remote_name(), remote_ip, remote_name); + + /* Find the new class for this session. */ + session.conn_class = pr_class_match_addr(session.c->remote_addr); + if (session.conn_class != NULL) { + pr_log_debug(DEBUG2, MOD_PROXY_PROTOCOL_VERSION + ": session requested from proxied client in '%s' class", + session.conn_class->cls_name); + + } else { + pr_log_debug(DEBUG5, MOD_PROXY_PROTOCOL_VERSION + ": session requested from proxied client in unknown class"); + } + } + + return 0; +} + +/* Module API tables + */ + +static conftable proxy_protocol_conftab[] = { + { "ProxyProtocolEngine", set_proxyprotocolengine, NULL }, + { "ProxyProtocolTimeout", set_proxyprotocoltimeout, NULL }, + { "ProxyProtocolVersion", set_proxyprotocolversion, NULL }, + + { NULL } +}; + +module proxy_protocol_module = { + /* Always NULL */ + NULL, NULL, + + /* Module API version */ + 0x20, + + /* Module name */ + "proxy_protocol", + + /* Module configuration handler table */ + proxy_protocol_conftab, + + /* Module command handler table */ + NULL, + + /* Module authentication handler table */ + NULL, + + /* Module initialization */ + NULL, + + /* Session initialization */ + proxy_protocol_sess_init, + + /* Module version */ + MOD_PROXY_PROTOCOL_VERSION +}; + diff --git a/mod_proxy_protocol.html b/mod_proxy_protocol.html new file mode 100644 index 0000000..ddd2d00 --- /dev/null +++ b/mod_proxy_protocol.html @@ -0,0 +1,237 @@ + + +ProFTPD module mod_proxy_protocol + + + + +
+
+

ProFTPD module mod_proxy_protocol

+
+

+ +

+The purpose of the mod_proxy_protocol module is to handle +protocols which are used by proxies, e.g. haproxy, for +conveying information about the real origin/client to the backend server. +Protocols like HTTP often have their own mechanism for doing so, via headers +such as "X-Forwarded-For". Unfortunately, FTP does not have such a +mechanism, nor does SSH. + +

+However, there are protocols which an provide this information without +impacting FTP. HAproxy's PROXY protocol is one such mechanism. The +mod_proxy_protocol module uses these mechanisms to change the +information about the "remote" client so that it is the real client, not the +proxy, whose IP address/port are logged and used for e.g. network ACLs. + +

+This module is contained in the mod_proxy_protocol.c file for +ProFTPD 1.3.x, and is not compiled by default. Installation +instructions are discussed here; detailed +notes on best practices for using this module are here. + +

+The most current version of mod_proxy_protocol can be found at: +

+  https://github.com/Castaglia/proftpd-mod_proxy_protocol.git
+
+ +

Author

+

+Please contact TJ Saunders <tj at castaglia.org> with any +questions, concerns, or suggestions regarding this module. + +

Directives

+ + +

+


+

ProxyProtocolEngine

+Syntax: ProxyProtocolEngine on|off
+Default: None
+Context: server config, <VirtualHost>, <Global>
+Module: mod_proxy_protocol
+Compatibility: 1.3.5rc4 and later + +

+The ProxyProtocolEngine directive enables the expectation +and handling of protocols which provide information on proxied connections; +support for these protocols is provided by mod_proxy_protocol. + +

+


+

ProxyProtocolTimeout

+Syntax: ProxyProtocolTimeout seconds
+Default: 3sec
+Context: server config, <VirtualHost>, <Global>
+Module: mod_proxy_protocol
+Compatibility: 1.3.5rc4 and later + +

+The ProxyProtocolTimeout directive is used to configure the +amount of time, in seconds, that mod_proxy_protocol will wait to +receive the full expected proxy information. If the full information is +not received within the given number of seconds, the connection to the client +is closed. + +

+


+

ProxyProtocolVersion

+Syntax: ProxyProtocolVersion protocolVersion
+Default: haproxyV1
+Context: server config, <VirtualHost>, <Global>
+Module: mod_proxy_protocol
+Compatibility: 1.3.5rc4 and later + +

+The ProxyProtocolVersion directive is used to configure the +protocol that mod_proxy_protocol expects to handle. The +currently supported values are: +

    +
  • haproxyV1 +
  • haproxyV2 +
+ +

+


+

Usage

+ +

+Example Configuration
+

+  <IfModule mod_proxy_protocol.c>
+    ProxyProtocolEngine on
+    ProxyProtocolTimeout 3sec
+
+    # Necessary to allow data transfers
+    AllowForeignAddress on
+  </IfModule>
+
+ +

+Module Load Order
+In order for mod_proxy_protocol to work its magic, it must +the first module in line to handle the bytes coming in from the client. +If some other module (such as mod_sftp or mod_tls) +tries to handle the incoming bytes first, Bad Things will happen, since those +modules will expect different protocols than the PROXY protocol. + +

+For mod_proxy_protocol to be the first module called, it must +the last module loaded. To do this as a static module, you would +use something like this when building proftpd: +

+  # ./configure --with-modules=...:mod_proxy_protocol
+
+ensuring that mod_proxy_protocol is the last module in +your --with-modules list. + +

+As a shared module, configuring mod_proxy_protocol to be the +last module loaded is much easier. Your configuration will have a list +of LoadModule directives; the last of which would be: +

+  LoadModule mod_proxy_protocol.c
+
+ +

+Trusting Senders of Proxy Data
+Use of these proxy protocols means changes in audit trails and/or client +access permissions (e.g. different mod_wrap2 and/or +mod_geoip rules will apply). Unscrupulous senders may try to +actively lie to your server about the original client using these protocols. +Thus you must trust the upstream machines before enabling the +mod_proxy_protocol module. + +

+Put another way: do not use the mod_proxy_protocol module +if your server handles connections from the open Internet. Doing so means +that any machine can use the proxy protocol to hide their activities, or +make it look like the connection is coming from someone else. Only accept +proxy information from trusted sources. + +

+Why AllowForeignAddress Is Needed
+One of the consequences of allowing mod_proxy_protocol to change +the remote IP address is that security checks performed on data transfers +will cause problems. For active data transfers (i.e. for clients +which send the PORT or EPRT commands), +proftpd requires that the IP address sent in the command matches +the IP address of the client which sends the command. Otherwise, a message +like the following is logged: +

+  Refused PORT 127,0,0,1,218,225 (address mismatch)
+
+and the command is rejected. + +

+Similarly for passive data transfers (i.e. for clients which send the +PASV or EPSV commands), proftpd requires +that the remote IP address of the client which connects to the data transfer +address must match the remote IP address of the client on the control +connection. If the addresses do no match, then the following is logged: +

+  SECURITY VIOLATION: Passive connection from 127.0.0.1 rejected.
+
+and the control connection is closed. + +

+These security measures are done to prevent abuses of FTP data transfers +such as the FTP bounce +attack. However, the very fact that mod_proxy_protocol changes +the remote IP address means that to allow data transfers when using this module, +you need to use: +

+  AllowForeignAddress on
+
+in the same virtual host section in which the ProxyProtocolEngine +directive appears. + +

+


+

Installation

+To install mod_proxy_protocol, copy the +mod_proxy_protocol.c file into: +
+  proftpd-dir/contrib/
+
+after unpacking the latest proftpd-1.3.x source code. For including +mod_proxy_protocol as a staticly linked module: +
+  $ ./configure --with-modules=...:mod_proxy_protocol
+
+To build mod_proxy_protocol as a DSO module: +
+  $ ./configure --enable-dso --with-shared=...:mod_proxy_protocol
+
+Then follow the usual steps: +
+  $ make
+  $ make install
+
+ +

+For those with an existing ProFTPD installation, you can use the +prxs tool to add mod_proxy_protocol, as a DSO module, +to your existing server: +

+  $ prxs -c -i -d mod_proxy_protocol.c
+
+ +

+


+ +© Copyright 2013-2017 TJ Saunders
+ All Rights Reserved
+
+ +
+ + + diff --git a/t/config/maxhostsperuser.t b/t/config/maxhostsperuser.t new file mode 100644 index 0000000..88e5fa7 --- /dev/null +++ b/t/config/maxhostsperuser.t @@ -0,0 +1,11 @@ +#!/usr/bin/env perl + +use lib qw(t/lib); +use strict; + +use Test::Unit::HarnessUnit; + +$| = 1; + +my $r = Test::Unit::HarnessUnit->new(); +$r->start("ProFTPD::Tests::Config::MaxHostsPerUser"); diff --git a/t/lib/ProFTPD/TestSuite/ProxiedFTP.pm b/t/lib/ProFTPD/TestSuite/ProxiedFTP.pm new file mode 100644 index 0000000..840055d --- /dev/null +++ b/t/lib/ProFTPD/TestSuite/ProxiedFTP.pm @@ -0,0 +1,66 @@ +package ProFTPD::TestSuite::ProxiedFTP; + +use strict; +use vars qw(@ISA); + +use Carp; +use Net::FTP; + +@ISA = qw(Net::FTP); + +my $proxy_info = undef; + +sub new { + my $class = shift; + my ($addr, $port, $proxy, $timeout) = @_; + $timeout = 5 unless defined($timeout); + my $debug = undef; + + $proxy_info = $proxy; + + if ($ENV{TEST_VERBOSE}) { + $debug = 10; + } + + my $self = $class->SUPER::new($addr, + Port => $port, + Timeout => $timeout, + Debug => $debug, + ); + + unless ($self) { + croak($@); + } + + return $self; +} + +# Override response() from Net::Cmd to trigger sending the PROXY command +sub response { + my $self = shift; + + if (defined($proxy_info)) { + if (ref($proxy_info)) { + my ($proto, $src_addr, $dst_addr, $src_port, $dst_port) = @$proxy_info; + $self->command("PROXY", $proto, $src_addr, $dst_addr, $src_port, $dst_port); + + } else { + $self->rawdatasend($proxy_info); + } + + $proxy_info = undef; + } + + $self->SUPER::response(); +} + +sub login { + my $self = shift; + + unless ($self->SUPER::login(@_)) { + croak("Failed to login: " . $self->code . " " . $self->message); + } + + return 1; +} +1; diff --git a/t/lib/ProFTPD/Tests/Config/MaxHostsPerUser.pm b/t/lib/ProFTPD/Tests/Config/MaxHostsPerUser.pm new file mode 100644 index 0000000..79e4081 --- /dev/null +++ b/t/lib/ProFTPD/Tests/Config/MaxHostsPerUser.pm @@ -0,0 +1,229 @@ +package ProFTPD::Tests::Config::MaxHostsPerUser; + +use lib qw(t/lib); +use base qw(ProFTPD::TestSuite::Child); +use strict; + +use File::Spec; +use IO::Handle; + +use ProFTPD::TestSuite::FTP; +use ProFTPD::TestSuite::ProxiedFTP; +use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite); + +$| = 1; + +my $order = 0; + +my $TESTS = { + maxhostsperuser_one => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + maxhostsperuser_one_multi_conns => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + +}; + +sub new { + return shift()->SUPER::new(@_); +} + +sub list_tests { + return testsuite_get_runnable_tests($TESTS); +} + +sub maxhostsperuser_one { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'config'); + + my $max_hosts = 1; + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + MaxHostsPerUser => $max_hosts, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + my $proxy_info = ['TCP4', '1.1.1.1', '127.0.0.1', 111, $port]; + + # 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); + + # First client should be able to connect and log in... + my $client1 = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '127.0.0.1', '127.0.0.1', 12345, $port]); + $client1->login($setup->{user}, $setup->{passwd}); + + # ...but the second client should be able to connect, but not login. + my $client2 = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + $proxy_info); + eval { $client2->login($setup->{user}, $setup->{passwd}) }; + unless ($@) { + die("Login succeeded unexpectedly"); + } + + $client1->quit(); + }; + + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh) }; + 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 maxhostsperuser_one_multi_conns { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'config'); + + my $max_hosts = 1; + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + MaxHostsPerUser => $max_hosts, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + my $proxy_info = ['TCP4', '1.1.1.1', '127.0.0.1', 111, $port]; + + # 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); + + # First client should be able to connect and log in... + my $client1 = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '127.0.0.1', '127.0.0.1', 12345, $port]); + $client1->login($setup->{user}, $setup->{passwd}); + + # ...but the second client should be able to connect, but not login. + my $client2 = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + $proxy_info); + eval { $client2->login($setup->{user}, $setup->{passwd}) }; + unless ($@) { + die("Login succeeded unexpectedly"); + } + + # Even though we can't log in, we should be able to connect quite + # a few more times + + my $clients = []; + for (my $i = 0; $i < 10; $i++) { + my $client = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + $proxy_info); + push(@$clients, $client); + } + + $client1->quit(); + }; + + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh) }; + 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; diff --git a/t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol.pm b/t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol.pm new file mode 100644 index 0000000..1e6dc03 --- /dev/null +++ b/t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol.pm @@ -0,0 +1,2233 @@ +package ProFTPD::Tests::Modules::mod_proxy_protocol; + +use lib qw(t/lib); +use base qw(ProFTPD::TestSuite::Child); +use strict; + +use File::Path qw(mkpath); +use File::Spec; +use IO::Handle; +use Net::Cmd qw(CMD_OK); + +use ProFTPD::TestSuite::FTP; +use ProFTPD::TestSuite::ProxiedFTP; +use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite); + +$| = 1; + +my $order = 0; + +my $TESTS = { + proxy_protocol_login_with_proxy => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_login_without_proxy => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_config_denyclass => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_bad_start_of_line => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_bad_end_of_line => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_bad_proto => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_bad_src_addr => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_dns_src_addr => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_bad_dst_addr => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_dns_dst_addr => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_bad_src_port => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_bad_dst_port => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_too_large_src_port => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_too_large_dst_port => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_tcp4_with_ipv6_src_addr => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_tcp4_with_ipv6_dst_addr => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_tcp6_with_ipv4_src_addr => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_tcp6_with_ipv4_dst_addr => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_tcp6_with_useipv6_off => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_matching_src_dst_info => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_unknown_proto => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_active_transfer_with_proxy => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_passive_transfer_with_proxy => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_active_transfer_with_proxy_allowforeignaddress => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + + proxy_protocol_passive_transfer_with_proxy_allowforeignaddress => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol)], + }, + +}; + +sub new { + return shift()->SUPER::new(@_); +} + +sub list_tests { + return testsuite_get_runnable_tests($TESTS); +} + +sub proxy_protocol_login_with_proxy { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + my $client = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', '2.2.2.2', 111, 222]); + $client->login($setup->{user}, $setup->{passwd}); + $client->quit(); + }; + + 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_login_without_proxy { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy_protocol'); + + my $proxy_timeout = 1; + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + ProxyProtocolTimeout => $proxy_timeout, + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1, 1, 1); + $client->login($setup->{user}, $setup->{passwd}); + $client->quit(); + }; + + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh, $proxy_timeout + 5) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + + $self->assert_child_ok($pid); + + if ($ex) { + $ex = undef; + + } else { + $ex = "Connection succeeded unexpectedly"; + } + + test_cleanup($setup->{log_file}, $ex); +} + +sub proxy_protocol_config_denyclass { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + if (open(my $fh, ">> $setup->{config_file}")) { + print $fh < + From 1.1.1.1 + + + + DenyClass test + +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(2); + + my $client = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', '2.2.2.2', 111, 222]); + eval { $client->login($setup->{user}, $setup->{passwd}) }; + unless ($@) { + die("Login succeeded unexpectedly"); + } + + $client->quit(); + }; + + 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_bad_start_of_line { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + "GET /index.html HTTP/1.1.1\r\n") + }; + unless ($@) { + die("Connection 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_bad_end_of_line { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', '2.2.2.2', 111, '222 ' . 'A' x 128]); + }; + unless ($@) { + die("Connection 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_bad_proto { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['IPV4', '1.1.1.1', '2.2.2.2', 111, '222']); + }; + unless ($@) { + die("Connection 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_bad_src_addr { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', 'foo', '2.2.2.2', 111, '222']); + }; + unless ($@) { + die("Connection 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_dns_src_addr { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', 'localhost', '2.2.2.2', 111, '222']); + }; + unless ($@) { + die("Connection 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_bad_dst_addr { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', 'bar', 111, '222']); + }; + unless ($@) { + die("Connection 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_dns_dst_addr { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', 'localhost', 111, '222']); + }; + unless ($@) { + die("Connection 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_bad_src_port { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', '2.2.2.2', 'baz', '222']); + }; + unless ($@) { + die("Connection 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_bad_dst_port { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', '2.2.2.2', 111, 'quxx']); + }; + unless ($@) { + die("Connection 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_too_large_src_port { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', '2.2.2.2', 70000, 222]); + }; + unless ($@) { + die("Connection 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_too_large_dst_port { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', '2.2.2.2', 111, 70000]); + }; + unless ($@) { + die("Connection 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_tcp4_with_ipv6_src_addr { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + UseIPv6 => 'on', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '::1', '2.2.2.2', 111, 222]); + }; + unless ($@) { + die("Connection 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_tcp4_with_ipv6_dst_addr { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + UseIPv6 => 'on', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', '::2', 111, 222]); + }; + unless ($@) { + die("Connection 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_tcp6_with_ipv4_src_addr { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + UseIPv6 => 'on', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP6', '1.1.1.1', '::2', 111, 222]); + }; + unless ($@) { + die("Connection 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_tcp6_with_ipv4_dst_addr { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + UseIPv6 => 'on', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP6', '::1', '2.2.2.2', 111, 222]); + }; + unless ($@) { + die("Connection 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_tcp6_with_useipv6_off { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + UseIPv6 => 'off', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP6', '::1', '::2', 111, 222]); + }; + unless ($@) { + die("Connection 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_matching_src_dst_info { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + eval { + ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', '1.1.1.1', 111, 111]); + }; + unless ($@) { + die("Connection 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_unknown_proto { + 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}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + my $client = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['UNKNOWN', '1.1.1.1', '2.2.2.2', 111, 222]); + $client->login($setup->{user}, $setup->{passwd}); + $client->quit(); + }; + + 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_active_transfer_with_proxy { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy_protocol'); + + my $test_file = File::Spec->rel2abs("$tmpdir/test.dat"); + if (open(my $fh, "> $test_file")) { + print $fh "Hello, World!\n"; + + unless (close($fh)) { + die("Can't write $test_file: $!"); + } + + } else { + die("Can't open $test_file: $!"); + } + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + # Note: If AllowForeignAddress is not allowed, then the PORT command + # will fail with the following e.g. error being logged: + # + # Refused PORT 127,0,0,1,218,225 (address mismatch) + # + AllowForeignAddress => 'off', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + $ENV{FTP_PASSIVE} = 0; + + my $client = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', '2.2.2.2', 111, 222]); + $client->login($setup->{user}, $setup->{passwd}); + + if ($client->get('test.dat', '/dev/null')) { + die("RETR test.dat succeeded unexpectedly"); + } + + $client->quit(); + }; + + 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_passive_transfer_with_proxy { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy_protocol'); + + my $test_file = File::Spec->rel2abs("$tmpdir/test.dat"); + if (open(my $fh, "> $test_file")) { + print $fh "Hello, World!\n"; + + unless (close($fh)) { + die("Can't write $test_file: $!"); + } + + } else { + die("Can't open $test_file: $!"); + } + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + # Note: If AllowForeignAddress is not allowed, then the data transfer + # will fail with the following e.g. error being logged: + # + # SECURITY VIOLATION: Passive connection from 127.0.0.1 rejected. + # + AllowForeignAddress => 'off', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + $ENV{FTP_PASSIVE} = 1; + + my $client = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', '2.2.2.2', 111, 222]); + $client->login($setup->{user}, $setup->{passwd}); + + if ($client->get('test.dat', '/dev/null')) { + die("RETR test.dat succeeded unexpectedly"); + } + + # Note: we should send QUIT here, but because proftpd treated this as + # a security violation, it terminated the control connection as well. + }; + + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh) }; + 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_active_transfer_with_proxy_allowforeignaddress { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy_protocol'); + + my $test_file = File::Spec->rel2abs("$tmpdir/test.dat"); + if (open(my $fh, "> $test_file")) { + print $fh "Hello, World!\n"; + + unless (close($fh)) { + die("Can't write $test_file: $!"); + } + + } else { + die("Can't open $test_file: $!"); + } + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + # Note: If AllowForeignAddress is not allowed, then the PORT command + # will fail with the following e.g. error being logged: + # + # Refused PORT 127,0,0,1,218,225 (address mismatch) + # + AllowForeignAddress => 'on', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + $ENV{FTP_PASSIVE} = 0; + + my $client = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', '2.2.2.2', 111, 222]); + $client->login($setup->{user}, $setup->{passwd}); + + unless ($client->get('test.dat', '/dev/null')) { + die("RETR test.dat failed: " . $client->code . " " . $client->message); + } + + my $resp_code = $client->code; + my $expected = 226; + $self->assert($expected == $resp_code, + test_msg("Expected response code $expected, got $resp_code")); + + $client->quit(); + }; + + 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_passive_transfer_with_proxy_allowforeignaddress { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy_protocol'); + + my $test_file = File::Spec->rel2abs("$tmpdir/test.dat"); + if (open(my $fh, "> $test_file")) { + print $fh "Hello, World!\n"; + + unless (close($fh)) { + die("Can't write $test_file: $!"); + } + + } else { + die("Can't open $test_file: $!"); + } + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + # Note: If AllowForeignAddress is not allowed, then the data transfer + # will fail with the following e.g. error being logged: + # + # SECURITY VIOLATION: Passive connection from 127.0.0.1 rejected. + # + AllowForeignAddress => 'on', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + $ENV{FTP_PASSIVE} = 1; + + my $client = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', '2.2.2.2', 111, 222]); + $client->login($setup->{user}, $setup->{passwd}); + + unless ($client->get('test.dat', '/dev/null')) { + die("RETR test.dat failed: " . $client->code . " " . $client->message); + } + + my $resp_code = $client->code; + my $expected = 226; + $self->assert($expected == $resp_code, + test_msg("Expected response code $expected, got $resp_code")); + + $client->quit(); + }; + + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh) }; + 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; diff --git a/t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol/sftp.pm b/t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol/sftp.pm new file mode 100644 index 0000000..4a9eb22 --- /dev/null +++ b/t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol/sftp.pm @@ -0,0 +1,150 @@ +package ProFTPD::Tests::Modules::mod_proxy_protocol::sftp; + +use lib qw(t/lib); +use base qw(ProFTPD::TestSuite::Child); +use strict; + +use File::Path qw(mkpath); +use File::Spec; +use IO::Handle; + +use ProFTPD::TestSuite::ProxiedFTP; +use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite); + +$| = 1; + +my $order = 0; + +my $TESTS = { + proxy_protocol_sftp_with_proxy => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol mod_sftp)], + }, + + proxy_protocol_sftp_without_proxy => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol mod_sftp)], + }, + +}; + +sub new { + return shift()->SUPER::new(@_); +} + +sub list_tests { +# return testsuite_get_runnable_tests($TESTS); + return qw( + proxy_protocol_sftp_with_proxy + ); +} + +sub set_up { + my $self = shift; + $self->SUPER::set_up(@_); + + # Make sure that mod_sftp does not complain about permissions on the hostkey + # files. + + my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); + my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); + + unless (chmod(0400, $rsa_host_key, $dsa_host_key)) { + die("Can't set perms on $rsa_host_key, $dsa_host_key: $!"); + } +} + +sub proxy_protocol_sftp_with_proxy { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy_protocol'); + + my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); + my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'ssh2:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + + 'mod_sftp.c' => [ + "SFTPEngine on", + "SFTPLog $setup->{log_file}", + "SFTPHostKey $rsa_host_key", + "SFTPHostKey $dsa_host_key", + ], + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + my $client = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port); + $client->send_proxy_raw('1.1.1.1', '2.2.2.2', 111, 222); + my $banner = $client->getline(); + chomp($banner); + + unless ($banner =~ /^SSH\-2\.0\-mod_sftp/) { + die("Received unexpected banner from mod_sftp: '$banner'"); + } + + print $client "SSH-2.0-ProFTPD_mod_proxy_protocol_sftp_Test\r\n"; + }; + + 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); +} + +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 new file mode 100644 index 0000000..13adb0c --- /dev/null +++ b/t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol/tls.pm @@ -0,0 +1,304 @@ +package ProFTPD::Tests::Modules::mod_proxy_protocol::tls; + +use lib qw(t/lib); +use base qw(ProFTPD::TestSuite::Child); +use strict; + +use File::Path qw(mkpath); +use File::Spec; +use IO::Handle; +use Net::Cmd qw(CMD_OK CMD_MORE); + +use ProFTPD::TestSuite::ProxiedFTP; +use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite); + +$| = 1; + +my $order = 0; + +my $TESTS = { + proxy_protocol_tls_login_with_proxy => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol mod_tls)], + }, + + proxy_protocol_tls_login_with_proxy_useimplicitssl => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol mod_tls)], + }, + +}; + +sub new { + return shift()->SUPER::new(@_); +} + +sub list_tests { + # Check for the required Perl modules: + # + # Net-SSLeay + # IO-Socket-SSL + + my $required = [qw( + Net::SSLeay + IO::Socket::SSL + )]; + + foreach my $req (@$required) { + eval "use $req"; + if ($@) { + print STDERR "\nWARNING:\n + Module '$req' not found, skipping all tests\n"; + + if ($ENV{TEST_VERBOSE}) { + print STDERR "Unable to load $req: $@\n"; + } + + return qw(testsuite_empty_test); + } + } + +# return testsuite_get_runnable_tests($TESTS); + return qw( + proxy_protocol_tls_login_with_proxy_useimplicitssl + ); +} + +sub proxy_protocol_tls_login_with_proxy { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy_protocol'); + + my $server_cert_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/server-cert.pem"); + my $ca_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/ca-cert.pem"); + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'netio:10 proxy_protocol:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + + 'mod_tls.c' => { + TLSEngine => 'on', + TLSLog => $setup->{log_file}, + TLSProtocol => 'SSLv3 TLSv1', + TLSRequired => 'on', + TLSRSACertificateFile => $server_cert_file, + TLSCACertificateFile => $ca_file, + }, + }, + }; + + 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 $client = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', '2.2.2.2', 111, 222]); + my $ok = $client->command("AUTH", "TLS")->response(); + unless ($ok == CMD_OK || $ok == CMD_MORE) { + die($client->message); + } + + my $ssl_opts = { + SSL_version => 'SSLv23', + }; + + my $ssl_client = IO::Socket::SSL->start_SSL($client, %$ssl_opts); + unless ($ssl_client) { + die("TLS handshake failed: " . IO::Socket::SSL::errstr()); + } + + push(@IO::Socket::SSL::ISA, 'Net::Cmd'); + + $ok = $ssl_client->command("USER", $setup->{user})->response(); + unless ($ok == CMD_OK || $ok == CMD_MORE) { + die($client->message); + } + + $ok = $ssl_client->command("PASS", $setup->{passwd})->response(); + unless ($ok == CMD_OK || $ok == CMD_MORE) { + die($client->message); + } + + $ok = $ssl_client->command("QUIT")->response(); + unless ($ok == CMD_OK) { + 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_login_with_proxy_useimplicitssl { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy_protocol'); + + my $server_cert_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/server-cert.pem"); + my $ca_file = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_tls/ca-cert.pem"); + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + + 'mod_tls.c' => { + TLSEngine => 'on', + TLSLog => $setup->{log_file}, + TLSProtocol => 'SSLv3 TLSv1', + TLSRequired => 'on', + TLSRSACertificateFile => $server_cert_file, + TLSCACertificateFile => $ca_file, + TLSOptions => 'UseImplicitSSL', + }, + }, + }; + + 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 $client = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port, + ['TCP4', '1.1.1.1', '2.2.2.2', 111, 222]); + + my $ssl_opts = { + SSL_version => 'SSLv23', + }; + + my $ssl_client = IO::Socket::SSL->start_SSL($client, %$ssl_opts); + unless ($ssl_client) { + die("TLS handshake failed: " . IO::Socket::SSL::errstr()); + } + + push(@IO::Socket::SSL::ISA, 'Net::Cmd'); + + my $ok = $ssl_client->response(); + unless ($ok == CMD_OK || $ok == CMD_MORE) { + die($client->message); + } + + $ok = $ssl_client->command("USER", $setup->{user})->response(); + unless ($ok == CMD_OK || $ok == CMD_MORE) { + die($client->message); + } + + $ok = $ssl_client->command("PASS", $setup->{passwd})->response(); + unless ($ok == CMD_OK || $ok == CMD_MORE) { + die($client->message); + } + + $ok = $ssl_client->command("QUIT")->response(); + unless ($ok == CMD_OK) { + 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); +} + +1; diff --git a/t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol/wrap2.pm b/t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol/wrap2.pm new file mode 100644 index 0000000..dabd588 --- /dev/null +++ b/t/lib/ProFTPD/Tests/Modules/mod_proxy_protocol/wrap2.pm @@ -0,0 +1,139 @@ +package ProFTPD::Tests::Modules::mod_proxy_protocol::wrap2; + +use lib qw(t/lib); +use base qw(ProFTPD::TestSuite::Child); +use strict; + +use File::Path qw(mkpath); +use File::Spec; +use IO::Handle; + +use ProFTPD::TestSuite::ProxiedFTP; +use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite); + +$| = 1; + +my $order = 0; + +my $TESTS = { + proxy_protocol_wrap2_config_deny => { + order => ++$order, + test_class => [qw(forking mod_proxy_protocol mod_wrap2)], + }, + +}; + +sub new { + return shift()->SUPER::new(@_); +} + +sub list_tests { + return testsuite_get_runnable_tests($TESTS); +} + +sub proxy_protocol_wrap2_config_deny { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'proxy_protocol'); + + my $allow_file = File::Spec->rel2abs("$tmpdir/wrap2.allow"); + if (open(my $fh, "> $allow_file")) { + unless (close($fh)) { + die("Can't write $allow_file: $!"); + } + + } else { + die("Can't open $allow_file: $!"); + } + + my $deny_file = File::Spec->rel2abs("$tmpdir/wrap2.deny"); + if (open(my $fh, "> $deny_file")) { + print $fh "ALL: 1.1.1.1\n"; + + unless (close($fh)) { + die("Can't write $deny_file: $!"); + } + + } else { + die("Can't open $deny_file: $!"); + } + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_proxy_protocol.c' => { + ProxyProtocolEngine => 'on', + }, + + 'mod_wrap2.c' => { + WrapEngine => 'on', + WrapTables => "file:$allow_file file:$deny_file", + WrapLog => $setup->{log_file}, + } + }, + }; + + 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; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + sleep(2); + + my $client = ProFTPD::TestSuite::ProxiedFTP->new('127.0.0.1', $port); + $client->send_proxy('1.1.1.1', '2.2.2.2', 111, 222); + eval { $client->login($setup->{user}, $setup->{passwd}) }; + unless ($@) { + die("Login 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); +} + +1; diff --git a/t/modules/mod_proxy_protocol.t b/t/modules/mod_proxy_protocol.t new file mode 100644 index 0000000..ef6b169 --- /dev/null +++ b/t/modules/mod_proxy_protocol.t @@ -0,0 +1,11 @@ +#!/usr/bin/env perl + +use lib qw(t/lib); +use strict; + +use Test::Unit::HarnessUnit; + +$| = 1; + +my $r = Test::Unit::HarnessUnit->new(); +$r->start("ProFTPD::Tests::Modules::mod_proxy_protocol"); diff --git a/t/modules/mod_proxy_protocol/sftp.t b/t/modules/mod_proxy_protocol/sftp.t new file mode 100644 index 0000000..80344c8 --- /dev/null +++ b/t/modules/mod_proxy_protocol/sftp.t @@ -0,0 +1,11 @@ +#!/usr/bin/env perl + +use lib qw(t/lib); +use strict; + +use Test::Unit::HarnessUnit; + +$| = 1; + +my $r = Test::Unit::HarnessUnit->new(); +$r->start("ProFTPD::Tests::Modules::mod_proxy_protocol::sftp"); diff --git a/t/modules/mod_proxy_protocol/tls.t b/t/modules/mod_proxy_protocol/tls.t new file mode 100644 index 0000000..784240b --- /dev/null +++ b/t/modules/mod_proxy_protocol/tls.t @@ -0,0 +1,11 @@ +#!/usr/bin/env perl + +use lib qw(t/lib); +use strict; + +use Test::Unit::HarnessUnit; + +$| = 1; + +my $r = Test::Unit::HarnessUnit->new(); +$r->start("ProFTPD::Tests::Modules::mod_proxy_protocol::tls"); diff --git a/t/modules/mod_proxy_protocol/wrap2.t b/t/modules/mod_proxy_protocol/wrap2.t new file mode 100644 index 0000000..6731759 --- /dev/null +++ b/t/modules/mod_proxy_protocol/wrap2.t @@ -0,0 +1,11 @@ +#!/usr/bin/env perl + +use lib qw(t/lib); +use strict; + +use Test::Unit::HarnessUnit; + +$| = 1; + +my $r = Test::Unit::HarnessUnit->new(); +$r->start("ProFTPD::Tests::Modules::mod_proxy_protocol::wrap2");