0001-Add-pg_hosts_file_rules-to-inspect-the-hosts-configu.patch

application/octet-stream

Filename: 0001-Add-pg_hosts_file_rules-to-inspect-the-hosts-configu.patch
Type: application/octet-stream
Part: 0
Message: pg_hosts: Add pg_hosts_file_rules()

Patch

Format: format-patch
Series: patch 0001
Subject: Add pg_hosts_file_rules() to inspect the hosts configuration file
File+
doc/src/sgml/system-views.sgml 148 0
src/backend/catalog/system_views.sql 5 0
src/backend/libpq/be-secure-common.c 11 3
src/backend/utils/adt/hbafuncs.c 158 0
src/include/catalog/pg_proc.dat 7 0
src/include/libpq/hba.h 1 0
src/test/regress/expected/rules.out 11 0
src/test/ssl/t/004_sni.pl 18 0
From 5db9a6bb286db74f85c6869d04d9db12f9836cce Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <zsolt.parragi@percona.com>
Date: Wed, 17 Jun 2026 12:23:50 +0000
Subject: [PATCH] Add pg_hosts_file_rules() to inspect the hosts configuration
 file

---
 doc/src/sgml/system-views.sgml       | 148 +++++++++++++++++++++++++
 src/backend/catalog/system_views.sql |   5 +
 src/backend/libpq/be-secure-common.c |  14 ++-
 src/backend/utils/adt/hbafuncs.c     | 158 +++++++++++++++++++++++++++
 src/include/catalog/pg_proc.dat      |   7 ++
 src/include/libpq/hba.h              |   1 +
 src/test/regress/expected/rules.out  |  11 ++
 src/test/ssl/t/004_sni.pl            |  18 +++
 8 files changed, 359 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 2ebec6928d5..e71c2e5edb5 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -101,6 +101,11 @@
       <entry>summary of client authentication configuration file contents</entry>
      </row>
 
+     <row>
+      <entry><link linkend="view-pg-hosts-file-rules"><structname>pg_hosts_file_rules</structname></link></entry>
+      <entry>summary of hosts configuration file contents</entry>
+     </row>
+
      <row>
       <entry><link linkend="view-pg-ident-file-mappings"><structname>pg_ident_file_mappings</structname></link></entry>
       <entry>summary of client user name mapping configuration file contents</entry>
@@ -1558,6 +1563,149 @@ AND c1.path[c2.level] = c2.path[c2.level];
   </para>
  </sect1>
 
+ <sect1 id="view-pg-hosts-file-rules">
+  <title><structname>pg_hosts_file_rules</structname></title>
+
+  <indexterm zone="view-pg-hosts-file-rules">
+   <primary>pg_hosts_file_rules</primary>
+  </indexterm>
+
+  <para>
+   The view <structname>pg_hosts_file_rules</structname> provides a summary of
+   the contents of the hosts configuration file, which maps incoming TLS
+   server name indication (SNI) host names to SSL certificate configuration.
+   A row appears in this view for each entry in the file, with annotations
+   indicating whether the entry could be parsed successfully.
+  </para>
+
+  <para>
+   This view can be helpful for checking whether planned changes in the
+   hosts configuration file will work, or for diagnosing a previous failure.
+   Note that this view reports on the <emphasis>current</emphasis> contents
+   of the file, not on what was last loaded by the server.
+  </para>
+
+  <para>
+   By default, the <structname>pg_hosts_file_rules</structname> view can be
+   read only by superusers.
+  </para>
+
+  <table>
+   <title><structname>pg_hosts_file_rules</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rule_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Number of this rule, if valid, otherwise <literal>NULL</literal>
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this rule
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>line_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Line number of this rule in <literal>file_name</literal>
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>hostnames</structfield> <type>text[]</type>
+      </para>
+      <para>
+       List of host name(s) to which this rule applies
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ssl_certificate</structfield> <type>text</type>
+      </para>
+      <para>
+       Path to the SSL certificate file used for these host names
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ssl_key</structfield> <type>text</type>
+      </para>
+      <para>
+       Path to the SSL private key file used for these host names
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ssl_ca</structfield> <type>text</type>
+      </para>
+      <para>
+       Path to the SSL certificate authority file, or null if not specified
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>passphrase_command</structfield> <type>text</type>
+      </para>
+      <para>
+       Command used to obtain the private key passphrase, or null if not
+       specified
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>passphrase_command_reload</structfield> <type>bool</type>
+      </para>
+      <para>
+       Whether the passphrase command is run on configuration reload
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>error</structfield> <type>text</type>
+      </para>
+      <para>
+       If not null, an error message indicating why this
+       entry could not be processed
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+
+  <para>
+   Usually, a row reflecting an incorrect entry will have values for only
+   the <structfield>line_number</structfield> and <structfield>error</structfield> fields.
+  </para>
+ </sect1>
+
  <sect1 id="view-pg-ident-file-mappings">
   <title><structname>pg_ident_file_mappings</structname></title>
 
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 8f129baec90..983a2537516 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -667,6 +667,11 @@ CREATE VIEW pg_hba_file_rules AS
 
 REVOKE ALL ON pg_hba_file_rules FROM PUBLIC;
 
+CREATE VIEW pg_hosts_file_rules AS
+   SELECT * FROM pg_hosts_file_rules() AS A;
+
+REVOKE ALL ON pg_hosts_file_rules FROM PUBLIC;
+
 CREATE VIEW pg_ident_file_mappings AS
    SELECT * FROM pg_ident_file_mappings() AS A;
 
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index 6ec887b8a47..9de816c0d9e 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -29,8 +29,6 @@
 #include "utils/builtins.h"
 #include "utils/guc.h"
 
-static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
-
 /*
  * Run ssl_passphrase_command
  *
@@ -191,10 +189,11 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
  * the TLS backend. Validation of the parsed values is left for the TLS backend
  * to implement.
  */
-static HostsLine *
+HostsLine *
 parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	HostsLine  *parsedline;
+	char	  **err_msg = &tok_line->err_msg;
 	List	   *tokens;
 	ListCell   *field;
 	AuthToken  *token;
@@ -217,6 +216,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
 		if ((tokens->length > 1) &&
 			(strcmp(hostname->string, "*") == 0 || strcmp(hostname->string, "/no_sni/") == 0))
 		{
+			*err_msg = "default and non-SNI entries cannot be mixed with other entries";
 			ereport(elevel,
 					errcode(ERRCODE_CONFIG_FILE_ERROR),
 					errmsg("default and non-SNI entries cannot be mixed with other entries"),
@@ -232,6 +232,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
 	field = lnext(tok_line->fields, field);
 	if (!field)
 	{
+		*err_msg = "missing entry at end of line";
 		ereport(elevel,
 				errcode(ERRCODE_CONFIG_FILE_ERROR),
 				errmsg("missing entry at end of line"),
@@ -242,6 +243,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
 	tokens = lfirst(field);
 	if (tokens->length > 1)
 	{
+		*err_msg = "multiple values specified for SSL certificate";
 		ereport(elevel,
 				errcode(ERRCODE_CONFIG_FILE_ERROR),
 				errmsg("multiple values specified for SSL certificate"),
@@ -256,6 +258,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
 	field = lnext(tok_line->fields, field);
 	if (!field)
 	{
+		*err_msg = "missing entry at end of line";
 		ereport(elevel,
 				errcode(ERRCODE_CONFIG_FILE_ERROR),
 				errmsg("missing entry at end of line"),
@@ -266,6 +269,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
 	tokens = lfirst(field);
 	if (tokens->length > 1)
 	{
+		*err_msg = "multiple values specified for SSL key";
 		ereport(elevel,
 				errcode(ERRCODE_CONFIG_FILE_ERROR),
 				errmsg("multiple values specified for SSL key"),
@@ -283,6 +287,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
 	tokens = lfirst(field);
 	if (tokens->length > 1)
 	{
+		*err_msg = "multiple values specified for SSL CA";
 		ereport(elevel,
 				errcode(ERRCODE_CONFIG_FILE_ERROR),
 				errmsg("multiple values specified for SSL CA"),
@@ -300,6 +305,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
 		tokens = lfirst(field);
 		if (tokens->length > 1)
 		{
+			*err_msg = "multiple values specified for SSL passphrase command";
 			ereport(elevel,
 					errcode(ERRCODE_CONFIG_FILE_ERROR),
 					errmsg("multiple values specified for SSL passphrase command"),
@@ -328,6 +334,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
 			 */
 			if (lnext(tok_line->fields, field))
 			{
+				*err_msg = "extra fields at end of line";
 				ereport(elevel,
 						errcode(ERRCODE_CONFIG_FILE_ERROR),
 						errmsg("extra fields at end of line"),
@@ -338,6 +345,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
 
 			if (tokens->length > 1 || !parse_bool(token->string, &parsedline->ssl_passphrase_reload))
 			{
+				*err_msg = "incorrect syntax for boolean value SSL_passphrase_cmd_reload";
 				ereport(elevel,
 						errcode(ERRCODE_CONFIG_FILE_ERROR),
 						errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index bd23eda3f79..b17d2b5bc12 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -30,6 +30,10 @@ static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 						  int rule_number, char *filename, int lineno,
 						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
+static void fill_hosts_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+							int rule_number, const char *filename, int lineno,
+							HostsLine *host, const char *err_msg);
+static void fill_hosts_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 							int map_number, char *filename, int lineno,
 							IdentLine *ident, const char *err_msg);
@@ -448,6 +452,160 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 	PG_RETURN_NULL();
 }
 
+/* Number of columns in pg_hosts_file_rules view */
+#define NUM_PG_HOSTS_FILE_RULES_ATTS	 10
+
+/*
+ * fill_hosts_line
+ *		Build one row of pg_hosts_file_rules view, add it to tuplestore.
+ *
+ * rule_number: unique identifier among valid rules (ignored when err_msg set)
+ * filename, lineno: always valid
+ * host: parsed entry (NULL when err_msg is set)
+ * err_msg: error message (NULL if none)
+ */
+static void
+fill_hosts_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+				int rule_number, const char *filename, int lineno,
+				HostsLine *host, const char *err_msg)
+{
+	Datum		values[NUM_PG_HOSTS_FILE_RULES_ATTS];
+	bool		nulls[NUM_PG_HOSTS_FILE_RULES_ATTS];
+	HeapTuple	tuple;
+	int			index;
+
+	Assert(tupdesc->natts == NUM_PG_HOSTS_FILE_RULES_ATTS);
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, 0, sizeof(nulls));
+	index = 0;
+
+	/* rule_number, nothing on error */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(rule_number);
+
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
+
+	/* line_number */
+	values[index++] = Int32GetDatum(lineno);
+
+	if (host != NULL)
+	{
+		/* hostnames text[] */
+		if (host->hostnames)
+			values[index++] = PointerGetDatum(strlist_to_textarray(host->hostnames));
+		else
+			nulls[index++] = true;
+
+		/* ssl_certificate */
+		if (host->ssl_cert)
+			values[index++] = CStringGetTextDatum(host->ssl_cert);
+		else
+			nulls[index++] = true;
+
+		/* ssl_key */
+		if (host->ssl_key)
+			values[index++] = CStringGetTextDatum(host->ssl_key);
+		else
+			nulls[index++] = true;
+
+		/* ssl_ca */
+		if (host->ssl_ca)
+			values[index++] = CStringGetTextDatum(host->ssl_ca);
+		else
+			nulls[index++] = true;
+
+		/* passphrase_command */
+		if (host->ssl_passphrase_cmd)
+			values[index++] = CStringGetTextDatum(host->ssl_passphrase_cmd);
+		else
+			nulls[index++] = true;
+
+		/* passphrase_command_reload */
+		values[index++] = BoolGetDatum(host->ssl_passphrase_reload);
+	}
+	else
+	{
+		/* no parse result: null the per-host columns (indexes 3..8) */
+		memset(&nulls[3], true, (NUM_PG_HOSTS_FILE_RULES_ATTS - 4) * sizeof(bool));
+	}
+
+	/* error */
+	if (err_msg)
+		values[NUM_PG_HOSTS_FILE_RULES_ATTS - 1] = CStringGetTextDatum(err_msg);
+	else
+		nulls[NUM_PG_HOSTS_FILE_RULES_ATTS - 1] = true;
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	tuplestore_puttuple(tuple_store, tuple);
+}
+
+/*
+ * fill_hosts_view
+ *		Read the pg_hosts file and fill the tuplestore with view records.
+ */
+static void
+fill_hosts_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
+{
+	FILE	   *file;
+	List	   *hosts_lines = NIL;
+	ListCell   *line;
+	int			rule_number = 0;
+	MemoryContext hostscxt;
+	MemoryContext oldcxt;
+
+	file = open_auth_file(HostsFileName, DEBUG3, 0, NULL);
+	if (file == NULL)
+		return;
+
+	tokenize_auth_file(HostsFileName, file, &hosts_lines, DEBUG3, 0);
+
+	hostscxt = AllocSetContextCreate(CurrentMemoryContext,
+									 "hosts parser context",
+									 ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(hostscxt);
+	foreach(line, hosts_lines)
+	{
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+		HostsLine  *hostsline = NULL;
+
+		if (tok_line->err_msg == NULL)
+			hostsline = parse_hosts_line(tok_line, DEBUG3);
+
+		if (tok_line->err_msg == NULL)
+			rule_number++;
+
+		fill_hosts_line(tuple_store, tupdesc, rule_number,
+						tok_line->file_name, tok_line->line_num, hostsline,
+						tok_line->err_msg);
+	}
+
+	free_auth_file(file, 0);
+	MemoryContextSwitchTo(oldcxt);
+	MemoryContextDelete(hostscxt);
+}
+
+/*
+ * pg_hosts_file_rules
+ *
+ * SQL-accessible SRF returning all entries in the pg_hosts configuration file.
+ */
+Datum
+pg_hosts_file_rules(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo *rsi;
+
+	InitMaterializedSRF(fcinfo, 0);
+
+	rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+	fill_hosts_view(rsi->setResult, rsi->setDesc);
+
+	PG_RETURN_NULL();
+}
+
 /* Number of columns in pg_ident_file_mappings view */
 #define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..37c9020e92d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6605,6 +6605,13 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules', proacl => '{POSTGRES=X}' },
+{ oid => '8801', descr => 'show pg_hosts configuration file rules',
+  proname => 'pg_hosts_file_rules', prorows => '1000', proretset => 't',
+  provolatile => 'v', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{int4,text,int4,_text,text,text,text,text,bool,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,hostnames,ssl_certificate,ssl_key,ssl_ca,passphrase_command,passphrase_command_reload,error}',
+  prosrc => 'pg_hosts_file_rules', proacl => '{POSTGRES=X}' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 4aa6258a345..c9fa8d8f1dc 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -194,6 +194,7 @@ extern int	check_usermap(const char *usermap_name,
 						  const char *pg_user, const char *system_user,
 						  bool case_insensitive);
 extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
+extern HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
 extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern FILE *open_auth_file(const char *filename, int elevel, int depth,
 							char **err_msg);
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a65a5bf0c4f..0a392f8ec1f 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1373,6 +1373,17 @@ pg_hba_file_rules| SELECT rule_number,
     options,
     error
    FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_hosts_file_rules| SELECT rule_number,
+    file_name,
+    line_number,
+    hostnames,
+    ssl_certificate,
+    ssl_key,
+    ssl_ca,
+    passphrase_command,
+    passphrase_command_reload,
+    error
+   FROM pg_hosts_file_rules() a(rule_number, file_name, line_number, hostnames, ssl_certificate, ssl_key, ssl_ca, passphrase_command, passphrase_command_reload, error);
 pg_ident_file_mappings| SELECT map_number,
     file_name,
     line_number,
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
index f1a135bb3c8..e39dd4951c6 100644
--- a/src/test/ssl/t/004_sni.pl
+++ b/src/test/ssl/t/004_sni.pl
@@ -454,4 +454,22 @@ $result = $node->restart(fail_ok => 1);
 is($result, 0,
 	'pg_hosts.conf: restart fails with non-boolean value in boolean field');
 
+# pg_hosts_file_rules() reflects the parsed conf file.  Write a known-good
+# configuration so the SRF reports at least one valid entry.
+$node->append_conf('pg_hba.conf', "local all all trust");
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+	'example.org server-cn-only.crt server-cn-only.key root+client_ca.crt');
+$node->restart;
+my $rules = $node->safe_psql('postgres',
+	"SELECT count(*) FROM pg_hosts_file_rules() WHERE error IS NULL");
+ok($rules >= 1, "pg_hosts_file_rules: valid conf entries are reported");
+
+# A malformed entry shows up as an error row, not a thrown error.
+$node->append_conf('pg_hosts.conf', "this line has too many fields a b c d e f");
+$node->reload;
+my $errs = $node->safe_psql('postgres',
+	"SELECT count(*) FROM pg_hosts_file_rules() WHERE error IS NOT NULL");
+ok($errs >= 1, "pg_hosts_file_rules: malformed conf entry becomes an error row");
+
 done_testing();
-- 
2.43.0