diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 21e1ba34a4e..e08d46782cc 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -2211,6 +2211,23 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname max_protocol_version + + + During the PostgreSQL 19 beta period, libpq connections that do not + specify a max_protocol_version will "grease" the + handshake by sending unsupported startup parameters, including version + 3.9999, in order to identify software that does not + correctly negotiate the connection. This replaces the default behavior + described below. + + + If you know that a server doesn't properly implement protocol version + negotiation, you can set max_protocol_version=3.0 to + revert to the standard behavior (preferably after notifying the server's + maintainers that their software needs to be fixed). + + + Specifies the protocol version to request from the server. The default is to use version 3.0 of the diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index 89ac680efd5..49f81676712 100644 --- a/doc/src/sgml/protocol.sgml +++ b/doc/src/sgml/protocol.sgml @@ -198,6 +198,17 @@ by default. + + + During the PostgreSQL 19 beta period, libpq will instead default to + requesting protocol version 3.9999, to test that servers and middleware + properly implement protocol version negotiation. Servers that support + negotiation will automatically downgrade to version 3.2 or 3.0. Users can + bypass this beta-only behavior by explicitly setting + max_protocol_version=3.0 in their connection string. + + + A single server can support multiple protocol versions. The initial startup-request message tells the server which protocol version the client diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index a0d2f749811..b42a0cb4c78 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -91,8 +91,9 @@ static int ldapServiceLookup(const char *purl, PQconninfoOption *options, /* This is part of the protocol so just define it */ #define ERRCODE_INVALID_PASSWORD "28P01" -/* This too */ +/* These too */ #define ERRCODE_CANNOT_CONNECT_NOW "57P03" +#define ERRCODE_PROTOCOL_VIOLATION "08P01" /* * Cope with the various platform-specific ways to spell TCP keepalive socket @@ -2142,15 +2143,13 @@ pqConnectOptions2(PGconn *conn) else { /* - * To not break connecting to older servers/poolers that do not yet - * support NegotiateProtocolVersion, default to the 3.0 protocol at - * least for a while longer. Except when min_protocol_version is set - * to something larger, then we might as well default to the latest. + * Default to PG_PROTOCOL_GREASE, which is larger than all real + * versions, to test negotiation. The server should automatically + * downgrade to a supported version. + * + * This behavior is for 19beta only. It will be reverted before RC1. */ - if (conn->min_pversion > PG_PROTOCOL(3, 0)) - conn->max_pversion = PG_PROTOCOL_LATEST; - else - conn->max_pversion = PG_PROTOCOL(3, 0); + conn->max_pversion = PG_PROTOCOL_GREASE; } if (conn->min_pversion > conn->max_pversion) @@ -4156,6 +4155,32 @@ keep_going: /* We will come back to here until there is /* Check to see if we should mention pgpassfile */ pgpassfileWarning(conn); + /* + * ...and whether we should mention grease. If the error + * message contains the PG_PROTOCOL_GREASE number (in + * major.minor, decimal, or hex format) or a complaint + * about a protocol violation before we've even started an + * authentication exchange, it's probably caused by a + * grease interaction. + */ + if (conn->max_pversion == PG_PROTOCOL_GREASE && + !conn->auth_req_received) + { + const char *sqlstate = PQresultErrorField(conn->result, + PG_DIAG_SQLSTATE); + + if ((sqlstate && + strcmp(sqlstate, ERRCODE_PROTOCOL_VIOLATION) == 0) || + (conn->errorMessage.len > 0 && + (strstr(conn->errorMessage.data, "3.9999") || + strstr(conn->errorMessage.data, "206607") || + strstr(conn->errorMessage.data, "3270F") || + strstr(conn->errorMessage.data, "3270f")))) + { + libpq_append_grease_info(conn); + } + } + CONNECTION_FAILED(); } /* Handle NegotiateProtocolVersion */ @@ -4386,6 +4411,14 @@ keep_going: /* We will come back to here until there is goto error_return; } + if (conn->max_pversion == PG_PROTOCOL_GREASE && + conn->pversion == PG_PROTOCOL_GREASE) + { + libpq_append_conn_error(conn, "server incorrectly accepted \"grease\" protocol version 3.9999 without negotiation"); + libpq_append_grease_info(conn); + goto error_return; + } + /* Almost there now ... */ conn->status = CONNECTION_CHECK_TARGET; goto keep_going; diff --git a/src/interfaces/libpq/fe-misc.c b/src/interfaces/libpq/fe-misc.c index 5e54353fbfe..13775cfb8b9 100644 --- a/src/interfaces/libpq/fe-misc.c +++ b/src/interfaces/libpq/fe-misc.c @@ -1423,3 +1423,21 @@ libpq_append_conn_error(PGconn *conn, const char *fmt,...) appendPQExpBufferChar(&conn->errorMessage, '\n'); } + +/* + * For 19beta only, some protocol errors will have additional information + * appended to help with the "grease" campaign. + */ +void +libpq_append_grease_info(PGconn *conn) +{ + /* translator: %s is a URL */ + libpq_append_conn_error(conn, + "\tThis indicates a bug in either the server being contacted\n" + "\tor a proxy handling the connection. Please consider\n" + "\treporting this to the maintainers of that software.\n" + "\tFor more information, including instructions on how to\n" + "\twork around this issue for now, visit\n" + "\t\t%s", + "https://www.postgresql.org/docs/devel/libpq-connect.html#LIBPQ-CONNECT-MAX-PROTOCOL-VERSION"); +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 90bbb2eba1f..8c1fda5caf0 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -1444,6 +1444,15 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) { int their_version; int num; + bool found_test_protocol_negotiation; + bool expect_test_protocol_negotiation; + + /* + * During 19beta only, if protocol grease is in use, assume that it's the + * cause of any invalid messages encountered below. We'll print extra + * information for the end user in that case. + */ + bool need_grease_info = (conn->max_pversion == PG_PROTOCOL_GREASE); if (pqGetInt(&their_version, 4, conn) != 0) goto eof; @@ -1504,6 +1513,7 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) PG_PROTOCOL_MAJOR(conn->min_pversion), PG_PROTOCOL_MINOR(conn->min_pversion)); + need_grease_info = false; /* this is valid server behavior */ goto failure; } @@ -1511,9 +1521,12 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) conn->pversion = their_version; /* - * We don't currently request any protocol extensions, so we don't expect - * the server to reply with any either. + * Check that all expected unsupported parameters are reported by the + * server. */ + found_test_protocol_negotiation = false; + expect_test_protocol_negotiation = (conn->max_pversion == PG_PROTOCOL_GREASE); + for (int i = 0; i < num; i++) { if (pqGets(&conn->workBuffer, conn)) @@ -1525,7 +1538,29 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported unsupported parameter name without a \"%s\" prefix (\"%s\")", "_pq_.", conn->workBuffer.data); goto failure; } - libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", conn->workBuffer.data); + + /* Check if this is the expected test parameter */ + if (expect_test_protocol_negotiation && + strcmp(conn->workBuffer.data, "_pq_.test_protocol_negotiation") == 0) + { + found_test_protocol_negotiation = true; + } + else + { + libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", + conn->workBuffer.data); + goto failure; + } + } + + /* + * If we requested protocol grease, the server must report + * _pq_.test_protocol_negotiation as unsupported. This ensures + * comprehensive NegotiateProtocolVersion implementation. + */ + if (expect_test_protocol_negotiation && !found_test_protocol_negotiation) + { + libpq_append_conn_error(conn, "server did not report the unsupported `_pq_.test_protocol_negotiation` parameter in its protocol negotiation message"); goto failure; } @@ -1534,6 +1569,8 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) eof: libpq_append_conn_error(conn, "received invalid protocol negotiation message: message too short"); failure: + if (need_grease_info) + libpq_append_grease_info(conn); conn->asyncStatus = PGASYNC_READY; pqSaveErrorResult(conn); return 1; @@ -2476,6 +2513,14 @@ build_startup_packet(const PGconn *conn, char *packet, if (conn->client_encoding_initial && conn->client_encoding_initial[0]) ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial); + /* + * Add the test_protocol_negotiation option when greasing, to test that + * servers properly report unsupported protocol options in addition to + * unsupported minor versions. + */ + if (conn->pversion == PG_PROTOCOL_GREASE) + ADD_STARTUP_OPTION("_pq_.test_protocol_negotiation", ""); + /* Add any environment-driven GUC settings needed */ for (next_eo = options; next_eo->envName; next_eo++) { diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index fb6a7cbf15d..bd7eb59f5f8 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -958,6 +958,7 @@ extern char *libpq_ngettext(const char *msgid, const char *msgid_plural, unsigne extern void libpq_append_error(PQExpBuffer errorMessage, const char *fmt,...) pg_attribute_printf(2, 3); extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3); +extern void libpq_append_grease_info(PGconn *conn); /* * These macros are needed to let error-handling code be portable between diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ce1a9995f46..409b3a7fa45 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version + * Test default protocol_version (GREASE - should negotiate down to 3.2) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30000) - pg_fatal("expected 30000, got %d", protocol_version); + if (protocol_version != 30002) + pg_fatal("expected 30002, got %d", protocol_version); PQfinish(conn);