diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 53832d0..e0ae721 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -495,6 +495,18 @@ hostnossl database user
+ redirect
+
+
+ Redirects the connection to the alternative server specified if it
+ matches the requested database user name and IP address. This is
+ only available for Protocol Version 3.1 and beyond.
+ See for details.
+
+
+
+
+
ldap
@@ -624,7 +636,7 @@ hostnossl database user
non-null error fields indicate problems in the
corresponding lines of the file.
-
+
To connect to a particular database, a user must not only pass the
@@ -1434,6 +1446,29 @@ omicron bryanh guest1
+
+ Redirect Authentication
+
+
+ redirect
+
+
+
+ This authentication mechanism works by specifying an alternative server to redirect
+ client connections to. The alternative server endpoint is specified by a hostname and port, separated by a single comma.
+
+
+
+ An example entry with redirect would look like the following:
+ host all all 127.0.0.1/32 redirect ,
+
+
+
+ Support for redirection is only available from wire protocol v3.1 upwards.
+
+
+
+
LDAP Authentication
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 3cec9e0..5b58270 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -4747,6 +4747,79 @@ GSSResponse (F)
+RedirectClient (B)
+
+
+
+
+
+
+
+ Byte1('M')
+
+
+
+ Redirects client connection to alternative server.
+
+
+
+
+
+ Int32
+
+
+
+ Length of message contents in bytes, including self.
+
+
+
+
+
+ String
+
+
+
+ Parameter name "server" to identify the redirect target host.
+
+
+
+
+
+ String
+
+
+
+ Specifies the target host name.
+
+
+
+
+
+ String
+
+
+
+ Parameter name "port" to identify the redirect target port.
+
+
+
+
+
+ String
+
+
+
+ Specifies the target host's port.
+
+
+
+
+
+
+
+
+
+
NegotiateProtocolVersion (B)
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 3014b17..a700a9b 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -206,6 +206,12 @@ static int pg_SSPI_make_upn(char *accountname,
static int CheckRADIUSAuth(Port *port);
static int PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd);
+/*----------------------------------------------------------------
+ * Connection Redirection
+ *----------------------------------------------------------------
+ */
+static int SendAlternativeServerName(Port *port, char **logdetail);
+
/*
* Maximum accepted size of GSS and SSPI authentication tokens.
@@ -598,6 +604,9 @@ ClientAuthentication(Port *port)
case uaTrust:
status = STATUS_OK;
break;
+ case uaRedirect:
+ status = SendAlternativeServerName(port, &logdetail);
+ break;
}
if (ClientAuthentication_hook)
@@ -609,6 +618,28 @@ ClientAuthentication(Port *port)
auth_failed(port, status, logdetail);
}
+/*
+ * Send alternative server information packet to the frontend.
+ */
+static int
+SendAlternativeServerName(Port *port, char **logdetail)
+{
+ StringInfoData buf;
+
+ CHECK_FOR_INTERRUPTS();
+
+ pq_beginmessage(&buf, 'M');
+ pq_sendstring(&buf, "server");
+ pq_sendstring(&buf, port->hba->alternativeservername);
+ pq_sendstring(&buf, "port");
+ pq_sendstring(&buf, port->hba->alternativeserverport);
+
+ pq_endmessage(&buf);
+ pq_flush();
+ proc_exit(0);
+
+ return STATUS_OK;
+}
/*
* Send an authentication request packet to the frontend.
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index acf625e..f3f7d6f 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -134,7 +134,8 @@ static const char *const UserAuthName[] =
"ldap",
"cert",
"radius",
- "peer"
+ "peer",
+ "redirect"
};
@@ -1358,6 +1359,8 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
#endif
else if (strcmp(token->string, "radius") == 0)
parsedline->auth_method = uaRADIUS;
+ else if (strcmp(token->string, "redirect") == 0)
+ parsedline->auth_method = uaRedirect;
else
{
ereport(elevel,
@@ -1384,6 +1387,49 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
return NULL;
}
+ if (parsedline->auth_method == uaRedirect)
+ {
+ /* Get the alternative server name and port */
+ field = lnext(field);
+ if (!field)
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("end-of-line before alternative server name"),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ *err_msg = "end-of-line before alternative server name";
+ return NULL;
+ }
+ tokens = lfirst(field);
+ if (tokens->length > 2)
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("multiple values specified for alternative server"),
+ errhint("Specify exactly one alternative server per line."),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ *err_msg = "multiple values specified for alternative server";
+ return NULL;
+ }
+
+ tokencell = list_head(tokens);
+ token = lfirst(tokencell);
+ parsedline->alternativeservername = pstrdup(token->string);
+
+ if (tokens->length == 2)
+ {
+ tokencell = lnext(tokencell);
+ token = lfirst(tokencell);
+ parsedline->alternativeserverport = pstrdup(token->string);
+ }
+ else
+ {
+ pg_itoa(DEF_PGPORT, parsedline->alternativeserverport);
+ }
+ }
+
/*
* XXX: When using ident on local connections, change it to peer, for
* backwards compatibility.
@@ -2100,6 +2146,21 @@ check_hba(hbaPort *port)
if (!check_role(port->user_name, roleid, hba->roles))
continue;
+ /*
+ * Check the protocol version to see if the client supports
+ * redirection
+ */
+ if (hba->auth_method == uaRedirect &&
+ PG_PROTOCOL_MAJOR(port->proto) < PG_PROTOCOL_MAJOR(PG_PROTOCOL_LATEST) ||
+ (PG_PROTOCOL_MAJOR(port->proto) == PG_PROTOCOL_MAJOR(PG_PROTOCOL_LATEST) &&
+ PG_PROTOCOL_MINOR(port->proto) < PG_PROTOCOL_MINOR(PG_PROTOCOL_LATEST)))
+ ereport(FATAL,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Redirection is only supported by protocol version 3.1 and above. \
+ Try connecting to %s:%s directly",
+ hba->alternativeservername,
+ hba->alternativeserverport)));
+
/* Found a record that matched! */
port->hba = hba;
return;
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index c853e36..3af0972 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -43,7 +43,7 @@
# directly connected to.
#
# METHOD can be "trust", "reject", "md5", "password", "scram-sha-256",
-# "gss", "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert".
+# "gss", "sspi", "ident", "peer", "pam", "ldap", "radius", "cert" or "redirect".
# Note that "password" sends passwords in clear text; "md5" or
# "scram-sha-256" are preferred since they send encrypted passwords.
#
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 5f68f4c..1e4f2bd 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -38,8 +38,9 @@ typedef enum UserAuth
uaLDAP,
uaCert,
uaRADIUS,
- uaPeer
-#define USER_AUTH_LAST uaPeer /* Must be last value of this enum */
+ uaPeer,
+ uaRedirect
+#define USER_AUTH_LAST uaRedirect /* Must be last value of this enum */
} UserAuth;
typedef enum IPCompareMethod
@@ -99,6 +100,8 @@ typedef struct HbaLine
char *radiusidentifiers_s;
List *radiusports;
char *radiusports_s;
+ char *alternativeservername;
+ char *alternativeserverport;
} HbaLine;
typedef struct IdentLine
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 77eebb0..26fa934 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -129,6 +129,7 @@ static int ldapServiceLookup(const char *purl, PQconninfoOption *options,
#else
#define DefaultSSLMode "disable"
#endif
+#define DefaultRedirectionLimit "2"
/* ----------
* Definition of the conninfo parameters and their fallback resources.
@@ -266,7 +267,7 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
{"scram_channel_binding", NULL, DefaultSCRAMChannelBinding, NULL,
"SCRAM-Channel-Binding", "D",
- 21, /* sizeof("tls-server-end-point") == 21 */
+ 21, /* sizeof("tls-server-end-point") == 21 */
offsetof(struct pg_conn, scram_channel_binding)},
/*
@@ -330,6 +331,11 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"Target-Session-Attrs", "", 11, /* sizeof("read-write") = 11 */
offsetof(struct pg_conn, target_session_attrs)},
+ {"redirect_limit", "PGREDIRECTLIMIT",
+ DefaultRedirectionLimit, NULL,
+ "Redirection-Count", "", 10, /* strlen(INT32_MAX) == 10 */
+ offsetof(struct pg_conn, redirect_limit)},
+
/* Terminating entry --- MUST BE LAST */
{NULL, NULL, NULL, NULL,
NULL, NULL, 0}
@@ -1805,11 +1811,11 @@ connectDBStart(PGconn *conn)
#endif
/*
- * Set up to try to connect, with protocol 3.0 as the first attempt.
+ * Set up to try to connect, with protocol 3.1 as the first attempt.
*/
conn->whichhost = 0;
conn->addr_cur = conn->connhost[0].addrlist;
- conn->pversion = PG_PROTOCOL(3, 0);
+ conn->pversion = PG_PROTOCOL(3, 1);
conn->send_appname = true;
conn->status = CONNECTION_NEEDED;
@@ -2007,6 +2013,14 @@ PQconnectPoll(PGconn *conn)
int optval;
PQExpBufferData savedMessage;
+ /* Variable declarations for redirection. */
+ int originalMsgLen; /* Length in bytes of message sans msg type */
+ int runningMsgLen; /* Length in bytes of message sans metadata */
+ int availableMsgLen;
+ char *altServer = NULL;
+ char *altPort = NULL;
+ bool redirectionError = false; /* Flag used to mark exceptions */
+
if (conn == NULL)
return PGRES_POLLING_FAILED;
@@ -2024,6 +2038,7 @@ PQconnectPoll(PGconn *conn)
/* These are reading states */
case CONNECTION_AWAITING_RESPONSE:
+ case CONNECTION_REDIRECTION:
case CONNECTION_AUTH_OK:
{
/* Load waiting data */
@@ -2655,6 +2670,25 @@ keep_going: /* We will come back to here until there is
return PGRES_POLLING_READING;
}
+ if (beresp == 'M')
+ {
+ conn->status = CONNECTION_REDIRECTION;
+ goto keep_going;
+ }
+
+ /*
+ * If server sends protocol negotiation message, default to
+ * 3.0 protocol.
+ */
+ if (beresp == 'v')
+ {
+ conn->pversion = PG_PROTOCOL(3, 0);
+ /* Must drop the old connection */
+ pqDropConnection(conn, true);
+ conn->status = CONNECTION_NEEDED;
+ goto keep_going;
+ }
+
/*
* Validate message type: we expect only an authentication
* request or an error here. Anything else probably means
@@ -2722,19 +2756,28 @@ keep_going: /* We will come back to here until there is
appendPQExpBufferChar(&conn->errorMessage, '\n');
/*
- * If we tried to open the connection in 3.0 protocol,
- * fall back to 2.0 protocol.
+ * If we tried to open the connection in 3.1 protocol,
+ * fall back to 3.0 protocol. If that fails as well, fall
+ * back to 2.0 protocol.
*/
- if (PG_PROTOCOL_MAJOR(conn->pversion) >= 3)
+ if (PG_PROTOCOL_MAJOR(conn->pversion) >= 3
+ && PG_PROTOCOL_MINOR(conn->pversion) >= 1)
+ {
+ conn->pversion = PG_PROTOCOL(3, 0);
+ }
+ else if (PG_PROTOCOL_MAJOR(conn->pversion) >= 3)
{
conn->pversion = PG_PROTOCOL(2, 0);
- /* Must drop the old connection */
- pqDropConnection(conn, true);
- conn->status = CONNECTION_NEEDED;
- goto keep_going;
+ }
+ else
+ {
+ goto error_return;
}
- goto error_return;
+ /* Must drop the old connection */
+ pqDropConnection(conn, true);
+ conn->status = CONNECTION_NEEDED;
+ goto keep_going;
}
/*
@@ -3097,6 +3140,146 @@ keep_going: /* We will come back to here until there is
conn->status = CONNECTION_OK;
return PGRES_POLLING_OK;
}
+
+ case CONNECTION_REDIRECTION:
+ {
+ /*
+ * Check if the number of redirect attempts exceeds the limit.
+ */
+ if (++conn->nRedirection > atoi(conn->redirect_limit))
+ {
+ appendPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("Exceeded the maximum number of redirection attempts."));
+ goto error_return;
+ }
+
+ /* Mark 'M' consumed: a single byte for message type. */
+ conn->inCursor = conn->inStart + 1;
+
+ /* Obtain message length from packet. */
+ if (pqGetInt(&originalMsgLen, sizeof(int32), conn))
+ return PGRES_POLLING_READING;
+
+ /* Obtain the number of bytes in payload. */
+ runningMsgLen = originalMsgLen - sizeof(int32);
+
+ /*
+ * Enlarge buffer if payload's size is greater than what is
+ * available.
+ */
+ availableMsgLen = conn->inEnd - conn->inCursor;
+ if (availableMsgLen < runningMsgLen)
+ {
+ if (pqCheckInBufferSpace(conn->inCursor + (size_t) runningMsgLen, conn))
+ return PGRES_POLLING_READING;
+ }
+
+ PQExpBuffer buf = createPQExpBuffer();
+
+ while (pqGets(buf, conn) != EOF)
+ {
+ if (!strcmp(buf->data, "server"))
+ {
+ if (pqGets(buf, conn) == EOF)
+ {
+ appendPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("failed to obtain server value from redirection packet"));
+
+ redirectionError = true;
+ break;
+ }
+ altServer = strdup(buf->data);
+ }
+ else if (!strcmp(buf->data, "port"))
+ {
+ if (pqGets(buf, conn) == EOF)
+ {
+ appendPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("failed to obtain port value from redirection packet"));
+
+ redirectionError = true;
+ break;
+ }
+ altPort = strdup(buf->data);
+ }
+ else
+ {
+ appendPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("unknown key in redirection server packet"));
+ redirectionError = true;
+ }
+
+ if (redirectionError)
+ {
+ /* Free buffer to prevent memory leak on error. */
+ destroyPQExpBuffer(buf);
+
+ /* Free strdup'd variables. */
+ if (altServer)
+ free(altServer);
+
+ if (altPort)
+ free(altPort);
+
+ goto error_return;
+ }
+
+ /*
+ * Buffer length does not account for null-terminated
+ * strings.
+ */
+ runningMsgLen -= buf->len + 1;
+ }
+
+ /* Free buffer used for reading string params in packet. */
+ destroyPQExpBuffer(buf);
+
+ /* Check for extraneous data in packet. */
+ if (conn->inCursor != conn->inStart + 1 + originalMsgLen)
+ {
+ appendPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("Extraneous data in redirection packet from server\n"));
+
+ /* Free strdup'd variables. */
+ if (altServer)
+ free(altServer);
+
+ if (altPort)
+ free(altPort);
+
+ goto error_return;
+ }
+
+ /* Mark incoming data consumed */
+ conn->inStart = conn->inCursor;
+
+ /* Drop existing connection. */
+ pqDropConnection(conn, true);
+
+ /* Set connection parameters. */
+ if (conn->pghost)
+ {
+ free(conn->pghost);
+ conn->pghost = altServer;
+ }
+
+ if (conn->pgport)
+ {
+ free(conn->pgport);
+ conn->pgport = altPort;
+ }
+
+ /* connectDBStart() sets appropriate connection status. */
+ if (!connectOptions2(conn) || !connectDBStart(conn))
+ {
+ conn->status = CONNECTION_BAD;
+ appendPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("Failed to establish connection to redirected database\n"));
+ }
+
+ goto keep_going;
+ }
+
case CONNECTION_CHECK_WRITABLE:
{
const char *displayed_host;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index ed9c806..38616d2 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -65,8 +65,10 @@ typedef enum
CONNECTION_NEEDED, /* Internal state: connect() needed */
CONNECTION_CHECK_WRITABLE, /* Check if we could make a writable
* connection. */
- CONNECTION_CONSUME /* Wait for any pending message and consume
+ CONNECTION_CONSUME, /* Wait for any pending message and consume
* them. */
+ CONNECTION_REDIRECTION /* Redirecting the connection to the
+ * alternative server specified in pg_hba.conf */
} ConnStatusType;
typedef enum
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index eba23dc..d4edd40 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -349,7 +349,7 @@ struct pg_conn
* retransmits */
char *keepalives_count; /* maximum number of TCP keepalive
* retransmits */
- char *scram_channel_binding; /* SCRAM channel binding type */
+ char *scram_channel_binding; /* SCRAM channel binding type */
char *sslmode; /* SSL mode (require,prefer,allow,disable) */
char *sslcompression; /* SSL compression (0 or 1) */
char *sslkey; /* client key filename */
@@ -495,6 +495,10 @@ struct pg_conn
/* Buffer for receiving various parts of messages */
PQExpBufferData workBuffer; /* expansible string */
+
+ char *redirect_limit; /* Specifies the maximum number of times to
+ * attempt redirection. */
+ int nRedirection; /* Number of redirects attempted so far */
};
/* PGcancel stores all data necessary to cancel a connection. A copy of this
@@ -742,8 +746,8 @@ extern char *pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len);
*
*/
extern int pgtls_verify_peer_name_matches_certificate_guts(PGconn *conn,
- int *names_examined,
- char **first_name);
+ int *names_examined,
+ char **first_name);
/* === miscellaneous macros === */
diff --git a/src/test/authentication/t/003_redirection.pl b/src/test/authentication/t/003_redirection.pl
new file mode 100644
index 0000000..db0b78c
--- /dev/null
+++ b/src/test/authentication/t/003_redirection.pl
@@ -0,0 +1,92 @@
+# Test redirection authentication method.
+#
+# This test cannot run on Windows as Postgres cannot be set up with Unix
+# sockets and needs to go through SSPI.
+
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More;
+if ($windows_os)
+{
+ plan skip_all => "authentication tests cannot run on Windows";
+}
+else
+{
+ plan tests => 3;
+}
+
+# Delete pg_hba.conf from the given node, add a new entry to it
+# and then execute a reload to refresh it.
+sub reset_pg_hba
+{
+ my $node = shift;
+ my $hba_method = shift;
+ my $redirect_endpoint = shift;
+
+ unlink($node->data_dir . '/pg_hba.conf');
+ $node->append_conf('pg_hba.conf', "local all all $hba_method $redirect_endpoint");
+ $node->reload;
+}
+
+# Test access for a single role, useful to wrap all tests into one.
+sub test_login
+{
+ my $node = shift;
+ my $role = shift;
+ my $password = shift;
+ my $expected_res = shift;
+ my $status_string = 'failed';
+
+ $status_string = 'success' if ($expected_res eq 0);
+
+ $ENV{"PGPASSWORD"} = $password;
+ my $res = $node->psql('postgres', undef, extra_params => [ '-U', $role ]);
+ is($res, $expected_res,
+ "authentication $status_string for role $role with password $password"
+ );
+}
+
+# Initialize node1.
+my $node1 = get_new_node('master');
+$node1->init;
+$node1->start;
+$node1->safe_psql(
+ 'postgres',
+ "CREATE ROLE pguser1 LOGIN PASSWORD 'postgres';
+");
+
+# Initialize node2, the redirect target.
+my $node2 = get_new_node('redirect');
+$node2->init;
+$node2->start;
+$node2->safe_psql(
+ 'postgres',
+ "CREATE ROLE pguser2 LOGIN PASSWORD 'postgres';
+");
+
+# Host is identical as both nodes reside on the same machine
+my $host = $node1->host;
+
+my $node1_port = $node1->port;
+# 1. Test a redirected connection from node1 to itself.
+# Add the redirect authentication method to the node1's pg_hba.conf to set up redirection to itself.
+reset_pg_hba($node1, 'redirect', "$host,$node1_port");
+# A redirect from a node to itself should fail after PGREDIRECTLIMIT (default is 5) retries.
+test_login($node1, 'pguser1', "postgres", 2);
+
+
+my $node2_port = $node2->port;
+# 2. Test a redirected connection from node1 to node2 with correct creds.
+# Add the redirect authentication method to the node1's pg_hba.conf to set up redirection to node2.
+reset_pg_hba($node1, 'redirect', "$host,$node2_port");
+# A redirect from a node to another should succeed, given the correct creds are used.
+test_login($node1, 'pguser2', "postgres", 0);
+
+
+# 3. Test a redirected connection from node1 to node2 with wrong creds.
+# Add the redirect authentication method to the node1's pg_hba.conf to set up redirection to node2.
+reset_pg_hba($node1, 'redirect', "$host,$node2_port");
+# A redirect from a node to another should fail if the wrong creds are used.
+test_login($node1, 'pguser', "postgres", 2);