test(client): add comprehensive integration tests for HMAC authentication
Verify HMAC negotiation, session closure, and error handling for authenticated probes to ensure protocol security and reliability.
This commit is contained in:
1 parent
3f33f630b2
commit
797120056e
2 files changed
+455
-3
No files matched your search
@@ -0,0 +1,296 @@
|
||||
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, 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);
|
||||
}
|
||||
Reference in new issue
Block a user