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/migration.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/migration.rs')
-rw-r--r-- | third_party/rust/neqo-transport/src/connection/tests/migration.rs | 953 |
1 files changed, 953 insertions, 0 deletions
diff --git a/third_party/rust/neqo-transport/src/connection/tests/migration.rs b/third_party/rust/neqo-transport/src/connection/tests/migration.rs new file mode 100644 index 0000000000..8307a7dd84 --- /dev/null +++ b/third_party/rust/neqo-transport/src/connection/tests/migration.rs @@ -0,0 +1,953 @@ +// 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, + net::{IpAddr, Ipv6Addr, SocketAddr}, + rc::Rc, + time::{Duration, Instant}, +}; + +use neqo_common::{Datagram, Decoder}; +use test_fixture::{ + self, addr, addr_v4, + assertions::{assert_v4_path, assert_v6_path}, + fixture_init, new_neqo_qlog, now, +}; + +use super::{ + super::{Connection, Output, State, StreamType}, + connect_fail, connect_force_idle, connect_rtt_idle, default_client, default_server, + maybe_authenticate, new_client, new_server, send_something, CountingConnectionIdGenerator, +}; +use crate::{ + cid::LOCAL_ACTIVE_CID_LIMIT, + connection::tests::send_something_paced, + frame::FRAME_TYPE_NEW_CONNECTION_ID, + packet::PacketBuilder, + path::{PATH_MTU_V4, PATH_MTU_V6}, + tparams::{self, PreferredAddress, TransportParameter}, + ConnectionError, ConnectionId, ConnectionIdDecoder, ConnectionIdGenerator, ConnectionIdRef, + ConnectionParameters, EmptyConnectionIdGenerator, Error, +}; + +/// This should be a valid-seeming transport parameter. +/// And it should have different values to `addr` and `addr_v4`. +const SAMPLE_PREFERRED_ADDRESS: &[u8] = &[ + 0xc0, 0x00, 0x02, 0x02, 0x01, 0xbb, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0xbb, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, +]; + +// These tests generally use two paths: +// The connection is established on a path with the same IPv6 address on both ends. +// Migrations move to a path with the same IPv4 address on both ends. +// This simplifies validation as the same assertions can be used for client and server. +// The risk is that there is a place where source/destination local/remote is inverted. + +fn loopback() -> SocketAddr { + SocketAddr::new(IpAddr::V6(Ipv6Addr::from(1)), 443) +} + +fn change_path(d: &Datagram, a: SocketAddr) -> Datagram { + Datagram::new(a, a, d.tos(), d.ttl(), &d[..]) +} + +fn new_port(a: SocketAddr) -> SocketAddr { + let (port, _) = a.port().overflowing_add(410); + SocketAddr::new(a.ip(), port) +} + +fn change_source_port(d: &Datagram) -> Datagram { + Datagram::new( + new_port(d.source()), + d.destination(), + d.tos(), + d.ttl(), + &d[..], + ) +} + +/// As these tests use a new path, that path often has a non-zero RTT. +/// Pacing can be a problem when testing that path. This skips time forward. +fn skip_pacing(c: &mut Connection, now: Instant) -> Instant { + let pacing = c.process_output(now).callback(); + assert_ne!(pacing, Duration::new(0, 0)); + now + pacing +} + +#[test] +fn rebinding_port() { + let mut client = default_client(); + let mut server = default_server(); + connect_force_idle(&mut client, &mut server); + + let dgram = send_something(&mut client, now()); + let dgram = change_source_port(&dgram); + + server.process_input(&dgram, now()); + // Have the server send something so that it generates a packet. + let stream_id = server.stream_create(StreamType::UniDi).unwrap(); + server.stream_close_send(stream_id).unwrap(); + let dgram = server.process_output(now()).dgram(); + let dgram = dgram.unwrap(); + assert_eq!(dgram.source(), addr()); + assert_eq!(dgram.destination(), new_port(addr())); +} + +/// This simulates an attack where a valid packet is forwarded on +/// a different path. This shows how both paths are probed and the +/// server eventually returns to the original path. +#[test] +fn path_forwarding_attack() { + let mut client = default_client(); + let mut server = default_server(); + connect_force_idle(&mut client, &mut server); + let mut now = now(); + + let dgram = send_something(&mut client, now); + let dgram = change_path(&dgram, addr_v4()); + server.process_input(&dgram, now); + + // The server now probes the new (primary) path. + let new_probe = server.process_output(now).dgram().unwrap(); + assert_eq!(server.stats().frame_tx.path_challenge, 1); + assert_v4_path(&new_probe, false); // Can't be padded. + + // The server also probes the old path. + let old_probe = server.process_output(now).dgram().unwrap(); + assert_eq!(server.stats().frame_tx.path_challenge, 2); + assert_v6_path(&old_probe, true); + + // New data from the server is sent on the new path, but that is + // now constrained by the amplification limit. + let stream_id = server.stream_create(StreamType::UniDi).unwrap(); + server.stream_close_send(stream_id).unwrap(); + assert!(server.process_output(now).dgram().is_none()); + + // The client should respond to the challenge on the new path. + // The server couldn't pad, so the client is also amplification limited. + let new_resp = client.process(Some(&new_probe), now).dgram().unwrap(); + assert_eq!(client.stats().frame_rx.path_challenge, 1); + assert_eq!(client.stats().frame_tx.path_challenge, 1); + assert_eq!(client.stats().frame_tx.path_response, 1); + assert_v4_path(&new_resp, false); + + // The client also responds to probes on the old path. + let old_resp = client.process(Some(&old_probe), now).dgram().unwrap(); + assert_eq!(client.stats().frame_rx.path_challenge, 2); + assert_eq!(client.stats().frame_tx.path_challenge, 1); + assert_eq!(client.stats().frame_tx.path_response, 2); + assert_v6_path(&old_resp, true); + + // But the client still sends data on the old path. + let client_data1 = send_something(&mut client, now); + assert_v6_path(&client_data1, false); // Just data. + + // Receiving the PATH_RESPONSE from the client opens the amplification + // limit enough for the server to respond. + // This is padded because it includes PATH_CHALLENGE. + let server_data1 = server.process(Some(&new_resp), now).dgram().unwrap(); + assert_v4_path(&server_data1, true); + assert_eq!(server.stats().frame_tx.path_challenge, 3); + + // The client responds to this probe on the new path. + client.process_input(&server_data1, now); + let stream_before = client.stats().frame_tx.stream; + let padded_resp = send_something(&mut client, now); + assert_eq!(stream_before, client.stats().frame_tx.stream); + assert_v4_path(&padded_resp, true); // This is padded! + + // But new data from the client stays on the old path. + let client_data2 = client.process_output(now).dgram().unwrap(); + assert_v6_path(&client_data2, false); + + // The server keeps sending on the new path. + now = skip_pacing(&mut server, now); + let server_data2 = send_something(&mut server, now); + assert_v4_path(&server_data2, false); + + // Until new data is received from the client on the old path. + server.process_input(&client_data2, now); + // The server sends a probe on the "old" path. + let server_data3 = send_something(&mut server, now); + assert_v4_path(&server_data3, true); + // But switches data transmission to the "new" path. + let server_data4 = server.process_output(now).dgram().unwrap(); + assert_v6_path(&server_data4, false); +} + +#[test] +fn migrate_immediate() { + let mut client = default_client(); + let mut server = default_server(); + connect_force_idle(&mut client, &mut server); + let now = now(); + + client + .migrate(Some(addr_v4()), Some(addr_v4()), true, now) + .unwrap(); + + let client1 = send_something(&mut client, now); + assert_v4_path(&client1, true); // Contains PATH_CHALLENGE. + let client2 = send_something(&mut client, now); + assert_v4_path(&client2, false); // Doesn't. + + let server_delayed = send_something(&mut server, now); + + // The server accepts the first packet and migrates (but probes). + let server1 = server.process(Some(&client1), now).dgram().unwrap(); + assert_v4_path(&server1, true); + let server2 = server.process_output(now).dgram().unwrap(); + assert_v6_path(&server2, true); + + // The second packet has no real effect, it just elicits an ACK. + let all_before = server.stats().frame_tx.all; + let ack_before = server.stats().frame_tx.ack; + let server3 = server.process(Some(&client2), now).dgram(); + assert!(server3.is_some()); + assert_eq!(server.stats().frame_tx.all, all_before + 1); + assert_eq!(server.stats().frame_tx.ack, ack_before + 1); + + // Receiving a packet sent by the server before migration doesn't change path. + client.process_input(&server_delayed, now); + // The client has sent two unpaced packets and this new path has no RTT estimate + // so this might be paced. + let (client3, _t) = send_something_paced(&mut client, now, true); + assert_v4_path(&client3, false); +} + +/// RTT estimates for paths should be preserved across migrations. +#[test] +fn migrate_rtt() { + const RTT: Duration = Duration::from_millis(20); + let mut client = default_client(); + let mut server = default_server(); + let now = connect_rtt_idle(&mut client, &mut server, RTT); + + client + .migrate(Some(addr_v4()), Some(addr_v4()), true, now) + .unwrap(); + // The RTT might be increased for the new path, so allow a little flexibility. + let rtt = client.paths.rtt(); + assert!(rtt > RTT); + assert!(rtt < RTT * 2); +} + +#[test] +fn migrate_immediate_fail() { + let mut client = default_client(); + let mut server = default_server(); + connect_force_idle(&mut client, &mut server); + let mut now = now(); + + client + .migrate(Some(addr_v4()), Some(addr_v4()), true, now) + .unwrap(); + + let probe = client.process_output(now).dgram().unwrap(); + assert_v4_path(&probe, true); // Contains PATH_CHALLENGE. + + for _ in 0..2 { + let cb = client.process_output(now).callback(); + assert_ne!(cb, Duration::new(0, 0)); + now += cb; + + let before = client.stats().frame_tx; + let probe = client.process_output(now).dgram().unwrap(); + assert_v4_path(&probe, true); // Contains PATH_CHALLENGE. + let after = client.stats().frame_tx; + assert_eq!(after.path_challenge, before.path_challenge + 1); + assert_eq!(after.padding, before.padding + 1); + assert_eq!(after.all, before.all + 2); + + // This might be a PTO, which will result in sending a probe. + if let Some(probe) = client.process_output(now).dgram() { + assert_v4_path(&probe, false); // Contains PATH_CHALLENGE. + let after = client.stats().frame_tx; + assert_eq!(after.ping, before.ping + 1); + assert_eq!(after.all, before.all + 3); + } + } + + let pto = client.process_output(now).callback(); + assert_ne!(pto, Duration::new(0, 0)); + now += pto; + + // The client should fall back to the original path and retire the connection ID. + let fallback = client.process_output(now).dgram(); + assert_v6_path(&fallback.unwrap(), false); + assert_eq!(client.stats().frame_tx.retire_connection_id, 1); +} + +/// Migrating to the same path shouldn't do anything special, +/// except that the path is probed. +#[test] +fn migrate_same() { + let mut client = default_client(); + let mut server = default_server(); + connect_force_idle(&mut client, &mut server); + let now = now(); + + client + .migrate(Some(addr()), Some(addr()), true, now) + .unwrap(); + + let probe = client.process_output(now).dgram().unwrap(); + assert_v6_path(&probe, true); // Contains PATH_CHALLENGE. + assert_eq!(client.stats().frame_tx.path_challenge, 1); + + let resp = server.process(Some(&probe), now).dgram().unwrap(); + assert_v6_path(&resp, true); + assert_eq!(server.stats().frame_tx.path_response, 1); + assert_eq!(server.stats().frame_tx.path_challenge, 0); + + // Everything continues happily. + client.process_input(&resp, now); + let contd = send_something(&mut client, now); + assert_v6_path(&contd, false); +} + +/// Migrating to the same path, if it fails, causes the connection to fail. +#[test] +fn migrate_same_fail() { + let mut client = default_client(); + let mut server = default_server(); + connect_force_idle(&mut client, &mut server); + let mut now = now(); + + client + .migrate(Some(addr()), Some(addr()), true, now) + .unwrap(); + + let probe = client.process_output(now).dgram().unwrap(); + assert_v6_path(&probe, true); // Contains PATH_CHALLENGE. + + for _ in 0..2 { + let cb = client.process_output(now).callback(); + assert_ne!(cb, Duration::new(0, 0)); + now += cb; + + let before = client.stats().frame_tx; + let probe = client.process_output(now).dgram().unwrap(); + assert_v6_path(&probe, true); // Contains PATH_CHALLENGE. + let after = client.stats().frame_tx; + assert_eq!(after.path_challenge, before.path_challenge + 1); + assert_eq!(after.padding, before.padding + 1); + assert_eq!(after.all, before.all + 2); + + // This might be a PTO, which will result in sending a probe. + if let Some(probe) = client.process_output(now).dgram() { + assert_v6_path(&probe, false); // Contains PATH_CHALLENGE. + let after = client.stats().frame_tx; + assert_eq!(after.ping, before.ping + 1); + assert_eq!(after.all, before.all + 3); + } + } + + let pto = client.process_output(now).callback(); + assert_ne!(pto, Duration::new(0, 0)); + now += pto; + + // The client should mark this path as failed and close immediately. + let res = client.process_output(now); + assert!(matches!(res, Output::None)); + assert!(matches!( + client.state(), + State::Closed(ConnectionError::Transport(Error::NoAvailablePath)) + )); +} + +/// This gets the connection ID from a datagram using the default +/// connection ID generator/decoder. +fn get_cid(d: &Datagram) -> ConnectionIdRef { + let gen = CountingConnectionIdGenerator::default(); + assert_eq!(d[0] & 0x80, 0); // Only support short packets for now. + gen.decode_cid(&mut Decoder::from(&d[1..])).unwrap() +} + +fn migration(mut client: Connection) { + let mut server = default_server(); + connect_force_idle(&mut client, &mut server); + let now = now(); + + client + .migrate(Some(addr_v4()), Some(addr_v4()), false, now) + .unwrap(); + + let probe = client.process_output(now).dgram().unwrap(); + assert_v4_path(&probe, true); // Contains PATH_CHALLENGE. + assert_eq!(client.stats().frame_tx.path_challenge, 1); + let probe_cid = ConnectionId::from(get_cid(&probe)); + + let resp = server.process(Some(&probe), now).dgram().unwrap(); + assert_v4_path(&resp, true); + assert_eq!(server.stats().frame_tx.path_response, 1); + assert_eq!(server.stats().frame_tx.path_challenge, 1); + + // Data continues to be exchanged on the new path. + let client_data = send_something(&mut client, now); + assert_ne!(get_cid(&client_data), probe_cid); + assert_v6_path(&client_data, false); + server.process_input(&client_data, now); + let server_data = send_something(&mut server, now); + assert_v6_path(&server_data, false); + + // Once the client receives the probe response, it migrates to the new path. + client.process_input(&resp, now); + assert_eq!(client.stats().frame_rx.path_challenge, 1); + let migrate_client = send_something(&mut client, now); + assert_v4_path(&migrate_client, true); // Responds to server probe. + + // The server now sees the migration and will switch over. + // However, it will probe the old path again, even though it has just + // received a response to its last probe, because it needs to verify + // that the migration is genuine. + server.process_input(&migrate_client, now); + let stream_before = server.stats().frame_tx.stream; + let probe_old_server = send_something(&mut server, now); + // This is just the double-check probe; no STREAM frames. + assert_v6_path(&probe_old_server, true); + assert_eq!(server.stats().frame_tx.path_challenge, 2); + assert_eq!(server.stats().frame_tx.stream, stream_before); + + // The server then sends data on the new path. + let migrate_server = server.process_output(now).dgram().unwrap(); + assert_v4_path(&migrate_server, false); + assert_eq!(server.stats().frame_tx.path_challenge, 2); + assert_eq!(server.stats().frame_tx.stream, stream_before + 1); + + // The client receives these checks and responds to the probe, but uses the new path. + client.process_input(&migrate_server, now); + client.process_input(&probe_old_server, now); + let old_probe_resp = send_something(&mut client, now); + assert_v6_path(&old_probe_resp, true); + let client_confirmation = client.process_output(now).dgram().unwrap(); + assert_v4_path(&client_confirmation, false); + + // The server has now sent 2 packets, so it is blocked on the pacer. Wait. + let server_pacing = server.process_output(now).callback(); + assert_ne!(server_pacing, Duration::new(0, 0)); + // ... then confirm that the server sends on the new path still. + let server_confirmation = send_something(&mut server, now + server_pacing); + assert_v4_path(&server_confirmation, false); +} + +#[test] +fn migration_graceful() { + migration(default_client()); +} + +/// A client should be able to migrate when it has a zero-length connection ID. +#[test] +fn migration_client_empty_cid() { + fixture_init(); + let client = Connection::new_client( + test_fixture::DEFAULT_SERVER_NAME, + test_fixture::DEFAULT_ALPN, + Rc::new(RefCell::new(EmptyConnectionIdGenerator::default())), + addr(), + addr(), + ConnectionParameters::default(), + now(), + ) + .unwrap(); + migration(client); +} + +/// Drive the handshake in the most expeditious fashion. +/// Returns the packet containing `HANDSHAKE_DONE` from the server. +fn fast_handshake(client: &mut Connection, server: &mut Connection) -> Option<Datagram> { + let dgram = client.process_output(now()).dgram(); + let dgram = server.process(dgram.as_ref(), now()).dgram(); + client.process_input(&dgram.unwrap(), now()); + assert!(maybe_authenticate(client)); + let dgram = client.process_output(now()).dgram(); + server.process(dgram.as_ref(), now()).dgram() +} + +fn preferred_address(hs_client: SocketAddr, hs_server: SocketAddr, preferred: SocketAddr) { + let mtu = match hs_client.ip() { + IpAddr::V4(_) => PATH_MTU_V4, + IpAddr::V6(_) => PATH_MTU_V6, + }; + let assert_orig_path = |d: &Datagram, full_mtu: bool| { + assert_eq!( + d.destination(), + if d.source() == hs_client { + hs_server + } else if d.source() == hs_server { + hs_client + } else { + panic!(); + } + ); + if full_mtu { + assert_eq!(d.len(), mtu); + } + }; + let assert_toward_spa = |d: &Datagram, full_mtu: bool| { + assert_eq!(d.destination(), preferred); + assert_eq!(d.source(), hs_client); + if full_mtu { + assert_eq!(d.len(), mtu); + } + }; + let assert_from_spa = |d: &Datagram, full_mtu: bool| { + assert_eq!(d.destination(), hs_client); + assert_eq!(d.source(), preferred); + if full_mtu { + assert_eq!(d.len(), mtu); + } + }; + + fixture_init(); + let (log, _contents) = new_neqo_qlog(); + let mut client = Connection::new_client( + test_fixture::DEFAULT_SERVER_NAME, + test_fixture::DEFAULT_ALPN, + Rc::new(RefCell::new(EmptyConnectionIdGenerator::default())), + hs_client, + hs_server, + ConnectionParameters::default(), + now(), + ) + .unwrap(); + client.set_qlog(log); + let spa = match preferred { + SocketAddr::V6(v6) => PreferredAddress::new(None, Some(v6)), + SocketAddr::V4(v4) => PreferredAddress::new(Some(v4), None), + }; + let mut server = new_server(ConnectionParameters::default().preferred_address(spa)); + + let dgram = fast_handshake(&mut client, &mut server); + + // The client is about to process HANDSHAKE_DONE. + // It should start probing toward the server's preferred address. + let probe = client.process(dgram.as_ref(), now()).dgram().unwrap(); + assert_toward_spa(&probe, true); + assert_eq!(client.stats().frame_tx.path_challenge, 1); + assert_ne!(client.process_output(now()).callback(), Duration::new(0, 0)); + + // Data continues on the main path for the client. + let data = send_something(&mut client, now()); + assert_orig_path(&data, false); + + // The server responds to the probe. + let resp = server.process(Some(&probe), now()).dgram().unwrap(); + assert_from_spa(&resp, true); + assert_eq!(server.stats().frame_tx.path_challenge, 1); + assert_eq!(server.stats().frame_tx.path_response, 1); + + // Data continues on the main path for the server. + server.process_input(&data, now()); + let data = send_something(&mut server, now()); + assert_orig_path(&data, false); + + // Client gets the probe response back and it migrates. + client.process_input(&resp, now()); + client.process_input(&data, now()); + let data = send_something(&mut client, now()); + assert_toward_spa(&data, true); + assert_eq!(client.stats().frame_tx.stream, 2); + assert_eq!(client.stats().frame_tx.path_response, 1); + + // The server sees the migration and probes the old path. + let probe = server.process(Some(&data), now()).dgram().unwrap(); + assert_orig_path(&probe, true); + assert_eq!(server.stats().frame_tx.path_challenge, 2); + + // But data now goes on the new path. + let data = send_something(&mut server, now()); + assert_from_spa(&data, false); +} + +/// Migration works for a new port number. +#[test] +fn preferred_address_new_port() { + let a = addr(); + preferred_address(a, a, new_port(a)); +} + +/// Migration works for a new address too. +#[test] +fn preferred_address_new_address() { + let mut preferred = addr(); + preferred.set_ip(IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 2))); + preferred_address(addr(), addr(), preferred); +} + +/// Migration works for IPv4 addresses. +#[test] +fn preferred_address_new_port_v4() { + let a = addr_v4(); + preferred_address(a, a, new_port(a)); +} + +/// Migrating to a loopback address is OK if we started there. +#[test] +fn preferred_address_loopback() { + let a = loopback(); + preferred_address(a, a, new_port(a)); +} + +fn expect_no_migration(client: &mut Connection, server: &mut Connection) { + let dgram = fast_handshake(client, server); + + // The client won't probe now, though it could; it remains idle. + let out = client.process(dgram.as_ref(), now()); + assert_ne!(out.callback(), Duration::new(0, 0)); + + // Data continues on the main path for the client. + let data = send_something(client, now()); + assert_v6_path(&data, false); + assert_eq!(client.stats().frame_tx.path_challenge, 0); +} + +fn preferred_address_ignored(spa: PreferredAddress) { + let mut client = default_client(); + let mut server = new_server(ConnectionParameters::default().preferred_address(spa)); + + expect_no_migration(&mut client, &mut server); +} + +/// Using a loopback address in the preferred address is ignored. +#[test] +fn preferred_address_ignore_loopback() { + preferred_address_ignored(PreferredAddress::new_any(None, Some(loopback()))); +} + +/// A preferred address in the wrong address family is ignored. +#[test] +fn preferred_address_ignore_different_family() { + preferred_address_ignored(PreferredAddress::new_any(Some(addr_v4()), None)); +} + +/// Disabling preferred addresses at the client means that it ignores a perfectly +/// good preferred address. +#[test] +fn preferred_address_disabled_client() { + let mut client = new_client(ConnectionParameters::default().disable_preferred_address()); + let mut preferred = addr(); + preferred.set_ip(IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 2))); + let spa = PreferredAddress::new_any(None, Some(preferred)); + let mut server = new_server(ConnectionParameters::default().preferred_address(spa)); + + expect_no_migration(&mut client, &mut server); +} + +#[test] +fn preferred_address_empty_cid() { + fixture_init(); + + let spa = PreferredAddress::new_any(None, Some(new_port(addr()))); + let res = Connection::new_server( + test_fixture::DEFAULT_KEYS, + test_fixture::DEFAULT_ALPN, + Rc::new(RefCell::new(EmptyConnectionIdGenerator::default())), + ConnectionParameters::default().preferred_address(spa), + ); + assert_eq!(res.unwrap_err(), Error::ConnectionIdsExhausted); +} + +/// A server cannot include a preferred address if it chooses an empty connection ID. +#[test] +fn preferred_address_server_empty_cid() { + let mut client = default_client(); + let mut server = Connection::new_server( + test_fixture::DEFAULT_KEYS, + test_fixture::DEFAULT_ALPN, + Rc::new(RefCell::new(EmptyConnectionIdGenerator::default())), + ConnectionParameters::default(), + ) + .unwrap(); + + server + .set_local_tparam( + tparams::PREFERRED_ADDRESS, + TransportParameter::Bytes(SAMPLE_PREFERRED_ADDRESS.to_vec()), + ) + .unwrap(); + + connect_fail( + &mut client, + &mut server, + Error::TransportParameterError, + Error::PeerError(Error::TransportParameterError.code()), + ); +} + +/// A client shouldn't send a preferred address transport parameter. +#[test] +fn preferred_address_client() { + let mut client = default_client(); + let mut server = default_server(); + + client + .set_local_tparam( + tparams::PREFERRED_ADDRESS, + TransportParameter::Bytes(SAMPLE_PREFERRED_ADDRESS.to_vec()), + ) + .unwrap(); + + connect_fail( + &mut client, + &mut server, + Error::PeerError(Error::TransportParameterError.code()), + Error::TransportParameterError, + ); +} + +/// Test that migration isn't permitted if the connection isn't in the right state. +#[test] +fn migration_invalid_state() { + let mut client = default_client(); + assert!(client + .migrate(Some(addr()), Some(addr()), false, now()) + .is_err()); + + let mut server = default_server(); + assert!(server + .migrate(Some(addr()), Some(addr()), false, now()) + .is_err()); + connect_force_idle(&mut client, &mut server); + + assert!(server + .migrate(Some(addr()), Some(addr()), false, now()) + .is_err()); + + client.close(now(), 0, "closing"); + assert!(client + .migrate(Some(addr()), Some(addr()), false, now()) + .is_err()); + let close = client.process(None, now()).dgram(); + + let dgram = server.process(close.as_ref(), now()).dgram(); + assert!(server + .migrate(Some(addr()), Some(addr()), false, now()) + .is_err()); + + client.process_input(&dgram.unwrap(), now()); + assert!(client + .migrate(Some(addr()), Some(addr()), false, now()) + .is_err()); +} + +#[test] +fn migration_invalid_address() { + let mut client = default_client(); + let mut server = default_server(); + connect_force_idle(&mut client, &mut server); + + let mut cant_migrate = |local, remote| { + assert_eq!( + client.migrate(local, remote, true, now()).unwrap_err(), + Error::InvalidMigration + ); + }; + + // Providing neither address is pointless and therefore an error. + cant_migrate(None, None); + + // Providing a zero port number isn't valid. + let mut zero_port = addr(); + zero_port.set_port(0); + cant_migrate(None, Some(zero_port)); + cant_migrate(Some(zero_port), None); + + // An unspecified remote address is bad. + let mut remote_unspecified = addr(); + remote_unspecified.set_ip(IpAddr::V6(Ipv6Addr::from(0))); + cant_migrate(None, Some(remote_unspecified)); + + // Mixed address families is bad. + cant_migrate(Some(addr()), Some(addr_v4())); + cant_migrate(Some(addr_v4()), Some(addr())); + + // Loopback to non-loopback is bad. + cant_migrate(Some(addr()), Some(loopback())); + cant_migrate(Some(loopback()), Some(addr())); + assert_eq!( + client + .migrate(Some(addr()), Some(loopback()), true, now()) + .unwrap_err(), + Error::InvalidMigration + ); + assert_eq!( + client + .migrate(Some(loopback()), Some(addr()), true, now()) + .unwrap_err(), + Error::InvalidMigration + ); +} + +/// This inserts a frame into packets that provides a single new +/// connection ID and retires all others. +struct RetireAll { + cid_gen: Rc<RefCell<dyn ConnectionIdGenerator>>, +} + +impl crate::connection::test_internal::FrameWriter for RetireAll { + fn write_frames(&mut self, builder: &mut PacketBuilder) { + // Use a sequence number that is large enough that all existing values + // will be lower (so they get retired). As the code doesn't care about + // gaps in sequence numbers, this is safe, even though the gap might + // hint that there are more outstanding connection IDs that are allowed. + const SEQNO: u64 = 100; + let cid = self.cid_gen.borrow_mut().generate_cid().unwrap(); + builder + .encode_varint(FRAME_TYPE_NEW_CONNECTION_ID) + .encode_varint(SEQNO) + .encode_varint(SEQNO) // Retire Prior To + .encode_vec(1, &cid) + .encode(&[0x7f; 16]); + } +} + +/// Test that forcing retirement of connection IDs forces retirement of all active +/// connection IDs and the use of of newer one. +#[test] +fn retire_all() { + let mut client = default_client(); + let cid_gen: Rc<RefCell<dyn ConnectionIdGenerator>> = + Rc::new(RefCell::new(CountingConnectionIdGenerator::default())); + let mut server = Connection::new_server( + test_fixture::DEFAULT_KEYS, + test_fixture::DEFAULT_ALPN, + Rc::clone(&cid_gen), + ConnectionParameters::default(), + ) + .unwrap(); + connect_force_idle(&mut client, &mut server); + + let original_cid = ConnectionId::from(get_cid(&send_something(&mut client, now()))); + + server.test_frame_writer = Some(Box::new(RetireAll { cid_gen })); + let ncid = send_something(&mut server, now()); + server.test_frame_writer = None; + + let new_cid_before = client.stats().frame_rx.new_connection_id; + let retire_cid_before = client.stats().frame_tx.retire_connection_id; + client.process_input(&ncid, now()); + let retire = send_something(&mut client, now()); + assert_eq!( + client.stats().frame_rx.new_connection_id, + new_cid_before + 1 + ); + assert_eq!( + client.stats().frame_tx.retire_connection_id, + retire_cid_before + LOCAL_ACTIVE_CID_LIMIT + ); + + assert_ne!(get_cid(&retire), original_cid); +} + +/// During a graceful migration, if the probed path can't get a new connection ID due +/// to being forced to retire the one it is using, the migration will fail. +#[test] +fn retire_prior_to_migration_failure() { + let mut client = default_client(); + let cid_gen: Rc<RefCell<dyn ConnectionIdGenerator>> = + Rc::new(RefCell::new(CountingConnectionIdGenerator::default())); + let mut server = Connection::new_server( + test_fixture::DEFAULT_KEYS, + test_fixture::DEFAULT_ALPN, + Rc::clone(&cid_gen), + ConnectionParameters::default(), + ) + .unwrap(); + connect_force_idle(&mut client, &mut server); + + let original_cid = ConnectionId::from(get_cid(&send_something(&mut client, now()))); + + client + .migrate(Some(addr_v4()), Some(addr_v4()), false, now()) + .unwrap(); + + // The client now probes the new path. + let probe = client.process_output(now()).dgram().unwrap(); + assert_v4_path(&probe, true); + assert_eq!(client.stats().frame_tx.path_challenge, 1); + let probe_cid = ConnectionId::from(get_cid(&probe)); + assert_ne!(original_cid, probe_cid); + + // Have the server receive the probe, but separately have it decide to + // retire all of the available connection IDs. + server.test_frame_writer = Some(Box::new(RetireAll { cid_gen })); + let retire_all = send_something(&mut server, now()); + server.test_frame_writer = None; + + let resp = server.process(Some(&probe), now()).dgram().unwrap(); + assert_v4_path(&resp, true); + assert_eq!(server.stats().frame_tx.path_response, 1); + assert_eq!(server.stats().frame_tx.path_challenge, 1); + + // Have the client receive the NEW_CONNECTION_ID with Retire Prior To. + client.process_input(&retire_all, now()); + // This packet contains the probe response, which should be fine, but it + // also includes PATH_CHALLENGE for the new path, and the client can't + // respond without a connection ID. We treat this as a connection error. + client.process_input(&resp, now()); + assert!(matches!( + client.state(), + State::Closing { + error: ConnectionError::Transport(Error::InvalidMigration), + .. + } + )); +} + +/// The timing of when frames arrive can mean that the migration path can +/// get the last available connection ID. +#[test] +fn retire_prior_to_migration_success() { + let mut client = default_client(); + let cid_gen: Rc<RefCell<dyn ConnectionIdGenerator>> = + Rc::new(RefCell::new(CountingConnectionIdGenerator::default())); + let mut server = Connection::new_server( + test_fixture::DEFAULT_KEYS, + test_fixture::DEFAULT_ALPN, + Rc::clone(&cid_gen), + ConnectionParameters::default(), + ) + .unwrap(); + connect_force_idle(&mut client, &mut server); + + let original_cid = ConnectionId::from(get_cid(&send_something(&mut client, now()))); + + client + .migrate(Some(addr_v4()), Some(addr_v4()), false, now()) + .unwrap(); + + // The client now probes the new path. + let probe = client.process_output(now()).dgram().unwrap(); + assert_v4_path(&probe, true); + assert_eq!(client.stats().frame_tx.path_challenge, 1); + let probe_cid = ConnectionId::from(get_cid(&probe)); + assert_ne!(original_cid, probe_cid); + + // Have the server receive the probe, but separately have it decide to + // retire all of the available connection IDs. + server.test_frame_writer = Some(Box::new(RetireAll { cid_gen })); + let retire_all = send_something(&mut server, now()); + server.test_frame_writer = None; + + let resp = server.process(Some(&probe), now()).dgram().unwrap(); + assert_v4_path(&resp, true); + assert_eq!(server.stats().frame_tx.path_response, 1); + assert_eq!(server.stats().frame_tx.path_challenge, 1); + + // Have the client receive the NEW_CONNECTION_ID with Retire Prior To second. + // As this occurs in a very specific order, migration succeeds. + client.process_input(&resp, now()); + client.process_input(&retire_all, now()); + + // Migration succeeds and the new path gets the last connection ID. + let dgram = send_something(&mut client, now()); + assert_v4_path(&dgram, false); + assert_ne!(get_cid(&dgram), original_cid); + assert_ne!(get_cid(&dgram), probe_cid); +} |