From 2b457cf5f6f31d0b8db7e79ea0da7c467b154823 Mon Sep 17 00:00:00 2001
From: Mario Fetka <mario@localhost.localdomain>
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 <code>haproxy</code>.
+
+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" <sp> PROTO <sp> SRC3 <sp> DST3 <sp> SRC4 <sp> <DST4> "\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 @@
+<html>
+<head>
+<title>ProFTPD module mod_proxy_protocol</title>
+</head>
+
+<body bgcolor=white>
+
+<hr>
+<center>
+<h2><b>ProFTPD module <code>mod_proxy_protocol</code></b></h2>
+</center>
+<hr><br>
+
+<p>
+The purpose of the <code>mod_proxy_protocol</code> module is to handle
+protocols which are used by proxies, <i>e.g.</i> <code>haproxy</code>, 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 <b>not</b> have such a
+mechanism, nor does SSH.
+
+<p>
+However, there <em>are</em> protocols which an provide this information without
+impacting FTP.  HAproxy's <a href="http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt"><code>PROXY</code></a> protocol is one such mechanism.  The
+<code>mod_proxy_protocol</code> 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 <i>e.g.</i> network ACLs.
+
+<p>
+This module is contained in the <code>mod_proxy_protocol.c</code> file for
+ProFTPD 1.3.<i>x</i>, and is not compiled by default.  Installation
+instructions are discussed <a href="#Installation">here</a>; detailed
+notes on best practices for using this module are <a href="#Usage">here</a>.
+
+<p>
+The most current version of <code>mod_proxy_protocol</code> can be found at:
+<pre>
+  <a href="https://github.com/Castaglia/proftpd-mod_proxy_protocol.git">https://github.com/Castaglia/proftpd-mod_proxy_protocol.git</a>
+</pre>
+
+<h2>Author</h2>
+<p>
+Please contact TJ Saunders &lt;tj <i>at</i> castaglia.org&gt; with any
+questions, concerns, or suggestions regarding this module.
+
+<h2>Directives</h2>
+<ul>
+  <li><a href="#ProxyProtocolEngine">ProxyProtocolEngine</a>
+  <li><a href="#ProxyProtocolTimeout">ProxyProtocolTimeout</a>
+  <li><a href="#ProxyProtocolVersion">ProxyProtocolVersion</a>
+</ul>
+
+<p>
+<hr>
+<h2><a name="ProxyProtocolEngine">ProxyProtocolEngine</a></h2>
+<strong>Syntax:</strong> ProxyProtocolEngine <em>on|off</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code>&lt;VirtualHost&gt;</code>, <code>&lt;Global&gt;</code><br>
+<strong>Module:</strong> mod_proxy_protocol<br>
+<strong>Compatibility:</strong> 1.3.5rc4 and later
+
+<p>
+The <code>ProxyProtocolEngine</code> directive enables the expectation
+and handling of protocols which provide information on proxied connections;
+support for these protocols is provided by <code>mod_proxy_protocol</code>.
+
+<p>
+<hr>
+<h2><a name="ProxyProtocolTimeout">ProxyProtocolTimeout</a></h2>
+<strong>Syntax:</strong> ProxyProtocolTimeout <em>seconds</em><br>
+<strong>Default:</strong> 3sec<br>
+<strong>Context:</strong> server config, <code>&lt;VirtualHost&gt;</code>, <code>&lt;Global&gt;</code><br>
+<strong>Module:</strong> mod_proxy_protocol<br>
+<strong>Compatibility:</strong> 1.3.5rc4 and later
+
+<p>
+The <code>ProxyProtocolTimeout</code> directive is used to configure the
+amount of time, in seconds, that <code>mod_proxy_protocol</code> 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.
+
+<p>
+<hr>
+<h2><a name="ProxyProtocolVersion">ProxyProtocolVersion</a></h2>
+<strong>Syntax:</strong> ProxyProtocolVersion <em>protocolVersion</em><br>
+<strong>Default:</strong> haproxyV1<br>
+<strong>Context:</strong> server config, <code>&lt;VirtualHost&gt;</code>, <code>&lt;Global&gt;</code><br>
+<strong>Module:</strong> mod_proxy_protocol<br>
+<strong>Compatibility:</strong> 1.3.5rc4 and later
+
+<p>
+The <code>ProxyProtocolVersion</code> directive is used to configure the
+protocol that <code>mod_proxy_protocol</code> expects to handle.  The
+currently supported values are:
+<ul>
+  <li><code>haproxyV1</code>
+  <li><code>haproxyV2</code>
+</ul>
+
+<p>
+<hr>
+<h2><a name="Usage">Usage</a></h2>
+
+<p>
+<b>Example Configuration</b><br>
+<pre>
+  &lt;IfModule mod_proxy_protocol.c&gt;
+    ProxyProtocolEngine on
+    ProxyProtocolTimeout 3sec
+
+    # Necessary to allow data transfers
+    AllowForeignAddress on
+  &lt;/IfModule&gt;
+</pre>
+
+<p>
+<b>Module Load Order</b><br>
+In order for <code>mod_proxy_protocol</code> to work its magic, it <b>must</b>
+the first module in line to handle the bytes coming in from the client.
+If some other module (such as <code>mod_sftp</code> or <code>mod_tls</code>)
+tries to handle the incoming bytes first, Bad Things will happen, since those
+modules will expect different protocols than the <code>PROXY</code> protocol.
+
+<p>
+For <code>mod_proxy_protocol</code> to be the first module called, it must
+the <b>last</b> module loaded.  To do this as a static module, you would
+use something like this when building proftpd:
+<pre>
+  # ./configure --with-modules=...:mod_proxy_protocol
+</pre>
+ensuring that <code>mod_proxy_protocol</code> is the <b>last</b> module in
+your <code>--with-modules</code> list.
+
+<p>
+As a shared module, configuring <code>mod_proxy_protocol</code> to be the
+last module loaded is much easier.  Your configuration will have a list
+of <code>LoadModule</code> directives; the last of which would be:
+<pre>
+  LoadModule mod_proxy_protocol.c
+</pre>
+
+<p>
+<b>Trusting Senders of Proxy Data</b><br>
+Use of these proxy protocols means changes in audit trails and/or client
+access permissions (<i>e.g.</i> different <code>mod_wrap2</code> and/or
+<code>mod_geoip</code> rules will apply).  Unscrupulous senders may try to
+actively lie to your server about the original client using these protocols.
+Thus you <b>must</b> trust the upstream machines <b>before</b> enabling the
+<code>mod_proxy_protocol</code> module.
+
+<p>
+Put another way: do <b>not</b> use the <code>mod_proxy_protocol</code> 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.  <b>Only accept
+proxy information from trusted sources.</b>
+
+<p>
+<b>Why <code>AllowForeignAddress</code> Is Needed</b><br>
+One of the consequences of allowing <code>mod_proxy_protocol</code> to change
+the remote IP address is that security checks performed on data transfers
+will cause problems.  For active data transfers (<i>i.e.</i> for clients
+which send the <code>PORT</code> or <code>EPRT</code> commands),
+<code>proftpd</code> 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:
+<pre>
+  Refused PORT 127,0,0,1,218,225 (address mismatch)
+</pre>
+and the command is rejected.
+
+<p>
+Similarly for passive data transfers (<i>i.e.</i> for clients which send the
+<code>PASV</code> or <code>EPSV</code> commands), <code>proftpd</code> requires
+that the remote IP address of the client which connects to the data transfer
+address <b>must</b> match the remote IP address of the client on the control
+connection.  If the addresses do no match, then the following is logged:
+<pre>
+  SECURITY VIOLATION: Passive connection from 127.0.0.1 rejected.
+</pre>
+and the control connection is closed.
+
+<p>
+These security measures are done to prevent abuses of FTP data transfers
+such as the <a href="http://www.proftpd.org/docs/howto/FXP.html">FTP bounce</a>
+attack.  However, the very fact that <code>mod_proxy_protocol</code> changes
+the remote IP address means that to allow data transfers when using this module,
+you need to use:
+<pre>
+  AllowForeignAddress on
+</pre>
+in the same virtual host section in which the <code>ProxyProtocolEngine</code>
+directive appears.
+
+<p>
+<hr>
+<h2><a name="Installation">Installation</a></h2>
+To install <code>mod_proxy_protocol</code>, copy the
+<code>mod_proxy_protocol.c</code> file into:
+<pre>
+  <i>proftpd-dir</i>/contrib/
+</pre>
+after unpacking the latest proftpd-1.3.<i>x</i> source code.  For including
+<code>mod_proxy_protocol</code> as a staticly linked module:
+<pre>
+  $ ./configure --with-modules=...:mod_proxy_protocol
+</pre>
+To build <code>mod_proxy_protocol</code> as a DSO module:
+<pre>
+  $ ./configure --enable-dso --with-shared=...:mod_proxy_protocol
+</pre>
+Then follow the usual steps:
+<pre>
+  $ make
+  $ make install
+</pre>
+
+<p>
+For those with an existing ProFTPD installation, you can use the
+<code>prxs</code> tool to add <code>mod_proxy_protocol</code>, as a DSO module,
+to your existing server:
+<pre>
+  $ prxs -c -i -d mod_proxy_protocol.c
+</pre>
+
+<p>
+<hr>
+<font size=2><b><i>
+&copy; Copyright 2013-2017 TJ Saunders<br>
+ All Rights Reserved<br>
+</i></b></font>
+
+<hr>
+</body>
+</html>
+
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 <<EOC;
+<Class test>
+  From 1.1.1.1
+</Class>
+
+<Limit LOGIN>
+  DenyClass test
+</Limit>
+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");