Hello.

It is necessary to restart PostgreSQL to bind to a different network
interface, thus breaking the active connections. This patch draft
makes it no longer necessary, just assign the new value to
`listen_addresses` GUC and send SIGHUP to Postmaster.

Do you think it is useful? Is there any chance to push it upstream?
Overall thoughts?
From 97b35aa15902a9b055475fdfb016547096a43667 Mon Sep 17 00:00:00 2001
From: Ivan Kovmir <[email protected]>
Date: Mon, 29 Sep 2025 15:53:00 +0200
Subject: [PATCH 1/1] Change bound network interfaces without restart

Assign a different GUC `listen_addresses` value and send
SIGHUP to Postmaster to change bound network interface without
restarting the server, thus breaking currently held connections.

```
alter system set listen_addresses='SOME.OTHER.IP.ADDRESS';
select pg_reload_conf();
```

Multiple comma-separated addresses as well as `*` are possible.
---
 src/backend/postmaster/postmaster.c       | 188 ++++++++++++++++++++++
 src/backend/utils/misc/guc_parameters.dat |   3 +-
 src/include/utils/guc_hooks.h             |   1 +
 3 files changed, 191 insertions(+), 1 deletion(-)

diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index e1d643b013d..fa0d45b844f 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -72,6 +72,7 @@
 #include <ctype.h>
 #include <sys/stat.h>
 #include <sys/socket.h>
+#include <arpa/inet.h>
 #include <fcntl.h>
 #include <sys/param.h>
 #include <netdb.h>
@@ -94,6 +95,7 @@
 #include "access/xlogrecovery.h"
 #include "common/file_perm.h"
 #include "common/pg_prng.h"
+#include "common/ip.h"
 #include "lib/ilist.h"
 #include "libpq/libpq.h"
 #include "libpq/pqsignal.h"
@@ -122,6 +124,7 @@
 #include "utils/pidfile.h"
 #include "utils/timestamp.h"
 #include "utils/varlena.h"
+#include "utils/guc_hooks.h"

 #ifdef EXEC_BACKEND
 #include "common/file_utils.h"
@@ -418,6 +421,7 @@ static void CloseServerPorts(int status, Datum arg);
 static void unlink_external_pid_file(int status, Datum arg);
 static void getInstallationPaths(const char *argv0);
 static void checkControlFile(void);
+static void ConfigurePostmasterWaitSet(bool accept_connections);
 static void handle_pm_pmsignal_signal(SIGNAL_ARGS);
 static void handle_pm_child_exit_signal(SIGNAL_ARGS);
 static void handle_pm_reload_request_signal(SIGNAL_ARGS);
@@ -1445,6 +1449,190 @@ CloseServerPorts(int status, Datum arg)
 	 */
 }

+/*
+ * Attempt bind to all the hostnames in *newval,
+ * then determine which ones should be closed and close them.
+ */
+void
+assign_listen_addresses(const char *newval, void *extra)
+{
+	int 		status;
+	int 		n_bound, n_closed;
+	char		*rawstring;
+	List		*elemlist;
+	ListCell	*l;
+
+	char portNumberStr[32];
+	int retval;
+	int i, j;
+	struct addrinfo *addrs = NULL, *addr;
+	struct addrinfo hint;
+	struct sockaddr_storage sock_addr;
+	socklen_t sock_addr_len = sizeof(sock_addr);
+	/* Marks which interfaces we should remain bound to. */
+	bool remain_bound[MAXLISTEN];
+
+	char ip_str[INET6_ADDRSTRLEN];
+
+	/* Is there a better test? */
+	if (ListenSockets == NULL)
+		return; /* Not postmaster process. */
+
+	/* Need a modifiable copy of ListenAddresses. */
+	rawstring = pstrdup(newval);
+
+	/* Parse the string into a list of hostnames. */
+	if (!SplitGUCList(rawstring, ',', &elemlist))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("invalid list syntax in parameter \"%s\"",
+						"listen_addresses")));
+	}
+	/* Bind to each hostname individually. */
+	n_bound = 0;
+	foreach(l, elemlist)
+	{
+		/* This foreach() is very similar to one within PostmasterMain().
+		 * Should it be taken out into a common function call?
+		 * I doubt it since the code snippets are not identical. */
+		char	   *curhost = (char *) lfirst(l);
+
+		if (strcmp(curhost, "*") == 0)
+		{
+			status = ListenServerPort(AF_UNSPEC, NULL,
+									  (unsigned short) PostPortNumber,
+									  NULL,
+									  ListenSockets,
+									  &NumListenSockets,
+									  MAXLISTEN);
+		}
+		else
+		{
+			status = ListenServerPort(AF_UNSPEC, curhost,
+									  (unsigned short) PostPortNumber,
+									  NULL,
+									  ListenSockets,
+									  &NumListenSockets,
+									  MAXLISTEN);
+		}
+
+		if (status == STATUS_OK)
+		{
+			n_bound++;
+			/* Should it be like that? The comment atop the function says
+			 * it may be dangerous to overwrite the string with a shorter
+			 * one. */
+			if (n_bound == 1)
+				AddToDataDirLockFile(LOCK_FILE_LINE_LISTEN_ADDR, curhost);
+		}
+	}
+
+	/* Done binding, now drop the no longer needed sockets... */
+
+	MemSet(&hint, 0, sizeof(hint));
+	hint.ai_family = AF_UNSPEC; /* Empty search condition for getaddrinfo().*/
+
+	for (i = 0; i < MAXLISTEN; i++)
+		remain_bound[i] = false; /* Reset. */
+
+	/* Should port number be mutable too? */
+	snprintf(portNumberStr, sizeof(portNumberStr), "%d", PostPortNumber);
+	/* The following foreach(){} determines which sockets are to be closed.
+	 * We resolve hosts twice: once to bind, and once to close them. Ugly? */
+	foreach(l, elemlist) /* Iterate through hostnames. */
+	{
+		char	   *curhost = (char *) lfirst(l);
+
+		retval = pg_getaddrinfo_all(curhost, portNumberStr, &hint, &addrs);
+		if (retval != 0)
+		{
+			ereport(ERROR,
+					(errmsg("could not translate host name \"%s\", service \"%s\" to address: %s",
+							curhost, portNumberStr, gai_strerror(retval))));
+		}
+
+		/* Iterate through resolved addresses. */
+		for (addr = addrs; addr; addr = addr->ai_next)
+		{
+			/* Iterate through bound addresses. */
+			for (i = 0; i < NumListenSockets; i++)
+			{
+				/* Is it portable? */
+				getsockname(ListenSockets[i], (struct sockaddr *)&sock_addr,
+							&sock_addr_len);
+
+				/* Ignore UNIX sockets. */
+				if (sock_addr.ss_family == AF_UNIX)
+				{
+					remain_bound[i] = true;
+					continue;
+				}
+
+				/* If whatever we are bound to matches with the requested
+				 * address/hostname, then stay bound to it. */
+				if ( ((struct sockaddr_in *)&sock_addr)->sin_addr.s_addr ==
+				     ((struct sockaddr_in *)addr->ai_addr)->sin_addr.s_addr )
+					remain_bound[i] = true;
+
+				/* IPv6 not handled? */
+			}
+		}
+
+		/* Needed or should be delegated to memory contexts? */
+		pg_freeaddrinfo_all(hint.ai_family, addrs);
+	}
+
+	/* Close whatever sockets should be closed.
+	 * There could be a better algorithm. */
+	n_closed = 0;
+	/* Iterate through and close... */
+	for (i = 0; i < NumListenSockets; i++)
+	{
+		if (remain_bound[i] == true)
+			continue;
+
+		///* Get human-readable IP address for the log entry. */
+		//if (getsockname(ListenSockets[i], (struct sockaddr*)&sock_addr, &sock_addr_len) == -1) {
+		//	ereport(ERROR, (errmsg("getsockname()")) );
+		//}
+		//
+		//if (sock_addr.ss_family == AF_INET) { /* IPv4 */
+		//	struct sockaddr_in *s = (struct sockaddr_in *)&addr;
+		//	inet_ntop(AF_INET, &s->sin_addr, ip_str, sizeof(ip_str));
+		//}
+		//else
+		//{ /* IPv6 */
+		//	struct sockaddr_in6 *s6 = (struct sockaddr_in6 *)&addr;
+		//	inet_ntop(AF_INET6, &s6->sin6_addr, ip_str, sizeof(ip_str));
+		//} /* We know we will not encounter UNIX sockets. */
+		///* Any IPv4 interface is 0.0.0.0 on my machine. Why? */
+		//ereport(LOG, (errmsg("closing %s", ip_str)) );
+
+		closesocket(ListenSockets[i]);
+		ListenSockets[i] = -1;
+		n_closed++;
+	}
+	/* Having closed the sockets we created empty slots in between the array
+	 * elements, get rid of them... */
+	j = 0;
+	for (i = 0; i < NumListenSockets; i++)
+	{
+		if (ListenSockets[i] != -1)
+			ListenSockets[j++] = ListenSockets[i];
+	}
+	NumListenSockets = j;
+
+	/* Resubscribe to socket updates. */
+	/* Is the if statement needed? */
+	if (n_bound > 0 || n_closed > 0)
+		ConfigurePostmasterWaitSet(true);
+
+	/* Should memory be handled by memory contexts? */
+	list_free(elemlist);
+	pfree(rawstring);
+}
+
 /*
  * on_proc_exit callback to delete external_pid_file
  */
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 6bc6be13d2a..e87cf690d4f 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2943,11 +2943,12 @@
   boot_val => 'DEFAULT_PGSOCKET_DIR',
 },

-{ name => 'listen_addresses', type => 'string', context => 'PGC_POSTMASTER', group => 'CONN_AUTH_SETTINGS',
+{ name => 'listen_addresses', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SETTINGS',
   short_desc => 'Sets the host name or IP address(es) to listen to.',
   flags => 'GUC_LIST_INPUT',
   variable => 'ListenAddresses',
   boot_val => '"localhost"',
+  assign_hook => 'assign_listen_addresses',
 },

 # Can't be set by ALTER SYSTEM as it can lead to recursive definition
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 82ac8646a8d..6b38d5c8e9c 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -81,6 +81,7 @@ extern bool check_log_stats(bool *newval, void **extra, GucSource source);
 extern bool check_log_timezone(char **newval, void **extra, GucSource source);
 extern void assign_log_timezone(const char *newval, void *extra);
 extern const char *show_log_timezone(void);
+extern void assign_listen_addresses(const char *newval, void *extra);
 extern void assign_maintenance_io_concurrency(int newval, void *extra);
 extern void assign_io_max_combine_limit(int newval, void *extra);
 extern void assign_io_combine_limit(int newval, void *extra);
--
2.51.0

Reply via email to