use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use quinn::crypto::rustls::QuicServerConfig;
use tokio::sync::watch;
use tokio::task::JoinSet;
use super::http3_err;
use super::request::handle_h3_request;
use crate::conn_limit::PerIpConnLimit;
use crate::error::ServerError;
use crate::listener::drained;
use crate::{MAX_CONCURRENT_STREAMS, ServerState};
pub(crate) fn build_quinn_server_config(
quic_cfg: Arc<plecto_control::TlsServerConfig>,
) -> Result<quinn::ServerConfig, ServerError> {
let crypto = QuicServerConfig::try_from(quic_cfg).map_err(http3_err)?;
let mut server_config = quinn::ServerConfig::with_crypto(Arc::new(crypto));
let mut transport = quinn::TransportConfig::default();
transport.max_concurrent_bidi_streams(MAX_CONCURRENT_STREAMS.into());
server_config.transport_config(Arc::new(transport));
Ok(server_config)
}
pub(crate) fn build_h3_endpoint(
quic_cfg: Arc<plecto_control::TlsServerConfig>,
tcp_addr: SocketAddr,
) -> Result<quinn::Endpoint, ServerError> {
let server_config = build_quinn_server_config(quic_cfg)?;
let udp_addr = SocketAddr::new(tcp_addr.ip(), tcp_addr.port());
quinn::Endpoint::server(server_config, udp_addr).map_err(http3_err)
}
pub(crate) async fn serve_h3(
state: Arc<ServerState>,
endpoint: quinn::Endpoint,
mut drain: watch::Receiver<bool>,
drain_window: Duration,
) {
let mut conns = JoinSet::new();
let mut applied_quic: Option<Arc<plecto_control::TlsServerConfig>> = None;
loop {
let current = state.control.quic_tls_config();
let changed = match (&applied_quic, ¤t) {
(Some(a), Some(b)) => !Arc::ptr_eq(a, b),
(None, None) => false,
_ => true,
};
if changed {
match ¤t {
Some(cfg) => match build_quinn_server_config(cfg.clone()) {
Ok(sc) => {
endpoint.set_server_config(Some(sc));
applied_quic = current.clone();
tracing::info!("HTTP/3 QUIC TLS config updated for new connections");
}
Err(e) => {
tracing::error!(error = %e, "HTTP/3 QUIC TLS config update failed; keeping previous");
}
},
None => {
endpoint.set_server_config(None);
applied_quic = None;
tracing::info!(
"HTTP/3 QUIC TLS cleared; new connections will not complete handshake"
);
}
}
}
let permit = tokio::select! {
_ = drained(&mut drain) => break,
Some(_) = conns.join_next() => continue,
permit = state.conn_limit.clone().acquire_owned() => match permit {
Ok(p) => p,
Err(_) => return, },
};
let incoming = tokio::select! {
_ = drained(&mut drain) => break,
Some(_) = conns.join_next() => continue, incoming = endpoint.accept() => match incoming {
Some(i) => i,
None => return, },
};
let peer_ip = incoming.remote_address().ip();
let Some(ip_guard) = PerIpConnLimit::try_acquire(&state.per_ip_conn_limit, peer_ip) else {
tracing::debug!(peer = %peer_ip, "per-IP connection limit reached; refusing QUIC connection");
incoming.refuse();
continue; };
let state = state.clone();
let drain = drain.clone();
conns.spawn(async move {
let _permit = permit; let _ip_guard = ip_guard; serve_h3_connection(state, incoming, drain).await;
});
}
let all_connections_done = async { while conns.join_next().await.is_some() {} };
let _ = tokio::time::timeout(drain_window, all_connections_done).await;
endpoint.close(0u32.into(), b"");
endpoint.wait_idle().await;
}
async fn serve_h3_connection(
state: Arc<ServerState>,
incoming: quinn::Incoming,
mut drain: watch::Receiver<bool>,
) {
let conn = match incoming.await {
Ok(c) => c,
Err(e) => {
tracing::debug!(error = %e, "QUIC connection failed");
return;
}
};
let peer = conn.remote_address();
let mut h3conn = match h3::server::Connection::<h3_quinn::Connection, Bytes>::new(
h3_quinn::Connection::new(conn),
)
.await
{
Ok(c) => c,
Err(e) => {
tracing::debug!(error = %e, "h3 connection setup failed");
return;
}
};
let mut requests = JoinSet::new();
let mut draining = false;
loop {
let accepted = tokio::select! {
_ = drained(&mut drain), if !draining => {
draining = true;
if let Err(e) = h3conn.shutdown(0).await {
tracing::debug!(error = %e, "h3 GOAWAY failed; closing the connection");
break;
}
if requests.is_empty() {
break;
}
continue;
}
Some(_) = requests.join_next(), if !requests.is_empty() => {
if draining && requests.is_empty() {
break; }
continue;
}
accepted = h3conn.accept() => accepted,
};
match accepted {
Ok(Some(resolver)) => {
let state = state.clone();
requests.spawn(async move {
if let Err(e) = handle_h3_request(state, peer, resolver).await {
tracing::debug!(error = %e, "h3 request failed");
}
});
}
Ok(None) => break,
Err(e) => {
tracing::debug!(error = %e, "h3 accept failed");
break;
}
}
}
requests.detach_all();
}