pg.ddx.io pgsql-hackers@postgresql.org mailing list archive
help / color / mirror / Atom feedPROXY protocol support
56+ messages / 16 participants
[nested] [flat]
* PROXY protocol support
@ 2019-05-19 15:36 Julien Riou <julien@riou.xyz>
2019-05-19 15:59 ` Re: PROXY protocol support Stephen Frost <sfrost@snowman.net>
2019-05-20 15:28 ` Re: PROXY protocol support Konstantin Knizhnik <k.knizhnik@postgrespro.ru>
2019-05-20 17:05 ` Re: PROXY protocol support Bruno Lavoie <bl@brunol.com>
0 siblings, 3 replies; 56+ messages in thread
From: Julien Riou @ 2019-05-19 15:36 UTC (permalink / raw)
To: pgsql-hackers@lists.postgresql.org
Hello,
Nowadays, PostgreSQL is often used behind proxies. Some are PostgreSQL
protocol aware (Pgpool, PgBouncer), some are pure TCP (HAProxy). From
the database instance point of view, all clients come from the proxy.
There are two major problems with this topology:
* It neutralizes the host based authentication. Every client shares
the same source. Either we allow this source or not but we cannot allow
clients on a more fine-grained basis, or not by the IP address.
* It makes debugging harder. If we have a DDL or a slow query logged, we
cannot use the source to identify who is responsible.
On one hand, we can move the authentication and logging mechanisms to
PostgreSQL based proxies but they will never be as complete as
PostgreSQL itself. And they don't have features like HTTP health checks
to redirect trafic to nodes (health, role, whatever behind the URL). On
the other hand, those features are not implemented at all because they
don't know the PostgreSQL protocol, they simply forward requests.
In the HTTP reverse proxies world, there's a "dirty hack" to identify
the source IP address: add an HTTP header "X-Forwared-For" to the
request. It's the destination duty to do whatever they want with this
information. With this feature in mind, someone from HAProxy has
implemented this mechanism at the protocol level. It's called the PROXY
protocol.
With this piece of logic at the beginning of the protocol, we could
implement a totally transparent proxy and benefit from the great
features of PostgreSQL regarding clients. Note that MariaDB support the
PROXY protocol in MaxScale (proxy) and MariaDB Server in recent
versions.
My question is, what do you think of this feature? Is it worth to spend
time implementing it in PostgreSQL or not?
Links:
- http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
- https://mariadb.com/kb/en/library/proxy-protocol-support/
Thanks,
Julien
PS: I've already sent this message to a wrong mailing list. Stephen
Frost said it's implemented in pgbouncer but all I can find is an open
issue: https://github.com/pgbouncer/pgbouncer/issues/241.
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2019-05-19 15:36 PROXY protocol support Julien Riou <julien@riou.xyz>
@ 2019-05-19 15:59 ` Stephen Frost <sfrost@snowman.net>
2019-05-19 20:53 ` Re: PROXY protocol support Julien Riou <julien@riou.xyz>
2 siblings, 1 reply; 56+ messages in thread
From: Stephen Frost @ 2019-05-19 15:59 UTC (permalink / raw)
To: Julien Riou <julien@riou.xyz>; +Cc: pgsql-hackers@lists.postgresql.org
Greetings,
* Julien Riou (julien@riou.xyz) wrote:
> Nowadays, PostgreSQL is often used behind proxies. Some are PostgreSQL
> protocol aware (Pgpool, PgBouncer), some are pure TCP (HAProxy). From
> the database instance point of view, all clients come from the proxy.
>
> There are two major problems with this topology:
>
> * It neutralizes the host based authentication. Every client shares
> the same source. Either we allow this source or not but we cannot allow
> clients on a more fine-grained basis, or not by the IP address.
You can instead have the IP-based checking done at the pooler.
> * It makes debugging harder. If we have a DDL or a slow query logged, we
> cannot use the source to identify who is responsible.
Protocol-level poolers are able to do this, and pgbouncer does (see
application_name_add_host).
> On one hand, we can move the authentication and logging mechanisms to
> PostgreSQL based proxies but they will never be as complete as
> PostgreSQL itself. And they don't have features like HTTP health checks
> to redirect trafic to nodes (health, role, whatever behind the URL). On
> the other hand, those features are not implemented at all because they
> don't know the PostgreSQL protocol, they simply forward requests.
>
> In the HTTP reverse proxies world, there's a "dirty hack" to identify
> the source IP address: add an HTTP header "X-Forwared-For" to the
> request. It's the destination duty to do whatever they want with this
> information. With this feature in mind, someone from HAProxy has
> implemented this mechanism at the protocol level. It's called the PROXY
> protocol.
Someone from HAProxy could certainly implement something similar by
having HAProxy understand PostgreSQL's protocol.
> With this piece of logic at the beginning of the protocol, we could
> implement a totally transparent proxy and benefit from the great
> features of PostgreSQL regarding clients. Note that MariaDB support the
> PROXY protocol in MaxScale (proxy) and MariaDB Server in recent
> versions.
pgbouncer is already a transparent proxy that understands the PG
protocol, and, even better, it has support for transaction-level pooling
(as well as connection-level), which is really critical for larger PG
deployments as PG backend startup is (relatively) expensive.
> PS: I've already sent this message to a wrong mailing list. Stephen
> Frost said it's implemented in pgbouncer but all I can find is an open
> issue: https://github.com/pgbouncer/pgbouncer/issues/241.
That would be some *other* proxy system (Amazon's ELB) that apparently
also doesn't understand the PG protocol and therefore doesn't have a
feature similar to pgbouncer's application_name_add_host.
I haven't looked very closely at if it'd be possible to interpret the
PROXY protocol thing that Amazon's ELB can do without confusing it with
a regular PG authentication startup and I'm not sure if we'd really want
to wed ourselves to something like that. Certainly, what pgbouncer does
works quite well and is about as transparent to clients as possible.
You'd almost certainly want something like pgbouncer after the ELB
anyway to avoid having tons of connections to PG and avoid spinning up
new backends constantly.
Thanks,
Stephen
Attachments:
[application/pgp-signature] signature.asc (818B, ../../20190519155903.GI6197@tamriel.snowman.net/2-signature.asc)
download
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2019-05-19 15:36 PROXY protocol support Julien Riou <julien@riou.xyz>
2019-05-19 15:59 ` Re: PROXY protocol support Stephen Frost <sfrost@snowman.net>
@ 2019-05-19 20:53 ` Julien Riou <julien@riou.xyz>
0 siblings, 0 replies; 56+ messages in thread
From: Julien Riou @ 2019-05-19 20:53 UTC (permalink / raw)
To: pgsql-hackers@lists.postgresql.org,Stephen Frost <sfrost@snowman.net>
On May 19, 2019 5:59:04 PM GMT+02:00, Stephen Frost <sfrost@snowman.net> wrote:
>Greetings,
>
>* Julien Riou (julien@riou.xyz) wrote:
>> Nowadays, PostgreSQL is often used behind proxies. Some are
>PostgreSQL
>> protocol aware (Pgpool, PgBouncer), some are pure TCP (HAProxy). From
>> the database instance point of view, all clients come from the proxy.
>>
>> There are two major problems with this topology:
>>
>> * It neutralizes the host based authentication. Every client shares
>> the same source. Either we allow this source or not but we cannot
>allow
>> clients on a more fine-grained basis, or not by the IP address.
>
>You can instead have the IP-based checking done at the pooler.
>
>> * It makes debugging harder. If we have a DDL or a slow query logged,
>we
>> cannot use the source to identify who is responsible.
>
>Protocol-level poolers are able to do this, and pgbouncer does (see
>application_name_add_host).
>
>> On one hand, we can move the authentication and logging mechanisms to
>> PostgreSQL based proxies but they will never be as complete as
>> PostgreSQL itself. And they don't have features like HTTP health
>checks
>> to redirect trafic to nodes (health, role, whatever behind the URL).
>On
>> the other hand, those features are not implemented at all because
>they
>> don't know the PostgreSQL protocol, they simply forward requests.
>>
>> In the HTTP reverse proxies world, there's a "dirty hack" to identify
>> the source IP address: add an HTTP header "X-Forwared-For" to the
>> request. It's the destination duty to do whatever they want with this
>> information. With this feature in mind, someone from HAProxy has
>> implemented this mechanism at the protocol level. It's called the
>PROXY
>> protocol.
>
>Someone from HAProxy could certainly implement something similar by
>having HAProxy understand PostgreSQL's protocol.
>
>> With this piece of logic at the beginning of the protocol, we could
>> implement a totally transparent proxy and benefit from the great
>> features of PostgreSQL regarding clients. Note that MariaDB support
>the
>> PROXY protocol in MaxScale (proxy) and MariaDB Server in recent
>> versions.
>
>pgbouncer is already a transparent proxy that understands the PG
>protocol, and, even better, it has support for transaction-level
>pooling
>(as well as connection-level), which is really critical for larger PG
>deployments as PG backend startup is (relatively) expensive.
>
>> PS: I've already sent this message to a wrong mailing list. Stephen
>> Frost said it's implemented in pgbouncer but all I can find is an
>open
>> issue: https://github.com/pgbouncer/pgbouncer/issues/241.
>
>That would be some *other* proxy system (Amazon's ELB) that apparently
>also doesn't understand the PG protocol and therefore doesn't have a
>feature similar to pgbouncer's application_name_add_host.
>
>I haven't looked very closely at if it'd be possible to interpret the
>PROXY protocol thing that Amazon's ELB can do without confusing it with
>a regular PG authentication startup and I'm not sure if we'd really
>want
>to wed ourselves to something like that. Certainly, what pgbouncer
>does
>works quite well and is about as transparent to clients as possible.
>
>You'd almost certainly want something like pgbouncer after the ELB
>anyway to avoid having tons of connections to PG and avoid spinning up
>new backends constantly.
>
>Thanks,
>
>Stephen
It could be proprietary Amazon load balancers I don't have experience with, or simple HAProxy coupled with a Patroni HTTP API to tell if a backend is healthy or not.
The PgBouncer approach is interesting. I'm already using the application name as a workaround to identify containerized applications but didn't used it for setting the source IP.
If we take a look at the MariaDB implementation, they check for errors in the startup packet then run the PROXY protocol decoding then return a real error if it doesn't work. As our bouncers are all behind a pool of HAProxy, and if we consider PgBouncer as a trusted extension of PostgreSQL, maybe implementing it in PgBouncer first will be easier.
Thanks for your insightful comments.
Julien
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2019-05-19 15:36 PROXY protocol support Julien Riou <julien@riou.xyz>
@ 2019-05-20 15:28 ` Konstantin Knizhnik <k.knizhnik@postgrespro.ru>
2 siblings, 0 replies; 56+ messages in thread
From: Konstantin Knizhnik @ 2019-05-20 15:28 UTC (permalink / raw)
To: Julien Riou <julien@riou.xyz>; pgsql-hackers@lists.postgresql.org
On 19.05.2019 18:36, Julien Riou wrote:
> Hello,
>
> Nowadays, PostgreSQL is often used behind proxies. Some are PostgreSQL
> protocol aware (Pgpool, PgBouncer), some are pure TCP (HAProxy). From
> the database instance point of view, all clients come from the proxy.
>
> There are two major problems with this topology:
>
> * It neutralizes the host based authentication. Every client shares
> the same source. Either we allow this source or not but we cannot allow
> clients on a more fine-grained basis, or not by the IP address.
>
> * It makes debugging harder. If we have a DDL or a slow query logged, we
> cannot use the source to identify who is responsible.
>
> On one hand, we can move the authentication and logging mechanisms to
> PostgreSQL based proxies but they will never be as complete as
> PostgreSQL itself. And they don't have features like HTTP health checks
> to redirect trafic to nodes (health, role, whatever behind the URL). On
> the other hand, those features are not implemented at all because they
> don't know the PostgreSQL protocol, they simply forward requests.
>
> In the HTTP reverse proxies world, there's a "dirty hack" to identify
> the source IP address: add an HTTP header "X-Forwared-For" to the
> request. It's the destination duty to do whatever they want with this
> information. With this feature in mind, someone from HAProxy has
> implemented this mechanism at the protocol level. It's called the PROXY
> protocol.
>
> With this piece of logic at the beginning of the protocol, we could
> implement a totally transparent proxy and benefit from the great
> features of PostgreSQL regarding clients. Note that MariaDB support the
> PROXY protocol in MaxScale (proxy) and MariaDB Server in recent
> versions.
>
> My question is, what do you think of this feature? Is it worth to spend
> time implementing it in PostgreSQL or not?
>
> Links:
> - http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
> - https://mariadb.com/kb/en/library/proxy-protocol-support/
>
> Thanks,
> Julien
>
> PS: I've already sent this message to a wrong mailing list. Stephen
> Frost said it's implemented in pgbouncer but all I can find is an open
> issue: https://github.com/pgbouncer/pgbouncer/issues/241.
>
>
Hi,
From my point of view it will be better to support embedded connection
pooler in Postgres.
In this case all mentioned problems can be more or less
straightforwardly solved without inventing new protocol.
There is my prototype implementation of built-in connection pooler on
commit-fest:
https://commitfest.postgresql.org/23/2067/
--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2019-05-19 15:36 PROXY protocol support Julien Riou <julien@riou.xyz>
@ 2019-05-20 17:05 ` Bruno Lavoie <bl@brunol.com>
2 siblings, 0 replies; 56+ messages in thread
From: Bruno Lavoie @ 2019-05-20 17:05 UTC (permalink / raw)
To: Julien Riou <julien@riou.xyz>; +Cc: pgsql-hackers@lists.postgresql.org
+1 on this one...
MySQL and derivatives support it very well.. it is a standard that can be
used with either haproxy or better, ProxySQL.
Would be nice to have it in core.
It is a show stopper for us to use proxying because of compliance and
tracability reasons.
Le dim. 19 mai 2019 11:36 AM, Julien Riou <julien@riou.xyz> a écrit :
> Hello,
>
> Nowadays, PostgreSQL is often used behind proxies. Some are PostgreSQL
> protocol aware (Pgpool, PgBouncer), some are pure TCP (HAProxy). From
> the database instance point of view, all clients come from the proxy.
>
> There are two major problems with this topology:
>
> * It neutralizes the host based authentication. Every client shares
> the same source. Either we allow this source or not but we cannot allow
> clients on a more fine-grained basis, or not by the IP address.
>
> * It makes debugging harder. If we have a DDL or a slow query logged, we
> cannot use the source to identify who is responsible.
>
> On one hand, we can move the authentication and logging mechanisms to
> PostgreSQL based proxies but they will never be as complete as
> PostgreSQL itself. And they don't have features like HTTP health checks
> to redirect trafic to nodes (health, role, whatever behind the URL). On
> the other hand, those features are not implemented at all because they
> don't know the PostgreSQL protocol, they simply forward requests.
>
> In the HTTP reverse proxies world, there's a "dirty hack" to identify
> the source IP address: add an HTTP header "X-Forwared-For" to the
> request. It's the destination duty to do whatever they want with this
> information. With this feature in mind, someone from HAProxy has
> implemented this mechanism at the protocol level. It's called the PROXY
> protocol.
>
> With this piece of logic at the beginning of the protocol, we could
> implement a totally transparent proxy and benefit from the great
> features of PostgreSQL regarding clients. Note that MariaDB support the
> PROXY protocol in MaxScale (proxy) and MariaDB Server in recent
> versions.
>
> My question is, what do you think of this feature? Is it worth to spend
> time implementing it in PostgreSQL or not?
>
> Links:
> - http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
> - https://mariadb.com/kb/en/library/proxy-protocol-support/
>
> Thanks,
> Julien
>
> PS: I've already sent this message to a wrong mailing list. Stephen
> Frost said it's implemented in pgbouncer but all I can find is an open
> issue: https://github.com/pgbouncer/pgbouncer/issues/241.
>
>
>
^ permalink raw reply [nested|flat] 56+ messages in thread
* PROXY protocol support
@ 2021-03-02 17:43 Magnus Hagander <magnus@hagander.net>
2021-03-02 18:42 ` Re: PROXY protocol support Arthur Nascimento <tureba@gmail.com>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 14:13 ` Re: PROXY protocol support Bruno Lavoie <bl@brunol.com>
2021-03-04 01:42 ` Re: PROXY protocol support Tatsuo Ishii <ishii@sraoss.co.jp>
0 siblings, 4 replies; 56+ messages in thread
From: Magnus Hagander @ 2021-03-02 17:43 UTC (permalink / raw)
To: PostgreSQL Developers <pgsql-hackers@lists.postgresql.org>
PFA a simple patch that implements support for the PROXY protocol.
This is a protocol common and very light weight in proxies and load
balancers (haproxy is one common example, but also for example the AWS
cloud load balancers). Basically this protocol prefixes the normal
connection with a header and a specification of what the original host
was, allowing the server to unwrap that and get the correct client
address instead of just the proxy ip address. It is a one-way protocol
in that there is no response from the server, it's just purely a
prefix of the IP information.
Using this when PostgreSQL is behind a proxy allows us to keep using
pg_hba.conf rules based on the original ip address, as well as track
the original address in log messages and pg_stat_activity etc.
The implementation adds a parameter named proxy_servers which lists
the ips or ip+cidr mask to be trusted. Since a proxy can decide what
the origin is, and this is used for security decisions, it's very
important to not just trust any server, only those that are
intentionally used. By default, no servers are listed, and thus the
protocol is disabled.
When specified, and the connection on the normal port has the proxy
prefix on it, and the connection comes in from one of the addresses
listed as valid proxy servers, we will replace the actual IP address
of the client with the one specified in the proxy packet.
Currently there is no information about the proxy server in the
pg_stat_activity view, it's only available as a log message. But maybe
it should go in pg_stat_activity as well? Or in a separate
pg_stat_proxy view?
(In passing, I note that pq_discardbytes were in pqcomm.h, yet listed
as static in pqcomm.c -- but now made non-static)
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
Attachments:
[text/x-patch] proxy_protocol.patch (18.5K, ../../CABUevExJ0ifpUEiX4uOREy0s2kHBrBrb=pXLEHhpMTR1vVR1XA@mail.gmail.com/2-proxy_protocol.patch)
download | inline diff:
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index b420486a0a..d4f6fad5b0 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -353,6 +353,15 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl
the client's host name instead of the IP address in the log.
</para>
+ <para>
+ If <xref linkend="guc-proxy-servers"/> is enabled and the
+ connection is made through a proxy server using the PROXY
+ protocol, the actual IP address of the client will be used
+ for matching. If a connection is made through a proxy server
+ not using the PROXY protocol, the IP address of the
+ proxy server will be used.
+ </para>
+
<para>
These fields do not apply to <literal>local</literal> records.
</para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b5718fc136..fc7de25378 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -682,6 +682,30 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-proxy-servers" xreflabel="proxy_servers">
+ <term><varname>proxy_servers</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>proxy_servers</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ A comma separated list of one or more host names or cidr specifications
+ of proxy servers to trust. If a connection using the PROXY protocol is made
+ from one of these IP addresses, <productname>PostgreSQL</productname> will
+ read the client IP address from the PROXY header and consider that the
+ address of the client, instead of listing all connections as coming from
+ the proxy server.
+ </para>
+ <para>
+ If a proxy connection is made from an IP address not covered by this
+ list, the connection will be rejected. By default no proxy is trusted
+ and all proxy connections will be rejected. This parameter can only
+ be set at server start.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-max-connections" xreflabel="max_connections">
<term><varname>max_connections</varname> (<type>integer</type>)
<indexterm>
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 27a298f110..9163761cc2 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -53,6 +53,7 @@
* pq_getmessage - get a message with length word from connection
* pq_getbyte - get next byte from connection
* pq_peekbyte - peek at next byte from connection
+ * pq_peekbytes - peek at a known number of bytes from connection
* pq_putbytes - send bytes to connection (not flushed until pq_flush)
* pq_flush - flush pending output
* pq_flush_if_writable - flush pending output if writable without blocking
@@ -1039,6 +1040,27 @@ pq_peekbyte(void)
return (unsigned char) PqRecvBuffer[PqRecvPointer];
}
+
+/* --------------------------------
+ * pq_peekbytes - peek at a known number of bytes from connection.
+ * Note! Does NOT wait for more data to arrive.
+ *
+ * returns 0 if OK, EOF if trouble
+ * --------------------------------
+ */
+int
+pq_peekbytes(char *s, size_t len)
+{
+ Assert(PqCommReadingMsg);
+
+ if (PqRecvLength - PqRecvPointer < len)
+ return EOF;
+
+ memcpy(s, PqRecvBuffer + PqRecvPointer, len);
+
+ return 0;
+}
+
/* --------------------------------
* pq_getbyte_if_available - get a single byte from connection,
* if available
@@ -1135,7 +1157,7 @@ pq_getbytes(char *s, size_t len)
* returns 0 if OK, EOF if trouble
* --------------------------------
*/
-static int
+int
pq_discardbytes(size_t len)
{
size_t amount;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 3f1ce135a8..0473129cb4 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -102,6 +102,7 @@
#include "common/string.h"
#include "lib/ilist.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
@@ -204,6 +205,10 @@ char *Unix_socket_directories;
/* The TCP listen address(es) */
char *ListenAddresses;
+/* Trusted proxy servers */
+char *TrustedProxyServersString = NULL;
+struct sockaddr_storage *TrustedProxyServers = NULL;
+
/*
* ReservedBackends is the number of backends reserved for superuser use.
* This number is taken out of the pool size given by MaxConnections so
@@ -1911,6 +1916,203 @@ initMasks(fd_set *rmask)
return maxsock + 1;
}
+static int
+UnwrapProxyConnection(Port *port)
+{
+ char proxyver;
+ uint16 proxyaddrlen;
+ SockAddr raddr_save;
+ int i;
+ bool allowed = false;
+
+ /*
+ * These structs are from the PROXY protocol docs at
+ * http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
+ */
+ union
+ {
+ struct
+ { /* for TCP/UDP over IPv4, len = 12 */
+ uint32 src_addr;
+ uint32 dst_addr;
+ uint16 src_port;
+ uint16 dst_port;
+ } ip4;
+ struct
+ { /* for TCP/UDP over IPv6, len = 36 */
+ uint8 src_addr[16];
+ uint8 dst_addr[16];
+ uint16 src_port;
+ uint16 dst_port;
+ } ip6;
+ } proxyaddr;
+ struct
+ {
+ uint8 sig[12]; /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */
+ uint8 ver_cmd; /* protocol version and command */
+ uint8 fam; /* protocol family and address */
+ uint16 len; /* number of following bytes part of the
+ * header */
+ } proxyheader;
+
+ /* Store a copy of the original address, for logging */
+ memcpy(&raddr_save, &port->raddr, port->raddr.salen);
+
+ pq_startmsgread();
+
+ /* Peek at the very first byte just to trigger a read */
+ if (pq_peekbyte() == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete startup packet")));
+ return STATUS_ERROR;
+ }
+
+ /*
+ * PROXY requests always start with: \x0D \x0A \x0D \x0A \x00 \x0D \x0A
+ * \x51 \x55 \x49 \x54 \x0A
+ */
+
+ if (pq_peekbytes((char *) &proxyheader, sizeof(proxyheader)) != 0)
+ {
+ /*
+ * Not enough bytes to be a proxy header, so fall through to normal
+ * processing
+ */
+ pq_endmsgread();
+ return STATUS_OK;
+ }
+
+ if (memcmp(proxyheader.sig, "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a", sizeof(proxyheader.sig)) != 0)
+ {
+ /*
+ * Data is there but it wasn't a proxy header. Also fall through to
+ * normal processing
+ */
+ pq_endmsgread();
+ return STATUS_OK;
+ }
+
+ /* Header is valid. Verify that the proxy is actually authorized! */
+ for (i = 0; i < *((int *) TrustedProxyServers); i += 2)
+ {
+ if (raddr_save.addr.ss_family == TrustedProxyServers[i + 1].ss_family &&
+ pg_range_sockaddr(&raddr_save.addr,
+ &TrustedProxyServers[i + 1],
+ &TrustedProxyServers[i + 2]))
+ {
+ allowed = true;
+ break;
+ }
+ }
+ if (!allowed)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("proxy connection from unauthorized server")));
+ return STATUS_ERROR;
+ }
+
+ /*
+ * This is a valid proxy header, so unwrap it. First, skip past the header
+ * itself
+ */
+ pq_discardbytes(sizeof(proxyheader));
+
+ /* Proxy version is in the high 4 bits of the first byte */
+ proxyver = (proxyheader.ver_cmd & 0xF0) >> 4;
+ if (proxyver != 2)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol version: %x", proxyver)));
+ return STATUS_ERROR;
+ }
+
+ proxyaddrlen = pg_ntoh16(proxyheader.len);
+
+ if (proxyaddrlen > sizeof(proxyaddr))
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("oversized proxy packet")));
+ return STATUS_ERROR;
+ }
+ if (pq_getbytes((char *) &proxyaddr, proxyaddrlen) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Lower 4 bits hold type of connection */
+ if (proxyheader.fam == 0)
+ {
+ /* LOCAL connection, so we ignore the address included */
+ }
+ else if (proxyheader.fam == 0x11)
+ {
+ /* TCPv4 */
+ port->raddr.addr.ss_family = AF_INET;
+ port->raddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
+ }
+ else if (proxyheader.fam == 0x21)
+ {
+ /* TCPv6 */
+ port->raddr.addr.ss_family = AF_INET6;
+ port->raddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->raddr.addr)->sin6_addr, proxyaddr.ip6.src_addr, 16);
+ ((struct sockaddr_in6 *) &port->raddr.addr)->sin6_port = proxyaddr.ip6.src_port;
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol connection type: %x", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+
+ /* If there is any more header data present, skip past it */
+ if (proxyaddrlen > sizeof(proxyaddr))
+ pq_discardbytes(proxyaddrlen - sizeof(proxyaddr));
+
+
+ pq_endmsgread();
+
+ /*
+ * Log what we've done if connection logging is enabled. We log the proxy
+ * connection here, and let the normal connection logging mechanism log
+ * the unwrapped connection.
+ */
+ if (Log_connections)
+ {
+ char remote_host[NI_MAXHOST];
+ char remote_port[NI_MAXSERV];
+ int ret;
+
+ remote_host[0] = '\0';
+ remote_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&raddr_save.addr, raddr_save.salen,
+ remote_host, sizeof(remote_host),
+ remote_port, sizeof(remote_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ ereport(LOG,
+ (errmsg("proxy connection from: host=%s port=%s",
+ remote_host,
+ remote_port)));
+
+ }
+
+ return STATUS_OK;
+}
/*
* Read a client's startup packet and do something according to it.
@@ -4344,6 +4546,33 @@ BackendInitialize(Port *port)
InitializeTimeouts(); /* establishes SIGALRM handler */
PG_SETMASK(&StartupBlockSig);
+ /*
+ * Ready to begin client interaction. We will give up and _exit(1) after
+ * a time delay, so that a broken client can't hog a connection
+ * indefinitely. PreAuthDelay and any DNS interactions above don't count
+ * against the time limit.
+ *
+ * Note: AuthenticationTimeout is applied here while waiting for the
+ * startup packet, and then again in InitPostgres for the duration of any
+ * authentication operations. So a hostile client could tie up the
+ * process for nearly twice AuthenticationTimeout before we kick him off.
+ *
+ * Note: because PostgresMain will call InitializeTimeouts again, the
+ * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
+ * since we never use it again after this function.
+ */
+ RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
+
+ /* Check if this is a proxy connection and if so unwrap the proxying */
+ if (TrustedProxyServers)
+ {
+ if (UnwrapProxyConnection(port) != STATUS_OK)
+ proc_exit(0);
+ }
+
+ disable_timeout(STARTUP_PACKET_TIMEOUT, false);
+
/*
* Get the remote host name and port for logging and status display.
*/
@@ -4395,28 +4624,11 @@ BackendInitialize(Port *port)
strspn(remote_host, "0123456789ABCDEFabcdef:") < strlen(remote_host))
port->remote_hostname = strdup(remote_host);
- /*
- * Ready to begin client interaction. We will give up and _exit(1) after
- * a time delay, so that a broken client can't hog a connection
- * indefinitely. PreAuthDelay and any DNS interactions above don't count
- * against the time limit.
- *
- * Note: AuthenticationTimeout is applied here while waiting for the
- * startup packet, and then again in InitPostgres for the duration of any
- * authentication operations. So a hostile client could tie up the
- * process for nearly twice AuthenticationTimeout before we kick him off.
- *
- * Note: because PostgresMain will call InitializeTimeouts again, the
- * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
- * since we never use it again after this function.
- */
- RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
- enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
-
/*
* Receive the startup packet (which might turn out to be a cancel request
* packet).
*/
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
status = ProcessStartupPacket(port, false, false);
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d626731723..381067b737 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -46,10 +46,12 @@
#include "commands/user.h"
#include "commands/vacuum.h"
#include "commands/variable.h"
+#include "common/ip.h"
#include "common/string.h"
#include "funcapi.h"
#include "jit/jit.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
@@ -227,6 +229,8 @@ static bool check_recovery_target_lsn(char **newval, void **extra, GucSource sou
static void assign_recovery_target_lsn(const char *newval, void *extra);
static bool check_primary_slot_name(char **newval, void **extra, GucSource source);
static bool check_default_with_oids(bool *newval, void **extra, GucSource source);
+static bool check_proxy_servers(char **newval, void **extra, GucSource source);
+static void assign_proxy_servers(const char *newval, void *extra);
/* Private functions in guc-file.l that need to be called from guc.c */
static ConfigVariable *ProcessConfigFileInternal(GucContext context,
@@ -4241,6 +4245,17 @@ static struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_servers", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the addresses for trusted proxy servers."),
+ NULL,
+ GUC_LIST_INPUT
+ },
+ &TrustedProxyServersString,
+ "",
+ check_proxy_servers, assign_proxy_servers, NULL
+ },
+
{
/*
* Can't be set by ALTER SYSTEM as it can lead to recursive definition
@@ -12228,4 +12243,108 @@ check_default_with_oids(bool *newval, void **extra, GucSource source)
return true;
}
+static bool
+check_proxy_servers(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ struct sockaddr_storage *myextra;
+
+ /* Special case when it's empty */
+ if (**newval == '\0')
+ {
+ *extra = NULL;
+ return true;
+ }
+
+ /* Need a modifiable copy of string */
+ rawstring = pstrdup(*newval);
+
+ /* Parse string into list of identifiers */
+ if (!SplitIdentifierString(rawstring, ',', &elemlist))
+ {
+ /* syntax error in list */
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ if (list_length(elemlist) == 0)
+ {
+ /* If it had only whitespace */
+ pfree(rawstring);
+ list_free(elemlist);
+
+ *extra = NULL;
+ return true;
+ }
+
+ /*
+ * We store the result in an array of sockaddr_storage. The first entry is
+ * just an overloaded int which holds the size of the array.
+ */
+ myextra = (struct sockaddr_storage *) guc_malloc(ERROR, sizeof(struct sockaddr_storage) * (list_length(elemlist) * 2 + 1));
+ *((int *) &myextra[0]) = list_length(elemlist);
+
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+ char *netmasktok = NULL;
+ int ret;
+ struct addrinfo *gai_result;
+ struct addrinfo hints;
+
+ netmasktok = strchr(tok, '/');
+ if (netmasktok)
+ {
+ *netmasktok = '\0';
+ netmasktok++;
+ }
+
+ memset((char *) &hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ ret = pg_getaddrinfo_all(tok, NULL, &hints, &gai_result);
+ if (ret != 0 || gai_result == NULL)
+ {
+ GUC_check_errdetail("Invalid IP addrress %s", tok);
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+
+ memcpy((char *) &myextra[foreach_current_index(l) * 2 + 1], gai_result->ai_addr, gai_result->ai_addrlen);
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+
+ /* A NULL netmasktok means the fully set hostmask */
+ if (pg_sockaddr_cidr_mask(&myextra[foreach_current_index(l) * 2 + 2], netmasktok, myextra[foreach_current_index(l) * 2 + 1].ss_family) != 0)
+ {
+ if (netmasktok)
+ GUC_check_errdetail("Invalid netmask %s", netmasktok);
+ else
+ GUC_check_errdetail("Could not create netmask");
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+ }
+
+ pfree(rawstring);
+ list_free(elemlist);
+ *extra = (void *) myextra;
+
+ return true;
+}
+
+static void
+assign_proxy_servers(const char *newval, void *extra)
+{
+ TrustedProxyServers = (struct sockaddr_storage *) extra;
+}
+
#include "guc-file.c"
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ee06528bb0..aa7ac35f67 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -61,6 +61,8 @@
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
+#proxy_servers = '' # what IP/netmasks of proxy servers to trust
+ # (change requires restart)
#max_connections = 100 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
#unix_socket_directories = '/tmp' # comma-separated list of directories
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index e4e5c21565..6125b93e86 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -74,6 +74,8 @@ extern bool pq_is_reading_msg(void);
extern int pq_getmessage(StringInfo s, int maxlen);
extern int pq_getbyte(void);
extern int pq_peekbyte(void);
+extern int pq_peekbytes(char *s, size_t len);
+extern int pq_discardbytes(size_t len);
extern int pq_getbyte_if_available(unsigned char *c);
extern int pq_putbytes(const char *s, size_t len);
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index cfa59c4dc0..38e7644371 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -21,6 +21,8 @@ extern int Unix_socket_permissions;
extern char *Unix_socket_group;
extern char *Unix_socket_directories;
extern char *ListenAddresses;
+extern char *TrustedProxyServersString;
+extern struct sockaddr_storage *TrustedProxyServers;
extern bool ClientAuthInProgress;
extern int PreAuthDelay;
extern int AuthenticationTimeout;
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-03-02 18:42 ` Arthur Nascimento <tureba@gmail.com>
3 siblings, 0 replies; 56+ messages in thread
From: Arthur Nascimento @ 2021-03-02 18:42 UTC (permalink / raw)
To: Magnus Hagander <magnus@hagander.net>; +Cc: PostgreSQL Developers <pgsql-hackers@lists.postgresql.org>
Hi,
On Tue, 2 Mar 2021 at 14:43, Magnus Hagander <magnus@hagander.net> wrote:
> PFA a simple patch that implements support for the PROXY protocol.
Nice. I didn't know I needed this. But in hindsight, I would've used
it quite a few times in the past if I could have.
> The implementation adds a parameter named proxy_servers which lists
> the ips or ip+cidr mask to be trusted. Since a proxy can decide what
> the origin is, and this is used for security decisions, it's very
> important to not just trust any server, only those that are
> intentionally used. By default, no servers are listed, and thus the
> protocol is disabled.
Might make sense to add special cases for 'samehost' and 'samenet', as
in hba rules, as proxy servers are commonly on the same machine or
share one of the same internal networks.
Despite the security issues, I'm sure people will soon try and set
proxy_servers='*' or 'all' if they think this setting works as
listen_addresses or as pg_hba. But I don't think I'd make these use
cases easier.
Tureba - Arthur Nascimento
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-03-03 00:50 ` Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
3 siblings, 1 reply; 56+ messages in thread
From: Jacob Champion @ 2021-03-03 00:50 UTC (permalink / raw)
To: magnus@hagander.net <magnus@hagander.net>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Tue, 2021-03-02 at 18:43 +0100, Magnus Hagander wrote:
> PFA a simple patch that implements support for the PROXY protocol.
I'm not all the way through the patch yet, but this part jumped out at
me:
> + if (memcmp(proxyheader.sig, "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a", sizeof(proxyheader.sig)) != 0)
> + {
> + /*
> + * Data is there but it wasn't a proxy header. Also fall through to
> + * normal processing
> + */
> + pq_endmsgread();
> + return STATUS_OK;
From my reading, the spec explicitly disallows this sort of fallback
behavior:
> The receiver MUST be configured to only receive the protocol described in this
> specification and MUST not try to guess whether the protocol header is present
> or not. This means that the protocol explicitly prevents port sharing between
> public and private access.
You might say, "if we already trust the proxy server, why should we
care?" but I think the point is that you want to catch
misconfigurations where the middlebox is forwarding bare TCP without
adding a PROXY header of its own, which will "work" for innocent
clients but in reality is a ticking timebomb. If you've decided to
trust an intermediary to use PROXY connections, then you must _only_
accept PROXY connections from that intermediary. Does that seem like a
reasonable interpretation?
--Jacob
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
@ 2021-03-03 09:00 ` Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2021-03-03 09:00 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Wed, Mar 3, 2021 at 1:50 AM Jacob Champion <pchampion@vmware.com> wrote:
>
> On Tue, 2021-03-02 at 18:43 +0100, Magnus Hagander wrote:
> > PFA a simple patch that implements support for the PROXY protocol.
>
> I'm not all the way through the patch yet, but this part jumped out at
> me:
>
> > + if (memcmp(proxyheader.sig, "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a", sizeof(proxyheader.sig)) != 0)
> > + {
> > + /*
> > + * Data is there but it wasn't a proxy header. Also fall through to
> > + * normal processing
> > + */
> > + pq_endmsgread();
> > + return STATUS_OK;
>
> From my reading, the spec explicitly disallows this sort of fallback
> behavior:
>
> > The receiver MUST be configured to only receive the protocol described in this
> > specification and MUST not try to guess whether the protocol header is present
> > or not. This means that the protocol explicitly prevents port sharing between
> > public and private access.
>
> You might say, "if we already trust the proxy server, why should we
> care?" but I think the point is that you want to catch
> misconfigurations where the middlebox is forwarding bare TCP without
> adding a PROXY header of its own, which will "work" for innocent
> clients but in reality is a ticking timebomb. If you've decided to
> trust an intermediary to use PROXY connections, then you must _only_
> accept PROXY connections from that intermediary. Does that seem like a
> reasonable interpretation?
I definitely missed that part of the spec. Ugh.
That said, I'm not sure it's *actually* an issue in the case of
PostgreSQL. Given that doing what you're suggesting, accidentally
passing connections without PROXY, will get caught in pg_hba.conf.
That said, I agree with your interpretation, and it's pretty easy to
change it to that. Basically we just have to do the IP check *before*
doing the PROXY protocol check. It makes testing a bit more difficult
though, but maybe worth it?
I've attached a POC that does that. Note that I have *not* updated the docs!
Another option would of course be to listen on a separate port for it,
which seems to be the "haproxy way". That would be slightly more code
(we'd still want to keep the code for validating the list of trusted
proxies I'd say), but maybe worth doing?
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
Attachments:
[text/x-patch] proxy_protocol_only.patch (18.0K, ../../CABUevEwpMNDB7UGAS596wEu_OrvAdfzZ1tJJHNXsNWvofGOqrQ@mail.gmail.com/2-proxy_protocol_only.patch)
download | inline diff:
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index b420486a0a..d4f6fad5b0 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -353,6 +353,15 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl
the client's host name instead of the IP address in the log.
</para>
+ <para>
+ If <xref linkend="guc-proxy-servers"/> is enabled and the
+ connection is made through a proxy server using the PROXY
+ protocol, the actual IP address of the client will be used
+ for matching. If a connection is made through a proxy server
+ not using the PROXY protocol, the IP address of the
+ proxy server will be used.
+ </para>
+
<para>
These fields do not apply to <literal>local</literal> records.
</para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b5718fc136..fc7de25378 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -682,6 +682,30 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-proxy-servers" xreflabel="proxy_servers">
+ <term><varname>proxy_servers</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>proxy_servers</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ A comma separated list of one or more host names or cidr specifications
+ of proxy servers to trust. If a connection using the PROXY protocol is made
+ from one of these IP addresses, <productname>PostgreSQL</productname> will
+ read the client IP address from the PROXY header and consider that the
+ address of the client, instead of listing all connections as coming from
+ the proxy server.
+ </para>
+ <para>
+ If a proxy connection is made from an IP address not covered by this
+ list, the connection will be rejected. By default no proxy is trusted
+ and all proxy connections will be rejected. This parameter can only
+ be set at server start.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-max-connections" xreflabel="max_connections">
<term><varname>max_connections</varname> (<type>integer</type>)
<indexterm>
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 27a298f110..9163761cc2 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -53,6 +53,7 @@
* pq_getmessage - get a message with length word from connection
* pq_getbyte - get next byte from connection
* pq_peekbyte - peek at next byte from connection
+ * pq_peekbytes - peek at a known number of bytes from connection
* pq_putbytes - send bytes to connection (not flushed until pq_flush)
* pq_flush - flush pending output
* pq_flush_if_writable - flush pending output if writable without blocking
@@ -1039,6 +1040,27 @@ pq_peekbyte(void)
return (unsigned char) PqRecvBuffer[PqRecvPointer];
}
+
+/* --------------------------------
+ * pq_peekbytes - peek at a known number of bytes from connection.
+ * Note! Does NOT wait for more data to arrive.
+ *
+ * returns 0 if OK, EOF if trouble
+ * --------------------------------
+ */
+int
+pq_peekbytes(char *s, size_t len)
+{
+ Assert(PqCommReadingMsg);
+
+ if (PqRecvLength - PqRecvPointer < len)
+ return EOF;
+
+ memcpy(s, PqRecvBuffer + PqRecvPointer, len);
+
+ return 0;
+}
+
/* --------------------------------
* pq_getbyte_if_available - get a single byte from connection,
* if available
@@ -1135,7 +1157,7 @@ pq_getbytes(char *s, size_t len)
* returns 0 if OK, EOF if trouble
* --------------------------------
*/
-static int
+int
pq_discardbytes(size_t len)
{
size_t amount;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 3f1ce135a8..514ab72d40 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -102,6 +102,7 @@
#include "common/string.h"
#include "lib/ilist.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
@@ -204,6 +205,10 @@ char *Unix_socket_directories;
/* The TCP listen address(es) */
char *ListenAddresses;
+/* Trusted proxy servers */
+char *TrustedProxyServersString = NULL;
+struct sockaddr_storage *TrustedProxyServers = NULL;
+
/*
* ReservedBackends is the number of backends reserved for superuser use.
* This number is taken out of the pool size given by MaxConnections so
@@ -1911,6 +1916,184 @@ initMasks(fd_set *rmask)
return maxsock + 1;
}
+static int
+UnwrapProxyConnection(Port *port)
+{
+ char proxyver;
+ uint16 proxyaddrlen;
+ SockAddr raddr_save;
+ int i;
+ bool useproxy = false;
+
+ /*
+ * These structs are from the PROXY protocol docs at
+ * http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
+ */
+ union
+ {
+ struct
+ { /* for TCP/UDP over IPv4, len = 12 */
+ uint32 src_addr;
+ uint32 dst_addr;
+ uint16 src_port;
+ uint16 dst_port;
+ } ip4;
+ struct
+ { /* for TCP/UDP over IPv6, len = 36 */
+ uint8 src_addr[16];
+ uint8 dst_addr[16];
+ uint16 src_port;
+ uint16 dst_port;
+ } ip6;
+ } proxyaddr;
+ struct
+ {
+ uint8 sig[12]; /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */
+ uint8 ver_cmd; /* protocol version and command */
+ uint8 fam; /* protocol family and address */
+ uint16 len; /* number of following bytes part of the
+ * header */
+ } proxyheader;
+
+
+ for (i = 0; i < *((int *) TrustedProxyServers); i += 2)
+ {
+ if (port->raddr.addr.ss_family == TrustedProxyServers[i + 1].ss_family &&
+ pg_range_sockaddr(&port->raddr.addr,
+ &TrustedProxyServers[i + 1],
+ &TrustedProxyServers[i + 2]))
+ {
+ useproxy = true;
+ break;
+ }
+ }
+ if (!useproxy)
+ {
+ /*
+ * Connection is not from one of our trusted proxies, so reject it.
+ */
+ return STATUS_OK;
+ }
+
+ /* Store a copy of the original address, for logging */
+ memcpy(&raddr_save, &port->raddr, port->raddr.salen);
+
+ pq_startmsgread();
+
+ /*
+ * PROXY requests always start with: \x0D \x0A \x0D \x0A \x00 \x0D \x0A
+ * \x51 \x55 \x49 \x54 \x0A
+ */
+
+ if (pq_getbytes((char *) &proxyheader, sizeof(proxyheader)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ if (memcmp(proxyheader.sig, "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a", sizeof(proxyheader.sig)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Proxy version is in the high 4 bits of the first byte */
+ proxyver = (proxyheader.ver_cmd & 0xF0) >> 4;
+ if (proxyver != 2)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol version: %x", proxyver)));
+ return STATUS_ERROR;
+ }
+
+ proxyaddrlen = pg_ntoh16(proxyheader.len);
+
+ if (proxyaddrlen > sizeof(proxyaddr))
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("oversized proxy packet")));
+ return STATUS_ERROR;
+ }
+ if (pq_getbytes((char *) &proxyaddr, proxyaddrlen) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Lower 4 bits hold type of connection */
+ if (proxyheader.fam == 0)
+ {
+ /* LOCAL connection, so we ignore the address included */
+ }
+ else if (proxyheader.fam == 0x11)
+ {
+ /* TCPv4 */
+ port->raddr.addr.ss_family = AF_INET;
+ port->raddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
+ }
+ else if (proxyheader.fam == 0x21)
+ {
+ /* TCPv6 */
+ port->raddr.addr.ss_family = AF_INET6;
+ port->raddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->raddr.addr)->sin6_addr, proxyaddr.ip6.src_addr, 16);
+ ((struct sockaddr_in6 *) &port->raddr.addr)->sin6_port = proxyaddr.ip6.src_port;
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol connection type: %x", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+
+ /* If there is any more header data present, skip past it */
+ if (proxyaddrlen > sizeof(proxyaddr))
+ pq_discardbytes(proxyaddrlen - sizeof(proxyaddr));
+
+
+ pq_endmsgread();
+
+ /*
+ * Log what we've done if connection logging is enabled. We log the proxy
+ * connection here, and let the normal connection logging mechanism log
+ * the unwrapped connection.
+ */
+ if (Log_connections)
+ {
+ char remote_host[NI_MAXHOST];
+ char remote_port[NI_MAXSERV];
+ int ret;
+
+ remote_host[0] = '\0';
+ remote_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&raddr_save.addr, raddr_save.salen,
+ remote_host, sizeof(remote_host),
+ remote_port, sizeof(remote_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ ereport(LOG,
+ (errmsg("proxy connection from: host=%s port=%s",
+ remote_host,
+ remote_port)));
+
+ }
+
+ return STATUS_OK;
+}
/*
* Read a client's startup packet and do something according to it.
@@ -4344,6 +4527,33 @@ BackendInitialize(Port *port)
InitializeTimeouts(); /* establishes SIGALRM handler */
PG_SETMASK(&StartupBlockSig);
+ /*
+ * Ready to begin client interaction. We will give up and _exit(1) after
+ * a time delay, so that a broken client can't hog a connection
+ * indefinitely. PreAuthDelay and any DNS interactions above don't count
+ * against the time limit.
+ *
+ * Note: AuthenticationTimeout is applied here while waiting for the
+ * startup packet, and then again in InitPostgres for the duration of any
+ * authentication operations. So a hostile client could tie up the
+ * process for nearly twice AuthenticationTimeout before we kick him off.
+ *
+ * Note: because PostgresMain will call InitializeTimeouts again, the
+ * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
+ * since we never use it again after this function.
+ */
+ RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
+
+ /* Check if this is a proxy connection and if so unwrap the proxying */
+ if (TrustedProxyServers)
+ {
+ if (UnwrapProxyConnection(port) != STATUS_OK)
+ proc_exit(0);
+ }
+
+ disable_timeout(STARTUP_PACKET_TIMEOUT, false);
+
/*
* Get the remote host name and port for logging and status display.
*/
@@ -4395,28 +4605,11 @@ BackendInitialize(Port *port)
strspn(remote_host, "0123456789ABCDEFabcdef:") < strlen(remote_host))
port->remote_hostname = strdup(remote_host);
- /*
- * Ready to begin client interaction. We will give up and _exit(1) after
- * a time delay, so that a broken client can't hog a connection
- * indefinitely. PreAuthDelay and any DNS interactions above don't count
- * against the time limit.
- *
- * Note: AuthenticationTimeout is applied here while waiting for the
- * startup packet, and then again in InitPostgres for the duration of any
- * authentication operations. So a hostile client could tie up the
- * process for nearly twice AuthenticationTimeout before we kick him off.
- *
- * Note: because PostgresMain will call InitializeTimeouts again, the
- * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
- * since we never use it again after this function.
- */
- RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
- enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
-
/*
* Receive the startup packet (which might turn out to be a cancel request
* packet).
*/
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
status = ProcessStartupPacket(port, false, false);
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d626731723..381067b737 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -46,10 +46,12 @@
#include "commands/user.h"
#include "commands/vacuum.h"
#include "commands/variable.h"
+#include "common/ip.h"
#include "common/string.h"
#include "funcapi.h"
#include "jit/jit.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
@@ -227,6 +229,8 @@ static bool check_recovery_target_lsn(char **newval, void **extra, GucSource sou
static void assign_recovery_target_lsn(const char *newval, void *extra);
static bool check_primary_slot_name(char **newval, void **extra, GucSource source);
static bool check_default_with_oids(bool *newval, void **extra, GucSource source);
+static bool check_proxy_servers(char **newval, void **extra, GucSource source);
+static void assign_proxy_servers(const char *newval, void *extra);
/* Private functions in guc-file.l that need to be called from guc.c */
static ConfigVariable *ProcessConfigFileInternal(GucContext context,
@@ -4241,6 +4245,17 @@ static struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_servers", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the addresses for trusted proxy servers."),
+ NULL,
+ GUC_LIST_INPUT
+ },
+ &TrustedProxyServersString,
+ "",
+ check_proxy_servers, assign_proxy_servers, NULL
+ },
+
{
/*
* Can't be set by ALTER SYSTEM as it can lead to recursive definition
@@ -12228,4 +12243,108 @@ check_default_with_oids(bool *newval, void **extra, GucSource source)
return true;
}
+static bool
+check_proxy_servers(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ struct sockaddr_storage *myextra;
+
+ /* Special case when it's empty */
+ if (**newval == '\0')
+ {
+ *extra = NULL;
+ return true;
+ }
+
+ /* Need a modifiable copy of string */
+ rawstring = pstrdup(*newval);
+
+ /* Parse string into list of identifiers */
+ if (!SplitIdentifierString(rawstring, ',', &elemlist))
+ {
+ /* syntax error in list */
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ if (list_length(elemlist) == 0)
+ {
+ /* If it had only whitespace */
+ pfree(rawstring);
+ list_free(elemlist);
+
+ *extra = NULL;
+ return true;
+ }
+
+ /*
+ * We store the result in an array of sockaddr_storage. The first entry is
+ * just an overloaded int which holds the size of the array.
+ */
+ myextra = (struct sockaddr_storage *) guc_malloc(ERROR, sizeof(struct sockaddr_storage) * (list_length(elemlist) * 2 + 1));
+ *((int *) &myextra[0]) = list_length(elemlist);
+
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+ char *netmasktok = NULL;
+ int ret;
+ struct addrinfo *gai_result;
+ struct addrinfo hints;
+
+ netmasktok = strchr(tok, '/');
+ if (netmasktok)
+ {
+ *netmasktok = '\0';
+ netmasktok++;
+ }
+
+ memset((char *) &hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ ret = pg_getaddrinfo_all(tok, NULL, &hints, &gai_result);
+ if (ret != 0 || gai_result == NULL)
+ {
+ GUC_check_errdetail("Invalid IP addrress %s", tok);
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+
+ memcpy((char *) &myextra[foreach_current_index(l) * 2 + 1], gai_result->ai_addr, gai_result->ai_addrlen);
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+
+ /* A NULL netmasktok means the fully set hostmask */
+ if (pg_sockaddr_cidr_mask(&myextra[foreach_current_index(l) * 2 + 2], netmasktok, myextra[foreach_current_index(l) * 2 + 1].ss_family) != 0)
+ {
+ if (netmasktok)
+ GUC_check_errdetail("Invalid netmask %s", netmasktok);
+ else
+ GUC_check_errdetail("Could not create netmask");
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+ }
+
+ pfree(rawstring);
+ list_free(elemlist);
+ *extra = (void *) myextra;
+
+ return true;
+}
+
+static void
+assign_proxy_servers(const char *newval, void *extra)
+{
+ TrustedProxyServers = (struct sockaddr_storage *) extra;
+}
+
#include "guc-file.c"
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ee06528bb0..aa7ac35f67 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -61,6 +61,8 @@
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
+#proxy_servers = '' # what IP/netmasks of proxy servers to trust
+ # (change requires restart)
#max_connections = 100 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
#unix_socket_directories = '/tmp' # comma-separated list of directories
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index e4e5c21565..6125b93e86 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -74,6 +74,8 @@ extern bool pq_is_reading_msg(void);
extern int pq_getmessage(StringInfo s, int maxlen);
extern int pq_getbyte(void);
extern int pq_peekbyte(void);
+extern int pq_peekbytes(char *s, size_t len);
+extern int pq_discardbytes(size_t len);
extern int pq_getbyte_if_available(unsigned char *c);
extern int pq_putbytes(const char *s, size_t len);
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index cfa59c4dc0..38e7644371 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -21,6 +21,8 @@ extern int Unix_socket_permissions;
extern char *Unix_socket_group;
extern char *Unix_socket_directories;
extern char *ListenAddresses;
+extern char *TrustedProxyServersString;
+extern struct sockaddr_storage *TrustedProxyServers;
extern bool ClientAuthInProgress;
extern int PreAuthDelay;
extern int AuthenticationTimeout;
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-03-03 09:39 ` Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
0 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2021-03-03 09:39 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Wed, Mar 3, 2021 at 10:00 AM Magnus Hagander <magnus@hagander.net> wrote:
>
> On Wed, Mar 3, 2021 at 1:50 AM Jacob Champion <pchampion@vmware.com> wrote:
> >
> > On Tue, 2021-03-02 at 18:43 +0100, Magnus Hagander wrote:
> > > PFA a simple patch that implements support for the PROXY protocol.
> >
> > I'm not all the way through the patch yet, but this part jumped out at
> > me:
> >
> > > + if (memcmp(proxyheader.sig, "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a", sizeof(proxyheader.sig)) != 0)
> > > + {
> > > + /*
> > > + * Data is there but it wasn't a proxy header. Also fall through to
> > > + * normal processing
> > > + */
> > > + pq_endmsgread();
> > > + return STATUS_OK;
> >
> > From my reading, the spec explicitly disallows this sort of fallback
> > behavior:
> >
> > > The receiver MUST be configured to only receive the protocol described in this
> > > specification and MUST not try to guess whether the protocol header is present
> > > or not. This means that the protocol explicitly prevents port sharing between
> > > public and private access.
> >
> > You might say, "if we already trust the proxy server, why should we
> > care?" but I think the point is that you want to catch
> > misconfigurations where the middlebox is forwarding bare TCP without
> > adding a PROXY header of its own, which will "work" for innocent
> > clients but in reality is a ticking timebomb. If you've decided to
> > trust an intermediary to use PROXY connections, then you must _only_
> > accept PROXY connections from that intermediary. Does that seem like a
> > reasonable interpretation?
>
> I definitely missed that part of the spec. Ugh.
>
> That said, I'm not sure it's *actually* an issue in the case of
> PostgreSQL. Given that doing what you're suggesting, accidentally
> passing connections without PROXY, will get caught in pg_hba.conf.
>
> That said, I agree with your interpretation, and it's pretty easy to
> change it to that. Basically we just have to do the IP check *before*
> doing the PROXY protocol check. It makes testing a bit more difficult
> though, but maybe worth it?
>
> I've attached a POC that does that. Note that I have *not* updated the docs!
>
> Another option would of course be to listen on a separate port for it,
> which seems to be the "haproxy way". That would be slightly more code
> (we'd still want to keep the code for validating the list of trusted
> proxies I'd say), but maybe worth doing?
In order to figure that out, I hacked up a poc on that. Once again
without updates to the docs, but shows approximately how much code
complexity it adds (not much).
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
Attachments:
[text/x-patch] proxy_protocol_separate_port.patch (23.1K, ../../CABUevEzh6AoLxOvAoZf4WgNYoQv1OReb=2_XE49o-wturqUYvw@mail.gmail.com/2-proxy_protocol_separate_port.patch)
download | inline diff:
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index b420486a0a..d4f6fad5b0 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -353,6 +353,15 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl
the client's host name instead of the IP address in the log.
</para>
+ <para>
+ If <xref linkend="guc-proxy-servers"/> is enabled and the
+ connection is made through a proxy server using the PROXY
+ protocol, the actual IP address of the client will be used
+ for matching. If a connection is made through a proxy server
+ not using the PROXY protocol, the IP address of the
+ proxy server will be used.
+ </para>
+
<para>
These fields do not apply to <literal>local</literal> records.
</para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b5718fc136..fc7de25378 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -682,6 +682,30 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-proxy-servers" xreflabel="proxy_servers">
+ <term><varname>proxy_servers</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>proxy_servers</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ A comma separated list of one or more host names or cidr specifications
+ of proxy servers to trust. If a connection using the PROXY protocol is made
+ from one of these IP addresses, <productname>PostgreSQL</productname> will
+ read the client IP address from the PROXY header and consider that the
+ address of the client, instead of listing all connections as coming from
+ the proxy server.
+ </para>
+ <para>
+ If a proxy connection is made from an IP address not covered by this
+ list, the connection will be rejected. By default no proxy is trusted
+ and all proxy connections will be rejected. This parameter can only
+ be set at server start.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-max-connections" xreflabel="max_connections">
<term><varname>max_connections</varname> (<type>integer</type>)
<indexterm>
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 27a298f110..401f2d2464 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -53,6 +53,7 @@
* pq_getmessage - get a message with length word from connection
* pq_getbyte - get next byte from connection
* pq_peekbyte - peek at next byte from connection
+ * pq_peekbytes - peek at a known number of bytes from connection
* pq_putbytes - send bytes to connection (not flushed until pq_flush)
* pq_flush - flush pending output
* pq_flush_if_writable - flush pending output if writable without blocking
@@ -336,7 +337,7 @@ socket_close(int code, Datum arg)
int
StreamServerPort(int family, const char *hostName, unsigned short portNumber,
const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen)
+ pgsocket ListenSocket[], bool ProxyList[], bool isProxy, int MaxListen)
{
pgsocket fd;
int err;
@@ -602,6 +603,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
familyDesc, addrDesc, (int) portNumber)));
ListenSocket[listen_index] = fd;
+ ProxyList[listen_index] = isProxy;
added++;
}
@@ -1039,6 +1041,27 @@ pq_peekbyte(void)
return (unsigned char) PqRecvBuffer[PqRecvPointer];
}
+
+/* --------------------------------
+ * pq_peekbytes - peek at a known number of bytes from connection.
+ * Note! Does NOT wait for more data to arrive.
+ *
+ * returns 0 if OK, EOF if trouble
+ * --------------------------------
+ */
+int
+pq_peekbytes(char *s, size_t len)
+{
+ Assert(PqCommReadingMsg);
+
+ if (PqRecvLength - PqRecvPointer < len)
+ return EOF;
+
+ memcpy(s, PqRecvBuffer + PqRecvPointer, len);
+
+ return 0;
+}
+
/* --------------------------------
* pq_getbyte_if_available - get a single byte from connection,
* if available
@@ -1135,7 +1158,7 @@ pq_getbytes(char *s, size_t len)
* returns 0 if OK, EOF if trouble
* --------------------------------
*/
-static int
+int
pq_discardbytes(size_t len)
{
size_t amount;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 3f1ce135a8..5b1e4f5592 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -102,6 +102,7 @@
#include "common/string.h"
#include "lib/ilist.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
@@ -195,15 +196,22 @@ BackgroundWorker *MyBgworkerEntry = NULL;
-/* The socket number we are listening for connections on */
+/* The TCP port number we are listening for connections on */
int PostPortNumber;
+/* The TCP port number we are listening for proxy connections on */
+int ProxyPortNumber;
+
/* The directory names for Unix socket(s) */
char *Unix_socket_directories;
/* The TCP listen address(es) */
char *ListenAddresses;
+/* Trusted proxy servers */
+char *TrustedProxyServersString = NULL;
+struct sockaddr_storage *TrustedProxyServers = NULL;
+
/*
* ReservedBackends is the number of backends reserved for superuser use.
* This number is taken out of the pool size given by MaxConnections so
@@ -218,6 +226,7 @@ int ReservedBackends;
/* The socket(s) we're listening to. */
#define MAXLISTEN 64
static pgsocket ListenSocket[MAXLISTEN];
+static bool ListenSocketIsProxy[MAXLISTEN];
/*
* These globals control the behavior of the postmaster in case some
@@ -1124,7 +1133,10 @@ PostmasterMain(int argc, char *argv[])
* charged with closing the sockets again at postmaster shutdown.
*/
for (i = 0; i < MAXLISTEN; i++)
+ {
ListenSocket[i] = PGINVALID_SOCKET;
+ ListenSocketIsProxy[i] = false;
+ }
on_proc_exit(CloseServerPorts, 0);
@@ -1156,12 +1168,14 @@ PostmasterMain(int argc, char *argv[])
status = StreamServerPort(AF_UNSPEC, NULL,
(unsigned short) PostPortNumber,
NULL,
- ListenSocket, MAXLISTEN);
+ ListenSocket, ListenSocketIsProxy,
+ false, MAXLISTEN);
else
status = StreamServerPort(AF_UNSPEC, curhost,
(unsigned short) PostPortNumber,
NULL,
- ListenSocket, MAXLISTEN);
+ ListenSocket, ListenSocketIsProxy,
+ false, MAXLISTEN);
if (status == STATUS_OK)
{
@@ -1177,6 +1191,27 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create listen socket for \"%s\"",
curhost)));
+
+ /* Also listen to the PROXY port on this address, if configured */
+ if (ProxyPortNumber)
+ {
+ if (strcmp(curhost, "*") == 0)
+ status = StreamServerPort(AF_UNSPEC, NULL,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, ListenSocketIsProxy,
+ true, MAXLISTEN);
+ else
+ status = StreamServerPort(AF_UNSPEC, curhost,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, ListenSocketIsProxy,
+ true, MAXLISTEN);
+ if (status != STATUS_OK)
+ ereport(WARNING,
+ (errmsg("could not create PROXY listen socket for \"%s\"",
+ curhost)));
+ }
}
if (!success && elemlist != NIL)
@@ -1254,7 +1289,8 @@ PostmasterMain(int argc, char *argv[])
status = StreamServerPort(AF_UNIX, NULL,
(unsigned short) PostPortNumber,
socketdir,
- ListenSocket, MAXLISTEN);
+ ListenSocket, ListenSocketIsProxy,
+ false, MAXLISTEN);
if (status == STATUS_OK)
{
@@ -1731,6 +1767,8 @@ ServerLoop(void)
port = ConnCreate(ListenSocket[i]);
if (port)
{
+ port->isProxy = ListenSocketIsProxy[i];
+
BackendStartup(port);
/*
@@ -1911,6 +1949,190 @@ initMasks(fd_set *rmask)
return maxsock + 1;
}
+static int
+UnwrapProxyConnection(Port *port)
+{
+ char proxyver;
+ uint16 proxyaddrlen;
+ SockAddr raddr_save;
+ int i;
+ bool useproxy = false;
+
+ /*
+ * These structs are from the PROXY protocol docs at
+ * http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
+ */
+ union
+ {
+ struct
+ { /* for TCP/UDP over IPv4, len = 12 */
+ uint32 src_addr;
+ uint32 dst_addr;
+ uint16 src_port;
+ uint16 dst_port;
+ } ip4;
+ struct
+ { /* for TCP/UDP over IPv6, len = 36 */
+ uint8 src_addr[16];
+ uint8 dst_addr[16];
+ uint16 src_port;
+ uint16 dst_port;
+ } ip6;
+ } proxyaddr;
+ struct
+ {
+ uint8 sig[12]; /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */
+ uint8 ver_cmd; /* protocol version and command */
+ uint8 fam; /* protocol family and address */
+ uint16 len; /* number of following bytes part of the
+ * header */
+ } proxyheader;
+
+
+ if (TrustedProxyServers)
+ {
+ for (i = 0; i < *((int *) TrustedProxyServers); i += 2)
+ {
+ if (port->raddr.addr.ss_family == TrustedProxyServers[i + 1].ss_family &&
+ pg_range_sockaddr(&port->raddr.addr,
+ &TrustedProxyServers[i + 1],
+ &TrustedProxyServers[i + 2]))
+ {
+ useproxy = true;
+ break;
+ }
+ }
+ }
+ if (!useproxy)
+ {
+ /*
+ * Connection is not from one of our trusted proxies, so reject it.
+ */
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("connection from unauthorized proxy server")));
+ return STATUS_ERROR;
+ }
+
+ /* Store a copy of the original address, for logging */
+ memcpy(&raddr_save, &port->raddr, port->raddr.salen);
+
+ pq_startmsgread();
+
+ /*
+ * PROXY requests always start with: \x0D \x0A \x0D \x0A \x00 \x0D \x0A
+ * \x51 \x55 \x49 \x54 \x0A
+ */
+
+ if (pq_getbytes((char *) &proxyheader, sizeof(proxyheader)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ if (memcmp(proxyheader.sig, "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a", sizeof(proxyheader.sig)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Proxy version is in the high 4 bits of the first byte */
+ proxyver = (proxyheader.ver_cmd & 0xF0) >> 4;
+ if (proxyver != 2)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol version: %x", proxyver)));
+ return STATUS_ERROR;
+ }
+
+ proxyaddrlen = pg_ntoh16(proxyheader.len);
+
+ if (proxyaddrlen > sizeof(proxyaddr))
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("oversized proxy packet")));
+ return STATUS_ERROR;
+ }
+ if (pq_getbytes((char *) &proxyaddr, proxyaddrlen) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Lower 4 bits hold type of connection */
+ if (proxyheader.fam == 0)
+ {
+ /* LOCAL connection, so we ignore the address included */
+ }
+ else if (proxyheader.fam == 0x11)
+ {
+ /* TCPv4 */
+ port->raddr.addr.ss_family = AF_INET;
+ port->raddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
+ }
+ else if (proxyheader.fam == 0x21)
+ {
+ /* TCPv6 */
+ port->raddr.addr.ss_family = AF_INET6;
+ port->raddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->raddr.addr)->sin6_addr, proxyaddr.ip6.src_addr, 16);
+ ((struct sockaddr_in6 *) &port->raddr.addr)->sin6_port = proxyaddr.ip6.src_port;
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol connection type: %x", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+
+ /* If there is any more header data present, skip past it */
+ if (proxyaddrlen > sizeof(proxyaddr))
+ pq_discardbytes(proxyaddrlen - sizeof(proxyaddr));
+
+
+ pq_endmsgread();
+
+ /*
+ * Log what we've done if connection logging is enabled. We log the proxy
+ * connection here, and let the normal connection logging mechanism log
+ * the unwrapped connection.
+ */
+ if (Log_connections)
+ {
+ char remote_host[NI_MAXHOST];
+ char remote_port[NI_MAXSERV];
+ int ret;
+
+ remote_host[0] = '\0';
+ remote_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&raddr_save.addr, raddr_save.salen,
+ remote_host, sizeof(remote_host),
+ remote_port, sizeof(remote_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ ereport(LOG,
+ (errmsg("proxy connection from: host=%s port=%s",
+ remote_host,
+ remote_port)));
+
+ }
+
+ return STATUS_OK;
+}
/*
* Read a client's startup packet and do something according to it.
@@ -4344,6 +4566,33 @@ BackendInitialize(Port *port)
InitializeTimeouts(); /* establishes SIGALRM handler */
PG_SETMASK(&StartupBlockSig);
+ /*
+ * Ready to begin client interaction. We will give up and _exit(1) after
+ * a time delay, so that a broken client can't hog a connection
+ * indefinitely. PreAuthDelay and any DNS interactions above don't count
+ * against the time limit.
+ *
+ * Note: AuthenticationTimeout is applied here while waiting for the
+ * startup packet, and then again in InitPostgres for the duration of any
+ * authentication operations. So a hostile client could tie up the
+ * process for nearly twice AuthenticationTimeout before we kick him off.
+ *
+ * Note: because PostgresMain will call InitializeTimeouts again, the
+ * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
+ * since we never use it again after this function.
+ */
+ RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
+
+ /* Check if this is a proxy connection and if so unwrap the proxying */
+ if (port->isProxy)
+ {
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
+ if (UnwrapProxyConnection(port) != STATUS_OK)
+ proc_exit(0);
+ disable_timeout(STARTUP_PACKET_TIMEOUT, false);
+ }
+
+
/*
* Get the remote host name and port for logging and status display.
*/
@@ -4395,28 +4644,11 @@ BackendInitialize(Port *port)
strspn(remote_host, "0123456789ABCDEFabcdef:") < strlen(remote_host))
port->remote_hostname = strdup(remote_host);
- /*
- * Ready to begin client interaction. We will give up and _exit(1) after
- * a time delay, so that a broken client can't hog a connection
- * indefinitely. PreAuthDelay and any DNS interactions above don't count
- * against the time limit.
- *
- * Note: AuthenticationTimeout is applied here while waiting for the
- * startup packet, and then again in InitPostgres for the duration of any
- * authentication operations. So a hostile client could tie up the
- * process for nearly twice AuthenticationTimeout before we kick him off.
- *
- * Note: because PostgresMain will call InitializeTimeouts again, the
- * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
- * since we never use it again after this function.
- */
- RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
- enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
-
/*
* Receive the startup packet (which might turn out to be a cancel request
* packet).
*/
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
status = ProcessStartupPacket(port, false, false);
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d626731723..9be50b1532 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -46,10 +46,12 @@
#include "commands/user.h"
#include "commands/vacuum.h"
#include "commands/variable.h"
+#include "common/ip.h"
#include "common/string.h"
#include "funcapi.h"
#include "jit/jit.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
@@ -227,6 +229,8 @@ static bool check_recovery_target_lsn(char **newval, void **extra, GucSource sou
static void assign_recovery_target_lsn(const char *newval, void *extra);
static bool check_primary_slot_name(char **newval, void **extra, GucSource source);
static bool check_default_with_oids(bool *newval, void **extra, GucSource source);
+static bool check_proxy_servers(char **newval, void **extra, GucSource source);
+static void assign_proxy_servers(const char *newval, void *extra);
/* Private functions in guc-file.l that need to be called from guc.c */
static ConfigVariable *ProcessConfigFileInternal(GucContext context,
@@ -2290,6 +2294,16 @@ static struct config_int ConfigureNamesInt[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_port", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the TCP port the server listens for PROXY connections on."),
+ NULL
+ },
+ &ProxyPortNumber,
+ 0, 0, 65535,
+ NULL, NULL, NULL
+ },
+
{
{"unix_socket_permissions", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
gettext_noop("Sets the access permissions of the Unix-domain socket."),
@@ -4241,6 +4255,17 @@ static struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_servers", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the addresses for trusted proxy servers."),
+ NULL,
+ GUC_LIST_INPUT
+ },
+ &TrustedProxyServersString,
+ "",
+ check_proxy_servers, assign_proxy_servers, NULL
+ },
+
{
/*
* Can't be set by ALTER SYSTEM as it can lead to recursive definition
@@ -12228,4 +12253,108 @@ check_default_with_oids(bool *newval, void **extra, GucSource source)
return true;
}
+static bool
+check_proxy_servers(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ struct sockaddr_storage *myextra;
+
+ /* Special case when it's empty */
+ if (**newval == '\0')
+ {
+ *extra = NULL;
+ return true;
+ }
+
+ /* Need a modifiable copy of string */
+ rawstring = pstrdup(*newval);
+
+ /* Parse string into list of identifiers */
+ if (!SplitIdentifierString(rawstring, ',', &elemlist))
+ {
+ /* syntax error in list */
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ if (list_length(elemlist) == 0)
+ {
+ /* If it had only whitespace */
+ pfree(rawstring);
+ list_free(elemlist);
+
+ *extra = NULL;
+ return true;
+ }
+
+ /*
+ * We store the result in an array of sockaddr_storage. The first entry is
+ * just an overloaded int which holds the size of the array.
+ */
+ myextra = (struct sockaddr_storage *) guc_malloc(ERROR, sizeof(struct sockaddr_storage) * (list_length(elemlist) * 2 + 1));
+ *((int *) &myextra[0]) = list_length(elemlist);
+
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+ char *netmasktok = NULL;
+ int ret;
+ struct addrinfo *gai_result;
+ struct addrinfo hints;
+
+ netmasktok = strchr(tok, '/');
+ if (netmasktok)
+ {
+ *netmasktok = '\0';
+ netmasktok++;
+ }
+
+ memset((char *) &hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ ret = pg_getaddrinfo_all(tok, NULL, &hints, &gai_result);
+ if (ret != 0 || gai_result == NULL)
+ {
+ GUC_check_errdetail("Invalid IP addrress %s", tok);
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+
+ memcpy((char *) &myextra[foreach_current_index(l) * 2 + 1], gai_result->ai_addr, gai_result->ai_addrlen);
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+
+ /* A NULL netmasktok means the fully set hostmask */
+ if (pg_sockaddr_cidr_mask(&myextra[foreach_current_index(l) * 2 + 2], netmasktok, myextra[foreach_current_index(l) * 2 + 1].ss_family) != 0)
+ {
+ if (netmasktok)
+ GUC_check_errdetail("Invalid netmask %s", netmasktok);
+ else
+ GUC_check_errdetail("Could not create netmask");
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+ }
+
+ pfree(rawstring);
+ list_free(elemlist);
+ *extra = (void *) myextra;
+
+ return true;
+}
+
+static void
+assign_proxy_servers(const char *newval, void *extra)
+{
+ TrustedProxyServers = (struct sockaddr_storage *) extra;
+}
+
#include "guc-file.c"
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ee06528bb0..aa7ac35f67 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -61,6 +61,8 @@
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
+#proxy_servers = '' # what IP/netmasks of proxy servers to trust
+ # (change requires restart)
#max_connections = 100 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
#unix_socket_directories = '/tmp' # comma-separated list of directories
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 7be1a67d69..57edda122a 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -121,6 +121,7 @@ typedef struct Port
{
pgsocket sock; /* File descriptor */
bool noblock; /* is the socket in non-blocking mode? */
+ bool isProxy; /* is the connection using PROXY protocol */
ProtocolVersion proto; /* FE/BE protocol version */
SockAddr laddr; /* local addr (postmaster) */
SockAddr raddr; /* remote addr (client) */
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index e4e5c21565..549e2d86a7 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -60,7 +60,7 @@ extern WaitEventSet *FeBeWaitSet;
extern int StreamServerPort(int family, const char *hostName,
unsigned short portNumber, const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen);
+ pgsocket ListenSocket[], bool ProxyList[], bool isProxy, int MaxListen);
extern int StreamConnection(pgsocket server_fd, Port *port);
extern void StreamClose(pgsocket sock);
extern void TouchSocketFiles(void);
@@ -74,6 +74,8 @@ extern bool pq_is_reading_msg(void);
extern int pq_getmessage(StringInfo s, int maxlen);
extern int pq_getbyte(void);
extern int pq_peekbyte(void);
+extern int pq_peekbytes(char *s, size_t len);
+extern int pq_discardbytes(size_t len);
extern int pq_getbyte_if_available(unsigned char *c);
extern int pq_putbytes(const char *s, size_t len);
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index cfa59c4dc0..9ed219dfda 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -17,10 +17,13 @@
extern bool EnableSSL;
extern int ReservedBackends;
extern PGDLLIMPORT int PostPortNumber;
+extern PGDLLIMPORT int ProxyPortNumber;
extern int Unix_socket_permissions;
extern char *Unix_socket_group;
extern char *Unix_socket_directories;
extern char *ListenAddresses;
+extern char *TrustedProxyServersString;
+extern struct sockaddr_storage *TrustedProxyServers;
extern bool ClientAuthInProgress;
extern int PreAuthDelay;
extern int AuthenticationTimeout;
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-03-04 20:07 ` Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 1 reply; 56+ messages in thread
From: Jacob Champion @ 2021-03-04 20:07 UTC (permalink / raw)
To: magnus@hagander.net <magnus@hagander.net>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Wed, 2021-03-03 at 10:39 +0100, Magnus Hagander wrote:
> On Wed, Mar 3, 2021 at 10:00 AM Magnus Hagander <magnus@hagander.net> wrote:
> > Another option would of course be to listen on a separate port for it,
> > which seems to be the "haproxy way". That would be slightly more code
> > (we'd still want to keep the code for validating the list of trusted
> > proxies I'd say), but maybe worth doing?
>
> In order to figure that out, I hacked up a poc on that. Once again
> without updates to the docs, but shows approximately how much code
> complexity it adds (not much).
From a configuration perspective, I like that the separate-port
approach can shift the burden of verifying trust to an external
firewall, and that it seems to match the behavior of other major server
software. But I don't have any insight into the relative security of
the two options in practice; hopefully someone else can chime in.
> memset((char *) &hints, 0, sizeof(hints));
> hints.ai_flags = AI_NUMERICHOST;
> hints.ai_family = AF_UNSPEC;
>
> ret = pg_getaddrinfo_all(tok, NULL, &hints, &gai_result);
Idle thought I had while setting up a local test rig: Are there any
compelling cases for allowing PROXY packets to arrive over Unix
sockets? (By which I mean, the proxy is running on the same machine as
Postgres, and connects to it using the .s.PGSQL socket file instead of
TCP.) Are there cases where you want some other software to interact
with the TCP stack instead of Postgres, but it'd still be nice to have
the original connection information available?
--Jacob
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
@ 2021-03-04 20:45 ` Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
0 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2021-03-04 20:45 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Thu, Mar 4, 2021 at 9:07 PM Jacob Champion <pchampion@vmware.com> wrote:
>
> On Wed, 2021-03-03 at 10:39 +0100, Magnus Hagander wrote:
> > On Wed, Mar 3, 2021 at 10:00 AM Magnus Hagander <magnus@hagander.net> wrote:
> > > Another option would of course be to listen on a separate port for it,
> > > which seems to be the "haproxy way". That would be slightly more code
> > > (we'd still want to keep the code for validating the list of trusted
> > > proxies I'd say), but maybe worth doing?
> >
> > In order to figure that out, I hacked up a poc on that. Once again
> > without updates to the docs, but shows approximately how much code
> > complexity it adds (not much).
>
> From a configuration perspective, I like that the separate-port
> approach can shift the burden of verifying trust to an external
> firewall, and that it seems to match the behavior of other major server
> software. But I don't have any insight into the relative security of
> the two options in practice; hopefully someone else can chime in.
Yeah I think that and the argument that the spec explicitly says it
should be on it's own port is the advantage. The disadvantage is,
well, more ports and more configuration. But it does definitely make a
more clean separation of concerns.
> > memset((char *) &hints, 0, sizeof(hints));
> > hints.ai_flags = AI_NUMERICHOST;
> > hints.ai_family = AF_UNSPEC;
> >
> > ret = pg_getaddrinfo_all(tok, NULL, &hints, &gai_result);
>
> Idle thought I had while setting up a local test rig: Are there any
> compelling cases for allowing PROXY packets to arrive over Unix
> sockets? (By which I mean, the proxy is running on the same machine as
> Postgres, and connects to it using the .s.PGSQL socket file instead of
> TCP.) Are there cases where you want some other software to interact
> with the TCP stack instead of Postgres, but it'd still be nice to have
> the original connection information available?
I'm uncertain what that usecase would be for something like haproxy,
tbh. It can't do connection pooling, so adding it on the same machine
as postgres itself wouldn't really add anything, I think?
Iid think about the other end, if you had a proxy on a different
machine accepting unix connections and passing them on over
PROXY-over-tcp. But I doubt it's useful to know it was unix in that
case (since it still couldn't do peer or such for the auth) --
instead, that seems like an argument where it'd be better to proxy
without using PROXY and just letting the IP address be.
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-03-04 23:21 ` Jacob Champion <pchampion@vmware.com>
2021-03-04 23:57 ` Re: PROXY protocol support Hannu Krosing <hannuk@google.com>
2021-03-05 00:33 ` Re: PROXY protocol support Álvaro Hernández <aht@ongres.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 3 replies; 56+ messages in thread
From: Jacob Champion @ 2021-03-04 23:21 UTC (permalink / raw)
To: magnus@hagander.net <magnus@hagander.net>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Thu, 2021-03-04 at 21:45 +0100, Magnus Hagander wrote:
> On Thu, Mar 4, 2021 at 9:07 PM Jacob Champion <pchampion@vmware.com> wrote:
> > Idle thought I had while setting up a local test rig: Are there any
> > compelling cases for allowing PROXY packets to arrive over Unix
> > sockets? (By which I mean, the proxy is running on the same machine as
> > Postgres, and connects to it using the .s.PGSQL socket file instead of
> > TCP.) Are there cases where you want some other software to interact
> > with the TCP stack instead of Postgres, but it'd still be nice to have
> > the original connection information available?
>
> I'm uncertain what that usecase would be for something like haproxy,
> tbh. It can't do connection pooling, so adding it on the same machine
> as postgres itself wouldn't really add anything, I think?
Yeah, I wasn't thinking HAproxy so much as some unspecified software
appliance that's performing Some Task before allowing a TCP client to
speak to Postgres. But it'd be better to hear from someone that has an
actual use case, instead of me spitballing.
> Iid think about the other end, if you had a proxy on a different
> machine accepting unix connections and passing them on over
> PROXY-over-tcp. But I doubt it's useful to know it was unix in that
> case (since it still couldn't do peer or such for the auth) --
> instead, that seems like an argument where it'd be better to proxy
> without using PROXY and just letting the IP address be.
You could potentially design a system that lets you proxy a "local all
all trust" setup from a different (trusted) machine, without having to
actually let people onto the machine that's running Postgres. That
would require some additional authentication on the PROXY connection
(i.e. something stronger than host-based auth) to actually be useful.
-- other notes --
A small nitpick on the current separate-port PoC is that I'm forced to
set up a "regular" TCP port, even if I only want the PROXY behavior.
The original-host logging isn't working for me:
WARNING: pg_getnameinfo_all() failed: ai_family not supported
LOG: proxy connection from: host=??? port=???
and I think the culprit is this:
> /* Store a copy of the original address, for logging */
> memcpy(&raddr_save, &port->raddr, port->raddr.salen);
port->raddr.salen is the length of port->raddr.addr; we want the length
of the copy to be sizeof(port->raddr) here, no?
--Jacob
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
@ 2021-03-04 23:57 ` Hannu Krosing <hannuk@google.com>
2021-03-05 08:59 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2 siblings, 1 reply; 56+ messages in thread
From: Hannu Krosing @ 2021-03-04 23:57 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; +Cc: magnus@hagander.net <magnus@hagander.net>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
The current proposal seems to miss the case of transaction pooling
(and statement pooling) where the same established connection
multiplexes transactions / statements from multiple remote clients.
What we would need for that case would be a functionl
pg_set_remote_client_address( be_key, remote_ip, remote_hostname)
where only be_key and remote_ip are required, but any string (up to a
certain length) would be accepted as hostname.
It would be really nice if we could send this request at protocol level but
if that is hard to do then having a function would get us half way there.
the be_key in the function is the key from PGcancel, which is stored
by libpq when making the connection, and it is there, to make sure
that only the directly connecting proxy can successfully call the function.
Cheers
Hannu
On Fri, Mar 5, 2021 at 12:21 AM Jacob Champion <pchampion@vmware.com> wrote:
>
> On Thu, 2021-03-04 at 21:45 +0100, Magnus Hagander wrote:
> > On Thu, Mar 4, 2021 at 9:07 PM Jacob Champion <pchampion@vmware.com> wrote:
> > > Idle thought I had while setting up a local test rig: Are there any
> > > compelling cases for allowing PROXY packets to arrive over Unix
> > > sockets? (By which I mean, the proxy is running on the same machine as
> > > Postgres, and connects to it using the .s.PGSQL socket file instead of
> > > TCP.) Are there cases where you want some other software to interact
> > > with the TCP stack instead of Postgres, but it'd still be nice to have
> > > the original connection information available?
> >
> > I'm uncertain what that usecase would be for something like haproxy,
> > tbh. It can't do connection pooling, so adding it on the same machine
> > as postgres itself wouldn't really add anything, I think?
>
> Yeah, I wasn't thinking HAproxy so much as some unspecified software
> appliance that's performing Some Task before allowing a TCP client to
> speak to Postgres. But it'd be better to hear from someone that has an
> actual use case, instead of me spitballing.
>
> > Iid think about the other end, if you had a proxy on a different
> > machine accepting unix connections and passing them on over
> > PROXY-over-tcp. But I doubt it's useful to know it was unix in that
> > case (since it still couldn't do peer or such for the auth) --
> > instead, that seems like an argument where it'd be better to proxy
> > without using PROXY and just letting the IP address be.
>
> You could potentially design a system that lets you proxy a "local all
> all trust" setup from a different (trusted) machine, without having to
> actually let people onto the machine that's running Postgres. That
> would require some additional authentication on the PROXY connection
> (i.e. something stronger than host-based auth) to actually be useful.
>
> -- other notes --
>
> A small nitpick on the current separate-port PoC is that I'm forced to
> set up a "regular" TCP port, even if I only want the PROXY behavior.
>
> The original-host logging isn't working for me:
>
> WARNING: pg_getnameinfo_all() failed: ai_family not supported
> LOG: proxy connection from: host=??? port=???
>
> and I think the culprit is this:
>
> > /* Store a copy of the original address, for logging */
> > memcpy(&raddr_save, &port->raddr, port->raddr.salen);
>
> port->raddr.salen is the length of port->raddr.addr; we want the length
> of the copy to be sizeof(port->raddr) here, no?
>
> --Jacob
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 23:57 ` Re: PROXY protocol support Hannu Krosing <hannuk@google.com>
@ 2021-03-05 08:59 ` Magnus Hagander <magnus@hagander.net>
0 siblings, 0 replies; 56+ messages in thread
From: Magnus Hagander @ 2021-03-05 08:59 UTC (permalink / raw)
To: Hannu Krosing <hannuk@google.com>; +Cc: Jacob Champion <pchampion@vmware.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Fri, Mar 5, 2021 at 12:57 AM Hannu Krosing <hannuk@google.com> wrote:
>
> The current proposal seems to miss the case of transaction pooling
> (and statement pooling) where the same established connection
> multiplexes transactions / statements from multiple remote clients.
Not at all.
The current proposal is there to implement the PROXY protocol. It
doesn't try to do anything with connection pooling at all.
Solving a similar problem for connection poolers would also definitely
be a useful thing, but it is entirely out of scope of this patch, and
is a completely separate implementation.
I'd definitely like to see that one solved as well, but let's look at
it on a different thread so we don't derail this one.
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
@ 2021-03-05 00:33 ` Álvaro Hernández <aht@ongres.com>
2021-03-05 09:03 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2 siblings, 1 reply; 56+ messages in thread
From: Álvaro Hernández @ 2021-03-05 00:33 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; magnus@hagander.net <magnus@hagander.net>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Fabrízio Mello <fabrizio@ongres.com>
On 5/3/21 0:21, Jacob Champion wrote:
> On Thu, 2021-03-04 at 21:45 +0100, Magnus Hagander wrote:
>> On Thu, Mar 4, 2021 at 9:07 PM Jacob Champion <pchampion@vmware.com> wrote:
>>> Idle thought I had while setting up a local test rig: Are there any
>>> compelling cases for allowing PROXY packets to arrive over Unix
>>> sockets? (By which I mean, the proxy is running on the same machine as
>>> Postgres, and connects to it using the .s.PGSQL socket file instead of
>>> TCP.) Are there cases where you want some other software to interact
>>> with the TCP stack instead of Postgres, but it'd still be nice to have
>>> the original connection information available?
>> I'm uncertain what that usecase would be for something like haproxy,
>> tbh. It can't do connection pooling, so adding it on the same machine
>> as postgres itself wouldn't really add anything, I think?
> Yeah, I wasn't thinking HAproxy so much as some unspecified software
> appliance that's performing Some Task before allowing a TCP client to
> speak to Postgres. But it'd be better to hear from someone that has an
> actual use case, instead of me spitballing.
Here's a use case: Envoy's Postgres filter (see [1], [2]). Right now
is able to capture protocol-level metrics and send them to a metrics
collector (eg. Prometheus) while proxying the traffic. More capabilities
are being added as of today, and will eventually manage HBA too. It
would greatly benefit from this proposal, since it proxies the traffic
with, obviously, its IP, not the client's. It may be used (we do)
locally fronting Postgres, via UDS (so it can be easily trusted).
Álvaro
[1]
https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/network_filters/postgres_proxy_f...
[2]
https://www.cncf.io/blog/2020/08/13/envoy-1-15-introduces-a-new-postgres-extension-with-monitoring-s...
--
Alvaro Hernandez
-----------
OnGres
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 00:33 ` Re: PROXY protocol support Álvaro Hernández <aht@ongres.com>
@ 2021-03-05 09:03 ` Magnus Hagander <magnus@hagander.net>
2021-03-05 13:49 ` Re: PROXY protocol support Álvaro Hernández <aht@ongres.com>
0 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2021-03-05 09:03 UTC (permalink / raw)
To: Álvaro Hernández <aht@ongres.com>; +Cc: Jacob Champion <pchampion@vmware.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Fabrízio Mello <fabrizio@ongres.com>
On Fri, Mar 5, 2021 at 1:33 AM Álvaro Hernández <aht@ongres.com> wrote:
>
>
>
> On 5/3/21 0:21, Jacob Champion wrote:
> > On Thu, 2021-03-04 at 21:45 +0100, Magnus Hagander wrote:
> >> On Thu, Mar 4, 2021 at 9:07 PM Jacob Champion <pchampion@vmware.com> wrote:
> >>> Idle thought I had while setting up a local test rig: Are there any
> >>> compelling cases for allowing PROXY packets to arrive over Unix
> >>> sockets? (By which I mean, the proxy is running on the same machine as
> >>> Postgres, and connects to it using the .s.PGSQL socket file instead of
> >>> TCP.) Are there cases where you want some other software to interact
> >>> with the TCP stack instead of Postgres, but it'd still be nice to have
> >>> the original connection information available?
> >> I'm uncertain what that usecase would be for something like haproxy,
> >> tbh. It can't do connection pooling, so adding it on the same machine
> >> as postgres itself wouldn't really add anything, I think?
> > Yeah, I wasn't thinking HAproxy so much as some unspecified software
> > appliance that's performing Some Task before allowing a TCP client to
> > speak to Postgres. But it'd be better to hear from someone that has an
> > actual use case, instead of me spitballing.
>
> Here's a use case: Envoy's Postgres filter (see [1], [2]). Right now
> is able to capture protocol-level metrics and send them to a metrics
> collector (eg. Prometheus) while proxying the traffic. More capabilities
> are being added as of today, and will eventually manage HBA too. It
> would greatly benefit from this proposal, since it proxies the traffic
> with, obviously, its IP, not the client's. It may be used (we do)
> locally fronting Postgres, via UDS (so it can be easily trusted).
Yeah, Envoy is definitely a great example of a usecase for the proxy
protocol in general.
Specifically about the Unix socket though -- doesn't envoy normally
run on a different instance (or in a different container at least),
thus normally uses tcp between envoy and postgres? Or would it be a
reasonable usecase that you ran it locally on the postgres server,
having it speak IP to the clients but unix sockets to the postgres
backend? I guess maybe it is outside of the containerized world?
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 00:33 ` Re: PROXY protocol support Álvaro Hernández <aht@ongres.com>
2021-03-05 09:03 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-03-05 13:49 ` Álvaro Hernández <aht@ongres.com>
0 siblings, 0 replies; 56+ messages in thread
From: Álvaro Hernández @ 2021-03-05 13:49 UTC (permalink / raw)
To: Magnus Hagander <magnus@hagander.net>; +Cc: Jacob Champion <pchampion@vmware.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; Fabrízio Mello <fabrizio@ongres.com>
On 5/3/21 10:03, Magnus Hagander wrote:
> On Fri, Mar 5, 2021 at 1:33 AM Álvaro Hernández <aht@ongres.com> wrote:
>>
>>
>> On 5/3/21 0:21, Jacob Champion wrote:
>>> On Thu, 2021-03-04 at 21:45 +0100, Magnus Hagander wrote:
>>>> On Thu, Mar 4, 2021 at 9:07 PM Jacob Champion <pchampion@vmware.com> wrote:
>>>>> Idle thought I had while setting up a local test rig: Are there any
>>>>> compelling cases for allowing PROXY packets to arrive over Unix
>>>>> sockets? (By which I mean, the proxy is running on the same machine as
>>>>> Postgres, and connects to it using the .s.PGSQL socket file instead of
>>>>> TCP.) Are there cases where you want some other software to interact
>>>>> with the TCP stack instead of Postgres, but it'd still be nice to have
>>>>> the original connection information available?
>>>> I'm uncertain what that usecase would be for something like haproxy,
>>>> tbh. It can't do connection pooling, so adding it on the same machine
>>>> as postgres itself wouldn't really add anything, I think?
>>> Yeah, I wasn't thinking HAproxy so much as some unspecified software
>>> appliance that's performing Some Task before allowing a TCP client to
>>> speak to Postgres. But it'd be better to hear from someone that has an
>>> actual use case, instead of me spitballing.
>> Here's a use case: Envoy's Postgres filter (see [1], [2]). Right now
>> is able to capture protocol-level metrics and send them to a metrics
>> collector (eg. Prometheus) while proxying the traffic. More capabilities
>> are being added as of today, and will eventually manage HBA too. It
>> would greatly benefit from this proposal, since it proxies the traffic
>> with, obviously, its IP, not the client's. It may be used (we do)
>> locally fronting Postgres, via UDS (so it can be easily trusted).
> Yeah, Envoy is definitely a great example of a usecase for the proxy
> protocol in general.
Actually Envoy already implements the Proxy protocol:
https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/listener_filters/proxy_protocol....
But I believe it would need some further cooperation with the Postgres
filter, unless they can be chained directly. Still, Postgres needs to
understand it, which is what your patch would add (thanks!).
>
> Specifically about the Unix socket though -- doesn't envoy normally
> run on a different instance (or in a different container at least),
> thus normally uses tcp between envoy and postgres? Or would it be a
> reasonable usecase that you ran it locally on the postgres server,
> having it speak IP to the clients but unix sockets to the postgres
> backend? I guess maybe it is outside of the containerized world?
>
This is exactly the architecture we use at StackGres [1][2]. We use
Envoy as a sidecar (so it runs on the same pod, server as Postgres) and
connects via UDS. But then exposes the connection to the outside clients
via TCP/IP. So in my opinion it is quite applicable to the container
world :)
Álvaro
[1] https://stackgres.io
[2]
https://stackgres.io/doc/latest/intro/architecture/#stackgres-pod-architecture-diagram
--
Alvaro Hernandez
-----------
OnGres
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
@ 2021-03-05 09:22 ` Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2021-03-05 09:22 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Fri, Mar 5, 2021 at 12:21 AM Jacob Champion <pchampion@vmware.com> wrote:
>
> On Thu, 2021-03-04 at 21:45 +0100, Magnus Hagander wrote:
> > On Thu, Mar 4, 2021 at 9:07 PM Jacob Champion <pchampion@vmware.com> wrote:
> > > Idle thought I had while setting up a local test rig: Are there any
> > > compelling cases for allowing PROXY packets to arrive over Unix
> > > sockets? (By which I mean, the proxy is running on the same machine as
> > > Postgres, and connects to it using the .s.PGSQL socket file instead of
> > > TCP.) Are there cases where you want some other software to interact
> > > with the TCP stack instead of Postgres, but it'd still be nice to have
> > > the original connection information available?
> >
> > I'm uncertain what that usecase would be for something like haproxy,
> > tbh. It can't do connection pooling, so adding it on the same machine
> > as postgres itself wouldn't really add anything, I think?
>
> Yeah, I wasn't thinking HAproxy so much as some unspecified software
> appliance that's performing Some Task before allowing a TCP client to
> speak to Postgres. But it'd be better to hear from someone that has an
> actual use case, instead of me spitballing.
>
> > Iid think about the other end, if you had a proxy on a different
> > machine accepting unix connections and passing them on over
> > PROXY-over-tcp. But I doubt it's useful to know it was unix in that
> > case (since it still couldn't do peer or such for the auth) --
> > instead, that seems like an argument where it'd be better to proxy
> > without using PROXY and just letting the IP address be.
>
> You could potentially design a system that lets you proxy a "local all
> all trust" setup from a different (trusted) machine, without having to
> actually let people onto the machine that's running Postgres. That
> would require some additional authentication on the PROXY connection
> (i.e. something stronger than host-based auth) to actually be useful.
>
> -- other notes --
>
> A small nitpick on the current separate-port PoC is that I'm forced to
> set up a "regular" TCP port, even if I only want the PROXY behavior.
Yeah. I'm not sure there's a good way to avoid that without making
configuations a lot more complex.
> The original-host logging isn't working for me:
>
> WARNING: pg_getnameinfo_all() failed: ai_family not supported
> LOG: proxy connection from: host=??? port=???
>
> and I think the culprit is this:
>
> > /* Store a copy of the original address, for logging */
> > memcpy(&raddr_save, &port->raddr, port->raddr.salen);
>
> port->raddr.salen is the length of port->raddr.addr; we want the length
> of the copy to be sizeof(port->raddr) here, no?
That's interesting -- it works perfectly fine here. What platform are
you testing on?
But yes, you are correct, it should do that. I guess it's a case of
the salen actually ending up being uninitialized in the copy, and thus
failing at a later stage. (I sent for sizeof(SockAddr) to make it
easier to read without having to look things up, but the net result is
the same)
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-03-05 19:11 ` Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 1 reply; 56+ messages in thread
From: Jacob Champion @ 2021-03-05 19:11 UTC (permalink / raw)
To: magnus@hagander.net <magnus@hagander.net>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Fri, 2021-03-05 at 10:22 +0100, Magnus Hagander wrote:
> On Fri, Mar 5, 2021 at 12:21 AM Jacob Champion <pchampion@vmware.com> wrote:
> > A small nitpick on the current separate-port PoC is that I'm forced to
> > set up a "regular" TCP port, even if I only want the PROXY behavior.
>
> Yeah. I'm not sure there's a good way to avoid that without making
> configuations a lot more complex.
A generic solution would also solve the "I want to listen on more than
one port" problem, but that's probably not something to tackle at the
same time.
> > The original-host logging isn't working for me:
> >
> > [...]
>
> That's interesting -- it works perfectly fine here. What platform are
> you testing on?
Ubuntu 20.04.
> But yes, you are correct, it should do that. I guess it's a case of
> the salen actually ending up being uninitialized in the copy, and thus
> failing at a later stage.
That seems right; EAI_FAMILY can be returned for a mismatched addrlen.
> (I sent for sizeof(SockAddr) to make it
> easier to read without having to look things up, but the net result is
> the same)
Cool. Did you mean to attach a patch?
== More Notes ==
(Stop me if I'm digging too far into a proof of concept patch.)
> + proxyaddrlen = pg_ntoh16(proxyheader.len);
> +
> + if (proxyaddrlen > sizeof(proxyaddr))
> + {
> + ereport(COMMERROR,
> + (errcode(ERRCODE_PROTOCOL_VIOLATION),
> + errmsg("oversized proxy packet")));
> + return STATUS_ERROR;
> + }
I think this is not quite right -- if there's additional data beyond
the IPv6 header size, that just means there are TLVs tacked onto the
header that we should ignore. (Or, eventually, use.)
Additionally, we need to check for underflow as well. A misbehaving
proxy might not send enough data to fill up the address block for the
address family in use.
> + /* If there is any more header data present, skip past it */
> + if (proxyaddrlen > sizeof(proxyaddr))
> + pq_discardbytes(proxyaddrlen - sizeof(proxyaddr));
This looks like dead code, given that we'll error out for the same
check above -- but once it's no longer dead code, the return value of
pq_discardbytes should be checked for EOF.
> + else if (proxyheader.fam == 0x11)
> + {
> + /* TCPv4 */
> + port->raddr.addr.ss_family = AF_INET;
> + port->raddr.salen = sizeof(struct sockaddr_in);
> + ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
> + ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
> + }
I'm trying to reason through the fallout of setting raddr and not
laddr. I understand why we're not setting laddr -- several places in
the code rely on the laddr to actually refer to a machine-local address
-- but the fact that there is no actual connection from raddr to laddr
could cause shenanigans. For example, the ident auth protocol will just
break (and it might be nice to explicitly disable it for PROXY
connections). Are there any other situations where a "faked" raddr
could throw off Postgres internals?
--Jacob
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
@ 2021-03-06 15:17 ` Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2021-03-06 15:17 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Fri, Mar 5, 2021 at 8:11 PM Jacob Champion <pchampion@vmware.com> wrote:
>
> On Fri, 2021-03-05 at 10:22 +0100, Magnus Hagander wrote:
> > On Fri, Mar 5, 2021 at 12:21 AM Jacob Champion <pchampion@vmware.com> wrote:
> > > The original-host logging isn't working for me:
> > >
> > > [...]
> >
> > That's interesting -- it works perfectly fine here. What platform are
> > you testing on?
>
> Ubuntu 20.04.
Curious. It doesn't show up on my debian.
But either way -- it was clearly wrong :)
> > (I sent for sizeof(SockAddr) to make it
> > easier to read without having to look things up, but the net result is
> > the same)
>
> Cool. Did you mean to attach a patch?
I didn't, I had some other hacks that were broken :) I've attached one
now which includes those changes.
> == More Notes ==
>
> (Stop me if I'm digging too far into a proof of concept patch.)
Definitely not -- much appreciated, and just what was needed to take
it from poc to a proper one!
> > + proxyaddrlen = pg_ntoh16(proxyheader.len);
> > +
> > + if (proxyaddrlen > sizeof(proxyaddr))
> > + {
> > + ereport(COMMERROR,
> > + (errcode(ERRCODE_PROTOCOL_VIOLATION),
> > + errmsg("oversized proxy packet")));
> > + return STATUS_ERROR;
> > + }
>
> I think this is not quite right -- if there's additional data beyond
> the IPv6 header size, that just means there are TLVs tacked onto the
> header that we should ignore. (Or, eventually, use.)
Yeah, you're right. Fallout of too much moving around. I think inthe
end that code should just be removed, in favor of the discard path as
you mentinoed below.
> Additionally, we need to check for underflow as well. A misbehaving
> proxy might not send enough data to fill up the address block for the
> address family in use.
I used to have that check. I seem to have lost it in restructuring. Added back!
> > + /* If there is any more header data present, skip past it */
> > + if (proxyaddrlen > sizeof(proxyaddr))
> > + pq_discardbytes(proxyaddrlen - sizeof(proxyaddr));
>
> This looks like dead code, given that we'll error out for the same
> check above -- but once it's no longer dead code, the return value of
> pq_discardbytes should be checked for EOF.
Yup.
> > + else if (proxyheader.fam == 0x11)
> > + {
> > + /* TCPv4 */
> > + port->raddr.addr.ss_family = AF_INET;
> > + port->raddr.salen = sizeof(struct sockaddr_in);
> > + ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
> > + ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
> > + }
>
> I'm trying to reason through the fallout of setting raddr and not
> laddr. I understand why we're not setting laddr -- several places in
> the code rely on the laddr to actually refer to a machine-local address
> -- but the fact that there is no actual connection from raddr to laddr
> could cause shenanigans. For example, the ident auth protocol will just
> break (and it might be nice to explicitly disable it for PROXY
> connections). Are there any other situations where a "faked" raddr
> could throw off Postgres internals?
That's a good point to discuss. I thought about it initially and
figured it'd be even worse to actually copy over laddr since that
woudl then suddenly have the IP address belonging to a different
machine.. And then I forgot to enumerate the other cases.
For ident, disabling the method seems reasonable.
Another thing that shows up with added support for running the proxy
protocol over Unix sockets, is that PostgreSQL refuses to do SSL over
Unix sockets. So that check has to be updated to allow it over proxy
connections. Same for GSSAPI.
An interesting thing is what to do about
inet_server_addr/inet_server_port. That sort of loops back up to the
original question of where/how to expose the information about the
proxy in general (since right now it just logs). Right now you can
actually use inet_server_port() to see if the connection was proxied
(as long as it was over tcp).
Attached is an updated, which covers your comments, as well as adds
unix socket support (per your question and Alvaros confirmed usecase).
It allows proxy connections over unix sockets, but I saw no need to
get into unix sockets over the proxy protocol (dealing with paths
between machines etc).
I changed the additional ListenSocket array to instead declare
ListenSocket as an array of structs holding two fields. Seems cleaner,
and especially should there be further extensions needed in the
future.
I've also added some trivial tests (man that took an ungodly amount of
fighting perl -- it's clearly been a long time since I used perl
properly). They probably need some more love but it's a start.
And of course rebased.
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
Attachments:
[text/x-patch] proxy_protocol_3.patch (35.0K, ../../CABUevEygBkZWyw0qYt8pMZ4jp3DOjMYKoj-68T_qFjeYqoi+6g@mail.gmail.com/2-proxy_protocol_3.patch)
download | inline diff:
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index b420486a0a..5d8fcc3d50 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -353,6 +353,15 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl
the client's host name instead of the IP address in the log.
</para>
+ <para>
+ If <xref linkend="guc-proxy-port"/> is enabled and the
+ connection is made through a proxy server using the PROXY
+ protocol, the actual IP address of the client will be used
+ for matching. If a connection is made through a proxy server
+ not using the PROXY protocol, the IP address of the
+ proxy server will be used.
+ </para>
+
<para>
These fields do not apply to <literal>local</literal> records.
</para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 967de73596..e1999bfeb5 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -682,6 +682,56 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-proxy-port" xreflabel="proxy_port">
+ <term><varname>proxy_port</varname> (<type>integer</type>)
+ <indexterm>
+ <primary><varname>proxy_port</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ The TCP port the server listens on for PROXY connections, disabled by
+ default. If set to a number, <productname>PostgreSQL</productname>
+ will listen on this port on the same addresses as for regular
+ connections, but expect all connections to use the PROXY protocol to
+ identify the client. This parameter can only be set at server start.
+ </para>
+ <para>
+ If a proxy connection is done over this port, and the proxy is listed
+ in <xref linkend="guc-proxy-servers" />, the actual client address
+ will be considered as the address of the client, instead of listing
+ all connections as coming from the proxy server.
+ </para>
+ <para>
+ The <ulink url="http://www.haproxy.org/download/1.9/doc/proxy-protocol.txt">PROXY
+ protocol</ulink> is maintained by <productname>HAProxy</productname>,
+ and supported in many proxies and load
+ balancers. <productname>PostgreSQL</productname> supports version 2
+ of the protocol.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-proxy-servers" xreflabel="proxy_servers">
+ <term><varname>proxy_servers</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>proxy_servers</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ A comma separated list of one or more host names, cidr specifications or the
+ literal <literal>unix</literal>, indicating which proxy servers to trust when
+ connecting on the port specified in <xref linkend="guc-proxy-port" />.
+ </para>
+ <para>
+ If a proxy connection is made from an IP address not covered by this
+ list, the connection will be rejected. By default no proxy is trusted
+ and all proxy connections will be rejected.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-max-connections" xreflabel="max_connections">
<term><varname>max_connections</varname> (<type>integer</type>)
<indexterm>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 994251e7d9..470e36db58 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -1761,6 +1761,14 @@ ident_inet(hbaPort *port)
*la = NULL,
hints;
+ if (port->isProxy)
+ {
+ ereport(LOG,
+ (errcode_for_socket_access(),
+ errmsg("Ident authentication cannot be used over PROXY connections")));
+ return STATUS_ERROR;
+ }
+
/*
* Might look a little weird to first convert it to text and then back to
* sockaddr, but it's protocol independent.
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 4c7b1e7bfd..9f92195097 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -308,13 +308,13 @@ socket_close(int code, Datum arg)
* Successfully opened sockets are added to the ListenSocket[] array (of
* length MaxListen), at the first position that isn't PGINVALID_SOCKET.
*
- * RETURNS: STATUS_OK or STATUS_ERROR
+ * RETURNS: The PQlistenSocket listening on, or NULL in case of error
*/
-int
+PQlistenSocket *
StreamServerPort(int family, const char *hostName, unsigned short portNumber,
const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen)
+ PQlistenSocket ListenSocket[], int MaxListen)
{
pgsocket fd;
int err;
@@ -359,10 +359,10 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("Unix-domain socket path \"%s\" is too long (maximum %d bytes)",
unixSocketPath,
(int) (UNIXSOCK_PATH_BUFLEN - 1))));
- return STATUS_ERROR;
+ return NULL;
}
if (Lock_AF_UNIX(unixSocketDir, unixSocketPath) != STATUS_OK)
- return STATUS_ERROR;
+ return NULL;
service = unixSocketPath;
}
else
@@ -385,7 +385,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
service, gai_strerror(ret))));
if (addrs)
pg_freeaddrinfo_all(hint.ai_family, addrs);
- return STATUS_ERROR;
+ return NULL;
}
for (addr = addrs; addr; addr = addr->ai_next)
@@ -402,7 +402,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
/* See if there is still room to add 1 more socket. */
for (; listen_index < MaxListen; listen_index++)
{
- if (ListenSocket[listen_index] == PGINVALID_SOCKET)
+ if (ListenSocket[listen_index].socket == PGINVALID_SOCKET)
break;
}
if (listen_index >= MaxListen)
@@ -579,16 +579,16 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("listening on %s address \"%s\", port %d",
familyDesc, addrDesc, (int) portNumber)));
- ListenSocket[listen_index] = fd;
+ ListenSocket[listen_index].socket = fd;
added++;
}
pg_freeaddrinfo_all(hint.ai_family, addrs);
if (!added)
- return STATUS_ERROR;
+ return NULL;
- return STATUS_OK;
+ return &ListenSocket[listen_index];
}
@@ -1113,7 +1113,7 @@ pq_getbytes(char *s, size_t len)
* returns 0 if OK, EOF if trouble
* --------------------------------
*/
-static int
+int
pq_discardbytes(size_t len)
{
size_t amount;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index edab95a19e..0344f907c9 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -102,6 +102,7 @@
#include "common/string.h"
#include "lib/ilist.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
@@ -195,15 +196,22 @@ BackgroundWorker *MyBgworkerEntry = NULL;
-/* The socket number we are listening for connections on */
+/* The TCP port number we are listening for connections on */
int PostPortNumber;
+/* The TCP port number we are listening for proxy connections on */
+int ProxyPortNumber;
+
/* The directory names for Unix socket(s) */
char *Unix_socket_directories;
/* The TCP listen address(es) */
char *ListenAddresses;
+/* Trusted proxy servers */
+char *TrustedProxyServersString = NULL;
+struct sockaddr_storage *TrustedProxyServers = NULL;
+
/*
* ReservedBackends is the number of backends reserved for superuser use.
* This number is taken out of the pool size given by MaxConnections so
@@ -217,7 +225,7 @@ int ReservedBackends;
/* The socket(s) we're listening to. */
#define MAXLISTEN 64
-static pgsocket ListenSocket[MAXLISTEN];
+static PQlistenSocket ListenSocket[MAXLISTEN];
/*
* These globals control the behavior of the postmaster in case some
@@ -581,6 +589,7 @@ PostmasterMain(int argc, char *argv[])
bool listen_addr_saved = false;
int i;
char *output_config_variable = NULL;
+ PQlistenSocket *socket = NULL;
InitProcessGlobals();
@@ -1124,7 +1133,10 @@ PostmasterMain(int argc, char *argv[])
* charged with closing the sockets again at postmaster shutdown.
*/
for (i = 0; i < MAXLISTEN; i++)
- ListenSocket[i] = PGINVALID_SOCKET;
+ {
+ ListenSocket[i].socket = PGINVALID_SOCKET;
+ ListenSocket[i].isProxy = false;
+ }
on_proc_exit(CloseServerPorts, 0);
@@ -1153,17 +1165,17 @@ PostmasterMain(int argc, char *argv[])
char *curhost = (char *) lfirst(l);
if (strcmp(curhost, "*") == 0)
- status = StreamServerPort(AF_UNSPEC, NULL,
+ socket = StreamServerPort(AF_UNSPEC, NULL,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
else
- status = StreamServerPort(AF_UNSPEC, curhost,
+ socket = StreamServerPort(AF_UNSPEC, curhost,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful host addr in lockfile */
@@ -1177,9 +1189,30 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create listen socket for \"%s\"",
curhost)));
+
+ /* Also listen to the PROXY port on this address, if configured */
+ if (ProxyPortNumber)
+ {
+ if (strcmp(curhost, "*") == 0)
+ socket = StreamServerPort(AF_UNSPEC, NULL,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ else
+ socket = StreamServerPort(AF_UNSPEC, curhost,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ socket->isProxy = true;
+ else
+ ereport(WARNING,
+ (errmsg("could not create PROXY listen socket for \"%s\"",
+ curhost)));
+ }
}
- if (!success && elemlist != NIL)
+ if (socket == NULL && elemlist != NIL)
ereport(FATAL,
(errmsg("could not create any TCP/IP sockets")));
@@ -1189,7 +1222,7 @@ PostmasterMain(int argc, char *argv[])
#ifdef USE_BONJOUR
/* Register for Bonjour only if we opened TCP socket(s) */
- if (enable_bonjour && ListenSocket[0] != PGINVALID_SOCKET)
+ if (enable_bonjour && ListenSocket[0].socket != PGINVALID_SOCKET)
{
DNSServiceErrorType err;
@@ -1251,12 +1284,12 @@ PostmasterMain(int argc, char *argv[])
{
char *socketdir = (char *) lfirst(l);
- status = StreamServerPort(AF_UNIX, NULL,
+ socket = StreamServerPort(AF_UNIX, NULL,
(unsigned short) PostPortNumber,
socketdir,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful Unix socket in lockfile */
@@ -1267,9 +1300,23 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create Unix-domain socket in directory \"%s\"",
socketdir)));
+
+ if (ProxyPortNumber)
+ {
+ socket = StreamServerPort(AF_UNIX, NULL,
+ (unsigned short) ProxyPortNumber,
+ socketdir,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ socket->isProxy = true;
+ else
+ ereport(WARNING,
+ (errmsg("could not create Unix-domain PROXY socket for \"%s\"",
+ socketdir)));
+ }
}
- if (!success && elemlist != NIL)
+ if (socket == NULL && elemlist != NIL)
ereport(FATAL,
(errmsg("could not create any Unix-domain sockets")));
@@ -1281,7 +1328,7 @@ PostmasterMain(int argc, char *argv[])
/*
* check that we have some socket to listen on
*/
- if (ListenSocket[0] == PGINVALID_SOCKET)
+ if (ListenSocket[0].socket == PGINVALID_SOCKET)
ereport(FATAL,
(errmsg("no socket created for listening")));
@@ -1430,10 +1477,10 @@ CloseServerPorts(int status, Datum arg)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -1722,15 +1769,17 @@ ServerLoop(void)
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] == PGINVALID_SOCKET)
+ if (ListenSocket[i].socket == PGINVALID_SOCKET)
break;
- if (FD_ISSET(ListenSocket[i], &rmask))
+ if (FD_ISSET(ListenSocket[i].socket, &rmask))
{
Port *port;
- port = ConnCreate(ListenSocket[i]);
+ port = ConnCreate(ListenSocket[i].socket);
if (port)
{
+ port->isProxy = ListenSocket[i].isProxy;
+
BackendStartup(port);
/*
@@ -1898,7 +1947,7 @@ initMasks(fd_set *rmask)
for (i = 0; i < MAXLISTEN; i++)
{
- int fd = ListenSocket[i];
+ int fd = ListenSocket[i].socket;
if (fd == PGINVALID_SOCKET)
break;
@@ -1911,6 +1960,213 @@ initMasks(fd_set *rmask)
return maxsock + 1;
}
+static int
+UnwrapProxyConnection(Port *port)
+{
+ char proxyver;
+ uint16 proxyaddrlen;
+ SockAddr raddr_save;
+ int i;
+ bool useproxy = false;
+
+ /*
+ * These structs are from the PROXY protocol docs at
+ * http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
+ */
+ union
+ {
+ struct
+ { /* for TCP/UDP over IPv4, len = 12 */
+ uint32 src_addr;
+ uint32 dst_addr;
+ uint16 src_port;
+ uint16 dst_port;
+ } ip4;
+ struct
+ { /* for TCP/UDP over IPv6, len = 36 */
+ uint8 src_addr[16];
+ uint8 dst_addr[16];
+ uint16 src_port;
+ uint16 dst_port;
+ } ip6;
+ } proxyaddr;
+ struct
+ {
+ uint8 sig[12]; /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */
+ uint8 ver_cmd; /* protocol version and command */
+ uint8 fam; /* protocol family and address */
+ uint16 len; /* number of following bytes part of the
+ * header */
+ } proxyheader;
+
+
+ /* Else if it's on our list of trusted proxies */
+ if (TrustedProxyServers)
+ {
+ for (i = 0; i < *((int *) TrustedProxyServers) * 2; i += 2)
+ {
+ if (port->raddr.addr.ss_family == TrustedProxyServers[i + 1].ss_family)
+ {
+ /*
+ * Connection over unix sockets don't give us the source, so
+ * just check if they're allowed at all. For IP connections,
+ * verify that it's an allowed address.
+ */
+ if (port->raddr.addr.ss_family == AF_UNIX ||
+ pg_range_sockaddr(&port->raddr.addr,
+ &TrustedProxyServers[i + 1],
+ &TrustedProxyServers[i + 2]))
+ {
+ useproxy = true;
+ break;
+ }
+ }
+ }
+ }
+ if (!useproxy)
+ {
+ /*
+ * Connection is not from one of our trusted proxies, so reject it.
+ */
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("connection from unauthorized proxy server")));
+ return STATUS_ERROR;
+ }
+
+ /* Store a copy of the original address, for logging */
+ memcpy(&raddr_save, &port->raddr, sizeof(SockAddr));
+
+ pq_startmsgread();
+
+ /*
+ * PROXY requests always start with:
+ * \x0D \x0A \x0D \x0A \x00 \x0D \x0A \x51 \x55 \x49 \x54 \x0A
+ */
+
+ if (pq_getbytes((char *) &proxyheader, sizeof(proxyheader)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ if (memcmp(proxyheader.sig, "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a", sizeof(proxyheader.sig)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Proxy version is in the high 4 bits of the first byte */
+ proxyver = (proxyheader.ver_cmd & 0xF0) >> 4;
+ if (proxyver != 2)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol version: %x", proxyver)));
+ return STATUS_ERROR;
+ }
+
+ proxyaddrlen = pg_ntoh16(proxyheader.len);
+
+ if (pq_getbytes((char *) &proxyaddr, proxyaddrlen > sizeof(proxyaddr) ? sizeof(proxyaddr) : proxyaddrlen) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Lower 4 bits hold type of connection */
+ if (proxyheader.fam == 0)
+ {
+ /* LOCAL connection, so we ignore the address included */
+ }
+ else if (proxyheader.fam == 0x11)
+ {
+ /* TCPv4 */
+ if (proxyaddrlen < 12)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET;
+ port->raddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
+ }
+ else if (proxyheader.fam == 0x21)
+ {
+ /* TCPv6 */
+ if (proxyaddrlen < 36)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET6;
+ port->raddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->raddr.addr)->sin6_addr, proxyaddr.ip6.src_addr, 16);
+ ((struct sockaddr_in6 *) &port->raddr.addr)->sin6_port = proxyaddr.ip6.src_port;
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol connection type: %x", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+
+ /* If there is any more header data present, skip past it */
+ if (proxyaddrlen > sizeof(proxyaddr))
+ {
+ if (pq_discardbytes(proxyaddrlen - sizeof(proxyaddr)) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ }
+
+ pq_endmsgread();
+
+ /*
+ * Log what we've done if connection logging is enabled. We log the proxy
+ * connection here, and let the normal connection logging mechanism log
+ * the unwrapped connection.
+ */
+ if (Log_connections)
+ {
+ char remote_host[NI_MAXHOST];
+ char remote_port[NI_MAXSERV];
+ int ret;
+
+ remote_host[0] = '\0';
+ remote_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&raddr_save.addr, raddr_save.salen,
+ remote_host, sizeof(remote_host),
+ remote_port, sizeof(remote_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ ereport(LOG,
+ (errmsg("proxy connection from: host=%s port=%s",
+ remote_host,
+ remote_port)));
+
+ }
+
+ return STATUS_OK;
+}
/*
* Read a client's startup packet and do something according to it.
@@ -2019,7 +2275,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
#ifdef USE_SSL
/* No SSL when disabled or on Unix sockets */
- if (!LoadedSSL || IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!LoadedSSL || (IS_AF_UNIX(port->laddr.addr.ss_family) && !port->isProxy))
SSLok = 'N';
else
SSLok = 'S'; /* Support for SSL */
@@ -2056,7 +2312,7 @@ retry1:
#ifdef ENABLE_GSS
/* No GSSAPI encryption when on Unix socket */
- if (!IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!IS_AF_UNIX(port->laddr.addr.ss_family) || port->isProxy)
GSSok = 'G';
#endif
@@ -2555,10 +2811,10 @@ ClosePostmasterPorts(bool am_syslogger)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -4320,6 +4576,33 @@ BackendInitialize(Port *port)
InitializeTimeouts(); /* establishes SIGALRM handler */
PG_SETMASK(&StartupBlockSig);
+ /*
+ * Ready to begin client interaction. We will give up and _exit(1) after
+ * a time delay, so that a broken client can't hog a connection
+ * indefinitely. PreAuthDelay and any DNS interactions above don't count
+ * against the time limit.
+ *
+ * Note: AuthenticationTimeout is applied here while waiting for the
+ * startup packet, and then again in InitPostgres for the duration of any
+ * authentication operations. So a hostile client could tie up the
+ * process for nearly twice AuthenticationTimeout before we kick him off.
+ *
+ * Note: because PostgresMain will call InitializeTimeouts again, the
+ * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
+ * since we never use it again after this function.
+ */
+ RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
+
+ /* Check if this is a proxy connection and if so unwrap the proxying */
+ if (port->isProxy)
+ {
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
+ if (UnwrapProxyConnection(port) != STATUS_OK)
+ proc_exit(0);
+ disable_timeout(STARTUP_PACKET_TIMEOUT, false);
+ }
+
+
/*
* Get the remote host name and port for logging and status display.
*/
@@ -4371,28 +4654,11 @@ BackendInitialize(Port *port)
strspn(remote_host, "0123456789ABCDEFabcdef:") < strlen(remote_host))
port->remote_hostname = strdup(remote_host);
- /*
- * Ready to begin client interaction. We will give up and _exit(1) after
- * a time delay, so that a broken client can't hog a connection
- * indefinitely. PreAuthDelay and any DNS interactions above don't count
- * against the time limit.
- *
- * Note: AuthenticationTimeout is applied here while waiting for the
- * startup packet, and then again in InitPostgres for the duration of any
- * authentication operations. So a hostile client could tie up the
- * process for nearly twice AuthenticationTimeout before we kick him off.
- *
- * Note: because PostgresMain will call InitializeTimeouts again, the
- * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
- * since we never use it again after this function.
- */
- RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
- enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
-
/*
* Receive the startup packet (which might turn out to be a cancel request
* packet).
*/
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
status = ProcessStartupPacket(port, false, false);
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 3fd1a5fbe2..8b3be40a26 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -46,10 +46,12 @@
#include "commands/user.h"
#include "commands/vacuum.h"
#include "commands/variable.h"
+#include "common/ip.h"
#include "common/string.h"
#include "funcapi.h"
#include "jit/jit.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
@@ -227,6 +229,8 @@ static bool check_recovery_target_lsn(char **newval, void **extra, GucSource sou
static void assign_recovery_target_lsn(const char *newval, void *extra);
static bool check_primary_slot_name(char **newval, void **extra, GucSource source);
static bool check_default_with_oids(bool *newval, void **extra, GucSource source);
+static bool check_proxy_servers(char **newval, void **extra, GucSource source);
+static void assign_proxy_servers(const char *newval, void *extra);
/* Private functions in guc-file.l that need to be called from guc.c */
static ConfigVariable *ProcessConfigFileInternal(GucContext context,
@@ -2291,6 +2295,16 @@ static struct config_int ConfigureNamesInt[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_port", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the TCP port the server listens for PROXY connections on."),
+ NULL
+ },
+ &ProxyPortNumber,
+ 0, 0, 65535,
+ NULL, NULL, NULL
+ },
+
{
{"unix_socket_permissions", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
gettext_noop("Sets the access permissions of the Unix-domain socket."),
@@ -4242,6 +4256,17 @@ static struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_servers", PGC_SIGHUP, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the addresses for trusted proxy servers."),
+ NULL,
+ GUC_LIST_INPUT
+ },
+ &TrustedProxyServersString,
+ "",
+ check_proxy_servers, assign_proxy_servers, NULL
+ },
+
{
/*
* Can't be set by ALTER SYSTEM as it can lead to recursive definition
@@ -12227,4 +12252,118 @@ check_default_with_oids(bool *newval, void **extra, GucSource source)
return true;
}
+static bool
+check_proxy_servers(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ struct sockaddr_storage *myextra;
+
+ /* Special case when it's empty */
+ if (**newval == '\0')
+ {
+ *extra = NULL;
+ return true;
+ }
+
+ /* Need a modifiable copy of string */
+ rawstring = pstrdup(*newval);
+
+ /* Parse string into list of identifiers */
+ if (!SplitIdentifierString(rawstring, ',', &elemlist))
+ {
+ /* syntax error in list */
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ if (list_length(elemlist) == 0)
+ {
+ /* If it had only whitespace */
+ pfree(rawstring);
+ list_free(elemlist);
+
+ *extra = NULL;
+ return true;
+ }
+
+ /*
+ * We store the result in an array of sockaddr_storage. The first entry is
+ * just an overloaded int which holds the size of the array.
+ */
+ myextra = (struct sockaddr_storage *) guc_malloc(ERROR, sizeof(struct sockaddr_storage) * (list_length(elemlist) * 2 + 1));
+ *((int *) &myextra[0]) = list_length(elemlist);
+
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+ char *netmasktok = NULL;
+ int ret;
+ struct addrinfo *gai_result;
+ struct addrinfo hints;
+
+ /*
+ * Unix sockets don't have endpoint addresses, so just flag them as
+ * AF_UNIX
+ */
+ if (pg_strcasecmp(tok, "unix") == 0)
+ {
+ myextra[foreach_current_index(l) * 2 + 1].ss_family = AF_UNIX;
+ continue;
+ }
+
+ netmasktok = strchr(tok, '/');
+ if (netmasktok)
+ {
+ *netmasktok = '\0';
+ netmasktok++;
+ }
+
+ memset((char *) &hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ ret = pg_getaddrinfo_all(tok, NULL, &hints, &gai_result);
+ if (ret != 0 || gai_result == NULL)
+ {
+ GUC_check_errdetail("Invalid IP addrress %s", tok);
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+
+ memcpy((char *) &myextra[foreach_current_index(l) * 2 + 1], gai_result->ai_addr, gai_result->ai_addrlen);
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+
+ /* A NULL netmasktok means the fully set hostmask */
+ if (pg_sockaddr_cidr_mask(&myextra[foreach_current_index(l) * 2 + 2], netmasktok, myextra[foreach_current_index(l) * 2 + 1].ss_family) != 0)
+ {
+ if (netmasktok)
+ GUC_check_errdetail("Invalid netmask %s", netmasktok);
+ else
+ GUC_check_errdetail("Could not create netmask");
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+ }
+
+ pfree(rawstring);
+ list_free(elemlist);
+ *extra = (void *) myextra;
+
+ return true;
+}
+
+static void
+assign_proxy_servers(const char *newval, void *extra)
+{
+ TrustedProxyServers = (struct sockaddr_storage *) extra;
+}
+
#include "guc-file.c"
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ee06528bb0..b002228393 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -61,6 +61,9 @@
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
+#proxy_port = 0 # port to listen to for proxy connections
+ # (change requires restart)
+#proxy_servers = '' # what proxy servers to trust
#max_connections = 100 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
#unix_socket_directories = '/tmp' # comma-separated list of directories
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 7be1a67d69..57edda122a 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -121,6 +121,7 @@ typedef struct Port
{
pgsocket sock; /* File descriptor */
bool noblock; /* is the socket in non-blocking mode? */
+ bool isProxy; /* is the connection using PROXY protocol */
ProtocolVersion proto; /* FE/BE protocol version */
SockAddr laddr; /* local addr (postmaster) */
SockAddr raddr; /* remote addr (client) */
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index b20deeb555..c06ee29f88 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -33,6 +33,12 @@ typedef struct
extern const PGDLLIMPORT PQcommMethods *PqCommMethods;
+typedef struct
+{
+ pgsocket socket;
+ bool isProxy;
+} PQlistenSocket;
+
#define pq_comm_reset() (PqCommMethods->comm_reset())
#define pq_flush() (PqCommMethods->flush())
#define pq_flush_if_writable() (PqCommMethods->flush_if_writable())
@@ -54,9 +60,9 @@ extern WaitEventSet *FeBeWaitSet;
#define FeBeWaitSetSocketPos 0
#define FeBeWaitSetLatchPos 1
-extern int StreamServerPort(int family, const char *hostName,
- unsigned short portNumber, const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen);
+extern PQlistenSocket *StreamServerPort(int family, const char *hostName,
+ unsigned short portNumber, const char *unixSocketDir,
+ PQlistenSocket PQlistenSocket[], int MaxListen);
extern int StreamConnection(pgsocket server_fd, Port *port);
extern void StreamClose(pgsocket sock);
extern void TouchSocketFiles(void);
@@ -69,6 +75,7 @@ extern bool pq_is_reading_msg(void);
extern int pq_getmessage(StringInfo s, int maxlen);
extern int pq_getbyte(void);
extern int pq_peekbyte(void);
+extern int pq_discardbytes(size_t len);
extern int pq_getbyte_if_available(unsigned char *c);
extern int pq_putmessage_v2(char msgtype, const char *s, size_t len);
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index cfa59c4dc0..9ed219dfda 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -17,10 +17,13 @@
extern bool EnableSSL;
extern int ReservedBackends;
extern PGDLLIMPORT int PostPortNumber;
+extern PGDLLIMPORT int ProxyPortNumber;
extern int Unix_socket_permissions;
extern char *Unix_socket_group;
extern char *Unix_socket_directories;
extern char *ListenAddresses;
+extern char *TrustedProxyServersString;
+extern struct sockaddr_storage *TrustedProxyServers;
extern bool ClientAuthInProgress;
extern int PreAuthDelay;
extern int AuthenticationTimeout;
diff --git a/src/test/protocol/Makefile b/src/test/protocol/Makefile
new file mode 100644
index 0000000000..bda49d6ecb
--- /dev/null
+++ b/src/test/protocol/Makefile
@@ -0,0 +1,23 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/protocol
+#
+# Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/protocol/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/protocol
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
+
+clean distclean maintainer-clean:
+ rm -rf tmp_check
diff --git a/src/test/protocol/t/001_proxy.pl b/src/test/protocol/t/001_proxy.pl
new file mode 100644
index 0000000000..c84d797e4f
--- /dev/null
+++ b/src/test/protocol/t/001_proxy.pl
@@ -0,0 +1,157 @@
+use strict;
+use warnings;
+use TestLib;
+use PostgresNode;
+use Test::More;
+use Socket qw(AF_INET AF_INET6 inet_pton);
+use IO::Socket;
+
+plan tests => 35;
+
+my $node = get_new_node('node');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+listen_addresses = 'localhost'
+log_connections = on
+});
+$node->append_conf(
+ 'pg_hba.conf', qq{
+host all all 11.22.33.44/32 trust
+host all all 1:2:3:4:5:6:0:9/128 trust
+});
+$node->append_conf('postgresql.conf', "proxy_port = " . ($node->port() + 1));
+
+$node->start;
+
+sub make_message
+{
+ my ($msg) = @_;
+ return pack("Na*", length($msg) + 4, $msg);
+}
+
+sub read_packet
+{
+ my ($socket) = @_;
+ my $buf = "";
+ $socket->recv($buf, 1024);
+ return $buf;
+}
+
+
+# Test normal connection through localhost
+sub test_connection
+{
+ my ($socket, $proxy, $what, $shouldbe, $shouldfail, $extra) = @_;
+ ok($socket, $what);
+
+ my $startup = make_message(
+ pack("N(Z*Z*)*x", 196608, (user => "mha", database => "postgres")));
+
+ $extra = "" if !defined($extra);
+
+ if (defined($proxy))
+ {
+ my $p = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A\x21";
+ if ($proxy =~ ":")
+ {
+ # ipv6
+ $p .= "\x21"; # TCP v6
+ $p .= pack "n", 36 + length($extra); # size
+ $p .= inet_pton(AF_INET6, $proxy);
+ $p .= "\0" x 16; # destination address
+ }
+ else
+ {
+ # ipv4
+ $p .= "\x11"; # TCP v4
+ $p .= pack "n", 12 + length($extra); # size
+ $p .= inet_pton(AF_INET, $proxy);
+ $p .= "\0\0\0\0"; # destination address
+ }
+ $p .= pack "n", 1919; # source port
+ $p .= pack "n", 0;
+ $p .= $extra;
+ print $socket $p;
+ }
+ print $socket $startup;
+
+ my $in = read_packet($socket);
+ if (defined($shouldfail))
+ {
+ isnt(substr($in, 0, 1), 'R', $what);
+ }
+ else
+ {
+ is(substr($in, 0, 1), 'R', $what);
+ }
+
+ SKIP:
+ {
+ skip "The rest of this test should fail", 3 if (defined($shouldfail));
+
+ is(substr($in, 8, 1), "\0", $what);
+
+ my ($resip, $resport) = split /\|/,
+ $node->safe_psql('postgres',
+ "SELECT client_addr, client_port FROM pg_stat_activity WHERE pid != pg_backend_pid() AND backend_type='client backend'"
+ );
+ is($resip, $shouldbe, $what);
+ if ($proxy)
+ {
+ is($resport, "1919", $what);
+ }
+ else
+ {
+ ok($resport, $what);
+ }
+ }
+
+ $socket->close();
+
+ return;
+}
+
+sub inet_socket
+{
+ my ($port) = @_;
+ return IO::Socket::INET->new(
+ PeerAddr => "127.0.0.1",
+ PeerPort => $port,
+ Proto => "tcp",
+ Type => SOCK_STREAM);
+}
+
+sub unix_socket
+{
+ my ($port) = @_;
+ return IO::Socket::UNIX->new(
+ Peer => $node->host() . "/.s.PGSQL." . $port,
+ Type => SOCK_STREAM);
+}
+
+
+# Test a regular connection first to make sure connecting etc works fine.
+test_connection(
+ inet_socket($node->port()), undef,
+ "normal ipv4 connection", "127.0.0.1");
+test_connection(unix_socket($node->port()),
+ undef, "normal unix connection", "");
+
+# Make sure we can't make a proxy connection until it's allowed
+test_connection(inet_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44", 1);
+
+# Allow proxy connections and test them
+$node->append_conf('postgresql.conf', "proxy_servers = 'unix, 127.0.0.1/32'");
+$node->restart();
+
+test_connection(inet_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44");
+test_connection(inet_socket($node->port() + 1),
+ "1:2:3:4:5:6::9", "proxy ipv6", "1:2:3:4:5:6:0:9");
+test_connection(unix_socket($node->port() + 1),
+ "11.22.33.44", "proxy unix", "11.22.33.44");
+
+test_connection(unix_socket($node->port() + 1),
+ "11.22.33.44", "proxy unix with extra", "11.22.33.44", undef, "abcdef"x100);
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-03-06 16:30 ` Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2021-03-06 16:30 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Sat, Mar 6, 2021 at 4:17 PM Magnus Hagander <magnus@hagander.net> wrote:
>
> On Fri, Mar 5, 2021 at 8:11 PM Jacob Champion <pchampion@vmware.com> wrote:
> >
> > On Fri, 2021-03-05 at 10:22 +0100, Magnus Hagander wrote:
> > > On Fri, Mar 5, 2021 at 12:21 AM Jacob Champion <pchampion@vmware.com> wrote:
> > > > The original-host logging isn't working for me:
> > > >
> > > > [...]
> > >
> > > That's interesting -- it works perfectly fine here. What platform are
> > > you testing on?
> >
> > Ubuntu 20.04.
>
> Curious. It doesn't show up on my debian.
>
> But either way -- it was clearly wrong :)
>
>
> > > (I sent for sizeof(SockAddr) to make it
> > > easier to read without having to look things up, but the net result is
> > > the same)
> >
> > Cool. Did you mean to attach a patch?
>
> I didn't, I had some other hacks that were broken :) I've attached one
> now which includes those changes.
>
>
> > == More Notes ==
> >
> > (Stop me if I'm digging too far into a proof of concept patch.)
>
> Definitely not -- much appreciated, and just what was needed to take
> it from poc to a proper one!
>
>
> > > + proxyaddrlen = pg_ntoh16(proxyheader.len);
> > > +
> > > + if (proxyaddrlen > sizeof(proxyaddr))
> > > + {
> > > + ereport(COMMERROR,
> > > + (errcode(ERRCODE_PROTOCOL_VIOLATION),
> > > + errmsg("oversized proxy packet")));
> > > + return STATUS_ERROR;
> > > + }
> >
> > I think this is not quite right -- if there's additional data beyond
> > the IPv6 header size, that just means there are TLVs tacked onto the
> > header that we should ignore. (Or, eventually, use.)
>
> Yeah, you're right. Fallout of too much moving around. I think inthe
> end that code should just be removed, in favor of the discard path as
> you mentinoed below.
>
>
> > Additionally, we need to check for underflow as well. A misbehaving
> > proxy might not send enough data to fill up the address block for the
> > address family in use.
>
> I used to have that check. I seem to have lost it in restructuring. Added back!
>
>
> > > + /* If there is any more header data present, skip past it */
> > > + if (proxyaddrlen > sizeof(proxyaddr))
> > > + pq_discardbytes(proxyaddrlen - sizeof(proxyaddr));
> >
> > This looks like dead code, given that we'll error out for the same
> > check above -- but once it's no longer dead code, the return value of
> > pq_discardbytes should be checked for EOF.
>
> Yup.
>
>
> > > + else if (proxyheader.fam == 0x11)
> > > + {
> > > + /* TCPv4 */
> > > + port->raddr.addr.ss_family = AF_INET;
> > > + port->raddr.salen = sizeof(struct sockaddr_in);
> > > + ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
> > > + ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
> > > + }
> >
> > I'm trying to reason through the fallout of setting raddr and not
> > laddr. I understand why we're not setting laddr -- several places in
> > the code rely on the laddr to actually refer to a machine-local address
> > -- but the fact that there is no actual connection from raddr to laddr
> > could cause shenanigans. For example, the ident auth protocol will just
> > break (and it might be nice to explicitly disable it for PROXY
> > connections). Are there any other situations where a "faked" raddr
> > could throw off Postgres internals?
>
> That's a good point to discuss. I thought about it initially and
> figured it'd be even worse to actually copy over laddr since that
> woudl then suddenly have the IP address belonging to a different
> machine.. And then I forgot to enumerate the other cases.
>
> For ident, disabling the method seems reasonable.
>
> Another thing that shows up with added support for running the proxy
> protocol over Unix sockets, is that PostgreSQL refuses to do SSL over
> Unix sockets. So that check has to be updated to allow it over proxy
> connections. Same for GSSAPI.
>
> An interesting thing is what to do about
> inet_server_addr/inet_server_port. That sort of loops back up to the
> original question of where/how to expose the information about the
> proxy in general (since right now it just logs). Right now you can
> actually use inet_server_port() to see if the connection was proxied
> (as long as it was over tcp).
>
> Attached is an updated, which covers your comments, as well as adds
> unix socket support (per your question and Alvaros confirmed usecase).
> It allows proxy connections over unix sockets, but I saw no need to
> get into unix sockets over the proxy protocol (dealing with paths
> between machines etc).
>
> I changed the additional ListenSocket array to instead declare
> ListenSocket as an array of structs holding two fields. Seems cleaner,
> and especially should there be further extensions needed in the
> future.
>
> I've also added some trivial tests (man that took an ungodly amount of
> fighting perl -- it's clearly been a long time since I used perl
> properly). They probably need some more love but it's a start.
>
> And of course rebased.
Pfft, I was hoping for cfbot to pick it up and test it on a different
platform. Of course, for it to do that, I need to include the test
directory in the Makefile. Here's a new one which adds that, no other
changes.
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
Attachments:
[text/x-patch] proxy_protocol_4.patch (35.5K, ../../CABUevEy9MP0THCJNOb4NN8aQ3La40=ah3gKbYvoAmUs1hjknzw@mail.gmail.com/2-proxy_protocol_4.patch)
download | inline diff:
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index b420486a0a..5d8fcc3d50 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -353,6 +353,15 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl
the client's host name instead of the IP address in the log.
</para>
+ <para>
+ If <xref linkend="guc-proxy-port"/> is enabled and the
+ connection is made through a proxy server using the PROXY
+ protocol, the actual IP address of the client will be used
+ for matching. If a connection is made through a proxy server
+ not using the PROXY protocol, the IP address of the
+ proxy server will be used.
+ </para>
+
<para>
These fields do not apply to <literal>local</literal> records.
</para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 967de73596..e1999bfeb5 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -682,6 +682,56 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-proxy-port" xreflabel="proxy_port">
+ <term><varname>proxy_port</varname> (<type>integer</type>)
+ <indexterm>
+ <primary><varname>proxy_port</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ The TCP port the server listens on for PROXY connections, disabled by
+ default. If set to a number, <productname>PostgreSQL</productname>
+ will listen on this port on the same addresses as for regular
+ connections, but expect all connections to use the PROXY protocol to
+ identify the client. This parameter can only be set at server start.
+ </para>
+ <para>
+ If a proxy connection is done over this port, and the proxy is listed
+ in <xref linkend="guc-proxy-servers" />, the actual client address
+ will be considered as the address of the client, instead of listing
+ all connections as coming from the proxy server.
+ </para>
+ <para>
+ The <ulink url="http://www.haproxy.org/download/1.9/doc/proxy-protocol.txt">PROXY
+ protocol</ulink> is maintained by <productname>HAProxy</productname>,
+ and supported in many proxies and load
+ balancers. <productname>PostgreSQL</productname> supports version 2
+ of the protocol.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-proxy-servers" xreflabel="proxy_servers">
+ <term><varname>proxy_servers</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>proxy_servers</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ A comma separated list of one or more host names, cidr specifications or the
+ literal <literal>unix</literal>, indicating which proxy servers to trust when
+ connecting on the port specified in <xref linkend="guc-proxy-port" />.
+ </para>
+ <para>
+ If a proxy connection is made from an IP address not covered by this
+ list, the connection will be rejected. By default no proxy is trusted
+ and all proxy connections will be rejected.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-max-connections" xreflabel="max_connections">
<term><varname>max_connections</varname> (<type>integer</type>)
<indexterm>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 994251e7d9..470e36db58 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -1761,6 +1761,14 @@ ident_inet(hbaPort *port)
*la = NULL,
hints;
+ if (port->isProxy)
+ {
+ ereport(LOG,
+ (errcode_for_socket_access(),
+ errmsg("Ident authentication cannot be used over PROXY connections")));
+ return STATUS_ERROR;
+ }
+
/*
* Might look a little weird to first convert it to text and then back to
* sockaddr, but it's protocol independent.
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 4c7b1e7bfd..9f92195097 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -308,13 +308,13 @@ socket_close(int code, Datum arg)
* Successfully opened sockets are added to the ListenSocket[] array (of
* length MaxListen), at the first position that isn't PGINVALID_SOCKET.
*
- * RETURNS: STATUS_OK or STATUS_ERROR
+ * RETURNS: The PQlistenSocket listening on, or NULL in case of error
*/
-int
+PQlistenSocket *
StreamServerPort(int family, const char *hostName, unsigned short portNumber,
const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen)
+ PQlistenSocket ListenSocket[], int MaxListen)
{
pgsocket fd;
int err;
@@ -359,10 +359,10 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("Unix-domain socket path \"%s\" is too long (maximum %d bytes)",
unixSocketPath,
(int) (UNIXSOCK_PATH_BUFLEN - 1))));
- return STATUS_ERROR;
+ return NULL;
}
if (Lock_AF_UNIX(unixSocketDir, unixSocketPath) != STATUS_OK)
- return STATUS_ERROR;
+ return NULL;
service = unixSocketPath;
}
else
@@ -385,7 +385,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
service, gai_strerror(ret))));
if (addrs)
pg_freeaddrinfo_all(hint.ai_family, addrs);
- return STATUS_ERROR;
+ return NULL;
}
for (addr = addrs; addr; addr = addr->ai_next)
@@ -402,7 +402,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
/* See if there is still room to add 1 more socket. */
for (; listen_index < MaxListen; listen_index++)
{
- if (ListenSocket[listen_index] == PGINVALID_SOCKET)
+ if (ListenSocket[listen_index].socket == PGINVALID_SOCKET)
break;
}
if (listen_index >= MaxListen)
@@ -579,16 +579,16 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("listening on %s address \"%s\", port %d",
familyDesc, addrDesc, (int) portNumber)));
- ListenSocket[listen_index] = fd;
+ ListenSocket[listen_index].socket = fd;
added++;
}
pg_freeaddrinfo_all(hint.ai_family, addrs);
if (!added)
- return STATUS_ERROR;
+ return NULL;
- return STATUS_OK;
+ return &ListenSocket[listen_index];
}
@@ -1113,7 +1113,7 @@ pq_getbytes(char *s, size_t len)
* returns 0 if OK, EOF if trouble
* --------------------------------
*/
-static int
+int
pq_discardbytes(size_t len)
{
size_t amount;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index edab95a19e..0344f907c9 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -102,6 +102,7 @@
#include "common/string.h"
#include "lib/ilist.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
@@ -195,15 +196,22 @@ BackgroundWorker *MyBgworkerEntry = NULL;
-/* The socket number we are listening for connections on */
+/* The TCP port number we are listening for connections on */
int PostPortNumber;
+/* The TCP port number we are listening for proxy connections on */
+int ProxyPortNumber;
+
/* The directory names for Unix socket(s) */
char *Unix_socket_directories;
/* The TCP listen address(es) */
char *ListenAddresses;
+/* Trusted proxy servers */
+char *TrustedProxyServersString = NULL;
+struct sockaddr_storage *TrustedProxyServers = NULL;
+
/*
* ReservedBackends is the number of backends reserved for superuser use.
* This number is taken out of the pool size given by MaxConnections so
@@ -217,7 +225,7 @@ int ReservedBackends;
/* The socket(s) we're listening to. */
#define MAXLISTEN 64
-static pgsocket ListenSocket[MAXLISTEN];
+static PQlistenSocket ListenSocket[MAXLISTEN];
/*
* These globals control the behavior of the postmaster in case some
@@ -581,6 +589,7 @@ PostmasterMain(int argc, char *argv[])
bool listen_addr_saved = false;
int i;
char *output_config_variable = NULL;
+ PQlistenSocket *socket = NULL;
InitProcessGlobals();
@@ -1124,7 +1133,10 @@ PostmasterMain(int argc, char *argv[])
* charged with closing the sockets again at postmaster shutdown.
*/
for (i = 0; i < MAXLISTEN; i++)
- ListenSocket[i] = PGINVALID_SOCKET;
+ {
+ ListenSocket[i].socket = PGINVALID_SOCKET;
+ ListenSocket[i].isProxy = false;
+ }
on_proc_exit(CloseServerPorts, 0);
@@ -1153,17 +1165,17 @@ PostmasterMain(int argc, char *argv[])
char *curhost = (char *) lfirst(l);
if (strcmp(curhost, "*") == 0)
- status = StreamServerPort(AF_UNSPEC, NULL,
+ socket = StreamServerPort(AF_UNSPEC, NULL,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
else
- status = StreamServerPort(AF_UNSPEC, curhost,
+ socket = StreamServerPort(AF_UNSPEC, curhost,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful host addr in lockfile */
@@ -1177,9 +1189,30 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create listen socket for \"%s\"",
curhost)));
+
+ /* Also listen to the PROXY port on this address, if configured */
+ if (ProxyPortNumber)
+ {
+ if (strcmp(curhost, "*") == 0)
+ socket = StreamServerPort(AF_UNSPEC, NULL,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ else
+ socket = StreamServerPort(AF_UNSPEC, curhost,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ socket->isProxy = true;
+ else
+ ereport(WARNING,
+ (errmsg("could not create PROXY listen socket for \"%s\"",
+ curhost)));
+ }
}
- if (!success && elemlist != NIL)
+ if (socket == NULL && elemlist != NIL)
ereport(FATAL,
(errmsg("could not create any TCP/IP sockets")));
@@ -1189,7 +1222,7 @@ PostmasterMain(int argc, char *argv[])
#ifdef USE_BONJOUR
/* Register for Bonjour only if we opened TCP socket(s) */
- if (enable_bonjour && ListenSocket[0] != PGINVALID_SOCKET)
+ if (enable_bonjour && ListenSocket[0].socket != PGINVALID_SOCKET)
{
DNSServiceErrorType err;
@@ -1251,12 +1284,12 @@ PostmasterMain(int argc, char *argv[])
{
char *socketdir = (char *) lfirst(l);
- status = StreamServerPort(AF_UNIX, NULL,
+ socket = StreamServerPort(AF_UNIX, NULL,
(unsigned short) PostPortNumber,
socketdir,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful Unix socket in lockfile */
@@ -1267,9 +1300,23 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create Unix-domain socket in directory \"%s\"",
socketdir)));
+
+ if (ProxyPortNumber)
+ {
+ socket = StreamServerPort(AF_UNIX, NULL,
+ (unsigned short) ProxyPortNumber,
+ socketdir,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ socket->isProxy = true;
+ else
+ ereport(WARNING,
+ (errmsg("could not create Unix-domain PROXY socket for \"%s\"",
+ socketdir)));
+ }
}
- if (!success && elemlist != NIL)
+ if (socket == NULL && elemlist != NIL)
ereport(FATAL,
(errmsg("could not create any Unix-domain sockets")));
@@ -1281,7 +1328,7 @@ PostmasterMain(int argc, char *argv[])
/*
* check that we have some socket to listen on
*/
- if (ListenSocket[0] == PGINVALID_SOCKET)
+ if (ListenSocket[0].socket == PGINVALID_SOCKET)
ereport(FATAL,
(errmsg("no socket created for listening")));
@@ -1430,10 +1477,10 @@ CloseServerPorts(int status, Datum arg)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -1722,15 +1769,17 @@ ServerLoop(void)
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] == PGINVALID_SOCKET)
+ if (ListenSocket[i].socket == PGINVALID_SOCKET)
break;
- if (FD_ISSET(ListenSocket[i], &rmask))
+ if (FD_ISSET(ListenSocket[i].socket, &rmask))
{
Port *port;
- port = ConnCreate(ListenSocket[i]);
+ port = ConnCreate(ListenSocket[i].socket);
if (port)
{
+ port->isProxy = ListenSocket[i].isProxy;
+
BackendStartup(port);
/*
@@ -1898,7 +1947,7 @@ initMasks(fd_set *rmask)
for (i = 0; i < MAXLISTEN; i++)
{
- int fd = ListenSocket[i];
+ int fd = ListenSocket[i].socket;
if (fd == PGINVALID_SOCKET)
break;
@@ -1911,6 +1960,213 @@ initMasks(fd_set *rmask)
return maxsock + 1;
}
+static int
+UnwrapProxyConnection(Port *port)
+{
+ char proxyver;
+ uint16 proxyaddrlen;
+ SockAddr raddr_save;
+ int i;
+ bool useproxy = false;
+
+ /*
+ * These structs are from the PROXY protocol docs at
+ * http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
+ */
+ union
+ {
+ struct
+ { /* for TCP/UDP over IPv4, len = 12 */
+ uint32 src_addr;
+ uint32 dst_addr;
+ uint16 src_port;
+ uint16 dst_port;
+ } ip4;
+ struct
+ { /* for TCP/UDP over IPv6, len = 36 */
+ uint8 src_addr[16];
+ uint8 dst_addr[16];
+ uint16 src_port;
+ uint16 dst_port;
+ } ip6;
+ } proxyaddr;
+ struct
+ {
+ uint8 sig[12]; /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */
+ uint8 ver_cmd; /* protocol version and command */
+ uint8 fam; /* protocol family and address */
+ uint16 len; /* number of following bytes part of the
+ * header */
+ } proxyheader;
+
+
+ /* Else if it's on our list of trusted proxies */
+ if (TrustedProxyServers)
+ {
+ for (i = 0; i < *((int *) TrustedProxyServers) * 2; i += 2)
+ {
+ if (port->raddr.addr.ss_family == TrustedProxyServers[i + 1].ss_family)
+ {
+ /*
+ * Connection over unix sockets don't give us the source, so
+ * just check if they're allowed at all. For IP connections,
+ * verify that it's an allowed address.
+ */
+ if (port->raddr.addr.ss_family == AF_UNIX ||
+ pg_range_sockaddr(&port->raddr.addr,
+ &TrustedProxyServers[i + 1],
+ &TrustedProxyServers[i + 2]))
+ {
+ useproxy = true;
+ break;
+ }
+ }
+ }
+ }
+ if (!useproxy)
+ {
+ /*
+ * Connection is not from one of our trusted proxies, so reject it.
+ */
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("connection from unauthorized proxy server")));
+ return STATUS_ERROR;
+ }
+
+ /* Store a copy of the original address, for logging */
+ memcpy(&raddr_save, &port->raddr, sizeof(SockAddr));
+
+ pq_startmsgread();
+
+ /*
+ * PROXY requests always start with:
+ * \x0D \x0A \x0D \x0A \x00 \x0D \x0A \x51 \x55 \x49 \x54 \x0A
+ */
+
+ if (pq_getbytes((char *) &proxyheader, sizeof(proxyheader)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ if (memcmp(proxyheader.sig, "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a", sizeof(proxyheader.sig)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Proxy version is in the high 4 bits of the first byte */
+ proxyver = (proxyheader.ver_cmd & 0xF0) >> 4;
+ if (proxyver != 2)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol version: %x", proxyver)));
+ return STATUS_ERROR;
+ }
+
+ proxyaddrlen = pg_ntoh16(proxyheader.len);
+
+ if (pq_getbytes((char *) &proxyaddr, proxyaddrlen > sizeof(proxyaddr) ? sizeof(proxyaddr) : proxyaddrlen) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Lower 4 bits hold type of connection */
+ if (proxyheader.fam == 0)
+ {
+ /* LOCAL connection, so we ignore the address included */
+ }
+ else if (proxyheader.fam == 0x11)
+ {
+ /* TCPv4 */
+ if (proxyaddrlen < 12)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET;
+ port->raddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
+ }
+ else if (proxyheader.fam == 0x21)
+ {
+ /* TCPv6 */
+ if (proxyaddrlen < 36)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET6;
+ port->raddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->raddr.addr)->sin6_addr, proxyaddr.ip6.src_addr, 16);
+ ((struct sockaddr_in6 *) &port->raddr.addr)->sin6_port = proxyaddr.ip6.src_port;
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol connection type: %x", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+
+ /* If there is any more header data present, skip past it */
+ if (proxyaddrlen > sizeof(proxyaddr))
+ {
+ if (pq_discardbytes(proxyaddrlen - sizeof(proxyaddr)) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ }
+
+ pq_endmsgread();
+
+ /*
+ * Log what we've done if connection logging is enabled. We log the proxy
+ * connection here, and let the normal connection logging mechanism log
+ * the unwrapped connection.
+ */
+ if (Log_connections)
+ {
+ char remote_host[NI_MAXHOST];
+ char remote_port[NI_MAXSERV];
+ int ret;
+
+ remote_host[0] = '\0';
+ remote_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&raddr_save.addr, raddr_save.salen,
+ remote_host, sizeof(remote_host),
+ remote_port, sizeof(remote_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ ereport(LOG,
+ (errmsg("proxy connection from: host=%s port=%s",
+ remote_host,
+ remote_port)));
+
+ }
+
+ return STATUS_OK;
+}
/*
* Read a client's startup packet and do something according to it.
@@ -2019,7 +2275,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
#ifdef USE_SSL
/* No SSL when disabled or on Unix sockets */
- if (!LoadedSSL || IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!LoadedSSL || (IS_AF_UNIX(port->laddr.addr.ss_family) && !port->isProxy))
SSLok = 'N';
else
SSLok = 'S'; /* Support for SSL */
@@ -2056,7 +2312,7 @@ retry1:
#ifdef ENABLE_GSS
/* No GSSAPI encryption when on Unix socket */
- if (!IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!IS_AF_UNIX(port->laddr.addr.ss_family) || port->isProxy)
GSSok = 'G';
#endif
@@ -2555,10 +2811,10 @@ ClosePostmasterPorts(bool am_syslogger)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -4320,6 +4576,33 @@ BackendInitialize(Port *port)
InitializeTimeouts(); /* establishes SIGALRM handler */
PG_SETMASK(&StartupBlockSig);
+ /*
+ * Ready to begin client interaction. We will give up and _exit(1) after
+ * a time delay, so that a broken client can't hog a connection
+ * indefinitely. PreAuthDelay and any DNS interactions above don't count
+ * against the time limit.
+ *
+ * Note: AuthenticationTimeout is applied here while waiting for the
+ * startup packet, and then again in InitPostgres for the duration of any
+ * authentication operations. So a hostile client could tie up the
+ * process for nearly twice AuthenticationTimeout before we kick him off.
+ *
+ * Note: because PostgresMain will call InitializeTimeouts again, the
+ * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
+ * since we never use it again after this function.
+ */
+ RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
+
+ /* Check if this is a proxy connection and if so unwrap the proxying */
+ if (port->isProxy)
+ {
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
+ if (UnwrapProxyConnection(port) != STATUS_OK)
+ proc_exit(0);
+ disable_timeout(STARTUP_PACKET_TIMEOUT, false);
+ }
+
+
/*
* Get the remote host name and port for logging and status display.
*/
@@ -4371,28 +4654,11 @@ BackendInitialize(Port *port)
strspn(remote_host, "0123456789ABCDEFabcdef:") < strlen(remote_host))
port->remote_hostname = strdup(remote_host);
- /*
- * Ready to begin client interaction. We will give up and _exit(1) after
- * a time delay, so that a broken client can't hog a connection
- * indefinitely. PreAuthDelay and any DNS interactions above don't count
- * against the time limit.
- *
- * Note: AuthenticationTimeout is applied here while waiting for the
- * startup packet, and then again in InitPostgres for the duration of any
- * authentication operations. So a hostile client could tie up the
- * process for nearly twice AuthenticationTimeout before we kick him off.
- *
- * Note: because PostgresMain will call InitializeTimeouts again, the
- * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
- * since we never use it again after this function.
- */
- RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
- enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
-
/*
* Receive the startup packet (which might turn out to be a cancel request
* packet).
*/
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
status = ProcessStartupPacket(port, false, false);
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 3fd1a5fbe2..8b3be40a26 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -46,10 +46,12 @@
#include "commands/user.h"
#include "commands/vacuum.h"
#include "commands/variable.h"
+#include "common/ip.h"
#include "common/string.h"
#include "funcapi.h"
#include "jit/jit.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
@@ -227,6 +229,8 @@ static bool check_recovery_target_lsn(char **newval, void **extra, GucSource sou
static void assign_recovery_target_lsn(const char *newval, void *extra);
static bool check_primary_slot_name(char **newval, void **extra, GucSource source);
static bool check_default_with_oids(bool *newval, void **extra, GucSource source);
+static bool check_proxy_servers(char **newval, void **extra, GucSource source);
+static void assign_proxy_servers(const char *newval, void *extra);
/* Private functions in guc-file.l that need to be called from guc.c */
static ConfigVariable *ProcessConfigFileInternal(GucContext context,
@@ -2291,6 +2295,16 @@ static struct config_int ConfigureNamesInt[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_port", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the TCP port the server listens for PROXY connections on."),
+ NULL
+ },
+ &ProxyPortNumber,
+ 0, 0, 65535,
+ NULL, NULL, NULL
+ },
+
{
{"unix_socket_permissions", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
gettext_noop("Sets the access permissions of the Unix-domain socket."),
@@ -4242,6 +4256,17 @@ static struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_servers", PGC_SIGHUP, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the addresses for trusted proxy servers."),
+ NULL,
+ GUC_LIST_INPUT
+ },
+ &TrustedProxyServersString,
+ "",
+ check_proxy_servers, assign_proxy_servers, NULL
+ },
+
{
/*
* Can't be set by ALTER SYSTEM as it can lead to recursive definition
@@ -12227,4 +12252,118 @@ check_default_with_oids(bool *newval, void **extra, GucSource source)
return true;
}
+static bool
+check_proxy_servers(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ struct sockaddr_storage *myextra;
+
+ /* Special case when it's empty */
+ if (**newval == '\0')
+ {
+ *extra = NULL;
+ return true;
+ }
+
+ /* Need a modifiable copy of string */
+ rawstring = pstrdup(*newval);
+
+ /* Parse string into list of identifiers */
+ if (!SplitIdentifierString(rawstring, ',', &elemlist))
+ {
+ /* syntax error in list */
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ if (list_length(elemlist) == 0)
+ {
+ /* If it had only whitespace */
+ pfree(rawstring);
+ list_free(elemlist);
+
+ *extra = NULL;
+ return true;
+ }
+
+ /*
+ * We store the result in an array of sockaddr_storage. The first entry is
+ * just an overloaded int which holds the size of the array.
+ */
+ myextra = (struct sockaddr_storage *) guc_malloc(ERROR, sizeof(struct sockaddr_storage) * (list_length(elemlist) * 2 + 1));
+ *((int *) &myextra[0]) = list_length(elemlist);
+
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+ char *netmasktok = NULL;
+ int ret;
+ struct addrinfo *gai_result;
+ struct addrinfo hints;
+
+ /*
+ * Unix sockets don't have endpoint addresses, so just flag them as
+ * AF_UNIX
+ */
+ if (pg_strcasecmp(tok, "unix") == 0)
+ {
+ myextra[foreach_current_index(l) * 2 + 1].ss_family = AF_UNIX;
+ continue;
+ }
+
+ netmasktok = strchr(tok, '/');
+ if (netmasktok)
+ {
+ *netmasktok = '\0';
+ netmasktok++;
+ }
+
+ memset((char *) &hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ ret = pg_getaddrinfo_all(tok, NULL, &hints, &gai_result);
+ if (ret != 0 || gai_result == NULL)
+ {
+ GUC_check_errdetail("Invalid IP addrress %s", tok);
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+
+ memcpy((char *) &myextra[foreach_current_index(l) * 2 + 1], gai_result->ai_addr, gai_result->ai_addrlen);
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+
+ /* A NULL netmasktok means the fully set hostmask */
+ if (pg_sockaddr_cidr_mask(&myextra[foreach_current_index(l) * 2 + 2], netmasktok, myextra[foreach_current_index(l) * 2 + 1].ss_family) != 0)
+ {
+ if (netmasktok)
+ GUC_check_errdetail("Invalid netmask %s", netmasktok);
+ else
+ GUC_check_errdetail("Could not create netmask");
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+ }
+
+ pfree(rawstring);
+ list_free(elemlist);
+ *extra = (void *) myextra;
+
+ return true;
+}
+
+static void
+assign_proxy_servers(const char *newval, void *extra)
+{
+ TrustedProxyServers = (struct sockaddr_storage *) extra;
+}
+
#include "guc-file.c"
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ee06528bb0..b002228393 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -61,6 +61,9 @@
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
+#proxy_port = 0 # port to listen to for proxy connections
+ # (change requires restart)
+#proxy_servers = '' # what proxy servers to trust
#max_connections = 100 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
#unix_socket_directories = '/tmp' # comma-separated list of directories
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 7be1a67d69..57edda122a 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -121,6 +121,7 @@ typedef struct Port
{
pgsocket sock; /* File descriptor */
bool noblock; /* is the socket in non-blocking mode? */
+ bool isProxy; /* is the connection using PROXY protocol */
ProtocolVersion proto; /* FE/BE protocol version */
SockAddr laddr; /* local addr (postmaster) */
SockAddr raddr; /* remote addr (client) */
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index b20deeb555..c06ee29f88 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -33,6 +33,12 @@ typedef struct
extern const PGDLLIMPORT PQcommMethods *PqCommMethods;
+typedef struct
+{
+ pgsocket socket;
+ bool isProxy;
+} PQlistenSocket;
+
#define pq_comm_reset() (PqCommMethods->comm_reset())
#define pq_flush() (PqCommMethods->flush())
#define pq_flush_if_writable() (PqCommMethods->flush_if_writable())
@@ -54,9 +60,9 @@ extern WaitEventSet *FeBeWaitSet;
#define FeBeWaitSetSocketPos 0
#define FeBeWaitSetLatchPos 1
-extern int StreamServerPort(int family, const char *hostName,
- unsigned short portNumber, const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen);
+extern PQlistenSocket *StreamServerPort(int family, const char *hostName,
+ unsigned short portNumber, const char *unixSocketDir,
+ PQlistenSocket PQlistenSocket[], int MaxListen);
extern int StreamConnection(pgsocket server_fd, Port *port);
extern void StreamClose(pgsocket sock);
extern void TouchSocketFiles(void);
@@ -69,6 +75,7 @@ extern bool pq_is_reading_msg(void);
extern int pq_getmessage(StringInfo s, int maxlen);
extern int pq_getbyte(void);
extern int pq_peekbyte(void);
+extern int pq_discardbytes(size_t len);
extern int pq_getbyte_if_available(unsigned char *c);
extern int pq_putmessage_v2(char msgtype, const char *s, size_t len);
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index cfa59c4dc0..9ed219dfda 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -17,10 +17,13 @@
extern bool EnableSSL;
extern int ReservedBackends;
extern PGDLLIMPORT int PostPortNumber;
+extern PGDLLIMPORT int ProxyPortNumber;
extern int Unix_socket_permissions;
extern char *Unix_socket_group;
extern char *Unix_socket_directories;
extern char *ListenAddresses;
+extern char *TrustedProxyServersString;
+extern struct sockaddr_storage *TrustedProxyServers;
extern bool ClientAuthInProgress;
extern int PreAuthDelay;
extern int AuthenticationTimeout;
diff --git a/src/test/Makefile b/src/test/Makefile
index f7859c2fd5..cfb9a319a2 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -13,7 +13,7 @@ top_builddir = ../..
include $(top_builddir)/src/Makefile.global
SUBDIRS = perl regress isolation modules authentication recovery subscription \
- locale
+ locale protocol
# Test suites that are not safe by default but can be run if selected
# by the user via the whitespace-separated list in variable
diff --git a/src/test/protocol/Makefile b/src/test/protocol/Makefile
new file mode 100644
index 0000000000..bda49d6ecb
--- /dev/null
+++ b/src/test/protocol/Makefile
@@ -0,0 +1,23 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/protocol
+#
+# Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/protocol/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/protocol
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
+
+clean distclean maintainer-clean:
+ rm -rf tmp_check
diff --git a/src/test/protocol/t/001_proxy.pl b/src/test/protocol/t/001_proxy.pl
new file mode 100644
index 0000000000..c84d797e4f
--- /dev/null
+++ b/src/test/protocol/t/001_proxy.pl
@@ -0,0 +1,157 @@
+use strict;
+use warnings;
+use TestLib;
+use PostgresNode;
+use Test::More;
+use Socket qw(AF_INET AF_INET6 inet_pton);
+use IO::Socket;
+
+plan tests => 35;
+
+my $node = get_new_node('node');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+listen_addresses = 'localhost'
+log_connections = on
+});
+$node->append_conf(
+ 'pg_hba.conf', qq{
+host all all 11.22.33.44/32 trust
+host all all 1:2:3:4:5:6:0:9/128 trust
+});
+$node->append_conf('postgresql.conf', "proxy_port = " . ($node->port() + 1));
+
+$node->start;
+
+sub make_message
+{
+ my ($msg) = @_;
+ return pack("Na*", length($msg) + 4, $msg);
+}
+
+sub read_packet
+{
+ my ($socket) = @_;
+ my $buf = "";
+ $socket->recv($buf, 1024);
+ return $buf;
+}
+
+
+# Test normal connection through localhost
+sub test_connection
+{
+ my ($socket, $proxy, $what, $shouldbe, $shouldfail, $extra) = @_;
+ ok($socket, $what);
+
+ my $startup = make_message(
+ pack("N(Z*Z*)*x", 196608, (user => "mha", database => "postgres")));
+
+ $extra = "" if !defined($extra);
+
+ if (defined($proxy))
+ {
+ my $p = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A\x21";
+ if ($proxy =~ ":")
+ {
+ # ipv6
+ $p .= "\x21"; # TCP v6
+ $p .= pack "n", 36 + length($extra); # size
+ $p .= inet_pton(AF_INET6, $proxy);
+ $p .= "\0" x 16; # destination address
+ }
+ else
+ {
+ # ipv4
+ $p .= "\x11"; # TCP v4
+ $p .= pack "n", 12 + length($extra); # size
+ $p .= inet_pton(AF_INET, $proxy);
+ $p .= "\0\0\0\0"; # destination address
+ }
+ $p .= pack "n", 1919; # source port
+ $p .= pack "n", 0;
+ $p .= $extra;
+ print $socket $p;
+ }
+ print $socket $startup;
+
+ my $in = read_packet($socket);
+ if (defined($shouldfail))
+ {
+ isnt(substr($in, 0, 1), 'R', $what);
+ }
+ else
+ {
+ is(substr($in, 0, 1), 'R', $what);
+ }
+
+ SKIP:
+ {
+ skip "The rest of this test should fail", 3 if (defined($shouldfail));
+
+ is(substr($in, 8, 1), "\0", $what);
+
+ my ($resip, $resport) = split /\|/,
+ $node->safe_psql('postgres',
+ "SELECT client_addr, client_port FROM pg_stat_activity WHERE pid != pg_backend_pid() AND backend_type='client backend'"
+ );
+ is($resip, $shouldbe, $what);
+ if ($proxy)
+ {
+ is($resport, "1919", $what);
+ }
+ else
+ {
+ ok($resport, $what);
+ }
+ }
+
+ $socket->close();
+
+ return;
+}
+
+sub inet_socket
+{
+ my ($port) = @_;
+ return IO::Socket::INET->new(
+ PeerAddr => "127.0.0.1",
+ PeerPort => $port,
+ Proto => "tcp",
+ Type => SOCK_STREAM);
+}
+
+sub unix_socket
+{
+ my ($port) = @_;
+ return IO::Socket::UNIX->new(
+ Peer => $node->host() . "/.s.PGSQL." . $port,
+ Type => SOCK_STREAM);
+}
+
+
+# Test a regular connection first to make sure connecting etc works fine.
+test_connection(
+ inet_socket($node->port()), undef,
+ "normal ipv4 connection", "127.0.0.1");
+test_connection(unix_socket($node->port()),
+ undef, "normal unix connection", "");
+
+# Make sure we can't make a proxy connection until it's allowed
+test_connection(inet_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44", 1);
+
+# Allow proxy connections and test them
+$node->append_conf('postgresql.conf', "proxy_servers = 'unix, 127.0.0.1/32'");
+$node->restart();
+
+test_connection(inet_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44");
+test_connection(inet_socket($node->port() + 1),
+ "1:2:3:4:5:6::9", "proxy ipv6", "1:2:3:4:5:6:0:9");
+test_connection(unix_socket($node->port() + 1),
+ "11.22.33.44", "proxy unix", "11.22.33.44");
+
+test_connection(unix_socket($node->port() + 1),
+ "11.22.33.44", "proxy unix with extra", "11.22.33.44", undef, "abcdef"x100);
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-03-09 10:25 ` Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 08:08 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 2 replies; 56+ messages in thread
From: Magnus Hagander @ 2021-03-09 10:25 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Sat, Mar 6, 2021 at 5:30 PM Magnus Hagander <magnus@hagander.net> wrote:
>
> On Sat, Mar 6, 2021 at 4:17 PM Magnus Hagander <magnus@hagander.net> wrote:
> >
> > On Fri, Mar 5, 2021 at 8:11 PM Jacob Champion <pchampion@vmware.com> wrote:
> > >
> > > On Fri, 2021-03-05 at 10:22 +0100, Magnus Hagander wrote:
> > > > On Fri, Mar 5, 2021 at 12:21 AM Jacob Champion <pchampion@vmware.com> wrote:
> > > > > The original-host logging isn't working for me:
> > > > >
> > > > > [...]
> > > >
> > > > That's interesting -- it works perfectly fine here. What platform are
> > > > you testing on?
> > >
> > > Ubuntu 20.04.
> >
> > Curious. It doesn't show up on my debian.
> >
> > But either way -- it was clearly wrong :)
> >
> >
> > > > (I sent for sizeof(SockAddr) to make it
> > > > easier to read without having to look things up, but the net result is
> > > > the same)
> > >
> > > Cool. Did you mean to attach a patch?
> >
> > I didn't, I had some other hacks that were broken :) I've attached one
> > now which includes those changes.
> >
> >
> > > == More Notes ==
> > >
> > > (Stop me if I'm digging too far into a proof of concept patch.)
> >
> > Definitely not -- much appreciated, and just what was needed to take
> > it from poc to a proper one!
> >
> >
> > > > + proxyaddrlen = pg_ntoh16(proxyheader.len);
> > > > +
> > > > + if (proxyaddrlen > sizeof(proxyaddr))
> > > > + {
> > > > + ereport(COMMERROR,
> > > > + (errcode(ERRCODE_PROTOCOL_VIOLATION),
> > > > + errmsg("oversized proxy packet")));
> > > > + return STATUS_ERROR;
> > > > + }
> > >
> > > I think this is not quite right -- if there's additional data beyond
> > > the IPv6 header size, that just means there are TLVs tacked onto the
> > > header that we should ignore. (Or, eventually, use.)
> >
> > Yeah, you're right. Fallout of too much moving around. I think inthe
> > end that code should just be removed, in favor of the discard path as
> > you mentinoed below.
> >
> >
> > > Additionally, we need to check for underflow as well. A misbehaving
> > > proxy might not send enough data to fill up the address block for the
> > > address family in use.
> >
> > I used to have that check. I seem to have lost it in restructuring. Added back!
> >
> >
> > > > + /* If there is any more header data present, skip past it */
> > > > + if (proxyaddrlen > sizeof(proxyaddr))
> > > > + pq_discardbytes(proxyaddrlen - sizeof(proxyaddr));
> > >
> > > This looks like dead code, given that we'll error out for the same
> > > check above -- but once it's no longer dead code, the return value of
> > > pq_discardbytes should be checked for EOF.
> >
> > Yup.
> >
> >
> > > > + else if (proxyheader.fam == 0x11)
> > > > + {
> > > > + /* TCPv4 */
> > > > + port->raddr.addr.ss_family = AF_INET;
> > > > + port->raddr.salen = sizeof(struct sockaddr_in);
> > > > + ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
> > > > + ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
> > > > + }
> > >
> > > I'm trying to reason through the fallout of setting raddr and not
> > > laddr. I understand why we're not setting laddr -- several places in
> > > the code rely on the laddr to actually refer to a machine-local address
> > > -- but the fact that there is no actual connection from raddr to laddr
> > > could cause shenanigans. For example, the ident auth protocol will just
> > > break (and it might be nice to explicitly disable it for PROXY
> > > connections). Are there any other situations where a "faked" raddr
> > > could throw off Postgres internals?
> >
> > That's a good point to discuss. I thought about it initially and
> > figured it'd be even worse to actually copy over laddr since that
> > woudl then suddenly have the IP address belonging to a different
> > machine.. And then I forgot to enumerate the other cases.
> >
> > For ident, disabling the method seems reasonable.
> >
> > Another thing that shows up with added support for running the proxy
> > protocol over Unix sockets, is that PostgreSQL refuses to do SSL over
> > Unix sockets. So that check has to be updated to allow it over proxy
> > connections. Same for GSSAPI.
> >
> > An interesting thing is what to do about
> > inet_server_addr/inet_server_port. That sort of loops back up to the
> > original question of where/how to expose the information about the
> > proxy in general (since right now it just logs). Right now you can
> > actually use inet_server_port() to see if the connection was proxied
> > (as long as it was over tcp).
> >
> > Attached is an updated, which covers your comments, as well as adds
> > unix socket support (per your question and Alvaros confirmed usecase).
> > It allows proxy connections over unix sockets, but I saw no need to
> > get into unix sockets over the proxy protocol (dealing with paths
> > between machines etc).
> >
> > I changed the additional ListenSocket array to instead declare
> > ListenSocket as an array of structs holding two fields. Seems cleaner,
> > and especially should there be further extensions needed in the
> > future.
> >
> > I've also added some trivial tests (man that took an ungodly amount of
> > fighting perl -- it's clearly been a long time since I used perl
> > properly). They probably need some more love but it's a start.
> >
> > And of course rebased.
>
> Pfft, I was hoping for cfbot to pick it up and test it on a different
> platform. Of course, for it to do that, I need to include the test
> directory in the Makefile. Here's a new one which adds that, no other
> changes.
So cfbot didn't like thato ne one bit. Turns out that it's not a great
idea to hardcode the username "mha" in the tests :)
And also changed to only use unix sockets for the tests on linux, and
tcp only on windows. Because that's how our tests are supposed to be.
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
Attachments:
[text/x-patch] proxy_protocol_5.patch (35.4K, ../../CABUevEzE9FZfjc7ZS=iHzaw+8GFcFjzA7AqEyZ9fBJt=rkgj-A@mail.gmail.com/2-proxy_protocol_5.patch)
download | inline diff:
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index b420486a0a..5d8fcc3d50 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -353,6 +353,15 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl
the client's host name instead of the IP address in the log.
</para>
+ <para>
+ If <xref linkend="guc-proxy-port"/> is enabled and the
+ connection is made through a proxy server using the PROXY
+ protocol, the actual IP address of the client will be used
+ for matching. If a connection is made through a proxy server
+ not using the PROXY protocol, the IP address of the
+ proxy server will be used.
+ </para>
+
<para>
These fields do not apply to <literal>local</literal> records.
</para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 529876895b..6c2bd2f4fe 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -682,6 +682,56 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-proxy-port" xreflabel="proxy_port">
+ <term><varname>proxy_port</varname> (<type>integer</type>)
+ <indexterm>
+ <primary><varname>proxy_port</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ The TCP port the server listens on for PROXY connections, disabled by
+ default. If set to a number, <productname>PostgreSQL</productname>
+ will listen on this port on the same addresses as for regular
+ connections, but expect all connections to use the PROXY protocol to
+ identify the client. This parameter can only be set at server start.
+ </para>
+ <para>
+ If a proxy connection is done over this port, and the proxy is listed
+ in <xref linkend="guc-proxy-servers" />, the actual client address
+ will be considered as the address of the client, instead of listing
+ all connections as coming from the proxy server.
+ </para>
+ <para>
+ The <ulink url="http://www.haproxy.org/download/1.9/doc/proxy-protocol.txt">PROXY
+ protocol</ulink> is maintained by <productname>HAProxy</productname>,
+ and supported in many proxies and load
+ balancers. <productname>PostgreSQL</productname> supports version 2
+ of the protocol.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-proxy-servers" xreflabel="proxy_servers">
+ <term><varname>proxy_servers</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>proxy_servers</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ A comma separated list of one or more host names, cidr specifications or the
+ literal <literal>unix</literal>, indicating which proxy servers to trust when
+ connecting on the port specified in <xref linkend="guc-proxy-port" />.
+ </para>
+ <para>
+ If a proxy connection is made from an IP address not covered by this
+ list, the connection will be rejected. By default no proxy is trusted
+ and all proxy connections will be rejected.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-max-connections" xreflabel="max_connections">
<term><varname>max_connections</varname> (<type>integer</type>)
<indexterm>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 994251e7d9..470e36db58 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -1761,6 +1761,14 @@ ident_inet(hbaPort *port)
*la = NULL,
hints;
+ if (port->isProxy)
+ {
+ ereport(LOG,
+ (errcode_for_socket_access(),
+ errmsg("Ident authentication cannot be used over PROXY connections")));
+ return STATUS_ERROR;
+ }
+
/*
* Might look a little weird to first convert it to text and then back to
* sockaddr, but it's protocol independent.
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 4c7b1e7bfd..9f92195097 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -308,13 +308,13 @@ socket_close(int code, Datum arg)
* Successfully opened sockets are added to the ListenSocket[] array (of
* length MaxListen), at the first position that isn't PGINVALID_SOCKET.
*
- * RETURNS: STATUS_OK or STATUS_ERROR
+ * RETURNS: The PQlistenSocket listening on, or NULL in case of error
*/
-int
+PQlistenSocket *
StreamServerPort(int family, const char *hostName, unsigned short portNumber,
const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen)
+ PQlistenSocket ListenSocket[], int MaxListen)
{
pgsocket fd;
int err;
@@ -359,10 +359,10 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("Unix-domain socket path \"%s\" is too long (maximum %d bytes)",
unixSocketPath,
(int) (UNIXSOCK_PATH_BUFLEN - 1))));
- return STATUS_ERROR;
+ return NULL;
}
if (Lock_AF_UNIX(unixSocketDir, unixSocketPath) != STATUS_OK)
- return STATUS_ERROR;
+ return NULL;
service = unixSocketPath;
}
else
@@ -385,7 +385,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
service, gai_strerror(ret))));
if (addrs)
pg_freeaddrinfo_all(hint.ai_family, addrs);
- return STATUS_ERROR;
+ return NULL;
}
for (addr = addrs; addr; addr = addr->ai_next)
@@ -402,7 +402,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
/* See if there is still room to add 1 more socket. */
for (; listen_index < MaxListen; listen_index++)
{
- if (ListenSocket[listen_index] == PGINVALID_SOCKET)
+ if (ListenSocket[listen_index].socket == PGINVALID_SOCKET)
break;
}
if (listen_index >= MaxListen)
@@ -579,16 +579,16 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("listening on %s address \"%s\", port %d",
familyDesc, addrDesc, (int) portNumber)));
- ListenSocket[listen_index] = fd;
+ ListenSocket[listen_index].socket = fd;
added++;
}
pg_freeaddrinfo_all(hint.ai_family, addrs);
if (!added)
- return STATUS_ERROR;
+ return NULL;
- return STATUS_OK;
+ return &ListenSocket[listen_index];
}
@@ -1113,7 +1113,7 @@ pq_getbytes(char *s, size_t len)
* returns 0 if OK, EOF if trouble
* --------------------------------
*/
-static int
+int
pq_discardbytes(size_t len)
{
size_t amount;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index edab95a19e..0344f907c9 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -102,6 +102,7 @@
#include "common/string.h"
#include "lib/ilist.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
@@ -195,15 +196,22 @@ BackgroundWorker *MyBgworkerEntry = NULL;
-/* The socket number we are listening for connections on */
+/* The TCP port number we are listening for connections on */
int PostPortNumber;
+/* The TCP port number we are listening for proxy connections on */
+int ProxyPortNumber;
+
/* The directory names for Unix socket(s) */
char *Unix_socket_directories;
/* The TCP listen address(es) */
char *ListenAddresses;
+/* Trusted proxy servers */
+char *TrustedProxyServersString = NULL;
+struct sockaddr_storage *TrustedProxyServers = NULL;
+
/*
* ReservedBackends is the number of backends reserved for superuser use.
* This number is taken out of the pool size given by MaxConnections so
@@ -217,7 +225,7 @@ int ReservedBackends;
/* The socket(s) we're listening to. */
#define MAXLISTEN 64
-static pgsocket ListenSocket[MAXLISTEN];
+static PQlistenSocket ListenSocket[MAXLISTEN];
/*
* These globals control the behavior of the postmaster in case some
@@ -581,6 +589,7 @@ PostmasterMain(int argc, char *argv[])
bool listen_addr_saved = false;
int i;
char *output_config_variable = NULL;
+ PQlistenSocket *socket = NULL;
InitProcessGlobals();
@@ -1124,7 +1133,10 @@ PostmasterMain(int argc, char *argv[])
* charged with closing the sockets again at postmaster shutdown.
*/
for (i = 0; i < MAXLISTEN; i++)
- ListenSocket[i] = PGINVALID_SOCKET;
+ {
+ ListenSocket[i].socket = PGINVALID_SOCKET;
+ ListenSocket[i].isProxy = false;
+ }
on_proc_exit(CloseServerPorts, 0);
@@ -1153,17 +1165,17 @@ PostmasterMain(int argc, char *argv[])
char *curhost = (char *) lfirst(l);
if (strcmp(curhost, "*") == 0)
- status = StreamServerPort(AF_UNSPEC, NULL,
+ socket = StreamServerPort(AF_UNSPEC, NULL,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
else
- status = StreamServerPort(AF_UNSPEC, curhost,
+ socket = StreamServerPort(AF_UNSPEC, curhost,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful host addr in lockfile */
@@ -1177,9 +1189,30 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create listen socket for \"%s\"",
curhost)));
+
+ /* Also listen to the PROXY port on this address, if configured */
+ if (ProxyPortNumber)
+ {
+ if (strcmp(curhost, "*") == 0)
+ socket = StreamServerPort(AF_UNSPEC, NULL,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ else
+ socket = StreamServerPort(AF_UNSPEC, curhost,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ socket->isProxy = true;
+ else
+ ereport(WARNING,
+ (errmsg("could not create PROXY listen socket for \"%s\"",
+ curhost)));
+ }
}
- if (!success && elemlist != NIL)
+ if (socket == NULL && elemlist != NIL)
ereport(FATAL,
(errmsg("could not create any TCP/IP sockets")));
@@ -1189,7 +1222,7 @@ PostmasterMain(int argc, char *argv[])
#ifdef USE_BONJOUR
/* Register for Bonjour only if we opened TCP socket(s) */
- if (enable_bonjour && ListenSocket[0] != PGINVALID_SOCKET)
+ if (enable_bonjour && ListenSocket[0].socket != PGINVALID_SOCKET)
{
DNSServiceErrorType err;
@@ -1251,12 +1284,12 @@ PostmasterMain(int argc, char *argv[])
{
char *socketdir = (char *) lfirst(l);
- status = StreamServerPort(AF_UNIX, NULL,
+ socket = StreamServerPort(AF_UNIX, NULL,
(unsigned short) PostPortNumber,
socketdir,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful Unix socket in lockfile */
@@ -1267,9 +1300,23 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create Unix-domain socket in directory \"%s\"",
socketdir)));
+
+ if (ProxyPortNumber)
+ {
+ socket = StreamServerPort(AF_UNIX, NULL,
+ (unsigned short) ProxyPortNumber,
+ socketdir,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ socket->isProxy = true;
+ else
+ ereport(WARNING,
+ (errmsg("could not create Unix-domain PROXY socket for \"%s\"",
+ socketdir)));
+ }
}
- if (!success && elemlist != NIL)
+ if (socket == NULL && elemlist != NIL)
ereport(FATAL,
(errmsg("could not create any Unix-domain sockets")));
@@ -1281,7 +1328,7 @@ PostmasterMain(int argc, char *argv[])
/*
* check that we have some socket to listen on
*/
- if (ListenSocket[0] == PGINVALID_SOCKET)
+ if (ListenSocket[0].socket == PGINVALID_SOCKET)
ereport(FATAL,
(errmsg("no socket created for listening")));
@@ -1430,10 +1477,10 @@ CloseServerPorts(int status, Datum arg)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -1722,15 +1769,17 @@ ServerLoop(void)
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] == PGINVALID_SOCKET)
+ if (ListenSocket[i].socket == PGINVALID_SOCKET)
break;
- if (FD_ISSET(ListenSocket[i], &rmask))
+ if (FD_ISSET(ListenSocket[i].socket, &rmask))
{
Port *port;
- port = ConnCreate(ListenSocket[i]);
+ port = ConnCreate(ListenSocket[i].socket);
if (port)
{
+ port->isProxy = ListenSocket[i].isProxy;
+
BackendStartup(port);
/*
@@ -1898,7 +1947,7 @@ initMasks(fd_set *rmask)
for (i = 0; i < MAXLISTEN; i++)
{
- int fd = ListenSocket[i];
+ int fd = ListenSocket[i].socket;
if (fd == PGINVALID_SOCKET)
break;
@@ -1911,6 +1960,213 @@ initMasks(fd_set *rmask)
return maxsock + 1;
}
+static int
+UnwrapProxyConnection(Port *port)
+{
+ char proxyver;
+ uint16 proxyaddrlen;
+ SockAddr raddr_save;
+ int i;
+ bool useproxy = false;
+
+ /*
+ * These structs are from the PROXY protocol docs at
+ * http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
+ */
+ union
+ {
+ struct
+ { /* for TCP/UDP over IPv4, len = 12 */
+ uint32 src_addr;
+ uint32 dst_addr;
+ uint16 src_port;
+ uint16 dst_port;
+ } ip4;
+ struct
+ { /* for TCP/UDP over IPv6, len = 36 */
+ uint8 src_addr[16];
+ uint8 dst_addr[16];
+ uint16 src_port;
+ uint16 dst_port;
+ } ip6;
+ } proxyaddr;
+ struct
+ {
+ uint8 sig[12]; /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */
+ uint8 ver_cmd; /* protocol version and command */
+ uint8 fam; /* protocol family and address */
+ uint16 len; /* number of following bytes part of the
+ * header */
+ } proxyheader;
+
+
+ /* Else if it's on our list of trusted proxies */
+ if (TrustedProxyServers)
+ {
+ for (i = 0; i < *((int *) TrustedProxyServers) * 2; i += 2)
+ {
+ if (port->raddr.addr.ss_family == TrustedProxyServers[i + 1].ss_family)
+ {
+ /*
+ * Connection over unix sockets don't give us the source, so
+ * just check if they're allowed at all. For IP connections,
+ * verify that it's an allowed address.
+ */
+ if (port->raddr.addr.ss_family == AF_UNIX ||
+ pg_range_sockaddr(&port->raddr.addr,
+ &TrustedProxyServers[i + 1],
+ &TrustedProxyServers[i + 2]))
+ {
+ useproxy = true;
+ break;
+ }
+ }
+ }
+ }
+ if (!useproxy)
+ {
+ /*
+ * Connection is not from one of our trusted proxies, so reject it.
+ */
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("connection from unauthorized proxy server")));
+ return STATUS_ERROR;
+ }
+
+ /* Store a copy of the original address, for logging */
+ memcpy(&raddr_save, &port->raddr, sizeof(SockAddr));
+
+ pq_startmsgread();
+
+ /*
+ * PROXY requests always start with:
+ * \x0D \x0A \x0D \x0A \x00 \x0D \x0A \x51 \x55 \x49 \x54 \x0A
+ */
+
+ if (pq_getbytes((char *) &proxyheader, sizeof(proxyheader)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ if (memcmp(proxyheader.sig, "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a", sizeof(proxyheader.sig)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Proxy version is in the high 4 bits of the first byte */
+ proxyver = (proxyheader.ver_cmd & 0xF0) >> 4;
+ if (proxyver != 2)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol version: %x", proxyver)));
+ return STATUS_ERROR;
+ }
+
+ proxyaddrlen = pg_ntoh16(proxyheader.len);
+
+ if (pq_getbytes((char *) &proxyaddr, proxyaddrlen > sizeof(proxyaddr) ? sizeof(proxyaddr) : proxyaddrlen) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Lower 4 bits hold type of connection */
+ if (proxyheader.fam == 0)
+ {
+ /* LOCAL connection, so we ignore the address included */
+ }
+ else if (proxyheader.fam == 0x11)
+ {
+ /* TCPv4 */
+ if (proxyaddrlen < 12)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET;
+ port->raddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
+ }
+ else if (proxyheader.fam == 0x21)
+ {
+ /* TCPv6 */
+ if (proxyaddrlen < 36)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET6;
+ port->raddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->raddr.addr)->sin6_addr, proxyaddr.ip6.src_addr, 16);
+ ((struct sockaddr_in6 *) &port->raddr.addr)->sin6_port = proxyaddr.ip6.src_port;
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol connection type: %x", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+
+ /* If there is any more header data present, skip past it */
+ if (proxyaddrlen > sizeof(proxyaddr))
+ {
+ if (pq_discardbytes(proxyaddrlen - sizeof(proxyaddr)) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ }
+
+ pq_endmsgread();
+
+ /*
+ * Log what we've done if connection logging is enabled. We log the proxy
+ * connection here, and let the normal connection logging mechanism log
+ * the unwrapped connection.
+ */
+ if (Log_connections)
+ {
+ char remote_host[NI_MAXHOST];
+ char remote_port[NI_MAXSERV];
+ int ret;
+
+ remote_host[0] = '\0';
+ remote_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&raddr_save.addr, raddr_save.salen,
+ remote_host, sizeof(remote_host),
+ remote_port, sizeof(remote_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ ereport(LOG,
+ (errmsg("proxy connection from: host=%s port=%s",
+ remote_host,
+ remote_port)));
+
+ }
+
+ return STATUS_OK;
+}
/*
* Read a client's startup packet and do something according to it.
@@ -2019,7 +2275,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
#ifdef USE_SSL
/* No SSL when disabled or on Unix sockets */
- if (!LoadedSSL || IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!LoadedSSL || (IS_AF_UNIX(port->laddr.addr.ss_family) && !port->isProxy))
SSLok = 'N';
else
SSLok = 'S'; /* Support for SSL */
@@ -2056,7 +2312,7 @@ retry1:
#ifdef ENABLE_GSS
/* No GSSAPI encryption when on Unix socket */
- if (!IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!IS_AF_UNIX(port->laddr.addr.ss_family) || port->isProxy)
GSSok = 'G';
#endif
@@ -2555,10 +2811,10 @@ ClosePostmasterPorts(bool am_syslogger)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -4320,6 +4576,33 @@ BackendInitialize(Port *port)
InitializeTimeouts(); /* establishes SIGALRM handler */
PG_SETMASK(&StartupBlockSig);
+ /*
+ * Ready to begin client interaction. We will give up and _exit(1) after
+ * a time delay, so that a broken client can't hog a connection
+ * indefinitely. PreAuthDelay and any DNS interactions above don't count
+ * against the time limit.
+ *
+ * Note: AuthenticationTimeout is applied here while waiting for the
+ * startup packet, and then again in InitPostgres for the duration of any
+ * authentication operations. So a hostile client could tie up the
+ * process for nearly twice AuthenticationTimeout before we kick him off.
+ *
+ * Note: because PostgresMain will call InitializeTimeouts again, the
+ * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
+ * since we never use it again after this function.
+ */
+ RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
+
+ /* Check if this is a proxy connection and if so unwrap the proxying */
+ if (port->isProxy)
+ {
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
+ if (UnwrapProxyConnection(port) != STATUS_OK)
+ proc_exit(0);
+ disable_timeout(STARTUP_PACKET_TIMEOUT, false);
+ }
+
+
/*
* Get the remote host name and port for logging and status display.
*/
@@ -4371,28 +4654,11 @@ BackendInitialize(Port *port)
strspn(remote_host, "0123456789ABCDEFabcdef:") < strlen(remote_host))
port->remote_hostname = strdup(remote_host);
- /*
- * Ready to begin client interaction. We will give up and _exit(1) after
- * a time delay, so that a broken client can't hog a connection
- * indefinitely. PreAuthDelay and any DNS interactions above don't count
- * against the time limit.
- *
- * Note: AuthenticationTimeout is applied here while waiting for the
- * startup packet, and then again in InitPostgres for the duration of any
- * authentication operations. So a hostile client could tie up the
- * process for nearly twice AuthenticationTimeout before we kick him off.
- *
- * Note: because PostgresMain will call InitializeTimeouts again, the
- * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
- * since we never use it again after this function.
- */
- RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
- enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
-
/*
* Receive the startup packet (which might turn out to be a cancel request
* packet).
*/
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
status = ProcessStartupPacket(port, false, false);
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index e337df42cb..67e59664b6 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -46,10 +46,12 @@
#include "commands/user.h"
#include "commands/vacuum.h"
#include "commands/variable.h"
+#include "common/ip.h"
#include "common/string.h"
#include "funcapi.h"
#include "jit/jit.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
@@ -227,6 +229,8 @@ static bool check_recovery_target_lsn(char **newval, void **extra, GucSource sou
static void assign_recovery_target_lsn(const char *newval, void *extra);
static bool check_primary_slot_name(char **newval, void **extra, GucSource source);
static bool check_default_with_oids(bool *newval, void **extra, GucSource source);
+static bool check_proxy_servers(char **newval, void **extra, GucSource source);
+static void assign_proxy_servers(const char *newval, void *extra);
/* Private functions in guc-file.l that need to be called from guc.c */
static ConfigVariable *ProcessConfigFileInternal(GucContext context,
@@ -2300,6 +2304,16 @@ static struct config_int ConfigureNamesInt[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_port", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the TCP port the server listens for PROXY connections on."),
+ NULL
+ },
+ &ProxyPortNumber,
+ 0, 0, 65535,
+ NULL, NULL, NULL
+ },
+
{
{"unix_socket_permissions", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
gettext_noop("Sets the access permissions of the Unix-domain socket."),
@@ -4251,6 +4265,17 @@ static struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_servers", PGC_SIGHUP, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the addresses for trusted proxy servers."),
+ NULL,
+ GUC_LIST_INPUT
+ },
+ &TrustedProxyServersString,
+ "",
+ check_proxy_servers, assign_proxy_servers, NULL
+ },
+
{
/*
* Can't be set by ALTER SYSTEM as it can lead to recursive definition
@@ -12236,4 +12261,118 @@ check_default_with_oids(bool *newval, void **extra, GucSource source)
return true;
}
+static bool
+check_proxy_servers(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ struct sockaddr_storage *myextra;
+
+ /* Special case when it's empty */
+ if (**newval == '\0')
+ {
+ *extra = NULL;
+ return true;
+ }
+
+ /* Need a modifiable copy of string */
+ rawstring = pstrdup(*newval);
+
+ /* Parse string into list of identifiers */
+ if (!SplitIdentifierString(rawstring, ',', &elemlist))
+ {
+ /* syntax error in list */
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ if (list_length(elemlist) == 0)
+ {
+ /* If it had only whitespace */
+ pfree(rawstring);
+ list_free(elemlist);
+
+ *extra = NULL;
+ return true;
+ }
+
+ /*
+ * We store the result in an array of sockaddr_storage. The first entry is
+ * just an overloaded int which holds the size of the array.
+ */
+ myextra = (struct sockaddr_storage *) guc_malloc(ERROR, sizeof(struct sockaddr_storage) * (list_length(elemlist) * 2 + 1));
+ *((int *) &myextra[0]) = list_length(elemlist);
+
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+ char *netmasktok = NULL;
+ int ret;
+ struct addrinfo *gai_result;
+ struct addrinfo hints;
+
+ /*
+ * Unix sockets don't have endpoint addresses, so just flag them as
+ * AF_UNIX
+ */
+ if (pg_strcasecmp(tok, "unix") == 0)
+ {
+ myextra[foreach_current_index(l) * 2 + 1].ss_family = AF_UNIX;
+ continue;
+ }
+
+ netmasktok = strchr(tok, '/');
+ if (netmasktok)
+ {
+ *netmasktok = '\0';
+ netmasktok++;
+ }
+
+ memset((char *) &hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ ret = pg_getaddrinfo_all(tok, NULL, &hints, &gai_result);
+ if (ret != 0 || gai_result == NULL)
+ {
+ GUC_check_errdetail("Invalid IP addrress %s", tok);
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+
+ memcpy((char *) &myextra[foreach_current_index(l) * 2 + 1], gai_result->ai_addr, gai_result->ai_addrlen);
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+
+ /* A NULL netmasktok means the fully set hostmask */
+ if (pg_sockaddr_cidr_mask(&myextra[foreach_current_index(l) * 2 + 2], netmasktok, myextra[foreach_current_index(l) * 2 + 1].ss_family) != 0)
+ {
+ if (netmasktok)
+ GUC_check_errdetail("Invalid netmask %s", netmasktok);
+ else
+ GUC_check_errdetail("Could not create netmask");
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+ }
+
+ pfree(rawstring);
+ list_free(elemlist);
+ *extra = (void *) myextra;
+
+ return true;
+}
+
+static void
+assign_proxy_servers(const char *newval, void *extra)
+{
+ TrustedProxyServers = (struct sockaddr_storage *) extra;
+}
+
#include "guc-file.c"
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index c6483fa1ff..a52ca19fa9 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -61,6 +61,9 @@
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
+#proxy_port = 0 # port to listen to for proxy connections
+ # (change requires restart)
+#proxy_servers = '' # what proxy servers to trust
#max_connections = 100 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
#unix_socket_directories = '/tmp' # comma-separated list of directories
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 30fb4e613d..2051ceb6e6 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -121,6 +121,7 @@ typedef struct Port
{
pgsocket sock; /* File descriptor */
bool noblock; /* is the socket in non-blocking mode? */
+ bool isProxy; /* is the connection using PROXY protocol */
ProtocolVersion proto; /* FE/BE protocol version */
SockAddr laddr; /* local addr (postmaster) */
SockAddr raddr; /* remote addr (client) */
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index b20deeb555..c06ee29f88 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -33,6 +33,12 @@ typedef struct
extern const PGDLLIMPORT PQcommMethods *PqCommMethods;
+typedef struct
+{
+ pgsocket socket;
+ bool isProxy;
+} PQlistenSocket;
+
#define pq_comm_reset() (PqCommMethods->comm_reset())
#define pq_flush() (PqCommMethods->flush())
#define pq_flush_if_writable() (PqCommMethods->flush_if_writable())
@@ -54,9 +60,9 @@ extern WaitEventSet *FeBeWaitSet;
#define FeBeWaitSetSocketPos 0
#define FeBeWaitSetLatchPos 1
-extern int StreamServerPort(int family, const char *hostName,
- unsigned short portNumber, const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen);
+extern PQlistenSocket *StreamServerPort(int family, const char *hostName,
+ unsigned short portNumber, const char *unixSocketDir,
+ PQlistenSocket PQlistenSocket[], int MaxListen);
extern int StreamConnection(pgsocket server_fd, Port *port);
extern void StreamClose(pgsocket sock);
extern void TouchSocketFiles(void);
@@ -69,6 +75,7 @@ extern bool pq_is_reading_msg(void);
extern int pq_getmessage(StringInfo s, int maxlen);
extern int pq_getbyte(void);
extern int pq_peekbyte(void);
+extern int pq_discardbytes(size_t len);
extern int pq_getbyte_if_available(unsigned char *c);
extern int pq_putmessage_v2(char msgtype, const char *s, size_t len);
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index cfa59c4dc0..9ed219dfda 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -17,10 +17,13 @@
extern bool EnableSSL;
extern int ReservedBackends;
extern PGDLLIMPORT int PostPortNumber;
+extern PGDLLIMPORT int ProxyPortNumber;
extern int Unix_socket_permissions;
extern char *Unix_socket_group;
extern char *Unix_socket_directories;
extern char *ListenAddresses;
+extern char *TrustedProxyServersString;
+extern struct sockaddr_storage *TrustedProxyServers;
extern bool ClientAuthInProgress;
extern int PreAuthDelay;
extern int AuthenticationTimeout;
diff --git a/src/test/Makefile b/src/test/Makefile
index f7859c2fd5..cfb9a319a2 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -13,7 +13,7 @@ top_builddir = ../..
include $(top_builddir)/src/Makefile.global
SUBDIRS = perl regress isolation modules authentication recovery subscription \
- locale
+ locale protocol
# Test suites that are not safe by default but can be run if selected
# by the user via the whitespace-separated list in variable
diff --git a/src/test/protocol/Makefile b/src/test/protocol/Makefile
new file mode 100644
index 0000000000..bda49d6ecb
--- /dev/null
+++ b/src/test/protocol/Makefile
@@ -0,0 +1,23 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/protocol
+#
+# Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/protocol/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/protocol
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
+
+clean distclean maintainer-clean:
+ rm -rf tmp_check
diff --git a/src/test/protocol/t/001_proxy.pl b/src/test/protocol/t/001_proxy.pl
new file mode 100644
index 0000000000..edc032d49c
--- /dev/null
+++ b/src/test/protocol/t/001_proxy.pl
@@ -0,0 +1,151 @@
+use strict;
+use warnings;
+use TestLib;
+use PostgresNode;
+use Test::More;
+use Socket qw(AF_INET AF_INET6 inet_pton);
+use IO::Socket;
+
+plan tests => 25;
+
+my $node = get_new_node('node');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+log_connections = on
+});
+$node->append_conf(
+ 'pg_hba.conf', qq{
+host all all 11.22.33.44/32 trust
+host all all 1:2:3:4:5:6:0:9/128 trust
+});
+$node->append_conf('postgresql.conf', "proxy_port = " . ($node->port() + 1));
+
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE USER proxytest;');
+
+sub make_message
+{
+ my ($msg) = @_;
+ return pack("Na*", length($msg) + 4, $msg);
+}
+
+sub read_packet
+{
+ my ($socket) = @_;
+ my $buf = "";
+ $socket->recv($buf, 1024);
+ return $buf;
+}
+
+
+# Test normal connection through localhost
+sub test_connection
+{
+ my ($socket, $proxy, $what, $shouldbe, $shouldfail, $extra) = @_;
+ ok($socket, $what);
+
+ my $startup = make_message(
+ pack("N(Z*Z*)*x", 196608, (user => "proxytest", database => "postgres")));
+
+ $extra = "" if !defined($extra);
+
+ if (defined($proxy))
+ {
+ my $p = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A\x21";
+ if ($proxy =~ ":")
+ {
+ # ipv6
+ $p .= "\x21"; # TCP v6
+ $p .= pack "n", 36 + length($extra); # size
+ $p .= inet_pton(AF_INET6, $proxy);
+ $p .= "\0" x 16; # destination address
+ }
+ else
+ {
+ # ipv4
+ $p .= "\x11"; # TCP v4
+ $p .= pack "n", 12 + length($extra); # size
+ $p .= inet_pton(AF_INET, $proxy);
+ $p .= "\0\0\0\0"; # destination address
+ }
+ $p .= pack "n", 1919; # source port
+ $p .= pack "n", 0;
+ $p .= $extra;
+ print $socket $p;
+ }
+ print $socket $startup;
+
+ my $in = read_packet($socket);
+ if (defined($shouldfail))
+ {
+ isnt(substr($in, 0, 1), 'R', $what);
+ }
+ else
+ {
+ is(substr($in, 0, 1), 'R', $what);
+ }
+
+ SKIP:
+ {
+ skip "The rest of this test should fail", 3 if (defined($shouldfail));
+
+ is(substr($in, 8, 1), "\0", $what);
+
+ my ($resip, $resport) = split /\|/,
+ $node->safe_psql('postgres',
+ "SELECT client_addr, client_port FROM pg_stat_activity WHERE pid != pg_backend_pid() AND backend_type='client backend'"
+ );
+ is($resip, $shouldbe, $what);
+ if ($proxy)
+ {
+ is($resport, "1919", $what);
+ }
+ else
+ {
+ ok($resport, $what);
+ }
+ }
+
+ $socket->close();
+
+ return;
+}
+
+sub make_socket
+{
+ my ($port) = @_;
+ if ($PostgresNode::use_tcp) {
+ return IO::Socket::INET->new(
+ PeerAddr => "127.0.0.1",
+ PeerPort => $port,
+ Proto => "tcp",
+ Type => SOCK_STREAM);
+ }
+ else {
+ return IO::Socket::UNIX->new(
+ Peer => $node->host() . "/.s.PGSQL." . $port,
+ Type => SOCK_STREAM);
+ }
+}
+
+# Test a regular connection first to make sure connecting etc works fine.
+test_connection(make_socket($node->port()),
+ undef, "normal connection", $PostgresNode::use_tcp ? "127.0.0.1": "");
+
+# Make sure we can't make a proxy connection until it's allowed
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44", 1);
+
+# Allow proxy connections and test them
+$node->append_conf('postgresql.conf', "proxy_servers = 'unix, 127.0.0.1/32'");
+$node->restart();
+
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44");
+test_connection(make_socket($node->port() + 1),
+ "1:2:3:4:5:6::9", "proxy ipv6", "1:2:3:4:5:6:0:9");
+
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy with extra", "11.22.33.44", undef, "abcdef"x100);
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-03-10 23:05 ` Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
1 sibling, 1 reply; 56+ messages in thread
From: Jacob Champion @ 2021-03-10 23:05 UTC (permalink / raw)
To: magnus@hagander.net <magnus@hagander.net>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Tue, 2021-03-09 at 11:25 +0100, Magnus Hagander wrote:
> I've also added some trivial tests (man that took an ungodly amount of
> fighting perl -- it's clearly been a long time since I used perl
> properly).
Yeah. The tests I'm writing for this and NSS have been the same way;
it's a real problem. I'm basically writing supplemental tests in Python
as the "daily driver", then trying to port whatever is easiest (not
much) into Perl, when I get time.
== More Notes ==
Some additional spec-compliance stuff:
> /* Lower 4 bits hold type of connection */
> if (proxyheader.fam == 0)
> {
> /* LOCAL connection, so we ignore the address included */
> }
(fam == 0) is the UNSPEC case, which isn't necessarily LOCAL. We have
to do something different for the LOCAL case:
> - \x0 : LOCAL : [...] 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.
So we should ignore the entire "protocol block" (by which I believe
they mean the protocol-and-address-family byte) in the case of LOCAL,
and just accept it with the original address info intact. That seems to
match the sample code in the back of the spec. The current behavior in
the patch will apply the PROXY behavior incorrectly if the sender sends
a LOCAL header with something other than UNSPEC -- which is strange
behavior but not explicitly prohibited as far as I can see.
We also need to reject all connections that aren't either LOCAL or
PROXY commands:
> - other values are unassigned and must not be emitted by senders.
> Receivers must drop connections presenting unexpected values here.
...and naturally it'd be Nice (tm) if the tests covered those corner
cases.
Over on the struct side:
> + struct
> + { /* for TCP/UDP over IPv4, len = 12 */
> + uint32 src_addr;
> + uint32 dst_addr;
> + uint16 src_port;
> + uint16 dst_port;
> + } ip4;
> ... snip ...
> + /* TCPv4 */
> + if (proxyaddrlen < 12)
> + {
Given the importance of these hardcoded lengths matching reality, is it
possible to add some static assertions to make sure that sizeof(<ipv4
block>) == 12 and so on? That would also save any poor souls who are
using compilers with nonstandard struct-packing behavior.
--Jacob
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
@ 2021-06-29 09:48 ` Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
0 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2021-06-29 09:48 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Thu, Mar 11, 2021 at 12:05 AM Jacob Champion <pchampion@vmware.com> wrote:
>
> On Tue, 2021-03-09 at 11:25 +0100, Magnus Hagander wrote:
> > I've also added some trivial tests (man that took an ungodly amount of
> > fighting perl -- it's clearly been a long time since I used perl
> > properly).
>
> Yeah. The tests I'm writing for this and NSS have been the same way;
> it's a real problem. I'm basically writing supplemental tests in Python
> as the "daily driver", then trying to port whatever is easiest (not
> much) into Perl, when I get time.
>
> == More Notes ==
>
> Some additional spec-compliance stuff:
>
> > /* Lower 4 bits hold type of connection */
> > if (proxyheader.fam == 0)
> > {
> > /* LOCAL connection, so we ignore the address included */
> > }
>
> (fam == 0) is the UNSPEC case, which isn't necessarily LOCAL. We have
> to do something different for the LOCAL case:
Oh ugh. yeah, and the comment is wrong too -- it got the "command"
confused with "connection family". Too many copy/paste I think.
> > - \x0 : LOCAL : [...] 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.
>
> So we should ignore the entire "protocol block" (by which I believe
> they mean the protocol-and-address-family byte) in the case of LOCAL,
> and just accept it with the original address info intact. That seems to
> match the sample code in the back of the spec. The current behavior in
> the patch will apply the PROXY behavior incorrectly if the sender sends
> a LOCAL header with something other than UNSPEC -- which is strange
> behavior but not explicitly prohibited as far as I can see.
Yeah, I think we do the right thing in the "right usecase".
> We also need to reject all connections that aren't either LOCAL or
> PROXY commands:
Indeed.
> > - other values are unassigned and must not be emitted by senders.
> > Receivers must drop connections presenting unexpected values here.
>
> ...and naturally it'd be Nice (tm) if the tests covered those corner
> cases.
I think that's covered in the attached update.
> Over on the struct side:
>
> > + struct
> > + { /* for TCP/UDP over IPv4, len = 12 */
> > + uint32 src_addr;
> > + uint32 dst_addr;
> > + uint16 src_port;
> > + uint16 dst_port;
> > + } ip4;
> > ... snip ...
> > + /* TCPv4 */
> > + if (proxyaddrlen < 12)
> > + {
>
> Given the importance of these hardcoded lengths matching reality, is it
> possible to add some static assertions to make sure that sizeof(<ipv4
> block>) == 12 and so on? That would also save any poor souls who are
> using compilers with nonstandard struct-packing behavior.
Yeah, probably makes sense. Added.
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
Attachments:
[text/x-patch] proxy_protocol_7.patch (36.8K, ../../CABUevEwJK52jqoXQOhGr2-6bA+MG3sFfELfPJ4hDGFoyH-orWA@mail.gmail.com/2-proxy_protocol_7.patch)
download | inline diff:
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 02f0489112..a3ff09b3ac 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -353,6 +353,15 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl
the client's host name instead of the IP address in the log.
</para>
+ <para>
+ If <xref linkend="guc-proxy-port"/> is enabled and the
+ connection is made through a proxy server using the PROXY
+ protocol, the actual IP address of the client will be used
+ for matching. If a connection is made through a proxy server
+ not using the PROXY protocol, the IP address of the
+ proxy server will be used.
+ </para>
+
<para>
These fields do not apply to <literal>local</literal> records.
</para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 6098f6b020..c8a7d2a3b7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -682,6 +682,56 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-proxy-port" xreflabel="proxy_port">
+ <term><varname>proxy_port</varname> (<type>integer</type>)
+ <indexterm>
+ <primary><varname>proxy_port</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ The TCP port the server listens on for PROXY connections, disabled by
+ default. If set to a number, <productname>PostgreSQL</productname>
+ will listen on this port on the same addresses as for regular
+ connections, but expect all connections to use the PROXY protocol to
+ identify the client. This parameter can only be set at server start.
+ </para>
+ <para>
+ If a proxy connection is done over this port, and the proxy is listed
+ in <xref linkend="guc-proxy-servers" />, the actual client address
+ will be considered as the address of the client, instead of listing
+ all connections as coming from the proxy server.
+ </para>
+ <para>
+ The <ulink url="http://www.haproxy.org/download/1.9/doc/proxy-protocol.txt">PROXY
+ protocol</ulink> is maintained by <productname>HAProxy</productname>,
+ and supported in many proxies and load
+ balancers. <productname>PostgreSQL</productname> supports version 2
+ of the protocol.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-proxy-servers" xreflabel="proxy_servers">
+ <term><varname>proxy_servers</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>proxy_servers</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ A comma separated list of one or more host names, cidr specifications or the
+ literal <literal>unix</literal>, indicating which proxy servers to trust when
+ connecting on the port specified in <xref linkend="guc-proxy-port" />.
+ </para>
+ <para>
+ If a proxy connection is made from an IP address not covered by this
+ list, the connection will be rejected. By default no proxy is trusted
+ and all proxy connections will be rejected.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-max-connections" xreflabel="max_connections">
<term><varname>max_connections</varname> (<type>integer</type>)
<indexterm>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 967b5ef73c..6cf6e51708 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -1851,6 +1851,14 @@ ident_inet(hbaPort *port)
*la = NULL,
hints;
+ if (port->isProxy)
+ {
+ ereport(LOG,
+ (errcode_for_socket_access(),
+ errmsg("Ident authentication cannot be used over PROXY connections")));
+ return STATUS_ERROR;
+ }
+
/*
* Might look a little weird to first convert it to text and then back to
* sockaddr, but it's protocol independent.
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 89a5f901aa..dd63be54e2 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -311,13 +311,13 @@ socket_close(int code, Datum arg)
* Successfully opened sockets are added to the ListenSocket[] array (of
* length MaxListen), at the first position that isn't PGINVALID_SOCKET.
*
- * RETURNS: STATUS_OK or STATUS_ERROR
+ * RETURNS: The PQlistenSocket listening on, or NULL in case of error
*/
-int
+PQlistenSocket *
StreamServerPort(int family, const char *hostName, unsigned short portNumber,
const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen)
+ PQlistenSocket ListenSocket[], int MaxListen)
{
pgsocket fd;
int err;
@@ -362,10 +362,10 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("Unix-domain socket path \"%s\" is too long (maximum %d bytes)",
unixSocketPath,
(int) (UNIXSOCK_PATH_BUFLEN - 1))));
- return STATUS_ERROR;
+ return NULL;
}
if (Lock_AF_UNIX(unixSocketDir, unixSocketPath) != STATUS_OK)
- return STATUS_ERROR;
+ return NULL;
service = unixSocketPath;
}
else
@@ -388,7 +388,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
service, gai_strerror(ret))));
if (addrs)
pg_freeaddrinfo_all(hint.ai_family, addrs);
- return STATUS_ERROR;
+ return NULL;
}
for (addr = addrs; addr; addr = addr->ai_next)
@@ -405,7 +405,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
/* See if there is still room to add 1 more socket. */
for (; listen_index < MaxListen; listen_index++)
{
- if (ListenSocket[listen_index] == PGINVALID_SOCKET)
+ if (ListenSocket[listen_index].socket == PGINVALID_SOCKET)
break;
}
if (listen_index >= MaxListen)
@@ -584,16 +584,16 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("listening on %s address \"%s\", port %d",
familyDesc, addrDesc, (int) portNumber)));
- ListenSocket[listen_index] = fd;
+ ListenSocket[listen_index].socket = fd;
added++;
}
pg_freeaddrinfo_all(hint.ai_family, addrs);
if (!added)
- return STATUS_ERROR;
+ return NULL;
- return STATUS_OK;
+ return &ListenSocket[listen_index];
}
@@ -1118,7 +1118,7 @@ pq_getbytes(char *s, size_t len)
* returns 0 if OK, EOF if trouble
* --------------------------------
*/
-static int
+int
pq_discardbytes(size_t len)
{
size_t amount;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 5a050898fe..725d6fac34 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -102,6 +102,7 @@
#include "common/string.h"
#include "lib/ilist.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
@@ -196,15 +197,22 @@ BackgroundWorker *MyBgworkerEntry = NULL;
-/* The socket number we are listening for connections on */
+/* The TCP port number we are listening for connections on */
int PostPortNumber;
+/* The TCP port number we are listening for proxy connections on */
+int ProxyPortNumber;
+
/* The directory names for Unix socket(s) */
char *Unix_socket_directories;
/* The TCP listen address(es) */
char *ListenAddresses;
+/* Trusted proxy servers */
+char *TrustedProxyServersString = NULL;
+struct sockaddr_storage *TrustedProxyServers = NULL;
+
/*
* ReservedBackends is the number of backends reserved for superuser use.
* This number is taken out of the pool size given by MaxConnections so
@@ -218,7 +226,7 @@ int ReservedBackends;
/* The socket(s) we're listening to. */
#define MAXLISTEN 64
-static pgsocket ListenSocket[MAXLISTEN];
+static PQlistenSocket ListenSocket[MAXLISTEN];
/*
* These globals control the behavior of the postmaster in case some
@@ -586,6 +594,7 @@ PostmasterMain(int argc, char *argv[])
bool listen_addr_saved = false;
int i;
char *output_config_variable = NULL;
+ PQlistenSocket *socket = NULL;
InitProcessGlobals();
@@ -1135,7 +1144,10 @@ PostmasterMain(int argc, char *argv[])
* charged with closing the sockets again at postmaster shutdown.
*/
for (i = 0; i < MAXLISTEN; i++)
- ListenSocket[i] = PGINVALID_SOCKET;
+ {
+ ListenSocket[i].socket = PGINVALID_SOCKET;
+ ListenSocket[i].isProxy = false;
+ }
on_proc_exit(CloseServerPorts, 0);
@@ -1164,17 +1176,17 @@ PostmasterMain(int argc, char *argv[])
char *curhost = (char *) lfirst(l);
if (strcmp(curhost, "*") == 0)
- status = StreamServerPort(AF_UNSPEC, NULL,
+ socket = StreamServerPort(AF_UNSPEC, NULL,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
else
- status = StreamServerPort(AF_UNSPEC, curhost,
+ socket = StreamServerPort(AF_UNSPEC, curhost,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful host addr in lockfile */
@@ -1188,9 +1200,30 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create listen socket for \"%s\"",
curhost)));
+
+ /* Also listen to the PROXY port on this address, if configured */
+ if (ProxyPortNumber)
+ {
+ if (strcmp(curhost, "*") == 0)
+ socket = StreamServerPort(AF_UNSPEC, NULL,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ else
+ socket = StreamServerPort(AF_UNSPEC, curhost,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ socket->isProxy = true;
+ else
+ ereport(WARNING,
+ (errmsg("could not create PROXY listen socket for \"%s\"",
+ curhost)));
+ }
}
- if (!success && elemlist != NIL)
+ if (socket == NULL && elemlist != NIL)
ereport(FATAL,
(errmsg("could not create any TCP/IP sockets")));
@@ -1200,7 +1233,7 @@ PostmasterMain(int argc, char *argv[])
#ifdef USE_BONJOUR
/* Register for Bonjour only if we opened TCP socket(s) */
- if (enable_bonjour && ListenSocket[0] != PGINVALID_SOCKET)
+ if (enable_bonjour && ListenSocket[0].socket != PGINVALID_SOCKET)
{
DNSServiceErrorType err;
@@ -1262,12 +1295,12 @@ PostmasterMain(int argc, char *argv[])
{
char *socketdir = (char *) lfirst(l);
- status = StreamServerPort(AF_UNIX, NULL,
+ socket = StreamServerPort(AF_UNIX, NULL,
(unsigned short) PostPortNumber,
socketdir,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful Unix socket in lockfile */
@@ -1278,9 +1311,23 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create Unix-domain socket in directory \"%s\"",
socketdir)));
+
+ if (ProxyPortNumber)
+ {
+ socket = StreamServerPort(AF_UNIX, NULL,
+ (unsigned short) ProxyPortNumber,
+ socketdir,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ socket->isProxy = true;
+ else
+ ereport(WARNING,
+ (errmsg("could not create Unix-domain PROXY socket for \"%s\"",
+ socketdir)));
+ }
}
- if (!success && elemlist != NIL)
+ if (socket == NULL && elemlist != NIL)
ereport(FATAL,
(errmsg("could not create any Unix-domain sockets")));
@@ -1292,7 +1339,7 @@ PostmasterMain(int argc, char *argv[])
/*
* check that we have some socket to listen on
*/
- if (ListenSocket[0] == PGINVALID_SOCKET)
+ if (ListenSocket[0].socket == PGINVALID_SOCKET)
ereport(FATAL,
(errmsg("no socket created for listening")));
@@ -1441,10 +1488,10 @@ CloseServerPorts(int status, Datum arg)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -1733,15 +1780,17 @@ ServerLoop(void)
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] == PGINVALID_SOCKET)
+ if (ListenSocket[i].socket == PGINVALID_SOCKET)
break;
- if (FD_ISSET(ListenSocket[i], &rmask))
+ if (FD_ISSET(ListenSocket[i].socket, &rmask))
{
Port *port;
- port = ConnCreate(ListenSocket[i]);
+ port = ConnCreate(ListenSocket[i].socket);
if (port)
{
+ port->isProxy = ListenSocket[i].isProxy;
+
BackendStartup(port);
/*
@@ -1909,7 +1958,7 @@ initMasks(fd_set *rmask)
for (i = 0; i < MAXLISTEN; i++)
{
- int fd = ListenSocket[i];
+ int fd = ListenSocket[i].socket;
if (fd == PGINVALID_SOCKET)
break;
@@ -1922,6 +1971,256 @@ initMasks(fd_set *rmask)
return maxsock + 1;
}
+static int
+UnwrapProxyConnection(Port *port)
+{
+ char proxyver;
+ uint16 proxyaddrlen;
+ SockAddr raddr_save;
+ int i;
+ bool useproxy = false;
+
+ /*
+ * These structs are from the PROXY protocol docs at
+ * http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
+ */
+ union
+ {
+ struct
+ { /* for TCP/UDP over IPv4, len = 12 */
+ uint32 src_addr;
+ uint32 dst_addr;
+ uint16 src_port;
+ uint16 dst_port;
+ } ip4;
+ struct
+ { /* for TCP/UDP over IPv6, len = 36 */
+ uint8 src_addr[16];
+ uint8 dst_addr[16];
+ uint16 src_port;
+ uint16 dst_port;
+ } ip6;
+ } proxyaddr;
+ struct
+ {
+ uint8 sig[12]; /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */
+ uint8 ver_cmd; /* protocol version and command */
+ uint8 fam; /* protocol family and address */
+ uint16 len; /* number of following bytes part of the
+ * header */
+ } proxyheader;
+
+ /*
+ * Assert the size of the structs that are part of the protocol,
+ * to defend against strange compilers.
+ */
+ StaticAssertStmt(sizeof(proxyheader) == 16, "proxy header struct has invalid size");
+ StaticAssertStmt(sizeof(proxyaddr.ip4) == 12, "proxy address ipv4 struct has invalid size");
+ StaticAssertStmt(sizeof(proxyaddr.ip6) == 36, "proxy address ipv6 struct has invalid size");
+
+
+ /* Else if it's on our list of trusted proxies */
+ if (TrustedProxyServers)
+ {
+ for (i = 0; i < *((int *) TrustedProxyServers) * 2; i += 2)
+ {
+ if (port->raddr.addr.ss_family == TrustedProxyServers[i + 1].ss_family)
+ {
+ /*
+ * Connection over unix sockets don't give us the source, so
+ * just check if they're allowed at all. For IP connections,
+ * verify that it's an allowed address.
+ */
+ if (port->raddr.addr.ss_family == AF_UNIX ||
+ pg_range_sockaddr(&port->raddr.addr,
+ &TrustedProxyServers[i + 1],
+ &TrustedProxyServers[i + 2]))
+ {
+ useproxy = true;
+ break;
+ }
+ }
+ }
+ }
+ if (!useproxy)
+ {
+ /*
+ * Connection is not from one of our trusted proxies, so reject it.
+ */
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("connection from unauthorized proxy server")));
+ return STATUS_ERROR;
+ }
+
+ /* Store a copy of the original address, for logging */
+ memcpy(&raddr_save, &port->raddr, sizeof(SockAddr));
+
+ pq_startmsgread();
+
+ /*
+ * PROXY requests always start with:
+ * \x0D \x0A \x0D \x0A \x00 \x0D \x0A \x51 \x55 \x49 \x54 \x0A
+ */
+
+ if (pq_getbytes((char *) &proxyheader, sizeof(proxyheader)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ if (memcmp(proxyheader.sig, "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a", sizeof(proxyheader.sig)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Proxy version is in the high 4 bits of the first byte */
+ proxyver = (proxyheader.ver_cmd & 0xF0) >> 4;
+ if (proxyver != 2)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol version: %x", proxyver)));
+ return STATUS_ERROR;
+ }
+
+ /*
+ * Proxy command is in the low 4 bits of the first byte.
+ * 0x00 = local, 0x01 = proxy, all others should be rejected
+ */
+ if ((proxyheader.ver_cmd & 0x0F) == 0x00)
+ {
+ if (proxyheader.fam != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol family %x for local connection", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+ }
+ else if ((proxyheader.ver_cmd & 0x0F) == 0x01)
+ {
+ if (proxyheader.fam == 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol family 0 for non-local connection")));
+ return STATUS_ERROR;
+ }
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol command: %x", (proxyheader.ver_cmd & 0x0f))));
+ return STATUS_ERROR;
+ }
+
+ proxyaddrlen = pg_ntoh16(proxyheader.len);
+
+ if (pq_getbytes((char *) &proxyaddr, proxyaddrlen > sizeof(proxyaddr) ? sizeof(proxyaddr) : proxyaddrlen) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Connection family */
+ if (proxyheader.fam == 0)
+ {
+ /*
+ * UNSPEC connection over LOCAL (verified above).
+ * in this case we just ignore the address included.
+ */
+ }
+ else if (proxyheader.fam == 0x11)
+ {
+ /* TCPv4 */
+ if (proxyaddrlen < 12)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET;
+ port->raddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
+ }
+ else if (proxyheader.fam == 0x21)
+ {
+ /* TCPv6 */
+ if (proxyaddrlen < 36)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET6;
+ port->raddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->raddr.addr)->sin6_addr, proxyaddr.ip6.src_addr, 16);
+ ((struct sockaddr_in6 *) &port->raddr.addr)->sin6_port = proxyaddr.ip6.src_port;
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol connection type: %x", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+
+ /* If there is any more header data present, skip past it */
+ if (proxyaddrlen > sizeof(proxyaddr))
+ {
+ if (pq_discardbytes(proxyaddrlen - sizeof(proxyaddr)) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ }
+
+ pq_endmsgread();
+
+ /*
+ * Log what we've done if connection logging is enabled. We log the proxy
+ * connection here, and let the normal connection logging mechanism log
+ * the unwrapped connection.
+ */
+ if (Log_connections)
+ {
+ char remote_host[NI_MAXHOST];
+ char remote_port[NI_MAXSERV];
+ int ret;
+
+ remote_host[0] = '\0';
+ remote_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&raddr_save.addr, raddr_save.salen,
+ remote_host, sizeof(remote_host),
+ remote_port, sizeof(remote_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ ereport(LOG,
+ (errmsg("proxy connection from: host=%s port=%s",
+ remote_host,
+ remote_port)));
+
+ }
+
+ return STATUS_OK;
+}
/*
* Read a client's startup packet and do something according to it.
@@ -2030,7 +2329,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
#ifdef USE_SSL
/* No SSL when disabled or on Unix sockets */
- if (!LoadedSSL || IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!LoadedSSL || (IS_AF_UNIX(port->laddr.addr.ss_family) && !port->isProxy))
SSLok = 'N';
else
SSLok = 'S'; /* Support for SSL */
@@ -2067,7 +2366,7 @@ retry1:
#ifdef ENABLE_GSS
/* No GSSAPI encryption when on Unix socket */
- if (!IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!IS_AF_UNIX(port->laddr.addr.ss_family) || port->isProxy)
GSSok = 'G';
#endif
@@ -2579,10 +2878,10 @@ ClosePostmasterPorts(bool am_syslogger)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -4363,6 +4662,33 @@ BackendInitialize(Port *port)
InitializeTimeouts(); /* establishes SIGALRM handler */
PG_SETMASK(&StartupBlockSig);
+ /*
+ * Ready to begin client interaction. We will give up and _exit(1) after
+ * a time delay, so that a broken client can't hog a connection
+ * indefinitely. PreAuthDelay and any DNS interactions above don't count
+ * against the time limit.
+ *
+ * Note: AuthenticationTimeout is applied here while waiting for the
+ * startup packet, and then again in InitPostgres for the duration of any
+ * authentication operations. So a hostile client could tie up the
+ * process for nearly twice AuthenticationTimeout before we kick him off.
+ *
+ * Note: because PostgresMain will call InitializeTimeouts again, the
+ * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
+ * since we never use it again after this function.
+ */
+ RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
+
+ /* Check if this is a proxy connection and if so unwrap the proxying */
+ if (port->isProxy)
+ {
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
+ if (UnwrapProxyConnection(port) != STATUS_OK)
+ proc_exit(0);
+ disable_timeout(STARTUP_PACKET_TIMEOUT, false);
+ }
+
+
/*
* Get the remote host name and port for logging and status display.
*/
@@ -4414,28 +4740,11 @@ BackendInitialize(Port *port)
strspn(remote_host, "0123456789ABCDEFabcdef:") < strlen(remote_host))
port->remote_hostname = strdup(remote_host);
- /*
- * Ready to begin client interaction. We will give up and _exit(1) after
- * a time delay, so that a broken client can't hog a connection
- * indefinitely. PreAuthDelay and any DNS interactions above don't count
- * against the time limit.
- *
- * Note: AuthenticationTimeout is applied here while waiting for the
- * startup packet, and then again in InitPostgres for the duration of any
- * authentication operations. So a hostile client could tie up the
- * process for nearly twice AuthenticationTimeout before we kick him off.
- *
- * Note: because PostgresMain will call InitializeTimeouts again, the
- * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
- * since we never use it again after this function.
- */
- RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
- enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
-
/*
* Receive the startup packet (which might turn out to be a cancel request
* packet).
*/
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
status = ProcessStartupPacket(port, false, false);
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 480e8cd199..2eac7e7264 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -50,10 +50,12 @@
#include "commands/user.h"
#include "commands/vacuum.h"
#include "commands/variable.h"
+#include "common/ip.h"
#include "common/string.h"
#include "funcapi.h"
#include "jit/jit.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
@@ -234,6 +236,8 @@ static bool check_recovery_target_lsn(char **newval, void **extra, GucSource sou
static void assign_recovery_target_lsn(const char *newval, void *extra);
static bool check_primary_slot_name(char **newval, void **extra, GucSource source);
static bool check_default_with_oids(bool *newval, void **extra, GucSource source);
+static bool check_proxy_servers(char **newval, void **extra, GucSource source);
+static void assign_proxy_servers(const char *newval, void *extra);
/* Private functions in guc-file.l that need to be called from guc.c */
static ConfigVariable *ProcessConfigFileInternal(GucContext context,
@@ -2358,6 +2362,16 @@ static struct config_int ConfigureNamesInt[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_port", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the TCP port the server listens for PROXY connections on."),
+ NULL
+ },
+ &ProxyPortNumber,
+ 0, 0, 65535,
+ NULL, NULL, NULL
+ },
+
{
{"unix_socket_permissions", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
gettext_noop("Sets the access permissions of the Unix-domain socket."),
@@ -4331,6 +4345,17 @@ static struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_servers", PGC_SIGHUP, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the addresses for trusted proxy servers."),
+ NULL,
+ GUC_LIST_INPUT
+ },
+ &TrustedProxyServersString,
+ "",
+ check_proxy_servers, assign_proxy_servers, NULL
+ },
+
{
/*
* Can't be set by ALTER SYSTEM as it can lead to recursive definition
@@ -12536,4 +12561,118 @@ check_default_with_oids(bool *newval, void **extra, GucSource source)
return true;
}
+static bool
+check_proxy_servers(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ struct sockaddr_storage *myextra;
+
+ /* Special case when it's empty */
+ if (**newval == '\0')
+ {
+ *extra = NULL;
+ return true;
+ }
+
+ /* Need a modifiable copy of string */
+ rawstring = pstrdup(*newval);
+
+ /* Parse string into list of identifiers */
+ if (!SplitIdentifierString(rawstring, ',', &elemlist))
+ {
+ /* syntax error in list */
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ if (list_length(elemlist) == 0)
+ {
+ /* If it had only whitespace */
+ pfree(rawstring);
+ list_free(elemlist);
+
+ *extra = NULL;
+ return true;
+ }
+
+ /*
+ * We store the result in an array of sockaddr_storage. The first entry is
+ * just an overloaded int which holds the size of the array.
+ */
+ myextra = (struct sockaddr_storage *) guc_malloc(ERROR, sizeof(struct sockaddr_storage) * (list_length(elemlist) * 2 + 1));
+ *((int *) &myextra[0]) = list_length(elemlist);
+
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+ char *netmasktok = NULL;
+ int ret;
+ struct addrinfo *gai_result;
+ struct addrinfo hints;
+
+ /*
+ * Unix sockets don't have endpoint addresses, so just flag them as
+ * AF_UNIX
+ */
+ if (pg_strcasecmp(tok, "unix") == 0)
+ {
+ myextra[foreach_current_index(l) * 2 + 1].ss_family = AF_UNIX;
+ continue;
+ }
+
+ netmasktok = strchr(tok, '/');
+ if (netmasktok)
+ {
+ *netmasktok = '\0';
+ netmasktok++;
+ }
+
+ memset((char *) &hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ ret = pg_getaddrinfo_all(tok, NULL, &hints, &gai_result);
+ if (ret != 0 || gai_result == NULL)
+ {
+ GUC_check_errdetail("Invalid IP addrress %s", tok);
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+
+ memcpy((char *) &myextra[foreach_current_index(l) * 2 + 1], gai_result->ai_addr, gai_result->ai_addrlen);
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+
+ /* A NULL netmasktok means the fully set hostmask */
+ if (pg_sockaddr_cidr_mask(&myextra[foreach_current_index(l) * 2 + 2], netmasktok, myextra[foreach_current_index(l) * 2 + 1].ss_family) != 0)
+ {
+ if (netmasktok)
+ GUC_check_errdetail("Invalid netmask %s", netmasktok);
+ else
+ GUC_check_errdetail("Could not create netmask");
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+ }
+
+ pfree(rawstring);
+ list_free(elemlist);
+ *extra = (void *) myextra;
+
+ return true;
+}
+
+static void
+assign_proxy_servers(const char *newval, void *extra)
+{
+ TrustedProxyServers = (struct sockaddr_storage *) extra;
+}
+
#include "guc-file.c"
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b696abfe54..2e224fef36 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -61,6 +61,9 @@
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
+#proxy_port = 0 # port to listen to for proxy connections
+ # (change requires restart)
+#proxy_servers = '' # what proxy servers to trust
#max_connections = 100 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
#unix_socket_directories = '/tmp' # comma-separated list of directories
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 02015efe13..471c76fb30 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -126,6 +126,7 @@ typedef struct Port
{
pgsocket sock; /* File descriptor */
bool noblock; /* is the socket in non-blocking mode? */
+ bool isProxy; /* is the connection using PROXY protocol */
ProtocolVersion proto; /* FE/BE protocol version */
SockAddr laddr; /* local addr (postmaster) */
SockAddr raddr; /* remote addr (client) */
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 6c51b2f20f..cdaae030e1 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -42,6 +42,12 @@ typedef struct
extern const PGDLLIMPORT PQcommMethods *PqCommMethods;
+typedef struct
+{
+ pgsocket socket;
+ bool isProxy;
+} PQlistenSocket;
+
#define pq_comm_reset() (PqCommMethods->comm_reset())
#define pq_flush() (PqCommMethods->flush())
#define pq_flush_if_writable() (PqCommMethods->flush_if_writable())
@@ -63,9 +69,9 @@ extern WaitEventSet *FeBeWaitSet;
#define FeBeWaitSetSocketPos 0
#define FeBeWaitSetLatchPos 1
-extern int StreamServerPort(int family, const char *hostName,
- unsigned short portNumber, const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen);
+extern PQlistenSocket *StreamServerPort(int family, const char *hostName,
+ unsigned short portNumber, const char *unixSocketDir,
+ PQlistenSocket PQlistenSocket[], int MaxListen);
extern int StreamConnection(pgsocket server_fd, Port *port);
extern void StreamClose(pgsocket sock);
extern void TouchSocketFiles(void);
@@ -78,6 +84,7 @@ extern bool pq_is_reading_msg(void);
extern int pq_getmessage(StringInfo s, int maxlen);
extern int pq_getbyte(void);
extern int pq_peekbyte(void);
+extern int pq_discardbytes(size_t len);
extern int pq_getbyte_if_available(unsigned char *c);
extern int pq_putmessage_v2(char msgtype, const char *s, size_t len);
extern bool pq_check_connection(void);
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index 0efdd7c232..2a029ef786 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -17,10 +17,13 @@
extern bool EnableSSL;
extern int ReservedBackends;
extern PGDLLIMPORT int PostPortNumber;
+extern PGDLLIMPORT int ProxyPortNumber;
extern int Unix_socket_permissions;
extern char *Unix_socket_group;
extern char *Unix_socket_directories;
extern char *ListenAddresses;
+extern char *TrustedProxyServersString;
+extern struct sockaddr_storage *TrustedProxyServers;
extern bool ClientAuthInProgress;
extern int PreAuthDelay;
extern int AuthenticationTimeout;
diff --git a/src/test/Makefile b/src/test/Makefile
index 46275915ff..4ad030034c 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -12,7 +12,8 @@ subdir = src/test
top_builddir = ../..
include $(top_builddir)/src/Makefile.global
-SUBDIRS = perl regress isolation modules authentication recovery subscription
+SUBDIRS = perl regress isolation modules authentication recovery subscription \
+ protocol
# Test suites that are not safe by default but can be run if selected
# by the user via the whitespace-separated list in variable
diff --git a/src/test/protocol/Makefile b/src/test/protocol/Makefile
new file mode 100644
index 0000000000..bda49d6ecb
--- /dev/null
+++ b/src/test/protocol/Makefile
@@ -0,0 +1,23 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/protocol
+#
+# Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/protocol/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/protocol
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
+
+clean distclean maintainer-clean:
+ rm -rf tmp_check
diff --git a/src/test/protocol/t/001_proxy.pl b/src/test/protocol/t/001_proxy.pl
new file mode 100644
index 0000000000..edc032d49c
--- /dev/null
+++ b/src/test/protocol/t/001_proxy.pl
@@ -0,0 +1,151 @@
+use strict;
+use warnings;
+use TestLib;
+use PostgresNode;
+use Test::More;
+use Socket qw(AF_INET AF_INET6 inet_pton);
+use IO::Socket;
+
+plan tests => 25;
+
+my $node = get_new_node('node');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+log_connections = on
+});
+$node->append_conf(
+ 'pg_hba.conf', qq{
+host all all 11.22.33.44/32 trust
+host all all 1:2:3:4:5:6:0:9/128 trust
+});
+$node->append_conf('postgresql.conf', "proxy_port = " . ($node->port() + 1));
+
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE USER proxytest;');
+
+sub make_message
+{
+ my ($msg) = @_;
+ return pack("Na*", length($msg) + 4, $msg);
+}
+
+sub read_packet
+{
+ my ($socket) = @_;
+ my $buf = "";
+ $socket->recv($buf, 1024);
+ return $buf;
+}
+
+
+# Test normal connection through localhost
+sub test_connection
+{
+ my ($socket, $proxy, $what, $shouldbe, $shouldfail, $extra) = @_;
+ ok($socket, $what);
+
+ my $startup = make_message(
+ pack("N(Z*Z*)*x", 196608, (user => "proxytest", database => "postgres")));
+
+ $extra = "" if !defined($extra);
+
+ if (defined($proxy))
+ {
+ my $p = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A\x21";
+ if ($proxy =~ ":")
+ {
+ # ipv6
+ $p .= "\x21"; # TCP v6
+ $p .= pack "n", 36 + length($extra); # size
+ $p .= inet_pton(AF_INET6, $proxy);
+ $p .= "\0" x 16; # destination address
+ }
+ else
+ {
+ # ipv4
+ $p .= "\x11"; # TCP v4
+ $p .= pack "n", 12 + length($extra); # size
+ $p .= inet_pton(AF_INET, $proxy);
+ $p .= "\0\0\0\0"; # destination address
+ }
+ $p .= pack "n", 1919; # source port
+ $p .= pack "n", 0;
+ $p .= $extra;
+ print $socket $p;
+ }
+ print $socket $startup;
+
+ my $in = read_packet($socket);
+ if (defined($shouldfail))
+ {
+ isnt(substr($in, 0, 1), 'R', $what);
+ }
+ else
+ {
+ is(substr($in, 0, 1), 'R', $what);
+ }
+
+ SKIP:
+ {
+ skip "The rest of this test should fail", 3 if (defined($shouldfail));
+
+ is(substr($in, 8, 1), "\0", $what);
+
+ my ($resip, $resport) = split /\|/,
+ $node->safe_psql('postgres',
+ "SELECT client_addr, client_port FROM pg_stat_activity WHERE pid != pg_backend_pid() AND backend_type='client backend'"
+ );
+ is($resip, $shouldbe, $what);
+ if ($proxy)
+ {
+ is($resport, "1919", $what);
+ }
+ else
+ {
+ ok($resport, $what);
+ }
+ }
+
+ $socket->close();
+
+ return;
+}
+
+sub make_socket
+{
+ my ($port) = @_;
+ if ($PostgresNode::use_tcp) {
+ return IO::Socket::INET->new(
+ PeerAddr => "127.0.0.1",
+ PeerPort => $port,
+ Proto => "tcp",
+ Type => SOCK_STREAM);
+ }
+ else {
+ return IO::Socket::UNIX->new(
+ Peer => $node->host() . "/.s.PGSQL." . $port,
+ Type => SOCK_STREAM);
+ }
+}
+
+# Test a regular connection first to make sure connecting etc works fine.
+test_connection(make_socket($node->port()),
+ undef, "normal connection", $PostgresNode::use_tcp ? "127.0.0.1": "");
+
+# Make sure we can't make a proxy connection until it's allowed
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44", 1);
+
+# Allow proxy connections and test them
+$node->append_conf('postgresql.conf', "proxy_servers = 'unix, 127.0.0.1/32'");
+$node->restart();
+
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44");
+test_connection(make_socket($node->port() + 1),
+ "1:2:3:4:5:6::9", "proxy ipv6", "1:2:3:4:5:6:0:9");
+
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy with extra", "11.22.33.44", undef, "abcdef"x100);
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-07-08 23:42 ` Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 1 reply; 56+ messages in thread
From: Jacob Champion @ 2021-07-08 23:42 UTC (permalink / raw)
To: magnus@hagander.net <magnus@hagander.net>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
Hi Magnus,
I'm only just starting to page this back into my head, so this is by no
means a full review of the v7 changes -- just stuff I've noticed over
the last day or so of poking around.
On Tue, 2021-06-29 at 11:48 +0200, Magnus Hagander wrote:
> On Thu, Mar 11, 2021 at 12:05 AM Jacob Champion <pchampion@vmware.com> wrote:
> > On Tue, 2021-03-09 at 11:25 +0100, Magnus Hagander wrote:
> > > - \x0 : LOCAL : [...] 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.
> >
> > So we should ignore the entire "protocol block" (by which I believe
> > they mean the protocol-and-address-family byte) in the case of LOCAL,
> > and just accept it with the original address info intact. That seems to
> > match the sample code in the back of the spec. The current behavior in
> > the patch will apply the PROXY behavior incorrectly if the sender sends
> > a LOCAL header with something other than UNSPEC -- which is strange
> > behavior but not explicitly prohibited as far as I can see.
>
> Yeah, I think we do the right thing in the "right usecase".
The current implementation is, I think, stricter than the spec asks
for. We're supposed to ignore the family for LOCAL cases, and it's not
clear to me whether we're supposed to also ignore the entire "fam"
family-and-protocol byte (the phrase "protocol block" is not actually
defined in the spec).
It's probably not a big deal in practice, but it could mess with
interoperability for lazier proxy implementations. I think I'll ask the
HAProxy folks for some clarification tomorrow.
> + <term><varname>proxy_servers</varname> (<type>string</type>)
> + <indexterm>
> + <primary><varname>proxy_servers</varname> configuration parameter</primary>
> + </indexterm>
> + </term>
> + <listitem>
> + <para>
> + A comma separated list of one or more host names, cidr specifications or the
> + literal <literal>unix</literal>, indicating which proxy servers to trust when
> + connecting on the port specified in <xref linkend="guc-proxy-port" />.
The documentation mentions that host names are valid in proxy_servers,
but check_proxy_servers() uses the AI_NUMERICHOST hint with
getaddrinfo(), so host names get rejected.
> + GUC_check_errdetail("Invalid IP addrress %s", tok);
s/addrress/address/
I've been thinking more about your earlier comment:
> An interesting thing is what to do about
> inet_server_addr/inet_server_port. That sort of loops back up to the
> original question of where/how to expose the information about the
> proxy in general (since right now it just logs). Right now you can
> actually use inet_server_port() to see if the connection was proxied
> (as long as it was over tcp).
IMO these should return the "virtual" dst_addr/port, instead of
exposing the physical connection information to the client. That way,
if you intend for your proxy to be transparent, you're not advertising
your network internals to connected clients. It also means that clients
can reasonably expect to be able to reconnect to the addr:port that we
give them, and prevents confusion if the proxy is using an address
family that the client doesn't even support (e.g. the client is IPv4-
only but the proxy connects via IPv6).
--Jacob
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
@ 2021-07-12 16:28 ` Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
0 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2021-07-12 16:28 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Fri, Jul 9, 2021 at 1:42 AM Jacob Champion <pchampion@vmware.com> wrote:
>
> Hi Magnus,
>
> I'm only just starting to page this back into my head, so this is by no
> means a full review of the v7 changes -- just stuff I've noticed over
> the last day or so of poking around.
>
> On Tue, 2021-06-29 at 11:48 +0200, Magnus Hagander wrote:
> > On Thu, Mar 11, 2021 at 12:05 AM Jacob Champion <pchampion@vmware.com> wrote:
> > > On Tue, 2021-03-09 at 11:25 +0100, Magnus Hagander wrote:
> > > > - \x0 : LOCAL : [...] 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.
> > >
> > > So we should ignore the entire "protocol block" (by which I believe
> > > they mean the protocol-and-address-family byte) in the case of LOCAL,
> > > and just accept it with the original address info intact. That seems to
> > > match the sample code in the back of the spec. The current behavior in
> > > the patch will apply the PROXY behavior incorrectly if the sender sends
> > > a LOCAL header with something other than UNSPEC -- which is strange
> > > behavior but not explicitly prohibited as far as I can see.
> >
> > Yeah, I think we do the right thing in the "right usecase".
>
> The current implementation is, I think, stricter than the spec asks
> for. We're supposed to ignore the family for LOCAL cases, and it's not
> clear to me whether we're supposed to also ignore the entire "fam"
> family-and-protocol byte (the phrase "protocol block" is not actually
> defined in the spec).
>
> It's probably not a big deal in practice, but it could mess with
> interoperability for lazier proxy implementations. I think I'll ask the
> HAProxy folks for some clarification tomorrow.
Thanks!
Yeah, I have no problem being stricter than necessary, unless that
actually causes any interop problems. It's a lot worse to not be
strict enough..
> > + <term><varname>proxy_servers</varname> (<type>string</type>)
> > + <indexterm>
> > + <primary><varname>proxy_servers</varname> configuration parameter</primary>
> > + </indexterm>
> > + </term>
> > + <listitem>
> > + <para>
> > + A comma separated list of one or more host names, cidr specifications or the
> > + literal <literal>unix</literal>, indicating which proxy servers to trust when
> > + connecting on the port specified in <xref linkend="guc-proxy-port" />.
>
> The documentation mentions that host names are valid in proxy_servers,
> but check_proxy_servers() uses the AI_NUMERICHOST hint with
> getaddrinfo(), so host names get rejected.
Ah, good point. Should say "ip addresses".
>
> > + GUC_check_errdetail("Invalid IP addrress %s", tok);
>
> s/addrress/address/
Oops.
> I've been thinking more about your earlier comment:
>
> > An interesting thing is what to do about
> > inet_server_addr/inet_server_port. That sort of loops back up to the
> > original question of where/how to expose the information about the
> > proxy in general (since right now it just logs). Right now you can
> > actually use inet_server_port() to see if the connection was proxied
> > (as long as it was over tcp).
>
> IMO these should return the "virtual" dst_addr/port, instead of
> exposing the physical connection information to the client. That way,
> if you intend for your proxy to be transparent, you're not advertising
> your network internals to connected clients. It also means that clients
> can reasonably expect to be able to reconnect to the addr:port that we
> give them, and prevents confusion if the proxy is using an address
> family that the client doesn't even support (e.g. the client is IPv4-
> only but the proxy connects via IPv6).
That reasoning I think makes a lot of sense, especially with the
comment about being able to connect back to it.
The question at that point extends to, would we also add extra
functions to get the data on the proxy connection itself? Maybe add a
inet_proxy_addr()/inet_proxy_port()? Better names?
PFA a patch that fixes the above errors, and changes
inet_server_addr()/inet_server_port(). Does not yet add anything to
receive the actual local port in this case.
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
Attachments:
[text/x-patch] proxy_protocol_8.patch (41.6K, ../../CABUevEwHUmYBXDvOWjN1wcn4mfZ1h1rwvg2AWYS9kD4ycykYFw@mail.gmail.com/2-proxy_protocol_8.patch)
download | inline diff:
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 02f0489112..a3ff09b3ac 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -353,6 +353,15 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl
the client's host name instead of the IP address in the log.
</para>
+ <para>
+ If <xref linkend="guc-proxy-port"/> is enabled and the
+ connection is made through a proxy server using the PROXY
+ protocol, the actual IP address of the client will be used
+ for matching. If a connection is made through a proxy server
+ not using the PROXY protocol, the IP address of the
+ proxy server will be used.
+ </para>
+
<para>
These fields do not apply to <literal>local</literal> records.
</para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 381d8636ab..778a20a179 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -682,6 +682,56 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-proxy-port" xreflabel="proxy_port">
+ <term><varname>proxy_port</varname> (<type>integer</type>)
+ <indexterm>
+ <primary><varname>proxy_port</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ The TCP port the server listens on for PROXY connections, disabled by
+ default. If set to a number, <productname>PostgreSQL</productname>
+ will listen on this port on the same addresses as for regular
+ connections, but expect all connections to use the PROXY protocol to
+ identify the client. This parameter can only be set at server start.
+ </para>
+ <para>
+ If a proxy connection is done over this port, and the proxy is listed
+ in <xref linkend="guc-proxy-servers" />, the actual client address
+ will be considered as the address of the client, instead of listing
+ all connections as coming from the proxy server.
+ </para>
+ <para>
+ The <ulink url="http://www.haproxy.org/download/1.9/doc/proxy-protocol.txt">PROXY
+ protocol</ulink> is maintained by <productname>HAProxy</productname>,
+ and supported in many proxies and load
+ balancers. <productname>PostgreSQL</productname> supports version 2
+ of the protocol.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-proxy-servers" xreflabel="proxy_servers">
+ <term><varname>proxy_servers</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>proxy_servers</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ A comma separated list of one or more ip addresses, cidr specifications or the
+ literal <literal>unix</literal>, indicating which proxy servers to trust when
+ connecting on the port specified in <xref linkend="guc-proxy-port" />.
+ </para>
+ <para>
+ If a proxy connection is made from an IP address not covered by this
+ list, the connection will be rejected. By default no proxy is trusted
+ and all proxy connections will be rejected.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-max-connections" xreflabel="max_connections">
<term><varname>max_connections</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 6388385edc..3884412a8b 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -21872,7 +21872,12 @@ SELECT * FROM pg_ls_dir('.') WITH ORDINALITY AS t(ls,n);
connection,
or <literal>NULL</literal> if the current connection is via a
Unix-domain socket.
- </para></entry>
+ </para>
+ <para>
+ If the connection is a PROXY connection, this function returns the
+ IP address used to connect to the proxy server.
+ </para>
+ </entry>
</row>
<row>
@@ -21888,7 +21893,13 @@ SELECT * FROM pg_ls_dir('.') WITH ORDINALITY AS t(ls,n);
connection,
or <literal>NULL</literal> if the current connection is via a
Unix-domain socket.
- </para></entry>
+ </para>
+ <para>
+ If the connection is a PROXY connection, this function returns the
+ port used to connect to the proxy server.
+ </para>
+
+ </entry>
</row>
<row>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 8cc23ef7fb..65f8e69131 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -1696,6 +1696,14 @@ ident_inet(hbaPort *port)
*la = NULL,
hints;
+ if (port->isProxy)
+ {
+ ereport(LOG,
+ (errcode_for_socket_access(),
+ errmsg("Ident authentication cannot be used over PROXY connections")));
+ return STATUS_ERROR;
+ }
+
/*
* Might look a little weird to first convert it to text and then back to
* sockaddr, but it's protocol independent.
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 89a5f901aa..a8d6c5fa4c 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -311,13 +311,13 @@ socket_close(int code, Datum arg)
* Successfully opened sockets are added to the ListenSocket[] array (of
* length MaxListen), at the first position that isn't PGINVALID_SOCKET.
*
- * RETURNS: STATUS_OK or STATUS_ERROR
+ * RETURNS: The PQlistenSocket listening on, or NULL in case of error
*/
-int
+PQlistenSocket *
StreamServerPort(int family, const char *hostName, unsigned short portNumber,
const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen)
+ PQlistenSocket ListenSocket[], int MaxListen)
{
pgsocket fd;
int err;
@@ -362,10 +362,10 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("Unix-domain socket path \"%s\" is too long (maximum %d bytes)",
unixSocketPath,
(int) (UNIXSOCK_PATH_BUFLEN - 1))));
- return STATUS_ERROR;
+ return NULL;
}
if (Lock_AF_UNIX(unixSocketDir, unixSocketPath) != STATUS_OK)
- return STATUS_ERROR;
+ return NULL;
service = unixSocketPath;
}
else
@@ -388,7 +388,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
service, gai_strerror(ret))));
if (addrs)
pg_freeaddrinfo_all(hint.ai_family, addrs);
- return STATUS_ERROR;
+ return NULL;
}
for (addr = addrs; addr; addr = addr->ai_next)
@@ -405,7 +405,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
/* See if there is still room to add 1 more socket. */
for (; listen_index < MaxListen; listen_index++)
{
- if (ListenSocket[listen_index] == PGINVALID_SOCKET)
+ if (ListenSocket[listen_index].socket == PGINVALID_SOCKET)
break;
}
if (listen_index >= MaxListen)
@@ -584,16 +584,16 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("listening on %s address \"%s\", port %d",
familyDesc, addrDesc, (int) portNumber)));
- ListenSocket[listen_index] = fd;
+ ListenSocket[listen_index].socket = fd;
added++;
}
pg_freeaddrinfo_all(hint.ai_family, addrs);
if (!added)
- return STATUS_ERROR;
+ return NULL;
- return STATUS_OK;
+ return &ListenSocket[listen_index];
}
@@ -747,6 +747,9 @@ StreamConnection(pgsocket server_fd, Port *port)
return STATUS_ERROR;
}
+ /* copy over to daddr to make sure it's set for the non-proxy case */
+ memcpy(&port->daddr, &port->laddr, sizeof(port->laddr));
+
/* select NODELAY and KEEPALIVE options if it's a TCP connection */
if (!IS_AF_UNIX(port->laddr.addr.ss_family))
{
@@ -1118,7 +1121,7 @@ pq_getbytes(char *s, size_t len)
* returns 0 if OK, EOF if trouble
* --------------------------------
*/
-static int
+int
pq_discardbytes(size_t len)
{
size_t amount;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 5a050898fe..5991fa9f2b 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -102,6 +102,7 @@
#include "common/string.h"
#include "lib/ilist.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
@@ -196,15 +197,22 @@ BackgroundWorker *MyBgworkerEntry = NULL;
-/* The socket number we are listening for connections on */
+/* The TCP port number we are listening for connections on */
int PostPortNumber;
+/* The TCP port number we are listening for proxy connections on */
+int ProxyPortNumber;
+
/* The directory names for Unix socket(s) */
char *Unix_socket_directories;
/* The TCP listen address(es) */
char *ListenAddresses;
+/* Trusted proxy servers */
+char *TrustedProxyServersString = NULL;
+struct sockaddr_storage *TrustedProxyServers = NULL;
+
/*
* ReservedBackends is the number of backends reserved for superuser use.
* This number is taken out of the pool size given by MaxConnections so
@@ -218,7 +226,7 @@ int ReservedBackends;
/* The socket(s) we're listening to. */
#define MAXLISTEN 64
-static pgsocket ListenSocket[MAXLISTEN];
+static PQlistenSocket ListenSocket[MAXLISTEN];
/*
* These globals control the behavior of the postmaster in case some
@@ -586,6 +594,7 @@ PostmasterMain(int argc, char *argv[])
bool listen_addr_saved = false;
int i;
char *output_config_variable = NULL;
+ PQlistenSocket *socket = NULL;
InitProcessGlobals();
@@ -1135,7 +1144,10 @@ PostmasterMain(int argc, char *argv[])
* charged with closing the sockets again at postmaster shutdown.
*/
for (i = 0; i < MAXLISTEN; i++)
- ListenSocket[i] = PGINVALID_SOCKET;
+ {
+ ListenSocket[i].socket = PGINVALID_SOCKET;
+ ListenSocket[i].isProxy = false;
+ }
on_proc_exit(CloseServerPorts, 0);
@@ -1164,17 +1176,17 @@ PostmasterMain(int argc, char *argv[])
char *curhost = (char *) lfirst(l);
if (strcmp(curhost, "*") == 0)
- status = StreamServerPort(AF_UNSPEC, NULL,
+ socket = StreamServerPort(AF_UNSPEC, NULL,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
else
- status = StreamServerPort(AF_UNSPEC, curhost,
+ socket = StreamServerPort(AF_UNSPEC, curhost,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful host addr in lockfile */
@@ -1188,9 +1200,30 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create listen socket for \"%s\"",
curhost)));
+
+ /* Also listen to the PROXY port on this address, if configured */
+ if (ProxyPortNumber)
+ {
+ if (strcmp(curhost, "*") == 0)
+ socket = StreamServerPort(AF_UNSPEC, NULL,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ else
+ socket = StreamServerPort(AF_UNSPEC, curhost,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ socket->isProxy = true;
+ else
+ ereport(WARNING,
+ (errmsg("could not create PROXY listen socket for \"%s\"",
+ curhost)));
+ }
}
- if (!success && elemlist != NIL)
+ if (socket == NULL && elemlist != NIL)
ereport(FATAL,
(errmsg("could not create any TCP/IP sockets")));
@@ -1200,7 +1233,7 @@ PostmasterMain(int argc, char *argv[])
#ifdef USE_BONJOUR
/* Register for Bonjour only if we opened TCP socket(s) */
- if (enable_bonjour && ListenSocket[0] != PGINVALID_SOCKET)
+ if (enable_bonjour && ListenSocket[0].socket != PGINVALID_SOCKET)
{
DNSServiceErrorType err;
@@ -1262,12 +1295,12 @@ PostmasterMain(int argc, char *argv[])
{
char *socketdir = (char *) lfirst(l);
- status = StreamServerPort(AF_UNIX, NULL,
+ socket = StreamServerPort(AF_UNIX, NULL,
(unsigned short) PostPortNumber,
socketdir,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful Unix socket in lockfile */
@@ -1278,9 +1311,23 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create Unix-domain socket in directory \"%s\"",
socketdir)));
+
+ if (ProxyPortNumber)
+ {
+ socket = StreamServerPort(AF_UNIX, NULL,
+ (unsigned short) ProxyPortNumber,
+ socketdir,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ socket->isProxy = true;
+ else
+ ereport(WARNING,
+ (errmsg("could not create Unix-domain PROXY socket for \"%s\"",
+ socketdir)));
+ }
}
- if (!success && elemlist != NIL)
+ if (socket == NULL && elemlist != NIL)
ereport(FATAL,
(errmsg("could not create any Unix-domain sockets")));
@@ -1292,7 +1339,7 @@ PostmasterMain(int argc, char *argv[])
/*
* check that we have some socket to listen on
*/
- if (ListenSocket[0] == PGINVALID_SOCKET)
+ if (ListenSocket[0].socket == PGINVALID_SOCKET)
ereport(FATAL,
(errmsg("no socket created for listening")));
@@ -1441,10 +1488,10 @@ CloseServerPorts(int status, Datum arg)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -1733,15 +1780,17 @@ ServerLoop(void)
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] == PGINVALID_SOCKET)
+ if (ListenSocket[i].socket == PGINVALID_SOCKET)
break;
- if (FD_ISSET(ListenSocket[i], &rmask))
+ if (FD_ISSET(ListenSocket[i].socket, &rmask))
{
Port *port;
- port = ConnCreate(ListenSocket[i]);
+ port = ConnCreate(ListenSocket[i].socket);
if (port)
{
+ port->isProxy = ListenSocket[i].isProxy;
+
BackendStartup(port);
/*
@@ -1909,7 +1958,7 @@ initMasks(fd_set *rmask)
for (i = 0; i < MAXLISTEN; i++)
{
- int fd = ListenSocket[i];
+ int fd = ListenSocket[i].socket;
if (fd == PGINVALID_SOCKET)
break;
@@ -1922,6 +1971,284 @@ initMasks(fd_set *rmask)
return maxsock + 1;
}
+static int
+UnwrapProxyConnection(Port *port)
+{
+ char proxyver;
+ uint16 proxyaddrlen;
+ SockAddr raddr_save;
+ SockAddr laddr_save;
+ int i;
+ bool useproxy = false;
+
+ /*
+ * These structs are from the PROXY protocol docs at
+ * http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
+ */
+ union
+ {
+ struct
+ { /* for TCP/UDP over IPv4, len = 12 */
+ uint32 src_addr;
+ uint32 dst_addr;
+ uint16 src_port;
+ uint16 dst_port;
+ } ip4;
+ struct
+ { /* for TCP/UDP over IPv6, len = 36 */
+ uint8 src_addr[16];
+ uint8 dst_addr[16];
+ uint16 src_port;
+ uint16 dst_port;
+ } ip6;
+ } proxyaddr;
+ struct
+ {
+ uint8 sig[12]; /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */
+ uint8 ver_cmd; /* protocol version and command */
+ uint8 fam; /* protocol family and address */
+ uint16 len; /* number of following bytes part of the
+ * header */
+ } proxyheader;
+
+ /*
+ * Assert the size of the structs that are part of the protocol,
+ * to defend against strange compilers.
+ */
+ StaticAssertStmt(sizeof(proxyheader) == 16, "proxy header struct has invalid size");
+ StaticAssertStmt(sizeof(proxyaddr.ip4) == 12, "proxy address ipv4 struct has invalid size");
+ StaticAssertStmt(sizeof(proxyaddr.ip6) == 36, "proxy address ipv6 struct has invalid size");
+
+
+ /* Else if it's on our list of trusted proxies */
+ if (TrustedProxyServers)
+ {
+ for (i = 0; i < *((int *) TrustedProxyServers) * 2; i += 2)
+ {
+ if (port->raddr.addr.ss_family == TrustedProxyServers[i + 1].ss_family)
+ {
+ /*
+ * Connection over unix sockets don't give us the source, so
+ * just check if they're allowed at all. For IP connections,
+ * verify that it's an allowed address.
+ */
+ if (port->raddr.addr.ss_family == AF_UNIX ||
+ pg_range_sockaddr(&port->raddr.addr,
+ &TrustedProxyServers[i + 1],
+ &TrustedProxyServers[i + 2]))
+ {
+ useproxy = true;
+ break;
+ }
+ }
+ }
+ }
+ if (!useproxy)
+ {
+ /*
+ * Connection is not from one of our trusted proxies, so reject it.
+ */
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("connection from unauthorized proxy server")));
+ return STATUS_ERROR;
+ }
+
+ /* Store a copy of the original address, for logging */
+ memcpy(&raddr_save, &port->raddr, sizeof(SockAddr));
+ memcpy(&laddr_save, &port->laddr, sizeof(SockAddr));
+
+ pq_startmsgread();
+
+ /*
+ * PROXY requests always start with:
+ * \x0D \x0A \x0D \x0A \x00 \x0D \x0A \x51 \x55 \x49 \x54 \x0A
+ */
+
+ if (pq_getbytes((char *) &proxyheader, sizeof(proxyheader)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ if (memcmp(proxyheader.sig, "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a", sizeof(proxyheader.sig)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Proxy version is in the high 4 bits of the first byte */
+ proxyver = (proxyheader.ver_cmd & 0xF0) >> 4;
+ if (proxyver != 2)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol version: %x", proxyver)));
+ return STATUS_ERROR;
+ }
+
+ /*
+ * Proxy command is in the low 4 bits of the first byte.
+ * 0x00 = local, 0x01 = proxy, all others should be rejected
+ */
+ if ((proxyheader.ver_cmd & 0x0F) == 0x00)
+ {
+ if (proxyheader.fam != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol family %x for local connection", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+ }
+ else if ((proxyheader.ver_cmd & 0x0F) == 0x01)
+ {
+ if (proxyheader.fam == 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol family 0 for non-local connection")));
+ return STATUS_ERROR;
+ }
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol command: %x", (proxyheader.ver_cmd & 0x0f))));
+ return STATUS_ERROR;
+ }
+
+ proxyaddrlen = pg_ntoh16(proxyheader.len);
+
+ if (pq_getbytes((char *) &proxyaddr, proxyaddrlen > sizeof(proxyaddr) ? sizeof(proxyaddr) : proxyaddrlen) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Connection family */
+ if (proxyheader.fam == 0)
+ {
+ /*
+ * UNSPEC connection over LOCAL (verified above).
+ * in this case we just ignore the address included.
+ */
+ }
+ else if (proxyheader.fam == 0x11)
+ {
+ /* TCPv4 */
+ if (proxyaddrlen < 12)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET;
+ port->raddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
+
+ port->daddr.addr.ss_family = AF_INET;
+ port->daddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->daddr.addr)->sin_addr.s_addr = proxyaddr.ip4.dst_addr;
+ ((struct sockaddr_in *) &port->daddr.addr)->sin_port = proxyaddr.ip4.dst_port;
+ }
+ else if (proxyheader.fam == 0x21)
+ {
+ /* TCPv6 */
+ if (proxyaddrlen < 36)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET6;
+ port->raddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->raddr.addr)->sin6_addr, proxyaddr.ip6.src_addr, 16);
+ ((struct sockaddr_in6 *) &port->raddr.addr)->sin6_port = proxyaddr.ip6.src_port;
+
+
+ port->daddr.addr.ss_family = AF_INET6;
+ port->daddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->daddr.addr)->sin6_addr, proxyaddr.ip6.dst_addr, 16);
+ ((struct sockaddr_in6 *) &port->daddr.addr)->sin6_port = proxyaddr.ip6.dst_port;
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol connection type: %x", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+
+ /* If there is any more header data present, skip past it */
+ if (proxyaddrlen > sizeof(proxyaddr))
+ {
+ if (pq_discardbytes(proxyaddrlen - sizeof(proxyaddr)) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ }
+
+ pq_endmsgread();
+
+ /*
+ * Log what we've done if connection logging is enabled. We log the proxy
+ * connection here, and let the normal connection logging mechanism log
+ * the unwrapped connection.
+ */
+ if (Log_connections)
+ {
+ char remote_host[NI_MAXHOST];
+ char remote_port[NI_MAXSERV];
+ char proxy_host[NI_MAXHOST];
+ char proxy_port[NI_MAXSERV];
+ int ret;
+
+ remote_host[0] = '\0';
+ remote_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&raddr_save.addr, raddr_save.salen,
+ remote_host, sizeof(remote_host),
+ remote_port, sizeof(remote_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ proxy_host[0] = '\0';
+ proxy_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&laddr_save.addr, laddr_save.salen,
+ proxy_host, sizeof(proxy_host),
+ proxy_port, sizeof(proxy_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+
+ ereport(LOG,
+ (errmsg("proxy connection from: host=%s port=%s (proxy host=%s port=%s)",
+ remote_host,
+ remote_port,
+ proxy_host,
+ proxy_port)));
+
+ }
+
+ return STATUS_OK;
+}
/*
* Read a client's startup packet and do something according to it.
@@ -2030,7 +2357,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
#ifdef USE_SSL
/* No SSL when disabled or on Unix sockets */
- if (!LoadedSSL || IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!LoadedSSL || (IS_AF_UNIX(port->laddr.addr.ss_family) && !port->isProxy))
SSLok = 'N';
else
SSLok = 'S'; /* Support for SSL */
@@ -2067,7 +2394,7 @@ retry1:
#ifdef ENABLE_GSS
/* No GSSAPI encryption when on Unix socket */
- if (!IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!IS_AF_UNIX(port->laddr.addr.ss_family) || port->isProxy)
GSSok = 'G';
#endif
@@ -2579,10 +2906,10 @@ ClosePostmasterPorts(bool am_syslogger)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -4363,6 +4690,33 @@ BackendInitialize(Port *port)
InitializeTimeouts(); /* establishes SIGALRM handler */
PG_SETMASK(&StartupBlockSig);
+ /*
+ * Ready to begin client interaction. We will give up and _exit(1) after
+ * a time delay, so that a broken client can't hog a connection
+ * indefinitely. PreAuthDelay and any DNS interactions above don't count
+ * against the time limit.
+ *
+ * Note: AuthenticationTimeout is applied here while waiting for the
+ * startup packet, and then again in InitPostgres for the duration of any
+ * authentication operations. So a hostile client could tie up the
+ * process for nearly twice AuthenticationTimeout before we kick him off.
+ *
+ * Note: because PostgresMain will call InitializeTimeouts again, the
+ * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
+ * since we never use it again after this function.
+ */
+ RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
+
+ /* Check if this is a proxy connection and if so unwrap the proxying */
+ if (port->isProxy)
+ {
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
+ if (UnwrapProxyConnection(port) != STATUS_OK)
+ proc_exit(0);
+ disable_timeout(STARTUP_PACKET_TIMEOUT, false);
+ }
+
+
/*
* Get the remote host name and port for logging and status display.
*/
@@ -4414,28 +4768,11 @@ BackendInitialize(Port *port)
strspn(remote_host, "0123456789ABCDEFabcdef:") < strlen(remote_host))
port->remote_hostname = strdup(remote_host);
- /*
- * Ready to begin client interaction. We will give up and _exit(1) after
- * a time delay, so that a broken client can't hog a connection
- * indefinitely. PreAuthDelay and any DNS interactions above don't count
- * against the time limit.
- *
- * Note: AuthenticationTimeout is applied here while waiting for the
- * startup packet, and then again in InitPostgres for the duration of any
- * authentication operations. So a hostile client could tie up the
- * process for nearly twice AuthenticationTimeout before we kick him off.
- *
- * Note: because PostgresMain will call InitializeTimeouts again, the
- * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
- * since we never use it again after this function.
- */
- RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
- enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
-
/*
* Receive the startup packet (which might turn out to be a cancel request
* packet).
*/
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
status = ProcessStartupPacket(port, false, false);
/*
diff --git a/src/backend/utils/adt/network.c b/src/backend/utils/adt/network.c
index 0ab54316f8..9198a29f51 100644
--- a/src/backend/utils/adt/network.c
+++ b/src/backend/utils/adt/network.c
@@ -1802,6 +1802,8 @@ inet_client_port(PG_FUNCTION_ARGS)
/*
* IP address that the server accepted the connection on (NULL if Unix socket)
+ * If the connection is a PROXY connection, then this returns the IP address/port of
+ * the proxy server, and not the local connection!
*/
Datum
inet_server_addr(PG_FUNCTION_ARGS)
@@ -1813,7 +1815,7 @@ inet_server_addr(PG_FUNCTION_ARGS)
if (port == NULL)
PG_RETURN_NULL();
- switch (port->laddr.addr.ss_family)
+ switch (port->daddr.addr.ss_family)
{
case AF_INET:
#ifdef HAVE_IPV6
@@ -1826,14 +1828,14 @@ inet_server_addr(PG_FUNCTION_ARGS)
local_host[0] = '\0';
- ret = pg_getnameinfo_all(&port->laddr.addr, port->laddr.salen,
+ ret = pg_getnameinfo_all(&port->daddr.addr, port->daddr.salen,
local_host, sizeof(local_host),
NULL, 0,
NI_NUMERICHOST | NI_NUMERICSERV);
if (ret != 0)
PG_RETURN_NULL();
- clean_ipv6_addr(port->laddr.addr.ss_family, local_host);
+ clean_ipv6_addr(port->daddr.addr.ss_family, local_host);
PG_RETURN_INET_P(network_in(local_host, false));
}
@@ -1841,6 +1843,8 @@ inet_server_addr(PG_FUNCTION_ARGS)
/*
* port that the server accepted the connection on (NULL if Unix socket)
+ * If the connection is a PROXY connection, then this returns the IP address/port of
+ * the proxy server, and not the local connection!
*/
Datum
inet_server_port(PG_FUNCTION_ARGS)
@@ -1852,7 +1856,7 @@ inet_server_port(PG_FUNCTION_ARGS)
if (port == NULL)
PG_RETURN_NULL();
- switch (port->laddr.addr.ss_family)
+ switch (port->daddr.addr.ss_family)
{
case AF_INET:
#ifdef HAVE_IPV6
@@ -1865,7 +1869,7 @@ inet_server_port(PG_FUNCTION_ARGS)
local_port[0] = '\0';
- ret = pg_getnameinfo_all(&port->laddr.addr, port->laddr.salen,
+ ret = pg_getnameinfo_all(&port->daddr.addr, port->daddr.salen,
NULL, 0,
local_port, sizeof(local_port),
NI_NUMERICHOST | NI_NUMERICSERV);
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 480e8cd199..e2e5af2fd8 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -50,10 +50,12 @@
#include "commands/user.h"
#include "commands/vacuum.h"
#include "commands/variable.h"
+#include "common/ip.h"
#include "common/string.h"
#include "funcapi.h"
#include "jit/jit.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
@@ -234,6 +236,8 @@ static bool check_recovery_target_lsn(char **newval, void **extra, GucSource sou
static void assign_recovery_target_lsn(const char *newval, void *extra);
static bool check_primary_slot_name(char **newval, void **extra, GucSource source);
static bool check_default_with_oids(bool *newval, void **extra, GucSource source);
+static bool check_proxy_servers(char **newval, void **extra, GucSource source);
+static void assign_proxy_servers(const char *newval, void *extra);
/* Private functions in guc-file.l that need to be called from guc.c */
static ConfigVariable *ProcessConfigFileInternal(GucContext context,
@@ -2358,6 +2362,16 @@ static struct config_int ConfigureNamesInt[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_port", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the TCP port the server listens for PROXY connections on."),
+ NULL
+ },
+ &ProxyPortNumber,
+ 0, 0, 65535,
+ NULL, NULL, NULL
+ },
+
{
{"unix_socket_permissions", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
gettext_noop("Sets the access permissions of the Unix-domain socket."),
@@ -4331,6 +4345,17 @@ static struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_servers", PGC_SIGHUP, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the addresses for trusted proxy servers."),
+ NULL,
+ GUC_LIST_INPUT
+ },
+ &TrustedProxyServersString,
+ "",
+ check_proxy_servers, assign_proxy_servers, NULL
+ },
+
{
/*
* Can't be set by ALTER SYSTEM as it can lead to recursive definition
@@ -12536,4 +12561,118 @@ check_default_with_oids(bool *newval, void **extra, GucSource source)
return true;
}
+static bool
+check_proxy_servers(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ struct sockaddr_storage *myextra;
+
+ /* Special case when it's empty */
+ if (**newval == '\0')
+ {
+ *extra = NULL;
+ return true;
+ }
+
+ /* Need a modifiable copy of string */
+ rawstring = pstrdup(*newval);
+
+ /* Parse string into list of identifiers */
+ if (!SplitIdentifierString(rawstring, ',', &elemlist))
+ {
+ /* syntax error in list */
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ if (list_length(elemlist) == 0)
+ {
+ /* If it had only whitespace */
+ pfree(rawstring);
+ list_free(elemlist);
+
+ *extra = NULL;
+ return true;
+ }
+
+ /*
+ * We store the result in an array of sockaddr_storage. The first entry is
+ * just an overloaded int which holds the size of the array.
+ */
+ myextra = (struct sockaddr_storage *) guc_malloc(ERROR, sizeof(struct sockaddr_storage) * (list_length(elemlist) * 2 + 1));
+ *((int *) &myextra[0]) = list_length(elemlist);
+
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+ char *netmasktok = NULL;
+ int ret;
+ struct addrinfo *gai_result;
+ struct addrinfo hints;
+
+ /*
+ * Unix sockets don't have endpoint addresses, so just flag them as
+ * AF_UNIX
+ */
+ if (pg_strcasecmp(tok, "unix") == 0)
+ {
+ myextra[foreach_current_index(l) * 2 + 1].ss_family = AF_UNIX;
+ continue;
+ }
+
+ netmasktok = strchr(tok, '/');
+ if (netmasktok)
+ {
+ *netmasktok = '\0';
+ netmasktok++;
+ }
+
+ memset((char *) &hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ ret = pg_getaddrinfo_all(tok, NULL, &hints, &gai_result);
+ if (ret != 0 || gai_result == NULL)
+ {
+ GUC_check_errdetail("Invalid IP address %s", tok);
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+
+ memcpy((char *) &myextra[foreach_current_index(l) * 2 + 1], gai_result->ai_addr, gai_result->ai_addrlen);
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+
+ /* A NULL netmasktok means the fully set hostmask */
+ if (pg_sockaddr_cidr_mask(&myextra[foreach_current_index(l) * 2 + 2], netmasktok, myextra[foreach_current_index(l) * 2 + 1].ss_family) != 0)
+ {
+ if (netmasktok)
+ GUC_check_errdetail("Invalid netmask %s", netmasktok);
+ else
+ GUC_check_errdetail("Could not create netmask");
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+ }
+
+ pfree(rawstring);
+ list_free(elemlist);
+ *extra = (void *) myextra;
+
+ return true;
+}
+
+static void
+assign_proxy_servers(const char *newval, void *extra)
+{
+ TrustedProxyServers = (struct sockaddr_storage *) extra;
+}
+
#include "guc-file.c"
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b696abfe54..2e224fef36 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -61,6 +61,9 @@
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
+#proxy_port = 0 # port to listen to for proxy connections
+ # (change requires restart)
+#proxy_servers = '' # what proxy servers to trust
#max_connections = 100 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
#unix_socket_directories = '/tmp' # comma-separated list of directories
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 02015efe13..e91a4d7607 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -126,9 +126,11 @@ typedef struct Port
{
pgsocket sock; /* File descriptor */
bool noblock; /* is the socket in non-blocking mode? */
+ bool isProxy; /* is the connection using PROXY protocol */
ProtocolVersion proto; /* FE/BE protocol version */
SockAddr laddr; /* local addr (postmaster) */
SockAddr raddr; /* remote addr (client) */
+ SockAddr daddr; /* destination addr (postmaster, or proxy server if proxy protocol used) */
char *remote_host; /* name (or ip addr) of remote host */
char *remote_hostname; /* name (not ip addr) of remote host, if
* available */
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 6c51b2f20f..cdaae030e1 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -42,6 +42,12 @@ typedef struct
extern const PGDLLIMPORT PQcommMethods *PqCommMethods;
+typedef struct
+{
+ pgsocket socket;
+ bool isProxy;
+} PQlistenSocket;
+
#define pq_comm_reset() (PqCommMethods->comm_reset())
#define pq_flush() (PqCommMethods->flush())
#define pq_flush_if_writable() (PqCommMethods->flush_if_writable())
@@ -63,9 +69,9 @@ extern WaitEventSet *FeBeWaitSet;
#define FeBeWaitSetSocketPos 0
#define FeBeWaitSetLatchPos 1
-extern int StreamServerPort(int family, const char *hostName,
- unsigned short portNumber, const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen);
+extern PQlistenSocket *StreamServerPort(int family, const char *hostName,
+ unsigned short portNumber, const char *unixSocketDir,
+ PQlistenSocket PQlistenSocket[], int MaxListen);
extern int StreamConnection(pgsocket server_fd, Port *port);
extern void StreamClose(pgsocket sock);
extern void TouchSocketFiles(void);
@@ -78,6 +84,7 @@ extern bool pq_is_reading_msg(void);
extern int pq_getmessage(StringInfo s, int maxlen);
extern int pq_getbyte(void);
extern int pq_peekbyte(void);
+extern int pq_discardbytes(size_t len);
extern int pq_getbyte_if_available(unsigned char *c);
extern int pq_putmessage_v2(char msgtype, const char *s, size_t len);
extern bool pq_check_connection(void);
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index 0efdd7c232..2a029ef786 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -17,10 +17,13 @@
extern bool EnableSSL;
extern int ReservedBackends;
extern PGDLLIMPORT int PostPortNumber;
+extern PGDLLIMPORT int ProxyPortNumber;
extern int Unix_socket_permissions;
extern char *Unix_socket_group;
extern char *Unix_socket_directories;
extern char *ListenAddresses;
+extern char *TrustedProxyServersString;
+extern struct sockaddr_storage *TrustedProxyServers;
extern bool ClientAuthInProgress;
extern int PreAuthDelay;
extern int AuthenticationTimeout;
diff --git a/src/test/Makefile b/src/test/Makefile
index 46275915ff..4ad030034c 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -12,7 +12,8 @@ subdir = src/test
top_builddir = ../..
include $(top_builddir)/src/Makefile.global
-SUBDIRS = perl regress isolation modules authentication recovery subscription
+SUBDIRS = perl regress isolation modules authentication recovery subscription \
+ protocol
# Test suites that are not safe by default but can be run if selected
# by the user via the whitespace-separated list in variable
diff --git a/src/test/protocol/Makefile b/src/test/protocol/Makefile
new file mode 100644
index 0000000000..bda49d6ecb
--- /dev/null
+++ b/src/test/protocol/Makefile
@@ -0,0 +1,23 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/protocol
+#
+# Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/protocol/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/protocol
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
+
+clean distclean maintainer-clean:
+ rm -rf tmp_check
diff --git a/src/test/protocol/t/001_proxy.pl b/src/test/protocol/t/001_proxy.pl
new file mode 100644
index 0000000000..edc032d49c
--- /dev/null
+++ b/src/test/protocol/t/001_proxy.pl
@@ -0,0 +1,151 @@
+use strict;
+use warnings;
+use TestLib;
+use PostgresNode;
+use Test::More;
+use Socket qw(AF_INET AF_INET6 inet_pton);
+use IO::Socket;
+
+plan tests => 25;
+
+my $node = get_new_node('node');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+log_connections = on
+});
+$node->append_conf(
+ 'pg_hba.conf', qq{
+host all all 11.22.33.44/32 trust
+host all all 1:2:3:4:5:6:0:9/128 trust
+});
+$node->append_conf('postgresql.conf', "proxy_port = " . ($node->port() + 1));
+
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE USER proxytest;');
+
+sub make_message
+{
+ my ($msg) = @_;
+ return pack("Na*", length($msg) + 4, $msg);
+}
+
+sub read_packet
+{
+ my ($socket) = @_;
+ my $buf = "";
+ $socket->recv($buf, 1024);
+ return $buf;
+}
+
+
+# Test normal connection through localhost
+sub test_connection
+{
+ my ($socket, $proxy, $what, $shouldbe, $shouldfail, $extra) = @_;
+ ok($socket, $what);
+
+ my $startup = make_message(
+ pack("N(Z*Z*)*x", 196608, (user => "proxytest", database => "postgres")));
+
+ $extra = "" if !defined($extra);
+
+ if (defined($proxy))
+ {
+ my $p = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A\x21";
+ if ($proxy =~ ":")
+ {
+ # ipv6
+ $p .= "\x21"; # TCP v6
+ $p .= pack "n", 36 + length($extra); # size
+ $p .= inet_pton(AF_INET6, $proxy);
+ $p .= "\0" x 16; # destination address
+ }
+ else
+ {
+ # ipv4
+ $p .= "\x11"; # TCP v4
+ $p .= pack "n", 12 + length($extra); # size
+ $p .= inet_pton(AF_INET, $proxy);
+ $p .= "\0\0\0\0"; # destination address
+ }
+ $p .= pack "n", 1919; # source port
+ $p .= pack "n", 0;
+ $p .= $extra;
+ print $socket $p;
+ }
+ print $socket $startup;
+
+ my $in = read_packet($socket);
+ if (defined($shouldfail))
+ {
+ isnt(substr($in, 0, 1), 'R', $what);
+ }
+ else
+ {
+ is(substr($in, 0, 1), 'R', $what);
+ }
+
+ SKIP:
+ {
+ skip "The rest of this test should fail", 3 if (defined($shouldfail));
+
+ is(substr($in, 8, 1), "\0", $what);
+
+ my ($resip, $resport) = split /\|/,
+ $node->safe_psql('postgres',
+ "SELECT client_addr, client_port FROM pg_stat_activity WHERE pid != pg_backend_pid() AND backend_type='client backend'"
+ );
+ is($resip, $shouldbe, $what);
+ if ($proxy)
+ {
+ is($resport, "1919", $what);
+ }
+ else
+ {
+ ok($resport, $what);
+ }
+ }
+
+ $socket->close();
+
+ return;
+}
+
+sub make_socket
+{
+ my ($port) = @_;
+ if ($PostgresNode::use_tcp) {
+ return IO::Socket::INET->new(
+ PeerAddr => "127.0.0.1",
+ PeerPort => $port,
+ Proto => "tcp",
+ Type => SOCK_STREAM);
+ }
+ else {
+ return IO::Socket::UNIX->new(
+ Peer => $node->host() . "/.s.PGSQL." . $port,
+ Type => SOCK_STREAM);
+ }
+}
+
+# Test a regular connection first to make sure connecting etc works fine.
+test_connection(make_socket($node->port()),
+ undef, "normal connection", $PostgresNode::use_tcp ? "127.0.0.1": "");
+
+# Make sure we can't make a proxy connection until it's allowed
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44", 1);
+
+# Allow proxy connections and test them
+$node->append_conf('postgresql.conf', "proxy_servers = 'unix, 127.0.0.1/32'");
+$node->restart();
+
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44");
+test_connection(make_socket($node->port() + 1),
+ "1:2:3:4:5:6::9", "proxy ipv6", "1:2:3:4:5:6:0:9");
+
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy with extra", "11.22.33.44", undef, "abcdef"x100);
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-07-14 18:23 ` Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 1 reply; 56+ messages in thread
From: Jacob Champion @ 2021-07-14 18:23 UTC (permalink / raw)
To: magnus@hagander.net <magnus@hagander.net>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Mon, 2021-07-12 at 18:28 +0200, Magnus Hagander wrote:
> Yeah, I have no problem being stricter than necessary, unless that
> actually causes any interop problems. It's a lot worse to not be
> strict enough..
Agreed. Haven't heard back from the HAProxy mailing list yet, so
staying strict seems reasonable in the meantime. That could always be
rolled back later.
> > I've been thinking more about your earlier comment:
> >
> > > An interesting thing is what to do about
> > > inet_server_addr/inet_server_port. That sort of loops back up to the
> > > original question of where/how to expose the information about the
> > > proxy in general (since right now it just logs). Right now you can
> > > actually use inet_server_port() to see if the connection was proxied
> > > (as long as it was over tcp).
> >
> > IMO these should return the "virtual" dst_addr/port, instead of
> > exposing the physical connection information to the client. That way,
> > if you intend for your proxy to be transparent, you're not advertising
> > your network internals to connected clients. It also means that clients
> > can reasonably expect to be able to reconnect to the addr:port that we
> > give them, and prevents confusion if the proxy is using an address
> > family that the client doesn't even support (e.g. the client is IPv4-
> > only but the proxy connects via IPv6).
>
> That reasoning I think makes a lot of sense, especially with the
> comment about being able to connect back to it.
>
> The question at that point extends to, would we also add extra
> functions to get the data on the proxy connection itself? Maybe add a
> inet_proxy_addr()/inet_proxy_port()? Better names?
What's the intended use case? I have trouble viewing those as anything
but information disclosure vectors, but I'm jaded. :)
If the goal is to give a last-ditch debugging tool to someone whose
proxy isn't behaving properly -- though I'd hope the proxy in question
has its own ways to debug its behavior -- maybe they could be locked
behind one of the pg_monitor roles, so that they're only available to
someone who could get that information anyway?
> PFA a patch that fixes the above errors, and changes
> inet_server_addr()/inet_server_port(). Does not yet add anything to
> receive the actual local port in this case.
Looking good in local testing. I'm going to reread the spec with fresh
eyes and do a full review pass, but this is shaping up nicely IMO.
Something that I haven't thought about very hard yet is proxy
authentication, but I think the simple IP authentication will be enough
for a first version. For the Unix socket case, it looks like anyone
currently relying on peer auth will need to switch to a
unix_socket_group/_permissions model. For now, that sounds like a
reasonable v1 restriction, though I think not being able to set the
proxy socket's permissions separately from the "normal" one might lead
to some complications in more advanced setups.
--Jacob
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
@ 2021-09-07 10:24 ` Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
0 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2021-09-07 10:24 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Wed, Jul 14, 2021 at 8:24 PM Jacob Champion <pchampion@vmware.com> wrote:
>
> On Mon, 2021-07-12 at 18:28 +0200, Magnus Hagander wrote:
> > Yeah, I have no problem being stricter than necessary, unless that
> > actually causes any interop problems. It's a lot worse to not be
> > strict enough..
>
> Agreed. Haven't heard back from the HAProxy mailing list yet, so
> staying strict seems reasonable in the meantime. That could always be
> rolled back later.
Any further feedback from them now, two months later? :)
(Sorry, I was out on vacation for the end of the last CF, so didn't
get around to this one, but it seemed there'd be plenty of time in
this CF)
> > > I've been thinking more about your earlier comment:
> > >
> > > > An interesting thing is what to do about
> > > > inet_server_addr/inet_server_port. That sort of loops back up to the
> > > > original question of where/how to expose the information about the
> > > > proxy in general (since right now it just logs). Right now you can
> > > > actually use inet_server_port() to see if the connection was proxied
> > > > (as long as it was over tcp).
> > >
> > > IMO these should return the "virtual" dst_addr/port, instead of
> > > exposing the physical connection information to the client. That way,
> > > if you intend for your proxy to be transparent, you're not advertising
> > > your network internals to connected clients. It also means that clients
> > > can reasonably expect to be able to reconnect to the addr:port that we
> > > give them, and prevents confusion if the proxy is using an address
> > > family that the client doesn't even support (e.g. the client is IPv4-
> > > only but the proxy connects via IPv6).
> >
> > That reasoning I think makes a lot of sense, especially with the
> > comment about being able to connect back to it.
> >
> > The question at that point extends to, would we also add extra
> > functions to get the data on the proxy connection itself? Maybe add a
> > inet_proxy_addr()/inet_proxy_port()? Better names?
>
> What's the intended use case? I have trouble viewing those as anything
> but information disclosure vectors, but I'm jaded. :)
"Covering all the bases"?
I'm not entirely sure what the point is of the *existing* functions
for that though, so I'm definitely not wedded to including it.
> If the goal is to give a last-ditch debugging tool to someone whose
> proxy isn't behaving properly -- though I'd hope the proxy in question
> has its own ways to debug its behavior -- maybe they could be locked
> behind one of the pg_monitor roles, so that they're only available to
> someone who could get that information anyway?
Yeah, agreed.
> > PFA a patch that fixes the above errors, and changes
> > inet_server_addr()/inet_server_port(). Does not yet add anything to
> > receive the actual local port in this case.
>
> Looking good in local testing. I'm going to reread the spec with fresh
> eyes and do a full review pass, but this is shaping up nicely IMO.
Thanks!
> Something that I haven't thought about very hard yet is proxy
> authentication, but I think the simple IP authentication will be enough
> for a first version. For the Unix socket case, it looks like anyone
> currently relying on peer auth will need to switch to a
> unix_socket_group/_permissions model. For now, that sounds like a
> reasonable v1 restriction, though I think not being able to set the
> proxy socket's permissions separately from the "normal" one might lead
> to some complications in more advanced setups.
Agreed in principle, but I think those are some quite uncommon
usecases, so definitely something we don't need to cover in a first
feature.
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-09-08 18:51 ` Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
0 siblings, 1 reply; 56+ messages in thread
From: Jacob Champion @ 2021-09-08 18:51 UTC (permalink / raw)
To: magnus@hagander.net <magnus@hagander.net>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Tue, 2021-09-07 at 12:24 +0200, Magnus Hagander wrote:
> On Wed, Jul 14, 2021 at 8:24 PM Jacob Champion <pchampion@vmware.com> wrote:
> > On Mon, 2021-07-12 at 18:28 +0200, Magnus Hagander wrote:
> > > Yeah, I have no problem being stricter than necessary, unless that
> > > actually causes any interop problems. It's a lot worse to not be
> > > strict enough..
> >
> > Agreed. Haven't heard back from the HAProxy mailing list yet, so
> > staying strict seems reasonable in the meantime. That could always be
> > rolled back later.
>
> Any further feedback from them now, two months later? :)
Not yet :( I've bumped the thread; in the meantime I still think the
stricter operation is fine, since in the worst case you just make it
less strict in the future.
> (Sorry, I was out on vacation for the end of the last CF, so didn't
> get around to this one, but it seemed there'd be plenty of time in
> this CF)
No worries!
> > > The question at that point extends to, would we also add extra
> > > functions to get the data on the proxy connection itself? Maybe add a
> > > inet_proxy_addr()/inet_proxy_port()? Better names?
> >
> > What's the intended use case? I have trouble viewing those as anything
> > but information disclosure vectors, but I'm jaded. :)
>
> "Covering all the bases"?
>
> I'm not entirely sure what the point is of the *existing* functions
> for that though, so I'm definitely not wedded to including it.
I guess I'm in the same boat. I'm probably not the right person to
weigh in.
> > Looking good in local testing. I'm going to reread the spec with fresh
> > eyes and do a full review pass, but this is shaping up nicely IMO.
>
> Thanks!
I still owe you that overall review. Hoping to get to it this week.
> > Something that I haven't thought about very hard yet is proxy
> > authentication, but I think the simple IP authentication will be enough
> > for a first version. For the Unix socket case, it looks like anyone
> > currently relying on peer auth will need to switch to a
> > unix_socket_group/_permissions model. For now, that sounds like a
> > reasonable v1 restriction, though I think not being able to set the
> > proxy socket's permissions separately from the "normal" one might lead
> > to some complications in more advanced setups.
>
> Agreed in principle, but I think those are some quite uncommon
> usecases, so definitely something we don't need to cover in a first
> feature.
Hm. I guess I'm overly optimistic that "properly securing your
database" is not such an uncommon case, but... :)
--Jacob
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
@ 2021-09-09 23:44 ` Jacob Champion <pchampion@vmware.com>
2021-09-28 13:23 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 1 reply; 56+ messages in thread
From: Jacob Champion @ 2021-09-09 23:44 UTC (permalink / raw)
To: magnus@hagander.net <magnus@hagander.net>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Wed, 2021-09-08 at 18:51 +0000, Jacob Champion wrote:
> I still owe you that overall review. Hoping to get to it this week.
And here it is. I focused on things other than UnwrapProxyConnection()
for this round, since I think that piece is looking solid.
> + if (port->isProxy)
> + {
> + ereport(LOG,
> + (errcode_for_socket_access(),
> + errmsg("Ident authentication cannot be used over PROXY connections")));
What are the rules on COMMERROR vs LOG when dealing with authentication
code? I always thought COMMERROR was required, but I see now that LOG
(among others) is suppressed to the client during authentication.
> #ifdef USE_SSL
> /* No SSL when disabled or on Unix sockets */
> - if (!LoadedSSL || IS_AF_UNIX(port->laddr.addr.ss_family))
> + if (!LoadedSSL || (IS_AF_UNIX(port->laddr.addr.ss_family) && !port->isProxy))
> SSLok = 'N';
> else
> SSLok = 'S'; /* Support for SSL */
> @@ -2087,7 +2414,7 @@ retry1:
>
> #ifdef ENABLE_GSS
> /* No GSSAPI encryption when on Unix socket */
> - if (!IS_AF_UNIX(port->laddr.addr.ss_family))
> + if (!IS_AF_UNIX(port->laddr.addr.ss_family) || port->isProxy)
> GSSok = 'G';
Now that we have port->daddr, could these checks be simplified to just
IS_AF_UNIX(port->daddr...)? Or is there a corner case I'm missing for
the port->isProxy case?
> + * Note: AuthenticationTimeout is applied here while waiting for the
> + * startup packet, and then again in InitPostgres for the duration of any
> + * authentication operations. So a hostile client could tie up the
> + * process for nearly twice AuthenticationTimeout before we kick him off.
This comment needs to be adjusted after the move; waiting for the
startup packet comes later, and it looks like we can now tie up 3x the
timeout for the proxy case.
> + /* Check if this is a proxy connection and if so unwrap the proxying */
> + if (port->isProxy)
> + {
> + enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
> + if (UnwrapProxyConnection(port) != STATUS_OK)
> + proc_exit(0);
I think the timeout here could comfortably be substantially less than
the overall authentication timeout, since the proxy should send its
header immediately even if the client takes its time with the startup
packet. The spec suggests allowing 3 seconds minimum to cover a
retransmission. Maybe something to tune in the future?
> + /* Also listen to the PROXY port on this address, if configured */
> + if (ProxyPortNumber)
> + {
> + if (strcmp(curhost, "*") == 0)
> + socket = StreamServerPort(AF_UNSPEC, NULL,
> + (unsigned short) ProxyPortNumber,
> + NULL,
> + ListenSocket, MAXLISTEN);
Sorry if you already talked about this upthread somewhere, but it looks
like another downside of treating "proxy mode" as a server-wide on/off
switch is that it cuts the effective MAXLISTEN in half, from 64 to 32,
since we're opening two sockets for every address. If I've understood
that correctly, it might be worth mentioning in the docs.
> - if (!success && elemlist != NIL)
> + if (socket == NULL && elemlist != NIL)
> ereport(FATAL,
> (errmsg("could not create any TCP/IP sockets")));
With this change in PostmasterMain, it looks like `success` is no
longer a useful variable. But I'm not convinced that this is the
correct logic -- this is just checking to see if the last socket
creation succeeded, as opposed to seeing if any of them succeeded. Is
that what you intended?
> +plan tests => 25;
> +
> +my $node = get_new_node('node');
The TAP test will need to be rebased over the changes in 201a76183e.
--Jacob
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
@ 2021-09-28 13:23 ` Magnus Hagander <magnus@hagander.net>
2021-11-03 13:36 ` Re: PROXY protocol support Daniel Gustafsson <daniel@yesql.se>
0 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2021-09-28 13:23 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Fri, Sep 10, 2021 at 1:44 AM Jacob Champion <pchampion@vmware.com> wrote:
>
> On Wed, 2021-09-08 at 18:51 +0000, Jacob Champion wrote:
> > I still owe you that overall review. Hoping to get to it this week.
>
> And here it is. I focused on things other than UnwrapProxyConnection()
> for this round, since I think that piece is looking solid.
Thanks!
> > + if (port->isProxy)
> > + {
> > + ereport(LOG,
> > + (errcode_for_socket_access(),
> > + errmsg("Ident authentication cannot be used over PROXY connections")));
>
> What are the rules on COMMERROR vs LOG when dealing with authentication
> code? I always thought COMMERROR was required, but I see now that LOG
> (among others) is suppressed to the client during authentication.
I honestly don't know :) In this case, LOG is what's used for all the
other message in errors in ident_inet(), so I picked it for
consistency.
> > #ifdef USE_SSL
> > /* No SSL when disabled or on Unix sockets */
> > - if (!LoadedSSL || IS_AF_UNIX(port->laddr.addr.ss_family))
> > + if (!LoadedSSL || (IS_AF_UNIX(port->laddr.addr.ss_family) && !port->isProxy))
> > SSLok = 'N';
> > else
> > SSLok = 'S'; /* Support for SSL */
> > @@ -2087,7 +2414,7 @@ retry1:
> >
> > #ifdef ENABLE_GSS
> > /* No GSSAPI encryption when on Unix socket */
> > - if (!IS_AF_UNIX(port->laddr.addr.ss_family))
> > + if (!IS_AF_UNIX(port->laddr.addr.ss_family) || port->isProxy)
> > GSSok = 'G';
>
> Now that we have port->daddr, could these checks be simplified to just
> IS_AF_UNIX(port->daddr...)? Or is there a corner case I'm missing for
> the port->isProxy case?
Yeah, I think they could.
> > + * Note: AuthenticationTimeout is applied here while waiting for the
> > + * startup packet, and then again in InitPostgres for the duration of any
> > + * authentication operations. So a hostile client could tie up the
> > + * process for nearly twice AuthenticationTimeout before we kick him off.
>
> This comment needs to be adjusted after the move; waiting for the
> startup packet comes later, and it looks like we can now tie up 3x the
> timeout for the proxy case.
Good point.
> > + /* Check if this is a proxy connection and if so unwrap the proxying */
> > + if (port->isProxy)
> > + {
> > + enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
> > + if (UnwrapProxyConnection(port) != STATUS_OK)
> > + proc_exit(0);
>
> I think the timeout here could comfortably be substantially less than
> the overall authentication timeout, since the proxy should send its
> header immediately even if the client takes its time with the startup
> packet. The spec suggests allowing 3 seconds minimum to cover a
> retransmission. Maybe something to tune in the future?
Maybe. I'll leave it with a new comment for now about us diong it, and
that we may want to consider igt in the future.
>
> > + /* Also listen to the PROXY port on this address, if configured */
> > + if (ProxyPortNumber)
> > + {
> > + if (strcmp(curhost, "*") == 0)
> > + socket = StreamServerPort(AF_UNSPEC, NULL,
> > + (unsigned short) ProxyPortNumber,
> > + NULL,
> > + ListenSocket, MAXLISTEN);
>
> Sorry if you already talked about this upthread somewhere, but it looks
> like another downside of treating "proxy mode" as a server-wide on/off
> switch is that it cuts the effective MAXLISTEN in half, from 64 to 32,
> since we're opening two sockets for every address. If I've understood
> that correctly, it might be worth mentioning in the docs.
Correct. I don't see a way to avoid that without complicating things
(as long as we want the ports to be separate), but I also don't see it
as something that's reality to be an issue in reality.
I would agree with documenting it, but I can't actually find us
documenting the MAXLISTEN value anywhere. Do we?
> > - if (!success && elemlist != NIL)
> > + if (socket == NULL && elemlist != NIL)
> > ereport(FATAL,
> > (errmsg("could not create any TCP/IP sockets")));
>
> With this change in PostmasterMain, it looks like `success` is no
> longer a useful variable. But I'm not convinced that this is the
> correct logic -- this is just checking to see if the last socket
> creation succeeded, as opposed to seeing if any of them succeeded. Is
> that what you intended?
Eh, no, that's clearly a code-moving-bug.
I think the reasonable thing is to succeed if we create either a
regular socket *or* a proxy one, but FATAL out if you configured
either of them but we failed co create any.
> > +plan tests => 25;
> > +
> > +my $node = get_new_node('node');
>
> The TAP test will need to be rebased over the changes in 201a76183e.
Done, and adjustments above according to your comments, along with a
small docs fix "a proxy connection is done" -> "a proxy connection is
made".
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
Attachments:
[text/x-patch] proxy_protocol_9.patch (41.4K, ../../CABUevExeD7fn_=4m2EZVix48OYg5Js+1NBRU=tcXNwkLRk=sZg@mail.gmail.com/2-proxy_protocol_9.patch)
download | inline diff:
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 02f0489112..a3ff09b3ac 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -353,6 +353,15 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl
the client's host name instead of the IP address in the log.
</para>
+ <para>
+ If <xref linkend="guc-proxy-port"/> is enabled and the
+ connection is made through a proxy server using the PROXY
+ protocol, the actual IP address of the client will be used
+ for matching. If a connection is made through a proxy server
+ not using the PROXY protocol, the IP address of the
+ proxy server will be used.
+ </para>
+
<para>
These fields do not apply to <literal>local</literal> records.
</para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0a8e35c59f..b1b3613fd1 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -682,6 +682,56 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-proxy-port" xreflabel="proxy_port">
+ <term><varname>proxy_port</varname> (<type>integer</type>)
+ <indexterm>
+ <primary><varname>proxy_port</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ The TCP port the server listens on for PROXY connections, disabled by
+ default. If set to a number, <productname>PostgreSQL</productname>
+ will listen on this port on the same addresses as for regular
+ connections, but expect all connections to use the PROXY protocol to
+ identify the client. This parameter can only be set at server start.
+ </para>
+ <para>
+ If a proxy connection is made over this port, and the proxy is listed
+ in <xref linkend="guc-proxy-servers" />, the actual client address
+ will be considered as the address of the client, instead of listing
+ all connections as coming from the proxy server.
+ </para>
+ <para>
+ The <ulink url="http://www.haproxy.org/download/1.9/doc/proxy-protocol.txt">PROXY
+ protocol</ulink> is maintained by <productname>HAProxy</productname>,
+ and supported in many proxies and load
+ balancers. <productname>PostgreSQL</productname> supports version 2
+ of the protocol.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-proxy-servers" xreflabel="proxy_servers">
+ <term><varname>proxy_servers</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>proxy_servers</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ A comma separated list of one or more ip addresses, cidr specifications or the
+ literal <literal>unix</literal>, indicating which proxy servers to trust when
+ connecting on the port specified in <xref linkend="guc-proxy-port" />.
+ </para>
+ <para>
+ If a proxy connection is made from an IP address not covered by this
+ list, the connection will be rejected. By default no proxy is trusted
+ and all proxy connections will be rejected.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-max-connections" xreflabel="max_connections">
<term><varname>max_connections</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 78812b2dbe..8af4d8b69c 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -22213,7 +22213,12 @@ SELECT * FROM pg_ls_dir('.') WITH ORDINALITY AS t(ls,n);
connection,
or <literal>NULL</literal> if the current connection is via a
Unix-domain socket.
- </para></entry>
+ </para>
+ <para>
+ If the connection is a PROXY connection, this function returns the
+ IP address used to connect to the proxy server.
+ </para>
+ </entry>
</row>
<row>
@@ -22229,7 +22234,13 @@ SELECT * FROM pg_ls_dir('.') WITH ORDINALITY AS t(ls,n);
connection,
or <literal>NULL</literal> if the current connection is via a
Unix-domain socket.
- </para></entry>
+ </para>
+ <para>
+ If the connection is a PROXY connection, this function returns the
+ port used to connect to the proxy server.
+ </para>
+
+ </entry>
</row>
<row>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index a317aef1c9..f8c32ad492 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -1696,6 +1696,14 @@ ident_inet(hbaPort *port)
*la = NULL,
hints;
+ if (port->isProxy)
+ {
+ ereport(LOG,
+ (errcode_for_socket_access(),
+ errmsg("Ident authentication cannot be used over PROXY connections")));
+ return STATUS_ERROR;
+ }
+
/*
* Might look a little weird to first convert it to text and then back to
* sockaddr, but it's protocol independent.
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 89a5f901aa..a8d6c5fa4c 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -311,13 +311,13 @@ socket_close(int code, Datum arg)
* Successfully opened sockets are added to the ListenSocket[] array (of
* length MaxListen), at the first position that isn't PGINVALID_SOCKET.
*
- * RETURNS: STATUS_OK or STATUS_ERROR
+ * RETURNS: The PQlistenSocket listening on, or NULL in case of error
*/
-int
+PQlistenSocket *
StreamServerPort(int family, const char *hostName, unsigned short portNumber,
const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen)
+ PQlistenSocket ListenSocket[], int MaxListen)
{
pgsocket fd;
int err;
@@ -362,10 +362,10 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("Unix-domain socket path \"%s\" is too long (maximum %d bytes)",
unixSocketPath,
(int) (UNIXSOCK_PATH_BUFLEN - 1))));
- return STATUS_ERROR;
+ return NULL;
}
if (Lock_AF_UNIX(unixSocketDir, unixSocketPath) != STATUS_OK)
- return STATUS_ERROR;
+ return NULL;
service = unixSocketPath;
}
else
@@ -388,7 +388,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
service, gai_strerror(ret))));
if (addrs)
pg_freeaddrinfo_all(hint.ai_family, addrs);
- return STATUS_ERROR;
+ return NULL;
}
for (addr = addrs; addr; addr = addr->ai_next)
@@ -405,7 +405,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
/* See if there is still room to add 1 more socket. */
for (; listen_index < MaxListen; listen_index++)
{
- if (ListenSocket[listen_index] == PGINVALID_SOCKET)
+ if (ListenSocket[listen_index].socket == PGINVALID_SOCKET)
break;
}
if (listen_index >= MaxListen)
@@ -584,16 +584,16 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("listening on %s address \"%s\", port %d",
familyDesc, addrDesc, (int) portNumber)));
- ListenSocket[listen_index] = fd;
+ ListenSocket[listen_index].socket = fd;
added++;
}
pg_freeaddrinfo_all(hint.ai_family, addrs);
if (!added)
- return STATUS_ERROR;
+ return NULL;
- return STATUS_OK;
+ return &ListenSocket[listen_index];
}
@@ -747,6 +747,9 @@ StreamConnection(pgsocket server_fd, Port *port)
return STATUS_ERROR;
}
+ /* copy over to daddr to make sure it's set for the non-proxy case */
+ memcpy(&port->daddr, &port->laddr, sizeof(port->laddr));
+
/* select NODELAY and KEEPALIVE options if it's a TCP connection */
if (!IS_AF_UNIX(port->laddr.addr.ss_family))
{
@@ -1118,7 +1121,7 @@ pq_getbytes(char *s, size_t len)
* returns 0 if OK, EOF if trouble
* --------------------------------
*/
-static int
+int
pq_discardbytes(size_t len)
{
size_t amount;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index e2a76ba055..797a317a04 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -101,6 +101,7 @@
#include "common/string.h"
#include "lib/ilist.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
@@ -196,15 +197,22 @@ BackgroundWorker *MyBgworkerEntry = NULL;
-/* The socket number we are listening for connections on */
+/* The TCP port number we are listening for connections on */
int PostPortNumber;
+/* The TCP port number we are listening for proxy connections on */
+int ProxyPortNumber;
+
/* The directory names for Unix socket(s) */
char *Unix_socket_directories;
/* The TCP listen address(es) */
char *ListenAddresses;
+/* Trusted proxy servers */
+char *TrustedProxyServersString = NULL;
+struct sockaddr_storage *TrustedProxyServers = NULL;
+
/*
* ReservedBackends is the number of backends reserved for superuser use.
* This number is taken out of the pool size given by MaxConnections so
@@ -218,7 +226,7 @@ int ReservedBackends;
/* The socket(s) we're listening to. */
#define MAXLISTEN 64
-static pgsocket ListenSocket[MAXLISTEN];
+static PQlistenSocket ListenSocket[MAXLISTEN];
/*
* These globals control the behavior of the postmaster in case some
@@ -586,6 +594,7 @@ PostmasterMain(int argc, char *argv[])
bool listen_addr_saved = false;
int i;
char *output_config_variable = NULL;
+ PQlistenSocket *socket = NULL;
InitProcessGlobals();
@@ -1185,7 +1194,10 @@ PostmasterMain(int argc, char *argv[])
* charged with closing the sockets again at postmaster shutdown.
*/
for (i = 0; i < MAXLISTEN; i++)
- ListenSocket[i] = PGINVALID_SOCKET;
+ {
+ ListenSocket[i].socket = PGINVALID_SOCKET;
+ ListenSocket[i].isProxy = false;
+ }
on_proc_exit(CloseServerPorts, 0);
@@ -1214,17 +1226,17 @@ PostmasterMain(int argc, char *argv[])
char *curhost = (char *) lfirst(l);
if (strcmp(curhost, "*") == 0)
- status = StreamServerPort(AF_UNSPEC, NULL,
+ socket = StreamServerPort(AF_UNSPEC, NULL,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
else
- status = StreamServerPort(AF_UNSPEC, curhost,
+ socket = StreamServerPort(AF_UNSPEC, curhost,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful host addr in lockfile */
@@ -1238,6 +1250,30 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create listen socket for \"%s\"",
curhost)));
+
+ /* Also listen to the PROXY port on this address, if configured */
+ if (ProxyPortNumber)
+ {
+ if (strcmp(curhost, "*") == 0)
+ socket = StreamServerPort(AF_UNSPEC, NULL,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ else
+ socket = StreamServerPort(AF_UNSPEC, curhost,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ {
+ success++;
+ socket->isProxy = true;
+ }
+ else
+ ereport(WARNING,
+ (errmsg("could not create PROXY listen socket for \"%s\"",
+ curhost)));
+ }
}
if (!success && elemlist != NIL)
@@ -1250,7 +1286,7 @@ PostmasterMain(int argc, char *argv[])
#ifdef USE_BONJOUR
/* Register for Bonjour only if we opened TCP socket(s) */
- if (enable_bonjour && ListenSocket[0] != PGINVALID_SOCKET)
+ if (enable_bonjour && ListenSocket[0].socket != PGINVALID_SOCKET)
{
DNSServiceErrorType err;
@@ -1312,12 +1348,12 @@ PostmasterMain(int argc, char *argv[])
{
char *socketdir = (char *) lfirst(l);
- status = StreamServerPort(AF_UNIX, NULL,
+ socket = StreamServerPort(AF_UNIX, NULL,
(unsigned short) PostPortNumber,
socketdir,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful Unix socket in lockfile */
@@ -1328,9 +1364,23 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create Unix-domain socket in directory \"%s\"",
socketdir)));
+
+ if (ProxyPortNumber)
+ {
+ socket = StreamServerPort(AF_UNIX, NULL,
+ (unsigned short) ProxyPortNumber,
+ socketdir,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ socket->isProxy = true;
+ else
+ ereport(WARNING,
+ (errmsg("could not create Unix-domain PROXY socket for \"%s\"",
+ socketdir)));
+ }
}
- if (!success && elemlist != NIL)
+ if (socket == NULL && elemlist != NIL)
ereport(FATAL,
(errmsg("could not create any Unix-domain sockets")));
@@ -1342,7 +1392,7 @@ PostmasterMain(int argc, char *argv[])
/*
* check that we have some socket to listen on
*/
- if (ListenSocket[0] == PGINVALID_SOCKET)
+ if (ListenSocket[0].socket == PGINVALID_SOCKET)
ereport(FATAL,
(errmsg("no socket created for listening")));
@@ -1497,10 +1547,10 @@ CloseServerPorts(int status, Datum arg)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -1789,15 +1839,17 @@ ServerLoop(void)
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] == PGINVALID_SOCKET)
+ if (ListenSocket[i].socket == PGINVALID_SOCKET)
break;
- if (FD_ISSET(ListenSocket[i], &rmask))
+ if (FD_ISSET(ListenSocket[i].socket, &rmask))
{
Port *port;
- port = ConnCreate(ListenSocket[i]);
+ port = ConnCreate(ListenSocket[i].socket);
if (port)
{
+ port->isProxy = ListenSocket[i].isProxy;
+
BackendStartup(port);
/*
@@ -1965,7 +2017,7 @@ initMasks(fd_set *rmask)
for (i = 0; i < MAXLISTEN; i++)
{
- int fd = ListenSocket[i];
+ int fd = ListenSocket[i].socket;
if (fd == PGINVALID_SOCKET)
break;
@@ -1978,6 +2030,284 @@ initMasks(fd_set *rmask)
return maxsock + 1;
}
+static int
+UnwrapProxyConnection(Port *port)
+{
+ char proxyver;
+ uint16 proxyaddrlen;
+ SockAddr raddr_save;
+ SockAddr laddr_save;
+ int i;
+ bool useproxy = false;
+
+ /*
+ * These structs are from the PROXY protocol docs at
+ * http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
+ */
+ union
+ {
+ struct
+ { /* for TCP/UDP over IPv4, len = 12 */
+ uint32 src_addr;
+ uint32 dst_addr;
+ uint16 src_port;
+ uint16 dst_port;
+ } ip4;
+ struct
+ { /* for TCP/UDP over IPv6, len = 36 */
+ uint8 src_addr[16];
+ uint8 dst_addr[16];
+ uint16 src_port;
+ uint16 dst_port;
+ } ip6;
+ } proxyaddr;
+ struct
+ {
+ uint8 sig[12]; /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */
+ uint8 ver_cmd; /* protocol version and command */
+ uint8 fam; /* protocol family and address */
+ uint16 len; /* number of following bytes part of the
+ * header */
+ } proxyheader;
+
+ /*
+ * Assert the size of the structs that are part of the protocol,
+ * to defend against strange compilers.
+ */
+ StaticAssertStmt(sizeof(proxyheader) == 16, "proxy header struct has invalid size");
+ StaticAssertStmt(sizeof(proxyaddr.ip4) == 12, "proxy address ipv4 struct has invalid size");
+ StaticAssertStmt(sizeof(proxyaddr.ip6) == 36, "proxy address ipv6 struct has invalid size");
+
+
+ /* Else if it's on our list of trusted proxies */
+ if (TrustedProxyServers)
+ {
+ for (i = 0; i < *((int *) TrustedProxyServers) * 2; i += 2)
+ {
+ if (port->raddr.addr.ss_family == TrustedProxyServers[i + 1].ss_family)
+ {
+ /*
+ * Connection over unix sockets don't give us the source, so
+ * just check if they're allowed at all. For IP connections,
+ * verify that it's an allowed address.
+ */
+ if (port->raddr.addr.ss_family == AF_UNIX ||
+ pg_range_sockaddr(&port->raddr.addr,
+ &TrustedProxyServers[i + 1],
+ &TrustedProxyServers[i + 2]))
+ {
+ useproxy = true;
+ break;
+ }
+ }
+ }
+ }
+ if (!useproxy)
+ {
+ /*
+ * Connection is not from one of our trusted proxies, so reject it.
+ */
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("connection from unauthorized proxy server")));
+ return STATUS_ERROR;
+ }
+
+ /* Store a copy of the original address, for logging */
+ memcpy(&raddr_save, &port->raddr, sizeof(SockAddr));
+ memcpy(&laddr_save, &port->laddr, sizeof(SockAddr));
+
+ pq_startmsgread();
+
+ /*
+ * PROXY requests always start with:
+ * \x0D \x0A \x0D \x0A \x00 \x0D \x0A \x51 \x55 \x49 \x54 \x0A
+ */
+
+ if (pq_getbytes((char *) &proxyheader, sizeof(proxyheader)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ if (memcmp(proxyheader.sig, "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a", sizeof(proxyheader.sig)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Proxy version is in the high 4 bits of the first byte */
+ proxyver = (proxyheader.ver_cmd & 0xF0) >> 4;
+ if (proxyver != 2)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol version: %x", proxyver)));
+ return STATUS_ERROR;
+ }
+
+ /*
+ * Proxy command is in the low 4 bits of the first byte.
+ * 0x00 = local, 0x01 = proxy, all others should be rejected
+ */
+ if ((proxyheader.ver_cmd & 0x0F) == 0x00)
+ {
+ if (proxyheader.fam != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol family %x for local connection", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+ }
+ else if ((proxyheader.ver_cmd & 0x0F) == 0x01)
+ {
+ if (proxyheader.fam == 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol family 0 for non-local connection")));
+ return STATUS_ERROR;
+ }
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol command: %x", (proxyheader.ver_cmd & 0x0f))));
+ return STATUS_ERROR;
+ }
+
+ proxyaddrlen = pg_ntoh16(proxyheader.len);
+
+ if (pq_getbytes((char *) &proxyaddr, proxyaddrlen > sizeof(proxyaddr) ? sizeof(proxyaddr) : proxyaddrlen) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Connection family */
+ if (proxyheader.fam == 0)
+ {
+ /*
+ * UNSPEC connection over LOCAL (verified above).
+ * in this case we just ignore the address included.
+ */
+ }
+ else if (proxyheader.fam == 0x11)
+ {
+ /* TCPv4 */
+ if (proxyaddrlen < 12)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET;
+ port->raddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
+
+ port->daddr.addr.ss_family = AF_INET;
+ port->daddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->daddr.addr)->sin_addr.s_addr = proxyaddr.ip4.dst_addr;
+ ((struct sockaddr_in *) &port->daddr.addr)->sin_port = proxyaddr.ip4.dst_port;
+ }
+ else if (proxyheader.fam == 0x21)
+ {
+ /* TCPv6 */
+ if (proxyaddrlen < 36)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET6;
+ port->raddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->raddr.addr)->sin6_addr, proxyaddr.ip6.src_addr, 16);
+ ((struct sockaddr_in6 *) &port->raddr.addr)->sin6_port = proxyaddr.ip6.src_port;
+
+
+ port->daddr.addr.ss_family = AF_INET6;
+ port->daddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->daddr.addr)->sin6_addr, proxyaddr.ip6.dst_addr, 16);
+ ((struct sockaddr_in6 *) &port->daddr.addr)->sin6_port = proxyaddr.ip6.dst_port;
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol connection type: %x", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+
+ /* If there is any more header data present, skip past it */
+ if (proxyaddrlen > sizeof(proxyaddr))
+ {
+ if (pq_discardbytes(proxyaddrlen - sizeof(proxyaddr)) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ }
+
+ pq_endmsgread();
+
+ /*
+ * Log what we've done if connection logging is enabled. We log the proxy
+ * connection here, and let the normal connection logging mechanism log
+ * the unwrapped connection.
+ */
+ if (Log_connections)
+ {
+ char remote_host[NI_MAXHOST];
+ char remote_port[NI_MAXSERV];
+ char proxy_host[NI_MAXHOST];
+ char proxy_port[NI_MAXSERV];
+ int ret;
+
+ remote_host[0] = '\0';
+ remote_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&raddr_save.addr, raddr_save.salen,
+ remote_host, sizeof(remote_host),
+ remote_port, sizeof(remote_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ proxy_host[0] = '\0';
+ proxy_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&laddr_save.addr, laddr_save.salen,
+ proxy_host, sizeof(proxy_host),
+ proxy_port, sizeof(proxy_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+
+ ereport(LOG,
+ (errmsg("proxy connection from: host=%s port=%s (proxy host=%s port=%s)",
+ remote_host,
+ remote_port,
+ proxy_host,
+ proxy_port)));
+
+ }
+
+ return STATUS_OK;
+}
/*
* Read a client's startup packet and do something according to it.
@@ -2086,7 +2416,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
#ifdef USE_SSL
/* No SSL when disabled or on Unix sockets */
- if (!LoadedSSL || IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!LoadedSSL || IS_AF_UNIX(port->daddr.addr.ss_family))
SSLok = 'N';
else
SSLok = 'S'; /* Support for SSL */
@@ -2123,7 +2453,7 @@ retry1:
#ifdef ENABLE_GSS
/* No GSSAPI encryption when on Unix socket */
- if (!IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!IS_AF_UNIX(port->daddr.addr.ss_family))
GSSok = 'G';
#endif
@@ -2635,10 +2965,10 @@ ClosePostmasterPorts(bool am_syslogger)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -4422,6 +4752,31 @@ BackendInitialize(Port *port)
InitializeTimeouts(); /* establishes SIGALRM handler */
PG_SETMASK(&StartupBlockSig);
+ /*
+ * Ready to begin client interaction. We will give up and _exit(1) after
+ * a time delay, so that a broken client can't hog a connection
+ * indefinitely. PreAuthDelay and any DNS interactions above don't count
+ * against the time limit.
+ *
+ * If this is a proxy connection, we apply the timeout once while waiting
+ * for the proxy header. It is then reapplied further down when we process
+ * the startup packet, which means it can apply multiple times.
+ *
+ * For the time being we re-use AuthenticationTimeout for this, but it may
+ * be considered for a separate tunable in the future.
+ */
+ RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
+
+ /* Check if this is a proxy connection and if so unwrap the proxying */
+ if (port->isProxy)
+ {
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
+ if (UnwrapProxyConnection(port) != STATUS_OK)
+ proc_exit(0);
+ disable_timeout(STARTUP_PACKET_TIMEOUT, false);
+ }
+
+
/*
* Get the remote host name and port for logging and status display.
*/
@@ -4474,27 +4829,20 @@ BackendInitialize(Port *port)
port->remote_hostname = strdup(remote_host);
/*
- * Ready to begin client interaction. We will give up and _exit(1) after
- * a time delay, so that a broken client can't hog a connection
- * indefinitely. PreAuthDelay and any DNS interactions above don't count
- * against the time limit.
+ * Receive the startup packet (which might turn out to be a cancel request
+ * packet).
*
* Note: AuthenticationTimeout is applied here while waiting for the
* startup packet, and then again in InitPostgres for the duration of any
* authentication operations. So a hostile client could tie up the
- * process for nearly twice AuthenticationTimeout before we kick him off.
+ * process for nearly twice (or three times in the case of a proxy connection)
+ * AuthenticationTimeout before we kick him off.
*
* Note: because PostgresMain will call InitializeTimeouts again, the
* registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
* since we never use it again after this function.
*/
- RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
-
- /*
- * Receive the startup packet (which might turn out to be a cancel request
- * packet).
- */
status = ProcessStartupPacket(port, false, false);
/*
diff --git a/src/backend/utils/adt/network.c b/src/backend/utils/adt/network.c
index 0ab54316f8..9198a29f51 100644
--- a/src/backend/utils/adt/network.c
+++ b/src/backend/utils/adt/network.c
@@ -1802,6 +1802,8 @@ inet_client_port(PG_FUNCTION_ARGS)
/*
* IP address that the server accepted the connection on (NULL if Unix socket)
+ * If the connection is a PROXY connection, then this returns the IP address/port of
+ * the proxy server, and not the local connection!
*/
Datum
inet_server_addr(PG_FUNCTION_ARGS)
@@ -1813,7 +1815,7 @@ inet_server_addr(PG_FUNCTION_ARGS)
if (port == NULL)
PG_RETURN_NULL();
- switch (port->laddr.addr.ss_family)
+ switch (port->daddr.addr.ss_family)
{
case AF_INET:
#ifdef HAVE_IPV6
@@ -1826,14 +1828,14 @@ inet_server_addr(PG_FUNCTION_ARGS)
local_host[0] = '\0';
- ret = pg_getnameinfo_all(&port->laddr.addr, port->laddr.salen,
+ ret = pg_getnameinfo_all(&port->daddr.addr, port->daddr.salen,
local_host, sizeof(local_host),
NULL, 0,
NI_NUMERICHOST | NI_NUMERICSERV);
if (ret != 0)
PG_RETURN_NULL();
- clean_ipv6_addr(port->laddr.addr.ss_family, local_host);
+ clean_ipv6_addr(port->daddr.addr.ss_family, local_host);
PG_RETURN_INET_P(network_in(local_host, false));
}
@@ -1841,6 +1843,8 @@ inet_server_addr(PG_FUNCTION_ARGS)
/*
* port that the server accepted the connection on (NULL if Unix socket)
+ * If the connection is a PROXY connection, then this returns the IP address/port of
+ * the proxy server, and not the local connection!
*/
Datum
inet_server_port(PG_FUNCTION_ARGS)
@@ -1852,7 +1856,7 @@ inet_server_port(PG_FUNCTION_ARGS)
if (port == NULL)
PG_RETURN_NULL();
- switch (port->laddr.addr.ss_family)
+ switch (port->daddr.addr.ss_family)
{
case AF_INET:
#ifdef HAVE_IPV6
@@ -1865,7 +1869,7 @@ inet_server_port(PG_FUNCTION_ARGS)
local_port[0] = '\0';
- ret = pg_getnameinfo_all(&port->laddr.addr, port->laddr.salen,
+ ret = pg_getnameinfo_all(&port->daddr.addr, port->daddr.salen,
NULL, 0,
local_port, sizeof(local_port),
NI_NUMERICHOST | NI_NUMERICSERV);
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d2ce4a8450..dc8a44b0af 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -50,10 +50,12 @@
#include "commands/user.h"
#include "commands/vacuum.h"
#include "commands/variable.h"
+#include "common/ip.h"
#include "common/string.h"
#include "funcapi.h"
#include "jit/jit.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
@@ -234,6 +236,8 @@ static bool check_recovery_target_lsn(char **newval, void **extra, GucSource sou
static void assign_recovery_target_lsn(const char *newval, void *extra);
static bool check_primary_slot_name(char **newval, void **extra, GucSource source);
static bool check_default_with_oids(bool *newval, void **extra, GucSource source);
+static bool check_proxy_servers(char **newval, void **extra, GucSource source);
+static void assign_proxy_servers(const char *newval, void *extra);
/* Private functions in guc-file.l that need to be called from guc.c */
static ConfigVariable *ProcessConfigFileInternal(GucContext context,
@@ -2382,6 +2386,16 @@ static struct config_int ConfigureNamesInt[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_port", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the TCP port the server listens for PROXY connections on."),
+ NULL
+ },
+ &ProxyPortNumber,
+ 0, 0, 65535,
+ NULL, NULL, NULL
+ },
+
{
{"unix_socket_permissions", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
gettext_noop("Sets the access permissions of the Unix-domain socket."),
@@ -4355,6 +4369,17 @@ static struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_servers", PGC_SIGHUP, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the addresses for trusted proxy servers."),
+ NULL,
+ GUC_LIST_INPUT
+ },
+ &TrustedProxyServersString,
+ "",
+ check_proxy_servers, assign_proxy_servers, NULL
+ },
+
{
/*
* Can't be set by ALTER SYSTEM as it can lead to recursive definition
@@ -12559,4 +12584,118 @@ check_default_with_oids(bool *newval, void **extra, GucSource source)
return true;
}
+static bool
+check_proxy_servers(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ struct sockaddr_storage *myextra;
+
+ /* Special case when it's empty */
+ if (**newval == '\0')
+ {
+ *extra = NULL;
+ return true;
+ }
+
+ /* Need a modifiable copy of string */
+ rawstring = pstrdup(*newval);
+
+ /* Parse string into list of identifiers */
+ if (!SplitIdentifierString(rawstring, ',', &elemlist))
+ {
+ /* syntax error in list */
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ if (list_length(elemlist) == 0)
+ {
+ /* If it had only whitespace */
+ pfree(rawstring);
+ list_free(elemlist);
+
+ *extra = NULL;
+ return true;
+ }
+
+ /*
+ * We store the result in an array of sockaddr_storage. The first entry is
+ * just an overloaded int which holds the size of the array.
+ */
+ myextra = (struct sockaddr_storage *) guc_malloc(ERROR, sizeof(struct sockaddr_storage) * (list_length(elemlist) * 2 + 1));
+ *((int *) &myextra[0]) = list_length(elemlist);
+
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+ char *netmasktok = NULL;
+ int ret;
+ struct addrinfo *gai_result;
+ struct addrinfo hints;
+
+ /*
+ * Unix sockets don't have endpoint addresses, so just flag them as
+ * AF_UNIX
+ */
+ if (pg_strcasecmp(tok, "unix") == 0)
+ {
+ myextra[foreach_current_index(l) * 2 + 1].ss_family = AF_UNIX;
+ continue;
+ }
+
+ netmasktok = strchr(tok, '/');
+ if (netmasktok)
+ {
+ *netmasktok = '\0';
+ netmasktok++;
+ }
+
+ memset((char *) &hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ ret = pg_getaddrinfo_all(tok, NULL, &hints, &gai_result);
+ if (ret != 0 || gai_result == NULL)
+ {
+ GUC_check_errdetail("Invalid IP address %s", tok);
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+
+ memcpy((char *) &myextra[foreach_current_index(l) * 2 + 1], gai_result->ai_addr, gai_result->ai_addrlen);
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+
+ /* A NULL netmasktok means the fully set hostmask */
+ if (pg_sockaddr_cidr_mask(&myextra[foreach_current_index(l) * 2 + 2], netmasktok, myextra[foreach_current_index(l) * 2 + 1].ss_family) != 0)
+ {
+ if (netmasktok)
+ GUC_check_errdetail("Invalid netmask %s", netmasktok);
+ else
+ GUC_check_errdetail("Could not create netmask");
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+ }
+
+ pfree(rawstring);
+ list_free(elemlist);
+ *extra = (void *) myextra;
+
+ return true;
+}
+
+static void
+assign_proxy_servers(const char *newval, void *extra)
+{
+ TrustedProxyServers = (struct sockaddr_storage *) extra;
+}
+
#include "guc-file.c"
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 3fe9a53cb3..aec19705ab 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -62,6 +62,9 @@
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
+#proxy_port = 0 # port to listen to for proxy connections
+ # (change requires restart)
+#proxy_servers = '' # what proxy servers to trust
#max_connections = 100 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
#unix_socket_directories = '/tmp' # comma-separated list of directories
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 02015efe13..e91a4d7607 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -126,9 +126,11 @@ typedef struct Port
{
pgsocket sock; /* File descriptor */
bool noblock; /* is the socket in non-blocking mode? */
+ bool isProxy; /* is the connection using PROXY protocol */
ProtocolVersion proto; /* FE/BE protocol version */
SockAddr laddr; /* local addr (postmaster) */
SockAddr raddr; /* remote addr (client) */
+ SockAddr daddr; /* destination addr (postmaster, or proxy server if proxy protocol used) */
char *remote_host; /* name (or ip addr) of remote host */
char *remote_hostname; /* name (not ip addr) of remote host, if
* available */
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 6c51b2f20f..cdaae030e1 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -42,6 +42,12 @@ typedef struct
extern const PGDLLIMPORT PQcommMethods *PqCommMethods;
+typedef struct
+{
+ pgsocket socket;
+ bool isProxy;
+} PQlistenSocket;
+
#define pq_comm_reset() (PqCommMethods->comm_reset())
#define pq_flush() (PqCommMethods->flush())
#define pq_flush_if_writable() (PqCommMethods->flush_if_writable())
@@ -63,9 +69,9 @@ extern WaitEventSet *FeBeWaitSet;
#define FeBeWaitSetSocketPos 0
#define FeBeWaitSetLatchPos 1
-extern int StreamServerPort(int family, const char *hostName,
- unsigned short portNumber, const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen);
+extern PQlistenSocket *StreamServerPort(int family, const char *hostName,
+ unsigned short portNumber, const char *unixSocketDir,
+ PQlistenSocket PQlistenSocket[], int MaxListen);
extern int StreamConnection(pgsocket server_fd, Port *port);
extern void StreamClose(pgsocket sock);
extern void TouchSocketFiles(void);
@@ -78,6 +84,7 @@ extern bool pq_is_reading_msg(void);
extern int pq_getmessage(StringInfo s, int maxlen);
extern int pq_getbyte(void);
extern int pq_peekbyte(void);
+extern int pq_discardbytes(size_t len);
extern int pq_getbyte_if_available(unsigned char *c);
extern int pq_putmessage_v2(char msgtype, const char *s, size_t len);
extern bool pq_check_connection(void);
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index 0efdd7c232..2a029ef786 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -17,10 +17,13 @@
extern bool EnableSSL;
extern int ReservedBackends;
extern PGDLLIMPORT int PostPortNumber;
+extern PGDLLIMPORT int ProxyPortNumber;
extern int Unix_socket_permissions;
extern char *Unix_socket_group;
extern char *Unix_socket_directories;
extern char *ListenAddresses;
+extern char *TrustedProxyServersString;
+extern struct sockaddr_storage *TrustedProxyServers;
extern bool ClientAuthInProgress;
extern int PreAuthDelay;
extern int AuthenticationTimeout;
diff --git a/src/test/Makefile b/src/test/Makefile
index 46275915ff..4ad030034c 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -12,7 +12,8 @@ subdir = src/test
top_builddir = ../..
include $(top_builddir)/src/Makefile.global
-SUBDIRS = perl regress isolation modules authentication recovery subscription
+SUBDIRS = perl regress isolation modules authentication recovery subscription \
+ protocol
# Test suites that are not safe by default but can be run if selected
# by the user via the whitespace-separated list in variable
diff --git a/src/test/protocol/Makefile b/src/test/protocol/Makefile
new file mode 100644
index 0000000000..bda49d6ecb
--- /dev/null
+++ b/src/test/protocol/Makefile
@@ -0,0 +1,23 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/protocol
+#
+# Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/protocol/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/protocol
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
+
+clean distclean maintainer-clean:
+ rm -rf tmp_check
diff --git a/src/test/protocol/t/001_proxy.pl b/src/test/protocol/t/001_proxy.pl
new file mode 100644
index 0000000000..ad560da6f7
--- /dev/null
+++ b/src/test/protocol/t/001_proxy.pl
@@ -0,0 +1,151 @@
+use strict;
+use warnings;
+use TestLib;
+use PostgresNode;
+use Test::More;
+use Socket qw(AF_INET AF_INET6 inet_pton);
+use IO::Socket;
+
+plan tests => 25;
+
+my $node = PostgresNode->new('node');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+log_connections = on
+});
+$node->append_conf(
+ 'pg_hba.conf', qq{
+host all all 11.22.33.44/32 trust
+host all all 1:2:3:4:5:6:0:9/128 trust
+});
+$node->append_conf('postgresql.conf', "proxy_port = " . ($node->port() + 1));
+
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE USER proxytest;');
+
+sub make_message
+{
+ my ($msg) = @_;
+ return pack("Na*", length($msg) + 4, $msg);
+}
+
+sub read_packet
+{
+ my ($socket) = @_;
+ my $buf = "";
+ $socket->recv($buf, 1024);
+ return $buf;
+}
+
+
+# Test normal connection through localhost
+sub test_connection
+{
+ my ($socket, $proxy, $what, $shouldbe, $shouldfail, $extra) = @_;
+ ok($socket, $what);
+
+ my $startup = make_message(
+ pack("N(Z*Z*)*x", 196608, (user => "proxytest", database => "postgres")));
+
+ $extra = "" if !defined($extra);
+
+ if (defined($proxy))
+ {
+ my $p = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A\x21";
+ if ($proxy =~ ":")
+ {
+ # ipv6
+ $p .= "\x21"; # TCP v6
+ $p .= pack "n", 36 + length($extra); # size
+ $p .= inet_pton(AF_INET6, $proxy);
+ $p .= "\0" x 16; # destination address
+ }
+ else
+ {
+ # ipv4
+ $p .= "\x11"; # TCP v4
+ $p .= pack "n", 12 + length($extra); # size
+ $p .= inet_pton(AF_INET, $proxy);
+ $p .= "\0\0\0\0"; # destination address
+ }
+ $p .= pack "n", 1919; # source port
+ $p .= pack "n", 0;
+ $p .= $extra;
+ print $socket $p;
+ }
+ print $socket $startup;
+
+ my $in = read_packet($socket);
+ if (defined($shouldfail))
+ {
+ isnt(substr($in, 0, 1), 'R', $what);
+ }
+ else
+ {
+ is(substr($in, 0, 1), 'R', $what);
+ }
+
+ SKIP:
+ {
+ skip "The rest of this test should fail", 3 if (defined($shouldfail));
+
+ is(substr($in, 8, 1), "\0", $what);
+
+ my ($resip, $resport) = split /\|/,
+ $node->safe_psql('postgres',
+ "SELECT client_addr, client_port FROM pg_stat_activity WHERE pid != pg_backend_pid() AND backend_type='client backend'"
+ );
+ is($resip, $shouldbe, $what);
+ if ($proxy)
+ {
+ is($resport, "1919", $what);
+ }
+ else
+ {
+ ok($resport, $what);
+ }
+ }
+
+ $socket->close();
+
+ return;
+}
+
+sub make_socket
+{
+ my ($port) = @_;
+ if ($PostgresNode::use_tcp) {
+ return IO::Socket::INET->new(
+ PeerAddr => "127.0.0.1",
+ PeerPort => $port,
+ Proto => "tcp",
+ Type => SOCK_STREAM);
+ }
+ else {
+ return IO::Socket::UNIX->new(
+ Peer => $node->host() . "/.s.PGSQL." . $port,
+ Type => SOCK_STREAM);
+ }
+}
+
+# Test a regular connection first to make sure connecting etc works fine.
+test_connection(make_socket($node->port()),
+ undef, "normal connection", $PostgresNode::use_tcp ? "127.0.0.1": "");
+
+# Make sure we can't make a proxy connection until it's allowed
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44", 1);
+
+# Allow proxy connections and test them
+$node->append_conf('postgresql.conf', "proxy_servers = 'unix, 127.0.0.1/32'");
+$node->restart();
+
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44");
+test_connection(make_socket($node->port() + 1),
+ "1:2:3:4:5:6::9", "proxy ipv6", "1:2:3:4:5:6:0:9");
+
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy with extra", "11.22.33.44", undef, "abcdef"x100);
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-28 13:23 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-11-03 13:36 ` Daniel Gustafsson <daniel@yesql.se>
2021-11-04 11:03 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 1 reply; 56+ messages in thread
From: Daniel Gustafsson @ 2021-11-03 13:36 UTC (permalink / raw)
To: Magnus Hagander <magnus@hagander.net>; +Cc: Jacob Champion <pchampion@vmware.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
> On 28 Sep 2021, at 15:23, Magnus Hagander <magnus@hagander.net> wrote:
> On Fri, Sep 10, 2021 at 1:44 AM Jacob Champion <pchampion@vmware.com> wrote:
>> The TAP test will need to be rebased over the changes in 201a76183e.
>
> Done
And now the TAP test will need to be rebased over the changes in
b3b4d8e68ae83f432f43f035c7eb481ef93e1583.
--
Daniel Gustafsson https://vmware.com/
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-28 13:23 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-03 13:36 ` Re: PROXY protocol support Daniel Gustafsson <daniel@yesql.se>
@ 2021-11-04 11:03 ` Magnus Hagander <magnus@hagander.net>
2021-11-15 23:03 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
0 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2021-11-04 11:03 UTC (permalink / raw)
To: Daniel Gustafsson <daniel@yesql.se>; +Cc: Jacob Champion <pchampion@vmware.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Wed, Nov 3, 2021 at 2:36 PM Daniel Gustafsson <daniel@yesql.se> wrote:
> > On 28 Sep 2021, at 15:23, Magnus Hagander <magnus@hagander.net> wrote:
> > On Fri, Sep 10, 2021 at 1:44 AM Jacob Champion <pchampion@vmware.com>
> wrote:
>
> >> The TAP test will need to be rebased over the changes in 201a76183e.
> >
> > Done
>
> And now the TAP test will need to be rebased over the changes in
> b3b4d8e68ae83f432f43f035c7eb481ef93e1583.
>
Thanks for the pointer, PFA a rebase.
--
Magnus Hagander
Me: https://www.hagander.net/ <http://www.hagander.net/;
Work: https://www.redpill-linpro.com/ <http://www.redpill-linpro.com/;
Attachments:
[text/x-patch] proxy_protocol_10.patch (41.5K, ../../CABUevEy=nTqkfBCA5=vrWPEaS+D7rBLeoKVbk_e2WprGpTTMDQ@mail.gmail.com/3-proxy_protocol_10.patch)
download | inline diff:
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 02f0489112..a3ff09b3ac 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -353,6 +353,15 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl
the client's host name instead of the IP address in the log.
</para>
+ <para>
+ If <xref linkend="guc-proxy-port"/> is enabled and the
+ connection is made through a proxy server using the PROXY
+ protocol, the actual IP address of the client will be used
+ for matching. If a connection is made through a proxy server
+ not using the PROXY protocol, the IP address of the
+ proxy server will be used.
+ </para>
+
<para>
These fields do not apply to <literal>local</literal> records.
</para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index de77f14573..5211c1f7b1 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -682,6 +682,56 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-proxy-port" xreflabel="proxy_port">
+ <term><varname>proxy_port</varname> (<type>integer</type>)
+ <indexterm>
+ <primary><varname>proxy_port</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ The TCP port the server listens on for PROXY connections, disabled by
+ default. If set to a number, <productname>PostgreSQL</productname>
+ will listen on this port on the same addresses as for regular
+ connections, but expect all connections to use the PROXY protocol to
+ identify the client. This parameter can only be set at server start.
+ </para>
+ <para>
+ If a proxy connection is made over this port, and the proxy is listed
+ in <xref linkend="guc-proxy-servers" />, the actual client address
+ will be considered as the address of the client, instead of listing
+ all connections as coming from the proxy server.
+ </para>
+ <para>
+ The <ulink url="http://www.haproxy.org/download/1.9/doc/proxy-protocol.txt">PROXY
+ protocol</ulink> is maintained by <productname>HAProxy</productname>,
+ and supported in many proxies and load
+ balancers. <productname>PostgreSQL</productname> supports version 2
+ of the protocol.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-proxy-servers" xreflabel="proxy_servers">
+ <term><varname>proxy_servers</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>proxy_servers</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ A comma separated list of one or more ip addresses, cidr specifications or the
+ literal <literal>unix</literal>, indicating which proxy servers to trust when
+ connecting on the port specified in <xref linkend="guc-proxy-port" />.
+ </para>
+ <para>
+ If a proxy connection is made from an IP address not covered by this
+ list, the connection will be rejected. By default no proxy is trusted
+ and all proxy connections will be rejected.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-max-connections" xreflabel="max_connections">
<term><varname>max_connections</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 4b49dff2ff..74f38d4891 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -22238,7 +22238,12 @@ SELECT * FROM pg_ls_dir('.') WITH ORDINALITY AS t(ls,n);
connection,
or <literal>NULL</literal> if the current connection is via a
Unix-domain socket.
- </para></entry>
+ </para>
+ <para>
+ If the connection is a PROXY connection, this function returns the
+ IP address used to connect to the proxy server.
+ </para>
+ </entry>
</row>
<row>
@@ -22254,7 +22259,13 @@ SELECT * FROM pg_ls_dir('.') WITH ORDINALITY AS t(ls,n);
connection,
or <literal>NULL</literal> if the current connection is via a
Unix-domain socket.
- </para></entry>
+ </para>
+ <para>
+ If the connection is a PROXY connection, this function returns the
+ port used to connect to the proxy server.
+ </para>
+
+ </entry>
</row>
<row>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index a317aef1c9..f8c32ad492 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -1696,6 +1696,14 @@ ident_inet(hbaPort *port)
*la = NULL,
hints;
+ if (port->isProxy)
+ {
+ ereport(LOG,
+ (errcode_for_socket_access(),
+ errmsg("Ident authentication cannot be used over PROXY connections")));
+ return STATUS_ERROR;
+ }
+
/*
* Might look a little weird to first convert it to text and then back to
* sockaddr, but it's protocol independent.
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 89a5f901aa..a8d6c5fa4c 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -311,13 +311,13 @@ socket_close(int code, Datum arg)
* Successfully opened sockets are added to the ListenSocket[] array (of
* length MaxListen), at the first position that isn't PGINVALID_SOCKET.
*
- * RETURNS: STATUS_OK or STATUS_ERROR
+ * RETURNS: The PQlistenSocket listening on, or NULL in case of error
*/
-int
+PQlistenSocket *
StreamServerPort(int family, const char *hostName, unsigned short portNumber,
const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen)
+ PQlistenSocket ListenSocket[], int MaxListen)
{
pgsocket fd;
int err;
@@ -362,10 +362,10 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("Unix-domain socket path \"%s\" is too long (maximum %d bytes)",
unixSocketPath,
(int) (UNIXSOCK_PATH_BUFLEN - 1))));
- return STATUS_ERROR;
+ return NULL;
}
if (Lock_AF_UNIX(unixSocketDir, unixSocketPath) != STATUS_OK)
- return STATUS_ERROR;
+ return NULL;
service = unixSocketPath;
}
else
@@ -388,7 +388,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
service, gai_strerror(ret))));
if (addrs)
pg_freeaddrinfo_all(hint.ai_family, addrs);
- return STATUS_ERROR;
+ return NULL;
}
for (addr = addrs; addr; addr = addr->ai_next)
@@ -405,7 +405,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
/* See if there is still room to add 1 more socket. */
for (; listen_index < MaxListen; listen_index++)
{
- if (ListenSocket[listen_index] == PGINVALID_SOCKET)
+ if (ListenSocket[listen_index].socket == PGINVALID_SOCKET)
break;
}
if (listen_index >= MaxListen)
@@ -584,16 +584,16 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("listening on %s address \"%s\", port %d",
familyDesc, addrDesc, (int) portNumber)));
- ListenSocket[listen_index] = fd;
+ ListenSocket[listen_index].socket = fd;
added++;
}
pg_freeaddrinfo_all(hint.ai_family, addrs);
if (!added)
- return STATUS_ERROR;
+ return NULL;
- return STATUS_OK;
+ return &ListenSocket[listen_index];
}
@@ -747,6 +747,9 @@ StreamConnection(pgsocket server_fd, Port *port)
return STATUS_ERROR;
}
+ /* copy over to daddr to make sure it's set for the non-proxy case */
+ memcpy(&port->daddr, &port->laddr, sizeof(port->laddr));
+
/* select NODELAY and KEEPALIVE options if it's a TCP connection */
if (!IS_AF_UNIX(port->laddr.addr.ss_family))
{
@@ -1118,7 +1121,7 @@ pq_getbytes(char *s, size_t len)
* returns 0 if OK, EOF if trouble
* --------------------------------
*/
-static int
+int
pq_discardbytes(size_t len)
{
size_t amount;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index e2a76ba055..797a317a04 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -101,6 +101,7 @@
#include "common/string.h"
#include "lib/ilist.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
@@ -196,15 +197,22 @@ BackgroundWorker *MyBgworkerEntry = NULL;
-/* The socket number we are listening for connections on */
+/* The TCP port number we are listening for connections on */
int PostPortNumber;
+/* The TCP port number we are listening for proxy connections on */
+int ProxyPortNumber;
+
/* The directory names for Unix socket(s) */
char *Unix_socket_directories;
/* The TCP listen address(es) */
char *ListenAddresses;
+/* Trusted proxy servers */
+char *TrustedProxyServersString = NULL;
+struct sockaddr_storage *TrustedProxyServers = NULL;
+
/*
* ReservedBackends is the number of backends reserved for superuser use.
* This number is taken out of the pool size given by MaxConnections so
@@ -218,7 +226,7 @@ int ReservedBackends;
/* The socket(s) we're listening to. */
#define MAXLISTEN 64
-static pgsocket ListenSocket[MAXLISTEN];
+static PQlistenSocket ListenSocket[MAXLISTEN];
/*
* These globals control the behavior of the postmaster in case some
@@ -586,6 +594,7 @@ PostmasterMain(int argc, char *argv[])
bool listen_addr_saved = false;
int i;
char *output_config_variable = NULL;
+ PQlistenSocket *socket = NULL;
InitProcessGlobals();
@@ -1185,7 +1194,10 @@ PostmasterMain(int argc, char *argv[])
* charged with closing the sockets again at postmaster shutdown.
*/
for (i = 0; i < MAXLISTEN; i++)
- ListenSocket[i] = PGINVALID_SOCKET;
+ {
+ ListenSocket[i].socket = PGINVALID_SOCKET;
+ ListenSocket[i].isProxy = false;
+ }
on_proc_exit(CloseServerPorts, 0);
@@ -1214,17 +1226,17 @@ PostmasterMain(int argc, char *argv[])
char *curhost = (char *) lfirst(l);
if (strcmp(curhost, "*") == 0)
- status = StreamServerPort(AF_UNSPEC, NULL,
+ socket = StreamServerPort(AF_UNSPEC, NULL,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
else
- status = StreamServerPort(AF_UNSPEC, curhost,
+ socket = StreamServerPort(AF_UNSPEC, curhost,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful host addr in lockfile */
@@ -1238,6 +1250,30 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create listen socket for \"%s\"",
curhost)));
+
+ /* Also listen to the PROXY port on this address, if configured */
+ if (ProxyPortNumber)
+ {
+ if (strcmp(curhost, "*") == 0)
+ socket = StreamServerPort(AF_UNSPEC, NULL,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ else
+ socket = StreamServerPort(AF_UNSPEC, curhost,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ {
+ success++;
+ socket->isProxy = true;
+ }
+ else
+ ereport(WARNING,
+ (errmsg("could not create PROXY listen socket for \"%s\"",
+ curhost)));
+ }
}
if (!success && elemlist != NIL)
@@ -1250,7 +1286,7 @@ PostmasterMain(int argc, char *argv[])
#ifdef USE_BONJOUR
/* Register for Bonjour only if we opened TCP socket(s) */
- if (enable_bonjour && ListenSocket[0] != PGINVALID_SOCKET)
+ if (enable_bonjour && ListenSocket[0].socket != PGINVALID_SOCKET)
{
DNSServiceErrorType err;
@@ -1312,12 +1348,12 @@ PostmasterMain(int argc, char *argv[])
{
char *socketdir = (char *) lfirst(l);
- status = StreamServerPort(AF_UNIX, NULL,
+ socket = StreamServerPort(AF_UNIX, NULL,
(unsigned short) PostPortNumber,
socketdir,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful Unix socket in lockfile */
@@ -1328,9 +1364,23 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create Unix-domain socket in directory \"%s\"",
socketdir)));
+
+ if (ProxyPortNumber)
+ {
+ socket = StreamServerPort(AF_UNIX, NULL,
+ (unsigned short) ProxyPortNumber,
+ socketdir,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ socket->isProxy = true;
+ else
+ ereport(WARNING,
+ (errmsg("could not create Unix-domain PROXY socket for \"%s\"",
+ socketdir)));
+ }
}
- if (!success && elemlist != NIL)
+ if (socket == NULL && elemlist != NIL)
ereport(FATAL,
(errmsg("could not create any Unix-domain sockets")));
@@ -1342,7 +1392,7 @@ PostmasterMain(int argc, char *argv[])
/*
* check that we have some socket to listen on
*/
- if (ListenSocket[0] == PGINVALID_SOCKET)
+ if (ListenSocket[0].socket == PGINVALID_SOCKET)
ereport(FATAL,
(errmsg("no socket created for listening")));
@@ -1497,10 +1547,10 @@ CloseServerPorts(int status, Datum arg)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -1789,15 +1839,17 @@ ServerLoop(void)
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] == PGINVALID_SOCKET)
+ if (ListenSocket[i].socket == PGINVALID_SOCKET)
break;
- if (FD_ISSET(ListenSocket[i], &rmask))
+ if (FD_ISSET(ListenSocket[i].socket, &rmask))
{
Port *port;
- port = ConnCreate(ListenSocket[i]);
+ port = ConnCreate(ListenSocket[i].socket);
if (port)
{
+ port->isProxy = ListenSocket[i].isProxy;
+
BackendStartup(port);
/*
@@ -1965,7 +2017,7 @@ initMasks(fd_set *rmask)
for (i = 0; i < MAXLISTEN; i++)
{
- int fd = ListenSocket[i];
+ int fd = ListenSocket[i].socket;
if (fd == PGINVALID_SOCKET)
break;
@@ -1978,6 +2030,284 @@ initMasks(fd_set *rmask)
return maxsock + 1;
}
+static int
+UnwrapProxyConnection(Port *port)
+{
+ char proxyver;
+ uint16 proxyaddrlen;
+ SockAddr raddr_save;
+ SockAddr laddr_save;
+ int i;
+ bool useproxy = false;
+
+ /*
+ * These structs are from the PROXY protocol docs at
+ * http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
+ */
+ union
+ {
+ struct
+ { /* for TCP/UDP over IPv4, len = 12 */
+ uint32 src_addr;
+ uint32 dst_addr;
+ uint16 src_port;
+ uint16 dst_port;
+ } ip4;
+ struct
+ { /* for TCP/UDP over IPv6, len = 36 */
+ uint8 src_addr[16];
+ uint8 dst_addr[16];
+ uint16 src_port;
+ uint16 dst_port;
+ } ip6;
+ } proxyaddr;
+ struct
+ {
+ uint8 sig[12]; /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */
+ uint8 ver_cmd; /* protocol version and command */
+ uint8 fam; /* protocol family and address */
+ uint16 len; /* number of following bytes part of the
+ * header */
+ } proxyheader;
+
+ /*
+ * Assert the size of the structs that are part of the protocol,
+ * to defend against strange compilers.
+ */
+ StaticAssertStmt(sizeof(proxyheader) == 16, "proxy header struct has invalid size");
+ StaticAssertStmt(sizeof(proxyaddr.ip4) == 12, "proxy address ipv4 struct has invalid size");
+ StaticAssertStmt(sizeof(proxyaddr.ip6) == 36, "proxy address ipv6 struct has invalid size");
+
+
+ /* Else if it's on our list of trusted proxies */
+ if (TrustedProxyServers)
+ {
+ for (i = 0; i < *((int *) TrustedProxyServers) * 2; i += 2)
+ {
+ if (port->raddr.addr.ss_family == TrustedProxyServers[i + 1].ss_family)
+ {
+ /*
+ * Connection over unix sockets don't give us the source, so
+ * just check if they're allowed at all. For IP connections,
+ * verify that it's an allowed address.
+ */
+ if (port->raddr.addr.ss_family == AF_UNIX ||
+ pg_range_sockaddr(&port->raddr.addr,
+ &TrustedProxyServers[i + 1],
+ &TrustedProxyServers[i + 2]))
+ {
+ useproxy = true;
+ break;
+ }
+ }
+ }
+ }
+ if (!useproxy)
+ {
+ /*
+ * Connection is not from one of our trusted proxies, so reject it.
+ */
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("connection from unauthorized proxy server")));
+ return STATUS_ERROR;
+ }
+
+ /* Store a copy of the original address, for logging */
+ memcpy(&raddr_save, &port->raddr, sizeof(SockAddr));
+ memcpy(&laddr_save, &port->laddr, sizeof(SockAddr));
+
+ pq_startmsgread();
+
+ /*
+ * PROXY requests always start with:
+ * \x0D \x0A \x0D \x0A \x00 \x0D \x0A \x51 \x55 \x49 \x54 \x0A
+ */
+
+ if (pq_getbytes((char *) &proxyheader, sizeof(proxyheader)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ if (memcmp(proxyheader.sig, "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a", sizeof(proxyheader.sig)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Proxy version is in the high 4 bits of the first byte */
+ proxyver = (proxyheader.ver_cmd & 0xF0) >> 4;
+ if (proxyver != 2)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol version: %x", proxyver)));
+ return STATUS_ERROR;
+ }
+
+ /*
+ * Proxy command is in the low 4 bits of the first byte.
+ * 0x00 = local, 0x01 = proxy, all others should be rejected
+ */
+ if ((proxyheader.ver_cmd & 0x0F) == 0x00)
+ {
+ if (proxyheader.fam != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol family %x for local connection", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+ }
+ else if ((proxyheader.ver_cmd & 0x0F) == 0x01)
+ {
+ if (proxyheader.fam == 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol family 0 for non-local connection")));
+ return STATUS_ERROR;
+ }
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol command: %x", (proxyheader.ver_cmd & 0x0f))));
+ return STATUS_ERROR;
+ }
+
+ proxyaddrlen = pg_ntoh16(proxyheader.len);
+
+ if (pq_getbytes((char *) &proxyaddr, proxyaddrlen > sizeof(proxyaddr) ? sizeof(proxyaddr) : proxyaddrlen) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Connection family */
+ if (proxyheader.fam == 0)
+ {
+ /*
+ * UNSPEC connection over LOCAL (verified above).
+ * in this case we just ignore the address included.
+ */
+ }
+ else if (proxyheader.fam == 0x11)
+ {
+ /* TCPv4 */
+ if (proxyaddrlen < 12)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET;
+ port->raddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
+
+ port->daddr.addr.ss_family = AF_INET;
+ port->daddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->daddr.addr)->sin_addr.s_addr = proxyaddr.ip4.dst_addr;
+ ((struct sockaddr_in *) &port->daddr.addr)->sin_port = proxyaddr.ip4.dst_port;
+ }
+ else if (proxyheader.fam == 0x21)
+ {
+ /* TCPv6 */
+ if (proxyaddrlen < 36)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET6;
+ port->raddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->raddr.addr)->sin6_addr, proxyaddr.ip6.src_addr, 16);
+ ((struct sockaddr_in6 *) &port->raddr.addr)->sin6_port = proxyaddr.ip6.src_port;
+
+
+ port->daddr.addr.ss_family = AF_INET6;
+ port->daddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->daddr.addr)->sin6_addr, proxyaddr.ip6.dst_addr, 16);
+ ((struct sockaddr_in6 *) &port->daddr.addr)->sin6_port = proxyaddr.ip6.dst_port;
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol connection type: %x", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+
+ /* If there is any more header data present, skip past it */
+ if (proxyaddrlen > sizeof(proxyaddr))
+ {
+ if (pq_discardbytes(proxyaddrlen - sizeof(proxyaddr)) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ }
+
+ pq_endmsgread();
+
+ /*
+ * Log what we've done if connection logging is enabled. We log the proxy
+ * connection here, and let the normal connection logging mechanism log
+ * the unwrapped connection.
+ */
+ if (Log_connections)
+ {
+ char remote_host[NI_MAXHOST];
+ char remote_port[NI_MAXSERV];
+ char proxy_host[NI_MAXHOST];
+ char proxy_port[NI_MAXSERV];
+ int ret;
+
+ remote_host[0] = '\0';
+ remote_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&raddr_save.addr, raddr_save.salen,
+ remote_host, sizeof(remote_host),
+ remote_port, sizeof(remote_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ proxy_host[0] = '\0';
+ proxy_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&laddr_save.addr, laddr_save.salen,
+ proxy_host, sizeof(proxy_host),
+ proxy_port, sizeof(proxy_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+
+ ereport(LOG,
+ (errmsg("proxy connection from: host=%s port=%s (proxy host=%s port=%s)",
+ remote_host,
+ remote_port,
+ proxy_host,
+ proxy_port)));
+
+ }
+
+ return STATUS_OK;
+}
/*
* Read a client's startup packet and do something according to it.
@@ -2086,7 +2416,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
#ifdef USE_SSL
/* No SSL when disabled or on Unix sockets */
- if (!LoadedSSL || IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!LoadedSSL || IS_AF_UNIX(port->daddr.addr.ss_family))
SSLok = 'N';
else
SSLok = 'S'; /* Support for SSL */
@@ -2123,7 +2453,7 @@ retry1:
#ifdef ENABLE_GSS
/* No GSSAPI encryption when on Unix socket */
- if (!IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!IS_AF_UNIX(port->daddr.addr.ss_family))
GSSok = 'G';
#endif
@@ -2635,10 +2965,10 @@ ClosePostmasterPorts(bool am_syslogger)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -4422,6 +4752,31 @@ BackendInitialize(Port *port)
InitializeTimeouts(); /* establishes SIGALRM handler */
PG_SETMASK(&StartupBlockSig);
+ /*
+ * Ready to begin client interaction. We will give up and _exit(1) after
+ * a time delay, so that a broken client can't hog a connection
+ * indefinitely. PreAuthDelay and any DNS interactions above don't count
+ * against the time limit.
+ *
+ * If this is a proxy connection, we apply the timeout once while waiting
+ * for the proxy header. It is then reapplied further down when we process
+ * the startup packet, which means it can apply multiple times.
+ *
+ * For the time being we re-use AuthenticationTimeout for this, but it may
+ * be considered for a separate tunable in the future.
+ */
+ RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
+
+ /* Check if this is a proxy connection and if so unwrap the proxying */
+ if (port->isProxy)
+ {
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
+ if (UnwrapProxyConnection(port) != STATUS_OK)
+ proc_exit(0);
+ disable_timeout(STARTUP_PACKET_TIMEOUT, false);
+ }
+
+
/*
* Get the remote host name and port for logging and status display.
*/
@@ -4474,27 +4829,20 @@ BackendInitialize(Port *port)
port->remote_hostname = strdup(remote_host);
/*
- * Ready to begin client interaction. We will give up and _exit(1) after
- * a time delay, so that a broken client can't hog a connection
- * indefinitely. PreAuthDelay and any DNS interactions above don't count
- * against the time limit.
+ * Receive the startup packet (which might turn out to be a cancel request
+ * packet).
*
* Note: AuthenticationTimeout is applied here while waiting for the
* startup packet, and then again in InitPostgres for the duration of any
* authentication operations. So a hostile client could tie up the
- * process for nearly twice AuthenticationTimeout before we kick him off.
+ * process for nearly twice (or three times in the case of a proxy connection)
+ * AuthenticationTimeout before we kick him off.
*
* Note: because PostgresMain will call InitializeTimeouts again, the
* registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
* since we never use it again after this function.
*/
- RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
-
- /*
- * Receive the startup packet (which might turn out to be a cancel request
- * packet).
- */
status = ProcessStartupPacket(port, false, false);
/*
diff --git a/src/backend/utils/adt/network.c b/src/backend/utils/adt/network.c
index 0ab54316f8..9198a29f51 100644
--- a/src/backend/utils/adt/network.c
+++ b/src/backend/utils/adt/network.c
@@ -1802,6 +1802,8 @@ inet_client_port(PG_FUNCTION_ARGS)
/*
* IP address that the server accepted the connection on (NULL if Unix socket)
+ * If the connection is a PROXY connection, then this returns the IP address/port of
+ * the proxy server, and not the local connection!
*/
Datum
inet_server_addr(PG_FUNCTION_ARGS)
@@ -1813,7 +1815,7 @@ inet_server_addr(PG_FUNCTION_ARGS)
if (port == NULL)
PG_RETURN_NULL();
- switch (port->laddr.addr.ss_family)
+ switch (port->daddr.addr.ss_family)
{
case AF_INET:
#ifdef HAVE_IPV6
@@ -1826,14 +1828,14 @@ inet_server_addr(PG_FUNCTION_ARGS)
local_host[0] = '\0';
- ret = pg_getnameinfo_all(&port->laddr.addr, port->laddr.salen,
+ ret = pg_getnameinfo_all(&port->daddr.addr, port->daddr.salen,
local_host, sizeof(local_host),
NULL, 0,
NI_NUMERICHOST | NI_NUMERICSERV);
if (ret != 0)
PG_RETURN_NULL();
- clean_ipv6_addr(port->laddr.addr.ss_family, local_host);
+ clean_ipv6_addr(port->daddr.addr.ss_family, local_host);
PG_RETURN_INET_P(network_in(local_host, false));
}
@@ -1841,6 +1843,8 @@ inet_server_addr(PG_FUNCTION_ARGS)
/*
* port that the server accepted the connection on (NULL if Unix socket)
+ * If the connection is a PROXY connection, then this returns the IP address/port of
+ * the proxy server, and not the local connection!
*/
Datum
inet_server_port(PG_FUNCTION_ARGS)
@@ -1852,7 +1856,7 @@ inet_server_port(PG_FUNCTION_ARGS)
if (port == NULL)
PG_RETURN_NULL();
- switch (port->laddr.addr.ss_family)
+ switch (port->daddr.addr.ss_family)
{
case AF_INET:
#ifdef HAVE_IPV6
@@ -1865,7 +1869,7 @@ inet_server_port(PG_FUNCTION_ARGS)
local_port[0] = '\0';
- ret = pg_getnameinfo_all(&port->laddr.addr, port->laddr.salen,
+ ret = pg_getnameinfo_all(&port->daddr.addr, port->daddr.salen,
NULL, 0,
local_port, sizeof(local_port),
NI_NUMERICHOST | NI_NUMERICSERV);
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index e91d5a3cfd..6aa0e7fca6 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -50,10 +50,12 @@
#include "commands/user.h"
#include "commands/vacuum.h"
#include "commands/variable.h"
+#include "common/ip.h"
#include "common/string.h"
#include "funcapi.h"
#include "jit/jit.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
@@ -235,6 +237,8 @@ static bool check_recovery_target_lsn(char **newval, void **extra, GucSource sou
static void assign_recovery_target_lsn(const char *newval, void *extra);
static bool check_primary_slot_name(char **newval, void **extra, GucSource source);
static bool check_default_with_oids(bool *newval, void **extra, GucSource source);
+static bool check_proxy_servers(char **newval, void **extra, GucSource source);
+static void assign_proxy_servers(const char *newval, void *extra);
/* Private functions in guc-file.l that need to be called from guc.c */
static ConfigVariable *ProcessConfigFileInternal(GucContext context,
@@ -2383,6 +2387,16 @@ static struct config_int ConfigureNamesInt[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_port", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the TCP port the server listens for PROXY connections on."),
+ NULL
+ },
+ &ProxyPortNumber,
+ 0, 0, 65535,
+ NULL, NULL, NULL
+ },
+
{
{"unix_socket_permissions", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
gettext_noop("Sets the access permissions of the Unix-domain socket."),
@@ -4368,6 +4382,17 @@ static struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_servers", PGC_SIGHUP, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the addresses for trusted proxy servers."),
+ NULL,
+ GUC_LIST_INPUT
+ },
+ &TrustedProxyServersString,
+ "",
+ check_proxy_servers, assign_proxy_servers, NULL
+ },
+
{
/*
* Can't be set by ALTER SYSTEM as it can lead to recursive definition
@@ -12572,4 +12597,118 @@ check_default_with_oids(bool *newval, void **extra, GucSource source)
return true;
}
+static bool
+check_proxy_servers(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ struct sockaddr_storage *myextra;
+
+ /* Special case when it's empty */
+ if (**newval == '\0')
+ {
+ *extra = NULL;
+ return true;
+ }
+
+ /* Need a modifiable copy of string */
+ rawstring = pstrdup(*newval);
+
+ /* Parse string into list of identifiers */
+ if (!SplitIdentifierString(rawstring, ',', &elemlist))
+ {
+ /* syntax error in list */
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ if (list_length(elemlist) == 0)
+ {
+ /* If it had only whitespace */
+ pfree(rawstring);
+ list_free(elemlist);
+
+ *extra = NULL;
+ return true;
+ }
+
+ /*
+ * We store the result in an array of sockaddr_storage. The first entry is
+ * just an overloaded int which holds the size of the array.
+ */
+ myextra = (struct sockaddr_storage *) guc_malloc(ERROR, sizeof(struct sockaddr_storage) * (list_length(elemlist) * 2 + 1));
+ *((int *) &myextra[0]) = list_length(elemlist);
+
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+ char *netmasktok = NULL;
+ int ret;
+ struct addrinfo *gai_result;
+ struct addrinfo hints;
+
+ /*
+ * Unix sockets don't have endpoint addresses, so just flag them as
+ * AF_UNIX
+ */
+ if (pg_strcasecmp(tok, "unix") == 0)
+ {
+ myextra[foreach_current_index(l) * 2 + 1].ss_family = AF_UNIX;
+ continue;
+ }
+
+ netmasktok = strchr(tok, '/');
+ if (netmasktok)
+ {
+ *netmasktok = '\0';
+ netmasktok++;
+ }
+
+ memset((char *) &hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ ret = pg_getaddrinfo_all(tok, NULL, &hints, &gai_result);
+ if (ret != 0 || gai_result == NULL)
+ {
+ GUC_check_errdetail("Invalid IP address %s", tok);
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+
+ memcpy((char *) &myextra[foreach_current_index(l) * 2 + 1], gai_result->ai_addr, gai_result->ai_addrlen);
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+
+ /* A NULL netmasktok means the fully set hostmask */
+ if (pg_sockaddr_cidr_mask(&myextra[foreach_current_index(l) * 2 + 2], netmasktok, myextra[foreach_current_index(l) * 2 + 1].ss_family) != 0)
+ {
+ if (netmasktok)
+ GUC_check_errdetail("Invalid netmask %s", netmasktok);
+ else
+ GUC_check_errdetail("Could not create netmask");
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+ }
+
+ pfree(rawstring);
+ list_free(elemlist);
+ *extra = (void *) myextra;
+
+ return true;
+}
+
+static void
+assign_proxy_servers(const char *newval, void *extra)
+{
+ TrustedProxyServers = (struct sockaddr_storage *) extra;
+}
+
#include "guc-file.c"
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 1cbc9feeb6..3e67fa6b3c 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -62,6 +62,9 @@
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
+#proxy_port = 0 # port to listen to for proxy connections
+ # (change requires restart)
+#proxy_servers = '' # what proxy servers to trust
#max_connections = 100 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
#unix_socket_directories = '/tmp' # comma-separated list of directories
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 02015efe13..e91a4d7607 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -126,9 +126,11 @@ typedef struct Port
{
pgsocket sock; /* File descriptor */
bool noblock; /* is the socket in non-blocking mode? */
+ bool isProxy; /* is the connection using PROXY protocol */
ProtocolVersion proto; /* FE/BE protocol version */
SockAddr laddr; /* local addr (postmaster) */
SockAddr raddr; /* remote addr (client) */
+ SockAddr daddr; /* destination addr (postmaster, or proxy server if proxy protocol used) */
char *remote_host; /* name (or ip addr) of remote host */
char *remote_hostname; /* name (not ip addr) of remote host, if
* available */
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 6c51b2f20f..cdaae030e1 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -42,6 +42,12 @@ typedef struct
extern const PGDLLIMPORT PQcommMethods *PqCommMethods;
+typedef struct
+{
+ pgsocket socket;
+ bool isProxy;
+} PQlistenSocket;
+
#define pq_comm_reset() (PqCommMethods->comm_reset())
#define pq_flush() (PqCommMethods->flush())
#define pq_flush_if_writable() (PqCommMethods->flush_if_writable())
@@ -63,9 +69,9 @@ extern WaitEventSet *FeBeWaitSet;
#define FeBeWaitSetSocketPos 0
#define FeBeWaitSetLatchPos 1
-extern int StreamServerPort(int family, const char *hostName,
- unsigned short portNumber, const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen);
+extern PQlistenSocket *StreamServerPort(int family, const char *hostName,
+ unsigned short portNumber, const char *unixSocketDir,
+ PQlistenSocket PQlistenSocket[], int MaxListen);
extern int StreamConnection(pgsocket server_fd, Port *port);
extern void StreamClose(pgsocket sock);
extern void TouchSocketFiles(void);
@@ -78,6 +84,7 @@ extern bool pq_is_reading_msg(void);
extern int pq_getmessage(StringInfo s, int maxlen);
extern int pq_getbyte(void);
extern int pq_peekbyte(void);
+extern int pq_discardbytes(size_t len);
extern int pq_getbyte_if_available(unsigned char *c);
extern int pq_putmessage_v2(char msgtype, const char *s, size_t len);
extern bool pq_check_connection(void);
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index 0efdd7c232..2a029ef786 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -17,10 +17,13 @@
extern bool EnableSSL;
extern int ReservedBackends;
extern PGDLLIMPORT int PostPortNumber;
+extern PGDLLIMPORT int ProxyPortNumber;
extern int Unix_socket_permissions;
extern char *Unix_socket_group;
extern char *Unix_socket_directories;
extern char *ListenAddresses;
+extern char *TrustedProxyServersString;
+extern struct sockaddr_storage *TrustedProxyServers;
extern bool ClientAuthInProgress;
extern int PreAuthDelay;
extern int AuthenticationTimeout;
diff --git a/src/test/Makefile b/src/test/Makefile
index 46275915ff..4ad030034c 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -12,7 +12,8 @@ subdir = src/test
top_builddir = ../..
include $(top_builddir)/src/Makefile.global
-SUBDIRS = perl regress isolation modules authentication recovery subscription
+SUBDIRS = perl regress isolation modules authentication recovery subscription \
+ protocol
# Test suites that are not safe by default but can be run if selected
# by the user via the whitespace-separated list in variable
diff --git a/src/test/protocol/Makefile b/src/test/protocol/Makefile
new file mode 100644
index 0000000000..bda49d6ecb
--- /dev/null
+++ b/src/test/protocol/Makefile
@@ -0,0 +1,23 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/protocol
+#
+# Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/protocol/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/protocol
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
+
+clean distclean maintainer-clean:
+ rm -rf tmp_check
diff --git a/src/test/protocol/t/001_proxy.pl b/src/test/protocol/t/001_proxy.pl
new file mode 100644
index 0000000000..64619058c8
--- /dev/null
+++ b/src/test/protocol/t/001_proxy.pl
@@ -0,0 +1,151 @@
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Socket qw(AF_INET AF_INET6 inet_pton);
+use IO::Socket;
+
+plan tests => 25;
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+log_connections = on
+});
+$node->append_conf(
+ 'pg_hba.conf', qq{
+host all all 11.22.33.44/32 trust
+host all all 1:2:3:4:5:6:0:9/128 trust
+});
+$node->append_conf('postgresql.conf', "proxy_port = " . ($node->port() + 1));
+
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE USER proxytest;');
+
+sub make_message
+{
+ my ($msg) = @_;
+ return pack("Na*", length($msg) + 4, $msg);
+}
+
+sub read_packet
+{
+ my ($socket) = @_;
+ my $buf = "";
+ $socket->recv($buf, 1024);
+ return $buf;
+}
+
+
+# Test normal connection through localhost
+sub test_connection
+{
+ my ($socket, $proxy, $what, $shouldbe, $shouldfail, $extra) = @_;
+ ok($socket, $what);
+
+ my $startup = make_message(
+ pack("N(Z*Z*)*x", 196608, (user => "proxytest", database => "postgres")));
+
+ $extra = "" if !defined($extra);
+
+ if (defined($proxy))
+ {
+ my $p = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A\x21";
+ if ($proxy =~ ":")
+ {
+ # ipv6
+ $p .= "\x21"; # TCP v6
+ $p .= pack "n", 36 + length($extra); # size
+ $p .= inet_pton(AF_INET6, $proxy);
+ $p .= "\0" x 16; # destination address
+ }
+ else
+ {
+ # ipv4
+ $p .= "\x11"; # TCP v4
+ $p .= pack "n", 12 + length($extra); # size
+ $p .= inet_pton(AF_INET, $proxy);
+ $p .= "\0\0\0\0"; # destination address
+ }
+ $p .= pack "n", 1919; # source port
+ $p .= pack "n", 0;
+ $p .= $extra;
+ print $socket $p;
+ }
+ print $socket $startup;
+
+ my $in = read_packet($socket);
+ if (defined($shouldfail))
+ {
+ isnt(substr($in, 0, 1), 'R', $what);
+ }
+ else
+ {
+ is(substr($in, 0, 1), 'R', $what);
+ }
+
+ SKIP:
+ {
+ skip "The rest of this test should fail", 3 if (defined($shouldfail));
+
+ is(substr($in, 8, 1), "\0", $what);
+
+ my ($resip, $resport) = split /\|/,
+ $node->safe_psql('postgres',
+ "SELECT client_addr, client_port FROM pg_stat_activity WHERE pid != pg_backend_pid() AND backend_type='client backend'"
+ );
+ is($resip, $shouldbe, $what);
+ if ($proxy)
+ {
+ is($resport, "1919", $what);
+ }
+ else
+ {
+ ok($resport, $what);
+ }
+ }
+
+ $socket->close();
+
+ return;
+}
+
+sub make_socket
+{
+ my ($port) = @_;
+ if ($PostgreSQL::Test::Cluster::use_tcp) {
+ return IO::Socket::INET->new(
+ PeerAddr => "127.0.0.1",
+ PeerPort => $port,
+ Proto => "tcp",
+ Type => SOCK_STREAM);
+ }
+ else {
+ return IO::Socket::UNIX->new(
+ Peer => $node->host() . "/.s.PGSQL." . $port,
+ Type => SOCK_STREAM);
+ }
+}
+
+# Test a regular connection first to make sure connecting etc works fine.
+test_connection(make_socket($node->port()),
+ undef, "normal connection", $PostgreSQL::Test::Cluster::use_tcp ? "127.0.0.1": "");
+
+# Make sure we can't make a proxy connection until it's allowed
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44", 1);
+
+# Allow proxy connections and test them
+$node->append_conf('postgresql.conf', "proxy_servers = 'unix, 127.0.0.1/32'");
+$node->restart();
+
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44");
+test_connection(make_socket($node->port() + 1),
+ "1:2:3:4:5:6::9", "proxy ipv6", "1:2:3:4:5:6:0:9");
+
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy with extra", "11.22.33.44", undef, "abcdef"x100);
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-28 13:23 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-03 13:36 ` Re: PROXY protocol support Daniel Gustafsson <daniel@yesql.se>
2021-11-04 11:03 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-11-15 23:03 ` Jacob Champion <pchampion@vmware.com>
2022-02-25 10:41 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 1 reply; 56+ messages in thread
From: Jacob Champion @ 2021-11-15 23:03 UTC (permalink / raw)
To: daniel@yesql.se <daniel@yesql.se>; magnus@hagander.net <magnus@hagander.net>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Thu, 2021-11-04 at 12:03 +0100, Magnus Hagander wrote:
> Thanks for the pointer, PFA a rebase.
I think the Unix socket handling needs the same "success" fix that you
applied to the TCP socket handling above it:
> @@ -1328,9 +1364,23 @@ PostmasterMain(int argc, char *argv[])
> ereport(WARNING,
> (errmsg("could not create Unix-domain socket in directory \"%s\"",
> socketdir)));
> +
> + if (ProxyPortNumber)
> + {
> + socket = StreamServerPort(AF_UNIX, NULL,
> + (unsigned short) ProxyPortNumber,
> + socketdir,
> + ListenSocket, MAXLISTEN);
> + if (socket)
> + socket->isProxy = true;
> + else
> + ereport(WARNING,
> + (errmsg("could not create Unix-domain PROXY socket for \"%s\"",
> + socketdir)));
> + }
> }
>
> - if (!success && elemlist != NIL)
> + if (socket == NULL && elemlist != NIL)
> ereport(FATAL,
> (errmsg("could not create any Unix-domain sockets")));
Other than that, I can find nothing else to improve, and I think this
is ready for more eyes than mine. :)
--
To tie off some loose ends from upthread:
I didn't find any MAXLISTEN documentation either, so I guess it's only
a documentation issue if someone runs into it, heh.
I was not able to find any other cases (besides ident) where using
daddr instead of laddr would break things. I am going a bit snow-blind
on the patch, though, and there's a lot of auth code.
I never did hear back from the PROXY spec maintainer on how strict to
be with LOCAL; another contributor did chime in but only to add that
they didn't know the answer. That conversation is at [1], in case
someone picks it up in the future.
A summary of possible improvements talked about upthread, for a future
v2:
- SQL functions to get the laddr info (scoped to superusers, somehow),
if there's a use case for them
- Setting up PROXY Unix socket permissions separately from the "main"
socket
- Allowing PROXY-only communication (disable the "main" port)
Thanks,
--Jacob
[1] https://www.mail-archive.com/haproxy@formilux.org/msg40899.html
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-28 13:23 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-03 13:36 ` Re: PROXY protocol support Daniel Gustafsson <daniel@yesql.se>
2021-11-04 11:03 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-15 23:03 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
@ 2022-02-25 10:41 ` Magnus Hagander <magnus@hagander.net>
2022-03-09 16:23 ` Re: PROXY protocol support Peter Eisentraut <peter.eisentraut@enterprisedb.com>
0 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2022-02-25 10:41 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; +Cc: daniel@yesql.se <daniel@yesql.se>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Tue, Nov 16, 2021 at 12:03 AM Jacob Champion <pchampion@vmware.com> wrote:
>
> On Thu, 2021-11-04 at 12:03 +0100, Magnus Hagander wrote:
> > Thanks for the pointer, PFA a rebase.
>
> I think the Unix socket handling needs the same "success" fix that you
> applied to the TCP socket handling above it:
>
> > @@ -1328,9 +1364,23 @@ PostmasterMain(int argc, char *argv[])
> > ereport(WARNING,
> > (errmsg("could not create Unix-domain socket in directory \"%s\"",
> > socketdir)));
> > +
> > + if (ProxyPortNumber)
> > + {
> > + socket = StreamServerPort(AF_UNIX, NULL,
> > + (unsigned short) ProxyPortNumber,
> > + socketdir,
> > + ListenSocket, MAXLISTEN);
> > + if (socket)
> > + socket->isProxy = true;
> > + else
> > + ereport(WARNING,
> > + (errmsg("could not create Unix-domain PROXY socket for \"%s\"",
> > + socketdir)));
> > + }
> > }
> >
> > - if (!success && elemlist != NIL)
> > + if (socket == NULL && elemlist != NIL)
> > ereport(FATAL,
> > (errmsg("could not create any Unix-domain sockets")));
>
> Other than that, I can find nothing else to improve, and I think this
> is ready for more eyes than mine. :)
Here's another rebase on top of the AF_UNIX patch.
> To tie off some loose ends from upthread:
>
> I didn't find any MAXLISTEN documentation either, so I guess it's only
> a documentation issue if someone runs into it, heh.
>
> I was not able to find any other cases (besides ident) where using
> daddr instead of laddr would break things. I am going a bit snow-blind
> on the patch, though, and there's a lot of auth code.
Yeah, that's definitely a good reason for more eyes on it.
> A summary of possible improvements talked about upthread, for a future
> v2:
>
> - SQL functions to get the laddr info (scoped to superusers, somehow),
> if there's a use case for them
>
> - Setting up PROXY Unix socket permissions separately from the "main"
> socket
>
> - Allowing PROXY-only communication (disable the "main" port)
These all seem useful, but I'm liking the idea of putting them in a
v2, to avoid expanding the scope too much.
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
Attachments:
[text/x-patch] proxy_protocol_11.patch (40.9K, ../../CABUevEzoaXRaOXocOUmXVENZ4N6fR+ag=Ur574r6QDHGgSq8YQ@mail.gmail.com/2-proxy_protocol_11.patch)
download | inline diff:
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 02f0489112..a3ff09b3ac 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -353,6 +353,15 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl
the client's host name instead of the IP address in the log.
</para>
+ <para>
+ If <xref linkend="guc-proxy-port"/> is enabled and the
+ connection is made through a proxy server using the PROXY
+ protocol, the actual IP address of the client will be used
+ for matching. If a connection is made through a proxy server
+ not using the PROXY protocol, the IP address of the
+ proxy server will be used.
+ </para>
+
<para>
These fields do not apply to <literal>local</literal> records.
</para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 7ed8c82a9d..e0847b6347 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -682,6 +682,56 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-proxy-port" xreflabel="proxy_port">
+ <term><varname>proxy_port</varname> (<type>integer</type>)
+ <indexterm>
+ <primary><varname>proxy_port</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ The TCP port the server listens on for PROXY connections, disabled by
+ default. If set to a number, <productname>PostgreSQL</productname>
+ will listen on this port on the same addresses as for regular
+ connections, but expect all connections to use the PROXY protocol to
+ identify the client. This parameter can only be set at server start.
+ </para>
+ <para>
+ If a proxy connection is made over this port, and the proxy is listed
+ in <xref linkend="guc-proxy-servers" />, the actual client address
+ will be considered as the address of the client, instead of listing
+ all connections as coming from the proxy server.
+ </para>
+ <para>
+ The <ulink url="http://www.haproxy.org/download/1.9/doc/proxy-protocol.txt">PROXY
+ protocol</ulink> is maintained by <productname>HAProxy</productname>,
+ and supported in many proxies and load
+ balancers. <productname>PostgreSQL</productname> supports version 2
+ of the protocol.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-proxy-servers" xreflabel="proxy_servers">
+ <term><varname>proxy_servers</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>proxy_servers</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ A comma separated list of one or more ip addresses, cidr specifications or the
+ literal <literal>unix</literal>, indicating which proxy servers to trust when
+ connecting on the port specified in <xref linkend="guc-proxy-port" />.
+ </para>
+ <para>
+ If a proxy connection is made from an IP address not covered by this
+ list, the connection will be rejected. By default no proxy is trusted
+ and all proxy connections will be rejected.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-max-connections" xreflabel="max_connections">
<term><varname>max_connections</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index df3cd5987b..10dce1beca 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -22323,7 +22323,12 @@ SELECT * FROM pg_ls_dir('.') WITH ORDINALITY AS t(ls,n);
connection,
or <literal>NULL</literal> if the current connection is via a
Unix-domain socket.
- </para></entry>
+ </para>
+ <para>
+ If the connection is a PROXY connection, this function returns the
+ IP address used to connect to the proxy server.
+ </para>
+ </entry>
</row>
<row>
@@ -22339,7 +22344,13 @@ SELECT * FROM pg_ls_dir('.') WITH ORDINALITY AS t(ls,n);
connection,
or <literal>NULL</literal> if the current connection is via a
Unix-domain socket.
- </para></entry>
+ </para>
+ <para>
+ If the connection is a PROXY connection, this function returns the
+ port used to connect to the proxy server.
+ </para>
+
+ </entry>
</row>
<row>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index efc53f3135..cdc20455fe 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -1698,6 +1698,14 @@ ident_inet(hbaPort *port)
*la = NULL,
hints;
+ if (port->isProxy)
+ {
+ ereport(LOG,
+ (errcode_for_socket_access(),
+ errmsg("Ident authentication cannot be used over PROXY connections")));
+ return STATUS_ERROR;
+ }
+
/*
* Might look a little weird to first convert it to text and then back to
* sockaddr, but it's protocol independent.
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 47923b9e9d..abe5b4c5ea 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -326,13 +326,13 @@ socket_close(int code, Datum arg)
* Successfully opened sockets are added to the ListenSocket[] array (of
* length MaxListen), at the first position that isn't PGINVALID_SOCKET.
*
- * RETURNS: STATUS_OK or STATUS_ERROR
+ * RETURNS: The PQlistenSocket listening on, or NULL in case of error
*/
-int
+PQlistenSocket *
StreamServerPort(int family, const char *hostName, unsigned short portNumber,
const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen)
+ PQlistenSocket ListenSocket[], int MaxListen)
{
pgsocket fd;
int err;
@@ -378,10 +378,10 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("Unix-domain socket path \"%s\" is too long (maximum %d bytes)",
unixSocketPath,
(int) (UNIXSOCK_PATH_BUFLEN - 1))));
- return STATUS_ERROR;
+ return NULL;
}
if (Lock_AF_UNIX(unixSocketDir, unixSocketPath) != STATUS_OK)
- return STATUS_ERROR;
+ return NULL;
service = unixSocketPath;
}
else
@@ -404,7 +404,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
service, gai_strerror(ret))));
if (addrs)
pg_freeaddrinfo_all(hint.ai_family, addrs);
- return STATUS_ERROR;
+ return NULL;
}
for (addr = addrs; addr; addr = addr->ai_next)
@@ -421,7 +421,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
/* See if there is still room to add 1 more socket. */
for (; listen_index < MaxListen; listen_index++)
{
- if (ListenSocket[listen_index] == PGINVALID_SOCKET)
+ if (ListenSocket[listen_index].socket == PGINVALID_SOCKET)
break;
}
if (listen_index >= MaxListen)
@@ -600,16 +600,16 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("listening on %s address \"%s\", port %d",
familyDesc, addrDesc, (int) portNumber)));
- ListenSocket[listen_index] = fd;
+ ListenSocket[listen_index].socket = fd;
added++;
}
pg_freeaddrinfo_all(hint.ai_family, addrs);
if (!added)
- return STATUS_ERROR;
+ return NULL;
- return STATUS_OK;
+ return &ListenSocket[listen_index];
}
@@ -763,6 +763,9 @@ StreamConnection(pgsocket server_fd, Port *port)
return STATUS_ERROR;
}
+ /* copy over to daddr to make sure it's set for the non-proxy case */
+ memcpy(&port->daddr, &port->laddr, sizeof(port->laddr));
+
/* select NODELAY and KEEPALIVE options if it's a TCP connection */
if (port->laddr.addr.ss_family != AF_UNIX)
{
@@ -1134,7 +1137,7 @@ pq_getbytes(char *s, size_t len)
* returns 0 if OK, EOF if trouble
* --------------------------------
*/
-static int
+int
pq_discardbytes(size_t len)
{
size_t amount;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 80bb269599..caad6d61a0 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -103,6 +103,7 @@
#include "common/string.h"
#include "lib/ilist.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
@@ -198,15 +199,22 @@ BackgroundWorker *MyBgworkerEntry = NULL;
-/* The socket number we are listening for connections on */
+/* The TCP port number we are listening for connections on */
int PostPortNumber;
+/* The TCP port number we are listening for proxy connections on */
+int ProxyPortNumber;
+
/* The directory names for Unix socket(s) */
char *Unix_socket_directories;
/* The TCP listen address(es) */
char *ListenAddresses;
+/* Trusted proxy servers */
+char *TrustedProxyServersString = NULL;
+struct sockaddr_storage *TrustedProxyServers = NULL;
+
/*
* ReservedBackends is the number of backends reserved for superuser use.
* This number is taken out of the pool size given by MaxConnections so
@@ -220,7 +228,7 @@ int ReservedBackends;
/* The socket(s) we're listening to. */
#define MAXLISTEN 64
-static pgsocket ListenSocket[MAXLISTEN];
+static PQlistenSocket ListenSocket[MAXLISTEN];
/*
* These globals control the behavior of the postmaster in case some
@@ -588,6 +596,7 @@ PostmasterMain(int argc, char *argv[])
bool listen_addr_saved = false;
int i;
char *output_config_variable = NULL;
+ PQlistenSocket *socket = NULL;
InitProcessGlobals();
@@ -1185,7 +1194,10 @@ PostmasterMain(int argc, char *argv[])
* charged with closing the sockets again at postmaster shutdown.
*/
for (i = 0; i < MAXLISTEN; i++)
- ListenSocket[i] = PGINVALID_SOCKET;
+ {
+ ListenSocket[i].socket = PGINVALID_SOCKET;
+ ListenSocket[i].isProxy = false;
+ }
on_proc_exit(CloseServerPorts, 0);
@@ -1214,17 +1226,17 @@ PostmasterMain(int argc, char *argv[])
char *curhost = (char *) lfirst(l);
if (strcmp(curhost, "*") == 0)
- status = StreamServerPort(AF_UNSPEC, NULL,
+ socket = StreamServerPort(AF_UNSPEC, NULL,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
else
- status = StreamServerPort(AF_UNSPEC, curhost,
+ socket = StreamServerPort(AF_UNSPEC, curhost,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful host addr in lockfile */
@@ -1238,6 +1250,30 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create listen socket for \"%s\"",
curhost)));
+
+ /* Also listen to the PROXY port on this address, if configured */
+ if (ProxyPortNumber)
+ {
+ if (strcmp(curhost, "*") == 0)
+ socket = StreamServerPort(AF_UNSPEC, NULL,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ else
+ socket = StreamServerPort(AF_UNSPEC, curhost,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ {
+ success++;
+ socket->isProxy = true;
+ }
+ else
+ ereport(WARNING,
+ (errmsg("could not create PROXY listen socket for \"%s\"",
+ curhost)));
+ }
}
if (!success && elemlist != NIL)
@@ -1250,7 +1286,7 @@ PostmasterMain(int argc, char *argv[])
#ifdef USE_BONJOUR
/* Register for Bonjour only if we opened TCP socket(s) */
- if (enable_bonjour && ListenSocket[0] != PGINVALID_SOCKET)
+ if (enable_bonjour && ListenSocket[0].socket != PGINVALID_SOCKET)
{
DNSServiceErrorType err;
@@ -1312,12 +1348,12 @@ PostmasterMain(int argc, char *argv[])
{
char *socketdir = (char *) lfirst(l);
- status = StreamServerPort(AF_UNIX, NULL,
+ socket = StreamServerPort(AF_UNIX, NULL,
(unsigned short) PostPortNumber,
socketdir,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful Unix socket in lockfile */
@@ -1328,9 +1364,23 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create Unix-domain socket in directory \"%s\"",
socketdir)));
+
+ if (ProxyPortNumber)
+ {
+ socket = StreamServerPort(AF_UNIX, NULL,
+ (unsigned short) ProxyPortNumber,
+ socketdir,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ socket->isProxy = true;
+ else
+ ereport(WARNING,
+ (errmsg("could not create Unix-domain PROXY socket for \"%s\"",
+ socketdir)));
+ }
}
- if (!success && elemlist != NIL)
+ if (socket == NULL && elemlist != NIL)
ereport(FATAL,
(errmsg("could not create any Unix-domain sockets")));
@@ -1342,7 +1392,7 @@ PostmasterMain(int argc, char *argv[])
/*
* check that we have some socket to listen on
*/
- if (ListenSocket[0] == PGINVALID_SOCKET)
+ if (ListenSocket[0].socket == PGINVALID_SOCKET)
ereport(FATAL,
(errmsg("no socket created for listening")));
@@ -1497,10 +1547,10 @@ CloseServerPorts(int status, Datum arg)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -1789,15 +1839,17 @@ ServerLoop(void)
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] == PGINVALID_SOCKET)
+ if (ListenSocket[i].socket == PGINVALID_SOCKET)
break;
- if (FD_ISSET(ListenSocket[i], &rmask))
+ if (FD_ISSET(ListenSocket[i].socket, &rmask))
{
Port *port;
- port = ConnCreate(ListenSocket[i]);
+ port = ConnCreate(ListenSocket[i].socket);
if (port)
{
+ port->isProxy = ListenSocket[i].isProxy;
+
BackendStartup(port);
/*
@@ -1965,7 +2017,7 @@ initMasks(fd_set *rmask)
for (i = 0; i < MAXLISTEN; i++)
{
- int fd = ListenSocket[i];
+ int fd = ListenSocket[i].socket;
if (fd == PGINVALID_SOCKET)
break;
@@ -1978,6 +2030,284 @@ initMasks(fd_set *rmask)
return maxsock + 1;
}
+static int
+UnwrapProxyConnection(Port *port)
+{
+ char proxyver;
+ uint16 proxyaddrlen;
+ SockAddr raddr_save;
+ SockAddr laddr_save;
+ int i;
+ bool useproxy = false;
+
+ /*
+ * These structs are from the PROXY protocol docs at
+ * http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
+ */
+ union
+ {
+ struct
+ { /* for TCP/UDP over IPv4, len = 12 */
+ uint32 src_addr;
+ uint32 dst_addr;
+ uint16 src_port;
+ uint16 dst_port;
+ } ip4;
+ struct
+ { /* for TCP/UDP over IPv6, len = 36 */
+ uint8 src_addr[16];
+ uint8 dst_addr[16];
+ uint16 src_port;
+ uint16 dst_port;
+ } ip6;
+ } proxyaddr;
+ struct
+ {
+ uint8 sig[12]; /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */
+ uint8 ver_cmd; /* protocol version and command */
+ uint8 fam; /* protocol family and address */
+ uint16 len; /* number of following bytes part of the
+ * header */
+ } proxyheader;
+
+ /*
+ * Assert the size of the structs that are part of the protocol,
+ * to defend against strange compilers.
+ */
+ StaticAssertStmt(sizeof(proxyheader) == 16, "proxy header struct has invalid size");
+ StaticAssertStmt(sizeof(proxyaddr.ip4) == 12, "proxy address ipv4 struct has invalid size");
+ StaticAssertStmt(sizeof(proxyaddr.ip6) == 36, "proxy address ipv6 struct has invalid size");
+
+
+ /* Else if it's on our list of trusted proxies */
+ if (TrustedProxyServers)
+ {
+ for (i = 0; i < *((int *) TrustedProxyServers) * 2; i += 2)
+ {
+ if (port->raddr.addr.ss_family == TrustedProxyServers[i + 1].ss_family)
+ {
+ /*
+ * Connection over unix sockets don't give us the source, so
+ * just check if they're allowed at all. For IP connections,
+ * verify that it's an allowed address.
+ */
+ if (port->raddr.addr.ss_family == AF_UNIX ||
+ pg_range_sockaddr(&port->raddr.addr,
+ &TrustedProxyServers[i + 1],
+ &TrustedProxyServers[i + 2]))
+ {
+ useproxy = true;
+ break;
+ }
+ }
+ }
+ }
+ if (!useproxy)
+ {
+ /*
+ * Connection is not from one of our trusted proxies, so reject it.
+ */
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("connection from unauthorized proxy server")));
+ return STATUS_ERROR;
+ }
+
+ /* Store a copy of the original address, for logging */
+ memcpy(&raddr_save, &port->raddr, sizeof(SockAddr));
+ memcpy(&laddr_save, &port->laddr, sizeof(SockAddr));
+
+ pq_startmsgread();
+
+ /*
+ * PROXY requests always start with:
+ * \x0D \x0A \x0D \x0A \x00 \x0D \x0A \x51 \x55 \x49 \x54 \x0A
+ */
+
+ if (pq_getbytes((char *) &proxyheader, sizeof(proxyheader)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ if (memcmp(proxyheader.sig, "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a", sizeof(proxyheader.sig)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Proxy version is in the high 4 bits of the first byte */
+ proxyver = (proxyheader.ver_cmd & 0xF0) >> 4;
+ if (proxyver != 2)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol version: %x", proxyver)));
+ return STATUS_ERROR;
+ }
+
+ /*
+ * Proxy command is in the low 4 bits of the first byte.
+ * 0x00 = local, 0x01 = proxy, all others should be rejected
+ */
+ if ((proxyheader.ver_cmd & 0x0F) == 0x00)
+ {
+ if (proxyheader.fam != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol family %x for local connection", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+ }
+ else if ((proxyheader.ver_cmd & 0x0F) == 0x01)
+ {
+ if (proxyheader.fam == 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol family 0 for non-local connection")));
+ return STATUS_ERROR;
+ }
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol command: %x", (proxyheader.ver_cmd & 0x0f))));
+ return STATUS_ERROR;
+ }
+
+ proxyaddrlen = pg_ntoh16(proxyheader.len);
+
+ if (pq_getbytes((char *) &proxyaddr, proxyaddrlen > sizeof(proxyaddr) ? sizeof(proxyaddr) : proxyaddrlen) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Connection family */
+ if (proxyheader.fam == 0)
+ {
+ /*
+ * UNSPEC connection over LOCAL (verified above).
+ * in this case we just ignore the address included.
+ */
+ }
+ else if (proxyheader.fam == 0x11)
+ {
+ /* TCPv4 */
+ if (proxyaddrlen < 12)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET;
+ port->raddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
+
+ port->daddr.addr.ss_family = AF_INET;
+ port->daddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->daddr.addr)->sin_addr.s_addr = proxyaddr.ip4.dst_addr;
+ ((struct sockaddr_in *) &port->daddr.addr)->sin_port = proxyaddr.ip4.dst_port;
+ }
+ else if (proxyheader.fam == 0x21)
+ {
+ /* TCPv6 */
+ if (proxyaddrlen < 36)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET6;
+ port->raddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->raddr.addr)->sin6_addr, proxyaddr.ip6.src_addr, 16);
+ ((struct sockaddr_in6 *) &port->raddr.addr)->sin6_port = proxyaddr.ip6.src_port;
+
+
+ port->daddr.addr.ss_family = AF_INET6;
+ port->daddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->daddr.addr)->sin6_addr, proxyaddr.ip6.dst_addr, 16);
+ ((struct sockaddr_in6 *) &port->daddr.addr)->sin6_port = proxyaddr.ip6.dst_port;
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol connection type: %x", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+
+ /* If there is any more header data present, skip past it */
+ if (proxyaddrlen > sizeof(proxyaddr))
+ {
+ if (pq_discardbytes(proxyaddrlen - sizeof(proxyaddr)) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ }
+
+ pq_endmsgread();
+
+ /*
+ * Log what we've done if connection logging is enabled. We log the proxy
+ * connection here, and let the normal connection logging mechanism log
+ * the unwrapped connection.
+ */
+ if (Log_connections)
+ {
+ char remote_host[NI_MAXHOST];
+ char remote_port[NI_MAXSERV];
+ char proxy_host[NI_MAXHOST];
+ char proxy_port[NI_MAXSERV];
+ int ret;
+
+ remote_host[0] = '\0';
+ remote_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&raddr_save.addr, raddr_save.salen,
+ remote_host, sizeof(remote_host),
+ remote_port, sizeof(remote_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ proxy_host[0] = '\0';
+ proxy_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&laddr_save.addr, laddr_save.salen,
+ proxy_host, sizeof(proxy_host),
+ proxy_port, sizeof(proxy_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+
+ ereport(LOG,
+ (errmsg("proxy connection from: host=%s port=%s (proxy host=%s port=%s)",
+ remote_host,
+ remote_port,
+ proxy_host,
+ proxy_port)));
+
+ }
+
+ return STATUS_OK;
+}
/*
* Read a client's startup packet and do something according to it.
@@ -2659,10 +2989,10 @@ ClosePostmasterPorts(bool am_syslogger)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -4455,6 +4785,31 @@ BackendInitialize(Port *port)
InitializeTimeouts(); /* establishes SIGALRM handler */
PG_SETMASK(&StartupBlockSig);
+ /*
+ * Ready to begin client interaction. We will give up and _exit(1) after
+ * a time delay, so that a broken client can't hog a connection
+ * indefinitely. PreAuthDelay and any DNS interactions above don't count
+ * against the time limit.
+ *
+ * If this is a proxy connection, we apply the timeout once while waiting
+ * for the proxy header. It is then reapplied further down when we process
+ * the startup packet, which means it can apply multiple times.
+ *
+ * For the time being we re-use AuthenticationTimeout for this, but it may
+ * be considered for a separate tunable in the future.
+ */
+ RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
+
+ /* Check if this is a proxy connection and if so unwrap the proxying */
+ if (port->isProxy)
+ {
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
+ if (UnwrapProxyConnection(port) != STATUS_OK)
+ proc_exit(0);
+ disable_timeout(STARTUP_PACKET_TIMEOUT, false);
+ }
+
+
/*
* Get the remote host name and port for logging and status display.
*/
@@ -4507,27 +4862,20 @@ BackendInitialize(Port *port)
port->remote_hostname = strdup(remote_host);
/*
- * Ready to begin client interaction. We will give up and _exit(1) after
- * a time delay, so that a broken client can't hog a connection
- * indefinitely. PreAuthDelay and any DNS interactions above don't count
- * against the time limit.
+ * Receive the startup packet (which might turn out to be a cancel request
+ * packet).
*
* Note: AuthenticationTimeout is applied here while waiting for the
* startup packet, and then again in InitPostgres for the duration of any
* authentication operations. So a hostile client could tie up the
- * process for nearly twice AuthenticationTimeout before we kick him off.
+ * process for nearly twice (or three times in the case of a proxy connection)
+ * AuthenticationTimeout before we kick him off.
*
* Note: because PostgresMain will call InitializeTimeouts again, the
* registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
* since we never use it again after this function.
*/
- RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
-
- /*
- * Receive the startup packet (which might turn out to be a cancel request
- * packet).
- */
status = ProcessStartupPacket(port, false, false);
/*
diff --git a/src/backend/utils/adt/network.c b/src/backend/utils/adt/network.c
index 0ab54316f8..9198a29f51 100644
--- a/src/backend/utils/adt/network.c
+++ b/src/backend/utils/adt/network.c
@@ -1802,6 +1802,8 @@ inet_client_port(PG_FUNCTION_ARGS)
/*
* IP address that the server accepted the connection on (NULL if Unix socket)
+ * If the connection is a PROXY connection, then this returns the IP address/port of
+ * the proxy server, and not the local connection!
*/
Datum
inet_server_addr(PG_FUNCTION_ARGS)
@@ -1813,7 +1815,7 @@ inet_server_addr(PG_FUNCTION_ARGS)
if (port == NULL)
PG_RETURN_NULL();
- switch (port->laddr.addr.ss_family)
+ switch (port->daddr.addr.ss_family)
{
case AF_INET:
#ifdef HAVE_IPV6
@@ -1826,14 +1828,14 @@ inet_server_addr(PG_FUNCTION_ARGS)
local_host[0] = '\0';
- ret = pg_getnameinfo_all(&port->laddr.addr, port->laddr.salen,
+ ret = pg_getnameinfo_all(&port->daddr.addr, port->daddr.salen,
local_host, sizeof(local_host),
NULL, 0,
NI_NUMERICHOST | NI_NUMERICSERV);
if (ret != 0)
PG_RETURN_NULL();
- clean_ipv6_addr(port->laddr.addr.ss_family, local_host);
+ clean_ipv6_addr(port->daddr.addr.ss_family, local_host);
PG_RETURN_INET_P(network_in(local_host, false));
}
@@ -1841,6 +1843,8 @@ inet_server_addr(PG_FUNCTION_ARGS)
/*
* port that the server accepted the connection on (NULL if Unix socket)
+ * If the connection is a PROXY connection, then this returns the IP address/port of
+ * the proxy server, and not the local connection!
*/
Datum
inet_server_port(PG_FUNCTION_ARGS)
@@ -1852,7 +1856,7 @@ inet_server_port(PG_FUNCTION_ARGS)
if (port == NULL)
PG_RETURN_NULL();
- switch (port->laddr.addr.ss_family)
+ switch (port->daddr.addr.ss_family)
{
case AF_INET:
#ifdef HAVE_IPV6
@@ -1865,7 +1869,7 @@ inet_server_port(PG_FUNCTION_ARGS)
local_port[0] = '\0';
- ret = pg_getnameinfo_all(&port->laddr.addr, port->laddr.salen,
+ ret = pg_getnameinfo_all(&port->daddr.addr, port->daddr.salen,
NULL, 0,
local_port, sizeof(local_port),
NI_NUMERICHOST | NI_NUMERICSERV);
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 1e3650184b..c21c39db24 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -52,10 +52,12 @@
#include "commands/user.h"
#include "commands/vacuum.h"
#include "commands/variable.h"
+#include "common/ip.h"
#include "common/string.h"
#include "funcapi.h"
#include "jit/jit.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
@@ -239,6 +241,8 @@ static bool check_recovery_target_lsn(char **newval, void **extra, GucSource sou
static void assign_recovery_target_lsn(const char *newval, void *extra);
static bool check_primary_slot_name(char **newval, void **extra, GucSource source);
static bool check_default_with_oids(bool *newval, void **extra, GucSource source);
+static bool check_proxy_servers(char **newval, void **extra, GucSource source);
+static void assign_proxy_servers(const char *newval, void *extra);
/* Private functions in guc-file.l that need to be called from guc.c */
static ConfigVariable *ProcessConfigFileInternal(GucContext context,
@@ -2401,6 +2405,16 @@ static struct config_int ConfigureNamesInt[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_port", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the TCP port the server listens for PROXY connections on."),
+ NULL
+ },
+ &ProxyPortNumber,
+ 0, 0, 65535,
+ NULL, NULL, NULL
+ },
+
{
{"unix_socket_permissions", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
gettext_noop("Sets the access permissions of the Unix-domain socket."),
@@ -4399,6 +4413,17 @@ static struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_servers", PGC_SIGHUP, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the addresses for trusted proxy servers."),
+ NULL,
+ GUC_LIST_INPUT
+ },
+ &TrustedProxyServersString,
+ "",
+ check_proxy_servers, assign_proxy_servers, NULL
+ },
+
{
/*
* Can't be set by ALTER SYSTEM as it can lead to recursive definition
@@ -12680,4 +12705,118 @@ check_default_with_oids(bool *newval, void **extra, GucSource source)
return true;
}
+static bool
+check_proxy_servers(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ struct sockaddr_storage *myextra;
+
+ /* Special case when it's empty */
+ if (**newval == '\0')
+ {
+ *extra = NULL;
+ return true;
+ }
+
+ /* Need a modifiable copy of string */
+ rawstring = pstrdup(*newval);
+
+ /* Parse string into list of identifiers */
+ if (!SplitIdentifierString(rawstring, ',', &elemlist))
+ {
+ /* syntax error in list */
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ if (list_length(elemlist) == 0)
+ {
+ /* If it had only whitespace */
+ pfree(rawstring);
+ list_free(elemlist);
+
+ *extra = NULL;
+ return true;
+ }
+
+ /*
+ * We store the result in an array of sockaddr_storage. The first entry is
+ * just an overloaded int which holds the size of the array.
+ */
+ myextra = (struct sockaddr_storage *) guc_malloc(ERROR, sizeof(struct sockaddr_storage) * (list_length(elemlist) * 2 + 1));
+ *((int *) &myextra[0]) = list_length(elemlist);
+
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+ char *netmasktok = NULL;
+ int ret;
+ struct addrinfo *gai_result;
+ struct addrinfo hints;
+
+ /*
+ * Unix sockets don't have endpoint addresses, so just flag them as
+ * AF_UNIX
+ */
+ if (pg_strcasecmp(tok, "unix") == 0)
+ {
+ myextra[foreach_current_index(l) * 2 + 1].ss_family = AF_UNIX;
+ continue;
+ }
+
+ netmasktok = strchr(tok, '/');
+ if (netmasktok)
+ {
+ *netmasktok = '\0';
+ netmasktok++;
+ }
+
+ memset((char *) &hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ ret = pg_getaddrinfo_all(tok, NULL, &hints, &gai_result);
+ if (ret != 0 || gai_result == NULL)
+ {
+ GUC_check_errdetail("Invalid IP address %s", tok);
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+
+ memcpy((char *) &myextra[foreach_current_index(l) * 2 + 1], gai_result->ai_addr, gai_result->ai_addrlen);
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+
+ /* A NULL netmasktok means the fully set hostmask */
+ if (pg_sockaddr_cidr_mask(&myextra[foreach_current_index(l) * 2 + 2], netmasktok, myextra[foreach_current_index(l) * 2 + 1].ss_family) != 0)
+ {
+ if (netmasktok)
+ GUC_check_errdetail("Invalid netmask %s", netmasktok);
+ else
+ GUC_check_errdetail("Could not create netmask");
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+ }
+
+ pfree(rawstring);
+ list_free(elemlist);
+ *extra = (void *) myextra;
+
+ return true;
+}
+
+static void
+assign_proxy_servers(const char *newval, void *extra)
+{
+ TrustedProxyServers = (struct sockaddr_storage *) extra;
+}
+
#include "guc-file.c"
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 4a094bb38b..079ab91e9b 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -62,6 +62,9 @@
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
+#proxy_port = 0 # port to listen to for proxy connections
+ # (change requires restart)
+#proxy_servers = '' # what proxy servers to trust
#max_connections = 100 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
#unix_socket_directories = '/tmp' # comma-separated list of directories
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index dd3e5efba3..9188e78a88 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -126,9 +126,11 @@ typedef struct Port
{
pgsocket sock; /* File descriptor */
bool noblock; /* is the socket in non-blocking mode? */
+ bool isProxy; /* is the connection using PROXY protocol */
ProtocolVersion proto; /* FE/BE protocol version */
SockAddr laddr; /* local addr (postmaster) */
SockAddr raddr; /* remote addr (client) */
+ SockAddr daddr; /* destination addr (postmaster, or proxy server if proxy protocol used) */
char *remote_host; /* name (or ip addr) of remote host */
char *remote_hostname; /* name (not ip addr) of remote host, if
* available */
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index d348a55812..16dfb47cd6 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -42,6 +42,12 @@ typedef struct
extern const PGDLLIMPORT PQcommMethods *PqCommMethods;
+typedef struct
+{
+ pgsocket socket;
+ bool isProxy;
+} PQlistenSocket;
+
#define pq_comm_reset() (PqCommMethods->comm_reset())
#define pq_flush() (PqCommMethods->flush())
#define pq_flush_if_writable() (PqCommMethods->flush_if_writable())
@@ -64,9 +70,9 @@ extern WaitEventSet *FeBeWaitSet;
#define FeBeWaitSetLatchPos 1
#define FeBeWaitSetNEvents 3
-extern int StreamServerPort(int family, const char *hostName,
- unsigned short portNumber, const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen);
+extern PQlistenSocket *StreamServerPort(int family, const char *hostName,
+ unsigned short portNumber, const char *unixSocketDir,
+ PQlistenSocket PQlistenSocket[], int MaxListen);
extern int StreamConnection(pgsocket server_fd, Port *port);
extern void StreamClose(pgsocket sock);
extern void TouchSocketFiles(void);
@@ -79,6 +85,7 @@ extern bool pq_is_reading_msg(void);
extern int pq_getmessage(StringInfo s, int maxlen);
extern int pq_getbyte(void);
extern int pq_peekbyte(void);
+extern int pq_discardbytes(size_t len);
extern int pq_getbyte_if_available(unsigned char *c);
extern bool pq_buffer_has_data(void);
extern int pq_putmessage_v2(char msgtype, const char *s, size_t len);
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index 324a30ec1a..bcde67dd00 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -17,10 +17,13 @@
extern bool EnableSSL;
extern int ReservedBackends;
extern PGDLLIMPORT int PostPortNumber;
+extern PGDLLIMPORT int ProxyPortNumber;
extern int Unix_socket_permissions;
extern char *Unix_socket_group;
extern char *Unix_socket_directories;
extern char *ListenAddresses;
+extern char *TrustedProxyServersString;
+extern struct sockaddr_storage *TrustedProxyServers;
extern bool ClientAuthInProgress;
extern int PreAuthDelay;
extern int AuthenticationTimeout;
diff --git a/src/test/Makefile b/src/test/Makefile
index 46275915ff..4ad030034c 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -12,7 +12,8 @@ subdir = src/test
top_builddir = ../..
include $(top_builddir)/src/Makefile.global
-SUBDIRS = perl regress isolation modules authentication recovery subscription
+SUBDIRS = perl regress isolation modules authentication recovery subscription \
+ protocol
# Test suites that are not safe by default but can be run if selected
# by the user via the whitespace-separated list in variable
diff --git a/src/test/protocol/Makefile b/src/test/protocol/Makefile
new file mode 100644
index 0000000000..bda49d6ecb
--- /dev/null
+++ b/src/test/protocol/Makefile
@@ -0,0 +1,23 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/protocol
+#
+# Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/protocol/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/protocol
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
+
+clean distclean maintainer-clean:
+ rm -rf tmp_check
diff --git a/src/test/protocol/t/001_proxy.pl b/src/test/protocol/t/001_proxy.pl
new file mode 100644
index 0000000000..64619058c8
--- /dev/null
+++ b/src/test/protocol/t/001_proxy.pl
@@ -0,0 +1,151 @@
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Socket qw(AF_INET AF_INET6 inet_pton);
+use IO::Socket;
+
+plan tests => 25;
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+log_connections = on
+});
+$node->append_conf(
+ 'pg_hba.conf', qq{
+host all all 11.22.33.44/32 trust
+host all all 1:2:3:4:5:6:0:9/128 trust
+});
+$node->append_conf('postgresql.conf', "proxy_port = " . ($node->port() + 1));
+
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE USER proxytest;');
+
+sub make_message
+{
+ my ($msg) = @_;
+ return pack("Na*", length($msg) + 4, $msg);
+}
+
+sub read_packet
+{
+ my ($socket) = @_;
+ my $buf = "";
+ $socket->recv($buf, 1024);
+ return $buf;
+}
+
+
+# Test normal connection through localhost
+sub test_connection
+{
+ my ($socket, $proxy, $what, $shouldbe, $shouldfail, $extra) = @_;
+ ok($socket, $what);
+
+ my $startup = make_message(
+ pack("N(Z*Z*)*x", 196608, (user => "proxytest", database => "postgres")));
+
+ $extra = "" if !defined($extra);
+
+ if (defined($proxy))
+ {
+ my $p = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A\x21";
+ if ($proxy =~ ":")
+ {
+ # ipv6
+ $p .= "\x21"; # TCP v6
+ $p .= pack "n", 36 + length($extra); # size
+ $p .= inet_pton(AF_INET6, $proxy);
+ $p .= "\0" x 16; # destination address
+ }
+ else
+ {
+ # ipv4
+ $p .= "\x11"; # TCP v4
+ $p .= pack "n", 12 + length($extra); # size
+ $p .= inet_pton(AF_INET, $proxy);
+ $p .= "\0\0\0\0"; # destination address
+ }
+ $p .= pack "n", 1919; # source port
+ $p .= pack "n", 0;
+ $p .= $extra;
+ print $socket $p;
+ }
+ print $socket $startup;
+
+ my $in = read_packet($socket);
+ if (defined($shouldfail))
+ {
+ isnt(substr($in, 0, 1), 'R', $what);
+ }
+ else
+ {
+ is(substr($in, 0, 1), 'R', $what);
+ }
+
+ SKIP:
+ {
+ skip "The rest of this test should fail", 3 if (defined($shouldfail));
+
+ is(substr($in, 8, 1), "\0", $what);
+
+ my ($resip, $resport) = split /\|/,
+ $node->safe_psql('postgres',
+ "SELECT client_addr, client_port FROM pg_stat_activity WHERE pid != pg_backend_pid() AND backend_type='client backend'"
+ );
+ is($resip, $shouldbe, $what);
+ if ($proxy)
+ {
+ is($resport, "1919", $what);
+ }
+ else
+ {
+ ok($resport, $what);
+ }
+ }
+
+ $socket->close();
+
+ return;
+}
+
+sub make_socket
+{
+ my ($port) = @_;
+ if ($PostgreSQL::Test::Cluster::use_tcp) {
+ return IO::Socket::INET->new(
+ PeerAddr => "127.0.0.1",
+ PeerPort => $port,
+ Proto => "tcp",
+ Type => SOCK_STREAM);
+ }
+ else {
+ return IO::Socket::UNIX->new(
+ Peer => $node->host() . "/.s.PGSQL." . $port,
+ Type => SOCK_STREAM);
+ }
+}
+
+# Test a regular connection first to make sure connecting etc works fine.
+test_connection(make_socket($node->port()),
+ undef, "normal connection", $PostgreSQL::Test::Cluster::use_tcp ? "127.0.0.1": "");
+
+# Make sure we can't make a proxy connection until it's allowed
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44", 1);
+
+# Allow proxy connections and test them
+$node->append_conf('postgresql.conf', "proxy_servers = 'unix, 127.0.0.1/32'");
+$node->restart();
+
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44");
+test_connection(make_socket($node->port() + 1),
+ "1:2:3:4:5:6::9", "proxy ipv6", "1:2:3:4:5:6:0:9");
+
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy with extra", "11.22.33.44", undef, "abcdef"x100);
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-28 13:23 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-03 13:36 ` Re: PROXY protocol support Daniel Gustafsson <daniel@yesql.se>
2021-11-04 11:03 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-15 23:03 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2022-02-25 10:41 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2022-03-09 16:23 ` Peter Eisentraut <peter.eisentraut@enterprisedb.com>
2022-03-09 16:29 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 1 reply; 56+ messages in thread
From: Peter Eisentraut @ 2022-03-09 16:23 UTC (permalink / raw)
To: Magnus Hagander <magnus@hagander.net>; Jacob Champion <pchampion@vmware.com>; +Cc: daniel@yesql.se <daniel@yesql.se>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
A general question on this feature: AFAICT, you can only send the proxy
header once at the beginning of the connection. So this wouldn't be of
use for PostgreSQL-protocol connection poolers (pgbouncer, pgpool),
where the same server connection can be used for clients from different
source addresses. Do I understand that correctly?
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-28 13:23 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-03 13:36 ` Re: PROXY protocol support Daniel Gustafsson <daniel@yesql.se>
2021-11-04 11:03 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-15 23:03 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2022-02-25 10:41 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-03-09 16:23 ` Re: PROXY protocol support Peter Eisentraut <peter.eisentraut@enterprisedb.com>
@ 2022-03-09 16:29 ` Magnus Hagander <magnus@hagander.net>
2022-04-01 22:16 ` Re: PROXY protocol support wilfried roset <wilfried.roset@gmail.com>
0 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2022-03-09 16:29 UTC (permalink / raw)
To: Peter Eisentraut <peter.eisentraut@enterprisedb.com>; +Cc: Jacob Champion <pchampion@vmware.com>; daniel@yesql.se <daniel@yesql.se>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Wed, Mar 9, 2022 at 5:23 PM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:
>
> A general question on this feature: AFAICT, you can only send the proxy
> header once at the beginning of the connection. So this wouldn't be of
> use for PostgreSQL-protocol connection poolers (pgbouncer, pgpool),
> where the same server connection can be used for clients from different
> source addresses. Do I understand that correctly?
Correct. It's only sent at connection startup, so if you're re-using
the connection it would also re-use the IP address of the first one.
For reusing the connection for multiple clients, you'd want something
different, like a "priviliged mode in the tunnel" that the pooler can
handle.
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-28 13:23 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-03 13:36 ` Re: PROXY protocol support Daniel Gustafsson <daniel@yesql.se>
2021-11-04 11:03 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-15 23:03 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2022-02-25 10:41 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-03-09 16:23 ` Re: PROXY protocol support Peter Eisentraut <peter.eisentraut@enterprisedb.com>
2022-03-09 16:29 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2022-04-01 22:16 ` wilfried roset <wilfried.roset@gmail.com>
2022-04-08 11:58 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 1 reply; 56+ messages in thread
From: wilfried roset @ 2022-04-01 22:16 UTC (permalink / raw)
To: pgsql-hackers@lists.postgresql.org; +Cc: Magnus Hagander <magnus@hagander.net>
Hi,
I've been able to test the patch. Here is a recap of the experimentation.
# Setup
All tests have been done witch 3 VMs (PostgreSQL, HAproxy, psql client) on
Debian 11 communicating over private network.
* PostgreSQL have been built with proxy_protocol_11.patch applied on master branch (465ab24296).
* psql client is from postgresql-client-13 from Debian 11 repository.
* HAproxy version used is 2.5.5-1~bpo11+1 installed from https://haproxy.debian.net
# Configuration
PostgresSQL has been configured to listen only on its private IP. To enable
proxy protocol support `proxy_port` has been configured to `5431` and
`proxy_servers` to `10.0.0.0/24`. `log_connections` has been turned on to make
sure the correct IP address is logged. `log_min_duration_statement` has been
configured to 0 to log all queries. Finally `log_destination` has been
configured to `csvlog`.
pg_hba.conf is like this:
local all all trust
host all all 127.0.0.1/32 trust
host all all ::1/128 trust
local replication all trust
host replication all 127.0.0.1/32 trust
host replication all ::1/128 trust
host all all 10.0.0.208/32 md5
Where 10.0.0.208 is the IP host the psql client's VM.
HAproxy has two frontends, one for proxy protocol (port 5431) and one for
regular TCP traffic. The configuration looks like this:
listen postgresql
bind 10.0.0.222:5432
server pg 10.0.0.253:5432 check
listen postgresql_proxy
bind 10.0.0.222:5431
server pg 10.0.0.253:5431 send-proxy-v2
Where 10.0.0.222 is the IP of HAproxy's VM and 10.0.0.253 is the IP of
PostgreSQL's VM.
# Tests
* from psql's vm to haproxy on port 5432 (no proxy protocol)
--> connection denied by pg_hba.conf, as expected
* from psql's vm to postgresql's VM on port 5432 (no proxy protocol)
--> connection success with psql's vm ip in logfile and pg_stat_activity
* from psql's vm to postgresql's VM on port 5431 (proxy protocol)
--> unable to open a connection, as expected
* from psql's vm to haproxy on port 5431 (proxy protocol)
--> connection success with psql's vm ip in logfile and pg_stat_activity
I've also tested without proxy protocol enable (and pg_hba.conf updated
accordingly), PostgreSQL behave as expected.
# Conclusion
From my point of view the documentation is clear enough and the feature works
as expected.
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-28 13:23 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-03 13:36 ` Re: PROXY protocol support Daniel Gustafsson <daniel@yesql.se>
2021-11-04 11:03 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-15 23:03 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2022-02-25 10:41 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-03-09 16:23 ` Re: PROXY protocol support Peter Eisentraut <peter.eisentraut@enterprisedb.com>
2022-03-09 16:29 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-04-01 22:16 ` Re: PROXY protocol support wilfried roset <wilfried.roset@gmail.com>
@ 2022-04-08 11:58 ` Magnus Hagander <magnus@hagander.net>
2022-07-28 20:05 ` Re: PROXY protocol support Jacob Champion <jchampion@timescale.com>
0 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2022-04-08 11:58 UTC (permalink / raw)
To: wilfried roset <wilfried.roset@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org
On Sat, Apr 2, 2022 at 12:17 AM wilfried roset <wilfried.roset@gmail.com>
wrote:
> Hi,
>
> I've been able to test the patch. Here is a recap of the experimentation.
>
> # Setup
>
> All tests have been done witch 3 VMs (PostgreSQL, HAproxy, psql client) on
> Debian 11 communicating over private network.
> * PostgreSQL have been built with proxy_protocol_11.patch applied on
> master branch (465ab24296).
> * psql client is from postgresql-client-13 from Debian 11 repository.
> * HAproxy version used is 2.5.5-1~bpo11+1 installed from
> https://haproxy.debian.net
>
> # Configuration
>
> PostgresSQL has been configured to listen only on its private IP. To enable
> proxy protocol support `proxy_port` has been configured to `5431` and
> `proxy_servers` to `10.0.0.0/24` <http://10.0.0.0/24;. `log_connections`
> has been turned on to make
> sure the correct IP address is logged. `log_min_duration_statement` has
> been
> configured to 0 to log all queries. Finally `log_destination` has been
> configured to `csvlog`.
>
> pg_hba.conf is like this:
>
> local all all trust
> host all all 127.0.0.1/32 trust
> host all all ::1/128 trust
> local replication all trust
> host replication all 127.0.0.1/32 trust
> host replication all ::1/128 trust
> host all all 10.0.0.208/32 md5
>
> Where 10.0.0.208 is the IP host the psql client's VM.
>
> HAproxy has two frontends, one for proxy protocol (port 5431) and one for
> regular TCP traffic. The configuration looks like this:
>
> listen postgresql
> bind 10.0.0.222:5432
> server pg 10.0.0.253:5432 check
>
> listen postgresql_proxy
> bind 10.0.0.222:5431
> server pg 10.0.0.253:5431 send-proxy-v2
>
> Where 10.0.0.222 is the IP of HAproxy's VM and 10.0.0.253 is the IP of
> PostgreSQL's VM.
>
> # Tests
>
> * from psql's vm to haproxy on port 5432 (no proxy protocol)
> --> connection denied by pg_hba.conf, as expected
>
> * from psql's vm to postgresql's VM on port 5432 (no proxy protocol)
> --> connection success with psql's vm ip in logfile and pg_stat_activity
>
> * from psql's vm to postgresql's VM on port 5431 (proxy protocol)
> --> unable to open a connection, as expected
>
> * from psql's vm to haproxy on port 5431 (proxy protocol)
> --> connection success with psql's vm ip in logfile and pg_stat_activity
>
> I've also tested without proxy protocol enable (and pg_hba.conf updated
> accordingly), PostgreSQL behave as expected.
>
> # Conclusion
>
> From my point of view the documentation is clear enough and the feature
> works
> as expected.
Hi!
Thanks for this review and testing!
I think it could do with at least noe more look-over at the source code
level as well at this point though since it's been sitting around for a
while, so it won't make it in for this deadline. But hopefully I can get it
in early in the next cycle!
--
Magnus Hagander
Me: https://www.hagander.net/ <http://www.hagander.net/;
Work: https://www.redpill-linpro.com/ <http://www.redpill-linpro.com/;
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-28 13:23 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-03 13:36 ` Re: PROXY protocol support Daniel Gustafsson <daniel@yesql.se>
2021-11-04 11:03 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-15 23:03 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2022-02-25 10:41 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-03-09 16:23 ` Re: PROXY protocol support Peter Eisentraut <peter.eisentraut@enterprisedb.com>
2022-03-09 16:29 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-04-01 22:16 ` Re: PROXY protocol support wilfried roset <wilfried.roset@gmail.com>
2022-04-08 11:58 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2022-07-28 20:05 ` Jacob Champion <jchampion@timescale.com>
2024-02-03 11:37 ` Re: PROXY protocol support Julien Riou <julien@riou.xyz>
0 siblings, 1 reply; 56+ messages in thread
From: Jacob Champion @ 2022-07-28 20:05 UTC (permalink / raw)
To: pgsql-hackers@lists.postgresql.org; +Cc: Magnus Hagander <magnus@hagander.net>
This needs a rebase, but after that I expect it to be RfC.
--Jacob
The new status of this patch is: Waiting on Author
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-28 13:23 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-03 13:36 ` Re: PROXY protocol support Daniel Gustafsson <daniel@yesql.se>
2021-11-04 11:03 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-15 23:03 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2022-02-25 10:41 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-03-09 16:23 ` Re: PROXY protocol support Peter Eisentraut <peter.eisentraut@enterprisedb.com>
2022-03-09 16:29 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-04-01 22:16 ` Re: PROXY protocol support wilfried roset <wilfried.roset@gmail.com>
2022-04-08 11:58 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-07-28 20:05 ` Re: PROXY protocol support Jacob Champion <jchampion@timescale.com>
@ 2024-02-03 11:37 ` Julien Riou <julien@riou.xyz>
2026-06-20 05:46 ` Re: PROXY protocol support Julien Riou <julien@riou.xyz>
0 siblings, 1 reply; 56+ messages in thread
From: Julien Riou @ 2024-02-03 11:37 UTC (permalink / raw)
To: Jacob Champion <jchampion@timescale.com>; pgsql-hackers@lists.postgresql.org; +Cc: Magnus Hagander <magnus@hagander.net>
On 7/28/22 22:05, Jacob Champion wrote:
> This needs a rebase, but after that I expect it to be RfC.
>
> --Jacob
>
> The new status of this patch is: Waiting on Author
Hello folks,
Thank you all for this awesome work!
I'm looking for this feature for years now. Last year, I've tried to
rebase the patch without success. Unfortunately, this is out of my league.
Magnus, please let me know if I can help.
Have a nice day,
Julien
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-28 13:23 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-03 13:36 ` Re: PROXY protocol support Daniel Gustafsson <daniel@yesql.se>
2021-11-04 11:03 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-15 23:03 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2022-02-25 10:41 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-03-09 16:23 ` Re: PROXY protocol support Peter Eisentraut <peter.eisentraut@enterprisedb.com>
2022-03-09 16:29 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-04-01 22:16 ` Re: PROXY protocol support wilfried roset <wilfried.roset@gmail.com>
2022-04-08 11:58 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-07-28 20:05 ` Re: PROXY protocol support Jacob Champion <jchampion@timescale.com>
2024-02-03 11:37 ` Re: PROXY protocol support Julien Riou <julien@riou.xyz>
@ 2026-06-20 05:46 ` Julien Riou <julien@riou.xyz>
2026-06-20 09:18 ` Re: PROXY protocol support Julien Riou <julien@riou.xyz>
2026-09-23 00:27 ` Re: PROXY protocol support Manu <manuelreyesbravo@gmail.com>
0 siblings, 2 replies; 56+ messages in thread
From: Julien Riou @ 2026-06-20 05:46 UTC (permalink / raw)
To: pgsql-hackers@lists.postgresql.org; +Cc: Magnus Hagander <magnus@hagander.net>
Hi,
After multiple years, I retried to work on this feature again and, this
time, I finally did it.
As I could not rebase your patch Magnus, I decided to restart from
scratch, with all this thread history in mind and what was in my head too.
The first change drifting from the initial implementation is this patch
uses the same port for both proxy protocol aware connections and for not
proxy protocol aware connections. To follow the spec, if a connection
comes from a trusted network but doesn't provide the PROXY header, the
server will reject it. Having only one port to listen is simpler from an
operator point of view. No need to configure and document more
networking rules. Existing rules will work as usual.
I wanted this patch to be as transparent as possible so the protocol is
parsed at the very end to let a chance for existing parsing rules to be
executed before the proxy protocol. No performance regression expected,
unless you enable the feature by configuring a list of proxy networks or
if your connection is not valid but this should be insignificant.
There is one exception for direct SSL though. The spec precises that any
proxy connection must provide the header, but this is not the case for
direct SSL. The fix is small. I have included the SSL handling in the
proxy protocol parsing. No change of behavior, no major code
refactoring. Let me know if you have a better implementation.
As a security measure, an untrusted connection, not in the trusted proxy
networks, that doesn't provide the header is rejected with the generic
"incomplete startup packet" message instead of an explicit proxy error.
This prevent untrusted clients, possibly scanning the service, to
discover that the server supports the proxy protocol.
The patch supports both v1 and v2 versions of the protocol. If a
connection is trusted and valid, the client_addr, hostname and port are
replaced by the ones provided in the proxy header in pg_stat_activity.
The original values are available in proxy_addr, hostname and port. The
pg_hba.conf uses the parsed client information, which is the goal of
this feature. But also, both the parsed client and proxy information are
added to the loggers (stderr, csvlog and jsonlog).
All those changes are tested in a protocol TAP tests suite. I wanted
them to be as readable as possible, so excuse me if they are not
straightforward. I could not find helpers to wait and parse csvlog and
jsonlog loggers so I have created them in the protocol suite. These
functions are not aware of the format so they wait and return a line
matching a regex. I could make them format aware to wait for CSV or JSON
field and values to show up. This patch is already big so let me know if
you want me to add those helpers in the test libraries now or in a
future patch.
I hope you'll find this patch useful.
Have a nice day,
--
Julien
Attachments:
[text/x-patch] proxy_protocol_12.patch (95.2K, ../../db5262fd-1d0b-4f24-8bcc-e17577cf3eb8@riou.xyz/2-proxy_protocol_12.patch)
download | inline diff:
From 806fb8b4b0abbf15f9717567e7b5a422b60f862a Mon Sep 17 00:00:00 2001
From: Julien Riou <julien.riou@ovhcloud.com>
Date: Sun, 14 Jun 2026 10:22:02 +0000
Subject: [PATCH] Implement server-side support for the PROXY protocol
Add support for HAProxy's PROXY protocol, versions 1 and 2, so the real
client address can be recovered from connections arriving through a trusted
proxy. The new proxy_networks GUC lists the networks whose peers are allowed
to prepend a PROXY header to declare the originating client. This list
supports unix sockets with the "unix" token. It is empty by default, which
disables the feature.
The header is parsed lazily. The server only looks for it on connections
coming from a trusted network and falls through to the normal startup path
otherwise. Existing clients that are not from the proxy networks and that
do not speak the PROXY protocol are unaffected.
For connections from a trusted proxy, client_addr and client_port in
pg_stat_activity and the host-based authentication checks reflect the
address from the header, while the proxy's own endpoint is exposed via the
new proxy_addr, proxy_hostname and proxy_port columns. The %H and %R
escapes expose the proxy information in log_line_prefix. The proxy host and
port are also emitted in the CSV and JSON log formats.
---
doc/src/sgml/client-auth.sgml | 21 +
doc/src/sgml/config.sgml | 77 ++-
doc/src/sgml/monitoring.sgml | 44 ++
doc/src/sgml/protocol.sgml | 16 +
src/backend/catalog/system_views.sql | 3 +
src/backend/libpq/Makefile | 3 +-
src/backend/libpq/auth.c | 12 +
src/backend/libpq/meson.build | 1 +
src/backend/libpq/pqcomm.c | 2 +-
src/backend/libpq/proxy_protocol.c | 517 ++++++++++++++++++
src/backend/tcop/backend_startup.c | 112 +++-
src/backend/utils/activity/backend_status.c | 35 ++
src/backend/utils/adt/pgstatfuncs.c | 59 +-
src/backend/utils/error/csvlog.c | 13 +
src/backend/utils/error/elog.c | 49 ++
src/backend/utils/error/jsonlog.c | 8 +
src/backend/utils/misc/guc_parameters.dat | 10 +
src/backend/utils/misc/guc_tables.c | 1 +
src/backend/utils/misc/postgresql.conf.sample | 3 +
src/include/catalog/pg_proc.dat | 6 +-
src/include/libpq/libpq-be.h | 6 +
src/include/libpq/libpq.h | 1 +
src/include/libpq/proxy_protocol.h | 38 ++
src/include/utils/backend_status.h | 4 +
src/include/utils/guc_hooks.h | 3 +
src/test/Makefile | 1 +
src/test/meson.build | 1 +
src/test/protocol/.gitignore | 2 +
src/test/protocol/Makefile | 25 +
src/test/protocol/README | 28 +
src/test/protocol/meson.build | 17 +
src/test/protocol/t/001_proxy_protocol.pl | 515 +++++++++++++++++
src/test/protocol/t/002_proxy_protocol_ssl.pl | 115 ++++
src/test/protocol/t/ProxyProtocol.pm | 385 +++++++++++++
src/test/regress/expected/rules.out | 11 +-
src/tools/pgindent/typedefs.list | 3 +
36 files changed, 2131 insertions(+), 16 deletions(-)
create mode 100644 src/backend/libpq/proxy_protocol.c
create mode 100644 src/include/libpq/proxy_protocol.h
create mode 100644 src/test/protocol/.gitignore
create mode 100644 src/test/protocol/Makefile
create mode 100644 src/test/protocol/README
create mode 100644 src/test/protocol/meson.build
create mode 100644 src/test/protocol/t/001_proxy_protocol.pl
create mode 100644 src/test/protocol/t/002_proxy_protocol_ssl.pl
create mode 100644 src/test/protocol/t/ProxyProtocol.pm
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index e4e65f8feb1..a5c93530261 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -421,6 +421,16 @@ include_dir <replaceable>directory</replaceable>
These fields do not apply to <literal>local</literal> records.
</para>
+ <note>
+ <para>
+ When a connection arrives through a connection-forwarding proxy and a
+ PROXY protocol header is accepted, the address matched against this
+ field is the client address declared in that header, not the proxy's
+ own address. See <xref linkend="guc-proxy-networks"/> for
+ details.
+ </para>
+ </note>
+
<note>
<para>
Users sometimes wonder why host names are handled
@@ -1708,6 +1718,17 @@ omicron bryanh guest1
since <productname>PostgreSQL</productname> does not have any way to decrypt the
returned string to determine the actual user name.
</para>
+
+ <para>
+ Ident authentication is rejected for connections received through
+ the <link linkend="protocol-flow-proxy">PROXY protocol</link>. A client
+ connected to the proxy rather than to the server. The port pair
+ <replaceable>X</replaceable> and <replaceable>Y</replaceable> known to the
+ server does not describe any connection that the client's ident server has
+ a record of, and the query would reach the ident server from the server's
+ address rather than the proxy's. Any answer would therefore be
+ meaningless.
+ </para>
</sect1>
<sect1 id="auth-peer">
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index fa566c9e553..2aa8ed353d1 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -712,6 +712,51 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-proxy-networks" xreflabel="proxy_networks">
+ <term><varname>proxy_networks</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>proxy_networks</varname> configuration parameter</primary>
+ </indexterm>
+ <indexterm>
+ <primary>PROXY protocol</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Specifies the networks from which the server will accept a
+ PROXY protocol header. When the server is reached through a
+ connection-forwarding proxy (like <productname>HAProxy</productname>),
+ the proxy can prepend a small header, as defined by the
+ <ulink url="https://www.haproxy.org/download/2.9/doc/proxy-protocol.txt">PROXY
+ protocol</ulink>, that declares the address of the real client.
+ When such a header is accepted, the declared client
+ address replaces the proxy's address for host-based authentication
+ (see <xref linkend="auth-pg-hba-conf"/>) and in the server log.
+ </para>
+ <para>
+ The value is a comma-separated list of CIDR networks or the special
+ token <literal>unix</literal> for Unix-domain socket (for example
+ <literal>10.0.0.0/8, 192.168.1.10, ::1/128, unix</literal>). A
+ PROXY protocol header is honored only when the actual peer address
+ falls within one of these networks. A header received from any
+ other address is rejected as an invalid connection attempt, so that
+ ordinary clients cannot spoof their address. The default is an
+ empty string, which disables the PROXY protocol support entirely.
+ This parameter can only be set in the
+ <filename>postgresql.conf</filename> file or on the server command
+ line.
+ </para>
+ <para>
+ Because a peer within these networks is treated as a proxy rather than
+ a direct client, it is <emphasis>required</emphasis> to lead with a
+ PROXY protocol header. A connection from one of these networks that
+ begins with an ordinary startup packet, or an SSL or GSS negotiation
+ request, instead of a PROXY header is rejected. This prevents the
+ proxy's own address from being mistaken for a client's.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-max-connections" xreflabel="max_connections">
<term><varname>max_connections</varname> (<type>integer</type>)
<indexterm>
@@ -8107,11 +8152,12 @@ local0.* /var/log/postgresql
<listitem>
<para>
By default, connection log messages only show the IP address of the
- connecting host. Turning this parameter on causes logging of the
- host name as well. Note that depending on your host name resolution
- setup this might impose a non-negligible performance penalty.
- This parameter can only be set in the <filename>postgresql.conf</filename>
- file or on the server command line.
+ connecting host and the proxy host if the connection came from a
+ trusted proxy. Turning this parameter on causes logging of the host
+ names as well. Note that depending on your host name resolution setup
+ this might impose a non-negligible performance penalty. This parameter
+ can only be set in the <filename>postgresql.conf</filename> file or on
+ the server command line.
</para>
</listitem>
</varlistentry>
@@ -8181,6 +8227,16 @@ local0.* /var/log/postgresql
<entry>Remote host name or IP address</entry>
<entry>yes</entry>
</row>
+ <row>
+ <entry><literal>%H</literal></entry>
+ <entry>Proxy host name or IP address</entry>
+ <entry>yes</entry>
+ </row>
+ <row>
+ <entry><literal>%R</literal></entry>
+ <entry>Proxy host name or IP address, and proxy port</entry>
+ <entry>yes</entry>
+ </row>
<row>
<entry><literal>%L</literal></entry>
<entry>Local address (the IP address on the server that the
@@ -8641,6 +8697,7 @@ CREATE TABLE postgres_log
backend_type text,
leader_pid integer,
query_id bigint,
+ proxy_connection text,
PRIMARY KEY (session_id, session_line_num)
);
</programlisting>
@@ -8767,6 +8824,16 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
<entry>number</entry>
<entry>Client port</entry>
</row>
+ <row>
+ <entry><literal>proxy_host</literal></entry>
+ <entry>string</entry>
+ <entry>Proxy host</entry>
+ </row>
+ <row>
+ <entry><literal>proxy_port</literal></entry>
+ <entry>number</entry>
+ <entry>Proxy port</entry>
+ </row>
<row>
<entry><literal>session_id</literal></entry>
<entry>string</entry>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 08d5b824552..ab0443446eb 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -843,6 +843,10 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser
If this field is null, it indicates either that the client is
connected via a Unix socket on the server machine or that this is an
internal process such as autovacuum.
+ When the connection was made through a trusted proxy using the PROXY
+ protocol, this is the client address sent by the proxy, not the address
+ of the proxy itself (which is shown in <structfield>proxy_addr</structfield>
+ instead).
</para></entry>
</row>
@@ -865,6 +869,46 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser
TCP port number that the client is using for communication
with this backend, or <literal>-1</literal> if a Unix socket is used.
If this field is null, it indicates that this is an internal server process.
+ When the connection was made through a trusted proxy using the PROXY
+ protocol, this is the client port sent by the proxy, not the port of the
+ proxy itself (which is shown in <structfield>proxy_port</structfield>
+ instead).
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>proxy_addr</structfield> <type>inet</type>
+ </para>
+ <para>
+ IP address of the trusted proxy that forwarded this connection.
+ If this field is null, it indicates either that the PROXY protocol has
+ not been used, or the proxy forwarded the connection over a Unix socket,
+ or the proxy used a <literal>LOCAL<literal> command.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>proxy_hostname</structfield> <type>text</type>
+ </para>
+ <para>
+ Host name of the proxy that forwarded this connection, as reported by a
+ reverse DNS lookup of <structfield>proxy_addr</structfield>. This field
+ will only be non-null when the client connected through a trusted proxy
+ using the PROXY protocol, and only when <xref linkend="guc-log-hostname"/>
+ is enabled.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>proxy_port</structfield> <type>integer</type>
+ </para>
+ <para>
+ TCP port number of the trusted proxy that forwarded this connection, or
+ <literal>-1</literal> if the proxy forwarded it over a Unix socket.
+ This field is null when the PROXY protocol was not used.
</para></entry>
</row>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 49f81676712..556fd325d7a 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1693,6 +1693,22 @@ SELCT 1/0;<!-- this typo is intentional -->
</para>
</sect2>
+ <sect2 id="protocol-flow-proxy">
+ <title><acronym>PROXY</acronym> Protocol</title>
+
+ <para>
+ When a connection is forwarded by a proxy, the address the server sees is
+ the proxy's, not the real client's. To preserve the
+ originating address, the proxy can prepend a header conforming to the
+ <ulink url="https://www.haproxy.org/download/2.9/doc/proxy-protocol.txt">PROXY
+ protocol</ulink> designed by <productname>HAProxy</productname>. The
+ header carries the original source and destination addresses and ports.
+ The header is sent once, before any other data, ahead of the SSLRequest,
+ GSSENCRequest, or StartupMessage. Both version 1 (text) and version 2
+ (binary) of the protocol are accepted.
+ </para>
+ </sect2>
+
<sect2 id="protocol-flow-ssl">
<title><acronym>SSL</acronym> Session Encryption</title>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 8f129baec90..e3988344966 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -948,6 +948,9 @@ CREATE VIEW pg_stat_activity AS
S.client_addr,
S.client_hostname,
S.client_port,
+ S.proxy_addr,
+ S.proxy_hostname,
+ S.proxy_port,
S.backend_start,
S.xact_start,
S.query_start,
diff --git a/src/backend/libpq/Makefile b/src/backend/libpq/Makefile
index 98eb2a8242d..5f8863d0dbf 100644
--- a/src/backend/libpq/Makefile
+++ b/src/backend/libpq/Makefile
@@ -28,7 +28,8 @@ OBJS = \
pqcomm.o \
pqformat.o \
pqmq.o \
- pqsignal.o
+ pqsignal.o \
+ proxy_protocol.o
ifeq ($(with_ssl),openssl)
OBJS += be-secure-openssl.o
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 2af5615e54a..5d9b4119c2f 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -1701,6 +1701,18 @@ ident_inet(Port *port)
*la = NULL,
hints;
+ /*
+ * Ident is incompatible with the PROXY protocol. A proxied client
+ * connected to the proxy, not to the server, so its ident server has no
+ * record of any connection matching the address pair in the query.
+ */
+ if (port->proxy_protocol)
+ {
+ ereport(LOG,
+ (errmsg("ident authentication is not supported over connections using the PROXY protocol")));
+ return STATUS_ERROR;
+ }
+
/*
* Might look a little weird to first convert it to text and then back to
* sockaddr, but it's protocol independent.
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 8571f652844..30584ee8dfd 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -15,6 +15,7 @@ backend_sources += files(
'pqformat.c',
'pqmq.c',
'pqsignal.c',
+ 'proxy_protocol.c',
)
if ssl.found()
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 4a442f22df6..4eeb4eb4d45 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -1094,7 +1094,7 @@ pq_getbytes(void *b, size_t len)
* returns 0 if OK, EOF if trouble
* --------------------------------
*/
-static int
+int
pq_discardbytes(size_t len)
{
size_t amount;
diff --git a/src/backend/libpq/proxy_protocol.c b/src/backend/libpq/proxy_protocol.c
new file mode 100644
index 00000000000..6a66c12926b
--- /dev/null
+++ b/src/backend/libpq/proxy_protocol.c
@@ -0,0 +1,517 @@
+/*-------------------------------------------------------------------------
+ *
+ * proxy_protocol.c
+ * Functions related to parse the PROXY Protocol (versions 1 and 2).
+ * https://www.haproxy.org/download/2.9/doc/proxy-protocol.txt
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ * src/backend/libpq/proxy_protocol.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include "common/ip.h"
+#include "libpq/ifaddr.h"
+#include "libpq/libpq.h"
+#include "libpq/proxy_protocol.h"
+#include "port/pg_bswap.h"
+#include "utils/guc.h"
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
+
+/* Raw text of the proxy_networks GUC. */
+char *ProxyNetworks = NULL;
+
+/*
+ * Pre-parsed form of proxy_networks, produced by the check hook and
+ * installed by the assign hook. Stored with guc_malloc() so that the GUC
+ * machinery owns its lifetime.
+ */
+typedef struct ProxyNet
+{
+ struct sockaddr_storage addr; /* network address */
+ struct sockaddr_storage mask; /* network mask */
+} ProxyNet;
+
+typedef struct ProxyNets
+{
+ bool trust_unix; /* trust Unix-socket peers to send a header */
+ int nnets;
+ ProxyNet nets[FLEXIBLE_ARRAY_MEMBER];
+} ProxyNets;
+
+/*
+ * The special proxy_networks token to trust Unix-domain socket peers
+ * to send a PROXY header.
+ */
+#define PROXY_UNIX_TOKEN "unix"
+
+static ProxyNets *proxy_networks = NULL;
+
+/* The 12-byte PROXY protocol v2 signature. */
+static const uint8 v2_signature[12] =
+"\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a";
+
+/* Largest possible v1 header line, including the trailing CRLF. */
+#define PROXY_V1_MAX_LEN 107
+
+/* Supported v2 address families. */
+#define PROXY_V2_AF_UNSPEC 0x00
+#define PROXY_V2_TCP4 0x11
+#define PROXY_V2_TCP6 0x21
+
+/*
+ * Length of the leading address block for each supported v2 family.
+ * Any bytes beyond these lengths are TLV vectors and are skipped.
+ */
+#define PROXY_V2_TCP4_ADDRLEN 12 /* 2 x 4-byte addr + 2 x 2-byte port */
+#define PROXY_V2_TCP6_ADDRLEN 36 /* 2 x 16-byte addr + 2 x 2-byte port */
+
+/* Supported v2 commands */
+#define PROXY_V2_CMD_LOCAL 0x0
+#define PROXY_V2_CMD_PROXY 0x1
+
+/*
+ * Parse a single "address" or "address/masklen" network specification into a
+ * ProxyNet. Returns true on success. No DNS is performed (numeric host
+ * only), so this is safe to call from a GUC check hook.
+ */
+static bool
+parse_proxy_network(const char *spec, ProxyNet *net)
+{
+ char *str = pstrdup(spec);
+ char *slash;
+ struct addrinfo hints;
+ struct addrinfo *gai_result = NULL;
+ bool ok = false;
+
+ memset(net, 0, sizeof(*net));
+
+ slash = strchr(str, '/');
+ if (slash)
+ *slash = '\0';
+
+ MemSet(&hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ if (pg_getaddrinfo_all(str, NULL, &hints, &gai_result) == 0 &&
+ gai_result != NULL &&
+ gai_result->ai_addrlen <= sizeof(net->addr))
+ {
+ memcpy(&net->addr, gai_result->ai_addr, gai_result->ai_addrlen);
+
+ ok = pg_sockaddr_cidr_mask(&net->mask, slash ? slash + 1 : NULL,
+ net->addr.ss_family) >= 0;
+ }
+
+ if (gai_result)
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+ pfree(str);
+ return ok;
+}
+
+/*
+ * GUC check hook for proxy_networks. Parses the comma-separated
+ * list of networks into a ProxyNets structure stored in *extra.
+ */
+bool
+check_proxy_networks(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ int nnets;
+ int i;
+ ProxyNets *result;
+
+ /* Need a modifiable copy of the string */
+ rawstring = pstrdup(*newval);
+
+ if (!SplitGUCList(rawstring, ',', &elemlist))
+ {
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ nnets = list_length(elemlist);
+
+ result = (ProxyNets *) guc_malloc(LOG,
+ offsetof(ProxyNets, nets) +
+ nnets * sizeof(ProxyNet));
+ if (result == NULL)
+ {
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+ result->nnets = 0;
+ result->trust_unix = false;
+
+ i = 0;
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+
+ if (pg_strcasecmp(tok, PROXY_UNIX_TOKEN) == 0)
+ {
+ result->trust_unix = true;
+ continue;
+ }
+
+ if (!parse_proxy_network(tok, &result->nets[i]))
+ {
+ GUC_check_errdetail("Invalid network specification: \"%s\".", tok);
+ guc_free(result);
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+ i++;
+ }
+ result->nnets = i;
+
+ pfree(rawstring);
+ list_free(elemlist);
+
+ *extra = result;
+ return true;
+}
+
+/*
+ * GUC assign hook for proxy_networks.
+ */
+void
+assign_proxy_networks(const char *newval, void *extra)
+{
+ proxy_networks = (ProxyNets *) extra;
+}
+
+/*
+ * Reports whether PROXY protocol parsing is enabled.
+ */
+bool
+ProxyProtocolEnabled(void)
+{
+ return proxy_networks != NULL &&
+ (proxy_networks->nnets > 0 || proxy_networks->trust_unix);
+}
+
+/*
+ * Reports whether the given peer address falls within one of the trusted
+ * proxy networks.
+ */
+static bool
+proxy_source_trusted(const SockAddr *raddr)
+{
+ int i;
+
+ if (proxy_networks == NULL)
+ return false;
+
+ if (raddr->addr.ss_family == AF_UNIX)
+ return proxy_networks->trust_unix;
+
+ if (raddr->addr.ss_family != AF_INET &&
+ raddr->addr.ss_family != AF_INET6)
+ return false;
+
+ for (i = 0; i < proxy_networks->nnets; i++)
+ {
+ if (raddr->addr.ss_family == proxy_networks->nets[i].addr.ss_family &&
+ pg_range_sockaddr(&raddr->addr,
+ &proxy_networks->nets[i].addr,
+ &proxy_networks->nets[i].mask))
+ return true;
+ }
+ return false;
+}
+
+/*
+ * Reports whether this connection must begin with a PROXY header.
+ */
+bool
+ProxyProtocolRequired(Port *port)
+{
+ return proxy_source_trusted(&port->raddr);
+}
+
+/*
+ * Build an IPv4 or IPv6 SockAddr from a numeric address string and a port
+ * string for PROXY v1.
+ */
+static bool
+make_v1_sockaddr(int family, const char *addr, const char *port, SockAddr *sa)
+{
+ char *endptr;
+ long portnum;
+
+ errno = 0;
+ portnum = strtol(port, &endptr, 10);
+ if (endptr == port || *endptr != '\0' || errno != 0 ||
+ portnum < 0 || portnum > 65535)
+ return false;
+
+ memset(&sa->addr, 0, sizeof(sa->addr));
+
+ if (family == AF_INET)
+ {
+ struct sockaddr_in *sin = (struct sockaddr_in *) &sa->addr;
+
+ if (inet_pton(AF_INET, addr, &sin->sin_addr) != 1)
+ return false;
+ sin->sin_family = AF_INET;
+ sin->sin_port = pg_hton16((uint16) portnum);
+ sa->salen = sizeof(struct sockaddr_in);
+ }
+ else
+ {
+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) &sa->addr;
+
+ if (inet_pton(AF_INET6, addr, &sin6->sin6_addr) != 1)
+ return false;
+ sin6->sin6_family = AF_INET6;
+ sin6->sin6_port = pg_hton16((uint16) portnum);
+ sa->salen = sizeof(struct sockaddr_in6);
+ }
+
+ return true;
+}
+
+/*
+ * Parse a human-readable PROXY header (version 1).
+ */
+static ProxyProtocolResult
+parse_proxy_v1(Port *port, bool *have_client, SockAddr *client)
+{
+ char line[PROXY_V1_MAX_LEN + 1];
+ int len = 0;
+ char *saveptr;
+ char *proto;
+ char *src_addr;
+ char *dst_addr;
+ char *src_port;
+ char *dst_port;
+ int family;
+
+ /* The first four bytes ("PROX") have already been consumed by the caller */
+ line[len++] = 'P';
+ line[len++] = 'R';
+ line[len++] = 'O';
+ line[len++] = 'X';
+
+ for (;;)
+ {
+ int c = pq_getbyte();
+
+ if (c == EOF)
+ return PROXY_PROTO_ERROR;
+ if (len >= PROXY_V1_MAX_LEN)
+ return PROXY_PROTO_ERROR; /* no CRLF within the size limit */
+ line[len++] = (char) c;
+ if (c == '\n')
+ break;
+ }
+
+ /* The line must end with CRLF. */
+ if (len < 2 || line[len - 2] != '\r' || line[len - 1] != '\n')
+ return PROXY_PROTO_ERROR;
+ line[len - 2] = '\0';
+
+ /* It must start with the exact "PROXY " token. */
+ if (strncmp(line, "PROXY ", 6) != 0)
+ return PROXY_PROTO_ERROR;
+
+ proto = strtok_r(line + 6, " ", &saveptr);
+ if (proto == NULL)
+ return PROXY_PROTO_ERROR;
+
+ if (strcmp(proto, "UNKNOWN") == 0)
+ {
+ /* Address is unknown. Keep the real peer address. */
+ *have_client = false;
+ return PROXY_PROTO_DONE;
+ }
+ else if (strcmp(proto, "TCP4") == 0)
+ family = AF_INET;
+ else if (strcmp(proto, "TCP6") == 0)
+ family = AF_INET6;
+ else
+ return PROXY_PROTO_ERROR;
+
+ src_addr = strtok_r(NULL, " ", &saveptr);
+ dst_addr = strtok_r(NULL, " ", &saveptr);
+ src_port = strtok_r(NULL, " ", &saveptr);
+ dst_port = strtok_r(NULL, " ", &saveptr);
+
+ if (src_addr == NULL || dst_addr == NULL ||
+ src_port == NULL || dst_port == NULL)
+ return PROXY_PROTO_ERROR;
+
+ /* No further tokens are allowed. */
+ if (strtok_r(NULL, " ", &saveptr) != NULL)
+ return PROXY_PROTO_ERROR;
+
+ if (!make_v1_sockaddr(family, src_addr, src_port, client))
+ return PROXY_PROTO_ERROR;
+
+ *have_client = true;
+ return PROXY_PROTO_DONE;
+}
+
+/*
+ * Parse a binary PROXY header (version 2).
+ */
+static ProxyProtocolResult
+parse_proxy_v2(Port *port, bool *have_client, SockAddr *client)
+{
+ uint8 sigrest[8];
+ uint8 hdr[4];
+ uint8 ver,
+ cmd,
+ fam;
+ uint16 datalen;
+ uint16 toread;
+ uint8 data[PROXY_V2_TCP6_ADDRLEN]; /* largest supported address
+ * block */
+
+ /* Read and verify the remaining 8 bytes of the signature. */
+ if (pq_getbytes(sigrest, sizeof(sigrest)) == EOF)
+ return PROXY_PROTO_ERROR;
+
+ if (memcmp(sigrest, v2_signature + 4, sizeof(sigrest)) != 0)
+ return PROXY_PROTO_ERROR;
+
+ /* Read version+command, family+protocol, and the 2-byte length. */
+ if (pq_getbytes(hdr, sizeof(hdr)) == EOF)
+ return PROXY_PROTO_ERROR;
+
+ ver = hdr[0] >> 4;
+ cmd = hdr[0] & 0x0f;
+ fam = hdr[1];
+ datalen = ((uint16) hdr[2] << 8) | hdr[3];
+
+ if (ver != 0x2)
+ return PROXY_PROTO_ERROR;
+
+ /*
+ * Read the address block. We only need the leading address bytes. Any
+ * trailing TLV vectors are discarded so that the stream stays in sync for
+ * the genuine startup packet.
+ */
+ toread = Min(datalen, (uint16) sizeof(data));
+
+ if (toread > 0 && pq_getbytes(data, toread) == EOF)
+ return PROXY_PROTO_ERROR;
+
+ if (datalen > toread && pq_discardbytes(datalen - toread) == EOF)
+ return PROXY_PROTO_ERROR;
+
+ switch (cmd)
+ {
+ case PROXY_V2_CMD_LOCAL: /* mainly used for health checks */
+ {
+ *have_client = false;
+ return PROXY_PROTO_DONE;
+ }
+ case PROXY_V2_CMD_PROXY:
+ break;
+ default:
+ return PROXY_PROTO_ERROR;
+
+ }
+
+ memset(&client->addr, 0, sizeof(client->addr));
+
+ switch (fam)
+ {
+ case PROXY_V2_TCP4:
+ {
+ struct sockaddr_in *sin = (struct sockaddr_in *) &client->addr;
+
+ /* The address block must be present. TLVs may follow it. */
+ if (datalen < PROXY_V2_TCP4_ADDRLEN)
+ return PROXY_PROTO_ERROR;
+ sin->sin_family = AF_INET;
+ memcpy(&sin->sin_addr, data, 4); /* source address */
+ memcpy(&sin->sin_port, data + 8, 2); /* source port */
+ client->salen = sizeof(struct sockaddr_in);
+ *have_client = true;
+ break;
+ }
+ case PROXY_V2_TCP6:
+ {
+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) &client->addr;
+
+ /* The address block must be present. TLVs may follow it. */
+ if (datalen < PROXY_V2_TCP6_ADDRLEN)
+ return PROXY_PROTO_ERROR;
+ sin6->sin6_family = AF_INET6;
+ memcpy(&sin6->sin6_addr, data, 16); /* source address */
+ memcpy(&sin6->sin6_port, data + 32, 2); /* source port */
+ client->salen = sizeof(struct sockaddr_in6);
+ *have_client = true;
+ break;
+ }
+ case PROXY_V2_AF_UNSPEC:
+ default:
+ *have_client = false;
+ break;
+ }
+
+ return PROXY_PROTO_DONE;
+}
+
+/*
+ * Try to process a PROXY protocol header.
+ *
+ */
+ProxyProtocolResult
+ProcessProxyProtocol(Port *port, const char firstbytes[4])
+{
+ bool is_v1;
+ bool is_v2;
+ bool have_client = false;
+ SockAddr client;
+ ProxyProtocolResult result;
+
+ /* Only connections from a trusted proxy are eligible */
+ if (!proxy_source_trusted(&port->raddr))
+ return PROXY_PROTO_NONE;
+
+ is_v1 = (memcmp(firstbytes, "PROX", 4) == 0);
+ is_v2 = (memcmp(firstbytes, v2_signature, 4) == 0);
+
+ if (!is_v1 && !is_v2)
+ return PROXY_PROTO_NONE;
+
+ if (is_v1)
+ result = parse_proxy_v1(port, &have_client, &client);
+ else
+ result = parse_proxy_v2(port, &have_client, &client);
+
+ if (result != PROXY_PROTO_DONE)
+ return result;
+
+ /* Header accepted */
+ port->proxy_protocol = true;
+ if (have_client)
+ {
+ port->proxy_addr = port->raddr;
+ port->raddr = client;
+ }
+
+ pq_endmsgread();
+
+ return PROXY_PROTO_DONE;
+}
diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c
index 25205cee0fa..30c7f5f4874 100644
--- a/src/backend/tcop/backend_startup.c
+++ b/src/backend/tcop/backend_startup.c
@@ -25,6 +25,7 @@
#include "libpq/libpq-be.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
+#include "libpq/proxy_protocol.h"
#include "miscadmin.h"
#include "postmaster/postmaster.h"
#include "replication/walsender.h"
@@ -145,6 +146,8 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac)
Port *port;
char remote_host[NI_MAXHOST];
char remote_port[NI_MAXSERV];
+ char proxy_host[NI_MAXHOST];
+ char proxy_port[NI_MAXSERV];
StringInfoData ps_data;
MemoryContext oldcontext;
@@ -182,6 +185,8 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac)
/* set these to empty in case they are needed before we set them up */
port->remote_host = "";
port->remote_port = "";
+ port->proxy_host = "";
+ port->proxy_port = "";
/*
* We arrange to do _exit(1) if we receive SIGTERM or timeout while trying
@@ -294,6 +299,61 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac)
if (status == STATUS_OK)
status = ProcessStartupPacket(port);
+ /*
+ * If a PROXY protocol header replaced port->raddr with the real client
+ * address (marked by a non-zero proxy_addr.salen), recompute the cached
+ * host/port strings so that log output reflects the originating client.
+ * A LOCAL command, a v1 UNKNOWN, or an unsupported family leaves both
+ * addresses untouched and the proxy host/port empty.
+ */
+ if (status == STATUS_OK && port->proxy_protocol &&
+ port->proxy_addr.salen > 0)
+ {
+ remote_host[0] = '\0';
+ remote_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&port->raddr.addr, port->raddr.salen,
+ remote_host, sizeof(remote_host),
+ remote_port, sizeof(remote_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ port->remote_host = MemoryContextStrdup(TopMemoryContext, remote_host);
+ port->remote_port = MemoryContextStrdup(TopMemoryContext, remote_port);
+
+ if (log_hostname &&
+ ret == 0 &&
+ strspn(remote_host, "0123456789.") < strlen(remote_host) &&
+ strspn(remote_host, "0123456789ABCDEFabcdef:") < strlen(remote_host))
+ port->remote_hostname = MemoryContextStrdup(TopMemoryContext, remote_host);
+
+ /*
+ * Also resolve the proxy's own address so that it can be reported
+ * separately via the %H and %R log_line_prefix escapes. As with the
+ * client address above, log_hostname controls whether a reverse DNS
+ * lookup is attempted.
+ */
+ proxy_host[0] = '\0';
+ proxy_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&port->proxy_addr.addr, port->proxy_addr.salen,
+ proxy_host, sizeof(proxy_host),
+ proxy_port, sizeof(proxy_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ port->proxy_host = MemoryContextStrdup(TopMemoryContext, proxy_host);
+ port->proxy_port = MemoryContextStrdup(TopMemoryContext, proxy_port);
+
+ if (log_hostname &&
+ ret == 0 &&
+ strspn(proxy_host, "0123456789.") < strlen(proxy_host) &&
+ strspn(proxy_host, "0123456789ABCDEFabcdef:") < strlen(proxy_host))
+ port->proxy_hostname = MemoryContextStrdup(TopMemoryContext, proxy_host);
+ }
+
/*
* If we're going to reject the connection due to database state, say so
* now instead of wasting cycles on an authentication exchange. (This also
@@ -486,11 +546,13 @@ static int
ProcessStartupPacket(Port *port)
{
int32 len;
+ char firstbytes[4]; /* raw first 4 bytes, for PROXY detection */
char *buf = NULL;
ProtocolVersion proto;
MemoryContext oldcontext;
bool gss_done;
bool ssl_done;
+ bool proxy_done;
/*
* Set ssl_done and/or gss_done when negotiation of an encrypted layer
@@ -502,6 +564,7 @@ ProcessStartupPacket(Port *port)
*/
gss_done = false;
ssl_done = false;
+ proxy_done = false;
retry:
pq_startmsgread();
@@ -537,15 +600,62 @@ retry:
goto fail;
}
+ /* Preserve the raw, network-order length bytes for PROXY detection. */
+ memcpy(firstbytes, &len, 4);
+
len = pg_ntoh32(len);
len -= 4;
if (len < (int32) sizeof(ProtocolVersion) ||
len > MAX_STARTUP_PACKET_LENGTH)
+ {
+ /*
+ * The length looks invalid. Before rejecting the connection, check
+ * whether these bytes are actually the start of a PROXY protocol
+ * header sent by a trusted proxy (see proxy_networks). A genuine
+ * startup packet always begins with two zero bytes, so this test
+ * never misfires on real client traffic. If a header is found and
+ * accepted, port->raddr is replaced with the real client address and
+ * we loop back to read the genuine startup packet.
+ */
+ if (!ssl_done && !gss_done && !proxy_done && ProxyProtocolEnabled())
+ {
+ switch (ProcessProxyProtocol(port, firstbytes))
+ {
+ case PROXY_PROTO_DONE:
+ proxy_done = true;
+
+ /*
+ * A direct SSL negotiation happens before PROXY header
+ * detection. Run it now, after consuming the header.
+ */
+ if (ProcessSSLStartup(port) != STATUS_OK)
+ goto fail;
+ goto retry;
+ case PROXY_PROTO_ERROR:
+ case PROXY_PROTO_NONE:
+ break;
+ }
+ }
+
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete startup packet")));
+ goto fail;
+ }
+
+ /*
+ * We have a plausible startup, cancel, or negotiation packet. If it
+ * arrived from a trusted proxy network but was not preceded by a PROXY
+ * header (proxy_done is still unset), reject it. Such peers must
+ * announce the real client via the PROXY protocol, which has to come
+ * first, ahead of any SSL or GSS negotiation.
+ */
+ if (!proxy_done && ProxyProtocolRequired(port))
{
ereport(COMMERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
- errmsg("invalid length of startup packet")));
+ errmsg("connection from a trusted proxy network must use the PROXY protocol")));
goto fail;
}
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index d685fc5cd87..be51f2d52a5 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -52,6 +52,7 @@ PgBackendStatus *MyBEEntry = NULL;
static PgBackendStatus *BackendStatusArray = NULL;
static char *BackendAppnameBuffer = NULL;
static char *BackendClientHostnameBuffer = NULL;
+static char *BackendProxyHostnameBuffer = NULL;
static char *BackendActivityBuffer = NULL;
static Size BackendActivityBufferSize = 0;
#ifdef USE_SSL
@@ -106,6 +107,11 @@ BackendStatusShmemRequest(void *arg)
.ptr = (void **) &BackendClientHostnameBuffer,
);
+ ShmemRequestStruct(.name = "Backend Proxy Host Name Buffer",
+ .size = mul_size(NAMEDATALEN, NumBackendStatSlots),
+ .ptr = (void **) &BackendProxyHostnameBuffer,
+ );
+
BackendActivityBufferSize = mul_size(pgstat_track_activity_query_size,
NumBackendStatSlots);
ShmemRequestStruct(.name = "Backend Activity Buffer",
@@ -154,6 +160,14 @@ BackendStatusShmemInit(void *arg)
buffer += NAMEDATALEN;
}
+ /* Initialize st_proxyhostname pointers. */
+ buffer = BackendProxyHostnameBuffer;
+ for (i = 0; i < NumBackendStatSlots; i++)
+ {
+ BackendStatusArray[i].st_proxyhostname = buffer;
+ buffer += NAMEDATALEN;
+ }
+
/* Initialize st_activity pointers. */
buffer = BackendActivityBuffer;
for (i = 0; i < NumBackendStatSlots; i++)
@@ -279,6 +293,12 @@ pgstat_bestart_initial(void)
else
MemSet(&lbeentry.st_clientaddr, 0, sizeof(lbeentry.st_clientaddr));
+ if (MyProcPort && MyProcPort->proxy_protocol)
+ memcpy(&lbeentry.st_proxyaddr, &MyProcPort->proxy_addr,
+ sizeof(lbeentry.st_proxyaddr));
+ else
+ MemSet(&lbeentry.st_proxyaddr, 0, sizeof(lbeentry.st_proxyaddr));
+
lbeentry.st_ssl = false;
lbeentry.st_gss = false;
@@ -319,10 +339,18 @@ pgstat_bestart_initial(void)
NAMEDATALEN);
else
lbeentry.st_clienthostname[0] = '\0';
+
+ if (MyProcPort && MyProcPort->proxy_hostname)
+ strlcpy(lbeentry.st_proxyhostname, MyProcPort->proxy_hostname,
+ NAMEDATALEN);
+ else
+ lbeentry.st_proxyhostname[0] = '\0';
+
lbeentry.st_activity_raw[0] = '\0';
/* Also make sure the last byte in each string area is always 0 */
lbeentry.st_appname[NAMEDATALEN - 1] = '\0';
lbeentry.st_clienthostname[NAMEDATALEN - 1] = '\0';
+ lbeentry.st_proxyhostname[NAMEDATALEN - 1] = '\0';
lbeentry.st_activity_raw[pgstat_track_activity_query_size - 1] = '\0';
/* These structs can just start from zeroes each time */
@@ -789,6 +817,7 @@ pgstat_read_current_status(void)
LocalPgBackendStatus *localentry;
char *localappname,
*localclienthostname,
+ *localproxyhostname,
*localactivity;
#ifdef USE_SSL
PgBackendSSLStatus *localsslstatus;
@@ -820,6 +849,9 @@ pgstat_read_current_status(void)
localclienthostname = (char *)
MemoryContextAlloc(backendStatusSnapContext,
NAMEDATALEN * NumBackendStatSlots);
+ localproxyhostname = (char *)
+ MemoryContextAlloc(backendStatusSnapContext,
+ NAMEDATALEN * NumBackendStatSlots);
localactivity = (char *)
MemoryContextAllocHuge(backendStatusSnapContext,
(Size) pgstat_track_activity_query_size *
@@ -873,6 +905,8 @@ pgstat_read_current_status(void)
localentry->backendStatus.st_appname = localappname;
strcpy(localclienthostname, beentry->st_clienthostname);
localentry->backendStatus.st_clienthostname = localclienthostname;
+ strcpy(localproxyhostname, beentry->st_proxyhostname);
+ localentry->backendStatus.st_proxyhostname = localproxyhostname;
strcpy(localactivity, beentry->st_activity_raw);
localentry->backendStatus.st_activity_raw = localactivity;
#ifdef USE_SSL
@@ -920,6 +954,7 @@ pgstat_read_current_status(void)
localentry++;
localappname += NAMEDATALEN;
localclienthostname += NAMEDATALEN;
+ localproxyhostname += NAMEDATALEN;
localactivity += pgstat_track_activity_query_size;
#ifdef USE_SSL
localsslstatus++;
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 6f9c9c72de5..626ee377bfd 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -355,7 +355,7 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
Datum
pg_stat_get_activity(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_ACTIVITY_COLS 31
+#define PG_STAT_GET_ACTIVITY_COLS 34
int num_backends = pgstat_fetch_stat_numbackends();
int curr_backend;
int pid = PG_ARGISNULL(0) ? -1 : PG_GETARG_INT32(0);
@@ -669,6 +669,60 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
nulls[30] = true;
else
values[30] = Int64GetDatum(beentry->st_query_id);
+
+ /* Proxy information */
+ if (pg_memory_is_all_zeros(&beentry->st_proxyaddr,
+ sizeof(beentry->st_proxyaddr)))
+ {
+ nulls[31] = true;
+ nulls[32] = true;
+ nulls[33] = true;
+ }
+ else if (beentry->st_proxyaddr.addr.ss_family == AF_INET ||
+ beentry->st_proxyaddr.addr.ss_family == AF_INET6)
+ {
+ char proxy_host[NI_MAXHOST];
+ char proxy_port[NI_MAXSERV];
+ int ret;
+
+ proxy_host[0] = '\0';
+ proxy_port[0] = '\0';
+ ret = pg_getnameinfo_all(&beentry->st_proxyaddr.addr,
+ beentry->st_proxyaddr.salen,
+ proxy_host, sizeof(proxy_host),
+ proxy_port, sizeof(proxy_port),
+ NI_NUMERICHOST | NI_NUMERICSERV);
+ if (ret == 0)
+ {
+ clean_ipv6_addr(beentry->st_proxyaddr.addr.ss_family, proxy_host);
+ values[31] = DirectFunctionCall1(inet_in,
+ CStringGetDatum(proxy_host));
+ if (beentry->st_proxyhostname &&
+ beentry->st_proxyhostname[0])
+ values[32] = CStringGetTextDatum(beentry->st_proxyhostname);
+ else
+ nulls[32] = true;
+ values[33] = Int32GetDatum(atoi(proxy_port));
+ }
+ else
+ {
+ nulls[31] = true;
+ nulls[32] = true;
+ nulls[33] = true;
+ }
+ }
+ else if (beentry->st_proxyaddr.addr.ss_family == AF_UNIX)
+ {
+ nulls[31] = true;
+ nulls[32] = true;
+ values[33] = Int32GetDatum(-1);
+ }
+ else
+ {
+ nulls[31] = true;
+ nulls[32] = true;
+ nulls[33] = true;
+ }
}
else
{
@@ -698,6 +752,9 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
nulls[28] = true;
nulls[29] = true;
nulls[30] = true;
+ nulls[31] = true;
+ nulls[32] = true;
+ nulls[33] = true;
}
tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/src/backend/utils/error/csvlog.c b/src/backend/utils/error/csvlog.c
index 2b2b9484bdc..988fb4973e0 100644
--- a/src/backend/utils/error/csvlog.c
+++ b/src/backend/utils/error/csvlog.c
@@ -249,7 +249,20 @@ write_csvlog(ErrorData *edata)
/* query id */
appendStringInfo(&buf, "%" PRId64, pgstat_get_my_query_id());
+ appendStringInfoChar(&buf, ',');
+ /* Proxy host and port */
+ if (MyProcPort && MyProcPort->proxy_host && MyProcPort->proxy_host[0] != '\0')
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoString(&buf, MyProcPort->proxy_host);
+ if (MyProcPort->proxy_port && MyProcPort->proxy_port[0] != '\0')
+ {
+ appendStringInfoChar(&buf, ':');
+ appendStringInfoString(&buf, MyProcPort->proxy_port);
+ }
+ appendStringInfoChar(&buf, '"');
+ }
appendStringInfoChar(&buf, '\n');
/* If in the syslogger process, try to write messages direct to file */
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 50c53b571a0..1573fb02c77 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -3595,6 +3595,55 @@ log_status_format(StringInfo buf, const char *format, ErrorData *edata)
appendStringInfoSpaces(buf,
padding > 0 ? padding : -padding);
break;
+ case 'H':
+ if (MyProcPort && MyProcPort->proxy_host)
+ {
+ if (padding != 0)
+ appendStringInfo(buf, "%*s", padding, MyProcPort->proxy_host);
+ else
+ appendStringInfoString(buf, MyProcPort->proxy_host);
+ }
+ else if (padding != 0)
+ appendStringInfoSpaces(buf,
+ padding > 0 ? padding : -padding);
+ break;
+ case 'R':
+ if (MyProcPort && MyProcPort->proxy_host)
+ {
+ if (padding != 0)
+ {
+ if (MyProcPort->proxy_port && MyProcPort->proxy_port[0] != '\0')
+ {
+ /*
+ * As with remote port, the port number may be appended
+ * appended onto the end, so build a single string
+ * containing the proxy_host and optionally the
+ * proxy_port (if set) so we can properly align
+ * it.
+ */
+ char *hostport;
+
+ hostport = psprintf("%s(%s)", MyProcPort->proxy_host, MyProcPort->proxy_port);
+ appendStringInfo(buf, "%*s", padding, hostport);
+ pfree(hostport);
+ }
+ else
+ appendStringInfo(buf, "%*s", padding, MyProcPort->proxy_host);
+ }
+ else
+ {
+ /* padding is 0, so we don't need a temp buffer */
+ appendStringInfoString(buf, MyProcPort->proxy_host);
+ if (MyProcPort->proxy_port &&
+ MyProcPort->proxy_port[0] != '\0')
+ appendStringInfo(buf, "(%s)",
+ MyProcPort->proxy_port);
+ }
+ }
+ else if (padding != 0)
+ appendStringInfoSpaces(buf,
+ padding > 0 ? padding : -padding);
+ break;
case 'q':
/* in postmaster and friends, stop if %q is seen */
/* in a backend, just ignore */
diff --git a/src/backend/utils/error/jsonlog.c b/src/backend/utils/error/jsonlog.c
index e5ba22794d2..cea1e42e6e9 100644
--- a/src/backend/utils/error/jsonlog.c
+++ b/src/backend/utils/error/jsonlog.c
@@ -167,6 +167,14 @@ write_jsonlog(ErrorData *edata)
appendJSONKeyValue(&buf, "remote_port", MyProcPort->remote_port, false);
}
+ /* Proxy host and port */
+ if (MyProcPort && MyProcPort->proxy_host && MyProcPort->proxy_host[0] != '\0')
+ {
+ appendJSONKeyValue(&buf, "proxy_host", MyProcPort->proxy_host, true);
+ if (MyProcPort->proxy_port && MyProcPort->proxy_port[0] != '\0')
+ appendJSONKeyValue(&buf, "proxy_port", MyProcPort->proxy_port, false);
+ }
+
/* Session id */
appendJSONKeyValueFmt(&buf, "session_id", true, "%" PRIx64 ".%x",
MyStartTime, MyProcPid);
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index afaa058b046..d99b1542945 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2404,6 +2404,16 @@
check_hook => 'check_primary_slot_name',
},
+{ name => 'proxy_networks', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SETTINGS',
+ short_desc => 'Sets the networks from which PROXY protocol headers are accepted.',
+ long_desc => 'A comma-separated list of CIDR networks or the special token "unix" for Unix-domain socket. A connection whose peer address matches one of these networks must prepend a PROXY protocol header to declare the real client address. An empty string disables PROXY protocol support.',
+ flags => 'GUC_LIST_INPUT',
+ variable => 'ProxyNetworks',
+ boot_val => '""',
+ check_hook => 'check_proxy_networks',
+ assign_hook => 'assign_proxy_networks',
+},
+
{ name => 'quote_all_identifiers', type => 'bool', context => 'PGC_USERSET', group => 'COMPAT_OPTIONS_PREVIOUS',
short_desc => 'When generating SQL fragments, quote all identifiers.',
variable => 'quote_all_identifiers',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 290ccbc543e..f2ed4246882 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -54,6 +54,7 @@
#include "libpq/auth.h"
#include "libpq/libpq.h"
#include "libpq/oauth.h"
+#include "libpq/proxy_protocol.h"
#include "libpq/scram.h"
#include "nodes/queryjumble.h"
#include "optimizer/cost.h"
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ac38cddaaf9..337cc6861d3 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -64,6 +64,9 @@
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
+#proxy_networks = '' # comma-separated list of trusted proxy CIDRs
+ # or "unix" for Unix-socket peers sending the
+ # PROXY protocol header
#max_connections = 100 # (change requires restart)
#reserved_connections = 0 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..3944f518371 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5690,9 +5690,9 @@
proname => 'pg_stat_get_activity', prorows => '100', proisstrict => 'f',
proretset => 't', provolatile => 's', proparallel => 'r',
prorettype => 'record', proargtypes => 'int4',
- proallargtypes => '{int4,oid,int4,oid,text,text,text,text,text,timestamptz,timestamptz,timestamptz,timestamptz,inet,text,int4,xid,xid,text,bool,text,text,int4,text,numeric,text,bool,text,bool,bool,int4,int8}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{pid,datid,pid,usesysid,application_name,state,query,wait_event_type,wait_event,xact_start,query_start,backend_start,state_change,client_addr,client_hostname,client_port,backend_xid,backend_xmin,backend_type,ssl,sslversion,sslcipher,sslbits,ssl_client_dn,ssl_client_serial,ssl_issuer_dn,gss_auth,gss_princ,gss_enc,gss_delegation,leader_pid,query_id}',
+ proallargtypes => '{int4,oid,int4,oid,text,text,text,text,text,timestamptz,timestamptz,timestamptz,timestamptz,inet,text,int4,xid,xid,text,bool,text,text,int4,text,numeric,text,bool,text,bool,bool,int4,int8,inet,text,int4}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{pid,datid,pid,usesysid,application_name,state,query,wait_event_type,wait_event,xact_start,query_start,backend_start,state_change,client_addr,client_hostname,client_port,backend_xid,backend_xmin,backend_type,ssl,sslversion,sslcipher,sslbits,ssl_client_dn,ssl_client_serial,ssl_issuer_dn,gss_auth,gss_princ,gss_enc,gss_delegation,leader_pid,query_id,proxy_addr,proxy_hostname,proxy_port}',
prosrc => 'pg_stat_get_activity' },
{ oid => '6318', descr => 'describe wait events',
proname => 'pg_get_wait_events', procost => '10', prorows => '250',
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..60454df4b79 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -138,6 +138,12 @@ typedef struct Port
int remote_hostname_resolv; /* see above */
int remote_hostname_errcode; /* see above */
char *remote_port; /* text rep of remote port */
+ bool proxy_protocol; /* whether this connection used the PROXY
+ protocol */
+ SockAddr proxy_addr; /* real TCP peer (the proxy itself) */
+ char *proxy_host; /* name (or ip addr) of the proxy */
+ char *proxy_hostname; /* name (not ip addr) of the proxy */
+ char *proxy_port; /* text rep of the proxy's port */
/* local_host is filled only if needed (see log_status_format) */
char local_host[64]; /* ip addr of local socket for client conn */
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index d15073a0a93..0ff4fc64584 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -75,6 +75,7 @@ extern void TouchSocketFiles(void);
extern void RemoveSocketFiles(void);
extern Port *pq_init(ClientSocket *client_sock);
extern int pq_getbytes(void *b, size_t len);
+extern int pq_discardbytes(size_t len);
extern void pq_startmsgread(void);
extern void pq_endmsgread(void);
extern bool pq_is_reading_msg(void);
diff --git a/src/include/libpq/proxy_protocol.h b/src/include/libpq/proxy_protocol.h
new file mode 100644
index 00000000000..06e99bb570a
--- /dev/null
+++ b/src/include/libpq/proxy_protocol.h
@@ -0,0 +1,38 @@
+/*-------------------------------------------------------------------------
+ *
+ * proxy_protocol.h
+ * Interface of libpq/proxy_protocol.c
+ *
+ * src/include/libpq/proxy_protocol.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PROXY_PROTOCOL_H
+#define PROXY_PROTOCOL_H
+
+#include "libpq/libpq-be.h"
+
+/* Comma-separated list of networks allowed to send PROXY headers. */
+extern PGDLLIMPORT char *ProxyNetworks;
+
+/* Types of PROXY protocol parsing results. */
+typedef enum ProxyProtocolResult
+{
+ PROXY_PROTO_NONE, /* not a PROXY header (or not from a trusted
+ * source). Caller should handle the data
+ * normally */
+ PROXY_PROTO_DONE, /* a valid PROXY header was consumed, port has
+ * been updated and the real startup packet
+ * should now be read */
+ PROXY_PROTO_ERROR, /* a PROXY header from a trusted source was
+ * malformed, the connection must be closed */
+} ProxyProtocolResult;
+
+extern bool ProxyProtocolEnabled(void);
+
+extern bool ProxyProtocolRequired(Port *port);
+
+extern ProxyProtocolResult ProcessProxyProtocol(Port *port,
+ const char firstbytes[4]);
+
+#endif
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index a334e096e4a..7b71e0d5440 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -133,6 +133,10 @@ typedef struct PgBackendStatus
SockAddr st_clientaddr;
char *st_clienthostname; /* MUST be null-terminated */
+ /* Proxy information */
+ SockAddr st_proxyaddr;
+ char *st_proxyhostname; /* MUST be null-terminated */
+
/* Information about SSL connection */
bool st_ssl;
PgBackendSSLStatus *st_sslstatus;
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 307f4fbaefe..b75a28fdc28 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -95,6 +95,9 @@ extern bool check_multixact_offset_buffers(int *newval, void **extra,
extern bool check_notify_buffers(int *newval, void **extra, GucSource source);
extern bool check_primary_slot_name(char **newval, void **extra,
GucSource source);
+extern bool check_proxy_networks(char **newval, void **extra,
+ GucSource source);
+extern void assign_proxy_networks(const char *newval, void *extra);
extern bool check_random_seed(double *newval, void **extra, GucSource source);
extern void assign_random_seed(double newval, void *extra);
extern const char *show_random_seed(void);
diff --git a/src/test/Makefile b/src/test/Makefile
index 3eb0a06abb4..2eb65073bc6 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -18,6 +18,7 @@ SUBDIRS = \
modules \
perl \
postmaster \
+ protocol \
recovery \
regress \
subscription
diff --git a/src/test/meson.build b/src/test/meson.build
index cd45cbf57fb..6844952d894 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -5,6 +5,7 @@ subdir('isolation')
subdir('authentication')
subdir('postmaster')
+subdir('protocol')
subdir('recovery')
subdir('subscription')
subdir('modules')
diff --git a/src/test/protocol/.gitignore b/src/test/protocol/.gitignore
new file mode 100644
index 00000000000..871e943d50e
--- /dev/null
+++ b/src/test/protocol/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/protocol/Makefile b/src/test/protocol/Makefile
new file mode 100644
index 00000000000..4228cf6d2a3
--- /dev/null
+++ b/src/test/protocol/Makefile
@@ -0,0 +1,25 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/protocol
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/protocol/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/protocol
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+export OPENSSL with_ssl
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
+
+clean distclean:
+ rm -rf tmp_check
diff --git a/src/test/protocol/README b/src/test/protocol/README
new file mode 100644
index 00000000000..6eb3d5a000e
--- /dev/null
+++ b/src/test/protocol/README
@@ -0,0 +1,28 @@
+src/test/protocol/README
+
+Regression tests for wire protocol features
+===========================================
+
+This directory contains a test suite for features of the frontend/backend
+wire protocol that are exercised by driving raw connections, such as the
+PROXY protocol.
+
+
+Running the tests
+=================
+
+NOTE: You must have given the --enable-tap-tests argument to configure.
+
+Run
+ make check
+or
+ make installcheck
+You can use "make installcheck" if you previously did "make install".
+In that case, the code in the installation tree is tested. With
+"make check", a temporary installation tree is built from the current
+sources and then tested.
+
+Either way, this test initializes, starts, and stops a test Postgres
+cluster.
+
+See src/test/perl/README for more info about running these tests.
diff --git a/src/test/protocol/meson.build b/src/test/protocol/meson.build
new file mode 100644
index 00000000000..27975e58743
--- /dev/null
+++ b/src/test/protocol/meson.build
@@ -0,0 +1,17 @@
+# Copyright (c) 2022-2026, PostgreSQL Global Development Group
+
+tests += {
+ 'name': 'protocol',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'tap': {
+ 'env': {
+ 'with_ssl': ssl_library,
+ 'OPENSSL': openssl.found() ? openssl.full_path() : '',
+ },
+ 'tests': [
+ 't/001_proxy_protocol.pl',
+ 't/002_proxy_protocol_ssl.pl',
+ ],
+ },
+}
diff --git a/src/test/protocol/t/001_proxy_protocol.pl b/src/test/protocol/t/001_proxy_protocol.pl
new file mode 100644
index 00000000000..7140e98e15d
--- /dev/null
+++ b/src/test/protocol/t/001_proxy_protocol.pl
@@ -0,0 +1,515 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Tests for the PROXY protocol.
+
+use strict;
+use warnings FATAL => 'all';
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use ProxyProtocol;
+
+my $host = '127.0.0.1';
+
+# Network ranges trusted as proxies
+my $loopback_net = "$host/32";
+my $loopback_v6_net = '::1/128';
+my $client_net = '192.0.2.0/24'; # TEST-NET-1, v1 TCP4 client
+my $client_v4_net = '198.51.100.0/24'; # TEST-NET-2, v2 TCP4 client
+my $client_v6_net = '2001:db8::/20'; # documentation range, TCP6 clients
+my $ident_net = '203.0.113.0/24'; # TEST-NET-3, mapped to ident
+my $untrusted_net = '10.0.0.0/8'; # a network the loopback peer is not in
+
+# Client (source) addresses carried in the PROXY headers.
+my $v1_client_v4 = '192.0.2.1'; # in $client_net
+my $v2_client_v4 = '198.51.100.5'; # in $client_v4_net
+my $v1_client_v6 = '2001:db8::1';
+my $v2_client_v6 = '2001:db8::dead';
+my $ident_client = '203.0.113.20'; # in $ident_net
+my $unused_dest_v4 = '198.51.100.9';
+my $unused_dest_v6 = '2001:db8::2';
+
+# Source ports declared in the PROXY headers
+my $v1_client_port = 56324;
+my $v2_client_port = 40000;
+my $unused_dest_port = 5432;
+
+my $node = PostgreSQL::Test::Cluster->new('proxy_protocol');
+$node->init;
+$node->append_conf('postgresql.conf',
+ "listen_addresses = '$host'\n"
+ . "proxy_networks = '$loopback_net'\n"
+ . "log_line_prefix = 'PXLOG h=%h H=%H r=%r R=%R '\n"
+ . "log_statement = 'all'\n");
+$node->append_conf('pg_hba.conf',
+ "host all all $loopback_net trust\n"
+ . "host all all $loopback_v6_net trust\n"
+ . "host all all $client_net trust\n"
+ . "host all all $client_v4_net trust\n"
+ . "host all all $client_v6_net trust\n"
+ . "host all all $ident_net ident\n");
+$node->start;
+
+my $port = $node->port;
+my $user = $node->safe_psql('postgres', 'SELECT current_user');
+
+set_connection(host => $host, port => $port, user => $user);
+
+my $unix_connect = sub { $node->raw_connect };
+
+# A query to check the client address.
+my $CLIENT_ADDR = 'SELECT host(inet_client_addr())';
+
+
+# ----------------------------------------------------------------------------
+# Protocol validation.
+# ----------------------------------------------------------------------------
+# Trusted peers over TCP.
+my $r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok($r->{ok}, 'v1 TCP4 header: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $v1_client_v4, 'v1 TCP4 header: client address substituted');
+
+$r = proxy_query(
+ proxy_v1(
+ 6, $v1_client_v6, $unused_dest_v6,
+ $v1_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok($r->{ok}, 'v1 TCP6 header: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $v1_client_v6, 'v1 TCP6 header: client address substituted');
+
+$r = proxy_query(
+ proxy_v2(
+ 4, $v2_client_v4, $unused_dest_v4,
+ $v2_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok($r->{ok}, 'v2 TCP4 header: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $v2_client_v4, 'v2 TCP4 header: client address substituted');
+
+$r = proxy_query(
+ proxy_v2(
+ 6, $v2_client_v6, $unused_dest_v6,
+ $v2_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok($r->{ok}, 'v2 TCP6 header: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $v2_client_v6, 'v2 TCP6 header: client address substituted');
+
+# TLV vectors after the address block are accepted and ignored.
+my $tlv = "\x04" . pack('n', 3) . "abc" # PP2_TYPE_NOOP, 3 bytes
+ . "\xee"
+ . pack('n', 600)
+ . ("\x00" x 600); # opaque type, spans many reads
+$r = proxy_query(
+ proxy_v2(
+ 4, $v2_client_v4, $unused_dest_v4,
+ $v2_client_port, $unused_dest_port, $tlv),
+ $CLIENT_ADDR);
+ok($r->{ok}, 'v2 header with trailing TLVs: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $v2_client_v4,
+ 'v2 header with trailing TLVs: TLVs ignored, client address substituted'
+);
+
+# v1 UNKNOWN keeps the real peer address.
+$r = proxy_query(proxy_v1_unknown(), $CLIENT_ADDR);
+ok($r->{ok}, 'v1 UNKNOWN header: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $host, 'v1 UNKNOWN header: real peer address kept');
+
+# v2 LOCAL keeps the real peer address.
+$r = proxy_query(proxy_v2_local(), $CLIENT_ADDR);
+ok($r->{ok}, 'v2 LOCAL header: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $host, 'v2 LOCAL header: real peer address kept');
+
+# Unix socket.
+SKIP:
+{
+ skip "Unix-domain sockets not in use on this platform", 6
+ unless $PostgreSQL::Test::Utils::use_unix_sockets
+ && $node->raw_connect_works;
+
+ $node->append_conf('postgresql.conf',
+ "proxy_networks = 'unix, $loopback_net'\n");
+ $node->reload;
+
+ # v1 TCP address from an Unix peer must pass
+ $r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ $CLIENT_ADDR,
+ $unix_connect);
+ ok($r->{ok}, 'unix: v1 header over a Unix socket succeeds')
+ or diag("error: $r->{error}");
+ is($r->{value}, $v1_client_v4,
+ 'unix: v1 header substitutes the parsed TCP client over a Unix socket'
+ );
+
+ # v2 TCP address from an Unix peer must pass
+ $r = proxy_query(
+ proxy_v2(
+ 4, $v2_client_v4, $unused_dest_v4,
+ $v2_client_port, $unused_dest_port),
+ $CLIENT_ADDR,
+ $unix_connect);
+ ok($r->{ok}, 'unix: v2 header over a Unix socket succeeds')
+ or diag("error: $r->{error}");
+ is($r->{value}, $v2_client_v4,
+ 'unix: v2 header substitutes the parsed TCP client over a Unix socket'
+ );
+
+ # A trusted peer must lead with a PROXY header
+ my $unix_logoff = -s $node->logfile;
+ $r = proxy_query(undef, $CLIENT_ADDR, $unix_connect);
+ ok(!$r->{ok},
+ 'unix: header-less connection over a Unix socket is rejected');
+ $node->wait_for_log(
+ qr/connection from a trusted proxy network must use the PROXY protocol/,
+ $unix_logoff);
+ ok(1,
+ 'unix: header-less Unix-socket connection logs the PROXY requirement'
+ );
+
+ $node->append_conf('postgresql.conf',
+ "proxy_networks = '$loopback_net'\n");
+ $node->reload;
+}
+
+# A trusted peer must lead with a PROXY header
+# For plain.
+my $logoff = -s $node->logfile;
+$r = proxy_query(undef, $CLIENT_ADDR);
+ok(!$r->{ok}, 'plain startup packet from a trusted peer is rejected');
+$node->wait_for_log(
+ qr/connection from a trusted proxy network must use the PROXY protocol/,
+ $logoff);
+ok(1, 'plain connection from a trusted network logs the PROXY requirement');
+
+# For SSL.
+$logoff = -s $node->logfile;
+$r = ssl_request();
+ok(!$r->{ok},
+ 'SSL negotiation request from a trusted peer with no PROXY header is rejected'
+);
+$node->wait_for_log(
+ qr/connection from a trusted proxy network must use the PROXY protocol/,
+ $logoff);
+ok(1,
+ 'SSL-first connection from a trusted network logs the PROXY requirement');
+
+# Reject ident authentication
+$logoff = -s $node->logfile;
+$r = proxy_query(
+ proxy_v1(
+ 4, $ident_client, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok(!$r->{ok}, 'ident authentication over a proxied connection is rejected');
+$node->wait_for_log(
+ qr/ident authentication is not supported over connections using the PROXY protocol/,
+ $logoff);
+ok(1, 'ident over the PROXY protocol logs that the method is unsupported');
+
+# Reject malformed header from a trusted peer.
+$logoff = -s $node->logfile;
+$r = proxy_query("PROXY BOGUS arguments here\r\n", $CLIENT_ADDR);
+ok(!$r->{ok}, 'malformed v1 header from trusted peer is rejected');
+$node->wait_for_log(qr/incomplete startup packet/, $logoff);
+ok(1, 'malformed header logs the generic "incomplete startup packet"');
+
+# Reject connection from an untrusted peer.
+$node->append_conf('postgresql.conf', "proxy_networks = '$untrusted_net'\n");
+$node->reload;
+
+$logoff = -s $node->logfile;
+$r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok(!$r->{ok}, 'header from untrusted peer is rejected');
+$node->wait_for_log(qr/incomplete startup packet/, $logoff);
+ok(1, 'untrusted header is handled as an incomplete startup packet');
+
+# Accept peers outside of the proxy networks without the PROXY header
+$r = proxy_query(undef, $CLIENT_ADDR);
+ok($r->{ok}, 'header-less connection from untrusted peer succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $host, 'real peer address reported for an ordinary connection');
+
+# Ignore header when proxy networks is empty
+$node->append_conf('postgresql.conf', "proxy_networks = ''\n");
+$node->reload;
+
+$logoff = -s $node->logfile;
+$r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok(!$r->{ok}, 'header ignored when proxy_networks is empty');
+$node->wait_for_log(qr/incomplete startup packet/, $logoff);
+ok(1, 'disabled feature handles header as an incomplete startup packet');
+
+# ----------------------------------------------------------------------------
+# GUC validation.
+# ----------------------------------------------------------------------------
+my ($ret, $stdout, $stderr);
+
+($ret, $stdout, $stderr) = $node->psql('postgres',
+ "ALTER SYSTEM SET proxy_networks = 'not-an-address'");
+isnt($ret, 0, 'invalid network specification is rejected');
+like(
+ $stderr,
+ qr/invalid value for parameter "proxy_networks"/,
+ 'invalid network: error mentions the parameter');
+like(
+ $stderr,
+ qr/Invalid network specification/,
+ 'invalid network: error shows the reason');
+
+($ret, $stdout, $stderr) =
+ $node->psql('postgres', "ALTER SYSTEM SET proxy_networks = '10.0.0.0/99'");
+isnt($ret, 0, 'invalid CIDR mask length is rejected');
+
+($ret, $stdout, $stderr) = $node->psql('postgres',
+ "ALTER SYSTEM SET proxy_networks = '$loopback_net, $untrusted_net, $loopback_v6_net'"
+);
+is($ret, 0, 'a list of valid networks is accepted')
+ or diag("stderr: $stderr");
+
+# Ensure the special "unix" token is accepted.
+($ret, $stdout, $stderr) = $node->psql('postgres',
+ "ALTER SYSTEM SET proxy_networks = 'unix, $loopback_net'");
+is($ret, 0, 'the "unix" token is accepted in proxy_networks')
+ or diag("stderr: $stderr");
+
+$node->safe_psql('postgres', 'ALTER SYSTEM RESET proxy_networks');
+
+
+# ----------------------------------------------------------------------------
+# pg_stat_activity validation.
+# ----------------------------------------------------------------------------
+$node->append_conf('postgresql.conf', "proxy_networks = '$loopback_net'\n");
+$node->reload;
+
+# v1 PROXY returns proxy_addr and proxy_port for the proxy and client_addr
+# and client_port are parsed from the header.
+my $STAT_PROXY = q{SELECT format('%s|%s|%s|%s|%s',
+ proxy_addr, (proxy_hostname IS NULL), (proxy_port IS NOT NULL),
+ client_addr, client_port)
+ FROM pg_stat_activity WHERE pid = pg_backend_pid()};
+
+$r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ $STAT_PROXY);
+ok($r->{ok}, 'pg_stat_activity over a proxied connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, "$host|t|t|$v1_client_v4|$v1_client_port",
+ 'pg_stat_activity: proxy_addr and port are the proxy, client_addr and port are the real peer'
+);
+
+# v2 LOCAL returns null for proxy_addr and proxy_port.
+my $STAT_PLAIN = q{SELECT format('%s|%s|%s',
+ (proxy_addr IS NULL), (proxy_port IS NULL), client_addr)
+ FROM pg_stat_activity WHERE pid = pg_backend_pid()};
+
+$r = proxy_query(proxy_v2_local(), $STAT_PLAIN);
+ok($r->{ok}, 'pg_stat_activity over a LOCAL connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, "t|t|$host",
+ 'pg_stat_activity: proxy_addr and port are null for a LOCAL command');
+
+# No PROXY header from untrusted network returns null for proxy_addr and
+# proxy_port.
+$node->append_conf('postgresql.conf', "proxy_networks = '$untrusted_net'\n");
+$node->reload;
+
+$r = proxy_query(undef, $STAT_PLAIN);
+ok($r->{ok},
+ 'pg_stat_activity over an ordinary connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, "t|t|$host",
+ 'pg_stat_activity: proxy_addr and port are null for ordinary connection'
+);
+
+SKIP:
+{
+ skip "Unix-domain sockets not in use on this platform", 2
+ unless $PostgreSQL::Test::Utils::use_unix_sockets
+ && $node->raw_connect_works;
+
+ $node->append_conf('postgresql.conf',
+ "proxy_networks = 'unix, $loopback_net'\n");
+ $node->reload;
+
+ my $stat_unix_proxy = q{SELECT format('%s|%s|%s|%s',
+ (proxy_addr IS NULL), proxy_port, client_addr, client_port)
+ FROM pg_stat_activity WHERE pid = pg_backend_pid()};
+ $r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ $stat_unix_proxy,
+ $unix_connect);
+ ok($r->{ok},
+ 'pg_stat_activity: connection over a proxied Unix socket succeeds'
+ ) or diag("error: $r->{error}");
+ is($r->{value}, "t|-1|$v1_client_v4|$v1_client_port",
+ 'pg_stat_activity: proxy_addr is NULL and proxy_port is -1, like a Unix-socket client'
+ );
+}
+
+# ----------------------------------------------------------------------------
+# Logs validation.
+# ----------------------------------------------------------------------------
+$node->append_conf('postgresql.conf', "proxy_networks = '$loopback_net'\n");
+$node->reload;
+
+# Check client and proxy escapes for a proxied connection.
+$logoff = -s $node->logfile;
+$r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ "SELECT 'pxlog-proxied'");
+ok($r->{ok}, 'logging: proxied connection succeeds')
+ or diag("error: $r->{error}");
+$node->wait_for_log(
+ qr{PXLOG h=\Q$v1_client_v4\E H=\Q$host\E r=\Q$v1_client_v4\E\($v1_client_port\) R=\Q$host\E\(\d+\) LOG:\s+statement: SELECT 'pxlog-proxied'},
+ $logoff);
+ok(1, 'logging: %h/%r show the client, %H/%R show the proxy');
+
+# Ensure proxy escapes are empty for an ordinary connection.
+$node->append_conf('postgresql.conf', "proxy_networks = '$untrusted_net'\n");
+$node->reload;
+
+$logoff = -s $node->logfile;
+$r = proxy_query(undef, "SELECT 'pxlog-ordinary'");
+ok($r->{ok}, 'logging over an ordinary connection succeeds')
+ or diag("error: $r->{error}");
+$node->wait_for_log(
+ qr{PXLOG h=\Q$host\E H= r=\Q$host\E\(\d+\) R= LOG:\s+statement: SELECT 'pxlog-ordinary'},
+ $logoff);
+ok(1, 'logging: %H/%R are empty without the PROXY protocol');
+
+# Test csvlog and jsonlog destinations.
+$node->append_conf(
+ 'postgresql.conf',
+ "logging_collector = on\n"
+ . "log_destination = 'stderr, csvlog, jsonlog'\n"
+ . "log_rotation_age = 0\n"
+ # Re-trust loopback for the proxied case below.
+ . "proxy_networks = '$loopback_net'\n");
+$node->restart;
+
+# Wait for the collector to report the csv/json file names.
+my $clf_path = $node->data_dir . '/current_logfiles';
+PostgreSQL::Test::Utils::wait_for_file($clf_path, qr/^csvlog /m);
+PostgreSQL::Test::Utils::wait_for_file($clf_path, qr/^jsonlog /m);
+my $current_logfiles = slurp_file($clf_path);
+
+$r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ "SELECT 'pxlog-loggers-proxied'");
+ok($r->{ok}, 'loggers: proxied connection succeeds')
+ or diag("error: $r->{error}");
+
+my $csvline =
+ wait_for_logger_line($node, $current_logfiles, 'csvlog',
+ 'pxlog-loggers-proxied');
+like($csvline, qr/"\Q$host\E:\d+"/,
+ 'csvlog: proxy_host:proxy_port detected');
+
+my $jsonline =
+ wait_for_logger_line($node, $current_logfiles, 'jsonlog',
+ 'pxlog-loggers-proxied');
+like($jsonline, qr/"proxy_host":"\Q$host\E"/,
+ 'jsonlog: proxy_host key holds the proxy host');
+like($jsonline, qr/"proxy_port":\d+/,
+ 'jsonlog: proxy_port key holds the proxy port');
+
+# Ordinary connection returns null for proxy_host and proxy_port.
+$node->append_conf('postgresql.conf', "proxy_networks = '$untrusted_net'\n");
+$node->reload;
+
+$r = proxy_query(undef, "SELECT 'pxlog-loggers-ordinary'");
+ok($r->{ok}, 'loggers: ordinary connection succeeds')
+ or diag("error: $r->{error}");
+
+$csvline =
+ wait_for_logger_line($node, $current_logfiles, 'csvlog',
+ 'pxlog-loggers-ordinary');
+like($csvline, qr/,$/,
+ 'csvlog: proxy_connection column is empty without the PROXY protocol');
+
+$jsonline =
+ wait_for_logger_line($node, $current_logfiles, 'jsonlog',
+ 'pxlog-loggers-ordinary');
+unlike($jsonline, qr/"proxy_host"/,
+ 'jsonlog: proxy_host key is omitted without the PROXY protocol');
+unlike($jsonline, qr/"proxy_port"/,
+ 'jsonlog: proxy_port key is omitted without the PROXY protocol');
+
+SKIP:
+{
+ skip "Unix-domain sockets not in use on this platform", 5
+ unless $PostgreSQL::Test::Utils::use_unix_sockets
+ && $node->raw_connect_works;
+
+ $node->append_conf('postgresql.conf',
+ "proxy_networks = 'unix, $loopback_net'\n");
+ $node->reload;
+
+ $r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ "SELECT 'pxlog-loggers-unix'",
+ $unix_connect);
+ ok($r->{ok}, 'loggers: proxied Unix-socket connection succeeds')
+ or diag("error: $r->{error}");
+
+ my $sl =
+ wait_for_logger_line($node, $current_logfiles, 'stderr',
+ 'pxlog-loggers-unix');
+ like(
+ $sl,
+ qr/h=\Q$v1_client_v4\E H=\[local\] r=\Q$v1_client_v4\E\($v1_client_port\) R=\[local\] /,
+ 'stderr: %H/%R returns [local] with no port for a Unix-socket proxy');
+
+ $csvline =
+ wait_for_logger_line($node, $current_logfiles, 'csvlog',
+ 'pxlog-loggers-unix');
+ like($csvline, qr/"\[local\]"$/,
+ 'csvlog: trailing proxy_connection returns [local] for a Unix-socket proxy'
+ );
+
+ $jsonline =
+ wait_for_logger_line($node, $current_logfiles, 'jsonlog',
+ 'pxlog-loggers-unix');
+ like($jsonline, qr/"proxy_host":"\[local\]"/,
+ 'jsonlog: proxy_host returns [local] for a Unix-socket proxy');
+ unlike($jsonline, qr/"proxy_port"/,
+ 'jsonlog: proxy_port key is omitted for Unix-socket proxy'
+ );
+}
+
+done_testing();
diff --git a/src/test/protocol/t/002_proxy_protocol_ssl.pl b/src/test/protocol/t/002_proxy_protocol_ssl.pl
new file mode 100644
index 00000000000..65ec596a59e
--- /dev/null
+++ b/src/test/protocol/t/002_proxy_protocol_ssl.pl
@@ -0,0 +1,115 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Tests for the PROXY protocol with sslnegotiation=direct.
+# Run only if OpenSSL is enabled with ALPN support.
+
+use strict;
+use warnings FATAL => 'all';
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use File::Copy qw(copy);
+use Test::More;
+
+use ProxyProtocol;
+
+if (($ENV{with_ssl} || '') ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+unless (eval { require IO::Socket::SSL; 1 })
+{
+ plan skip_all => 'IO::Socket::SSL not available';
+}
+unless (IO::Socket::SSL->can('can_alpn') && IO::Socket::SSL->can_alpn)
+{
+ plan skip_all => 'IO::Socket::SSL lacks ALPN support';
+}
+
+my $host = '127.0.0.1';
+
+# Network ranges trusted as proxies
+my $loopback_net = "$host/32";
+my $client_net = '192.0.2.0/24'; # TEST-NET-1, v1 TCP4 client
+
+# Client (source) addresses carried in the PROXY headers.
+my $v1_client = '192.0.2.1'; # in $client_net
+my $v2_client = '192.0.2.5'; # in $client_net
+my $unused_dest = '198.51.100.9';
+
+# Source ports declared in the PROXY headers
+my $v1_client_port = 56324;
+my $v2_client_port = 40000;
+my $unused_dest_port = 5432;
+
+# Reuse the committed test certificate from the SSL test suite.
+my $ssldir = "$FindBin::RealBin/../../ssl/ssl";
+plan skip_all => "test certificate not found in $ssldir"
+ unless -f "$ssldir/server-cn-only.crt";
+
+my $node = PostgreSQL::Test::Cluster->new('proxy_protocol_ssl');
+$node->init;
+
+my $certfile = $node->data_dir . '/server.crt';
+my $keyfile = $node->data_dir . '/server.key';
+copy("$ssldir/server-cn-only.crt", $certfile)
+ or die "could not copy server certificate: $!";
+copy("$ssldir/server-cn-only.key", $keyfile)
+ or die "could not copy server key: $!";
+chmod 0600, $keyfile or die "could not chmod server key: $!";
+
+$node->append_conf('postgresql.conf',
+ "listen_addresses = '$host'\n"
+ . "ssl = on\n"
+ . "ssl_cert_file = '$certfile'\n"
+ . "ssl_key_file = '$keyfile'\n"
+ . "proxy_networks = '$loopback_net'\n");
+$node->append_conf('pg_hba.conf',
+ "host all all $loopback_net trust\n"
+ . "host all all $client_net trust\n");
+$node->start;
+
+my $port = $node->port;
+my $user = $node->safe_psql('postgres', 'SELECT current_user');
+
+set_connection(host => $host, port => $port, user => $user);
+
+# A query to check the client address.
+my $CLIENT_ADDR = 'SELECT host(inet_client_addr())';
+
+# ----------------------------------------------------------------------------
+# Protocol validation.
+# ----------------------------------------------------------------------------
+my $r = proxy_query_with_ssl(
+ proxy_v1(4, $v1_client, $unused_dest, $v1_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok($r->{ok}, 'v1 header before direct SSL: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $v1_client,
+ 'v1 header before direct SSL: client address substituted');
+
+$r = proxy_query_with_ssl(
+ proxy_v2(4, $v2_client, $unused_dest, $v2_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok($r->{ok}, 'v2 header before direct SSL: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $v2_client,
+ 'v2 header before direct SSL: client address substituted');
+
+# ----------------------------------------------------------------------------
+# Logs validation.
+# ----------------------------------------------------------------------------
+my $logoff = -s $node->logfile;
+$r = proxy_query_with_ssl(undef, $CLIENT_ADDR);
+ok(!$r->{ok},
+ 'direct SSL with no PROXY header from a trusted peer is rejected');
+$node->wait_for_log(
+ qr/connection from a trusted proxy network must use the PROXY protocol/,
+ $logoff);
+ok(1, 'direct SSL without a header logs the PROXY requirement');
+
+done_testing();
diff --git a/src/test/protocol/t/ProxyProtocol.pm b/src/test/protocol/t/ProxyProtocol.pm
new file mode 100644
index 00000000000..078ea22e7a2
--- /dev/null
+++ b/src/test/protocol/t/ProxyProtocol.pm
@@ -0,0 +1,385 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+ProxyProtocol - helpers for driving PROXY protocol connections in TAP tests
+
+=head1 SYNOPSIS
+
+ use lib $FindBin::RealBin;
+ use ProxyProtocol;
+
+ # Build PROXY headers.
+ my $v1 = proxy_v1(4, '192.0.2.1', '198.51.100.9', 56324, 5432);
+ my $v2 = proxy_v2(4, '203.0.113.5', '198.51.100.9', 40000, 5432);
+
+ # Authenticate (trust method) and run one query over a connected socket.
+ my $addr = authenticate_and_query($sock, $user,
+ 'SELECT host(inet_client_addr())');
+
+ # Or bind the per-test connection details once and drive whole queries.
+ set_connection(host => $host, port => $port, user => $user);
+ my $r = proxy_query($v1, 'SELECT host(inet_client_addr())');
+
+=head1 DESCRIPTION
+
+A minimal hand-rolled implementation of the parts of the v3 frontend/backend
+protocol and the PROXY protocol that the regression tests need. It is enough
+to prepend a PROXY header, authenticate with the trust method, and run a single
+query over a raw socket, whether plaintext or TLS.
+
+=cut
+
+package ProxyProtocol;
+
+use strict;
+use warnings FATAL => 'all';
+use Exporter 'import';
+use Socket qw(inet_aton inet_pton AF_INET6);
+use IO::Socket::INET;
+
+our @EXPORT = qw(
+ startup_packet
+ read_message
+ error_text
+ proxy_v1
+ proxy_v1_unknown
+ proxy_v2
+ proxy_v2_local
+ authenticate_and_query
+ set_connection
+ proxy_query
+ ssl_request
+ proxy_query_with_ssl
+ logger_file_name
+ wait_for_logger_line
+);
+
+use constant PROXY_V2_SIG =>
+ "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a";
+
+sub startup_packet
+{
+ my (%params) = @_;
+ my $body = pack('N', 0x00030000); # protocol version 3.0
+ for my $key (sort keys %params)
+ {
+ $body .= $key . "\0" . $params{$key} . "\0";
+ }
+ $body .= "\0"; # empty key name terminates the list
+ return pack('N', length($body) + 4) . $body;
+}
+
+# Returns undef when the peer closed the connection before $n bytes arrived, so
+# callers can treat a server-side disconnect as a distinct outcome.
+sub read_exact
+{
+ my ($sock, $n) = @_;
+ my $buf = '';
+ while (length($buf) < $n)
+ {
+ my $chunk;
+ my $r = sysread($sock, $chunk, $n - length($buf));
+ return if !defined $r || $r == 0;
+ $buf .= $chunk;
+ }
+ return $buf;
+}
+
+# Read one typed backend message. Returns ($type, $payload), or an empty list
+# once the connection has closed.
+sub read_message
+{
+ my ($sock) = @_;
+ my $type = read_exact($sock, 1);
+ return () unless defined $type;
+ my $lenbytes = read_exact($sock, 4);
+ return () unless defined $lenbytes;
+ my $len = unpack('N', $lenbytes);
+ my $payload = '';
+ if ($len > 4)
+ {
+ $payload = read_exact($sock, $len - 4);
+ return () unless defined $payload;
+ }
+ return ($type, $payload);
+}
+
+# Pull the human-readable text (the 'M' field) out of an ErrorResponse or
+# NoticeResponse body, which is the only part the tests assert on.
+sub error_text
+{
+ my ($payload) = @_;
+ for my $field (split /\0/, $payload)
+ {
+ return substr($field, 1) if substr($field, 0, 1) eq 'M';
+ }
+ return '';
+}
+
+# Build the PROXY v1 payload.
+sub proxy_v1
+{
+ my ($family, $src, $dst, $sport, $dport) = @_;
+ my $proto = $family == 4 ? 'TCP4' : 'TCP6';
+ return "PROXY $proto $src $dst $sport $dport\r\n";
+}
+
+# Build the PROXY v1 payload for the unknown command.
+sub proxy_v1_unknown
+{
+ return "PROXY UNKNOWN\r\n";
+}
+
+# Build the PROXY v2 payload.
+sub proxy_v2
+{
+ my ($family, $src, $dst, $sport, $dport, $tlv) = @_;
+ $tlv = '' unless defined $tlv;
+ my $vercmd = "\x21"; # version 2, command PROXY
+ my ($fambyte, $addr);
+ if ($family == 4)
+ {
+ $fambyte = "\x11"; # TCP4
+ $addr =
+ inet_aton($src)
+ . inet_aton($dst)
+ . pack('n', $sport)
+ . pack('n', $dport);
+ }
+ else
+ {
+ $fambyte = "\x21"; # TCP6
+ $addr =
+ inet_pton(AF_INET6, $src)
+ . inet_pton(AF_INET6, $dst)
+ . pack('n', $sport)
+ . pack('n', $dport);
+ }
+ return
+ PROXY_V2_SIG
+ . $vercmd
+ . $fambyte
+ . pack('n', length($addr) + length($tlv))
+ . $addr
+ . $tlv;
+}
+
+# Build the PROXY v2 payload for the LOCAL command.
+sub proxy_v2_local
+{
+ return PROXY_V2_SIG . "\x20" . "\x00"
+ . pack('n', 0); # command LOCAL, AF_UNSPEC
+}
+
+# Drive a full session over an already-connected stream, whether plaintext or
+# TLS, and return the first column of the first row (undef for NULL or no row).
+# Dies with a descriptive message on EOF, a server ErrorResponse, or a non-zero
+# authentication request, so callers can map any failure to a single eval.
+sub authenticate_and_query
+{
+ my ($stream, $user, $query) = @_;
+
+ print $stream startup_packet(user => $user, database => 'postgres');
+
+ while (1)
+ {
+ my ($type, $payload) = read_message($stream);
+ defined $type or die "connection closed during startup\n";
+ die error_text($payload) . "\n" if $type eq 'E';
+ if ($type eq 'R')
+ {
+ my $code = unpack('N', $payload);
+ die "authentication required (code $code)\n" if $code != 0;
+ }
+ last if $type eq 'Z'; # ReadyForQuery
+ }
+
+ print $stream 'Q' . pack('N', length($query) + 5) . $query . "\0";
+
+ my $value;
+ while (1)
+ {
+ my ($type, $payload) = read_message($stream);
+ defined $type or die "connection closed during query\n";
+ die error_text($payload) . "\n" if $type eq 'E';
+ if ($type eq 'D') # DataRow
+ {
+ my $ncols = unpack('n', substr($payload, 0, 2));
+ if ($ncols >= 1)
+ {
+ my $collen = unpack('N', substr($payload, 2, 4));
+ $value =
+ ($collen == 0xFFFFFFFF)
+ ? undef
+ : substr($payload, 6, $collen);
+ }
+ }
+ last if $type eq 'Z';
+ }
+
+ return $value;
+}
+
+# The per-test connection details that the query drivers below reuse, bound once
+# with set_connection() so the individual calls need only the header and query.
+my ($conn_host, $conn_port, $conn_user);
+
+sub set_connection
+{
+ my (%params) = @_;
+ $conn_host = $params{host};
+ $conn_port = $params{port};
+ $conn_user = $params{user};
+ return;
+}
+
+# Connect to the server, optionally sending $prefix (a PROXY header) ahead of
+# the startup packet, then authenticate and run a one-column query. The
+# connection defaults to TCP loopback. Pass $connect, a coderef returning a
+# connected socket, to use another transport such as a Unix socket.
+#
+# Returns a hash-ref with ok => 1 and the query value on success, or ok => 0
+# and an error string otherwise.
+sub proxy_query
+{
+ my ($prefix, $query, $connect) = @_;
+ my $result = { ok => 0, error => 'unknown' };
+
+ eval {
+ local $SIG{ALRM} = sub { die "timeout\n" };
+ alarm($PostgreSQL::Test::Utils::timeout_default);
+
+ my $sock =
+ $connect
+ ? $connect->()
+ : IO::Socket::INET->new(
+ PeerHost => $conn_host,
+ PeerPort => $conn_port,
+ Proto => 'tcp');
+ die "cannot connect: $!\n" unless $sock;
+ $sock->autoflush(1);
+
+ print $sock $prefix if defined $prefix;
+ my $value = authenticate_and_query($sock, $conn_user, $query);
+
+ close $sock;
+ alarm(0);
+ $result = { ok => 1, value => $value };
+ };
+ return { ok => 0, error => $@ } if $@;
+ return $result;
+}
+
+# Open a raw connection and send an SSL negotiation request as the very first
+# bytes, with no preceding PROXY header.
+#
+# Returns a hash-ref with ok => 1 if the request succeeded, or ok => 0 and an
+# error string if it failed.
+sub ssl_request
+{
+ my $result = { ok => 0, error => 'unknown' };
+
+ eval {
+ local $SIG{ALRM} = sub { die "timeout\n" };
+ alarm($PostgreSQL::Test::Utils::timeout_default);
+
+ my $sock = IO::Socket::INET->new(
+ PeerHost => $conn_host,
+ PeerPort => $conn_port,
+ Proto => 'tcp') or die "cannot connect: $!\n";
+ $sock->autoflush(1);
+
+ # SSLRequest is a length of 8 followed by the negotiate-SSL request code.
+ print $sock pack('N', 8) . pack('N', 80877103);
+
+ while (1)
+ {
+ my ($type, $payload) = read_message($sock);
+ die "connection closed\n" unless defined $type;
+ die error_text($payload) . "\n" if $type eq 'E';
+ last if $type eq 'Z';
+ }
+ close $sock;
+ alarm(0);
+ $result = { ok => 1 };
+ };
+ return { ok => 0, error => $@ } if $@;
+ return $result;
+}
+
+# Connect over TCP, send $prefix (a PROXY header) in cleartext, then open a
+# direct SSL connection (no SSLRequest) offering the PostgreSQL ALPN protocol,
+# authenticate over TLS, and run a one-column query. The caller must have
+# loaded IO::Socket::SSL, which the module leaves optional.
+#
+# Returns a hash-ref with ok => 1 and the query value on success, or ok => 0
+# and an error string otherwise.
+sub proxy_query_with_ssl
+{
+ my ($prefix, $query) = @_;
+ my $result = { ok => 0, error => 'unknown' };
+
+ eval {
+ local $SIG{ALRM} = sub { die "timeout\n" };
+ alarm($PostgreSQL::Test::Utils::timeout_default);
+
+ my $sock = IO::Socket::INET->new(
+ PeerHost => $conn_host,
+ PeerPort => $conn_port,
+ Proto => 'tcp') or die "cannot connect: $!\n";
+ $sock->autoflush(1);
+
+ # The PROXY header travels in cleartext, ahead of the TLS handshake.
+ print $sock $prefix if defined $prefix;
+
+ my $ssl = IO::Socket::SSL->start_SSL(
+ $sock,
+ SSL_verify_mode => 0,
+ SSL_alpn_protocols => ['postgresql'])
+ or die "TLS handshake failed: "
+ . (IO::Socket::SSL->errstr // 'unknown') . "\n";
+
+ die "ALPN did not negotiate 'postgresql'\n"
+ unless defined $ssl->alpn_selected
+ && $ssl->alpn_selected eq 'postgresql';
+
+ my $value = authenticate_and_query($ssl, $conn_user, $query);
+
+ close $ssl;
+ alarm(0);
+ $result = { ok => 1, value => $value };
+ };
+ return { ok => 0, error => $@ } if $@;
+ return $result;
+}
+
+# Given the contents of current_logfiles, return the file name (relative to the
+# data directory) recorded for the given destination, 'csvlog' or 'jsonlog'.
+sub logger_file_name
+{
+ my ($current_logfiles, $format) = @_;
+ return ($current_logfiles =~ /^$format (.*)$/m) ? $1 : undef;
+}
+
+# Wait for the collected $format log file to contain $marker, then return the
+# line carrying it. A unique marker query is what pins a log record to one
+# specific connection.
+sub wait_for_logger_line
+{
+ my ($node, $current_logfiles, $format, $marker) = @_;
+ my $path =
+ $node->data_dir . '/' . logger_file_name($current_logfiles, $format);
+
+ PostgreSQL::Test::Utils::wait_for_file($path, quotemeta($marker));
+
+ foreach my $line (split(/\n/, PostgreSQL::Test::Utils::slurp_file($path)))
+ {
+ return $line if index($line, $marker) >= 0;
+ }
+ return '';
+}
+
+1;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a65a5bf0c4f..ed574ca2fe1 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1786,6 +1786,9 @@ pg_stat_activity| SELECT s.datid,
s.client_addr,
s.client_hostname,
s.client_port,
+ s.proxy_addr,
+ s.proxy_hostname,
+ s.proxy_port,
s.backend_start,
s.xact_start,
s.query_start,
@@ -1798,7 +1801,7 @@ pg_stat_activity| SELECT s.datid,
s.query_id,
s.query,
s.backend_type
- FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+ FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, proxy_addr, proxy_hostname, proxy_port)
LEFT JOIN pg_database d ON ((s.datid = d.oid)))
LEFT JOIN pg_authid u ON ((s.usesysid = u.oid)));
pg_stat_all_indexes| SELECT c.oid AS relid,
@@ -1944,7 +1947,7 @@ pg_stat_gssapi| SELECT pid,
gss_princ AS principal,
gss_enc AS encrypted,
gss_delegation AS credentials_delegated
- FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+ FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, proxy_addr, proxy_hostname, proxy_port)
WHERE (client_port IS NOT NULL);
pg_stat_io| SELECT backend_type,
object,
@@ -2247,7 +2250,7 @@ pg_stat_replication| SELECT s.pid,
w.sync_priority,
w.sync_state,
w.reply_time
- FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+ FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, proxy_addr, proxy_hostname, proxy_port)
JOIN pg_stat_get_wal_senders() w(pid, state, sent_lsn, write_lsn, flush_lsn, replay_lsn, write_lag, flush_lag, replay_lag, sync_priority, sync_state, reply_time) ON ((s.pid = w.pid)))
LEFT JOIN pg_authid u ON ((s.usesysid = u.oid)));
pg_stat_replication_slots| SELECT s.slot_name,
@@ -2284,7 +2287,7 @@ pg_stat_ssl| SELECT pid,
ssl_client_dn AS client_dn,
ssl_client_serial AS client_serial,
ssl_issuer_dn AS issuer_dn
- FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+ FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, proxy_addr, proxy_hostname, proxy_port)
WHERE (client_port IS NOT NULL);
pg_stat_subscription| SELECT su.oid AS subid,
su.subname,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cd51760f973 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2450,6 +2450,9 @@ PropGraphLabelAndProperties
PropGraphProperties
PropGraphVertex
ProtocolVersion
+ProxyNet
+ProxyNets
+ProxyProtocolResult
PrsStorage
PruneFreezeParams
PruneFreezeResult
--
2.39.5
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-28 13:23 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-03 13:36 ` Re: PROXY protocol support Daniel Gustafsson <daniel@yesql.se>
2021-11-04 11:03 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-15 23:03 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2022-02-25 10:41 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-03-09 16:23 ` Re: PROXY protocol support Peter Eisentraut <peter.eisentraut@enterprisedb.com>
2022-03-09 16:29 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-04-01 22:16 ` Re: PROXY protocol support wilfried roset <wilfried.roset@gmail.com>
2022-04-08 11:58 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-07-28 20:05 ` Re: PROXY protocol support Jacob Champion <jchampion@timescale.com>
2024-02-03 11:37 ` Re: PROXY protocol support Julien Riou <julien@riou.xyz>
2026-06-20 05:46 ` Re: PROXY protocol support Julien Riou <julien@riou.xyz>
@ 2026-06-20 09:18 ` Julien Riou <julien@riou.xyz>
1 sibling, 0 replies; 56+ messages in thread
From: Julien Riou @ 2026-06-20 09:18 UTC (permalink / raw)
To: pgsql-hackers@lists.postgresql.org; +Cc: Magnus Hagander <magnus@hagander.net>
> or the proxy used a <literal>LOCAL<literal> command.
Fixed the literal end marker.
> $node->start;
The node start fails in the 002_proxy_protocol_ssl.pl on Windows but
there is no error in stderr. I have added the logs to debug what really
happens in the CI.
I have updated my e-mail address too.
--
Julien
Attachments:
[text/x-patch] proxy_protocol_13.patch (95.3K, ../../82e06d0f-d621-4595-9a37-52a5826dd61d@riou.xyz/2-proxy_protocol_13.patch)
download | inline diff:
From 9ff460a2431a20207515b34994c5eda4a5de5b10 Mon Sep 17 00:00:00 2001
From: Julien Riou <julien@riou.xyz>
Date: Sat, 20 Jun 2026 09:08:02 +0000
Subject: [PATCH] Implement server-side support for the PROXY protocol
Add support for HAProxy's PROXY protocol, versions 1 and 2, so the real
client address can be recovered from connections arriving through a trusted
proxy. The new proxy_networks GUC lists the networks whose peers are allowed
to prepend a PROXY header to declare the originating client. This list
supports unix sockets with the "unix" token. It is empty by default, which
disables the feature.
The header is parsed lazily. The server only looks for it on connections
coming from a trusted network and falls through to the normal startup path
otherwise. Existing clients that are not from the proxy networks and that
do not speak the PROXY protocol are unaffected.
For connections from a trusted proxy, client_addr and client_port in
pg_stat_activity and the host-based authentication checks reflect the
address from the header, while the proxy's own endpoint is exposed via the
new proxy_addr, proxy_hostname and proxy_port columns. The %H and %R
escapes expose the proxy information in log_line_prefix. The proxy host and
port are also emitted in the CSV and JSON log formats.
---
doc/src/sgml/client-auth.sgml | 21 +
doc/src/sgml/config.sgml | 77 ++-
doc/src/sgml/monitoring.sgml | 44 ++
doc/src/sgml/protocol.sgml | 16 +
src/backend/catalog/system_views.sql | 3 +
src/backend/libpq/Makefile | 3 +-
src/backend/libpq/auth.c | 12 +
src/backend/libpq/meson.build | 1 +
src/backend/libpq/pqcomm.c | 2 +-
src/backend/libpq/proxy_protocol.c | 517 ++++++++++++++++++
src/backend/tcop/backend_startup.c | 112 +++-
src/backend/utils/activity/backend_status.c | 35 ++
src/backend/utils/adt/pgstatfuncs.c | 59 +-
src/backend/utils/error/csvlog.c | 13 +
src/backend/utils/error/elog.c | 49 ++
src/backend/utils/error/jsonlog.c | 8 +
src/backend/utils/misc/guc_parameters.dat | 10 +
src/backend/utils/misc/guc_tables.c | 1 +
src/backend/utils/misc/postgresql.conf.sample | 3 +
src/include/catalog/pg_proc.dat | 6 +-
src/include/libpq/libpq-be.h | 6 +
src/include/libpq/libpq.h | 1 +
src/include/libpq/proxy_protocol.h | 38 ++
src/include/utils/backend_status.h | 4 +
src/include/utils/guc_hooks.h | 3 +
src/test/Makefile | 1 +
src/test/meson.build | 1 +
src/test/protocol/.gitignore | 2 +
src/test/protocol/Makefile | 25 +
src/test/protocol/README | 28 +
src/test/protocol/meson.build | 17 +
src/test/protocol/t/001_proxy_protocol.pl | 509 +++++++++++++++++
src/test/protocol/t/002_proxy_protocol_ssl.pl | 121 ++++
src/test/protocol/t/ProxyProtocol.pm | 385 +++++++++++++
src/test/regress/expected/rules.out | 11 +-
src/tools/pgindent/typedefs.list | 3 +
36 files changed, 2131 insertions(+), 16 deletions(-)
create mode 100644 src/backend/libpq/proxy_protocol.c
create mode 100644 src/include/libpq/proxy_protocol.h
create mode 100644 src/test/protocol/.gitignore
create mode 100644 src/test/protocol/Makefile
create mode 100644 src/test/protocol/README
create mode 100644 src/test/protocol/meson.build
create mode 100644 src/test/protocol/t/001_proxy_protocol.pl
create mode 100644 src/test/protocol/t/002_proxy_protocol_ssl.pl
create mode 100644 src/test/protocol/t/ProxyProtocol.pm
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index e4e65f8feb1..a5c93530261 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -421,6 +421,16 @@ include_dir <replaceable>directory</replaceable>
These fields do not apply to <literal>local</literal> records.
</para>
+ <note>
+ <para>
+ When a connection arrives through a connection-forwarding proxy and a
+ PROXY protocol header is accepted, the address matched against this
+ field is the client address declared in that header, not the proxy's
+ own address. See <xref linkend="guc-proxy-networks"/> for
+ details.
+ </para>
+ </note>
+
<note>
<para>
Users sometimes wonder why host names are handled
@@ -1708,6 +1718,17 @@ omicron bryanh guest1
since <productname>PostgreSQL</productname> does not have any way to decrypt the
returned string to determine the actual user name.
</para>
+
+ <para>
+ Ident authentication is rejected for connections received through
+ the <link linkend="protocol-flow-proxy">PROXY protocol</link>. A client
+ connected to the proxy rather than to the server. The port pair
+ <replaceable>X</replaceable> and <replaceable>Y</replaceable> known to the
+ server does not describe any connection that the client's ident server has
+ a record of, and the query would reach the ident server from the server's
+ address rather than the proxy's. Any answer would therefore be
+ meaningless.
+ </para>
</sect1>
<sect1 id="auth-peer">
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index fa566c9e553..2aa8ed353d1 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -712,6 +712,51 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-proxy-networks" xreflabel="proxy_networks">
+ <term><varname>proxy_networks</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>proxy_networks</varname> configuration parameter</primary>
+ </indexterm>
+ <indexterm>
+ <primary>PROXY protocol</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Specifies the networks from which the server will accept a
+ PROXY protocol header. When the server is reached through a
+ connection-forwarding proxy (like <productname>HAProxy</productname>),
+ the proxy can prepend a small header, as defined by the
+ <ulink url="https://www.haproxy.org/download/2.9/doc/proxy-protocol.txt">PROXY
+ protocol</ulink>, that declares the address of the real client.
+ When such a header is accepted, the declared client
+ address replaces the proxy's address for host-based authentication
+ (see <xref linkend="auth-pg-hba-conf"/>) and in the server log.
+ </para>
+ <para>
+ The value is a comma-separated list of CIDR networks or the special
+ token <literal>unix</literal> for Unix-domain socket (for example
+ <literal>10.0.0.0/8, 192.168.1.10, ::1/128, unix</literal>). A
+ PROXY protocol header is honored only when the actual peer address
+ falls within one of these networks. A header received from any
+ other address is rejected as an invalid connection attempt, so that
+ ordinary clients cannot spoof their address. The default is an
+ empty string, which disables the PROXY protocol support entirely.
+ This parameter can only be set in the
+ <filename>postgresql.conf</filename> file or on the server command
+ line.
+ </para>
+ <para>
+ Because a peer within these networks is treated as a proxy rather than
+ a direct client, it is <emphasis>required</emphasis> to lead with a
+ PROXY protocol header. A connection from one of these networks that
+ begins with an ordinary startup packet, or an SSL or GSS negotiation
+ request, instead of a PROXY header is rejected. This prevents the
+ proxy's own address from being mistaken for a client's.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-max-connections" xreflabel="max_connections">
<term><varname>max_connections</varname> (<type>integer</type>)
<indexterm>
@@ -8107,11 +8152,12 @@ local0.* /var/log/postgresql
<listitem>
<para>
By default, connection log messages only show the IP address of the
- connecting host. Turning this parameter on causes logging of the
- host name as well. Note that depending on your host name resolution
- setup this might impose a non-negligible performance penalty.
- This parameter can only be set in the <filename>postgresql.conf</filename>
- file or on the server command line.
+ connecting host and the proxy host if the connection came from a
+ trusted proxy. Turning this parameter on causes logging of the host
+ names as well. Note that depending on your host name resolution setup
+ this might impose a non-negligible performance penalty. This parameter
+ can only be set in the <filename>postgresql.conf</filename> file or on
+ the server command line.
</para>
</listitem>
</varlistentry>
@@ -8181,6 +8227,16 @@ local0.* /var/log/postgresql
<entry>Remote host name or IP address</entry>
<entry>yes</entry>
</row>
+ <row>
+ <entry><literal>%H</literal></entry>
+ <entry>Proxy host name or IP address</entry>
+ <entry>yes</entry>
+ </row>
+ <row>
+ <entry><literal>%R</literal></entry>
+ <entry>Proxy host name or IP address, and proxy port</entry>
+ <entry>yes</entry>
+ </row>
<row>
<entry><literal>%L</literal></entry>
<entry>Local address (the IP address on the server that the
@@ -8641,6 +8697,7 @@ CREATE TABLE postgres_log
backend_type text,
leader_pid integer,
query_id bigint,
+ proxy_connection text,
PRIMARY KEY (session_id, session_line_num)
);
</programlisting>
@@ -8767,6 +8824,16 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
<entry>number</entry>
<entry>Client port</entry>
</row>
+ <row>
+ <entry><literal>proxy_host</literal></entry>
+ <entry>string</entry>
+ <entry>Proxy host</entry>
+ </row>
+ <row>
+ <entry><literal>proxy_port</literal></entry>
+ <entry>number</entry>
+ <entry>Proxy port</entry>
+ </row>
<row>
<entry><literal>session_id</literal></entry>
<entry>string</entry>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 08d5b824552..b0d4322fb10 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -843,6 +843,10 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser
If this field is null, it indicates either that the client is
connected via a Unix socket on the server machine or that this is an
internal process such as autovacuum.
+ When the connection was made through a trusted proxy using the PROXY
+ protocol, this is the client address sent by the proxy, not the address
+ of the proxy itself (which is shown in <structfield>proxy_addr</structfield>
+ instead).
</para></entry>
</row>
@@ -865,6 +869,46 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser
TCP port number that the client is using for communication
with this backend, or <literal>-1</literal> if a Unix socket is used.
If this field is null, it indicates that this is an internal server process.
+ When the connection was made through a trusted proxy using the PROXY
+ protocol, this is the client port sent by the proxy, not the port of the
+ proxy itself (which is shown in <structfield>proxy_port</structfield>
+ instead).
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>proxy_addr</structfield> <type>inet</type>
+ </para>
+ <para>
+ IP address of the trusted proxy that forwarded this connection.
+ If this field is null, it indicates either that the PROXY protocol has
+ not been used, or the proxy forwarded the connection over a Unix socket,
+ or the proxy used a <literal>LOCAL</literal> command.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>proxy_hostname</structfield> <type>text</type>
+ </para>
+ <para>
+ Host name of the proxy that forwarded this connection, as reported by a
+ reverse DNS lookup of <structfield>proxy_addr</structfield>. This field
+ will only be non-null when the client connected through a trusted proxy
+ using the PROXY protocol, and only when <xref linkend="guc-log-hostname"/>
+ is enabled.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>proxy_port</structfield> <type>integer</type>
+ </para>
+ <para>
+ TCP port number of the trusted proxy that forwarded this connection, or
+ <literal>-1</literal> if the proxy forwarded it over a Unix socket.
+ This field is null when the PROXY protocol was not used.
</para></entry>
</row>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 49f81676712..556fd325d7a 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1693,6 +1693,22 @@ SELCT 1/0;<!-- this typo is intentional -->
</para>
</sect2>
+ <sect2 id="protocol-flow-proxy">
+ <title><acronym>PROXY</acronym> Protocol</title>
+
+ <para>
+ When a connection is forwarded by a proxy, the address the server sees is
+ the proxy's, not the real client's. To preserve the
+ originating address, the proxy can prepend a header conforming to the
+ <ulink url="https://www.haproxy.org/download/2.9/doc/proxy-protocol.txt">PROXY
+ protocol</ulink> designed by <productname>HAProxy</productname>. The
+ header carries the original source and destination addresses and ports.
+ The header is sent once, before any other data, ahead of the SSLRequest,
+ GSSENCRequest, or StartupMessage. Both version 1 (text) and version 2
+ (binary) of the protocol are accepted.
+ </para>
+ </sect2>
+
<sect2 id="protocol-flow-ssl">
<title><acronym>SSL</acronym> Session Encryption</title>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 8f129baec90..e3988344966 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -948,6 +948,9 @@ CREATE VIEW pg_stat_activity AS
S.client_addr,
S.client_hostname,
S.client_port,
+ S.proxy_addr,
+ S.proxy_hostname,
+ S.proxy_port,
S.backend_start,
S.xact_start,
S.query_start,
diff --git a/src/backend/libpq/Makefile b/src/backend/libpq/Makefile
index 98eb2a8242d..5f8863d0dbf 100644
--- a/src/backend/libpq/Makefile
+++ b/src/backend/libpq/Makefile
@@ -28,7 +28,8 @@ OBJS = \
pqcomm.o \
pqformat.o \
pqmq.o \
- pqsignal.o
+ pqsignal.o \
+ proxy_protocol.o
ifeq ($(with_ssl),openssl)
OBJS += be-secure-openssl.o
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 2af5615e54a..5d9b4119c2f 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -1701,6 +1701,18 @@ ident_inet(Port *port)
*la = NULL,
hints;
+ /*
+ * Ident is incompatible with the PROXY protocol. A proxied client
+ * connected to the proxy, not to the server, so its ident server has no
+ * record of any connection matching the address pair in the query.
+ */
+ if (port->proxy_protocol)
+ {
+ ereport(LOG,
+ (errmsg("ident authentication is not supported over connections using the PROXY protocol")));
+ return STATUS_ERROR;
+ }
+
/*
* Might look a little weird to first convert it to text and then back to
* sockaddr, but it's protocol independent.
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 8571f652844..30584ee8dfd 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -15,6 +15,7 @@ backend_sources += files(
'pqformat.c',
'pqmq.c',
'pqsignal.c',
+ 'proxy_protocol.c',
)
if ssl.found()
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 4a442f22df6..4eeb4eb4d45 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -1094,7 +1094,7 @@ pq_getbytes(void *b, size_t len)
* returns 0 if OK, EOF if trouble
* --------------------------------
*/
-static int
+int
pq_discardbytes(size_t len)
{
size_t amount;
diff --git a/src/backend/libpq/proxy_protocol.c b/src/backend/libpq/proxy_protocol.c
new file mode 100644
index 00000000000..6a66c12926b
--- /dev/null
+++ b/src/backend/libpq/proxy_protocol.c
@@ -0,0 +1,517 @@
+/*-------------------------------------------------------------------------
+ *
+ * proxy_protocol.c
+ * Functions related to parse the PROXY Protocol (versions 1 and 2).
+ * https://www.haproxy.org/download/2.9/doc/proxy-protocol.txt
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ * src/backend/libpq/proxy_protocol.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include "common/ip.h"
+#include "libpq/ifaddr.h"
+#include "libpq/libpq.h"
+#include "libpq/proxy_protocol.h"
+#include "port/pg_bswap.h"
+#include "utils/guc.h"
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
+
+/* Raw text of the proxy_networks GUC. */
+char *ProxyNetworks = NULL;
+
+/*
+ * Pre-parsed form of proxy_networks, produced by the check hook and
+ * installed by the assign hook. Stored with guc_malloc() so that the GUC
+ * machinery owns its lifetime.
+ */
+typedef struct ProxyNet
+{
+ struct sockaddr_storage addr; /* network address */
+ struct sockaddr_storage mask; /* network mask */
+} ProxyNet;
+
+typedef struct ProxyNets
+{
+ bool trust_unix; /* trust Unix-socket peers to send a header */
+ int nnets;
+ ProxyNet nets[FLEXIBLE_ARRAY_MEMBER];
+} ProxyNets;
+
+/*
+ * The special proxy_networks token to trust Unix-domain socket peers
+ * to send a PROXY header.
+ */
+#define PROXY_UNIX_TOKEN "unix"
+
+static ProxyNets *proxy_networks = NULL;
+
+/* The 12-byte PROXY protocol v2 signature. */
+static const uint8 v2_signature[12] =
+"\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a";
+
+/* Largest possible v1 header line, including the trailing CRLF. */
+#define PROXY_V1_MAX_LEN 107
+
+/* Supported v2 address families. */
+#define PROXY_V2_AF_UNSPEC 0x00
+#define PROXY_V2_TCP4 0x11
+#define PROXY_V2_TCP6 0x21
+
+/*
+ * Length of the leading address block for each supported v2 family.
+ * Any bytes beyond these lengths are TLV vectors and are skipped.
+ */
+#define PROXY_V2_TCP4_ADDRLEN 12 /* 2 x 4-byte addr + 2 x 2-byte port */
+#define PROXY_V2_TCP6_ADDRLEN 36 /* 2 x 16-byte addr + 2 x 2-byte port */
+
+/* Supported v2 commands */
+#define PROXY_V2_CMD_LOCAL 0x0
+#define PROXY_V2_CMD_PROXY 0x1
+
+/*
+ * Parse a single "address" or "address/masklen" network specification into a
+ * ProxyNet. Returns true on success. No DNS is performed (numeric host
+ * only), so this is safe to call from a GUC check hook.
+ */
+static bool
+parse_proxy_network(const char *spec, ProxyNet *net)
+{
+ char *str = pstrdup(spec);
+ char *slash;
+ struct addrinfo hints;
+ struct addrinfo *gai_result = NULL;
+ bool ok = false;
+
+ memset(net, 0, sizeof(*net));
+
+ slash = strchr(str, '/');
+ if (slash)
+ *slash = '\0';
+
+ MemSet(&hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ if (pg_getaddrinfo_all(str, NULL, &hints, &gai_result) == 0 &&
+ gai_result != NULL &&
+ gai_result->ai_addrlen <= sizeof(net->addr))
+ {
+ memcpy(&net->addr, gai_result->ai_addr, gai_result->ai_addrlen);
+
+ ok = pg_sockaddr_cidr_mask(&net->mask, slash ? slash + 1 : NULL,
+ net->addr.ss_family) >= 0;
+ }
+
+ if (gai_result)
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+ pfree(str);
+ return ok;
+}
+
+/*
+ * GUC check hook for proxy_networks. Parses the comma-separated
+ * list of networks into a ProxyNets structure stored in *extra.
+ */
+bool
+check_proxy_networks(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ int nnets;
+ int i;
+ ProxyNets *result;
+
+ /* Need a modifiable copy of the string */
+ rawstring = pstrdup(*newval);
+
+ if (!SplitGUCList(rawstring, ',', &elemlist))
+ {
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ nnets = list_length(elemlist);
+
+ result = (ProxyNets *) guc_malloc(LOG,
+ offsetof(ProxyNets, nets) +
+ nnets * sizeof(ProxyNet));
+ if (result == NULL)
+ {
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+ result->nnets = 0;
+ result->trust_unix = false;
+
+ i = 0;
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+
+ if (pg_strcasecmp(tok, PROXY_UNIX_TOKEN) == 0)
+ {
+ result->trust_unix = true;
+ continue;
+ }
+
+ if (!parse_proxy_network(tok, &result->nets[i]))
+ {
+ GUC_check_errdetail("Invalid network specification: \"%s\".", tok);
+ guc_free(result);
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+ i++;
+ }
+ result->nnets = i;
+
+ pfree(rawstring);
+ list_free(elemlist);
+
+ *extra = result;
+ return true;
+}
+
+/*
+ * GUC assign hook for proxy_networks.
+ */
+void
+assign_proxy_networks(const char *newval, void *extra)
+{
+ proxy_networks = (ProxyNets *) extra;
+}
+
+/*
+ * Reports whether PROXY protocol parsing is enabled.
+ */
+bool
+ProxyProtocolEnabled(void)
+{
+ return proxy_networks != NULL &&
+ (proxy_networks->nnets > 0 || proxy_networks->trust_unix);
+}
+
+/*
+ * Reports whether the given peer address falls within one of the trusted
+ * proxy networks.
+ */
+static bool
+proxy_source_trusted(const SockAddr *raddr)
+{
+ int i;
+
+ if (proxy_networks == NULL)
+ return false;
+
+ if (raddr->addr.ss_family == AF_UNIX)
+ return proxy_networks->trust_unix;
+
+ if (raddr->addr.ss_family != AF_INET &&
+ raddr->addr.ss_family != AF_INET6)
+ return false;
+
+ for (i = 0; i < proxy_networks->nnets; i++)
+ {
+ if (raddr->addr.ss_family == proxy_networks->nets[i].addr.ss_family &&
+ pg_range_sockaddr(&raddr->addr,
+ &proxy_networks->nets[i].addr,
+ &proxy_networks->nets[i].mask))
+ return true;
+ }
+ return false;
+}
+
+/*
+ * Reports whether this connection must begin with a PROXY header.
+ */
+bool
+ProxyProtocolRequired(Port *port)
+{
+ return proxy_source_trusted(&port->raddr);
+}
+
+/*
+ * Build an IPv4 or IPv6 SockAddr from a numeric address string and a port
+ * string for PROXY v1.
+ */
+static bool
+make_v1_sockaddr(int family, const char *addr, const char *port, SockAddr *sa)
+{
+ char *endptr;
+ long portnum;
+
+ errno = 0;
+ portnum = strtol(port, &endptr, 10);
+ if (endptr == port || *endptr != '\0' || errno != 0 ||
+ portnum < 0 || portnum > 65535)
+ return false;
+
+ memset(&sa->addr, 0, sizeof(sa->addr));
+
+ if (family == AF_INET)
+ {
+ struct sockaddr_in *sin = (struct sockaddr_in *) &sa->addr;
+
+ if (inet_pton(AF_INET, addr, &sin->sin_addr) != 1)
+ return false;
+ sin->sin_family = AF_INET;
+ sin->sin_port = pg_hton16((uint16) portnum);
+ sa->salen = sizeof(struct sockaddr_in);
+ }
+ else
+ {
+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) &sa->addr;
+
+ if (inet_pton(AF_INET6, addr, &sin6->sin6_addr) != 1)
+ return false;
+ sin6->sin6_family = AF_INET6;
+ sin6->sin6_port = pg_hton16((uint16) portnum);
+ sa->salen = sizeof(struct sockaddr_in6);
+ }
+
+ return true;
+}
+
+/*
+ * Parse a human-readable PROXY header (version 1).
+ */
+static ProxyProtocolResult
+parse_proxy_v1(Port *port, bool *have_client, SockAddr *client)
+{
+ char line[PROXY_V1_MAX_LEN + 1];
+ int len = 0;
+ char *saveptr;
+ char *proto;
+ char *src_addr;
+ char *dst_addr;
+ char *src_port;
+ char *dst_port;
+ int family;
+
+ /* The first four bytes ("PROX") have already been consumed by the caller */
+ line[len++] = 'P';
+ line[len++] = 'R';
+ line[len++] = 'O';
+ line[len++] = 'X';
+
+ for (;;)
+ {
+ int c = pq_getbyte();
+
+ if (c == EOF)
+ return PROXY_PROTO_ERROR;
+ if (len >= PROXY_V1_MAX_LEN)
+ return PROXY_PROTO_ERROR; /* no CRLF within the size limit */
+ line[len++] = (char) c;
+ if (c == '\n')
+ break;
+ }
+
+ /* The line must end with CRLF. */
+ if (len < 2 || line[len - 2] != '\r' || line[len - 1] != '\n')
+ return PROXY_PROTO_ERROR;
+ line[len - 2] = '\0';
+
+ /* It must start with the exact "PROXY " token. */
+ if (strncmp(line, "PROXY ", 6) != 0)
+ return PROXY_PROTO_ERROR;
+
+ proto = strtok_r(line + 6, " ", &saveptr);
+ if (proto == NULL)
+ return PROXY_PROTO_ERROR;
+
+ if (strcmp(proto, "UNKNOWN") == 0)
+ {
+ /* Address is unknown. Keep the real peer address. */
+ *have_client = false;
+ return PROXY_PROTO_DONE;
+ }
+ else if (strcmp(proto, "TCP4") == 0)
+ family = AF_INET;
+ else if (strcmp(proto, "TCP6") == 0)
+ family = AF_INET6;
+ else
+ return PROXY_PROTO_ERROR;
+
+ src_addr = strtok_r(NULL, " ", &saveptr);
+ dst_addr = strtok_r(NULL, " ", &saveptr);
+ src_port = strtok_r(NULL, " ", &saveptr);
+ dst_port = strtok_r(NULL, " ", &saveptr);
+
+ if (src_addr == NULL || dst_addr == NULL ||
+ src_port == NULL || dst_port == NULL)
+ return PROXY_PROTO_ERROR;
+
+ /* No further tokens are allowed. */
+ if (strtok_r(NULL, " ", &saveptr) != NULL)
+ return PROXY_PROTO_ERROR;
+
+ if (!make_v1_sockaddr(family, src_addr, src_port, client))
+ return PROXY_PROTO_ERROR;
+
+ *have_client = true;
+ return PROXY_PROTO_DONE;
+}
+
+/*
+ * Parse a binary PROXY header (version 2).
+ */
+static ProxyProtocolResult
+parse_proxy_v2(Port *port, bool *have_client, SockAddr *client)
+{
+ uint8 sigrest[8];
+ uint8 hdr[4];
+ uint8 ver,
+ cmd,
+ fam;
+ uint16 datalen;
+ uint16 toread;
+ uint8 data[PROXY_V2_TCP6_ADDRLEN]; /* largest supported address
+ * block */
+
+ /* Read and verify the remaining 8 bytes of the signature. */
+ if (pq_getbytes(sigrest, sizeof(sigrest)) == EOF)
+ return PROXY_PROTO_ERROR;
+
+ if (memcmp(sigrest, v2_signature + 4, sizeof(sigrest)) != 0)
+ return PROXY_PROTO_ERROR;
+
+ /* Read version+command, family+protocol, and the 2-byte length. */
+ if (pq_getbytes(hdr, sizeof(hdr)) == EOF)
+ return PROXY_PROTO_ERROR;
+
+ ver = hdr[0] >> 4;
+ cmd = hdr[0] & 0x0f;
+ fam = hdr[1];
+ datalen = ((uint16) hdr[2] << 8) | hdr[3];
+
+ if (ver != 0x2)
+ return PROXY_PROTO_ERROR;
+
+ /*
+ * Read the address block. We only need the leading address bytes. Any
+ * trailing TLV vectors are discarded so that the stream stays in sync for
+ * the genuine startup packet.
+ */
+ toread = Min(datalen, (uint16) sizeof(data));
+
+ if (toread > 0 && pq_getbytes(data, toread) == EOF)
+ return PROXY_PROTO_ERROR;
+
+ if (datalen > toread && pq_discardbytes(datalen - toread) == EOF)
+ return PROXY_PROTO_ERROR;
+
+ switch (cmd)
+ {
+ case PROXY_V2_CMD_LOCAL: /* mainly used for health checks */
+ {
+ *have_client = false;
+ return PROXY_PROTO_DONE;
+ }
+ case PROXY_V2_CMD_PROXY:
+ break;
+ default:
+ return PROXY_PROTO_ERROR;
+
+ }
+
+ memset(&client->addr, 0, sizeof(client->addr));
+
+ switch (fam)
+ {
+ case PROXY_V2_TCP4:
+ {
+ struct sockaddr_in *sin = (struct sockaddr_in *) &client->addr;
+
+ /* The address block must be present. TLVs may follow it. */
+ if (datalen < PROXY_V2_TCP4_ADDRLEN)
+ return PROXY_PROTO_ERROR;
+ sin->sin_family = AF_INET;
+ memcpy(&sin->sin_addr, data, 4); /* source address */
+ memcpy(&sin->sin_port, data + 8, 2); /* source port */
+ client->salen = sizeof(struct sockaddr_in);
+ *have_client = true;
+ break;
+ }
+ case PROXY_V2_TCP6:
+ {
+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) &client->addr;
+
+ /* The address block must be present. TLVs may follow it. */
+ if (datalen < PROXY_V2_TCP6_ADDRLEN)
+ return PROXY_PROTO_ERROR;
+ sin6->sin6_family = AF_INET6;
+ memcpy(&sin6->sin6_addr, data, 16); /* source address */
+ memcpy(&sin6->sin6_port, data + 32, 2); /* source port */
+ client->salen = sizeof(struct sockaddr_in6);
+ *have_client = true;
+ break;
+ }
+ case PROXY_V2_AF_UNSPEC:
+ default:
+ *have_client = false;
+ break;
+ }
+
+ return PROXY_PROTO_DONE;
+}
+
+/*
+ * Try to process a PROXY protocol header.
+ *
+ */
+ProxyProtocolResult
+ProcessProxyProtocol(Port *port, const char firstbytes[4])
+{
+ bool is_v1;
+ bool is_v2;
+ bool have_client = false;
+ SockAddr client;
+ ProxyProtocolResult result;
+
+ /* Only connections from a trusted proxy are eligible */
+ if (!proxy_source_trusted(&port->raddr))
+ return PROXY_PROTO_NONE;
+
+ is_v1 = (memcmp(firstbytes, "PROX", 4) == 0);
+ is_v2 = (memcmp(firstbytes, v2_signature, 4) == 0);
+
+ if (!is_v1 && !is_v2)
+ return PROXY_PROTO_NONE;
+
+ if (is_v1)
+ result = parse_proxy_v1(port, &have_client, &client);
+ else
+ result = parse_proxy_v2(port, &have_client, &client);
+
+ if (result != PROXY_PROTO_DONE)
+ return result;
+
+ /* Header accepted */
+ port->proxy_protocol = true;
+ if (have_client)
+ {
+ port->proxy_addr = port->raddr;
+ port->raddr = client;
+ }
+
+ pq_endmsgread();
+
+ return PROXY_PROTO_DONE;
+}
diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c
index 25205cee0fa..30c7f5f4874 100644
--- a/src/backend/tcop/backend_startup.c
+++ b/src/backend/tcop/backend_startup.c
@@ -25,6 +25,7 @@
#include "libpq/libpq-be.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
+#include "libpq/proxy_protocol.h"
#include "miscadmin.h"
#include "postmaster/postmaster.h"
#include "replication/walsender.h"
@@ -145,6 +146,8 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac)
Port *port;
char remote_host[NI_MAXHOST];
char remote_port[NI_MAXSERV];
+ char proxy_host[NI_MAXHOST];
+ char proxy_port[NI_MAXSERV];
StringInfoData ps_data;
MemoryContext oldcontext;
@@ -182,6 +185,8 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac)
/* set these to empty in case they are needed before we set them up */
port->remote_host = "";
port->remote_port = "";
+ port->proxy_host = "";
+ port->proxy_port = "";
/*
* We arrange to do _exit(1) if we receive SIGTERM or timeout while trying
@@ -294,6 +299,61 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac)
if (status == STATUS_OK)
status = ProcessStartupPacket(port);
+ /*
+ * If a PROXY protocol header replaced port->raddr with the real client
+ * address (marked by a non-zero proxy_addr.salen), recompute the cached
+ * host/port strings so that log output reflects the originating client.
+ * A LOCAL command, a v1 UNKNOWN, or an unsupported family leaves both
+ * addresses untouched and the proxy host/port empty.
+ */
+ if (status == STATUS_OK && port->proxy_protocol &&
+ port->proxy_addr.salen > 0)
+ {
+ remote_host[0] = '\0';
+ remote_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&port->raddr.addr, port->raddr.salen,
+ remote_host, sizeof(remote_host),
+ remote_port, sizeof(remote_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ port->remote_host = MemoryContextStrdup(TopMemoryContext, remote_host);
+ port->remote_port = MemoryContextStrdup(TopMemoryContext, remote_port);
+
+ if (log_hostname &&
+ ret == 0 &&
+ strspn(remote_host, "0123456789.") < strlen(remote_host) &&
+ strspn(remote_host, "0123456789ABCDEFabcdef:") < strlen(remote_host))
+ port->remote_hostname = MemoryContextStrdup(TopMemoryContext, remote_host);
+
+ /*
+ * Also resolve the proxy's own address so that it can be reported
+ * separately via the %H and %R log_line_prefix escapes. As with the
+ * client address above, log_hostname controls whether a reverse DNS
+ * lookup is attempted.
+ */
+ proxy_host[0] = '\0';
+ proxy_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&port->proxy_addr.addr, port->proxy_addr.salen,
+ proxy_host, sizeof(proxy_host),
+ proxy_port, sizeof(proxy_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ port->proxy_host = MemoryContextStrdup(TopMemoryContext, proxy_host);
+ port->proxy_port = MemoryContextStrdup(TopMemoryContext, proxy_port);
+
+ if (log_hostname &&
+ ret == 0 &&
+ strspn(proxy_host, "0123456789.") < strlen(proxy_host) &&
+ strspn(proxy_host, "0123456789ABCDEFabcdef:") < strlen(proxy_host))
+ port->proxy_hostname = MemoryContextStrdup(TopMemoryContext, proxy_host);
+ }
+
/*
* If we're going to reject the connection due to database state, say so
* now instead of wasting cycles on an authentication exchange. (This also
@@ -486,11 +546,13 @@ static int
ProcessStartupPacket(Port *port)
{
int32 len;
+ char firstbytes[4]; /* raw first 4 bytes, for PROXY detection */
char *buf = NULL;
ProtocolVersion proto;
MemoryContext oldcontext;
bool gss_done;
bool ssl_done;
+ bool proxy_done;
/*
* Set ssl_done and/or gss_done when negotiation of an encrypted layer
@@ -502,6 +564,7 @@ ProcessStartupPacket(Port *port)
*/
gss_done = false;
ssl_done = false;
+ proxy_done = false;
retry:
pq_startmsgread();
@@ -537,15 +600,62 @@ retry:
goto fail;
}
+ /* Preserve the raw, network-order length bytes for PROXY detection. */
+ memcpy(firstbytes, &len, 4);
+
len = pg_ntoh32(len);
len -= 4;
if (len < (int32) sizeof(ProtocolVersion) ||
len > MAX_STARTUP_PACKET_LENGTH)
+ {
+ /*
+ * The length looks invalid. Before rejecting the connection, check
+ * whether these bytes are actually the start of a PROXY protocol
+ * header sent by a trusted proxy (see proxy_networks). A genuine
+ * startup packet always begins with two zero bytes, so this test
+ * never misfires on real client traffic. If a header is found and
+ * accepted, port->raddr is replaced with the real client address and
+ * we loop back to read the genuine startup packet.
+ */
+ if (!ssl_done && !gss_done && !proxy_done && ProxyProtocolEnabled())
+ {
+ switch (ProcessProxyProtocol(port, firstbytes))
+ {
+ case PROXY_PROTO_DONE:
+ proxy_done = true;
+
+ /*
+ * A direct SSL negotiation happens before PROXY header
+ * detection. Run it now, after consuming the header.
+ */
+ if (ProcessSSLStartup(port) != STATUS_OK)
+ goto fail;
+ goto retry;
+ case PROXY_PROTO_ERROR:
+ case PROXY_PROTO_NONE:
+ break;
+ }
+ }
+
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete startup packet")));
+ goto fail;
+ }
+
+ /*
+ * We have a plausible startup, cancel, or negotiation packet. If it
+ * arrived from a trusted proxy network but was not preceded by a PROXY
+ * header (proxy_done is still unset), reject it. Such peers must
+ * announce the real client via the PROXY protocol, which has to come
+ * first, ahead of any SSL or GSS negotiation.
+ */
+ if (!proxy_done && ProxyProtocolRequired(port))
{
ereport(COMMERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
- errmsg("invalid length of startup packet")));
+ errmsg("connection from a trusted proxy network must use the PROXY protocol")));
goto fail;
}
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index d685fc5cd87..be51f2d52a5 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -52,6 +52,7 @@ PgBackendStatus *MyBEEntry = NULL;
static PgBackendStatus *BackendStatusArray = NULL;
static char *BackendAppnameBuffer = NULL;
static char *BackendClientHostnameBuffer = NULL;
+static char *BackendProxyHostnameBuffer = NULL;
static char *BackendActivityBuffer = NULL;
static Size BackendActivityBufferSize = 0;
#ifdef USE_SSL
@@ -106,6 +107,11 @@ BackendStatusShmemRequest(void *arg)
.ptr = (void **) &BackendClientHostnameBuffer,
);
+ ShmemRequestStruct(.name = "Backend Proxy Host Name Buffer",
+ .size = mul_size(NAMEDATALEN, NumBackendStatSlots),
+ .ptr = (void **) &BackendProxyHostnameBuffer,
+ );
+
BackendActivityBufferSize = mul_size(pgstat_track_activity_query_size,
NumBackendStatSlots);
ShmemRequestStruct(.name = "Backend Activity Buffer",
@@ -154,6 +160,14 @@ BackendStatusShmemInit(void *arg)
buffer += NAMEDATALEN;
}
+ /* Initialize st_proxyhostname pointers. */
+ buffer = BackendProxyHostnameBuffer;
+ for (i = 0; i < NumBackendStatSlots; i++)
+ {
+ BackendStatusArray[i].st_proxyhostname = buffer;
+ buffer += NAMEDATALEN;
+ }
+
/* Initialize st_activity pointers. */
buffer = BackendActivityBuffer;
for (i = 0; i < NumBackendStatSlots; i++)
@@ -279,6 +293,12 @@ pgstat_bestart_initial(void)
else
MemSet(&lbeentry.st_clientaddr, 0, sizeof(lbeentry.st_clientaddr));
+ if (MyProcPort && MyProcPort->proxy_protocol)
+ memcpy(&lbeentry.st_proxyaddr, &MyProcPort->proxy_addr,
+ sizeof(lbeentry.st_proxyaddr));
+ else
+ MemSet(&lbeentry.st_proxyaddr, 0, sizeof(lbeentry.st_proxyaddr));
+
lbeentry.st_ssl = false;
lbeentry.st_gss = false;
@@ -319,10 +339,18 @@ pgstat_bestart_initial(void)
NAMEDATALEN);
else
lbeentry.st_clienthostname[0] = '\0';
+
+ if (MyProcPort && MyProcPort->proxy_hostname)
+ strlcpy(lbeentry.st_proxyhostname, MyProcPort->proxy_hostname,
+ NAMEDATALEN);
+ else
+ lbeentry.st_proxyhostname[0] = '\0';
+
lbeentry.st_activity_raw[0] = '\0';
/* Also make sure the last byte in each string area is always 0 */
lbeentry.st_appname[NAMEDATALEN - 1] = '\0';
lbeentry.st_clienthostname[NAMEDATALEN - 1] = '\0';
+ lbeentry.st_proxyhostname[NAMEDATALEN - 1] = '\0';
lbeentry.st_activity_raw[pgstat_track_activity_query_size - 1] = '\0';
/* These structs can just start from zeroes each time */
@@ -789,6 +817,7 @@ pgstat_read_current_status(void)
LocalPgBackendStatus *localentry;
char *localappname,
*localclienthostname,
+ *localproxyhostname,
*localactivity;
#ifdef USE_SSL
PgBackendSSLStatus *localsslstatus;
@@ -820,6 +849,9 @@ pgstat_read_current_status(void)
localclienthostname = (char *)
MemoryContextAlloc(backendStatusSnapContext,
NAMEDATALEN * NumBackendStatSlots);
+ localproxyhostname = (char *)
+ MemoryContextAlloc(backendStatusSnapContext,
+ NAMEDATALEN * NumBackendStatSlots);
localactivity = (char *)
MemoryContextAllocHuge(backendStatusSnapContext,
(Size) pgstat_track_activity_query_size *
@@ -873,6 +905,8 @@ pgstat_read_current_status(void)
localentry->backendStatus.st_appname = localappname;
strcpy(localclienthostname, beentry->st_clienthostname);
localentry->backendStatus.st_clienthostname = localclienthostname;
+ strcpy(localproxyhostname, beentry->st_proxyhostname);
+ localentry->backendStatus.st_proxyhostname = localproxyhostname;
strcpy(localactivity, beentry->st_activity_raw);
localentry->backendStatus.st_activity_raw = localactivity;
#ifdef USE_SSL
@@ -920,6 +954,7 @@ pgstat_read_current_status(void)
localentry++;
localappname += NAMEDATALEN;
localclienthostname += NAMEDATALEN;
+ localproxyhostname += NAMEDATALEN;
localactivity += pgstat_track_activity_query_size;
#ifdef USE_SSL
localsslstatus++;
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 6f9c9c72de5..626ee377bfd 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -355,7 +355,7 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
Datum
pg_stat_get_activity(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_ACTIVITY_COLS 31
+#define PG_STAT_GET_ACTIVITY_COLS 34
int num_backends = pgstat_fetch_stat_numbackends();
int curr_backend;
int pid = PG_ARGISNULL(0) ? -1 : PG_GETARG_INT32(0);
@@ -669,6 +669,60 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
nulls[30] = true;
else
values[30] = Int64GetDatum(beentry->st_query_id);
+
+ /* Proxy information */
+ if (pg_memory_is_all_zeros(&beentry->st_proxyaddr,
+ sizeof(beentry->st_proxyaddr)))
+ {
+ nulls[31] = true;
+ nulls[32] = true;
+ nulls[33] = true;
+ }
+ else if (beentry->st_proxyaddr.addr.ss_family == AF_INET ||
+ beentry->st_proxyaddr.addr.ss_family == AF_INET6)
+ {
+ char proxy_host[NI_MAXHOST];
+ char proxy_port[NI_MAXSERV];
+ int ret;
+
+ proxy_host[0] = '\0';
+ proxy_port[0] = '\0';
+ ret = pg_getnameinfo_all(&beentry->st_proxyaddr.addr,
+ beentry->st_proxyaddr.salen,
+ proxy_host, sizeof(proxy_host),
+ proxy_port, sizeof(proxy_port),
+ NI_NUMERICHOST | NI_NUMERICSERV);
+ if (ret == 0)
+ {
+ clean_ipv6_addr(beentry->st_proxyaddr.addr.ss_family, proxy_host);
+ values[31] = DirectFunctionCall1(inet_in,
+ CStringGetDatum(proxy_host));
+ if (beentry->st_proxyhostname &&
+ beentry->st_proxyhostname[0])
+ values[32] = CStringGetTextDatum(beentry->st_proxyhostname);
+ else
+ nulls[32] = true;
+ values[33] = Int32GetDatum(atoi(proxy_port));
+ }
+ else
+ {
+ nulls[31] = true;
+ nulls[32] = true;
+ nulls[33] = true;
+ }
+ }
+ else if (beentry->st_proxyaddr.addr.ss_family == AF_UNIX)
+ {
+ nulls[31] = true;
+ nulls[32] = true;
+ values[33] = Int32GetDatum(-1);
+ }
+ else
+ {
+ nulls[31] = true;
+ nulls[32] = true;
+ nulls[33] = true;
+ }
}
else
{
@@ -698,6 +752,9 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
nulls[28] = true;
nulls[29] = true;
nulls[30] = true;
+ nulls[31] = true;
+ nulls[32] = true;
+ nulls[33] = true;
}
tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/src/backend/utils/error/csvlog.c b/src/backend/utils/error/csvlog.c
index ee4e2a7104a..bab74a5280f 100644
--- a/src/backend/utils/error/csvlog.c
+++ b/src/backend/utils/error/csvlog.c
@@ -249,7 +249,20 @@ write_csvlog(ErrorData *edata)
/* query id */
appendStringInfo(&buf, "%" PRId64, pgstat_get_my_query_id());
+ appendStringInfoChar(&buf, ',');
+ /* Proxy host and port */
+ if (MyProcPort && MyProcPort->proxy_host && MyProcPort->proxy_host[0] != '\0')
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoString(&buf, MyProcPort->proxy_host);
+ if (MyProcPort->proxy_port && MyProcPort->proxy_port[0] != '\0')
+ {
+ appendStringInfoChar(&buf, ':');
+ appendStringInfoString(&buf, MyProcPort->proxy_port);
+ }
+ appendStringInfoChar(&buf, '"');
+ }
appendStringInfoChar(&buf, '\n');
/* If in the syslogger process, try to write messages direct to file */
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index a6936a0c664..a5cd9ab79b0 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -3595,6 +3595,55 @@ log_status_format(StringInfo buf, const char *format, ErrorData *edata)
appendStringInfoSpaces(buf,
padding > 0 ? padding : -padding);
break;
+ case 'H':
+ if (MyProcPort && MyProcPort->proxy_host)
+ {
+ if (padding != 0)
+ appendStringInfo(buf, "%*s", padding, MyProcPort->proxy_host);
+ else
+ appendStringInfoString(buf, MyProcPort->proxy_host);
+ }
+ else if (padding != 0)
+ appendStringInfoSpaces(buf,
+ padding > 0 ? padding : -padding);
+ break;
+ case 'R':
+ if (MyProcPort && MyProcPort->proxy_host)
+ {
+ if (padding != 0)
+ {
+ if (MyProcPort->proxy_port && MyProcPort->proxy_port[0] != '\0')
+ {
+ /*
+ * As with remote port, the port number may be appended
+ * appended onto the end, so build a single string
+ * containing the proxy_host and optionally the
+ * proxy_port (if set) so we can properly align
+ * it.
+ */
+ char *hostport;
+
+ hostport = psprintf("%s(%s)", MyProcPort->proxy_host, MyProcPort->proxy_port);
+ appendStringInfo(buf, "%*s", padding, hostport);
+ pfree(hostport);
+ }
+ else
+ appendStringInfo(buf, "%*s", padding, MyProcPort->proxy_host);
+ }
+ else
+ {
+ /* padding is 0, so we don't need a temp buffer */
+ appendStringInfoString(buf, MyProcPort->proxy_host);
+ if (MyProcPort->proxy_port &&
+ MyProcPort->proxy_port[0] != '\0')
+ appendStringInfo(buf, "(%s)",
+ MyProcPort->proxy_port);
+ }
+ }
+ else if (padding != 0)
+ appendStringInfoSpaces(buf,
+ padding > 0 ? padding : -padding);
+ break;
case 'q':
/* in postmaster and friends, stop if %q is seen */
/* in a backend, just ignore */
diff --git a/src/backend/utils/error/jsonlog.c b/src/backend/utils/error/jsonlog.c
index 4a76072830d..eb5142e1358 100644
--- a/src/backend/utils/error/jsonlog.c
+++ b/src/backend/utils/error/jsonlog.c
@@ -167,6 +167,14 @@ write_jsonlog(ErrorData *edata)
appendJSONKeyValue(&buf, "remote_port", MyProcPort->remote_port, false);
}
+ /* Proxy host and port */
+ if (MyProcPort && MyProcPort->proxy_host && MyProcPort->proxy_host[0] != '\0')
+ {
+ appendJSONKeyValue(&buf, "proxy_host", MyProcPort->proxy_host, true);
+ if (MyProcPort->proxy_port && MyProcPort->proxy_port[0] != '\0')
+ appendJSONKeyValue(&buf, "proxy_port", MyProcPort->proxy_port, false);
+ }
+
/* Session id */
appendJSONKeyValueFmt(&buf, "session_id", true, "%" PRIx64 ".%x",
MyStartTime, MyProcPid);
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index afaa058b046..d99b1542945 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2404,6 +2404,16 @@
check_hook => 'check_primary_slot_name',
},
+{ name => 'proxy_networks', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SETTINGS',
+ short_desc => 'Sets the networks from which PROXY protocol headers are accepted.',
+ long_desc => 'A comma-separated list of CIDR networks or the special token "unix" for Unix-domain socket. A connection whose peer address matches one of these networks must prepend a PROXY protocol header to declare the real client address. An empty string disables PROXY protocol support.',
+ flags => 'GUC_LIST_INPUT',
+ variable => 'ProxyNetworks',
+ boot_val => '""',
+ check_hook => 'check_proxy_networks',
+ assign_hook => 'assign_proxy_networks',
+},
+
{ name => 'quote_all_identifiers', type => 'bool', context => 'PGC_USERSET', group => 'COMPAT_OPTIONS_PREVIOUS',
short_desc => 'When generating SQL fragments, quote all identifiers.',
variable => 'quote_all_identifiers',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 290ccbc543e..f2ed4246882 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -54,6 +54,7 @@
#include "libpq/auth.h"
#include "libpq/libpq.h"
#include "libpq/oauth.h"
+#include "libpq/proxy_protocol.h"
#include "libpq/scram.h"
#include "nodes/queryjumble.h"
#include "optimizer/cost.h"
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ac38cddaaf9..337cc6861d3 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -64,6 +64,9 @@
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
+#proxy_networks = '' # comma-separated list of trusted proxy CIDRs
+ # or "unix" for Unix-socket peers sending the
+ # PROXY protocol header
#max_connections = 100 # (change requires restart)
#reserved_connections = 0 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..3944f518371 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5690,9 +5690,9 @@
proname => 'pg_stat_get_activity', prorows => '100', proisstrict => 'f',
proretset => 't', provolatile => 's', proparallel => 'r',
prorettype => 'record', proargtypes => 'int4',
- proallargtypes => '{int4,oid,int4,oid,text,text,text,text,text,timestamptz,timestamptz,timestamptz,timestamptz,inet,text,int4,xid,xid,text,bool,text,text,int4,text,numeric,text,bool,text,bool,bool,int4,int8}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{pid,datid,pid,usesysid,application_name,state,query,wait_event_type,wait_event,xact_start,query_start,backend_start,state_change,client_addr,client_hostname,client_port,backend_xid,backend_xmin,backend_type,ssl,sslversion,sslcipher,sslbits,ssl_client_dn,ssl_client_serial,ssl_issuer_dn,gss_auth,gss_princ,gss_enc,gss_delegation,leader_pid,query_id}',
+ proallargtypes => '{int4,oid,int4,oid,text,text,text,text,text,timestamptz,timestamptz,timestamptz,timestamptz,inet,text,int4,xid,xid,text,bool,text,text,int4,text,numeric,text,bool,text,bool,bool,int4,int8,inet,text,int4}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{pid,datid,pid,usesysid,application_name,state,query,wait_event_type,wait_event,xact_start,query_start,backend_start,state_change,client_addr,client_hostname,client_port,backend_xid,backend_xmin,backend_type,ssl,sslversion,sslcipher,sslbits,ssl_client_dn,ssl_client_serial,ssl_issuer_dn,gss_auth,gss_princ,gss_enc,gss_delegation,leader_pid,query_id,proxy_addr,proxy_hostname,proxy_port}',
prosrc => 'pg_stat_get_activity' },
{ oid => '6318', descr => 'describe wait events',
proname => 'pg_get_wait_events', procost => '10', prorows => '250',
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..60454df4b79 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -138,6 +138,12 @@ typedef struct Port
int remote_hostname_resolv; /* see above */
int remote_hostname_errcode; /* see above */
char *remote_port; /* text rep of remote port */
+ bool proxy_protocol; /* whether this connection used the PROXY
+ protocol */
+ SockAddr proxy_addr; /* real TCP peer (the proxy itself) */
+ char *proxy_host; /* name (or ip addr) of the proxy */
+ char *proxy_hostname; /* name (not ip addr) of the proxy */
+ char *proxy_port; /* text rep of the proxy's port */
/* local_host is filled only if needed (see log_status_format) */
char local_host[64]; /* ip addr of local socket for client conn */
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index d15073a0a93..0ff4fc64584 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -75,6 +75,7 @@ extern void TouchSocketFiles(void);
extern void RemoveSocketFiles(void);
extern Port *pq_init(ClientSocket *client_sock);
extern int pq_getbytes(void *b, size_t len);
+extern int pq_discardbytes(size_t len);
extern void pq_startmsgread(void);
extern void pq_endmsgread(void);
extern bool pq_is_reading_msg(void);
diff --git a/src/include/libpq/proxy_protocol.h b/src/include/libpq/proxy_protocol.h
new file mode 100644
index 00000000000..06e99bb570a
--- /dev/null
+++ b/src/include/libpq/proxy_protocol.h
@@ -0,0 +1,38 @@
+/*-------------------------------------------------------------------------
+ *
+ * proxy_protocol.h
+ * Interface of libpq/proxy_protocol.c
+ *
+ * src/include/libpq/proxy_protocol.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PROXY_PROTOCOL_H
+#define PROXY_PROTOCOL_H
+
+#include "libpq/libpq-be.h"
+
+/* Comma-separated list of networks allowed to send PROXY headers. */
+extern PGDLLIMPORT char *ProxyNetworks;
+
+/* Types of PROXY protocol parsing results. */
+typedef enum ProxyProtocolResult
+{
+ PROXY_PROTO_NONE, /* not a PROXY header (or not from a trusted
+ * source). Caller should handle the data
+ * normally */
+ PROXY_PROTO_DONE, /* a valid PROXY header was consumed, port has
+ * been updated and the real startup packet
+ * should now be read */
+ PROXY_PROTO_ERROR, /* a PROXY header from a trusted source was
+ * malformed, the connection must be closed */
+} ProxyProtocolResult;
+
+extern bool ProxyProtocolEnabled(void);
+
+extern bool ProxyProtocolRequired(Port *port);
+
+extern ProxyProtocolResult ProcessProxyProtocol(Port *port,
+ const char firstbytes[4]);
+
+#endif
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index a334e096e4a..7b71e0d5440 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -133,6 +133,10 @@ typedef struct PgBackendStatus
SockAddr st_clientaddr;
char *st_clienthostname; /* MUST be null-terminated */
+ /* Proxy information */
+ SockAddr st_proxyaddr;
+ char *st_proxyhostname; /* MUST be null-terminated */
+
/* Information about SSL connection */
bool st_ssl;
PgBackendSSLStatus *st_sslstatus;
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 307f4fbaefe..b75a28fdc28 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -95,6 +95,9 @@ extern bool check_multixact_offset_buffers(int *newval, void **extra,
extern bool check_notify_buffers(int *newval, void **extra, GucSource source);
extern bool check_primary_slot_name(char **newval, void **extra,
GucSource source);
+extern bool check_proxy_networks(char **newval, void **extra,
+ GucSource source);
+extern void assign_proxy_networks(const char *newval, void *extra);
extern bool check_random_seed(double *newval, void **extra, GucSource source);
extern void assign_random_seed(double newval, void *extra);
extern const char *show_random_seed(void);
diff --git a/src/test/Makefile b/src/test/Makefile
index 3eb0a06abb4..2eb65073bc6 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -18,6 +18,7 @@ SUBDIRS = \
modules \
perl \
postmaster \
+ protocol \
recovery \
regress \
subscription
diff --git a/src/test/meson.build b/src/test/meson.build
index cd45cbf57fb..6844952d894 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -5,6 +5,7 @@ subdir('isolation')
subdir('authentication')
subdir('postmaster')
+subdir('protocol')
subdir('recovery')
subdir('subscription')
subdir('modules')
diff --git a/src/test/protocol/.gitignore b/src/test/protocol/.gitignore
new file mode 100644
index 00000000000..871e943d50e
--- /dev/null
+++ b/src/test/protocol/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/protocol/Makefile b/src/test/protocol/Makefile
new file mode 100644
index 00000000000..4228cf6d2a3
--- /dev/null
+++ b/src/test/protocol/Makefile
@@ -0,0 +1,25 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/protocol
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/protocol/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/protocol
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+export OPENSSL with_ssl
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
+
+clean distclean:
+ rm -rf tmp_check
diff --git a/src/test/protocol/README b/src/test/protocol/README
new file mode 100644
index 00000000000..6eb3d5a000e
--- /dev/null
+++ b/src/test/protocol/README
@@ -0,0 +1,28 @@
+src/test/protocol/README
+
+Regression tests for wire protocol features
+===========================================
+
+This directory contains a test suite for features of the frontend/backend
+wire protocol that are exercised by driving raw connections, such as the
+PROXY protocol.
+
+
+Running the tests
+=================
+
+NOTE: You must have given the --enable-tap-tests argument to configure.
+
+Run
+ make check
+or
+ make installcheck
+You can use "make installcheck" if you previously did "make install".
+In that case, the code in the installation tree is tested. With
+"make check", a temporary installation tree is built from the current
+sources and then tested.
+
+Either way, this test initializes, starts, and stops a test Postgres
+cluster.
+
+See src/test/perl/README for more info about running these tests.
diff --git a/src/test/protocol/meson.build b/src/test/protocol/meson.build
new file mode 100644
index 00000000000..27975e58743
--- /dev/null
+++ b/src/test/protocol/meson.build
@@ -0,0 +1,17 @@
+# Copyright (c) 2022-2026, PostgreSQL Global Development Group
+
+tests += {
+ 'name': 'protocol',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'tap': {
+ 'env': {
+ 'with_ssl': ssl_library,
+ 'OPENSSL': openssl.found() ? openssl.full_path() : '',
+ },
+ 'tests': [
+ 't/001_proxy_protocol.pl',
+ 't/002_proxy_protocol_ssl.pl',
+ ],
+ },
+}
diff --git a/src/test/protocol/t/001_proxy_protocol.pl b/src/test/protocol/t/001_proxy_protocol.pl
new file mode 100644
index 00000000000..7e55e267bd5
--- /dev/null
+++ b/src/test/protocol/t/001_proxy_protocol.pl
@@ -0,0 +1,509 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Tests for the PROXY protocol.
+
+use strict;
+use warnings FATAL => 'all';
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use ProxyProtocol;
+
+my $host = '127.0.0.1';
+
+# Network ranges trusted as proxies
+my $loopback_net = "$host/32";
+my $loopback_v6_net = '::1/128';
+my $client_net = '192.0.2.0/24'; # TEST-NET-1, v1 TCP4 client
+my $client_v4_net = '198.51.100.0/24'; # TEST-NET-2, v2 TCP4 client
+my $client_v6_net = '2001:db8::/20'; # documentation range, TCP6 clients
+my $ident_net = '203.0.113.0/24'; # TEST-NET-3, mapped to ident
+my $untrusted_net = '10.0.0.0/8'; # a network the loopback peer is not in
+
+# Client (source) addresses carried in the PROXY headers.
+my $v1_client_v4 = '192.0.2.1'; # in $client_net
+my $v2_client_v4 = '198.51.100.5'; # in $client_v4_net
+my $v1_client_v6 = '2001:db8::1';
+my $v2_client_v6 = '2001:db8::dead';
+my $ident_client = '203.0.113.20'; # in $ident_net
+my $unused_dest_v4 = '198.51.100.9';
+my $unused_dest_v6 = '2001:db8::2';
+
+# Source ports declared in the PROXY headers
+my $v1_client_port = 56324;
+my $v2_client_port = 40000;
+my $unused_dest_port = 5432;
+
+my $node = PostgreSQL::Test::Cluster->new('proxy_protocol');
+$node->init;
+$node->append_conf('postgresql.conf',
+ "listen_addresses = '$host'\n"
+ . "proxy_networks = '$loopback_net'\n"
+ . "log_line_prefix = 'PXLOG h=%h H=%H r=%r R=%R '\n"
+ . "log_statement = 'all'\n");
+$node->append_conf('pg_hba.conf',
+ "host all all $loopback_net trust\n"
+ . "host all all $loopback_v6_net trust\n"
+ . "host all all $client_net trust\n"
+ . "host all all $client_v4_net trust\n"
+ . "host all all $client_v6_net trust\n"
+ . "host all all $ident_net ident\n");
+$node->start;
+
+my $port = $node->port;
+my $user = $node->safe_psql('postgres', 'SELECT current_user');
+
+set_connection(host => $host, port => $port, user => $user);
+
+my $unix_connect = sub { $node->raw_connect };
+
+# A query to check the client address.
+my $CLIENT_ADDR = 'SELECT host(inet_client_addr())';
+
+
+# ----------------------------------------------------------------------------
+# Protocol validation.
+# ----------------------------------------------------------------------------
+# Trusted peers over TCP.
+my $r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok($r->{ok}, 'v1 TCP4 header: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $v1_client_v4, 'v1 TCP4 header: client address substituted');
+
+$r = proxy_query(
+ proxy_v1(
+ 6, $v1_client_v6, $unused_dest_v6,
+ $v1_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok($r->{ok}, 'v1 TCP6 header: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $v1_client_v6, 'v1 TCP6 header: client address substituted');
+
+$r = proxy_query(
+ proxy_v2(
+ 4, $v2_client_v4, $unused_dest_v4,
+ $v2_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok($r->{ok}, 'v2 TCP4 header: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $v2_client_v4, 'v2 TCP4 header: client address substituted');
+
+$r = proxy_query(
+ proxy_v2(
+ 6, $v2_client_v6, $unused_dest_v6,
+ $v2_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok($r->{ok}, 'v2 TCP6 header: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $v2_client_v6, 'v2 TCP6 header: client address substituted');
+
+# TLV vectors after the address block are accepted and ignored.
+my $tlv = "\x04" . pack('n', 3) . "abc" # PP2_TYPE_NOOP, 3 bytes
+ . "\xee" . pack('n', 600) . ("\x00" x 600); # opaque type, spans many reads
+$r = proxy_query(
+ proxy_v2(
+ 4, $v2_client_v4, $unused_dest_v4,
+ $v2_client_port, $unused_dest_port, $tlv),
+ $CLIENT_ADDR);
+ok($r->{ok}, 'v2 header with trailing TLVs: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $v2_client_v4,
+ 'v2 header with trailing TLVs: TLVs ignored, client address substituted');
+
+# v1 UNKNOWN keeps the real peer address.
+$r = proxy_query(proxy_v1_unknown(), $CLIENT_ADDR);
+ok($r->{ok}, 'v1 UNKNOWN header: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $host, 'v1 UNKNOWN header: real peer address kept');
+
+# v2 LOCAL keeps the real peer address.
+$r = proxy_query(proxy_v2_local(), $CLIENT_ADDR);
+ok($r->{ok}, 'v2 LOCAL header: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $host, 'v2 LOCAL header: real peer address kept');
+
+# Unix socket.
+SKIP:
+{
+ skip "Unix-domain sockets not in use on this platform", 6
+ unless $PostgreSQL::Test::Utils::use_unix_sockets
+ && $node->raw_connect_works;
+
+ $node->append_conf('postgresql.conf',
+ "proxy_networks = 'unix, $loopback_net'\n");
+ $node->reload;
+
+ # v1 TCP address from an Unix peer must pass
+ $r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ $CLIENT_ADDR,
+ $unix_connect);
+ ok($r->{ok}, 'unix: v1 header over a Unix socket succeeds')
+ or diag("error: $r->{error}");
+ is($r->{value}, $v1_client_v4,
+ 'unix: v1 header substitutes the parsed TCP client over a Unix socket'
+ );
+
+ # v2 TCP address from an Unix peer must pass
+ $r = proxy_query(
+ proxy_v2(
+ 4, $v2_client_v4, $unused_dest_v4,
+ $v2_client_port, $unused_dest_port),
+ $CLIENT_ADDR,
+ $unix_connect);
+ ok($r->{ok}, 'unix: v2 header over a Unix socket succeeds')
+ or diag("error: $r->{error}");
+ is($r->{value}, $v2_client_v4,
+ 'unix: v2 header substitutes the parsed TCP client over a Unix socket'
+ );
+
+ # A trusted peer must lead with a PROXY header
+ my $unix_logoff = -s $node->logfile;
+ $r = proxy_query(undef, $CLIENT_ADDR, $unix_connect);
+ ok(!$r->{ok},
+ 'unix: header-less connection over a Unix socket is rejected');
+ $node->wait_for_log(
+ qr/connection from a trusted proxy network must use the PROXY protocol/,
+ $unix_logoff);
+ ok(1,
+ 'unix: header-less Unix-socket connection logs the PROXY requirement'
+ );
+
+ $node->append_conf('postgresql.conf',
+ "proxy_networks = '$loopback_net'\n");
+ $node->reload;
+}
+
+# A trusted peer must lead with a PROXY header
+# For plain.
+my $logoff = -s $node->logfile;
+$r = proxy_query(undef, $CLIENT_ADDR);
+ok(!$r->{ok}, 'plain startup packet from a trusted peer is rejected');
+$node->wait_for_log(
+ qr/connection from a trusted proxy network must use the PROXY protocol/,
+ $logoff);
+ok(1, 'plain connection from a trusted network logs the PROXY requirement');
+
+# For SSL.
+$logoff = -s $node->logfile;
+$r = ssl_request();
+ok(!$r->{ok},
+ 'SSL negotiation request from a trusted peer with no PROXY header is rejected'
+);
+$node->wait_for_log(
+ qr/connection from a trusted proxy network must use the PROXY protocol/,
+ $logoff);
+ok(1,
+ 'SSL-first connection from a trusted network logs the PROXY requirement');
+
+# Reject ident authentication
+$logoff = -s $node->logfile;
+$r = proxy_query(
+ proxy_v1(
+ 4, $ident_client, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok(!$r->{ok}, 'ident authentication over a proxied connection is rejected');
+$node->wait_for_log(
+ qr/ident authentication is not supported over connections using the PROXY protocol/,
+ $logoff);
+ok(1, 'ident over the PROXY protocol logs that the method is unsupported');
+
+# Reject malformed header from a trusted peer.
+$logoff = -s $node->logfile;
+$r = proxy_query("PROXY BOGUS arguments here\r\n", $CLIENT_ADDR);
+ok(!$r->{ok}, 'malformed v1 header from trusted peer is rejected');
+$node->wait_for_log(qr/incomplete startup packet/, $logoff);
+ok(1, 'malformed header logs the generic "incomplete startup packet"');
+
+# Reject connection from an untrusted peer.
+$node->append_conf('postgresql.conf', "proxy_networks = '$untrusted_net'\n");
+$node->reload;
+
+$logoff = -s $node->logfile;
+$r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok(!$r->{ok}, 'header from untrusted peer is rejected');
+$node->wait_for_log(qr/incomplete startup packet/, $logoff);
+ok(1, 'untrusted header is handled as an incomplete startup packet');
+
+# Accept peers outside of the proxy networks without the PROXY header
+$r = proxy_query(undef, $CLIENT_ADDR);
+ok($r->{ok}, 'header-less connection from untrusted peer succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $host,
+ 'real peer address reported for an ordinary connection');
+
+# Ignore header when proxy networks is empty
+$node->append_conf('postgresql.conf', "proxy_networks = ''\n");
+$node->reload;
+
+$logoff = -s $node->logfile;
+$r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok(!$r->{ok}, 'header ignored when proxy_networks is empty');
+$node->wait_for_log(qr/incomplete startup packet/, $logoff);
+ok(1, 'disabled feature handles header as an incomplete startup packet');
+
+# ----------------------------------------------------------------------------
+# GUC validation.
+# ----------------------------------------------------------------------------
+my ($ret, $stdout, $stderr);
+
+($ret, $stdout, $stderr) = $node->psql('postgres',
+ "ALTER SYSTEM SET proxy_networks = 'not-an-address'");
+isnt($ret, 0, 'invalid network specification is rejected');
+like(
+ $stderr,
+ qr/invalid value for parameter "proxy_networks"/,
+ 'invalid network: error mentions the parameter');
+like(
+ $stderr,
+ qr/Invalid network specification/,
+ 'invalid network: error shows the reason');
+
+($ret, $stdout, $stderr) =
+ $node->psql('postgres', "ALTER SYSTEM SET proxy_networks = '10.0.0.0/99'");
+isnt($ret, 0, 'invalid CIDR mask length is rejected');
+
+($ret, $stdout, $stderr) = $node->psql('postgres',
+ "ALTER SYSTEM SET proxy_networks = '$loopback_net, $untrusted_net, $loopback_v6_net'"
+);
+is($ret, 0, 'a list of valid networks is accepted')
+ or diag("stderr: $stderr");
+
+# Ensure the special "unix" token is accepted.
+($ret, $stdout, $stderr) = $node->psql('postgres',
+ "ALTER SYSTEM SET proxy_networks = 'unix, $loopback_net'");
+is($ret, 0, 'the "unix" token is accepted in proxy_networks')
+ or diag("stderr: $stderr");
+
+$node->safe_psql('postgres', 'ALTER SYSTEM RESET proxy_networks');
+
+
+# ----------------------------------------------------------------------------
+# pg_stat_activity validation.
+# ----------------------------------------------------------------------------
+$node->append_conf('postgresql.conf', "proxy_networks = '$loopback_net'\n");
+$node->reload;
+
+# v1 PROXY returns proxy_addr and proxy_port for the proxy and client_addr
+# and client_port are parsed from the header.
+my $STAT_PROXY = q{SELECT format('%s|%s|%s|%s|%s',
+ proxy_addr, (proxy_hostname IS NULL), (proxy_port IS NOT NULL),
+ client_addr, client_port)
+ FROM pg_stat_activity WHERE pid = pg_backend_pid()};
+
+$r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ $STAT_PROXY);
+ok($r->{ok}, 'pg_stat_activity over a proxied connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, "$host|t|t|$v1_client_v4|$v1_client_port",
+ 'pg_stat_activity: proxy_addr and port are the proxy, client_addr and port are the real peer'
+);
+
+# v2 LOCAL returns null for proxy_addr and proxy_port.
+my $STAT_PLAIN = q{SELECT format('%s|%s|%s',
+ (proxy_addr IS NULL), (proxy_port IS NULL), client_addr)
+ FROM pg_stat_activity WHERE pid = pg_backend_pid()};
+
+$r = proxy_query(proxy_v2_local(), $STAT_PLAIN);
+ok($r->{ok}, 'pg_stat_activity over a LOCAL connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, "t|t|$host",
+ 'pg_stat_activity: proxy_addr and port are null for a LOCAL command');
+
+# No PROXY header from untrusted network returns null for proxy_addr and
+# proxy_port.
+$node->append_conf('postgresql.conf', "proxy_networks = '$untrusted_net'\n");
+$node->reload;
+
+$r = proxy_query(undef, $STAT_PLAIN);
+ok($r->{ok}, 'pg_stat_activity over an ordinary connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, "t|t|$host",
+ 'pg_stat_activity: proxy_addr and port are null for ordinary connection');
+
+SKIP:
+{
+ skip "Unix-domain sockets not in use on this platform", 2
+ unless $PostgreSQL::Test::Utils::use_unix_sockets
+ && $node->raw_connect_works;
+
+ $node->append_conf('postgresql.conf',
+ "proxy_networks = 'unix, $loopback_net'\n");
+ $node->reload;
+
+ my $stat_unix_proxy = q{SELECT format('%s|%s|%s|%s',
+ (proxy_addr IS NULL), proxy_port, client_addr, client_port)
+ FROM pg_stat_activity WHERE pid = pg_backend_pid()};
+ $r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ $stat_unix_proxy,
+ $unix_connect);
+ ok($r->{ok},
+ 'pg_stat_activity: connection over a proxied Unix socket succeeds')
+ or diag("error: $r->{error}");
+ is($r->{value}, "t|-1|$v1_client_v4|$v1_client_port",
+ 'pg_stat_activity: proxy_addr is NULL and proxy_port is -1, like a Unix-socket client'
+ );
+}
+
+# ----------------------------------------------------------------------------
+# Logs validation.
+# ----------------------------------------------------------------------------
+$node->append_conf('postgresql.conf', "proxy_networks = '$loopback_net'\n");
+$node->reload;
+
+# Check client and proxy escapes for a proxied connection.
+$logoff = -s $node->logfile;
+$r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ "SELECT 'pxlog-proxied'");
+ok($r->{ok}, 'logging: proxied connection succeeds')
+ or diag("error: $r->{error}");
+$node->wait_for_log(
+ qr{PXLOG h=\Q$v1_client_v4\E H=\Q$host\E r=\Q$v1_client_v4\E\($v1_client_port\) R=\Q$host\E\(\d+\) LOG:\s+statement: SELECT 'pxlog-proxied'},
+ $logoff);
+ok(1, 'logging: %h/%r show the client, %H/%R show the proxy');
+
+# Ensure proxy escapes are empty for an ordinary connection.
+$node->append_conf('postgresql.conf', "proxy_networks = '$untrusted_net'\n");
+$node->reload;
+
+$logoff = -s $node->logfile;
+$r = proxy_query(undef, "SELECT 'pxlog-ordinary'");
+ok($r->{ok}, 'logging over an ordinary connection succeeds')
+ or diag("error: $r->{error}");
+$node->wait_for_log(
+ qr{PXLOG h=\Q$host\E H= r=\Q$host\E\(\d+\) R= LOG:\s+statement: SELECT 'pxlog-ordinary'},
+ $logoff);
+ok(1, 'logging: %H/%R are empty without the PROXY protocol');
+
+# Test csvlog and jsonlog destinations.
+$node->append_conf(
+ 'postgresql.conf',
+ "logging_collector = on\n"
+ . "log_destination = 'stderr, csvlog, jsonlog'\n"
+ . "log_rotation_age = 0\n"
+ # Re-trust loopback for the proxied case below.
+ . "proxy_networks = '$loopback_net'\n");
+$node->restart;
+
+# Wait for the collector to report the csv/json file names.
+my $clf_path = $node->data_dir . '/current_logfiles';
+PostgreSQL::Test::Utils::wait_for_file($clf_path, qr/^csvlog /m);
+PostgreSQL::Test::Utils::wait_for_file($clf_path, qr/^jsonlog /m);
+my $current_logfiles = slurp_file($clf_path);
+
+$r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ "SELECT 'pxlog-loggers-proxied'");
+ok($r->{ok}, 'loggers: proxied connection succeeds')
+ or diag("error: $r->{error}");
+
+my $csvline =
+ wait_for_logger_line($node, $current_logfiles, 'csvlog',
+ 'pxlog-loggers-proxied');
+like($csvline, qr/"\Q$host\E:\d+"/, 'csvlog: proxy_host:proxy_port detected');
+
+my $jsonline =
+ wait_for_logger_line($node, $current_logfiles, 'jsonlog',
+ 'pxlog-loggers-proxied');
+like($jsonline, qr/"proxy_host":"\Q$host\E"/,
+ 'jsonlog: proxy_host key holds the proxy host');
+like($jsonline, qr/"proxy_port":\d+/,
+ 'jsonlog: proxy_port key holds the proxy port');
+
+# Ordinary connection returns null for proxy_host and proxy_port.
+$node->append_conf('postgresql.conf', "proxy_networks = '$untrusted_net'\n");
+$node->reload;
+
+$r = proxy_query(undef, "SELECT 'pxlog-loggers-ordinary'");
+ok($r->{ok}, 'loggers: ordinary connection succeeds')
+ or diag("error: $r->{error}");
+
+$csvline =
+ wait_for_logger_line($node, $current_logfiles, 'csvlog',
+ 'pxlog-loggers-ordinary');
+like($csvline, qr/,$/,
+ 'csvlog: proxy_connection column is empty without the PROXY protocol');
+
+$jsonline =
+ wait_for_logger_line($node, $current_logfiles, 'jsonlog',
+ 'pxlog-loggers-ordinary');
+unlike($jsonline, qr/"proxy_host"/,
+ 'jsonlog: proxy_host key is omitted without the PROXY protocol');
+unlike($jsonline, qr/"proxy_port"/,
+ 'jsonlog: proxy_port key is omitted without the PROXY protocol');
+
+SKIP:
+{
+ skip "Unix-domain sockets not in use on this platform", 5
+ unless $PostgreSQL::Test::Utils::use_unix_sockets
+ && $node->raw_connect_works;
+
+ $node->append_conf('postgresql.conf',
+ "proxy_networks = 'unix, $loopback_net'\n");
+ $node->reload;
+
+ $r = proxy_query(
+ proxy_v1(
+ 4, $v1_client_v4, $unused_dest_v4,
+ $v1_client_port, $unused_dest_port),
+ "SELECT 'pxlog-loggers-unix'",
+ $unix_connect);
+ ok($r->{ok}, 'loggers: proxied Unix-socket connection succeeds')
+ or diag("error: $r->{error}");
+
+ my $sl =
+ wait_for_logger_line($node, $current_logfiles, 'stderr',
+ 'pxlog-loggers-unix');
+ like(
+ $sl,
+ qr/h=\Q$v1_client_v4\E H=\[local\] r=\Q$v1_client_v4\E\($v1_client_port\) R=\[local\] /,
+ 'stderr: %H/%R returns [local] with no port for a Unix-socket proxy');
+
+ $csvline =
+ wait_for_logger_line($node, $current_logfiles, 'csvlog',
+ 'pxlog-loggers-unix');
+ like($csvline, qr/"\[local\]"$/,
+ 'csvlog: trailing proxy_connection returns [local] for a Unix-socket proxy'
+ );
+
+ $jsonline =
+ wait_for_logger_line($node, $current_logfiles, 'jsonlog',
+ 'pxlog-loggers-unix');
+ like($jsonline, qr/"proxy_host":"\[local\]"/,
+ 'jsonlog: proxy_host returns [local] for a Unix-socket proxy');
+ unlike($jsonline, qr/"proxy_port"/,
+ 'jsonlog: proxy_port key is omitted for Unix-socket proxy');
+}
+
+done_testing();
diff --git a/src/test/protocol/t/002_proxy_protocol_ssl.pl b/src/test/protocol/t/002_proxy_protocol_ssl.pl
new file mode 100644
index 00000000000..f478648dd08
--- /dev/null
+++ b/src/test/protocol/t/002_proxy_protocol_ssl.pl
@@ -0,0 +1,121 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Tests for the PROXY protocol with sslnegotiation=direct.
+# Run only if OpenSSL is enabled with ALPN support.
+
+use strict;
+use warnings FATAL => 'all';
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use File::Copy qw(copy);
+use Test::More;
+
+use ProxyProtocol;
+
+if (($ENV{with_ssl} || '') ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+unless (eval { require IO::Socket::SSL; 1 })
+{
+ plan skip_all => 'IO::Socket::SSL not available';
+}
+unless (IO::Socket::SSL->can('can_alpn') && IO::Socket::SSL->can_alpn)
+{
+ plan skip_all => 'IO::Socket::SSL lacks ALPN support';
+}
+
+my $host = '127.0.0.1';
+
+# Network ranges trusted as proxies
+my $loopback_net = "$host/32";
+my $client_net = '192.0.2.0/24'; # TEST-NET-1, v1 TCP4 client
+
+# Client (source) addresses carried in the PROXY headers.
+my $v1_client = '192.0.2.1'; # in $client_net
+my $v2_client = '192.0.2.5'; # in $client_net
+my $unused_dest = '198.51.100.9';
+
+# Source ports declared in the PROXY headers
+my $v1_client_port = 56324;
+my $v2_client_port = 40000;
+my $unused_dest_port = 5432;
+
+# Reuse the committed test certificate from the SSL test suite.
+my $ssldir = "$FindBin::RealBin/../../ssl/ssl";
+plan skip_all => "test certificate not found in $ssldir"
+ unless -f "$ssldir/server-cn-only.crt";
+
+my $node = PostgreSQL::Test::Cluster->new('proxy_protocol_ssl');
+$node->init;
+
+copy("$ssldir/server-cn-only.crt", $node->data_dir . '/server.crt')
+ or die "could not copy server certificate: $!";
+copy("$ssldir/server-cn-only.key", $node->data_dir . '/server.key')
+ or die "could not copy server key: $!";
+chmod 0600, $node->data_dir . '/server.key'
+ or die "could not chmod server key: $!";
+
+$node->append_conf('postgresql.conf',
+ "listen_addresses = '$host'\n"
+ . "ssl = on\n"
+ . "ssl_cert_file = 'server.crt'\n"
+ . "ssl_key_file = 'server.key'\n"
+ . "proxy_networks = '$loopback_net'\n");
+$node->append_conf('pg_hba.conf',
+ "host all all $loopback_net trust\n"
+ . "host all all $client_net trust\n");
+
+# cfbot's console output does not capture the server log
+unless ($node->start(fail_ok => 1))
+{
+ diag("server log after failed start:\n"
+ . PostgreSQL::Test::Utils::slurp_file($node->logfile));
+ BAIL_OUT('node failed to start');
+}
+
+my $port = $node->port;
+my $user = $node->safe_psql('postgres', 'SELECT current_user');
+
+set_connection(host => $host, port => $port, user => $user);
+
+# A query to check the client address.
+my $CLIENT_ADDR = 'SELECT host(inet_client_addr())';
+
+# ----------------------------------------------------------------------------
+# Protocol validation.
+# ----------------------------------------------------------------------------
+my $r = proxy_query_with_ssl(
+ proxy_v1(4, $v1_client, $unused_dest, $v1_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok($r->{ok}, 'v1 header before direct SSL: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $v1_client,
+ 'v1 header before direct SSL: client address substituted');
+
+$r = proxy_query_with_ssl(
+ proxy_v2(4, $v2_client, $unused_dest, $v2_client_port, $unused_dest_port),
+ $CLIENT_ADDR);
+ok($r->{ok}, 'v2 header before direct SSL: connection succeeds')
+ or diag("error: $r->{error}");
+is($r->{value}, $v2_client,
+ 'v2 header before direct SSL: client address substituted');
+
+# ----------------------------------------------------------------------------
+# Logs validation.
+# ----------------------------------------------------------------------------
+my $logoff = -s $node->logfile;
+$r = proxy_query_with_ssl(undef, $CLIENT_ADDR);
+ok(!$r->{ok},
+ 'direct SSL with no PROXY header from a trusted peer is rejected');
+$node->wait_for_log(
+ qr/connection from a trusted proxy network must use the PROXY protocol/,
+ $logoff);
+ok(1, 'direct SSL without a header logs the PROXY requirement');
+
+done_testing();
diff --git a/src/test/protocol/t/ProxyProtocol.pm b/src/test/protocol/t/ProxyProtocol.pm
new file mode 100644
index 00000000000..078ea22e7a2
--- /dev/null
+++ b/src/test/protocol/t/ProxyProtocol.pm
@@ -0,0 +1,385 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+ProxyProtocol - helpers for driving PROXY protocol connections in TAP tests
+
+=head1 SYNOPSIS
+
+ use lib $FindBin::RealBin;
+ use ProxyProtocol;
+
+ # Build PROXY headers.
+ my $v1 = proxy_v1(4, '192.0.2.1', '198.51.100.9', 56324, 5432);
+ my $v2 = proxy_v2(4, '203.0.113.5', '198.51.100.9', 40000, 5432);
+
+ # Authenticate (trust method) and run one query over a connected socket.
+ my $addr = authenticate_and_query($sock, $user,
+ 'SELECT host(inet_client_addr())');
+
+ # Or bind the per-test connection details once and drive whole queries.
+ set_connection(host => $host, port => $port, user => $user);
+ my $r = proxy_query($v1, 'SELECT host(inet_client_addr())');
+
+=head1 DESCRIPTION
+
+A minimal hand-rolled implementation of the parts of the v3 frontend/backend
+protocol and the PROXY protocol that the regression tests need. It is enough
+to prepend a PROXY header, authenticate with the trust method, and run a single
+query over a raw socket, whether plaintext or TLS.
+
+=cut
+
+package ProxyProtocol;
+
+use strict;
+use warnings FATAL => 'all';
+use Exporter 'import';
+use Socket qw(inet_aton inet_pton AF_INET6);
+use IO::Socket::INET;
+
+our @EXPORT = qw(
+ startup_packet
+ read_message
+ error_text
+ proxy_v1
+ proxy_v1_unknown
+ proxy_v2
+ proxy_v2_local
+ authenticate_and_query
+ set_connection
+ proxy_query
+ ssl_request
+ proxy_query_with_ssl
+ logger_file_name
+ wait_for_logger_line
+);
+
+use constant PROXY_V2_SIG =>
+ "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a";
+
+sub startup_packet
+{
+ my (%params) = @_;
+ my $body = pack('N', 0x00030000); # protocol version 3.0
+ for my $key (sort keys %params)
+ {
+ $body .= $key . "\0" . $params{$key} . "\0";
+ }
+ $body .= "\0"; # empty key name terminates the list
+ return pack('N', length($body) + 4) . $body;
+}
+
+# Returns undef when the peer closed the connection before $n bytes arrived, so
+# callers can treat a server-side disconnect as a distinct outcome.
+sub read_exact
+{
+ my ($sock, $n) = @_;
+ my $buf = '';
+ while (length($buf) < $n)
+ {
+ my $chunk;
+ my $r = sysread($sock, $chunk, $n - length($buf));
+ return if !defined $r || $r == 0;
+ $buf .= $chunk;
+ }
+ return $buf;
+}
+
+# Read one typed backend message. Returns ($type, $payload), or an empty list
+# once the connection has closed.
+sub read_message
+{
+ my ($sock) = @_;
+ my $type = read_exact($sock, 1);
+ return () unless defined $type;
+ my $lenbytes = read_exact($sock, 4);
+ return () unless defined $lenbytes;
+ my $len = unpack('N', $lenbytes);
+ my $payload = '';
+ if ($len > 4)
+ {
+ $payload = read_exact($sock, $len - 4);
+ return () unless defined $payload;
+ }
+ return ($type, $payload);
+}
+
+# Pull the human-readable text (the 'M' field) out of an ErrorResponse or
+# NoticeResponse body, which is the only part the tests assert on.
+sub error_text
+{
+ my ($payload) = @_;
+ for my $field (split /\0/, $payload)
+ {
+ return substr($field, 1) if substr($field, 0, 1) eq 'M';
+ }
+ return '';
+}
+
+# Build the PROXY v1 payload.
+sub proxy_v1
+{
+ my ($family, $src, $dst, $sport, $dport) = @_;
+ my $proto = $family == 4 ? 'TCP4' : 'TCP6';
+ return "PROXY $proto $src $dst $sport $dport\r\n";
+}
+
+# Build the PROXY v1 payload for the unknown command.
+sub proxy_v1_unknown
+{
+ return "PROXY UNKNOWN\r\n";
+}
+
+# Build the PROXY v2 payload.
+sub proxy_v2
+{
+ my ($family, $src, $dst, $sport, $dport, $tlv) = @_;
+ $tlv = '' unless defined $tlv;
+ my $vercmd = "\x21"; # version 2, command PROXY
+ my ($fambyte, $addr);
+ if ($family == 4)
+ {
+ $fambyte = "\x11"; # TCP4
+ $addr =
+ inet_aton($src)
+ . inet_aton($dst)
+ . pack('n', $sport)
+ . pack('n', $dport);
+ }
+ else
+ {
+ $fambyte = "\x21"; # TCP6
+ $addr =
+ inet_pton(AF_INET6, $src)
+ . inet_pton(AF_INET6, $dst)
+ . pack('n', $sport)
+ . pack('n', $dport);
+ }
+ return
+ PROXY_V2_SIG
+ . $vercmd
+ . $fambyte
+ . pack('n', length($addr) + length($tlv))
+ . $addr
+ . $tlv;
+}
+
+# Build the PROXY v2 payload for the LOCAL command.
+sub proxy_v2_local
+{
+ return PROXY_V2_SIG . "\x20" . "\x00"
+ . pack('n', 0); # command LOCAL, AF_UNSPEC
+}
+
+# Drive a full session over an already-connected stream, whether plaintext or
+# TLS, and return the first column of the first row (undef for NULL or no row).
+# Dies with a descriptive message on EOF, a server ErrorResponse, or a non-zero
+# authentication request, so callers can map any failure to a single eval.
+sub authenticate_and_query
+{
+ my ($stream, $user, $query) = @_;
+
+ print $stream startup_packet(user => $user, database => 'postgres');
+
+ while (1)
+ {
+ my ($type, $payload) = read_message($stream);
+ defined $type or die "connection closed during startup\n";
+ die error_text($payload) . "\n" if $type eq 'E';
+ if ($type eq 'R')
+ {
+ my $code = unpack('N', $payload);
+ die "authentication required (code $code)\n" if $code != 0;
+ }
+ last if $type eq 'Z'; # ReadyForQuery
+ }
+
+ print $stream 'Q' . pack('N', length($query) + 5) . $query . "\0";
+
+ my $value;
+ while (1)
+ {
+ my ($type, $payload) = read_message($stream);
+ defined $type or die "connection closed during query\n";
+ die error_text($payload) . "\n" if $type eq 'E';
+ if ($type eq 'D') # DataRow
+ {
+ my $ncols = unpack('n', substr($payload, 0, 2));
+ if ($ncols >= 1)
+ {
+ my $collen = unpack('N', substr($payload, 2, 4));
+ $value =
+ ($collen == 0xFFFFFFFF)
+ ? undef
+ : substr($payload, 6, $collen);
+ }
+ }
+ last if $type eq 'Z';
+ }
+
+ return $value;
+}
+
+# The per-test connection details that the query drivers below reuse, bound once
+# with set_connection() so the individual calls need only the header and query.
+my ($conn_host, $conn_port, $conn_user);
+
+sub set_connection
+{
+ my (%params) = @_;
+ $conn_host = $params{host};
+ $conn_port = $params{port};
+ $conn_user = $params{user};
+ return;
+}
+
+# Connect to the server, optionally sending $prefix (a PROXY header) ahead of
+# the startup packet, then authenticate and run a one-column query. The
+# connection defaults to TCP loopback. Pass $connect, a coderef returning a
+# connected socket, to use another transport such as a Unix socket.
+#
+# Returns a hash-ref with ok => 1 and the query value on success, or ok => 0
+# and an error string otherwise.
+sub proxy_query
+{
+ my ($prefix, $query, $connect) = @_;
+ my $result = { ok => 0, error => 'unknown' };
+
+ eval {
+ local $SIG{ALRM} = sub { die "timeout\n" };
+ alarm($PostgreSQL::Test::Utils::timeout_default);
+
+ my $sock =
+ $connect
+ ? $connect->()
+ : IO::Socket::INET->new(
+ PeerHost => $conn_host,
+ PeerPort => $conn_port,
+ Proto => 'tcp');
+ die "cannot connect: $!\n" unless $sock;
+ $sock->autoflush(1);
+
+ print $sock $prefix if defined $prefix;
+ my $value = authenticate_and_query($sock, $conn_user, $query);
+
+ close $sock;
+ alarm(0);
+ $result = { ok => 1, value => $value };
+ };
+ return { ok => 0, error => $@ } if $@;
+ return $result;
+}
+
+# Open a raw connection and send an SSL negotiation request as the very first
+# bytes, with no preceding PROXY header.
+#
+# Returns a hash-ref with ok => 1 if the request succeeded, or ok => 0 and an
+# error string if it failed.
+sub ssl_request
+{
+ my $result = { ok => 0, error => 'unknown' };
+
+ eval {
+ local $SIG{ALRM} = sub { die "timeout\n" };
+ alarm($PostgreSQL::Test::Utils::timeout_default);
+
+ my $sock = IO::Socket::INET->new(
+ PeerHost => $conn_host,
+ PeerPort => $conn_port,
+ Proto => 'tcp') or die "cannot connect: $!\n";
+ $sock->autoflush(1);
+
+ # SSLRequest is a length of 8 followed by the negotiate-SSL request code.
+ print $sock pack('N', 8) . pack('N', 80877103);
+
+ while (1)
+ {
+ my ($type, $payload) = read_message($sock);
+ die "connection closed\n" unless defined $type;
+ die error_text($payload) . "\n" if $type eq 'E';
+ last if $type eq 'Z';
+ }
+ close $sock;
+ alarm(0);
+ $result = { ok => 1 };
+ };
+ return { ok => 0, error => $@ } if $@;
+ return $result;
+}
+
+# Connect over TCP, send $prefix (a PROXY header) in cleartext, then open a
+# direct SSL connection (no SSLRequest) offering the PostgreSQL ALPN protocol,
+# authenticate over TLS, and run a one-column query. The caller must have
+# loaded IO::Socket::SSL, which the module leaves optional.
+#
+# Returns a hash-ref with ok => 1 and the query value on success, or ok => 0
+# and an error string otherwise.
+sub proxy_query_with_ssl
+{
+ my ($prefix, $query) = @_;
+ my $result = { ok => 0, error => 'unknown' };
+
+ eval {
+ local $SIG{ALRM} = sub { die "timeout\n" };
+ alarm($PostgreSQL::Test::Utils::timeout_default);
+
+ my $sock = IO::Socket::INET->new(
+ PeerHost => $conn_host,
+ PeerPort => $conn_port,
+ Proto => 'tcp') or die "cannot connect: $!\n";
+ $sock->autoflush(1);
+
+ # The PROXY header travels in cleartext, ahead of the TLS handshake.
+ print $sock $prefix if defined $prefix;
+
+ my $ssl = IO::Socket::SSL->start_SSL(
+ $sock,
+ SSL_verify_mode => 0,
+ SSL_alpn_protocols => ['postgresql'])
+ or die "TLS handshake failed: "
+ . (IO::Socket::SSL->errstr // 'unknown') . "\n";
+
+ die "ALPN did not negotiate 'postgresql'\n"
+ unless defined $ssl->alpn_selected
+ && $ssl->alpn_selected eq 'postgresql';
+
+ my $value = authenticate_and_query($ssl, $conn_user, $query);
+
+ close $ssl;
+ alarm(0);
+ $result = { ok => 1, value => $value };
+ };
+ return { ok => 0, error => $@ } if $@;
+ return $result;
+}
+
+# Given the contents of current_logfiles, return the file name (relative to the
+# data directory) recorded for the given destination, 'csvlog' or 'jsonlog'.
+sub logger_file_name
+{
+ my ($current_logfiles, $format) = @_;
+ return ($current_logfiles =~ /^$format (.*)$/m) ? $1 : undef;
+}
+
+# Wait for the collected $format log file to contain $marker, then return the
+# line carrying it. A unique marker query is what pins a log record to one
+# specific connection.
+sub wait_for_logger_line
+{
+ my ($node, $current_logfiles, $format, $marker) = @_;
+ my $path =
+ $node->data_dir . '/' . logger_file_name($current_logfiles, $format);
+
+ PostgreSQL::Test::Utils::wait_for_file($path, quotemeta($marker));
+
+ foreach my $line (split(/\n/, PostgreSQL::Test::Utils::slurp_file($path)))
+ {
+ return $line if index($line, $marker) >= 0;
+ }
+ return '';
+}
+
+1;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a65a5bf0c4f..ed574ca2fe1 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1786,6 +1786,9 @@ pg_stat_activity| SELECT s.datid,
s.client_addr,
s.client_hostname,
s.client_port,
+ s.proxy_addr,
+ s.proxy_hostname,
+ s.proxy_port,
s.backend_start,
s.xact_start,
s.query_start,
@@ -1798,7 +1801,7 @@ pg_stat_activity| SELECT s.datid,
s.query_id,
s.query,
s.backend_type
- FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+ FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, proxy_addr, proxy_hostname, proxy_port)
LEFT JOIN pg_database d ON ((s.datid = d.oid)))
LEFT JOIN pg_authid u ON ((s.usesysid = u.oid)));
pg_stat_all_indexes| SELECT c.oid AS relid,
@@ -1944,7 +1947,7 @@ pg_stat_gssapi| SELECT pid,
gss_princ AS principal,
gss_enc AS encrypted,
gss_delegation AS credentials_delegated
- FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+ FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, proxy_addr, proxy_hostname, proxy_port)
WHERE (client_port IS NOT NULL);
pg_stat_io| SELECT backend_type,
object,
@@ -2247,7 +2250,7 @@ pg_stat_replication| SELECT s.pid,
w.sync_priority,
w.sync_state,
w.reply_time
- FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+ FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, proxy_addr, proxy_hostname, proxy_port)
JOIN pg_stat_get_wal_senders() w(pid, state, sent_lsn, write_lsn, flush_lsn, replay_lsn, write_lag, flush_lag, replay_lag, sync_priority, sync_state, reply_time) ON ((s.pid = w.pid)))
LEFT JOIN pg_authid u ON ((s.usesysid = u.oid)));
pg_stat_replication_slots| SELECT s.slot_name,
@@ -2284,7 +2287,7 @@ pg_stat_ssl| SELECT pid,
ssl_client_dn AS client_dn,
ssl_client_serial AS client_serial,
ssl_issuer_dn AS issuer_dn
- FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+ FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, proxy_addr, proxy_hostname, proxy_port)
WHERE (client_port IS NOT NULL);
pg_stat_subscription| SELECT su.oid AS subid,
su.subname,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1969d467c1d..24bd56f22fa 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2450,6 +2450,9 @@ PropGraphLabelAndProperties
PropGraphProperties
PropGraphVertex
ProtocolVersion
+ProxyNet
+ProxyNets
+ProxyProtocolResult
PrsStorage
PruneFreezeParams
PruneFreezeResult
--
2.39.5
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-09-28 13:23 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-03 13:36 ` Re: PROXY protocol support Daniel Gustafsson <daniel@yesql.se>
2021-11-04 11:03 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-11-15 23:03 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2022-02-25 10:41 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-03-09 16:23 ` Re: PROXY protocol support Peter Eisentraut <peter.eisentraut@enterprisedb.com>
2022-03-09 16:29 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-04-01 22:16 ` Re: PROXY protocol support wilfried roset <wilfried.roset@gmail.com>
2022-04-08 11:58 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2022-07-28 20:05 ` Re: PROXY protocol support Jacob Champion <jchampion@timescale.com>
2024-02-03 11:37 ` Re: PROXY protocol support Julien Riou <julien@riou.xyz>
2026-06-20 05:46 ` Re: PROXY protocol support Julien Riou <julien@riou.xyz>
@ 2026-09-23 00:27 ` Manu <manuelreyesbravo@gmail.com>
1 sibling, 0 replies; 56+ messages in thread
From: Manu @ 2026-09-23 00:27 UTC (permalink / raw)
To: Julien Riou <julien@riou.xyz>; +Cc: Magnus Hagander <magnus@hagander.net>; Jacob Champion <jacob.champion@enterprisedb.com>; pgsql-hackers@lists.postgresql.org
Hi Julien,
I tested the rewrite. It works, including the parts that are easy to
claim and tedious to check, so here is the evidence rather than a "looks
good".
I did not use libpq for this: the attached script builds the PROXY
header by hand and then sends a StartupMessage, which makes it possible
to send what a normal client cannot - truncated headers, wrong
signatures, a length that lies. Everything below is on your v12 applied
to master (one conflict, typedefs.list, trivial).
1. It does what it says
With proxy_networks = '127.0.0.1/32', a pg_hba.conf that only trusts the
address inside the header, and a connection arriving from 127.0.0.1:
client_addr | client_port | proxy_addr | proxy_port
--------------+-------------+------------+------------
198.51.100.7 | 51234 | 127.0.0.1 | 50664
and authentication matched the pg_hba line for 198.51.100.7, not the one
for 127.0.0.1. That is the whole point of the feature and it holds.
Per header type, same setup:
v1 IPv4 server uses the header address
v1 IPv6 server uses the header address
v2 IPv4 server uses the header address
v2 LOCAL connection accepted with the real address, per spec
no header rejected
garbage v1 rejected
bad v2 signature rejected
2. The three claims in your message
one port for both with proxy_networks empty, an ordinary
connection works and a PROXY header is
rejected as the garbage it is at that point
trusted, no header rejected
untrusted, with header rejected, and nothing is leaked
For the last one I set proxy_networks = '10.0.0.0/8' and connected from
127.0.0.1, so the client is outside the trusted set:
no header accepted, ordinary connection
v1 header server closes without replying
v2 header server closes without replying
and an unpatched master, same client, same v1 header, also closes
without replying. The two are indistinguishable from the outside, so a
scanner cannot tell the feature is compiled in. That is the claim, and
it holds.
3. Truncated headers: not a problem, but I had to check
Two cases left the connection hanging, which is what a streaming parser
should do:
v1 cut before the CRLF
v2 announcing 200 bytes of addresses and sending 4
The question is whether anything closes them. With
authentication_timeout = 3s, all of them are closed by the server at
3.0s, same as a connection that sends nothing at all. So the existing
mechanism covers it and there is nothing to fix. I am mentioning it
because it is the first thing a reviewer worried about resource
exhaustion will ask, and now it is answered.
4. One thing to decide about the logs
Since the header is parsed late, "connection received" still prints the
proxy's address, and the next line authenticates against the client's:
LOG: connection received: host=127.0.0.1 port=50664
LOG: connection authenticated: ... (pg_hba.conf:3) <- the 198.51.100.7 rule
The same connection appears with two different addresses in consecutive
lines. Both are true and it follows from parsing late, which I agree is
the right call, but an operator reading logs will trip on it. Either
"connection received" should be emitted after the header is parsed, or
the documentation should say that this line carries the proxy address.
src/test/protocol passes, 2 files, 71 tests.
I have not benchmarked the "no performance regression expected" claim
yet. If that is a blocker for anyone I can measure connection setup with
and without proxy_networks set.
Regards,
Manu
#!/usr/bin/env python3
"""Cliente que habla PROXY protocol v1 y v2 contra un PostgreSQL parchado.
proxy_client.py <puerto> <caso>
No usa libpq: arma el header PROXY a mano y despues el StartupMessage, que es
lo unico que hace falta para comprobar que el servidor toma la direccion del
header y no la del socket. Asi se puede probar tambien lo que un cliente
normal no puede mandar: headers invalidos, truncados o de familias raras.
Casos:
v1 header de texto valido, IPv4
v1_ipv6 header de texto valido, IPv6
v2 header binario valido, IPv4
v2_local header binario con comando LOCAL (el proxy habla de si mismo)
sin_header conexion directa, sin header
v1_basura "PROXY " seguido de basura
v1_corto header v1 cortado a la mitad
v2_mal_sig header binario con la firma equivocada
v2_largo v2 que declara mas bytes de los que manda
"""
import socket
import struct
import sys
V2_SIG = b"\r\n\r\n\x00\r\nQUIT\n"
def startup(user=b"postgres", db=b"postgres"):
body = struct.pack("!i", 196608) + b"user\x00" + user + b"\x00" \
+ b"database\x00" + db + b"\x00\x00"
return struct.pack("!i", len(body) + 4) + body
def header(caso):
if caso == "v1":
return b"PROXY TCP4 198.51.100.7 203.0.113.9 51234 5432\r\n"
if caso == "v1_ipv6":
return b"PROXY TCP6 2001:db8::7 2001:db8::9 51234 5432\r\n"
if caso == "v2":
# ver 2 / PROXY, TCP over IPv4, 12 bytes de direcciones
addr = socket.inet_aton("198.51.100.7") + socket.inet_aton("203.0.113.9") \
+ struct.pack("!HH", 51234, 5432)
return V2_SIG + bytes([0x21, 0x11]) + struct.pack("!H", len(addr)) + addr
if caso == "v2_local":
return V2_SIG + bytes([0x20, 0x00]) + struct.pack("!H", 0)
if caso == "sin_header":
return b""
if caso == "v1_basura":
return b"PROXY estoesbasura\r\n"
if caso == "v1_corto":
return b"PROXY TCP4 198.51.100.7 203."
if caso == "v2_mal_sig":
addr = socket.inet_aton("198.51.100.7") + socket.inet_aton("203.0.113.9") \
+ struct.pack("!HH", 51234, 5432)
return b"\x00" * 12 + bytes([0x21, 0x11]) + struct.pack("!H", len(addr)) + addr
if caso == "v2_largo":
return V2_SIG + bytes([0x21, 0x11]) + struct.pack("!H", 200) + b"\x00" * 4
raise SystemExit(f"caso desconocido: {caso}")
def main(port, caso):
s = socket.create_connection(("127.0.0.1", int(port)), timeout=5)
h = header(caso)
if h:
s.sendall(h)
s.sendall(startup())
try:
data = s.recv(4096)
except socket.timeout:
print(f"{caso}: TIMEOUT (el servidor no contesto)")
return
if not data:
print(f"{caso}: el servidor cerro sin responder")
return
tipo = chr(data[0])
if tipo == "E":
# mensaje de error: campos separados por \0
campos = data[5:].split(b"\x00")
msg = next((c[1:].decode(errors="replace") for c in campos
if c[:1] in (b"M",)), "?")
print(f"{caso}: ERROR -> {msg}")
elif tipo == "R":
print(f"{caso}: conexion aceptada (mensaje de autenticacion)")
else:
print(f"{caso}: respuesta tipo '{tipo}'")
s.close()
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])
Attachments:
[text/plain] nocfbot-proxy-client.py.txt (3.2K, ../../179012327876.1567269.13282166184972783880@gmail.com/2-nocfbot-proxy-client.py.txt)
download | inline:
#!/usr/bin/env python3
"""Cliente que habla PROXY protocol v1 y v2 contra un PostgreSQL parchado.
proxy_client.py <puerto> <caso>
No usa libpq: arma el header PROXY a mano y despues el StartupMessage, que es
lo unico que hace falta para comprobar que el servidor toma la direccion del
header y no la del socket. Asi se puede probar tambien lo que un cliente
normal no puede mandar: headers invalidos, truncados o de familias raras.
Casos:
v1 header de texto valido, IPv4
v1_ipv6 header de texto valido, IPv6
v2 header binario valido, IPv4
v2_local header binario con comando LOCAL (el proxy habla de si mismo)
sin_header conexion directa, sin header
v1_basura "PROXY " seguido de basura
v1_corto header v1 cortado a la mitad
v2_mal_sig header binario con la firma equivocada
v2_largo v2 que declara mas bytes de los que manda
"""
import socket
import struct
import sys
V2_SIG = b"\r\n\r\n\x00\r\nQUIT\n"
def startup(user=b"postgres", db=b"postgres"):
body = struct.pack("!i", 196608) + b"user\x00" + user + b"\x00" \
+ b"database\x00" + db + b"\x00\x00"
return struct.pack("!i", len(body) + 4) + body
def header(caso):
if caso == "v1":
return b"PROXY TCP4 198.51.100.7 203.0.113.9 51234 5432\r\n"
if caso == "v1_ipv6":
return b"PROXY TCP6 2001:db8::7 2001:db8::9 51234 5432\r\n"
if caso == "v2":
# ver 2 / PROXY, TCP over IPv4, 12 bytes de direcciones
addr = socket.inet_aton("198.51.100.7") + socket.inet_aton("203.0.113.9") \
+ struct.pack("!HH", 51234, 5432)
return V2_SIG + bytes([0x21, 0x11]) + struct.pack("!H", len(addr)) + addr
if caso == "v2_local":
return V2_SIG + bytes([0x20, 0x00]) + struct.pack("!H", 0)
if caso == "sin_header":
return b""
if caso == "v1_basura":
return b"PROXY estoesbasura\r\n"
if caso == "v1_corto":
return b"PROXY TCP4 198.51.100.7 203."
if caso == "v2_mal_sig":
addr = socket.inet_aton("198.51.100.7") + socket.inet_aton("203.0.113.9") \
+ struct.pack("!HH", 51234, 5432)
return b"\x00" * 12 + bytes([0x21, 0x11]) + struct.pack("!H", len(addr)) + addr
if caso == "v2_largo":
return V2_SIG + bytes([0x21, 0x11]) + struct.pack("!H", 200) + b"\x00" * 4
raise SystemExit(f"caso desconocido: {caso}")
def main(port, caso):
s = socket.create_connection(("127.0.0.1", int(port)), timeout=5)
h = header(caso)
if h:
s.sendall(h)
s.sendall(startup())
try:
data = s.recv(4096)
except socket.timeout:
print(f"{caso}: TIMEOUT (el servidor no contesto)")
return
if not data:
print(f"{caso}: el servidor cerro sin responder")
return
tipo = chr(data[0])
if tipo == "E":
# mensaje de error: campos separados por \0
campos = data[5:].split(b"\x00")
msg = next((c[1:].decode(errors="replace") for c in campos
if c[:1] in (b"M",)), "?")
print(f"{caso}: ERROR -> {msg}")
elif tipo == "R":
print(f"{caso}: conexion aceptada (mensaje de autenticacion)")
else:
print(f"{caso}: respuesta tipo '{tipo}'")
s.close()
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 00:50 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-05 09:22 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-06-29 08:08 ` Magnus Hagander <magnus@hagander.net>
1 sibling, 0 replies; 56+ messages in thread
From: Magnus Hagander @ 2021-06-29 08:08 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Tue, Mar 9, 2021 at 11:25 AM Magnus Hagander <magnus@hagander.net> wrote:
>
> On Sat, Mar 6, 2021 at 5:30 PM Magnus Hagander <magnus@hagander.net> wrote:
> >
> > On Sat, Mar 6, 2021 at 4:17 PM Magnus Hagander <magnus@hagander.net> wrote:
> > >
> > > On Fri, Mar 5, 2021 at 8:11 PM Jacob Champion <pchampion@vmware.com> wrote:
> > > >
> > > > On Fri, 2021-03-05 at 10:22 +0100, Magnus Hagander wrote:
> > > > > On Fri, Mar 5, 2021 at 12:21 AM Jacob Champion <pchampion@vmware.com> wrote:
> > > > > > The original-host logging isn't working for me:
> > > > > >
> > > > > > [...]
> > > > >
> > > > > That's interesting -- it works perfectly fine here. What platform are
> > > > > you testing on?
> > > >
> > > > Ubuntu 20.04.
> > >
> > > Curious. It doesn't show up on my debian.
> > >
> > > But either way -- it was clearly wrong :)
> > >
> > >
> > > > > (I sent for sizeof(SockAddr) to make it
> > > > > easier to read without having to look things up, but the net result is
> > > > > the same)
> > > >
> > > > Cool. Did you mean to attach a patch?
> > >
> > > I didn't, I had some other hacks that were broken :) I've attached one
> > > now which includes those changes.
> > >
> > >
> > > > == More Notes ==
> > > >
> > > > (Stop me if I'm digging too far into a proof of concept patch.)
> > >
> > > Definitely not -- much appreciated, and just what was needed to take
> > > it from poc to a proper one!
> > >
> > >
> > > > > + proxyaddrlen = pg_ntoh16(proxyheader.len);
> > > > > +
> > > > > + if (proxyaddrlen > sizeof(proxyaddr))
> > > > > + {
> > > > > + ereport(COMMERROR,
> > > > > + (errcode(ERRCODE_PROTOCOL_VIOLATION),
> > > > > + errmsg("oversized proxy packet")));
> > > > > + return STATUS_ERROR;
> > > > > + }
> > > >
> > > > I think this is not quite right -- if there's additional data beyond
> > > > the IPv6 header size, that just means there are TLVs tacked onto the
> > > > header that we should ignore. (Or, eventually, use.)
> > >
> > > Yeah, you're right. Fallout of too much moving around. I think inthe
> > > end that code should just be removed, in favor of the discard path as
> > > you mentinoed below.
> > >
> > >
> > > > Additionally, we need to check for underflow as well. A misbehaving
> > > > proxy might not send enough data to fill up the address block for the
> > > > address family in use.
> > >
> > > I used to have that check. I seem to have lost it in restructuring. Added back!
> > >
> > >
> > > > > + /* If there is any more header data present, skip past it */
> > > > > + if (proxyaddrlen > sizeof(proxyaddr))
> > > > > + pq_discardbytes(proxyaddrlen - sizeof(proxyaddr));
> > > >
> > > > This looks like dead code, given that we'll error out for the same
> > > > check above -- but once it's no longer dead code, the return value of
> > > > pq_discardbytes should be checked for EOF.
> > >
> > > Yup.
> > >
> > >
> > > > > + else if (proxyheader.fam == 0x11)
> > > > > + {
> > > > > + /* TCPv4 */
> > > > > + port->raddr.addr.ss_family = AF_INET;
> > > > > + port->raddr.salen = sizeof(struct sockaddr_in);
> > > > > + ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
> > > > > + ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
> > > > > + }
> > > >
> > > > I'm trying to reason through the fallout of setting raddr and not
> > > > laddr. I understand why we're not setting laddr -- several places in
> > > > the code rely on the laddr to actually refer to a machine-local address
> > > > -- but the fact that there is no actual connection from raddr to laddr
> > > > could cause shenanigans. For example, the ident auth protocol will just
> > > > break (and it might be nice to explicitly disable it for PROXY
> > > > connections). Are there any other situations where a "faked" raddr
> > > > could throw off Postgres internals?
> > >
> > > That's a good point to discuss. I thought about it initially and
> > > figured it'd be even worse to actually copy over laddr since that
> > > woudl then suddenly have the IP address belonging to a different
> > > machine.. And then I forgot to enumerate the other cases.
> > >
> > > For ident, disabling the method seems reasonable.
> > >
> > > Another thing that shows up with added support for running the proxy
> > > protocol over Unix sockets, is that PostgreSQL refuses to do SSL over
> > > Unix sockets. So that check has to be updated to allow it over proxy
> > > connections. Same for GSSAPI.
> > >
> > > An interesting thing is what to do about
> > > inet_server_addr/inet_server_port. That sort of loops back up to the
> > > original question of where/how to expose the information about the
> > > proxy in general (since right now it just logs). Right now you can
> > > actually use inet_server_port() to see if the connection was proxied
> > > (as long as it was over tcp).
> > >
> > > Attached is an updated, which covers your comments, as well as adds
> > > unix socket support (per your question and Alvaros confirmed usecase).
> > > It allows proxy connections over unix sockets, but I saw no need to
> > > get into unix sockets over the proxy protocol (dealing with paths
> > > between machines etc).
> > >
> > > I changed the additional ListenSocket array to instead declare
> > > ListenSocket as an array of structs holding two fields. Seems cleaner,
> > > and especially should there be further extensions needed in the
> > > future.
> > >
> > > I've also added some trivial tests (man that took an ungodly amount of
> > > fighting perl -- it's clearly been a long time since I used perl
> > > properly). They probably need some more love but it's a start.
> > >
> > > And of course rebased.
> >
> > Pfft, I was hoping for cfbot to pick it up and test it on a different
> > platform. Of course, for it to do that, I need to include the test
> > directory in the Makefile. Here's a new one which adds that, no other
> > changes.
>
> So cfbot didn't like thato ne one bit. Turns out that it's not a great
> idea to hardcode the username "mha" in the tests :)
>
> And also changed to only use unix sockets for the tests on linux, and
> tcp only on windows. Because that's how our tests are supposed to be.
PFA a rebase to make cfbot happy.
There's another set or review notes from Jacob on March 11, that I
will also address, but it's not included in this version.
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
Attachments:
[text/x-patch] proxy_protocol_6.patch (35.5K, ../../CABUevEza0JmZk6HLznfVF-B--atWig9qpMeaZp=e-fNGZBq32g@mail.gmail.com/2-proxy_protocol_6.patch)
download | inline diff:
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 02f0489112..a3ff09b3ac 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -353,6 +353,15 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl
the client's host name instead of the IP address in the log.
</para>
+ <para>
+ If <xref linkend="guc-proxy-port"/> is enabled and the
+ connection is made through a proxy server using the PROXY
+ protocol, the actual IP address of the client will be used
+ for matching. If a connection is made through a proxy server
+ not using the PROXY protocol, the IP address of the
+ proxy server will be used.
+ </para>
+
<para>
These fields do not apply to <literal>local</literal> records.
</para>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 6098f6b020..c8a7d2a3b7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -682,6 +682,56 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-proxy-port" xreflabel="proxy_port">
+ <term><varname>proxy_port</varname> (<type>integer</type>)
+ <indexterm>
+ <primary><varname>proxy_port</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ The TCP port the server listens on for PROXY connections, disabled by
+ default. If set to a number, <productname>PostgreSQL</productname>
+ will listen on this port on the same addresses as for regular
+ connections, but expect all connections to use the PROXY protocol to
+ identify the client. This parameter can only be set at server start.
+ </para>
+ <para>
+ If a proxy connection is done over this port, and the proxy is listed
+ in <xref linkend="guc-proxy-servers" />, the actual client address
+ will be considered as the address of the client, instead of listing
+ all connections as coming from the proxy server.
+ </para>
+ <para>
+ The <ulink url="http://www.haproxy.org/download/1.9/doc/proxy-protocol.txt">PROXY
+ protocol</ulink> is maintained by <productname>HAProxy</productname>,
+ and supported in many proxies and load
+ balancers. <productname>PostgreSQL</productname> supports version 2
+ of the protocol.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-proxy-servers" xreflabel="proxy_servers">
+ <term><varname>proxy_servers</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>proxy_servers</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ A comma separated list of one or more host names, cidr specifications or the
+ literal <literal>unix</literal>, indicating which proxy servers to trust when
+ connecting on the port specified in <xref linkend="guc-proxy-port" />.
+ </para>
+ <para>
+ If a proxy connection is made from an IP address not covered by this
+ list, the connection will be rejected. By default no proxy is trusted
+ and all proxy connections will be rejected.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-max-connections" xreflabel="max_connections">
<term><varname>max_connections</varname> (<type>integer</type>)
<indexterm>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 967b5ef73c..6cf6e51708 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -1851,6 +1851,14 @@ ident_inet(hbaPort *port)
*la = NULL,
hints;
+ if (port->isProxy)
+ {
+ ereport(LOG,
+ (errcode_for_socket_access(),
+ errmsg("Ident authentication cannot be used over PROXY connections")));
+ return STATUS_ERROR;
+ }
+
/*
* Might look a little weird to first convert it to text and then back to
* sockaddr, but it's protocol independent.
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 89a5f901aa..dd63be54e2 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -311,13 +311,13 @@ socket_close(int code, Datum arg)
* Successfully opened sockets are added to the ListenSocket[] array (of
* length MaxListen), at the first position that isn't PGINVALID_SOCKET.
*
- * RETURNS: STATUS_OK or STATUS_ERROR
+ * RETURNS: The PQlistenSocket listening on, or NULL in case of error
*/
-int
+PQlistenSocket *
StreamServerPort(int family, const char *hostName, unsigned short portNumber,
const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen)
+ PQlistenSocket ListenSocket[], int MaxListen)
{
pgsocket fd;
int err;
@@ -362,10 +362,10 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("Unix-domain socket path \"%s\" is too long (maximum %d bytes)",
unixSocketPath,
(int) (UNIXSOCK_PATH_BUFLEN - 1))));
- return STATUS_ERROR;
+ return NULL;
}
if (Lock_AF_UNIX(unixSocketDir, unixSocketPath) != STATUS_OK)
- return STATUS_ERROR;
+ return NULL;
service = unixSocketPath;
}
else
@@ -388,7 +388,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
service, gai_strerror(ret))));
if (addrs)
pg_freeaddrinfo_all(hint.ai_family, addrs);
- return STATUS_ERROR;
+ return NULL;
}
for (addr = addrs; addr; addr = addr->ai_next)
@@ -405,7 +405,7 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
/* See if there is still room to add 1 more socket. */
for (; listen_index < MaxListen; listen_index++)
{
- if (ListenSocket[listen_index] == PGINVALID_SOCKET)
+ if (ListenSocket[listen_index].socket == PGINVALID_SOCKET)
break;
}
if (listen_index >= MaxListen)
@@ -584,16 +584,16 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
(errmsg("listening on %s address \"%s\", port %d",
familyDesc, addrDesc, (int) portNumber)));
- ListenSocket[listen_index] = fd;
+ ListenSocket[listen_index].socket = fd;
added++;
}
pg_freeaddrinfo_all(hint.ai_family, addrs);
if (!added)
- return STATUS_ERROR;
+ return NULL;
- return STATUS_OK;
+ return &ListenSocket[listen_index];
}
@@ -1118,7 +1118,7 @@ pq_getbytes(char *s, size_t len)
* returns 0 if OK, EOF if trouble
* --------------------------------
*/
-static int
+int
pq_discardbytes(size_t len)
{
size_t amount;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 5a050898fe..afa40bcdce 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -102,6 +102,7 @@
#include "common/string.h"
#include "lib/ilist.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
@@ -196,15 +197,22 @@ BackgroundWorker *MyBgworkerEntry = NULL;
-/* The socket number we are listening for connections on */
+/* The TCP port number we are listening for connections on */
int PostPortNumber;
+/* The TCP port number we are listening for proxy connections on */
+int ProxyPortNumber;
+
/* The directory names for Unix socket(s) */
char *Unix_socket_directories;
/* The TCP listen address(es) */
char *ListenAddresses;
+/* Trusted proxy servers */
+char *TrustedProxyServersString = NULL;
+struct sockaddr_storage *TrustedProxyServers = NULL;
+
/*
* ReservedBackends is the number of backends reserved for superuser use.
* This number is taken out of the pool size given by MaxConnections so
@@ -218,7 +226,7 @@ int ReservedBackends;
/* The socket(s) we're listening to. */
#define MAXLISTEN 64
-static pgsocket ListenSocket[MAXLISTEN];
+static PQlistenSocket ListenSocket[MAXLISTEN];
/*
* These globals control the behavior of the postmaster in case some
@@ -586,6 +594,7 @@ PostmasterMain(int argc, char *argv[])
bool listen_addr_saved = false;
int i;
char *output_config_variable = NULL;
+ PQlistenSocket *socket = NULL;
InitProcessGlobals();
@@ -1135,7 +1144,10 @@ PostmasterMain(int argc, char *argv[])
* charged with closing the sockets again at postmaster shutdown.
*/
for (i = 0; i < MAXLISTEN; i++)
- ListenSocket[i] = PGINVALID_SOCKET;
+ {
+ ListenSocket[i].socket = PGINVALID_SOCKET;
+ ListenSocket[i].isProxy = false;
+ }
on_proc_exit(CloseServerPorts, 0);
@@ -1164,17 +1176,17 @@ PostmasterMain(int argc, char *argv[])
char *curhost = (char *) lfirst(l);
if (strcmp(curhost, "*") == 0)
- status = StreamServerPort(AF_UNSPEC, NULL,
+ socket = StreamServerPort(AF_UNSPEC, NULL,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
else
- status = StreamServerPort(AF_UNSPEC, curhost,
+ socket = StreamServerPort(AF_UNSPEC, curhost,
(unsigned short) PostPortNumber,
NULL,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful host addr in lockfile */
@@ -1188,9 +1200,30 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create listen socket for \"%s\"",
curhost)));
+
+ /* Also listen to the PROXY port on this address, if configured */
+ if (ProxyPortNumber)
+ {
+ if (strcmp(curhost, "*") == 0)
+ socket = StreamServerPort(AF_UNSPEC, NULL,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ else
+ socket = StreamServerPort(AF_UNSPEC, curhost,
+ (unsigned short) ProxyPortNumber,
+ NULL,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ socket->isProxy = true;
+ else
+ ereport(WARNING,
+ (errmsg("could not create PROXY listen socket for \"%s\"",
+ curhost)));
+ }
}
- if (!success && elemlist != NIL)
+ if (socket == NULL && elemlist != NIL)
ereport(FATAL,
(errmsg("could not create any TCP/IP sockets")));
@@ -1200,7 +1233,7 @@ PostmasterMain(int argc, char *argv[])
#ifdef USE_BONJOUR
/* Register for Bonjour only if we opened TCP socket(s) */
- if (enable_bonjour && ListenSocket[0] != PGINVALID_SOCKET)
+ if (enable_bonjour && ListenSocket[0].socket != PGINVALID_SOCKET)
{
DNSServiceErrorType err;
@@ -1262,12 +1295,12 @@ PostmasterMain(int argc, char *argv[])
{
char *socketdir = (char *) lfirst(l);
- status = StreamServerPort(AF_UNIX, NULL,
+ socket = StreamServerPort(AF_UNIX, NULL,
(unsigned short) PostPortNumber,
socketdir,
ListenSocket, MAXLISTEN);
- if (status == STATUS_OK)
+ if (socket)
{
success++;
/* record the first successful Unix socket in lockfile */
@@ -1278,9 +1311,23 @@ PostmasterMain(int argc, char *argv[])
ereport(WARNING,
(errmsg("could not create Unix-domain socket in directory \"%s\"",
socketdir)));
+
+ if (ProxyPortNumber)
+ {
+ socket = StreamServerPort(AF_UNIX, NULL,
+ (unsigned short) ProxyPortNumber,
+ socketdir,
+ ListenSocket, MAXLISTEN);
+ if (socket)
+ socket->isProxy = true;
+ else
+ ereport(WARNING,
+ (errmsg("could not create Unix-domain PROXY socket for \"%s\"",
+ socketdir)));
+ }
}
- if (!success && elemlist != NIL)
+ if (socket == NULL && elemlist != NIL)
ereport(FATAL,
(errmsg("could not create any Unix-domain sockets")));
@@ -1292,7 +1339,7 @@ PostmasterMain(int argc, char *argv[])
/*
* check that we have some socket to listen on
*/
- if (ListenSocket[0] == PGINVALID_SOCKET)
+ if (ListenSocket[0].socket == PGINVALID_SOCKET)
ereport(FATAL,
(errmsg("no socket created for listening")));
@@ -1441,10 +1488,10 @@ CloseServerPorts(int status, Datum arg)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -1733,15 +1780,17 @@ ServerLoop(void)
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] == PGINVALID_SOCKET)
+ if (ListenSocket[i].socket == PGINVALID_SOCKET)
break;
- if (FD_ISSET(ListenSocket[i], &rmask))
+ if (FD_ISSET(ListenSocket[i].socket, &rmask))
{
Port *port;
- port = ConnCreate(ListenSocket[i]);
+ port = ConnCreate(ListenSocket[i].socket);
if (port)
{
+ port->isProxy = ListenSocket[i].isProxy;
+
BackendStartup(port);
/*
@@ -1909,7 +1958,7 @@ initMasks(fd_set *rmask)
for (i = 0; i < MAXLISTEN; i++)
{
- int fd = ListenSocket[i];
+ int fd = ListenSocket[i].socket;
if (fd == PGINVALID_SOCKET)
break;
@@ -1922,6 +1971,213 @@ initMasks(fd_set *rmask)
return maxsock + 1;
}
+static int
+UnwrapProxyConnection(Port *port)
+{
+ char proxyver;
+ uint16 proxyaddrlen;
+ SockAddr raddr_save;
+ int i;
+ bool useproxy = false;
+
+ /*
+ * These structs are from the PROXY protocol docs at
+ * http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
+ */
+ union
+ {
+ struct
+ { /* for TCP/UDP over IPv4, len = 12 */
+ uint32 src_addr;
+ uint32 dst_addr;
+ uint16 src_port;
+ uint16 dst_port;
+ } ip4;
+ struct
+ { /* for TCP/UDP over IPv6, len = 36 */
+ uint8 src_addr[16];
+ uint8 dst_addr[16];
+ uint16 src_port;
+ uint16 dst_port;
+ } ip6;
+ } proxyaddr;
+ struct
+ {
+ uint8 sig[12]; /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */
+ uint8 ver_cmd; /* protocol version and command */
+ uint8 fam; /* protocol family and address */
+ uint16 len; /* number of following bytes part of the
+ * header */
+ } proxyheader;
+
+
+ /* Else if it's on our list of trusted proxies */
+ if (TrustedProxyServers)
+ {
+ for (i = 0; i < *((int *) TrustedProxyServers) * 2; i += 2)
+ {
+ if (port->raddr.addr.ss_family == TrustedProxyServers[i + 1].ss_family)
+ {
+ /*
+ * Connection over unix sockets don't give us the source, so
+ * just check if they're allowed at all. For IP connections,
+ * verify that it's an allowed address.
+ */
+ if (port->raddr.addr.ss_family == AF_UNIX ||
+ pg_range_sockaddr(&port->raddr.addr,
+ &TrustedProxyServers[i + 1],
+ &TrustedProxyServers[i + 2]))
+ {
+ useproxy = true;
+ break;
+ }
+ }
+ }
+ }
+ if (!useproxy)
+ {
+ /*
+ * Connection is not from one of our trusted proxies, so reject it.
+ */
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("connection from unauthorized proxy server")));
+ return STATUS_ERROR;
+ }
+
+ /* Store a copy of the original address, for logging */
+ memcpy(&raddr_save, &port->raddr, sizeof(SockAddr));
+
+ pq_startmsgread();
+
+ /*
+ * PROXY requests always start with:
+ * \x0D \x0A \x0D \x0A \x00 \x0D \x0A \x51 \x55 \x49 \x54 \x0A
+ */
+
+ if (pq_getbytes((char *) &proxyheader, sizeof(proxyheader)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ if (memcmp(proxyheader.sig, "\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a", sizeof(proxyheader.sig)) != 0)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Proxy version is in the high 4 bits of the first byte */
+ proxyver = (proxyheader.ver_cmd & 0xF0) >> 4;
+ if (proxyver != 2)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol version: %x", proxyver)));
+ return STATUS_ERROR;
+ }
+
+ proxyaddrlen = pg_ntoh16(proxyheader.len);
+
+ if (pq_getbytes((char *) &proxyaddr, proxyaddrlen > sizeof(proxyaddr) ? sizeof(proxyaddr) : proxyaddrlen) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+
+ /* Lower 4 bits hold type of connection */
+ if (proxyheader.fam == 0)
+ {
+ /* LOCAL connection, so we ignore the address included */
+ }
+ else if (proxyheader.fam == 0x11)
+ {
+ /* TCPv4 */
+ if (proxyaddrlen < 12)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET;
+ port->raddr.salen = sizeof(struct sockaddr_in);
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_addr.s_addr = proxyaddr.ip4.src_addr;
+ ((struct sockaddr_in *) &port->raddr.addr)->sin_port = proxyaddr.ip4.src_port;
+ }
+ else if (proxyheader.fam == 0x21)
+ {
+ /* TCPv6 */
+ if (proxyaddrlen < 36)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ port->raddr.addr.ss_family = AF_INET6;
+ port->raddr.salen = sizeof(struct sockaddr_in6);
+ memcpy(&((struct sockaddr_in6 *) &port->raddr.addr)->sin6_addr, proxyaddr.ip6.src_addr, 16);
+ ((struct sockaddr_in6 *) &port->raddr.addr)->sin6_port = proxyaddr.ip6.src_port;
+ }
+ else
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("invalid proxy protocol connection type: %x", proxyheader.fam)));
+ return STATUS_ERROR;
+ }
+
+ /* If there is any more header data present, skip past it */
+ if (proxyaddrlen > sizeof(proxyaddr))
+ {
+ if (pq_discardbytes(proxyaddrlen - sizeof(proxyaddr)) == EOF)
+ {
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("incomplete proxy packet")));
+ return STATUS_ERROR;
+ }
+ }
+
+ pq_endmsgread();
+
+ /*
+ * Log what we've done if connection logging is enabled. We log the proxy
+ * connection here, and let the normal connection logging mechanism log
+ * the unwrapped connection.
+ */
+ if (Log_connections)
+ {
+ char remote_host[NI_MAXHOST];
+ char remote_port[NI_MAXSERV];
+ int ret;
+
+ remote_host[0] = '\0';
+ remote_port[0] = '\0';
+ if ((ret = pg_getnameinfo_all(&raddr_save.addr, raddr_save.salen,
+ remote_host, sizeof(remote_host),
+ remote_port, sizeof(remote_port),
+ (log_hostname ? 0 : NI_NUMERICHOST) | NI_NUMERICSERV)) != 0)
+ ereport(WARNING,
+ (errmsg_internal("pg_getnameinfo_all() failed: %s",
+ gai_strerror(ret))));
+
+ ereport(LOG,
+ (errmsg("proxy connection from: host=%s port=%s",
+ remote_host,
+ remote_port)));
+
+ }
+
+ return STATUS_OK;
+}
/*
* Read a client's startup packet and do something according to it.
@@ -2030,7 +2286,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
#ifdef USE_SSL
/* No SSL when disabled or on Unix sockets */
- if (!LoadedSSL || IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!LoadedSSL || (IS_AF_UNIX(port->laddr.addr.ss_family) && !port->isProxy))
SSLok = 'N';
else
SSLok = 'S'; /* Support for SSL */
@@ -2067,7 +2323,7 @@ retry1:
#ifdef ENABLE_GSS
/* No GSSAPI encryption when on Unix socket */
- if (!IS_AF_UNIX(port->laddr.addr.ss_family))
+ if (!IS_AF_UNIX(port->laddr.addr.ss_family) || port->isProxy)
GSSok = 'G';
#endif
@@ -2579,10 +2835,10 @@ ClosePostmasterPorts(bool am_syslogger)
*/
for (i = 0; i < MAXLISTEN; i++)
{
- if (ListenSocket[i] != PGINVALID_SOCKET)
+ if (ListenSocket[i].socket != PGINVALID_SOCKET)
{
- StreamClose(ListenSocket[i]);
- ListenSocket[i] = PGINVALID_SOCKET;
+ StreamClose(ListenSocket[i].socket);
+ ListenSocket[i].socket = PGINVALID_SOCKET;
}
}
@@ -4363,6 +4619,33 @@ BackendInitialize(Port *port)
InitializeTimeouts(); /* establishes SIGALRM handler */
PG_SETMASK(&StartupBlockSig);
+ /*
+ * Ready to begin client interaction. We will give up and _exit(1) after
+ * a time delay, so that a broken client can't hog a connection
+ * indefinitely. PreAuthDelay and any DNS interactions above don't count
+ * against the time limit.
+ *
+ * Note: AuthenticationTimeout is applied here while waiting for the
+ * startup packet, and then again in InitPostgres for the duration of any
+ * authentication operations. So a hostile client could tie up the
+ * process for nearly twice AuthenticationTimeout before we kick him off.
+ *
+ * Note: because PostgresMain will call InitializeTimeouts again, the
+ * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
+ * since we never use it again after this function.
+ */
+ RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
+
+ /* Check if this is a proxy connection and if so unwrap the proxying */
+ if (port->isProxy)
+ {
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
+ if (UnwrapProxyConnection(port) != STATUS_OK)
+ proc_exit(0);
+ disable_timeout(STARTUP_PACKET_TIMEOUT, false);
+ }
+
+
/*
* Get the remote host name and port for logging and status display.
*/
@@ -4414,28 +4697,11 @@ BackendInitialize(Port *port)
strspn(remote_host, "0123456789ABCDEFabcdef:") < strlen(remote_host))
port->remote_hostname = strdup(remote_host);
- /*
- * Ready to begin client interaction. We will give up and _exit(1) after
- * a time delay, so that a broken client can't hog a connection
- * indefinitely. PreAuthDelay and any DNS interactions above don't count
- * against the time limit.
- *
- * Note: AuthenticationTimeout is applied here while waiting for the
- * startup packet, and then again in InitPostgres for the duration of any
- * authentication operations. So a hostile client could tie up the
- * process for nearly twice AuthenticationTimeout before we kick him off.
- *
- * Note: because PostgresMain will call InitializeTimeouts again, the
- * registration of STARTUP_PACKET_TIMEOUT will be lost. This is okay
- * since we never use it again after this function.
- */
- RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);
- enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
-
/*
* Receive the startup packet (which might turn out to be a cancel request
* packet).
*/
+ enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);
status = ProcessStartupPacket(port, false, false);
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 480e8cd199..2eac7e7264 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -50,10 +50,12 @@
#include "commands/user.h"
#include "commands/vacuum.h"
#include "commands/variable.h"
+#include "common/ip.h"
#include "common/string.h"
#include "funcapi.h"
#include "jit/jit.h"
#include "libpq/auth.h"
+#include "libpq/ifaddr.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
@@ -234,6 +236,8 @@ static bool check_recovery_target_lsn(char **newval, void **extra, GucSource sou
static void assign_recovery_target_lsn(const char *newval, void *extra);
static bool check_primary_slot_name(char **newval, void **extra, GucSource source);
static bool check_default_with_oids(bool *newval, void **extra, GucSource source);
+static bool check_proxy_servers(char **newval, void **extra, GucSource source);
+static void assign_proxy_servers(const char *newval, void *extra);
/* Private functions in guc-file.l that need to be called from guc.c */
static ConfigVariable *ProcessConfigFileInternal(GucContext context,
@@ -2358,6 +2362,16 @@ static struct config_int ConfigureNamesInt[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_port", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the TCP port the server listens for PROXY connections on."),
+ NULL
+ },
+ &ProxyPortNumber,
+ 0, 0, 65535,
+ NULL, NULL, NULL
+ },
+
{
{"unix_socket_permissions", PGC_POSTMASTER, CONN_AUTH_SETTINGS,
gettext_noop("Sets the access permissions of the Unix-domain socket."),
@@ -4331,6 +4345,17 @@ static struct config_string ConfigureNamesString[] =
NULL, NULL, NULL
},
+ {
+ {"proxy_servers", PGC_SIGHUP, CONN_AUTH_SETTINGS,
+ gettext_noop("Sets the addresses for trusted proxy servers."),
+ NULL,
+ GUC_LIST_INPUT
+ },
+ &TrustedProxyServersString,
+ "",
+ check_proxy_servers, assign_proxy_servers, NULL
+ },
+
{
/*
* Can't be set by ALTER SYSTEM as it can lead to recursive definition
@@ -12536,4 +12561,118 @@ check_default_with_oids(bool *newval, void **extra, GucSource source)
return true;
}
+static bool
+check_proxy_servers(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ struct sockaddr_storage *myextra;
+
+ /* Special case when it's empty */
+ if (**newval == '\0')
+ {
+ *extra = NULL;
+ return true;
+ }
+
+ /* Need a modifiable copy of string */
+ rawstring = pstrdup(*newval);
+
+ /* Parse string into list of identifiers */
+ if (!SplitIdentifierString(rawstring, ',', &elemlist))
+ {
+ /* syntax error in list */
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ if (list_length(elemlist) == 0)
+ {
+ /* If it had only whitespace */
+ pfree(rawstring);
+ list_free(elemlist);
+
+ *extra = NULL;
+ return true;
+ }
+
+ /*
+ * We store the result in an array of sockaddr_storage. The first entry is
+ * just an overloaded int which holds the size of the array.
+ */
+ myextra = (struct sockaddr_storage *) guc_malloc(ERROR, sizeof(struct sockaddr_storage) * (list_length(elemlist) * 2 + 1));
+ *((int *) &myextra[0]) = list_length(elemlist);
+
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+ char *netmasktok = NULL;
+ int ret;
+ struct addrinfo *gai_result;
+ struct addrinfo hints;
+
+ /*
+ * Unix sockets don't have endpoint addresses, so just flag them as
+ * AF_UNIX
+ */
+ if (pg_strcasecmp(tok, "unix") == 0)
+ {
+ myextra[foreach_current_index(l) * 2 + 1].ss_family = AF_UNIX;
+ continue;
+ }
+
+ netmasktok = strchr(tok, '/');
+ if (netmasktok)
+ {
+ *netmasktok = '\0';
+ netmasktok++;
+ }
+
+ memset((char *) &hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ ret = pg_getaddrinfo_all(tok, NULL, &hints, &gai_result);
+ if (ret != 0 || gai_result == NULL)
+ {
+ GUC_check_errdetail("Invalid IP addrress %s", tok);
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+
+ memcpy((char *) &myextra[foreach_current_index(l) * 2 + 1], gai_result->ai_addr, gai_result->ai_addrlen);
+ pg_freeaddrinfo_all(hints.ai_family, gai_result);
+
+ /* A NULL netmasktok means the fully set hostmask */
+ if (pg_sockaddr_cidr_mask(&myextra[foreach_current_index(l) * 2 + 2], netmasktok, myextra[foreach_current_index(l) * 2 + 1].ss_family) != 0)
+ {
+ if (netmasktok)
+ GUC_check_errdetail("Invalid netmask %s", netmasktok);
+ else
+ GUC_check_errdetail("Could not create netmask");
+ pfree(rawstring);
+ list_free(elemlist);
+ free(myextra);
+ return false;
+ }
+ }
+
+ pfree(rawstring);
+ list_free(elemlist);
+ *extra = (void *) myextra;
+
+ return true;
+}
+
+static void
+assign_proxy_servers(const char *newval, void *extra)
+{
+ TrustedProxyServers = (struct sockaddr_storage *) extra;
+}
+
#include "guc-file.c"
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b696abfe54..2e224fef36 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -61,6 +61,9 @@
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
+#proxy_port = 0 # port to listen to for proxy connections
+ # (change requires restart)
+#proxy_servers = '' # what proxy servers to trust
#max_connections = 100 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
#unix_socket_directories = '/tmp' # comma-separated list of directories
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 02015efe13..471c76fb30 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -126,6 +126,7 @@ typedef struct Port
{
pgsocket sock; /* File descriptor */
bool noblock; /* is the socket in non-blocking mode? */
+ bool isProxy; /* is the connection using PROXY protocol */
ProtocolVersion proto; /* FE/BE protocol version */
SockAddr laddr; /* local addr (postmaster) */
SockAddr raddr; /* remote addr (client) */
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 6c51b2f20f..cdaae030e1 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -42,6 +42,12 @@ typedef struct
extern const PGDLLIMPORT PQcommMethods *PqCommMethods;
+typedef struct
+{
+ pgsocket socket;
+ bool isProxy;
+} PQlistenSocket;
+
#define pq_comm_reset() (PqCommMethods->comm_reset())
#define pq_flush() (PqCommMethods->flush())
#define pq_flush_if_writable() (PqCommMethods->flush_if_writable())
@@ -63,9 +69,9 @@ extern WaitEventSet *FeBeWaitSet;
#define FeBeWaitSetSocketPos 0
#define FeBeWaitSetLatchPos 1
-extern int StreamServerPort(int family, const char *hostName,
- unsigned short portNumber, const char *unixSocketDir,
- pgsocket ListenSocket[], int MaxListen);
+extern PQlistenSocket *StreamServerPort(int family, const char *hostName,
+ unsigned short portNumber, const char *unixSocketDir,
+ PQlistenSocket PQlistenSocket[], int MaxListen);
extern int StreamConnection(pgsocket server_fd, Port *port);
extern void StreamClose(pgsocket sock);
extern void TouchSocketFiles(void);
@@ -78,6 +84,7 @@ extern bool pq_is_reading_msg(void);
extern int pq_getmessage(StringInfo s, int maxlen);
extern int pq_getbyte(void);
extern int pq_peekbyte(void);
+extern int pq_discardbytes(size_t len);
extern int pq_getbyte_if_available(unsigned char *c);
extern int pq_putmessage_v2(char msgtype, const char *s, size_t len);
extern bool pq_check_connection(void);
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index 0efdd7c232..2a029ef786 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -17,10 +17,13 @@
extern bool EnableSSL;
extern int ReservedBackends;
extern PGDLLIMPORT int PostPortNumber;
+extern PGDLLIMPORT int ProxyPortNumber;
extern int Unix_socket_permissions;
extern char *Unix_socket_group;
extern char *Unix_socket_directories;
extern char *ListenAddresses;
+extern char *TrustedProxyServersString;
+extern struct sockaddr_storage *TrustedProxyServers;
extern bool ClientAuthInProgress;
extern int PreAuthDelay;
extern int AuthenticationTimeout;
diff --git a/src/test/Makefile b/src/test/Makefile
index 46275915ff..4ad030034c 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -12,7 +12,8 @@ subdir = src/test
top_builddir = ../..
include $(top_builddir)/src/Makefile.global
-SUBDIRS = perl regress isolation modules authentication recovery subscription
+SUBDIRS = perl regress isolation modules authentication recovery subscription \
+ protocol
# Test suites that are not safe by default but can be run if selected
# by the user via the whitespace-separated list in variable
diff --git a/src/test/protocol/Makefile b/src/test/protocol/Makefile
new file mode 100644
index 0000000000..bda49d6ecb
--- /dev/null
+++ b/src/test/protocol/Makefile
@@ -0,0 +1,23 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/protocol
+#
+# Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/protocol/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/protocol
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+ $(prove_check)
+
+installcheck:
+ $(prove_installcheck)
+
+clean distclean maintainer-clean:
+ rm -rf tmp_check
diff --git a/src/test/protocol/t/001_proxy.pl b/src/test/protocol/t/001_proxy.pl
new file mode 100644
index 0000000000..edc032d49c
--- /dev/null
+++ b/src/test/protocol/t/001_proxy.pl
@@ -0,0 +1,151 @@
+use strict;
+use warnings;
+use TestLib;
+use PostgresNode;
+use Test::More;
+use Socket qw(AF_INET AF_INET6 inet_pton);
+use IO::Socket;
+
+plan tests => 25;
+
+my $node = get_new_node('node');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+log_connections = on
+});
+$node->append_conf(
+ 'pg_hba.conf', qq{
+host all all 11.22.33.44/32 trust
+host all all 1:2:3:4:5:6:0:9/128 trust
+});
+$node->append_conf('postgresql.conf', "proxy_port = " . ($node->port() + 1));
+
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE USER proxytest;');
+
+sub make_message
+{
+ my ($msg) = @_;
+ return pack("Na*", length($msg) + 4, $msg);
+}
+
+sub read_packet
+{
+ my ($socket) = @_;
+ my $buf = "";
+ $socket->recv($buf, 1024);
+ return $buf;
+}
+
+
+# Test normal connection through localhost
+sub test_connection
+{
+ my ($socket, $proxy, $what, $shouldbe, $shouldfail, $extra) = @_;
+ ok($socket, $what);
+
+ my $startup = make_message(
+ pack("N(Z*Z*)*x", 196608, (user => "proxytest", database => "postgres")));
+
+ $extra = "" if !defined($extra);
+
+ if (defined($proxy))
+ {
+ my $p = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A\x21";
+ if ($proxy =~ ":")
+ {
+ # ipv6
+ $p .= "\x21"; # TCP v6
+ $p .= pack "n", 36 + length($extra); # size
+ $p .= inet_pton(AF_INET6, $proxy);
+ $p .= "\0" x 16; # destination address
+ }
+ else
+ {
+ # ipv4
+ $p .= "\x11"; # TCP v4
+ $p .= pack "n", 12 + length($extra); # size
+ $p .= inet_pton(AF_INET, $proxy);
+ $p .= "\0\0\0\0"; # destination address
+ }
+ $p .= pack "n", 1919; # source port
+ $p .= pack "n", 0;
+ $p .= $extra;
+ print $socket $p;
+ }
+ print $socket $startup;
+
+ my $in = read_packet($socket);
+ if (defined($shouldfail))
+ {
+ isnt(substr($in, 0, 1), 'R', $what);
+ }
+ else
+ {
+ is(substr($in, 0, 1), 'R', $what);
+ }
+
+ SKIP:
+ {
+ skip "The rest of this test should fail", 3 if (defined($shouldfail));
+
+ is(substr($in, 8, 1), "\0", $what);
+
+ my ($resip, $resport) = split /\|/,
+ $node->safe_psql('postgres',
+ "SELECT client_addr, client_port FROM pg_stat_activity WHERE pid != pg_backend_pid() AND backend_type='client backend'"
+ );
+ is($resip, $shouldbe, $what);
+ if ($proxy)
+ {
+ is($resport, "1919", $what);
+ }
+ else
+ {
+ ok($resport, $what);
+ }
+ }
+
+ $socket->close();
+
+ return;
+}
+
+sub make_socket
+{
+ my ($port) = @_;
+ if ($PostgresNode::use_tcp) {
+ return IO::Socket::INET->new(
+ PeerAddr => "127.0.0.1",
+ PeerPort => $port,
+ Proto => "tcp",
+ Type => SOCK_STREAM);
+ }
+ else {
+ return IO::Socket::UNIX->new(
+ Peer => $node->host() . "/.s.PGSQL." . $port,
+ Type => SOCK_STREAM);
+ }
+}
+
+# Test a regular connection first to make sure connecting etc works fine.
+test_connection(make_socket($node->port()),
+ undef, "normal connection", $PostgresNode::use_tcp ? "127.0.0.1": "");
+
+# Make sure we can't make a proxy connection until it's allowed
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44", 1);
+
+# Allow proxy connections and test them
+$node->append_conf('postgresql.conf', "proxy_servers = 'unix, 127.0.0.1/32'");
+$node->restart();
+
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy ipv4", "11.22.33.44");
+test_connection(make_socket($node->port() + 1),
+ "1:2:3:4:5:6::9", "proxy ipv6", "1:2:3:4:5:6:0:9");
+
+test_connection(make_socket($node->port() + 1),
+ "11.22.33.44", "proxy with extra", "11.22.33.44", undef, "abcdef"x100);
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-03-03 14:13 ` Bruno Lavoie <bl@brunol.com>
3 siblings, 0 replies; 56+ messages in thread
From: Bruno Lavoie @ 2021-03-03 14:13 UTC (permalink / raw)
To: Magnus Hagander <magnus@hagander.net>; +Cc: PostgreSQL Developers <pgsql-hackers@lists.postgresql.org>
+10 on this one!
Hosting a farm of read replicas and r/w endpoint behind an HAproxy makes
the powerful pg_hba purpose by hiding the real source address... which is
bad for some environments with strict conformance and audit requirements
Le mar. 2 mars 2021 à 12:43, Magnus Hagander <magnus@hagander.net> a écrit :
> PFA a simple patch that implements support for the PROXY protocol.
>
> This is a protocol common and very light weight in proxies and load
> balancers (haproxy is one common example, but also for example the AWS
> cloud load balancers). Basically this protocol prefixes the normal
> connection with a header and a specification of what the original host
> was, allowing the server to unwrap that and get the correct client
> address instead of just the proxy ip address. It is a one-way protocol
> in that there is no response from the server, it's just purely a
> prefix of the IP information.
>
> Using this when PostgreSQL is behind a proxy allows us to keep using
> pg_hba.conf rules based on the original ip address, as well as track
> the original address in log messages and pg_stat_activity etc.
>
> The implementation adds a parameter named proxy_servers which lists
> the ips or ip+cidr mask to be trusted. Since a proxy can decide what
> the origin is, and this is used for security decisions, it's very
> important to not just trust any server, only those that are
> intentionally used. By default, no servers are listed, and thus the
> protocol is disabled.
>
> When specified, and the connection on the normal port has the proxy
> prefix on it, and the connection comes in from one of the addresses
> listed as valid proxy servers, we will replace the actual IP address
> of the client with the one specified in the proxy packet.
>
> Currently there is no information about the proxy server in the
> pg_stat_activity view, it's only available as a log message. But maybe
> it should go in pg_stat_activity as well? Or in a separate
> pg_stat_proxy view?
>
> (In passing, I note that pq_discardbytes were in pqcomm.h, yet listed
> as static in pqcomm.c -- but now made non-static)
>
> --
> Magnus Hagander
> Me: https://www.hagander.net/
> Work: https://www.redpill-linpro.com/
>
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-03-04 01:42 ` Tatsuo Ishii <ishii@sraoss.co.jp>
2021-03-04 19:45 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
3 siblings, 1 reply; 56+ messages in thread
From: Tatsuo Ishii @ 2021-03-04 01:42 UTC (permalink / raw)
To: magnus@hagander.net; +Cc: pgsql-hackers@lists.postgresql.org
> PFA a simple patch that implements support for the PROXY protocol.
>
> This is a protocol common and very light weight in proxies and load
> balancers (haproxy is one common example, but also for example the AWS
> cloud load balancers). Basically this protocol prefixes the normal
> connection with a header and a specification of what the original host
> was, allowing the server to unwrap that and get the correct client
> address instead of just the proxy ip address. It is a one-way protocol
> in that there is no response from the server, it's just purely a
> prefix of the IP information.
Is there any formal specification for the "a protocol common and very
light weight in proxies"? I am asking because I was expecting that is
explained in your patch (hopefully in "Frontend/Backend Protocol"
chapter) but I couldn't find it in your patch.
Also we need a regression test for this feature.
Best regards,
--
Tatsuo Ishii
SRA OSS, Inc. Japan
English: http://www.sraoss.co.jp/index_en.php
Japanese:http://www.sraoss.co.jp
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 01:42 ` Re: PROXY protocol support Tatsuo Ishii <ishii@sraoss.co.jp>
@ 2021-03-04 19:45 ` Jacob Champion <pchampion@vmware.com>
2021-03-04 20:29 ` Re: PROXY protocol support Jan Wieck <jan@wi3ck.info>
2021-03-04 20:47 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 2 replies; 56+ messages in thread
From: Jacob Champion @ 2021-03-04 19:45 UTC (permalink / raw)
To: ishii@sraoss.co.jp <ishii@sraoss.co.jp>; magnus@hagander.net <magnus@hagander.net>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Thu, 2021-03-04 at 10:42 +0900, Tatsuo Ishii wrote:
> Is there any formal specification for the "a protocol common and very
> light weight in proxies"?
See
https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
which is maintained by HAProxy Technologies.
--Jacob
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 01:42 ` Re: PROXY protocol support Tatsuo Ishii <ishii@sraoss.co.jp>
2021-03-04 19:45 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
@ 2021-03-04 20:29 ` Jan Wieck <jan@wi3ck.info>
2021-03-04 20:40 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
1 sibling, 1 reply; 56+ messages in thread
From: Jan Wieck @ 2021-03-04 20:29 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; ishii@sraoss.co.jp <ishii@sraoss.co.jp>; magnus@hagander.net <magnus@hagander.net>; +Cc: pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On 3/4/21 2:45 PM, Jacob Champion wrote:
> On Thu, 2021-03-04 at 10:42 +0900, Tatsuo Ishii wrote:
>> Is there any formal specification for the "a protocol common and very
>> light weight in proxies"?
>
> See
>
> https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
>
> which is maintained by HAProxy Technologies.
>
> --Jacob
>
This looks like it would only need a few extra protocol messages to be
understood by the backend. It might be possible to implement that with
the loadable wire protocol extensions proposed here:
https://commitfest.postgresql.org/32/3018/
Regards, Jan
--
Jan Wieck
Principle Database Engineer
Amazon Web Services
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 01:42 ` Re: PROXY protocol support Tatsuo Ishii <ishii@sraoss.co.jp>
2021-03-04 19:45 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:29 ` Re: PROXY protocol support Jan Wieck <jan@wi3ck.info>
@ 2021-03-04 20:40 ` Magnus Hagander <magnus@hagander.net>
2021-03-04 21:01 ` Re: PROXY protocol support Jan Wieck <jan@wi3ck.info>
0 siblings, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2021-03-04 20:40 UTC (permalink / raw)
To: Jan Wieck <jan@wi3ck.info>; +Cc: Jacob Champion <pchampion@vmware.com>; ishii@sraoss.co.jp <ishii@sraoss.co.jp>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Thu, Mar 4, 2021 at 9:29 PM Jan Wieck <jan@wi3ck.info> wrote:
>
> On 3/4/21 2:45 PM, Jacob Champion wrote:
> > On Thu, 2021-03-04 at 10:42 +0900, Tatsuo Ishii wrote:
> >> Is there any formal specification for the "a protocol common and very
> >> light weight in proxies"?
> >
> > See
> >
> > https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
> >
> > which is maintained by HAProxy Technologies.
> >
> > --Jacob
> >
>
> This looks like it would only need a few extra protocol messages to be
> understood by the backend. It might be possible to implement that with
> the loadable wire protocol extensions proposed here:
>
> https://commitfest.postgresql.org/32/3018/
Actually the whole point of it is that it *doesn't* need any new
protocol messages. And that it *wraps* whatever is there, definitely
doesn't replace it. It should equally be wrapping whatever an
extension uses.
So while the base topic is not unrelated, I don't think there is any
overlap between these.
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 01:42 ` Re: PROXY protocol support Tatsuo Ishii <ishii@sraoss.co.jp>
2021-03-04 19:45 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:29 ` Re: PROXY protocol support Jan Wieck <jan@wi3ck.info>
2021-03-04 20:40 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-03-04 21:01 ` Jan Wieck <jan@wi3ck.info>
2021-03-04 22:38 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 1 reply; 56+ messages in thread
From: Jan Wieck @ 2021-03-04 21:01 UTC (permalink / raw)
To: Magnus Hagander <magnus@hagander.net>; +Cc: Jacob Champion <pchampion@vmware.com>; ishii@sraoss.co.jp <ishii@sraoss.co.jp>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On 3/4/21 3:40 PM, Magnus Hagander wrote:
> On Thu, Mar 4, 2021 at 9:29 PM Jan Wieck <jan@wi3ck.info> wrote:
>> This looks like it would only need a few extra protocol messages to be
>> understood by the backend. It might be possible to implement that with
>> the loadable wire protocol extensions proposed here:
>>
>> https://commitfest.postgresql.org/32/3018/
>
> Actually the whole point of it is that it *doesn't* need any new
> protocol messages. And that it *wraps* whatever is there, definitely
> doesn't replace it. It should equally be wrapping whatever an
> extension uses.
>
> So while the base topic is not unrelated, I don't think there is any
> overlap between these.
I might be missing something here, but isn't sending some extra,
informational *header*, which is understood by the backend, in essence a
protocol extension?
Regards, Jan
--
Jan Wieck
Principle Database Engineer
Amazon Web Services
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 01:42 ` Re: PROXY protocol support Tatsuo Ishii <ishii@sraoss.co.jp>
2021-03-04 19:45 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:29 ` Re: PROXY protocol support Jan Wieck <jan@wi3ck.info>
2021-03-04 20:40 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 21:01 ` Re: PROXY protocol support Jan Wieck <jan@wi3ck.info>
@ 2021-03-04 22:38 ` Magnus Hagander <magnus@hagander.net>
0 siblings, 0 replies; 56+ messages in thread
From: Magnus Hagander @ 2021-03-04 22:38 UTC (permalink / raw)
To: Jan Wieck <jan@wi3ck.info>; +Cc: Jacob Champion <pchampion@vmware.com>; ishii@sraoss.co.jp <ishii@sraoss.co.jp>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Thu, Mar 4, 2021 at 10:01 PM Jan Wieck <jan@wi3ck.info> wrote:
>
> On 3/4/21 3:40 PM, Magnus Hagander wrote:
> > On Thu, Mar 4, 2021 at 9:29 PM Jan Wieck <jan@wi3ck.info> wrote:
> >> This looks like it would only need a few extra protocol messages to be
> >> understood by the backend. It might be possible to implement that with
> >> the loadable wire protocol extensions proposed here:
> >>
> >> https://commitfest.postgresql.org/32/3018/
> >
> > Actually the whole point of it is that it *doesn't* need any new
> > protocol messages. And that it *wraps* whatever is there, definitely
> > doesn't replace it. It should equally be wrapping whatever an
> > extension uses.
> >
> > So while the base topic is not unrelated, I don't think there is any
> > overlap between these.
>
> I might be missing something here, but isn't sending some extra,
> informational *header*, which is understood by the backend, in essence a
> protocol extension?
Bad choice of words, I guess.
The points being, there is a single packet sent ahead of the normal
stream. There are no new messages in "the postgresql protocol" or "the
febe protocol" or whatever we call it. And it doesn't change the
properties of any part of that protocol. And, importantly for the
simplicity, there is no negotiation and there are no packets going the
other way.
But sure, you can call it a protocol extension if you want. And yes,
it could probably be built on top of part of the ideas in that other
patch, but most of it would be useless (the abstraction of the listen
functionality into listen_have_free_slot/listen_add_socket would be
the big thing that could be used)
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 01:42 ` Re: PROXY protocol support Tatsuo Ishii <ishii@sraoss.co.jp>
2021-03-04 19:45 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
@ 2021-03-04 20:47 ` Magnus Hagander <magnus@hagander.net>
2021-03-04 23:08 ` Re: PROXY protocol support Tatsuo Ishii <ishii@sraoss.co.jp>
1 sibling, 1 reply; 56+ messages in thread
From: Magnus Hagander @ 2021-03-04 20:47 UTC (permalink / raw)
To: Jacob Champion <pchampion@vmware.com>; +Cc: ishii@sraoss.co.jp <ishii@sraoss.co.jp>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
On Thu, Mar 4, 2021 at 8:45 PM Jacob Champion <pchampion@vmware.com> wrote:
>
> On Thu, 2021-03-04 at 10:42 +0900, Tatsuo Ishii wrote:
> > Is there any formal specification for the "a protocol common and very
> > light weight in proxies"?
>
> See
>
> https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
Yeah, it's currently in one of the comments, but should probably be
added to the docs side as well.
And yes tests :) Probably not a regression test, but some level of tap
testing should definitely be added. We'll just have to find a way to
do that without making haproxy a dependency to run the tests :)
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 01:42 ` Re: PROXY protocol support Tatsuo Ishii <ishii@sraoss.co.jp>
2021-03-04 19:45 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:47 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
@ 2021-03-04 23:08 ` Tatsuo Ishii <ishii@sraoss.co.jp>
2021-03-05 09:14 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
0 siblings, 1 reply; 56+ messages in thread
From: Tatsuo Ishii @ 2021-03-04 23:08 UTC (permalink / raw)
To: magnus@hagander.net; +Cc: pchampion@vmware.com; ishii@sraoss.co.jp; pgsql-hackers@lists.postgresql.org
>> On Thu, 2021-03-04 at 10:42 +0900, Tatsuo Ishii wrote:
>> > Is there any formal specification for the "a protocol common and very
>> > light weight in proxies"?
>>
>> See
>>
>> https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
>
> Yeah, it's currently in one of the comments, but should probably be
> added to the docs side as well.
It seems the protocol is HAproxy product specific and I think it would
be better to be mentioned in the docs.
> And yes tests :) Probably not a regression test, but some level of tap
> testing should definitely be added. We'll just have to find a way to
> do that without making haproxy a dependency to run the tests :)
Agreed.
--
Tatsuo Ishii
SRA OSS, Inc. Japan
English: http://www.sraoss.co.jp/index_en.php
Japanese:http://www.sraoss.co.jp
^ permalink raw reply [nested|flat] 56+ messages in thread
* Re: PROXY protocol support
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 01:42 ` Re: PROXY protocol support Tatsuo Ishii <ishii@sraoss.co.jp>
2021-03-04 19:45 ` Re: PROXY protocol support Jacob Champion <pchampion@vmware.com>
2021-03-04 20:47 ` Re: PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-04 23:08 ` Re: PROXY protocol support Tatsuo Ishii <ishii@sraoss.co.jp>
@ 2021-03-05 09:14 ` Magnus Hagander <magnus@hagander.net>
0 siblings, 0 replies; 56+ messages in thread
From: Magnus Hagander @ 2021-03-05 09:14 UTC (permalink / raw)
To: Tatsuo Ishii <ishii@sraoss.co.jp>; +Cc: Jacob Champion <pchampion@vmware.com>; PostgreSQL Developers <pgsql-hackers@lists.postgresql.org>
On Fri, Mar 5, 2021 at 12:08 AM Tatsuo Ishii <ishii@sraoss.co.jp> wrote:
>
> >> On Thu, 2021-03-04 at 10:42 +0900, Tatsuo Ishii wrote:
> >> > Is there any formal specification for the "a protocol common and very
> >> > light weight in proxies"?
> >>
> >> See
> >>
> >> https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
> >
> > Yeah, it's currently in one of the comments, but should probably be
> > added to the docs side as well.
>
> It seems the protocol is HAproxy product specific and I think it would
> be better to be mentioned in the docs.
It's definitely not HAProxy specific, it's more or less an industry
standard. It's just maintained by them. That said, yes, it should be
referenced in the docs.
--
Magnus Hagander
Me: https://www.hagander.net/
Work: https://www.redpill-linpro.com/
^ permalink raw reply [nested|flat] 56+ messages in thread
end of thread, other threads:[~2026-09-23 00:27 UTC | newest]
Thread overview: 56+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2019-05-19 15:36 PROXY protocol support Julien Riou <julien@riou.xyz>
2019-05-19 15:59 ` Stephen Frost <sfrost@snowman.net>
2019-05-19 20:53 ` Julien Riou <julien@riou.xyz>
2019-05-20 15:28 ` Konstantin Knizhnik <k.knizhnik@postgrespro.ru>
2019-05-20 17:05 ` Bruno Lavoie <bl@brunol.com>
2021-03-02 17:43 PROXY protocol support Magnus Hagander <magnus@hagander.net>
2021-03-02 18:42 ` Arthur Nascimento <tureba@gmail.com>
2021-03-03 00:50 ` Jacob Champion <pchampion@vmware.com>
2021-03-03 09:00 ` Magnus Hagander <magnus@hagander.net>
2021-03-03 09:39 ` Magnus Hagander <magnus@hagander.net>
2021-03-04 20:07 ` Jacob Champion <pchampion@vmware.com>
2021-03-04 20:45 ` Magnus Hagander <magnus@hagander.net>
2021-03-04 23:21 ` Jacob Champion <pchampion@vmware.com>
2021-03-04 23:57 ` Hannu Krosing <hannuk@google.com>
2021-03-05 08:59 ` Magnus Hagander <magnus@hagander.net>
2021-03-05 00:33 ` Álvaro Hernández <aht@ongres.com>
2021-03-05 09:03 ` Magnus Hagander <magnus@hagander.net>
2021-03-05 13:49 ` Álvaro Hernández <aht@ongres.com>
2021-03-05 09:22 ` Magnus Hagander <magnus@hagander.net>
2021-03-05 19:11 ` Jacob Champion <pchampion@vmware.com>
2021-03-06 15:17 ` Magnus Hagander <magnus@hagander.net>
2021-03-06 16:30 ` Magnus Hagander <magnus@hagander.net>
2021-03-09 10:25 ` Magnus Hagander <magnus@hagander.net>
2021-03-10 23:05 ` Jacob Champion <pchampion@vmware.com>
2021-06-29 09:48 ` Magnus Hagander <magnus@hagander.net>
2021-07-08 23:42 ` Jacob Champion <pchampion@vmware.com>
2021-07-12 16:28 ` Magnus Hagander <magnus@hagander.net>
2021-07-14 18:23 ` Jacob Champion <pchampion@vmware.com>
2021-09-07 10:24 ` Magnus Hagander <magnus@hagander.net>
2021-09-08 18:51 ` Jacob Champion <pchampion@vmware.com>
2021-09-09 23:44 ` Jacob Champion <pchampion@vmware.com>
2021-09-28 13:23 ` Magnus Hagander <magnus@hagander.net>
2021-11-03 13:36 ` Daniel Gustafsson <daniel@yesql.se>
2021-11-04 11:03 ` Magnus Hagander <magnus@hagander.net>
2021-11-15 23:03 ` Jacob Champion <pchampion@vmware.com>
2022-02-25 10:41 ` Magnus Hagander <magnus@hagander.net>
2022-03-09 16:23 ` Peter Eisentraut <peter.eisentraut@enterprisedb.com>
2022-03-09 16:29 ` Magnus Hagander <magnus@hagander.net>
2022-04-01 22:16 ` wilfried roset <wilfried.roset@gmail.com>
2022-04-08 11:58 ` Magnus Hagander <magnus@hagander.net>
2022-07-28 20:05 ` Jacob Champion <jchampion@timescale.com>
2024-02-03 11:37 ` Julien Riou <julien@riou.xyz>
2026-06-20 05:46 ` Julien Riou <julien@riou.xyz>
2026-06-20 09:18 ` Julien Riou <julien@riou.xyz>
2026-09-23 00:27 ` Manu <manuelreyesbravo@gmail.com>
2021-06-29 08:08 ` Magnus Hagander <magnus@hagander.net>
2021-03-03 14:13 ` Bruno Lavoie <bl@brunol.com>
2021-03-04 01:42 ` Tatsuo Ishii <ishii@sraoss.co.jp>
2021-03-04 19:45 ` Jacob Champion <pchampion@vmware.com>
2021-03-04 20:29 ` Jan Wieck <jan@wi3ck.info>
2021-03-04 20:40 ` Magnus Hagander <magnus@hagander.net>
2021-03-04 21:01 ` Jan Wieck <jan@wi3ck.info>
2021-03-04 22:38 ` Magnus Hagander <magnus@hagander.net>
2021-03-04 20:47 ` Magnus Hagander <magnus@hagander.net>
2021-03-04 23:08 ` Tatsuo Ishii <ishii@sraoss.co.jp>
2021-03-05 09:14 ` Magnus Hagander <magnus@hagander.net>
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox