This commit is contained in:
Mateusz Gruszczyński committed 2026-10-11 22:12:27 +02:00
1 parent 75104f87ba
commit 4cc994b3bc
86 files changed
+942 -223

No files matched your search

+3 -3
View File
@@ -110,8 +110,8 @@ impl HttpBody for ReceiveBody {
}
impl Drop for ReceiveBody {
fn drop(&mut self) {
// Dropping the request stream cancels the receive side at QUIC level.
// Explicit STOP_SENDING is used only from poll_frame paths where the
// stream handle is available and a protocol error was detected.
// Do not call stop_sending here: h3/quinn can still have a pending
// receive read and has previously panicked on that cancellation path.
// Protocol framing violations explicitly stop only from poll_frame.
}
}
+91
View File
@@ -0,0 +1,91 @@
struct Lease {
_permit: OwnedSemaphorePermit,
metrics: Arc<Metrics>,
}
impl Drop for Lease {
fn drop(&mut self) {
self.metrics
.http3_active_connections
.fetch_sub(1, Ordering::Relaxed);
self.metrics
.active_connections
.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,
);
}
/// Service-level breadcrumbs for a full HTTP/3 GET / navigation. User-provided
/// query strings, headers and error details must not reach this diagnostic.
#[allow(clippy::too_many_arguments)]
fn log_root_navigation(
stage: &'static str,
peer: SocketAddr,
local: SocketAddr,
sni: &Option<String>,
stream_id: &str,
phase: &'static str,
response_status: u16,
body_bytes_sent: u64,
cancelled: bool,
) {
match stage {
"started" => tracing::info!(
peer=%peer, local=%local, sni=?sni, stream_id=%stream_id,
method="GET", path="/", "http3_navigation_started"
),
"complete" => tracing::info!(
peer=%peer, local=%local, sni=?sni, stream_id=%stream_id,
method="GET", path="/", response_status, body_bytes_sent,
"http3_navigation_complete"
),
"interrupted" => tracing::warn!(
peer=%peer, local=%local, sni=?sni, stream_id=%stream_id,
method="GET", path="/", phase, response_status, body_bytes_sent,
cancelled, "http3_navigation_interrupted"
),
_ => {}
}
}
fn log_h3_response_complete(peer: SocketAddr, stream_id: &str, response_status: u16, body_bytes_sent: u64) {
tracing::debug!(
peer=%peer, stream_id=%stream_id, response_status, body_bytes_sent,
"HTTP/3 response completed"
);
}
+33
View File
@@ -0,0 +1,33 @@
/// A client can cancel an obsolete in-flight request during navigation/F5.
/// RFC 9114 section 4.1.1 gives this a stream reset code, not an HTTP 400.
/// Treat only peer-originated cancellation as benign; local protocol errors
/// and other remote reset codes remain visible to operators.
fn client_cancelled_code(code: Code) -> bool {
code == Code::H3_REQUEST_CANCELLED || code == Code::H3_NO_ERROR
}
fn client_cancelled_request(error: &h3::error::StreamError) -> bool {
matches!(
error,
h3::error::StreamError::RemoteTerminate { code, .. } if client_cancelled_code(*code)
)
}
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())
}
+60
View File
@@ -0,0 +1,60 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Settings {
pub alt_svc_max_age: u64,
pub max_concurrent_connections: u32,
pub max_concurrent_bidistream: u32,
pub max_concurrent_unistream: u32,
/// QUIC idle timeout in seconds; zero disables the transport idle timeout.
pub max_idle_timeout: u64,
}
impl Default for Settings {
fn default() -> Self {
Self {
alt_svc_max_age: 3600,
max_concurrent_connections: 512,
max_concurrent_bidistream: 100,
max_concurrent_unistream: 100,
max_idle_timeout: 10,
}
}
}
impl Settings {
pub fn validate(&self) -> Result<()> {
ensure!(
self.alt_svc_max_age <= 604800,
"HTTP/3 Alt-Svc lifetime must be at most seven days"
);
ensure!(
(1..=4096).contains(&self.max_concurrent_connections),
"HTTP/3 connections per listener must be 1..4096"
);
ensure!(
(1..=1024).contains(&self.max_concurrent_bidistream),
"HTTP/3 bidirectional streams must be 1..1024"
);
ensure!((3..=1024).contains(&self.max_concurrent_unistream),"HTTP/3 requires at least three peer unidirectional streams (control and QPACK), maximum 1024");
ensure!(
self.max_idle_timeout <= 86400,
"HTTP/3 idle timeout must be 0..86400 seconds"
);
Ok(())
}
pub fn transport(&self) -> Result<quinn::TransportConfig> {
self.validate()?;
let mut transport = quinn::TransportConfig::default();
transport
.max_concurrent_bidi_streams(self.max_concurrent_bidistream.into())
.max_concurrent_uni_streams(self.max_concurrent_unistream.into())
.max_idle_timeout(if self.max_idle_timeout == 0 {
None
} else {
Some(Duration::from_secs(self.max_idle_timeout).try_into()?)
})
.stream_receive_window((256 * 1024u32).into())
.receive_window((8 * 1024 * 1024u32).into())
.send_window(8 * 1024 * 1024)
.datagram_receive_buffer_size(None);
Ok(transport)
}
}
+9
View File
@@ -67,6 +67,15 @@ mod tests {
assert!(!h.contains_key("alt-svc"));
}
#[test]
fn client_cancellation_is_a_stream_event_not_an_http_400() {
// Exercise the production classifier; RemoteTerminate is
// non-exhaustive in hpx-h3 and cannot be constructed by this crate.
assert!(client_cancelled_code(Code::H3_REQUEST_CANCELLED));
assert!(client_cancelled_code(Code::H3_NO_ERROR));
assert!(!client_cancelled_code(Code::H3_MESSAGE_ERROR));
assert!(!client_cancelled_code(Code::H3_INTERNAL_ERROR));
}
#[test]
fn h3_rejects_http1_connection_specific_headers() {
for name in ["connection", "transfer-encoding", "upgrade"] {
let mut h = HeaderMap::new();