Files
irtt-rs/crates/irtt-integration-tests/tests/async_client_lifecycle.rs

725 lines
25 KiB
Rust

use irtt_client::{Authentication, HmacKey, OpenPolicy, SessionRequest};
use std::{
future::Future,
net::UdpSocket,
pin::Pin,
sync::mpsc,
task::{Context, Poll, Waker},
thread::{self, JoinHandle},
};
use irtt_proto::{
decode_request, encode_echo_reply, encode_open_reply, flags, verify_packet_hmac,
DecodedRequestKind, EchoReply, OpenReply, Params, ReceivedStats, StampAt, TimestampFields,
};
use tokio::runtime::{Builder, Runtime};
mod support;
use irtt_client::{AsyncClient, ClientConfig, ClientError, ClientEvent, OpenOutcome};
use irtt_client::{Client, RunMode, SendProbeError};
use irtt_server::ServerConfig;
use std::{
io,
net::SocketAddr,
time::{Duration, Instant},
};
use support::InTreeServer;
const TOKEN: u64 = 0x1234_5678_90ab_cdef;
struct TestServer {
addr: SocketAddr,
packets: mpsc::Receiver<Vec<u8>>,
done: JoinHandle<()>,
}
impl TestServer {
fn finish(self) -> Vec<Vec<u8>> {
self.done.join().unwrap();
self.packets.try_iter().collect()
}
}
fn runtime() -> Runtime {
Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.unwrap()
}
fn config(key: Option<Vec<u8>>, dscp: u8) -> ClientConfig {
ClientConfig {
open: OpenPolicy {
timeouts: vec![Duration::from_millis(200)],
..Default::default()
},
request: SessionRequest {
received_stats: ReceivedStats::None,
stamp_at: StampAt::None,
dscp,
..Default::default()
},
auth: key.map_or(Authentication::Unauthenticated, |key| {
Authentication::Hmac(HmacKey::new(key))
}),
..ClientConfig::default()
}
}
async fn opened_client(addr: SocketAddr, dscp: u8) -> AsyncClient {
let mut client = AsyncClient::connect(addr.to_string(), config(None, dscp))
.await
.unwrap();
client.open().await.unwrap();
client
}
fn start_server<F>(handler: F) -> TestServer
where
F: FnOnce(UdpSocket, mpsc::Sender<Vec<u8>>) + Send + 'static,
{
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
socket
.set_read_timeout(Some(Duration::from_secs(2)))
.unwrap();
let addr = socket.local_addr().unwrap();
let (tx, packets) = mpsc::channel();
let done = thread::spawn(move || handler(socket, tx));
TestServer {
addr,
packets,
done,
}
}
fn recv_packet(socket: &UdpSocket, tx: &mpsc::Sender<Vec<u8>>) -> (Vec<u8>, SocketAddr) {
let mut buffer = [0_u8; 8192];
let (len, peer) = socket.recv_from(&mut buffer).unwrap();
let packet = buffer[..len].to_vec();
tx.send(packet.clone()).unwrap();
(packet, peer)
}
/// Admits a request the way a compliant server does: structural decode first,
/// then HMAC presence policy, then authentication. Returns `None` for anything
/// a server would silently discard.
fn decode_server_request<'a>(
packet: &'a [u8],
key: Option<&[u8]>,
) -> Option<DecodedRequestKind<'a>> {
let request = decode_request(packet).ok()?;
if request.hmac_present != key.is_some() {
return None;
}
if let Some(key) = key {
verify_packet_hmac(key, packet).ok()?;
}
Some(request.kind)
}
/// Returns the requested parameters and whether this was a no-test open.
fn open_request(packet: &[u8], key: Option<&[u8]>) -> (Params, bool) {
match decode_server_request(packet, key) {
Some(DecodedRequestKind::Open { params, no_test }) => {
(Params::decode(params).unwrap(), no_test)
}
other => panic!("expected an authenticated open request, got {other:?}"),
}
}
fn echo_request_sequence(packet: &[u8], key: Option<&[u8]>) -> u32 {
match decode_server_request(packet, key) {
Some(DecodedRequestKind::Echo { sequence, .. }) => sequence,
other => panic!("expected an authenticated echo request, got {other:?}"),
}
}
fn close_request_token(packet: &[u8], key: Option<&[u8]>) -> u64 {
match decode_server_request(packet, key) {
Some(DecodedRequestKind::Close { token }) => token,
other => panic!("expected an authenticated close request, got {other:?}"),
}
}
fn send_open_reply(
socket: &UdpSocket,
peer: SocketAddr,
params: Params,
key: Option<&[u8]>,
reply_flags: u8,
token: u64,
) {
let reply = encode_open_reply(
&OpenReply {
flags: reply_flags,
token,
params,
},
key,
)
.unwrap();
socket.send_to(&reply, peer).unwrap();
}
fn open_session(
socket: &UdpSocket,
tx: &mpsc::Sender<Vec<u8>>,
key: Option<&[u8]>,
) -> (Params, SocketAddr) {
let (packet, peer) = recv_packet(socket, tx);
let (params, _) = open_request(&packet, key);
send_open_reply(
socket,
peer,
params.clone(),
key,
flags::FLAG_OPEN | flags::FLAG_REPLY,
TOKEN,
);
(params, peer)
}
fn echo_reply(
params: &Params,
sequence: u32,
token: u64,
reply_flags: u8,
key: Option<&[u8]>,
) -> Vec<u8> {
encode_echo_reply(
&EchoReply {
flags: reply_flags,
token,
sequence,
recv_count: None,
recv_window: None,
timestamps: TimestampFields::default(),
payload: Vec::new(),
},
params,
key,
)
.unwrap()
}
fn start_no_test_server() -> TestServer {
start_server(move |socket, tx| {
let (open_packet, peer) = recv_packet(&socket, &tx);
let (params, no_test) = open_request(&open_packet, None);
assert!(no_test);
send_open_reply(
&socket,
peer,
params,
None,
flags::FLAG_OPEN | flags::FLAG_REPLY | flags::FLAG_CLOSE,
0,
);
})
}
fn start_peer_close_server() -> TestServer {
start_server(move |socket, tx| {
let (params, peer) = open_session(&socket, &tx, None);
let (probe_packet, _) = recv_packet(&socket, &tx);
let sequence = echo_request_sequence(&probe_packet, None);
socket
.send_to(
&echo_reply(
&params,
sequence,
TOKEN,
flags::FLAG_REPLY | flags::FLAG_CLOSE,
None,
),
peer,
)
.unwrap();
})
}
fn poll_once<F: Future>(future: Pin<&mut F>) -> Poll<F::Output> {
let mut context = Context::from_waker(Waker::noop());
Future::poll(future, &mut context)
}
fn poll_recv_once(client: &mut AsyncClient) -> Poll<Result<Vec<ClientEvent>, ClientError>> {
let mut future = Box::pin(client.recv());
poll_once(Pin::as_mut(&mut future))
}
#[test]
fn connect_requires_current_runtime_when_polled() {
let mut future = Box::pin(AsyncClient::connect(
"127.0.0.1:2112",
ClientConfig::default(),
));
assert!(matches!(
poll_once(Pin::as_mut(&mut future)),
Poll::Ready(Err(ClientError::NoTokioRuntime))
));
}
#[test]
fn recv_before_open_fails_on_first_poll_without_socket_readiness() {
runtime().block_on(async {
let remote = SocketAddr::from(([127, 0, 0, 1], 2112));
let mut client = AsyncClient::connect(remote.to_string(), config(None, 0))
.await
.unwrap();
assert!(matches!(
poll_recv_once(&mut client),
Poll::Ready(Err(ClientError::NotOpen))
));
let mut send = Box::pin(client.send_probe());
assert!(matches!(
poll_once(send.as_mut()),
Poll::Ready(Err(SendProbeError::NotCommitted(ClientError::NotOpen)))
));
});
}
#[test]
fn ignored_open_traffic_cannot_extend_the_absolute_attempt_deadline() {
let server = start_server(move |socket, tx| {
let (first_packet, peer) = recv_packet(&socket, &tx);
let (params, _) = open_request(&first_packet, None);
socket
.set_read_timeout(Some(Duration::from_millis(10)))
.unwrap();
let started = Instant::now();
let mut next_noise = started;
let mut buffer = [0_u8; 512];
while started.elapsed() < Duration::from_millis(350) {
let now = Instant::now();
if now >= next_noise {
socket.send_to(&[0_u8], peer).unwrap();
next_noise = now + Duration::from_millis(25);
}
match socket.recv_from(&mut buffer) {
Ok((len, second_peer)) => {
tx.send(buffer[..len].to_vec()).unwrap();
send_open_reply(
&socket,
second_peer,
params,
None,
flags::FLAG_OPEN | flags::FLAG_REPLY,
TOKEN,
);
return;
}
Err(error)
if matches!(
error.kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
) => {}
Err(error) => panic!("{error}"),
}
}
panic!("second open request did not arrive after the first absolute deadline");
});
let mut client_config = config(None, 0);
client_config.open.timeouts = vec![Duration::from_millis(200), Duration::from_millis(200)];
runtime().block_on(async {
let mut client = AsyncClient::connect(server.addr.to_string(), client_config)
.await
.unwrap();
assert!(matches!(
client.open().await.unwrap(),
OpenOutcome::Started(_)
));
});
assert_eq!(server.finish().len(), 2);
}
#[test]
fn authenticated_rejection_is_terminal_without_retry() {
let server = start_server(move |socket, tx| {
let (open_packet, peer) = recv_packet(&socket, &tx);
let (params, _) = open_request(&open_packet, None);
send_open_reply(
&socket,
peer,
params,
None,
flags::FLAG_OPEN | flags::FLAG_REPLY | flags::FLAG_CLOSE,
0,
);
});
let mut client_config = config(None, 0);
client_config.open.timeouts = vec![Duration::from_millis(200), Duration::from_millis(200)];
runtime().block_on(async {
let mut client = AsyncClient::connect(server.addr.to_string(), client_config)
.await
.unwrap();
assert!(matches!(
client.open().await,
Err(ClientError::ServerRejected)
));
});
assert_eq!(server.finish().len(), 1);
}
#[test]
fn recv_after_local_close_and_no_test_fails_on_first_poll() {
let close_server = start_server(move |socket, tx| {
open_session(&socket, &tx, None);
let (close_packet, _) = recv_packet(&socket, &tx);
assert_eq!(close_request_token(&close_packet, None), TOKEN);
});
runtime().block_on(async {
let mut client = opened_client(close_server.addr, 0).await;
client.close().await.unwrap();
assert!(matches!(
poll_recv_once(&mut client),
Poll::Ready(Err(ClientError::AlreadyClosed))
));
let mut send = Box::pin(client.send_probe());
assert!(matches!(
poll_once(send.as_mut()),
Poll::Ready(Err(SendProbeError::NotCommitted(
ClientError::AlreadyClosed
)))
));
});
assert_eq!(close_server.finish().len(), 2);
let no_test_server = start_no_test_server();
runtime().block_on(async {
let mut client_config = config(None, 0);
client_config.request.run_mode = RunMode::NoTest;
let mut client = AsyncClient::connect(no_test_server.addr.to_string(), client_config)
.await
.unwrap();
client.open().await.unwrap();
assert!(matches!(
poll_recv_once(&mut client),
Poll::Ready(Err(ClientError::AlreadyCompleted))
));
let mut send = Box::pin(client.send_probe());
assert!(matches!(
poll_once(send.as_mut()),
Poll::Ready(Err(SendProbeError::NotCommitted(
ClientError::AlreadyCompleted
)))
));
});
assert_eq!(no_test_server.finish().len(), 1);
}
#[test]
fn authenticated_async_lifecycle_negotiates_dscp() {
let key = b"async-lifecycle-key".to_vec();
let server = InTreeServer::start(ServerConfig::default().with_hmac_key(key.clone()));
runtime().block_on(async {
let mut client = AsyncClient::connect(server.addr.to_string(), config(Some(key), 46))
.await
.unwrap();
let opened = client.open().await.unwrap();
assert_eq!(open_negotiated(&opened).accepted.dscp, 46);
client.send_probe().await.unwrap();
assert!(matches!(client.recv().await.unwrap().as_slice(), [ClientEvent::EchoReply { seq: 0, .. }]));
assert!(matches!(client.close().await.unwrap().as_slice(), [ClientEvent::SessionClosed { token, .. }] if *token == open_token(&opened)));
});
}
#[test]
fn blocking_and_async_peer_close_are_semantically_equivalent() {
let blocking_peer_close_server = start_peer_close_server();
let mut blocking_peer_close =
Client::connect(blocking_peer_close_server.addr.to_string(), config(None, 0)).unwrap();
blocking_peer_close
.set_recv_timeout(Some(Duration::from_millis(200)))
.unwrap();
blocking_peer_close.open().unwrap();
blocking_peer_close.send_probe().unwrap();
let blocking_peer_close_events = blocking_peer_close.recv_once().unwrap();
assert!(blocking_peer_close.is_peer_closed());
assert_eq!(blocking_peer_close_server.finish().len(), 2);
let async_peer_close_server = start_peer_close_server();
let async_peer_close_events = runtime().block_on(async {
let mut client =
AsyncClient::connect(async_peer_close_server.addr.to_string(), config(None, 0))
.await
.unwrap();
client.open().await.unwrap();
client.send_probe().await.unwrap();
let events = client.recv().await.unwrap();
assert!(client.is_peer_closed());
events
});
assert_eq!(async_peer_close_server.finish().len(), 2);
assert_eq!(
blocking_peer_close_events.len(),
async_peer_close_events.len()
);
for (blocking, asynchronous) in blocking_peer_close_events
.iter()
.zip(&async_peer_close_events)
{
assert_matching_event_shape(blocking, asynchronous);
}
}
fn open_negotiated(outcome: &OpenOutcome) -> &irtt_client::NegotiationResult {
match outcome {
OpenOutcome::Started(irtt_client::SessionStarted {
negotiation: negotiated,
..
})
| OpenOutcome::NoTestCompleted(irtt_client::NoTestCompleted {
negotiation: negotiated,
..
}) => negotiated,
}
}
fn open_token(outcome: &OpenOutcome) -> u64 {
match outcome {
OpenOutcome::Started(irtt_client::SessionStarted { token, .. }) => *token,
OpenOutcome::NoTestCompleted(_) => panic!("no-test open has no session token"),
}
}
fn assert_matching_event_shape(blocking: &ClientEvent, asynchronous: &ClientEvent) {
assert_eq!(event_shape(blocking), event_shape(asynchronous));
}
fn event_shape(event: &ClientEvent) -> (&'static str, u64, usize) {
match event {
ClientEvent::EchoSent { seq, bytes, .. } => ("sent", u64::from(*seq), *bytes),
ClientEvent::EchoReply { seq, bytes, .. } => ("reply", u64::from(*seq), *bytes),
ClientEvent::SessionClosed { .. } => ("closed", 0, 0),
other => panic!("unexpected equivalence event: {other:?}"),
}
}
/// A peer that ignores the first `ignored_requests` datagrams before serving a
/// compliant open, one echo, and one close.
///
/// Silence is the point here, so this stays a narrow fake peer rather than a
/// real `irtt-server`: a compliant server has no way to be deliberately mute
/// for exactly one open attempt.
fn start_silent_then_open_server(ignored_requests: usize) -> TestServer {
start_server(move |socket, tx| {
for _ in 0..ignored_requests {
let _ = recv_packet(&socket, &tx);
}
let (params, peer) = open_session(&socket, &tx, None);
let (probe_packet, _) = recv_packet(&socket, &tx);
let sequence = echo_request_sequence(&probe_packet, None);
socket
.send_to(
&echo_reply(&params, sequence, TOKEN, flags::FLAG_REPLY, None),
peer,
)
.unwrap();
let (close_packet, _) = recv_packet(&socket, &tx);
assert_eq!(close_request_token(&close_packet, None), TOKEN);
})
}
/// Stable name for a `ClientError` variant, for comparing what each driver
/// promises rather than how it produced it.
fn error_name(error: &ClientError) -> &'static str {
match error {
ClientError::OpenTimeout => "open timeout",
ClientError::NotOpen => "not open",
ClientError::AlreadyOpen => "already open",
ClientError::AlreadyClosed => "already closed",
ClientError::AlreadyCompleted => "already completed",
ClientError::ServerRejected => "server rejected",
other => panic!("unexpected conformance error: {other:?}"),
}
}
fn probe_sequences(events: &[ClientEvent]) -> Vec<u32> {
events
.iter()
.filter_map(|event| match event {
ClientEvent::EchoSent { seq, .. } => Some(*seq),
_ => None,
})
.collect()
}
fn reply_sequences(events: &[ClientEvent]) -> Vec<u32> {
events
.iter()
.filter_map(|event| match event {
ClientEvent::EchoReply { seq, .. } => Some(*seq),
_ => None,
})
.collect()
}
#[test]
fn blocking_and_async_roll_back_a_failed_open_and_accept_a_retry() {
// One open attempt is ignored, so the first open times out and the second
// must be able to open the same connected client.
let blocking_server = start_silent_then_open_server(1);
let mut blocking = Client::connect(blocking_server.addr.to_string(), config(None, 0)).unwrap();
blocking
.set_recv_timeout(Some(Duration::from_millis(200)))
.unwrap();
assert!(!blocking.has_pending_probes());
let blocking_first = blocking.open().unwrap_err();
assert!(!blocking.has_pending_probes());
let blocking_second = blocking.open().unwrap();
assert!(!blocking.has_pending_probes());
let blocking_sent = [ClientEvent::from(blocking.send_probe().unwrap())];
let blocking_reply = blocking.recv_once().unwrap();
let blocking_close = blocking.close().unwrap();
assert_eq!(blocking_server.finish().len(), 4);
let async_server = start_silent_then_open_server(1);
let (async_first, async_second, async_sent, async_reply, async_close) =
runtime().block_on(async {
let mut client = AsyncClient::connect(async_server.addr.to_string(), config(None, 0))
.await
.unwrap();
assert!(!client.has_pending_probes());
let first = client.open().await.unwrap_err();
assert!(!client.has_pending_probes());
let second = client.open().await.unwrap();
assert!(!client.has_pending_probes());
let sent = [ClientEvent::from(client.send_probe().await.unwrap())];
let reply = client.recv().await.unwrap();
let closed = client.close().await.unwrap();
(first, second, sent, reply, closed)
});
assert_eq!(async_server.finish().len(), 4);
// Both drivers promise the same failure, and neither leaves the client in a
// state that refuses the retry.
assert_eq!(error_name(&blocking_first), "open timeout");
assert_eq!(error_name(&blocking_first), error_name(&async_first));
assert!(matches!(blocking_second, OpenOutcome::Started(_)));
assert!(matches!(async_second, OpenOutcome::Started(_)));
assert_eq!(
open_negotiated(&blocking_second),
open_negotiated(&async_second)
);
assert_eq!(open_token(&blocking_second), open_token(&async_second));
// The retried session starts a fresh probe sequence in both drivers.
assert_eq!(probe_sequences(&blocking_sent), [0]);
assert_eq!(probe_sequences(&async_sent), [0]);
assert_eq!(reply_sequences(&blocking_reply), [0]);
assert_eq!(reply_sequences(&async_reply), [0]);
assert_eq!(blocking_sent.len(), async_sent.len());
assert_eq!(blocking_reply.len(), async_reply.len());
assert_eq!(blocking_close.len(), async_close.len());
assert_matching_event_shape(&blocking_close[0], &async_close[0]);
}
#[test]
fn blocking_and_async_complete_every_caller_paced_probe() {
const PROBES: u32 = 3;
let mut caller_config = config(None, 0);
caller_config.request.duration = Some(Duration::from_millis(1));
caller_config.request.interval = Duration::from_secs(1);
let blocking_server = InTreeServer::start(ServerConfig::default());
let mut blocking =
Client::connect(blocking_server.addr.to_string(), caller_config.clone()).unwrap();
blocking
.set_recv_timeout(Some(Duration::from_millis(200)))
.unwrap();
let outcome = blocking.open().unwrap();
assert_eq!(blocking.negotiation(), Some(open_negotiated(&outcome)));
assert!(!blocking.has_pending_probes());
// Oversleeping cannot invalidate this: low-level sends remain caller-owned
// after the negotiated duration. The compliant server has no duration cap.
thread::sleep(Duration::from_millis(2));
let mut blocking_sent = Vec::new();
let mut blocking_replies = Vec::new();
for _ in 0..PROBES {
blocking_sent.push(ClientEvent::from(blocking.send_probe().unwrap()));
assert!(blocking.has_pending_probes());
assert!(blocking.next_probe_timeout_deadline().is_some());
blocking_replies.extend(blocking.recv_once().unwrap());
assert!(!blocking.has_pending_probes());
}
blocking.close().unwrap();
drop(blocking_server);
let async_server = InTreeServer::start(ServerConfig::default());
let (async_sent, async_replies) = runtime().block_on(async {
let mut client = AsyncClient::connect(async_server.addr.to_string(), caller_config)
.await
.unwrap();
let outcome = client.open().await.unwrap();
assert_eq!(client.negotiation(), Some(open_negotiated(&outcome)));
tokio::time::sleep(Duration::from_millis(2)).await;
let mut sent = Vec::new();
let mut replies = Vec::new();
for _ in 0..PROBES {
sent.push(ClientEvent::from(client.send_probe().await.unwrap()));
assert!(client.has_pending_probes());
assert!(client.next_probe_timeout_deadline().is_some());
replies.extend(client.recv().await.unwrap());
assert!(!client.has_pending_probes());
}
client.close().await.unwrap();
(sent, replies)
});
drop(async_server);
let expected: Vec<u32> = (0..PROBES).collect();
assert_eq!(probe_sequences(&blocking_sent), expected);
assert_eq!(probe_sequences(&async_sent), expected);
assert_eq!(reply_sequences(&blocking_replies), expected);
assert_eq!(reply_sequences(&async_replies), expected);
// Compare the whole emitted streams, not just the probe and reply events
// the sequence helpers keep: an extra event on either side is a difference.
assert_eq!(blocking_sent.len(), async_sent.len());
assert_eq!(blocking_replies.len(), async_replies.len());
for event in blocking_sent.iter().chain(&async_sent) {
assert!(matches!(
event,
ClientEvent::EchoSent {
scheduled_at: None,
timer_error: None,
..
}
));
}
for (blocking, asynchronous) in blocking_sent.iter().zip(&async_sent) {
assert_matching_event_shape(blocking, asynchronous);
}
for (blocking, asynchronous) in blocking_replies.iter().zip(&async_replies) {
assert_matching_event_shape(blocking, asynchronous);
}
}
#[test]
fn ignored_open_traffic_uses_one_request_and_deadline_per_attempt() {
let server = start_server(move |socket, tx| {
for _ in 0..2 {
let (_, peer) = recv_packet(&socket, &tx);
for _ in 0..3 {
socket.send_to(&[0_u8], peer).unwrap();
}
}
});
let mut client_config = config(None, 0);
client_config.open.timeouts = vec![Duration::from_millis(200), Duration::from_millis(200)];
runtime().block_on(async {
let mut client = AsyncClient::connect(server.addr.to_string(), client_config)
.await
.unwrap();
assert!(matches!(client.open().await, Err(ClientError::OpenTimeout)));
});
assert_eq!(server.finish().len(), 2);
}