use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
pub struct QuicMigrationConformanceHarness {
path_validator: MockPathValidator,
connection_id_manager: MockConnectionIdManager,
amplification_tracker: AmplificationTracker,
transport_params: TransportParams,
}
impl QuicMigrationConformanceHarness {
pub fn new() -> Self {
Self {
path_validator: MockPathValidator::new(),
connection_id_manager: MockConnectionIdManager::new(),
amplification_tracker: AmplificationTracker::new(),
transport_params: TransportParams::default(),
}
}
pub fn with_transport_params(mut self, params: TransportParams) -> Self {
self.transport_params = params;
self
}
}
#[derive(Debug)]
pub struct MockPathValidator {
pending_challenges: Arc<Mutex<HashMap<u64, PathChallenge>>>,
validated_paths: Arc<Mutex<Vec<SocketAddr>>>,
challenge_counter: Arc<Mutex<u64>>,
}
impl MockPathValidator {
pub fn new() -> Self {
Self {
pending_challenges: Arc::new(Mutex::new(HashMap::new())),
validated_paths: Arc::new(Mutex::new(Vec::new())),
challenge_counter: Arc::new(Mutex::new(0)),
}
}
pub fn send_path_challenge(&self, new_path: SocketAddr) -> u64 {
let mut counter = self.challenge_counter.lock().unwrap();
*counter += 1;
let challenge_id = *counter;
let mut pending = self.pending_challenges.lock().unwrap();
pending.insert(
challenge_id,
PathChallenge {
id: challenge_id,
path: new_path,
sent_at: Instant::now(),
data: generate_challenge_data(challenge_id),
},
);
challenge_id
}
pub fn process_path_response(&self, challenge_id: u64, response_data: &[u8]) -> bool {
let mut pending = self.pending_challenges.lock().unwrap();
if let Some(challenge) = pending.remove(&challenge_id) {
if response_data == &challenge.data {
let mut validated = self.validated_paths.lock().unwrap();
validated.push(challenge.path);
true
} else {
false
}
} else {
false
}
}
pub fn is_path_validated(&self, path: SocketAddr) -> bool {
let validated = self.validated_paths.lock().unwrap();
validated.contains(&path)
}
pub fn pending_challenge_count(&self) -> usize {
self.pending_challenges.lock().unwrap().len()
}
}
#[derive(Debug, Clone)]
pub struct PathChallenge {
pub id: u64,
pub path: SocketAddr,
pub sent_at: Instant,
pub data: [u8; 8],
}
fn generate_challenge_data(challenge_id: u64) -> [u8; 8] {
challenge_id.to_be_bytes()
}
#[derive(Debug)]
pub struct MockConnectionIdManager {
active_ids: Arc<Mutex<Vec<ConnectionId>>>,
retired_ids: Arc<Mutex<Vec<ConnectionId>>>,
id_counter: Arc<Mutex<u64>>,
}
impl MockConnectionIdManager {
pub fn new() -> Self {
Self {
active_ids: Arc::new(Mutex::new(Vec::new())),
retired_ids: Arc::new(Mutex::new(Vec::new())),
id_counter: Arc::new(Mutex::new(0)),
}
}
pub fn issue_connection_id(&self) -> ConnectionId {
let mut counter = self.id_counter.lock().unwrap();
*counter += 1;
let conn_id = ConnectionId {
sequence: *counter,
id: format!("conn_{}", *counter).into_bytes(),
retire_prior_to: 0,
};
let mut active = self.active_ids.lock().unwrap();
active.push(conn_id.clone());
conn_id
}
pub fn retire_connection_id(&self, sequence: u64) -> bool {
let mut active = self.active_ids.lock().unwrap();
let mut retired = self.retired_ids.lock().unwrap();
if let Some(pos) = active.iter().position(|id| id.sequence == sequence) {
let conn_id = active.remove(pos);
retired.push(conn_id);
true
} else {
false
}
}
pub fn is_retired(&self, sequence: u64) -> bool {
let retired = self.retired_ids.lock().unwrap();
retired.iter().any(|id| id.sequence == sequence)
}
pub fn active_count(&self) -> usize {
self.active_ids.lock().unwrap().len()
}
pub fn retired_count(&self) -> usize {
self.retired_ids.lock().unwrap().len()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ConnectionId {
pub sequence: u64,
pub id: Vec<u8>,
pub retire_prior_to: u64,
}
#[derive(Debug)]
pub struct AmplificationTracker {
received_bytes: Arc<Mutex<u64>>,
sent_bytes: Arc<Mutex<u64>>,
amplification_limit: u64,
}
impl AmplificationTracker {
pub fn new() -> Self {
Self {
received_bytes: Arc::new(Mutex::new(0)),
sent_bytes: Arc::new(Mutex::new(0)),
amplification_limit: 3,
}
}
pub fn record_received(&self, bytes: u64) {
let mut received = self.received_bytes.lock().unwrap();
*received += bytes;
}
pub fn record_sent(&self, bytes: u64) {
let mut sent = self.sent_bytes.lock().unwrap();
*sent += bytes;
}
pub fn is_amplification_exceeded(&self) -> bool {
let received = *self.received_bytes.lock().unwrap();
let sent = *self.sent_bytes.lock().unwrap();
if received == 0 {
sent > 1200 } else {
sent > received * self.amplification_limit
}
}
pub fn amplification_ratio(&self) -> f64 {
let received = *self.received_bytes.lock().unwrap();
let sent = *self.sent_bytes.lock().unwrap();
if received == 0 {
if sent == 0 { 0.0 } else { f64::INFINITY }
} else {
sent as f64 / received as f64
}
}
pub fn reset(&self) {
*self.received_bytes.lock().unwrap() = 0;
*self.sent_bytes.lock().unwrap() = 0;
}
}
#[derive(Debug, Clone)]
pub struct TransportParams {
pub disable_active_migration: bool,
pub active_connection_id_limit: u64,
pub preferred_address: Option<SocketAddr>,
}
impl Default for TransportParams {
fn default() -> Self {
Self {
disable_active_migration: false,
active_connection_id_limit: 2,
preferred_address: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum MigrationTestResult {
Pass,
Fail { reason: String },
Skip { reason: String },
}
pub trait ConformanceTest {
fn name(&self) -> &str;
fn rfc_section(&self) -> &str;
fn description(&self) -> &str;
fn requirement_level(&self) -> RequirementLevel;
fn run(&self, harness: &mut QuicMigrationConformanceHarness) -> MigrationTestResult;
}
#[derive(Debug, Clone, PartialEq)]
pub enum RequirementLevel {
Must,
Should,
May,
}
pub struct PathChallengeResponseTest;
impl ConformanceTest for PathChallengeResponseTest {
fn name(&self) -> &str {
"path_challenge_response_validation"
}
fn rfc_section(&self) -> &str {
"RFC 9000 §9.1"
}
fn description(&self) -> &str {
"PATH_CHALLENGE/PATH_RESPONSE round-trip validates new path"
}
fn requirement_level(&self) -> RequirementLevel {
RequirementLevel::Must
}
fn run(&self, harness: &mut QuicMigrationConformanceHarness) -> MigrationTestResult {
let new_path = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 443);
let challenge_id = harness.path_validator.send_path_challenge(new_path);
if harness.path_validator.pending_challenge_count() != 1 {
return MigrationTestResult::Fail {
reason: "PATH_CHALLENGE not properly tracked".to_string(),
};
}
let challenge_data = generate_challenge_data(challenge_id);
let validation_success = harness
.path_validator
.process_path_response(challenge_id, &challenge_data);
if !validation_success {
return MigrationTestResult::Fail {
reason: "PATH_RESPONSE validation failed".to_string(),
};
}
if !harness.path_validator.is_path_validated(new_path) {
return MigrationTestResult::Fail {
reason: "New path not marked as validated after successful challenge".to_string(),
};
}
if harness.path_validator.pending_challenge_count() != 0 {
return MigrationTestResult::Fail {
reason: "Completed challenge still marked as pending".to_string(),
};
}
MigrationTestResult::Pass
}
}
pub struct ConnectionIdRetirementTest;
impl ConformanceTest for ConnectionIdRetirementTest {
fn name(&self) -> &str {
"connection_id_retirement"
}
fn rfc_section(&self) -> &str {
"RFC 9000 §9.5"
}
fn description(&self) -> &str {
"Old Connection IDs are properly retired during migration"
}
fn requirement_level(&self) -> RequirementLevel {
RequirementLevel::Must
}
fn run(&self, harness: &mut QuicMigrationConformanceHarness) -> MigrationTestResult {
let old_conn_id = harness.connection_id_manager.issue_connection_id();
let new_conn_id = harness.connection_id_manager.issue_connection_id();
if harness.connection_id_manager.active_count() != 2 {
return MigrationTestResult::Fail {
reason: "Expected 2 active connection IDs".to_string(),
};
}
let retirement_success = harness
.connection_id_manager
.retire_connection_id(old_conn_id.sequence);
if !retirement_success {
return MigrationTestResult::Fail {
reason: "Connection ID retirement failed".to_string(),
};
}
if !harness
.connection_id_manager
.is_retired(old_conn_id.sequence)
{
return MigrationTestResult::Fail {
reason: "Retired connection ID not marked as retired".to_string(),
};
}
if harness.connection_id_manager.active_count() != 1 {
return MigrationTestResult::Fail {
reason: "Active connection ID count not decremented after retirement".to_string(),
};
}
if harness.connection_id_manager.retired_count() != 1 {
return MigrationTestResult::Fail {
reason: "Retired connection ID count not incremented".to_string(),
};
}
MigrationTestResult::Pass
}
}
pub struct AntiAmplificationTest;
impl ConformanceTest for AntiAmplificationTest {
fn name(&self) -> &str {
"anti_amplification_limits"
}
fn rfc_section(&self) -> &str {
"RFC 9000 §8.1"
}
fn description(&self) -> &str {
"Anti-amplification limits enforced during migration (3x received)"
}
fn requirement_level(&self) -> RequirementLevel {
RequirementLevel::Must
}
fn run(&self, harness: &mut QuicMigrationConformanceHarness) -> MigrationTestResult {
harness.amplification_tracker.reset();
harness.amplification_tracker.record_received(1000);
harness.amplification_tracker.record_sent(3000);
if harness.amplification_tracker.is_amplification_exceeded() {
return MigrationTestResult::Fail {
reason: "False positive: 3x amplification should be allowed".to_string(),
};
}
harness.amplification_tracker.record_sent(1);
if !harness.amplification_tracker.is_amplification_exceeded() {
return MigrationTestResult::Fail {
reason: "Anti-amplification limit not enforced at 3x + 1 byte".to_string(),
};
}
let ratio = harness.amplification_tracker.amplification_ratio();
if ratio <= 3.0 {
return MigrationTestResult::Fail {
reason: format!("Amplification ratio should exceed 3.0, got {:.2}", ratio),
};
}
harness.amplification_tracker.reset();
harness.amplification_tracker.record_sent(1200);
if harness.amplification_tracker.is_amplification_exceeded() {
return MigrationTestResult::Fail {
reason: "Initial 1200 byte packet should be allowed".to_string(),
};
}
harness.amplification_tracker.record_sent(1);
if !harness.amplification_tracker.is_amplification_exceeded() {
return MigrationTestResult::Fail {
reason: "Initial packet limit (1200 bytes) not enforced".to_string(),
};
}
MigrationTestResult::Pass
}
}
pub struct NatRebindingTest;
impl ConformanceTest for NatRebindingTest {
fn name(&self) -> &str {
"nat_rebinding_tolerance"
}
fn rfc_section(&self) -> &str {
"RFC 9000 §9.6"
}
fn description(&self) -> &str {
"NAT rebinding tolerated without explicit migration"
}
fn requirement_level(&self) -> RequirementLevel {
RequirementLevel::Should
}
fn run(&self, harness: &mut QuicMigrationConformanceHarness) -> MigrationTestResult {
let original_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)), 12345);
let rebound_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)), 54321);
let challenge_id = harness.path_validator.send_path_challenge(original_addr);
let challenge_data = generate_challenge_data(challenge_id);
harness
.path_validator
.process_path_response(challenge_id, &challenge_data);
if !harness.path_validator.is_path_validated(original_addr) {
return MigrationTestResult::Fail {
reason: "Failed to validate original path".to_string(),
};
}
let initial_challenge_count = harness.path_validator.pending_challenge_count();
let final_challenge_count = harness.path_validator.pending_challenge_count();
if final_challenge_count != initial_challenge_count {
return MigrationTestResult::Fail {
reason: "NAT rebinding should not automatically trigger path validation"
.to_string(),
};
}
MigrationTestResult::Pass
}
}
pub struct DisabledMigrationTest;
impl ConformanceTest for DisabledMigrationTest {
fn name(&self) -> &str {
"disable_active_migration_parameter"
}
fn rfc_section(&self) -> &str {
"RFC 9000 §18.2"
}
fn description(&self) -> &str {
"Migration disabled when disable_active_migration transport parameter set"
}
fn requirement_level(&self) -> RequirementLevel {
RequirementLevel::Must
}
fn run(&self, harness: &mut QuicMigrationConformanceHarness) -> MigrationTestResult {
harness.transport_params.disable_active_migration = true;
let new_path = SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
443,
);
if harness.transport_params.disable_active_migration {
let initial_challenge_count = harness.path_validator.pending_challenge_count();
let migration_allowed = !harness.transport_params.disable_active_migration;
if migration_allowed {
return MigrationTestResult::Fail {
reason: "Migration allowed when disable_active_migration is set".to_string(),
};
}
let final_challenge_count = harness.path_validator.pending_challenge_count();
if final_challenge_count != initial_challenge_count {
return MigrationTestResult::Fail {
reason: "PATH_CHALLENGE sent despite disable_active_migration flag".to_string(),
};
}
if !harness.transport_params.disable_active_migration {
return MigrationTestResult::Fail {
reason: "disable_active_migration flag not properly set".to_string(),
};
}
} else {
return MigrationTestResult::Fail {
reason: "Test setup failed: disable_active_migration not set".to_string(),
};
}
harness.transport_params.disable_active_migration = false;
let migration_allowed = !harness.transport_params.disable_active_migration;
if !migration_allowed {
return MigrationTestResult::Fail {
reason: "Migration blocked when disable_active_migration is false".to_string(),
};
}
let _challenge_id = harness.path_validator.send_path_challenge(new_path);
if harness.path_validator.pending_challenge_count() == 0 {
return MigrationTestResult::Fail {
reason: "PATH_CHALLENGE not sent when migration is enabled".to_string(),
};
}
MigrationTestResult::Pass
}
}
pub fn run_migration_conformance_tests() -> Vec<(String, MigrationTestResult)> {
let mut harness = QuicMigrationConformanceHarness::new();
let mut results = Vec::new();
let tests: Vec<Box<dyn ConformanceTest>> = vec![
Box::new(PathChallengeResponseTest),
Box::new(ConnectionIdRetirementTest),
Box::new(AntiAmplificationTest),
Box::new(NatRebindingTest),
Box::new(DisabledMigrationTest),
];
for test in tests {
println!("Running test: {} - {}", test.name(), test.description());
let result = test.run(&mut harness);
match &result {
MigrationTestResult::Pass => println!("✓ PASS"),
MigrationTestResult::Fail { reason } => println!("✗ FAIL: {}", reason),
MigrationTestResult::Skip { reason } => println!("- SKIP: {}", reason),
}
results.push((test.name().to_string(), result));
}
results
}
fn main() {
println!("QUIC Connection Migration Conformance Tests (RFC 9000 Section 9)");
println!("=================================================================");
println!();
let results = run_migration_conformance_tests();
println!();
println!("Summary:");
println!("--------");
let mut pass_count = 0;
let mut fail_count = 0;
let mut skip_count = 0;
for (test_name, result) in &results {
match result {
MigrationTestResult::Pass => {
pass_count += 1;
}
MigrationTestResult::Fail { reason } => {
fail_count += 1;
println!("FAILED: {} - {}", test_name, reason);
}
MigrationTestResult::Skip { reason } => {
skip_count += 1;
println!("SKIPPED: {} - {}", test_name, reason);
}
}
}
println!();
println!("Tests run: {}", results.len());
println!("Passed: {}", pass_count);
println!("Failed: {}", fail_count);
println!("Skipped: {}", skip_count);
if fail_count > 0 {
std::process::exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_challenge_response_validation() {
let mut harness = QuicMigrationConformanceHarness::new();
let test = PathChallengeResponseTest;
let result = test.run(&mut harness);
assert_eq!(result, MigrationTestResult::Pass);
}
#[test]
fn test_connection_id_retirement() {
let mut harness = QuicMigrationConformanceHarness::new();
let test = ConnectionIdRetirementTest;
let result = test.run(&mut harness);
assert_eq!(result, MigrationTestResult::Pass);
}
#[test]
fn test_anti_amplification_limits() {
let mut harness = QuicMigrationConformanceHarness::new();
let test = AntiAmplificationTest;
let result = test.run(&mut harness);
assert_eq!(result, MigrationTestResult::Pass);
}
#[test]
fn test_nat_rebinding_tolerance() {
let mut harness = QuicMigrationConformanceHarness::new();
let test = NatRebindingTest;
let result = test.run(&mut harness);
assert_eq!(result, MigrationTestResult::Pass);
}
#[test]
fn test_disable_active_migration_parameter() {
let mut harness = QuicMigrationConformanceHarness::new();
let test = DisabledMigrationTest;
let result = test.run(&mut harness);
assert_eq!(result, MigrationTestResult::Pass);
}
#[test]
fn test_full_migration_conformance_suite() {
let results = run_migration_conformance_tests();
for (test_name, result) in &results {
match result {
MigrationTestResult::Pass => {}
MigrationTestResult::Fail { reason } => {
panic!("Test {} failed: {}", test_name, reason);
}
MigrationTestResult::Skip { reason } => {
println!("Test {} skipped: {}", test_name, reason);
}
}
}
assert_eq!(results.len(), 5);
}
#[test]
fn test_path_response_data_mismatch() {
let validator = MockPathValidator::new();
let new_path = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 443);
let challenge_id = validator.send_path_challenge(new_path);
let wrong_data = [0xFF; 8];
let validation_success = validator.process_path_response(challenge_id, &wrong_data);
assert!(
!validation_success,
"Path validation should fail with incorrect response data"
);
assert!(
!validator.is_path_validated(new_path),
"Path should not be validated with wrong response"
);
}
#[test]
fn test_amplification_tracking_edge_cases() {
let tracker = AmplificationTracker::new();
assert_eq!(tracker.amplification_ratio(), 0.0);
assert!(!tracker.is_amplification_exceeded());
tracker.record_sent(1300); assert_eq!(tracker.amplification_ratio(), f64::INFINITY);
assert!(tracker.is_amplification_exceeded());
tracker.reset();
assert_eq!(tracker.amplification_ratio(), 0.0);
}
#[test]
fn test_connection_id_manager_edge_cases() {
let manager = MockConnectionIdManager::new();
let retirement_success = manager.retire_connection_id(999);
assert!(
!retirement_success,
"Should not be able to retire non-existent connection ID"
);
assert!(
!manager.is_retired(999),
"Non-existent ID should not be marked as retired"
);
assert_eq!(manager.active_count(), 0);
assert_eq!(manager.retired_count(), 0);
}
}