diff --git a/CMakeLists.txt b/CMakeLists.txt
index ad620fa..67cfd08 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -490,6 +490,8 @@
     src/openvpn/proto.h
     src/openvpn/proxy.c
     src/openvpn/proxy.h
+    src/openvpn/proxy_protocol.c
+    src/openvpn/proxy_protocol.h
     src/openvpn/ps.c
     src/openvpn/ps.h
     src/openvpn/push.c
diff --git a/doc/proxy-protocol.txt b/doc/proxy-protocol.txt
new file mode 100644
index 0000000..bd9b606
--- /dev/null
+++ b/doc/proxy-protocol.txt
@@ -0,0 +1,157 @@
+HAProxy PROXY protocol support for OpenVPN
+==========================================
+
+The PROXY protocol is a simple protocol that can be used to retreive the
+original source address of a connection that has been proxied. Every connection
+is prepended with a header reporting the client IP address and port. The
+protocol specification is available at:
+https://www.haproxy.org/download/2.4/doc/proxy-protocol.txt
+
+Currently there are two versions of the protocol, a text based version (v1) and
+a binary version (v2).
+
+Version 1
+---------
+
+The text based version is a human readable protocol that is easy to debug. It
+consists of a single line of text with the following fields:
+
+PROXY_SIGNATURE_V1 + single space + INET_PROTOCOL + single space + CLIENT_IP +
+single space + PROXY_IP + single space + CLIENT_PORT + single space +
+PROXY_PORT + "\r\n"
+
+The only supported INET_PROTOCOL in version 1 are "TCP4" and "TCP6".
+
+Version 2
+---------
+
+The binary version is a more efficient protocol that is more suitable for
+production use. It consists of a fixed length header followed by the original
+connection data.
+
+A PROXY Protocol binary header for a IPv4 incoming address has the format:
+ 0               1               2               3
+ 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|                                                               |
++                                                               +
+|                  Proxy Protocol v2 Signature                  |
++                                                               +
+|                                                               |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|Version|Command|   AF  | Proto.|         Address Length        |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|                      IPv4 Source Address                      |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|                    IPv4 Destination Address                   |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|          Source Port          |        Destination Port       |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|                                                               |
++                                                               +
++                              TLVs                             +
++                                                               +
+|                                                               |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+A PROXY Protocol binary header for a IPv6 incoming address has the format:
+ 0               1               2               3
+ 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|                                                               |
++                                                               +
+|                  Proxy Protocol v2 Signature                  |
++                                                               +
+|                                                               |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|Version|Command|   AF  | Proto.|         Address Length        |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|                                                               |
++                                                               +
+|                                                               |
++                      IPv6 Source Address                      +
+|                                                               |
++                                                               +
+|                                                               |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|                                                               |
++                                                               +
+|                                                               |
++                    IPv6 Destination Address                   +
+|                                                               |
++                                                               +
+|                                                               |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|          Source Port          |        Destination Port       |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|                                                               |
++                                                               +
++                              TLVs                             +
++                                                               +
+|                                                               |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+The fields are as follows:
+
+- Proxy Protocol v2 Signature: 12 bytes
+  The v2 signature of the protocol: "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A\x20".
+
+- Version: 4 bits
+  The version of the PROXY protocol (0x2).
+
+- Command: 4 bits
+  The command of the PROXY protocol can by one of the following:
+    - LOCAL: 0x0
+      The connection was initiated by the proxy.
+    - PROXY: 0x1
+      The connection was initiated by the client.
+
+- AF: 4 bits
+    The address family of the connection can be: AF_UNSPEC (0x0), AF_INET (0x1),
+    AF_INET6 (0x2) or AF_UNIX (0x3).
+
+- Proto: 4 bits
+    The protocol of the connection can be: UNSPEC (0x0), STREAM (0x1) or
+    DGRAM (0x2).
+
+- Address Length: 16 bits
+    The length of the rest of the header in bytes.
+
+Finally there may be a series of TLVs (Type-Length-Value) that can be used to
+carry additional information.
+
+OpenVPN testing
+---------------
+
+Currently only TCP is supported. To test the PROXY protocol support in OpenVPN,
+you can use HAProxy as a proxy with a configuration like this:
+
+global
+    log stdout format raw local0
+    log 127.0.0.1 local0 debug
+    daemon
+
+defaults
+    log     global
+    mode    tcp
+    option  tcplog
+    timeout connect 5000ms
+    timeout client  50000ms
+    timeout server  50000ms
+
+frontend openvpn_front
+    bind 10.10.20.1:1195
+    default_backend openvpn_back
+
+backend openvpn_back
+    server openvpn_server 10.10.10.1:1194 send-proxy
+    # server openvpn_server 10.10.10.1:1194 send-proxy-v2 proxy-v2-options ssl,cert-cn,ssl-cipher,cert-sig,cert-key,authority,crc32c,unique-id
+
+
+You can use the commented line to test the binary format and the TLV data.
+
+Note that you should set the keepalive option so that the connection is not
+reset by HAProxy for inactivity.
+
+The OpenVPN TCP server should read and parse the header and use the information
+to display the original source address in the logs.
diff --git a/src/openvpn/Makefile.am b/src/openvpn/Makefile.am
index 56cce9d..fc037ca 100644
--- a/src/openvpn/Makefile.am
+++ b/src/openvpn/Makefile.am
@@ -111,6 +111,7 @@
 	pool.c pool.h \
 	proto.c proto.h \
 	proxy.c proxy.h \
+	proxy_protocol.c proxy_protocol.h \
 	ps.c ps.h \
 	push.c push.h \
 	pushlist.h \
diff --git a/src/openvpn/init.c b/src/openvpn/init.c
index a49e563..cbce217 100644
--- a/src/openvpn/init.c
+++ b/src/openvpn/init.c
@@ -54,6 +54,7 @@
 #include "mss.h"
 #include "mudp.h"
 #include "dco.h"
+#include "proxy_protocol.h"
 
 #include "memdbg.h"
 
@@ -680,6 +681,11 @@
         c->c1.socks_proxy = NULL;
         c->c1.socks_proxy_owned = false;
     }
+    if (c->c1.proxy_protocol)
+    {
+        proxy_protocol_free(c->c1.proxy_protocol);
+        c->c1.proxy_protocol = NULL;
+    }
 }
 
 static void
@@ -4774,7 +4780,7 @@
         /* free up environmental variable store */
         do_env_set_destroy(c);
 
-        /* close HTTP or SOCKS proxy */
+        /* close HTTP or SOCKS proxy or PROXY protocol object */
         uninit_proxy(c);
 
         /* garbage collect */
diff --git a/src/openvpn/multi.c b/src/openvpn/multi.c
index 03177bb..ac4652d 100644
--- a/src/openvpn/multi.c
+++ b/src/openvpn/multi.c
@@ -51,6 +51,7 @@
 #include "ssl_util.h"
 #include "dco.h"
 #include "reflect_filter.h"
+#include "proxy_protocol.h"
 
 /*#define MULTI_DEBUG_EVENT_LOOP*/
 
@@ -779,6 +780,8 @@
         goto err;
     }
 
+    ALLOC_OBJ_CLEAR(mi->context.c1.proxy_protocol, struct proxy_protocol_info);
+
     mi->context.c2.tls_multi->multi_state = CAS_NOT_CONNECTED;
 
     if (hash_n_elements(m->hash) >= m->max_clients)
@@ -3207,6 +3210,57 @@
     gc_free(&gc);
 }
 
+void
+multi_replace_proxy_addr(struct multi_context *m, struct multi_instance *mi)
+{
+    struct mroute_addr real = {0};
+    struct hash *hash = m->hash;
+    struct gc_arena gc = gc_new();
+
+    if (!mroute_extract_openvpn_sockaddr(&real, &mi->context.c1.proxy_protocol->src, true))
+    {
+        goto done;
+    }
+
+    const uint32_t hv = hash_value(hash, &real);
+    struct hash_bucket *bucket = hash_bucket(hash, hv);
+
+    /* if the new address is already in the hash table, the new client takes precedence */
+    struct hash_element *he = hash_lookup_fast(hash, bucket, &real, hv);
+    if (he)
+    {
+        struct multi_instance *oldmi = (struct multi_instance *) he->value;
+        msg(D_MULTI_LOW, "Client's real address (behind proxy) matches "
+            "existing client address -- new client takes precedence");
+        oldmi->did_real_hash = false;
+        multi_close_instance(m, oldmi, false);
+        he->key = &mi->real;
+        he->value = mi;
+    }
+
+    clear_prefix();
+    msg(D_MULTI_LOW, "Replacing proxy address: %s with client's real address: %s",
+        mroute_addr_print(&mi->real, &gc),
+        mroute_addr_print(&real, &gc));
+
+    /* remove proxy address from hash table before changing address */
+    ASSERT(hash_remove(m->hash, &mi->real));
+    ASSERT(hash_remove(m->iter, &mi->real));
+
+    mi->real = real;
+    generate_prefix(mi);
+
+    ASSERT(hash_add(m->hash, &mi->real, mi, false));
+    ASSERT(hash_add(m->iter, &mi->real, mi, false));
+
+#ifdef ENABLE_MANAGEMENT
+    ASSERT(hash_add(m->cid_hash, &mi->context.c2.mda_context.cid, mi, true));
+#endif
+
+done:
+    gc_free(&gc);
+}
+
 /*
  * Called when an instance should be closed due to the
  * reception of a soft signal.
@@ -3348,6 +3402,7 @@
     struct mroute_addr src, dest;
     unsigned int mroute_flags;
     struct multi_instance *mi;
+    struct link_socket_info *lsi;
     bool ret = true;
     bool floated = false;
 
@@ -3387,15 +3442,31 @@
             }
         }
 
+        lsi = get_link_socket_info(c);
+
+        /* check for PROXY protocol */
+        if (lsi->proto == PROTO_TCP_SERVER && !c->c1.proxy_protocol->version && BLEN(&c->c2.buf) > 0)
+        {
+            if (!proxy_protocol_parse(c->c1.proxy_protocol, &c->c2.buf))
+            {
+                /* set version to invalid to avoid checking again */
+                c->c1.proxy_protocol->version = PROXY_PROTOCOL_VERSION_INVALID;
+            }
+            else
+            {
+                multi_replace_proxy_addr(m, m->pending);
+                /* avoid processing the packet as an openvpn one */
+                buf_reset_len(&c->c2.buf);
+            }
+        }
+
         if (BLEN(&c->c2.buf) > 0)
         {
-            struct link_socket_info *lsi;
             const uint8_t *orig_buf;
 
             /* decrypt in instance context */
 
             perf_push(PERF_PROC_IN_LINK);
-            lsi = get_link_socket_info(c);
             orig_buf = c->c2.buf.data;
             if (process_incoming_link_part1(c, lsi, floated))
             {
diff --git a/src/openvpn/openvpn.h b/src/openvpn/openvpn.h
index e98c93e..ded6103 100644
--- a/src/openvpn/openvpn.h
+++ b/src/openvpn/openvpn.h
@@ -45,6 +45,7 @@
 #include "pool.h"
 #include "plugin.h"
 #include "manage.h"
+#include "proxy_protocol.h"
 
 /*
  * Our global key schedules, packaged thusly
@@ -191,6 +192,8 @@
     struct socks_proxy_info *socks_proxy;
     bool socks_proxy_owned;
 
+    struct proxy_protocol_info *proxy_protocol;
+
     /* persist --ifconfig-pool db to file */
     struct ifconfig_pool_persist *ifconfig_pool_persist;
     bool ifconfig_pool_persist_owned;
diff --git a/src/openvpn/proxy_protocol.c b/src/openvpn/proxy_protocol.c
new file mode 100644
index 0000000..10a68c2
--- /dev/null
+++ b/src/openvpn/proxy_protocol.c
@@ -0,0 +1,472 @@
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single TCP/UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2002-2024 OpenVPN Inc <sales@openvpn.net>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License version 2
+ *  as published by the Free Software Foundation.
+ *
+ *  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, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "error.h"
+#include "proxy_protocol.h"
+
+#define PROXY_PROTOCOL_V1_MAX_WORDS 6
+#define PROXY_PROTOCOL_V1_MAX_WORD_LEN 40
+
+typedef enum
+{
+    PROXY_PROTOCOL_PARSING_STATE_INVALID = -1,
+    PROXY_PROTOCOL_PARSING_STATE_OK = 0,
+    PROXY_PROTOCOL_PARSING_STATE_IGNORE = 1,
+} proxy_protocol_parsing_state_t;
+
+static const size_t PROXY_PROTOCOL_V2_ADDR_LEN_IPV4 = 12;
+static const size_t PROXY_PROTOCOL_V2_ADDR_LEN_IPV6 = 36;
+static const size_t PROXY_PROTOCOL_V2_ADDR_LEN_UNIX = 216;
+
+static proxy_protocol_header_t header;
+static uint16_t header_len;
+static proxy_protocol_parsing_state_t parsing_state = PROXY_PROTOCOL_PARSING_STATE_OK;
+
+proxy_protocol_version_t
+proxy_protocol_version(const uint8_t *buf, const int buf_len)
+{
+    if (buf_len >= PROXY_PROTOCOL_V2_MIN_HDR_LEN
+        && memcmp(buf, PROXY_PROTOCOL_V2_SIG, PROXY_PROTOCOL_V2_SIG_LEN) == 0)
+    {
+        return PROXY_PROTOCOL_VERSION_2;
+    }
+    else if (buf_len >= PROXY_PROTOCOL_V1_MIN_HDR_LEN
+             && memcmp(buf, PROXY_PROTOCOL_V1_SIG, PROXY_PROTOCOL_V1_SIG_LEN) == 0)
+    {
+        return PROXY_PROTOCOL_VERSION_1;
+    }
+    return PROXY_PROTOCOL_VERSION_INVALID;
+}
+
+int
+proxy_protocol_header_len(const uint8_t *buf, const int buf_len,
+                          const proxy_protocol_version_t version)
+{
+    memcpy(&header, buf, buf_len);
+
+    switch (version)
+    {
+        case PROXY_PROTOCOL_VERSION_1:
+        {
+            char *end = memchr(header.v1.line, '\r', buf_len - 1);
+            if (!end || end[1] != '\n')
+            {
+                return -1; /* partial or invalid header */
+            }
+            return (int)(end + 2 - header.v1.line);
+        }
+
+        case PROXY_PROTOCOL_VERSION_2:
+            return PROXY_PROTOCOL_V2_MIN_HDR_LEN + ntohps(header.v2.len);
+
+        default:
+            return -1;
+    }
+}
+
+/*
+ * Parse a port number from a string.
+ *
+ * @param port_str - The string to parse.
+ *
+ * @return - The port number or -1 if the string is invalid.
+ */
+uint16_t
+proxy_protocol_parse_port(const char *port_str)
+{
+    char *endptr;
+    errno = 0;
+
+    long port = strtol(port_str, &endptr, 10);
+    if (errno != 0 || endptr == port_str || *endptr != '\0' || port <= 0 || port > 65535)
+    {
+        return -1;
+    }
+    return (uint16_t)port;
+}
+
+/*
+ * Set the source and destination addresses and ports in the proxy protocol
+ * info structure for later use.
+ *
+ * @param ppi - The proxy protocol info structure to store the addresses.
+ * @param ver - The version of the PROXY protocol.
+ * @param fam - The address family (supports: AF_INET or AF_INET6).
+ * @param st - The socket type.
+ * @param src_addr - The source address.
+ * @param dst_addr - The destination address.
+ * @param src_port - The source port.
+ * @param dst_port - The destination port.
+ */
+void
+proxy_protocol_set_addr(struct proxy_protocol_info *ppi,
+                        const proxy_protocol_version_t ver, const int fam, const int st,
+                        const void *src_addr, const void *dst_addr,
+                        const uint16_t src_port, const uint16_t dst_port)
+{
+
+    ppi->version = ver;
+    ppi->sock_type = st;
+    if (fam == AF_INET)
+    {
+        ppi->src.addr.sa.sa_family = AF_INET;
+        ppi->dst.addr.sa.sa_family = AF_INET;
+        memcpy(&ppi->src.addr.in4.sin_addr, (uint32_t *)src_addr,
+               sizeof(struct in_addr));
+        memcpy(&ppi->dst.addr.in4.sin_addr, (uint32_t *)dst_addr,
+               sizeof(struct in_addr));
+        ppi->src.addr.in4.sin_port = src_port;
+        ppi->dst.addr.in4.sin_port = dst_port;
+        msg(M_DEBUG, "PROXY protocol: SRC: %s:%u",
+            inet_ntoa(ppi->src.addr.in4.sin_addr),
+            ntohs(ppi->src.addr.in4.sin_port));
+        msg(M_DEBUG, "PROXY protocol: DST: %s:%u",
+            inet_ntoa(ppi->dst.addr.in4.sin_addr),
+            ntohs(ppi->dst.addr.in4.sin_port));
+    }
+    else if (fam == AF_INET6)
+    {
+        ppi->src.addr.sa.sa_family = AF_INET6;
+        ppi->dst.addr.sa.sa_family = AF_INET6;
+        memcpy(&ppi->src.addr.in6.sin6_addr, src_addr, sizeof(struct in6_addr));
+        memcpy(&ppi->dst.addr.in6.sin6_addr, dst_addr, sizeof(struct in6_addr));
+        ppi->src.addr.in6.sin6_port = src_port;
+        ppi->dst.addr.in6.sin6_port = dst_port;
+
+        char ip6_str[INET6_ADDRSTRLEN];
+        if (inet_ntop(AF_INET6, &ppi->src.addr.in6.sin6_addr, ip6_str,
+                      INET6_ADDRSTRLEN))
+        {
+            msg(M_DEBUG, "PROXY protocol: SRC: %s:%u", ip6_str,
+                ntohs(ppi->src.addr.in6.sin6_port));
+        }
+        else
+        {
+            msg(M_NONFATAL, "PROXY protocol: could not parse source address");
+        }
+        if (inet_ntop(AF_INET6, &ppi->dst.addr.in6.sin6_addr, ip6_str,
+                      INET6_ADDRSTRLEN))
+        {
+            msg(M_DEBUG, "PROXY protocol: DST: %s:%u", ip6_str,
+                ntohs(ppi->dst.addr.in6.sin6_port));
+        }
+        else
+        {
+            msg(M_NONFATAL,
+                "PROXY protocol: could not parse destination address");
+        }
+    }
+    else
+    {
+        msg(M_NONFATAL, "PROXY protocol: unsupported address family");
+    }
+}
+
+/*
+ * Split the v1 header line into words (space-separated)
+ *
+ * @param words - The array to store the words.
+ * @param line - The header line to split.
+ * @param len - The length of the header line.
+ *
+ * @return - The number of words found or -1 if an error occurred.
+ */
+int
+proxy_protocol_v1_split_words(char words[][PROXY_PROTOCOL_V1_MAX_WORD_LEN],
+                              const char *line,
+                              int len)
+{
+    int word_num = 0;
+    const char *start = line;
+    for (int i = 0; i < len; ++i)
+    {
+        if (line[i] == ' ')
+        {
+            if (word_num < PROXY_PROTOCOL_V1_MAX_WORDS)
+            {
+                int word_len = (int)(line + i - start);
+                if (word_len < PROXY_PROTOCOL_V1_MAX_WORD_LEN)
+                {
+                    memcpy(words[word_num], start, word_len);
+                    words[word_num][word_len] = '\0';
+                    ++word_num;
+                }
+                else
+                {
+                    msg(M_NONFATAL, "PROXY protocol v1: word too long");
+                    return -1;
+                }
+            }
+            start = line + i + 1;
+        }
+    }
+    return word_num;
+}
+
+/*
+ * Parse the PROXY protocol v1 header line.
+ *
+ * @param ppi - The proxy protocol info structure to store the parsed data.
+ * @param line - The header line to parse.
+ * @param len - The length of the header line.
+ *
+ * @return - true if the header was successfully parsed.
+ */
+bool
+proxy_protocol_parse_v1(struct proxy_protocol_info *ppi,
+                        const char *line,
+                        int len)
+{
+    const proxy_protocol_version_t version = PROXY_PROTOCOL_VERSION_1;
+    char words[PROXY_PROTOCOL_V1_MAX_WORDS][PROXY_PROTOCOL_V1_MAX_WORD_LEN];
+
+    char *end = memchr(line, '\r', len - 1);
+    if (!end || end[1] != '\n') /* partial or invalid header */
+    {
+        return false;
+    }
+    *end = ' '; /* replace CRLF with space for easier splitting */
+    int size = (int)(end - line + 1);
+
+    int word_num = proxy_protocol_v1_split_words(words, line, size);
+    if (word_num < 3)
+    {
+        msg(M_NONFATAL, "PROXY protocol v1: could not split header");
+        return false;
+    }
+
+    if (strcmp(words[1], "UNKNOWN") == 0)
+    {
+        msg(M_DEBUG, "PROXY protocol v1: UNKNOWN protocol, ignoring header");
+        return true;
+    }
+    else if (strcmp(words[1], "TCP4") == 0)
+    {
+        msg(M_DEBUG, "PROXY protocol v1: TCP4 protocol");
+        struct in_addr ip4_src_addr, ip4_dst_addr;
+        if (inet_pton(AF_INET, words[2], &ip4_src_addr) != 1)
+        {
+            msg(M_NONFATAL,
+                "PROXY protocol v1: could not parse source address");
+            return false;
+        }
+        if (inet_pton(AF_INET, words[3], &ip4_dst_addr) != 1)
+        {
+            msg(M_NONFATAL,
+                "PROXY protocol v1: could not parse destination address");
+            return false;
+        }
+        proxy_protocol_set_addr(ppi, version, AF_INET, SOCK_STREAM,
+                                &ip4_src_addr, &ip4_dst_addr,
+                                ntohs(proxy_protocol_parse_port(words[4])),
+                                ntohs(proxy_protocol_parse_port(words[5])));
+        return true;
+    }
+    else if (strcmp(words[1], "TCP6") == 0)
+    {
+        msg(M_DEBUG, "PROXY protocol v1: TCP6 protocol");
+        struct in6_addr ip6_src_addr, ip6_dst_addr;
+        if (inet_pton(AF_INET6, words[2], &ip6_src_addr) != 1)
+        {
+            msg(M_NONFATAL,
+                "PROXY protocol v1: could not parse source address");
+            return false;
+        }
+        if (inet_pton(AF_INET6, words[3], &ip6_dst_addr) != 1)
+        {
+            msg(M_NONFATAL,
+                "PROXY protocol v1: could not parse destination address");
+            return false;
+        }
+        proxy_protocol_set_addr(ppi, version, AF_INET6, SOCK_STREAM,
+                                &ip6_src_addr, &ip6_dst_addr,
+                                ntohs(proxy_protocol_parse_port(words[4])),
+                                ntohs(proxy_protocol_parse_port(words[5])));
+        return true;
+    }
+    else
+    {
+        msg(M_NONFATAL, "PROXY protocol v1: unsupported protocol");
+        return false;
+    }
+}
+
+/*
+ * Parse the PROXY protocol v2 header.
+ *
+ * @param ppi - The proxy protocol info structure to store the parsed data.
+ * @param header - The header to parse.
+ *
+ * @return - The number of bytes parsed or 0 if the header can be ignored.
+ */
+int
+proxy_protocol_parse_v2(struct proxy_protocol_info *ppi)
+{
+    size_t addr_len = 0;
+    int pos = PROXY_PROTOCOL_V2_SIG_LEN + sizeof(header.v2.ver_cmd);
+    const proxy_protocol_version_t version = PROXY_PROTOCOL_VERSION_2;
+
+    switch (header.v2.ver_cmd & PROXY_PROTOCOL_V2_CMD_MASK)
+    {
+        case PROXY_PROTOCOL_V2_LOCAL_CMD:
+            /* the receiver must accept this connection as valid and must use
+             * the real connection endpoints and discard the protocol block
+             * including the family which is ignored */
+            msg(M_DEBUG, "PROXY protocol v2: LOCAL command");
+            parsing_state = PROXY_PROTOCOL_PARSING_STATE_IGNORE;
+            break;
+
+        case PROXY_PROTOCOL_V2_PROXY_CMD:
+            msg(M_DEBUG, "PROXY protocol v2: PROXY command");
+            break;
+
+        default:
+            msg(M_DEBUG, "PROXY protocol v2: UNSPEC or unknown command");
+            parsing_state = PROXY_PROTOCOL_PARSING_STATE_INVALID;
+    }
+
+    if (parsing_state != PROXY_PROTOCOL_PARSING_STATE_OK)
+    {
+        return pos;
+    }
+    pos += sizeof(header.v2.fam);
+
+    switch (header.v2.fam)
+    {
+        case PROXY_PROTOCOL_V2_AF_INET | PROXY_PROTOCOL_V2_TP_STREAM:
+            msg(M_DEBUG, "PROXY protocol v2: TCP over IPv4.");
+            proxy_protocol_set_addr(ppi, version, AF_INET, SOCK_STREAM,
+                                    &header.v2.addr.ip4.src_addr,
+                                    &header.v2.addr.ip4.dst_addr,
+                                    header.v2.addr.ip4.src_port,
+                                    header.v2.addr.ip4.dst_port);
+            addr_len = PROXY_PROTOCOL_V2_ADDR_LEN_IPV4;
+            break;
+
+        case PROXY_PROTOCOL_V2_AF_INET | PROXY_PROTOCOL_V2_TP_DGRAM:
+            msg(M_DEBUG, "PROXY protocol v2: UDP over IPv4");
+            proxy_protocol_set_addr(ppi, version, AF_INET, SOCK_DGRAM,
+                                    &header.v2.addr.ip4.src_addr,
+                                    &header.v2.addr.ip4.dst_addr,
+                                    header.v2.addr.ip4.src_port,
+                                    header.v2.addr.ip4.dst_port);
+            addr_len = PROXY_PROTOCOL_V2_ADDR_LEN_IPV4;
+            break;
+
+        case PROXY_PROTOCOL_V2_AF_INET6 | PROXY_PROTOCOL_V2_TP_STREAM:
+            msg(M_DEBUG, "PROXY protocol v2: TCP over IPv6");
+            proxy_protocol_set_addr(ppi, version, AF_INET6, SOCK_STREAM,
+                                    header.v2.addr.ip6.src_addr,
+                                    header.v2.addr.ip6.dst_addr,
+                                    header.v2.addr.ip6.src_port,
+                                    header.v2.addr.ip6.dst_port);
+            addr_len = PROXY_PROTOCOL_V2_ADDR_LEN_IPV6;
+            break;
+
+        case PROXY_PROTOCOL_V2_AF_INET6 | PROXY_PROTOCOL_V2_TP_DGRAM:
+            msg(M_DEBUG, "PROXY protocol v2: UDP over IPv6");
+            proxy_protocol_set_addr(ppi, version, AF_INET6, SOCK_DGRAM,
+                                    header.v2.addr.ip6.src_addr,
+                                    header.v2.addr.ip6.dst_addr,
+                                    header.v2.addr.ip6.src_port,
+                                    header.v2.addr.ip6.dst_port);
+            addr_len = PROXY_PROTOCOL_V2_ADDR_LEN_IPV6;
+            break;
+
+        case PROXY_PROTOCOL_V2_AF_UNIX | PROXY_PROTOCOL_V2_TP_STREAM:
+            msg(M_DEBUG, "PROXY protocol v2: AF_UNIX stream");
+            proxy_protocol_set_addr(ppi, version, AF_UNIX, SOCK_STREAM,
+                                    header.v2.addr.unx.src_addr,
+                                    header.v2.addr.unx.dst_addr, 0, 0);
+            addr_len = PROXY_PROTOCOL_V2_ADDR_LEN_UNIX;
+            break;
+
+        case PROXY_PROTOCOL_V2_AF_UNIX | PROXY_PROTOCOL_V2_TP_DGRAM:
+            msg(M_DEBUG, "PROXY protocol v2: AF_UNIX datagram");
+            proxy_protocol_set_addr(ppi, version, AF_UNIX, SOCK_DGRAM,
+                                    header.v2.addr.unx.src_addr,
+                                    header.v2.addr.unx.dst_addr, 0, 0);
+            addr_len = PROXY_PROTOCOL_V2_ADDR_LEN_UNIX;
+            break;
+
+        default:
+            msg(M_DEBUG, "PROXY protocol v2: UNSPEC or unknown address family");
+            parsing_state = PROXY_PROTOCOL_PARSING_STATE_INVALID;
+            break;
+    }
+    return (int)(pos + sizeof(header.v2.len) + addr_len);
+}
+
+bool
+proxy_protocol_parse(struct proxy_protocol_info *ppi, const struct buffer *buf)
+{
+    const proxy_protocol_version_t version = proxy_protocol_version(BPTR(buf), BLEN(buf));
+
+    header_len = BLEN(buf);
+    memcpy(&header, BPTR(buf), header_len);
+
+    switch (version)
+    {
+        case PROXY_PROTOCOL_VERSION_2:
+        {
+            if ((header.v2.ver_cmd & PROXY_PROTOCOL_V2_VER_MASK) == PROXY_PROTOCOL_V2_VER)
+            {
+                proxy_protocol_parse_v2(ppi);
+                if (parsing_state == PROXY_PROTOCOL_PARSING_STATE_IGNORE)
+                {
+                    msg(M_DEBUG, "PROXY protocol v2: ignoring header");
+                    return true;
+                }
+                else if (parsing_state == PROXY_PROTOCOL_PARSING_STATE_INVALID)
+                {
+                    return false;
+                }
+                msg(M_DEBUG, "PROXY protocol v2: header parsed");
+                return true;
+            }
+            else
+            {
+                msg(M_NONFATAL, "PROXY protocol v2: expected version 2, got %d",
+                    header.v2.ver_cmd & PROXY_PROTOCOL_V2_VER_MASK);
+                return false;
+            }
+        }
+
+        case PROXY_PROTOCOL_VERSION_1:
+            msg(M_DEBUG, "PROXY protocol header v1: %.*s", BLEN(buf) - 2, header.v1.line);
+            return proxy_protocol_parse_v1(ppi, header.v1.line, BLEN(buf));
+
+        default:
+            return false;
+    }
+}
+
+void
+proxy_protocol_free(struct proxy_protocol_info *ppi)
+{
+    gc_free(&ppi->gc);
+}
diff --git a/src/openvpn/proxy_protocol.h b/src/openvpn/proxy_protocol.h
new file mode 100644
index 0000000..8e9af03
--- /dev/null
+++ b/src/openvpn/proxy_protocol.h
@@ -0,0 +1,165 @@
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single TCP/UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2002-2024 OpenVPN Inc <sales@openvpn.net>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License version 2
+ *  as published by the Free Software Foundation.
+ *
+ *  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, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef PROXY_PROTOCOL_H
+#define PROXY_PROTOCOL_H
+
+#include "buffer.h"
+#include "openvpn.h"
+#include "socket.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+
+/* https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt */
+
+#define PROXY_PROTOCOL_V1_SIG          "PROXY"
+#define PROXY_PROTOCOL_V1_SIG_LEN      5
+#define PROXY_PROTOCOL_V1_MIN_HDR_LEN  8
+#define PROXY_PROTOCOL_V1_LINE_MAX_LEN 108
+
+#define PROXY_PROTOCOL_V2_SIG         "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"
+#define PROXY_PROTOCOL_V2_SIG_LEN     12
+#define PROXY_PROTOCOL_V2_MIN_HDR_LEN 16
+
+#define PROXY_PROTOCOL_V2_VER_MASK 0xF0
+#define PROXY_PROTOCOL_V2_VER      (0x2 << 4)
+
+#define PROXY_PROTOCOL_V2_CMD_MASK  0x0F
+#define PROXY_PROTOCOL_V2_LOCAL_CMD (0x0 << 0)
+#define PROXY_PROTOCOL_V2_PROXY_CMD (0x1 << 0)
+
+#define PROXY_PROTOCOL_V2_AF_MASK   0xF0
+#define PROXY_PROTOCOL_V2_AF_UNSPEC (0x0 << 4)
+#define PROXY_PROTOCOL_V2_AF_INET   (0x1 << 4)
+#define PROXY_PROTOCOL_V2_AF_INET6  (0x2 << 4)
+#define PROXY_PROTOCOL_V2_AF_UNIX   (0x3 << 4)
+
+#define PROXY_PROTOCOL_V2_TP_MASK   0x0F
+#define PROXY_PROTOCOL_V2_TP_UNSPEC (0x0 << 0)
+#define PROXY_PROTOCOL_V2_TP_STREAM (0x1 << 0)
+#define PROXY_PROTOCOL_V2_TP_DGRAM  (0x2 << 0)
+
+typedef enum
+{
+    PROXY_PROTOCOL_VERSION_INVALID = -1,
+
+    PROXY_PROTOCOL_VERSION_UNKNOWN = 0,
+    PROXY_PROTOCOL_VERSION_1,
+    PROXY_PROTOCOL_VERSION_2,
+} proxy_protocol_version_t;
+
+/* HAProxy PROXY protocol header */
+typedef union
+{
+    struct
+    {
+        char line[PROXY_PROTOCOL_V1_LINE_MAX_LEN];
+    } v1;
+    struct
+    {
+        uint8_t sig[PROXY_PROTOCOL_V2_SIG_LEN];
+        uint8_t ver_cmd;
+        uint8_t fam;
+        uint16_t len;
+        union
+        {
+            struct
+            {
+                uint32_t src_addr;
+                uint32_t dst_addr;
+                uint16_t src_port;
+                uint16_t dst_port;
+            } ip4;
+            struct
+            {
+                uint8_t src_addr[16];
+                uint8_t dst_addr[16];
+                uint16_t src_port;
+                uint16_t dst_port;
+            } ip6;
+            struct
+            {
+                uint8_t src_addr[108];
+                uint8_t dst_addr[108];
+            } unx;
+        } addr;
+    } v2;
+} proxy_protocol_header_t;
+
+struct proxy_protocol_info
+{
+    struct gc_arena gc;
+
+    proxy_protocol_version_t version;
+    int sock_type;
+    struct openvpn_sockaddr src;
+    struct openvpn_sockaddr dst;
+};
+
+/*
+ * Check if the buffer contains a PROXY protocol header and return the version.
+ *
+ * @param buf - The buffer to check.
+ * @param buf_len - The length of the buffer.
+ *
+ * @return - The PROXY protocol version or PROXY_PROTOCOL_VERSION_INVALID if
+ *          the buffer does not contain a valid PROXY protocol header.
+ */
+proxy_protocol_version_t
+proxy_protocol_version(const uint8_t *buf, int buf_len);
+
+/*
+ * Get the length of the PROXY protocol header.
+ *
+ * @param buf - The buffer to check.
+ * @param buf_len - The length of the buffer.
+ * @param version - The PROXY protocol version.
+ *
+ * @return - The length of the header or -1 if the header is partial or invalid.
+ */
+int
+proxy_protocol_header_len(const uint8_t *buf, int len,
+                          proxy_protocol_version_t version);
+
+/*
+ * Parse the PROXY protocol header.
+ *
+ * @param ppi - The proxy protocol info structure to store the parsed data.
+ * @param buf - The buffer containing the header.
+ * @param version - The version of the PROXY protocol.
+ *
+ * @return - true if the header was successfully parsed.
+ */
+bool
+proxy_protocol_parse(struct proxy_protocol_info *ppi, const struct buffer *buf);
+
+/*
+ * Free the allocated memory.
+ *
+ * @param ppi - The proxy protocol info structure.
+ */
+void
+proxy_protocol_free(struct proxy_protocol_info *ppi);
+
+#endif /* PROXY_PROTOCOL_H */
diff --git a/src/openvpn/socket.c b/src/openvpn/socket.c
index 17c5e76..7fb382c 100644
--- a/src/openvpn/socket.c
+++ b/src/openvpn/socket.c
@@ -39,6 +39,7 @@
 #include "manage.h"
 #include "openvpn.h"
 #include "forward.h"
+#include "proxy_protocol.h"
 
 #include "memdbg.h"
 
@@ -2680,10 +2681,20 @@
 
         if (sb->len < 1 || sb->len > sb->maxlen)
         {
-            msg(M_WARN, "WARNING: Bad encapsulated packet length from peer (%d), which must be > 0 and <= %d -- please ensure that --tun-mtu or --link-mtu is equal on both peers -- this condition could also indicate a possible active attack on the TCP link -- [Attempting restart...]", sb->len, sb->maxlen);
-            stream_buf_reset(sb);
-            sb->error = true;
-            return false;
+            /* check if it's a PROXY protocol header */
+
+            /* undo the reading of net_size */
+            ASSERT(buf_prepend(&sb->buf, sizeof(net_size)));
+            const proxy_protocol_version_t pp_version = proxy_protocol_version(BPTR(&sb->buf), BLEN(&sb->buf));
+            const int pp_len = proxy_protocol_header_len(BPTR(&sb->buf), BLEN(&sb->buf), pp_version);
+            if (pp_version <= 0 || pp_len < 0 || pp_len > sb->buf.len)
+            {
+                msg(M_WARN, "WARNING: Bad encapsulated packet length from peer (%d), which must be > 0 and <= %d -- please ensure that --tun-mtu or --link-mtu is equal on both peers -- this condition could also indicate a possible active attack on the TCP link -- [Attempting restart...]", sb->len, sb->maxlen);
+                stream_buf_reset(sb);
+                sb->error = true;
+                return false;
+            }
+            sb->len = pp_len;
         }
     }
 
