firegex/backend/proxysrc/src/quic_hello.rs
2026-10-01 10:05:17 +02:00

679 lines
26 KiB
Rust

//! What a QUIC client offers, read off its first packets before anybody answers them.
//!
//! The TLS path mirrors ALPN by holding the ClientHello open, asking the service with the
//! client's own list and telling the client what came back. QUIC was thought to leave
//! nothing to hold: the ClientHello travels inside an Initial packet, encrypted. But the
//! Initial keys are derived from the Destination Connection ID the client put in the
//! clear (RFC 9001 §5.2) — deliberately, so a middlebox can read exactly this — and so
//! the list can be read before the handshake is answered. That is what this does.
//!
//! What it replaced was a list of candidates the operator had to write down per service,
//! `h3` unless told otherwise: a QUIC service speaking anything else was unreachable
//! through firegex until somebody found the setting. With the client's own list the
//! service chooses from what the client actually offered, exactly as over TLS, and there
//! is nothing to configure.
//!
//! It reads; it never answers. The socket the endpoint owns is wrapped
//! ([`SniffingSocket`]) so every datagram quinn is about to process passes by here first,
//! and only long-header Initial packets are looked at — one comparison for everything
//! else. A ClientHello too large for one packet (post-quantum key shares make that
//! ordinary) is reassembled from its CRYPTO frames across datagrams, in whatever order
//! they arrive.
use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::io::{self, IoSliceMut};
use std::net::SocketAddr;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use quinn::udp::{RecvMeta, Transmit};
use quinn::{AsyncUdpSocket, UdpPoller};
use rustls::quic::{Suite, Version};
use rustls::Side;
/// How many clients' hellos may be in progress at once. Past it the one that has been
/// waiting longest makes room — a hello is claimed within milliseconds of arriving, so the
/// oldest is the one least likely to still be wanted. Forged Initials cost the sender
/// nothing (the keys are public, so a valid one can be made from any address), and with
/// `MAX_HELLO` this bounds what a flood can make this process hold to 16 MiB.
///
/// It used to refuse newcomers instead, so a thousand forged Initials every `KEPT_FOR`
/// were enough to stop every real client's hello from being read.
const MAX_PENDING: usize = 1024;
/// How long an unfinished or unclaimed hello is kept.
const KEPT_FOR: Duration = Duration::from_secs(10);
/// How often the table is swept for hellos older than `KEPT_FOR`. Not on every packet: a
/// sweep walks the whole table, and every packet of a flood would pay for it.
const SWEEP_EVERY: Duration = Duration::from_secs(1);
/// Most CRYPTO bytes kept for one hello. A ClientHello is one or two kilobytes, a few with
/// post-quantum key shares and a resumption ticket; this is a cap on what a peer can make
/// this process hold, not a size anybody meets.
const MAX_HELLO: usize = 16 * 1024;
/// Most CRYPTO fragments kept for one hello, whatever their size. Real clients split a
/// hello into a handful; the cap is what stops a peer sending pieces nobody counted —
/// empty ones, which `MAX_HELLO` did not see at all, grew one hello without end and had
/// every packet sort all of them again, inside the endpoint's receive path.
const MAX_FRAGMENTS: usize = 64;
/// One client's hello as it arrives.
struct Pending {
/// The connection ID its Initials were protected with. A new one means a new attempt —
/// a Retry answered, or another connection from the same port.
dcid: Vec<u8>,
fragments: Vec<(u64, Vec<u8>)>,
held: usize,
alpn: Option<Vec<Vec<u8>>>,
since: Instant,
}
/// The hellos in progress, and the order they began in.
struct Table {
by_client: HashMap<SocketAddr, Pending>,
/// When each entry began, oldest first. An entry replaced or claimed leaves its line
/// here behind it; eviction skips a line whose time no longer matches the entry's.
order: VecDeque<(SocketAddr, Instant)>,
swept: Instant,
}
impl Table {
fn new() -> Self {
Self { by_client: HashMap::new(), order: VecDeque::new(), swept: Instant::now() }
}
fn sweep(&mut self, now: Instant) {
if now.duration_since(self.swept) < SWEEP_EVERY && self.by_client.len() < MAX_PENDING {
return;
}
self.swept = now;
self.by_client.retain(|_, p| now.duration_since(p.since) < KEPT_FOR);
let by_client = &self.by_client;
self.order.retain(|(from, since)| by_client.get(from).is_some_and(|p| p.since == *since));
}
/// Make room for one more by letting go of the entry that began longest ago.
fn evict_oldest(&mut self) {
while let Some((from, since)) = self.order.pop_front() {
if self.by_client.get(&from).is_some_and(|p| p.since == since) {
self.by_client.remove(&from);
return;
}
}
}
fn start(&mut self, from: SocketAddr, dcid: Vec<u8>, now: Instant) -> &mut Pending {
self.order.push_back((from, now));
self.by_client.insert(from, Pending {
dcid,
fragments: Vec::new(),
held: 0,
alpn: None,
since: now,
});
self.by_client.get_mut(&from).expect("just inserted")
}
}
/// The hellos being read, by the address they came from.
pub struct Hellos {
pending: Mutex<Table>,
arrived: tokio::sync::Notify,
suite: Suite,
}
impl fmt::Debug for Hellos {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Hellos").finish_non_exhaustive()
}
}
impl Default for Hellos {
fn default() -> Self {
Self::new()
}
}
impl Hellos {
pub fn new() -> Self {
// Every version's Initial packets are protected with this suite, whatever the
// handshake goes on to choose: RFC 9001 §5.2.
let suite = rustls::crypto::ring::cipher_suite::TLS13_AES_128_GCM_SHA256
.tls13()
.and_then(|s| s.quic_suite())
.expect("TLS13_AES_128_GCM_SHA256 carries QUIC");
Self {
pending: Mutex::new(Table::new()),
arrived: tokio::sync::Notify::new(),
suite,
}
}
/// Look at one datagram received from `from`. Cheap for anything but an Initial.
pub fn observe(&self, from: SocketAddr, datagram: &[u8]) {
let mut rest = datagram;
// An Initial can have other packets coalesced behind it in one datagram; only
// the Initials matter, and they come first.
while let Some(packet) = decrypt_initial(&self.suite, rest) {
rest = &rest[packet.length..];
self.absorb(from, packet);
}
}
fn absorb(&self, from: SocketAddr, packet: Initial) {
let Ok(mut table) = self.pending.lock() else { return };
let now = Instant::now();
table.sweep(now);
// A new attempt — a Retry answered, another connection from the same port — or one
// kept past its time starts again; an address seen for the first time needs room.
let fresh = match table.by_client.get(&from) {
Some(entry) => entry.dcid != packet.dcid || now.duration_since(entry.since) >= KEPT_FOR,
None => {
if table.by_client.len() >= MAX_PENDING {
table.evict_oldest();
}
true
}
};
let entry = if fresh {
table.start(from, packet.dcid.clone(), now)
} else {
table.by_client.get_mut(&from).expect("looked up above")
};
if entry.alpn.is_some() {
return;
}
let mut grew = false;
for (offset, data) in packet.crypto {
// An empty piece says nothing, and one past where any hello could reach is not
// part of one; neither is worth keeping.
if data.is_empty() {
continue;
}
let reaches = offset.saturating_add(data.len() as u64);
if reaches > MAX_HELLO as u64
|| entry.fragments.len() >= MAX_FRAGMENTS
|| entry.held + data.len() > MAX_HELLO
{
break;
}
entry.held += data.len();
entry.fragments.push((offset, data));
grew = true;
}
if !grew {
return;
}
if let Some(alpn) = client_hello_alpn(&entry.fragments) {
entry.alpn = Some(alpn);
entry.fragments.clear();
self.arrived.notify_waiters();
}
}
fn take(&self, from: SocketAddr) -> Option<Vec<Vec<u8>>> {
let mut table = self.pending.lock().ok()?;
if table.by_client.get(&from)?.alpn.is_some() {
return table.by_client.remove(&from).and_then(|p| p.alpn);
}
None
}
/// The protocols the client at `from` offered, waiting up to `wait` for the rest of a
/// hello split across datagrams. `None` when it could not be read.
pub async fn offered(&self, from: SocketAddr, wait: Duration) -> Option<Vec<Vec<u8>>> {
let deadline = tokio::time::Instant::now() + wait;
loop {
// Registered before looking, so a hello completed in between is not missed.
let notified = self.arrived.notified();
tokio::pin!(notified);
notified.as_mut().enable();
if let Some(alpn) = self.take(from) {
return Some(alpn);
}
if tokio::time::timeout_at(deadline, notified).await.is_err() {
return self.take(from);
}
}
}
}
/// The socket a relay's endpoint receives through, reading hellos on the way past.
#[derive(Debug)]
pub struct SniffingSocket {
inner: Arc<dyn AsyncUdpSocket>,
hellos: Arc<Hellos>,
}
impl SniffingSocket {
pub fn new(inner: Arc<dyn AsyncUdpSocket>, hellos: Arc<Hellos>) -> Self {
Self { inner, hellos }
}
}
impl AsyncUdpSocket for SniffingSocket {
fn create_io_poller(self: Arc<Self>) -> Pin<Box<dyn UdpPoller>> {
Arc::clone(&self.inner).create_io_poller()
}
fn try_send(&self, transmit: &Transmit) -> io::Result<()> {
self.inner.try_send(transmit)
}
fn poll_recv(
&self,
cx: &mut Context,
bufs: &mut [IoSliceMut<'_>],
meta: &mut [RecvMeta],
) -> Poll<io::Result<usize>> {
let polled = self.inner.poll_recv(cx, bufs, meta);
if let Poll::Ready(Ok(count)) = &polled {
for (buf, meta) in bufs.iter().zip(meta.iter()).take(*count) {
let received = &buf[..meta.len.min(buf.len())];
// Several datagrams of `stride` bytes each where the kernel coalesced them.
let stride = if meta.stride == 0 { received.len() } else { meta.stride };
for datagram in received.chunks(stride.max(1)) {
if is_long_header(datagram) {
self.hellos.observe(meta.addr, datagram);
}
}
}
}
polled
}
fn local_addr(&self) -> io::Result<SocketAddr> {
self.inner.local_addr()
}
fn max_transmit_segments(&self) -> usize {
self.inner.max_transmit_segments()
}
fn max_receive_segments(&self) -> usize {
self.inner.max_receive_segments()
}
fn may_fragment(&self) -> bool {
self.inner.may_fragment()
}
}
fn is_long_header(datagram: &[u8]) -> bool {
datagram.first().is_some_and(|b| b & 0x80 != 0)
}
/// One Initial packet, decrypted: what it was protected with, and its CRYPTO frames.
struct Initial {
/// How many bytes of the datagram it took, so a packet coalesced behind it is found.
length: usize,
dcid: Vec<u8>,
crypto: Vec<(u64, Vec<u8>)>,
}
fn varint(buf: &[u8], pos: &mut usize) -> Option<u64> {
let first = *buf.get(*pos)?;
let len = 1usize << (first >> 6);
let bytes = buf.get(*pos..*pos + len)?;
let mut value = u64::from(first & 0x3f);
for byte in &bytes[1..] {
value = (value << 8) | u64::from(*byte);
}
*pos += len;
Some(value)
}
/// Decrypt the Initial packet at the start of `packet`, if that is what it is.
fn decrypt_initial(suite: &Suite, packet: &[u8]) -> Option<Initial> {
let first = *packet.first()?;
if first & 0x80 == 0 {
return None;
}
let version = u32::from_be_bytes(packet.get(1..5)?.try_into().ok()?);
let (version, initial_type) = match version {
0x0000_0001 => (Version::V1, 0),
0x6b33_43cf => (Version::V2, 1),
0xff00_001d..=0xff00_0020 => (Version::V1Draft, 0),
_ => return None,
};
if (first >> 4) & 0x03 != initial_type {
return None;
}
let mut pos = 5;
let dcid_len = usize::from(*packet.get(pos)?);
let dcid = packet.get(pos + 1..pos + 1 + dcid_len)?.to_vec();
pos += 1 + dcid_len;
let scid_len = usize::from(*packet.get(pos)?);
pos += 1 + scid_len;
let token_len = usize::try_from(varint(packet, &mut pos)?).ok()?;
pos = pos.checked_add(token_len)?;
let length = usize::try_from(varint(packet, &mut pos)?).ok()?;
let pn_offset = pos;
let end = pn_offset.checked_add(length)?;
if end > packet.len() {
return None;
}
let keys = suite.keys(&dcid, Side::Server, version);
let sample_len = keys.remote.header.sample_len();
let sample = packet.get(pn_offset + 4..pn_offset + 4 + sample_len)?.to_vec();
let mut buf = packet[..end].to_vec();
let (head, tail) = buf.split_at_mut(1);
let pn_field = tail.get_mut(pn_offset - 1..pn_offset + 3)?;
keys.remote
.header
.decrypt_in_place(&sample, &mut head[0], pn_field)
.ok()?;
let pn_len = usize::from(buf[0] & 0x03) + 1;
let mut number = 0u64;
for byte in &buf[pn_offset..pn_offset + pn_len] {
number = (number << 8) | u64::from(*byte);
}
let (header, payload) = buf.split_at_mut(pn_offset + pn_len);
let plain = keys
.remote
.packet
.decrypt_in_place(number, header, payload)
.ok()?;
Some(Initial {
length: end,
dcid,
crypto: crypto_frames(plain)?,
})
}
/// The CRYPTO frames of a decrypted Initial payload. `None` for a frame an Initial cannot
/// carry, which means this was not what it looked like.
fn crypto_frames(payload: &[u8]) -> Option<Vec<(u64, Vec<u8>)>> {
let mut frames = Vec::new();
let mut pos = 0;
while pos < payload.len() {
match varint(payload, &mut pos)? {
0x00 | 0x01 => {} // PADDING, PING
kind @ (0x02 | 0x03) => {
// ACK: largest, delay, range count, first range, the ranges, and ECN.
varint(payload, &mut pos)?;
varint(payload, &mut pos)?;
let ranges = varint(payload, &mut pos)?;
varint(payload, &mut pos)?;
for _ in 0..ranges {
varint(payload, &mut pos)?;
varint(payload, &mut pos)?;
}
if kind == 0x03 {
for _ in 0..3 {
varint(payload, &mut pos)?;
}
}
}
0x06 => {
let offset = varint(payload, &mut pos)?;
let len = usize::try_from(varint(payload, &mut pos)?).ok()?;
let data = payload.get(pos..pos.checked_add(len)?)?;
frames.push((offset, data.to_vec()));
pos += len;
}
// CONNECTION_CLOSE ends what there is to read.
0x1c => break,
_ => return None,
}
}
Some(frames)
}
/// The ALPN list of a ClientHello, once enough of it has arrived to have one.
///
/// `Some(vec![])` for a complete hello that offers none — which QUIC does not allow, and
/// which the handshake is left to refuse.
fn client_hello_alpn(fragments: &[(u64, Vec<u8>)]) -> Option<Vec<Vec<u8>>> {
// What has arrived contiguously from the start, in whatever order it came.
let mut sorted: Vec<&(u64, Vec<u8>)> = fragments.iter().collect();
sorted.sort_by_key(|(offset, _)| *offset);
let mut hello: Vec<u8> = Vec::new();
for (offset, data) in sorted {
let offset = usize::try_from(*offset).ok()?;
if offset > hello.len() {
break;
}
let overlap = hello.len() - offset;
if overlap < data.len() {
hello.extend_from_slice(&data[overlap..]);
}
}
if hello.len() < 4 || hello[0] != 0x01 {
return None;
}
let len = (usize::from(hello[1]) << 16) | (usize::from(hello[2]) << 8) | usize::from(hello[3]);
let body = hello.get(4..4 + len)?;
Some(alpn_of(body).unwrap_or_default())
}
fn alpn_of(body: &[u8]) -> Option<Vec<Vec<u8>>> {
let u16_at = |pos: usize| -> Option<usize> {
Some(usize::from(u16::from_be_bytes(body.get(pos..pos + 2)?.try_into().ok()?)))
};
// Legacy version and random, then the session id, the suites and the compressions.
let mut pos = 2 + 32;
pos += 1 + usize::from(*body.get(pos)?);
pos += 2 + u16_at(pos)?;
pos += 1 + usize::from(*body.get(pos)?);
let end = pos.checked_add(2 + u16_at(pos)?)?;
pos += 2;
while pos + 4 <= end {
let kind = u16_at(pos)?;
let len = u16_at(pos + 2)?;
let data = body.get(pos + 4..pos + 4 + len)?;
pos += 4 + len;
if kind != 16 {
continue;
}
let mut names = Vec::new();
let mut at = 2;
while at < data.len() {
let name_len = usize::from(data[at]);
names.push(data.get(at + 1..at + 1 + name_len)?.to_vec());
at += 1 + name_len;
}
return Some(names);
}
Some(Vec::new())
}
#[cfg(test)]
mod tests {
use super::*;
fn push_varint(out: &mut Vec<u8>, value: u64) {
if value < 64 {
out.push(value as u8);
} else if value < 16384 {
out.extend_from_slice(&(0x4000 | value as u16).to_be_bytes());
} else {
out.extend_from_slice(&(0x8000_0000 | value as u32).to_be_bytes());
}
}
/// A ClientHello offering `alpn`, and nothing else a parser would need.
fn hello_with(alpn: &[&[u8]]) -> Vec<u8> {
let mut names = Vec::new();
for name in alpn {
names.push(name.len() as u8);
names.extend_from_slice(name);
}
let mut ext = vec![0x00, 0x10];
ext.extend_from_slice(&((names.len() + 2) as u16).to_be_bytes());
ext.extend_from_slice(&(names.len() as u16).to_be_bytes());
ext.extend_from_slice(&names);
let mut body = vec![0x03, 0x03];
body.extend_from_slice(&[0u8; 32]);
body.push(0); // no session id
body.extend_from_slice(&[0x00, 0x02, 0x13, 0x01]); // one suite
body.extend_from_slice(&[0x01, 0x00]); // no compression
body.extend_from_slice(&(ext.len() as u16).to_be_bytes());
body.extend_from_slice(&ext);
let mut hello = vec![0x01];
hello.extend_from_slice(&(body.len() as u32).to_be_bytes()[1..]);
hello.extend_from_slice(&body);
hello
}
/// A client Initial carrying these CRYPTO fragments, protected the way a client
/// protects one — with rustls's own client-side keys, so what is read back is checked
/// against the other half of the library rather than against itself.
fn client_initial(dcid: &[u8], number: u64, crypto: &[(u64, &[u8])]) -> Vec<u8> {
let keys = Hellos::new().suite.keys(dcid, Side::Client, Version::V1);
let mut payload = Vec::new();
for (offset, data) in crypto {
payload.push(0x06);
push_varint(&mut payload, *offset);
push_varint(&mut payload, data.len() as u64);
payload.extend_from_slice(data);
}
payload.push(0x01); // a PING, as Chrome scatters among the fragments
payload.resize(payload.len().max(1100), 0); // PADDING, to the size a client sends
let mut packet = vec![0xc1, 0, 0, 0, 1, dcid.len() as u8];
packet.extend_from_slice(dcid);
packet.extend_from_slice(&[0, 0]); // no source id, no token
packet.extend_from_slice(&(0x4000 | (2 + payload.len() + 16) as u16).to_be_bytes());
let pn_offset = packet.len();
packet.extend_from_slice(&(number as u16).to_be_bytes());
let tag = keys.local.packet.encrypt_in_place(number, &packet, &mut payload).unwrap();
packet.extend_from_slice(&payload);
packet.extend_from_slice(tag.as_ref());
let sample = packet[pn_offset + 4..pn_offset + 20].to_vec();
let (first, rest) = packet.split_at_mut(1);
keys.local
.header
.encrypt_in_place(&sample, &mut first[0], &mut rest[pn_offset - 1..pn_offset + 1])
.unwrap();
packet
}
fn from() -> SocketAddr {
"192.0.2.1:4433".parse().unwrap()
}
#[test]
fn a_protected_initial_is_read_for_what_it_offers() {
let hellos = Hellos::new();
let hello = hello_with(&[b"h3", b"fgex-test"]);
hellos.observe(from(), &client_initial(b"\x83\x94\xc8\xf0\x3e\x51\x57\x08", 0, &[(0, &hello)]));
assert_eq!(hellos.take(from()), Some(vec![b"h3".to_vec(), b"fgex-test".to_vec()]));
}
/// Post-quantum key shares make a ClientHello bigger than one packet, and clients
/// scatter its pieces: two Initials, the second one first.
#[test]
fn a_hello_across_two_packets_arriving_backwards_is_reassembled() {
let hellos = Hellos::new();
let hello = hello_with(&[b"doq"]);
let (a, b) = hello.split_at(30);
let dcid = b"\x01\x02\x03\x04\x05\x06\x07\x08";
hellos.observe(from(), &client_initial(dcid, 1, &[(30, b)]));
assert_eq!(hellos.take(from()), None, "half a hello was read as a whole one");
hellos.observe(from(), &client_initial(dcid, 0, &[(0, a)]));
assert_eq!(hellos.take(from()), Some(vec![b"doq".to_vec()]));
}
/// A Retry answered is a new Initial under a new connection ID, and what was gathered
/// under the old one is not mixed into it.
#[test]
fn a_new_connection_id_starts_again() {
let hellos = Hellos::new();
let old = hello_with(&[b"old"]);
let new = hello_with(&[b"new"]);
hellos.observe(from(), &client_initial(b"\x11\x11\x11\x11\x11\x11\x11\x11", 0, &[(0, &old[..20])]));
hellos.observe(from(), &client_initial(b"\x22\x22\x22\x22\x22\x22\x22\x22", 0, &[(0, &new)]));
assert_eq!(hellos.take(from()), Some(vec![b"new".to_vec()]));
}
#[test]
fn anything_but_an_initial_is_passed_over() {
let hellos = Hellos::new();
hellos.observe(from(), &[0x40, 1, 2, 3, 4, 5, 6, 7]);
hellos.observe(from(), &[0xe0, 0, 0, 0, 1, 0, 0]);
let mut tampered = client_initial(b"\x09\x09\x09\x09\x09\x09\x09\x09", 0, &[(0, &hello_with(&[b"h3"]))]);
let last = tampered.len() - 1;
tampered[last] ^= 0xff;
hellos.observe(from(), &tampered);
assert_eq!(hellos.take(from()), None);
}
fn kept_for(hellos: &Hellos, client: SocketAddr) -> Option<usize> {
hellos.pending.lock().unwrap().by_client.get(&client).map(|p| p.fragments.len())
}
/// Empty CRYPTO frames cost nothing to send and were counted by nothing: an Initial
/// full of them grew one client's hello by hundreds of pieces per packet, without end,
/// and every packet sorted all of them again inside the endpoint's receive path.
#[test]
fn empty_pieces_are_not_kept() {
let hellos = Hellos::new();
let empties: Vec<(u64, &[u8])> = (0..300u64).map(|i| (i, &b""[..])).collect();
let dcid = b"\x31\x32\x33\x34\x35\x36\x37\x38";
for number in 0..50 {
hellos.observe(from(), &client_initial(dcid, number, &empties));
}
assert_eq!(kept_for(&hellos, from()), Some(0));
}
/// Pieces that never join up from the start are held, but only so many of them.
#[test]
fn a_hello_is_kept_in_a_bounded_number_of_pieces() {
let hellos = Hellos::new();
let dcid = b"\x41\x42\x43\x44\x45\x46\x47\x48";
for number in 0..200u64 {
hellos.observe(from(), &client_initial(dcid, number, &[(100 + number * 2, b"x")]));
}
let kept = kept_for(&hellos, from()).unwrap();
assert!(kept <= MAX_FRAGMENTS, "{kept} pieces kept for one hello");
}
#[test]
fn a_piece_beyond_any_hello_is_not_kept() {
let hellos = Hellos::new();
let dcid = b"\x51\x52\x53\x54\x55\x56\x57\x58";
hellos.observe(from(), &client_initial(dcid, 0, &[(MAX_HELLO as u64 + 1, b"far")]));
assert_eq!(kept_for(&hellos, from()), Some(0));
}
/// A table full of hellos nobody finished makes room rather than turning newcomers
/// away: refusing them meant a thousand forged Initials every few seconds stopped
/// every real client's hello from being read.
#[test]
fn a_full_table_makes_room_for_the_next_client() {
let hellos = Hellos::new();
let unfinished = hello_with(&[b"h3"]);
for n in 0..MAX_PENDING as u32 {
let forged = SocketAddr::from(([10, (n >> 16) as u8, (n >> 8) as u8, n as u8], 443));
let dcid = n.to_be_bytes().repeat(2);
hellos.observe(forged, &client_initial(&dcid, 0, &[(0, &unfinished[..10])]));
}
assert_eq!(hellos.pending.lock().unwrap().by_client.len(), MAX_PENDING);
let hello = hello_with(&[b"fgex-late"]);
hellos.observe(from(), &client_initial(b"\x61\x62\x63\x64\x65\x66\x67\x68", 0, &[(0, &hello)]));
assert_eq!(hellos.take(from()), Some(vec![b"fgex-late".to_vec()]));
assert!(hellos.pending.lock().unwrap().by_client.len() <= MAX_PENDING);
}
#[tokio::test]
async fn a_hello_still_arriving_is_waited_for() {
let hellos = Arc::new(Hellos::new());
let hello = hello_with(&[b"h3"]);
let packet = client_initial(b"\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11", 0, &[(0, &hello)]);
let later = Arc::clone(&hellos);
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
later.observe(from(), &packet);
});
assert_eq!(hellos.offered(from(), Duration::from_secs(2)).await, Some(vec![b"h3".to_vec()]));
assert_eq!(hellos.offered(from(), Duration::from_millis(50)).await, None);
}
}