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. -- 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 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);