feat(server): generate echo payload fill
This commit is contained in:
12 files changed
+721
-41
No files matched your search
@@ -725,7 +725,11 @@ fn a_single_clock_midpoint_emits_exactly_one_field() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reply_payload_is_zero_filled_and_never_reflects_the_request() {
|
||||
fn a_reply_payload_is_server_generated_and_never_reflects_the_request() {
|
||||
// The inbound tail is opaque, and ServerFill describes the *server's* bytes:
|
||||
// a request's payload is neither echoed, mixed in, nor used to seed
|
||||
// anything. This session expressed no fill preference, so the region
|
||||
// carries the server's default fill; `fill` covers the modes themselves.
|
||||
let params = echo_params(ReceivedStats::None, StampAt::None, Clock::Unspecified, 64);
|
||||
let (mut core, token, negotiated) = session_with(¶ms);
|
||||
|
||||
@@ -744,7 +748,14 @@ fn a_reply_payload_is_zero_filled_and_never_reflects_the_request() {
|
||||
assert_eq!(packet.bytes().len(), 64);
|
||||
assert_eq!(reply.payload.len(), 48);
|
||||
assert!(
|
||||
reply.payload.iter().all(|byte| *byte == 0),
|
||||
"the payload region must be zero-filled, never request or buffer residue"
|
||||
!reply.payload.contains(&0xa5),
|
||||
"no request byte may appear in the reply payload"
|
||||
);
|
||||
assert!(
|
||||
reply
|
||||
.payload
|
||||
.chunks(4)
|
||||
.all(|chunk| chunk == &b"irtt"[..chunk.len()]),
|
||||
"the region carries the server's default fill"
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,31 @@
|
||||
//! Server fill negotiation: what an open reply reports, and what the session
|
||||
//! will actually fill with.
|
||||
//! Server fill: what an open reply reports, what the session will fill with,
|
||||
//! and the bytes an echo reply actually carries.
|
||||
//!
|
||||
//! The two are deliberately different questions. A client that expressed no
|
||||
//! preference keeps its absent or empty descriptor in the negotiated parameters
|
||||
//! while the session uses this server's default fill, and only a descriptor the
|
||||
//! server could not honor is rewritten. Asserting the reply alone would not
|
||||
//! separate those, so these tests read the session's effective mode too.
|
||||
//! The first two are deliberately different questions. A client that expressed
|
||||
//! no preference keeps its absent or empty descriptor in the negotiated
|
||||
//! parameters while the session uses this server's default fill, and only a
|
||||
//! descriptor the server could not honor is rewritten. Asserting the reply alone
|
||||
//! would not separate those, so the negotiation tests read the session's
|
||||
//! effective mode too, and the payload tests then pin the bytes on the wire.
|
||||
//!
|
||||
//! Payload lengths here are derived from `irtt-proto`'s own layout rather than
|
||||
//! written as offsets, and are deliberately not multiples of the pattern length.
|
||||
|
||||
use irtt_proto::{Params, ServerFill};
|
||||
use irtt_proto::{echo_header_len, Clock, Params, ReceivedStats, ServerFill, StampAt};
|
||||
|
||||
use super::support::{
|
||||
client_params, core_with_tokens, expect_no_test_reply, expect_normal_open_reply,
|
||||
no_test_request, open_request, peer, unthrottled, ScriptedTokens,
|
||||
client_params, core_for, core_with_tokens, echo_params, echo_request, expect_echo_reply,
|
||||
expect_no_test_reply, expect_normal_open_reply, no_test_request, open_negotiated, open_request,
|
||||
other_peer, peer, unthrottled, ScriptedTokens, KEY,
|
||||
};
|
||||
use crate::{core::ServerCore, fill::FillMode};
|
||||
|
||||
const TOKEN_A: u64 = 0x0102_0304_0506_0708;
|
||||
const TOKEN_B: u64 = 0x1112_1314_1516_1718;
|
||||
|
||||
/// Request payload bytes an echo carries, so a reply reflecting them would be
|
||||
/// obvious.
|
||||
const REQUEST_FILLER: u8 = 0xa5;
|
||||
|
||||
/// Params requesting `descriptor` as the server fill.
|
||||
fn requesting(descriptor: Option<&str>) -> Params {
|
||||
@@ -152,3 +162,224 @@ fn a_no_test_open_negotiates_fill_exactly_as_a_normal_open_does() {
|
||||
Some(crate::fill::DEFAULT_FILL_DESCRIPTOR)
|
||||
);
|
||||
}
|
||||
|
||||
/// Params for a session requesting `descriptor` whose echo replies have exactly
|
||||
/// `payload_len` payload bytes.
|
||||
///
|
||||
/// The negotiated length is the mandatory field block plus the region wanted,
|
||||
/// asked of `irtt-proto` rather than written as a constant: the block grows with
|
||||
/// the negotiated statistics, the timestamps and authentication's 16 bytes, and
|
||||
/// a test that hard-coded an offset would silently drift from the encoder.
|
||||
fn payload_params(descriptor: Option<&str>, payload_len: usize, hmac: bool) -> Params {
|
||||
let mut params = Params {
|
||||
server_fill: requesting(descriptor).server_fill,
|
||||
..echo_params(ReceivedStats::None, StampAt::None, Clock::Unspecified, 0)
|
||||
};
|
||||
params.length = (echo_header_len(hmac, ¶ms) + payload_len) as i64;
|
||||
params
|
||||
}
|
||||
|
||||
/// The payload regions of `count` successive echo replies on one session
|
||||
/// requesting `descriptor`.
|
||||
///
|
||||
/// Every request carries [`REQUEST_FILLER`] payload bytes, so a reply that
|
||||
/// reflected its request would be visible in any of these tests rather than only
|
||||
/// in the one about it.
|
||||
fn echo_payloads(
|
||||
descriptor: Option<&str>,
|
||||
payload_len: usize,
|
||||
hmac_key: Option<&[u8]>,
|
||||
count: u32,
|
||||
) -> Vec<Vec<u8>> {
|
||||
let requested = payload_params(descriptor, payload_len, hmac_key.is_some());
|
||||
let mut core = core_for(hmac_key, ScriptedTokens::new([TOKEN_A]));
|
||||
let (token, negotiated) = open_negotiated(&mut core, peer(), &requested, hmac_key);
|
||||
|
||||
// The request fills its own payload region, which is the same size as the
|
||||
// reply's: the strongest shape for "a reply never reflects its request".
|
||||
let request_payload = vec![REQUEST_FILLER; payload_len];
|
||||
(0..count)
|
||||
.map(|sequence| {
|
||||
let request = echo_request(token, sequence, &negotiated, &request_payload, hmac_key);
|
||||
let packet = core
|
||||
.handle_datagram(peer(), &request)
|
||||
.unwrap()
|
||||
.expect("an admissible echo must be answered");
|
||||
let reply = expect_echo_reply(&packet, &negotiated, hmac_key);
|
||||
assert_eq!(
|
||||
packet.bytes().len() as i64,
|
||||
negotiated.length,
|
||||
"a reply is exactly the negotiated length"
|
||||
);
|
||||
assert_eq!(reply.payload.len(), payload_len, "payload region length");
|
||||
reply.payload
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The payload region of one echo reply on a session requesting `descriptor`.
|
||||
fn echo_payload(descriptor: Option<&str>, payload_len: usize) -> Vec<u8> {
|
||||
echo_payloads(descriptor, payload_len, None, 1).remove(0)
|
||||
}
|
||||
|
||||
/// `pattern` repeated to `len` bytes, starting at its first byte.
|
||||
fn repeated(pattern: &[u8], len: usize) -> Vec<u8> {
|
||||
pattern.iter().copied().cycle().take(len).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_preference_fills_with_the_default_pattern() {
|
||||
// The wire consequence of the negotiation test above: absent descriptor,
|
||||
// default bytes. 7 is not a multiple of 4, so the last repeat is partial.
|
||||
assert_eq!(echo_payload(None, 7), b"irttirt".to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_preference_fills_with_the_default_pattern() {
|
||||
assert_eq!(echo_payload(Some(""), 7), b"irttirt".to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_or_malformed_descriptor_fills_with_the_default_pattern() {
|
||||
for descriptor in ["bogus", "RAND", "pattern:", "pattern:abc", "pattern:zz"] {
|
||||
assert_eq!(
|
||||
echo_payload(Some(descriptor), 7),
|
||||
b"irttirt".to_vec(),
|
||||
"{descriptor}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_fill_produces_a_zero_payload_and_never_the_request() {
|
||||
// `irtt-rs` policy, and a deliberate divergence: the clean evidence records
|
||||
// the reference server leaving this region as unspecified residual buffer
|
||||
// content — returning the requester's own payload, or fragments of earlier
|
||||
// traffic from another client. Zeroes disclose nothing.
|
||||
let payload = echo_payload(Some("none"), 7);
|
||||
assert_eq!(payload, vec![0; 7]);
|
||||
assert!(!payload.contains(&REQUEST_FILLER));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patterns_repeat_from_their_first_byte() {
|
||||
for (descriptor, expected) in [
|
||||
("pattern:00", &[0x00][..]),
|
||||
("pattern:ff00", &[0xff, 0x00][..]),
|
||||
// The reply follows the bytes the descriptor decodes to, whatever case
|
||||
// it was written in.
|
||||
("pattern:AaBb", &[0xaa, 0xbb][..]),
|
||||
("pattern:69727474", b"irtt"),
|
||||
] {
|
||||
assert_eq!(
|
||||
echo_payload(Some(descriptor), 7),
|
||||
repeated(expected, 7),
|
||||
"{descriptor}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pattern_restarts_for_every_reply() {
|
||||
// Deliberate `irtt-rs` policy. The clean evidence records the reference
|
||||
// implementation advancing one phase continuously across replies, sessions
|
||||
// and listeners, and states that payload phase carries no protocol meaning
|
||||
// and that a conforming client must not depend on it — so this server keeps
|
||||
// no fill state at all and every reply starts at byte zero.
|
||||
let payloads = echo_payloads(Some("pattern:010203"), 5, None, 2);
|
||||
assert_eq!(payloads[0], vec![0x01, 0x02, 0x03, 0x01, 0x02]);
|
||||
assert_eq!(
|
||||
payloads[1], payloads[0],
|
||||
"the phase resets rather than continuing at 03 01 02 03 01"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sessions_fill_independently() {
|
||||
// Interleaved on one core, so a shared cursor or a global phase would show
|
||||
// up as one session's bytes drifting into the other's.
|
||||
let mut core = core_with_tokens(unthrottled(), ScriptedTokens::new([TOKEN_A, TOKEN_B]));
|
||||
let a_requested = payload_params(Some("pattern:aabb"), 5, false);
|
||||
let b_requested = payload_params(Some("pattern:112233"), 5, false);
|
||||
let (a_token, a) = open_negotiated(&mut core, peer(), &a_requested, None);
|
||||
let (b_token, b) = open_negotiated(&mut core, other_peer(), &b_requested, None);
|
||||
|
||||
let mut payload_of = |endpoint, token, sequence, params: &Params| {
|
||||
let packet = core
|
||||
.handle_datagram(endpoint, &echo_request(token, sequence, params, &[], None))
|
||||
.unwrap()
|
||||
.expect("an admissible echo must be answered");
|
||||
expect_echo_reply(&packet, params, None).payload
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
payload_of(peer(), a_token, 0, &a),
|
||||
vec![0xaa, 0xbb, 0xaa, 0xbb, 0xaa]
|
||||
);
|
||||
assert_eq!(
|
||||
payload_of(other_peer(), b_token, 0, &b),
|
||||
vec![0x11, 0x22, 0x33, 0x11, 0x22]
|
||||
);
|
||||
assert_eq!(
|
||||
payload_of(peer(), a_token, 1, &a),
|
||||
vec![0xaa, 0xbb, 0xaa, 0xbb, 0xaa],
|
||||
"session A is unaffected by session B's reply"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_random_session_fills_the_whole_region() {
|
||||
// Deterministic assertions only: the region is the negotiated one and the
|
||||
// reply decodes and authenticates. What the operating system's random
|
||||
// source returns is not a property this suite can assert without flipping a
|
||||
// coin, and `fill`'s unit tests cover propagation and the failure fallback.
|
||||
for hmac_key in [None, Some(KEY)] {
|
||||
// `echo_payloads` already asserts the datagram length and the region
|
||||
// length of each reply, and decoding verifies the MAC. Nothing is
|
||||
// asserted about the byte values, deliberately: a random payload
|
||||
// containing any particular byte is chance, not behavior.
|
||||
assert_eq!(echo_payloads(Some("rand"), 7, hmac_key, 2).len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_consumes_payload_space_at_the_same_negotiated_length() {
|
||||
// One negotiated length, two field blocks: authentication's 16 bytes come
|
||||
// out of the payload region, not out of the datagram. Every remaining
|
||||
// payload byte is still filled, and the authenticated reply's MAC covers
|
||||
// those bytes — `expect_echo_reply` verifies it while decoding.
|
||||
let unauthenticated = payload_params(Some("pattern:aa"), 48, false);
|
||||
let length = unauthenticated.length;
|
||||
let authenticated = Params {
|
||||
length,
|
||||
..payload_params(Some("pattern:aa"), 0, true)
|
||||
};
|
||||
|
||||
let plain = {
|
||||
let mut core = core_for(None, ScriptedTokens::new([TOKEN_A]));
|
||||
let (token, negotiated) = open_negotiated(&mut core, peer(), &unauthenticated, None);
|
||||
let packet = core
|
||||
.handle_datagram(peer(), &echo_request(token, 0, &negotiated, &[], None))
|
||||
.unwrap()
|
||||
.expect("an admissible echo must be answered");
|
||||
assert_eq!(packet.bytes().len() as i64, length);
|
||||
expect_echo_reply(&packet, &negotiated, None).payload
|
||||
};
|
||||
let signed = {
|
||||
let mut core = core_for(Some(KEY), ScriptedTokens::new([TOKEN_A]));
|
||||
let (token, negotiated) = open_negotiated(&mut core, peer(), &authenticated, Some(KEY));
|
||||
let packet = core
|
||||
.handle_datagram(peer(), &echo_request(token, 0, &negotiated, &[], Some(KEY)))
|
||||
.unwrap()
|
||||
.expect("an admissible echo must be answered");
|
||||
assert_eq!(packet.bytes().len() as i64, length);
|
||||
expect_echo_reply(&packet, &negotiated, Some(KEY)).payload
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
signed.len() + irtt_proto::HMAC_SIZE,
|
||||
plain.len(),
|
||||
"authentication takes its 16 bytes from the payload region"
|
||||
);
|
||||
assert!(plain.iter().chain(&signed).all(|byte| *byte == 0xaa));
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use irtt_proto::{Clock, Params, ReceivedStats, StampAt, FLAG_CLOSE};
|
||||
use irtt_proto::{echo_header_len, Clock, Params, ReceivedStats, ServerFill, StampAt, FLAG_CLOSE};
|
||||
|
||||
use super::support::{
|
||||
close_request, core_with_sources, echo_at, echo_params, echo_request,
|
||||
@@ -416,9 +416,20 @@ fn the_maximum_duration_deadline_starts_at_the_first_served_echo() {
|
||||
|
||||
#[test]
|
||||
fn the_first_echo_past_the_maximum_duration_carries_close_and_ends_the_session() {
|
||||
// The session negotiates a fill and a payload region, so this covers the
|
||||
// close's payload as well: a server close is an ordinary echo reply with a
|
||||
// flag added, and it fills exactly as the session's other replies do.
|
||||
let requested = Params {
|
||||
server_fill: Some(ServerFill {
|
||||
value: "pattern:aabb".to_owned(),
|
||||
}),
|
||||
length: (echo_header_len(false, ¶ms_at(SECOND)) + 5) as i64,
|
||||
..params_at(SECOND)
|
||||
};
|
||||
let patterned = vec![0xaa, 0xbb, 0xaa, 0xbb, 0xaa];
|
||||
let (mut core, clock, token, negotiated) = manual_core(
|
||||
max_duration_of(Duration::from_secs(1)),
|
||||
¶ms_at(SECOND),
|
||||
&requested,
|
||||
ScriptedTokens::new([TOKEN_A]),
|
||||
None,
|
||||
);
|
||||
@@ -437,6 +448,7 @@ fn the_first_echo_past_the_maximum_duration_carries_close_and_ends_the_session()
|
||||
.expect("short of the deadline must be answered normally");
|
||||
let reply = expect_echo_reply(&packet, &negotiated, None);
|
||||
assert_eq!(reply.flags & FLAG_CLOSE, 0, "not yet");
|
||||
assert_eq!(reply.payload, patterned, "an ordinary reply's fill");
|
||||
|
||||
// At the deadline exactly.
|
||||
let packet = echo_at(&mut core, &clock, 13 * SECOND, token, 2, &negotiated, None)
|
||||
@@ -449,7 +461,10 @@ fn the_first_echo_past_the_maximum_duration_carries_close_and_ends_the_session()
|
||||
(Some(3), Some(0x7)),
|
||||
"the triggering request is included in the statistics it reports"
|
||||
);
|
||||
assert!(reply.payload.is_empty());
|
||||
assert_eq!(
|
||||
reply.payload, patterned,
|
||||
"the close keeps the session's fill; it is not a special packet"
|
||||
);
|
||||
assert_eq!(
|
||||
packet.bytes().len(),
|
||||
irtt_proto::echo_packet_len(false, &negotiated).unwrap(),
|
||||
|
||||
Reference in new issue
Block a user