Skip to main content

eggress_protocol_http/
h2_connect.rs

1use std::collections::HashMap;
2use std::future::Future;
3use std::hash::{Hash, Hasher};
4use std::pin::Pin;
5use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
6use std::sync::{Arc, Mutex};
7use std::task::{Context, Poll};
8use std::time::{Duration, Instant};
9
10use bytes::Bytes;
11use h2::server::Connection;
12use tokio::io::{AsyncReadExt, AsyncWriteExt};
13use tokio::net::TcpStream;
14use tokio::sync::{Notify, Semaphore};
15
16use crate::error::HttpError;
17use eggress_core::connector::is_dns_rebinding_risk;
18use eggress_core::{TargetAddr, TargetHost};
19
20// ===== H2 Protocol Metrics (atomic counters for bridging into MetricsRegistry) =====
21
22/// Atomic counters for H2 protocol-level metrics. The `MetricsRegistry`
23/// bridges these into Prometheus via `set_h2_metrics()` / `render_prometheus()`.
24pub struct H2ProtocolMetrics {
25    pub connections_opened: AtomicU64,
26    pub connections_closed: AtomicU64,
27    pub streams_opened: AtomicU64,
28    pub streams_closed: AtomicU64,
29    pub goaway_received: AtomicU64,
30    pub handshake_failures: AtomicU64,
31    pub auth_failures: AtomicU64,
32    pub flow_control_stalls: AtomicU64,
33    pub pool_exhausted: AtomicU64,
34    pub bytes_relayed: AtomicU64,
35}
36
37impl H2ProtocolMetrics {
38    pub const fn new() -> Self {
39        Self {
40            connections_opened: AtomicU64::new(0),
41            connections_closed: AtomicU64::new(0),
42            streams_opened: AtomicU64::new(0),
43            streams_closed: AtomicU64::new(0),
44            goaway_received: AtomicU64::new(0),
45            handshake_failures: AtomicU64::new(0),
46            auth_failures: AtomicU64::new(0),
47            flow_control_stalls: AtomicU64::new(0),
48            pool_exhausted: AtomicU64::new(0),
49            bytes_relayed: AtomicU64::new(0),
50        }
51    }
52}
53
54impl Default for H2ProtocolMetrics {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60/// Global H2 protocol metrics instance.
61pub static H2_PROTOCOL_METRICS: once_cell::sync::Lazy<Arc<H2ProtocolMetrics>> =
62    once_cell::sync::Lazy::new(|| Arc::new(H2ProtocolMetrics::new()));
63
64#[derive(Debug, thiserror::Error)]
65pub enum H2ConnectError {
66    #[error("IO error: {0}")]
67    Io(#[from] std::io::Error),
68    #[error("H2 protocol error: {0}")]
69    H2(String),
70    #[error("HTTP error: {0}")]
71    Http(#[from] HttpError),
72    #[error("pool exhausted: no connections available and pool at capacity")]
73    PoolExhausted,
74    #[error("DNS rebinding detected: target resolved to reserved/private address {0}")]
75    DnsRebinding(std::net::IpAddr),
76}
77
78impl From<h2::Error> for H2ConnectError {
79    fn from(e: h2::Error) -> Self {
80        H2ConnectError::H2(e.to_string())
81    }
82}
83
84pub struct H2StreamWrite {
85    send_stream: h2::SendStream<Bytes>,
86    capacity: usize,
87}
88
89impl H2StreamWrite {
90    pub fn new(send_stream: h2::SendStream<Bytes>) -> Self {
91        Self {
92            send_stream,
93            capacity: 0,
94        }
95    }
96}
97
98impl tokio::io::AsyncWrite for H2StreamWrite {
99    fn poll_write(
100        mut self: Pin<&mut Self>,
101        cx: &mut Context<'_>,
102        buf: &[u8],
103    ) -> Poll<Result<usize, std::io::Error>> {
104        if self.capacity == 0 {
105            self.send_stream.reserve_capacity(buf.len());
106            match self.send_stream.poll_capacity(cx) {
107                Poll::Ready(Some(Ok(capacity))) => {
108                    self.capacity = capacity;
109                }
110                Poll::Ready(Some(Err(e))) => {
111                    return Poll::Ready(Err(std::io::Error::other(e)));
112                }
113                Poll::Ready(None) => {
114                    return Poll::Ready(Err(std::io::Error::other("h2 stream closed")));
115                }
116                Poll::Pending => {
117                    H2_PROTOCOL_METRICS
118                        .flow_control_stalls
119                        .fetch_add(1, Ordering::Relaxed);
120                    return Poll::Pending;
121                }
122            }
123        }
124
125        let len = buf.len().min(self.capacity);
126        self.send_stream
127            .send_data(Bytes::copy_from_slice(&buf[..len]), false)
128            .map_err(std::io::Error::other)?;
129        self.capacity -= len;
130        Poll::Ready(Ok(len))
131    }
132
133    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
134        Poll::Ready(Ok(()))
135    }
136
137    fn poll_shutdown(
138        mut self: Pin<&mut Self>,
139        _cx: &mut Context<'_>,
140    ) -> Poll<Result<(), std::io::Error>> {
141        self.send_stream
142            .send_data(Bytes::new(), true)
143            .map_err(std::io::Error::other)?;
144        Poll::Ready(Ok(()))
145    }
146}
147
148pub async fn h2_connect_relay(
149    mut recv_stream: h2::RecvStream,
150    send_stream: h2::SendStream<Bytes>,
151    target: TargetAddr,
152) -> Result<(), H2ConnectError> {
153    let tcp = match &target.host {
154        TargetHost::Ip(_) => TcpStream::connect(target.to_string()).await?,
155        TargetHost::Domain(domain) => {
156            let lookup = format!("{}:{}", domain, target.port);
157            let mut addrs = tokio::net::lookup_host(&lookup)
158                .await
159                .map_err(|e| H2ConnectError::H2(format!("DNS resolution failed: {e}")))?;
160            let resolved = addrs.next().ok_or_else(|| {
161                H2ConnectError::H2("DNS resolution failed: no addresses found".to_string())
162            })?;
163            if is_dns_rebinding_risk(&resolved.ip()) {
164                return Err(H2ConnectError::DnsRebinding(resolved.ip()));
165            }
166            TcpStream::connect(resolved).await?
167        }
168    };
169    let (mut tcp_read, mut tcp_write) = tcp.into_split();
170    let mut h2_write = H2StreamWrite::new(send_stream);
171
172    let h2_to_tcp = async move {
173        loop {
174            match recv_stream.data().await {
175                Some(Ok(data)) => {
176                    let len = data.len();
177                    tcp_write.write_all(&data).await?;
178                    H2_PROTOCOL_METRICS
179                        .bytes_relayed
180                        .fetch_add(len as u64, Ordering::Relaxed);
181                }
182                Some(Err(e)) => {
183                    return Err(std::io::Error::other(e));
184                }
185                None => break,
186            }
187        }
188        Ok::<(), std::io::Error>(())
189    };
190
191    let tcp_to_h2 = async {
192        let mut buf = [0u8; 8192];
193        loop {
194            let n = tcp_read.read(&mut buf).await?;
195            if n == 0 {
196                break;
197            }
198            h2_write.write_all(&buf[..n]).await?;
199            H2_PROTOCOL_METRICS
200                .bytes_relayed
201                .fetch_add(n as u64, Ordering::Relaxed);
202        }
203        Ok::<(), std::io::Error>(())
204    };
205
206    let h2_task = tokio::spawn(h2_to_tcp);
207    let tcp_result = tcp_to_h2.await;
208    let h2_result = h2_task.await.unwrap();
209
210    h2_result?;
211    tcp_result?;
212    Ok(())
213}
214
215pub async fn handle_h2_connect(
216    mut connection: Connection<TcpStream, Bytes>,
217) -> Result<(), H2ConnectError> {
218    loop {
219        match connection.accept().await {
220            Some(Ok((request, mut send_response))) => {
221                if *request.method() == http::Method::CONNECT {
222                    let authority = request
223                        .uri()
224                        .authority()
225                        .ok_or_else(|| H2ConnectError::H2("missing authority".into()))?;
226
227                    let target_str = match authority.port_u16() {
228                        Some(port) => format!("{}:{}", authority.host(), port),
229                        None => format!("{}:443", authority.host()),
230                    };
231
232                    let target: TargetAddr = target_str
233                        .parse()
234                        .map_err(|e: String| H2ConnectError::H2(e))?;
235
236                    let response = http::Response::builder().status(200).body(()).unwrap();
237
238                    let send_stream = send_response.send_response(response, false)?;
239                    let recv_stream = request.into_body();
240
241                    tokio::spawn(async move {
242                        if let Err(e) = h2_connect_relay(recv_stream, send_stream, target).await {
243                            tracing::warn!("h2 connect relay error: {}", e);
244                        }
245                    });
246                } else {
247                    send_response.send_reset(h2::Reason::PROTOCOL_ERROR);
248                }
249            }
250            Some(Err(e)) => {
251                return Err(H2ConnectError::H2(e.to_string()));
252            }
253            None => break,
254        }
255    }
256    Ok(())
257}
258
259pub struct H2StreamRead {
260    recv: h2::RecvStream,
261    buffer: Bytes,
262}
263
264impl H2StreamRead {
265    pub fn new(recv: h2::RecvStream) -> Self {
266        Self {
267            recv,
268            buffer: Bytes::new(),
269        }
270    }
271}
272
273impl tokio::io::AsyncRead for H2StreamRead {
274    fn poll_read(
275        self: std::pin::Pin<&mut Self>,
276        cx: &mut Context<'_>,
277        buf: &mut tokio::io::ReadBuf<'_>,
278    ) -> Poll<std::io::Result<()>> {
279        let this = self.get_mut();
280
281        if !this.buffer.is_empty() {
282            let len = this.buffer.len().min(buf.remaining());
283            buf.put_slice(&this.buffer.split_to(len));
284            this.recv
285                .flow_control()
286                .release_capacity(len)
287                .map_err(std::io::Error::other)?;
288            return Poll::Ready(Ok(()));
289        }
290
291        let poll = {
292            let mut data_fut = Box::pin(this.recv.data());
293            data_fut.as_mut().poll(cx)
294        };
295        match poll {
296            Poll::Ready(Some(Ok(data))) => {
297                let len = data.len().min(buf.remaining());
298                buf.put_slice(&data[..len]);
299                if len < data.len() {
300                    this.buffer = data.slice(len..);
301                }
302                this.recv
303                    .flow_control()
304                    .release_capacity(len)
305                    .map_err(std::io::Error::other)?;
306                Poll::Ready(Ok(()))
307            }
308            Poll::Ready(Some(Err(e))) => Poll::Ready(Err(std::io::Error::other(e))),
309            Poll::Ready(None) => Poll::Ready(Ok(())),
310            Poll::Pending => Poll::Pending,
311        }
312    }
313}
314
315fn h2_base64_encode(input: &[u8]) -> String {
316    const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
317    let mut result = String::with_capacity(input.len().div_ceil(3) * 4);
318    for chunk in input.chunks(3) {
319        let b0 = chunk[0] as u32;
320        let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
321        let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
322        let triple = (b0 << 16) | (b1 << 8) | b2;
323        result.push(TABLE[((triple >> 18) & 0x3F) as usize] as char);
324        result.push(TABLE[((triple >> 12) & 0x3F) as usize] as char);
325        if chunk.len() > 1 {
326            result.push(TABLE[((triple >> 6) & 0x3F) as usize] as char);
327        } else {
328            result.push('=');
329        }
330        if chunk.len() > 2 {
331            result.push(TABLE[(triple & 0x3F) as usize] as char);
332        } else {
333            result.push('=');
334        }
335    }
336    result
337}
338
339/// Perform an H2 CONNECT handshake as a client.
340///
341/// Establishes an HTTP/2 connection over the given stream, sends a CONNECT
342/// request for the specified target authority, and returns the bidirectional
343/// stream pair plus a connection task handle.
344///
345/// The caller must keep the `JoinHandle` alive (or `.abort()` it) for the
346/// duration of the relay — dropping it will close the H2 connection.
347pub async fn h2_connect_client<S>(
348    stream: S,
349    target: &TargetAddr,
350    auth: Option<(&str, &str)>,
351) -> Result<
352    (
353        h2::SendStream<Bytes>,
354        h2::RecvStream,
355        tokio::task::JoinHandle<Result<(), h2::Error>>,
356    ),
357    H2ConnectError,
358>
359where
360    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
361{
362    let (mut send_request, conn) = h2::client::handshake(stream).await?;
363
364    let conn_handle = tokio::spawn(async move {
365        conn.await?;
366        Ok(())
367    });
368
369    let authority = match target.port {
370        443 => target.host.to_string(),
371        port => format!("{}:{}", target.host, port),
372    };
373
374    let mut builder = http::Request::builder()
375        .method(http::Method::CONNECT)
376        .uri(&authority)
377        .header(http::header::HOST, &authority);
378
379    if let Some((user, pass)) = auth {
380        let credentials = format!("{}:{}", user, pass);
381        let encoded = h2_base64_encode(credentials.as_bytes());
382        builder = builder.header(
383            http::header::PROXY_AUTHORIZATION,
384            format!("Basic {}", encoded),
385        );
386    }
387
388    let request = builder
389        .body(())
390        .map_err(|e| H2ConnectError::H2(e.to_string()))?;
391
392    let (response_future, send_stream) = send_request.send_request(request, false)?;
393
394    let response = response_future.await?;
395    if response.status() != http::StatusCode::OK {
396        return Err(H2ConnectError::H2(format!(
397            "CONNECT rejected with status {}",
398            response.status()
399        )));
400    }
401
402    let recv_stream = response.into_body();
403    Ok((send_stream, recv_stream, conn_handle))
404}
405
406// ===== H2 Connection Pool =====
407
408/// Pool key identifying a unique H2 upstream connection group.
409///
410/// Includes `hop_index` to prevent cross-chain pooling: when the same
411/// upstream endpoint appears at different positions in distinct chains,
412/// connections must not be shared because the preceding hops differ.
413#[derive(Debug, Clone, Eq, PartialEq, Hash)]
414pub struct H2PoolKey {
415    pub endpoint_host: String,
416    pub endpoint_port: u16,
417    pub use_tls: bool,
418    pub server_name: Option<String>,
419    pub auth_hash: Option<u64>,
420    pub hop_index: usize,
421}
422
423impl H2PoolKey {
424    pub fn new(
425        host: &str,
426        port: u16,
427        use_tls: bool,
428        server_name: Option<&str>,
429        auth: Option<(&str, &str)>,
430    ) -> Self {
431        Self::with_hop_index(host, port, use_tls, server_name, auth, 0)
432    }
433
434    /// Create a pool key with an explicit hop index for cross-chain isolation.
435    pub fn with_hop_index(
436        host: &str,
437        port: u16,
438        use_tls: bool,
439        server_name: Option<&str>,
440        auth: Option<(&str, &str)>,
441        hop_index: usize,
442    ) -> Self {
443        let auth_hash = auth.map(|(u, p)| {
444            let mut hasher = std::collections::hash_map::DefaultHasher::new();
445            u.hash(&mut hasher);
446            p.hash(&mut hasher);
447            hasher.finish()
448        });
449        Self {
450            endpoint_host: host.to_string(),
451            endpoint_port: port,
452            use_tls,
453            server_name: server_name.map(|s| s.to_string()),
454            auth_hash,
455            hop_index,
456        }
457    }
458}
459
460/// Metadata for a pooled H2 connection.
461pub struct H2ConnectionEntry {
462    sender: Arc<Mutex<h2::client::SendRequest<Bytes>>>,
463    conn_handle: tokio::task::JoinHandle<Result<(), h2::Error>>,
464    #[allow(dead_code)]
465    created_at: Instant,
466    last_used: Arc<Mutex<Instant>>,
467    active_streams: Arc<AtomicU64>,
468    retired: Arc<AtomicBool>,
469    notify: Arc<Notify>,
470}
471
472impl H2ConnectionEntry {
473    fn is_available(&self, max_concurrent_streams: u32) -> bool {
474        !self.retired.load(Ordering::Acquire)
475            && self.active_streams.load(Ordering::Acquire) < max_concurrent_streams as u64
476    }
477
478    fn mark_retired(&self) {
479        self.retired.store(true, Ordering::Release);
480    }
481}
482
483impl Drop for H2ConnectionEntry {
484    fn drop(&mut self) {
485        H2_PROTOCOL_METRICS
486            .connections_closed
487            .fetch_add(1, Ordering::Relaxed);
488        self.conn_handle.abort();
489    }
490}
491
492/// Bounded H2 connection pool with idle timeout and GOAWAY-aware retirement.
493pub struct H2ConnectionPool {
494    entries: Mutex<Vec<Arc<H2ConnectionEntry>>>,
495    semaphore: Semaphore,
496    pool_size: u32,
497    idle_timeout: Duration,
498    max_concurrent_streams: u32,
499    #[allow(dead_code)]
500    created_at: Instant,
501    reaper_running: AtomicBool,
502}
503
504impl H2ConnectionPool {
505    pub fn new(pool_size: u32, idle_timeout: Duration, max_concurrent_streams: u32) -> Arc<Self> {
506        Arc::new(Self {
507            entries: Mutex::new(Vec::new()),
508            semaphore: Semaphore::new(pool_size as usize),
509            pool_size,
510            idle_timeout,
511            max_concurrent_streams,
512            created_at: Instant::now(),
513            reaper_running: AtomicBool::new(false),
514        })
515    }
516
517    /// Try to acquire an existing idle connection from the pool.
518    fn try_acquire_entry(&self) -> Option<Arc<H2ConnectionEntry>> {
519        let entries = self.entries.lock().unwrap().clone();
520        let now = Instant::now();
521        for entry in entries.iter() {
522            if entry.is_available(self.max_concurrent_streams)
523                && now.duration_since(*entry.last_used.lock().unwrap()) < self.idle_timeout
524            {
525                entry.active_streams.fetch_add(1, Ordering::AcqRel);
526                *entry.last_used.lock().unwrap() = now;
527                return Some(Arc::clone(entry));
528            }
529        }
530        None
531    }
532
533    /// Create a new H2 connection and add it to the pool.
534    async fn create_entry<S>(
535        self: &Arc<Self>,
536        stream: S,
537    ) -> Result<Arc<H2ConnectionEntry>, H2ConnectError>
538    where
539        S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
540    {
541        let (send_request, conn) = h2::client::handshake(stream).await?;
542
543        let conn_handle = tokio::spawn(async move {
544            conn.await?;
545            Ok(())
546        });
547
548        let sender = Arc::new(Mutex::new(send_request));
549        let entry = Arc::new(H2ConnectionEntry {
550            sender: Arc::clone(&sender),
551            conn_handle,
552            created_at: Instant::now(),
553            last_used: Arc::new(Mutex::new(Instant::now())),
554            active_streams: Arc::new(AtomicU64::new(1)),
555            retired: Arc::new(AtomicBool::new(false)),
556            notify: Arc::new(Notify::new()),
557        });
558
559        self.entries.lock().unwrap().push(Arc::clone(&entry));
560        H2_PROTOCOL_METRICS
561            .connections_opened
562            .fetch_add(1, Ordering::Relaxed);
563        self.maybe_start_reaper();
564        Ok(entry)
565    }
566
567    fn maybe_start_reaper(self: &Arc<Self>) {
568        if self
569            .reaper_running
570            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
571            .is_err()
572        {
573            return;
574        }
575        let pool = Arc::clone(self);
576        tokio::spawn(async move {
577            loop {
578                tokio::time::sleep(pool.idle_timeout / 2).await;
579                pool.reap_idle_entries();
580            }
581        });
582    }
583
584    fn reap_idle_entries(&self) {
585        let now = Instant::now();
586        let entries = self.entries.lock().unwrap().clone();
587        for entry in &entries {
588            if entry.retired.load(Ordering::Acquire) {
589                continue;
590            }
591            let last_used = *entry.last_used.lock().unwrap();
592            if now.duration_since(last_used) >= self.idle_timeout
593                && entry.active_streams.load(Ordering::Acquire) == 0
594            {
595                entry.mark_retired();
596            }
597        }
598        self.entries
599            .lock()
600            .unwrap()
601            .retain(|entry| !entry.retired.load(Ordering::Acquire));
602    }
603
604    /// Release a connection back to the pool after a stream completes.
605    pub fn release(&self, entry: &Arc<H2ConnectionEntry>) {
606        entry.active_streams.fetch_sub(1, Ordering::AcqRel);
607        *entry.last_used.lock().unwrap() = Instant::now();
608        entry.notify.notify_waiters();
609    }
610
611    /// Mark a connection as retired (e.g., on GOAWAY).
612    pub fn retire(&self, entry: &Arc<H2ConnectionEntry>) {
613        entry.mark_retired();
614    }
615
616    /// Get pool statistics.
617    pub fn stats(&self) -> H2PoolStats {
618        let entries = self.entries.lock().unwrap();
619        let active = entries
620            .iter()
621            .filter(|e| !e.retired.load(Ordering::Acquire))
622            .count();
623        let total_streams: u64 = entries
624            .iter()
625            .map(|e| e.active_streams.load(Ordering::Acquire))
626            .sum();
627        H2PoolStats {
628            pool_size: self.pool_size,
629            active_connections: active as u32,
630            total_streams,
631            idle_timeout_secs: self.idle_timeout.as_secs(),
632        }
633    }
634}
635
636/// Pool statistics snapshot.
637#[derive(Debug, Clone)]
638pub struct H2PoolStats {
639    pub pool_size: u32,
640    pub active_connections: u32,
641    pub total_streams: u64,
642    pub idle_timeout_secs: u64,
643}
644
645/// Global H2 connection pool registry, keyed by (endpoint_host, endpoint_port, use_tls, server_name, auth_hash).
646pub struct H2PoolRegistry {
647    pools: std::sync::RwLock<HashMap<H2PoolKey, Arc<H2ConnectionPool>>>,
648    default_pool_size: u32,
649    default_idle_timeout: Duration,
650    default_max_concurrent_streams: u32,
651}
652
653impl H2PoolRegistry {
654    pub fn new() -> Self {
655        Self {
656            pools: std::sync::RwLock::new(HashMap::new()),
657            default_pool_size: 4,
658            default_idle_timeout: Duration::from_secs(60),
659            default_max_concurrent_streams: 100,
660        }
661    }
662
663    /// Get or create a pool for the given key.
664    pub fn get_or_create(&self, key: &H2PoolKey) -> Arc<H2ConnectionPool> {
665        {
666            let pools = self.pools.read().unwrap();
667            if let Some(pool) = pools.get(key) {
668                return Arc::clone(pool);
669            }
670        }
671        let mut pools = self.pools.write().unwrap();
672        pools
673            .entry(key.clone())
674            .or_insert_with(|| {
675                H2ConnectionPool::new(
676                    self.default_pool_size,
677                    self.default_idle_timeout,
678                    self.default_max_concurrent_streams,
679                )
680            })
681            .clone()
682    }
683
684    /// Configure default pool settings.
685    pub fn with_defaults(
686        pool_size: u32,
687        idle_timeout: Duration,
688        max_concurrent_streams: u32,
689    ) -> Self {
690        Self {
691            pools: std::sync::RwLock::new(HashMap::new()),
692            default_pool_size: pool_size,
693            default_idle_timeout: idle_timeout,
694            default_max_concurrent_streams: max_concurrent_streams,
695        }
696    }
697}
698
699impl Default for H2PoolRegistry {
700    fn default() -> Self {
701        Self::new()
702    }
703}
704
705/// Global pool registry instance.
706pub static H2_POOL_REGISTRY: once_cell::sync::Lazy<H2PoolRegistry> =
707    once_cell::sync::Lazy::new(H2PoolRegistry::new);
708
709/// A guard that releases an H2 connection back to the pool when dropped.
710pub struct H2PoolGuard {
711    entry: Arc<H2ConnectionEntry>,
712    pool: Arc<H2ConnectionPool>,
713}
714
715impl Drop for H2PoolGuard {
716    fn drop(&mut self) {
717        H2_PROTOCOL_METRICS
718            .streams_closed
719            .fetch_add(1, Ordering::Relaxed);
720        self.pool.release(&self.entry);
721    }
722}
723
724impl H2PoolGuard {
725    /// Mark this connection as retired (e.g., on GOAWAY).
726    pub fn retire(&self) {
727        H2_PROTOCOL_METRICS
728            .goaway_received
729            .fetch_add(1, Ordering::Relaxed);
730        H2_PROTOCOL_METRICS
731            .connections_closed
732            .fetch_add(1, Ordering::Relaxed);
733        self.pool.retire(&self.entry);
734    }
735
736    /// Get the sender for creating new streams on this connection.
737    pub fn sender(&self) -> &Arc<Mutex<h2::client::SendRequest<Bytes>>> {
738        &self.entry.sender
739    }
740}
741
742/// Perform an H2 CONNECT handshake using a pooled connection.
743///
744/// Acquires a connection from the pool (or creates a new one), sends a CONNECT
745/// request, and returns the bidirectional streams with a pool guard. When the
746/// guard is dropped, the connection is released back to the pool.
747pub async fn h2_connect_client_pooled<S>(
748    stream: S,
749    target: &TargetAddr,
750    auth: Option<(&str, &str)>,
751    pool_key: &H2PoolKey,
752) -> Result<(h2::SendStream<Bytes>, h2::RecvStream, H2PoolGuard), H2ConnectError>
753where
754    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
755{
756    let pool = H2_POOL_REGISTRY.get_or_create(pool_key);
757
758    // Try to acquire an existing connection from the pool
759    if let Some(result) = try_pooled_connection(&pool, target, auth).await {
760        return result;
761    }
762
763    // No available connection — create a new one (semaphore limits total)
764    let _permit = pool.semaphore.acquire().await.map_err(|_| {
765        H2_PROTOCOL_METRICS
766            .pool_exhausted
767            .fetch_add(1, Ordering::Relaxed);
768        H2ConnectError::PoolExhausted
769    })?;
770
771    let entry = pool.create_entry(stream).await?;
772
773    let authority = match target.port {
774        443 => target.host.to_string(),
775        port => format!("{}:{}", target.host, port),
776    };
777
778    let mut builder = http::Request::builder()
779        .method(http::Method::CONNECT)
780        .uri(&authority)
781        .header(http::header::HOST, &authority);
782
783    if let Some((user, pass)) = auth {
784        let credentials = format!("{}:{}", user, pass);
785        let encoded = h2_base64_encode(credentials.as_bytes());
786        builder = builder.header(
787            http::header::PROXY_AUTHORIZATION,
788            format!("Basic {}", encoded),
789        );
790    }
791
792    let request = builder
793        .body(())
794        .map_err(|e| H2ConnectError::H2(e.to_string()))?;
795
796    let (response_future, send_stream) = {
797        let mut sender = entry.sender.lock().unwrap();
798        sender.send_request(request, false)?
799    };
800
801    let response = response_future.await?;
802    if response.status() != http::StatusCode::OK {
803        pool.retire(&entry);
804        if response.status() == http::StatusCode::PROXY_AUTHENTICATION_REQUIRED {
805            H2_PROTOCOL_METRICS
806                .auth_failures
807                .fetch_add(1, Ordering::Relaxed);
808        }
809        return Err(H2ConnectError::H2(format!(
810            "CONNECT rejected with status {}",
811            response.status()
812        )));
813    }
814
815    let recv_stream = response.into_body();
816    H2_PROTOCOL_METRICS
817        .streams_opened
818        .fetch_add(1, Ordering::Relaxed);
819    let guard = H2PoolGuard {
820        entry: Arc::clone(&entry),
821        pool: Arc::clone(&pool),
822    };
823    Ok((send_stream, recv_stream, guard))
824}
825
826/// Try to send a CONNECT request on an existing pooled connection.
827/// Returns `Some(result)` if a connection was found, `None` if no connection available.
828async fn try_pooled_connection(
829    pool: &Arc<H2ConnectionPool>,
830    target: &TargetAddr,
831    auth: Option<(&str, &str)>,
832) -> Option<Result<(h2::SendStream<Bytes>, h2::RecvStream, H2PoolGuard), H2ConnectError>> {
833    let entry = pool.try_acquire_entry()?;
834
835    let authority = match target.port {
836        443 => target.host.to_string(),
837        port => format!("{}:{}", target.host, port),
838    };
839
840    let mut builder = http::Request::builder()
841        .method(http::Method::CONNECT)
842        .uri(&authority)
843        .header(http::header::HOST, &authority);
844
845    if let Some((user, pass)) = auth {
846        let credentials = format!("{}:{}", user, pass);
847        let encoded = h2_base64_encode(credentials.as_bytes());
848        builder = builder.header(
849            http::header::PROXY_AUTHORIZATION,
850            format!("Basic {}", encoded),
851        );
852    }
853
854    let request = match builder.body(()) {
855        Ok(r) => r,
856        Err(e) => return Some(Err(H2ConnectError::H2(e.to_string()))),
857    };
858
859    let result = {
860        let mut sender = entry.sender.lock().unwrap();
861        sender.send_request(request, false)
862    };
863
864    match result {
865        Ok((response_future, send_stream)) => {
866            let response = match response_future.await {
867                Ok(r) => r,
868                Err(e) => {
869                    pool.retire(&entry);
870                    return Some(Err(e.into()));
871                }
872            };
873            if response.status() != http::StatusCode::OK {
874                pool.retire(&entry);
875                if response.status() == http::StatusCode::PROXY_AUTHENTICATION_REQUIRED {
876                    H2_PROTOCOL_METRICS
877                        .auth_failures
878                        .fetch_add(1, Ordering::Relaxed);
879                }
880                return Some(Err(H2ConnectError::H2(format!(
881                    "CONNECT rejected with status {}",
882                    response.status()
883                ))));
884            }
885            H2_PROTOCOL_METRICS
886                .streams_opened
887                .fetch_add(1, Ordering::Relaxed);
888            let recv_stream = response.into_body();
889            let guard = H2PoolGuard {
890                entry: Arc::clone(&entry),
891                pool: Arc::clone(pool),
892            };
893            Some(Ok((send_stream, recv_stream, guard)))
894        }
895        Err(_) => {
896            // GOAWAY or connection error — retire this entry, fall through to new connection
897            pool.retire(&entry);
898            None
899        }
900    }
901}
902
903#[cfg(test)]
904mod tests {
905    use super::*;
906
907    #[test]
908    fn test_h2_connect_error_display() {
909        let err = H2ConnectError::Io(std::io::Error::new(
910            std::io::ErrorKind::ConnectionRefused,
911            "test",
912        ));
913        assert!(err.to_string().contains("IO error"));
914    }
915
916    #[test]
917    fn test_h2_connect_error_from_h2() {
918        let err = H2ConnectError::H2("test error".into());
919        assert_eq!(err.to_string(), "H2 protocol error: test error");
920    }
921
922    #[test]
923    fn test_h2_connect_error_display_variants() {
924        let err = H2ConnectError::Io(std::io::Error::new(
925            std::io::ErrorKind::BrokenPipe,
926            "broken",
927        ));
928        assert!(err.to_string().contains("broken"));
929
930        let err = H2ConnectError::H2("stream reset".into());
931        assert!(err.to_string().contains("stream reset"));
932    }
933
934    #[test]
935    fn test_h2_connect_error_from_std_io() {
936        let io_err = std::io::Error::other("test io");
937        let err: H2ConnectError = io_err.into();
938        assert!(matches!(err, H2ConnectError::Io(_)));
939    }
940
941    #[test]
942    fn test_h2_connect_error_pool_exhausted() {
943        let err = H2ConnectError::PoolExhausted;
944        assert!(err.to_string().contains("pool exhausted"));
945    }
946
947    #[test]
948    fn test_pool_key_equality() {
949        let k1 = H2PoolKey::new("127.0.0.1", 8080, false, None, None);
950        let k2 = H2PoolKey::new("127.0.0.1", 8080, false, None, None);
951        assert_eq!(k1, k2);
952
953        let k3 = H2PoolKey::new("127.0.0.1", 8080, true, None, None);
954        assert_ne!(k1, k3);
955
956        let k4 = H2PoolKey::new("127.0.0.1", 8080, false, Some("sni.example.com"), None);
957        assert_ne!(k1, k4);
958    }
959
960    #[test]
961    fn test_pool_key_auth_hash() {
962        let k1 = H2PoolKey::new("h", 1, false, None, Some(("u", "p")));
963        let k2 = H2PoolKey::new("h", 1, false, None, Some(("u", "p")));
964        let k3 = H2PoolKey::new("h", 1, false, None, Some(("u", "q")));
965        assert_eq!(k1, k2);
966        assert_ne!(k1, k3);
967    }
968
969    #[test]
970    fn test_pool_stats() {
971        let pool = H2ConnectionPool::new(4, Duration::from_secs(60), 100);
972        let stats = pool.stats();
973        assert_eq!(stats.pool_size, 4);
974        assert_eq!(stats.active_connections, 0);
975        assert_eq!(stats.total_streams, 0);
976    }
977
978    #[test]
979    fn test_pool_registry_get_or_create() {
980        let registry = H2PoolRegistry::new();
981        let key = H2PoolKey::new("127.0.0.1", 8080, false, None, None);
982        let p1 = registry.get_or_create(&key);
983        let p2 = registry.get_or_create(&key);
984        assert!(Arc::ptr_eq(&p1, &p2));
985
986        let key2 = H2PoolKey::new("127.0.0.1", 9090, false, None, None);
987        let p3 = registry.get_or_create(&key2);
988        assert!(!Arc::ptr_eq(&p1, &p3));
989    }
990
991    #[test]
992    fn test_pool_key_isolates_different_auth_credentials() {
993        let k_user_a = H2PoolKey::new(
994            "proxy.example.com",
995            443,
996            true,
997            None,
998            Some(("alice", "secret")),
999        );
1000        let k_user_b = H2PoolKey::new(
1001            "proxy.example.com",
1002            443,
1003            true,
1004            None,
1005            Some(("bob", "secret")),
1006        );
1007        let k_no_auth = H2PoolKey::new("proxy.example.com", 443, true, None, None);
1008
1009        assert_ne!(
1010            k_user_a, k_user_b,
1011            "different users must produce different pool keys"
1012        );
1013        assert_ne!(
1014            k_user_a, k_no_auth,
1015            "auth vs no-auth must produce different pool keys"
1016        );
1017        assert_ne!(k_user_b, k_no_auth);
1018
1019        let registry = H2PoolRegistry::new();
1020        let p1 = registry.get_or_create(&k_user_a);
1021        let p2 = registry.get_or_create(&k_user_b);
1022        let p3 = registry.get_or_create(&k_no_auth);
1023        assert!(!Arc::ptr_eq(&p1, &p2));
1024        assert!(!Arc::ptr_eq(&p1, &p3));
1025        assert!(!Arc::ptr_eq(&p2, &p3));
1026    }
1027
1028    #[test]
1029    fn test_pool_key_isolates_tls_vs_plaintext() {
1030        let k_tls = H2PoolKey::new(
1031            "proxy.example.com",
1032            443,
1033            true,
1034            Some("proxy.example.com"),
1035            None,
1036        );
1037        let k_plain = H2PoolKey::new(
1038            "proxy.example.com",
1039            443,
1040            false,
1041            Some("proxy.example.com"),
1042            None,
1043        );
1044        assert_ne!(
1045            k_tls, k_plain,
1046            "TLS vs plaintext must produce different pool keys"
1047        );
1048    }
1049
1050    #[test]
1051    fn test_pool_key_isolates_server_name() {
1052        let k_sni_a = H2PoolKey::new("1.2.3.4", 443, true, Some("a.example.com"), None);
1053        let k_sni_b = H2PoolKey::new("1.2.3.4", 443, true, Some("b.example.com"), None);
1054        assert_ne!(
1055            k_sni_a, k_sni_b,
1056            "different SNI must produce different pool keys"
1057        );
1058    }
1059
1060    #[tokio::test]
1061    async fn test_handle_h2_connect_accepts() {
1062        let server_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1063        let server_addr = server_listener.local_addr().unwrap();
1064
1065        let server_handle = tokio::spawn(async move {
1066            let (stream, _) = server_listener.accept().await.unwrap();
1067            let conn = h2::server::handshake(stream).await.unwrap();
1068            handle_h2_connect(conn).await.ok();
1069        });
1070
1071        let client_stream = TcpStream::connect(server_addr).await.unwrap();
1072        let (mut send_request, conn) = h2::client::handshake(client_stream).await.unwrap();
1073
1074        let conn_handle = tokio::spawn(async move {
1075            conn.await.ok();
1076        });
1077
1078        let request = http::Request::builder()
1079            .method(http::Method::CONNECT)
1080            .uri("127.0.0.1:9999")
1081            .body(())
1082            .unwrap();
1083
1084        let (response_future, _send_stream) = send_request.send_request(request, true).unwrap();
1085
1086        let response = tokio::time::timeout(std::time::Duration::from_secs(3), response_future)
1087            .await
1088            .unwrap()
1089            .unwrap();
1090        assert_eq!(response.status(), http::StatusCode::OK);
1091
1092        drop(send_request);
1093        drop(_send_stream);
1094        conn_handle.abort();
1095        server_handle.abort();
1096    }
1097
1098    // NOTE: Connection reuse is tested at the integration level in
1099    // upstream_protocols.rs::h2_upstream_connection_reuse which exercises the
1100    // full stack through the ServiceSupervisor.
1101    //
1102    // RST_STREAM and GOAWAY fault injection tests are at the integration level
1103    // in upstream_protocols.rs::h2_upstream_rst_stream_recovery and
1104    // h2_upstream_goaway_recovery.
1105}