From 62895e078c64bf3b055466581afaaa0943244cf6 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Wed, 14 Aug 2024 18:25:16 +0200
Subject: [PATCH v16 4/8] libpq: Add min/max_protocol_version connection
 options

All officially 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 many unspported PostgreSQL
server versions. Connecting to such other applications thus fails if a
client requests a protocol version different than 3.0.

This patch adds a max_protocol_version connection option to libpq that
specifies the protocol version that libpq should request from the
server. Currently all allowed values result in the use of 3.0, but that
will be changed 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.

We also add the similar min_protocol_version connection option, to allow
a client to specify that connecting should fail if a lower protocol
version is attempted by the server. This can be used to ensure certain
protocol features are in used, which can be particularly useful if those
features impact security.
---
 doc/src/sgml/libpq.sgml           |  70 +++++++++++++++++++
 src/interfaces/libpq/fe-connect.c | 111 +++++++++++++++++++++++++++++-
 src/interfaces/libpq/libpq-int.h  |   4 ++
 3 files changed, 184 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 105b22b3171..385ddb87c65 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2345,6 +2345,56 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
        </para>
       </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>
+        and <literal>latest</literal>. The <literal>latest</literal> value is
+        equivalent to the latest protocol version that is supported by the used
+        libpq version, which currently is <literal>3.2</literal>, but this will
+        change in future libpq releases.
+       </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
+        that case the latest version supported by libpq is used. If the server
+        does not support the requested protocol version of the client the
+        connection will be automatically downgraded to a lower minor protocol
+        version, which the server does support. 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>
+        and <literal>latest</literal>. The <literal>latest</literal> value is
+        equivalent to the latest protocol version that is supported by the used
+        libpq version, which currently is <literal>3.0</literal>, but this will
+        change in future libpq releases.
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -9191,6 +9241,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>
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index fbe693a39e5..d0844a03396 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -366,6 +366,16 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Load-Balance-Hosts", "", 8,	/* sizeof("disable") = 8 */
 	offsetof(struct pg_conn, load_balance_hosts)},
 
+	{"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)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -451,6 +461,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 */
@@ -1836,6 +1847,42 @@ pqConnectOptions2(PGconn *conn)
 		}
 	}
 
+	if (conn->min_protocol_version)
+	{
+		if (!pqParseProtocolVersion(conn->min_protocol_version, &conn->min_pversion, conn, "min_protocol_version"))
+			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"))
+			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
 	 */
@@ -2838,7 +2885,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;
@@ -3851,6 +3898,14 @@ keep_going:						/* We will come back to here until there is
 
 					/* OK, we read the message; mark data consumed */
 					pqParseDone(conn, conn->inCursor);
+
+					if (conn->pversion < 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(conn->pversion), PG_PROTOCOL_MINOR(conn->pversion), PG_PROTOCOL_MAJOR(conn->min_pversion), PG_PROTOCOL_MINOR(conn->min_pversion));
+
+						goto error_return;
+					}
+
 					goto keep_going;
 				}
 
@@ -7806,6 +7861,60 @@ 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)
+{
+	char	   *end;
+	int			major;
+	int			minor;
+	ProtocolVersion version;
+
+	if (strcmp(value, "latest") == 0)
+	{
+		*result = PG_PROTOCOL_LATEST;
+		return true;
+	}
+
+	major = strtol(value, &end, 10);
+	if (*end != '.')
+	{
+		conn->status = CONNECTION_BAD;
+		libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+								context,
+								value);
+		return false;
+	}
+
+	minor = strtol(&end[1], &end, 10);
+	if (*end != '\0')
+	{
+		conn->status = CONNECTION_BAD;
+		libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+								context,
+								value);
+		return false;
+	}
+
+	version = PG_PROTOCOL(major, minor);
+	if (version > PG_PROTOCOL_LATEST ||
+		version < PG_PROTOCOL_EARLIEST)
+	{
+		conn->status = CONNECTION_BAD;
+		libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+								context,
+								value);
+		return false;
+	}
+
+	*result = version;
+	return true;
+}
+
 /*
  * To keep the API consistent, the locking stubs are always provided, even
  * if they are not required.
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index dcebca98988..46b8376f2ea 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -428,6 +428,8 @@ struct pg_conn
 	char	   *target_session_attrs;	/* desired session properties */
 	char	   *require_auth;	/* name of the expected auth method */
 	char	   *load_balance_hosts; /* load balance over hosts */
+	char	   *min_protocol_version;	/* minimum used protocol version */
+	char	   *max_protocol_version;	/* maximum used protocol version */
 
 	bool		cancelRequest;	/* true if this connection is used to send a
 								 * cancel request, instead of being a normal
@@ -518,6 +520,8 @@ struct pg_conn
 	AddrInfo   *addr;			/* the array of addresses for the currently
 								 * tried host */
 	bool		send_appname;	/* okay to send application_name? */
+	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 */
-- 
2.34.1

