feat(quic,http3): add HTTP/3 proxy handling and hot-reload QUIC TLS configuration

This commit is contained in:
2026-03-19 20:27:57 +00:00
parent 9e1103e7a7
commit af970c447e
10 changed files with 1911 additions and 1780 deletions

View File

@@ -19,6 +19,8 @@ use rustproxy_config::{RouteConfig, TransportProtocol};
use rustproxy_metrics::MetricsCollector;
use rustproxy_routing::{MatchContext, RouteManager};
use rustproxy_http::h3_service::H3ProxyService;
use crate::connection_tracker::ConnectionTracker;
/// Create a QUIC server endpoint on the given port with the provided TLS config.
@@ -55,6 +57,7 @@ pub async fn quic_accept_loop(
metrics: Arc<MetricsCollector>,
conn_tracker: Arc<ConnectionTracker>,
cancel: CancellationToken,
h3_service: Option<Arc<H3ProxyService>>,
) {
loop {
let incoming = tokio::select! {
@@ -113,9 +116,10 @@ pub async fn quic_accept_loop(
let metrics = Arc::clone(&metrics);
let conn_tracker = Arc::clone(&conn_tracker);
let cancel = cancel.child_token();
let h3_svc = h3_service.clone();
tokio::spawn(async move {
match handle_quic_connection(incoming, route, port, Arc::clone(&metrics), &cancel).await {
match handle_quic_connection(incoming, route, port, Arc::clone(&metrics), &cancel, h3_svc).await {
Ok(()) => debug!("QUIC connection from {} completed", remote_addr),
Err(e) => debug!("QUIC connection from {} error: {}", remote_addr, e),
}
@@ -139,6 +143,7 @@ async fn handle_quic_connection(
port: u16,
metrics: Arc<MetricsCollector>,
cancel: &CancellationToken,
h3_service: Option<Arc<H3ProxyService>>,
) -> anyhow::Result<()> {
let connection = incoming.await?;
let remote_addr = connection.remote_address();
@@ -151,10 +156,20 @@ async fn handle_quic_connection(
.unwrap_or(false);
if enable_http3 {
// Phase 5: dispatch to H3ProxyService
// For now, log and accept streams for basic handling
debug!("HTTP/3 enabled for route {:?}, dispatching to H3 handler", route.name);
handle_h3_connection(connection, route, port, &metrics, cancel).await
if let Some(ref h3_svc) = h3_service {
debug!("HTTP/3 enabled for route {:?}, dispatching to H3ProxyService", route.name);
h3_svc.handle_connection(connection, &route, port).await
} else {
warn!("HTTP/3 enabled for route {:?} but H3ProxyService not initialized", route.name);
// Keep connection alive until cancelled
tokio::select! {
_ = cancel.cancelled() => {}
reason = connection.closed() => {
debug!("HTTP/3 connection closed (no service): {}", reason);
}
}
Ok(())
}
} else {
// Non-HTTP3 QUIC: bidirectional stream forwarding to TCP backend
handle_quic_stream_forwarding(connection, route, port, metrics, cancel).await
@@ -257,29 +272,6 @@ async fn forward_quic_stream_to_tcp(
Ok((bytes_in, bytes_out))
}
/// Placeholder for HTTP/3 connection handling (Phase 5).
///
/// Once h3_service is implemented, this will delegate to it.
async fn handle_h3_connection(
connection: quinn::Connection,
_route: RouteConfig,
_port: u16,
_metrics: &MetricsCollector,
cancel: &CancellationToken,
) -> anyhow::Result<()> {
warn!("HTTP/3 handling not yet fully implemented — accepting connection but no request processing");
// Keep the connection alive until cancelled or closed
tokio::select! {
_ = cancel.cancelled() => {}
reason = connection.closed() => {
debug!("HTTP/3 connection closed: {}", reason);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -21,13 +21,15 @@ use rustproxy_config::{RouteActionType, TransportProtocol};
use rustproxy_metrics::MetricsCollector;
use rustproxy_routing::{MatchContext, RouteManager};
use rustproxy_http::h3_service::H3ProxyService;
use crate::connection_tracker::ConnectionTracker;
use crate::udp_session::{SessionKey, UdpSession, UdpSessionConfig, UdpSessionTable};
/// Manages UDP listeners across all configured ports.
pub struct UdpListenerManager {
/// Port → recv loop task handle
listeners: HashMap<u16, JoinHandle<()>>,
/// Port → (recv loop task handle, optional QUIC endpoint for TLS updates)
listeners: HashMap<u16, (JoinHandle<()>, Option<quinn::Endpoint>)>,
/// Hot-reloadable route table
route_manager: Arc<ArcSwap<RouteManager>>,
/// Shared metrics collector
@@ -44,13 +46,18 @@ pub struct UdpListenerManager {
relay_writer: Arc<Mutex<Option<tokio::net::unix::OwnedWriteHalf>>>,
/// Cancel token for the current relay reply reader task
relay_reader_cancel: Option<CancellationToken>,
/// H3 proxy service for HTTP/3 request handling
h3_service: Option<Arc<H3ProxyService>>,
}
impl Drop for UdpListenerManager {
fn drop(&mut self) {
self.cancel_token.cancel();
for (_, handle) in self.listeners.drain() {
for (_, (handle, endpoint)) in self.listeners.drain() {
handle.abort();
if let Some(ep) = endpoint {
ep.close(quinn::VarInt::from_u32(0), b"shutdown");
}
}
}
}
@@ -72,9 +79,15 @@ impl UdpListenerManager {
datagram_handler_relay: Arc::new(RwLock::new(None)),
relay_writer: Arc::new(Mutex::new(None)),
relay_reader_cancel: None,
h3_service: None,
}
}
/// Set the H3 proxy service for HTTP/3 request handling.
pub fn set_h3_service(&mut self, svc: Arc<H3ProxyService>) {
self.h3_service = Some(svc);
}
/// Update the route manager (for hot-reload).
pub fn update_routes(&self, route_manager: Arc<RouteManager>) {
self.route_manager.store(route_manager);
@@ -109,8 +122,9 @@ impl UdpListenerManager {
if has_quic {
if let Some(tls) = tls_config {
// Create QUIC endpoint
// Create QUIC endpoint; clone it so we can hot-swap TLS later
let endpoint = crate::quic_handler::create_quic_endpoint(port, tls)?;
let endpoint_for_updates = endpoint.clone(); // quinn::Endpoint is Arc-based
let handle = tokio::spawn(crate::quic_handler::quic_accept_loop(
endpoint,
port,
@@ -118,8 +132,9 @@ impl UdpListenerManager {
Arc::clone(&self.metrics),
Arc::clone(&self.conn_tracker),
self.cancel_token.child_token(),
self.h3_service.clone(),
));
self.listeners.insert(port, handle);
self.listeners.insert(port, (handle, Some(endpoint_for_updates)));
info!("QUIC endpoint started on port {}", port);
return Ok(());
} else {
@@ -145,7 +160,7 @@ impl UdpListenerManager {
self.cancel_token.child_token(),
));
self.listeners.insert(port, handle);
self.listeners.insert(port, (handle, None));
// Start the session cleanup task if this is the first port
if self.listeners.len() == 1 {
@@ -157,8 +172,11 @@ impl UdpListenerManager {
/// Stop listening on a UDP port.
pub fn remove_port(&mut self, port: u16) {
if let Some(handle) = self.listeners.remove(&port) {
if let Some((handle, endpoint)) = self.listeners.remove(&port) {
handle.abort();
if let Some(ep) = endpoint {
ep.close(quinn::VarInt::from_u32(0), b"port removed");
}
info!("UDP listener removed from port {}", port);
}
}
@@ -173,14 +191,37 @@ impl UdpListenerManager {
/// Stop all listeners and clean up.
pub async fn stop(&mut self) {
self.cancel_token.cancel();
for (port, handle) in self.listeners.drain() {
for (port, (handle, endpoint)) in self.listeners.drain() {
handle.abort();
if let Some(ep) = endpoint {
ep.close(quinn::VarInt::from_u32(0), b"shutdown");
}
debug!("UDP listener stopped on port {}", port);
}
info!("All UDP listeners stopped, {} sessions remaining",
self.session_table.session_count());
}
/// Update TLS config on all active QUIC endpoints (cert refresh).
/// Only affects new incoming connections — existing connections are undisturbed.
/// Uses quinn's Endpoint::set_server_config() for zero-downtime hot-swap.
pub fn update_quic_tls(&self, tls_config: Arc<rustls::ServerConfig>) {
for (port, (_handle, endpoint)) in &self.listeners {
if let Some(ep) = endpoint {
match quinn::crypto::rustls::QuicServerConfig::try_from(Arc::clone(&tls_config)) {
Ok(quic_crypto) => {
let server_config = quinn::ServerConfig::with_crypto(Arc::new(quic_crypto));
ep.set_server_config(Some(server_config));
info!("Updated QUIC TLS config on port {}", port);
}
Err(e) => {
warn!("Failed to update QUIC TLS config on port {}: {}", port, e);
}
}
}
}
}
/// Set the datagram handler relay socket path and establish connection.
pub async fn set_datagram_handler_relay(&mut self, path: String) {
// Cancel previous relay reader task if any