Skip to main content

eggress_protocol_http/
h2_connect.rs

1use std::collections::HashMap;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
5use std::sync::{Arc, LazyLock, Mutex};
6use std::task::{Context, Poll};
7use std::time::{Duration, Instant};
8
9use base64::Engine;
10use bytes::Bytes;
11#[cfg(test)]
12use h2::server::Connection;
13use tokio::io::{AsyncReadExt, AsyncWriteExt};
14#[cfg(test)]
15use tokio::net::TcpStream;
16use tokio::sync::{Notify, Semaphore};
17use tokio_util::sync::CancellationToken;
18
19use crate::error::HttpError;
20use eggress_core::connector::{ConnectOptions, DirectConnector};
21use eggress_core::{BoxStream, ConnectError, TargetAddr};
22
23const H2_RELAY_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
24
25// ===== H2 Protocol Metrics (atomic counters for bridging into MetricsRegistry) =====
26
27/// Atomic counters for H2 protocol-level metrics. The `MetricsRegistry`
28/// bridges these into Prometheus via `set_h2_metrics()` / `render_prometheus()`.
29pub struct H2ProtocolMetrics {
30    pub connections_opened: AtomicU64,
31    pub connections_closed: AtomicU64,
32    pub streams_opened: AtomicU64,
33    pub streams_closed: AtomicU64,
34    pub goaway_received: AtomicU64,
35    pub handshake_failures: AtomicU64,
36    pub auth_failures: AtomicU64,
37    pub flow_control_stalls: AtomicU64,
38    pub pool_exhausted: AtomicU64,
39    pub bytes_relayed: AtomicU64,
40}
41
42impl H2ProtocolMetrics {
43    pub const fn new() -> Self {
44        Self {
45            connections_opened: AtomicU64::new(0),
46            connections_closed: AtomicU64::new(0),
47            streams_opened: AtomicU64::new(0),
48            streams_closed: AtomicU64::new(0),
49            goaway_received: AtomicU64::new(0),
50            handshake_failures: AtomicU64::new(0),
51            auth_failures: AtomicU64::new(0),
52            flow_control_stalls: AtomicU64::new(0),
53            pool_exhausted: AtomicU64::new(0),
54            bytes_relayed: AtomicU64::new(0),
55        }
56    }
57}
58
59impl Default for H2ProtocolMetrics {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65/// Global H2 protocol metrics instance.
66pub static H2_PROTOCOL_METRICS: LazyLock<Arc<H2ProtocolMetrics>> =
67    LazyLock::new(|| Arc::new(H2ProtocolMetrics::new()));
68
69#[derive(Debug, thiserror::Error)]
70pub enum H2ConnectError {
71    #[error("IO error: {0}")]
72    Io(#[from] std::io::Error),
73    #[error("H2 protocol error: {0}")]
74    H2(String),
75    #[error("HTTP error: {0}")]
76    Http(#[from] HttpError),
77    #[error("pool exhausted: no connections available and pool at capacity")]
78    PoolExhausted,
79    #[error("DNS rebinding detected: target resolved to reserved/private address {0}")]
80    DnsRebinding(std::net::IpAddr),
81}
82
83impl From<h2::Error> for H2ConnectError {
84    fn from(e: h2::Error) -> Self {
85        H2ConnectError::H2(e.to_string())
86    }
87}
88
89pub struct H2StreamWrite {
90    send_stream: h2::SendStream<Bytes>,
91    capacity: usize,
92}
93
94impl H2StreamWrite {
95    pub fn new(send_stream: h2::SendStream<Bytes>) -> Self {
96        Self {
97            send_stream,
98            capacity: 0,
99        }
100    }
101}
102
103impl tokio::io::AsyncWrite for H2StreamWrite {
104    fn poll_write(
105        mut self: Pin<&mut Self>,
106        cx: &mut Context<'_>,
107        buf: &[u8],
108    ) -> Poll<Result<usize, std::io::Error>> {
109        if self.capacity == 0 {
110            self.send_stream.reserve_capacity(buf.len());
111            match self.send_stream.poll_capacity(cx) {
112                Poll::Ready(Some(Ok(capacity))) => {
113                    if capacity == 0 {
114                        // h2 advertises zero capacity; `reserve_capacity`
115                        // has registered us for a wake-up. Returning
116                        // `Pending` prevents the AsyncWrite caller from
117                        // busy-looping on `Ok(0)`.
118                        H2_PROTOCOL_METRICS
119                            .flow_control_stalls
120                            .fetch_add(1, Ordering::Relaxed);
121                        return Poll::Pending;
122                    }
123                    self.capacity = capacity;
124                }
125                Poll::Ready(Some(Err(e))) => {
126                    return Poll::Ready(Err(std::io::Error::other(e)));
127                }
128                Poll::Ready(None) => {
129                    return Poll::Ready(Err(std::io::Error::other("h2 stream closed")));
130                }
131                Poll::Pending => {
132                    H2_PROTOCOL_METRICS
133                        .flow_control_stalls
134                        .fetch_add(1, Ordering::Relaxed);
135                    return Poll::Pending;
136                }
137            }
138        }
139
140        let len = buf.len().min(self.capacity);
141        self.send_stream
142            .send_data(Bytes::copy_from_slice(&buf[..len]), false)
143            .map_err(std::io::Error::other)?;
144        self.capacity -= len;
145        Poll::Ready(Ok(len))
146    }
147
148    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
149        Poll::Ready(Ok(()))
150    }
151
152    fn poll_shutdown(
153        mut self: Pin<&mut Self>,
154        _cx: &mut Context<'_>,
155    ) -> Poll<Result<(), std::io::Error>> {
156        self.send_stream
157            .send_data(Bytes::new(), true)
158            .map_err(std::io::Error::other)?;
159        Poll::Ready(Ok(()))
160    }
161}
162
163/// Relay one established H2 CONNECT stream to `target`.
164///
165/// This low-level helper applies the reserved/private target policy to both
166/// DNS results and literal IP targets. Authentication and connection
167/// admission remain the caller's responsibility.
168pub async fn h2_connect_relay(
169    mut recv_stream: h2::RecvStream,
170    send_stream: h2::SendStream<Bytes>,
171    target: TargetAddr,
172) -> Result<(), H2ConnectError> {
173    let tcp: BoxStream = DirectConnector
174        .connect_with_options(
175            &target,
176            &ConnectOptions {
177                enforce_dns_rebinding_check: true,
178                enforce_literal_ip_check: true,
179                ..ConnectOptions::default()
180            },
181        )
182        .await
183        .map_err(|error| match error {
184            ConnectError::ReservedTarget(ip) => H2ConnectError::DnsRebinding(ip),
185            ConnectError::Io(error) => H2ConnectError::Io(error),
186            error => H2ConnectError::H2(error.to_string()),
187        })?;
188    let (mut tcp_read, mut tcp_write) = tokio::io::split(tcp);
189    let mut h2_write = H2StreamWrite::new(send_stream);
190
191    let h2_to_tcp = async move {
192        loop {
193            match recv_stream.data().await {
194                Some(Ok(data)) => {
195                    let len = data.len();
196                    tcp_write.write_all(&data).await?;
197                    H2_PROTOCOL_METRICS
198                        .bytes_relayed
199                        .fetch_add(len as u64, Ordering::Relaxed);
200                }
201                Some(Err(e)) => {
202                    return Err(std::io::Error::other(e));
203                }
204                None => break,
205            }
206        }
207        Ok::<(), std::io::Error>(())
208    };
209
210    let tcp_to_h2 = async {
211        let mut buf = [0u8; 65536];
212        loop {
213            let n = tcp_read.read(&mut buf).await?;
214            if n == 0 {
215                h2_write.shutdown().await?;
216                break;
217            }
218            h2_write.write_all(&buf[..n]).await?;
219            H2_PROTOCOL_METRICS
220                .bytes_relayed
221                .fetch_add(n as u64, Ordering::Relaxed);
222        }
223        Ok::<(), std::io::Error>(())
224    };
225
226    let h2_task = tokio::spawn(h2_to_tcp);
227    let tcp_result = tcp_to_h2.await;
228    let mut h2_task = h2_task;
229    let h2_result = match tokio::time::timeout(H2_RELAY_DRAIN_TIMEOUT, &mut h2_task).await {
230        Ok(result) => {
231            result.map_err(|error| H2ConnectError::H2(format!("H2 relay task failed: {error}")))?
232        }
233        Err(_) => {
234            tracing::warn!(
235                "H2 relay drain timed out after target close; aborting h2->tcp direction"
236            );
237            h2_task.abort();
238            let _ = h2_task.await;
239            // Treat drain timeout as graceful close with partial bytes already accounted
240            // in H2_PROTOCOL_METRICS; do not return a hard error so callers can
241            // still observe bytes relayed.
242            tcp_result?;
243            return Ok(());
244        }
245    };
246
247    h2_result?;
248    tcp_result?;
249    Ok(())
250}
251
252/// Test-only accept loop for a plain H2 CONNECT proxy.
253///
254/// Deliberately not part of the public API: it performs no authentication and
255/// applies no outbound screening, so exposing it would hand embedders an
256/// unauthenticated open-proxy relay. Production listeners must use
257/// `eggress-server`'s `serve_h2_connection`, which authenticates and routes
258/// through the policy-enforcing executor.
259#[cfg(test)]
260pub(crate) async fn handle_h2_connect(
261    mut connection: Connection<TcpStream, Bytes>,
262) -> Result<(), H2ConnectError> {
263    loop {
264        match connection.accept().await {
265            Some(Ok((request, mut send_response))) => {
266                if *request.method() == http::Method::CONNECT {
267                    let authority = request
268                        .uri()
269                        .authority()
270                        .ok_or_else(|| H2ConnectError::H2("missing authority".into()))?;
271
272                    let target_str = match authority.port_u16() {
273                        Some(port) => format!("{}:{}", authority.host(), port),
274                        None => format!("{}:443", authority.host()),
275                    };
276
277                    let target: TargetAddr = target_str
278                        .parse()
279                        .map_err(|e: String| H2ConnectError::H2(e))?;
280
281                    let response = http::Response::builder()
282                        .status(200)
283                        .body(())
284                        .expect("static response builds");
285
286                    let send_stream = send_response.send_response(response, false)?;
287                    let recv_stream = request.into_body();
288
289                    tokio::spawn(async move {
290                        if let Err(e) = h2_connect_relay(recv_stream, send_stream, target).await {
291                            tracing::warn!("h2 connect relay error: {}", e);
292                        }
293                    });
294                } else {
295                    send_response.send_reset(h2::Reason::PROTOCOL_ERROR);
296                }
297            }
298            Some(Err(e)) => {
299                return Err(H2ConnectError::H2(e.to_string()));
300            }
301            None => break,
302        }
303    }
304    Ok(())
305}
306
307pub struct H2StreamRead {
308    recv: h2::RecvStream,
309    buffer: Bytes,
310}
311
312impl H2StreamRead {
313    pub fn new(recv: h2::RecvStream) -> Self {
314        Self {
315            recv,
316            buffer: Bytes::new(),
317        }
318    }
319}
320
321impl tokio::io::AsyncRead for H2StreamRead {
322    fn poll_read(
323        self: std::pin::Pin<&mut Self>,
324        cx: &mut Context<'_>,
325        buf: &mut tokio::io::ReadBuf<'_>,
326    ) -> Poll<std::io::Result<()>> {
327        let this = self.get_mut();
328
329        if !this.buffer.is_empty() {
330            let len = this.buffer.len().min(buf.remaining());
331            buf.put_slice(&this.buffer.split_to(len));
332            this.recv
333                .flow_control()
334                .release_capacity(len)
335                .map_err(std::io::Error::other)?;
336            return Poll::Ready(Ok(()));
337        }
338
339        let poll = {
340            let mut data_fut = Box::pin(this.recv.data());
341            data_fut.as_mut().poll(cx)
342        };
343        match poll {
344            Poll::Ready(Some(Ok(data))) => {
345                let len = data.len().min(buf.remaining());
346                buf.put_slice(&data[..len]);
347                if len < data.len() {
348                    this.buffer = data.slice(len..);
349                }
350                this.recv
351                    .flow_control()
352                    .release_capacity(len)
353                    .map_err(std::io::Error::other)?;
354                Poll::Ready(Ok(()))
355            }
356            Poll::Ready(Some(Err(e))) => Poll::Ready(Err(std::io::Error::other(e))),
357            Poll::Ready(None) => Poll::Ready(Ok(())),
358            Poll::Pending => Poll::Pending,
359        }
360    }
361}
362
363/// Perform an H2 CONNECT handshake as a client.
364///
365/// Establishes an HTTP/2 connection over the given stream, sends a CONNECT
366/// request for the specified target authority, and returns the bidirectional
367/// stream pair plus a connection task handle.
368///
369/// The caller must keep the `JoinHandle` alive (or `.abort()` it) for the
370/// duration of the relay — dropping it will close the H2 connection.
371pub async fn h2_connect_client<S>(
372    stream: S,
373    target: &TargetAddr,
374    auth: Option<(&str, &str)>,
375) -> Result<
376    (
377        h2::SendStream<Bytes>,
378        h2::RecvStream,
379        tokio::task::JoinHandle<Result<(), h2::Error>>,
380    ),
381    H2ConnectError,
382>
383where
384    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
385{
386    let (mut send_request, conn) = h2::client::handshake(stream).await?;
387
388    let conn_handle = tokio::spawn(async move {
389        if let Err(e) = conn.await {
390            tracing::debug!(%e, "H2 connection driver terminated with error");
391            return Err(e);
392        }
393        Ok(())
394    });
395
396    let authority = match target.port {
397        443 => target.host.to_string(),
398        port => format!("{}:{}", target.host, port),
399    };
400
401    let mut builder = http::Request::builder()
402        .method(http::Method::CONNECT)
403        .uri(&authority)
404        .header(http::header::HOST, &authority);
405
406    if let Some((user, pass)) = auth {
407        let credentials = format!("{}:{}", user, pass);
408        let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
409        builder = builder.header(
410            http::header::PROXY_AUTHORIZATION,
411            format!("Basic {}", encoded),
412        );
413    }
414
415    let request = builder
416        .body(())
417        .map_err(|e| H2ConnectError::H2(e.to_string()))?;
418
419    let (response_future, send_stream) = send_request.send_request(request, false)?;
420
421    let response = response_future.await?;
422    if response.status() != http::StatusCode::OK {
423        return Err(H2ConnectError::H2(format!(
424            "CONNECT rejected with status {}",
425            response.status()
426        )));
427    }
428
429    let recv_stream = response.into_body();
430    Ok((send_stream, recv_stream, conn_handle))
431}
432
433// ===== H2 Connection Pool =====
434
435/// Pool key identifying a unique H2 upstream connection group.
436///
437/// Includes `hop_index` to prevent cross-chain pooling: when the same
438/// upstream endpoint appears at different positions in distinct chains,
439/// connections must not be shared because the preceding hops differ.
440#[derive(Debug, Clone, Eq, PartialEq, Hash)]
441pub struct H2PoolKey {
442    pub endpoint_host: String,
443    pub endpoint_port: u16,
444    pub use_tls: bool,
445    pub server_name: Option<String>,
446    /// SHA-256 digest of the credentials. Pool isolation must not rest on a
447    /// 64-bit non-keyed hash: attacker-chosen `(user, password)` pairs could
448    /// otherwise collide and cross-reuse a pooled connection authenticated
449    /// as another identity.
450    pub auth_hash: Option<[u8; 32]>,
451    pub hop_index: usize,
452}
453
454impl H2PoolKey {
455    pub fn new(
456        host: &str,
457        port: u16,
458        use_tls: bool,
459        server_name: Option<&str>,
460        auth: Option<(&str, &str)>,
461    ) -> Self {
462        Self::with_hop_index(host, port, use_tls, server_name, auth, 0)
463    }
464
465    /// Create a pool key with an explicit hop index for cross-chain isolation.
466    pub fn with_hop_index(
467        host: &str,
468        port: u16,
469        use_tls: bool,
470        server_name: Option<&str>,
471        auth: Option<(&str, &str)>,
472        hop_index: usize,
473    ) -> Self {
474        let auth_hash = auth.map(|(u, p)| {
475            use sha2::{Digest, Sha256};
476            let mut hasher = Sha256::new();
477            hasher.update(u.as_bytes());
478            hasher.update([0]);
479            hasher.update(p.as_bytes());
480            hasher.finalize().into()
481        });
482        Self {
483            endpoint_host: host.to_string(),
484            endpoint_port: port,
485            use_tls,
486            server_name: server_name.map(|s| s.to_string()),
487            auth_hash,
488            hop_index,
489        }
490    }
491}
492
493/// Metadata for a pooled H2 connection.
494pub struct H2ConnectionEntry {
495    // `send_request` is synchronous and the guard is dropped before awaiting
496    // the response, so this lock never blocks a Tokio worker on I/O.
497    sender: Arc<Mutex<h2::client::SendRequest<Bytes>>>,
498    conn_handle: tokio::task::JoinHandle<Result<(), h2::Error>>,
499    #[allow(dead_code)]
500    created_at: Instant,
501    last_used: AtomicU64,
502    active_streams: AtomicU64,
503    retired: AtomicBool,
504    notify: Notify,
505}
506
507impl H2ConnectionEntry {
508    fn try_acquire(&self, max_concurrent_streams: u32) -> bool {
509        if self.retired.load(Ordering::Acquire) {
510            return false;
511        }
512        let mut active = self.active_streams.load(Ordering::Acquire);
513        loop {
514            if active >= max_concurrent_streams as u64 {
515                return false;
516            }
517            match self.active_streams.compare_exchange_weak(
518                active,
519                active + 1,
520                Ordering::AcqRel,
521                Ordering::Acquire,
522            ) {
523                Ok(_) => {
524                    if self.retired.load(Ordering::Acquire) {
525                        self.active_streams.fetch_sub(1, Ordering::Release);
526                        return false;
527                    }
528                    return true;
529                }
530                Err(next) => active = next,
531            }
532        }
533    }
534
535    fn mark_retired(&self) {
536        self.retired.store(true, Ordering::Release);
537    }
538}
539
540impl Drop for H2ConnectionEntry {
541    fn drop(&mut self) {
542        H2_PROTOCOL_METRICS
543            .connections_closed
544            .fetch_add(1, Ordering::Relaxed);
545        self.conn_handle.abort();
546    }
547}
548
549/// Bounded H2 connection pool with idle timeout and GOAWAY-aware retirement.
550pub struct H2ConnectionPool {
551    // Pool bookkeeping is synchronous and each guard is released before any
552    // async operation; do not hold these locks across an await.
553    entries: Mutex<Vec<Arc<H2ConnectionEntry>>>,
554    semaphore: Semaphore,
555    pool_size: u32,
556    idle_timeout: Duration,
557    max_concurrent_streams: u32,
558    created_at: Instant,
559    reaper_running: AtomicBool,
560    reaper_cancel: CancellationToken,
561}
562
563impl H2ConnectionPool {
564    pub fn new(pool_size: u32, idle_timeout: Duration, max_concurrent_streams: u32) -> Arc<Self> {
565        Arc::new(Self {
566            entries: Mutex::new(Vec::new()),
567            semaphore: Semaphore::new(pool_size as usize),
568            pool_size,
569            idle_timeout,
570            max_concurrent_streams,
571            created_at: Instant::now(),
572            reaper_running: AtomicBool::new(false),
573            reaper_cancel: CancellationToken::new(),
574        })
575    }
576
577    fn now_ticks(&self) -> u64 {
578        Instant::now()
579            .duration_since(self.created_at)
580            .as_nanos()
581            .min(u64::MAX as u128) as u64
582    }
583
584    /// Try to acquire an existing idle connection from the pool.
585    fn try_acquire_entry(&self) -> Option<Arc<H2ConnectionEntry>> {
586        let now = self.now_ticks();
587        let idle_timeout = self.idle_timeout.as_nanos().min(u64::MAX as u128) as u64;
588        // Snapshot Arcs out of the pool first (O-04): try_acquire is atomic,
589        // so there is no need to hold the pool mutex while probing entries.
590        let entries: Vec<Arc<H2ConnectionEntry>> = self
591            .entries
592            .lock()
593            .unwrap_or_else(|e| e.into_inner())
594            .iter()
595            .cloned()
596            .collect();
597        for entry in &entries {
598            if now.saturating_sub(entry.last_used.load(Ordering::Acquire)) < idle_timeout
599                && entry.try_acquire(self.max_concurrent_streams)
600            {
601                entry.last_used.store(now, Ordering::Release);
602                return Some(Arc::clone(entry));
603            }
604        }
605        None
606    }
607
608    /// Create a new H2 connection and add it to the pool.
609    async fn create_entry<S>(
610        self: &Arc<Self>,
611        stream: S,
612    ) -> Result<Arc<H2ConnectionEntry>, H2ConnectError>
613    where
614        S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
615    {
616        let (send_request, conn) = h2::client::handshake(stream).await?;
617
618        let conn_handle = tokio::spawn(async move {
619            if let Err(e) = conn.await {
620                tracing::debug!(%e, "H2 pooled connection driver terminated with error");
621                return Err(e);
622            }
623            Ok(())
624        });
625
626        let sender = Arc::new(Mutex::new(send_request));
627        let entry = Arc::new(H2ConnectionEntry {
628            sender: Arc::clone(&sender),
629            conn_handle,
630            created_at: Instant::now(),
631            last_used: AtomicU64::new(self.now_ticks()),
632            active_streams: AtomicU64::new(1),
633            retired: AtomicBool::new(false),
634            notify: Notify::new(),
635        });
636
637        self.entries
638            .lock()
639            .unwrap_or_else(|e| e.into_inner())
640            .push(Arc::clone(&entry));
641        H2_PROTOCOL_METRICS
642            .connections_opened
643            .fetch_add(1, Ordering::Relaxed);
644        self.maybe_start_reaper();
645        Ok(entry)
646    }
647
648    fn maybe_start_reaper(self: &Arc<Self>) {
649        if self
650            .reaper_running
651            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
652            .is_err()
653        {
654            return;
655        }
656        let pool = Arc::downgrade(self);
657        let cancel = self.reaper_cancel.clone();
658        let interval = std::cmp::max(self.idle_timeout / 2, Duration::from_millis(1));
659        tokio::spawn(async move {
660            loop {
661                tokio::select! {
662                    _ = cancel.cancelled() => break,
663                    _ = tokio::time::sleep(interval) => {
664                        let Some(pool) = pool.upgrade() else { break };
665                        pool.reap_idle_entries();
666                    }
667                }
668            }
669        });
670    }
671
672    fn reap_idle_entries(&self) {
673        let now = self.now_ticks();
674        let idle_timeout = self.idle_timeout.as_nanos().min(u64::MAX as u128) as u64;
675        self.entries
676            .lock()
677            .unwrap_or_else(|e| e.into_inner())
678            .retain(|entry| {
679                if entry.retired.load(Ordering::Acquire) {
680                    return false;
681                }
682                if now.saturating_sub(entry.last_used.load(Ordering::Acquire)) >= idle_timeout
683                    && entry.active_streams.load(Ordering::Acquire) == 0
684                {
685                    entry.mark_retired();
686                    return false;
687                }
688                true
689            });
690    }
691
692    fn is_empty(&self) -> bool {
693        self.entries
694            .lock()
695            .unwrap_or_else(|e| e.into_inner())
696            .is_empty()
697    }
698
699    /// Release a connection back to the pool after a stream completes.
700    pub fn release(&self, entry: &Arc<H2ConnectionEntry>) {
701        entry.active_streams.fetch_sub(1, Ordering::AcqRel);
702        entry.last_used.store(self.now_ticks(), Ordering::Release);
703        entry.notify.notify_waiters();
704    }
705
706    /// Mark a connection as retired (e.g., on GOAWAY).
707    pub fn retire(&self, entry: &Arc<H2ConnectionEntry>) {
708        entry.mark_retired();
709    }
710
711    /// Get pool statistics.
712    pub fn stats(&self) -> H2PoolStats {
713        let entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
714        let active = entries
715            .iter()
716            .filter(|e| !e.retired.load(Ordering::Acquire))
717            .count();
718        let total_streams: u64 = entries
719            .iter()
720            .map(|e| e.active_streams.load(Ordering::Acquire))
721            .sum();
722        H2PoolStats {
723            pool_size: self.pool_size,
724            active_connections: active as u32,
725            total_streams,
726            idle_timeout_secs: self.idle_timeout.as_secs(),
727        }
728    }
729}
730
731impl Drop for H2ConnectionPool {
732    fn drop(&mut self) {
733        self.reaper_cancel.cancel();
734    }
735}
736
737/// Pool statistics snapshot.
738#[derive(Debug, Clone)]
739pub struct H2PoolStats {
740    pub pool_size: u32,
741    pub active_connections: u32,
742    pub total_streams: u64,
743    pub idle_timeout_secs: u64,
744}
745
746/// Global H2 connection pool registry, keyed by (endpoint_host, endpoint_port, use_tls, server_name, auth_hash).
747pub struct H2PoolRegistry {
748    // Registry access only creates or looks up pools synchronously; no guard
749    // may be held across an async operation.
750    pools: std::sync::RwLock<HashMap<H2PoolKey, Arc<H2ConnectionPool>>>,
751    default_pool_size: u32,
752    default_idle_timeout: Duration,
753    default_max_concurrent_streams: u32,
754}
755
756impl H2PoolRegistry {
757    pub fn new() -> Self {
758        Self {
759            pools: std::sync::RwLock::new(HashMap::new()),
760            default_pool_size: 4,
761            default_idle_timeout: Duration::from_secs(60),
762            default_max_concurrent_streams: 100,
763        }
764    }
765
766    /// Get or create a pool for the given key.
767    pub fn get_or_create(&self, key: &H2PoolKey) -> Arc<H2ConnectionPool> {
768        let mut pools = self.pools.write().unwrap_or_else(|e| e.into_inner());
769        if pools.len() >= 64 {
770            pools.retain(|_, pool| Arc::strong_count(pool) > 1 || !pool.is_empty());
771        }
772        pools
773            .entry(key.clone())
774            .or_insert_with(|| {
775                H2ConnectionPool::new(
776                    self.default_pool_size,
777                    self.default_idle_timeout,
778                    self.default_max_concurrent_streams,
779                )
780            })
781            .clone()
782    }
783
784    /// Remove idle pools that are no longer referenced by an active stream.
785    ///
786    /// Pruning acquires a write lock, so we skip the work when the registry
787    /// is small. The threshold keeps contention bounded under H2 load.
788    pub fn prune_idle_pools(&self) {
789        let mut pools = self.pools.write().unwrap_or_else(|e| e.into_inner());
790        if pools.len() < 64 {
791            return;
792        }
793        pools.retain(|_, pool| Arc::strong_count(pool) > 1 || !pool.is_empty());
794    }
795
796    /// Drop all registry-owned pools, for example after a configuration reload.
797    pub fn clear(&self) {
798        self.pools
799            .write()
800            .unwrap_or_else(|e| e.into_inner())
801            .clear();
802    }
803
804    /// Configure default pool settings.
805    pub fn with_defaults(
806        pool_size: u32,
807        idle_timeout: Duration,
808        max_concurrent_streams: u32,
809    ) -> Self {
810        Self {
811            pools: std::sync::RwLock::new(HashMap::new()),
812            default_pool_size: pool_size,
813            default_idle_timeout: idle_timeout,
814            default_max_concurrent_streams: max_concurrent_streams,
815        }
816    }
817}
818
819impl Default for H2PoolRegistry {
820    fn default() -> Self {
821        Self::new()
822    }
823}
824
825/// Global pool registry instance.
826pub static H2_POOL_REGISTRY: LazyLock<H2PoolRegistry> = LazyLock::new(H2PoolRegistry::new);
827
828/// A guard that releases an H2 connection back to the pool when dropped.
829pub struct H2PoolGuard {
830    entry: Arc<H2ConnectionEntry>,
831    pool: Arc<H2ConnectionPool>,
832}
833
834impl Drop for H2PoolGuard {
835    fn drop(&mut self) {
836        H2_PROTOCOL_METRICS
837            .streams_closed
838            .fetch_add(1, Ordering::Relaxed);
839        self.pool.release(&self.entry);
840    }
841}
842
843impl H2PoolGuard {
844    /// Mark this connection as retired (e.g., on GOAWAY).
845    pub fn retire(&self) {
846        H2_PROTOCOL_METRICS
847            .goaway_received
848            .fetch_add(1, Ordering::Relaxed);
849        H2_PROTOCOL_METRICS
850            .connections_closed
851            .fetch_add(1, Ordering::Relaxed);
852        self.pool.retire(&self.entry);
853    }
854
855    /// Get the sender for creating new streams on this connection.
856    pub fn sender(&self) -> &Arc<Mutex<h2::client::SendRequest<Bytes>>> {
857        &self.entry.sender
858    }
859}
860
861/// Perform an H2 CONNECT handshake using a pooled connection.
862///
863/// Acquires a connection from the pool (or creates a new one), sends a CONNECT
864/// request, and returns the bidirectional streams with a pool guard. When the
865/// guard is dropped, the connection is released back to the pool.
866pub async fn h2_connect_client_pooled<S>(
867    stream: S,
868    target: &TargetAddr,
869    auth: Option<(&str, &str)>,
870    pool_key: &H2PoolKey,
871) -> Result<(h2::SendStream<Bytes>, h2::RecvStream, H2PoolGuard), H2ConnectError>
872where
873    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
874{
875    H2_POOL_REGISTRY.prune_idle_pools();
876    let pool = H2_POOL_REGISTRY.get_or_create(pool_key);
877
878    // Try to acquire an existing connection from the pool
879    if let Some(result) = try_pooled_connection(&pool, target, auth).await {
880        return result;
881    }
882
883    // Existing entries reserve stream capacity with `active_streams`; the
884    // semaphore only limits newly created physical H2 connections.
885    // No available connection — create a new one.
886    let _permit = pool.semaphore.acquire().await.map_err(|_| {
887        H2_PROTOCOL_METRICS
888            .pool_exhausted
889            .fetch_add(1, Ordering::Relaxed);
890        H2ConnectError::PoolExhausted
891    })?;
892
893    let entry = pool.create_entry(stream).await?;
894
895    let authority = match target.port {
896        443 => target.host.to_string(),
897        port => format!("{}:{}", target.host, port),
898    };
899
900    let mut builder = http::Request::builder()
901        .method(http::Method::CONNECT)
902        .uri(&authority)
903        .header(http::header::HOST, &authority);
904
905    if let Some((user, pass)) = auth {
906        let credentials = format!("{}:{}", user, pass);
907        let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
908        builder = builder.header(
909            http::header::PROXY_AUTHORIZATION,
910            format!("Basic {}", encoded),
911        );
912    }
913
914    let request = builder
915        .body(())
916        .map_err(|e| H2ConnectError::H2(e.to_string()))?;
917
918    let (response_future, send_stream) = {
919        let mut sender = entry.sender.lock().unwrap_or_else(|e| e.into_inner());
920        sender.send_request(request, false)?
921    };
922
923    let response = response_future.await?;
924    if response.status() != http::StatusCode::OK {
925        pool.retire(&entry);
926        if response.status() == http::StatusCode::PROXY_AUTHENTICATION_REQUIRED {
927            H2_PROTOCOL_METRICS
928                .auth_failures
929                .fetch_add(1, Ordering::Relaxed);
930        }
931        return Err(H2ConnectError::H2(format!(
932            "CONNECT rejected with status {}",
933            response.status()
934        )));
935    }
936
937    let recv_stream = response.into_body();
938    H2_PROTOCOL_METRICS
939        .streams_opened
940        .fetch_add(1, Ordering::Relaxed);
941    let guard = H2PoolGuard {
942        entry: Arc::clone(&entry),
943        pool: Arc::clone(&pool),
944    };
945    Ok((send_stream, recv_stream, guard))
946}
947
948/// Try to send a CONNECT request on an existing pooled connection.
949/// Returns `Some(result)` if a connection was found, `None` if no connection available.
950async fn try_pooled_connection(
951    pool: &Arc<H2ConnectionPool>,
952    target: &TargetAddr,
953    auth: Option<(&str, &str)>,
954) -> Option<Result<(h2::SendStream<Bytes>, h2::RecvStream, H2PoolGuard), H2ConnectError>> {
955    let entry = pool.try_acquire_entry()?;
956
957    let authority = match target.port {
958        443 => target.host.to_string(),
959        port => format!("{}:{}", target.host, port),
960    };
961
962    let mut builder = http::Request::builder()
963        .method(http::Method::CONNECT)
964        .uri(&authority)
965        .header(http::header::HOST, &authority);
966
967    if let Some((user, pass)) = auth {
968        let credentials = format!("{}:{}", user, pass);
969        let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
970        builder = builder.header(
971            http::header::PROXY_AUTHORIZATION,
972            format!("Basic {}", encoded),
973        );
974    }
975
976    let request = match builder.body(()) {
977        Ok(r) => r,
978        Err(e) => return Some(Err(H2ConnectError::H2(e.to_string()))),
979    };
980
981    let result = {
982        let mut sender = entry.sender.lock().unwrap_or_else(|e| e.into_inner());
983        sender.send_request(request, false)
984    };
985
986    match result {
987        Ok((response_future, send_stream)) => {
988            let response = match response_future.await {
989                Ok(r) => r,
990                Err(e) => {
991                    // Balance the try_acquire bump before retiring so the entry
992                    // can be reaped cleanly and other capacity is freed.
993                    entry.active_streams.fetch_sub(1, Ordering::AcqRel);
994                    pool.retire(&entry);
995                    return Some(Err(e.into()));
996                }
997            };
998            if response.status() != http::StatusCode::OK {
999                entry.active_streams.fetch_sub(1, Ordering::AcqRel);
1000                pool.retire(&entry);
1001                if response.status() == http::StatusCode::PROXY_AUTHENTICATION_REQUIRED {
1002                    H2_PROTOCOL_METRICS
1003                        .auth_failures
1004                        .fetch_add(1, Ordering::Relaxed);
1005                }
1006                return Some(Err(H2ConnectError::H2(format!(
1007                    "CONNECT rejected with status {}",
1008                    response.status()
1009                ))));
1010            }
1011            H2_PROTOCOL_METRICS
1012                .streams_opened
1013                .fetch_add(1, Ordering::Relaxed);
1014            let recv_stream = response.into_body();
1015            let guard = H2PoolGuard {
1016                entry: Arc::clone(&entry),
1017                pool: Arc::clone(pool),
1018            };
1019            Some(Ok((send_stream, recv_stream, guard)))
1020        }
1021        Err(_) => {
1022            // GOAWAY or connection error — retire this entry, fall through to new connection
1023            entry.active_streams.fetch_sub(1, Ordering::AcqRel);
1024            pool.retire(&entry);
1025            None
1026        }
1027    }
1028}
1029
1030#[cfg(test)]
1031mod tests {
1032    use super::*;
1033
1034    #[test]
1035    fn test_h2_connect_error_display() {
1036        let err = H2ConnectError::Io(std::io::Error::new(
1037            std::io::ErrorKind::ConnectionRefused,
1038            "test",
1039        ));
1040        assert!(err.to_string().contains("IO error"));
1041    }
1042
1043    #[test]
1044    fn test_h2_connect_error_from_h2() {
1045        let err = H2ConnectError::H2("test error".into());
1046        assert_eq!(err.to_string(), "H2 protocol error: test error");
1047    }
1048
1049    #[test]
1050    fn test_h2_connect_error_display_variants() {
1051        let err = H2ConnectError::Io(std::io::Error::new(
1052            std::io::ErrorKind::BrokenPipe,
1053            "broken",
1054        ));
1055        assert!(err.to_string().contains("broken"));
1056
1057        let err = H2ConnectError::H2("stream reset".into());
1058        assert!(err.to_string().contains("stream reset"));
1059    }
1060
1061    #[test]
1062    fn test_h2_connect_error_from_std_io() {
1063        let io_err = std::io::Error::other("test io");
1064        let err: H2ConnectError = io_err.into();
1065        assert!(matches!(err, H2ConnectError::Io(_)));
1066    }
1067
1068    #[test]
1069    fn test_h2_connect_error_pool_exhausted() {
1070        let err = H2ConnectError::PoolExhausted;
1071        assert!(err.to_string().contains("pool exhausted"));
1072    }
1073
1074    #[test]
1075    fn test_pool_key_equality() {
1076        let k1 = H2PoolKey::new("127.0.0.1", 8080, false, None, None);
1077        let k2 = H2PoolKey::new("127.0.0.1", 8080, false, None, None);
1078        assert_eq!(k1, k2);
1079
1080        let k3 = H2PoolKey::new("127.0.0.1", 8080, true, None, None);
1081        assert_ne!(k1, k3);
1082
1083        let k4 = H2PoolKey::new("127.0.0.1", 8080, false, Some("sni.example.com"), None);
1084        assert_ne!(k1, k4);
1085    }
1086
1087    #[test]
1088    fn test_pool_key_auth_hash() {
1089        let k1 = H2PoolKey::new("h", 1, false, None, Some(("u", "p")));
1090        let k2 = H2PoolKey::new("h", 1, false, None, Some(("u", "p")));
1091        let k3 = H2PoolKey::new("h", 1, false, None, Some(("u", "q")));
1092        assert_eq!(k1, k2);
1093        assert_ne!(k1, k3);
1094    }
1095
1096    #[test]
1097    fn test_pool_stats() {
1098        let pool = H2ConnectionPool::new(4, Duration::from_secs(60), 100);
1099        let stats = pool.stats();
1100        assert_eq!(stats.pool_size, 4);
1101        assert_eq!(stats.active_connections, 0);
1102        assert_eq!(stats.total_streams, 0);
1103    }
1104
1105    #[test]
1106    fn test_pool_registry_get_or_create() {
1107        let registry = H2PoolRegistry::new();
1108        let key = H2PoolKey::new("127.0.0.1", 8080, false, None, None);
1109        let p1 = registry.get_or_create(&key);
1110        let p2 = registry.get_or_create(&key);
1111        assert!(Arc::ptr_eq(&p1, &p2));
1112
1113        let key2 = H2PoolKey::new("127.0.0.1", 9090, false, None, None);
1114        let p3 = registry.get_or_create(&key2);
1115        assert!(!Arc::ptr_eq(&p1, &p3));
1116    }
1117
1118    #[test]
1119    fn test_pool_registry_prunes_idle_and_clears() {
1120        let registry = H2PoolRegistry::new();
1121        let key = H2PoolKey::new("127.0.0.1", 8080, false, None, None);
1122        let pool = registry.get_or_create(&key);
1123        drop(pool);
1124        // Below the prune threshold (64) pruning is a no-op; clearing
1125        // always removes every entry.
1126        registry.prune_idle_pools();
1127        assert!(!registry.pools.read().unwrap().is_empty());
1128        registry.clear();
1129        assert!(registry.pools.read().unwrap().is_empty());
1130
1131        let _pool = registry.get_or_create(&key);
1132        registry.clear();
1133        assert!(registry.pools.read().unwrap().is_empty());
1134    }
1135
1136    #[test]
1137    fn test_pool_key_isolates_different_auth_credentials() {
1138        let k_user_a = H2PoolKey::new(
1139            "proxy.example.com",
1140            443,
1141            true,
1142            None,
1143            Some(("alice", "secret")),
1144        );
1145        let k_user_b = H2PoolKey::new(
1146            "proxy.example.com",
1147            443,
1148            true,
1149            None,
1150            Some(("bob", "secret")),
1151        );
1152        let k_no_auth = H2PoolKey::new("proxy.example.com", 443, true, None, None);
1153
1154        assert_ne!(
1155            k_user_a, k_user_b,
1156            "different users must produce different pool keys"
1157        );
1158        assert_ne!(
1159            k_user_a, k_no_auth,
1160            "auth vs no-auth must produce different pool keys"
1161        );
1162        assert_ne!(k_user_b, k_no_auth);
1163
1164        let registry = H2PoolRegistry::new();
1165        let p1 = registry.get_or_create(&k_user_a);
1166        let p2 = registry.get_or_create(&k_user_b);
1167        let p3 = registry.get_or_create(&k_no_auth);
1168        assert!(!Arc::ptr_eq(&p1, &p2));
1169        assert!(!Arc::ptr_eq(&p1, &p3));
1170        assert!(!Arc::ptr_eq(&p2, &p3));
1171    }
1172
1173    #[test]
1174    fn test_pool_key_isolates_tls_vs_plaintext() {
1175        let k_tls = H2PoolKey::new(
1176            "proxy.example.com",
1177            443,
1178            true,
1179            Some("proxy.example.com"),
1180            None,
1181        );
1182        let k_plain = H2PoolKey::new(
1183            "proxy.example.com",
1184            443,
1185            false,
1186            Some("proxy.example.com"),
1187            None,
1188        );
1189        assert_ne!(
1190            k_tls, k_plain,
1191            "TLS vs plaintext must produce different pool keys"
1192        );
1193    }
1194
1195    #[test]
1196    fn test_pool_key_isolates_server_name() {
1197        let k_sni_a = H2PoolKey::new("1.2.3.4", 443, true, Some("a.example.com"), None);
1198        let k_sni_b = H2PoolKey::new("1.2.3.4", 443, true, Some("b.example.com"), None);
1199        assert_ne!(
1200            k_sni_a, k_sni_b,
1201            "different SNI must produce different pool keys"
1202        );
1203    }
1204
1205    #[tokio::test]
1206    async fn test_handle_h2_connect_accepts() {
1207        let server_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1208        let server_addr = server_listener.local_addr().unwrap();
1209
1210        let server_handle = tokio::spawn(async move {
1211            let (stream, _) = server_listener.accept().await.unwrap();
1212            let conn = h2::server::handshake(stream).await.unwrap();
1213            handle_h2_connect(conn).await.ok();
1214        });
1215
1216        let client_stream = TcpStream::connect(server_addr).await.unwrap();
1217        let (mut send_request, conn) = h2::client::handshake(client_stream).await.unwrap();
1218
1219        let conn_handle = tokio::spawn(async move {
1220            conn.await.ok();
1221        });
1222
1223        let request = http::Request::builder()
1224            .method(http::Method::CONNECT)
1225            .uri("127.0.0.1:9999")
1226            .body(())
1227            .unwrap();
1228
1229        let (response_future, _send_stream) = send_request.send_request(request, true).unwrap();
1230
1231        let response = tokio::time::timeout(std::time::Duration::from_secs(3), response_future)
1232            .await
1233            .unwrap()
1234            .unwrap();
1235        assert_eq!(response.status(), http::StatusCode::OK);
1236
1237        drop(send_request);
1238        drop(_send_stream);
1239        conn_handle.abort();
1240        server_handle.abort();
1241    }
1242
1243    #[tokio::test]
1244    async fn test_h2_connect_relay_rejects_reserved_literal_target() {
1245        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1246        let address = listener.local_addr().unwrap();
1247
1248        let server_handle = tokio::spawn(async move {
1249            let (stream, _) = listener.accept().await.unwrap();
1250            let mut connection = h2::server::handshake(stream).await.unwrap();
1251            let (request, mut response) = connection.accept().await.unwrap().unwrap();
1252            let send_stream = response
1253                .send_response(
1254                    http::Response::builder().status(200).body(()).unwrap(),
1255                    false,
1256                )
1257                .unwrap();
1258            h2_connect_relay(
1259                request.into_body(),
1260                send_stream,
1261                "127.0.0.1:1".parse().unwrap(),
1262            )
1263            .await
1264        });
1265
1266        let client_stream = TcpStream::connect(address).await.unwrap();
1267        let (mut send_request, connection) = h2::client::handshake(client_stream).await.unwrap();
1268        let connection_handle = tokio::spawn(async move { connection.await.ok() });
1269        let request = http::Request::builder()
1270            .method(http::Method::CONNECT)
1271            .uri("127.0.0.1:1")
1272            .body(())
1273            .unwrap();
1274        let (response, _send_stream) = send_request.send_request(request, true).unwrap();
1275        let _ = response.await;
1276
1277        assert!(matches!(
1278            server_handle.await.unwrap(),
1279            Err(H2ConnectError::DnsRebinding(_))
1280        ));
1281        connection_handle.abort();
1282    }
1283
1284    // NOTE: Connection reuse is tested at the integration level in
1285    // upstream_protocols.rs::h2_upstream_connection_reuse which exercises the
1286    // full stack through the ServiceSupervisor.
1287    //
1288    // RST_STREAM and GOAWAY fault injection tests are at the integration level
1289    // in upstream_protocols.rs::h2_upstream_rst_stream_recovery and
1290    // h2_upstream_goaway_recovery.
1291}