use clap::Parser; use futures::{
FutureExt as _,
future::{Either, select, select_all},
}; use neqo_common::{Datagram, hex, qdebug, qerror, qinfo, qwarn}; use neqo_http3::Http3Server; use neqo_transport::{OutputBatch, RandomConnectionIdGenerator, Version, server::ValidateAddress}; use neqo_udp::{DatagramIter, RecvBuf}; use nss::{
AntiReplay, Cipher, PrivateKey, PublicKey,
constants::{TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256},
generate_ech_keys, init_db, random,
}; use thiserror::Error; use tokio::time::Sleep;
/// List of IP:port to listen on #[arg(default_value = "[::]:4433")]
hosts: Vec<String>,
#[arg(short = 'd', long)] /// NSS database directory [default: `$TEST_FIXTURE_DB` or the bundled NSS test DB].
db: Option<PathBuf>,
#[arg(short = 'k', long, default_value = "key")] /// Name of key from NSS database.
key: String,
#[arg(name = "retry", long)] /// Force a retry
retry: bool,
#[arg(name = "ech", long)] /// Enable encrypted client hello (ECH). /// This generates a new set of ECH keys when it is invoked. /// The resulting configuration is printed to stdout in hexadecimal format.
ech: bool,
}
fn now(&self) -> Instant { ifself.shared.qns_test.is_some() { // When NSS starts its anti-replay it blocks any acceptance of 0-RTT for a // single period. This ensures that an attacker that is able to force a // server to reboot is unable to use that to flush the anti-replay buffers // and have something replayed. // // However, this is a massive inconvenience for us when we are testing. // As we can't initialize `AntiReplay` in the past (see `neqo_common::time` // for why), fast forward time here so that the connections get times from // in the future. // // This is NOT SAFE. Don't do this.
now() + ANTI_REPLAY_WINDOW
} else {
now()
}
}
/// Apply common [`Args`]-driven configuration to any server that implements [`ServerConfig`]. pub(super) fn configure_server(server: &mutimpl ServerConfig, args: &Args) {
server.set_ciphers(&args.get_ciphers());
server.set_qlog_dir(args.shared.qlog_dir.clone()); if args.retry {
server.set_validation(ValidateAddress::Always);
} if args.ech { let (sk, pk) = generate_ech_keys().expect("should create ECH keys");
server.enable_ech(random::<1>()[0], "public.example", &sk, &pk);
qinfo!("ECHConfigList: {}", hex(server.ech_config()));
}
}
/// Generate a response [`SendData`] for a given request path. /// /// In QNS test mode, reads the corresponding file from `/www/`. Returns `Err` /// if the file cannot be read (caller sends a 404) or if the path contains /// `..` components. /// In non-QNS mode, trims `/` from the path, parses the remainder as a byte /// count, and generates that many zero bytes. If parsing fails, sends the /// path bytes instead. pub(super) fn response_for_path(path: &str, is_qns_test: bool) -> Result<SendData, ()> { if is_qns_test { if path.split('/').any(|segment| segment == "..") {
qerror!("Rejecting path with '..' component: {path}"); return Err(());
} let file_path: PathBuf = ["/www", path.trim_matches('/')].iter().collect();
fs::read(file_path).map(SendData::from).map_err(|e| {
qerror!("Failed to read {path}: {e}");
})
} else {
Ok(path
.trim_matches('/')
.parse::<usize>()
.map_or_else(|_| path.into(), SendData::zeroes))
}
}
#[expect(clippy::module_name_repetitions, reason = "This is OK.")] pubtrait HttpServer: Display { fn process_multiple<'a, D: IntoIterator<Item = Datagram<&'a mut [u8]>>>(
&mutself,
dgrams: D,
now: Instant,
max_datagrams: NonZeroUsize,
) -> OutputBatch; fn process_events(&mutself, now: Instant); fn has_events(&self) -> bool; /// Enables an [`HttpServer`] to drive asynchronous operations. /// /// Needed in Firefox's HTTP/3 proxy test server implementation to drive TCP /// and UDP sockets to the proxy target. /// /// <https://github.com/mozilla-firefox/firefox/blob/main/netwerk/test/http3server/src/main.rs> fn poll(self: Pin<&mutSelf>, _cx: &mut Context<'_>) -> Poll<()> {
Poll::Pending
}
}
/// Tries to find a socket, but then just falls back to sending from the first. fn find_socket(
sockets: &mut [(SocketAddr, crate::udp::Socket)],
addr: SocketAddr,
) -> &mutcrate::udp::Socket { let ((_host, first_socket), rest) = sockets.split_first_mut().unwrap();
rest.iter_mut()
.map(|(_host, socket)| socket)
.find(|socket| socket.local_addr().is_ok_and(|a| a == addr))
.unwrap_or(first_socket)
}
// Free function (i.e. not taking `&mut self: ServerRunner`) to be callable by // `ServerRunner::read_and_process` while holding a reference to // `ServerRunner::recv_buf`. asyncfn process_inner(
server: &mut S,
timeout: &mut Option<Pin<Box<Sleep>>>,
sockets: &mut [(SocketAddr, crate::udp::Socket)],
now: &dynFn() -> Instant, mut input_dgrams: Option<DatagramIter<'_>>,
) -> Result<(), io::Error> { // Each socket has a maximum number of GSO segments it can handle. When // calling `server.process_multiple` we don't know which socket will be // used. Take the smallest maximum GSO segments from all sockets to // ensure that we don't send more segments than any socket can handle. // // Ideally we would have a way to know which socket will be used. Likely // not worth it for a test-only server implementation which is mostly // used with a single socket only. let smallest_max_gso_segments = sockets
.iter()
.map(|(_, socket)| socket.max_gso_segments())
.min()
.expect("At least one socket must be present")
.try_into()
.inspect_err(|_| qerror!("Socket return GSO size of 0"))
.map_err(|_| io::Error::from(io::ErrorKind::Unsupported))?;
loop { match server.process_multiple(
input_dgrams.take().into_iter().flatten(),
now(),
smallest_max_gso_segments,
) {
OutputBatch::DatagramBatch(dgram) => { let socket = Self::find_socket(sockets, dgram.source()); loop { // Optimistically attempt sending datagram. In case the // OS buffer is full, wait till socket is writable then // try again. match socket.send(&dgram) {
Ok(()) => break,
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
socket.writable().await?; // Now try again.
}
Err(e) if e.raw_os_error() == Some(libc::EIO)
&& dgram.num_datagrams() > 1 =>
{
qinfo!( "`libc::sendmsg` failed with {e}; quinn-udp will halt segmentation offload"
); // Drop the packets and let QUIC handle retransmission. break;
}
e @ Err(_) => return e,
}
}
}
OutputBatch::Callback(new_timeout) => {
qdebug!("Setting timeout of {new_timeout:?}");
*timeout = Some(Box::pin(tokio::time::sleep(new_timeout))); break;
}
OutputBatch::None => break,
}
}
Ok(())
}
// Wait for any of the sockets to be readable or the timeout to fire. asyncfn ready(&mutself) -> Result<Ready, io::Error> { let sockets_ready = select_all( self.sockets
.iter()
.map(|(_host, socket)| Box::pin(socket.readable())),
)
.map(|(res, inx, _)| match res {
Ok(()) => Ok(Ready::Socket(inx)),
Err(e) => Err(e),
});
// Note: this is the exception to the case where we use `Args::now`. let anti_replay = AntiReplay::new(now(), ANTI_REPLAY_WINDOW, 7, 14)?; let cid_mgr = Rc::new(RefCell::new(RandomConnectionIdGenerator::new(10)));
if args.shared.alpn == "h3" { let runner = Runner::new(
http3::HttpServer::new(&args, anti_replay, cid_mgr), Box::new(move || args.now()),
sockets,
); let local_addrs = runner.local_addresses();
Ok((Box::pin(runner.run()), local_addrs))
} else { let runner = Runner::new(
http09::HttpServer::new(&args, anti_replay, cid_mgr)?, Box::new(move || args.now()),
sockets,
); let local_addrs = runner.local_addresses();
Ok((Box::pin(runner.run()), local_addrs))
}
}
#[cfg(test)] mod tests { usesuper::response_for_path;
#[test] fn response_for_path_qns_not_found() { // Non-existent file should return Err. let result = response_for_path("/no_such_file_xyz", true);
assert!(result.is_err());
}
#[test] fn response_for_path_non_qns_count() { let data = response_for_path("/1000", false).expect("should succeed");
assert_eq!(data.len(), 1000);
}
#[test] fn response_for_path_non_qns_non_numeric_sends_path_bytes() { // Non-numeric path falls back to sending the path bytes themselves. let data = response_for_path("/hello", false).expect("should succeed");
assert_eq!(data.len(), "/hello".len());
}
#[test] fn response_for_path_qns_dotdot_rejected() { // Paths with ".." components must be rejected in QNS mode to prevent // directory traversal outside /www/. for path in ["/../etc/passwd", "/foo/../etc/passwd", "/.."] {
assert!(response_for_path(path, true).is_err(), "path: {path}");
}
}
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.29 Sekunden
(vorverarbeitet am 2026-08-25)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.