libpq: Add min/max_protocol_version connection options
authorHeikki Linnakangas <[email protected]>
Wed, 2 Apr 2025 13:41:45 +0000 (16:41 +0300)
committerHeikki Linnakangas <[email protected]>
Wed, 2 Apr 2025 13:41:45 +0000 (16:41 +0300)
All supported version of the PostgreSQL server send the
NegotiateProtocolVersion message when an unsupported minor protocol
version is requested by a client. But many other applications that
implement the PostgreSQL protocol (connection poolers, or other
databases) do not, and the same is true for PostgreSQL server versions
older than 9.3. Connecting to such other applications thus fails if a
client requests a protocol version different than 3.0.

This  adds a max_protocol_version connection option to libpq that
specifies the protocol version that libpq should request from the
server. Currently only 3.0 is supported, but that will change in a
future commit that bumps the protocol version. Even after that version
bump the default will likely stay 3.0 for the time being. Once more of
the ecosystem supports the NegotiateProtocolVersion message we might
want to change the default to the latest minor version.

This also adds the similar min_protocol_version connection option, to
allow the client to specify that connecting should fail if a lower
protocol version is attempted by the server. This can be used to
ensure that certain protocol features are used, which can be
particularly useful if those features impact security.

Author: Jelte Fennema-Nio <[email protected]>
Reviewed-by: Robert Haas <[email protected]> (earlier versions)
Discussion: https://www.postgresql.org/message-id/CAGECzQTfc_O%[email protected]
Discussion: https://www.postgresql.org/message-id/CAGECzQRbAGqJnnJJxTdKewTsNOovUt4bsx3NFfofz3m2j-t7tA@mail.gmail.com

doc/src/sgml/libpq.sgml
doc/src/sgml/protocol.sgml
src/include/libpq/pqcomm.h
src/interfaces/libpq/fe-connect.c
src/interfaces/libpq/fe-protocol3.c
src/interfaces/libpq/libpq-int.h
src/test/modules/libpq_pipeline/libpq_pipeline.c

index b359fbff295b7d0b3b94d9a3b3dadc22fd02ea35..d7051190320c8360778444b66232f22541bb4ad7 100644 (file)
@@ -2144,6 +2144,54 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-min-protocol-version" xreflabel="min_protocol_version">
+      <term><literal>min_protocol_version</literal></term>
+      <listitem>
+       <para>
+        Specifies the minimum protocol version to allow for the connection.
+        The default is to allow any version of the
+        <productname>PostgreSQL</productname> protocol supported by libpq,
+        which currently means <literal>3.0</literal>. If the server
+        does not support at least this protocol version the connection will be
+        closed.
+       </para>
+
+       <para>
+        The current supported values are
+        <literal>3.0</literal>, <literal>3.2</literal>,
+        and <literal>latest</literal>. The <literal>latest</literal> value is
+        equivalent to the latest protocol version supported by the libpq
+        version being used, which is currently <literal>3.2</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-max-protocol-version" xreflabel="max_protocol_version">
+      <term><literal>max_protocol_version</literal></term>
+      <listitem>
+       <para>
+        Specifies the protocol version to request from the server.
+        The default is to use version <literal>3.0</literal> of the
+        <productname>PostgreSQL</productname> protocol, unless the connection
+        string specifies a feature that relies on a higher protocol version,
+        in which case the latest version supported by libpq is used. If the
+        server does not support the protocol version requested by the client,
+        the connection is automatically downgraded to a lower minor protocol
+        version that the server supports. After the connection attempt has
+        completed you can use <xref linkend="libpq-PQprotocolVersion"/> to
+        find out which exact protocol version was negotiated.
+       </para>
+
+       <para>
+        The current supported values are
+        <literal>3.0</literal>, <literal>3.2</literal>,
+        and <literal>latest</literal>. The <literal>latest</literal> value is
+        equivalent to the latest protocol version supported by the libpq
+        version being used, which is currently <literal>3.2</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-ssl-max-protocol-version" xreflabel="ssl_max_protocol_version">
       <term><literal>ssl_max_protocol_version</literal></term>
       <listitem>
@@ -9329,6 +9377,26 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
       linkend="libpq-connect-load-balance-hosts"/> connection parameter.
      </para>
     </listitem>
+
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGMINPROTOCOLVERSION</envar></primary>
+      </indexterm>
+      <envar>PGMINPROTOCOLVERSION</envar> behaves the same as the <xref
+      linkend="libpq-connect-min-protocol-version"/> connection parameter.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGMAXPROTOCOLVERSION</envar></primary>
+      </indexterm>
+      <envar>PGMAXPROTOCOLVERSION</envar> behaves the same as the <xref
+      linkend="libpq-connect-max-protocol-version"/> connection parameter.
+     </para>
+    </listitem>
    </itemizedlist>
   </para>
 
index 6008f3ac5f8b866f45584ecb15b2e963deaa1c0f..628da4cd7ccb78af139463a787bbd092ed0d5598 100644 (file)
  </para>
 
  <para>
-  This document describes version 3.0 of the protocol, implemented in
-  <productname>PostgreSQL</productname> 7.4 and later.  For descriptions
-  of the earlier protocol versions, see previous releases of the
-  <productname>PostgreSQL</productname> documentation.  A single server
+  This document describes version 3.2 of the protocol, introduced in
+  <productname>PostgreSQL</productname> version 18. The server and the libpq
+  client library are backwards compatible with protocol version 3.0,
+  implemented in <productname>PostgreSQL</productname> 7.4 and later.
+  For descriptions of earlier protocol versions, see previous releases of the
+  <productname>PostgreSQL</productname> documentation.
+ </para>
+
+ <para>
+  A single server
   can support multiple protocol versions.  The initial startup-request
   message tells the server which protocol version the client is attempting to
   use.  If the major version requested by the client is not supported by
   the server, the connection will be rejected (for example, this would occur
   if the client requested protocol version 4.0, which does not exist as of
   this writing).  If the minor version requested by the client is not
-  supported by the server (e.g., the client requests version 3.1, but the
+  supported by the server (e.g., the client requests version 3.2, but the
   server supports only 3.0), the server may either reject the connection or
   may respond with a NegotiateProtocolVersion message containing the highest
   minor protocol version which it supports.  The client may then choose either
   to abort the connection.
  </para>
 
+ <para>
+  The protocol negotiation was introduced in
+  <productname>PostgreSQL</productname> version 9.3.21. Earlier versions would
+  reject the connection if the client requested a minor version that was not
+  supported by the server.
+ </para>
+
   <para>
    In order to serve multiple clients efficiently, the server launches
    a new <quote>backend</quote> process for each client.
         this message indicates the highest supported minor version.  This
         message will also be sent if the client requested unsupported protocol
         options (i.e., beginning with <literal>_pq_.</literal>) in the
-        startup packet.  This message will be followed by an ErrorResponse or
-        a message indicating the success or failure of authentication.
+        startup packet.
+       </para>
+       <para>
+        After this message, the authentication will continue using the version
+        indicated by the server.  If the client does not support the older
+        version, it should immediately close the connection.  If the server
+        does not send this message, it supports the client's requested
+        protocol version and all the protocol options.
        </para>
       </listitem>
      </varlistentry>
index 46b37e0e4ebb7b535c90e5ce424abf1603029f46..0aceb7147c7212e04a9b2ed217d8d6bbf1370acb 100644 (file)
@@ -91,11 +91,10 @@ is_unixsock_path(const char *path)
 
 /*
  * The earliest and latest frontend/backend protocol version supported.
- * (Only protocol version 3 is currently supported)
  */
 
 #define PG_PROTOCOL_EARLIEST   PG_PROTOCOL(3,0)
-#define PG_PROTOCOL_LATEST     PG_PROTOCOL(3,0)
+#define PG_PROTOCOL_LATEST     PG_PROTOCOL(3,2)
 
 typedef uint32 ProtocolVersion; /* FE/BE protocol version number */
 
index 0256753bd3eb8f8258ce01eee55702fdaf841754..5e3275ffd76f1dd67a4db185d9c9eff9ac1044da 100644 (file)
@@ -325,6 +325,16 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
        "Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */
    offsetof(struct pg_conn, require_auth)},
 
+   {"min_protocol_version", "PGMINPROTOCOLVERSION",
+       NULL, NULL,
+       "Min-Protocol-Version", "", 6,  /* sizeof("latest") = 6 */
+   offsetof(struct pg_conn, min_protocol_version)},
+
+   {"max_protocol_version", "PGMAXPROTOCOLVERSION",
+       NULL, NULL,
+       "Max-Protocol-Version", "", 6,  /* sizeof("latest") = 6 */
+   offsetof(struct pg_conn, max_protocol_version)},
+
    {"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL,
        "SSL-Minimum-Protocol-Version", "", 8,  /* sizeof("TLSv1.x") == 8 */
    offsetof(struct pg_conn, ssl_min_protocol_version)},
@@ -483,6 +493,7 @@ static void pgpassfileWarning(PGconn *conn);
 static void default_threadlock(int acquire);
 static bool sslVerifyProtocolVersion(const char *version);
 static bool sslVerifyProtocolRange(const char *min, const char *max);
+static bool pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, const char *context);
 
 
 /* global variable because fe-auth.c needs to access it */
@@ -2081,6 +2092,48 @@ pqConnectOptions2(PGconn *conn)
        }
    }
 
+   if (conn->min_protocol_version)
+   {
+       if (!pqParseProtocolVersion(conn->min_protocol_version, &conn->min_pversion, conn, "min_protocol_version"))
+       {
+           conn->status = CONNECTION_BAD;
+           return false;
+       }
+   }
+   else
+   {
+       conn->min_pversion = PG_PROTOCOL_EARLIEST;
+   }
+
+   if (conn->max_protocol_version)
+   {
+       if (!pqParseProtocolVersion(conn->max_protocol_version, &conn->max_pversion, conn, "max_protocol_version"))
+       {
+           conn->status = CONNECTION_BAD;
+           return false;
+       }
+   }
+   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.
+        */
+       if (conn->min_pversion > PG_PROTOCOL(3, 0))
+           conn->max_pversion = PG_PROTOCOL_LATEST;
+       else
+           conn->max_pversion = PG_PROTOCOL(3, 0);
+   }
+
+   if (conn->min_pversion > conn->max_pversion)
+   {
+       conn->status = CONNECTION_BAD;
+       libpq_append_conn_error(conn, "min_protocol_version is greater than max_protocol_version");
+       return false;
+   }
+
    /*
     * Resolve special "auto" client_encoding from the locale
     */
@@ -3084,7 +3137,7 @@ keep_going:                       /* We will come back to here until there is
         * must persist across individual connection attempts, but we must
         * reset them when we start to consider a new server.
         */
-       conn->pversion = PG_PROTOCOL(3, 0);
+       conn->pversion = conn->max_pversion;
        conn->send_appname = true;
        conn->failed_enc_methods = 0;
        conn->current_enc_method = 0;
@@ -4102,6 +4155,7 @@ keep_going:                       /* We will come back to here until there is
 
                    /* OK, we read the message; mark data consumed */
                    pqParseDone(conn, conn->inCursor);
+
                    goto keep_going;
                }
 
@@ -8157,6 +8211,38 @@ error:
    return false;
 }
 
+/*
+ * Parse and try to interpret "value" as a ProtocolVersion value, and if
+ * successful, store it in *result.
+ */
+static bool
+pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn,
+                      const char *context)
+{
+   if (strcmp(value, "latest") == 0)
+   {
+       *result = PG_PROTOCOL_LATEST;
+       return true;
+   }
+   if (strcmp(value, "3.0") == 0)
+   {
+       *result = PG_PROTOCOL(3, 0);
+       return true;
+   }
+
+   /* 3.1 never existed, we went straight from 3.0 to 3.2 */
+
+   if (strcmp(value, "3.2") == 0)
+   {
+       *result = PG_PROTOCOL(3, 2);
+       return true;
+   }
+
+   libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+                           context, value);
+   return false;
+}
+
 /*
  * To keep the API consistent, the locking stubs are always provided, even
  * if they are not required.
index 43e3519e4bd9a242f5349bb3622d20990d2c1eb1..7ba49ea4592022a5c7ff4cc1272a8821f3b73ceb 100644 (file)
@@ -1432,6 +1432,13 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
        goto failure;
    }
 
+   /* 3.1 never existed, we went straight from 3.0 to 3.2 */
+   if (their_version == PG_PROTOCOL(3, 1))
+   {
+       libpq_append_conn_error(conn, "received invalid protocol negotiation message: server requests downgrade to non-existent 3.1 protocol version");
+       goto failure;
+   }
+
    if (num < 0)
    {
        libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported negative number of unsupported parameters");
@@ -1444,6 +1451,17 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
        goto failure;
    }
 
+   if (their_version < conn->min_pversion)
+   {
+       libpq_append_conn_error(conn, "server only supports protocol version %d.%d, but min_protocol_version was set to %d.%d",
+                               PG_PROTOCOL_MAJOR(their_version),
+                               PG_PROTOCOL_MINOR(their_version),
+                               PG_PROTOCOL_MAJOR(conn->min_pversion),
+                               PG_PROTOCOL_MINOR(conn->min_pversion));
+
+       goto failure;
+   }
+
    /* the version is acceptable */
    conn->pversion = their_version;
 
index d9f2e9ad74377c2675c557fbcd0553d2b9d13c6c..232e0b00f7501e615126ac02ab8e1506c7b1d9a9 100644 (file)
@@ -417,6 +417,8 @@ struct pg_conn
    char       *gsslib;         /* What GSS library to use ("gssapi" or
                                 * "sspi") */
    char       *gssdelegation;  /* Try to delegate GSS credentials? (0 or 1) */
+   char       *min_protocol_version;   /* minimum used protocol version */
+   char       *max_protocol_version;   /* maximum used protocol version */
    char       *ssl_min_protocol_version;   /* minimum TLS protocol version */
    char       *ssl_max_protocol_version;   /* maximum TLS protocol version */
    char       *target_session_attrs;   /* desired session properties */
@@ -539,6 +541,8 @@ struct pg_conn
    void       *scram_client_key_binary;    /* binary SCRAM client key */
    size_t      scram_server_key_len;
    void       *scram_server_key_binary;    /* binary SCRAM server key */
+   ProtocolVersion min_pversion;   /* protocol version to request */
+   ProtocolVersion max_pversion;   /* protocol version to request */
 
    /* Miscellaneous stuff */
    int         be_pid;         /* PID of backend --- needed for cancels */
index ac9ac95135f381609b7eec9cecd522e3f50c3314..9a3c0236325c6e89bf40a31137545acb26711a41 100644 (file)
@@ -206,15 +206,17 @@ copy_connection(PGconn *conn)
    PQconninfoOption *opts = PQconninfo(conn);
    const char **keywords;
    const char **vals;
-   int         nopts = 1;
-   int         i = 0;
+   int         nopts = 0;
+   int         i;
 
    for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
        nopts++;
+   nopts++;                    /* for the NULL terminator */
 
    keywords = pg_malloc(sizeof(char *) * nopts);
    vals = pg_malloc(sizeof(char *) * nopts);
 
+   i = 0;
    for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
    {
        if (opt->val)
@@ -1405,6 +1407,110 @@ test_prepared(PGconn *conn)
    fprintf(stderr, "ok\n");
 }
 
+/*
+ * Test max_protocol_version options.
+ */
+static void
+test_protocol_version(PGconn *conn)
+{
+   const char **keywords;
+   const char **vals;
+   int         nopts;
+   PQconninfoOption *opts = PQconninfo(conn);
+   int         protocol_version;
+   int         max_protocol_version_index;
+   int         i;
+
+   /*
+    * Prepare keywords/vals arrays, copied from the existing connection, with
+    * an extra slot for 'max_protocol_version'.
+    */
+   nopts = 0;
+   for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
+       nopts++;
+   nopts++;                    /* max_protocol_version */
+   nopts++;                    /* NULL terminator */
+
+   keywords = pg_malloc0(sizeof(char *) * nopts);
+   vals = pg_malloc0(sizeof(char *) * nopts);
+
+   i = 0;
+   for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
+   {
+       if (opt->val)
+       {
+           keywords[i] = opt->keyword;
+           vals[i] = opt->val;
+           i++;
+       }
+   }
+
+   max_protocol_version_index = i;
+   keywords[i] = "max_protocol_version";   /* value is filled in below */
+   i++;
+   keywords[i] = vals[i] = NULL;
+
+   /*
+    * Test max_protocol_version=3.0
+    */
+   vals[max_protocol_version_index] = "3.0";
+   conn = PQconnectdbParams(keywords, vals, false);
+
+   if (PQstatus(conn) != CONNECTION_OK)
+       pg_fatal("Connection to database failed: %s",
+                PQerrorMessage(conn));
+
+   protocol_version = PQfullProtocolVersion(conn);
+   if (protocol_version != 30000)
+       pg_fatal("expected 30000, got %d", protocol_version);
+
+   PQfinish(conn);
+
+   /*
+    * Test max_protocol_version=3.1. It's not valid, we went straight from
+    * 3.0 to 3.2.
+    */
+   vals[max_protocol_version_index] = "3.1";
+   conn = PQconnectdbParams(keywords, vals, false);
+
+   if (PQstatus(conn) != CONNECTION_BAD)
+       pg_fatal("Connecting with max_protocol_version 3.1 should have failed.");
+
+   PQfinish(conn);
+
+   /*
+    * Test max_protocol_version=3.2
+    */
+   vals[max_protocol_version_index] = "3.2";
+   conn = PQconnectdbParams(keywords, vals, false);
+
+   if (PQstatus(conn) != CONNECTION_OK)
+       pg_fatal("Connection to database failed: %s",
+                PQerrorMessage(conn));
+
+   protocol_version = PQfullProtocolVersion(conn);
+   if (protocol_version != 30002)
+       pg_fatal("expected 30002, got %d", protocol_version);
+
+   PQfinish(conn);
+
+   /*
+    * Test max_protocol_version=latest. 'latest' currently means '3.2'.
+    */
+   vals[max_protocol_version_index] = "latest";
+   conn = PQconnectdbParams(keywords, vals, false);
+
+   if (PQstatus(conn) != CONNECTION_OK)
+       pg_fatal("Connection to database failed: %s",
+                PQerrorMessage(conn));
+
+   protocol_version = PQfullProtocolVersion(conn);
+   if (protocol_version != 30002)
+       pg_fatal("expected 30002, got %d", protocol_version);
+
+   PQfinish(conn);
+}
+
 /* Notice processor: print notices, and count how many we got */
 static void
 notice_processor(void *arg, const char *message)
@@ -2153,6 +2259,7 @@ print_test_list(void)
    printf("pipeline_idle\n");
    printf("pipelined_insert\n");
    printf("prepared\n");
+   printf("protocol_version\n");
    printf("simple_pipeline\n");
    printf("singlerow\n");
    printf("transaction\n");
@@ -2263,6 +2370,8 @@ main(int argc, char **argv)
        test_pipelined_insert(conn, numrows);
    else if (strcmp(testname, "prepared") == 0)
        test_prepared(conn);
+   else if (strcmp(testname, "protocol_version") == 0)
+       test_protocol_version(conn);
    else if (strcmp(testname, "simple_pipeline") == 0)
        test_simple_pipeline(conn);
    else if (strcmp(testname, "singlerow") == 0)