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