Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/sqlx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"

Expand Down Expand Up @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion sqlx-postgres/src/connection/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ impl PgStream {
pub(super) async fn connect(options: &PgConnectOptions) -> Result<Self, Error> {
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),
Copy link
Contributor Author

@mbj mbj Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've modified the integration tests to cover this behavior, they now:

  • instruct the ssl tests to connect to a host: sqlx.rs that does not resolve to a postgresql instance on the public DNS.
  • but provide the hostaddr to point it to 127.0.0.1 bypassing DNS lookups
  • proof that cert validation fully works in this scenario in upgrading to verify-full from verify-ca.

I use sqlx.rs as hostname for this test as this is the hostname present in the test certificates.

options.port,
MaybeUpgradeTls(options),
)
.await?
}
};

let socket = socket_result?;
Expand Down
4 changes: 2 additions & 2 deletions sqlx-postgres/src/options/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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
[`webpki-roots`]: https://docs.rs/webpki-roots
58 changes: 54 additions & 4 deletions sqlx-postgres/src/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod ssl_mode;
#[derive(Debug, Clone)]
pub struct PgConnectOptions {
pub(crate) host: String,
pub(crate) host_addr: Option<String>,
pub(crate) port: u16,
pub(crate) socket: Option<PathBuf>,
pub(crate) username: String,
Expand Down Expand Up @@ -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();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now not all strings are valid here, only numeric IPs are. But we are not doing any validation on the strings at all for all the sibling fields, so I did not start to do that here also. In fact there is not even an error path in this API.

That I do not agree to the defaulting to ENVs in the first place was discussed in previous PRs, for the moment I consider the bugfix more important than improving tghe API so I just keep it "as wrong in my eyes as the siblings" while fixing host != hostaddr.


let username = var("PGUSER").ok().unwrap_or_else(whoami::username);

Expand All @@ -71,6 +71,7 @@ impl PgConnectOptions {
PgConnectOptions {
port,
host,
host_addr,
socket: None,
username,
password: var("PGPASSWORD").ok(),
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand Down
25 changes: 23 additions & 2 deletions sqlx-postgres/src/options/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ impl PgConnectOptions {

"hostaddr" => {
value.parse::<IpAddr>().map_err(Error::config)?;
options = options.host(&value)
options = options.host_addr(&value)
}

"port" => options = options.port(value.parse().map_err(Error::config)?),
Expand Down Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion tests/certs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OT: I consider it a terrible design descision to silently truncate extensions, instead they should have bailed "unacknowledged extension A, B, C present".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context: rustls does not use the CN field to populate present hostnames for hostname checks. Instead it exclusively looks at the subject alternative DNS names. All modern CAs always emit subject alternative names, so we need to properly create one to allow verify-full in tests (followup commit does so after fixing the hostaddr bug).

```
19 changes: 10 additions & 9 deletions tests/certs/server.crt
Original file line number Diff line number Diff line change
@@ -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-----
Copy link
Contributor Author

@mbj mbj Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            b6:c7:1f:4b:11:c0:a1:8f
        Signature Algorithm: ED25519
        Issuer: C=us, ST=california, O=Internet Widgits Pty Ltd, CN=Austin Bonander
        Validity
            Not Before: Jul  1 05:21:56 2025 GMT
            Not After : Jun 29 05:21:56 2035 GMT
        Subject: C=us, ST=california, O=SQLx.rs, CN=sqlx.rs
        Subject Public Key Info:
            Public Key Algorithm: ED25519
                ED25519 Public-Key:
                pub:
                    0d:f7:4b:6a:ac:aa:96:6c:b1:47:18:ae:99:5e:31:
                    70:e3:e7:f9:a1:6f:9e:87:3d:a3:b7:08:82:20:63:
                    97:3c
        X509v3 extensions:
            X509v3 Subject Key Identifier:
                3D:4A:67:F7:91:87:16:E3:1E:EF:ED:A9:1B:9A:DB:98:94:B9:E7:FC
            X509v3 Authority Key Identifier:
                0B:0D:A9:56:91:8A:CF:6C:64:22:36:D5:1D:88:74:2E:7C:DD:91:6E
    Signature Algorithm: ED25519
    Signature Value:
        13:11:0e:cd:af:48:ad:27:21:a1:0b:35:37:03:f1:7c:27:5f:
        5c:c1:e2:5e:9b:20:31:38:43:ba:28:3e:f6:16:ae:5b:e6:f5:
        2a:50:51:dd:97:96:f0:03:88:c1:ac:cc:85:db:2d:04:53:a1:
        ba:66:96:3d:0f:f0:64:d5:24:0a

After:

$ openssl x509 -text -noout -in server.crt
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            2c:da:80:96:d8:8e:10:fe:5f:79:0a:e3:20:15:d8:03:a6:02:d1:f5
        Signature Algorithm: ED25519
        Issuer: C=us, ST=california, O=Internet Widgits Pty Ltd, CN=Austin Bonander
        Validity
            Not Before: Nov 12 21:49:58 2025 GMT
            Not After : Nov 10 21:49:58 2035 GMT
        Subject: C=us, ST=california, O=SQLx.rs, CN=sqlx.rs
        Subject Public Key Info:
            Public Key Algorithm: ED25519
                ED25519 Public-Key:
                pub:
                    0d:f7:4b:6a:ac:aa:96:6c:b1:47:18:ae:99:5e:31:
                    70:e3:e7:f9:a1:6f:9e:87:3d:a3:b7:08:82:20:63:
                    97:3c
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                DNS:sqlx.rs
            X509v3 Subject Key Identifier:
                3D:4A:67:F7:91:87:16:E3:1E:EF:ED:A9:1B:9A:DB:98:94:B9:E7:FC
            X509v3 Authority Key Identifier:
                0B:0D:A9:56:91:8A:CF:6C:64:22:36:D5:1D:88:74:2E:7C:DD:91:6E
    Signature Algorithm: ED25519
    Signature Value:
        ac:a8:b2:66:6f:b8:22:2b:5a:4b:db:e1:03:7c:93:61:70:7f:
        4f:4b:20:20:8d:8e:1e:7b:d5:7c:0d:c6:9f:5e:50:41:30:24:
        e8:e5:f7:9e:a8:e9:21:71:0e:58:0a:0a:bd:65:94:04:af:cf:
        97:0f:9b:a8:d4:57:6c:25:69:07

Note the subject alternative name is now present (was absent before)!

4 changes: 2 additions & 2 deletions tests/x.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
)
Expand All @@ -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}",
)
Expand Down
Loading