refactor(proto): consolidate tests

This commit is contained in:
Nils Andreas Svee committed 2026-10-06 17:02:54 +07:00
1 parent 4c941a7d78
commit 5bc27ef894
13 files changed
+950 -1752

No files matched your search

-169
View File
@@ -304,172 +304,3 @@ fn read_i64(packet: &[u8], pos: &mut usize) -> i64 {
*pos += TIMESTAMP_SIZE; *pos += TIMESTAMP_SIZE;
value value
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::{
hmac,
params::{Clock, ReceivedStats, StampAt},
write_header, FLAG_HMAC, HMAC_SIZE, RECV_COUNT_SIZE, RECV_WINDOW_SIZE,
};
fn default_params() -> Params {
Params {
received_stats: ReceivedStats::Both,
stamp_at: StampAt::Both,
clock: Clock::Both,
..Params::default()
}
}
fn params_with_payload_space(payload_space: usize) -> Params {
let header_len = PacketLayout::echo(false, &Params::default()).header_len();
Params {
length: (header_len + payload_space) as i64,
..Params::default()
}
}
/// Zero-fills every negotiated reply field, matching what a peer that
/// reports no optional values would put on the wire.
fn push_zeroed_layout_tail(layout: PacketLayout, out: &mut Vec<u8>) {
if layout.recv_count {
out.extend_from_slice(&[0; RECV_COUNT_SIZE]);
}
if layout.recv_window {
out.extend_from_slice(&[0; RECV_WINDOW_SIZE]);
}
for _ in 0..layout.timestamp_count() {
out.extend_from_slice(&[0; TIMESTAMP_SIZE]);
}
}
#[test]
fn echo_reply_decodes_default_fields() {
let packet = [
0x14, 0xa7, 0x5b, 0x02, 0x13, 0x52, 0x71, 0x87, 0xab, 0xb6, 0x96, 0x78, 0x02, 0x00,
0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xb8, 0x1a, 0x33, 0x0c, 0x86, 0x6d, 0xaa, 0x18, 0xde, 0x26, 0x35, 0x95, 0x00, 0x00,
0x00, 0x00, 0x80, 0x4d, 0x33, 0x0c, 0x86, 0x6d, 0xaa, 0x18, 0xb2, 0x57, 0x35, 0x95,
0x00, 0x00, 0x00, 0x00,
];
let reply = decode_echo_reply(&packet, &default_params(), None).unwrap();
assert_eq!(reply.token, 0x7896_b6ab_8771_5213);
assert_eq!(reply.sequence, 2);
assert_eq!(reply.recv_count, Some(3));
assert_eq!(reply.recv_window, Some(7));
assert!(reply.timestamps.recv_wall.is_some());
assert!(reply.timestamps.midpoint_wall.is_none());
}
#[test]
fn echo_reply_decodes_exact_negotiated_length_with_payload() {
let params = params_with_payload_space(4);
let mut packet = Vec::new();
write_header(&mut packet, FLAG_REPLY);
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
packet.extend_from_slice(&2_u32.to_le_bytes());
packet.extend_from_slice(&[1, 2, 3, 4]);
let reply = decode_echo_reply(&packet, &params, None).unwrap();
assert_eq!(reply.payload, vec![1, 2, 3, 4]);
}
#[test]
fn echo_reply_rejects_shorter_than_negotiated_length() {
let params = params_with_payload_space(4);
let mut packet = Vec::new();
write_header(&mut packet, FLAG_REPLY);
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
packet.extend_from_slice(&2_u32.to_le_bytes());
assert_eq!(
decode_echo_reply(&packet, &params, None),
Err(ProtoError::PacketLengthMismatch {
expected: 20,
actual: 16,
})
);
}
#[test]
fn echo_reply_rejects_longer_than_negotiated_length() {
let params = Params::default();
let mut packet = Vec::new();
write_header(&mut packet, FLAG_REPLY);
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
packet.extend_from_slice(&2_u32.to_le_bytes());
packet.push(0);
assert_eq!(
decode_echo_reply(&packet, &params, None),
Err(ProtoError::PacketLengthMismatch {
expected: 16,
actual: 17,
})
);
}
#[test]
fn hmac_echo_reply_decodes_default_fields_after_hmac() {
let params = default_params();
let layout = PacketLayout::echo(true, &params);
let mut packet = Vec::with_capacity(layout.header_len());
write_header(&mut packet, FLAG_REPLY | FLAG_HMAC);
packet.extend_from_slice(&[0; HMAC_SIZE]);
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
packet.extend_from_slice(&2_u32.to_le_bytes());
push_zeroed_layout_tail(layout, &mut packet);
hmac::compute_hmac_in_place(b"testkey", &mut packet, hmac::hmac_offset()).unwrap();
assert_eq!(packet.len(), 76);
let reply = decode_echo_reply(&packet, &params, Some(b"testkey")).unwrap();
assert_eq!(reply.token, 0x7896_b6ab_8771_5213);
assert_eq!(reply.sequence, 2);
assert_eq!(reply.recv_count, Some(0));
assert_eq!(reply.recv_window, Some(0));
assert_eq!(reply.payload.len(), 0);
}
#[test]
fn hmac_echo_reply_decodes_exact_negotiated_length_with_payload() {
let mut params = Params {
length: 48,
..Params::default()
};
params.received_stats = ReceivedStats::Both;
let layout = PacketLayout::echo(true, &params);
let mut packet = Vec::with_capacity(echo_packet_len(true, &params).unwrap());
write_header(&mut packet, FLAG_REPLY | FLAG_HMAC);
packet.extend_from_slice(&[0; HMAC_SIZE]);
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
packet.extend_from_slice(&2_u32.to_le_bytes());
push_zeroed_layout_tail(layout, &mut packet);
packet.extend_from_slice(&[1, 2, 3, 4]);
hmac::compute_hmac_in_place(b"testkey", &mut packet, hmac::hmac_offset()).unwrap();
let reply = decode_echo_reply(&packet, &params, Some(b"testkey")).unwrap();
assert_eq!(reply.payload, vec![1, 2, 3, 4]);
}
#[test]
fn hmac_echo_reply_rejects_length_mismatch_before_hmac_verification() {
let params = Params::default();
let mut packet = Vec::new();
write_header(&mut packet, FLAG_REPLY | FLAG_HMAC);
packet.extend_from_slice(&[0; HMAC_SIZE]);
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
packet.extend_from_slice(&2_u32.to_le_bytes());
hmac::compute_hmac_in_place(b"testkey", &mut packet, hmac::hmac_offset()).unwrap();
packet.push(0);
assert_eq!(
decode_echo_reply(&packet, &params, Some(b"testkey")),
Err(ProtoError::PacketLengthMismatch {
expected: 32,
actual: 33,
})
);
}
}
-98
View File
@@ -136,101 +136,3 @@ fn with_hmac_flag(flags: u8, authenticated: bool) -> u8 {
flags & !FLAG_HMAC flags & !FLAG_HMAC
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::{
flags::{FLAG_OPEN, FLAG_REPLY},
MAGIC,
};
const KEY: &[u8] = b"testkey";
const OPEN_REQUEST_RULES: &[FlagRule] =
&[FlagRule::Require(FLAG_OPEN), FlagRule::Reject(FLAG_REPLY)];
fn header(flags: u8) -> Vec<u8> {
let mut packet = MAGIC.to_vec();
packet.push(flags);
packet
}
#[test]
fn decode_rejects_bad_magic_and_reserved_flags() {
assert_eq!(
decode(&[0, MAGIC[1], MAGIC[2], FLAG_OPEN], None, &[]),
Err(ProtoError::BadMagic)
);
assert_eq!(
decode(&header(FLAG_OPEN | 0x10), None, &[]),
Err(ProtoError::ReservedFlags(0x10))
);
}
#[test]
fn decode_enforces_required_and_rejected_flags() {
assert_eq!(
decode(&header(0), None, OPEN_REQUEST_RULES),
Err(ProtoError::MissingFlag(FLAG_OPEN))
);
assert_eq!(
decode(&header(FLAG_OPEN | FLAG_REPLY), None, OPEN_REQUEST_RULES),
Err(ProtoError::UnexpectedFlag(FLAG_REPLY))
);
}
#[test]
fn decode_rejects_hmac_presence_mismatch() {
let mut authenticated = header(FLAG_OPEN | FLAG_HMAC);
authenticated.extend_from_slice(&[0; HMAC_SIZE]);
assert_eq!(
decode(&authenticated, None, OPEN_REQUEST_RULES),
Err(ProtoError::HmacPresenceMismatch)
);
assert_eq!(
decode(&header(FLAG_OPEN), Some(KEY), OPEN_REQUEST_RULES),
Err(ProtoError::HmacPresenceMismatch)
);
}
#[test]
fn structural_decode_reports_hmac_presence_without_a_key() {
let mut authenticated = header(FLAG_OPEN | FLAG_HMAC);
authenticated.extend_from_slice(&[0xff; HMAC_SIZE]);
let envelope = decode_structural(&authenticated).unwrap();
assert!(envelope.hmac_present);
assert_eq!(envelope.body_offset, HEADER_SIZE + HMAC_SIZE);
let envelope = decode_structural(&header(FLAG_OPEN)).unwrap();
assert!(!envelope.hmac_present);
assert_eq!(envelope.body_offset, HEADER_SIZE);
}
#[test]
fn verify_rejects_bad_hmac() {
let mut packet = begin_checked(FLAG_OPEN, Some(KEY), OPEN_REQUEST_RULES, 22).unwrap();
packet.extend_from_slice(&[1, 2]);
let mut packet = finish(packet, Some(KEY)).unwrap();
*packet.last_mut().unwrap() ^= 0x80;
decode(&packet, Some(KEY), OPEN_REQUEST_RULES).unwrap();
assert_eq!(verify(&packet, Some(KEY)), Err(ProtoError::BadHmac));
}
#[test]
fn encoder_key_authoritatively_normalizes_hmac_flag() {
let plain =
begin_checked(FLAG_OPEN | FLAG_HMAC, None, OPEN_REQUEST_RULES, HEADER_SIZE).unwrap();
assert_eq!(plain[3], FLAG_OPEN);
let authenticated = begin_checked(
FLAG_OPEN,
Some(KEY),
OPEN_REQUEST_RULES,
HEADER_SIZE + HMAC_SIZE,
)
.unwrap();
assert_eq!(authenticated[3], FLAG_OPEN | FLAG_HMAC);
}
}
-73
View File
@@ -96,76 +96,3 @@ pub fn verify_packet_hmac(key: &[u8], packet: &[u8]) -> Result<()> {
pub(crate) fn hmac_offset() -> usize { pub(crate) fn hmac_offset() -> usize {
HEADER_SIZE HEADER_SIZE
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::{
flags::{FLAG_HMAC, FLAG_OPEN},
MAGIC,
};
fn reference_copy_zero_hmac(
key: &[u8],
packet: &[u8],
hmac_offset: usize,
) -> Result<[u8; HMAC_SIZE]> {
if packet.len().saturating_sub(hmac_offset) < HMAC_SIZE {
return Err(ProtoError::InvalidHmacOffset);
}
let mut copy = packet.to_vec();
copy[hmac_offset..hmac_offset + HMAC_SIZE].fill(0);
let mut mac = HmacMd5::new_from_slice(key).expect("HMAC accepts keys of any size");
mac.update(&copy);
let bytes = mac.finalize().into_bytes();
let mut out = [0u8; HMAC_SIZE];
out.copy_from_slice(&bytes);
Ok(out)
}
fn packet_with_hmac_field(hmac_bytes: [u8; HMAC_SIZE]) -> Vec<u8> {
let mut packet = Vec::new();
packet.extend_from_slice(&MAGIC);
packet.push(FLAG_OPEN | FLAG_HMAC);
packet.extend_from_slice(&hmac_bytes);
packet.extend_from_slice(&[1, 2, 3, 4]);
packet
}
#[test]
fn compute_and_verify_hmac() {
let mut packet = packet_with_hmac_field([0; HMAC_SIZE]);
compute_hmac_in_place(b"testkey", &mut packet, hmac_offset()).unwrap();
verify_hmac(b"testkey", &packet, hmac_offset()).unwrap();
assert_eq!(
verify_hmac(b"wrong", &packet, hmac_offset()),
Err(ProtoError::BadHmac)
);
}
#[test]
fn compute_hmac_matches_copy_and_zero_semantics() {
let mut packet = packet_with_hmac_field([0xa5; HMAC_SIZE]);
packet.extend_from_slice(&[5, 6, 7, 8, 9]);
let digest = compute_hmac(b"testkey", &packet, hmac_offset()).unwrap();
let reference = reference_copy_zero_hmac(b"testkey", &packet, hmac_offset()).unwrap();
assert_eq!(digest, reference);
}
#[test]
fn verify_hmac_rejects_modified_authenticated_byte() {
let mut packet = packet_with_hmac_field([0; HMAC_SIZE]);
compute_hmac_in_place(b"testkey", &mut packet, hmac_offset()).unwrap();
verify_hmac(b"testkey", &packet, hmac_offset()).unwrap();
packet[hmac_offset() + HMAC_SIZE + 1] ^= 0x01;
assert_eq!(
verify_hmac(b"testkey", &packet, hmac_offset()),
Err(ProtoError::BadHmac)
);
}
}
-191
View File
@@ -162,194 +162,3 @@ pub fn echo_packet_len(hmac: bool, params: &Params) -> Result<usize> {
}; };
Ok(header_len.max(requested)) Ok(header_len.max(requested))
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::params::{Clock, ReceivedStats, StampAt};
fn params(stats: ReceivedStats, stamp_at: StampAt, clock: Clock) -> Params {
Params {
received_stats: stats,
stamp_at,
clock,
..Params::default()
}
}
fn expected_optional_len(stats: ReceivedStats, stamp_at: StampAt, clock: Clock) -> usize {
let stats_len = match stats {
ReceivedStats::None => 0,
ReceivedStats::Count => RECV_COUNT_SIZE,
ReceivedStats::Window => RECV_WINDOW_SIZE,
ReceivedStats::Both => RECV_COUNT_SIZE + RECV_WINDOW_SIZE,
};
let clock_count = match clock {
Clock::Unspecified => 0,
Clock::Wall | Clock::Monotonic => 1,
Clock::Both => 2,
};
let timestamp_groups = match stamp_at {
StampAt::None => 0,
StampAt::Send | StampAt::Receive | StampAt::Midpoint => 1,
StampAt::Both => 2,
};
stats_len + timestamp_groups * clock_count * TIMESTAMP_SIZE
}
#[test]
fn verified_layout_lengths() {
assert_eq!(PacketLayout::open_request(false).header_len(), 4);
assert_eq!(PacketLayout::open_request(true).header_len(), 20);
assert_eq!(PacketLayout::open_reply(false).header_len(), 12);
assert_eq!(PacketLayout::open_reply(true).header_len(), 28);
assert_eq!(PacketLayout::close_request(false).header_len(), 12);
assert_eq!(PacketLayout::close_request(true).header_len(), 28);
assert_eq!(
echo_header_len(
false,
&params(ReceivedStats::None, StampAt::None, Clock::Both)
),
16
);
assert_eq!(
echo_header_len(
false,
&params(ReceivedStats::Count, StampAt::Send, Clock::Wall)
),
28
);
assert_eq!(
echo_header_len(
false,
&params(ReceivedStats::Window, StampAt::Receive, Clock::Monotonic)
),
32
);
assert_eq!(
echo_header_len(
false,
&params(ReceivedStats::Both, StampAt::Midpoint, Clock::Both)
),
44
);
assert_eq!(
echo_header_len(
false,
&params(ReceivedStats::Both, StampAt::Both, Clock::Both)
),
60
);
assert_eq!(
echo_header_len(
true,
&params(ReceivedStats::Both, StampAt::Both, Clock::Both)
),
76
);
}
#[test]
fn layout_matrix_matches_stats_timestamps_clock_and_hmac_rules() {
for stats in [
ReceivedStats::None,
ReceivedStats::Count,
ReceivedStats::Window,
ReceivedStats::Both,
] {
for stamp_at in [
StampAt::None,
StampAt::Send,
StampAt::Receive,
StampAt::Both,
StampAt::Midpoint,
] {
for clock in [Clock::Wall, Clock::Monotonic, Clock::Both] {
for hmac in [false, true] {
let params = params(stats, stamp_at, clock);
let layout = PacketLayout::echo(hmac, &params);
let expected_len = HEADER_SIZE
+ TOKEN_SIZE
+ SEQ_SIZE
+ if hmac { HMAC_SIZE } else { 0 }
+ expected_optional_len(stats, stamp_at, clock);
assert_eq!(
layout.header_len(),
expected_len,
"unexpected length for stats={stats:?} stamp_at={stamp_at:?} clock={clock:?} hmac={hmac}"
);
assert_eq!(layout.recv_count, stats.has_count());
assert_eq!(layout.recv_window, stats.has_window());
assert_eq!(
layout.recv_wall,
matches!(stamp_at, StampAt::Receive | StampAt::Both)
&& clock.has_wall()
);
assert_eq!(
layout.recv_mono,
matches!(stamp_at, StampAt::Receive | StampAt::Both)
&& clock.has_mono()
);
assert_eq!(
layout.midpoint_wall,
matches!(stamp_at, StampAt::Midpoint) && clock.has_wall()
);
assert_eq!(
layout.midpoint_mono,
matches!(stamp_at, StampAt::Midpoint) && clock.has_mono()
);
assert_eq!(
layout.send_wall,
matches!(stamp_at, StampAt::Send | StampAt::Both) && clock.has_wall()
);
assert_eq!(
layout.send_mono,
matches!(stamp_at, StampAt::Send | StampAt::Both) && clock.has_mono()
);
if hmac {
assert_eq!(
echo_header_len(true, &params) - echo_header_len(false, &params),
HMAC_SIZE
);
}
}
}
}
}
}
#[test]
fn negotiated_length_is_floored_at_the_required_field_block() {
// The 60-byte header of this layout is the floor. A negative negotiated
// length is not an error: it asks for nothing beyond the mandatory
// fields, exactly as zero does.
let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
for (length, expected) in [(-4096, 60), (-1, 60), (0, 60), (20, 60), (92, 92)] {
p.length = length;
assert_eq!(
echo_packet_len(false, &p),
Ok(expected),
"unexpected packet length for negotiated length {length}"
);
}
}
/// A positive length wider than `usize` must stay an error rather than
/// becoming a saturated buffer size. Only reachable where `usize` is
/// narrower than `i64`; on a 64-bit target every positive `i64` converts.
#[cfg(target_pointer_width = "32")]
#[test]
fn a_length_wider_than_usize_is_rejected_rather_than_saturated() {
let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
p.length = 5_000_000_000;
assert_eq!(
echo_packet_len(false, &p),
Err(ProtoError::PacketLengthUnrepresentable {
length: 5_000_000_000
})
);
}
}
-50
View File
@@ -98,53 +98,3 @@ pub fn decode_open_reply(packet: &[u8], hmac_key: Option<&[u8]>) -> Result<OpenR
params: Params::decode(&packet[pos..])?, params: Params::decode(&packet[pos..])?,
}) })
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::{
hmac,
params::{Clock, ReceivedStats, StampAt},
FLAG_HMAC, HMAC_SIZE,
};
fn default_params() -> Params {
Params {
protocol_version: 1,
duration_ns: 3_000_000_000,
interval_ns: 1_000_000_000,
received_stats: ReceivedStats::Both,
stamp_at: StampAt::Both,
clock: Clock::Both,
..Params::default()
}
}
#[test]
fn open_reply_decodes_token() {
let mut packet = vec![0x14, 0xa7, 0x5b, 0x03];
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
packet.extend_from_slice(&default_params().encode());
let reply = decode_open_reply(&packet, None).unwrap();
assert_eq!(reply.token, 0x7896_b6ab_8771_5213);
assert_eq!(reply.params, default_params());
}
#[test]
fn hmac_open_reply_decodes_token_after_hmac_field() {
let mut packet = vec![0x14, 0xa7, 0x5b, FLAG_OPEN | FLAG_REPLY | FLAG_HMAC];
packet.extend_from_slice(&[0; HMAC_SIZE]);
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
packet.extend_from_slice(&default_params().encode());
hmac::compute_hmac_in_place(b"testkey", &mut packet, hmac::hmac_offset()).unwrap();
assert_eq!(
packet.len(),
4 + HMAC_SIZE + TOKEN_SIZE + default_params().encode().len()
);
let reply = decode_open_reply(&packet, Some(b"testkey")).unwrap();
assert_eq!(reply.token, 0x7896_b6ab_8771_5213);
assert_eq!(reply.params, default_params());
}
}
-770
View File
@@ -368,773 +368,3 @@ fn read_int(input: &[u8], pos: &mut usize) -> Result<i64> {
*pos += used; *pos += used;
Ok(value) Ok(value)
} }
#[cfg(test)]
mod tests {
use super::*;
fn assert_round_trip(params: Params) {
assert_eq!(Params::decode(&params.encode()), Ok(params));
}
fn encode_server_fill_value(value: &[u8]) -> Vec<u8> {
let mut encoded = Vec::new();
varint::encode_uvarint(9, &mut encoded);
varint::encode_uvarint(value.len() as u64, &mut encoded);
encoded.extend_from_slice(value);
encoded
}
fn encode_int(tag: u64, value: i64) -> Vec<u8> {
let mut encoded = Vec::new();
varint::encode_uvarint(tag, &mut encoded);
varint::encode_varint(value, &mut encoded);
encoded
}
#[test]
fn params_round_trip() {
let params = Params {
protocol_version: 1,
duration_ns: 3_000_000_000,
interval_ns: 1_000_000_000,
length: 1472,
received_stats: ReceivedStats::Both,
stamp_at: StampAt::Both,
clock: Clock::Both,
dscp: 184,
server_fill: Some(ServerFill {
value: "pattern:abc".to_owned(),
}),
};
assert_round_trip(params);
}
#[test]
fn params_round_trip_negotiated_option_modes() {
for received_stats in [
ReceivedStats::None,
ReceivedStats::Count,
ReceivedStats::Window,
ReceivedStats::Both,
] {
assert_round_trip(Params {
protocol_version: 1,
received_stats,
..Params::default()
});
}
for stamp_at in [
StampAt::None,
StampAt::Send,
StampAt::Receive,
StampAt::Both,
StampAt::Midpoint,
] {
assert_round_trip(Params {
protocol_version: 1,
stamp_at,
clock: Clock::Both,
..Params::default()
});
}
for clock in [Clock::Wall, Clock::Monotonic, Clock::Both] {
assert_round_trip(Params {
protocol_version: 1,
stamp_at: StampAt::Both,
clock,
..Params::default()
});
}
}
#[test]
fn params_round_trip_dscp_values_are_encoded_without_shifting() {
// `Params::dscp` is a raw wire byte; encode/decode carry whatever
// value is set without interpreting or shifting it, regardless of
// whether it happens to look like a codepoint (46) or a raw TOS byte
// (184).
for dscp in [0, 46, 63, 64, 184, -1] {
let params = Params {
protocol_version: 1,
dscp,
..Params::default()
};
assert_round_trip(params);
}
let params = Params {
protocol_version: 1,
dscp: 46,
..Params::default()
};
let encoded = params.encode();
assert!(
encoded.windows(2).any(|bytes| bytes == [8, 92]),
"dscp value 46 must be encoded as param value 46"
);
assert!(
!encoded.windows(3).any(|bytes| bytes == [8, 0xf0, 0x02]),
"Params::encode must not shift dscp 46 to 184; any codepoint shift is a caller concern"
);
assert_eq!(Params::decode(&encoded).unwrap().dscp, 46);
}
#[test]
fn server_fill_absent_short_and_max_length_round_trip() {
assert_round_trip(Params {
protocol_version: 1,
server_fill: None,
..Params::default()
});
assert_round_trip(Params {
protocol_version: 1,
server_fill: Some(ServerFill {
value: "rand".to_owned(),
}),
..Params::default()
});
assert_round_trip(Params {
protocol_version: 1,
server_fill: Some(ServerFill {
value: "0123456789abcdef0123456789abcdef".to_owned(),
}),
..Params::default()
});
}
#[test]
fn server_fill_decode_accepts_max_length() {
let value = b"0123456789abcdef0123456789abcdef";
let params = Params::decode(&encode_server_fill_value(value)).unwrap();
assert_eq!(
params.server_fill,
Some(ServerFill {
value: "0123456789abcdef0123456789abcdef".to_owned(),
})
);
}
#[test]
fn server_fill_decode_rejects_oversized_length() {
let value = b"0123456789abcdef0123456789abcdefx";
assert_eq!(
Params::decode(&encode_server_fill_value(value)),
Err(ProtoError::ParameterLengthTooLarge { tag: 9, length: 33 })
);
}
#[test]
fn server_fill_tag_and_length_are_encoded_before_utf8_bytes() {
let params = Params {
protocol_version: 1,
server_fill: Some(ServerFill {
value: "rand".to_owned(),
}),
..Params::default()
};
let encoded = params.encode();
assert!(encoded
.windows(6)
.any(|bytes| bytes == [9, 4, b'r', b'a', b'n', b'd']));
}
#[test]
fn unknown_tags_are_ignored() {
let mut encoded = Vec::new();
varint::encode_uvarint(99, &mut encoded);
varint::encode_varint(123, &mut encoded);
varint::encode_uvarint(1, &mut encoded);
varint::encode_varint(1, &mut encoded);
let params = Params::decode(&encoded).unwrap();
assert_eq!(params.protocol_version, 1);
}
#[test]
fn invalid_received_stats_value_is_rejected() {
let mut encoded = Vec::new();
varint::encode_uvarint(5, &mut encoded);
varint::encode_varint(4, &mut encoded);
assert_eq!(
Params::decode(&encoded),
Err(ProtoError::InvalidEnum {
name: "ReceivedStats",
value: 4,
})
);
}
#[test]
fn invalid_timestamp_value_is_rejected() {
let mut encoded = Vec::new();
varint::encode_uvarint(6, &mut encoded);
varint::encode_varint(5, &mut encoded);
assert_eq!(
Params::decode(&encoded),
Err(ProtoError::InvalidEnum {
name: "StampAt",
value: 5,
})
);
}
#[test]
fn explicit_clock_zero_is_rejected() {
let mut encoded = Vec::new();
varint::encode_uvarint(7, &mut encoded);
varint::encode_varint(0, &mut encoded);
assert_eq!(
Params::decode(&encoded),
Err(ProtoError::InvalidEnum {
name: "Clock",
value: 0,
})
);
assert_eq!(
Params::decode_with_presence(&encoded),
Err(ProtoError::InvalidEnum {
name: "Clock",
value: 0,
})
);
}
#[test]
fn explicit_clock_values_one_to_three_are_accepted() {
for (value, expected) in [(1, Clock::Wall), (2, Clock::Monotonic), (3, Clock::Both)] {
let decoded = Params::decode_with_presence(&encode_int(7, value)).unwrap();
assert_eq!(decoded.params.clock, expected);
assert!(decoded.presence.clock);
}
}
#[test]
fn empty_payload_decodes_to_wire_defaults_with_nothing_present() {
let decoded = Params::decode_with_presence(&[]).unwrap();
assert_eq!(decoded.params, Params::default());
assert_eq!(decoded.params.protocol_version, 0);
assert_eq!(decoded.params.duration_ns, 0);
assert_eq!(decoded.params.interval_ns, 0);
assert_eq!(decoded.params.length, 0);
assert_eq!(decoded.params.dscp, 0);
assert_eq!(decoded.params.received_stats, ReceivedStats::None);
assert_eq!(decoded.params.stamp_at, StampAt::None);
// The wire default clock is zero, not `Both`.
assert_eq!(decoded.params.clock, Clock::Unspecified);
assert!(!decoded.params.clock.has_wall());
assert!(!decoded.params.clock.has_mono());
assert_eq!(decoded.params.server_fill, None);
assert_eq!(decoded.presence, ParamPresence::default());
assert_eq!(
decoded.presence,
ParamPresence {
protocol_version: false,
duration_ns: false,
interval_ns: false,
length: false,
received_stats: false,
stamp_at: false,
clock: false,
dscp: false,
server_fill: false,
}
);
}
#[test]
fn absent_and_explicitly_zero_parameters_share_a_value_but_not_a_presence() {
// Duration and Interval are the pair a server must tell apart: absent is
// accepted, explicitly zero is not.
let absent = Params::decode_with_presence(&encode_int(1, 1)).unwrap();
assert_eq!(absent.params.duration_ns, 0);
assert!(!absent.presence.duration_ns);
assert_eq!(absent.params.interval_ns, 0);
assert!(!absent.presence.interval_ns);
assert_eq!(absent.params.clock, Clock::Unspecified);
assert!(!absent.presence.clock);
let explicit_duration = Params::decode_with_presence(&encode_int(2, 0)).unwrap();
assert_eq!(explicit_duration.params.duration_ns, 0);
assert!(explicit_duration.presence.duration_ns);
let explicit_interval = Params::decode_with_presence(&encode_int(3, 0)).unwrap();
assert_eq!(explicit_interval.params.interval_ns, 0);
assert!(explicit_interval.presence.interval_ns);
}
#[test]
fn every_known_tag_reports_its_own_presence() {
let mut encoded = Vec::new();
for (tag, value) in [
(1, 1),
(2, 5),
(3, 6),
(4, 7),
(5, 3),
(6, 4),
(7, 3),
(8, 184),
] {
encoded.extend_from_slice(&encode_int(tag, value));
}
encoded.extend_from_slice(&encode_server_fill_value(b"rand"));
let decoded = Params::decode_with_presence(&encoded).unwrap();
assert_eq!(
decoded.presence,
ParamPresence {
protocol_version: true,
duration_ns: true,
interval_ns: true,
length: true,
received_stats: true,
stamp_at: true,
clock: true,
dscp: true,
server_fill: true,
}
);
}
#[test]
fn unspecified_clock_is_encoded_by_omission() {
let params = Params {
protocol_version: 1,
clock: Clock::Unspecified,
..Params::default()
};
let encoded = params.encode();
assert_eq!(encoded, encode_int(1, 1));
assert!(
!encoded.windows(2).any(|bytes| bytes[0] == 7),
"an unspecified clock must omit the tag rather than encode an explicit zero"
);
assert_round_trip(params);
}
#[test]
fn decode_and_decode_with_presence_agree() {
let mut payloads = vec![
Vec::new(),
encode_int(1, 1),
encode_int(2, 0),
encode_server_fill_value(b"rand"),
];
payloads.push(
Params {
protocol_version: 1,
duration_ns: 3_000_000_000,
interval_ns: 1_000_000_000,
length: 1472,
received_stats: ReceivedStats::Both,
stamp_at: StampAt::Both,
clock: Clock::Both,
dscp: 184,
server_fill: Some(ServerFill {
value: "rand".to_owned(),
}),
}
.encode(),
);
// Malformed payloads must fail identically through both entry points.
payloads.push(vec![1, 0x80]);
payloads.push(encode_int(7, 0));
for payload in payloads {
assert_eq!(
Params::decode(&payload),
Params::decode_with_presence(&payload).map(|decoded| decoded.params),
"decode and decode_with_presence disagreed on {payload:02x?}"
);
}
}
#[test]
fn malformed_server_fill_is_rejected() {
let encoded = [9, 4, b'a', b'b'];
assert_eq!(Params::decode(&encoded), Err(ProtoError::MalformedParams));
}
#[test]
fn non_utf8_server_fill_is_rejected() {
let encoded = [9, 1, 0xff];
assert_eq!(Params::decode(&encoded), Err(ProtoError::InvalidUtf8));
}
#[cfg(target_pointer_width = "32")]
#[test]
fn server_fill_length_too_large_for_usize_is_rejected() {
let mut encoded = Vec::new();
varint::encode_uvarint(9, &mut encoded);
varint::encode_uvarint(u64::from(u32::MAX) + 1, &mut encoded);
assert_eq!(
Params::decode(&encoded),
Err(ProtoError::ParameterLengthTooLarge {
tag: 9,
length: u64::from(u32::MAX) + 1,
})
);
}
#[test]
fn truncated_varint_parameter_is_rejected() {
let encoded = [1, 0x80];
assert_eq!(Params::decode(&encoded), Err(ProtoError::TruncatedVarint));
}
#[test]
fn truncated_unknown_parameter_value_is_rejected() {
let encoded = [99];
assert_eq!(Params::decode(&encoded), Err(ProtoError::TruncatedVarint));
}
mod properties {
use super::*;
use proptest::prelude::*;
fn received_stats_strategy() -> impl Strategy<Value = ReceivedStats> {
prop_oneof![
Just(ReceivedStats::None),
Just(ReceivedStats::Count),
Just(ReceivedStats::Window),
Just(ReceivedStats::Both),
]
}
fn stamp_at_strategy() -> impl Strategy<Value = StampAt> {
prop_oneof![
Just(StampAt::None),
Just(StampAt::Send),
Just(StampAt::Receive),
Just(StampAt::Both),
Just(StampAt::Midpoint),
]
}
/// A `Clock` valid for direct construction, including `Unspecified`
/// (wire omission). Never produces an explicit zero on the wire; see
/// `explicit_clock_strategy` for that.
fn clock_strategy() -> impl Strategy<Value = Clock> {
prop_oneof![
Just(Clock::Unspecified),
Just(Clock::Wall),
Just(Clock::Monotonic),
Just(Clock::Both),
]
}
/// A `Clock` value valid to encode *explicitly* on the wire (tag 7).
/// Excludes `Unspecified`/zero, which is only ever reached by omission.
fn explicit_clock_value_strategy() -> impl Strategy<Value = i64> {
1_i64..=3
}
fn server_fill_strategy() -> impl Strategy<Value = Option<ServerFill>> {
prop_oneof![
Just(None),
proptest::collection::vec(proptest::char::any(), 0..=8).prop_map(|chars| Some(
ServerFill {
value: chars.into_iter().collect(),
}
)),
]
}
fn params_strategy() -> impl Strategy<Value = Params> {
(
any::<i64>(),
any::<i64>(),
any::<i64>(),
any::<i64>(),
any::<i64>(),
received_stats_strategy(),
stamp_at_strategy(),
clock_strategy(),
server_fill_strategy(),
)
.prop_map(
|(
protocol_version,
duration_ns,
interval_ns,
length,
dscp,
received_stats,
stamp_at,
clock,
server_fill,
)| Params {
protocol_version,
duration_ns,
interval_ns,
length,
received_stats,
stamp_at,
clock,
dscp,
server_fill,
},
)
}
fn encode_tagged_int(tag: u64, value: i64, out: &mut Vec<u8>) {
varint::encode_uvarint(tag, out);
varint::encode_varint(value, out);
}
fn encode_tagged_server_fill(value: &str, out: &mut Vec<u8>) {
varint::encode_uvarint(9, out);
varint::encode_uvarint(value.len() as u64, out);
out.extend_from_slice(value.as_bytes());
}
proptest! {
#[test]
fn valid_params_round_trip(params in params_strategy()) {
let encoded = params.encode();
let decoded = Params::decode(&encoded).unwrap();
prop_assert_eq!(decoded, params);
}
#[test]
fn encoded_param_presence_matches_omission_rules(params in params_strategy()) {
let encoded = params.encode();
let decoded = Params::decode_with_presence(&encoded).unwrap();
prop_assert_eq!(&decoded.params, &params);
prop_assert_eq!(decoded.presence.protocol_version, params.protocol_version != 0);
prop_assert_eq!(decoded.presence.duration_ns, params.duration_ns != 0);
prop_assert_eq!(decoded.presence.interval_ns, params.interval_ns != 0);
prop_assert_eq!(decoded.presence.length, params.length != 0);
prop_assert_eq!(decoded.presence.dscp, params.dscp != 0);
prop_assert_eq!(
decoded.presence.received_stats,
params.received_stats != ReceivedStats::None
);
prop_assert_eq!(decoded.presence.stamp_at, params.stamp_at != StampAt::None);
prop_assert_eq!(decoded.presence.clock, params.clock != Clock::Unspecified);
prop_assert_eq!(decoded.presence.server_fill, params.server_fill.is_some());
}
#[test]
fn repeated_protocol_version_is_last_wins(first: i64, second: i64) {
let mut encoded = Vec::new();
encode_tagged_int(1, first, &mut encoded);
encode_tagged_int(1, second, &mut encoded);
let decoded = Params::decode_with_presence(&encoded).unwrap();
prop_assert_eq!(decoded.params.protocol_version, second);
prop_assert!(decoded.presence.protocol_version);
}
#[test]
fn repeated_duration_ns_is_last_wins(first: i64, second: i64) {
let mut encoded = Vec::new();
encode_tagged_int(2, first, &mut encoded);
encode_tagged_int(2, second, &mut encoded);
let decoded = Params::decode_with_presence(&encoded).unwrap();
prop_assert_eq!(decoded.params.duration_ns, second);
prop_assert!(decoded.presence.duration_ns);
}
#[test]
fn repeated_interval_ns_is_last_wins(first: i64, second: i64) {
let mut encoded = Vec::new();
encode_tagged_int(3, first, &mut encoded);
encode_tagged_int(3, second, &mut encoded);
let decoded = Params::decode_with_presence(&encoded).unwrap();
prop_assert_eq!(decoded.params.interval_ns, second);
prop_assert!(decoded.presence.interval_ns);
}
#[test]
fn repeated_length_is_last_wins(first: i64, second: i64) {
let mut encoded = Vec::new();
encode_tagged_int(4, first, &mut encoded);
encode_tagged_int(4, second, &mut encoded);
let decoded = Params::decode_with_presence(&encoded).unwrap();
prop_assert_eq!(decoded.params.length, second);
prop_assert!(decoded.presence.length);
}
#[test]
fn repeated_received_stats_is_last_wins(first in 0_i64..=3, second in 0_i64..=3) {
let mut encoded = Vec::new();
encode_tagged_int(5, first, &mut encoded);
encode_tagged_int(5, second, &mut encoded);
let decoded = Params::decode_with_presence(&encoded).unwrap();
prop_assert_eq!(decoded.params.received_stats, ReceivedStats::try_from(second).unwrap());
prop_assert!(decoded.presence.received_stats);
}
#[test]
fn repeated_stamp_at_is_last_wins(first in 0_i64..=4, second in 0_i64..=4) {
let mut encoded = Vec::new();
encode_tagged_int(6, first, &mut encoded);
encode_tagged_int(6, second, &mut encoded);
let decoded = Params::decode_with_presence(&encoded).unwrap();
prop_assert_eq!(decoded.params.stamp_at, StampAt::try_from(second).unwrap());
prop_assert!(decoded.presence.stamp_at);
}
#[test]
fn repeated_clock_is_last_wins(
first in explicit_clock_value_strategy(),
second in explicit_clock_value_strategy(),
) {
let mut encoded = Vec::new();
encode_tagged_int(7, first, &mut encoded);
encode_tagged_int(7, second, &mut encoded);
let decoded = Params::decode_with_presence(&encoded).unwrap();
prop_assert_eq!(decoded.params.clock, Clock::try_from(second).unwrap());
prop_assert!(decoded.presence.clock);
}
#[test]
fn repeated_dscp_is_last_wins(first: i64, second: i64) {
let mut encoded = Vec::new();
encode_tagged_int(8, first, &mut encoded);
encode_tagged_int(8, second, &mut encoded);
let decoded = Params::decode_with_presence(&encoded).unwrap();
prop_assert_eq!(decoded.params.dscp, second);
prop_assert!(decoded.presence.dscp);
}
#[test]
fn repeated_server_fill_is_last_wins(
first in proptest::collection::vec(proptest::char::any(), 0..=8)
.prop_map(|chars| chars.into_iter().collect::<String>()),
second in proptest::collection::vec(proptest::char::any(), 0..=8)
.prop_map(|chars| chars.into_iter().collect::<String>()),
) {
let mut encoded = Vec::new();
encode_tagged_server_fill(&first, &mut encoded);
encode_tagged_server_fill(&second, &mut encoded);
let decoded = Params::decode_with_presence(&encoded).unwrap();
prop_assert_eq!(
decoded.params.server_fill,
Some(ServerFill { value: second })
);
prop_assert!(decoded.presence.server_fill);
}
#[test]
fn unknown_scalar_tag_prefix_does_not_disturb_known_params(
params in params_strategy(),
unknown_tag in 10_u64..=u16::MAX as u64,
unknown_value: u64,
) {
let mut prefixed = Vec::new();
varint::encode_uvarint(unknown_tag, &mut prefixed);
varint::encode_uvarint(unknown_value, &mut prefixed);
prefixed.extend_from_slice(&params.encode());
let decoded = Params::decode(&prefixed).unwrap();
prop_assert_eq!(decoded, params.clone());
let decoded_with_presence = Params::decode_with_presence(&prefixed).unwrap();
let expected_presence = Params::decode_with_presence(&params.encode()).unwrap().presence;
prop_assert_eq!(decoded_with_presence.presence, expected_presence);
}
#[test]
fn unknown_scalar_tag_suffix_does_not_disturb_known_params(
params in params_strategy(),
unknown_tag in 10_u64..=u16::MAX as u64,
unknown_value: u64,
) {
let mut suffixed = params.encode();
varint::encode_uvarint(unknown_tag, &mut suffixed);
varint::encode_uvarint(unknown_value, &mut suffixed);
let decoded = Params::decode(&suffixed).unwrap();
prop_assert_eq!(decoded, params.clone());
let decoded_with_presence = Params::decode_with_presence(&suffixed).unwrap();
let expected_presence = Params::decode_with_presence(&params.encode()).unwrap().presence;
prop_assert_eq!(decoded_with_presence.presence, expected_presence);
}
}
/// Omitted-vs-explicit-zero presence, for the fields where explicit
/// zero is valid on the wire. `Clock` is deliberately excluded: an
/// explicit zero there is rejected outright, not merely "present with
/// the default value" (see `explicit_clock_zero_is_rejected`).
#[test]
fn omitted_vs_explicit_zero_presence_for_zero_valid_fields() {
let scalar_tags = [1_u64, 2, 3, 4, 8];
for tag in scalar_tags {
let omitted = Params::decode_with_presence(&[]).unwrap();
let mut explicit_zero = Vec::new();
encode_tagged_int(tag, 0, &mut explicit_zero);
let explicit = Params::decode_with_presence(&explicit_zero).unwrap();
assert_eq!(omitted.params, explicit.params);
assert_eq!(explicit.params, Params::default());
let (omitted_presence, explicit_presence) = match tag {
1 => (
omitted.presence.protocol_version,
explicit.presence.protocol_version,
),
2 => (omitted.presence.duration_ns, explicit.presence.duration_ns),
3 => (omitted.presence.interval_ns, explicit.presence.interval_ns),
4 => (omitted.presence.length, explicit.presence.length),
8 => (omitted.presence.dscp, explicit.presence.dscp),
_ => unreachable!(),
};
assert!(!omitted_presence);
assert!(explicit_presence);
}
let enum_tags = [5_u64, 6];
for tag in enum_tags {
let omitted = Params::decode_with_presence(&[]).unwrap();
let mut explicit_zero = Vec::new();
encode_tagged_int(tag, 0, &mut explicit_zero);
let explicit = Params::decode_with_presence(&explicit_zero).unwrap();
assert_eq!(omitted.params, explicit.params);
let (omitted_presence, explicit_presence) = match tag {
5 => (
omitted.presence.received_stats,
explicit.presence.received_stats,
),
6 => (omitted.presence.stamp_at, explicit.presence.stamp_at),
_ => unreachable!(),
};
assert!(!omitted_presence);
assert!(explicit_presence);
}
// `Clock` deliberately breaks the pattern above: explicit zero is
// rejected rather than accepted-but-present.
let omitted = Params::decode_with_presence(&[]).unwrap();
assert_eq!(omitted.params.clock, Clock::Unspecified);
assert!(!omitted.presence.clock);
let mut explicit_clock_zero = Vec::new();
encode_tagged_int(7, 0, &mut explicit_clock_zero);
assert_eq!(
Params::decode_with_presence(&explicit_clock_zero),
Err(ProtoError::InvalidEnum {
name: "Clock",
value: 0,
})
);
}
}
}
-109
View File
@@ -59,112 +59,3 @@ pub fn decode_varint(input: &[u8]) -> Result<(i64, usize)> {
let (value, used) = decode_uvarint(input)?; let (value, used) = decode_uvarint(input)?;
Ok((zigzag_decode(value), used)) Ok((zigzag_decode(value), used))
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uvarint_round_trips() {
for value in [
0,
1,
2,
127,
128,
255,
16_384,
u32::MAX as u64,
i64::MAX as u64,
u64::MAX,
] {
let mut encoded = Vec::new();
encode_uvarint(value, &mut encoded);
assert_eq!(decode_uvarint(&encoded), Ok((value, encoded.len())));
}
}
#[test]
fn signed_varint_round_trips() {
for value in [
i64::MIN,
-9_223_372_036,
-2,
-1,
0,
1,
2,
1_000_000_000,
3_000_000_000,
i64::MAX,
] {
let mut encoded = Vec::new();
encode_varint(value, &mut encoded);
assert_eq!(decode_varint(&encoded), Ok((value, encoded.len())));
}
}
#[test]
fn verified_byte_examples() {
let cases: &[(i64, &[u8])] = &[
(1, &[0x02]),
(3_000_000_000, &[0x80, 0xf8, 0x82, 0xad, 0x16]),
(1_000_000_000, &[0x80, 0xa8, 0xd6, 0xb9, 0x07]),
(1472, &[0x80, 0x17]),
(3, &[0x06]),
(184, &[0xf0, 0x02]),
];
for (value, expected) in cases {
let mut encoded = Vec::new();
encode_varint(*value, &mut encoded);
assert_eq!(&encoded, expected);
assert_eq!(decode_varint(expected), Ok((*value, expected.len())));
}
let mut tag = Vec::new();
encode_uvarint(9, &mut tag);
assert_eq!(tag, [0x09]);
tag.clear();
encode_uvarint(24, &mut tag);
assert_eq!(tag, [0x18]);
}
mod properties {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn uvarint_round_trips(value: u64) {
let mut encoded = Vec::new();
encode_uvarint(value, &mut encoded);
prop_assert!(!encoded.is_empty());
prop_assert!(encoded.len() <= 10);
let (decoded, used) = decode_uvarint(&encoded).unwrap();
prop_assert_eq!(decoded, value);
prop_assert_eq!(used, encoded.len());
}
#[test]
fn signed_varint_round_trips(value: i64) {
let mut encoded = Vec::new();
encode_varint(value, &mut encoded);
prop_assert!(!encoded.is_empty());
prop_assert!(encoded.len() <= 10);
let (decoded, used) = decode_varint(&encoded).unwrap();
prop_assert_eq!(decoded, value);
prop_assert_eq!(used, encoded.len());
}
#[test]
fn zigzag_round_trips_from_signed(value: i64) {
prop_assert_eq!(zigzag_decode(zigzag_encode(value)), value);
}
#[test]
fn zigzag_round_trips_from_unsigned(value: u64) {
prop_assert_eq!(zigzag_encode(zigzag_decode(value)), value);
}
}
}
}
+61
View File
@@ -0,0 +1,61 @@
use irtt_proto::{compute_hmac, compute_hmac_in_place, verify_hmac, ProtoError};
fn captured_open() -> Vec<u8> {
// Captured vector 6; provenance and independently checked digest are in
// docs/protocol/test-vectors/README.md. This is an oracle for the primitive.
"14 a7 5b 09 ff 90 16 a7 aa 53 78 16 9e c3 a2 d5 54 dc 30 36
01 02 02 80 d0 ac f3 0e 03 80 a8 d6 b9 07 05 06 06 06 07 06"
.split_whitespace()
.map(|byte| u8::from_str_radix(byte, 16).unwrap())
.collect()
}
#[test]
fn computing_hmac_ignores_existing_digest_and_only_replaces_its_field() {
let expected = captured_open();
let mut packet = expected.clone();
packet[4..20].fill(0xa5);
let before = packet.clone();
assert_eq!(
compute_hmac(b"testkey", &packet, 4).unwrap().as_slice(),
&expected[4..20]
);
assert_eq!(packet, before);
compute_hmac_in_place(b"testkey", &mut packet, 4).unwrap();
assert_eq!(packet, expected);
}
#[test]
fn verification_rejects_wrong_key_and_modified_authenticated_bytes() {
let packet = captured_open();
verify_hmac(b"testkey", &packet, 4).unwrap();
assert_eq!(verify_hmac(b"wrong", &packet, 4), Err(ProtoError::BadHmac));
for offset in [0, 4, 21, packet.len() - 1] {
let mut modified = packet.clone();
modified[offset] ^= 1;
assert_eq!(
verify_hmac(b"testkey", &modified, 4),
Err(ProtoError::BadHmac)
);
}
}
#[test]
fn incomplete_or_out_of_bounds_digest_fields_are_rejected_without_mutation() {
for offset in [5, 20, 21, usize::MAX] {
let mut packet = vec![0xa5; 20];
assert_eq!(
compute_hmac(b"key", &packet, offset),
Err(ProtoError::InvalidHmacOffset)
);
assert_eq!(
verify_hmac(b"key", &packet, offset),
Err(ProtoError::InvalidHmacOffset)
);
assert_eq!(
compute_hmac_in_place(b"key", &mut packet, offset),
Err(ProtoError::InvalidHmacOffset)
);
assert_eq!(packet, [0xa5; 20]);
}
}
+165
View File
@@ -0,0 +1,165 @@
use irtt_proto::params::{Clock, ReceivedStats, StampAt};
use irtt_proto::{
echo_header_len, echo_packet_len, PacketLayout, Params, HMAC_SIZE, RECV_COUNT_SIZE,
RECV_WINDOW_SIZE, SEQ_SIZE, TIMESTAMP_SIZE, TOKEN_SIZE,
};
const HEADER_SIZE: usize = 4;
fn params(stats: ReceivedStats, stamp_at: StampAt, clock: Clock) -> Params {
Params {
received_stats: stats,
stamp_at,
clock,
..Params::default()
}
}
fn expected_optional_len(stats: ReceivedStats, stamp_at: StampAt, clock: Clock) -> usize {
let stats_len = match stats {
ReceivedStats::None => 0,
ReceivedStats::Count => RECV_COUNT_SIZE,
ReceivedStats::Window => RECV_WINDOW_SIZE,
ReceivedStats::Both => RECV_COUNT_SIZE + RECV_WINDOW_SIZE,
};
let clock_count = match clock {
Clock::Unspecified => 0,
Clock::Wall | Clock::Monotonic => 1,
Clock::Both => 2,
};
let timestamp_groups = match stamp_at {
StampAt::None => 0,
StampAt::Send | StampAt::Receive | StampAt::Midpoint => 1,
StampAt::Both => 2,
};
stats_len + timestamp_groups * clock_count * TIMESTAMP_SIZE
}
#[test]
fn open_and_close_layout_lengths() {
assert_eq!(PacketLayout::open_request(false).header_len(), 4);
assert_eq!(PacketLayout::open_request(true).header_len(), 20);
assert_eq!(PacketLayout::open_reply(false).header_len(), 12);
assert_eq!(PacketLayout::open_reply(true).header_len(), 28);
assert_eq!(PacketLayout::close_request(false).header_len(), 12);
assert_eq!(PacketLayout::close_request(true).header_len(), 28);
}
#[test]
fn layout_matrix_matches_stats_timestamps_clock_and_hmac_rules() {
for stats in [
ReceivedStats::None,
ReceivedStats::Count,
ReceivedStats::Window,
ReceivedStats::Both,
] {
for stamp_at in [
StampAt::None,
StampAt::Send,
StampAt::Receive,
StampAt::Both,
StampAt::Midpoint,
] {
for clock in [
Clock::Unspecified,
Clock::Wall,
Clock::Monotonic,
Clock::Both,
] {
for hmac in [false, true] {
let params = params(stats, stamp_at, clock);
let layout = PacketLayout::echo(hmac, &params);
let expected_len = HEADER_SIZE
+ TOKEN_SIZE
+ SEQ_SIZE
+ if hmac { HMAC_SIZE } else { 0 }
+ expected_optional_len(stats, stamp_at, clock);
assert_eq!(
layout.header_len(),
expected_len,
"unexpected length for stats={stats:?} stamp_at={stamp_at:?} clock={clock:?} hmac={hmac}"
);
assert_eq!(
layout.recv_count,
matches!(stats, ReceivedStats::Count | ReceivedStats::Both)
);
assert_eq!(
layout.recv_window,
matches!(stats, ReceivedStats::Window | ReceivedStats::Both)
);
assert_eq!(
layout.recv_wall,
matches!(stamp_at, StampAt::Receive | StampAt::Both)
&& matches!(clock, Clock::Wall | Clock::Both)
);
assert_eq!(
layout.recv_mono,
matches!(stamp_at, StampAt::Receive | StampAt::Both)
&& matches!(clock, Clock::Monotonic | Clock::Both)
);
assert_eq!(
layout.midpoint_wall,
matches!(stamp_at, StampAt::Midpoint)
&& matches!(clock, Clock::Wall | Clock::Both)
);
assert_eq!(
layout.midpoint_mono,
matches!(stamp_at, StampAt::Midpoint)
&& matches!(clock, Clock::Monotonic | Clock::Both)
);
assert_eq!(
layout.send_wall,
matches!(stamp_at, StampAt::Send | StampAt::Both)
&& matches!(clock, Clock::Wall | Clock::Both)
);
assert_eq!(
layout.send_mono,
matches!(stamp_at, StampAt::Send | StampAt::Both)
&& matches!(clock, Clock::Monotonic | Clock::Both)
);
if hmac {
assert_eq!(
echo_header_len(true, &params) - echo_header_len(false, &params),
HMAC_SIZE
);
}
}
}
}
}
}
#[test]
fn negotiated_length_is_floored_at_the_required_field_block() {
// The 60-byte header of this layout is the floor. A negative negotiated
// length is not an error: it asks for nothing beyond the mandatory
// fields, exactly as zero does.
let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
for (length, expected) in [(-4096, 60), (-1, 60), (0, 60), (20, 60), (92, 92)] {
p.length = length;
assert_eq!(
echo_packet_len(false, &p),
Ok(expected),
"unexpected packet length for negotiated length {length}"
);
}
}
/// A positive length wider than `usize` must stay an error rather than
/// becoming a saturated buffer size. Only reachable where `usize` is
/// narrower than `i64`; on a 64-bit target every positive `i64` converts.
#[cfg(target_pointer_width = "32")]
#[test]
fn a_length_wider_than_usize_is_rejected_rather_than_saturated() {
let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
p.length = 5_000_000_000;
assert_eq!(
echo_packet_len(false, &p),
Err(irtt_proto::ProtoError::PacketLengthUnrepresentable {
length: 5_000_000_000
})
);
}
+399
View File
@@ -0,0 +1,399 @@
use irtt_proto::{
varint, Clock, ParamPresence, Params, ProtoError, ReceivedStats, ServerFill, StampAt,
};
use proptest::prelude::*;
fn int(tag: u64, value: i64) -> Vec<u8> {
let mut out = Vec::new();
varint::encode_uvarint(tag, &mut out);
varint::encode_varint(value, &mut out);
out
}
fn fill(value: &[u8]) -> Vec<u8> {
let mut out = vec![9];
varint::encode_uvarint(value.len() as u64, &mut out);
out.extend_from_slice(value);
out
}
fn presence(p: ParamPresence) -> [bool; 9] {
[
p.protocol_version,
p.duration_ns,
p.interval_ns,
p.length,
p.received_stats,
p.stamp_at,
p.clock,
p.dscp,
p.server_fill,
]
}
fn assert_error(bytes: &[u8], expected: ProtoError) {
assert_eq!(Params::decode(bytes).unwrap_err(), expected, "{bytes:02x?}");
assert_eq!(
Params::decode_with_presence(bytes).unwrap_err(),
expected,
"{bytes:02x?}"
);
}
#[test]
fn known_encodings_preserve_raw_dscp_and_utf8_byte_lengths() {
for (params, bytes) in [
(
Params {
dscp: 46,
..Params::default()
},
vec![8, 92],
),
(
Params {
dscp: 184,
..Params::default()
},
vec![8, 0xf0, 2],
),
(
Params {
server_fill: Some(ServerFill {
value: "rand".into(),
}),
..Params::default()
},
vec![9, 4, b'r', b'a', b'n', b'd'],
),
(
Params {
server_fill: Some(ServerFill { value: "é".into() }),
..Params::default()
},
vec![9, 2, 0xc3, 0xa9],
),
] {
assert_eq!(params.encode(), bytes);
assert_eq!(Params::decode(&bytes).unwrap(), params);
}
}
#[test]
fn wire_defaults_and_explicit_zero_presence() {
let defaults = Params {
protocol_version: 0,
duration_ns: 0,
interval_ns: 0,
length: 0,
received_stats: ReceivedStats::None,
stamp_at: StampAt::None,
clock: Clock::Unspecified,
dscp: 0,
server_fill: None,
};
let absent = Params::decode_with_presence(&[]).unwrap();
assert_eq!(absent.params, defaults);
assert_eq!(presence(absent.presence), [false; 9]);
assert_eq!(defaults.encode(), Vec::<u8>::new());
assert!(!defaults.clock.has_wall());
assert!(!defaults.clock.has_mono());
// Clock is the sole integer tag for which explicit zero is invalid.
for tag in [1, 2, 3, 4, 5, 6, 8] {
let bytes = int(tag, 0);
let decoded = Params::decode_with_presence(&bytes).unwrap();
assert_eq!(decoded.params, defaults);
let mut expected = [false; 9];
expected[tag as usize - 1] = true;
assert_eq!(presence(decoded.presence), expected, "tag {tag}");
assert_eq!(Params::decode(&bytes).unwrap(), decoded.params);
}
let empty_fill = Params::decode_with_presence(&[9, 0]).unwrap();
assert_eq!(
empty_fill.params.server_fill,
Some(ServerFill {
value: String::new()
})
);
assert_eq!(
presence(empty_fill.presence),
[false, false, false, false, false, false, false, false, true]
);
assert_eq!(empty_fill.params.encode(), [9, 0]);
assert_eq!(
Params {
protocol_version: 1,
..defaults
}
.encode(),
[1, 2]
);
}
#[test]
fn known_tags_map_to_their_fields_and_report_isolated_presence() {
for (tag, value, expected) in [
(
1,
1,
Params {
protocol_version: 1,
..Params::default()
},
),
(
2,
5,
Params {
duration_ns: 5,
..Params::default()
},
),
(
3,
6,
Params {
interval_ns: 6,
..Params::default()
},
),
(
4,
7,
Params {
length: 7,
..Params::default()
},
),
(
8,
184,
Params {
dscp: 184,
..Params::default()
},
),
] {
let decoded = Params::decode_with_presence(&int(tag, value)).unwrap();
assert_eq!(decoded.params, expected);
let mut expected_presence = [false; 9];
expected_presence[tag as usize - 1] = true;
assert_eq!(presence(decoded.presence), expected_presence);
}
for (tag, modes) in [
(
5,
vec![
ReceivedStats::None as i64,
ReceivedStats::Count as i64,
ReceivedStats::Window as i64,
ReceivedStats::Both as i64,
],
),
(
6,
vec![
StampAt::None as i64,
StampAt::Send as i64,
StampAt::Receive as i64,
StampAt::Both as i64,
StampAt::Midpoint as i64,
],
),
(
7,
vec![
Clock::Wall as i64,
Clock::Monotonic as i64,
Clock::Both as i64,
],
),
] {
for (index, mode) in modes.into_iter().enumerate() {
let value = index as i64 + if tag == 7 { 1 } else { 0 };
let decoded = Params::decode_with_presence(&int(tag, value)).unwrap();
let actual = match tag {
5 => decoded.params.received_stats as i64,
6 => decoded.params.stamp_at as i64,
7 => decoded.params.clock as i64,
_ => unreachable!(),
};
assert_eq!(actual, mode);
let mut expected_presence = [false; 9];
expected_presence[tag as usize - 1] = true;
assert_eq!(presence(decoded.presence), expected_presence);
}
}
}
#[test]
fn malformed_values_and_invalid_enums_are_rejected_by_both_decoders() {
for (tag, value, name) in [
(5, 4, "ReceivedStats"),
(6, 5, "StampAt"),
(7, 0, "Clock"),
(7, 4, "Clock"),
(5, -1, "ReceivedStats"),
(6, -1, "StampAt"),
(7, -1, "Clock"),
] {
assert_error(&int(tag, value), ProtoError::InvalidEnum { name, value });
}
for bytes in [&[1, 0x80][..], &[99], &[0x80], &[9, 0x80]] {
assert_error(bytes, ProtoError::TruncatedVarint);
}
}
#[test]
fn server_fill_accepts_valid_bytes_and_enforces_length_and_utf8_boundaries() {
for value in [
"",
"rand",
"0123456789abcdef0123456789abcdef",
"éééééééééééééééé",
] {
let expected = Params {
server_fill: Some(ServerFill {
value: value.into(),
}),
..Params::default()
};
let bytes = fill(value.as_bytes());
assert_eq!(expected.encode(), bytes);
let decoded = Params::decode_with_presence(&bytes).unwrap();
assert_eq!(decoded.params, expected);
assert!(decoded.presence.server_fill);
assert_eq!(Params::decode(&bytes).unwrap(), expected);
}
assert_error(
&fill(b"0123456789abcdef0123456789abcdefx"),
ProtoError::ParameterLengthTooLarge { tag: 9, length: 33 },
);
assert_error(&[9, 4, b'a', b'b'], ProtoError::MalformedParams);
assert_error(&[9, 1, 0xff], ProtoError::InvalidUtf8);
}
#[cfg(target_pointer_width = "32")]
#[test]
fn server_fill_length_too_large_for_usize_is_rejected() {
let mut bytes = vec![9];
let length = u64::from(u32::MAX) + 1;
varint::encode_uvarint(length, &mut bytes);
assert_error(
&bytes,
ProtoError::ParameterLengthTooLarge { tag: 9, length },
);
}
fn string_strategy() -> impl Strategy<Value = String> {
// Eight arbitrary Unicode scalars occupy at most the allowed 32 bytes.
proptest::collection::vec(any::<char>(), 0..=8).prop_map(|chars| chars.into_iter().collect())
}
fn params_strategy() -> impl Strategy<Value = Params> {
(
any::<[i64; 5]>(),
prop::sample::select(vec![
ReceivedStats::None,
ReceivedStats::Count,
ReceivedStats::Window,
ReceivedStats::Both,
]),
prop::sample::select(vec![
StampAt::None,
StampAt::Send,
StampAt::Receive,
StampAt::Both,
StampAt::Midpoint,
]),
prop::sample::select(vec![
Clock::Unspecified,
Clock::Wall,
Clock::Monotonic,
Clock::Both,
]),
prop::option::of(string_strategy()),
)
.prop_map(
|(scalar, received_stats, stamp_at, clock, server_fill)| Params {
protocol_version: scalar[0],
duration_ns: scalar[1],
interval_ns: scalar[2],
length: scalar[3],
dscp: scalar[4],
received_stats,
stamp_at,
clock,
server_fill: server_fill.map(|value| ServerFill { value }),
},
)
}
fn all_tags(params: &Params) -> Vec<u8> {
let mut out = Vec::new();
// Deliberately bypass encode's omission: duplicate zero values must be present.
for (tag, value) in [
(1, params.protocol_version),
(2, params.duration_ns),
(3, params.interval_ns),
(4, params.length),
(5, params.received_stats as i64),
(6, params.stamp_at as i64),
(7, params.clock as i64),
(8, params.dscp),
] {
out.extend_from_slice(&int(tag, value));
}
out.extend_from_slice(&fill(params.server_fill.as_ref().unwrap().value.as_bytes()));
out
}
proptest! {
#[test]
fn valid_params_round_trip_with_omission_presence(params in params_strategy()) {
let encoded = params.encode();
let decoded = Params::decode_with_presence(&encoded).unwrap();
prop_assert_eq!(&decoded.params, &params);
prop_assert_eq!(Params::decode(&encoded).unwrap(), params.clone());
prop_assert_eq!(presence(decoded.presence), [
params.protocol_version != 0, params.duration_ns != 0,
params.interval_ns != 0, params.length != 0,
params.received_stats != ReceivedStats::None, params.stamp_at != StampAt::None,
params.clock != Clock::Unspecified, params.dscp != 0, params.server_fill.is_some(),
]);
}
#[test]
fn repeated_known_tags_are_last_wins(
mut first in params_strategy(), mut second in params_strategy(),
first_clock in 1_i64..=3, second_clock in 1_i64..=3,
first_fill in string_strategy(), second_fill in string_strategy(),
) {
first.clock = Clock::try_from(first_clock).unwrap();
second.clock = Clock::try_from(second_clock).unwrap();
first.server_fill = Some(ServerFill { value: first_fill });
second.server_fill = Some(ServerFill { value: second_fill });
let mut bytes = all_tags(&first);
bytes.extend_from_slice(&all_tags(&second));
let decoded = Params::decode_with_presence(&bytes).unwrap();
prop_assert_eq!(&decoded.params, &second);
prop_assert_eq!(Params::decode(&bytes).unwrap(), second);
prop_assert_eq!(presence(decoded.presence), [true; 9]);
}
#[test]
fn unknown_scalar_tags_do_not_disturb_known_params(
params in params_strategy(), tag in 10_u64..=u16::MAX as u64, value: u64,
) {
let encoded = params.encode();
let expected = Params::decode_with_presence(&encoded).unwrap();
let mut unknown = Vec::new();
varint::encode_uvarint(tag, &mut unknown);
varint::encode_uvarint(value, &mut unknown);
for bytes in [[unknown.as_slice(), &encoded].concat(), [&encoded, unknown.as_slice()].concat()] {
prop_assert_eq!(Params::decode_with_presence(&bytes).unwrap(), expected.clone());
prop_assert_eq!(Params::decode(&bytes).unwrap(), params.clone());
}
}
}
@@ -54,34 +54,42 @@ fn echo_reply(params: &Params, flags: u8) -> EchoReply {
} }
#[test] #[test]
fn authenticated_reply_encoders_set_hmac_from_key() { fn reply_encoders_normalize_hmac_flags_from_the_key() {
let params = params(); let params = params();
let open_reply = OpenReply { for key in [None, Some(KEY)] {
flags: FLAG_OPEN | FLAG_REPLY, for supplied_hmac in [0, FLAG_HMAC] {
let auth = if key.is_some() { FLAG_HMAC } else { 0 };
let open = OpenReply {
flags: FLAG_OPEN | FLAG_REPLY | supplied_hmac,
token: TOKEN, token: TOKEN,
params: params.clone(), params: params.clone(),
}; };
let packet = encode_open_reply(&open_reply, Some(KEY)).unwrap(); let packet = encode_open_reply(&open, key).unwrap();
assert_eq!(packet[3], FLAG_OPEN | FLAG_REPLY | FLAG_HMAC); assert_eq!(packet[3], FLAG_OPEN | FLAG_REPLY | auth);
let offset = if key.is_some() { 20 } else { 4 };
assert_eq!(&packet[offset..offset + 8], &TOKEN.to_le_bytes());
assert_eq!( assert_eq!(
decode_open_reply(&packet, Some(KEY)), decode_open_reply(&packet, key).unwrap(),
Ok(OpenReply { OpenReply {
flags: FLAG_OPEN | FLAG_REPLY | FLAG_HMAC, flags: packet[3],
..open_reply ..open
}) }
); );
let echo = echo_reply(&params, FLAG_REPLY | supplied_hmac);
let echo_reply = echo_reply(&params, FLAG_REPLY); let packet = encode_echo_reply(&echo, &params, key).unwrap();
let packet = encode_echo_reply(&echo_reply, &params, Some(KEY)).unwrap(); assert_eq!(packet[3], FLAG_REPLY | auth);
assert_eq!(packet[3], FLAG_REPLY | FLAG_HMAC); assert_eq!(&packet[offset..offset + 8], &TOKEN.to_le_bytes());
assert_eq!(&packet[offset + 8..offset + 12], &u32::MAX.to_le_bytes());
assert_eq!( assert_eq!(
decode_echo_reply(&packet, &params, Some(KEY)), decode_echo_reply(&packet, &params, key).unwrap(),
Ok(EchoReply { EchoReply {
flags: FLAG_REPLY | FLAG_HMAC, flags: packet[3],
..echo_reply ..echo
}) }
); );
} }
}
}
#[test] #[test]
fn rejected_open_reply_round_trips_and_zero_token_requires_close() { fn rejected_open_reply_round_trips_and_zero_token_requires_close() {
@@ -95,6 +103,12 @@ fn rejected_open_reply_round_trips_and_zero_token_requires_close() {
}; };
let packet = encode_open_reply(&rejected, None).unwrap(); let packet = encode_open_reply(&rejected, None).unwrap();
assert_eq!(decode_open_reply(&packet, None), Ok(rejected.clone())); assert_eq!(decode_open_reply(&packet, None), Ok(rejected.clone()));
let mut zero_without_close = packet.clone();
zero_without_close[3] &= !FLAG_CLOSE;
assert_eq!(
decode_open_reply(&zero_without_close, None),
Err(ProtoError::ZeroToken)
);
assert_eq!( assert_eq!(
encode_open_reply( encode_open_reply(
@@ -119,63 +133,51 @@ fn peer_close_echo_reply_round_trips_without_a_close_reply_codec() {
} }
#[test] #[test]
fn reply_encoders_supply_their_packet_type_rules() { fn reply_codecs_enforce_packet_type_and_reserved_flag_rules() {
let params = Params::default(); let params = Params::default();
assert_eq!( for (flags, expected) in [
encode_open_reply( (FLAG_REPLY, ProtoError::MissingFlag(FLAG_OPEN)),
&OpenReply { (FLAG_OPEN, ProtoError::MissingFlag(FLAG_REPLY)),
flags: FLAG_REPLY, (
FLAG_OPEN | FLAG_REPLY | 0x10,
ProtoError::ReservedFlags(0x10),
),
] {
let open = OpenReply {
flags,
token: TOKEN, token: TOKEN,
params: params.clone(), params: params.clone(),
}, };
None, assert_eq!(encode_open_reply(&open, None).unwrap_err(), expected);
), let mut packet = encode_open_reply(
Err(ProtoError::MissingFlag(FLAG_OPEN))
);
assert_eq!(
encode_echo_reply(
&EchoReply {
flags: 0,
..echo_reply(&params, FLAG_REPLY)
},
&params,
None,
),
Err(ProtoError::MissingFlag(FLAG_REPLY))
);
}
/// Every packet the reply encoders produce must be rejected by the inbound
/// request decoder: a server never answers a datagram carrying `FLAG_REPLY`.
#[test]
fn encoded_replies_are_never_admitted_as_inbound_requests() {
let params = Params::default();
for reply_flags in [FLAG_OPEN | FLAG_REPLY, FLAG_OPEN | FLAG_REPLY | FLAG_CLOSE] {
let packet = encode_open_reply(
&OpenReply { &OpenReply {
flags: reply_flags, flags: FLAG_OPEN | FLAG_REPLY,
token: if reply_flags & FLAG_CLOSE == 0 { ..open
TOKEN
} else {
0
},
params: params.clone(),
}, },
None, None,
) )
.unwrap(); .unwrap();
assert_eq!( packet[3] = flags;
decode_request(&packet), assert_eq!(decode_open_reply(&packet, None).unwrap_err(), expected);
Err(ProtoError::UnexpectedFlag(FLAG_REPLY))
);
} }
for (flags, expected) in [
for reply_flags in [FLAG_REPLY, FLAG_REPLY | FLAG_CLOSE] { (0, ProtoError::MissingFlag(FLAG_REPLY)),
let packet = encode_echo_reply(&echo_reply(&params, reply_flags), &params, None).unwrap(); (
FLAG_OPEN | FLAG_REPLY,
ProtoError::UnexpectedFlag(FLAG_OPEN),
),
(FLAG_REPLY | 0x10, ProtoError::ReservedFlags(0x10)),
] {
assert_eq!( assert_eq!(
decode_request(&packet), encode_echo_reply(&echo_reply(&params, flags), &params, None).unwrap_err(),
Err(ProtoError::UnexpectedFlag(FLAG_REPLY)) expected
);
let mut packet =
encode_echo_reply(&echo_reply(&params, FLAG_REPLY), &params, None).unwrap();
packet[3] = flags;
assert_eq!(
decode_echo_reply(&packet, &params, None).unwrap_err(),
expected
); );
} }
} }
@@ -207,7 +209,12 @@ fn echo_layout_combinations_round_trip() {
StampAt::Both, StampAt::Both,
StampAt::Midpoint, StampAt::Midpoint,
] { ] {
for clock in [Clock::Wall, Clock::Monotonic, Clock::Both] { for clock in [
Clock::Unspecified,
Clock::Wall,
Clock::Monotonic,
Clock::Both,
] {
for key in [None, Some(KEY)] { for key in [None, Some(KEY)] {
let params = Params { let params = Params {
protocol_version: 1, protocol_version: 1,
@@ -265,45 +272,49 @@ fn echo_reply_requires_exact_negotiated_optional_fields() {
#[test] #[test]
fn echo_payload_is_copied_zero_filled_and_bounded_in_both_directions() { fn echo_payload_is_copied_zero_filled_and_bounded_in_both_directions() {
let params = Params { for stats in [ReceivedStats::None, ReceivedStats::Both] {
length: 20, for key in [None, Some(KEY)] {
let mut params = Params {
received_stats: stats,
..Params::default() ..Params::default()
}; };
let offset = PacketLayout::echo(key.is_some(), &params).header_len();
let packet = encode_request(echo_request(&params, &[1, 2]), None).unwrap(); params.length = (offset + 4) as i64;
assert_eq!(&packet[16..], &[1, 2, 0, 0]); let packet = encode_request(echo_request(&params, &[1, 2]), key).unwrap();
assert_eq!(&packet[offset..], &[1, 2, 0, 0]);
assert_eq!( assert_eq!(
encode_request(echo_request(&params, &[0; 5]), None), encode_request(echo_request(&params, &[0; 5]), key),
Err(ProtoError::PayloadTooLarge { Err(ProtoError::PayloadTooLarge {
available: 4, available: 4,
provided: 5, provided: 5
}) })
); );
let reply = EchoReply { let reply = EchoReply {
payload: vec![3, 4], payload: vec![3, 4],
..echo_reply(&params, FLAG_REPLY) ..echo_reply(&params, FLAG_REPLY)
}; };
let packet = encode_echo_reply(&reply, &params, None).unwrap(); let packet = encode_echo_reply(&reply, &params, key).unwrap();
assert_eq!(&packet[16..], &[3, 4, 0, 0]); assert_eq!(&packet[offset..], &[3, 4, 0, 0]);
let decoded = decode_echo_reply(&packet, &params, None).unwrap(); let decoded = decode_echo_reply(&packet, &params, key).unwrap();
assert_eq!(decoded.payload, vec![3, 4, 0, 0]); assert_eq!(decoded.payload, vec![3, 4, 0, 0]);
assert_eq!(encode_echo_reply(&decoded, &params, None).unwrap(), packet); assert_eq!(encode_echo_reply(&decoded, &params, key).unwrap(), packet);
assert_eq!( assert_eq!(
encode_echo_reply( encode_echo_reply(
&EchoReply { &EchoReply {
payload: vec![0; 5], payload: vec![0; 5],
..echo_reply(&params, FLAG_REPLY) ..reply
}, },
&params, &params,
None, key
), ),
Err(ProtoError::PayloadTooLarge { Err(ProtoError::PayloadTooLarge {
available: 4, available: 4,
provided: 5, provided: 5
}) })
); );
} }
}
}
#[test] #[test]
fn a_negative_negotiated_length_still_encodes_in_both_directions() { fn a_negative_negotiated_length_still_encodes_in_both_directions() {
@@ -321,6 +332,14 @@ fn a_negative_negotiated_length_still_encodes_in_both_directions() {
let request = encode_request(echo_request(&params, &[]), None) let request = encode_request(echo_request(&params, &[]), None)
.expect("a negative negotiated length must still encode a request"); .expect("a negative negotiated length must still encode a request");
assert_eq!(request.len(), 16); assert_eq!(request.len(), 16);
assert_eq!(
decode_request(&request).unwrap().kind,
DecodedRequestKind::Echo {
token: TOKEN,
sequence: 17,
tail: &[]
}
);
let reply = encode_echo_reply(&echo_reply(&params, FLAG_REPLY), &params, None) let reply = encode_echo_reply(&echo_reply(&params, FLAG_REPLY), &params, None)
.expect("a negative negotiated length must still encode a reply"); .expect("a negative negotiated length must still encode a reply");
@@ -505,13 +524,15 @@ fn midpoint_dual_field_reply_authenticates_over_full_packet() {
let reply = decode_echo_reply(&packet, &params, Some(KEY)).unwrap(); let reply = decode_echo_reply(&packet, &params, Some(KEY)).unwrap();
assert_eq!(reply.timestamps.midpoint_wall, Some(555)); assert_eq!(reply.timestamps.midpoint_wall, Some(555));
let mut corrupted = packet; for offset in [HMAC_OFFSET, PacketLayout::echo(true, &params).header_len()] {
corrupted[HMAC_OFFSET] ^= 0xFF; let mut corrupted = packet.clone();
corrupted[offset] ^= 0xFF;
assert_eq!( assert_eq!(
decode_echo_reply(&corrupted, &params, Some(KEY)), decode_echo_reply(&corrupted, &params, Some(KEY)),
Err(ProtoError::BadHmac) Err(ProtoError::BadHmac)
); );
} }
}
#[test] #[test]
fn only_the_exact_compat_length_is_accepted_as_an_extension() { fn only_the_exact_compat_length_is_accepted_as_an_extension() {
@@ -539,61 +560,92 @@ fn only_the_exact_compat_length_is_accepted_as_an_extension() {
} }
#[test] #[test]
fn midpoint_both_clock_reply_has_no_additional_extension() { fn other_layouts_have_no_longer_form_exception() {
let params = midpoint_params(Clock::Both, 0); for (stamp_at, clock) in [
let reply = echo_reply(&params, FLAG_REPLY); (StampAt::Midpoint, Clock::Both),
let packet = encode_echo_reply(&reply, &params, None).unwrap(); (StampAt::Receive, Clock::Wall),
] {
let mut too_long = packet.clone();
too_long.extend_from_slice(&[0; 8]);
assert_eq!(
decode_echo_reply(&too_long, &params, None),
Err(ProtoError::PacketLengthMismatch {
expected: packet.len(),
actual: too_long.len(),
})
);
}
#[test]
fn non_midpoint_stamp_at_has_no_longer_form_exception() {
let params = Params { let params = Params {
received_stats: ReceivedStats::None, stamp_at,
stamp_at: StampAt::Receive, clock,
clock: Clock::Wall,
..Params::default() ..Params::default()
}; };
let reply = echo_reply(&params, FLAG_REPLY); let packet = encode_echo_reply(&echo_reply(&params, FLAG_REPLY), &params, None).unwrap();
let packet = encode_echo_reply(&reply, &params, None).unwrap();
let mut too_long = packet.clone(); let mut too_long = packet.clone();
too_long.extend_from_slice(&[0; 8]); too_long.extend_from_slice(&[0; 8]);
assert_eq!( assert_eq!(
decode_echo_reply(&too_long, &params, None), decode_echo_reply(&too_long, &params, None),
Err(ProtoError::PacketLengthMismatch { Err(ProtoError::PacketLengthMismatch {
expected: packet.len(), expected: packet.len(),
actual: too_long.len(), actual: too_long.len()
}) })
); );
} }
/// The midpoint compatibility length is a *reply* concept. An inbound ECHO
/// request of that length is just a longer request, which a receiver accepts
/// without applying any negotiated length rule.
#[test]
fn echo_request_decoding_has_no_compat_length_rule() {
let params = midpoint_params(Clock::Wall, 0);
let packet = encode_request(echo_request(&params, &[]), None).unwrap();
let mut too_long = packet.clone();
too_long.extend_from_slice(&[0; 8]);
let request = decode_request(&too_long).unwrap();
assert_eq!(
request.kind,
DecodedRequestKind::Echo {
token: TOKEN,
sequence: 17,
tail: &too_long[16..],
} }
#[test]
fn reply_decoders_enforce_hmac_presence_and_authenticate_the_body() {
let params = params();
for key in [None, Some(KEY)] {
let open = encode_open_reply(
&OpenReply {
flags: FLAG_OPEN | FLAG_REPLY,
token: TOKEN,
params: params.clone(),
},
key,
)
.unwrap();
let echo = encode_echo_reply(&echo_reply(&params, FLAG_REPLY), &params, key).unwrap();
let mismatched_key = if key.is_some() { None } else { Some(KEY) };
assert_eq!(
decode_open_reply(&open, mismatched_key),
Err(ProtoError::HmacPresenceMismatch)
);
assert_eq!(
decode_echo_reply(&echo, &params, mismatched_key),
Err(ProtoError::HmacPresenceMismatch)
);
if let Some(key) = key {
let mut bad_open = open;
let mut bad_echo = echo;
*bad_open.last_mut().unwrap() ^= 1;
*bad_echo.last_mut().unwrap() ^= 1;
assert_eq!(
decode_open_reply(&bad_open, Some(key)),
Err(ProtoError::BadHmac)
);
assert_eq!(
decode_echo_reply(&bad_echo, &params, Some(key)),
Err(ProtoError::BadHmac)
); );
} }
}
}
#[test]
fn echo_reply_length_errors_precede_hmac_verification() {
for key in [None, Some(KEY)] {
let mut params = Params::default();
let header = PacketLayout::echo(key.is_some(), &params).header_len();
params.length = (header + 4) as i64;
let packet = encode_echo_reply(&echo_reply(&params, FLAG_REPLY), &params, key).unwrap();
for actual in [header - 1, header, packet.len() - 1, packet.len() + 1] {
let mut malformed = packet.clone();
malformed.resize(actual, 0);
// Truncating or extending the signed packet also invalidates its MAC.
let expected = if actual < header {
ProtoError::PacketTooShort {
needed: header,
actual,
}
} else {
ProtoError::PacketLengthMismatch {
expected: packet.len(),
actual,
}
};
assert_eq!(decode_echo_reply(&malformed, &params, key), Err(expected));
}
}
}
+38 -165
View File
@@ -47,8 +47,6 @@ fn token_and_sequence() -> Vec<u8> {
body body
} }
// ---------- A. Classification ----------
fn classify(kind: &DecodedRequestKind<'_>) -> &'static str { fn classify(kind: &DecodedRequestKind<'_>) -> &'static str {
match kind { match kind {
DecodedRequestKind::Open { no_test: false, .. } => "open", DecodedRequestKind::Open { no_test: false, .. } => "open",
@@ -71,6 +69,34 @@ fn flag_combinations_classify_requests_with_orthogonal_hmac_presence() {
(FLAG_OPEN | FLAG_CLOSE | FLAG_HMAC, true, "no-test open"), (FLAG_OPEN | FLAG_CLOSE | FLAG_HMAC, true, "no-test open"),
]; ];
for no_test in [false, true] {
for key in [None, Some(KEY)] {
let params = Params::default();
let packet = encode_request(
RequestToEncode::Open {
params: &params,
no_test,
},
key,
)
.unwrap();
let expected_flags = FLAG_OPEN
| if no_test { FLAG_CLOSE } else { 0 }
| if key.is_some() { FLAG_HMAC } else { 0 };
assert_eq!(packet[3], expected_flags);
assert_eq!(
decode_request(&packet).unwrap().kind,
DecodedRequestKind::Open {
no_test,
params: &[]
}
);
if let Some(key) = key {
verify_packet_hmac(key, &packet).unwrap();
}
}
}
for (flags, hmac_present, expected) in cases { for (flags, hmac_present, expected) in cases {
// Long enough for any kind, so only the flags decide classification. // Long enough for any kind, so only the flags decide classification.
let packet = raw(flags, &token_and_sequence(), 64); let packet = raw(flags, &token_and_sequence(), 64);
@@ -82,6 +108,10 @@ fn flag_combinations_classify_requests_with_orthogonal_hmac_presence() {
request.hmac_present, hmac_present, request.hmac_present, hmac_present,
"flags 0x{flags:02x} HMAC presence" "flags 0x{flags:02x} HMAC presence"
); );
if let DecodedRequestKind::Open { params, .. } = request.kind {
let offset = if hmac_present { 20 } else { 4 };
assert_eq!(params, &packet[offset..]);
}
assert_eq!( assert_eq!(
classify(&request.kind), classify(&request.kind),
expected, expected,
@@ -90,8 +120,6 @@ fn flag_combinations_classify_requests_with_orthogonal_hmac_presence() {
} }
} }
// ---------- B. Invalid header / flags ----------
#[test] #[test]
fn structurally_invalid_headers_are_rejected() { fn structurally_invalid_headers_are_rejected() {
assert_eq!( assert_eq!(
@@ -135,8 +163,6 @@ fn any_reply_flagged_datagram_is_rejected_as_a_request() {
} }
} }
// ---------- C. Structural minimum lengths ----------
#[test] #[test]
fn structural_minimum_lengths_are_enforced_per_kind() { fn structural_minimum_lengths_are_enforced_per_kind() {
// (flags, minimum accepted length) // (flags, minimum accepted length)
@@ -172,8 +198,6 @@ fn structural_minimum_lengths_are_enforced_per_kind() {
} }
} }
// ---------- D. Field offsets ----------
#[test] #[test]
fn token_and_sequence_are_read_from_the_hmac_dependent_offsets() { fn token_and_sequence_are_read_from_the_hmac_dependent_offsets() {
for hmac in [false, true] { for hmac in [false, true] {
@@ -211,69 +235,25 @@ fn token_and_sequence_are_read_from_the_hmac_dependent_offsets() {
} }
} }
// ---------- E. Open parameter slice ----------
#[test] #[test]
fn open_params_are_borrowed_after_the_header_and_decoded_later() { fn open_params_are_borrowed_after_the_header_and_decoded_later() {
let encoded = params().encode(); // Structural parsing accepts even malformed Params; semantic errors belong
// to Params::decode and are covered in params.rs.
for body in [params().encode(), Vec::new(), vec![1, 0x80]] {
for hmac in [false, true] { for hmac in [false, true] {
let flags = FLAG_OPEN | if hmac { FLAG_HMAC } else { 0 }; let flags = FLAG_OPEN | if hmac { FLAG_HMAC } else { 0 };
let expected_offset = if hmac { 4 + HMAC_SIZE } else { 4 }; let offset = if hmac { 20 } else { 4 };
let packet = raw(flags, &encoded, expected_offset + encoded.len()); let packet = raw(flags, &body, offset + body.len());
match decode_request(&packet).unwrap().kind { match decode_request(&packet).unwrap().kind {
DecodedRequestKind::Open { params, .. } => { DecodedRequestKind::Open { params, .. } => {
assert_eq!(params, &packet[expected_offset..]); assert_eq!(params, &packet[offset..]);
assert_eq!(Params::decode(params).unwrap(), self::params());
} }
other => panic!("expected an open request, got {other:?}"), other => panic!("expected an open request, got {other:?}"),
} }
} }
} }
#[test]
fn empty_and_malformed_open_params_decode_structurally() {
let empty = header(FLAG_OPEN);
match decode_request(&empty).unwrap().kind {
DecodedRequestKind::Open { params, .. } => assert!(params.is_empty()),
other => panic!("expected an open request, got {other:?}"),
} }
// A truncated varint: structurally fine, semantically not. A receiver must
// be able to authenticate before spending effort on this.
let malformed = raw(FLAG_OPEN, &[1, 0x80], 6);
match decode_request(&malformed).unwrap().kind {
DecodedRequestKind::Open { params, .. } => {
assert_eq!(params, &[1, 0x80]);
assert_eq!(Params::decode(params), Err(ProtoError::TruncatedVarint));
}
other => panic!("expected an open request, got {other:?}"),
}
}
// ---------- F. Open precedence ----------
#[test]
fn open_precedence_beats_close_and_an_echo_shaped_body() {
// An OPEN|CLOSE datagram whose body looks exactly like a token and
// sequence number is still a no-test open, and those bytes are parameters.
let packet = raw(FLAG_OPEN | FLAG_CLOSE, &token_and_sequence(), 16);
assert_eq!(
decode_request(&packet).unwrap(),
DecodedRequest {
hmac_present: false,
kind: DecodedRequestKind::Open {
no_test: true,
params: &packet[4..],
},
}
);
}
// ---------- G. Trailing data ----------
#[test] #[test]
fn trailing_bytes_are_tolerated_and_no_length_ceiling_applies() { fn trailing_bytes_are_tolerated_and_no_length_ceiling_applies() {
let mut close = raw(FLAG_CLOSE, &TOKEN.to_le_bytes(), 12); let mut close = raw(FLAG_CLOSE, &TOKEN.to_le_bytes(), 12);
@@ -303,8 +283,6 @@ fn trailing_bytes_are_tolerated_and_no_length_ceiling_applies() {
} }
} }
// ---------- H. Zero token ----------
#[test] #[test]
fn a_zero_token_is_structurally_valid() { fn a_zero_token_is_structurally_valid() {
assert_eq!( assert_eq!(
@@ -323,8 +301,6 @@ fn a_zero_token_is_structurally_valid() {
); );
} }
// ---------- HMAC presence is not authentication ----------
#[test] #[test]
fn hmac_presence_is_reported_independently_of_mac_validity() { fn hmac_presence_is_reported_independently_of_mac_validity() {
// A structurally complete authenticated echo whose MAC is garbage. // A structurally complete authenticated echo whose MAC is garbage.
@@ -378,106 +354,3 @@ fn packet_verification_requires_a_present_and_complete_hmac_field() {
Err(ProtoError::BadMagic) Err(ProtoError::BadMagic)
); );
} }
// ---------- Encode -> decode cross-checks ----------
#[test]
fn encoded_requests_decode_back_to_their_sender_side_identity() {
for key in [None, Some(KEY)] {
for no_test in [false, true] {
let packet = encode_request(
RequestToEncode::Open {
params: &params(),
no_test,
},
key,
)
.unwrap();
let request = decode_request(&packet).unwrap();
assert_eq!(request.hmac_present, key.is_some());
match request.kind {
DecodedRequestKind::Open {
no_test: decoded,
params: encoded,
} => {
assert_eq!(decoded, no_test);
assert_eq!(Params::decode(encoded).unwrap(), params());
}
other => panic!("expected an open request, got {other:?}"),
}
if let Some(key) = key {
verify_packet_hmac(key, &packet).unwrap();
}
}
let packet = encode_request(RequestToEncode::Close { token: TOKEN }, key).unwrap();
let request = decode_request(&packet).unwrap();
assert_eq!(request.hmac_present, key.is_some());
assert_eq!(request.kind, DecodedRequestKind::Close { token: TOKEN });
// The ECHO tail is deliberately not compared against the logical
// payload: the sender placed it at a negotiated offset the receiver
// cannot recover without `Params`.
let echo_params = Params {
length: 96,
..params()
};
let packet = encode_request(
RequestToEncode::Echo {
token: TOKEN,
sequence: SEQUENCE,
params: &echo_params,
payload: &[1, 2, 3, 4],
},
key,
)
.unwrap();
let request = decode_request(&packet).unwrap();
assert_eq!(request.hmac_present, key.is_some());
match request.kind {
DecodedRequestKind::Echo {
token, sequence, ..
} => {
assert_eq!(token, TOKEN);
assert_eq!(sequence, SEQUENCE);
}
other => panic!("expected an echo request, got {other:?}"),
}
if let Some(key) = key {
verify_packet_hmac(key, &packet).unwrap();
}
}
}
#[test]
fn echo_encoding_floors_a_negative_negotiated_length_at_the_field_block() {
// A negative negotiated length is accepted during open and echoed back
// unchanged, so a real session can carry one. It must therefore stay
// encodable: it asks for no space beyond the mandatory fields.
let params = Params {
length: -1,
..Params::default()
};
let packet = encode_request(
RequestToEncode::Echo {
token: TOKEN,
sequence: SEQUENCE,
params: &params,
payload: &[],
},
None,
)
.expect("a negative negotiated length must still encode");
// header (4) + token (8) + sequence (4).
assert_eq!(packet.len(), 16);
let request = decode_request(&packet).unwrap();
assert_eq!(
request.kind,
DecodedRequestKind::Echo {
token: TOKEN,
sequence: SEQUENCE,
tail: &[],
}
);
}
+108
View File
@@ -0,0 +1,108 @@
use irtt_proto::{varint::*, ProtoError};
use proptest::prelude::*;
#[test]
fn boundary_values_round_trip() {
for value in [
0,
1,
2,
127,
128,
255,
16_384,
u32::MAX as u64,
i64::MAX as u64,
u64::MAX,
] {
let mut encoded = Vec::new();
encode_uvarint(value, &mut encoded);
assert_eq!(decode_uvarint(&encoded), Ok((value, encoded.len())));
}
for value in [
i64::MIN,
-9_223_372_036,
-2,
-1,
0,
1,
2,
1_000_000_000,
3_000_000_000,
i64::MAX,
] {
let mut encoded = Vec::new();
encode_varint(value, &mut encoded);
assert_eq!(decode_varint(&encoded), Ok((value, encoded.len())));
}
}
#[test]
fn verified_byte_examples() {
let cases: &[(i64, &[u8])] = &[
(1, &[0x02]),
(3_000_000_000, &[0x80, 0xf8, 0x82, 0xad, 0x16]),
(1_000_000_000, &[0x80, 0xa8, 0xd6, 0xb9, 0x07]),
(1472, &[0x80, 0x17]),
(3, &[0x06]),
(184, &[0xf0, 0x02]),
];
for (value, expected) in cases {
let mut encoded = Vec::new();
encode_varint(*value, &mut encoded);
assert_eq!(&encoded, expected);
assert_eq!(decode_varint(expected), Ok((*value, expected.len())));
}
for tag in [9, 24] {
let mut encoded = Vec::new();
encode_uvarint(tag, &mut encoded);
assert_eq!(encoded, [tag as u8]);
}
}
#[test]
fn truncated_and_overflowing_varints_are_rejected() {
// Incomplete input differs from a tenth byte whose bits cannot fit u64,
// or an eleventh byte after ten continuation bytes.
for (bytes, expected) in [
(vec![], ProtoError::TruncatedVarint),
(vec![0x80], ProtoError::TruncatedVarint),
(vec![0x80; 10], ProtoError::TruncatedVarint),
(
[vec![0x80; 9], vec![2]].concat(),
ProtoError::VarintOverflow,
),
(
[vec![0x80; 10], vec![0]].concat(),
ProtoError::VarintOverflow,
),
] {
assert_eq!(decode_uvarint(&bytes), Err(expected), "{bytes:02x?}");
assert_eq!(
decode_varint(&bytes),
decode_uvarint(&bytes).map(|(v, n)| (zigzag_decode(v), n))
);
}
}
proptest! {
#[test]
fn uvarint_round_trips(value: u64) {
let mut encoded = Vec::new();
encode_uvarint(value, &mut encoded);
prop_assert!(!encoded.is_empty());
prop_assert!(encoded.len() <= 10);
prop_assert_eq!(decode_uvarint(&encoded).unwrap(), (value, encoded.len()));
prop_assert_eq!(zigzag_encode(zigzag_decode(value)), value);
}
#[test]
fn signed_varint_round_trips(value: i64) {
let mut encoded = Vec::new();
encode_varint(value, &mut encoded);
prop_assert!(!encoded.is_empty());
prop_assert!(encoded.len() <= 10);
prop_assert_eq!(decode_varint(&encoded).unwrap(), (value, encoded.len()));
prop_assert_eq!(zigzag_decode(zigzag_encode(value)), value);
}
}