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        conn.await?;
390        Ok(())
391    });
392
393    let authority = match target.port {
394        443 => target.host.to_string(),
395        port => format!("{}:{}", target.host, port),
396    };
397
398    let mut builder = http::Request::builder()
399        .method(http::Method::CONNECT)
400        .uri(&authority)
401        .header(http::header::HOST, &authority);
402
403    if let Some((user, pass)) = auth {
404        let credentials = format!("{}:{}", user, pass);
405        let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
406        builder = builder.header(
407            http::header::PROXY_AUTHORIZATION,
408            format!("Basic {}", encoded),
409        );
410    }
411
412    let request = builder
413        .body(())
414        .map_err(|e| H2ConnectError::H2(e.to_string()))?;
415
416    let (response_future, send_stream) = send_request.send_request(request, false)?;
417
418    let response = response_future.await?;
419    if response.status() != http::StatusCode::OK {
420        return Err(H2ConnectError::H2(format!(
421            "CONNECT rejected with status {}",
422            response.status()
423        )));
424    }
425
426    let recv_stream = response.into_body();
427    Ok((send_stream, recv_stream, conn_handle))
428}
429
430// ===== H2 Connection Pool =====
431
432/// Pool key identifying a unique H2 upstream connection group.
433///
434/// Includes `hop_index` to prevent cross-chain pooling: when the same
435/// upstream endpoint appears at different positions in distinct chains,
436/// connections must not be shared because the preceding hops differ.
437#[derive(Debug, Clone, Eq, PartialEq, Hash)]
438pub struct H2PoolKey {
439    pub endpoint_host: String,
440    pub endpoint_port: u16,
441    pub use_tls: bool,
442    pub server_name: Option<String>,
443    /// SHA-256 digest of the credentials. Pool isolation must not rest on a
444    /// 64-bit non-keyed hash: attacker-chosen `(user, password)` pairs could
445    /// otherwise collide and cross-reuse a pooled connection authenticated
446    /// as another identity.
447    pub auth_hash: Option<[u8; 32]>,
448    pub hop_index: usize,
449}
450
451impl H2PoolKey {
452    pub fn new(
453        host: &str,
454        port: u16,
455        use_tls: bool,
456        server_name: Option<&str>,
457        auth: Option<(&str, &str)>,
458    ) -> Self {
459        Self::with_hop_index(host, port, use_tls, server_name, auth, 0)
460    }
461
462    /// Create a pool key with an explicit hop index for cross-chain isolation.
463    pub fn with_hop_index(
464        host: &str,
465        port: u16,
466        use_tls: bool,
467        server_name: Option<&str>,
468        auth: Option<(&str, &str)>,
469        hop_index: usize,
470    ) -> Self {
471        let auth_hash = auth.map(|(u, p)| {
472            use sha2::{Digest, Sha256};
473            let mut hasher = Sha256::new();
474            hasher.update(u.as_bytes());
475            hasher.update([0]);
476            hasher.update(p.as_bytes());
477            hasher.finalize().into()
478        });
479        Self {
480            endpoint_host: host.to_string(),
481            endpoint_port: port,
482            use_tls,
483            server_name: server_name.map(|s| s.to_string()),
484            auth_hash,
485            hop_index,
486        }
487    }
488}
489
490/// Metadata for a pooled H2 connection.
491pub struct H2ConnectionEntry {
492    // `send_request` is synchronous and the guard is dropped before awaiting
493    // the response, so this lock never blocks a Tokio worker on I/O.
494    sender: Arc<Mutex<h2::client::SendRequest<Bytes>>>,
495    conn_handle: tokio::task::JoinHandle<Result<(), h2::Error>>,
496    #[allow(dead_code)]
497    created_at: Instant,
498    last_used: AtomicU64,
499    active_streams: AtomicU64,
500    retired: AtomicBool,
501    notify: Notify,
502}
503
504impl H2ConnectionEntry {
505    fn try_acquire(&self, max_concurrent_streams: u32) -> bool {
506        if self.retired.load(Ordering::Acquire) {
507            return false;
508        }
509        let mut active = self.active_streams.load(Ordering::Acquire);
510        loop {
511            if active >= max_concurrent_streams as u64 {
512                return false;
513            }
514            match self.active_streams.compare_exchange_weak(
515                active,
516                active + 1,
517                Ordering::AcqRel,
518                Ordering::Acquire,
519            ) {
520                Ok(_) => {
521                    if self.retired.load(Ordering::Acquire) {
522                        self.active_streams.fetch_sub(1, Ordering::Release);
523                        return false;
524                    }
525                    return true;
526                }
527                Err(next) => active = next,
528            }
529        }
530    }
531
532    fn mark_retired(&self) {
533        self.retired.store(true, Ordering::Release);
534    }
535}
536
537impl Drop for H2ConnectionEntry {
538    fn drop(&mut self) {
539        H2_PROTOCOL_METRICS
540            .connections_closed
541            .fetch_add(1, Ordering::Relaxed);
542        self.conn_handle.abort();
543    }
544}
545
546/// Bounded H2 connection pool with idle timeout and GOAWAY-aware retirement.
547pub struct H2ConnectionPool {
548    // Pool bookkeeping is synchronous and each guard is released before any
549    // async operation; do not hold these locks across an await.
550    entries: Mutex<Vec<Arc<H2ConnectionEntry>>>,
551    semaphore: Semaphore,
552    pool_size: u32,
553    idle_timeout: Duration,
554    max_concurrent_streams: u32,
555    created_at: Instant,
556    reaper_running: AtomicBool,
557    reaper_cancel: CancellationToken,
558}
559
560impl H2ConnectionPool {
561    pub fn new(pool_size: u32, idle_timeout: Duration, max_concurrent_streams: u32) -> Arc<Self> {
562        Arc::new(Self {
563            entries: Mutex::new(Vec::new()),
564            semaphore: Semaphore::new(pool_size as usize),
565            pool_size,
566            idle_timeout,
567            max_concurrent_streams,
568            created_at: Instant::now(),
569            reaper_running: AtomicBool::new(false),
570            reaper_cancel: CancellationToken::new(),
571        })
572    }
573
574    fn now_ticks(&self) -> u64 {
575        Instant::now()
576            .duration_since(self.created_at)
577            .as_nanos()
578            .min(u64::MAX as u128) as u64
579    }
580
581    /// Try to acquire an existing idle connection from the pool.
582    fn try_acquire_entry(&self) -> Option<Arc<H2ConnectionEntry>> {
583        let now = self.now_ticks();
584        let idle_timeout = self.idle_timeout.as_nanos().min(u64::MAX as u128) as u64;
585        // Snapshot Arcs out of the pool first (O-04): try_acquire is atomic,
586        // so there is no need to hold the pool mutex while probing entries.
587        let entries: Vec<Arc<H2ConnectionEntry>> = self
588            .entries
589            .lock()
590            .unwrap_or_else(|e| e.into_inner())
591            .iter()
592            .cloned()
593            .collect();
594        for entry in &entries {
595            if now.saturating_sub(entry.last_used.load(Ordering::Acquire)) < idle_timeout
596                && entry.try_acquire(self.max_concurrent_streams)
597            {
598                entry.last_used.store(now, Ordering::Release);
599                return Some(Arc::clone(entry));
600            }
601        }
602        None
603    }
604
605    /// Create a new H2 connection and add it to the pool.
606    async fn create_entry<S>(
607        self: &Arc<Self>,
608        stream: S,
609    ) -> Result<Arc<H2ConnectionEntry>, H2ConnectError>
610    where
611        S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
612    {
613        let (send_request, conn) = h2::client::handshake(stream).await?;
614
615        let conn_handle = tokio::spawn(async move {
616            conn.await?;
617            Ok(())
618        });
619
620        let sender = Arc::new(Mutex::new(send_request));
621        let entry = Arc::new(H2ConnectionEntry {
622            sender: Arc::clone(&sender),
623            conn_handle,
624            created_at: Instant::now(),
625            last_used: AtomicU64::new(self.now_ticks()),
626            active_streams: AtomicU64::new(1),
627            retired: AtomicBool::new(false),
628            notify: Notify::new(),
629        });
630
631        self.entries
632            .lock()
633            .unwrap_or_else(|e| e.into_inner())
634            .push(Arc::clone(&entry));
635        H2_PROTOCOL_METRICS
636            .connections_opened
637            .fetch_add(1, Ordering::Relaxed);
638        self.maybe_start_reaper();
639        Ok(entry)
640    }
641
642    fn maybe_start_reaper(self: &Arc<Self>) {
643        if self
644            .reaper_running
645            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
646            .is_err()
647        {
648            return;
649        }
650        let pool = Arc::downgrade(self);
651        let cancel = self.reaper_cancel.clone();
652        let interval = std::cmp::max(self.idle_timeout / 2, Duration::from_millis(1));
653        tokio::spawn(async move {
654            loop {
655                tokio::select! {
656                    _ = cancel.cancelled() => break,
657                    _ = tokio::time::sleep(interval) => {
658                        let Some(pool) = pool.upgrade() else { break };
659                        pool.reap_idle_entries();
660                    }
661                }
662            }
663        });
664    }
665
666    fn reap_idle_entries(&self) {
667        let now = self.now_ticks();
668        let idle_timeout = self.idle_timeout.as_nanos().min(u64::MAX as u128) as u64;
669        self.entries
670            .lock()
671            .unwrap_or_else(|e| e.into_inner())
672            .retain(|entry| {
673                if entry.retired.load(Ordering::Acquire) {
674                    return false;
675                }
676                if now.saturating_sub(entry.last_used.load(Ordering::Acquire)) >= idle_timeout
677                    && entry.active_streams.load(Ordering::Acquire) == 0
678                {
679                    entry.mark_retired();
680                    return false;
681                }
682                true
683            });
684    }
685
686    fn is_empty(&self) -> bool {
687        self.entries
688            .lock()
689            .unwrap_or_else(|e| e.into_inner())
690            .is_empty()
691    }
692
693    /// Release a connection back to the pool after a stream completes.
694    pub fn release(&self, entry: &Arc<H2ConnectionEntry>) {
695        entry.active_streams.fetch_sub(1, Ordering::AcqRel);
696        entry.last_used.store(self.now_ticks(), Ordering::Release);
697        entry.notify.notify_waiters();
698    }
699
700    /// Mark a connection as retired (e.g., on GOAWAY).
701    pub fn retire(&self, entry: &Arc<H2ConnectionEntry>) {
702        entry.mark_retired();
703    }
704
705    /// Get pool statistics.
706    pub fn stats(&self) -> H2PoolStats {
707        let entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
708        let active = entries
709            .iter()
710            .filter(|e| !e.retired.load(Ordering::Acquire))
711            .count();
712        let total_streams: u64 = entries
713            .iter()
714            .map(|e| e.active_streams.load(Ordering::Acquire))
715            .sum();
716        H2PoolStats {
717            pool_size: self.pool_size,
718            active_connections: active as u32,
719            total_streams,
720            idle_timeout_secs: self.idle_timeout.as_secs(),
721        }
722    }
723}
724
725impl Drop for H2ConnectionPool {
726    fn drop(&mut self) {
727        self.reaper_cancel.cancel();
728    }
729}
730
731/// Pool statistics snapshot.
732#[derive(Debug, Clone)]
733pub struct H2PoolStats {
734    pub pool_size: u32,
735    pub active_connections: u32,
736    pub total_streams: u64,
737    pub idle_timeout_secs: u64,
738}
739
740/// Global H2 connection pool registry, keyed by (endpoint_host, endpoint_port, use_tls, server_name, auth_hash).
741pub struct H2PoolRegistry {
742    // Registry access only creates or looks up pools synchronously; no guard
743    // may be held across an async operation.
744    pools: std::sync::RwLock<HashMap<H2PoolKey, Arc<H2ConnectionPool>>>,
745    default_pool_size: u32,
746    default_idle_timeout: Duration,
747    default_max_concurrent_streams: u32,
748}
749
750impl H2PoolRegistry {
751    pub fn new() -> Self {
752        Self {
753            pools: std::sync::RwLock::new(HashMap::new()),
754            default_pool_size: 4,
755            default_idle_timeout: Duration::from_secs(60),
756            default_max_concurrent_streams: 100,
757        }
758    }
759
760    /// Get or create a pool for the given key.
761    pub fn get_or_create(&self, key: &H2PoolKey) -> Arc<H2ConnectionPool> {
762        let mut pools = self.pools.write().unwrap_or_else(|e| e.into_inner());
763        if pools.len() >= 64 {
764            pools.retain(|_, pool| Arc::strong_count(pool) > 1 || !pool.is_empty());
765        }
766        pools
767            .entry(key.clone())
768            .or_insert_with(|| {
769                H2ConnectionPool::new(
770                    self.default_pool_size,
771                    self.default_idle_timeout,
772                    self.default_max_concurrent_streams,
773                )
774            })
775            .clone()
776    }
777
778    /// Remove idle pools that are no longer referenced by an active stream.
779    ///
780    /// Pruning acquires a write lock, so we skip the work when the registry
781    /// is small. The threshold keeps contention bounded under H2 load.
782    pub fn prune_idle_pools(&self) {
783        let mut pools = self.pools.write().unwrap_or_else(|e| e.into_inner());
784        if pools.len() < 64 {
785            return;
786        }
787        pools.retain(|_, pool| Arc::strong_count(pool) > 1 || !pool.is_empty());
788    }
789
790    /// Drop all registry-owned pools, for example after a configuration reload.
791    pub fn clear(&self) {
792        self.pools
793            .write()
794            .unwrap_or_else(|e| e.into_inner())
795            .clear();
796    }
797
798    /// Configure default pool settings.
799    pub fn with_defaults(
800        pool_size: u32,
801        idle_timeout: Duration,
802        max_concurrent_streams: u32,
803    ) -> Self {
804        Self {
805            pools: std::sync::RwLock::new(HashMap::new()),
806            default_pool_size: pool_size,
807            default_idle_timeout: idle_timeout,
808            default_max_concurrent_streams: max_concurrent_streams,
809        }
810    }
811}
812
813impl Default for H2PoolRegistry {
814    fn default() -> Self {
815        Self::new()
816    }
817}
818
819/// Global pool registry instance.
820pub static H2_POOL_REGISTRY: LazyLock<H2PoolRegistry> = LazyLock::new(H2PoolRegistry::new);
821
822/// A guard that releases an H2 connection back to the pool when dropped.
823pub struct H2PoolGuard {
824    entry: Arc<H2ConnectionEntry>,
825    pool: Arc<H2ConnectionPool>,
826}
827
828impl Drop for H2PoolGuard {
829    fn drop(&mut self) {
830        H2_PROTOCOL_METRICS
831            .streams_closed
832            .fetch_add(1, Ordering::Relaxed);
833        self.pool.release(&self.entry);
834    }
835}
836
837impl H2PoolGuard {
838    /// Mark this connection as retired (e.g., on GOAWAY).
839    pub fn retire(&self) {
840        H2_PROTOCOL_METRICS
841            .goaway_received
842            .fetch_add(1, Ordering::Relaxed);
843        H2_PROTOCOL_METRICS
844            .connections_closed
845            .fetch_add(1, Ordering::Relaxed);
846        self.pool.retire(&self.entry);
847    }
848
849    /// Get the sender for creating new streams on this connection.
850    pub fn sender(&self) -> &Arc<Mutex<h2::client::SendRequest<Bytes>>> {
851        &self.entry.sender
852    }
853}
854
855/// Perform an H2 CONNECT handshake using a pooled connection.
856///
857/// Acquires a connection from the pool (or creates a new one), sends a CONNECT
858/// request, and returns the bidirectional streams with a pool guard. When the
859/// guard is dropped, the connection is released back to the pool.
860pub async fn h2_connect_client_pooled<S>(
861    stream: S,
862    target: &TargetAddr,
863    auth: Option<(&str, &str)>,
864    pool_key: &H2PoolKey,
865) -> Result<(h2::SendStream<Bytes>, h2::RecvStream, H2PoolGuard), H2ConnectError>
866where
867    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
868{
869    H2_POOL_REGISTRY.prune_idle_pools();
870    let pool = H2_POOL_REGISTRY.get_or_create(pool_key);
871
872    // Try to acquire an existing connection from the pool
873    if let Some(result) = try_pooled_connection(&pool, target, auth).await {
874        return result;
875    }
876
877    // Existing entries reserve stream capacity with `active_streams`; the
878    // semaphore only limits newly created physical H2 connections.
879    // No available connection — create a new one.
880    let _permit = pool.semaphore.acquire().await.map_err(|_| {
881        H2_PROTOCOL_METRICS
882            .pool_exhausted
883            .fetch_add(1, Ordering::Relaxed);
884        H2ConnectError::PoolExhausted
885    })?;
886
887    let entry = pool.create_entry(stream).await?;
888
889    let authority = match target.port {
890        443 => target.host.to_string(),
891        port => format!("{}:{}", target.host, port),
892    };
893
894    let mut builder = http::Request::builder()
895        .method(http::Method::CONNECT)
896        .uri(&authority)
897        .header(http::header::HOST, &authority);
898
899    if let Some((user, pass)) = auth {
900        let credentials = format!("{}:{}", user, pass);
901        let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
902        builder = builder.header(
903            http::header::PROXY_AUTHORIZATION,
904            format!("Basic {}", encoded),
905        );
906    }
907
908    let request = builder
909        .body(())
910        .map_err(|e| H2ConnectError::H2(e.to_string()))?;
911
912    let (response_future, send_stream) = {
913        let mut sender = entry.sender.lock().unwrap_or_else(|e| e.into_inner());
914        sender.send_request(request, false)?
915    };
916
917    let response = response_future.await?;
918    if response.status() != http::StatusCode::OK {
919        pool.retire(&entry);
920        if response.status() == http::StatusCode::PROXY_AUTHENTICATION_REQUIRED {
921            H2_PROTOCOL_METRICS
922                .auth_failures
923                .fetch_add(1, Ordering::Relaxed);
924        }
925        return Err(H2ConnectError::H2(format!(
926            "CONNECT rejected with status {}",
927            response.status()
928        )));
929    }
930
931    let recv_stream = response.into_body();
932    H2_PROTOCOL_METRICS
933        .streams_opened
934        .fetch_add(1, Ordering::Relaxed);
935    let guard = H2PoolGuard {
936        entry: Arc::clone(&entry),
937        pool: Arc::clone(&pool),
938    };
939    Ok((send_stream, recv_stream, guard))
940}
941
942/// Try to send a CONNECT request on an existing pooled connection.
943/// Returns `Some(result)` if a connection was found, `None` if no connection available.
944async fn try_pooled_connection(
945    pool: &Arc<H2ConnectionPool>,
946    target: &TargetAddr,
947    auth: Option<(&str, &str)>,
948) -> Option<Result<(h2::SendStream<Bytes>, h2::RecvStream, H2PoolGuard), H2ConnectError>> {
949    let entry = pool.try_acquire_entry()?;
950
951    let authority = match target.port {
952        443 => target.host.to_string(),
953        port => format!("{}:{}", target.host, port),
954    };
955
956    let mut builder = http::Request::builder()
957        .method(http::Method::CONNECT)
958        .uri(&authority)
959        .header(http::header::HOST, &authority);
960
961    if let Some((user, pass)) = auth {
962        let credentials = format!("{}:{}", user, pass);
963        let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
964        builder = builder.header(
965            http::header::PROXY_AUTHORIZATION,
966            format!("Basic {}", encoded),
967        );
968    }
969
970    let request = match builder.body(()) {
971        Ok(r) => r,
972        Err(e) => return Some(Err(H2ConnectError::H2(e.to_string()))),
973    };
974
975    let result = {
976        let mut sender = entry.sender.lock().unwrap_or_else(|e| e.into_inner());
977        sender.send_request(request, false)
978    };
979
980    match result {
981        Ok((response_future, send_stream)) => {
982            let response = match response_future.await {
983                Ok(r) => r,
984                Err(e) => {
985                    // Balance the try_acquire bump before retiring so the entry
986                    // can be reaped cleanly and other capacity is freed.
987                    entry.active_streams.fetch_sub(1, Ordering::AcqRel);
988                    pool.retire(&entry);
989                    return Some(Err(e.into()));
990                }
991            };
992            if response.status() != http::StatusCode::OK {
993                entry.active_streams.fetch_sub(1, Ordering::AcqRel);
994                pool.retire(&entry);
995                if response.status() == http::StatusCode::PROXY_AUTHENTICATION_REQUIRED {
996                    H2_PROTOCOL_METRICS
997                        .auth_failures
998                        .fetch_add(1, Ordering::Relaxed);
999                }
1000                return Some(Err(H2ConnectError::H2(format!(
1001                    "CONNECT rejected with status {}",
1002                    response.status()
1003                ))));
1004            }
1005            H2_PROTOCOL_METRICS
1006                .streams_opened
1007                .fetch_add(1, Ordering::Relaxed);
1008            let recv_stream = response.into_body();
1009            let guard = H2PoolGuard {
1010                entry: Arc::clone(&entry),
1011                pool: Arc::clone(pool),
1012            };
1013            Some(Ok((send_stream, recv_stream, guard)))
1014        }
1015        Err(_) => {
1016            // GOAWAY or connection error — retire this entry, fall through to new connection
1017            entry.active_streams.fetch_sub(1, Ordering::AcqRel);
1018            pool.retire(&entry);
1019            None
1020        }
1021    }
1022}
1023
1024#[cfg(test)]
1025mod tests {
1026    use super::*;
1027
1028    #[test]
1029    fn test_h2_connect_error_display() {
1030        let err = H2ConnectError::Io(std::io::Error::new(
1031            std::io::ErrorKind::ConnectionRefused,
1032            "test",
1033        ));
1034        assert!(err.to_string().contains("IO error"));
1035    }
1036
1037    #[test]
1038    fn test_h2_connect_error_from_h2() {
1039        let err = H2ConnectError::H2("test error".into());
1040        assert_eq!(err.to_string(), "H2 protocol error: test error");
1041    }
1042
1043    #[test]
1044    fn test_h2_connect_error_display_variants() {
1045        let err = H2ConnectError::Io(std::io::Error::new(
1046            std::io::ErrorKind::BrokenPipe,
1047            "broken",
1048        ));
1049        assert!(err.to_string().contains("broken"));
1050
1051        let err = H2ConnectError::H2("stream reset".into());
1052        assert!(err.to_string().contains("stream reset"));
1053    }
1054
1055    #[test]
1056    fn test_h2_connect_error_from_std_io() {
1057        let io_err = std::io::Error::other("test io");
1058        let err: H2ConnectError = io_err.into();
1059        assert!(matches!(err, H2ConnectError::Io(_)));
1060    }
1061
1062    #[test]
1063    fn test_h2_connect_error_pool_exhausted() {
1064        let err = H2ConnectError::PoolExhausted;
1065        assert!(err.to_string().contains("pool exhausted"));
1066    }
1067
1068    #[test]
1069    fn test_pool_key_equality() {
1070        let k1 = H2PoolKey::new("127.0.0.1", 8080, false, None, None);
1071        let k2 = H2PoolKey::new("127.0.0.1", 8080, false, None, None);
1072        assert_eq!(k1, k2);
1073
1074        let k3 = H2PoolKey::new("127.0.0.1", 8080, true, None, None);
1075        assert_ne!(k1, k3);
1076
1077        let k4 = H2PoolKey::new("127.0.0.1", 8080, false, Some("sni.example.com"), None);
1078        assert_ne!(k1, k4);
1079    }
1080
1081    #[test]
1082    fn test_pool_key_auth_hash() {
1083        let k1 = H2PoolKey::new("h", 1, false, None, Some(("u", "p")));
1084        let k2 = H2PoolKey::new("h", 1, false, None, Some(("u", "p")));
1085        let k3 = H2PoolKey::new("h", 1, false, None, Some(("u", "q")));
1086        assert_eq!(k1, k2);
1087        assert_ne!(k1, k3);
1088    }
1089
1090    #[test]
1091    fn test_pool_stats() {
1092        let pool = H2ConnectionPool::new(4, Duration::from_secs(60), 100);
1093        let stats = pool.stats();
1094        assert_eq!(stats.pool_size, 4);
1095        assert_eq!(stats.active_connections, 0);
1096        assert_eq!(stats.total_streams, 0);
1097    }
1098
1099    #[test]
1100    fn test_pool_registry_get_or_create() {
1101        let registry = H2PoolRegistry::new();
1102        let key = H2PoolKey::new("127.0.0.1", 8080, false, None, None);
1103        let p1 = registry.get_or_create(&key);
1104        let p2 = registry.get_or_create(&key);
1105        assert!(Arc::ptr_eq(&p1, &p2));
1106
1107        let key2 = H2PoolKey::new("127.0.0.1", 9090, false, None, None);
1108        let p3 = registry.get_or_create(&key2);
1109        assert!(!Arc::ptr_eq(&p1, &p3));
1110    }
1111
1112    #[test]
1113    fn test_pool_registry_prunes_idle_and_clears() {
1114        let registry = H2PoolRegistry::new();
1115        let key = H2PoolKey::new("127.0.0.1", 8080, false, None, None);
1116        let pool = registry.get_or_create(&key);
1117        drop(pool);
1118        // Below the prune threshold (64) pruning is a no-op; clearing
1119        // always removes every entry.
1120        registry.prune_idle_pools();
1121        assert!(!registry.pools.read().unwrap().is_empty());
1122        registry.clear();
1123        assert!(registry.pools.read().unwrap().is_empty());
1124
1125        let _pool = registry.get_or_create(&key);
1126        registry.clear();
1127        assert!(registry.pools.read().unwrap().is_empty());
1128    }
1129
1130    #[test]
1131    fn test_pool_key_isolates_different_auth_credentials() {
1132        let k_user_a = H2PoolKey::new(
1133            "proxy.example.com",
1134            443,
1135            true,
1136            None,
1137            Some(("alice", "secret")),
1138        );
1139        let k_user_b = H2PoolKey::new(
1140            "proxy.example.com",
1141            443,
1142            true,
1143            None,
1144            Some(("bob", "secret")),
1145        );
1146        let k_no_auth = H2PoolKey::new("proxy.example.com", 443, true, None, None);
1147
1148        assert_ne!(
1149            k_user_a, k_user_b,
1150            "different users must produce different pool keys"
1151        );
1152        assert_ne!(
1153            k_user_a, k_no_auth,
1154            "auth vs no-auth must produce different pool keys"
1155        );
1156        assert_ne!(k_user_b, k_no_auth);
1157
1158        let registry = H2PoolRegistry::new();
1159        let p1 = registry.get_or_create(&k_user_a);
1160        let p2 = registry.get_or_create(&k_user_b);
1161        let p3 = registry.get_or_create(&k_no_auth);
1162        assert!(!Arc::ptr_eq(&p1, &p2));
1163        assert!(!Arc::ptr_eq(&p1, &p3));
1164        assert!(!Arc::ptr_eq(&p2, &p3));
1165    }
1166
1167    #[test]
1168    fn test_pool_key_isolates_tls_vs_plaintext() {
1169        let k_tls = H2PoolKey::new(
1170            "proxy.example.com",
1171            443,
1172            true,
1173            Some("proxy.example.com"),
1174            None,
1175        );
1176        let k_plain = H2PoolKey::new(
1177            "proxy.example.com",
1178            443,
1179            false,
1180            Some("proxy.example.com"),
1181            None,
1182        );
1183        assert_ne!(
1184            k_tls, k_plain,
1185            "TLS vs plaintext must produce different pool keys"
1186        );
1187    }
1188
1189    #[test]
1190    fn test_pool_key_isolates_server_name() {
1191        let k_sni_a = H2PoolKey::new("1.2.3.4", 443, true, Some("a.example.com"), None);
1192        let k_sni_b = H2PoolKey::new("1.2.3.4", 443, true, Some("b.example.com"), None);
1193        assert_ne!(
1194            k_sni_a, k_sni_b,
1195            "different SNI must produce different pool keys"
1196        );
1197    }
1198
1199    #[tokio::test]
1200    async fn test_handle_h2_connect_accepts() {
1201        let server_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1202        let server_addr = server_listener.local_addr().unwrap();
1203
1204        let server_handle = tokio::spawn(async move {
1205            let (stream, _) = server_listener.accept().await.unwrap();
1206            let conn = h2::server::handshake(stream).await.unwrap();
1207            handle_h2_connect(conn).await.ok();
1208        });
1209
1210        let client_stream = TcpStream::connect(server_addr).await.unwrap();
1211        let (mut send_request, conn) = h2::client::handshake(client_stream).await.unwrap();
1212
1213        let conn_handle = tokio::spawn(async move {
1214            conn.await.ok();
1215        });
1216
1217        let request = http::Request::builder()
1218            .method(http::Method::CONNECT)
1219            .uri("127.0.0.1:9999")
1220            .body(())
1221            .unwrap();
1222
1223        let (response_future, _send_stream) = send_request.send_request(request, true).unwrap();
1224
1225        let response = tokio::time::timeout(std::time::Duration::from_secs(3), response_future)
1226            .await
1227            .unwrap()
1228            .unwrap();
1229        assert_eq!(response.status(), http::StatusCode::OK);
1230
1231        drop(send_request);
1232        drop(_send_stream);
1233        conn_handle.abort();
1234        server_handle.abort();
1235    }
1236
1237    #[tokio::test]
1238    async fn test_h2_connect_relay_rejects_reserved_literal_target() {
1239        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1240        let address = listener.local_addr().unwrap();
1241
1242        let server_handle = tokio::spawn(async move {
1243            let (stream, _) = listener.accept().await.unwrap();
1244            let mut connection = h2::server::handshake(stream).await.unwrap();
1245            let (request, mut response) = connection.accept().await.unwrap().unwrap();
1246            let send_stream = response
1247                .send_response(
1248                    http::Response::builder().status(200).body(()).unwrap(),
1249                    false,
1250                )
1251                .unwrap();
1252            h2_connect_relay(
1253                request.into_body(),
1254                send_stream,
1255                "127.0.0.1:1".parse().unwrap(),
1256            )
1257            .await
1258        });
1259
1260        let client_stream = TcpStream::connect(address).await.unwrap();
1261        let (mut send_request, connection) = h2::client::handshake(client_stream).await.unwrap();
1262        let connection_handle = tokio::spawn(async move { connection.await.ok() });
1263        let request = http::Request::builder()
1264            .method(http::Method::CONNECT)
1265            .uri("127.0.0.1:1")
1266            .body(())
1267            .unwrap();
1268        let (response, _send_stream) = send_request.send_request(request, true).unwrap();
1269        let _ = response.await;
1270
1271        assert!(matches!(
1272            server_handle.await.unwrap(),
1273            Err(H2ConnectError::DnsRebinding(_))
1274        ));
1275        connection_handle.abort();
1276    }
1277
1278    // NOTE: Connection reuse is tested at the integration level in
1279    // upstream_protocols.rs::h2_upstream_connection_reuse which exercises the
1280    // full stack through the ServiceSupervisor.
1281    //
1282    // RST_STREAM and GOAWAY fault injection tests are at the integration level
1283    // in upstream_protocols.rs::h2_upstream_rst_stream_recovery and
1284    // h2_upstream_goaway_recovery.
1285}