Skip to content

pgconn: support hostaddr in connection strings - #2656

Open
Leewwp wants to merge 3 commits into
jackc:masterfrom
Leewwp:fix/hostaddr-support
Open

Leewwp wants to merge 3 commits into
jackc:masterfrom
Leewwp:fix/hostaddr-support

Conversation

@Leewwp

@Leewwp Leewwp commented Sep 18, 2026 •

Copy link
Copy Markdown

Summary

hostaddr names the address to dial while host keeps its meaning for server identity — SNI and certificate verification. pgx did neither: the value fell through to the run-time parameters, so the server rejected it with unrecognized configuration parameter "hostaddr".

psql "postgresql://someuser@hostname.not.used:54320/somedb?sslmode=require&host=hostname.for.sni&hostaddr=192.168.1.100"

This treats hostaddr as a connection string field alongside host and port, and dials it instead of resolving the host name.

Changes

  • pgconn/config.go: hostaddr is added to notRuntimeParams, so ParseConfig consumes it instead of forwarding it to the server. Config and FallbackConfig gain a HostAddr field.
  • host and hostaddr are parallel lists, and hostaddr decides how many connections are described, as in pqConnectOptions2. Unlike port, a single address is not broadcast over several hosts: when host names are supplied alongside hostaddr, the two lists must have the same length, and any other count fails with could not match N host names to M hostaddr values instead of silently dialling one address for several hosts.
  • An explicit empty host overrides PGHOST and a host from the service file. That value supplies no host names, so it is not treated as a list and cannot produce the length error above.
  • buildConnectOneConfigs dials hostaddr directly when it is set, skipping name resolution, and keeps host as the originalHostname used for identity. A nonempty hostaddr is checked to be an IP literal first: a host name or a socket directory path is an error for that slot, not a fallback to name resolution or to a Unix domain socket.
  • A hostaddr is always a TCP connection, so the Unix-domain-socket TLS exemption no longer applies when one is present.
  • An empty hostaddr element means the slot carries no address, as in hostaddr=,127.0.0.1: the default host is tried first and the supplied address second.
  • .pgpass lookup uses hostaddr when no host name was supplied, and PGHOSTADDR is read as the environment form of hostaddr.

hostaddr without host is a complete target in libpq: there is no name to resolve and none to present. The default socket directory is therefore not substituted for the missing host name, which would otherwise become the TLS server name — Config.Host is left empty.

For the connection string above the parsed config is now:

Host="hostname.for.sni"  HostAddr="192.168.1.100"  Port=54320
dial 192.168.1.100:54320   originalHostname "hostname.for.sni"   TLS ServerName "hostname.for.sni"

Notes on libpq parity

Two limits, stated rather than glossed over:

  • The address check is a strict IP literal test. libpq resolves hostaddr with AI_NUMERICHOST, which on some platforms also accepts nonstandard IPv4 spellings: shorthand (127.1), hex (0x7f.0.0.1), and leading-zero forms (010.1.1.1, which macOS reads as 10.1.1.1). Those are rejected here. The literal check is the conservative direction for a value documented as an address.
  • Distinct .pgpass passwords for fallback hosts remain a pre-existing limitation, unchanged by this change.

Tests

  • pgconn/config_test.go: TestParseConfigHostAddr (including the issue's URL), TestParseConfigHostAddrWithoutHost, TestParseConfigHostAddrWithHostFromEnvironment, TestParseConfigHostAddrFromEnvironment, TestParseConfigHostAddrWithEmptyElement, TestParseConfigHostAddrWithExplicitEmptyHost, TestParseConfigHostAddrUsesHostAddrForPassfile, TestParseConfigHostAddrCountMismatch.
  • pgconn/pgconn_private_test.go: TestBuildConnectOneConfigsUsesHostAddrWithoutResolving asserts the dial address, the retained host name, and that LookupFunc is never called when hostaddr is set. TestBuildConnectOneConfigsRejectsNonNumericHostAddr covers a host name and a socket directory path.

Verification

go build ./...                        ok
go vet ./pgconn/                      clean
gofumpt -extra -l pgconn/             clean
gofmt -l pgconn/                      clean
go test ./pgconn/ -run HostAddr       10 tests pass
go test ./pgconn/                     identical set of failing test names before and after

The failures in ./pgconn are the ones that need a running PostgreSQL server, which this machine does not have. They are present on master as well, and the failing test names are identical before and after the change (96 top-level test names, 112 including subtests — the same set, counted two ways). That is not a substitute for a database-backed run or for CI, and the workflow run on this branch is still awaiting approval, so there is no CI result yet.

AI Disclosure

Per the contributing guide, this is an AI-assisted proposal. The implementation, the tests, and the verification above were produced with AI coding agents — GLM-5.3 via the ZCode CLI, and GPT-6 Sol / GPT-6 Astra via ChatGPT — which also cross-reviewed each other's output. I drove the process (setting the goals and reviewing each round of output and the libpq comparison), but I am not yet able to answer detailed questions about the change unaided. Prompts and session logs are available on request.

Fixes #2655

libpq accepts hostaddr as the address to dial while host keeps its meaning
for server identity, such as SNI and certificate verification. pgx instead
passed hostaddr to the server as a run-time parameter, which rejected it as
an unrecognized configuration parameter.

Parse hostaddr alongside host and port, and dial it directly instead of
resolving the host name. As with port, a single address applies to every
host and any other count must match the host count.

An address given without a host name is a complete target: no default
socket directory is substituted for the missing host, because it would
become the TLS server name.
@ur4t

ur4t commented Sep 18, 2026

Copy link
Copy Markdown

This PR works as expected for the example case from the issue.

@jackc

jackc commented Sep 26, 2026

Copy link
Copy Markdown
Owner

There are multiple cases it doesn't handle or handles differently than libpq.

Finding PR behavior Expected behavior
Mismatched lists host=h1,h2 hostaddr=127.0.0.1 reuses one address for both hosts. Reject mismatched counts. Only port has the single-value exception.
Leading empty address hostaddr=,127.0.0.1 fails parsing. Try the default host, then the supplied address.
Invalid addresses hostaddr=localhost reaches the dialer; hostaddr=/tmp selects a Unix socket. Require a numeric address and use TCP.
Password lookup Address-only connections search .pgpass using an empty hostname. Use hostaddr when host is absent.
Environment support PGHOSTADDR is ignored. Treat it as the default hostaddr.

…pass

Follow-up to the review of this pull request. libpq resolves hostaddr with
AI_NUMERICHOST and treats the hostaddr list as the authority on how many
connections are described; the parsing here did neither.

- hostaddr has no single-value form. The connection count follows the
  hostaddr list when it is present, as in pqConnectOptions2, and a mismatch
  against the host list is rejected instead of dialling one address for
  several hosts.
- An empty element gives its slot no address, so hostaddr=,127.0.0.1 tries
  the default Unix-domain socket first and the supplied address second.
- hostaddr must be a numeric address. A host name is no longer resolved and
  a socket directory is no longer dialled as a Unix domain socket; the
  affected slot reports an error instead.
- The .pgpass search key is hostaddr when no host name was written.
- PGHOSTADDR is read as the environment form of hostaddr.
@Leewwp

Leewwp commented Sep 28, 2026

Copy link
Copy Markdown
Author

Thanks for the detailed review. All five are addressed in two follow-up commits on this same
branch.

  • Mismatched lists: hostaddr no longer has the port-style single-value form. The connection
    count follows the hostaddr list when it is present, as in pqConnectOptions2, and a mismatch
    is rejected: host=h1,h2 hostaddr=127.0.0.1 now fails with "could not match 2 host names to
    1 hostaddr values" rather than dialling 127.0.0.1 for both. An explicit empty host that
    overrides PGHOST supplies no host names, so it cannot trigger that error either.
  • Leading empty address: an empty element gives its slot no address, so hostaddr=,127.0.0.1
    tries the default Unix-domain socket first and 127.0.0.1 second.
  • Invalid addresses: a nonempty hostaddr is checked with netip.ParseAddr before dialling.
    hostaddr=localhost no longer reaches the dialer, and hostaddr=/tmp is no longer dialled as
    a Unix socket.
  • Password lookup: the .pgpass key is the host name when one was written and hostaddr when it
    was not.
  • Environment support: PGHOSTADDR is read as the environment form of hostaddr.

One limit I want to be explicit about rather than overstate: netip.ParseAddr is stricter than
AI_NUMERICHOST. On macOS, getaddrinfo with AI_NUMERICHOST also accepts nonstandard IPv4
spellings (127.1, 0x7f.0.0.1, and 010.1.1.1, which it reads as 10.1.1.1), and those are
rejected here. The literal check is the conservative direction for a value documented as an
address, and it can be widened if you would rather match libpq's parsing exactly. The
pre-existing limitation on distinct .pgpass passwords for fallback hosts is unchanged.

Each case has a test. go build ./..., go vet ./pgconn/ and gofumpt -extra -l pgconn/ are
clean, and go test ./pgconn/ -run HostAddr passes 10 tests. The full ./pgconn suite needs a
running PostgreSQL server, which this machine does not have; the failing test names are the
same before and after (96 top-level, 112 including subtests — the same set), so that is not
evidence of a pass. The workflow run on this branch is still awaiting approval, so there is
no CI result yet either.

One more thing I should state plainly, per the contributing guide: this is an AI-assisted
proposal. The implementation, the tests, and the verification runs above were produced with
AI coding agents — GLM-5.3 via the ZCode CLI, and GPT-6 Sol / GPT-6 Astra via ChatGPT —
which also cross-reviewed each other's output. I drove the process (setting the goals and
reviewing each round of output and the libpq comparison), but I am not yet able to answer
detailed questions about the change unaided, so I don't want to claim otherwise. I can share
the prompts and the session logs if that is useful. I'm aware this may mean the change gets
reworked or rejected; if it is easier to take it as a starting point, please do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mismatch with libpq when hostaddr is specified in connection strings

3 participants