From 3882d5952ca1c383b8408530f8a5351562ab1eba Mon Sep 17 00:00:00 2001
From: Diego <mrstephenamell@gmail.com>
Date: Mon, 27 Jul 2026 14:06:35 -0300
Subject: [PATCH v2 1/2] libpq: add portaddr, the port equivalent of hostaddr

libpq has long allowed the host name that identifies a server to differ
from the address actually connected to: host names the server, hostaddr
gives the address to reach it at, and the password file is searched using
host.  Ports have no such separation.  The port parameter is both the
port connected to and the port used as the password file search key, so a
connection made through an intermediary listening on a different port --
an SSH tunnel, a connection proxy -- cannot use a password file entry
written for the server itself.

Writing the intermediary's port into the password file works when that
port is fixed and known ahead of time, but not when it is assigned at
connection time, as with "ssh -L 127.0.0.1:0:...", cloud database proxies,
and GUI clients that manage their own tunnels.  There the local port is
unknown when the password file is written, and differs between sessions.

Add a portaddr parameter, and a PGPORTADDR environment variable, that
completes the model: when portaddr is given it is the port connected to,
and port only identifies the server, exactly as host does when hostaddr is
given.  The password file lookup therefore keeps using port.  Like
hostaddr, portaddr applies only to TCP connections; it is ignored for
Unix-domain sockets, whose socket file name is determined by port.

psql's \connect drops a hostaddr inherited from the previous connection
when the host argument changes.  Do the same for portaddr when either the
host or the port argument changes, so that reconnecting elsewhere is not
silently redirected through the old address.

PGPORTADDR is added to the environment variables cleared by pg_regress and
PostgreSQL::Test::Utils, since like PGHOSTADDR it would otherwise redirect
test connections away from the temporary cluster.

The new TAP test opens a TCP listen socket, so it runs only when portaddr
is listed in PG_TEST_EXTRA.

Behavior is unchanged when portaddr is not specified.

Discussion: https://postgr.es/m/001a6f1d-4adb-42b2-8bf6-44154ed0ab97@gmail.com
---
 doc/src/sgml/libpq.sgml                |  97 ++++++++++++++-
 doc/src/sgml/ref/psql-ref.sgml         |   8 +-
 doc/src/sgml/regress.sgml              |  11 ++
 src/bin/psql/command.c                 |  24 +++-
 src/interfaces/libpq/fe-cancel.c       |   6 +
 src/interfaces/libpq/fe-connect.c      |  76 ++++++++++-
 src/interfaces/libpq/libpq-int.h       |   6 +
 src/interfaces/libpq/meson.build       |   1 +
 src/interfaces/libpq/t/001_uri.pl      |   6 +
 src/interfaces/libpq/t/007_portaddr.pl | 166 +++++++++++++++++++++++++
 src/test/perl/PostgreSQL/Test/Utils.pm |   1 +
 src/test/regress/pg_regress.c          |   2 +
 12 files changed, 391 insertions(+), 13 deletions(-)
 create mode 100644 src/interfaces/libpq/t/007_portaddr.pl

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 7d3c3bb66d8..c2b078bd6a9 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1085,12 +1085,14 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
      <para>
        It is possible to specify multiple hosts to connect to, so that they are
        tried in the given order. In the Keyword/Value format, the <literal>host</literal>,
-       <literal>hostaddr</literal>, and <literal>port</literal> options accept comma-separated
+       <literal>hostaddr</literal>, <literal>port</literal>, and
+       <literal>portaddr</literal> options accept comma-separated
        lists of values. The same number of elements must be given in each
        option that is specified, such
        that e.g., the first <literal>hostaddr</literal> corresponds to the first host name,
        the second <literal>hostaddr</literal> corresponds to the second host name, and so
-       forth. As an exception, if only one <literal>port</literal> is specified, it
+       forth. As an exception, if only one <literal>port</literal>
+       or <literal>portaddr</literal> is specified, it
        applies to all the hosts.
      </para>
 
@@ -1251,6 +1253,73 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-portaddr" xreflabel="portaddr">
+      <term><literal>portaddr</literal></term>
+      <listitem>
+      <para>
+       Port number to actually connect to at the server host.  When a nonempty
+       string is specified for this parameter, it determines where the
+       connection is made, and <xref linkend="libpq-connect-port"/> only
+       identifies the server &mdash; much as
+       <xref linkend="libpq-connect-host"/> still identifies the server when
+       <xref linkend="libpq-connect-hostaddr"/> is given.  If this parameter
+       is not specified, the value of <literal>port</literal> is connected to,
+       which is the historical behavior.  This parameter is ignored for
+       Unix-domain socket connections, whose socket file name is always
+       determined by <literal>port</literal>.
+      </para>
+
+      <para>
+       Using <literal>portaddr</literal> allows the connection to be made
+       through an intermediary listening on a different port, such as an
+       <acronym>SSH</acronym> tunnel or a connection proxy, while the other
+       connection parameters continue to describe the server itself.  In
+       particular, the port used to identify the connection in a password file
+       (see <xref linkend="libpq-pgpass"/>) is <literal>port</literal>, not
+       <literal>portaddr</literal>, so a password file entry written for the
+       server's own port keeps matching even when the intermediary listens on
+       a port that is not known in advance.  The following rules are used:
+        <itemizedlist>
+         <listitem>
+          <para>
+           If <literal>port</literal> is specified
+           without <literal>portaddr</literal>, the connection is made to
+           <literal>port</literal>, which also identifies the connection.
+          </para>
+         </listitem>
+         <listitem>
+          <para>
+           If <literal>portaddr</literal> is specified
+           without <literal>port</literal>, the value
+           for <literal>portaddr</literal> gives the port to connect to, and
+           the default port number identifies the connection.
+          </para>
+         </listitem>
+         <listitem>
+          <para>
+           If both <literal>port</literal> and <literal>portaddr</literal> are
+           specified, the value for <literal>portaddr</literal> gives the port
+           to connect to.  The value for <literal>port</literal> is used only
+           to identify the connection.
+          </para>
+         </listitem>
+        </itemizedlist>
+       Note that authentication is likely to fail if <literal>port</literal>
+       is not the port of the server reached
+       at <literal>portaddr</literal>, since the password file entry that is
+       selected will then be the wrong one.
+      </para>
+
+      <para>
+       A comma-separated list of <literal>portaddr</literal> values is also
+       accepted, in which case it must have the same length as the host list,
+       or it may specify a single value to be used for all hosts.  An empty
+       item in the list causes the corresponding <literal>port</literal> value
+       to be used.  See <xref linkend="libpq-multiple-hosts"/> for details.
+      </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-dbname" xreflabel="dbname">
       <term><literal>dbname</literal></term>
       <listitem>
@@ -2766,8 +2835,12 @@ char *PQport(const PGconn *conn);
       </para>
 
       <para>
-       If multiple ports were specified in the connection parameters,
-       <xref linkend="libpq-PQport"/> returns the port actually connected to.
+       If the connection parameters specified both <literal>port</literal> and
+       <literal>portaddr</literal>, then <xref linkend="libpq-PQport"/> will
+       return the <literal>port</literal> information.  If multiple ports were
+       specified in the connection parameters,
+       <xref linkend="libpq-PQport"/> returns the port of the host actually
+       connected to.
       </para>
 
       <para>
@@ -9107,6 +9180,18 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGPORTADDR</envar></primary>
+      </indexterm>
+      <envar>PGPORTADDR</envar> behaves the same as the <xref
+      linkend="libpq-connect-portaddr"/> connection parameter.
+      This can be set instead of or in addition to <envar>PGPORT</envar>
+      to connect through an intermediary listening on another port.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
@@ -9580,6 +9665,10 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
    the connection is a Unix-domain socket connection and
    the <literal>host</literal> parameter
    matches <application>libpq</application>'s default socket directory path.
+   The port field is matched to the <literal>port</literal> connection
+   parameter, never to the <literal>portaddr</literal> parameter; this way an
+   entry written for the server's own port keeps matching when the connection
+   is made through an intermediary listening on another port.
    In a standby server, a database field of <literal>replication</literal>
    matches streaming replication connections made to the primary server.
    The database field is of limited usefulness otherwise, because users have
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 3ec0a3c3b34..edf00ed7e99 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -990,9 +990,13 @@ INSERT INTO tbls1 VALUES ($1, $2) \parse stmt1
         exception is that if the <replaceable>host</replaceable> setting
         is changed from its previous value using the positional syntax,
         any <replaceable>hostaddr</replaceable> setting present in the
-        existing connection's parameters is dropped.
+        existing connection's parameters is dropped; likewise, if either the
+        <replaceable>host</replaceable> or the <replaceable>port</replaceable>
+        setting is changed, any <replaceable>portaddr</replaceable> setting is
+        dropped.
         Also, any password used for the existing connection will be re-used
-        only if the user, host, and port settings are not changed.
+        only if the user, host, hostaddr, port, and portaddr settings are not
+        changed.
         When the command neither specifies nor reuses a particular parameter,
         the <application>libpq</application> default is used.
         </para>
diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml
index c74941bfbf2..b6f4a2e0f40 100644
--- a/doc/src/sgml/regress.sgml
+++ b/doc/src/sgml/regress.sgml
@@ -343,6 +343,17 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
      </listitem>
     </varlistentry>
 
+    <varlistentry>
+     <term><literal>portaddr</literal></term>
+     <listitem>
+      <para>
+       Runs the test <filename>src/interfaces/libpq/t/007_portaddr.pl</filename>.
+       The <literal>portaddr</literal> connection parameter only applies to
+       TCP connections, so this test opens a TCP/IP listen socket.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>regress_dump_restore</literal></term>
      <listitem>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index ee85c05a00d..7f4090e0d8d 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -3920,6 +3920,7 @@ do_connect(enum trivalue reuse_previous_specification,
 	PQconninfoOption *cinfo;
 	int			nconnopts = 0;
 	bool		same_host = false;
+	bool		same_port = false;
 	char	   *password = NULL;
 	char	   *client_encoding;
 	bool		success = true;
@@ -4011,8 +4012,8 @@ do_connect(enum trivalue reuse_previous_specification,
 						/*
 						 * Check whether connstring provides options affecting
 						 * password re-use.  While any change in user, host,
-						 * hostaddr, or port causes us to ignore the old
-						 * connection's password, we don't force that for
+						 * hostaddr, port, or portaddr causes us to ignore
+						 * the old connection's password, we don't force that for
 						 * dbname, since passwords aren't database-specific.
 						 */
 						if (replci->val == NULL ||
@@ -4021,7 +4022,8 @@ do_connect(enum trivalue reuse_previous_specification,
 							if (strcmp(replci->keyword, "user") == 0 ||
 								strcmp(replci->keyword, "host") == 0 ||
 								strcmp(replci->keyword, "hostaddr") == 0 ||
-								strcmp(replci->keyword, "port") == 0)
+								strcmp(replci->keyword, "port") == 0 ||
+								strcmp(replci->keyword, "portaddr") == 0)
 								keep_password = false;
 						}
 						/* Also note whether connstring contains a password. */
@@ -4088,7 +4090,7 @@ do_connect(enum trivalue reuse_previous_specification,
 			 * management issues: PQconninfoFree would misbehave on Windows.)
 			 * However, to avoid dependencies on the order in which parameters
 			 * appear in the array, make a preliminary scan to set
-			 * keep_password and same_host correctly.
+			 * keep_password, same_host and same_port correctly.
 			 *
 			 * While any change in user, host, or port causes us to ignore the
 			 * old connection's password, we don't force that for dbname,
@@ -4112,7 +4114,9 @@ do_connect(enum trivalue reuse_previous_specification,
 				}
 				else if (port && strcmp(ci->keyword, "port") == 0)
 				{
-					if (!(ci->val && strcmp(port, ci->val) == 0))
+					if (ci->val && strcmp(port, ci->val) == 0)
+						same_port = true;
+					else
 						keep_password = false;
 				}
 			}
@@ -4198,6 +4202,16 @@ do_connect(enum trivalue reuse_previous_specification,
 			}
 			else if (port && strcmp(ci->keyword, "port") == 0)
 				values[paramnum++] = port;
+			else if (((host && !same_host) || (port && !same_port)) &&
+					 strcmp(ci->keyword, "portaddr") == 0)
+			{
+				/*
+				 * An old portaddr describes where to reach a particular
+				 * server, so drop it if either the host or the port value is
+				 * changing.
+				 */
+				values[paramnum++] = NULL;
+			}
 			/* If !keep_password, we unconditionally drop old password */
 			else if ((password || !keep_password) &&
 					 strcmp(ci->keyword, "password") == 0)
diff --git a/src/interfaces/libpq/fe-cancel.c b/src/interfaces/libpq/fe-cancel.c
index 4b5945979c4..f9768a9c1c2 100644
--- a/src/interfaces/libpq/fe-cancel.c
+++ b/src/interfaces/libpq/fe-cancel.c
@@ -156,6 +156,12 @@ PQcancelCreate(PGconn *conn)
 		if (!cancelConn->connhost[0].port)
 			goto oom_error;
 	}
+	if (originalHost.portaddr)
+	{
+		cancelConn->connhost[0].portaddr = strdup(originalHost.portaddr);
+		if (!cancelConn->connhost[0].portaddr)
+			goto oom_error;
+	}
 	if (originalHost.password)
 	{
 		cancelConn->connhost[0].password = strdup(originalHost.password);
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 17c2288e9bc..69c38c6a367 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -243,6 +243,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Database-Port", "", 6,
 	offsetof(struct pg_conn, pgport)},
 
+	{"portaddr", "PGPORTADDR", NULL, NULL,
+		"Database-Port-Address", "", 6,
+	offsetof(struct pg_conn, pgportaddr)},
+
 	{"client_encoding", "PGCLIENTENCODING", NULL, NULL,
 		"Client-Encoding", "", 10,
 	offsetof(struct pg_conn, client_encoding_initial)},
@@ -1403,6 +1407,48 @@ pqConnectOptions2(PGconn *conn)
 		}
 	}
 
+	/*
+	 * Next, work out the port number to actually connect to for each host
+	 * name, if portaddr was given.  As for port above, these fields may be
+	 * left null or empty; we will use the corresponding port field whenever
+	 * we read such a portaddr field.
+	 */
+	if (conn->pgportaddr != NULL && conn->pgportaddr[0] != '\0')
+	{
+		int			i;
+		char	   *s = conn->pgportaddr;
+		bool		more = true;
+
+		for (i = 0; i < conn->nconnhost && more; i++)
+		{
+			conn->connhost[i].portaddr = parse_comma_separated_list(&s, &more);
+			if (conn->connhost[i].portaddr == NULL)
+				goto oom_error;
+		}
+
+		/*
+		 * If exactly one portaddr was given, use it for every host.
+		 * Otherwise, there must be exactly as many portaddrs as there were
+		 * hosts.
+		 */
+		if (i == 1 && !more)
+		{
+			for (i = 1; i < conn->nconnhost; i++)
+			{
+				conn->connhost[i].portaddr = strdup(conn->connhost[0].portaddr);
+				if (conn->connhost[i].portaddr == NULL)
+					goto oom_error;
+			}
+		}
+		else if (more || i != conn->nconnhost)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "could not match %d portaddr values to %d hosts",
+									count_comma_separated_elems(conn->pgportaddr), conn->nconnhost);
+			return false;
+		}
+	}
+
 	/*
 	 * If user name was not given, fetch it.  (Most likely, the fetch will
 	 * fail, since the only way we get here is if pg_fe_getauthname() failed
@@ -1459,7 +1505,10 @@ pqConnectOptions2(PGconn *conn)
 				/*
 				 * Try to get a password for this host from file.  We use host
 				 * for the hostname search key if given, else hostaddr (at
-				 * least one of them is guaranteed nonempty by now).
+				 * least one of them is guaranteed nonempty by now).  Likewise,
+				 * the port search key is always port, never portaddr: the
+				 * search keys identify the server we mean to reach, not the
+				 * address we happen to reach it at.
 				 */
 				const char *pwhost = conn->connhost[i].host;
 				const char *password_errmsg = NULL;
@@ -2448,7 +2497,9 @@ emitHostIdentityInfo(PGconn *conn, const char *host_addr)
 			displayed_host = conn->connhost[conn->whichhost].hostaddr;
 		else
 			displayed_host = conn->connhost[conn->whichhost].host;
-		displayed_port = conn->connhost[conn->whichhost].port;
+		displayed_port = conn->connhost[conn->whichhost].portaddr;
+		if (displayed_port == NULL || displayed_port[0] == '\0')
+			displayed_port = conn->connhost[conn->whichhost].port;
 		if (displayed_port == NULL || displayed_port[0] == '\0')
 			displayed_port = DEF_PGPORT_STR;
 
@@ -3069,6 +3120,25 @@ keep_going:						/* We will come back to here until there is
 				goto keep_going;
 			}
 		}
+
+		/*
+		 * If portaddr was given, that is the port we actually connect to, and
+		 * port only serves to identify the server, just as host does when
+		 * hostaddr is given.  portaddr is ignored for Unix-domain socket
+		 * connections, which are named by port.
+		 */
+		if (ch->type != CHT_UNIX_SOCKET &&
+			ch->portaddr != NULL && ch->portaddr[0] != '\0')
+		{
+			if (!pqParseIntParam(ch->portaddr, &thisport, conn, "portaddr"))
+				goto error_return;
+
+			if (thisport < 1 || thisport > 65535)
+			{
+				libpq_append_conn_error(conn, "invalid port number: \"%s\"", ch->portaddr);
+				goto keep_going;
+			}
+		}
 		snprintf(portstr, sizeof(portstr), "%d", thisport);
 
 		/* Use pg_getaddrinfo_all() to resolve the address */
@@ -5110,6 +5180,7 @@ freePGconn(PGconn *conn)
 	free(conn->pghost);
 	free(conn->pghostaddr);
 	free(conn->pgport);
+	free(conn->pgportaddr);
 	free(conn->connect_timeout);
 	free(conn->pgtcp_user_timeout);
 	free(conn->client_encoding_initial);
@@ -5201,6 +5272,7 @@ pqReleaseConnHosts(PGconn *conn)
 			free(conn->connhost[i].host);
 			free(conn->connhost[i].hostaddr);
 			free(conn->connhost[i].port);
+			free(conn->connhost[i].portaddr);
 			if (conn->connhost[i].password != NULL)
 			{
 				explicit_bzero(conn->connhost[i].password,
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 3f921207a14..3ccb063d453 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -361,6 +361,8 @@ typedef struct pg_conn_host
 	char	   *hostaddr;		/* host numeric IP address */
 	char	   *port;			/* port number (if NULL or empty, use
 								 * DEF_PGPORT[_STR]) */
+	char	   *portaddr;		/* port number actually connected to, for TCP
+								 * connections (if NULL or empty, use port) */
 	char	   *password;		/* password for this host, read from the
 								 * password file; NULL if not sought or not
 								 * found in password file. */
@@ -383,6 +385,10 @@ struct pg_conn
 								 * precedence over pghost. */
 	char	   *pgport;			/* the server's communication port number, or
 								 * a comma-separated list of ports */
+	char	   *pgportaddr;		/* the port number to connect to, or a
+								 * comma-separated list of same.  Takes
+								 * precedence over pgport, except for
+								 * Unix-domain socket connections. */
 	char	   *connect_timeout;	/* connection timeout (numeric string) */
 	char	   *pgtcp_user_timeout; /* tcp user timeout (numeric string) */
 	char	   *client_encoding_initial;	/* encoding to use */
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index b0ae72167a1..9087d2972ec 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -161,6 +161,7 @@ tests += {
       't/004_load_balance_dns.pl',
       't/005_negotiate_encryption.pl',
       't/006_service.pl',
+      't/007_portaddr.pl',
     ],
     'env': {
       'with_ssl': ssl_library,
diff --git a/src/interfaces/libpq/t/001_uri.pl b/src/interfaces/libpq/t/001_uri.pl
index 64f257ae046..613b03e6aec 100644
--- a/src/interfaces/libpq/t/001_uri.pl
+++ b/src/interfaces/libpq/t/001_uri.pl
@@ -56,6 +56,12 @@ my @tests = (
 		q{host='example.com' hostaddr='63.1.2.4' (inet)},
 		q{},
 	],
+	[
+		q{postgresql://host:12345/db?portaddr=6000},
+		q{dbname='db' host='host' port='12345' portaddr='6000' (inet)},
+		q{},
+	],
+	[ q{postgresql://?portaddr=6000}, q{portaddr='6000' (local)}, q{}, ],
 	[ q{postgresql://%68ost/}, q{host='host' (inet)}, q{}, ],
 	[
 		q{postgresql://host/db?user=uri-user},
diff --git a/src/interfaces/libpq/t/007_portaddr.pl b/src/interfaces/libpq/t/007_portaddr.pl
new file mode 100644
index 00000000000..5ecb573d8a7
--- /dev/null
+++ b/src/interfaces/libpq/t/007_portaddr.pl
@@ -0,0 +1,166 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# This tests the portaddr connection parameter, which separates the port that
+# libpq actually connects to from the port that identifies the server.
+#
+# portaddr only applies to TCP connections, so the server has to listen on a
+# TCP port here, which is why this test is not enabled by default.
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bportaddr\b/)
+{
+	plan skip_all =>
+	  'Potentially unsafe test portaddr not enabled in PG_TEST_EXTRA';
+}
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->append_conf('postgresql.conf', "listen_addresses = '127.0.0.1'");
+$node->start;
+
+# The port the server really listens on, plus a port that nothing listens on.
+# Reaching the server while naming the latter is only possible via portaddr.
+my $realport = $node->port;
+my $unusedport = PostgreSQL::Test::Cluster::get_free_port();
+
+# Sanity check: without portaddr, the unused port is unreachable.
+$node->connect_fails(
+	"host=127.0.0.1 port=$unusedport",
+	"connection to an unused port fails without portaddr",
+	expected_stderr =>
+	  qr/connection to server at "127\.0\.0\.1", port $unusedport failed/);
+
+# portaddr determines the port we connect to ...
+$node->connect_ok(
+	"host=127.0.0.1 port=$unusedport portaddr=$realport",
+	"portaddr determines the port connected to",
+	sql => "SELECT 'connected'",
+	expected_stdout => qr/^connected$/);
+
+# ... while port still identifies the connection, as reported by PQport.
+$node->connect_ok(
+	"host=127.0.0.1 port=$unusedport portaddr=$realport",
+	"PQport reports port, not portaddr",
+	sql => "\\echo :PORT",
+	expected_stdout => qr/^$unusedport$/);
+
+# An empty portaddr means "connect to port", the historical behavior.
+$node->connect_ok(
+	"host=127.0.0.1 port=$realport portaddr=",
+	"empty portaddr falls back to port",
+	sql => "SELECT 'connected'",
+	expected_stdout => qr/^connected$/);
+
+# A connection failure reports the port we actually tried to reach.
+$node->connect_fails(
+	"host=127.0.0.1 port=$realport portaddr=$unusedport",
+	"connection failure reports the portaddr port",
+	expected_stderr =>
+	  qr/connection to server at "127\.0\.0\.1", port $unusedport failed/);
+
+# Invalid values are rejected the same way port is.
+$node->connect_fails(
+	"host=127.0.0.1 port=$realport portaddr=65536",
+	"portaddr must be a valid port number",
+	expected_stderr => qr/invalid port number: "65536"/);
+
+$node->connect_fails(
+	"host=127.0.0.1 port=$realport portaddr=notanumber",
+	"portaddr must be an integer",
+	expected_stderr =>
+	  qr/invalid integer value "notanumber" for connection option "portaddr"/);
+
+# The portaddr list must match the host list, unless it has a single element.
+$node->connect_fails(
+	"host=127.0.0.1,127.0.0.1 port=$unusedport portaddr=$realport,$realport,$realport",
+	"portaddr list must match the host list",
+	expected_stderr => qr/could not match 3 portaddr values to 2 hosts/);
+
+# A single portaddr applies to every host.  The first host here is a socket
+# path that does not exist, so it fails and the second host is tried; that
+# second host can only be reached if the lone portaddr was copied to it.
+$node->connect_ok(
+	"host=/nonexistent,127.0.0.1 port=$unusedport portaddr=$realport",
+	"a single portaddr applies to all hosts",
+	sql => "SELECT 'connected'",
+	expected_stdout => qr/^connected$/);
+
+# An empty item in the list uses the corresponding port value.  The first host
+# is directed at the unused port and fails, so the second one is tried, and it
+# can only succeed by falling back to its port.
+$node->connect_ok(
+	"host=127.0.0.1,127.0.0.1 port=$unusedport,$realport portaddr=$unusedport,",
+	"an empty list item falls back to the corresponding port",
+	sql => "SELECT 'connected'",
+	expected_stdout => qr/^connected$/);
+
+# PGPORTADDR behaves the same as the parameter.
+{
+	local $ENV{PGPORTADDR} = $realport;
+
+	$node->connect_ok(
+		"host=127.0.0.1 port=$unusedport",
+		"PGPORTADDR environment variable is honored",
+		sql => "SELECT 'connected'",
+		expected_stdout => qr/^connected$/);
+}
+
+# portaddr is ignored for Unix-domain socket connections, which are named by
+# port; naming an unused port there must not change anything.
+if ($use_unix_sockets)
+{
+	$node->connect_ok(
+		$node->connstr('postgres') . " portaddr=$unusedport",
+		"portaddr is ignored for Unix-domain socket connections",
+		sql => "SELECT 'connected'",
+		expected_stdout => qr/^connected$/);
+}
+
+# The password file is searched using port, never portaddr.  This is the point
+# of the parameter: an entry written for the server's own port keeps matching
+# when the connection is made through an intermediary on another port.
+$node->safe_psql('postgres',
+	"CREATE ROLE portaddr_role LOGIN PASSWORD 'secret'");
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', "local all all trust");
+$node->append_conf('pg_hba.conf',
+	"host all portaddr_role 127.0.0.1/32 scram-sha-256");
+$node->append_conf('pg_hba.conf', "host all all 127.0.0.1/32 trust");
+$node->reload;
+
+my $pgpassfile = "${PostgreSQL::Test::Utils::tmp_check}/pgpass_portaddr";
+$ENV{PGPASSFILE} = $pgpassfile;
+
+# An entry keyed to the port that identifies the server matches, even though
+# the connection is actually made to a different port.
+unlink($pgpassfile);
+append_to_file($pgpassfile,
+	"127.0.0.1:$unusedport:postgres:portaddr_role:secret\n");
+chmod 0600, $pgpassfile or die;
+
+$node->connect_ok(
+	"host=127.0.0.1 port=$unusedport portaddr=$realport user=portaddr_role",
+	"password file is searched using port",
+	sql => "SELECT 'authenticated'",
+	expected_stdout => qr/^authenticated$/);
+
+# Conversely, an entry keyed to the port actually connected to does not match.
+unlink($pgpassfile);
+append_to_file($pgpassfile,
+	"127.0.0.1:$realport:postgres:portaddr_role:secret\n");
+chmod 0600, $pgpassfile or die;
+
+$node->connect_fails(
+	"host=127.0.0.1 port=$unusedport portaddr=$realport user=portaddr_role",
+	"password file is not searched using portaddr",
+	expected_stderr => qr/no password supplied/);
+
+unlink($pgpassfile);
+delete $ENV{PGPASSFILE};
+
+done_testing();
diff --git a/src/test/perl/PostgreSQL/Test/Utils.pm b/src/test/perl/PostgreSQL/Test/Utils.pm
index d3e6abf7a68..0e98a15a9f1 100644
--- a/src/test/perl/PostgreSQL/Test/Utils.pm
+++ b/src/test/perl/PostgreSQL/Test/Utils.pm
@@ -134,6 +134,7 @@ BEGIN
 	  PGKRBSRVNAME
 	  PGPASSFILE
 	  PGPASSWORD
+	  PGPORTADDR
 	  PGREQUIREPEER
 	  PGREQUIRESSL
 	  PGSERVICE
diff --git a/src/test/regress/pg_regress.c b/src/test/regress/pg_regress.c
index 21d00f792d2..6a19b50825e 100644
--- a/src/test/regress/pg_regress.c
+++ b/src/test/regress/pg_regress.c
@@ -837,6 +837,7 @@ initialize_environment(void)
 		unsetenv("PGKRBSRVNAME");
 		unsetenv("PGPASSFILE");
 		unsetenv("PGPASSWORD");
+		unsetenv("PGPORTADDR");
 		unsetenv("PGREQUIREPEER");
 		unsetenv("PGREQUIRESSL");
 		unsetenv("PGSERVICE");
@@ -893,6 +894,7 @@ initialize_environment(void)
 
 			snprintf(s, sizeof(s), "%d", port);
 			setenv("PGPORT", s, 1);
+			unsetenv("PGPORTADDR");
 		}
 		if (user != NULL)
 			setenv("PGUSER", user, 1);
-- 
2.43.0

