public inbox for [email protected]help / color / mirror / Atom feed
Feature: Use DNS SRV records for connecting 8+ messages / 7 participants [nested] [flat]
* Feature: Use DNS SRV records for connecting @ 2019-08-13 09:50 Feike Steenbergen <[email protected]> 0 siblings, 2 replies; 8+ messages in thread From: Feike Steenbergen @ 2019-08-13 09:50 UTC (permalink / raw) To: pgsql-hackers Hi all, I'd like to get some feedback on whether or not implementing a DNS SRV feature for connecting to PostgreSQL would be desirable/useful. The main use case is to have a DNS SRV record that lists all the possible primaries of a given replicated PostgreSQL cluster. With auto failover solutions like patroni, pg_auto_failover, stolon, etc. any of these endpoints could be serving the primary server at any point in time. Combined with target_session_attrs a connection string to a highly-available cluster could be something like: psql "dnssrv=mydb.prod.example.com target_session_attr=read_write" Which would then resolve the SRV record _postgresql._tcp.mydb.prod.example.com and using the method described in RFC 2782 connect to the host/port combination one by one until it finds the primary. A benefit of using SRV records would be that the port is also part of the DNS record and therefore a single IP could be used to serve many databases on separate ports. When working with a cloud environment or containerized setup (or both) this would open up some good possibilities. Note: We currently can already do this somehow by specifying multiple hosts/ports in the connection string, however it would be useful if we could refer to a single SRV record instead, as that would have a list of hosts and ports to connect to. DNS SRV is described in detail here: https://tools.ietf.org/html/rfc2782 I'd love to hear some support/dissent, regards, Feike ^ permalink raw reply [nested|flat] 8+ messages in thread
* Re: Feature: Use DNS SRV records for connecting @ 2019-08-13 10:21 Graham Leggett <[email protected]> parent: Feike Steenbergen <[email protected]> 1 sibling, 0 replies; 8+ messages in thread From: Graham Leggett @ 2019-08-13 10:21 UTC (permalink / raw) To: Feike Steenbergen <[email protected]>; +Cc: pgsql-hackers On 13 Aug 2019, at 11:50, Feike Steenbergen <[email protected]> wrote: > I'd like to get some feedback on whether or not implementing a DNS SRV feature > for connecting to PostgreSQL would be desirable/useful. A big +1. We currently use SRV records to tell postgresql what kind of server it is. This way all of our postgresql servers have an identical configuration, they just tailor themselves on startup as appropriate: _postgresql-master._tcp.sql.example.com. The above record in our case declares who the master is. If the postgresql startup says “hey, that’s me” it configures itself as a master. If the postgresql startup says “hey, that’s not me” it configures itself as a slave of the master. We also use TXT records to define the databases we want (with protection against DNS security issues, we never remove a database based on a TXT record, but signed DNS records will help here). _postgresql.sql.example.com TXT "v=PGSQL1;d=mydb;u=myuser" We use a series of systemd “daemons” that are configured to run before and after postgresql to do the actual configuration on bootup, but it would be great if postgresql could just do this out the box. Regards, Graham — Attachments: [application/pkcs7-signature] smime.p7s (3.2K, 3-smime.p7s) download ^ permalink raw reply [nested|flat] 8+ messages in thread
* Re: Feature: Use DNS SRV records for connecting @ 2019-08-13 14:43 Tom Lane <[email protected]> parent: Feike Steenbergen <[email protected]> 1 sibling, 1 reply; 8+ messages in thread From: Tom Lane @ 2019-08-13 14:43 UTC (permalink / raw) To: Feike Steenbergen <[email protected]>; +Cc: pgsql-hackers Feike Steenbergen <[email protected]> writes: > I'd like to get some feedback on whether or not implementing a DNS SRV feature > for connecting to PostgreSQL would be desirable/useful. How would we get at that data without writing our own DNS client? (AFAIK, our existing DNS interactions are all handled by getnameinfo() or other library-supplied functions.) Maybe that'd be worth doing, but it sounds like a lot of work and a lot of new code to maintain, relative to the value of the feature. regards, tom lane ^ permalink raw reply [nested|flat] 8+ messages in thread
* Re: Feature: Use DNS SRV records for connecting @ 2019-08-14 18:01 Andres Freund <[email protected]> parent: Tom Lane <[email protected]> 0 siblings, 1 reply; 8+ messages in thread From: Andres Freund @ 2019-08-14 18:01 UTC (permalink / raw) To: Tom Lane <[email protected]>; +Cc: Feike Steenbergen <[email protected]>; pgsql-hackers Hi, On 2019-08-13 10:43:07 -0400, Tom Lane wrote: > How would we get at that data without writing our own DNS client? > (AFAIK, our existing DNS interactions are all handled by getnameinfo() > or other library-supplied functions.) > Maybe that'd be worth doing, but it sounds like a lot of work and a > lot of new code to maintain, relative to the value of the feature. It might have enough independent advantages to make it worthwhile though. Right now our non-blocking interfaces aren't actually in a number of cases, due to name resolution being blocking. While that's documented, it imo means that our users need to use a non-blocking DNS library, if they need non-blocking PQconnectPoll() - it's imo not that practical to just use IPs in most cases. We also don't have particularly good control over the order of hostnames returned by getaddrinfo, which makes it harder to implement reliable round-robin etc. Greetings, Andres Freund ^ permalink raw reply [nested|flat] 8+ messages in thread
* Re: Feature: Use DNS SRV records for connecting @ 2026-04-22 09:48 Andrey Borodin <[email protected]> parent: Andres Freund <[email protected]> 0 siblings, 2 replies; 8+ messages in thread From: Andrey Borodin @ 2026-04-22 09:48 UTC (permalink / raw) To: Andres Freund <[email protected]>; +Cc: Tom Lane <[email protected]>; Feike Steenbergen <[email protected]>; pgsql-hackers > On 14 Aug 2019, at 23:01, Andres Freund <[email protected]> wrote: > > On 2019-08-13 10:43:07 -0400, Tom Lane wrote: >> How would we get at that data without writing our own DNS client? >> (AFAIK, our existing DNS interactions are all handled by getnameinfo() >> or other library-supplied functions.) > >> Maybe that'd be worth doing, but it sounds like a lot of work and a >> lot of new code to maintain, relative to the value of the feature. > > It might have enough independent advantages to make it worthwhile > though. Here is a patch prototyping SRV-based discovery for libpq, reviving the idea from the 2019 thread. Tom asked how we'd get SRV data without writing our own DNS client. On POSIX, res_query(3) from libresolv does the lookup and dn_expand(3) decompresses names in the response - the same library that Kerberos and LDAP support already depend on. On Windows, DnsQuery() from windns.h returns a typed linked list with no wire-format parsing needed. The resolver is about 200 lines and uses no DNS logic of our own. Regarding non-blocking resolution raised by Andres: res_query() is blocking, as is the existing getaddrinfo() call for regular hosts. This patch does not make things worse, but does not improve them either. Async DNS resolution is a separate, larger problem. A new connection parameter "srvhost" (env: "PGSRVHOST") causes libpq to query "_postgresql._tcp.<srvhost>" before connecting. I chose "srvhost" over the original "dnssrv" suggestion to align with the existing "host" parameter naming. Two URI schemes are supported as shorthand: psql "postgresql+srv://cluster.example.com/mydb?target_session_attrs=read-write" psql "postgres+srv://cluster.example.com/mydb?target_session_attrs=read-write" Given the DNS records: _postgresql._tcp.cluster.example.com. SRV 10 50 5432 primary.example.com. _postgresql._tcp.cluster.example.com. SRV 20 50 5432 standby1.example.com. _postgresql._tcp.cluster.example.com. SRV 20 50 5432 standby2.example.com. The resolved host list is sorted per RFC 2782 and injected into the existing multi-host machinery before connhost[] is built, so target_session_attrs, load_balance_hosts, and failover work on the expanded list without any changes to PQconnectPoll. "srvhost" is mutually exclusive with "host" and "hostaddr". Multiple hosts in a "+srv" URI are rejected - expansion is DNS's responsibility. I'm proposing this across the PostgreSQL driver ecosystem (pgx, pgjdbc, npgsql, and now libpq). I would be happy to hear a feedback. Best regards, Andrey Borodin. Attachments: [application/octet-stream] 0001-libpq-Add-DNS-SRV-record-support-for-service-discove.patch (33.8K, 2-0001-libpq-Add-DNS-SRV-record-support-for-service-discove.patch) download | inline diff: From 30b8f2eac8dc8f8b31556e242db90329e8f392b2 Mon Sep 17 00:00:00 2001 From: Andrey Borodin <[email protected]> Date: Wed, 22 Apr 2026 11:14:17 +0500 Subject: [PATCH] libpq: Add DNS SRV record support for service discovery Add a new connection parameter "srvhost" that causes libpq to query _postgresql._tcp.<srvhost> SRV records before connecting, replacing the normal host list with the sorted result. This allows a whole PostgreSQL cluster to be addressed by a single DNS name. New connection options: srvhost=<domain> DNS domain for SRV lookup (env: PGSRVHOST) postgresql+srv://<domain>/<db> URI shorthand for srvhost postgres+srv://<domain>/<db> alias SRV records are sorted per RFC 2782 (priority ascending, weight descending) and injected into the existing multi-host machinery before connhost[] is built, so target_session_attrs, load_balance_hosts, and failover all work transparently on the expanded host list. srvhost is mutually exclusive with host and hostaddr. Multiple hosts in a +srv URI are rejected because expansion is DNS's job. On POSIX, resolution uses res_query(3) + dn_expand(3) to parse the DNS wire format directly, avoiding ns_initparse() which is absent on musl. On Windows, DnsQuery() from windns.h is used instead. Platforms without either receive a clear error at connect time. The resolver is called from pqConnectOptions2() before the connhost[] array is allocated, requiring no changes to PQconnectPoll or the connection state machine. --- configure | 56 ++++ configure.ac | 11 + doc/src/sgml/libpq.sgml | 52 +++ meson.build | 20 ++ src/include/pg_config.h.in | 3 + src/interfaces/libpq/Makefile | 13 +- src/interfaces/libpq/fe-connect-srv.c | 437 ++++++++++++++++++++++++++ src/interfaces/libpq/fe-connect-srv.h | 29 ++ src/interfaces/libpq/fe-connect.c | 83 ++++- src/interfaces/libpq/libpq-int.h | 5 + src/interfaces/libpq/meson.build | 1 + src/interfaces/libpq/t/007_srv.pl | 159 ++++++++++ 12 files changed, 863 insertions(+), 6 deletions(-) create mode 100644 src/interfaces/libpq/fe-connect-srv.c create mode 100644 src/interfaces/libpq/fe-connect-srv.h create mode 100644 src/interfaces/libpq/t/007_srv.pl diff --git a/configure b/configure index f66c1054a7..175a0bfcaf 100755 --- a/configure +++ b/configure @@ -12615,6 +12615,62 @@ if test "$ac_res" != no; then : fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for library containing res_query" >&5 +$as_echo_n "checking for library containing res_query... " >&6; } +if ${ac_cv_search_res_query+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_func_search_save_LIBS=$LIBS +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +#ifdef __cplusplus +extern "C" +#endif +char res_query (); +int +main () +{ +return res_query (); + ; + return 0; +} +_ACEOF +for ac_lib in '' resolv; do + if test -z "$ac_lib"; then + ac_res="none required" + else + ac_res=-l$ac_lib + LIBS="-l$ac_lib $ac_func_search_save_LIBS" + fi + if ac_fn_c_try_link "$LINENO"; then : + ac_cv_search_res_query=$ac_res +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext + if ${ac_cv_search_res_query+:} false; then : + break +fi +done +if ${ac_cv_search_res_query+:} false; then : + +else + ac_cv_search_res_query=no +fi +rm conftest.$ac_ext +LIBS=$ac_func_search_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_res_query" >&5 +$as_echo "$ac_cv_search_res_query" >&6; } +ac_res=$ac_cv_search_res_query +if test "$ac_res" != no; then : + test "$ac_res" = "none required" || LIBS="$ac_res $LIBS" + +$as_echo "#define HAVE_RES_QUERY 1" >>confdefs.h + +fi + + if test "$with_readline" = yes; then diff --git a/configure.ac b/configure.ac index 8d176bd346..36d836275b 100644 --- a/configure.ac +++ b/configure.ac @@ -1386,6 +1386,17 @@ AC_SEARCH_LIBS(backtrace_symbols, execinfo) AC_SEARCH_LIBS(pthread_barrier_wait, pthread) +dnl +dnl DNS SRV record support in libpq. +dnl On Linux and some BSDs res_query() lives in libresolv; on macOS it is +dnl available from the system library without any extra -l flag. +dnl AC_SEARCH_LIBS handles both cases: it tries libc first, then libresolv. +dnl +AC_SEARCH_LIBS(res_query, resolv, + [AC_DEFINE([HAVE_RES_QUERY], 1, + [Define to 1 if you have the res_query() DNS resolver function.])]) +AC_CHECK_HEADERS([arpa/nameser.h resolv.h]) + if test "$with_readline" = yes; then PGAC_CHECK_READLINE if test x"$pgac_cv_check_readline" = x"no"; then diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 0a19c2b553..db3e669a74 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -1129,6 +1129,48 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname The currently recognized parameter key words are: <variablelist> + <varlistentry id="libpq-connect-srvhost" xreflabel="srvhost"> + <term><literal>srvhost</literal></term> + <listitem> + <para> + DNS domain name used for SRV-based service discovery (RFC 2782). + When set, libpq queries DNS for + <literal>_postgresql._tcp.<replaceable>srvhost</replaceable></literal> + SRV records and derives the list of hosts and ports to try from the + sorted result. Records are ordered by priority (ascending) then + weight (descending), exactly as specified in RFC 2782. + </para> + <para> + This parameter is mutually exclusive with <literal>host</literal> and + <literal>hostaddr</literal>. It works together with all other + connection parameters, including <literal>target_session_attrs</literal> + and <literal>load_balance_hosts</literal>, which are applied to the + expanded host list after DNS resolution. + </para> + <para> + The environment variable equivalent is <envar>PGSRVHOST</envar>. + </para> + <para> + The <literal>postgresql+srv://</literal> and + <literal>postgres+srv://</literal> URI schemes are a shorthand for + setting <literal>srvhost</literal> from the URI host part: + +<synopsis> +postgresql+srv://<replaceable>cluster.example.com</replaceable>/<replaceable>mydb</replaceable> +</synopsis> + + is equivalent to: + +<synopsis> +postgresql:///<replaceable>mydb</replaceable>?srvhost=<replaceable>cluster.example.com</replaceable> +</synopsis> + + Multiple hosts are not permitted in a <literal>+srv</literal> URI + because the host expansion is performed by DNS. + </para> + </listitem> + </varlistentry> + <varlistentry id="libpq-connect-host" xreflabel="host"> <term><literal>host</literal></term> <listitem> @@ -9060,6 +9102,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) information into simple client applications, for example. <itemizedlist> + <listitem> + <para> + <indexterm> + <primary><envar>PGSRVHOST</envar></primary> + </indexterm> + <envar>PGSRVHOST</envar> behaves the same as the <xref + linkend="libpq-connect-srvhost"/> connection parameter. + </para> + </listitem> + <listitem> <para> <indexterm> diff --git a/meson.build b/meson.build index 20b887f1a1..76533e7f45 100644 --- a/meson.build +++ b/meson.build @@ -376,6 +376,8 @@ elif host_system == 'windows' secur32_dep = cc.find_library('secur32', required: true) backend_deps += secur32_dep libpq_deps += secur32_dep + # DnsQuery() for SRV record lookup on Windows + libpq_deps += cc.find_library('dnsapi', required: true) postgres_inc_d += 'src/include/port/win32' if cc.get_id() == 'msvc' @@ -1851,6 +1853,24 @@ endif +############################################################### +# DNS SRV support (libpq) +############################################################### + +# res_query() and dn_expand() are used by fe-connect-srv.c to resolve SRV +# records. On most POSIX systems they live in libresolv; on macOS they are +# in the system library but still require explicit linking with libresolv. +if host_machine.system() != 'windows' + resolv_dep = cc.find_library('resolv', required: false) + if resolv_dep.found() + cdata.set('HAVE_RES_QUERY', 1) + libpq_deps += resolv_dep + elif cc.has_function('res_query') + cdata.set('HAVE_RES_QUERY', 1) + endif +endif + + ############################################################### # Compiler tests ############################################################### diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in index 4f8113c144..40dbd52cff 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -204,6 +204,9 @@ /* Define to 1 if you have the <ifaddrs.h> header file. */ #undef HAVE_IFADDRS_H +/* Define to 1 if you have the `res_query' DNS resolver function. */ +#undef HAVE_RES_QUERY + /* Define to 1 if you have the `inet_aton' function. */ #undef HAVE_INET_ATON diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 0963995eed..4e20645543 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -34,6 +34,7 @@ OBJS = \ fe-auth-scram.o \ fe-cancel.o \ fe-connect.o \ + fe-connect-srv.o \ fe-exec.o \ fe-lobj.o \ fe-misc.o \ @@ -88,11 +89,21 @@ endif SHLIB_LINK_INTERNAL = -lpgcommon_shlib -lpgport_shlib ifneq ($(PORTNAME), win32) SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lsocket -lnsl -lresolv -lintl -ldl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) +# DNS SRV: link with libresolv if the library exists and -lresolv is not +# already in SHLIB_LINK (e.g. because configure added it to LIBS). +# On Linux/glibc res_query lives in libresolv; on macOS it is in +# /usr/lib/libresolv.dylib. The empty-source link test is portable. +ifeq ($(filter -lresolv, $(SHLIB_LINK)),) +HAVE_RESOLV_LIB := $(shell echo 'int main(void){return 0;}' | $(CC) -lresolv -o /dev/null -x c - 2>/dev/null && echo yes) +ifeq ($(HAVE_RESOLV_LIB), yes) +SHLIB_LINK += -lresolv +endif +endif else SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi32 -lssl -lsocket -lnsl -lresolv -lintl -lm $(PTHREAD_LIBS), $(LIBS)) $(LDAP_LIBS_FE) endif ifeq ($(PORTNAME), win32) -SHLIB_LINK += -lshell32 -lws2_32 -lsecur32 $(filter -lcomerr32 -lkrb5_32, $(LIBS)) +SHLIB_LINK += -lshell32 -lws2_32 -lsecur32 -ldnsapi $(filter -lcomerr32 -lkrb5_32, $(LIBS)) endif SHLIB_PREREQS = submake-libpgport diff --git a/src/interfaces/libpq/fe-connect-srv.c b/src/interfaces/libpq/fe-connect-srv.c new file mode 100644 index 0000000000..c5b7faf827 --- /dev/null +++ b/src/interfaces/libpq/fe-connect-srv.c @@ -0,0 +1,437 @@ +/*------------------------------------------------------------------------- + * + * fe-connect-srv.c + * DNS SRV record lookup for libpq service discovery. + * + * When a connection string specifies "srvhost=cluster.example.com" (or the + * equivalent URI "postgresql+srv://cluster.example.com/"), libpq resolves + * _postgresql._tcp.<srvhost> SRV records, sorts them by priority (ascending) + * then weight (descending) per RFC 2782, and expands the result into the + * conn->pghost and conn->pgport fields before pqConnectOptions2() builds the + * per-host connection array. All existing multi-host machinery (failover, + * target_session_attrs, load_balance_hosts) then works unchanged. + * + * Platform support: + * - POSIX systems with res_query(3): Linux (glibc), macOS, *BSD, Solaris. + * Guarded by HAVE_RES_QUERY which is set by configure / meson. + * - Windows: DnsQuery() from windns.h, linked with Dnsapi.lib. + * - All other platforms: srvhost is rejected at connection time with an + * informative error message. + * + * Copyright (c) 2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/interfaces/libpq/fe-connect-srv.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include "common/int.h" +#include "fe-connect-srv.h" +#include "libpq-int.h" +#include "pqexpbuffer.h" + +/* ---------- + * Internal SRV record representation. + * ---------- + */ +typedef struct +{ + uint16_t priority; + uint16_t weight; + uint16_t port; + char target[NI_MAXHOST]; +} SRVRecord; + +/* Sort: lower priority first; higher weight first within same priority. */ +static int +compareSRVRecords(const void *a, const void *b) +{ + const SRVRecord *sa = (const SRVRecord *) a; + const SRVRecord *sb = (const SRVRecord *) b; + + if (sa->priority != sb->priority) + return pg_cmp_u16(sa->priority, sb->priority); + return pg_cmp_u16(sb->weight, sa->weight); +} + +/* + * Build conn->pghost and conn->pgport as comma-separated strings from a + * sorted SRVRecord array. Returns true on success; on OOM sets the + * connection error and returns false. + */ +static bool +srv_build_host_port(PGconn *conn, SRVRecord *records, int nrecords) +{ + PQExpBufferData hostbuf; + PQExpBufferData portbuf; + + initPQExpBuffer(&hostbuf); + initPQExpBuffer(&portbuf); + + for (int i = 0; i < nrecords; i++) + { + if (i > 0) + { + appendPQExpBufferChar(&hostbuf, ','); + appendPQExpBufferChar(&portbuf, ','); + } + appendPQExpBufferStr(&hostbuf, records[i].target); + appendPQExpBuffer(&portbuf, "%u", records[i].port); + } + + if (PQExpBufferDataBroken(hostbuf) || PQExpBufferDataBroken(portbuf)) + { + termPQExpBuffer(&hostbuf); + termPQExpBuffer(&portbuf); + libpq_append_conn_error(conn, "out of memory"); + return false; + } + + /* + * Replace conn->pghost and conn->pgport with the SRV-derived values. + * The originals were either NULL or empty (caller verified), but free + * them defensively before overwriting. + */ + free(conn->pghost); + conn->pghost = strdup(hostbuf.data); + free(conn->pgport); + conn->pgport = strdup(portbuf.data); + + termPQExpBuffer(&hostbuf); + termPQExpBuffer(&portbuf); + + if (!conn->pghost || !conn->pgport) + { + libpq_append_conn_error(conn, "out of memory"); + return false; + } + + return true; +} + + +/* ==================================================================== + * Platform-specific implementations + * ==================================================================== */ + +#ifdef WIN32 + +/* + * Windows implementation using DnsQuery() from windns.h. + * Link with -ldnsapi (Dnsapi.lib). + */ +#include <windns.h> + +bool +pqLookupSRVHosts(PGconn *conn) +{ + char qname[NI_MAXHOST + 30]; + PDNS_RECORD pDnsRecord = NULL; + PDNS_RECORD pRec; + SRVRecord *records = NULL; + int nrecords = 0; + int maxrecords = 0; + DNS_STATUS status; + bool ok; + + snprintf(qname, sizeof(qname), "_postgresql._tcp.%s", conn->srvhost); + + status = DnsQuery_A(qname, DNS_TYPE_SRV, DNS_QUERY_STANDARD, + NULL, &pDnsRecord, NULL); + if (status != ERROR_SUCCESS) + { + libpq_append_conn_error(conn, + "DNS SRV lookup failed for \"%s\": error code %lu", + qname, (unsigned long) status); + return false; + } + + /* Collect SRV records from the linked list */ + for (pRec = pDnsRecord; pRec != NULL; pRec = pRec->pNext) + { + SRVRecord *tmp; + SRVRecord *rec; + size_t tlen; + + if (pRec->wType != DNS_TYPE_SRV) + continue; + + if (nrecords >= maxrecords) + { + maxrecords = maxrecords ? maxrecords * 2 : 4; + tmp = realloc(records, maxrecords * sizeof(SRVRecord)); + if (!tmp) + { + DnsRecordListFree(pDnsRecord, DnsFreeRecordList); + free(records); + libpq_append_conn_error(conn, "out of memory"); + return false; + } + records = tmp; + } + + rec = &records[nrecords]; + rec->priority = pRec->Data.SRV.wPriority; + rec->weight = pRec->Data.SRV.wWeight; + rec->port = pRec->Data.SRV.wPort; + + /* + * DnsQuery_A returns pNameTarget as a plain ANSI string (PSTR); + * no wide-char conversion needed. + */ + strlcpy(rec->target, pRec->Data.SRV.pNameTarget, sizeof(rec->target)); + + /* Strip trailing dot from FQDN */ + tlen = strlen(rec->target); + if (tlen > 0 && rec->target[tlen - 1] == '.') + rec->target[tlen - 1] = '\0'; + + nrecords++; + } + + DnsRecordListFree(pDnsRecord, DnsFreeRecordList); + + if (nrecords == 0) + { + libpq_append_conn_error(conn, "no SRV records found for \"%s\"", qname); + free(records); + return false; + } + + qsort(records, nrecords, sizeof(SRVRecord), compareSRVRecords); + + ok = srv_build_host_port(conn, records, nrecords); + + free(records); + return ok; +} + +#elif defined(HAVE_RES_QUERY) + +/* + * POSIX implementation using res_query() and manual DNS wire-format parsing. + * + * HAVE_RES_QUERY is set by configure (AC_SEARCH_LIBS) and meson + * (cc.has_function / find_library) when res_query() is available. + * + * We avoid depending on ns_initparse() / ns_parserr() since those symbols + * are not available on all platforms (e.g. musl libc). Instead we parse + * the wire format directly, using only dn_expand() for name decompression + * (which is universally available wherever res_query() is). + */ + +#include <arpa/nameser.h> +#include <resolv.h> +#include <netdb.h> + +/* DNS wire-format constants (from RFC 1035 / RFC 2782) */ +#ifndef T_SRV +#define T_SRV 33 /* RFC 2782: Service locator */ +#endif +#ifndef C_IN +#define C_IN 1 /* the Internet */ +#endif +#ifndef HFIXEDSZ +#define HFIXEDSZ 12 /* DNS message header size */ +#endif + +/* + * Skip a (possibly compressed) DNS domain name starting at cp. + * Returns bytes consumed, or -1 on error. + */ +static int +srv_skip_dname(const unsigned char *eom, const unsigned char *cp) +{ + const unsigned char *orig = cp; + + while (cp < eom) + { + int n = *cp & 0xFF; + + if (n == 0) + return (int) ((cp - orig) + 1); /* root label */ + if ((n & 0xC0) == 0xC0) + return (int) ((cp - orig) + 2); /* 2-byte pointer */ + if ((n & 0xC0) != 0) + return -1; /* unknown label type */ + cp += n + 1; + } + return -1; /* truncated */ +} + +bool +pqLookupSRVHosts(PGconn *conn) +{ + char qname[NI_MAXHOST + 30]; + unsigned char answer[4096]; + int len; + const unsigned char *cp; + const unsigned char *eom; + int qdcount, + ancount; + SRVRecord *records = NULL; + int nrecords = 0; + int maxrecords = 0; + bool retval = false; + + snprintf(qname, sizeof(qname), "_postgresql._tcp.%s", conn->srvhost); + + len = res_query(qname, C_IN, T_SRV, answer, sizeof(answer)); + if (len < 0) + { + /* + * h_errno is set by res_query on failure. HOST_NOT_FOUND and + * NO_DATA both indicate that the SRV RRset simply doesn't exist. + */ + const char *reason; + + switch (h_errno) + { + case HOST_NOT_FOUND: + reason = "host not found"; + break; + case NO_DATA: + reason = "no SRV records published"; + break; + case TRY_AGAIN: + reason = "temporary DNS failure, try again"; + break; + default: + reason = hstrerror(h_errno); + break; + } + libpq_append_conn_error(conn, + "DNS SRV lookup failed for \"%s\": %s", + qname, reason); + return false; + } + + if (len < HFIXEDSZ) + { + libpq_append_conn_error(conn, + "DNS response too short for \"%s\"", qname); + return false; + } + + eom = answer + len; + + /* + * Parse the DNS response header (RFC 1035 §4.1.1): + * bytes 4-5: QDCOUNT, bytes 6-7: ANCOUNT + */ + qdcount = (answer[4] << 8) | answer[5]; + ancount = (answer[6] << 8) | answer[7]; + cp = answer + HFIXEDSZ; + + /* Skip the question section */ + for (int i = 0; i < qdcount; i++) + { + int n = srv_skip_dname(eom, cp); + + if (n < 0) + goto parse_error; + cp += n + 4; /* name + QTYPE + QCLASS */ + if (cp > eom) + goto parse_error; + } + + /* Parse the answer section */ + for (int i = 0; i < ancount; i++) + { + int n; + uint16_t rtype, + rdlen; + char target[NI_MAXHOST]; + size_t tlen; + + n = srv_skip_dname(eom, cp); + if (n < 0) + goto parse_error; + cp += n; + + /* Need TYPE(2) + CLASS(2) + TTL(4) + RDLENGTH(2) = 10 bytes */ + if (cp + 10 > eom) + goto parse_error; + + rtype = (cp[0] << 8) | cp[1]; + rdlen = (cp[8] << 8) | cp[9]; + cp += 10; + + if (cp + rdlen > eom) + goto parse_error; + + if (rtype == T_SRV) + { + /* SRV RDATA: priority(2) weight(2) port(2) target(variable) */ + if (rdlen < 7) + goto parse_error; + + n = dn_expand(answer, eom, cp + 6, target, sizeof(target)); + if (n < 0) + goto parse_error; + + /* Strip trailing dot from FQDN */ + tlen = strlen(target); + if (tlen > 0 && target[tlen - 1] == '.') + target[tlen - 1] = '\0'; + + if (nrecords >= maxrecords) + { + SRVRecord *tmp; + + maxrecords = maxrecords ? maxrecords * 2 : 4; + tmp = realloc(records, maxrecords * sizeof(SRVRecord)); + if (!tmp) + { + libpq_append_conn_error(conn, "out of memory"); + goto cleanup; + } + records = tmp; + } + + records[nrecords].priority = (cp[0] << 8) | cp[1]; + records[nrecords].weight = (cp[2] << 8) | cp[3]; + records[nrecords].port = (cp[4] << 8) | cp[5]; + strlcpy(records[nrecords].target, target, + sizeof(records[nrecords].target)); + nrecords++; + } + + cp += rdlen; + } + + if (nrecords == 0) + { + libpq_append_conn_error(conn, + "no SRV records found for \"%s\"", qname); + goto cleanup; + } + + qsort(records, nrecords, sizeof(SRVRecord), compareSRVRecords); + + retval = srv_build_host_port(conn, records, nrecords); + goto cleanup; + +parse_error: + libpq_append_conn_error(conn, "malformed DNS response for \"%s\"", qname); + +cleanup: + free(records); + return retval; +} + +#else /* no SRV support on this platform */ + +bool +pqLookupSRVHosts(PGconn *conn) +{ + libpq_append_conn_error(conn, + "\"srvhost\" is not supported on this platform " + "(DNS SRV lookup requires res_query)"); + return false; +} + +#endif /* platform selection */ diff --git a/src/interfaces/libpq/fe-connect-srv.h b/src/interfaces/libpq/fe-connect-srv.h new file mode 100644 index 0000000000..d4117ccb0d --- /dev/null +++ b/src/interfaces/libpq/fe-connect-srv.h @@ -0,0 +1,29 @@ +/*------------------------------------------------------------------------- + * + * fe-connect-srv.h + * DNS SRV record lookup for libpq service discovery. + * + * Copyright (c) 2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/interfaces/libpq/fe-connect-srv.h + * + *------------------------------------------------------------------------- + */ +#ifndef FE_CONNECT_SRV_H +#define FE_CONNECT_SRV_H + +#include "libpq-int.h" + +/* + * pqLookupSRVHosts + * + * Resolve _postgresql._tcp.<conn->srvhost> SRV records and store the + * result in conn->pghost and conn->pgport as comma-separated strings, + * suitable for consumption by pqConnectOptions2(). + * + * Returns true on success, false on error (error message set in conn). + */ +extern bool pqLookupSRVHosts(PGconn *conn); + +#endif /* FE_CONNECT_SRV_H */ diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e6..1503fd6e36 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -30,6 +30,7 @@ #include "common/string.h" #include "fe-auth.h" #include "fe-auth-oauth.h" +#include "fe-connect-srv.h" #include "libpq-fe.h" #include "libpq-int.h" #include "mb/pg_wchar.h" @@ -231,6 +232,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Database-Name", "", 20, offsetof(struct pg_conn, dbName)}, + {"srvhost", "PGSRVHOST", NULL, NULL, + "Database-SRV-Host", "", 64, + offsetof(struct pg_conn, srvhost)}, + {"host", "PGHOST", NULL, NULL, "Database-Host", "", 40, offsetof(struct pg_conn, pghost)}, @@ -454,6 +459,9 @@ static const pg_fe_sasl_mech *supported_sasl_mechs[] = /* The connection URI must start with either of the following designators: */ static const char uri_designator[] = "postgresql://"; static const char short_uri_designator[] = "postgres://"; +/* SRV URI variants: the host is treated as the SRV domain, not a direct host */ +static const char srv_uri_designator[] = "postgresql+srv://"; +static const char short_srv_uri_designator[] = "postgres+srv://"; static bool connectOptions1(PGconn *conn, const char *conninfo); static bool init_allowed_encryption_methods(PGconn *conn); @@ -1258,6 +1266,29 @@ pqConnectOptions2(PGconn *conn) { int i; + /* + * If srvhost is set, validate mutual exclusivity with host/hostaddr and + * then resolve _postgresql._tcp.<srvhost> SRV records, populating + * conn->pghost and conn->pgport from the sorted results. This must + * happen before the host-array allocation below. + */ + if (conn->srvhost != NULL && conn->srvhost[0] != '\0') + { + if ((conn->pghost != NULL && conn->pghost[0] != '\0') || + (conn->pghostaddr != NULL && conn->pghostaddr[0] != '\0')) + { + conn->status = CONNECTION_BAD; + libpq_append_conn_error(conn, + "cannot use \"srvhost\" together with \"host\" or \"hostaddr\""); + return false; + } + if (!pqLookupSRVHosts(conn)) + { + conn->status = CONNECTION_BAD; + return false; + } + } + /* * Allocate memory for details about each host to which we might possibly * try to connect. For that, count the number of elements in the hostaddr @@ -6359,6 +6390,15 @@ parse_connection_string(const char *connstr, PQExpBuffer errorMessage, static int uri_prefix_length(const char *connstr) { + /* Check SRV URI variants first (longer prefixes before shorter) */ + if (strncmp(connstr, srv_uri_designator, + sizeof(srv_uri_designator) - 1) == 0) + return sizeof(srv_uri_designator) - 1; + + if (strncmp(connstr, short_srv_uri_designator, + sizeof(short_srv_uri_designator) - 1) == 0) + return sizeof(short_srv_uri_designator) - 1; + if (strncmp(connstr, uri_designator, sizeof(uri_designator) - 1) == 0) return sizeof(uri_designator) - 1; @@ -6910,6 +6950,14 @@ conninfo_uri_parse(const char *uri, PQExpBuffer errorMessage, * * postgresql://[user[:password]@][netloc][:port][,...][/dbname][?param1=value1&...] * + * The postgresql+srv:// and postgres+srv:// URI schemes are also recognized: + * + * postgresql+srv://[user[:password]@][srvdomain][/dbname][?param1=value1&...] + * + * In the +srv form, the netloc is interpreted as the SRV domain (stored in + * the "srvhost" connection parameter) rather than as a direct host address. + * Multiple netloc specifications are not allowed in the +srv form. + * * Any of the URI parts might use percent-encoding (%xy). */ static bool @@ -6917,6 +6965,7 @@ conninfo_uri_parse_options(PQconninfoOption *options, const char *uri, PQExpBuffer errorMessage) { int prefix_len; + bool is_srv_uri; char *p; char *buf = NULL; char *start; @@ -6944,8 +6993,10 @@ conninfo_uri_parse_options(PQconninfoOption *options, const char *uri, } start = buf; - /* Skip the URI prefix */ + /* Skip the URI prefix and detect if this is a +srv URI */ prefix_len = uri_prefix_length(uri); + is_srv_uri = (prefix_len == sizeof(srv_uri_designator) - 1 || + prefix_len == sizeof(short_srv_uri_designator) - 1); if (prefix_len == 0) { /* Should never happen */ @@ -7096,10 +7147,32 @@ conninfo_uri_parse_options(PQconninfoOption *options, const char *uri, /* Save final values for host and port. */ if (PQExpBufferDataBroken(hostbuf) || PQExpBufferDataBroken(portbuf)) goto cleanup; - if (hostbuf.data[0] && - !conninfo_storeval(options, "host", hostbuf.data, - errorMessage, false, true)) - goto cleanup; + if (hostbuf.data[0]) + { + if (is_srv_uri) + { + /* + * For postgresql+srv:// URIs the netloc is the SRV domain, not a + * direct host address. Store it in "srvhost" and reject multiple + * hosts (commas) since SRV already expands to multiple targets. + */ + if (strchr(hostbuf.data, ',') != NULL) + { + libpq_append_error(errorMessage, + "multiple hosts are not allowed in a postgresql+srv:// URI"); + goto cleanup; + } + if (!conninfo_storeval(options, "srvhost", hostbuf.data, + errorMessage, false, true)) + goto cleanup; + } + else + { + if (!conninfo_storeval(options, "host", hostbuf.data, + errorMessage, false, true)) + goto cleanup; + } + } if (portbuf.data[0] && !conninfo_storeval(options, "port", portbuf.data, errorMessage, false, true)) diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 23de98290c..cbfeee6288 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -371,6 +371,11 @@ typedef struct pg_conn_host struct pg_conn { /* Saved values of connection options */ + char *srvhost; /* DNS SRV cluster domain for service + * discovery; when set, _postgresql._tcp.<srvhost> + * is resolved at connect time and the result + * replaces pghost/pgport. Mutually exclusive + * with pghost and pghostaddr. */ char *pghost; /* the machine on which the server is running, * or a path to a UNIX-domain socket, or a * comma-separated list of machines and/or diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build index b0ae72167a..b5bf3f1faa 100644 --- a/src/interfaces/libpq/meson.build +++ b/src/interfaces/libpq/meson.build @@ -6,6 +6,7 @@ libpq_sources = files( 'fe-auth.c', 'fe-cancel.c', 'fe-connect.c', + 'fe-connect-srv.c', 'fe-exec.c', 'fe-lobj.c', 'fe-misc.c', diff --git a/src/interfaces/libpq/t/007_srv.pl b/src/interfaces/libpq/t/007_srv.pl new file mode 100644 index 0000000000..79e50c9ed6 --- /dev/null +++ b/src/interfaces/libpq/t/007_srv.pl @@ -0,0 +1,159 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::Cluster; +use Test::More; + +# This test exercises DNS SRV record support in libpq: +# +# 1. URI parsing: postgresql+srv:// and postgres+srv:// schemes store the +# netloc as "srvhost" rather than "host". +# 2. Error handling: multiple hosts in a +srv URI, mixing srvhost with host. +# 3. Live SRV lookup against a running PostgreSQL cluster. +# +# Live lookup is gated by PG_TEST_EXTRA=srv because it requires that valid +# SRV records exist in DNS. The test relies on the SRV_HOST environment +# variable (defaulting to the value recommended in the PostgreSQL docs). + + +# -------------------------------------------------------------------------- +# Part 1: URI parsing (no network required, uses libpq_uri_regress) +# -------------------------------------------------------------------------- + +my @uri_tests = ( + + # postgresql+srv:// — netloc becomes srvhost + [ + q{postgresql+srv://cluster.example.com/mydb}, + q{dbname='mydb' srvhost='cluster.example.com'}, + q{}, + ], + + # postgres+srv:// short form + [ + q{postgres+srv://cluster.example.com/mydb}, + q{dbname='mydb' srvhost='cluster.example.com'}, + q{}, + ], + + # with user, extra params + [ + q{postgresql+srv://[email protected]/mydb?target_session_attrs=read-write}, + q{user='alice' dbname='mydb' srvhost='cluster.example.com' target_session_attrs='read-write'}, + q{}, + ], + + # multiple hosts must be rejected for +srv URIs + [ + q{postgresql+srv://h1.example.com,h2.example.com/mydb}, + q{}, + q{multiple hosts are not allowed in a postgresql+srv:// URI}, + ], +); + +foreach my $t (@uri_tests) +{ + my ($uri, $want_out, $want_err) = @$t; + + my ($stdout, $stderr); + IPC::Run::run [ 'libpq_uri_regress', $uri ], + '>' => \$stdout, + '2>' => \$stderr; + chomp $stdout; + chomp $stderr; + + # Strip the trailing connection-type annotation "(local)"/"(inet)" so + # that the test is not sensitive to socket-vs-TCP defaults. + $stdout =~ s/\s+\(\w+\)$//; + + is($stdout, $want_out, "URI stdout: $uri"); + like($stderr, qr/\Q$want_err\E/, "URI stderr: $uri") + if $want_err ne ''; + is($stderr, '', "URI no stderr: $uri") + if $want_err eq ''; +} + +# -------------------------------------------------------------------------- +# Part 2: Live SRV lookup (optional, gated by PG_TEST_EXTRA) +# -------------------------------------------------------------------------- + +my $do_live = $ENV{PG_TEST_EXTRA} && $ENV{PG_TEST_EXTRA} =~ /\bsrv\b/; + +if (!$do_live) +{ + note 'Skipping live SRV test (not enabled in PG_TEST_EXTRA)'; + done_testing(); + exit 0; +} + +# The live test starts a PostgreSQL cluster on a local port, publishes the +# connection details via a mock hosts-file trick (as done in 004_load_balance_dns.pl), +# and verifies that a postgresql+srv:// URI resolves and connects. +# +# When running in CI, the SRV records for $SRV_HOST must resolve to 127.0.0.1 +# on port $SRV_PORT. + +my $srv_host = + $ENV{SRV_HOST} // 'pg-srvtest'; # DNS name whose SRV records point here +my $node = PostgreSQL::Test::Cluster->new('primary'); +$node->init; +$node->start; + +my $port = $node->port; + +note "Testing SRV connection via srvhost=$srv_host (expect port $port)"; + +# 1. keyword=value connection string +my ($ret, $out, $err); +$ret = $node->psql( + 'postgres', + 'SELECT 1', + stdout => \$out, + stderr => \$err, + extra_params => [ '-d', "srvhost=$srv_host" ], + on_error_stop => 0); +is($ret, 0, "srvhost= keyword connects via SRV") + or diag("stderr: $err"); + +# 2. URI form +$ret = $node->psql( + 'postgres', + 'SELECT 1', + stdout => \$out, + stderr => \$err, + extra_params => [ '-d', "postgresql+srv://$srv_host/postgres" ], + on_error_stop => 0); +is($ret, 0, "postgresql+srv:// URI connects via SRV") + or diag("stderr: $err"); + +# 3. target_session_attrs=any works with SRV +$ret = $node->psql( + 'postgres', + 'SELECT 1', + stdout => \$out, + stderr => \$err, + extra_params => [ + '-d', + "postgresql+srv://$srv_host/postgres?target_session_attrs=any", + ], + on_error_stop => 0); +is($ret, 0, "postgresql+srv:// with target_session_attrs=any") + or diag("stderr: $err"); + +# 4. Mixing srvhost and host is rejected +$ret = $node->psql( + 'postgres', + 'SELECT 1', + stdout => \$out, + stderr => \$err, + extra_params => [ '-d', "srvhost=$srv_host host=localhost" ], + on_error_stop => 0); +isnt($ret, 0, "srvhost + host= is rejected"); +like($err, qr/cannot use "srvhost" together with "host"/, + "correct error for srvhost + host conflict"); + +$node->stop; + +done_testing(); -- 2.50.1 (Apple Git-155) ^ permalink raw reply [nested|flat] 8+ messages in thread
* Re: Feature: Use DNS SRV records for connecting @ 2026-05-12 09:46 Andrey Borodin <[email protected]> parent: Andrey Borodin <[email protected]> 1 sibling, 1 reply; 8+ messages in thread From: Andrey Borodin @ 2026-05-12 09:46 UTC (permalink / raw) To: Jacob Champion <[email protected]>; +Cc: Tom Lane <[email protected]>; Feike Steenbergen <[email protected]>; pgsql-hackers; Andres Freund <[email protected]> Hi hackers! > On 22 Apr 2026, at 14:48, Andrey Borodin <[email protected]> wrote: > > I'm proposing this across the PostgreSQL driver ecosystem While preparing equivalent patches for pgx, pgjdbc and npgsql, Jack Christensen (pgx maintainer) raised a concern [0] about the postgres+srv:// URI scheme I proposed here. The issue is specific to Go's net/url package [1]. In Go 1.26 the URL parser was tightened and broke parsing of HA connection strings of the form postgresql://user@h1:1,h2:2/db (colons inside the authority after a comma). The Go team addressed this by relaxing validation, but only for the "postgres" and "postgresql" schemes - any new scheme, including "postgres+srv", gets the strict modern parser. For a single SRV name in the authority this is probably fine in practice, but Jack reasonably points out that deviating from postgresql:// is a compatibility bet without much upside. [2] Per RFC 3986 §3.1 the "+" in a scheme name is syntactically legal. Per RFC 7595 / BCP 35, schemes can be registered with IANA, but neither "postgresql" nor "mongodb+srv" is in the IANA registry today, so there is no formal gate to pass - just our own consensus on what to ship. On a related note, "mongodb", "redis" and "rediss" (Redis over TLS) are both registered with IANA as provisional schemes, while "mongodb+srv" is not - so there is no consistent precedent either way. Given the above, I see three options: 1. Drop the +srv URI scheme entirely. Keep only the srvhost= keyword parameter. Smallest API surface, no compatibility concerns for any driver. No URI form to use DNS SRV at all. srvhost=cluster.example.com dbname=mydb target_session_attrs=read-write 2. Keep postgresql+srv:// / postgres+srv:// as proposed. Aligns with the mongodb+srv:// precedent and reads naturally; a single cluster name in the authority is unaffected by Go's stricter parser, but driver authors take on a small risk for any future tightening. postgresql+srv://[email protected]/mydb?target_session_attrs=read-write postgres+srv://[email protected]/mydb 3. Some other shorthand (pgsrv://). Avoids "+" entirely but invents yet another name with no clear advantage. pgsrv://[email protected]/mydb?target_session_attrs=read-write My slight preference is (1). The srvhost= parameter alone expresses intent unambiguously and avoids opening a second scheme namespace that every driver in the ecosystem would need to replicate. If we go with option (2), registering "postgresql+srv" as a provisional scheme with IANA (First Come First Served per RFC 7595 §7.1) would take minimal effort and give the scheme a stable reference. Interestingly, even "postgresql" itself is not in the IANA registry despite being in use for over 20 years, so registration is not a prerequisite — but it would be a nice step toward treating SRV as a first-class URI scheme on par with mongodb+srv. On the other note "redis" and "rediss" are both provisioned with IANA. WDYT? Best regards, Andrey Borodin. [0] https://github.com/jackc/pgx/pull/2538 [1] https://github.com/golang/go/issues/75859 [2] https://github.com/jackc/pgx/issues/2404 ^ permalink raw reply [nested|flat] 8+ messages in thread
* Re: Feature: Use DNS SRV records for connecting @ 2026-05-12 16:07 Jacob Champion <[email protected]> parent: Andrey Borodin <[email protected]> 0 siblings, 0 replies; 8+ messages in thread From: Jacob Champion @ 2026-05-12 16:07 UTC (permalink / raw) To: Andrey Borodin <[email protected]>; +Cc: Tom Lane <[email protected]>; Feike Steenbergen <[email protected]>; pgsql-hackers; Andres Freund <[email protected]> Hello! I'm speaking only on the design questions here; I have not looked at the patch implementation yet. On Tue, May 12, 2026 at 2:47 AM Andrey Borodin <[email protected]> wrote: > While preparing equivalent patches for pgx, pgjdbc and npgsql, Jack > Christensen (pgx maintainer) raised a concern [0] about the > postgres+srv:// URI scheme I proposed here. > > The issue is specific to Go's net/url package [1]. In Go 1.26 the URL > parser was tightened and broke parsing of HA connection strings of > the form postgresql://user@h1:1,h2:2/db (colons inside the authority > after a comma). The Go team addressed this by relaxing validation, > but only for the "postgres" and "postgresql" schemes - any new > scheme, including "postgres+srv", gets the strict modern parser. For > a single SRV name in the authority this is probably fine in practice, > but Jack reasonably points out that deviating from postgresql:// is > a compatibility bet without much upside. [2] So this doesn't answer your broader question, really, but I consider it a Very Good Thing that Go is enforcing stricter validation. If we choose to introduce a brand-new scheme, I think we should do it by the book and not carry over our oddities from 2012. > Per RFC 3986 §3.1 the "+" in a scheme name is syntactically legal. > Per RFC 7595 / BCP 35, schemes can be registered with IANA, but > neither "postgresql" nor "mongodb+srv" is in the IANA registry today, > so there is no formal gate to pass - just our own consensus on what > to ship. > > On a related note, "mongodb", "redis" and "rediss" (Redis over TLS) are > both registered with IANA as provisional schemes, while "mongodb+srv" is > not - so there is no consistent precedent either way. As a continuation of the above, I think it'd be good to ask for IETF review on any new scheme. The [scheme]+[variant]: form has some uptake, but there are also expert reviewer notes saying that some attempts might make things difficult for implementers (or others in the ecosystem). > Given the above, I see three options: > > 1. Drop the +srv URI scheme entirely. Keep only the srvhost= keyword > parameter. Smallest API surface, no compatibility concerns for > any driver. No URI form to use DNS SRV at all. Either way, we'll need to define the semantics of mixing this new mode with the existing parameters. I think that's likely to introduce its own subtle compatibility concerns if we don't design it up front. (Your proposal doesn't introduce this problem, but we never should have allowed query parameters to modify the prior components -- scheme, authority, path -- of the URI. I am strongly motivated to fix that, though I don't really want to derail SRV conversations too much with it.) > 2. Keep postgresql+srv:// / postgres+srv:// as proposed. Aligns with > the mongodb+srv:// precedent and reads naturally; a single cluster > name in the authority is unaffected by Go's stricter parser, but > driver authors take on a small risk for any future tightening. Can you elaborate on the additional risk? IMO, if a URI identifies a single resource (the cluster), it doesn't seem like anyone needs to support our comma pseudo-syntax anymore. Drivers that want to connect to multiple clusters can take multiple fully formed URIs, and define how to configure that in a way that makes sense for them. > 3. Some other shorthand (pgsrv://). Avoids "+" entirely but invents > yet another name with no clear advantage. > > pgsrv://[email protected]/mydb?target_session_attrs=read-write I guess I don't see what this accomplishes. The `+` is not the problem, right? -- I would additionally propose options 4 and 5 (which you don't need to feel obligated to give any thought to): 4. Use the new behavior opportunistically somehow, similar to how https: has continued to evolve the underlying protocol and DNS lookup without any scheme changes at all. Whether this is technically feasible without usability and security concerns... I don't know at this point. 5. Moonshot: break compatibility outright, and design a scheme that does exactly what we want in 2026. Strong URI semantics, enforcement of TLS, useful DNS semantics, etc., etc. Represents complete derailment of the thread. :) Thanks, --Jacob ^ permalink raw reply [nested|flat] 8+ messages in thread
* Re: Feature: Use DNS SRV records for connecting @ 2026-05-12 21:05 Zsolt Parragi <[email protected]> parent: Andrey Borodin <[email protected]> 1 sibling, 0 replies; 8+ messages in thread From: Zsolt Parragi @ 2026-05-12 21:05 UTC (permalink / raw) To: Andrey Borodin <[email protected]>; +Cc: Andres Freund <[email protected]>; Tom Lane <[email protected]>; Feike Steenbergen <[email protected]>; pgsql-hackers Hello! Shouldn't srvhost be freed in freePGconn? + /* + * If srvhost is set, validate mutual exclusivity with host/hostaddr and + * then resolve _postgresql._tcp.<srvhost> SRV records, populating + * conn->pghost and conn->pgport from the sorted results. This must + * happen before the host-array allocation below. + */ + if (conn->srvhost != NULL && conn->srvhost[0] != '\0') Shouldn't this also cover port? The current behavior with it seems inconsistent. > The resolved host list is sorted per RFC 2782 and injected into the > existing multi-host machinery before connhost[] is built, so > target_session_attrs, load_balance_hosts, and failover work on the > expanded list without any changes to PQconnectPoll. Doesn't RFC 2782 specifies a weighted random selection? The current code seems to be deterministically sorted by weight. RFC also says that weight=0 should be specially handled, it provides a detailed algorithm about the random selection method. ^ permalink raw reply [nested|flat] 8+ messages in thread
end of thread, other threads:[~2026-05-12 21:05 UTC | newest] Thread overview: 8+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2019-08-13 09:50 Feature: Use DNS SRV records for connecting Feike Steenbergen <[email protected]> 2019-08-13 10:21 ` Graham Leggett <[email protected]> 2019-08-13 14:43 ` Tom Lane <[email protected]> 2019-08-14 18:01 ` Andres Freund <[email protected]> 2026-04-22 09:48 ` Andrey Borodin <[email protected]> 2026-05-12 09:46 ` Andrey Borodin <[email protected]> 2026-05-12 16:07 ` Jacob Champion <[email protected]> 2026-05-12 21:05 ` Zsolt Parragi <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox