mod support; use std::time::Duration; use irtt_client::{ Client, ClientConfig, ClientError, ClientEvent, ClientTimestamp, OpenOutcome, SocketConfig, WarningKind, }; use irtt_proto::{echo_packet_len, ProtoError, TimestampFields}; use support::{ config_for_params, default_params, params_for_modes, run_one_probe_with_config, standard_timestamps, start_bad_hmac_echo_reply_server, start_hmac_close_server, start_hmac_required_open_drop_server, BackendPeer, ServerObservation, TOKEN, }; #[test] fn hmac_open_success_negotiates_without_warnings() { let key = b"compat-secret".to_vec(); let params = default_params(); let config_params = params.clone(); let config_key = key.clone(); let run = run_one_probe_with_config( params.clone(), TimestampFields::default(), Some(key), |addr| ClientConfig { hmac_key: Some(config_key), ..config_for_params(addr, &config_params) }, ); assert_eq!(run.negotiated.params, params); match &run.observations[0] { ServerObservation::Open { params: got, hmac } => { assert_eq!(got, ¶ms); assert!(*hmac); } other => panic!("expected open observation, got {other:?}"), } assert_no_warning_or_error(&[run.sent, run.reply]); } #[test] fn hmac_echo_success_verifies_request_and_accepts_reply() { let key = b"compat-secret".to_vec(); let params = default_params(); let config_params = params.clone(); let config_key = key.clone(); let run = run_one_probe_with_config(params.clone(), standard_timestamps(), Some(key), |addr| { ClientConfig { hmac_key: Some(config_key), ..config_for_params(addr, &config_params) } }); match &run.observations[1] { ServerObservation::Echo { len, hmac, token, sequence, } => { assert_eq!(*len, echo_packet_len(true, ¶ms)); assert!(*hmac); assert_eq!(*token, TOKEN); assert_eq!(*sequence, 0); } other => panic!("expected echo observation, got {other:?}"), } match &run.reply { ClientEvent::EchoReply { seq, logical_seq, bytes, server_timing, .. } => { assert_eq!(*seq, 0); assert_eq!(*logical_seq, 0); assert_eq!(*bytes, echo_packet_len(true, ¶ms)); assert!(server_timing.is_some()); } other => panic!("expected EchoReply, got {other:?}"), } assert_no_duplicate_late_or_warning(&[run.reply]); } #[test] fn hmac_close_success_sends_authenticated_close_and_closes_session() { let key = b"compat-secret".to_vec(); let params = default_params(); let server = start_hmac_close_server(params.clone(), key.clone()); let mut config = config_for_params(server.addr, ¶ms); config.hmac_key = Some(key); let mut client = Client::connect(config).unwrap(); let outcome = client.open(ClientTimestamp::now()).unwrap(); assert_started(outcome, ¶ms); let events = client.close(ClientTimestamp::now()).unwrap(); assert!(matches!( events.as_slice(), [ClientEvent::SessionClosed { token: TOKEN, .. }] )); assert!(matches!( client.send_probe(), Err(ClientError::AlreadyClosed) )); let observations = server.observations(2); match &observations[1] { ServerObservation::Close { hmac, token } => { assert!(*hmac); assert_eq!(*token, TOKEN); } other => panic!("expected close observation, got {other:?}"), } server.join(); } #[test] fn missing_client_key_against_hmac_required_server_times_out() { let key = b"compat-secret".to_vec(); let server = start_hmac_required_open_drop_server(key, Duration::from_millis(250)); let mut config = config_for_params(server.addr, &default_params()); config.open_timeouts = vec![Duration::from_millis(200)]; config.hmac_key = None; let mut client = Client::connect(config).unwrap(); assert!(matches!( client.open(ClientTimestamp::now()), Err(ClientError::OpenTimeout) )); let observations = server.observations(1); assert!(matches!( observations.as_slice(), [ServerObservation::RejectedHmac { hmac: false, bad_hmac: true }] )); server.join(); } #[test] fn wrong_client_key_against_hmac_required_server_times_out() { let server_key = b"compat-secret".to_vec(); let server = start_hmac_required_open_drop_server(server_key, Duration::from_millis(250)); let mut config = config_for_params(server.addr, &default_params()); config.open_timeouts = vec![Duration::from_millis(200)]; config.hmac_key = Some(b"wrong-secret".to_vec()); let mut client = Client::connect(config).unwrap(); assert!(matches!( client.open(ClientTimestamp::now()), Err(ClientError::OpenTimeout) )); let observations = server.observations(1); assert!(matches!( observations.as_slice(), [ServerObservation::RejectedHmac { hmac: true, bad_hmac: true }] )); server.join(); } #[test] fn bad_hmac_echo_reply_is_rejected_without_echo_reply_event() { let key = b"compat-secret".to_vec(); let params = default_params(); let server = start_bad_hmac_echo_reply_server(params.clone(), key.clone()); let mut config = config_for_params(server.addr, ¶ms); config.hmac_key = Some(key); config.socket_config = SocketConfig { recv_timeout: Some(Duration::from_millis(500)), ..Default::default() }; let mut client = Client::connect(config).unwrap(); let outcome = client.open(ClientTimestamp::now()).unwrap(); assert_started(outcome, ¶ms); let sent = client.send_probe().unwrap(); assert_eq!(sent.len(), 1); let events = client.recv_once().unwrap(); assert!(matches!( events.as_slice(), [ClientEvent::Warning { kind: WarningKind::MalformedOrUnrelatedPacket, .. }] )); assert!(!events .iter() .any(|event| matches!(event, ClientEvent::EchoReply { .. }))); let observations = server.observations(2); assert!(matches!( observations.as_slice(), [ ServerObservation::Open { hmac: true, .. }, ServerObservation::Echo { hmac: true, token: TOKEN, sequence: 0, .. } ] )); server.join(); } #[test] fn hmac_open_reply_with_bad_hmac_fails_with_protocol_error() { let key = b"compat-secret".to_vec(); let wrong_key = b"wrong-secret".to_vec(); let params = default_params(); let server = support::start_bad_hmac_open_reply_server(params.clone(), key.clone(), wrong_key); let mut config = config_for_params(server.addr, ¶ms); config.hmac_key = Some(key); let mut client = Client::connect(config).unwrap(); assert!(matches!( client.open(ClientTimestamp::now()), Err(ClientError::Protocol(ProtoError::BadHmac)) )); server.join(); } #[test] fn non_hmac_client_open_does_not_set_hmac_flag() { let params = params_for_modes( irtt_proto::ReceivedStats::None, irtt_proto::StampAt::None, irtt_proto::Clock::Both, ); let server = support::start_open_server(params.clone(), None); let mut config = config_for_params(server.addr, ¶ms); config.hmac_key = None; let mut client = Client::connect(config).unwrap(); let outcome = client.open(ClientTimestamp::now()).unwrap(); assert_started(outcome, ¶ms); let observations = server.observations(1); assert!(matches!( observations.as_slice(), [ServerObservation::Open { hmac: false, .. }] )); server.join(); } fn assert_started(outcome: OpenOutcome, expected: &irtt_proto::Params) { match outcome { OpenOutcome::Started { token, negotiated, event: ClientEvent::SessionStarted { token: event_token, negotiated: event_negotiated, .. }, .. } => { assert_eq!(token, TOKEN); assert_eq!(event_token, TOKEN); assert_eq!(negotiated.params, *expected); assert_eq!(event_negotiated.params, *expected); } other => panic!("expected started open outcome, got {other:?}"), } } fn assert_no_warning_or_error(events: &[ClientEvent]) { assert!(!events.iter().any(|event| { matches!( event, ClientEvent::Warning { .. } | ClientEvent::DuplicateReply { .. } | ClientEvent::LateReply { .. } ) })); } fn assert_no_duplicate_late_or_warning(events: &[ClientEvent]) { assert_no_warning_or_error(events); } #[test] fn backend_hmac_correct_key_succeeds() { let key = b"compat-secret".to_vec(); let params = default_params(); let peer = BackendPeer::start_open_echo(params.clone(), Some(key.clone())); let mut config = config_for_params(peer.addr(), ¶ms); config.hmac_key = Some(key); let mut client = Client::connect(config).unwrap(); let outcome = client.open(ClientTimestamp::now()).unwrap(); assert!(matches!(outcome, OpenOutcome::Started { .. })); let sent = client.send_probe().unwrap(); assert_eq!(sent.len(), 1); let events = client.recv_once().unwrap(); assert_eq!(events.len(), 1); assert!(matches!(events[0], ClientEvent::EchoReply { .. })); client.close(ClientTimestamp::now()).unwrap(); } #[test] fn backend_hmac_wrong_key_fails() { let server_key = b"compat-secret".to_vec(); let peer = BackendPeer::start_hmac_required(server_key); let mut config = config_for_params(peer.addr(), &default_params()); config.open_timeouts = vec![Duration::from_millis(200)]; config.hmac_key = Some(b"wrong-secret".to_vec()); let mut client = Client::connect(config).unwrap(); assert!(matches!( client.open(ClientTimestamp::now()), Err(ClientError::OpenTimeout) )); } #[test] fn backend_hmac_missing_key_fails() { let server_key = b"compat-secret".to_vec(); let peer = BackendPeer::start_hmac_required(server_key); let mut config = config_for_params(peer.addr(), &default_params()); config.open_timeouts = vec![Duration::from_millis(200)]; config.hmac_key = None; let mut client = Client::connect(config).unwrap(); assert!(matches!( client.open(ClientTimestamp::now()), Err(ClientError::OpenTimeout) )); }