feat(smart-proxy): add UDP transport support with QUIC/HTTP3 routing and datagram handler relay

This commit is contained in:
2026-03-19 15:06:27 +00:00
parent cfa958cf3d
commit 4fb91cd868
34 changed files with 2978 additions and 55 deletions

View File

@@ -1,7 +1,7 @@
//! Backend connection pool for HTTP/1.1 and HTTP/2.
//! Backend connection pool for HTTP/1.1, HTTP/2, and HTTP/3 (QUIC).
//!
//! Reuses idle keep-alive connections to avoid per-request TCP+TLS handshakes.
//! HTTP/2 connections are multiplexed (clone the sender for each request).
//! HTTP/2 and HTTP/3 connections are multiplexed (clone the sender / share the connection).
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
@@ -19,9 +19,17 @@ const IDLE_TIMEOUT: Duration = Duration::from_secs(90);
/// Background eviction interval.
const EVICTION_INTERVAL: Duration = Duration::from_secs(30);
/// Maximum age for pooled HTTP/2 connections before proactive eviction.
/// Prevents staleness from backends that close idle connections (e.g. nginx GOAWAY).
/// 120s is well within typical server GOAWAY windows (nginx: ~60s idle, envoy: ~60s).
const MAX_H2_AGE: Duration = Duration::from_secs(120);
/// Maximum age for pooled QUIC/HTTP/3 connections.
const MAX_H3_AGE: Duration = Duration::from_secs(120);
/// Protocol for pool key discrimination.
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum PoolProtocol {
H1,
H2,
H3,
}
/// Identifies a unique backend endpoint.
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
@@ -29,7 +37,7 @@ pub struct PoolKey {
pub host: String,
pub port: u16,
pub use_tls: bool,
pub h2: bool,
pub protocol: PoolProtocol,
}
/// An idle HTTP/1.1 sender with a timestamp for eviction.
@@ -47,13 +55,22 @@ struct PooledH2 {
generation: u64,
}
/// A pooled QUIC/HTTP/3 connection (multiplexed like H2).
pub struct PooledH3 {
pub connection: quinn::Connection,
pub created_at: Instant,
pub generation: u64,
}
/// Backend connection pool.
pub struct ConnectionPool {
/// HTTP/1.1 idle connections indexed by backend key.
h1_pool: Arc<DashMap<PoolKey, Vec<IdleH1>>>,
/// HTTP/2 multiplexed connections indexed by backend key.
h2_pool: Arc<DashMap<PoolKey, PooledH2>>,
/// Monotonic generation counter for H2 pool entries.
/// HTTP/3 (QUIC) connections indexed by backend key.
h3_pool: Arc<DashMap<PoolKey, PooledH3>>,
/// Monotonic generation counter for H2/H3 pool entries.
h2_generation: AtomicU64,
/// Handle for the background eviction task.
eviction_handle: Option<tokio::task::JoinHandle<()>>,
@@ -64,16 +81,19 @@ impl ConnectionPool {
pub fn new() -> Self {
let h1_pool: Arc<DashMap<PoolKey, Vec<IdleH1>>> = Arc::new(DashMap::new());
let h2_pool: Arc<DashMap<PoolKey, PooledH2>> = Arc::new(DashMap::new());
let h3_pool: Arc<DashMap<PoolKey, PooledH3>> = Arc::new(DashMap::new());
let h1_clone = Arc::clone(&h1_pool);
let h2_clone = Arc::clone(&h2_pool);
let h3_clone = Arc::clone(&h3_pool);
let eviction_handle = tokio::spawn(async move {
Self::eviction_loop(h1_clone, h2_clone).await;
Self::eviction_loop(h1_clone, h2_clone, h3_clone).await;
});
Self {
h1_pool,
h2_pool,
h3_pool,
h2_generation: AtomicU64::new(0),
eviction_handle: Some(eviction_handle),
}
@@ -173,10 +193,57 @@ impl ConnectionPool {
gen
}
// ── HTTP/3 (QUIC) pool methods ──
/// Try to get a pooled QUIC connection for the given key.
/// QUIC connections are multiplexed — the connection is shared, not removed.
pub fn checkout_h3(&self, key: &PoolKey) -> Option<(quinn::Connection, Duration)> {
let entry = self.h3_pool.get(key)?;
let pooled = entry.value();
let age = pooled.created_at.elapsed();
if age >= MAX_H3_AGE {
drop(entry);
self.h3_pool.remove(key);
return None;
}
// Check if QUIC connection is still alive
if pooled.connection.close_reason().is_some() {
drop(entry);
self.h3_pool.remove(key);
return None;
}
Some((pooled.connection.clone(), age))
}
/// Register a QUIC connection in the pool. Returns the generation ID.
pub fn register_h3(&self, key: PoolKey, connection: quinn::Connection) -> u64 {
let gen = self.h2_generation.fetch_add(1, Ordering::Relaxed);
self.h3_pool.insert(key, PooledH3 {
connection,
created_at: Instant::now(),
generation: gen,
});
gen
}
/// Remove a QUIC connection only if generation matches.
pub fn remove_h3_if_generation(&self, key: &PoolKey, expected_gen: u64) {
if let Some(entry) = self.h3_pool.get(key) {
if entry.value().generation == expected_gen {
drop(entry);
self.h3_pool.remove(key);
}
}
}
/// Background eviction loop — runs every EVICTION_INTERVAL to remove stale connections.
async fn eviction_loop(
h1_pool: Arc<DashMap<PoolKey, Vec<IdleH1>>>,
h2_pool: Arc<DashMap<PoolKey, PooledH2>>,
h3_pool: Arc<DashMap<PoolKey, PooledH3>>,
) {
let mut interval = tokio::time::interval(EVICTION_INTERVAL);
loop {
@@ -206,6 +273,19 @@ impl ConnectionPool {
for key in dead_h2 {
h2_pool.remove(&key);
}
// Evict dead or aged-out H3 (QUIC) connections
let mut dead_h3 = Vec::new();
for entry in h3_pool.iter() {
if entry.value().connection.close_reason().is_some()
|| entry.value().created_at.elapsed() >= MAX_H3_AGE
{
dead_h3.push(entry.key().clone());
}
}
for key in dead_h3 {
h3_pool.remove(&key);
}
}
}
}