Skip to main content

qail_pg/driver/connection/
connect.rs

1//! Connection establishment — connect_*, TLS, mTLS, Unix socket.
2
3#[cfg(all(target_os = "linux", feature = "native-io-uring"))]
4use super::helpers::should_try_uring_plain;
5use super::helpers::{
6    connect_backend_for_stream, plain_connect_attempt_backend, record_connect_attempt,
7    record_connect_result,
8};
9use super::types::{
10    BUFFER_CAPACITY, CONNECT_BACKEND_TOKIO, CONNECT_TRANSPORT_GSSENC, CONNECT_TRANSPORT_MTLS,
11    CONNECT_TRANSPORT_PLAIN, CONNECT_TRANSPORT_TLS, ConnectParams, DEFAULT_CONNECT_TIMEOUT,
12    GSSENC_REQUEST, GssEncNegotiationResult, PgConnection, SSL_REQUEST, STMT_CACHE_CAPACITY,
13    StatementCache, TlsConfig, has_logical_replication_startup_mode,
14};
15use crate::driver::stream::PgStream;
16use crate::driver::{AuthSettings, ConnectOptions, GssEncMode, PgError, PgResult, TlsMode};
17use crate::protocol::PROTOCOL_VERSION_3_0;
18use crate::protocol::wire::FrontendMessage;
19use bytes::BytesMut;
20use std::collections::{HashMap, VecDeque};
21use std::sync::Arc;
22use std::time::Instant;
23use tokio::io::AsyncWriteExt;
24use tokio::net::TcpStream;
25
26#[inline]
27fn protocol_version_from_minor(minor: u16) -> i32 {
28    ((3i32) << 16) | i32::from(minor)
29}
30
31/// Pin the process-level rustls CryptoProvider before any `ClientConfig`
32/// is built. When feature unification enables both `ring` and `aws-lc-rs`
33/// in the same binary, rustls cannot auto-select a provider and panics on
34/// first use; installing aws-lc-rs explicitly keeps TLS connects
35/// deterministic in every feature combination. A concurrent install by
36/// another thread is fine — first writer wins, the rest are no-ops.
37fn ensure_crypto_provider() {
38    use tokio_rustls::rustls::crypto::{CryptoProvider, aws_lc_rs};
39    if CryptoProvider::get_default().is_none() {
40        let _ = aws_lc_rs::default_provider().install_default();
41    }
42}
43
44fn socket_addr(host: &str, port: u16) -> String {
45    if host.contains(':') && !host.starts_with('[') {
46        format!("[{}]:{}", host, port)
47    } else {
48        format!("{}:{}", host, port)
49    }
50}
51
52fn is_explicit_protocol_version_rejection(err: &PgError) -> bool {
53    let msg = match err {
54        PgError::Connection(msg) | PgError::Protocol(msg) | PgError::Auth(msg) => msg,
55        PgError::Query(msg) => msg,
56        PgError::QueryServer(server) => &server.message,
57        _ => return false,
58    };
59
60    let lower = msg.to_ascii_lowercase();
61    lower.contains("unsupported frontend protocol")
62        || lower.contains("frontend protocol") && lower.contains("unsupported")
63        || lower.contains("protocol version") && lower.contains("not support")
64}
65
66impl PgConnection {
67    /// Connect to PostgreSQL server without authentication (trust mode).
68    ///
69    /// # Arguments
70    ///
71    /// * `host` — PostgreSQL server hostname or IP.
72    /// * `port` — TCP port (typically 5432).
73    /// * `user` — PostgreSQL role name.
74    /// * `database` — Target database name.
75    pub async fn connect(host: &str, port: u16, user: &str, database: &str) -> PgResult<Self> {
76        Self::connect_with_password(host, port, user, database, None).await
77    }
78
79    /// Connect to PostgreSQL server with optional password authentication.
80    /// Includes a default 10-second timeout covering TCP connect + handshake.
81    ///
82    /// Startup requests protocol 3.2 by default and performs a one-shot retry
83    /// with protocol 3.0 only when startup fails due to explicit
84    /// protocol-version rejection from the server.
85    pub async fn connect_with_password(
86        host: &str,
87        port: u16,
88        user: &str,
89        database: &str,
90        password: Option<&str>,
91    ) -> PgResult<Self> {
92        Self::connect_with_password_and_auth(
93            host,
94            port,
95            user,
96            database,
97            password,
98            AuthSettings::default(),
99        )
100        .await
101    }
102
103    /// Connect to PostgreSQL with explicit enterprise options.
104    ///
105    /// Negotiation preface order follows libpq:
106    ///   1. If gss_enc_mode != Disable → try GSSENCRequest on fresh TCP
107    ///   2. If GSSENC rejected/unavailable and tls_mode != Disable → try SSLRequest
108    ///   3. If both rejected/unavailable → plain StartupMessage
109    ///
110    /// The StartupMessage protocol version behavior is the same as
111    /// `connect_with_password`: request protocol 3.2 first, then retry once
112    /// with 3.0 only on explicit protocol-version rejection.
113    pub async fn connect_with_options(
114        host: &str,
115        port: u16,
116        user: &str,
117        database: &str,
118        password: Option<&str>,
119        options: ConnectOptions,
120    ) -> PgResult<Self> {
121        let ConnectOptions {
122            tls_mode,
123            gss_enc_mode,
124            tls_ca_cert_pem,
125            mtls,
126            gss_token_provider,
127            auth,
128            io_uring,
129            startup_params,
130        } = options;
131
132        if mtls.is_some() && matches!(tls_mode, TlsMode::Disable) {
133            return Err(PgError::Connection(
134                "Invalid connect options: mTLS requires tls_mode=Prefer or Require".to_string(),
135            ));
136        }
137
138        // Enforce gss_enc_mode policy before mTLS early-return.
139        // GSSENC and mTLS are both transport-level encryption; using
140        // both simultaneously is not supported by the PostgreSQL protocol.
141        if gss_enc_mode == GssEncMode::Require && mtls.is_some() {
142            return Err(PgError::Connection(
143                "gssencmode=require is incompatible with mTLS — both provide \
144                 transport encryption; use one or the other"
145                    .to_string(),
146            ));
147        }
148
149        if let Some(mtls_config) = mtls {
150            // gss_enc_mode is Disable or Prefer here (Require rejected above).
151            // mTLS already provides transport encryption; skip GSSENC.
152            return Self::connect_mtls_with_password_and_auth_and_gss(
153                ConnectParams {
154                    host,
155                    port,
156                    user,
157                    database,
158                    password,
159                    auth_settings: auth,
160                    gss_token_provider,
161                    io_uring,
162                    protocol_minor: Self::default_protocol_minor(),
163                    startup_params: startup_params.clone(),
164                },
165                mtls_config,
166            )
167            .await;
168        }
169
170        // ── Phase 1: Try GSSENC if requested ──────────────────────────
171        if gss_enc_mode != GssEncMode::Disable {
172            match Self::try_gssenc_request(host, port).await {
173                Ok(GssEncNegotiationResult::Accepted(tcp_stream)) => {
174                    let connect_started = Instant::now();
175                    record_connect_attempt(CONNECT_TRANSPORT_GSSENC, CONNECT_BACKEND_TOKIO);
176                    #[cfg(all(feature = "enterprise-gssapi", target_os = "linux"))]
177                    {
178                        let default_minor = Self::default_protocol_minor();
179                        let gss_params = ConnectParams {
180                            host,
181                            port,
182                            user,
183                            database,
184                            password,
185                            auth_settings: auth,
186                            gss_token_provider: gss_token_provider.clone(),
187                            io_uring,
188                            protocol_minor: default_minor,
189                            startup_params: startup_params.clone(),
190                        };
191                        let mut result = Self::connect_gssenc_accepted_with_timeout(
192                            tcp_stream,
193                            gss_params.clone(),
194                        )
195                        .await;
196                        if let Err(err) = &result
197                            && default_minor > 0
198                            && is_explicit_protocol_version_rejection(err)
199                        {
200                            let downgrade_minor = (PROTOCOL_VERSION_3_0 & 0xFFFF) as u16;
201                            let retry_stream = match Self::try_gssenc_request(host, port).await {
202                                Ok(GssEncNegotiationResult::Accepted(stream)) => stream,
203                                Ok(GssEncNegotiationResult::Rejected) => {
204                                    return Err(PgError::Connection(
205                                        "Protocol downgrade retry failed: server rejected GSSENCRequest"
206                                            .to_string(),
207                                    ));
208                                }
209                                Ok(GssEncNegotiationResult::ServerError) => {
210                                    return Err(PgError::Connection(
211                                        "Protocol downgrade retry failed: server returned error to GSSENCRequest"
212                                            .to_string(),
213                                    ));
214                                }
215                                Err(e) => {
216                                    return Err(e);
217                                }
218                            };
219                            let mut retry_params = gss_params;
220                            retry_params.protocol_minor = downgrade_minor;
221                            result = Self::connect_gssenc_accepted_with_timeout(
222                                retry_stream,
223                                retry_params,
224                            )
225                            .await;
226                        }
227                        record_connect_result(
228                            CONNECT_TRANSPORT_GSSENC,
229                            CONNECT_BACKEND_TOKIO,
230                            &result,
231                            connect_started.elapsed(),
232                        );
233                        return result;
234                    }
235                    #[cfg(not(all(feature = "enterprise-gssapi", target_os = "linux")))]
236                    {
237                        let _ = tcp_stream;
238                        let err = PgError::Connection(
239                            "Server accepted GSSENCRequest but GSSAPI encryption requires \
240                             feature enterprise-gssapi on Linux"
241                                .to_string(),
242                        );
243                        metrics::histogram!(
244                            "qail_pg_connect_duration_seconds",
245                            "transport" => CONNECT_TRANSPORT_GSSENC,
246                            "backend" => CONNECT_BACKEND_TOKIO,
247                            "outcome" => "error"
248                        )
249                        .record(connect_started.elapsed().as_secs_f64());
250                        metrics::counter!(
251                            "qail_pg_connect_failure_total",
252                            "transport" => CONNECT_TRANSPORT_GSSENC,
253                            "backend" => CONNECT_BACKEND_TOKIO,
254                            "error_kind" => super::helpers::connect_error_kind(&err)
255                        )
256                        .increment(1);
257                        return Err(err);
258                    }
259                }
260                Ok(GssEncNegotiationResult::Rejected)
261                | Ok(GssEncNegotiationResult::ServerError) => {
262                    if gss_enc_mode == GssEncMode::Require {
263                        return Err(PgError::Connection(
264                            "gssencmode=require but server rejected GSSENCRequest".to_string(),
265                        ));
266                    }
267                    // gss_enc_mode == Prefer — fall through to TLS / plain
268                }
269                Err(e) => {
270                    if gss_enc_mode == GssEncMode::Require {
271                        return Err(e);
272                    }
273                    // gss_enc_mode == Prefer — connection error, fall through
274                    tracing::debug!(
275                        host = %host,
276                        port = %port,
277                        error = %e,
278                        "gssenc_prefer_fallthrough"
279                    );
280                }
281            }
282        }
283
284        // ── Phase 2: TLS / plain per sslmode ──────────────────────────
285        match tls_mode {
286            TlsMode::Disable => {
287                Self::connect_with_password_and_auth_and_gss(ConnectParams {
288                    host,
289                    port,
290                    user,
291                    database,
292                    password,
293                    auth_settings: auth,
294                    gss_token_provider,
295                    io_uring,
296                    protocol_minor: Self::default_protocol_minor(),
297                    startup_params: startup_params.clone(),
298                })
299                .await
300            }
301            TlsMode::Require => {
302                Self::connect_tls_with_auth_and_gss(
303                    ConnectParams {
304                        host,
305                        port,
306                        user,
307                        database,
308                        password,
309                        auth_settings: auth,
310                        gss_token_provider,
311                        io_uring,
312                        protocol_minor: Self::default_protocol_minor(),
313                        startup_params: startup_params.clone(),
314                    },
315                    tls_ca_cert_pem.as_deref(),
316                )
317                .await
318            }
319            TlsMode::Prefer => {
320                match Self::connect_tls_with_auth_and_gss(
321                    ConnectParams {
322                        host,
323                        port,
324                        user,
325                        database,
326                        password,
327                        auth_settings: auth,
328                        gss_token_provider: gss_token_provider.clone(),
329                        io_uring,
330                        protocol_minor: Self::default_protocol_minor(),
331                        startup_params: startup_params.clone(),
332                    },
333                    tls_ca_cert_pem.as_deref(),
334                )
335                .await
336                {
337                    Ok(conn) => Ok(conn),
338                    // Exact-sentinel match: only the SSLRequest-rejected case
339                    // may fall back to plaintext. Handshake and certificate
340                    // failures propagate and fail closed.
341                    Err(e) if e.is_tls_unsupported_by_server() => {
342                        Self::connect_with_password_and_auth_and_gss(ConnectParams {
343                            host,
344                            port,
345                            user,
346                            database,
347                            password,
348                            auth_settings: auth,
349                            gss_token_provider,
350                            io_uring,
351                            protocol_minor: Self::default_protocol_minor(),
352                            startup_params: startup_params.clone(),
353                        })
354                        .await
355                    }
356                    Err(e) => Err(e),
357                }
358            }
359        }
360    }
361
362    /// Attempt GSSAPI session encryption negotiation.
363    ///
364    /// Opens a fresh TCP connection, sends GSSENCRequest (80877104),
365    /// reads exactly one byte (CVE-2021-23222 safe), and returns
366    /// the result.  The entire operation is bounded by
367    /// `DEFAULT_CONNECT_TIMEOUT`.
368    async fn try_gssenc_request(host: &str, port: u16) -> PgResult<GssEncNegotiationResult> {
369        tokio::time::timeout(
370            DEFAULT_CONNECT_TIMEOUT,
371            Self::try_gssenc_request_inner(host, port),
372        )
373        .await
374        .map_err(|_| {
375            PgError::Connection(format!(
376                "GSSENCRequest timeout after {:?}",
377                DEFAULT_CONNECT_TIMEOUT
378            ))
379        })?
380    }
381
382    /// Inner GSSENCRequest logic without timeout wrapper.
383    async fn try_gssenc_request_inner(host: &str, port: u16) -> PgResult<GssEncNegotiationResult> {
384        use tokio::io::AsyncReadExt;
385
386        let addr = socket_addr(host, port);
387        let mut tcp_stream = TcpStream::connect(&addr).await?;
388        tcp_stream.set_nodelay(true)?;
389
390        // Send the 8-byte GSSENCRequest.
391        tcp_stream.write_all(&GSSENC_REQUEST).await?;
392        tcp_stream.flush().await?;
393
394        // CVE-2021-23222: Read exactly one byte.  The server must
395        // respond with a single 'G' or 'N'.  Any additional bytes
396        // in the buffer indicate a buffer-stuffing attack.
397        let mut response = [0u8; 1];
398        tcp_stream.read_exact(&mut response).await?;
399
400        match response[0] {
401            b'G' => {
402                // CVE-2021-23222 check: verify no extra bytes are buffered.
403                // Use a non-blocking peek to detect leftover data.
404                let mut peek_buf = [0u8; 1];
405                match tcp_stream.try_read(&mut peek_buf) {
406                    Ok(0) => {} // EOF — fine (shouldn't happen yet but harmless)
407                    Ok(_n) => {
408                        // Extra bytes after 'G' — possible buffer-stuffing.
409                        return Err(PgError::Connection(
410                            "Protocol violation: extra bytes after GSSENCRequest 'G' response \
411                             (possible CVE-2021-23222 buffer-stuffing attack)"
412                                .to_string(),
413                        ));
414                    }
415                    Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
416                        // No extra data — this is the expected path.
417                    }
418                    Err(e) => {
419                        return Err(PgError::Io(e));
420                    }
421                }
422                Ok(GssEncNegotiationResult::Accepted(tcp_stream))
423            }
424            b'N' => Ok(GssEncNegotiationResult::Rejected),
425            b'E' => {
426                // Server sent an ErrorMessage.  Per CVE-2024-10977 we
427                // must NOT display this to users since the server has
428                // not been authenticated.  Log at trace only.
429                tracing::trace!(
430                    host = %host,
431                    port = %port,
432                    "gssenc_request_server_error (suppressed per CVE-2024-10977)"
433                );
434                Ok(GssEncNegotiationResult::ServerError)
435            }
436            other => Err(PgError::Connection(format!(
437                "Unexpected response to GSSENCRequest: 0x{:02X} \
438                     (expected 'G'=0x47 or 'N'=0x4E)",
439                other
440            ))),
441        }
442    }
443
444    #[cfg(all(feature = "enterprise-gssapi", target_os = "linux"))]
445    async fn connect_gssenc_accepted_with_timeout(
446        tcp_stream: TcpStream,
447        params: ConnectParams<'_>,
448    ) -> PgResult<Self> {
449        let gssenc_fut = async {
450            let gss_stream = super::super::gss::gssenc_handshake(tcp_stream, params.host)
451                .await
452                .map_err(PgError::Auth)?;
453            let mut conn = Self {
454                stream: PgStream::GssEnc(gss_stream),
455                buffer: BytesMut::with_capacity(BUFFER_CAPACITY),
456                write_buf: BytesMut::with_capacity(BUFFER_CAPACITY),
457                sql_buf: BytesMut::with_capacity(512),
458                params_buf: Vec::with_capacity(16),
459                prepared_statements: HashMap::new(),
460                stmt_cache: StatementCache::new(STMT_CACHE_CAPACITY),
461                column_info_cache: HashMap::new(),
462                process_id: 0,
463                cancel_key_bytes: Vec::new(),
464                requested_protocol_minor: params.protocol_minor,
465                negotiated_protocol_minor: params.protocol_minor,
466                notifications: VecDeque::new(),
467                replication_stream_active: false,
468                replication_mode_enabled: has_logical_replication_startup_mode(
469                    &params.startup_params,
470                ),
471                last_replication_wal_end: None,
472                io_desynced: false,
473                pending_statement_closes: Vec::new(),
474                draining_statement_closes: false,
475            };
476            conn.send(FrontendMessage::Startup {
477                user: params.user.to_string(),
478                database: params.database.to_string(),
479                protocol_version: protocol_version_from_minor(params.protocol_minor),
480                startup_params: params.startup_params.clone(),
481            })
482            .await?;
483            conn.handle_startup(
484                params.user,
485                params.password,
486                params.auth_settings,
487                params.gss_token_provider,
488            )
489            .await?;
490            Ok(conn)
491        };
492        tokio::time::timeout(DEFAULT_CONNECT_TIMEOUT, gssenc_fut)
493            .await
494            .map_err(|_| {
495                PgError::Connection(format!(
496                    "GSSENC connection timeout after {:?} (handshake + auth)",
497                    DEFAULT_CONNECT_TIMEOUT
498                ))
499            })?
500    }
501
502    /// Connect to PostgreSQL server with optional password authentication and auth policy.
503    pub async fn connect_with_password_and_auth(
504        host: &str,
505        port: u16,
506        user: &str,
507        database: &str,
508        password: Option<&str>,
509        auth_settings: AuthSettings,
510    ) -> PgResult<Self> {
511        Self::connect_with_password_and_auth_and_gss(ConnectParams {
512            host,
513            port,
514            user,
515            database,
516            password,
517            auth_settings,
518            gss_token_provider: None,
519            io_uring: false,
520            protocol_minor: Self::default_protocol_minor(),
521            startup_params: Vec::new(),
522        })
523        .await
524    }
525
526    async fn connect_with_password_and_auth_and_gss(params: ConnectParams<'_>) -> PgResult<Self> {
527        let first = Self::connect_with_password_and_auth_and_gss_once(params.clone()).await;
528        if let Err(err) = &first
529            && params.protocol_minor > 0
530            && is_explicit_protocol_version_rejection(err)
531        {
532            let mut downgraded = params;
533            downgraded.protocol_minor = (PROTOCOL_VERSION_3_0 & 0xFFFF) as u16;
534            return Self::connect_with_password_and_auth_and_gss_once(downgraded).await;
535        }
536        first
537    }
538
539    async fn connect_with_password_and_auth_and_gss_once(
540        params: ConnectParams<'_>,
541    ) -> PgResult<Self> {
542        let connect_started = Instant::now();
543        let attempt_backend = plain_connect_attempt_backend(params.io_uring);
544        record_connect_attempt(CONNECT_TRANSPORT_PLAIN, attempt_backend);
545        let result = tokio::time::timeout(
546            DEFAULT_CONNECT_TIMEOUT,
547            Self::connect_with_password_inner(params),
548        )
549        .await
550        .map_err(|_| {
551            PgError::Connection(format!(
552                "Connection timeout after {:?} (TCP connect + handshake)",
553                DEFAULT_CONNECT_TIMEOUT
554            ))
555        })?;
556        let backend = result
557            .as_ref()
558            .map(|conn| connect_backend_for_stream(&conn.stream))
559            .unwrap_or(attempt_backend);
560        record_connect_result(
561            CONNECT_TRANSPORT_PLAIN,
562            backend,
563            &result,
564            connect_started.elapsed(),
565        );
566        result
567    }
568
569    /// Inner connection logic without timeout wrapper.
570    async fn connect_with_password_inner(params: ConnectParams<'_>) -> PgResult<Self> {
571        let ConnectParams {
572            host,
573            port,
574            user,
575            database,
576            password,
577            auth_settings,
578            gss_token_provider,
579            io_uring,
580            protocol_minor,
581            startup_params,
582        } = params;
583        let replication_mode_enabled = has_logical_replication_startup_mode(&startup_params);
584        let addr = socket_addr(host, port);
585        let stream = Self::connect_plain_stream(&addr, io_uring).await?;
586
587        let mut conn = Self {
588            stream,
589            buffer: BytesMut::with_capacity(BUFFER_CAPACITY),
590            write_buf: BytesMut::with_capacity(BUFFER_CAPACITY), // 64KB write buffer
591            sql_buf: BytesMut::with_capacity(512),
592            params_buf: Vec::with_capacity(16), // SQL encoding buffer
593            prepared_statements: HashMap::new(),
594            stmt_cache: StatementCache::new(STMT_CACHE_CAPACITY),
595            column_info_cache: HashMap::new(),
596            process_id: 0,
597            cancel_key_bytes: Vec::new(),
598            requested_protocol_minor: protocol_minor,
599            negotiated_protocol_minor: protocol_minor,
600            notifications: VecDeque::new(),
601            replication_stream_active: false,
602            replication_mode_enabled,
603            last_replication_wal_end: None,
604            io_desynced: false,
605            pending_statement_closes: Vec::new(),
606            draining_statement_closes: false,
607        };
608
609        conn.send(FrontendMessage::Startup {
610            user: user.to_string(),
611            database: database.to_string(),
612            protocol_version: protocol_version_from_minor(protocol_minor),
613            startup_params,
614        })
615        .await?;
616
617        conn.handle_startup(user, password, auth_settings, gss_token_provider)
618            .await?;
619
620        Ok(conn)
621    }
622
623    async fn connect_plain_stream(addr: &str, io_uring: bool) -> PgResult<PgStream> {
624        let tcp_stream = TcpStream::connect(addr).await?;
625        tcp_stream.set_nodelay(true)?;
626
627        #[cfg(all(target_os = "linux", feature = "native-io-uring"))]
628        {
629            if should_try_uring_plain(io_uring) {
630                let std_stream = tcp_stream.into_std()?;
631                let fallback_std = std_stream.try_clone()?;
632                match super::super::uring::UringTcpStream::from_std(std_stream) {
633                    Ok(uring_stream) => {
634                        tracing::info!(
635                            addr = %addr,
636                            "qail-pg: using io_uring plain TCP transport"
637                        );
638                        return Ok(PgStream::Uring(uring_stream));
639                    }
640                    Err(e) => {
641                        tracing::warn!(
642                            addr = %addr,
643                            error = %e,
644                            "qail-pg: io_uring stream conversion failed; falling back to tokio TCP"
645                        );
646                        fallback_std.set_nonblocking(true)?;
647                        let fallback = TcpStream::from_std(fallback_std)?;
648                        return Ok(PgStream::Tcp(fallback));
649                    }
650                }
651            }
652        }
653        #[cfg(not(all(target_os = "linux", feature = "native-io-uring")))]
654        {
655            let _ = io_uring;
656        }
657
658        Ok(PgStream::Tcp(tcp_stream))
659    }
660
661    /// Connect to PostgreSQL server with TLS encryption.
662    /// Includes a default 10-second timeout covering TCP connect + TLS + handshake.
663    pub async fn connect_tls(
664        host: &str,
665        port: u16,
666        user: &str,
667        database: &str,
668        password: Option<&str>,
669    ) -> PgResult<Self> {
670        Self::connect_tls_with_auth(
671            host,
672            port,
673            user,
674            database,
675            password,
676            AuthSettings::default(),
677            None,
678        )
679        .await
680    }
681
682    /// Connect to PostgreSQL over TLS with explicit auth policy and optional custom CA bundle.
683    pub async fn connect_tls_with_auth(
684        host: &str,
685        port: u16,
686        user: &str,
687        database: &str,
688        password: Option<&str>,
689        auth_settings: AuthSettings,
690        ca_cert_pem: Option<&[u8]>,
691    ) -> PgResult<Self> {
692        Self::connect_tls_with_auth_and_gss(
693            ConnectParams {
694                host,
695                port,
696                user,
697                database,
698                password,
699                auth_settings,
700                gss_token_provider: None,
701                io_uring: false,
702                protocol_minor: Self::default_protocol_minor(),
703                startup_params: Vec::new(),
704            },
705            ca_cert_pem,
706        )
707        .await
708    }
709
710    async fn connect_tls_with_auth_and_gss(
711        params: ConnectParams<'_>,
712        ca_cert_pem: Option<&[u8]>,
713    ) -> PgResult<Self> {
714        let first = Self::connect_tls_with_auth_and_gss_once(params.clone(), ca_cert_pem).await;
715        if let Err(err) = &first
716            && params.protocol_minor > 0
717            && is_explicit_protocol_version_rejection(err)
718        {
719            let mut downgraded = params;
720            downgraded.protocol_minor = (PROTOCOL_VERSION_3_0 & 0xFFFF) as u16;
721            return Self::connect_tls_with_auth_and_gss_once(downgraded, ca_cert_pem).await;
722        }
723        first
724    }
725
726    async fn connect_tls_with_auth_and_gss_once(
727        params: ConnectParams<'_>,
728        ca_cert_pem: Option<&[u8]>,
729    ) -> PgResult<Self> {
730        let connect_started = Instant::now();
731        record_connect_attempt(CONNECT_TRANSPORT_TLS, CONNECT_BACKEND_TOKIO);
732        let result = tokio::time::timeout(
733            DEFAULT_CONNECT_TIMEOUT,
734            Self::connect_tls_inner(params, ca_cert_pem),
735        )
736        .await
737        .map_err(|_| {
738            PgError::Connection(format!(
739                "TLS connection timeout after {:?}",
740                DEFAULT_CONNECT_TIMEOUT
741            ))
742        })?;
743        record_connect_result(
744            CONNECT_TRANSPORT_TLS,
745            CONNECT_BACKEND_TOKIO,
746            &result,
747            connect_started.elapsed(),
748        );
749        result
750    }
751
752    /// Inner TLS connection logic without timeout wrapper.
753    async fn connect_tls_inner(
754        params: ConnectParams<'_>,
755        ca_cert_pem: Option<&[u8]>,
756    ) -> PgResult<Self> {
757        let ConnectParams {
758            host,
759            port,
760            user,
761            database,
762            password,
763            auth_settings,
764            gss_token_provider,
765            io_uring: _,
766            protocol_minor,
767            startup_params,
768        } = params;
769        let replication_mode_enabled = has_logical_replication_startup_mode(&startup_params);
770        use tokio::io::AsyncReadExt;
771        use tokio_rustls::TlsConnector;
772        use tokio_rustls::rustls::ClientConfig;
773        use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, pem::PemObject};
774
775        let addr = socket_addr(host, port);
776        let mut tcp_stream = TcpStream::connect(&addr).await?;
777
778        // Send SSLRequest
779        tcp_stream.write_all(&SSL_REQUEST).await?;
780
781        // Read response
782        let mut response = [0u8; 1];
783        tcp_stream.read_exact(&mut response).await?;
784
785        if response[0] != b'S' {
786            return Err(PgError::tls_unsupported_by_server());
787        }
788
789        let mut root_cert_store = tokio_rustls::rustls::RootCertStore::empty();
790
791        if let Some(ca_pem) = ca_cert_pem {
792            let certs = CertificateDer::pem_slice_iter(ca_pem)
793                .collect::<Result<Vec<_>, _>>()
794                .map_err(|e| PgError::Connection(format!("Invalid CA certificate PEM: {}", e)))?;
795            if certs.is_empty() {
796                return Err(PgError::Connection(
797                    "No CA certificates found in provided PEM".to_string(),
798                ));
799            }
800            for cert in certs {
801                let _ = root_cert_store.add(cert);
802            }
803        } else {
804            let certs = rustls_native_certs::load_native_certs();
805            for cert in certs.certs {
806                let _ = root_cert_store.add(cert);
807            }
808        }
809
810        ensure_crypto_provider();
811        let config = ClientConfig::builder()
812            .with_root_certificates(root_cert_store)
813            .with_no_client_auth();
814
815        let connector = TlsConnector::from(Arc::new(config));
816        let server_name = ServerName::try_from(host.to_string())
817            .map_err(|_| PgError::Connection("Invalid hostname for TLS".to_string()))?;
818
819        let tls_stream = connector
820            .connect(server_name, tcp_stream)
821            .await
822            .map_err(|e| PgError::Connection(format!("TLS handshake failed: {}", e)))?;
823
824        let mut conn = Self {
825            stream: PgStream::Tls(Box::new(tls_stream)),
826            buffer: BytesMut::with_capacity(BUFFER_CAPACITY),
827            write_buf: BytesMut::with_capacity(BUFFER_CAPACITY),
828            sql_buf: BytesMut::with_capacity(512),
829            params_buf: Vec::with_capacity(16),
830            prepared_statements: HashMap::new(),
831            stmt_cache: StatementCache::new(STMT_CACHE_CAPACITY),
832            column_info_cache: HashMap::new(),
833            process_id: 0,
834            cancel_key_bytes: Vec::new(),
835            requested_protocol_minor: protocol_minor,
836            negotiated_protocol_minor: protocol_minor,
837            notifications: VecDeque::new(),
838            replication_stream_active: false,
839            replication_mode_enabled,
840            last_replication_wal_end: None,
841            io_desynced: false,
842            pending_statement_closes: Vec::new(),
843            draining_statement_closes: false,
844        };
845
846        conn.send(FrontendMessage::Startup {
847            user: user.to_string(),
848            database: database.to_string(),
849            protocol_version: protocol_version_from_minor(protocol_minor),
850            startup_params,
851        })
852        .await?;
853
854        conn.handle_startup(user, password, auth_settings, gss_token_provider)
855            .await?;
856
857        Ok(conn)
858    }
859
860    /// Connect with mutual TLS (client certificate authentication).
861    /// # Arguments
862    /// * `host` - PostgreSQL server hostname
863    /// * `port` - PostgreSQL server port
864    /// * `user` - Database user
865    /// * `database` - Database name
866    /// * `config` - TLS configuration with client cert/key
867    /// # Example
868    /// ```ignore
869    /// let config = TlsConfig {
870    ///     client_cert_pem: include_bytes!("client.crt").to_vec(),
871    ///     client_key_pem: include_bytes!("client.key").to_vec(),
872    ///     ca_cert_pem: Some(include_bytes!("ca.crt").to_vec()),
873    /// };
874    /// let conn = PgConnection::connect_mtls("localhost", 5432, "user", "db", config).await?;
875    /// ```
876    pub async fn connect_mtls(
877        host: &str,
878        port: u16,
879        user: &str,
880        database: &str,
881        config: TlsConfig,
882    ) -> PgResult<Self> {
883        Self::connect_mtls_with_password_and_auth(
884            host,
885            port,
886            user,
887            database,
888            None,
889            config,
890            AuthSettings::default(),
891        )
892        .await
893    }
894
895    /// Connect with mutual TLS and optional password fallback.
896    pub async fn connect_mtls_with_password_and_auth(
897        host: &str,
898        port: u16,
899        user: &str,
900        database: &str,
901        password: Option<&str>,
902        config: TlsConfig,
903        auth_settings: AuthSettings,
904    ) -> PgResult<Self> {
905        Self::connect_mtls_with_password_and_auth_and_gss(
906            ConnectParams {
907                host,
908                port,
909                user,
910                database,
911                password,
912                auth_settings,
913                gss_token_provider: None,
914                io_uring: false,
915                protocol_minor: Self::default_protocol_minor(),
916                startup_params: Vec::new(),
917            },
918            config,
919        )
920        .await
921    }
922
923    async fn connect_mtls_with_password_and_auth_and_gss(
924        params: ConnectParams<'_>,
925        config: TlsConfig,
926    ) -> PgResult<Self> {
927        let first =
928            Self::connect_mtls_with_password_and_auth_and_gss_once(params.clone(), config.clone())
929                .await;
930        if let Err(err) = &first
931            && params.protocol_minor > 0
932            && is_explicit_protocol_version_rejection(err)
933        {
934            let mut downgraded = params;
935            downgraded.protocol_minor = (PROTOCOL_VERSION_3_0 & 0xFFFF) as u16;
936            return Self::connect_mtls_with_password_and_auth_and_gss_once(downgraded, config)
937                .await;
938        }
939        first
940    }
941
942    async fn connect_mtls_with_password_and_auth_and_gss_once(
943        params: ConnectParams<'_>,
944        config: TlsConfig,
945    ) -> PgResult<Self> {
946        let connect_started = Instant::now();
947        record_connect_attempt(CONNECT_TRANSPORT_MTLS, CONNECT_BACKEND_TOKIO);
948        let result = tokio::time::timeout(
949            DEFAULT_CONNECT_TIMEOUT,
950            Self::connect_mtls_inner(params, config),
951        )
952        .await
953        .map_err(|_| {
954            PgError::Connection(format!(
955                "mTLS connection timeout after {:?}",
956                DEFAULT_CONNECT_TIMEOUT
957            ))
958        })?;
959        record_connect_result(
960            CONNECT_TRANSPORT_MTLS,
961            CONNECT_BACKEND_TOKIO,
962            &result,
963            connect_started.elapsed(),
964        );
965        result
966    }
967
968    /// Inner mTLS connection logic without timeout wrapper.
969    async fn connect_mtls_inner(params: ConnectParams<'_>, config: TlsConfig) -> PgResult<Self> {
970        let ConnectParams {
971            host,
972            port,
973            user,
974            database,
975            password,
976            auth_settings,
977            gss_token_provider,
978            io_uring: _,
979            protocol_minor,
980            startup_params,
981        } = params;
982        let replication_mode_enabled = has_logical_replication_startup_mode(&startup_params);
983        use tokio::io::AsyncReadExt;
984        use tokio_rustls::TlsConnector;
985        use tokio_rustls::rustls::{
986            ClientConfig,
987            pki_types::{CertificateDer, PrivateKeyDer, ServerName, pem::PemObject},
988        };
989
990        let addr = socket_addr(host, port);
991        let mut tcp_stream = TcpStream::connect(&addr).await?;
992
993        // Send SSLRequest
994        tcp_stream.write_all(&SSL_REQUEST).await?;
995
996        // Read response
997        let mut response = [0u8; 1];
998        tcp_stream.read_exact(&mut response).await?;
999
1000        if response[0] != b'S' {
1001            return Err(PgError::tls_unsupported_by_server());
1002        }
1003
1004        let mut root_cert_store = tokio_rustls::rustls::RootCertStore::empty();
1005
1006        if let Some(ca_pem) = &config.ca_cert_pem {
1007            let certs = CertificateDer::pem_slice_iter(ca_pem)
1008                .collect::<Result<Vec<_>, _>>()
1009                .map_err(|e| PgError::Connection(format!("Invalid CA certificate PEM: {}", e)))?;
1010            if certs.is_empty() {
1011                return Err(PgError::Connection(
1012                    "No CA certificates found in provided PEM".to_string(),
1013                ));
1014            }
1015            for cert in certs {
1016                let _ = root_cert_store.add(cert);
1017            }
1018        } else {
1019            // Use system certs
1020            let certs = rustls_native_certs::load_native_certs();
1021            for cert in certs.certs {
1022                let _ = root_cert_store.add(cert);
1023            }
1024        }
1025
1026        let client_certs: Vec<CertificateDer<'static>> =
1027            CertificateDer::pem_slice_iter(&config.client_cert_pem)
1028                .collect::<Result<Vec<_>, _>>()
1029                .map_err(|e| PgError::Connection(format!("Invalid client cert PEM: {}", e)))?;
1030        if client_certs.is_empty() {
1031            return Err(PgError::Connection(
1032                "No client certificates found in PEM".to_string(),
1033            ));
1034        }
1035
1036        let client_key = PrivateKeyDer::from_pem_slice(&config.client_key_pem)
1037            .map_err(|e| PgError::Connection(format!("Invalid client key PEM: {}", e)))?;
1038
1039        ensure_crypto_provider();
1040        let tls_config = ClientConfig::builder()
1041            .with_root_certificates(root_cert_store)
1042            .with_client_auth_cert(client_certs, client_key)
1043            .map_err(|e| PgError::Connection(format!("Invalid client cert/key: {}", e)))?;
1044
1045        let connector = TlsConnector::from(Arc::new(tls_config));
1046        let server_name = ServerName::try_from(host.to_string())
1047            .map_err(|_| PgError::Connection("Invalid hostname for TLS".to_string()))?;
1048
1049        let tls_stream = connector
1050            .connect(server_name, tcp_stream)
1051            .await
1052            .map_err(|e| PgError::Connection(format!("mTLS handshake failed: {}", e)))?;
1053
1054        let mut conn = Self {
1055            stream: PgStream::Tls(Box::new(tls_stream)),
1056            buffer: BytesMut::with_capacity(BUFFER_CAPACITY),
1057            write_buf: BytesMut::with_capacity(BUFFER_CAPACITY),
1058            sql_buf: BytesMut::with_capacity(512),
1059            params_buf: Vec::with_capacity(16),
1060            prepared_statements: HashMap::new(),
1061            stmt_cache: StatementCache::new(STMT_CACHE_CAPACITY),
1062            column_info_cache: HashMap::new(),
1063            process_id: 0,
1064            cancel_key_bytes: Vec::new(),
1065            requested_protocol_minor: protocol_minor,
1066            negotiated_protocol_minor: protocol_minor,
1067            notifications: VecDeque::new(),
1068            replication_stream_active: false,
1069            replication_mode_enabled,
1070            last_replication_wal_end: None,
1071            io_desynced: false,
1072            pending_statement_closes: Vec::new(),
1073            draining_statement_closes: false,
1074        };
1075
1076        conn.send(FrontendMessage::Startup {
1077            user: user.to_string(),
1078            database: database.to_string(),
1079            protocol_version: protocol_version_from_minor(protocol_minor),
1080            startup_params,
1081        })
1082        .await?;
1083
1084        conn.handle_startup(user, password, auth_settings, gss_token_provider)
1085            .await?;
1086
1087        Ok(conn)
1088    }
1089
1090    /// Connect to PostgreSQL server via Unix domain socket.
1091    #[cfg(unix)]
1092    pub async fn connect_unix(
1093        socket_path: &str,
1094        user: &str,
1095        database: &str,
1096        password: Option<&str>,
1097    ) -> PgResult<Self> {
1098        let default_minor = Self::default_protocol_minor();
1099        let first =
1100            Self::connect_unix_with_protocol(socket_path, user, database, password, default_minor)
1101                .await;
1102        if let Err(err) = &first
1103            && default_minor > 0
1104            && is_explicit_protocol_version_rejection(err)
1105        {
1106            let downgrade_minor = (PROTOCOL_VERSION_3_0 & 0xFFFF) as u16;
1107            return Self::connect_unix_with_protocol(
1108                socket_path,
1109                user,
1110                database,
1111                password,
1112                downgrade_minor,
1113            )
1114            .await;
1115        }
1116        first
1117    }
1118
1119    #[cfg(unix)]
1120    async fn connect_unix_with_protocol(
1121        socket_path: &str,
1122        user: &str,
1123        database: &str,
1124        password: Option<&str>,
1125        protocol_minor: u16,
1126    ) -> PgResult<Self> {
1127        use tokio::net::UnixStream;
1128
1129        let unix_stream = UnixStream::connect(socket_path).await?;
1130
1131        let mut conn = Self {
1132            stream: PgStream::Unix(unix_stream),
1133            buffer: BytesMut::with_capacity(BUFFER_CAPACITY),
1134            write_buf: BytesMut::with_capacity(BUFFER_CAPACITY),
1135            sql_buf: BytesMut::with_capacity(512),
1136            params_buf: Vec::with_capacity(16),
1137            prepared_statements: HashMap::new(),
1138            stmt_cache: StatementCache::new(STMT_CACHE_CAPACITY),
1139            column_info_cache: HashMap::new(),
1140            process_id: 0,
1141            cancel_key_bytes: Vec::new(),
1142            requested_protocol_minor: protocol_minor,
1143            negotiated_protocol_minor: protocol_minor,
1144            notifications: VecDeque::new(),
1145            replication_stream_active: false,
1146            replication_mode_enabled: false,
1147            last_replication_wal_end: None,
1148            io_desynced: false,
1149            pending_statement_closes: Vec::new(),
1150            draining_statement_closes: false,
1151        };
1152
1153        conn.send(FrontendMessage::Startup {
1154            user: user.to_string(),
1155            database: database.to_string(),
1156            protocol_version: protocol_version_from_minor(protocol_minor),
1157            startup_params: Vec::new(),
1158        })
1159        .await?;
1160
1161        conn.handle_startup(user, password, AuthSettings::default(), None)
1162            .await?;
1163
1164        Ok(conn)
1165    }
1166}
1167
1168#[cfg(test)]
1169mod tests {
1170    use super::{is_explicit_protocol_version_rejection, protocol_version_from_minor, socket_addr};
1171    use crate::driver::PgError;
1172
1173    #[test]
1174    fn protocol_version_from_minor_encodes_major_3() {
1175        assert_eq!(protocol_version_from_minor(2), 196610);
1176        assert_eq!(protocol_version_from_minor(0), 196608);
1177    }
1178
1179    #[test]
1180    fn socket_addr_brackets_ipv6_hosts() {
1181        assert_eq!(socket_addr("127.0.0.1", 5432), "127.0.0.1:5432");
1182        assert_eq!(socket_addr("::1", 5432), "[::1]:5432");
1183        assert_eq!(socket_addr("[::1]", 5432), "[::1]:5432");
1184    }
1185
1186    #[test]
1187    fn explicit_protocol_rejection_detection_is_case_insensitive() {
1188        let err = PgError::Connection("Unsupported frontend protocol 3.2".to_string());
1189        assert!(is_explicit_protocol_version_rejection(&err));
1190
1191        let err = PgError::Protocol("server: Protocol VERSION not supported".to_string());
1192        assert!(is_explicit_protocol_version_rejection(&err));
1193    }
1194
1195    #[test]
1196    fn explicit_protocol_rejection_does_not_match_unrelated_errors() {
1197        let err = PgError::Connection("connection reset by peer".to_string());
1198        assert!(!is_explicit_protocol_version_rejection(&err));
1199    }
1200}