This commit is contained in:
Mateusz Gruszczyński committed 2026-10-05 19:05:55 +02:00
1 parent ddc5c6593f
commit b9d137eee1
63 files changed
+1232 -445

No files matched your search

+75 -16
View File
@@ -28,7 +28,6 @@ use tokio::{
task::JoinSet,
};
use tower::ServiceExt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Settings {
@@ -191,6 +190,63 @@ impl Drop for Lease {
.fetch_sub(1, Ordering::Relaxed);
}
}
#[allow(clippy::too_many_arguments)]
fn log_protocol_error(
state: &App,
kind: &'static str,
detail: impl std::fmt::Display,
peer: SocketAddr,
local: SocketAddr,
status: u16,
authority: Option<String>,
host: Option<String>,
sni: Option<String>,
) {
let resolved = state.config.load().resolve_logging_vhost(
local,
true,
None,
authority.as_deref(),
host.as_deref(),
sni.as_deref(),
);
let (vhost, matched_by) = resolved
.map(|resolved| (Some(resolved.vhost), resolved.matched_by))
.unwrap_or((None, "none"));
state.logs.push_protocol_error_attributed(
kind,
detail,
peer,
local,
"quic",
"3",
status,
sni,
authority.or(host),
vhost,
matched_by,
);
}
fn request_identity<B>(request: &http::Request<B>) -> (Option<String>, Option<String>) {
let authority = request
.uri()
.authority()
.map(|value| value.as_str().to_owned());
let host = request
.headers()
.get(http::header::HOST)
.and_then(|value| value.to_str().ok())
.map(str::to_owned);
(authority, host)
}
fn connection_sni(connection: &quinn::Connection) -> Option<String> {
connection
.handshake_data()?
.downcast::<quinn::crypto::rustls::HandshakeData>()
.ok()?
.server_name
.and_then(|name| crate::config::request_host(&name).ok())
}
pub async fn serve(bound: Bound, router: Router, state: Arc<App>, grace: Duration) -> Result<()> {
let slots = Arc::new(Semaphore::new(
bound.settings.max_concurrent_connections as usize,
@@ -258,15 +314,16 @@ pub async fn serve(bound: Bound, router: Router, state: Arc<App>, grace: Duratio
None if !address.ip().is_unspecified()=>address,
None=>{connection.close(0x102u32.into(),b"local destination unavailable");return;},
};
let tls_sni=connection_sni(&connection);
state.metrics.connections.fetch_add(1,Ordering::Relaxed);
state.metrics.active_connections.fetch_add(1,Ordering::Relaxed);
state.metrics.http3_connections.fetch_add(1,Ordering::Relaxed);
state.metrics.http3_active_connections.fetch_add(1,Ordering::Relaxed);
state.metrics.tls_connections.fetch_add(1,Ordering::Relaxed);
let _lease=Lease {_permit:permit,metrics:state.metrics.clone()};
if let Err(error)=connection_loop(connection,local,router,state.clone(),limit,grace).await {
state.logs.push_protocol_error(
"http3_connection_error", &error, peer, local, "quic", "3", 0, None,
if let Err(error)=connection_loop(connection,local,router,state.clone(),limit,grace,tls_sni.clone()).await {
log_protocol_error(
&state, "http3_connection_error", &error, peer, local, 0, None, None, tls_sni,
);
tracing::debug!(%error,"HTTP/3 connection ended");
}
@@ -290,6 +347,7 @@ async fn connection_loop(
state: Arc<App>,
limit: usize,
grace: Duration,
tls_sni: Option<String>,
) -> Result<()> {
let mut builder = h3::server::builder();
builder.max_field_section_size(32 * 1024);
@@ -316,27 +374,27 @@ async fn connection_loop(
Ok(Some(resolver))=>{
// Completed JoinSet entries are drained before accepting more.
if requests.len()>=limit {
state.logs.push_protocol_error(
"http3_stream_limit_rejected", "HTTP/3 concurrent request stream limit reached",
connection.remote_address(), local, "quic", "3", 0, None,
log_protocol_error(
&state, "http3_stream_limit_rejected", "HTTP/3 concurrent request stream limit reached",
connection.remote_address(), local, 0, None, None, tls_sni.clone(),
);
drop(resolver);continue;
}
let router=router.clone();let state=state.clone();let peer=connection.remote_address();
let router=router.clone();let state=state.clone();let peer=connection.remote_address();let tls_sni=tls_sni.clone();
requests.spawn(async move {
let resolved=tokio::time::timeout(state.header_timeout,resolver.resolve_request()).await;
let (mut request,stream)=match resolved {
Ok(Ok(request))=>request,
Ok(Err(error))=>{
state.logs.push_protocol_error(
"http3_request_error", error, peer, local, "quic", "3", 400, None,
log_protocol_error(
&state, "http3_request_error", error, peer, local, 400, None, None, tls_sni.clone(),
);
return;
},
Err(_)=>{
state.logs.push_protocol_error(
"http3_header_timeout", "HTTP/3 request header timeout",
peer, local, "quic", "3", 408, None,
log_protocol_error(
&state, "http3_header_timeout", "HTTP/3 request header timeout",
peer, local, 408, None, None, tls_sni.clone(),
);
return;
},
@@ -347,9 +405,10 @@ async fn connection_loop(
let websocket=extended_connect && protocol.as_deref()==Some("websocket");
let (mut send,receive)=stream.split();
if invalid_headers(request.headers()) {
state.logs.push_protocol_error(
"http3_invalid_headers", "HTTP/3 request contains forbidden connection-specific headers",
peer, local, "quic", "3", 400, None,
let (authority, host) = request_identity(&request);
log_protocol_error(
&state, "http3_invalid_headers", "HTTP/3 request contains forbidden connection-specific headers",
peer, local, 400, authority, host, tls_sni.clone(),
);
send.stop_stream(Code::H3_MESSAGE_ERROR);return;
}