mirror of
https://github.com/element-hq/synapse.git
synced 2026-11-12 01:17:40 +00:00
Add TLS support to the native Rust Postgres backend
The Rust backend connected with `NoTls`, so it couldn't talk to a Postgres that requires (or, as libpq defaults, prefers) TLS — a blocker for most managed / networked databases. Honour the same libpq keys psycopg2 already forwards, so an existing `database.args` keeps working: `sslmode`, `sslrootcert`, `sslcert`, `sslkey`, `sslpassword`. Implemented on rustls (already in the tree via reqwest; pinned to the `ring` provider since `aws-lc-rs` needs a C toolchain we don't have). `tokio-postgres` only knows `sslmode` disable/prefer/require and does no verification itself, so a new `postgres/tls.rs` drives both the handshake mode and the verifier: disable -> no TLS allow/prefer -> encrypt opportunistically, no verification (accept-any) require -> encrypt, no verification (libpq's `require` does NOT verify) verify-ca -> verify the chain, not the hostname (WebPKI, name bypassed) verify-full -> verify chain + hostname (WebPKI) `sslrootcert` (or the system trust store) supplies the roots; `sslcert`/`sslkey` add a client certificate for mutual TLS. The cert-path keys and the `verify-*` modes are stripped from the DSN and passed to the pool as explicit params (`rust_dbapi.split_ssl_params`), since `tokio-postgres` can't parse them. Behaviour change: the default (no `sslmode`) is now libpq's `prefer` — attempt TLS, fall back to plaintext — where it was previously no TLS. This matches psycopg2; plaintext servers still connect via the fallback. Caveats vs libpq/openssl: rustls is stricter (requires SANs, rejects a non-CA self-signed root), and an encrypted client key (`sslpassword`) isn't supported and is rejected with a clear error. Validated end-to-end against an SSL Postgres: require / verify-ca / verify-full connect and use TLS, verify-full with only the system roots correctly rejects an unknown CA, and hostname is checked (verify-full) / ignored (verify-ca). Unit tests cover the sslmode->verifier mapping; the Rust-backend storage suites still pass with the new default (prefer falls back against the plaintext test server). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
This commit is contained in:
1 parent
ee9f2f90ff
commit
c8d43aebc7
10 files changed
+749
-22
No files matched your search
Generated
+127
-1
@@ -52,6 +52,12 @@ version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
@@ -141,6 +147,12 @@ version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
@@ -236,6 +248,29 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid 0.9.6",
|
||||
"der_derive",
|
||||
"flagset",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der_derive"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -253,7 +288,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer 0.12.1",
|
||||
"const-oid",
|
||||
"const-oid 0.10.2",
|
||||
"crypto-common 0.2.2",
|
||||
"ctutils",
|
||||
]
|
||||
@@ -287,6 +322,12 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
||||
|
||||
[[package]]
|
||||
name = "flagset"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -1361,6 +1402,7 @@ version = "0.23.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -1381,6 +1423,15 @@ dependencies = [
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.12.0"
|
||||
@@ -1579,6 +1630,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
@@ -1655,11 +1716,15 @@ dependencies = [
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rustc_version",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"tokio-postgres",
|
||||
"tokio-postgres-rustls",
|
||||
"ulid",
|
||||
]
|
||||
|
||||
@@ -1734,6 +1799,27 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tls_codec"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b"
|
||||
dependencies = [
|
||||
"tls_codec_derive",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tls_codec_derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.1"
|
||||
@@ -1774,6 +1860,20 @@ dependencies = [
|
||||
"whoami",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-postgres-rustls"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c2ad44aa0ae96db89c4742212ed41645b2f597311ff6e1945542a4d9fadc2fb"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"tokio-postgres",
|
||||
"tokio-rustls",
|
||||
"x509-cert",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.2"
|
||||
@@ -2192,6 +2292,18 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||
|
||||
[[package]]
|
||||
name = "x509-cert"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94"
|
||||
dependencies = [
|
||||
"const-oid 0.9.6",
|
||||
"der",
|
||||
"spki",
|
||||
"tls_codec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.0"
|
||||
@@ -2242,6 +2354,20 @@ name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
|
||||
@@ -77,6 +77,19 @@ itertools = "0.15.0"
|
||||
# libpq is an optional runtime dependency rather than a build-time one.
|
||||
libloading = "0.8"
|
||||
postgres-protocol = "0.6.10"
|
||||
# Use the `ring` crypto provider (not the default `aws-lc-rs`, which needs cmake
|
||||
# and isn't available here) — `ring` is already in the tree via reqwest.
|
||||
rustls = { version = "0.23", default-features = false, features = [
|
||||
"ring",
|
||||
"logging",
|
||||
"std",
|
||||
"tls12",
|
||||
] }
|
||||
rustls-native-certs = "0.8"
|
||||
rustls-pemfile = "2"
|
||||
tokio-postgres-rustls = { version = "0.14.0", default-features = false, features = [
|
||||
"ring",
|
||||
] }
|
||||
|
||||
[build-dependencies]
|
||||
blake2 = "0.11.0"
|
||||
|
||||
@@ -16,6 +16,7 @@ mod cursor_state;
|
||||
mod errors;
|
||||
mod helpers;
|
||||
pub mod pool;
|
||||
mod tls;
|
||||
mod value;
|
||||
|
||||
/// Register the `postgres` submodule (the `ConnectionPool`, `Connection` and
|
||||
|
||||
@@ -22,13 +22,15 @@ use deadpool::managed::{Manager, Metrics, Object, Pool, PoolError, RecycleError,
|
||||
use log::warn;
|
||||
use pyo3::exceptions::PyRuntimeError;
|
||||
use pyo3::prelude::*;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::runtime::Handle;
|
||||
use tokio_postgres::{Client, Config, NoTls, Statement};
|
||||
use tokio_postgres::{Client, Config, Connection as PgConnection, NoTls, Socket, Statement};
|
||||
|
||||
use crate::database::postgres::connection::Connection;
|
||||
use crate::database::postgres::errors::{pg_err_to_py, OperationalError};
|
||||
use crate::database::postgres::fixup_config_defaults;
|
||||
use crate::database::postgres::helpers::BlockingPostgres;
|
||||
use crate::database::postgres::tls::{self, TlsConnector, TlsParams};
|
||||
use crate::runtime::RustRuntime;
|
||||
|
||||
/// Per-connection session settings applied once when a pooled connection is
|
||||
@@ -81,7 +83,8 @@ impl SessionConfig {
|
||||
/// Creates and recycles [`tokio_postgres`] connections for a [`ConnectionPool`].
|
||||
pub struct ConnectionManager {
|
||||
/// The resolved connection config, parsed once from the DSN (with libpq's
|
||||
/// default host filled in if the DSN omitted one).
|
||||
/// default host filled in if the DSN omitted one, and the SSL mode set from
|
||||
/// the `ssl*` params).
|
||||
config: Config,
|
||||
/// Handle to the extension's shared tokio runtime (see
|
||||
/// [`crate::runtime::RustRuntime`]). Each connection's long-lived connection task
|
||||
@@ -90,24 +93,50 @@ pub struct ConnectionManager {
|
||||
handle: Handle,
|
||||
/// Session settings applied to each connection when it is opened.
|
||||
session: SessionConfig,
|
||||
/// How to (or whether to) negotiate TLS for each connection.
|
||||
tls: TlsConnector,
|
||||
}
|
||||
|
||||
impl ConnectionManager {
|
||||
/// Build a manager from a libpq-style DSN and session settings, spawning
|
||||
/// connection tasks on the runtime behind `handle`.
|
||||
/// Build a manager from a libpq-style DSN, session settings and TLS params,
|
||||
/// spawning connection tasks on the runtime behind `handle`.
|
||||
pub fn from_dsn(
|
||||
dsn: &str,
|
||||
handle: Handle,
|
||||
session: SessionConfig,
|
||||
tls_params: &TlsParams,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let mut config = fixup_config_defaults(dsn)?;
|
||||
// Resolve the `PGSSL*` env fallbacks here, next to the other `PG*`
|
||||
// env handling in `fixup_config_defaults`, keeping `tls::build` (and
|
||||
// its unit tests) a pure function of its params.
|
||||
let tls_params = tls::with_env_defaults(tls_params);
|
||||
let (ssl_mode, connector) = tls::build(&tls_params)?;
|
||||
config.ssl_mode(ssl_mode);
|
||||
Ok(Self {
|
||||
config: fixup_config_defaults(dsn)?,
|
||||
config,
|
||||
handle,
|
||||
session,
|
||||
tls: connector,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Drive a connection's background task (which pumps the socket) on the shared
|
||||
/// runtime. The task ends on its own when the `Client` is dropped, i.e. when the
|
||||
/// pool discards the connection. Generic over the stream so it works for both
|
||||
/// the plaintext and TLS connectors.
|
||||
fn spawn_connection_task<S>(handle: &Handle, connection: PgConnection<Socket, S>)
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
handle.spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
warn!("postgres connection error: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// A pooled [`Client`] plus its cache of prepared statements.
|
||||
///
|
||||
/// The cache lives *with* the pooled connection (not with a checkout): named
|
||||
@@ -180,13 +209,18 @@ impl Manager for ConnectionManager {
|
||||
// (which pumps the socket) on the shared runtime. The task ends on its
|
||||
// own when the `Client` is dropped, i.e. when the pool discards this
|
||||
// connection.
|
||||
let (client, connection) = self.config.connect(NoTls).await?;
|
||||
|
||||
self.handle.spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
warn!("postgres connection error: {e}");
|
||||
let client = match &self.tls {
|
||||
TlsConnector::NoTls => {
|
||||
let (client, connection) = self.config.connect(NoTls).await?;
|
||||
spawn_connection_task(&self.handle, connection);
|
||||
client
|
||||
}
|
||||
});
|
||||
TlsConnector::Rustls(connector) => {
|
||||
let (client, connection) = self.config.connect(connector.clone()).await?;
|
||||
spawn_connection_task(&self.handle, connection);
|
||||
client
|
||||
}
|
||||
};
|
||||
|
||||
// Apply per-connection session setup once, up front (the native
|
||||
// equivalent of the engine's `on_new_connection`).
|
||||
@@ -225,17 +259,25 @@ pub fn create_pool(
|
||||
max_size: usize,
|
||||
handle: Handle,
|
||||
) -> Result<ConnectionPool, anyhow::Error> {
|
||||
create_pool_with_session(dsn, max_size, handle, SessionConfig::default())
|
||||
create_pool_with_session(
|
||||
dsn,
|
||||
max_size,
|
||||
handle,
|
||||
SessionConfig::default(),
|
||||
&TlsParams::default(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a [`ConnectionPool`] with explicit per-connection [`SessionConfig`].
|
||||
/// Build a [`ConnectionPool`] with explicit per-connection [`SessionConfig`] and
|
||||
/// TLS params.
|
||||
pub fn create_pool_with_session(
|
||||
dsn: &str,
|
||||
max_size: usize,
|
||||
handle: Handle,
|
||||
session: SessionConfig,
|
||||
tls_params: &TlsParams,
|
||||
) -> Result<ConnectionPool, anyhow::Error> {
|
||||
let manager = ConnectionManager::from_dsn(dsn, handle, session)?;
|
||||
let manager = ConnectionManager::from_dsn(dsn, handle, session, tls_params)?;
|
||||
Ok(Pool::builder(manager).max_size(max_size).build()?)
|
||||
}
|
||||
|
||||
@@ -268,21 +310,34 @@ impl PyConnectionPool {
|
||||
/// (REPEATABLE READ) isolation level, plus `synchronous_commit` /
|
||||
/// `statement_timeout` if configured here.
|
||||
#[new]
|
||||
#[pyo3(signature = (runtime, dsn, max_size = 10, *, synchronous_commit = true, statement_timeout_ms = None))]
|
||||
#[pyo3(signature = (runtime, dsn, max_size = 10, *, synchronous_commit = true, statement_timeout_ms = None, sslmode = None, sslrootcert = None, sslcert = None, sslkey = None, sslpassword = None))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
runtime: &RustRuntime,
|
||||
dsn: &str,
|
||||
max_size: usize,
|
||||
synchronous_commit: bool,
|
||||
statement_timeout_ms: Option<i32>,
|
||||
sslmode: Option<String>,
|
||||
sslrootcert: Option<String>,
|
||||
sslcert: Option<String>,
|
||||
sslkey: Option<String>,
|
||||
sslpassword: Option<String>,
|
||||
) -> PyResult<Self> {
|
||||
let handle = runtime.tokio_handle()?;
|
||||
let session = SessionConfig {
|
||||
synchronous_commit,
|
||||
statement_timeout_ms,
|
||||
};
|
||||
let pool =
|
||||
create_pool_with_session(dsn, max_size, handle.clone(), session).map_err(|e| {
|
||||
let tls_params = TlsParams {
|
||||
sslmode,
|
||||
sslrootcert,
|
||||
sslcert,
|
||||
sslkey,
|
||||
sslpassword,
|
||||
};
|
||||
let pool = create_pool_with_session(dsn, max_size, handle.clone(), session, &tls_params)
|
||||
.map_err(|e| {
|
||||
PyRuntimeError::new_err(format!("failed to build connection pool: {e}"))
|
||||
})?;
|
||||
Ok(Self { pool, handle })
|
||||
@@ -427,7 +482,9 @@ mod tests {
|
||||
statement_timeout_ms: Some(1234),
|
||||
};
|
||||
let rt = test_runtime();
|
||||
let pool = create_pool_with_session(&dsn, 1, rt.handle().clone(), session).unwrap();
|
||||
let pool =
|
||||
create_pool_with_session(&dsn, 1, rt.handle().clone(), session, &TlsParams::default())
|
||||
.unwrap();
|
||||
|
||||
rt.block_on(async {
|
||||
let client = pool.get().await.unwrap();
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
// This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
//
|
||||
// Copyright (C) 2026 Element Creations Ltd
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// See the GNU Affero General Public License for more details:
|
||||
// <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
|
||||
//! TLS for the native Rust Postgres backend.
|
||||
//!
|
||||
//! psycopg2 hands `sslmode` / `sslrootcert` / `sslcert` / `sslkey` /
|
||||
//! `sslpassword` straight to libpq, so those keys already work in a Synapse
|
||||
//! `database.args`. This module reproduces libpq's behaviour on top of
|
||||
//! `tokio-postgres` + `rustls`, so the same config keeps working on the Rust
|
||||
//! backend.
|
||||
//!
|
||||
//! `tokio-postgres` only understands `sslmode` `disable`/`prefer`/`require` and
|
||||
//! does no verification itself, so we drive both the handshake mode *and* the
|
||||
//! verification level from here:
|
||||
//!
|
||||
//! | `sslmode` | encrypt | verify chain | verify host | verifier |
|
||||
//! |---------------|--------------|--------------|-------------|---------------------|
|
||||
//! | `disable` | no | – | – | (no TLS) |
|
||||
//! | `allow` | opportunistic| no | no | accept-any |
|
||||
//! | `prefer`\* | opportunistic| no | no | accept-any |
|
||||
//! | `require` | yes | no | no | accept-any |
|
||||
//! | `verify-ca` | yes | yes | no | chain-only |
|
||||
//! | `verify-full` | yes | yes | yes | full (WebPKI) |
|
||||
//!
|
||||
//! \* libpq's default when `sslmode` is unset.
|
||||
//!
|
||||
//! Note that libpq's `require` encrypts *without* verifying the certificate, so
|
||||
//! the accept-any verifier below is the compatible behaviour, not a shortcut.
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
||||
use rustls::client::WebPkiServerVerifier;
|
||||
use rustls::crypto::{ring, verify_tls12_signature, verify_tls13_signature, CryptoProvider};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime};
|
||||
use rustls::{
|
||||
CertificateError, ClientConfig, DigitallySignedStruct, Error as RustlsError, RootCertStore,
|
||||
SignatureScheme,
|
||||
};
|
||||
use tokio_postgres::config::SslMode;
|
||||
use tokio_postgres_rustls::MakeRustlsConnect;
|
||||
|
||||
/// The libpq TLS keywords, extracted from `database.args` before the DSN is
|
||||
/// built (see `synapse.storage.rust_dbapi.split_ssl_params`), since
|
||||
/// `tokio-postgres` can't parse the cert paths or the `verify-*` modes.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TlsParams {
|
||||
pub sslmode: Option<String>,
|
||||
pub sslrootcert: Option<String>,
|
||||
pub sslcert: Option<String>,
|
||||
pub sslkey: Option<String>,
|
||||
pub sslpassword: Option<String>,
|
||||
}
|
||||
|
||||
/// The TLS connector to hand to [`tokio_postgres::Config::connect`].
|
||||
#[derive(Clone)]
|
||||
pub enum TlsConnector {
|
||||
/// `sslmode=disable`: never negotiate TLS.
|
||||
NoTls,
|
||||
/// Any other mode: negotiate TLS with the verifier chosen per `sslmode`.
|
||||
Rustls(MakeRustlsConnect),
|
||||
}
|
||||
|
||||
/// How strictly the server certificate is checked, derived from `sslmode`.
|
||||
enum Verification {
|
||||
/// Encrypt but don't verify the certificate (`prefer` / `require`).
|
||||
AcceptAny,
|
||||
/// Verify the certificate chain but not the hostname (`verify-ca`).
|
||||
ChainOnly,
|
||||
/// Verify the chain and the hostname (`verify-full`).
|
||||
Full,
|
||||
}
|
||||
|
||||
/// Fill unset [`TlsParams`] fields from libpq's `PGSSL*` environment
|
||||
/// variables, as libpq does for keywords the connection string omits.
|
||||
///
|
||||
/// Called at pool/connection construction (see `ConnectionManager::from_dsn`,
|
||||
/// next to the other `PG*` env handling in `fixup_config_defaults`) rather
|
||||
/// than inside [`build`], which stays a pure function of its params.
|
||||
///
|
||||
/// Deliberately more conservative than libpq where a stray variable could
|
||||
/// break a working setup:
|
||||
/// - a set-but-empty variable is treated as unset, as libpq does;
|
||||
/// - `PGSSLCERT`/`PGSSLKEY` are imported only as a complete pair, and only
|
||||
/// when neither is configured explicitly. libpq resolves a lone sslcert's
|
||||
/// key lazily (`~/.postgresql/postgresql.key`) and only fails if the
|
||||
/// server actually requests a client certificate; we build the TLS config
|
||||
/// eagerly, so a leftover `PGSSLCERT` from some other tool must not abort
|
||||
/// every connection.
|
||||
pub(crate) fn with_env_defaults(params: &TlsParams) -> TlsParams {
|
||||
fn env_nonempty(var: &str) -> Option<String> {
|
||||
std::env::var(var).ok().filter(|value| !value.is_empty())
|
||||
}
|
||||
fn fallback(value: &Option<String>, var: &str) -> Option<String> {
|
||||
value.clone().or_else(|| env_nonempty(var))
|
||||
}
|
||||
|
||||
let (sslcert, sslkey) = match (¶ms.sslcert, ¶ms.sslkey) {
|
||||
(None, None) => match (env_nonempty("PGSSLCERT"), env_nonempty("PGSSLKEY")) {
|
||||
(Some(cert), Some(key)) => (Some(cert), Some(key)),
|
||||
_ => (None, None),
|
||||
},
|
||||
(cert, key) => (cert.clone(), key.clone()),
|
||||
};
|
||||
|
||||
TlsParams {
|
||||
sslmode: fallback(¶ms.sslmode, "PGSSLMODE"),
|
||||
sslrootcert: fallback(¶ms.sslrootcert, "PGSSLROOTCERT"),
|
||||
sslcert,
|
||||
sslkey,
|
||||
sslpassword: fallback(¶ms.sslpassword, "PGSSLPASSWORD"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve [`TlsParams`] into the `tokio-postgres` [`SslMode`] to set on the
|
||||
/// connection config and the [`TlsConnector`] to connect with. A pure
|
||||
/// function of its params: the `PGSSL*` env fallbacks are applied by the
|
||||
/// caller (see [`with_env_defaults`]).
|
||||
pub fn build(params: &TlsParams) -> anyhow::Result<(SslMode, TlsConnector)> {
|
||||
let mode = params.sslmode.as_deref().unwrap_or("prefer");
|
||||
let (ssl_mode, verification) = match mode {
|
||||
"disable" => return Ok((SslMode::Disable, TlsConnector::NoTls)),
|
||||
// libpq's `allow` tries plaintext first then TLS; `tokio-postgres` has no
|
||||
// such mode, so map it to `prefer` (TLS first, then plaintext) — both end
|
||||
// up encrypted-or-not without verification.
|
||||
"allow" | "prefer" => (SslMode::Prefer, Verification::AcceptAny),
|
||||
"require" => (SslMode::Require, Verification::AcceptAny),
|
||||
"verify-ca" => (SslMode::Require, Verification::ChainOnly),
|
||||
"verify-full" => (SslMode::Require, Verification::Full),
|
||||
other => return Err(anyhow!("unsupported sslmode: {other:?}")),
|
||||
};
|
||||
|
||||
let config = build_client_config(params, verification)?;
|
||||
Ok((
|
||||
ssl_mode,
|
||||
TlsConnector::Rustls(MakeRustlsConnect::new(config)),
|
||||
))
|
||||
}
|
||||
|
||||
/// Build the `rustls` client config: the verifier for the chosen level, plus a
|
||||
/// client certificate for mutual TLS if `sslcert`/`sslkey` are set.
|
||||
fn build_client_config(
|
||||
params: &TlsParams,
|
||||
verification: Verification,
|
||||
) -> anyhow::Result<ClientConfig> {
|
||||
// Use the `ring` provider explicitly (the crate is built without the default
|
||||
// `aws-lc-rs`, which needs a C toolchain we don't have here).
|
||||
let provider = Arc::new(ring::default_provider());
|
||||
|
||||
let builder = ClientConfig::builder_with_provider(provider.clone())
|
||||
.with_safe_default_protocol_versions()
|
||||
.context("no TLS protocol versions available")?;
|
||||
|
||||
let builder = match verification {
|
||||
Verification::AcceptAny => builder
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(AcceptAnyServerCert { provider })),
|
||||
Verification::ChainOnly => {
|
||||
let verifier = WebPkiServerVerifier::builder_with_provider(
|
||||
Arc::new(root_store(params)?),
|
||||
provider,
|
||||
)
|
||||
.build()
|
||||
.context("building the certificate verifier")?;
|
||||
builder
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoHostnameServerCert {
|
||||
inner: verifier,
|
||||
}))
|
||||
}
|
||||
Verification::Full => builder.with_root_certificates(root_store(params)?),
|
||||
};
|
||||
|
||||
// Mutual TLS: present a client certificate if configured.
|
||||
let config = match (¶ms.sslcert, ¶ms.sslkey) {
|
||||
(Some(cert), Some(key)) => builder
|
||||
.with_client_auth_cert(
|
||||
load_certs(cert)?,
|
||||
load_key(key, params.sslpassword.as_deref())?,
|
||||
)
|
||||
.context("configuring the client certificate")?,
|
||||
(None, None) => builder.with_no_client_auth(),
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"sslcert and sslkey must be set together for client-certificate auth"
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// The root store for chain verification: `sslrootcert` if given (a PEM CA
|
||||
/// bundle), otherwise the platform's native root store — as libpq falls back to
|
||||
/// the system trust store.
|
||||
fn root_store(params: &TlsParams) -> anyhow::Result<RootCertStore> {
|
||||
let mut store = RootCertStore::empty();
|
||||
match ¶ms.sslrootcert {
|
||||
Some(path) => {
|
||||
for cert in load_certs(path)? {
|
||||
store
|
||||
.add(cert)
|
||||
.with_context(|| format!("adding a CA certificate from {path}"))?;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let result = rustls_native_certs::load_native_certs();
|
||||
for cert in result.certs {
|
||||
// Ignore individual malformed system certs, as rustls does.
|
||||
let _ = store.add(cert);
|
||||
}
|
||||
if store.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"no usable system CA certificates found; set sslrootcert"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(store)
|
||||
}
|
||||
|
||||
/// Load a PEM certificate (chain) file into DER certificates.
|
||||
fn load_certs(path: &str) -> anyhow::Result<Vec<CertificateDer<'static>>> {
|
||||
let mut reader = BufReader::new(File::open(path).with_context(|| format!("opening {path}"))?);
|
||||
let certs = rustls_pemfile::certs(&mut reader)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.with_context(|| format!("reading certificates from {path}"))?;
|
||||
if certs.is_empty() {
|
||||
return Err(anyhow!("no certificates found in {path}"));
|
||||
}
|
||||
Ok(certs)
|
||||
}
|
||||
|
||||
/// Load a PEM private-key file. Encrypted keys are not supported by
|
||||
/// `rustls-pemfile`; if no usable key is found and an `sslpassword` was
|
||||
/// supplied, say so in the error. A password alongside an *unencrypted* key
|
||||
/// is simply ignored, as libpq ignores it — a stray `PGSSLPASSWORD` in the
|
||||
/// environment must not break working mutual TLS.
|
||||
fn load_key(path: &str, sslpassword: Option<&str>) -> anyhow::Result<PrivateKeyDer<'static>> {
|
||||
let mut reader = BufReader::new(File::open(path).with_context(|| format!("opening {path}"))?);
|
||||
match rustls_pemfile::private_key(&mut reader)
|
||||
.with_context(|| format!("reading a private key from {path}"))?
|
||||
{
|
||||
Some(key) => Ok(key),
|
||||
None if sslpassword.is_some() => Err(anyhow!(
|
||||
"no usable private key found in {path}: encrypted client keys \
|
||||
(sslpassword) are not supported by the Rust driver"
|
||||
)),
|
||||
None => Err(anyhow!("no private key found in {path}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept any server certificate (encryption without verification), for
|
||||
/// `sslmode=prefer`/`require`. The TLS handshake signatures are still checked by
|
||||
/// the crypto provider — only the certificate's trust chain is not.
|
||||
#[derive(Debug)]
|
||||
struct AcceptAnyServerCert {
|
||||
provider: Arc<CryptoProvider>,
|
||||
}
|
||||
|
||||
impl ServerCertVerifier for AcceptAnyServerCert {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> Result<ServerCertVerified, RustlsError> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, RustlsError> {
|
||||
verify_tls12_signature(
|
||||
message,
|
||||
cert,
|
||||
dss,
|
||||
&self.provider.signature_verification_algorithms,
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, RustlsError> {
|
||||
verify_tls13_signature(
|
||||
message,
|
||||
cert,
|
||||
dss,
|
||||
&self.provider.signature_verification_algorithms,
|
||||
)
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
self.provider
|
||||
.signature_verification_algorithms
|
||||
.supported_schemes()
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify the certificate chain but tolerate a hostname mismatch, for
|
||||
/// `sslmode=verify-ca`. Delegates everything to a [`WebPkiServerVerifier`] and
|
||||
/// only maps its hostname-mismatch error to success.
|
||||
#[derive(Debug)]
|
||||
struct NoHostnameServerCert {
|
||||
inner: Arc<WebPkiServerVerifier>,
|
||||
}
|
||||
|
||||
impl ServerCertVerifier for NoHostnameServerCert {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
end_entity: &CertificateDer<'_>,
|
||||
intermediates: &[CertificateDer<'_>],
|
||||
server_name: &ServerName<'_>,
|
||||
ocsp_response: &[u8],
|
||||
now: UnixTime,
|
||||
) -> Result<ServerCertVerified, RustlsError> {
|
||||
tolerate_name_mismatch(self.inner.verify_server_cert(
|
||||
end_entity,
|
||||
intermediates,
|
||||
server_name,
|
||||
ocsp_response,
|
||||
now,
|
||||
))
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, RustlsError> {
|
||||
self.inner.verify_tls12_signature(message, cert, dss)
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, RustlsError> {
|
||||
self.inner.verify_tls13_signature(message, cert, dss)
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
self.inner.supported_verify_schemes()
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a hostname-mismatch verification error to success, for `verify-ca`.
|
||||
/// rustls reports mismatches as `NotValidForNameContext` (`NotValidForName` is
|
||||
/// its context-free legacy spelling; match both in case a future verifier
|
||||
/// reverts to it). Everything else — expiry, unknown CA, … — passes through.
|
||||
fn tolerate_name_mismatch(
|
||||
result: Result<ServerCertVerified, RustlsError>,
|
||||
) -> Result<ServerCertVerified, RustlsError> {
|
||||
match result {
|
||||
Err(RustlsError::InvalidCertificate(
|
||||
CertificateError::NotValidForName | CertificateError::NotValidForNameContext { .. },
|
||||
)) => Ok(ServerCertVerified::assertion()),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn params(sslmode: &str) -> TlsParams {
|
||||
TlsParams {
|
||||
sslmode: Some(sslmode.to_owned()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disable_uses_no_tls() {
|
||||
let (mode, connector) = build(¶ms("disable")).unwrap();
|
||||
assert_eq!(mode, SslMode::Disable);
|
||||
assert!(matches!(connector, TlsConnector::NoTls));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_is_prefer() {
|
||||
let (mode, connector) = build(&TlsParams::default()).unwrap();
|
||||
assert_eq!(mode, SslMode::Prefer);
|
||||
assert!(matches!(connector, TlsConnector::Rustls(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn require_and_verify_modes_use_require_handshake() {
|
||||
for mode in ["require", "verify-ca", "verify-full"] {
|
||||
let (ssl_mode, connector) = build(¶ms(mode)).unwrap();
|
||||
assert_eq!(ssl_mode, SslMode::Require, "mode {mode}");
|
||||
assert!(matches!(connector, TlsConnector::Rustls(_)), "mode {mode}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_sslmode_is_rejected() {
|
||||
assert!(build(¶ms("bogus")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sslcert_without_sslkey_is_rejected() {
|
||||
let p = TlsParams {
|
||||
sslmode: Some("require".to_owned()),
|
||||
sslcert: Some("/tmp/cert.pem".to_owned()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(build(&p).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_ca_tolerates_both_hostname_mismatch_spellings() {
|
||||
// The context-carrying variant rustls's webpki verifier actually
|
||||
// returns for a hostname mismatch...
|
||||
let ctx = CertificateError::NotValidForNameContext {
|
||||
expected: ServerName::try_from("db.example.com").unwrap(),
|
||||
presented: vec!["DnsName(\"other.example.com\")".to_owned()],
|
||||
};
|
||||
assert!(tolerate_name_mismatch(Err(RustlsError::InvalidCertificate(ctx))).is_ok());
|
||||
|
||||
// ...and its context-free legacy spelling.
|
||||
assert!(tolerate_name_mismatch(Err(RustlsError::InvalidCertificate(
|
||||
CertificateError::NotValidForName
|
||||
)))
|
||||
.is_ok());
|
||||
|
||||
// Any other certificate failure still propagates.
|
||||
assert!(tolerate_name_mismatch(Err(RustlsError::InvalidCertificate(
|
||||
CertificateError::Expired
|
||||
)))
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
@@ -222,9 +222,10 @@ def _make_rust_pool(
|
||||
from synapse.storage.rust_pool import RustConnectionPool
|
||||
|
||||
db_args = db_config.config.get("args", {})
|
||||
dsn = rust_dbapi.build_dsn(
|
||||
dsn_args, ssl_params = rust_dbapi.split_ssl_params(
|
||||
{k: v for k, v in db_args.items() if not k.startswith("cp_")}
|
||||
)
|
||||
dsn = rust_dbapi.build_dsn(dsn_args)
|
||||
# Size the pool (threads and connections, 1:1) to the configured cp_max;
|
||||
# Twisted's adbapi default is 5.
|
||||
threads = db_args.get("cp_max", 5)
|
||||
@@ -237,6 +238,7 @@ def _make_rust_pool(
|
||||
threads=threads,
|
||||
synchronous_commit=engine.synchronous_commit,
|
||||
statement_timeout_ms=engine.statement_timeout,
|
||||
ssl_params=ssl_params,
|
||||
)
|
||||
pool.start()
|
||||
clock.add_system_event_trigger("during", "shutdown", pool.close)
|
||||
@@ -282,16 +284,19 @@ def make_conn(
|
||||
if isinstance(engine, RustPostgresEngine):
|
||||
# The Rust backend has no `module.connect`; open a standalone (pool-of-one)
|
||||
# connection from the same libpq args, with the engine's session settings.
|
||||
# The `ssl*` keys go to the pool as explicit params rather than in the DSN.
|
||||
from synapse.storage import rust_dbapi
|
||||
|
||||
if runtime is None:
|
||||
runtime = rust_dbapi.standalone_runtime()
|
||||
|
||||
dsn_args, ssl_params = rust_dbapi.split_ssl_params(db_params)
|
||||
native_db_conn = rust_dbapi.connect(
|
||||
runtime,
|
||||
rust_dbapi.build_dsn(db_params),
|
||||
rust_dbapi.build_dsn(dsn_args),
|
||||
synchronous_commit=engine.synchronous_commit,
|
||||
statement_timeout_ms=engine.statement_timeout,
|
||||
ssl_params=ssl_params,
|
||||
)
|
||||
else:
|
||||
native_db_conn = engine.module.connect(**db_params)
|
||||
|
||||
@@ -165,6 +165,41 @@ def build_dsn(params: Mapping[str, Any]) -> str:
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
# libpq TLS keywords. psycopg2 forwards these to libpq; the Rust pool takes them
|
||||
# as explicit params to :class:`ConnectionPool` rather than in the DSN, because
|
||||
# ``tokio_postgres`` can't parse the cert-file paths and rejects the
|
||||
# ``verify-ca`` / ``verify-full`` modes. See ``rust/src/database/postgres/tls.rs``.
|
||||
SSL_PARAM_KEYS = ("sslmode", "sslrootcert", "sslcert", "sslkey", "sslpassword")
|
||||
|
||||
|
||||
def split_ssl_params(
|
||||
args: Mapping[str, Any],
|
||||
) -> "tuple[dict[str, Any], dict[str, Any]]":
|
||||
"""Split connection args into (DSN args, TLS params).
|
||||
|
||||
The TLS params (the ``ssl*`` keys, ``None`` values dropped) are passed to the
|
||||
Rust pool separately; everything else goes into the DSN. Keeps existing
|
||||
psycopg2-style ``database.args`` — which may carry ``sslmode`` etc. — working
|
||||
on the Rust backend.
|
||||
|
||||
libpq's deprecated ``requiressl`` is translated to the ``sslmode`` it
|
||||
spells (``1`` → ``require``, else ``prefer``) rather than dropped — a
|
||||
config that demanded an encrypted connection must not silently lose that
|
||||
requirement. An explicit ``sslmode`` wins, as in libpq.
|
||||
"""
|
||||
ssl_params = {key: args[key] for key in SSL_PARAM_KEYS if args.get(key) is not None}
|
||||
requiressl = args.get("requiressl")
|
||||
if requiressl is not None and "sslmode" not in ssl_params:
|
||||
truthy = str(requiressl).lower() in ("1", "true", "yes", "on")
|
||||
ssl_params["sslmode"] = "require" if truthy else "prefer"
|
||||
dsn_args = {
|
||||
key: value
|
||||
for key, value in args.items()
|
||||
if key not in SSL_PARAM_KEYS and key != "requiressl"
|
||||
}
|
||||
return dsn_args, ssl_params
|
||||
|
||||
|
||||
class _StandaloneReactor:
|
||||
"""What ``RustRuntime`` asks of the reactor: somewhere to register its
|
||||
Twisted-dispatch reader. A standalone runtime only ever drives blocking
|
||||
@@ -213,13 +248,15 @@ def connect(
|
||||
*,
|
||||
synchronous_commit: bool = True,
|
||||
statement_timeout_ms: int | None = None,
|
||||
ssl_params: Mapping[str, Any] | None = None,
|
||||
) -> "Connection":
|
||||
"""Open a single standalone connection for bootstrap/one-off use.
|
||||
|
||||
The Rust shim is pool-only, so a lone connection is a pool of one; the
|
||||
returned :class:`Connection` keeps that pool alive for its lifetime. Used by
|
||||
``make_conn`` for the startup connection that runs schema preparation before
|
||||
the real pool exists.
|
||||
the real pool exists. ``ssl_params`` are the libpq ``ssl*`` keys (see
|
||||
:func:`split_ssl_params`).
|
||||
|
||||
``runtime`` is the ``RustRuntime`` whose tokio runtime drives the
|
||||
connection; it is started on first use, so this works during startup.
|
||||
@@ -230,6 +267,7 @@ def connect(
|
||||
1,
|
||||
synchronous_commit=synchronous_commit,
|
||||
statement_timeout_ms=statement_timeout_ms,
|
||||
**(ssl_params or {}),
|
||||
)
|
||||
return Connection(pool.connect(), pool=pool, owns_pool=True)
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ class RustConnectionPool:
|
||||
threads: int = 10,
|
||||
synchronous_commit: bool = True,
|
||||
statement_timeout_ms: int | None = None,
|
||||
ssl_params: "dict[str, Any] | None" = None,
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
@@ -90,6 +91,9 @@ class RustConnectionPool:
|
||||
synchronous_commit: passed to each pooled connection's session setup.
|
||||
statement_timeout_ms: passed to each pooled connection's session
|
||||
setup (statements running longer are aborted).
|
||||
ssl_params: the libpq ``ssl*`` keys (see
|
||||
:func:`synapse.storage.rust_dbapi.split_ssl_params`), passed to
|
||||
each pooled connection's TLS setup.
|
||||
|
||||
The owner is responsible for the lifecycle: call :meth:`start` before
|
||||
use and :meth:`close` on shutdown (e.g. via the Synapse clock's
|
||||
@@ -101,6 +105,7 @@ class RustConnectionPool:
|
||||
self._dsn = dsn
|
||||
self._synchronous_commit = synchronous_commit
|
||||
self._statement_timeout_ms = statement_timeout_ms
|
||||
self._ssl_params = dict(ssl_params or {})
|
||||
self._threads = threads
|
||||
self._pool: Any = self._open_pool()
|
||||
self.threadpool = ThreadPool(minthreads=1, maxthreads=threads, name=name)
|
||||
@@ -121,6 +126,7 @@ class RustConnectionPool:
|
||||
self._threads,
|
||||
synchronous_commit=self._synchronous_commit,
|
||||
statement_timeout_ms=self._statement_timeout_ms,
|
||||
**self._ssl_params,
|
||||
)
|
||||
|
||||
def _default_connection_factory(
|
||||
|
||||
@@ -30,6 +30,11 @@ class ConnectionPool:
|
||||
*,
|
||||
synchronous_commit: bool = True,
|
||||
statement_timeout_ms: int | None = None,
|
||||
sslmode: str | None = None,
|
||||
sslrootcert: str | None = None,
|
||||
sslcert: str | None = None,
|
||||
sslkey: str | None = None,
|
||||
sslpassword: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
``runtime`` is the homeserver's ``RustRuntime`` (``hs.get_rust_runtime()``);
|
||||
@@ -39,6 +44,9 @@ class ConnectionPool:
|
||||
Each connection gets the same session setup when opened: the default
|
||||
(REPEATABLE READ) isolation level, plus ``synchronous_commit`` off and
|
||||
``statement_timeout`` (milliseconds) if configured.
|
||||
|
||||
The ``ssl*`` keyword arguments are the libpq TLS settings; see
|
||||
``rust/src/database/postgres/tls.rs``.
|
||||
"""
|
||||
|
||||
def connect(self) -> Connection:
|
||||
|
||||
@@ -122,6 +122,24 @@ class BuildDsnTestCase(unittest.TestCase):
|
||||
rust_dbapi.build_dsn({"dbname": "d", key: value})
|
||||
self.assertIn(key, str(ctx.exception))
|
||||
|
||||
def test_requiressl_maps_to_sslmode(self) -> None:
|
||||
# libpq's deprecated requiressl=1 spells sslmode=require; the
|
||||
# encryption requirement must not be silently dropped. An explicit
|
||||
# sslmode wins, as in libpq.
|
||||
dsn_args, ssl_params = rust_dbapi.split_ssl_params(
|
||||
{"dbname": "d", "requiressl": 1}
|
||||
)
|
||||
self.assertEqual(ssl_params, {"sslmode": "require"})
|
||||
self.assertEqual(dsn_args, {"dbname": "d"})
|
||||
|
||||
_, ssl_params = rust_dbapi.split_ssl_params({"requiressl": 0})
|
||||
self.assertEqual(ssl_params, {"sslmode": "prefer"})
|
||||
|
||||
_, ssl_params = rust_dbapi.split_ssl_params(
|
||||
{"requiressl": 1, "sslmode": "verify-full"}
|
||||
)
|
||||
self.assertEqual(ssl_params, {"sslmode": "verify-full"})
|
||||
|
||||
def test_supported_dsn_keys_are_accepted_by_the_parser(self) -> None:
|
||||
# _SUPPORTED_DSN_KEYS mirrors tokio_postgres's Config::param keyword
|
||||
# set; if the crate is upgraded and a key is renamed or removed, this
|
||||
|
||||
Reference in new issue
Block a user