This commit is contained in:
Mateusz Gruszczyński committed 2026-10-20 11:19:55 +02:00
1 parent 8c0de520d7
commit 8f41b39c26
32 files changed
+934 -128

No files matched your search

+15
View File
@@ -7,6 +7,10 @@ pub struct Settings {
pub max_concurrent_unistream: u32,
/// QUIC idle timeout in seconds; zero disables the transport idle timeout.
pub max_idle_timeout: u64,
/// Desired effective UDP socket receive buffer (bytes). 0 = OS default.
pub udp_receive_buffer_bytes: u32,
/// Desired effective UDP socket send buffer (bytes). 0 = OS default.
pub udp_send_buffer_bytes: u32,
}
impl Default for Settings {
fn default() -> Self {
@@ -16,6 +20,8 @@ impl Default for Settings {
max_concurrent_bidistream: 100,
max_concurrent_unistream: 100,
max_idle_timeout: 10,
udp_receive_buffer_bytes: 4 * 1024 * 1024,
udp_send_buffer_bytes: 4 * 1024 * 1024,
}
}
}
@@ -38,6 +44,15 @@ impl Settings {
self.max_idle_timeout <= 86400,
"HTTP/3 idle timeout must be 0..86400 seconds"
);
for (name, value) in [
("receive", self.udp_receive_buffer_bytes),
("send", self.udp_send_buffer_bytes),
] {
ensure!(
value == 0 || (64 * 1024..=16 * 1024 * 1024).contains(&value),
"HTTP/3 UDP {name} buffer must be 0 or 65536..16777216 bytes"
);
}
Ok(())
}
pub fn transport(&self) -> Result<quinn::TransportConfig> {
+107 -9
View File
@@ -41,9 +41,15 @@ mod tests {
}
.validate()
.is_err());
assert!(Settings { udp_receive_buffer_bytes: 1, ..Default::default() }
.validate().is_err());
assert!(Settings { udp_send_buffer_bytes: 16 * 1024 * 1024 + 1, ..Default::default() }
.validate().is_err());
assert!(Settings { udp_send_buffer_bytes: 0, ..Default::default() }
.validate().is_ok());
}
#[test]
fn alt_svc_only_for_actual_tls_udp_listener() {
fn alt_svc_uses_latest_committed_lifetime_and_only_bound_udp() {
let binding = Listener {
address: "127.0.0.1:8443".parse().unwrap(),
tls: true,
@@ -52,19 +58,111 @@ mod tests {
h2c: false,
http3: Some(Settings::default()),
};
let active = [binding];
let mut h = HeaderMap::new();
h.insert("alt-svc", "h3=\":9999\"".parse().unwrap());
advertise(&mut h, Some(binding.address), true, &[binding]);
advertise(&mut h, Some(binding.address), true, &active, &active, true);
assert_eq!(h["alt-svc"], "h3=\":8443\"; ma=3600");
advertise(&mut h, Some(binding.address), false, &[binding]);
let mut updated = binding;
updated.http3.as_mut().unwrap().alt_svc_max_age = 19;
advertise(&mut h, Some(binding.address), true, &active, &[updated], true);
assert_eq!(h["alt-svc"], "h3=\":8443\"; ma=19");
advertise(&mut h, Some(binding.address), false, &active, &[updated], true);
assert!(!h.contains_key("alt-svc"));
advertise(
&mut h,
Some("127.0.0.2:8443".parse().unwrap()),
true,
&[binding],
);
advertise(&mut h, Some("127.0.0.2:8443".parse().unwrap()), true, &active, &[updated], true);
assert!(!h.contains_key("alt-svc"));
// An intended but not bound H3 socket MUST NOT be advertised.
let mut previously_h2_only = binding;
previously_h2_only.http3 = None;
advertise(&mut h, Some(binding.address), true, &[previously_h2_only], &[updated], true);
assert!(!h.contains_key("alt-svc"));
}
#[test]
fn disabling_h3_withdraws_cached_advertisement() {
let binding = Listener {
address: "0.0.0.0:443".parse().unwrap(),
tls: true,
http2: true,
http2_settings: None,
h2c: false,
http3: Some(Settings::default()),
};
let local = Some("192.0.2.2:443".parse().unwrap());
let mut h = HeaderMap::new();
// Vhost opt-out: immediate Alt-Svc: clear without affecting another host.
advertise(&mut h, local, true, &[binding], &[binding], false);
assert_eq!(h["alt-svc"], "clear");
// Listener opt-out: listener removal requires restart but the header
// is withdrawn immediately, while existing H3 sessions may survive.
let mut disabled = binding;
disabled.http3 = None;
advertise(&mut h, local, true, &[binding], &[disabled], true);
assert_eq!(h["alt-svc"], "clear");
advertise(&mut h, local, true, &[binding], &[], true);
assert_eq!(h["alt-svc"], "clear");
withdraw_advertisement(&mut h, local, true, &[binding]);
assert_eq!(h["alt-svc"], "clear");
withdraw_advertisement(&mut h, local, false, &[binding]);
assert!(!h.contains_key("alt-svc"));
}
#[test]
fn h3_hot_changes_do_not_misreport_restart() {
let active = Listener {
address: "127.0.0.1:8443".parse().unwrap(),
tls: true,
http2: true,
http2_settings: Some(crate::settings::Http2Connections::default()),
h2c: false,
http3: Some(Settings::default()),
};
assert!(!listeners_require_restart(&[active], &[active]));
let mut next = active;
let h3 = next.http3.as_mut().unwrap();
h3.alt_svc_max_age = 0;
h3.max_concurrent_connections = 40;
h3.max_concurrent_bidistream = 20;
h3.max_concurrent_unistream = 30;
h3.max_idle_timeout = 90;
assert!(!listeners_require_restart(&[active], &[next]));
// A capacity increase above the startup socket/permit cap needs restart.
next.http3.as_mut().unwrap().max_concurrent_connections = 513;
assert!(listeners_require_restart(&[active], &[next]));
next = active;
next.http3.as_mut().unwrap().udp_receive_buffer_bytes = 8 * 1024 * 1024;
assert!(listeners_require_restart(&[active], &[next]));
next = active;
next.http3.as_mut().unwrap().udp_send_buffer_bytes = 8 * 1024 * 1024;
assert!(listeners_require_restart(&[active], &[next]));
next = active;
next.http3 = None;
assert!(listeners_require_restart(&[active], &[next]));
next = active;
next.address = "127.0.0.1:9443".parse().unwrap();
assert!(listeners_require_restart(&[active], &[next]));
next = active;
next.http2 = false;
assert!(listeners_require_restart(&[active], &[next]));
assert!(listeners_require_restart(&[active], &[]));
}
#[test]
fn udp_buffers_keep_legacy_config_compatible() {
let old = r#"{"alt_svc_max_age":3600,"max_concurrent_connections":512,"max_concurrent_bidistream":100,"max_concurrent_unistream":100,"max_idle_timeout":10}"#;
let settings: Settings = serde_json::from_str(old).unwrap();
assert_eq!(settings.udp_receive_buffer_bytes, 4 * 1024 * 1024);
assert_eq!(settings.udp_send_buffer_bytes, 4 * 1024 * 1024);
assert_eq!(serde_json::from_str::<Settings>(&serde_json::to_string(&settings).unwrap()).unwrap(), settings);
}
#[test]
fn udp_socket_tuning_produces_readable_socket_options() {
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
let address = socket.local_addr().unwrap();
configure_udp_socket(&socket, address, Settings::default());
let socket = socket2::SockRef::from(&socket);
assert!(socket.recv_buffer_size().unwrap() > 0);
assert!(socket.send_buffer_size().unwrap() > 0);
}
#[test]
fn client_cancellation_is_a_stream_event_not_an_http_400() {