On Tue, Mar 9, 2021 at 11:25 AM Magnus Hagander <mag...@hagander.net> wrote: > > On Sat, Mar 6, 2021 at 5:30 PM Magnus Hagander <mag...@hagander.net> wrote: > > > > On Sat, Mar 6, 2021 at 4:17 PM Magnus Hagander <mag...@hagander.net> wrote: > > > > > > On Fri, Mar 5, 2021 at 8:11 PM Jacob Champion <pchamp...@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 <pchamp...@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/
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);