diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /third_party/rust/neqo-transport/src/connection/tests/handshake.rs | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/neqo-transport/src/connection/tests/handshake.rs')
-rw-r--r-- | third_party/rust/neqo-transport/src/connection/tests/handshake.rs | 1137 |
1 files changed, 1137 insertions, 0 deletions
diff --git a/third_party/rust/neqo-transport/src/connection/tests/handshake.rs b/third_party/rust/neqo-transport/src/connection/tests/handshake.rs new file mode 100644 index 0000000000..93385ac1bc --- /dev/null +++ b/third_party/rust/neqo-transport/src/connection/tests/handshake.rs @@ -0,0 +1,1137 @@ +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::{ + cell::RefCell, + convert::TryFrom, + mem, + net::{IpAddr, Ipv6Addr, SocketAddr}, + rc::Rc, + time::Duration, +}; + +use neqo_common::{event::Provider, qdebug, Datagram}; +use neqo_crypto::{ + constants::TLS_CHACHA20_POLY1305_SHA256, generate_ech_keys, AuthenticationStatus, +}; +use test_fixture::{ + self, addr, assertions, assertions::assert_coalesced_0rtt, datagram, fixture_init, now, + split_datagram, +}; + +use super::{ + super::{Connection, Output, State}, + assert_error, connect, connect_force_idle, connect_with_rtt, default_client, default_server, + get_tokens, handshake, maybe_authenticate, resumed_server, send_something, + CountingConnectionIdGenerator, AT_LEAST_PTO, DEFAULT_RTT, DEFAULT_STREAM_DATA, +}; +use crate::{ + connection::AddressValidation, + events::ConnectionEvent, + path::PATH_MTU_V6, + server::ValidateAddress, + tparams::{TransportParameter, MIN_ACK_DELAY}, + tracking::DEFAULT_ACK_DELAY, + ConnectionError, ConnectionParameters, EmptyConnectionIdGenerator, Error, StreamType, Version, +}; + +const ECH_CONFIG_ID: u8 = 7; +const ECH_PUBLIC_NAME: &str = "public.example"; + +#[test] +fn full_handshake() { + qdebug!("---- client: generate CH"); + let mut client = default_client(); + let out = client.process(None, now()); + assert!(out.as_dgram_ref().is_some()); + assert_eq!(out.as_dgram_ref().unwrap().len(), PATH_MTU_V6); + + qdebug!("---- server: CH -> SH, EE, CERT, CV, FIN"); + let mut server = default_server(); + let out = server.process(out.as_dgram_ref(), now()); + assert!(out.as_dgram_ref().is_some()); + assert_eq!(out.as_dgram_ref().unwrap().len(), PATH_MTU_V6); + + qdebug!("---- client: cert verification"); + let out = client.process(out.as_dgram_ref(), now()); + assert!(out.as_dgram_ref().is_some()); + + let out = server.process(out.as_dgram_ref(), now()); + assert!(out.as_dgram_ref().is_none()); + + assert!(maybe_authenticate(&mut client)); + + qdebug!("---- client: SH..FIN -> FIN"); + let out = client.process(out.as_dgram_ref(), now()); + assert!(out.as_dgram_ref().is_some()); + assert_eq!(*client.state(), State::Connected); + + qdebug!("---- server: FIN -> ACKS"); + let out = server.process(out.as_dgram_ref(), now()); + assert!(out.as_dgram_ref().is_some()); + assert_eq!(*server.state(), State::Confirmed); + + qdebug!("---- client: ACKS -> 0"); + let out = client.process(out.as_dgram_ref(), now()); + assert!(out.as_dgram_ref().is_none()); + assert_eq!(*client.state(), State::Confirmed); +} + +#[test] +fn handshake_failed_authentication() { + qdebug!("---- client: generate CH"); + let mut client = default_client(); + let out = client.process(None, now()); + assert!(out.as_dgram_ref().is_some()); + + qdebug!("---- server: CH -> SH, EE, CERT, CV, FIN"); + let mut server = default_server(); + let out = server.process(out.as_dgram_ref(), now()); + assert!(out.as_dgram_ref().is_some()); + + qdebug!("---- client: cert verification"); + let out = client.process(out.as_dgram_ref(), now()); + assert!(out.as_dgram_ref().is_some()); + + let out = server.process(out.as_dgram_ref(), now()); + assert!(out.as_dgram_ref().is_none()); + + let authentication_needed = |e| matches!(e, ConnectionEvent::AuthenticationNeeded); + assert!(client.events().any(authentication_needed)); + qdebug!("---- client: Alert(certificate_revoked)"); + client.authenticated(AuthenticationStatus::CertRevoked, now()); + + qdebug!("---- client: -> Alert(certificate_revoked)"); + let out = client.process(None, now()); + assert!(out.as_dgram_ref().is_some()); + + qdebug!("---- server: Alert(certificate_revoked)"); + let out = server.process(out.as_dgram_ref(), now()); + assert!(out.as_dgram_ref().is_some()); + assert_error(&client, &ConnectionError::Transport(Error::CryptoAlert(44))); + assert_error(&server, &ConnectionError::Transport(Error::PeerError(300))); +} + +#[test] +fn no_alpn() { + fixture_init(); + let mut client = Connection::new_client( + "example.com", + &["bad-alpn"], + Rc::new(RefCell::new(CountingConnectionIdGenerator::default())), + addr(), + addr(), + ConnectionParameters::default(), + now(), + ) + .unwrap(); + let mut server = default_server(); + + handshake(&mut client, &mut server, now(), Duration::new(0, 0)); + // TODO (mt): errors are immediate, which means that we never send CONNECTION_CLOSE + // and the client never sees the server's rejection of its handshake. + // assert_error(&client, ConnectionError::Transport(Error::CryptoAlert(120))); + assert_error( + &server, + &ConnectionError::Transport(Error::CryptoAlert(120)), + ); +} + +#[test] +fn dup_server_flight1() { + qdebug!("---- client: generate CH"); + let mut client = default_client(); + let out = client.process(None, now()); + assert!(out.as_dgram_ref().is_some()); + assert_eq!(out.as_dgram_ref().unwrap().len(), PATH_MTU_V6); + qdebug!("Output={:0x?}", out.as_dgram_ref()); + + qdebug!("---- server: CH -> SH, EE, CERT, CV, FIN"); + let mut server = default_server(); + let out_to_rep = server.process(out.as_dgram_ref(), now()); + assert!(out_to_rep.as_dgram_ref().is_some()); + qdebug!("Output={:0x?}", out_to_rep.as_dgram_ref()); + + qdebug!("---- client: cert verification"); + let out = client.process(Some(out_to_rep.as_dgram_ref().unwrap()), now()); + assert!(out.as_dgram_ref().is_some()); + qdebug!("Output={:0x?}", out.as_dgram_ref()); + + let out = server.process(out.as_dgram_ref(), now()); + assert!(out.as_dgram_ref().is_none()); + + assert!(maybe_authenticate(&mut client)); + + qdebug!("---- client: SH..FIN -> FIN"); + let out = client.process(None, now()); + assert!(out.as_dgram_ref().is_some()); + qdebug!("Output={:0x?}", out.as_dgram_ref()); + + assert_eq!(3, client.stats().packets_rx); + assert_eq!(0, client.stats().dups_rx); + assert_eq!(1, client.stats().dropped_rx); + + qdebug!("---- Dup, ignored"); + let out = client.process(out_to_rep.as_dgram_ref(), now()); + assert!(out.as_dgram_ref().is_none()); + qdebug!("Output={:0x?}", out.as_dgram_ref()); + + // Four packets total received, 1 of them is a dup and one has been dropped because Initial keys + // are dropped. Add 2 counts of the padding that the server adds to Initial packets. + assert_eq!(6, client.stats().packets_rx); + assert_eq!(1, client.stats().dups_rx); + assert_eq!(3, client.stats().dropped_rx); +} + +// Test that we split crypto data if they cannot fit into one packet. +// To test this we will use a long server certificate. +#[test] +fn crypto_frame_split() { + let mut client = default_client(); + + let mut server = Connection::new_server( + test_fixture::LONG_CERT_KEYS, + test_fixture::DEFAULT_ALPN, + Rc::new(RefCell::new(CountingConnectionIdGenerator::default())), + ConnectionParameters::default(), + ) + .expect("create a server"); + + let client1 = client.process(None, now()); + assert!(client1.as_dgram_ref().is_some()); + + // The entire server flight doesn't fit in a single packet because the + // certificate is large, therefore the server will produce 2 packets. + let server1 = server.process(client1.as_dgram_ref(), now()); + assert!(server1.as_dgram_ref().is_some()); + let server2 = server.process(None, now()); + assert!(server2.as_dgram_ref().is_some()); + + let client2 = client.process(server1.as_dgram_ref(), now()); + // This is an ack. + assert!(client2.as_dgram_ref().is_some()); + // The client might have the certificate now, so we can't guarantee that + // this will work. + let auth1 = maybe_authenticate(&mut client); + assert_eq!(*client.state(), State::Handshaking); + + // let server process the ack for the first packet. + let server3 = server.process(client2.as_dgram_ref(), now()); + assert!(server3.as_dgram_ref().is_none()); + + // Consume the second packet from the server. + let client3 = client.process(server2.as_dgram_ref(), now()); + + // Check authentication. + let auth2 = maybe_authenticate(&mut client); + assert!(auth1 ^ auth2); + // Now client has all data to finish handshake. + assert_eq!(*client.state(), State::Connected); + + let client4 = client.process(server3.as_dgram_ref(), now()); + // One of these will contain data depending on whether Authentication was completed + // after the first or second server packet. + assert!(client3.as_dgram_ref().is_some() ^ client4.as_dgram_ref().is_some()); + + mem::drop(server.process(client3.as_dgram_ref(), now())); + mem::drop(server.process(client4.as_dgram_ref(), now())); + + assert_eq!(*client.state(), State::Connected); + assert_eq!(*server.state(), State::Confirmed); +} + +/// Run a single ChaCha20-Poly1305 test and get a PTO. +#[test] +fn chacha20poly1305() { + let mut server = default_server(); + let mut client = Connection::new_client( + test_fixture::DEFAULT_SERVER_NAME, + test_fixture::DEFAULT_ALPN, + Rc::new(RefCell::new(EmptyConnectionIdGenerator::default())), + addr(), + addr(), + ConnectionParameters::default(), + now(), + ) + .expect("create a default client"); + client.set_ciphers(&[TLS_CHACHA20_POLY1305_SHA256]).unwrap(); + connect_force_idle(&mut client, &mut server); +} + +/// Test that a server can send 0.5 RTT application data. +#[test] +fn send_05rtt() { + let mut client = default_client(); + let mut server = default_server(); + + let c1 = client.process(None, now()).dgram(); + assert!(c1.is_some()); + let s1 = server.process(c1.as_ref(), now()).dgram().unwrap(); + assert_eq!(s1.len(), PATH_MTU_V6); + + // The server should accept writes at this point. + let s2 = send_something(&mut server, now()); + + // Complete the handshake at the client. + client.process_input(&s1, now()); + maybe_authenticate(&mut client); + assert_eq!(*client.state(), State::Connected); + + // The client should receive the 0.5-RTT data now. + client.process_input(&s2, now()); + let mut buf = vec![0; DEFAULT_STREAM_DATA.len() + 1]; + let stream_id = client + .events() + .find_map(|e| { + if let ConnectionEvent::RecvStreamReadable { stream_id } = e { + Some(stream_id) + } else { + None + } + }) + .unwrap(); + let (l, ended) = client.stream_recv(stream_id, &mut buf).unwrap(); + assert_eq!(&buf[..l], DEFAULT_STREAM_DATA); + assert!(ended); +} + +/// Test that a client buffers 0.5-RTT data when it arrives early. +#[test] +fn reorder_05rtt() { + let mut client = default_client(); + let mut server = default_server(); + + let c1 = client.process(None, now()).dgram(); + assert!(c1.is_some()); + let s1 = server.process(c1.as_ref(), now()).dgram().unwrap(); + + // The server should accept writes at this point. + let s2 = send_something(&mut server, now()); + + // We can't use the standard facility to complete the handshake, so + // drive it as aggressively as possible. + client.process_input(&s2, now()); + assert_eq!(client.stats().saved_datagrams, 1); + + // After processing the first packet, the client should go back and + // process the 0.5-RTT packet data, which should make data available. + client.process_input(&s1, now()); + // We can't use `maybe_authenticate` here as that consumes events. + client.authenticated(AuthenticationStatus::Ok, now()); + assert_eq!(*client.state(), State::Connected); + + let mut buf = vec![0; DEFAULT_STREAM_DATA.len() + 1]; + let stream_id = client + .events() + .find_map(|e| { + if let ConnectionEvent::RecvStreamReadable { stream_id } = e { + Some(stream_id) + } else { + None + } + }) + .unwrap(); + let (l, ended) = client.stream_recv(stream_id, &mut buf).unwrap(); + assert_eq!(&buf[..l], DEFAULT_STREAM_DATA); + assert!(ended); +} + +#[test] +fn reorder_05rtt_with_0rtt() { + const RTT: Duration = Duration::from_millis(100); + + let mut client = default_client(); + let mut server = default_server(); + let validation = AddressValidation::new(now(), ValidateAddress::NoToken).unwrap(); + let validation = Rc::new(RefCell::new(validation)); + server.set_validation(Rc::clone(&validation)); + let mut now = connect_with_rtt(&mut client, &mut server, now(), RTT); + + // Include RTT in sending the ticket or the ticket age reported by the + // client is wrong, which causes the server to reject 0-RTT. + now += RTT / 2; + server.send_ticket(now, &[]).unwrap(); + let ticket = server.process_output(now).dgram().unwrap(); + now += RTT / 2; + client.process_input(&ticket, now); + + let token = get_tokens(&mut client).pop().unwrap(); + let mut client = default_client(); + client.enable_resumption(now, token).unwrap(); + let mut server = resumed_server(&client); + + // Send ClientHello and some 0-RTT. + let c1 = send_something(&mut client, now); + assertions::assert_coalesced_0rtt(&c1[..]); + // Drop the 0-RTT from the coalesced datagram, so that the server + // acknowledges the next 0-RTT packet. + let (c1, _) = split_datagram(&c1); + let c2 = send_something(&mut client, now); + + // Handle the first packet and send 0.5-RTT in response. Drop the response. + now += RTT / 2; + mem::drop(server.process(Some(&c1), now).dgram().unwrap()); + // The gap in 0-RTT will result in this 0.5 RTT containing an ACK. + server.process_input(&c2, now); + let s2 = send_something(&mut server, now); + + // Save the 0.5 RTT. + now += RTT / 2; + client.process_input(&s2, now); + assert_eq!(client.stats().saved_datagrams, 1); + + // Now PTO at the client and cause the server to re-send handshake packets. + now += AT_LEAST_PTO; + let c3 = client.process(None, now).dgram(); + assert_coalesced_0rtt(c3.as_ref().unwrap()); + + now += RTT / 2; + let s3 = server.process(c3.as_ref(), now).dgram().unwrap(); + + // The client should be able to process the 0.5 RTT now. + // This should contain an ACK, so we are processing an ACK from the past. + now += RTT / 2; + client.process_input(&s3, now); + maybe_authenticate(&mut client); + let c4 = client.process(None, now).dgram(); + assert_eq!(*client.state(), State::Connected); + assert_eq!(client.paths.rtt(), RTT); + + now += RTT / 2; + server.process_input(&c4.unwrap(), now); + assert_eq!(*server.state(), State::Confirmed); + // Don't check server RTT as it will be massively inflated by a + // poor initial estimate received when the server dropped the + // Initial packet number space. +} + +/// Test that a server that coalesces 0.5 RTT with handshake packets +/// doesn't cause the client to drop application data. +#[test] +fn coalesce_05rtt() { + const RTT: Duration = Duration::from_millis(100); + let mut client = default_client(); + let mut server = default_server(); + let mut now = now(); + + // The first exchange doesn't offer a chance for the server to send. + // So drop the server flight and wait for the PTO. + let c1 = client.process(None, now).dgram(); + assert!(c1.is_some()); + now += RTT / 2; + let s1 = server.process(c1.as_ref(), now).dgram(); + assert!(s1.is_some()); + + // Drop the server flight. Then send some data. + let stream_id = server.stream_create(StreamType::UniDi).unwrap(); + assert!(server.stream_send(stream_id, DEFAULT_STREAM_DATA).is_ok()); + assert!(server.stream_close_send(stream_id).is_ok()); + + // Now after a PTO the client can send another packet. + // The server should then send its entire flight again, + // including the application data, which it sends in a 1-RTT packet. + now += AT_LEAST_PTO; + let c2 = client.process(None, now).dgram(); + assert!(c2.is_some()); + now += RTT / 2; + let s2 = server.process(c2.as_ref(), now).dgram(); + // Even though there is a 1-RTT packet at the end of the datagram, the + // flight should be padded to full size. + assert_eq!(s2.as_ref().unwrap().len(), PATH_MTU_V6); + + // The client should process the datagram. It can't process the 1-RTT + // packet until authentication completes though. So it saves it. + now += RTT / 2; + assert_eq!(client.stats().dropped_rx, 0); + mem::drop(client.process(s2.as_ref(), now).dgram()); + // This packet will contain an ACK, but we can ignore it. + assert_eq!(client.stats().dropped_rx, 0); + assert_eq!(client.stats().packets_rx, 3); + assert_eq!(client.stats().saved_datagrams, 1); + + // After (successful) authentication, the packet is processed. + maybe_authenticate(&mut client); + let c3 = client.process(None, now).dgram(); + assert!(c3.is_some()); + assert_eq!(client.stats().dropped_rx, 0); // No Initial padding. + assert_eq!(client.stats().packets_rx, 4); + assert_eq!(client.stats().saved_datagrams, 1); + assert_eq!(client.stats().frame_rx.padding, 1); // Padding uses frames. + + // Allow the handshake to complete. + now += RTT / 2; + let s3 = server.process(c3.as_ref(), now).dgram(); + assert!(s3.is_some()); + assert_eq!(*server.state(), State::Confirmed); + now += RTT / 2; + mem::drop(client.process(s3.as_ref(), now).dgram()); + assert_eq!(*client.state(), State::Confirmed); + + assert_eq!(client.stats().dropped_rx, 0); // No dropped packets. +} + +#[test] +fn reorder_handshake() { + const RTT: Duration = Duration::from_millis(100); + let mut client = default_client(); + let mut server = default_server(); + let mut now = now(); + + let c1 = client.process(None, now).dgram(); + assert!(c1.is_some()); + + now += RTT / 2; + let s1 = server.process(c1.as_ref(), now).dgram(); + assert!(s1.is_some()); + + // Drop the Initial packet from this. + let (_, s_hs) = split_datagram(&s1.unwrap()); + assert!(s_hs.is_some()); + + // Pass just the handshake packet in and the client can't handle it yet. + // It can only send another Initial packet. + now += RTT / 2; + let dgram = client.process(s_hs.as_ref(), now).dgram(); + assertions::assert_initial(dgram.as_ref().unwrap(), false); + assert_eq!(client.stats().saved_datagrams, 1); + assert_eq!(client.stats().packets_rx, 1); + + // Get the server to try again. + // Though we currently allow the server to arm its PTO timer, use + // a second client Initial packet to cause it to send again. + now += AT_LEAST_PTO; + let c2 = client.process(None, now).dgram(); + now += RTT / 2; + let s2 = server.process(c2.as_ref(), now).dgram(); + assert!(s2.is_some()); + + let (s_init, s_hs) = split_datagram(&s2.unwrap()); + assert!(s_hs.is_some()); + + // Processing the Handshake packet first should save it. + now += RTT / 2; + client.process_input(&s_hs.unwrap(), now); + assert_eq!(client.stats().saved_datagrams, 2); + assert_eq!(client.stats().packets_rx, 2); + + client.process_input(&s_init, now); + // Each saved packet should now be "received" again. + assert_eq!(client.stats().packets_rx, 7); + maybe_authenticate(&mut client); + let c3 = client.process(None, now).dgram(); + assert!(c3.is_some()); + + // Note that though packets were saved and processed very late, + // they don't cause the RTT to change. + now += RTT / 2; + let s3 = server.process(c3.as_ref(), now).dgram(); + assert_eq!(*server.state(), State::Confirmed); + // Don't check server RTT estimate as it will be inflated due to + // it making a guess based on retransmissions when it dropped + // the Initial packet number space. + + now += RTT / 2; + client.process_input(&s3.unwrap(), now); + assert_eq!(*client.state(), State::Confirmed); + assert_eq!(client.paths.rtt(), RTT); +} + +#[test] +fn reorder_1rtt() { + const RTT: Duration = Duration::from_millis(100); + const PACKETS: usize = 4; // Many, but not enough to overflow cwnd. + let mut client = default_client(); + let mut server = default_server(); + let mut now = now(); + + let c1 = client.process(None, now).dgram(); + assert!(c1.is_some()); + + now += RTT / 2; + let s1 = server.process(c1.as_ref(), now).dgram(); + assert!(s1.is_some()); + + now += RTT / 2; + client.process_input(&s1.unwrap(), now); + maybe_authenticate(&mut client); + let c2 = client.process(None, now).dgram(); + assert!(c2.is_some()); + + // Now get a bunch of packets from the client. + // Give them to the server before giving it `c2`. + for _ in 0..PACKETS { + let d = send_something(&mut client, now); + server.process_input(&d, now + RTT / 2); + } + // The server has now received those packets, and saved them. + // The two extra received are Initial + the junk we use for padding. + assert_eq!(server.stats().packets_rx, PACKETS + 2); + assert_eq!(server.stats().saved_datagrams, PACKETS); + assert_eq!(server.stats().dropped_rx, 1); + + now += RTT / 2; + let s2 = server.process(c2.as_ref(), now).dgram(); + // The server has now received those packets, and saved them. + // The two additional are a Handshake and a 1-RTT (w/ NEW_CONNECTION_ID). + assert_eq!(server.stats().packets_rx, PACKETS * 2 + 4); + assert_eq!(server.stats().saved_datagrams, PACKETS); + assert_eq!(server.stats().dropped_rx, 1); + assert_eq!(*server.state(), State::Confirmed); + assert_eq!(server.paths.rtt(), RTT); + + now += RTT / 2; + client.process_input(&s2.unwrap(), now); + assert_eq!(client.paths.rtt(), RTT); + + // All the stream data that was sent should now be available. + let streams = server + .events() + .filter_map(|e| { + if let ConnectionEvent::RecvStreamReadable { stream_id } = e { + Some(stream_id) + } else { + None + } + }) + .collect::<Vec<_>>(); + assert_eq!(streams.len(), PACKETS); + for stream_id in streams { + let mut buf = vec![0; DEFAULT_STREAM_DATA.len() + 1]; + let (recvd, fin) = server.stream_recv(stream_id, &mut buf).unwrap(); + assert_eq!(recvd, DEFAULT_STREAM_DATA.len()); + assert!(fin); + } +} + +#[cfg(not(feature = "fuzzing"))] +#[test] +fn corrupted_initial() { + let mut client = default_client(); + let mut server = default_server(); + let d = client.process(None, now()).dgram().unwrap(); + let mut corrupted = Vec::from(&d[..]); + // Find the last non-zero value and corrupt that. + let (idx, _) = corrupted + .iter() + .enumerate() + .rev() + .find(|(_, &v)| v != 0) + .unwrap(); + corrupted[idx] ^= 0x76; + let dgram = Datagram::new(d.source(), d.destination(), d.tos(), d.ttl(), corrupted); + server.process_input(&dgram, now()); + // The server should have received two packets, + // the first should be dropped, the second saved. + assert_eq!(server.stats().packets_rx, 2); + assert_eq!(server.stats().dropped_rx, 2); + assert_eq!(server.stats().saved_datagrams, 0); +} + +#[test] +// Absent path PTU discovery, max v6 packet size should be PATH_MTU_V6. +fn verify_pkt_honors_mtu() { + let mut client = default_client(); + let mut server = default_server(); + connect_force_idle(&mut client, &mut server); + + let now = now(); + + let res = client.process(None, now); + let idle_timeout = ConnectionParameters::default().get_idle_timeout(); + assert_eq!(res, Output::Callback(idle_timeout)); + + // Try to send a large stream and verify first packet is correctly sized + let stream_id = client.stream_create(StreamType::UniDi).unwrap(); + assert_eq!(client.stream_send(stream_id, &[0xbb; 2000]).unwrap(), 2000); + let pkt0 = client.process(None, now); + assert!(matches!(pkt0, Output::Datagram(_))); + assert_eq!(pkt0.as_dgram_ref().unwrap().len(), PATH_MTU_V6); +} + +#[test] +fn extra_initial_hs() { + let mut client = default_client(); + let mut server = default_server(); + let mut now = now(); + + let c_init = client.process(None, now).dgram(); + assert!(c_init.is_some()); + now += DEFAULT_RTT / 2; + let s_init = server.process(c_init.as_ref(), now).dgram(); + assert!(s_init.is_some()); + now += DEFAULT_RTT / 2; + + // Drop the Initial packet, keep only the Handshake. + let (_, undecryptable) = split_datagram(&s_init.unwrap()); + assert!(undecryptable.is_some()); + + // Feed the same undecryptable packet into the client a few times. + // Do that EXTRA_INITIALS times and each time the client will emit + // another Initial packet. + for _ in 0..=super::super::EXTRA_INITIALS { + let c_init = client.process(undecryptable.as_ref(), now).dgram(); + assertions::assert_initial(c_init.as_ref().unwrap(), false); + now += DEFAULT_RTT / 10; + } + + // After EXTRA_INITIALS, the client stops sending Initial packets. + let nothing = client.process(undecryptable.as_ref(), now).dgram(); + assert!(nothing.is_none()); + + // Until PTO, where another Initial can be used to complete the handshake. + now += AT_LEAST_PTO; + let c_init = client.process(None, now).dgram(); + assertions::assert_initial(c_init.as_ref().unwrap(), false); + now += DEFAULT_RTT / 2; + let s_init = server.process(c_init.as_ref(), now).dgram(); + now += DEFAULT_RTT / 2; + client.process_input(&s_init.unwrap(), now); + maybe_authenticate(&mut client); + let c_fin = client.process_output(now).dgram(); + assert_eq!(*client.state(), State::Connected); + now += DEFAULT_RTT / 2; + server.process_input(&c_fin.unwrap(), now); + assert_eq!(*server.state(), State::Confirmed); +} + +#[test] +fn extra_initial_invalid_cid() { + let mut client = default_client(); + let mut server = default_server(); + let mut now = now(); + + let c_init = client.process(None, now).dgram(); + assert!(c_init.is_some()); + now += DEFAULT_RTT / 2; + let s_init = server.process(c_init.as_ref(), now).dgram(); + assert!(s_init.is_some()); + now += DEFAULT_RTT / 2; + + // If the client receives a packet that contains the wrong connection + // ID, it won't send another Initial. + let (_, hs) = split_datagram(&s_init.unwrap()); + let hs = hs.unwrap(); + let mut copy = hs.to_vec(); + assert_ne!(copy[5], 0); // The DCID should be non-zero length. + copy[6] ^= 0xc4; + let dgram_copy = Datagram::new(hs.destination(), hs.source(), hs.tos(), hs.ttl(), copy); + let nothing = client.process(Some(&dgram_copy), now).dgram(); + assert!(nothing.is_none()); +} + +#[test] +fn connect_one_version() { + fn connect_v(version: Version) { + fixture_init(); + let mut client = Connection::new_client( + test_fixture::DEFAULT_SERVER_NAME, + test_fixture::DEFAULT_ALPN, + Rc::new(RefCell::new(CountingConnectionIdGenerator::default())), + addr(), + addr(), + ConnectionParameters::default().versions(version, vec![version]), + now(), + ) + .unwrap(); + let mut server = Connection::new_server( + test_fixture::DEFAULT_KEYS, + test_fixture::DEFAULT_ALPN, + Rc::new(RefCell::new(CountingConnectionIdGenerator::default())), + ConnectionParameters::default().versions(version, vec![version]), + ) + .unwrap(); + connect_force_idle(&mut client, &mut server); + assert_eq!(client.version(), version); + assert_eq!(server.version(), version); + } + + for v in Version::all() { + println!("Connecting with {v:?}"); + connect_v(v); + } +} + +#[test] +fn anti_amplification() { + let mut client = default_client(); + let mut server = default_server(); + let mut now = now(); + + // With a gigantic transport parameter, the server is unable to complete + // the handshake within the amplification limit. + let very_big = TransportParameter::Bytes(vec![0; PATH_MTU_V6 * 3]); + server.set_local_tparam(0xce16, very_big).unwrap(); + + let c_init = client.process_output(now).dgram(); + now += DEFAULT_RTT / 2; + let s_init1 = server.process(c_init.as_ref(), now).dgram().unwrap(); + assert_eq!(s_init1.len(), PATH_MTU_V6); + let s_init2 = server.process_output(now).dgram().unwrap(); + assert_eq!(s_init2.len(), PATH_MTU_V6); + + // Skip the gap for pacing here. + let s_pacing = server.process_output(now).callback(); + assert_ne!(s_pacing, Duration::new(0, 0)); + now += s_pacing; + + let s_init3 = server.process_output(now).dgram().unwrap(); + assert_eq!(s_init3.len(), PATH_MTU_V6); + let cb = server.process_output(now).callback(); + assert_ne!(cb, Duration::new(0, 0)); + + now += DEFAULT_RTT / 2; + client.process_input(&s_init1, now); + client.process_input(&s_init2, now); + let ack_count = client.stats().frame_tx.ack; + let frame_count = client.stats().frame_tx.all; + let ack = client.process(Some(&s_init3), now).dgram().unwrap(); + assert!(!maybe_authenticate(&mut client)); // No need yet. + + // The client sends a padded datagram, with just ACK for Handshake. + assert_eq!(client.stats().frame_tx.ack, ack_count + 1); + assert_eq!(client.stats().frame_tx.all, frame_count + 1); + assert_ne!(ack.len(), PATH_MTU_V6); // Not padded (it includes Handshake). + + now += DEFAULT_RTT / 2; + let remainder = server.process(Some(&ack), now).dgram(); + + now += DEFAULT_RTT / 2; + client.process_input(&remainder.unwrap(), now); + assert!(maybe_authenticate(&mut client)); // OK, we have all of it. + let fin = client.process_output(now).dgram(); + assert_eq!(*client.state(), State::Connected); + + now += DEFAULT_RTT / 2; + server.process_input(&fin.unwrap(), now); + assert_eq!(*server.state(), State::Confirmed); +} + +#[cfg(not(feature = "fuzzing"))] +#[test] +fn garbage_initial() { + let mut client = default_client(); + let mut server = default_server(); + + let dgram = client.process_output(now()).dgram().unwrap(); + let (initial, rest) = split_datagram(&dgram); + let mut corrupted = Vec::from(&initial[..initial.len() - 1]); + corrupted.push(initial[initial.len() - 1] ^ 0xb7); + corrupted.extend_from_slice(rest.as_ref().map_or(&[], |r| &r[..])); + let garbage = datagram(corrupted); + assert_eq!(Output::None, server.process(Some(&garbage), now())); +} + +#[test] +fn drop_initial_packet_from_wrong_address() { + let mut client = default_client(); + let out = client.process(None, now()); + assert!(out.as_dgram_ref().is_some()); + + let mut server = default_server(); + let out = server.process(out.as_dgram_ref(), now()); + assert!(out.as_dgram_ref().is_some()); + + let p = out.dgram().unwrap(); + let dgram = Datagram::new( + SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 2)), 443), + p.destination(), + p.tos(), + p.ttl(), + &p[..], + ); + + let out = client.process(Some(&dgram), now()); + assert!(out.as_dgram_ref().is_none()); +} + +#[test] +fn drop_handshake_packet_from_wrong_address() { + let mut client = default_client(); + let out = client.process(None, now()); + assert!(out.as_dgram_ref().is_some()); + + let mut server = default_server(); + let out = server.process(out.as_dgram_ref(), now()); + assert!(out.as_dgram_ref().is_some()); + + let (s_in, s_hs) = split_datagram(&out.dgram().unwrap()); + + // Pass the initial packet. + mem::drop(client.process(Some(&s_in), now()).dgram()); + + let p = s_hs.unwrap(); + let dgram = Datagram::new( + SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 2)), 443), + p.destination(), + p.tos(), + p.ttl(), + &p[..], + ); + + let out = client.process(Some(&dgram), now()); + assert!(out.as_dgram_ref().is_none()); +} + +#[test] +fn ech() { + let mut server = default_server(); + let (sk, pk) = generate_ech_keys().unwrap(); + server + .server_enable_ech(ECH_CONFIG_ID, ECH_PUBLIC_NAME, &sk, &pk) + .unwrap(); + + let mut client = default_client(); + client.client_enable_ech(server.ech_config()).unwrap(); + + connect(&mut client, &mut server); + + assert!(client.tls_info().unwrap().ech_accepted()); + assert!(server.tls_info().unwrap().ech_accepted()); + assert!(client.tls_preinfo().unwrap().ech_accepted().unwrap()); + assert!(server.tls_preinfo().unwrap().ech_accepted().unwrap()); +} + +fn damaged_ech_config(config: &[u8]) -> Vec<u8> { + let mut cfg = Vec::from(config); + // Ensure that the version and config_id is correct. + assert_eq!(cfg[2], 0xfe); + assert_eq!(cfg[3], 0x0d); + assert_eq!(cfg[6], ECH_CONFIG_ID); + // Change the config_id so that the server doesn't recognize it. + cfg[6] ^= 0x94; + cfg +} + +#[test] +fn ech_retry() { + fixture_init(); + let mut server = default_server(); + let (sk, pk) = generate_ech_keys().unwrap(); + server + .server_enable_ech(ECH_CONFIG_ID, ECH_PUBLIC_NAME, &sk, &pk) + .unwrap(); + + let mut client = default_client(); + client + .client_enable_ech(&damaged_ech_config(server.ech_config())) + .unwrap(); + + let dgram = client.process_output(now()).dgram(); + let dgram = server.process(dgram.as_ref(), now()).dgram(); + client.process_input(&dgram.unwrap(), now()); + let auth_event = ConnectionEvent::EchFallbackAuthenticationNeeded { + public_name: String::from(ECH_PUBLIC_NAME), + }; + assert!(client.events().any(|e| e == auth_event)); + client.authenticated(AuthenticationStatus::Ok, now()); + assert!(client.state().error().is_some()); + + // Tell the server about the error. + let dgram = client.process_output(now()).dgram(); + server.process_input(&dgram.unwrap(), now()); + assert_eq!( + server.state().error(), + Some(&ConnectionError::Transport(Error::PeerError(0x100 + 121))) + ); + + let Some(ConnectionError::Transport(Error::EchRetry(updated_config))) = client.state().error() + else { + panic!( + "Client state should be failed with EchRetry, is {:?}", + client.state() + ); + }; + + let mut server = default_server(); + server + .server_enable_ech(ECH_CONFIG_ID, ECH_PUBLIC_NAME, &sk, &pk) + .unwrap(); + let mut client = default_client(); + client.client_enable_ech(updated_config).unwrap(); + + connect(&mut client, &mut server); + + assert!(client.tls_info().unwrap().ech_accepted()); + assert!(server.tls_info().unwrap().ech_accepted()); + assert!(client.tls_preinfo().unwrap().ech_accepted().unwrap()); + assert!(server.tls_preinfo().unwrap().ech_accepted().unwrap()); +} + +#[test] +fn ech_retry_fallback_rejected() { + fixture_init(); + let mut server = default_server(); + let (sk, pk) = generate_ech_keys().unwrap(); + server + .server_enable_ech(ECH_CONFIG_ID, ECH_PUBLIC_NAME, &sk, &pk) + .unwrap(); + + let mut client = default_client(); + client + .client_enable_ech(&damaged_ech_config(server.ech_config())) + .unwrap(); + + let dgram = client.process_output(now()).dgram(); + let dgram = server.process(dgram.as_ref(), now()).dgram(); + client.process_input(&dgram.unwrap(), now()); + let auth_event = ConnectionEvent::EchFallbackAuthenticationNeeded { + public_name: String::from(ECH_PUBLIC_NAME), + }; + assert!(client.events().any(|e| e == auth_event)); + client.authenticated(AuthenticationStatus::PolicyRejection, now()); + assert!(client.state().error().is_some()); + + if let Some(ConnectionError::Transport(Error::EchRetry(_))) = client.state().error() { + panic!("Client should not get EchRetry error"); + } + + // Pass the error on. + let dgram = client.process_output(now()).dgram(); + server.process_input(&dgram.unwrap(), now()); + assert_eq!( + server.state().error(), + Some(&ConnectionError::Transport(Error::PeerError(298))) + ); // A bad_certificate alert. +} + +#[test] +fn bad_min_ack_delay() { + const EXPECTED_ERROR: ConnectionError = + ConnectionError::Transport(Error::TransportParameterError); + let mut server = default_server(); + let max_ad = u64::try_from(DEFAULT_ACK_DELAY.as_micros()).unwrap(); + server + .set_local_tparam(MIN_ACK_DELAY, TransportParameter::Integer(max_ad + 1)) + .unwrap(); + let mut client = default_client(); + + let dgram = client.process_output(now()).dgram(); + let dgram = server.process(dgram.as_ref(), now()).dgram(); + client.process_input(&dgram.unwrap(), now()); + client.authenticated(AuthenticationStatus::Ok, now()); + assert_eq!(client.state().error(), Some(&EXPECTED_ERROR)); + let dgram = client.process_output(now()).dgram(); + + server.process_input(&dgram.unwrap(), now()); + assert_eq!( + server.state().error(), + Some(&ConnectionError::Transport(Error::PeerError( + Error::TransportParameterError.code() + ))) + ); +} + +/// Ensure that the client probes correctly if it only receives Initial packets +/// from the server. +#[test] +fn only_server_initial() { + let mut server = default_server(); + let mut client = default_client(); + let mut now = now(); + + let client_dgram = client.process_output(now).dgram(); + + // Now fetch two flights of messages from the server. + let server_dgram1 = server.process(client_dgram.as_ref(), now).dgram(); + let server_dgram2 = server.process_output(now + AT_LEAST_PTO).dgram(); + + // Only pass on the Initial from the first. We should get a Handshake in return. + let (initial, handshake) = split_datagram(&server_dgram1.unwrap()); + assert!(handshake.is_some()); + + // The client will not acknowledge the Initial as it discards keys. + // It sends a Handshake probe instead, containing just a PING frame. + assert_eq!(client.stats().frame_tx.ping, 0); + let probe = client.process(Some(&initial), now).dgram(); + assertions::assert_handshake(&probe.unwrap()); + assert_eq!(client.stats().dropped_rx, 0); + assert_eq!(client.stats().frame_tx.ping, 1); + + let (initial, handshake) = split_datagram(&server_dgram2.unwrap()); + assert!(handshake.is_some()); + + // The same happens after a PTO, even though the client will discard the Initial packet. + now += AT_LEAST_PTO; + assert_eq!(client.stats().frame_tx.ping, 1); + let discarded = client.stats().dropped_rx; + let probe = client.process(Some(&initial), now).dgram(); + assertions::assert_handshake(&probe.unwrap()); + assert_eq!(client.stats().frame_tx.ping, 2); + assert_eq!(client.stats().dropped_rx, discarded + 1); + + // Pass the Handshake packet and complete the handshake. + client.process_input(&handshake.unwrap(), now); + maybe_authenticate(&mut client); + let dgram = client.process_output(now).dgram(); + let dgram = server.process(dgram.as_ref(), now).dgram(); + client.process_input(&dgram.unwrap(), now); + + assert_eq!(*client.state(), State::Confirmed); + assert_eq!(*server.state(), State::Confirmed); +} + +// Collect a few spare Initial packets as the handshake is exchanged. +// Later, replay those packets to see if they result in additional probes; they should not. +#[test] +fn no_extra_probes_after_confirmed() { + let mut server = default_server(); + let mut client = default_client(); + let mut now = now(); + + // First, collect a client Initial. + let spare_initial = client.process_output(now).dgram(); + assert!(spare_initial.is_some()); + + // Collect ANOTHER client Initial. + now += AT_LEAST_PTO; + let dgram = client.process_output(now).dgram(); + let (replay_initial, _) = split_datagram(dgram.as_ref().unwrap()); + + // Finally, run the handshake. + now += AT_LEAST_PTO * 2; + let dgram = client.process_output(now).dgram(); + let dgram = server.process(dgram.as_ref(), now).dgram(); + + // The server should have dropped the Initial keys now, so passing in the Initial + // should elicit a retransmit rather than having it completely ignored. + let spare_handshake = server.process(Some(&replay_initial), now).dgram(); + assert!(spare_handshake.is_some()); + + client.process_input(&dgram.unwrap(), now); + maybe_authenticate(&mut client); + let dgram = client.process_output(now).dgram(); + let dgram = server.process(dgram.as_ref(), now).dgram(); + client.process_input(&dgram.unwrap(), now); + + assert_eq!(*client.state(), State::Confirmed); + assert_eq!(*server.state(), State::Confirmed); + + let probe = server.process(spare_initial.as_ref(), now).dgram(); + assert!(probe.is_none()); + let probe = client.process(spare_handshake.as_ref(), now).dgram(); + assert!(probe.is_none()); +} + +#[test] +fn implicit_rtt_server() { + const RTT: Duration = Duration::from_secs(2); + let mut server = default_server(); + let mut client = default_client(); + let mut now = now(); + + let dgram = client.process_output(now).dgram(); + now += RTT / 2; + let dgram = server.process(dgram.as_ref(), now).dgram(); + now += RTT / 2; + let dgram = client.process(dgram.as_ref(), now).dgram(); + assertions::assert_handshake(dgram.as_ref().unwrap()); + now += RTT / 2; + server.process_input(&dgram.unwrap(), now); + + // The server doesn't receive any acknowledgments, but it can infer + // an RTT estimate from having discarded the Initial packet number space. + assert_eq!(server.stats().rtt, RTT); +} |