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
25pub 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
65pub 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_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
163pub 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 tcp_result?;
243 return Ok(());
244 }
245 };
246
247 h2_result?;
248 tcp_result?;
249 Ok(())
250}
251
252#[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
363pub 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#[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 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 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
493pub struct H2ConnectionEntry {
495 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
549pub struct H2ConnectionPool {
551 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 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 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 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 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 pub fn retire(&self, entry: &Arc<H2ConnectionEntry>) {
708 entry.mark_retired();
709 }
710
711 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#[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
746pub struct H2PoolRegistry {
748 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 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 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 pub fn clear(&self) {
798 self.pools
799 .write()
800 .unwrap_or_else(|e| e.into_inner())
801 .clear();
802 }
803
804 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
825pub static H2_POOL_REGISTRY: LazyLock<H2PoolRegistry> = LazyLock::new(H2PoolRegistry::new);
827
828pub 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 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 pub fn sender(&self) -> &Arc<Mutex<h2::client::SendRequest<Bytes>>> {
857 &self.entry.sender
858 }
859}
860
861pub 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 if let Some(result) = try_pooled_connection(&pool, target, auth).await {
880 return result;
881 }
882
883 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
948async 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 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 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 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 }