Source code
Revision control
Copy as Markdown
Other Tools
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use std::time::Duration;
use neqo_common::{Datagram, IpTos, IpTosEcn};
use test_fixture::{
assertions::{assert_v4_path, assert_v6_path},
fixture_init, now, DEFAULT_ADDR_V4,
};
use crate::{
connection::tests::{
connect_force_idle, connect_force_idle_with_modifier, default_client, default_server,
handshake_with_modifier, migration::get_cid, new_client, new_server, send_and_receive,
send_something, send_something_with_modifier, send_with_modifier_and_receive, DEFAULT_RTT,
},
ecn::ECN_TEST_COUNT,
path::MAX_PATH_PROBES,
ConnectionId, ConnectionParameters, StreamType,
};
fn assert_ecn_enabled(tos: IpTos) {
assert!(tos.is_ecn_marked());
}
fn assert_ecn_disabled(tos: IpTos) {
assert!(!tos.is_ecn_marked());
}
fn set_tos(mut d: Datagram, ecn: IpTosEcn) -> Datagram {
d.set_tos(ecn.into());
d
}
fn noop() -> fn(Datagram) -> Option<Datagram> {
Some
}
fn bleach() -> fn(Datagram) -> Option<Datagram> {
|d| Some(set_tos(d, IpTosEcn::NotEct))
}
fn remark() -> fn(Datagram) -> Option<Datagram> {
|d| {
if d.tos().is_ecn_marked() {
Some(set_tos(d, IpTosEcn::Ect1))
} else {
Some(d)
}
}
}
fn ce() -> fn(Datagram) -> Option<Datagram> {
|d| {
if d.tos().is_ecn_marked() {
Some(set_tos(d, IpTosEcn::Ce))
} else {
Some(d)
}
}
}
fn drop() -> fn(Datagram) -> Option<Datagram> {
|_| None
}
fn drop_ecn_marked_datagrams() -> fn(Datagram) -> Option<Datagram> {
|d| (!d.tos().is_ecn_marked()).then_some(d)
}
#[test]
fn handshake_delay_with_ecn_blackhole() {
let start = now();
let mut client = default_client();
let mut server = default_server();
let finish = handshake_with_modifier(
&mut client,
&mut server,
start,
DEFAULT_RTT,
drop_ecn_marked_datagrams(),
);
assert_eq!(
(finish - start).as_millis() / DEFAULT_RTT.as_millis(),
15,
"expected 6 RTT for client to detect blackhole, 6 RTT for server to detect blackhole and 3 RTT for handshake to be confirmed.",
);
}
#[test]
fn migration_delay_to_ecn_blackhole() {
let mut now = now();
let mut client = default_client();
let mut server = default_server();
// Do a handshake.
connect_force_idle(&mut client, &mut server);
// Migrate the client.
client
.migrate(Some(DEFAULT_ADDR_V4), Some(DEFAULT_ADDR_V4), false, now)
.unwrap();
// The client should send MAX_PATH_PROBES path challenges with ECN enabled, and then another
// MAX_PATH_PROBES without ECN.
let mut probes = 0;
while probes < MAX_PATH_PROBES * 2 {
match client.process_output(now) {
crate::Output::Callback(t) => {
now += t;
}
crate::Output::Datagram(d) => {
// The new path is IPv4.
if d.source().is_ipv4() {
// This should be a PATH_CHALLENGE.
probes += 1;
assert_eq!(client.stats().frame_tx.path_challenge, probes);
if probes <= MAX_PATH_PROBES {
// The first probes should be sent with ECN.
assert_ecn_enabled(d.tos());
} else {
// The next probes should be sent without ECN.
assert_ecn_disabled(d.tos());
}
}
}
crate::Output::None => panic!("unexpected output"),
}
}
}
#[test]
fn stats() {
let now = now();
let mut client = default_client();
let mut server = default_server();
connect_force_idle(&mut client, &mut server);
for _ in 0..ECN_TEST_COUNT {
let ack = send_and_receive(&mut client, &mut server, now);
client.process_input(&ack.unwrap(), now);
}
for _ in 0..ECN_TEST_COUNT {
let ack = send_and_receive(&mut server, &mut client, now);
server.process_input(&ack.unwrap(), now);
}
for stats in [client.stats(), server.stats()] {
assert_eq!(stats.ecn_paths_capable, 1);
assert_eq!(stats.ecn_paths_not_capable, 0);
for codepoint in [IpTosEcn::Ect1, IpTosEcn::Ce] {
assert_eq!(stats.ecn_tx[codepoint], 0);
assert_eq!(stats.ecn_rx[codepoint], 0);
}
}
assert!(client.stats().ecn_tx[IpTosEcn::Ect0] <= server.stats().ecn_rx[IpTosEcn::Ect0]);
assert!(server.stats().ecn_tx[IpTosEcn::Ect0] <= client.stats().ecn_rx[IpTosEcn::Ect0]);
}
#[test]
fn disables_on_loss() {
let now = now();
let mut client = default_client();
let mut server = default_server();
connect_force_idle(&mut client, &mut server);
// Right after the handshake, the ECN validation should still be in progress.
let client_pkt = send_something(&mut client, now);
assert_ecn_enabled(client_pkt.tos());
for _ in 0..ECN_TEST_COUNT {
send_something(&mut client, now);
}
// ECN should now be disabled.
let client_pkt = send_something(&mut client, now);
assert_ecn_disabled(client_pkt.tos());
}
#[test]
fn disables_on_remark() {
let now = now();
let mut client = default_client();
let mut server = default_server();
connect_force_idle(&mut client, &mut server);
for _ in 0..ECN_TEST_COUNT {
if let Some(ack) = send_with_modifier_and_receive(&mut client, &mut server, now, remark()) {
client.process_input(&ack, now);
}
}
// ECN should now be disabled.
let client_pkt = send_something(&mut client, now);
assert_ecn_disabled(client_pkt.tos());
}
/// This function performs a handshake over a path that modifies packets via `orig_path_modifier`.
/// It then sends `burst` packets on that path, and then migrates to a new path that
/// modifies packets via `new_path_modifier`. It sends `burst` packets on the new path.
/// The function returns the TOS value of the last packet sent on the old path and the TOS value
/// of the last packet sent on the new path to allow for verification of correct behavior.
pub fn migration_with_modifiers(
orig_path_modifier: fn(Datagram) -> Option<Datagram>,
new_path_modifier: fn(Datagram) -> Option<Datagram>,
burst: usize,
) -> (IpTos, IpTos, bool) {
fixture_init();
let mut client = new_client(ConnectionParameters::default().max_streams(StreamType::UniDi, 64));
let mut server = new_server(ConnectionParameters::default().max_streams(StreamType::UniDi, 64));
connect_force_idle_with_modifier(&mut client, &mut server, orig_path_modifier);
let mut now = now();
// Right after the handshake, the ECN validation should still be in progress.
let client_pkt = send_something(&mut client, now);
assert_ecn_enabled(client_pkt.tos());
server.process_input(&orig_path_modifier(client_pkt).unwrap(), now);
// Send some data on the current path.
for _ in 0..burst {
let client_pkt = send_something_with_modifier(&mut client, now, orig_path_modifier);
server.process_input(&client_pkt, now);
}
if let Some(ack) = server.process_output(now).dgram() {
client.process_input(&ack, now);
}
let client_pkt = send_something(&mut client, now);
let tos_before_migration = client_pkt.tos();
server.process_input(&orig_path_modifier(client_pkt).unwrap(), now);
client
.migrate(Some(DEFAULT_ADDR_V4), Some(DEFAULT_ADDR_V4), false, now)
.unwrap();
let mut migrated = false;
let probe = new_path_modifier(client.process_output(now).dgram().unwrap());
if let Some(probe) = probe {
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 = new_path_modifier(server.process(Some(&probe), now).dgram().unwrap()).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 old path.
let client_data = send_something_with_modifier(&mut client, now, orig_path_modifier);
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_with_modifier(&mut server, now, orig_path_modifier);
assert_v6_path(&server_data, false);
client.process_input(&server_data, now);
// 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);
migrated = true;
let migrate_client = send_something_with_modifier(&mut client, now, new_path_modifier);
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_with_modifier(&mut server, now, orig_path_modifier);
// This is just the double-check probe; no STREAM frames.
assert_v6_path(&probe_old_server, migrated);
assert_eq!(
server.stats().frame_tx.path_challenge,
if migrated { 2 } else { 0 }
);
assert_eq!(
server.stats().frame_tx.stream,
if migrated { stream_before } else { 1 }
);
if migrated {
// The server then sends data on the new path.
let migrate_server =
new_path_modifier(server.process_output(now).dgram().unwrap()).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_with_modifier(&mut client, now, new_path_modifier);
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_with_modifier(&mut server, now + server_pacing, new_path_modifier);
assert_v4_path(&server_confirmation, false);
client.process_input(&server_confirmation, now);
// Send some data on the new path.
for _ in 0..burst {
now += client.process_output(now).callback();
let client_pkt = send_something_with_modifier(&mut client, now, new_path_modifier);
server.process_input(&client_pkt, now);
}
if let Some(ack) = server.process_output(now).dgram() {
client.process_input(&ack, now);
}
}
now += client.process_output(now).callback();
let mut client_pkt = send_something(&mut client, now);
while !migrated && client_pkt.source() == DEFAULT_ADDR_V4 {
client_pkt = send_something(&mut client, now);
}
let tos_after_migration = client_pkt.tos();
(tos_before_migration, tos_after_migration, migrated)
}
#[test]
fn ecn_migration_zero_burst_all_cases() {
for orig_path_mod in &[noop(), bleach(), remark(), ce()] {
for new_path_mod in &[noop(), bleach(), remark(), ce(), drop()] {
let (before, after, migrated) =
migration_with_modifiers(*orig_path_mod, *new_path_mod, 0);
// Too few packets sent before and after migration to conclude ECN validation.
assert_ecn_enabled(before);
assert_ecn_enabled(after);
// Migration succeeds except if the new path drops ECN.
assert!(*new_path_mod == drop() || migrated);
}
}
}
#[test]
fn ecn_migration_noop_bleach_data() {
let (before, after, migrated) = migration_with_modifiers(noop(), bleach(), ECN_TEST_COUNT);
assert_ecn_enabled(before); // ECN validation concludes before migration.
assert_ecn_disabled(after); // ECN validation fails after migration due to bleaching.
assert!(migrated);
}
#[test]
fn ecn_migration_noop_remark_data() {
let (before, after, migrated) = migration_with_modifiers(noop(), remark(), ECN_TEST_COUNT);
assert_ecn_enabled(before); // ECN validation concludes before migration.
assert_ecn_disabled(after); // ECN validation fails after migration due to remarking.
assert!(migrated);
}
#[test]
fn ecn_migration_noop_ce_data() {
let (before, after, migrated) = migration_with_modifiers(noop(), ce(), ECN_TEST_COUNT);
assert_ecn_enabled(before); // ECN validation concludes before migration.
assert_ecn_enabled(after); // ECN validation concludes after migration, despite all CE marks.
assert!(migrated);
}
#[test]
fn ecn_migration_noop_drop_data() {
let (before, after, migrated) = migration_with_modifiers(noop(), drop(), ECN_TEST_COUNT);
assert_ecn_enabled(before); // ECN validation concludes before migration.
assert_ecn_enabled(after); // Migration failed, ECN on original path is still validated.
assert!(!migrated);
}
#[test]
fn ecn_migration_bleach_noop_data() {
let (before, after, migrated) = migration_with_modifiers(bleach(), noop(), ECN_TEST_COUNT);
assert_ecn_disabled(before); // ECN validation fails before migration due to bleaching.
assert_ecn_enabled(after); // ECN validation concludes after migration.
assert!(migrated);
}
#[test]
fn ecn_migration_bleach_bleach_data() {
let (before, after, migrated) = migration_with_modifiers(bleach(), bleach(), ECN_TEST_COUNT);
assert_ecn_disabled(before); // ECN validation fails before migration due to bleaching.
assert_ecn_disabled(after); // ECN validation fails after migration due to bleaching.
assert!(migrated);
}
#[test]
fn ecn_migration_bleach_remark_data() {
let (before, after, migrated) = migration_with_modifiers(bleach(), remark(), ECN_TEST_COUNT);
assert_ecn_disabled(before); // ECN validation fails before migration due to bleaching.
assert_ecn_disabled(after); // ECN validation fails after migration due to remarking.
assert!(migrated);
}
#[test]
fn ecn_migration_bleach_ce_data() {
let (before, after, migrated) = migration_with_modifiers(bleach(), ce(), ECN_TEST_COUNT);
assert_ecn_disabled(before); // ECN validation fails before migration due to bleaching.
assert_ecn_enabled(after); // ECN validation concludes after migration, despite all CE marks.
assert!(migrated);
}
#[test]
fn ecn_migration_bleach_drop_data() {
let (before, after, migrated) = migration_with_modifiers(bleach(), drop(), ECN_TEST_COUNT);
assert_ecn_disabled(before); // ECN validation fails before migration due to bleaching.
// Migration failed, ECN on original path is still disabled.
assert_ecn_disabled(after);
assert!(!migrated);
}
#[test]
fn ecn_migration_remark_noop_data() {
let (before, after, migrated) = migration_with_modifiers(remark(), noop(), ECN_TEST_COUNT);
assert_ecn_disabled(before); // ECN validation fails before migration due to remarking.
assert_ecn_enabled(after); // ECN validation succeeds after migration.
assert!(migrated);
}
#[test]
fn ecn_migration_remark_bleach_data() {
let (before, after, migrated) = migration_with_modifiers(remark(), bleach(), ECN_TEST_COUNT);
assert_ecn_disabled(before); // ECN validation fails before migration due to remarking.
assert_ecn_disabled(after); // ECN validation fails after migration due to bleaching.
assert!(migrated);
}
#[test]
fn ecn_migration_remark_remark_data() {
let (before, after, migrated) = migration_with_modifiers(remark(), remark(), ECN_TEST_COUNT);
assert_ecn_disabled(before); // ECN validation fails before migration due to remarking.
assert_ecn_disabled(after); // ECN validation fails after migration due to remarking.
assert!(migrated);
}
#[test]
fn ecn_migration_remark_ce_data() {
let (before, after, migrated) = migration_with_modifiers(remark(), ce(), ECN_TEST_COUNT);
assert_ecn_disabled(before); // ECN validation fails before migration due to remarking.
assert_ecn_enabled(after); // ECN validation concludes after migration, despite all CE marks.
assert!(migrated);
}
#[test]
fn ecn_migration_remark_drop_data() {
let (before, after, migrated) = migration_with_modifiers(remark(), drop(), ECN_TEST_COUNT);
assert_ecn_disabled(before); // ECN validation fails before migration due to remarking.
assert_ecn_disabled(after); // Migration failed, ECN on original path is still disabled.
assert!(!migrated);
}
#[test]
fn ecn_migration_ce_noop_data() {
let (before, after, migrated) = migration_with_modifiers(ce(), noop(), ECN_TEST_COUNT);
assert_ecn_enabled(before); // ECN validation concludes before migration, despite all CE marks.
assert_ecn_enabled(after); // ECN validation concludes after migration.
assert!(migrated);
}
#[test]
fn ecn_migration_ce_bleach_data() {
let (before, after, migrated) = migration_with_modifiers(ce(), bleach(), ECN_TEST_COUNT);
assert_ecn_enabled(before); // ECN validation concludes before migration, despite all CE marks.
assert_ecn_disabled(after); // ECN validation fails after migration due to bleaching
assert!(migrated);
}
#[test]
fn ecn_migration_ce_remark_data() {
let (before, after, migrated) = migration_with_modifiers(ce(), remark(), ECN_TEST_COUNT);
assert_ecn_enabled(before); // ECN validation concludes before migration, despite all CE marks.
assert_ecn_disabled(after); // ECN validation fails after migration due to remarking.
assert!(migrated);
}
#[test]
fn ecn_migration_ce_ce_data() {
let (before, after, migrated) = migration_with_modifiers(ce(), ce(), ECN_TEST_COUNT);
assert_ecn_enabled(before); // ECN validation concludes before migration, despite all CE marks.
assert_ecn_enabled(after); // ECN validation concludes after migration, despite all CE marks.
assert!(migrated);
}
#[test]
fn ecn_migration_ce_drop_data() {
let (before, after, migrated) = migration_with_modifiers(ce(), drop(), ECN_TEST_COUNT);
assert_ecn_enabled(before); // ECN validation concludes before migration, despite all CE marks.
// Migration failed, ECN on original path is still enabled.
assert_ecn_enabled(after);
assert!(!migrated);
}