On Tue, Nov 16, 2021 at 12:03 AM Jacob Champion <pchamp...@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/
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);