Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wFUCl-005BeN-2p for pgsql-hackers@arkaria.postgresql.org; Wed, 22 Apr 2026 09:49:13 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wFUCl-00CqDc-0G for pgsql-hackers@arkaria.postgresql.org; Wed, 22 Apr 2026 09:49:11 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wFUCk-00CqDU-1g for pgsql-hackers@lists.postgresql.org; Wed, 22 Apr 2026 09:49:10 +0000 Received: from forwardcorp1a.mail.yandex.net ([2a02:6b8:c0e:500:1:45:d181:df01]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1wFUCi-00000002S0r-0lUc for pgsql-hackers@postgresql.org; Wed, 22 Apr 2026 09:49:10 +0000 Received: from mail-nwsmtp-smtp-corp-main-66.iva.yp-c.yandex.net (mail-nwsmtp-smtp-corp-main-66.iva.yp-c.yandex.net [IPv6:2a02:6b8:c0c:bf1f:0:640:c739:0]) by forwardcorp1a.mail.yandex.net (Yandex) with ESMTPS id 000EFC01EB; Wed, 22 Apr 2026 12:49:04 +0300 (MSK) Received: from smtpclient.apple (unknown [2a02:6bf:8080:82f::1:14]) by mail-nwsmtp-smtp-corp-main-66.iva.yp-c.yandex.net (smtpcorp) with ESMTPSA id 0nPRNT0K30U0-EWLIO6Tk; Wed, 22 Apr 2026 12:49:03 +0300 X-Yandex-Fwd: 1 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yandex-team.ru; s=default; t=1776851344; bh=9Q4Pl3zQ0Jvj8UnVxkH9aPDno8cQKUIeAMaz01T9/gY=; h=References:To:Cc:In-Reply-To:Date:From:Message-Id:Subject; b=rkiUerkI8hDHfnSPX8TBX6myTT98oo1p78flKVf4o2bDy+W9CIyOAS0ZI2anWf886 AFUIQIc6ezbLqcwxySi8Wa2BiliN8TqUTD7WBgMX3PAvmqeP01fhFeakpgzPqQMVVg ou53zhVIUk1OvDem2e69XOk9k7JmbK57L4yObpCA= Authentication-Results: mail-nwsmtp-smtp-corp-main-66.iva.yp-c.yandex.net; dkim=pass header.i=@yandex-team.ru From: Andrey Borodin Message-Id: <8398C22D-429A-4980-9028-4F941F2B7483@yandex-team.ru> Content-Type: multipart/mixed; boundary="Apple-Mail=_AD10F7C8-2A32-4570-82CD-94B950B6CAA2" Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3864.500.181\)) Subject: Re: Feature: Use DNS SRV records for connecting Date: Wed, 22 Apr 2026 14:48:50 +0500 In-Reply-To: <20190814180143.62533ohqlaqcl7so@alap3.anarazel.de> Cc: Tom Lane , Feike Steenbergen , PostgreSQL mailing lists To: Andres Freund References: <7386.1565707387@sss.pgh.pa.us> <20190814180143.62533ohqlaqcl7so@alap3.anarazel.de> X-Mailer: Apple Mail (2.3864.500.181) List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --Apple-Mail=_AD10F7C8-2A32-4570-82CD-94B950B6CAA2 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=us-ascii > On 14 Aug 2019, at 23:01, Andres Freund wrote: >=20 > 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.) >=20 >> 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. >=20 > 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." 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=3Dread-wri= te" psql = "postgres+srv://cluster.example.com/mydb?target_session_attrs=3Dread-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. --Apple-Mail=_AD10F7C8-2A32-4570-82CD-94B950B6CAA2 Content-Disposition: attachment; filename=0001-libpq-Add-DNS-SRV-record-support-for-service-discove.patch Content-Type: application/octet-stream; x-unix-mode=0644; name="0001-libpq-Add-DNS-SRV-record-support-for-service-discove.patch" Content-Transfer-Encoding: quoted-printable =46rom=2030b8f2eac8dc8f8b31556e242db90329e8f392b2=20Mon=20Sep=2017=20= 00:00:00=202001=0AFrom:=20Andrey=20Borodin=20=0ADate:=20= Wed,=2022=20Apr=202026=2011:14:17=20+0500=0ASubject:=20[PATCH]=20libpq:=20= Add=20DNS=20SRV=20record=20support=20for=20service=20discovery=0A=0AAdd=20= a=20new=20connection=20parameter=20"srvhost"=20that=20causes=20libpq=20= to=20query=0A_postgresql._tcp.=20SRV=20records=20before=20= connecting,=20replacing=0Athe=20normal=20host=20list=20with=20the=20= sorted=20result.=20=20This=20allows=20a=20whole=0APostgreSQL=20cluster=20= to=20be=20addressed=20by=20a=20single=20DNS=20name.=0A=0ANew=20= connection=20options:=0A=20=20srvhost=3D=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20DNS=20domain=20for=20SRV=20lookup=20(env:=20PGSRVHOST)=0A= =20=20postgresql+srv:///=20URI=20shorthand=20for=20srvhost=0A= =20=20postgres+srv:///=20=20=20alias=0A=0ASRV=20records=20= are=20sorted=20per=20RFC=202782=20(priority=20ascending,=20weight=0A= descending)=20and=20injected=20into=20the=20existing=20multi-host=20= machinery=20before=0Aconnhost[]=20is=20built,=20so=20= target_session_attrs,=20load_balance_hosts,=20and=0Afailover=20all=20= work=20transparently=20on=20the=20expanded=20host=20list.=0A=0Asrvhost=20= is=20mutually=20exclusive=20with=20host=20and=20hostaddr.=20=20Multiple=20= hosts=0Ain=20a=20+srv=20URI=20are=20rejected=20because=20expansion=20is=20= DNS's=20job.=0A=0AOn=20POSIX,=20resolution=20uses=20res_query(3)=20+=20= dn_expand(3)=20to=20parse=20the=0ADNS=20wire=20format=20directly,=20= avoiding=20ns_initparse()=20which=20is=20absent=20on=0Amusl.=20=20On=20= Windows,=20DnsQuery()=20from=20windns.h=20is=20used=20instead.=0A= Platforms=20without=20either=20receive=20a=20clear=20error=20at=20= connect=20time.=0A=0AThe=20resolver=20is=20called=20from=20= pqConnectOptions2()=20before=20the=20connhost[]=0Aarray=20is=20= allocated,=20requiring=20no=20changes=20to=20PQconnectPoll=20or=20the=0A= connection=20state=20machine.=0A---=0A=20configure=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20|=20=2056=20= ++++=0A=20configure.ac=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20=20=20=20=20=20=20=20=20|=20=2011=20+=0A=20doc/src/sgml/libpq.sgml=20=20= =20=20=20=20=20=20=20=20=20=20=20=20=20|=20=2052=20+++=0A=20meson.build=20= =20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20= =20|=20=2020=20++=0A=20src/include/pg_config.h.in=20=20=20=20=20=20=20=20= =20=20=20=20|=20=20=203=20+=0A=20src/interfaces/libpq/Makefile=20=20=20=20= =20=20=20=20=20|=20=2013=20+-=0A=20src/interfaces/libpq/fe-connect-srv.c=20= |=20437=20++++++++++++++++++++++++++=0A=20= src/interfaces/libpq/fe-connect-srv.h=20|=20=2029=20++=0A=20= src/interfaces/libpq/fe-connect.c=20=20=20=20=20|=20=2083=20++++-=0A=20= src/interfaces/libpq/libpq-int.h=20=20=20=20=20=20|=20=20=205=20+=0A=20= src/interfaces/libpq/meson.build=20=20=20=20=20=20|=20=20=201=20+=0A=20= src/interfaces/libpq/t/007_srv.pl=20=20=20=20=20|=20159=20++++++++++=0A=20= 12=20files=20changed,=20863=20insertions(+),=206=20deletions(-)=0A=20= create=20mode=20100644=20src/interfaces/libpq/fe-connect-srv.c=0A=20= create=20mode=20100644=20src/interfaces/libpq/fe-connect-srv.h=0A=20= create=20mode=20100644=20src/interfaces/libpq/t/007_srv.pl=0A=0Adiff=20= --git=20a/configure=20b/configure=0Aindex=20f66c1054a7..175a0bfcaf=20= 100755=0A---=20a/configure=0A+++=20b/configure=0A@@=20-12615,6=20= +12615,62=20@@=20if=20test=20"$ac_res"=20!=3D=20no;=20then=20:=0A=20fi=0A= =20=0A=20=0A+{=20$as_echo=20"$as_me:${as_lineno-$LINENO}:=20checking=20= for=20library=20containing=20res_query"=20>&5=0A+$as_echo_n=20"checking=20= for=20library=20containing=20res_query...=20"=20>&6;=20}=0A+if=20= ${ac_cv_search_res_query+:}=20false;=20then=20:=0A+=20=20$as_echo_n=20= "(cached)=20"=20>&6=0A+else=0A+=20=20ac_func_search_save_LIBS=3D$LIBS=0A= +cat=20confdefs.h=20-=20<<_ACEOF=20>conftest.$ac_ext=0A+/*=20end=20= confdefs.h.=20=20*/=0A+=0A+#ifdef=20__cplusplus=0A+extern=20"C"=0A= +#endif=0A+char=20res_query=20();=0A+int=0A+main=20()=0A+{=0A+return=20= res_query=20();=0A+=20=20;=0A+=20=20return=200;=0A+}=0A+_ACEOF=0A+for=20= ac_lib=20in=20''=20resolv;=20do=0A+=20=20if=20test=20-z=20"$ac_lib";=20= then=0A+=20=20=20=20ac_res=3D"none=20required"=0A+=20=20else=0A+=20=20=20= =20ac_res=3D-l$ac_lib=0A+=20=20=20=20LIBS=3D"-l$ac_lib=20=20= $ac_func_search_save_LIBS"=0A+=20=20fi=0A+=20=20if=20ac_fn_c_try_link=20= "$LINENO";=20then=20:=0A+=20=20ac_cv_search_res_query=3D$ac_res=0A+fi=0A= +rm=20-f=20core=20conftest.err=20conftest.$ac_objext=20\=0A+=20=20=20=20= conftest$ac_exeext=0A+=20=20if=20${ac_cv_search_res_query+:}=20false;=20= then=20:=0A+=20=20break=0A+fi=0A+done=0A+if=20= ${ac_cv_search_res_query+:}=20false;=20then=20:=0A+=0A+else=0A+=20=20= ac_cv_search_res_query=3Dno=0A+fi=0A+rm=20conftest.$ac_ext=0A= +LIBS=3D$ac_func_search_save_LIBS=0A+fi=0A+{=20$as_echo=20= "$as_me:${as_lineno-$LINENO}:=20result:=20$ac_cv_search_res_query"=20>&5=0A= +$as_echo=20"$ac_cv_search_res_query"=20>&6;=20}=0A= +ac_res=3D$ac_cv_search_res_query=0A+if=20test=20"$ac_res"=20!=3D=20no;=20= then=20:=0A+=20=20test=20"$ac_res"=20=3D=20"none=20required"=20||=20= LIBS=3D"$ac_res=20$LIBS"=0A+=0A+$as_echo=20"#define=20HAVE_RES_QUERY=20= 1"=20>>confdefs.h=0A+=0A+fi=0A+=0A+=0A=20if=20test=20"$with_readline"=20= =3D=20yes;=20then=0A=20=0A=20=0Adiff=20--git=20a/configure.ac=20= b/configure.ac=0Aindex=208d176bd346..36d836275b=20100644=0A---=20= a/configure.ac=0A+++=20b/configure.ac=0A@@=20-1386,6=20+1386,17=20@@=20= AC_SEARCH_LIBS(backtrace_symbols,=20execinfo)=0A=20=0A=20= AC_SEARCH_LIBS(pthread_barrier_wait,=20pthread)=0A=20=0A+dnl=0A+dnl=20= DNS=20SRV=20record=20support=20in=20libpq.=0A+dnl=20On=20Linux=20and=20= some=20BSDs=20res_query()=20lives=20in=20libresolv;=20on=20macOS=20it=20= is=0A+dnl=20available=20from=20the=20system=20library=20without=20any=20= extra=20-l=20flag.=0A+dnl=20AC_SEARCH_LIBS=20handles=20both=20cases:=20= it=20tries=20libc=20first,=20then=20libresolv.=0A+dnl=0A= +AC_SEARCH_LIBS(res_query,=20resolv,=0A+=20=20= [AC_DEFINE([HAVE_RES_QUERY],=201,=0A+=20=20=20=20=20=20=20=20=20=20=20=20= =20[Define=20to=201=20if=20you=20have=20the=20res_query()=20DNS=20= resolver=20function.])])=0A+AC_CHECK_HEADERS([arpa/nameser.h=20= resolv.h])=0A+=0A=20if=20test=20"$with_readline"=20=3D=20yes;=20then=0A=20= =20=20PGAC_CHECK_READLINE=0A=20=20=20if=20test=20= x"$pgac_cv_check_readline"=20=3D=20x"no";=20then=0Adiff=20--git=20= a/doc/src/sgml/libpq.sgml=20b/doc/src/sgml/libpq.sgml=0Aindex=20= 0a19c2b553..db3e669a74=20100644=0A---=20a/doc/src/sgml/libpq.sgml=0A+++=20= b/doc/src/sgml/libpq.sgml=0A@@=20-1129,6=20+1129,48=20@@=20= postgresql://%2Fvar%2Flib%2Fpostgresql/dbname=0A=20=20=20=20=20The=20= currently=20recognized=20parameter=20key=20words=20are:=0A=20=0A=20=20=20= =20=20=0A+=20=20=20=20=20=0A+=20=20=20=20=20=20= srvhost=0A+=20=20=20=20=20=20=0A= +=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20DNS=20domain=20= name=20used=20for=20SRV-based=20service=20discovery=20(RFC=202782).=0A+=20= =20=20=20=20=20=20=20When=20set,=20libpq=20queries=20DNS=20for=0A+=20=20=20= =20=20=20=20=20= _postgresql._tcp.srvhost=0A= +=20=20=20=20=20=20=20=20SRV=20records=20and=20derives=20the=20list=20of=20= hosts=20and=20ports=20to=20try=20from=20the=0A+=20=20=20=20=20=20=20=20= sorted=20result.=20=20Records=20are=20ordered=20by=20priority=20= (ascending)=20then=0A+=20=20=20=20=20=20=20=20weight=20(descending),=20= exactly=20as=20specified=20in=20RFC=202782.=0A+=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20This=20= parameter=20is=20mutually=20exclusive=20with=20host=20= and=0A+=20=20=20=20=20=20=20=20hostaddr.=20=20It=20= works=20together=20with=20all=20other=0A+=20=20=20=20=20=20=20=20= connection=20parameters,=20including=20= target_session_attrs=0A+=20=20=20=20=20=20=20=20and=20= load_balance_hosts,=20which=20are=20applied=20to=20= the=0A+=20=20=20=20=20=20=20=20expanded=20host=20list=20after=20DNS=20= resolution.=0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=20The=20environment=20variable=20= equivalent=20is=20PGSRVHOST.=0A+=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=20=0A+=20=20=20=20=20=20=20=20The=20= postgresql+srv://=20and=0A+=20=20=20=20=20=20=20=20= postgres+srv://=20URI=20schemes=20are=20a=20shorthand=20= for=0A+=20=20=20=20=20=20=20=20setting=20srvhost=20= from=20the=20URI=20host=20part:=0A+=0A+=0A= +postgresql+srv://cluster.example.com/mydb=0A+=0A+=0A+=20=20=20=20=20=20=20=20is=20= equivalent=20to:=0A+=0A+=0A= +postgresql:///mydb?srvhost=3Dclus= ter.example.com=0A+=0A+=0A+=20=20=20=20=20=20=20= =20Multiple=20hosts=20are=20not=20permitted=20in=20a=20= +srv=20URI=0A+=20=20=20=20=20=20=20=20because=20the=20= host=20expansion=20is=20performed=20by=20DNS.=0A+=20=20=20=20=20=20=20= =0A+=20=20=20=20=20=20=0A+=20=20=20=20=20= =0A+=0A=20=20=20=20=20=20=0A=20=20=20=20=20=20=20= host=0A=20=20=20=20=20=20=20=0A= @@=20-9060,6=20+9102,16=20@@=20myEventProc(PGEventId=20evtId,=20void=20= *evtInfo,=20void=20*passThrough)=0A=20=20=20=20information=20into=20= simple=20client=20applications,=20for=20example.=0A=20=0A=20=20=20=20= =0A+=20=20=20=20=0A+=20=20=20=20=20=0A+=20=20= =20=20=20=20=0A+=20=20=20=20=20=20=20= PGSRVHOST=0A+=20=20=20=20=20=20= =0A+=20=20=20=20=20=20PGSRVHOST=20behaves=20= the=20same=20as=20the=20=20connection=20parameter.=0A+=20=20=20= =20=20=0A+=20=20=20=20=0A+=0A=20=20=20=20=20=0A= =20=20=20=20=20=20=0A=20=20=20=20=20=20=20=0Adiff=20= --git=20a/meson.build=20b/meson.build=0Aindex=2020b887f1a1..76533e7f45=20= 100644=0A---=20a/meson.build=0A+++=20b/meson.build=0A@@=20-376,6=20= +376,8=20@@=20elif=20host_system=20=3D=3D=20'windows'=0A=20=20=20= secur32_dep=20=3D=20cc.find_library('secur32',=20required:=20true)=0A=20=20= =20backend_deps=20+=3D=20secur32_dep=0A=20=20=20libpq_deps=20+=3D=20= secur32_dep=0A+=20=20#=20DnsQuery()=20for=20SRV=20record=20lookup=20on=20= Windows=0A+=20=20libpq_deps=20+=3D=20cc.find_library('dnsapi',=20= required:=20true)=0A=20=0A=20=20=20postgres_inc_d=20+=3D=20= 'src/include/port/win32'=0A=20=20=20if=20cc.get_id()=20=3D=3D=20'msvc'=0A= @@=20-1851,6=20+1853,24=20@@=20endif=0A=20=0A=20=0A=20=0A= +###############################################################=0A+#=20= DNS=20SRV=20support=20(libpq)=0A= +###############################################################=0A+=0A= +#=20res_query()=20and=20dn_expand()=20are=20used=20by=20= fe-connect-srv.c=20to=20resolve=20SRV=0A+#=20records.=20=20On=20most=20= POSIX=20systems=20they=20live=20in=20libresolv;=20on=20macOS=20they=20= are=0A+#=20in=20the=20system=20library=20but=20still=20require=20= explicit=20linking=20with=20libresolv.=0A+if=20host_machine.system()=20= !=3D=20'windows'=0A+=20=20resolv_dep=20=3D=20cc.find_library('resolv',=20= required:=20false)=0A+=20=20if=20resolv_dep.found()=0A+=20=20=20=20= cdata.set('HAVE_RES_QUERY',=201)=0A+=20=20=20=20libpq_deps=20+=3D=20= resolv_dep=0A+=20=20elif=20cc.has_function('res_query')=0A+=20=20=20=20= cdata.set('HAVE_RES_QUERY',=201)=0A+=20=20endif=0A+endif=0A+=0A+=0A=20= ###############################################################=0A=20#=20= Compiler=20tests=0A=20= ###############################################################=0Adiff=20= --git=20a/src/include/pg_config.h.in=20b/src/include/pg_config.h.in=0A= index=204f8113c144..40dbd52cff=20100644=0A---=20= a/src/include/pg_config.h.in=0A+++=20b/src/include/pg_config.h.in=0A@@=20= -204,6=20+204,9=20@@=0A=20/*=20Define=20to=201=20if=20you=20have=20the=20= =20header=20file.=20*/=0A=20#undef=20HAVE_IFADDRS_H=0A=20=0A= +/*=20Define=20to=201=20if=20you=20have=20the=20`res_query'=20DNS=20= resolver=20function.=20*/=0A+#undef=20HAVE_RES_QUERY=0A+=0A=20/*=20= Define=20to=201=20if=20you=20have=20the=20`inet_aton'=20function.=20*/=0A= =20#undef=20HAVE_INET_ATON=0A=20=0Adiff=20--git=20= a/src/interfaces/libpq/Makefile=20b/src/interfaces/libpq/Makefile=0A= index=200963995eed..4e20645543=20100644=0A---=20= a/src/interfaces/libpq/Makefile=0A+++=20b/src/interfaces/libpq/Makefile=0A= @@=20-34,6=20+34,7=20@@=20OBJS=20=3D=20\=0A=20=09fe-auth-scram.o=20\=0A=20= =09fe-cancel.o=20\=0A=20=09fe-connect.o=20\=0A+=09fe-connect-srv.o=20\=0A= =20=09fe-exec.o=20\=0A=20=09fe-lobj.o=20\=0A=20=09fe-misc.o=20\=0A@@=20= -88,11=20+89,21=20@@=20endif=0A=20SHLIB_LINK_INTERNAL=20=3D=20= -lpgcommon_shlib=20-lpgport_shlib=0A=20ifneq=20($(PORTNAME),=20win32)=0A=20= SHLIB_LINK=20+=3D=20$(filter=20-lcrypt=20-ldes=20-lcom_err=20-lcrypto=20= -lk5crypto=20-lkrb5=20-lgssapi_krb5=20-lgss=20-lgssapi=20-lssl=20= -lsocket=20-lnsl=20-lresolv=20-lintl=20-ldl=20-lm,=20$(LIBS))=20= $(LDAP_LIBS_FE)=20$(PTHREAD_LIBS)=0A+#=20DNS=20SRV:=20link=20with=20= libresolv=20if=20the=20library=20exists=20and=20-lresolv=20is=20not=0A+#=20= already=20in=20SHLIB_LINK=20(e.g.=20because=20configure=20added=20it=20= to=20LIBS).=0A+#=20On=20Linux/glibc=20res_query=20lives=20in=20= libresolv;=20on=20macOS=20it=20is=20in=0A+#=20/usr/lib/libresolv.dylib.=20= =20The=20empty-source=20link=20test=20is=20portable.=0A+ifeq=20($(filter=20= -lresolv,=20$(SHLIB_LINK)),)=0A+HAVE_RESOLV_LIB=20:=3D=20$(shell=20echo=20= 'int=20main(void){return=200;}'=20|=20$(CC)=20-lresolv=20-o=20/dev/null=20= -x=20c=20-=202>/dev/null=20&&=20echo=20yes)=0A+ifeq=20= ($(HAVE_RESOLV_LIB),=20yes)=0A+SHLIB_LINK=20+=3D=20-lresolv=0A+endif=0A= +endif=0A=20else=0A=20SHLIB_LINK=20+=3D=20$(filter=20-lcrypt=20-ldes=20= -lcom_err=20-lcrypto=20-lk5crypto=20-lkrb5=20-lgssapi32=20-lssl=20= -lsocket=20-lnsl=20-lresolv=20-lintl=20-lm=20$(PTHREAD_LIBS),=20$(LIBS))=20= $(LDAP_LIBS_FE)=0A=20endif=0A=20ifeq=20($(PORTNAME),=20win32)=0A= -SHLIB_LINK=20+=3D=20-lshell32=20-lws2_32=20-lsecur32=20$(filter=20= -lcomerr32=20-lkrb5_32,=20$(LIBS))=0A+SHLIB_LINK=20+=3D=20-lshell32=20= -lws2_32=20-lsecur32=20-ldnsapi=20$(filter=20-lcomerr32=20-lkrb5_32,=20= $(LIBS))=0A=20endif=0A=20SHLIB_PREREQS=20=3D=20submake-libpgport=0A=20=0A= diff=20--git=20a/src/interfaces/libpq/fe-connect-srv.c=20= b/src/interfaces/libpq/fe-connect-srv.c=0Anew=20file=20mode=20100644=0A= index=200000000000..c5b7faf827=0A---=20/dev/null=0A+++=20= b/src/interfaces/libpq/fe-connect-srv.c=0A@@=20-0,0=20+1,437=20@@=0A= +/*-----------------------------------------------------------------------= --=0A+=20*=0A+=20*=20fe-connect-srv.c=0A+=20*=09=20=20DNS=20SRV=20record=20= lookup=20for=20libpq=20service=20discovery.=0A+=20*=0A+=20*=20When=20a=20= connection=20string=20specifies=20"srvhost=3Dcluster.example.com"=20(or=20= the=0A+=20*=20equivalent=20URI=20= "postgresql+srv://cluster.example.com/"),=20libpq=20resolves=0A+=20*=20= _postgresql._tcp.=20SRV=20records,=20sorts=20them=20by=20= priority=20(ascending)=0A+=20*=20then=20weight=20(descending)=20per=20= RFC=202782,=20and=20expands=20the=20result=20into=20the=0A+=20*=20= conn->pghost=20and=20conn->pgport=20fields=20before=20= pqConnectOptions2()=20builds=20the=0A+=20*=20per-host=20connection=20= array.=20=20All=20existing=20multi-host=20machinery=20(failover,=0A+=20*=20= target_session_attrs,=20load_balance_hosts)=20then=20works=20unchanged.=0A= +=20*=0A+=20*=20Platform=20support:=0A+=20*=20=20=20-=20POSIX=20systems=20= with=20res_query(3):=20Linux=20(glibc),=20macOS,=20*BSD,=20Solaris.=0A+=20= *=20=20=20=20=20Guarded=20by=20HAVE_RES_QUERY=20which=20is=20set=20by=20= configure=20/=20meson.=0A+=20*=20=20=20-=20Windows:=20DnsQuery()=20from=20= windns.h,=20linked=20with=20Dnsapi.lib.=0A+=20*=20=20=20-=20All=20other=20= platforms:=20srvhost=20is=20rejected=20at=20connection=20time=20with=20= an=0A+=20*=20=20=20=20=20informative=20error=20message.=0A+=20*=0A+=20*=20= Copyright=20(c)=202026,=20PostgreSQL=20Global=20Development=20Group=0A+=20= *=0A+=20*=20IDENTIFICATION=0A+=20*=09=20=20= src/interfaces/libpq/fe-connect-srv.c=0A+=20*=0A+=20= *-------------------------------------------------------------------------= =0A+=20*/=0A+=0A+#include=20"postgres_fe.h"=0A+=0A+#include=20= "common/int.h"=0A+#include=20"fe-connect-srv.h"=0A+#include=20= "libpq-int.h"=0A+#include=20"pqexpbuffer.h"=0A+=0A+/*=20----------=0A+=20= *=20Internal=20SRV=20record=20representation.=0A+=20*=20----------=0A+=20= */=0A+typedef=20struct=0A+{=0A+=09uint16_t=09priority;=0A+=09uint16_t=09= weight;=0A+=09uint16_t=09port;=0A+=09char=09=09target[NI_MAXHOST];=0A+}=20= SRVRecord;=0A+=0A+/*=20Sort:=20lower=20priority=20first;=20higher=20= weight=20first=20within=20same=20priority.=20*/=0A+static=20int=0A= +compareSRVRecords(const=20void=20*a,=20const=20void=20*b)=0A+{=0A+=09= const=20SRVRecord=20*sa=20=3D=20(const=20SRVRecord=20*)=20a;=0A+=09const=20= SRVRecord=20*sb=20=3D=20(const=20SRVRecord=20*)=20b;=0A+=0A+=09if=20= (sa->priority=20!=3D=20sb->priority)=0A+=09=09return=20= pg_cmp_u16(sa->priority,=20sb->priority);=0A+=09return=20= pg_cmp_u16(sb->weight,=20sa->weight);=0A+}=0A+=0A+/*=0A+=20*=20Build=20= conn->pghost=20and=20conn->pgport=20as=20comma-separated=20strings=20= from=20a=0A+=20*=20sorted=20SRVRecord=20array.=20=20Returns=20true=20on=20= success;=20on=20OOM=20sets=20the=0A+=20*=20connection=20error=20and=20= returns=20false.=0A+=20*/=0A+static=20bool=0A+srv_build_host_port(PGconn=20= *conn,=20SRVRecord=20*records,=20int=20nrecords)=0A+{=0A+=09= PQExpBufferData=20hostbuf;=0A+=09PQExpBufferData=20portbuf;=0A+=0A+=09= initPQExpBuffer(&hostbuf);=0A+=09initPQExpBuffer(&portbuf);=0A+=0A+=09= for=20(int=20i=20=3D=200;=20i=20<=20nrecords;=20i++)=0A+=09{=0A+=09=09if=20= (i=20>=200)=0A+=09=09{=0A+=09=09=09appendPQExpBufferChar(&hostbuf,=20= ',');=0A+=09=09=09appendPQExpBufferChar(&portbuf,=20',');=0A+=09=09}=0A+=09= =09appendPQExpBufferStr(&hostbuf,=20records[i].target);=0A+=09=09= appendPQExpBuffer(&portbuf,=20"%u",=20records[i].port);=0A+=09}=0A+=0A+=09= if=20(PQExpBufferDataBroken(hostbuf)=20||=20= PQExpBufferDataBroken(portbuf))=0A+=09{=0A+=09=09= termPQExpBuffer(&hostbuf);=0A+=09=09termPQExpBuffer(&portbuf);=0A+=09=09= libpq_append_conn_error(conn,=20"out=20of=20memory");=0A+=09=09return=20= false;=0A+=09}=0A+=0A+=09/*=0A+=09=20*=20Replace=20conn->pghost=20and=20= conn->pgport=20with=20the=20SRV-derived=20values.=0A+=09=20*=20The=20= originals=20were=20either=20NULL=20or=20empty=20(caller=20verified),=20= but=20free=0A+=09=20*=20them=20defensively=20before=20overwriting.=0A+=09= =20*/=0A+=09free(conn->pghost);=0A+=09conn->pghost=20=3D=20= strdup(hostbuf.data);=0A+=09free(conn->pgport);=0A+=09conn->pgport=20=3D=20= strdup(portbuf.data);=0A+=0A+=09termPQExpBuffer(&hostbuf);=0A+=09= termPQExpBuffer(&portbuf);=0A+=0A+=09if=20(!conn->pghost=20||=20= !conn->pgport)=0A+=09{=0A+=09=09libpq_append_conn_error(conn,=20"out=20= of=20memory");=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09return=20true;=0A= +}=0A+=0A+=0A+/*=20= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=0A+=20*=20= Platform-specific=20implementations=0A+=20*=20= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=20*/=0A+=0A= +#ifdef=20WIN32=0A+=0A+/*=0A+=20*=20Windows=20implementation=20using=20= DnsQuery()=20from=20windns.h.=0A+=20*=20Link=20with=20-ldnsapi=20= (Dnsapi.lib).=0A+=20*/=0A+#include=20=0A+=0A+bool=0A= +pqLookupSRVHosts(PGconn=20*conn)=0A+{=0A+=09char=09=09qname[NI_MAXHOST=20= +=2030];=0A+=09PDNS_RECORD=20pDnsRecord=20=3D=20NULL;=0A+=09PDNS_RECORD=20= pRec;=0A+=09SRVRecord=20=20*records=20=3D=20NULL;=0A+=09int=09=09=09= nrecords=20=3D=200;=0A+=09int=09=09=09maxrecords=20=3D=200;=0A+=09= DNS_STATUS=09status;=0A+=09bool=09=09ok;=0A+=0A+=09snprintf(qname,=20= sizeof(qname),=20"_postgresql._tcp.%s",=20conn->srvhost);=0A+=0A+=09= status=20=3D=20DnsQuery_A(qname,=20DNS_TYPE_SRV,=20DNS_QUERY_STANDARD,=0A= +=09=09=09=09=09=09NULL,=20&pDnsRecord,=20NULL);=0A+=09if=20(status=20!=3D= =20ERROR_SUCCESS)=0A+=09{=0A+=09=09libpq_append_conn_error(conn,=0A+=09=09= =09=09=09=09=09=09"DNS=20SRV=20lookup=20failed=20for=20\"%s\":=20error=20= code=20%lu",=0A+=09=09=09=09=09=09=09=09qname,=20(unsigned=20long)=20= status);=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09/*=20Collect=20SRV=20= records=20from=20the=20linked=20list=20*/=0A+=09for=20(pRec=20=3D=20= pDnsRecord;=20pRec=20!=3D=20NULL;=20pRec=20=3D=20pRec->pNext)=0A+=09{=0A= +=09=09SRVRecord=20=20*tmp;=0A+=09=09SRVRecord=20=20*rec;=0A+=09=09= size_t=09=09tlen;=0A+=0A+=09=09if=20(pRec->wType=20!=3D=20DNS_TYPE_SRV)=0A= +=09=09=09continue;=0A+=0A+=09=09if=20(nrecords=20>=3D=20maxrecords)=0A+=09= =09{=0A+=09=09=09maxrecords=20=3D=20maxrecords=20?=20maxrecords=20*=202=20= :=204;=0A+=09=09=09tmp=20=3D=20realloc(records,=20maxrecords=20*=20= sizeof(SRVRecord));=0A+=09=09=09if=20(!tmp)=0A+=09=09=09{=0A+=09=09=09=09= DnsRecordListFree(pDnsRecord,=20DnsFreeRecordList);=0A+=09=09=09=09= free(records);=0A+=09=09=09=09libpq_append_conn_error(conn,=20"out=20of=20= memory");=0A+=09=09=09=09return=20false;=0A+=09=09=09}=0A+=09=09=09= records=20=3D=20tmp;=0A+=09=09}=0A+=0A+=09=09rec=20=3D=20= &records[nrecords];=0A+=09=09rec->priority=20=3D=20= pRec->Data.SRV.wPriority;=0A+=09=09rec->weight=20=3D=20= pRec->Data.SRV.wWeight;=0A+=09=09rec->port=20=3D=20pRec->Data.SRV.wPort;=0A= +=0A+=09=09/*=0A+=09=09=20*=20DnsQuery_A=20returns=20pNameTarget=20as=20= a=20plain=20ANSI=20string=20(PSTR);=0A+=09=09=20*=20no=20wide-char=20= conversion=20needed.=0A+=09=09=20*/=0A+=09=09strlcpy(rec->target,=20= pRec->Data.SRV.pNameTarget,=20sizeof(rec->target));=0A+=0A+=09=09/*=20= Strip=20trailing=20dot=20from=20FQDN=20*/=0A+=09=09tlen=20=3D=20= strlen(rec->target);=0A+=09=09if=20(tlen=20>=200=20&&=20rec->target[tlen=20= -=201]=20=3D=3D=20'.')=0A+=09=09=09rec->target[tlen=20-=201]=20=3D=20= '\0';=0A+=0A+=09=09nrecords++;=0A+=09}=0A+=0A+=09= DnsRecordListFree(pDnsRecord,=20DnsFreeRecordList);=0A+=0A+=09if=20= (nrecords=20=3D=3D=200)=0A+=09{=0A+=09=09libpq_append_conn_error(conn,=20= "no=20SRV=20records=20found=20for=20\"%s\"",=20qname);=0A+=09=09= free(records);=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09qsort(records,=20= nrecords,=20sizeof(SRVRecord),=20compareSRVRecords);=0A+=0A+=09ok=20=3D=20= srv_build_host_port(conn,=20records,=20nrecords);=0A+=0A+=09= free(records);=0A+=09return=20ok;=0A+}=0A+=0A+#elif=20= defined(HAVE_RES_QUERY)=0A+=0A+/*=0A+=20*=20POSIX=20implementation=20= using=20res_query()=20and=20manual=20DNS=20wire-format=20parsing.=0A+=20= *=0A+=20*=20HAVE_RES_QUERY=20is=20set=20by=20configure=20= (AC_SEARCH_LIBS)=20and=20meson=0A+=20*=20(cc.has_function=20/=20= find_library)=20when=20res_query()=20is=20available.=0A+=20*=0A+=20*=20= We=20avoid=20depending=20on=20ns_initparse()=20/=20ns_parserr()=20since=20= those=20symbols=0A+=20*=20are=20not=20available=20on=20all=20platforms=20= (e.g.=20musl=20libc).=20=20Instead=20we=20parse=0A+=20*=20the=20wire=20= format=20directly,=20using=20only=20dn_expand()=20for=20name=20= decompression=0A+=20*=20(which=20is=20universally=20available=20wherever=20= res_query()=20is).=0A+=20*/=0A+=0A+#include=20=0A= +#include=20=0A+#include=20=0A+=0A+/*=20DNS=20= wire-format=20constants=20(from=20RFC=201035=20/=20RFC=202782)=20*/=0A= +#ifndef=20T_SRV=0A+#define=20T_SRV=09=0933=09=09/*=20RFC=202782:=20= Service=20locator=20*/=0A+#endif=0A+#ifndef=20C_IN=0A+#define=20C_IN=09=09= 1=09=09/*=20the=20Internet=20*/=0A+#endif=0A+#ifndef=20HFIXEDSZ=0A= +#define=20HFIXEDSZ=0912=09=09/*=20DNS=20message=20header=20size=20*/=0A= +#endif=0A+=0A+/*=0A+=20*=20Skip=20a=20(possibly=20compressed)=20DNS=20= domain=20name=20starting=20at=20cp.=0A+=20*=20Returns=20bytes=20= consumed,=20or=20-1=20on=20error.=0A+=20*/=0A+static=20int=0A= +srv_skip_dname(const=20unsigned=20char=20*eom,=20const=20unsigned=20= char=20*cp)=0A+{=0A+=09const=20unsigned=20char=20*orig=20=3D=20cp;=0A+=0A= +=09while=20(cp=20<=20eom)=0A+=09{=0A+=09=09int=09=09=09n=20=3D=20*cp=20= &=200xFF;=0A+=0A+=09=09if=20(n=20=3D=3D=200)=0A+=09=09=09return=20(int)=20= ((cp=20-=20orig)=20+=201);=09/*=20root=20label=20*/=0A+=09=09if=20((n=20= &=200xC0)=20=3D=3D=200xC0)=0A+=09=09=09return=20(int)=20((cp=20-=20orig)=20= +=202);=09/*=202-byte=20pointer=20*/=0A+=09=09if=20((n=20&=200xC0)=20!=3D=20= 0)=0A+=09=09=09return=20-1;=09=09=09/*=20unknown=20label=20type=20*/=0A+=09= =09cp=20+=3D=20n=20+=201;=0A+=09}=0A+=09return=20-1;=09=09=09=09=09/*=20= truncated=20*/=0A+}=0A+=0A+bool=0A+pqLookupSRVHosts(PGconn=20*conn)=0A+{=0A= +=09char=09=09qname[NI_MAXHOST=20+=2030];=0A+=09unsigned=20char=20= answer[4096];=0A+=09int=09=09=09len;=0A+=09const=20unsigned=20char=20= *cp;=0A+=09const=20unsigned=20char=20*eom;=0A+=09int=09=09=09qdcount,=0A= +=09=09=09=09ancount;=0A+=09SRVRecord=20=20*records=20=3D=20NULL;=0A+=09= int=09=09=09nrecords=20=3D=200;=0A+=09int=09=09=09maxrecords=20=3D=200;=0A= +=09bool=09=09retval=20=3D=20false;=0A+=0A+=09snprintf(qname,=20= sizeof(qname),=20"_postgresql._tcp.%s",=20conn->srvhost);=0A+=0A+=09len=20= =3D=20res_query(qname,=20C_IN,=20T_SRV,=20answer,=20sizeof(answer));=0A+=09= if=20(len=20<=200)=0A+=09{=0A+=09=09/*=0A+=09=09=20*=20h_errno=20is=20= set=20by=20res_query=20on=20failure.=20=20HOST_NOT_FOUND=20and=0A+=09=09=20= *=20NO_DATA=20both=20indicate=20that=20the=20SRV=20RRset=20simply=20= doesn't=20exist.=0A+=09=09=20*/=0A+=09=09const=20char=20*reason;=0A+=0A+=09= =09switch=20(h_errno)=0A+=09=09{=0A+=09=09=09case=20HOST_NOT_FOUND:=0A+=09= =09=09=09reason=20=3D=20"host=20not=20found";=0A+=09=09=09=09break;=0A+=09= =09=09case=20NO_DATA:=0A+=09=09=09=09reason=20=3D=20"no=20SRV=20records=20= published";=0A+=09=09=09=09break;=0A+=09=09=09case=20TRY_AGAIN:=0A+=09=09= =09=09reason=20=3D=20"temporary=20DNS=20failure,=20try=20again";=0A+=09=09= =09=09break;=0A+=09=09=09default:=0A+=09=09=09=09reason=20=3D=20= hstrerror(h_errno);=0A+=09=09=09=09break;=0A+=09=09}=0A+=09=09= libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09"DNS=20SRV=20= lookup=20failed=20for=20\"%s\":=20%s",=0A+=09=09=09=09=09=09=09=09qname,=20= reason);=0A+=09=09return=20false;=0A+=09}=0A+=0A+=09if=20(len=20<=20= HFIXEDSZ)=0A+=09{=0A+=09=09libpq_append_conn_error(conn,=0A+=09=09=09=09=09= =09=09=09"DNS=20response=20too=20short=20for=20\"%s\"",=20qname);=0A+=09=09= return=20false;=0A+=09}=0A+=0A+=09eom=20=3D=20answer=20+=20len;=0A+=0A+=09= /*=0A+=09=20*=20Parse=20the=20DNS=20response=20header=20(RFC=201035=20= =C2=A74.1.1):=0A+=09=20*=20=20=20bytes=204-5:=20QDCOUNT,=20bytes=206-7:=20= ANCOUNT=0A+=09=20*/=0A+=09qdcount=20=3D=20(answer[4]=20<<=208)=20|=20= answer[5];=0A+=09ancount=20=3D=20(answer[6]=20<<=208)=20|=20answer[7];=0A= +=09cp=20=3D=20answer=20+=20HFIXEDSZ;=0A+=0A+=09/*=20Skip=20the=20= question=20section=20*/=0A+=09for=20(int=20i=20=3D=200;=20i=20<=20= qdcount;=20i++)=0A+=09{=0A+=09=09int=09=09=09n=20=3D=20= srv_skip_dname(eom,=20cp);=0A+=0A+=09=09if=20(n=20<=200)=0A+=09=09=09= goto=20parse_error;=0A+=09=09cp=20+=3D=20n=20+=204;=09=09=09/*=20name=20= +=20QTYPE=20+=20QCLASS=20*/=0A+=09=09if=20(cp=20>=20eom)=0A+=09=09=09= goto=20parse_error;=0A+=09}=0A+=0A+=09/*=20Parse=20the=20answer=20= section=20*/=0A+=09for=20(int=20i=20=3D=200;=20i=20<=20ancount;=20i++)=0A= +=09{=0A+=09=09int=09=09=09n;=0A+=09=09uint16_t=09rtype,=0A+=09=09=09=09=09= rdlen;=0A+=09=09char=09=09target[NI_MAXHOST];=0A+=09=09size_t=09=09tlen;=0A= +=0A+=09=09n=20=3D=20srv_skip_dname(eom,=20cp);=0A+=09=09if=20(n=20<=20= 0)=0A+=09=09=09goto=20parse_error;=0A+=09=09cp=20+=3D=20n;=0A+=0A+=09=09= /*=20Need=20TYPE(2)=20+=20CLASS(2)=20+=20TTL(4)=20+=20RDLENGTH(2)=20=3D=20= 10=20bytes=20*/=0A+=09=09if=20(cp=20+=2010=20>=20eom)=0A+=09=09=09goto=20= parse_error;=0A+=0A+=09=09rtype=20=3D=20(cp[0]=20<<=208)=20|=20cp[1];=0A= +=09=09rdlen=20=3D=20(cp[8]=20<<=208)=20|=20cp[9];=0A+=09=09cp=20+=3D=20= 10;=0A+=0A+=09=09if=20(cp=20+=20rdlen=20>=20eom)=0A+=09=09=09goto=20= parse_error;=0A+=0A+=09=09if=20(rtype=20=3D=3D=20T_SRV)=0A+=09=09{=0A+=09= =09=09/*=20SRV=20RDATA:=20priority(2)=20weight(2)=20port(2)=20= target(variable)=20*/=0A+=09=09=09if=20(rdlen=20<=207)=0A+=09=09=09=09= goto=20parse_error;=0A+=0A+=09=09=09n=20=3D=20dn_expand(answer,=20eom,=20= cp=20+=206,=20target,=20sizeof(target));=0A+=09=09=09if=20(n=20<=200)=0A= +=09=09=09=09goto=20parse_error;=0A+=0A+=09=09=09/*=20Strip=20trailing=20= dot=20from=20FQDN=20*/=0A+=09=09=09tlen=20=3D=20strlen(target);=0A+=09=09= =09if=20(tlen=20>=200=20&&=20target[tlen=20-=201]=20=3D=3D=20'.')=0A+=09=09= =09=09target[tlen=20-=201]=20=3D=20'\0';=0A+=0A+=09=09=09if=20(nrecords=20= >=3D=20maxrecords)=0A+=09=09=09{=0A+=09=09=09=09SRVRecord=20=20*tmp;=0A+=0A= +=09=09=09=09maxrecords=20=3D=20maxrecords=20?=20maxrecords=20*=202=20:=20= 4;=0A+=09=09=09=09tmp=20=3D=20realloc(records,=20maxrecords=20*=20= sizeof(SRVRecord));=0A+=09=09=09=09if=20(!tmp)=0A+=09=09=09=09{=0A+=09=09= =09=09=09libpq_append_conn_error(conn,=20"out=20of=20memory");=0A+=09=09=09= =09=09goto=20cleanup;=0A+=09=09=09=09}=0A+=09=09=09=09records=20=3D=20= tmp;=0A+=09=09=09}=0A+=0A+=09=09=09records[nrecords].priority=20=3D=20= (cp[0]=20<<=208)=20|=20cp[1];=0A+=09=09=09records[nrecords].weight=20=3D=20= (cp[2]=20<<=208)=20|=20cp[3];=0A+=09=09=09records[nrecords].port=20=3D=20= (cp[4]=20<<=208)=20|=20cp[5];=0A+=09=09=09= strlcpy(records[nrecords].target,=20target,=0A+=09=09=09=09=09= sizeof(records[nrecords].target));=0A+=09=09=09nrecords++;=0A+=09=09}=0A= +=0A+=09=09cp=20+=3D=20rdlen;=0A+=09}=0A+=0A+=09if=20(nrecords=20=3D=3D=20= 0)=0A+=09{=0A+=09=09libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09= =09"no=20SRV=20records=20found=20for=20\"%s\"",=20qname);=0A+=09=09goto=20= cleanup;=0A+=09}=0A+=0A+=09qsort(records,=20nrecords,=20= sizeof(SRVRecord),=20compareSRVRecords);=0A+=0A+=09retval=20=3D=20= srv_build_host_port(conn,=20records,=20nrecords);=0A+=09goto=20cleanup;=0A= +=0A+parse_error:=0A+=09libpq_append_conn_error(conn,=20"malformed=20DNS=20= response=20for=20\"%s\"",=20qname);=0A+=0A+cleanup:=0A+=09free(records);=0A= +=09return=20retval;=0A+}=0A+=0A+#else=09=09=09=09=09=09=09/*=20no=20SRV=20= support=20on=20this=20platform=20*/=0A+=0A+bool=0A= +pqLookupSRVHosts(PGconn=20*conn)=0A+{=0A+=09= libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09"\"srvhost\"=20is=20= not=20supported=20on=20this=20platform=20"=0A+=09=09=09=09=09=09=09"(DNS=20= SRV=20lookup=20requires=20res_query)");=0A+=09return=20false;=0A+}=0A+=0A= +#endif=09=09=09=09=09=09=09/*=20platform=20selection=20*/=0Adiff=20= --git=20a/src/interfaces/libpq/fe-connect-srv.h=20= b/src/interfaces/libpq/fe-connect-srv.h=0Anew=20file=20mode=20100644=0A= index=200000000000..d4117ccb0d=0A---=20/dev/null=0A+++=20= b/src/interfaces/libpq/fe-connect-srv.h=0A@@=20-0,0=20+1,29=20@@=0A= +/*-----------------------------------------------------------------------= --=0A+=20*=0A+=20*=20fe-connect-srv.h=0A+=20*=09=20=20DNS=20SRV=20record=20= lookup=20for=20libpq=20service=20discovery.=0A+=20*=0A+=20*=20Copyright=20= (c)=202026,=20PostgreSQL=20Global=20Development=20Group=0A+=20*=0A+=20*=20= IDENTIFICATION=0A+=20*=09=20=20src/interfaces/libpq/fe-connect-srv.h=0A+=20= *=0A+=20= *-------------------------------------------------------------------------= =0A+=20*/=0A+#ifndef=20FE_CONNECT_SRV_H=0A+#define=20FE_CONNECT_SRV_H=0A= +=0A+#include=20"libpq-int.h"=0A+=0A+/*=0A+=20*=20pqLookupSRVHosts=0A+=20= *=0A+=20*=20Resolve=20_postgresql._tcp.srvhost>=20SRV=20records=20= and=20store=20the=0A+=20*=20result=20in=20conn->pghost=20and=20= conn->pgport=20as=20comma-separated=20strings,=0A+=20*=20suitable=20for=20= consumption=20by=20pqConnectOptions2().=0A+=20*=0A+=20*=20Returns=20true=20= on=20success,=20false=20on=20error=20(error=20message=20set=20in=20= conn).=0A+=20*/=0A+extern=20bool=20pqLookupSRVHosts(PGconn=20*conn);=0A+=0A= +#endif=09=09=09=09=09=09=09/*=20FE_CONNECT_SRV_H=20*/=0Adiff=20--git=20= a/src/interfaces/libpq/fe-connect.c=20= b/src/interfaces/libpq/fe-connect.c=0Aindex=204272d386e6..1503fd6e36=20= 100644=0A---=20a/src/interfaces/libpq/fe-connect.c=0A+++=20= b/src/interfaces/libpq/fe-connect.c=0A@@=20-30,6=20+30,7=20@@=0A=20= #include=20"common/string.h"=0A=20#include=20"fe-auth.h"=0A=20#include=20= "fe-auth-oauth.h"=0A+#include=20"fe-connect-srv.h"=0A=20#include=20= "libpq-fe.h"=0A=20#include=20"libpq-int.h"=0A=20#include=20= "mb/pg_wchar.h"=0A@@=20-231,6=20+232,10=20@@=20static=20const=20= internalPQconninfoOption=20PQconninfoOptions[]=20=3D=20{=0A=20=09=09= "Database-Name",=20"",=2020,=0A=20=09offsetof(struct=20pg_conn,=20= dbName)},=0A=20=0A+=09{"srvhost",=20"PGSRVHOST",=20NULL,=20NULL,=0A+=09=09= "Database-SRV-Host",=20"",=2064,=0A+=09offsetof(struct=20pg_conn,=20= srvhost)},=0A+=0A=20=09{"host",=20"PGHOST",=20NULL,=20NULL,=0A=20=09=09= "Database-Host",=20"",=2040,=0A=20=09offsetof(struct=20pg_conn,=20= pghost)},=0A@@=20-454,6=20+459,9=20@@=20static=20const=20pg_fe_sasl_mech=20= *supported_sasl_mechs[]=20=3D=0A=20/*=20The=20connection=20URI=20must=20= start=20with=20either=20of=20the=20following=20designators:=20*/=0A=20= static=20const=20char=20uri_designator[]=20=3D=20"postgresql://";=0A=20= static=20const=20char=20short_uri_designator[]=20=3D=20"postgres://";=0A= +/*=20SRV=20URI=20variants:=20the=20host=20is=20treated=20as=20the=20SRV=20= domain,=20not=20a=20direct=20host=20*/=0A+static=20const=20char=20= srv_uri_designator[]=20=3D=20"postgresql+srv://";=0A+static=20const=20= char=20short_srv_uri_designator[]=20=3D=20"postgres+srv://";=0A=20=0A=20= static=20bool=20connectOptions1(PGconn=20*conn,=20const=20char=20= *conninfo);=0A=20static=20bool=20init_allowed_encryption_methods(PGconn=20= *conn);=0A@@=20-1258,6=20+1266,29=20@@=20pqConnectOptions2(PGconn=20= *conn)=0A=20{=0A=20=09int=09=09=09i;=0A=20=0A+=09/*=0A+=09=20*=20If=20= srvhost=20is=20set,=20validate=20mutual=20exclusivity=20with=20= host/hostaddr=20and=0A+=09=20*=20then=20resolve=20= _postgresql._tcp.=20SRV=20records,=20populating=0A+=09=20*=20= conn->pghost=20and=20conn->pgport=20from=20the=20sorted=20results.=20=20= This=20must=0A+=09=20*=20happen=20before=20the=20host-array=20allocation=20= below.=0A+=09=20*/=0A+=09if=20(conn->srvhost=20!=3D=20NULL=20&&=20= conn->srvhost[0]=20!=3D=20'\0')=0A+=09{=0A+=09=09if=20((conn->pghost=20= !=3D=20NULL=20&&=20conn->pghost[0]=20!=3D=20'\0')=20||=0A+=09=09=09= (conn->pghostaddr=20!=3D=20NULL=20&&=20conn->pghostaddr[0]=20!=3D=20= '\0'))=0A+=09=09{=0A+=09=09=09conn->status=20=3D=20CONNECTION_BAD;=0A+=09= =09=09libpq_append_conn_error(conn,=0A+=09=09=09=09=09=09=09=09=09= "cannot=20use=20\"srvhost\"=20together=20with=20\"host\"=20or=20= \"hostaddr\"");=0A+=09=09=09return=20false;=0A+=09=09}=0A+=09=09if=20= (!pqLookupSRVHosts(conn))=0A+=09=09{=0A+=09=09=09conn->status=20=3D=20= CONNECTION_BAD;=0A+=09=09=09return=20false;=0A+=09=09}=0A+=09}=0A+=0A=20=09= /*=0A=20=09=20*=20Allocate=20memory=20for=20details=20about=20each=20= host=20to=20which=20we=20might=20possibly=0A=20=09=20*=20try=20to=20= connect.=20=20For=20that,=20count=20the=20number=20of=20elements=20in=20= the=20hostaddr=0A@@=20-6359,6=20+6390,15=20@@=20= parse_connection_string(const=20char=20*connstr,=20PQExpBuffer=20= errorMessage,=0A=20static=20int=0A=20uri_prefix_length(const=20char=20= *connstr)=0A=20{=0A+=09/*=20Check=20SRV=20URI=20variants=20first=20= (longer=20prefixes=20before=20shorter)=20*/=0A+=09if=20(strncmp(connstr,=20= srv_uri_designator,=0A+=09=09=09=09sizeof(srv_uri_designator)=20-=201)=20= =3D=3D=200)=0A+=09=09return=20sizeof(srv_uri_designator)=20-=201;=0A+=0A= +=09if=20(strncmp(connstr,=20short_srv_uri_designator,=0A+=09=09=09=09= sizeof(short_srv_uri_designator)=20-=201)=20=3D=3D=200)=0A+=09=09return=20= sizeof(short_srv_uri_designator)=20-=201;=0A+=0A=20=09if=20= (strncmp(connstr,=20uri_designator,=0A=20=09=09=09=09= sizeof(uri_designator)=20-=201)=20=3D=3D=200)=0A=20=09=09return=20= sizeof(uri_designator)=20-=201;=0A@@=20-6910,6=20+6950,14=20@@=20= conninfo_uri_parse(const=20char=20*uri,=20PQExpBuffer=20errorMessage,=0A=20= =20*=0A=20=20*=20= postgresql://[user[:password]@][netloc][:port][,...][/dbname][?param1=3Dva= lue1&...]=0A=20=20*=0A+=20*=20The=20postgresql+srv://=20and=20= postgres+srv://=20URI=20schemes=20are=20also=20recognized:=0A+=20*=0A+=20= *=20= postgresql+srv://[user[:password]@][srvdomain][/dbname][?param1=3Dvalue1&.= ..]=0A+=20*=0A+=20*=20In=20the=20+srv=20form,=20the=20netloc=20is=20= interpreted=20as=20the=20SRV=20domain=20(stored=20in=0A+=20*=20the=20= "srvhost"=20connection=20parameter)=20rather=20than=20as=20a=20direct=20= host=20address.=0A+=20*=20Multiple=20netloc=20specifications=20are=20not=20= allowed=20in=20the=20+srv=20form.=0A+=20*=0A=20=20*=20Any=20of=20the=20= URI=20parts=20might=20use=20percent-encoding=20(%xy).=0A=20=20*/=0A=20= static=20bool=0A@@=20-6917,6=20+6965,7=20@@=20= conninfo_uri_parse_options(PQconninfoOption=20*options,=20const=20char=20= *uri,=0A=20=09=09=09=09=09=09=20=20=20PQExpBuffer=20errorMessage)=0A=20{=0A= =20=09int=09=09=09prefix_len;=0A+=09bool=09=09is_srv_uri;=0A=20=09char=09= =20=20=20*p;=0A=20=09char=09=20=20=20*buf=20=3D=20NULL;=0A=20=09char=09=20= =20=20*start;=0A@@=20-6944,8=20+6993,10=20@@=20= conninfo_uri_parse_options(PQconninfoOption=20*options,=20const=20char=20= *uri,=0A=20=09}=0A=20=09start=20=3D=20buf;=0A=20=0A-=09/*=20Skip=20the=20= URI=20prefix=20*/=0A+=09/*=20Skip=20the=20URI=20prefix=20and=20detect=20= if=20this=20is=20a=20+srv=20URI=20*/=0A=20=09prefix_len=20=3D=20= uri_prefix_length(uri);=0A+=09is_srv_uri=20=3D=20(prefix_len=20=3D=3D=20= sizeof(srv_uri_designator)=20-=201=20||=0A+=09=09=09=09=20=20prefix_len=20= =3D=3D=20sizeof(short_srv_uri_designator)=20-=201);=0A=20=09if=20= (prefix_len=20=3D=3D=200)=0A=20=09{=0A=20=09=09/*=20Should=20never=20= happen=20*/=0A@@=20-7096,10=20+7147,32=20@@=20= conninfo_uri_parse_options(PQconninfoOption=20*options,=20const=20char=20= *uri,=0A=20=09/*=20Save=20final=20values=20for=20host=20and=20port.=20*/=0A= =20=09if=20(PQExpBufferDataBroken(hostbuf)=20||=20= PQExpBufferDataBroken(portbuf))=0A=20=09=09goto=20cleanup;=0A-=09if=20= (hostbuf.data[0]=20&&=0A-=09=09!conninfo_storeval(options,=20"host",=20= hostbuf.data,=0A-=09=09=09=09=09=09=20=20=20errorMessage,=20false,=20= true))=0A-=09=09goto=20cleanup;=0A+=09if=20(hostbuf.data[0])=0A+=09{=0A+=09= =09if=20(is_srv_uri)=0A+=09=09{=0A+=09=09=09/*=0A+=09=09=09=20*=20For=20= postgresql+srv://=20URIs=20the=20netloc=20is=20the=20SRV=20domain,=20not=20= a=0A+=09=09=09=20*=20direct=20host=20address.=20=20Store=20it=20in=20= "srvhost"=20and=20reject=20multiple=0A+=09=09=09=20*=20hosts=20(commas)=20= since=20SRV=20already=20expands=20to=20multiple=20targets.=0A+=09=09=09=20= */=0A+=09=09=09if=20(strchr(hostbuf.data,=20',')=20!=3D=20NULL)=0A+=09=09= =09{=0A+=09=09=09=09libpq_append_error(errorMessage,=0A+=09=09=09=09=09=09= =09=09=20=20=20"multiple=20hosts=20are=20not=20allowed=20in=20a=20= postgresql+srv://=20URI");=0A+=09=09=09=09goto=20cleanup;=0A+=09=09=09}=0A= +=09=09=09if=20(!conninfo_storeval(options,=20"srvhost",=20hostbuf.data,=0A= +=09=09=09=09=09=09=09=09=20=20=20errorMessage,=20false,=20true))=0A+=09=09= =09=09goto=20cleanup;=0A+=09=09}=0A+=09=09else=0A+=09=09{=0A+=09=09=09if=20= (!conninfo_storeval(options,=20"host",=20hostbuf.data,=0A+=09=09=09=09=09= =09=09=09=20=20=20errorMessage,=20false,=20true))=0A+=09=09=09=09goto=20= cleanup;=0A+=09=09}=0A+=09}=0A=20=09if=20(portbuf.data[0]=20&&=0A=20=09=09= !conninfo_storeval(options,=20"port",=20portbuf.data,=0A=20=09=09=09=09=09= =09=20=20=20errorMessage,=20false,=20true))=0Adiff=20--git=20= a/src/interfaces/libpq/libpq-int.h=20b/src/interfaces/libpq/libpq-int.h=0A= index=2023de98290c..cbfeee6288=20100644=0A---=20= a/src/interfaces/libpq/libpq-int.h=0A+++=20= b/src/interfaces/libpq/libpq-int.h=0A@@=20-371,6=20+371,11=20@@=20= typedef=20struct=20pg_conn_host=0A=20struct=20pg_conn=0A=20{=0A=20=09/*=20= Saved=20values=20of=20connection=20options=20*/=0A+=09char=09=20=20=20= *srvhost;=09=09/*=20DNS=20SRV=20cluster=20domain=20for=20service=0A+=09=09= =09=09=09=09=09=09=20*=20discovery;=20when=20set,=20= _postgresql._tcp.=0A+=09=09=09=09=09=09=09=09=20*=20is=20= resolved=20at=20connect=20time=20and=20the=20result=0A+=09=09=09=09=09=09= =09=09=20*=20replaces=20pghost/pgport.=20=20Mutually=20exclusive=0A+=09=09= =09=09=09=09=09=09=20*=20with=20pghost=20and=20pghostaddr.=20*/=0A=20=09= char=09=20=20=20*pghost;=09=09=09/*=20the=20machine=20on=20which=20the=20= server=20is=20running,=0A=20=09=09=09=09=09=09=09=09=20*=20or=20a=20path=20= to=20a=20UNIX-domain=20socket,=20or=20a=0A=20=09=09=09=09=09=09=09=09=20= *=20comma-separated=20list=20of=20machines=20and/or=0Adiff=20--git=20= a/src/interfaces/libpq/meson.build=20b/src/interfaces/libpq/meson.build=0A= index=20b0ae72167a..b5bf3f1faa=20100644=0A---=20= a/src/interfaces/libpq/meson.build=0A+++=20= b/src/interfaces/libpq/meson.build=0A@@=20-6,6=20+6,7=20@@=20= libpq_sources=20=3D=20files(=0A=20=20=20'fe-auth.c',=0A=20=20=20= 'fe-cancel.c',=0A=20=20=20'fe-connect.c',=0A+=20=20'fe-connect-srv.c',=0A= =20=20=20'fe-exec.c',=0A=20=20=20'fe-lobj.c',=0A=20=20=20'fe-misc.c',=0A= diff=20--git=20a/src/interfaces/libpq/t/007_srv.pl=20= b/src/interfaces/libpq/t/007_srv.pl=0Anew=20file=20mode=20100644=0Aindex=20= 0000000000..79e50c9ed6=0A---=20/dev/null=0A+++=20= b/src/interfaces/libpq/t/007_srv.pl=0A@@=20-0,0=20+1,159=20@@=0A+#=20= Copyright=20(c)=202026,=20PostgreSQL=20Global=20Development=20Group=0A= +use=20strict;=0A+use=20warnings=20FATAL=20=3D>=20'all';=0A+=0A+use=20= PostgreSQL::Test::Utils;=0A+use=20PostgreSQL::Test::Cluster;=0A+use=20= Test::More;=0A+=0A+#=20This=20test=20exercises=20DNS=20SRV=20record=20= support=20in=20libpq:=0A+#=0A+#=20=20=201.=20URI=20parsing:=20= postgresql+srv://=20and=20postgres+srv://=20schemes=20store=20the=0A+#=20= =20=20=20=20=20netloc=20as=20"srvhost"=20rather=20than=20"host".=0A+#=20=20= =202.=20Error=20handling:=20multiple=20hosts=20in=20a=20+srv=20URI,=20= mixing=20srvhost=20with=20host.=0A+#=20=20=203.=20Live=20SRV=20lookup=20= against=20a=20running=20PostgreSQL=20cluster.=0A+#=0A+#=20Live=20lookup=20= is=20gated=20by=20PG_TEST_EXTRA=3Dsrv=20because=20it=20requires=20that=20= valid=0A+#=20SRV=20records=20exist=20in=20DNS.=20=20The=20test=20relies=20= on=20the=20SRV_HOST=20environment=0A+#=20variable=20(defaulting=20to=20= the=20value=20recommended=20in=20the=20PostgreSQL=20docs).=0A+=0A+=0A+#=20= --------------------------------------------------------------------------= =0A+#=20Part=201:=20URI=20parsing=20(no=20network=20required,=20uses=20= libpq_uri_regress)=0A+#=20= --------------------------------------------------------------------------= =0A+=0A+my=20@uri_tests=20=3D=20(=0A+=0A+=09#=20postgresql+srv://=20=E2=80= =94=20netloc=20becomes=20srvhost=0A+=09[=0A+=09=09= q{postgresql+srv://cluster.example.com/mydb},=0A+=09=09q{dbname=3D'mydb'=20= srvhost=3D'cluster.example.com'},=0A+=09=09q{},=0A+=09],=0A+=0A+=09#=20= postgres+srv://=20short=20form=0A+=09[=0A+=09=09= q{postgres+srv://cluster.example.com/mydb},=0A+=09=09q{dbname=3D'mydb'=20= srvhost=3D'cluster.example.com'},=0A+=09=09q{},=0A+=09],=0A+=0A+=09#=20= with=20user,=20extra=20params=0A+=09[=0A+=09=09= q{postgresql+srv://alice@cluster.example.com/mydb?target_session_attrs=3Dr= ead-write},=0A+=09=09q{user=3D'alice'=20dbname=3D'mydb'=20= srvhost=3D'cluster.example.com'=20target_session_attrs=3D'read-write'},=0A= +=09=09q{},=0A+=09],=0A+=0A+=09#=20multiple=20hosts=20must=20be=20= rejected=20for=20+srv=20URIs=0A+=09[=0A+=09=09= q{postgresql+srv://h1.example.com,h2.example.com/mydb},=0A+=09=09q{},=0A= +=09=09q{multiple=20hosts=20are=20not=20allowed=20in=20a=20= postgresql+srv://=20URI},=0A+=09],=0A+);=0A+=0A+foreach=20my=20$t=20= (@uri_tests)=0A+{=0A+=09my=20($uri,=20$want_out,=20$want_err)=20=3D=20= @$t;=0A+=0A+=09my=20($stdout,=20$stderr);=0A+=09IPC::Run::run=20[=20= 'libpq_uri_regress',=20$uri=20],=0A+=09=20=20'>'=20=3D>=20\$stdout,=0A+=09= =20=20'2>'=20=3D>=20\$stderr;=0A+=09chomp=20$stdout;=0A+=09chomp=20= $stderr;=0A+=0A+=09#=20Strip=20the=20trailing=20connection-type=20= annotation=20"(local)"/"(inet)"=20so=0A+=09#=20that=20the=20test=20is=20= not=20sensitive=20to=20socket-vs-TCP=20defaults.=0A+=09$stdout=20=3D~=20= s/\s+\(\w+\)$//;=0A+=0A+=09is($stdout,=20$want_out,=20"URI=20stdout:=20= $uri");=0A+=09like($stderr,=20qr/\Q$want_err\E/,=20"URI=20stderr:=20= $uri")=0A+=09=20=20if=20$want_err=20ne=20'';=0A+=09is($stderr,=20'',=20= "URI=20no=20stderr:=20$uri")=0A+=09=20=20if=20$want_err=20eq=20'';=0A+}=0A= +=0A+#=20= --------------------------------------------------------------------------= =0A+#=20Part=202:=20Live=20SRV=20lookup=20(optional,=20gated=20by=20= PG_TEST_EXTRA)=0A+#=20= --------------------------------------------------------------------------= =0A+=0A+my=20$do_live=20=3D=20$ENV{PG_TEST_EXTRA}=20&&=20= $ENV{PG_TEST_EXTRA}=20=3D~=20/\bsrv\b/;=0A+=0A+if=20(!$do_live)=0A+{=0A+=09= note=20'Skipping=20live=20SRV=20test=20(not=20enabled=20in=20= PG_TEST_EXTRA)';=0A+=09done_testing();=0A+=09exit=200;=0A+}=0A+=0A+#=20= The=20live=20test=20starts=20a=20PostgreSQL=20cluster=20on=20a=20local=20= port,=20publishes=20the=0A+#=20connection=20details=20via=20a=20mock=20= hosts-file=20trick=20(as=20done=20in=20004_load_balance_dns.pl),=0A+#=20= and=20verifies=20that=20a=20postgresql+srv://=20URI=20resolves=20and=20= connects.=0A+#=0A+#=20When=20running=20in=20CI,=20the=20SRV=20records=20= for=20$SRV_HOST=20must=20resolve=20to=20127.0.0.1=0A+#=20on=20port=20= $SRV_PORT.=0A+=0A+my=20$srv_host=20=3D=0A+=20=20$ENV{SRV_HOST}=20//=20= 'pg-srvtest';=20=20=20#=20DNS=20name=20whose=20SRV=20records=20point=20= here=0A+my=20$node=20=3D=20PostgreSQL::Test::Cluster->new('primary');=0A= +$node->init;=0A+$node->start;=0A+=0A+my=20$port=20=3D=20$node->port;=0A= +=0A+note=20"Testing=20SRV=20connection=20via=20srvhost=3D$srv_host=20= (expect=20port=20$port)";=0A+=0A+#=201.=20keyword=3Dvalue=20connection=20= string=0A+my=20($ret,=20$out,=20$err);=0A+$ret=20=3D=20$node->psql(=0A+=09= 'postgres',=0A+=09'SELECT=201',=0A+=09stdout=20=20=20=20=20=20=20=20=20= =3D>=20\$out,=0A+=09stderr=20=20=20=20=20=20=20=20=20=3D>=20\$err,=0A+=09= extra_params=20=20=20=3D>=20[=20'-d',=20"srvhost=3D$srv_host"=20],=0A+=09= on_error_stop=20=20=3D>=200);=0A+is($ret,=200,=20"srvhost=3D=20keyword=20= connects=20via=20SRV")=0A+=20=20or=20diag("stderr:=20$err");=0A+=0A+#=20= 2.=20URI=20form=0A+$ret=20=3D=20$node->psql(=0A+=09'postgres',=0A+=09= 'SELECT=201',=0A+=09stdout=20=20=20=20=20=20=20=20=3D>=20\$out,=0A+=09= stderr=20=20=20=20=20=20=20=20=3D>=20\$err,=0A+=09extra_params=20=20=3D>=20= [=20'-d',=20"postgresql+srv://$srv_host/postgres"=20],=0A+=09= on_error_stop=20=3D>=200);=0A+is($ret,=200,=20"postgresql+srv://=20URI=20= connects=20via=20SRV")=0A+=20=20or=20diag("stderr:=20$err");=0A+=0A+#=20= 3.=20target_session_attrs=3Dany=20works=20with=20SRV=0A+$ret=20=3D=20= $node->psql(=0A+=09'postgres',=0A+=09'SELECT=201',=0A+=09stdout=20=20=20=20= =20=20=20=20=3D>=20\$out,=0A+=09stderr=20=20=20=20=20=20=20=20=3D>=20= \$err,=0A+=09extra_params=20=20=3D>=20[=0A+=09=09'-d',=0A+=09=09= "postgresql+srv://$srv_host/postgres?target_session_attrs=3Dany",=0A+=09= ],=0A+=09on_error_stop=20=3D>=200);=0A+is($ret,=200,=20= "postgresql+srv://=20with=20target_session_attrs=3Dany")=0A+=20=20or=20= diag("stderr:=20$err");=0A+=0A+#=204.=20Mixing=20srvhost=20and=20host=20= is=20rejected=0A+$ret=20=3D=20$node->psql(=0A+=09'postgres',=0A+=09= 'SELECT=201',=0A+=09stdout=20=20=20=20=20=20=20=20=3D>=20\$out,=0A+=09= stderr=20=20=20=20=20=20=20=20=3D>=20\$err,=0A+=09extra_params=20=20=3D>=20= [=20'-d',=20"srvhost=3D$srv_host=20host=3Dlocalhost"=20],=0A+=09= on_error_stop=20=3D>=200);=0A+isnt($ret,=200,=20"srvhost=20+=20host=3D=20= is=20rejected");=0A+like($err,=20qr/cannot=20use=20"srvhost"=20together=20= with=20"host"/,=0A+=09=20"correct=20error=20for=20srvhost=20+=20host=20= conflict");=0A+=0A+$node->stop;=0A+=0A+done_testing();=0A--=20=0A2.50.1=20= (Apple=20Git-155)=0A=0A= --Apple-Mail=_AD10F7C8-2A32-4570-82CD-94B950B6CAA2--