libpq: Prepare for protocol grease during 19beta

The main reason that libpq doesn't request protocol version 3.2 by
default is because other proxy/server implementations don't implement
the negotiation. This is a bit of a chicken-and-egg problem: We don't
bump the default version that libpq requests, but other implementations
may not be incentivized to implement version negotiation if their users
never run into issues.

One established practice to combat this is to flip Postel's Law on its
head, by sending parameters that the server cannot possibly support. If
the server fails the handshake instead of correctly negotiating, then
the problem is surfaced naturally. If the server instead claims to
support the bogus parameters, then we fail the connection to make the
lie obvious. This is called "grease" (or "greasing"), after the GREASE
mechanism in TLS that popularized the concept:

    https://www.rfc-editor.org/rfc/rfc8701.html

This patch reserves 3.9999 as an explicitly unsupported protocol version
number and `_pq_.test_protocol_negotiation` as an explicitly unsupported
protocol extension. A later commit will send these by default in order
to stress-test the ecosystem during the beta period; that commit will
then be reverted before 19 RC1, so that we can decide what to do with
whatever data has been gathered.

The _pq_.test_protocol_negotiation change here is intentionally docs-
only: after its implementation is reverted, the parameter should remain
reserved.

Extracted/adapted from a patch by Jelte Fennema-Nio.

Author: Jelte Fennema-Nio <postgres@jeltef.nl>
Co-authored-by: Jacob Champion <jacob.champion@enterprisedb.com>
Discussion: https://postgr.es/m/DDPR5BPWH1RJ.1LWAK6QAURVAY%40jeltef.nl
This commit is contained in:
Jacob Champion
2026-02-06 10:31:45 -08:00
parent e3d37853ec
commit d8d7c5dc8f
3 changed files with 46 additions and 1 deletions

View File

@ -270,6 +270,18 @@
</thead>
<tbody>
<row>
<entry>3.9999</entry>
<entry>-</entry>
<entry>Reserved for protocol greasing. libpq may use this version, which
is higher than any minor version the project ever expects to use, to
test that servers and middleware properly implement protocol version
negotiation. Servers <emphasis>must not</emphasis> add special-case
logic for this version; they should simply compare it to their latest
supported version (which will always be smaller) and downgrade via a
NegotiateProtocolVersion message.
</entry>
</row>
<row>
<entry>3.1</entry>
<entry>-</entry>
@ -353,6 +365,17 @@
otherwise continue the connection.
</entry>
</row>
<row>
<entry><literal>_pq_.test_protocol_negotiation</literal></entry>
<entry>Reserved for protocol greasing. libpq may send this extension to
test that servers and middleware properly implement protocol extension
negotiation. Servers <emphasis>must not</emphasis> add special-case
logic for this parameter; they should simply send the list of all
unsupported options (including this one) via a NegotiateProtocolVersion
message.
</entry>
</row>
</tbody>
</tgroup>
</table>

View File

@ -104,6 +104,16 @@ is_unixsock_path(const char *path)
*/
#define PG_PROTOCOL_RESERVED_31 PG_PROTOCOL(3,1)
/*
* PG_PROTOCOL_GREASE is an intentionally unsupported protocol version used
* for "greasing" (the practice of sending valid, but extraneous or otherwise
* unusual, messages to keep peer implementations honest). This helps ensure
* that servers properly implement protocol version negotiation. Version 3.9999
* was chosen since it is safely within the valid range, it is representable
* via PQfullProtocolVersion, and it is unlikely to ever be needed in practice.
*/
#define PG_PROTOCOL_GREASE PG_PROTOCOL(3,9999)
/*
* A client can send a cancel-current-operation request to the postmaster.
* This is uglier than sending it directly to the client's backend, but it

View File

@ -1451,7 +1451,19 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
if (pqGetInt(&num, 4, conn) != 0)
goto eof;
/* Check the protocol version */
/*
* Check the protocol version.
*
* PG_PROTOCOL_GREASE is intentionally unsupported and reserved. It's
* higher than any real version, so check for that first, to get the most
* specific error message. Then check the upper and lower bounds.
*/
if (their_version == PG_PROTOCOL_GREASE)
{
libpq_append_conn_error(conn, "received invalid protocol negotiation message: server requested \"grease\" protocol version 3.9999");
goto failure;
}
if (their_version > conn->pversion)
{
libpq_append_conn_error(conn, "received invalid protocol negotiation message: server requested downgrade to a higher-numbered version");