From 67bdcd713713cdabe932c8e18744fdbf982199d0 Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Wed, 12 Nov 2025 19:03:22 +0000 Subject: [PATCH 1/3] Fix whitespace --- sqlx-postgres/src/options/doc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-postgres/src/options/doc.md b/sqlx-postgres/src/options/doc.md index 33dd63b7a8..b3940a4231 100644 --- a/sqlx-postgres/src/options/doc.md +++ b/sqlx-postgres/src/options/doc.md @@ -182,4 +182,4 @@ let pool = PgPool::connect_with(opts).await?; [libpq-params]: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS [libpq-envars]: https://www.postgresql.org/docs/current/libpq-envars.html [rfc7468]: https://datatracker.ietf.org/doc/html/rfc7468 -[`webpki-roots`]: https://docs.rs/webpki-roots \ No newline at end of file +[`webpki-roots`]: https://docs.rs/webpki-roots From 207c34282671d045287b2be388fd5304838b7328 Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Wed, 12 Nov 2025 21:50:55 +0000 Subject: [PATCH 2/3] Fix test certificate generation to include subject alternative name * Since 3.x extensions in the CSR are not automatically carried * Needed as rustls does not use the CN to verify hostnames it uses subject alternative DNS names only. --- tests/certs/README.md | 2 +- tests/certs/server.crt | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/certs/README.md b/tests/certs/README.md index add100625b..3d5bd05c09 100644 --- a/tests/certs/README.md +++ b/tests/certs/README.md @@ -83,5 +83,5 @@ This adds a required x509 v3 extension: Create a signed certificate using our CA key and the CSR: ``` -openssl x509 -req -CA ca.crt -CAkey keys/ca.key -in server.csr -out server.crt -days 3650 -CAcreateserial +openssl x509 -req -CA ca.crt -CAkey keys/ca.key -in server.csr -out server.crt -days 3650 -CAcreateserial -copy_extensions copy ``` diff --git a/tests/certs/server.crt b/tests/certs/server.crt index 4c18ad1b6a..c38753f00b 100644 --- a/tests/certs/server.crt +++ b/tests/certs/server.crt @@ -1,11 +1,12 @@ -----BEGIN CERTIFICATE----- -MIIBnjCCAVCgAwIBAgIJALbHH0sRwKGPMAUGAytlcDBfMQswCQYDVQQGEwJ1czET -MBEGA1UECAwKY2FsaWZvcm5pYTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ -dHkgTHRkMRgwFgYDVQQDDA9BdXN0aW4gQm9uYW5kZXIwHhcNMjUwNzAxMDUyMTU2 -WhcNMzUwNjI5MDUyMTU2WjBGMQswCQYDVQQGEwJ1czETMBEGA1UECAwKY2FsaWZv -cm5pYTEQMA4GA1UECgwHU1FMeC5yczEQMA4GA1UEAwwHc3FseC5yczAqMAUGAytl -cAMhAA33S2qsqpZssUcYrpleMXDj5/mhb56HPaO3CIIgY5c8o0IwQDAdBgNVHQ4E -FgQUPUpn95GHFuMe7+2pG5rbmJS55/wwHwYDVR0jBBgwFoAUCw2pVpGKz2xkIjbV -HYh0LnzdkW4wBQYDK2VwA0EAExEOza9IrSchoQs1NwPxfCdfXMHiXpsgMThDuig+ -9hauW+b1KlBR3ZeW8AOIwazMhdstBFOhumaWPQ/wZNUkCg== +MIIBvTCCAW+gAwIBAgIULNqAltiOEP5feQrjIBXYA6YC0fUwBQYDK2VwMF8xCzAJ +BgNVBAYTAnVzMRMwEQYDVQQIDApjYWxpZm9ybmlhMSEwHwYDVQQKDBhJbnRlcm5l +dCBXaWRnaXRzIFB0eSBMdGQxGDAWBgNVBAMMD0F1c3RpbiBCb25hbmRlcjAeFw0y +NTExMTIyMTQ5NThaFw0zNTExMTAyMTQ5NThaMEYxCzAJBgNVBAYTAnVzMRMwEQYD +VQQIDApjYWxpZm9ybmlhMRAwDgYDVQQKDAdTUUx4LnJzMRAwDgYDVQQDDAdzcWx4 +LnJzMCowBQYDK2VwAyEADfdLaqyqlmyxRxiumV4xcOPn+aFvnoc9o7cIgiBjlzyj +VjBUMBIGA1UdEQQLMAmCB3NxbHgucnMwHQYDVR0OBBYEFD1KZ/eRhxbjHu/tqRua +25iUuef8MB8GA1UdIwQYMBaAFAsNqVaRis9sZCI21R2IdC583ZFuMAUGAytlcANB +AKyosmZvuCIrWkvb4QN8k2Fwf09LICCNjh571XwNxp9eUEEwJOjl956o6SFxDlgK +Cr1llASvz5cPm6jUV2wlaQc= -----END CERTIFICATE----- From f795fe994a6973ebe872c8c619706a4244c65d54 Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Wed, 12 Nov 2025 14:16:08 +0000 Subject: [PATCH 3/3] Fix postgresql hostaddr semantics * Before hostaddr was used to just be an alternative way to set `host`. But this is not how libpq actually behaves or intents hostaddr to be used. * Instead `hostaddr` is meant to contain a numeric IP address to connect to, bypassing DNS lookups. This can be useful if the DNS lookups are slow, or if the DNS lookup has to be bypassed as the port is proxied to localhost for ifrastructure reasons. The TLS verification is still always done against the `host` parameter. Basically it allows to do `verify-full` on proxied ports. --- .github/workflows/sqlx.yml | 4 +- sqlx-postgres/src/connection/stream.rs | 9 +++- sqlx-postgres/src/options/doc.md | 2 +- sqlx-postgres/src/options/mod.rs | 58 ++++++++++++++++++++++++-- sqlx-postgres/src/options/parse.rs | 25 ++++++++++- tests/x.py | 4 +- 6 files changed, 90 insertions(+), 12 deletions(-) diff --git a/.github/workflows/sqlx.yml b/.github/workflows/sqlx.yml index b2f81b75ad..5d523cbc64 100644 --- a/.github/workflows/sqlx.yml +++ b/.github/workflows/sqlx.yml @@ -263,7 +263,7 @@ jobs: --no-default-features --features any,postgres,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: - DATABASE_URL: postgres://postgres:password@localhost:5432/sqlx?sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt + DATABASE_URL: postgres://postgres:password@sqlx.rs:5432/sqlx?hostaddr=127.0.0.1&sslmode=verify-full&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt SQLX_OFFLINE_DIR: .sqlx RUSTFLAGS: -D warnings --cfg postgres="${{ matrix.postgres }}" @@ -322,7 +322,7 @@ jobs: --no-default-features --features any,postgres,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: - DATABASE_URL: postgres://postgres@localhost:5432/sqlx?sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt&sslkey=.%2Ftests%2Fcerts%2Fkeys%2Fclient.key&sslcert=.%2Ftests%2Fcerts%2Fclient.crt + DATABASE_URL: postgres://postgres@sqlx.rs:5432/sqlx?hostaddr=127.0.0.1&sslmode=verify-full&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt&sslkey=.%2Ftests%2Fcerts%2Fkeys%2Fclient.key&sslcert=.%2Ftests%2Fcerts%2Fclient.crt RUSTFLAGS: -D warnings --cfg postgres="${{ matrix.postgres }}" mysql: diff --git a/sqlx-postgres/src/connection/stream.rs b/sqlx-postgres/src/connection/stream.rs index e8a1aedc47..035ebe238a 100644 --- a/sqlx-postgres/src/connection/stream.rs +++ b/sqlx-postgres/src/connection/stream.rs @@ -44,7 +44,14 @@ impl PgStream { pub(super) async fn connect(options: &PgConnectOptions) -> Result { let socket_result = match options.fetch_socket() { Some(ref path) => net::connect_uds(path, MaybeUpgradeTls(options)).await?, - None => net::connect_tcp(&options.host, options.port, MaybeUpgradeTls(options)).await?, + None => { + net::connect_tcp( + options.host_addr.as_ref().unwrap_or(&options.host), + options.port, + MaybeUpgradeTls(options), + ) + .await? + } }; let socket = socket_result?; diff --git a/sqlx-postgres/src/options/doc.md b/sqlx-postgres/src/options/doc.md index b3940a4231..2c68e71f65 100644 --- a/sqlx-postgres/src/options/doc.md +++ b/sqlx-postgres/src/options/doc.md @@ -29,7 +29,7 @@ if a parameter is not passed in via URL, it is populated by reading | `password` | `PGPASSWORD` | Read from [`passfile`], if it exists. | | [`passfile`] | `PGPASSFILE` | `~/.pgpass` or `%APPDATA%\postgresql\pgpass.conf` (Windows) | | `host` | `PGHOST` | See [Note: Default Host](#note-default-host). | -| `hostaddr` | `PGHOSTADDR` | See [Note: Default Host](#note-default-host). | +| `hostaddr` | `PGHOSTADDR` | Numeric IP address, allows to overwrite DNS lookup | | `port` | `PGPORT` | `5432` | | `dbname` | `PGDATABASE` | Unset; defaults to the username server-side. | | `sslmode` | `PGSSLMODE` | `prefer`. See [`PgSslMode`] for details. | diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index efbc43989b..55236f6e9e 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -16,6 +16,7 @@ mod ssl_mode; #[derive(Debug, Clone)] pub struct PgConnectOptions { pub(crate) host: String, + pub(crate) host_addr: Option, pub(crate) port: u16, pub(crate) socket: Option, pub(crate) username: String, @@ -59,10 +60,9 @@ impl PgConnectOptions { .and_then(|v| v.parse().ok()) .unwrap_or(5432); - let host = var("PGHOSTADDR") - .ok() - .or_else(|| var("PGHOST").ok()) - .unwrap_or_else(|| default_host(port)); + let host = var("PGHOST").ok().unwrap_or_else(|| default_host(port)); + + let host_addr = var("PGHOSTADDR").ok(); let username = var("PGUSER").ok().unwrap_or_else(whoami::username); @@ -71,6 +71,7 @@ impl PgConnectOptions { PgConnectOptions { port, host, + host_addr, socket: None, username, password: var("PGPASSWORD").ok(), @@ -127,6 +128,33 @@ impl PgConnectOptions { self } + /// Sets the host address to connect to. + /// + /// This is different to the host parameter as it overwrites DNS lookups for TCP/IP + /// communication. This is particuarly useful when the DB port has to be + /// proxied to localhost for security reasons. + /// + /// # Example + /// + /// ```rust + /// # use sqlx_postgres::PgConnectOptions; + /// let options = PgConnectOptions::new() + /// .host("example.com"); + /// + /// // Initially, host_addr should be None (unless PGHOSTADDR env var is set) + /// // For this test, we assume it's not set + /// assert_eq!(options.get_host_addr(), None); + /// + /// let options = options.host_addr("127.0.0.1"); + /// + /// // After setting, host_addr should contain the specified value + /// assert_eq!(options.get_host_addr(), Some("127.0.0.1")); + /// ``` + pub fn host_addr(mut self, host_addr: &str) -> Self { + self.host_addr = Some(host_addr.to_string()); + self + } + /// Sets the port to connect to at the server host. /// /// The default port for PostgreSQL is `5432`. @@ -477,6 +505,28 @@ impl PgConnectOptions { &self.host } + /// Get the current host addr. + /// + /// # Example + /// + /// ```rust + /// # use sqlx_postgres::PgConnectOptions; + /// let options = PgConnectOptions::new() + /// .host("example.com"); + /// + /// // Initially, host_addr should be None (unless PGHOSTADDR env var is set) + /// // For this test, we assume it's not set + /// assert_eq!(options.get_host_addr(), None); + /// + /// let options = options.host_addr("127.0.0.1"); + /// + /// // After setting host_addr, it should return the configured value + /// assert_eq!(options.get_host_addr(), Some("127.0.0.1")); + /// ``` + pub fn get_host_addr(&self) -> Option<&str> { + self.host_addr.as_deref() + } + /// Get the server's port. /// /// # Example diff --git a/sqlx-postgres/src/options/parse.rs b/sqlx-postgres/src/options/parse.rs index e911305698..0ea2cf8fbd 100644 --- a/sqlx-postgres/src/options/parse.rs +++ b/sqlx-postgres/src/options/parse.rs @@ -76,7 +76,7 @@ impl PgConnectOptions { "hostaddr" => { value.parse::().map_err(Error::config)?; - options = options.host(&value) + options = options.host_addr(&value) } "port" => options = options.port(value.parse().map_err(Error::config)?), @@ -203,7 +203,28 @@ fn it_parses_hostaddr_correctly_from_parameter() { let opts = PgConnectOptions::from_str(url).unwrap(); assert_eq!(None, opts.socket); - assert_eq!("8.8.8.8", &opts.host); + assert_eq!("localhost", &opts.host); + assert_eq!(Some("8.8.8.8"), opts.host_addr.as_deref()); +} + +#[test] +fn it_parses_hostaddr_host_separately_from_parameter() { + let url = "postgres://example.com/?hostaddr=8.8.8.8"; + let opts = PgConnectOptions::from_str(url).unwrap(); + + assert_eq!(None, opts.socket); + assert_eq!("example.com", &opts.host); + assert_eq!(Some("8.8.8.8"), opts.host_addr.as_deref()); +} + +#[test] +fn it_parses_hostaddr_host_host_overwrite_from_query_from_parameter() { + let url = "postgres://example.com/?hostaddr=8.8.8.8&host=sqlx.rs"; + let opts = PgConnectOptions::from_str(url).unwrap(); + + assert_eq!(None, opts.socket); + assert_eq!("sqlx.rs", &opts.host); + assert_eq!(Some("8.8.8.8"), opts.host_addr.as_deref()); } #[test] diff --git a/tests/x.py b/tests/x.py index e1308f2fa4..40d8aa5612 100755 --- a/tests/x.py +++ b/tests/x.py @@ -188,7 +188,7 @@ def run(command, comment=None, env=None, service=None, tag=None, args=None, data run( f"cargo test --no-default-features --features any,postgres,macros,_unstable-all-types,runtime-{runtime},tls-{tls}", comment=f"test postgres {version} ssl", - database_url_args="sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt", + database_url_args="host=sqlx.rs&hostaddr=127.0.0.1&sslmode=verify-full&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt", service=f"postgres_{version}", tag=f"postgres_{version}_ssl" if runtime == "async-std" else f"postgres_{version}_ssl_{runtime}", ) @@ -197,7 +197,7 @@ def run(command, comment=None, env=None, service=None, tag=None, args=None, data run( f"cargo test --no-default-features --features any,postgres,macros,_unstable-all-types,runtime-{runtime},tls-{tls}", comment=f"test postgres {version}_client_ssl no-password", - database_url_args="sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt&sslkey=%2Ftests%2Fcerts%2Fkeys%2Fclient.key&sslcert=.%2Ftests%2Fcerts%2Fclient.crt", + database_url_args="host=sqlx.rs&hostaddr=127.0.0.1&sslmode=verify-full&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt&sslkey=%2Ftests%2Fcerts%2Fkeys%2Fclient.key&sslcert=.%2Ftests%2Fcerts%2Fclient.crt", service=f"postgres_{version}_client_ssl", tag=f"postgres_{version}_client_ssl_no_password" if runtime == "async-std" else f"postgres_{version}_client_ssl_no_password_{runtime}", )