[Openvpn-devel,L] Change in openvpn[master]: Add support for HAProxy's PROXY protocol

Message ID 192618d15a2b6ecb2eb10e078a5752ddb4723062-HTML@gerrit.openvpn.net
State New
Headers show
Series [Openvpn-devel,L] Change in openvpn[master]: Add support for HAProxy's PROXY protocol | expand

Commit Message

ordex (Code Review) July 26, 2024, 8:20 a.m. UTC
Attention is currently required from: flichtenheld, plaisthos.

Hello plaisthos, flichtenheld,

I'd like you to do a code review.
Please visit

    http://gerrit.openvpn.net/c/openvpn/+/685?usp=email

to review the following change.


Change subject: Add support for HAProxy's PROXY protocol
......................................................................

Add support for HAProxy's PROXY protocol

The PROXY protocol is a simple protocol that can be used to
communicate connection information such as a client's address across
multiple layers of NAT or TCP proxies. This commit adds support for
the parsing of the v1 and v2 PROXY protocol header but does not yet
parse the TLV data in the v2 header.

Check if a PROXY protocol header was sent by the client at the
beginning of the connection. If so, parse it, store the extracted
informations and replace the proxy address with the real client
address. Checking for the header is done only if the packet has an
invalid length so that there's minimal overhead for the common case.

Also add a small document file showing how to configure HAProxy to
test the feature.

Change-Id: I9766c68feaba71279f26c3310eee4b5ca215e6ac
Signed-off-by: Ralf Lici <ralflici95@gmail.com>
---
M CMakeLists.txt
A doc/proxy-protocol.txt
M src/openvpn/Makefile.am
M src/openvpn/init.c
M src/openvpn/multi.c
M src/openvpn/openvpn.h
A src/openvpn/proxy_protocol.c
A src/openvpn/proxy_protocol.h
M src/openvpn/socket.c
9 files changed, 895 insertions(+), 7 deletions(-)



  git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/85/685/1

Patch

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;
         }
     }