v0.31.5
This commit is contained in:
1 parent
75104f87ba
commit
4cc994b3bc
86 files changed
+942
-223
No files matched your search
+80
-137
@@ -28,66 +28,7 @@ use tokio::{
|
||||
task::JoinSet,
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
include!("http3/settings.rs");
|
||||
pub fn crypto(
|
||||
tls: &Arc<rustls::ServerConfig>,
|
||||
policy: &crate::tls::ProtocolPolicy,
|
||||
@@ -176,77 +117,6 @@ pub fn advertise(
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
);
|
||||
}
|
||||
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,
|
||||
@@ -386,9 +256,19 @@ async fn connection_loop(
|
||||
let (mut request,stream)=match resolved {
|
||||
Ok(Ok(request))=>request,
|
||||
Ok(Err(error))=>{
|
||||
log_protocol_error(
|
||||
&state, "http3_request_error", error, peer, local, 400, None, None, tls_sni.clone(),
|
||||
);
|
||||
if client_cancelled_request(&error) {
|
||||
// Record the cancellation with status=0 (no HTTP response)
|
||||
// for iOS navigation investigations, never synthetic 400.
|
||||
log_protocol_error(
|
||||
&state, "http3_peer_cancelled_before_headers",
|
||||
format!("phase=request_headers stream_status=cancelled error={error}"),
|
||||
peer, local, 0, None, None, tls_sni.clone(),
|
||||
);
|
||||
} else {
|
||||
log_protocol_error(
|
||||
&state, "http3_request_error", error, peer, local, 400, None, None, tls_sni.clone(),
|
||||
);
|
||||
}
|
||||
return;
|
||||
},
|
||||
Err(_)=>{
|
||||
@@ -404,6 +284,22 @@ async fn connection_loop(
|
||||
let extended_connect=request.method()==Method::CONNECT && protocol.is_some();
|
||||
let websocket=extended_connect && protocol.as_deref()==Some("websocket");
|
||||
let (mut send,receive)=stream.split();
|
||||
let stream_id=format!("{:?}",send.send_id());
|
||||
let diagnostic_method=request.method().as_str().to_owned();
|
||||
// Do not record query strings or cookies in transport diagnostics.
|
||||
let diagnostic_path=request.uri().path().chars().take(160).collect::<String>();
|
||||
let (diagnostic_authority, diagnostic_host)=request_identity(&request);
|
||||
// GET / is a full document navigation (the original logo).
|
||||
// Emit only query-free, bounded navigation metadata.
|
||||
let root_navigation=diagnostic_method=="GET" && diagnostic_path=="/";
|
||||
if root_navigation { log_root_navigation(
|
||||
"started", peer, local, &tls_sni, &stream_id, "request_headers", 0, 0, false,
|
||||
); }
|
||||
tracing::debug!(
|
||||
peer=%peer, stream_id=%stream_id,
|
||||
method=%diagnostic_method, path=%diagnostic_path,
|
||||
"HTTP/3 request headers accepted"
|
||||
);
|
||||
if invalid_headers(request.headers()) {
|
||||
let (authority, host) = request_identity(&request);
|
||||
log_protocol_error(
|
||||
@@ -437,8 +333,12 @@ async fn connection_loop(
|
||||
crate::proxy::headers::strip_hop_by_hop(&mut parts.headers);
|
||||
parts.version=Version::HTTP_3;
|
||||
let response=http::Response::from_parts(parts,());
|
||||
let response_status=response.status().as_u16();
|
||||
let mut response_phase="response_headers";
|
||||
let mut sent_body_bytes=0u64;
|
||||
let sent=async {
|
||||
tokio::time::timeout(state.body_idle_timeout,send.send_response(response)).await??;
|
||||
response_phase="response_body";
|
||||
if let Some(tunnel)=tunnel {
|
||||
let Some(mut receive)=tunnel_receive.take() else {
|
||||
send.stop_stream(Code::H3_INTERNAL_ERROR);
|
||||
@@ -462,21 +362,62 @@ async fn connection_loop(
|
||||
while let Some(frame)=body.frame().await {
|
||||
let frame=frame?;
|
||||
match frame.into_data() {
|
||||
Ok(data)=>{if !data.is_empty() {tokio::time::timeout(state.body_idle_timeout,send.send_data(data)).await??;}},
|
||||
Ok(data)=>{if !data.is_empty() {
|
||||
let data_len=data.len() as u64;
|
||||
tokio::time::timeout(state.body_idle_timeout,send.send_data(data)).await??;
|
||||
sent_body_bytes=sent_body_bytes.saturating_add(data_len);
|
||||
}},
|
||||
Err(frame)=>{if let Ok(mut trailers)=frame.into_trailers() {
|
||||
crate::proxy::headers::strip_hop_by_hop(&mut trailers);
|
||||
tokio::time::timeout(state.body_idle_timeout,send.send_trailers(trailers)).await??;
|
||||
}},
|
||||
}
|
||||
}
|
||||
response_phase="response_finish";
|
||||
tokio::time::timeout(state.body_idle_timeout,send.finish()).await??;
|
||||
Ok::<(),anyhow::Error>(())
|
||||
}.await;
|
||||
if let Err(error)=sent {tracing::debug!(%error,"HTTP/3 response interrupted");send.stop_stream(Code::H3_INTERNAL_ERROR);}
|
||||
if let Err(error)=sent {
|
||||
let cancelled=error.downcast_ref::<h3::error::StreamError>()
|
||||
.is_some_and(client_cancelled_request);
|
||||
let kind=if cancelled { "http3_peer_cancelled_after_headers" }
|
||||
else { "http3_response_write_error" };
|
||||
log_protocol_error(
|
||||
&state, kind,
|
||||
format!("phase={response_phase} stream_id={stream_id} method={diagnostic_method} path={diagnostic_path} response_status={response_status} body_bytes_sent={sent_body_bytes} error={error}"),
|
||||
peer, local, 0,
|
||||
diagnostic_authority, diagnostic_host, tls_sni.clone(),
|
||||
);
|
||||
if root_navigation { log_root_navigation(
|
||||
"interrupted", peer, local, &tls_sni, &stream_id, response_phase,
|
||||
response_status, sent_body_bytes, cancelled,
|
||||
); }
|
||||
if !cancelled {
|
||||
// Never mark a truncated upstream response as success.
|
||||
// Reset only this response stream, not sibling requests.
|
||||
send.stop_stream(Code::H3_INTERNAL_ERROR);
|
||||
}
|
||||
} else if root_navigation {
|
||||
log_root_navigation("complete", peer, local, &tls_sni, &stream_id,
|
||||
"response_finish", response_status, sent_body_bytes, false);
|
||||
} else {
|
||||
log_h3_response_complete(peer, &stream_id, response_status, sent_body_bytes);
|
||||
}
|
||||
});
|
||||
},
|
||||
Ok(None)=>break,
|
||||
Err(error)=>{requests.abort_all();return Err(error.into());},
|
||||
Err(error)=>{
|
||||
// The parser/QUIC transport can stop accepting after a peer
|
||||
// refresh while unrelated response streams are still alive.
|
||||
// Abort_all here reset those streams (including a new GET /).
|
||||
// Stop accepting and allow the bounded drain below instead.
|
||||
log_protocol_error(
|
||||
&state, "http3_accept_error", &error,
|
||||
connection.remote_address(), local, 0, None, None, tls_sni.clone(),
|
||||
);
|
||||
tracing::debug!(%error, "HTTP/3 accept stopped; draining in-flight streams");
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -492,6 +433,8 @@ async fn connection_loop(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
include!("http3/identity.rs");
|
||||
include!("http3/diagnostics.rs");
|
||||
include!("http3/body.rs");
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in new issue
Block a user