feat(project): Establish initial IRTT workspace and protocol definitions
This commit is contained in:
commit
cf74887bdf
21 files changed
+1601
No files matched your search
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "irtt-cli"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1 @@
|
||||
fn main() {}
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "irtt-client"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,4 @@
|
||||
//! Client crate skeleton.
|
||||
//!
|
||||
//! Networking and session runtime are intentionally out of scope for the
|
||||
//! current milestone.
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "irtt-proto"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
hmac = "0.12"
|
||||
md-5 = "0.10"
|
||||
subtle = "2"
|
||||
thiserror = "1"
|
||||
@@ -0,0 +1,46 @@
|
||||
use crate::{
|
||||
flags::{FLAG_CLOSE, FLAG_HMAC},
|
||||
hmac, write_header, Result, HMAC_SIZE,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CloseRequest {
|
||||
pub token: u64,
|
||||
}
|
||||
|
||||
pub fn encode_close_request(request: &CloseRequest, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
|
||||
let mut flags = FLAG_CLOSE;
|
||||
if hmac_key.is_some() {
|
||||
flags |= FLAG_HMAC;
|
||||
}
|
||||
|
||||
let mut out = Vec::new();
|
||||
write_header(&mut out, flags);
|
||||
if hmac_key.is_some() {
|
||||
out.extend_from_slice(&[0; HMAC_SIZE]);
|
||||
}
|
||||
out.extend_from_slice(&request.token.to_le_bytes());
|
||||
if let Some(key) = hmac_key {
|
||||
hmac::compute_hmac_in_place(key, &mut out, hmac::hmac_offset())?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn close_request_sizes() {
|
||||
let request = CloseRequest {
|
||||
token: 0x7896_b6ab_8771_5213,
|
||||
};
|
||||
assert_eq!(encode_close_request(&request, None).unwrap().len(), 12);
|
||||
assert_eq!(
|
||||
encode_close_request(&request, Some(b"testkey"))
|
||||
.unwrap()
|
||||
.len(),
|
||||
28
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
use crate::{
|
||||
flags::{has, FLAG_CLOSE, FLAG_HMAC, FLAG_OPEN, FLAG_REPLY},
|
||||
hmac,
|
||||
layout::{echo_packet_len, PacketLayout},
|
||||
open::{check_hmac_presence, reject},
|
||||
params::Params,
|
||||
validate_header, write_header, ProtoError, Result, HEADER_SIZE, HMAC_SIZE, RECV_COUNT_SIZE,
|
||||
RECV_WINDOW_SIZE, SEQ_SIZE, TIMESTAMP_SIZE, TOKEN_SIZE,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EchoRequest {
|
||||
pub token: u64,
|
||||
pub sequence: u32,
|
||||
pub params: Params,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EchoReply {
|
||||
pub flags: u8,
|
||||
pub token: u64,
|
||||
pub sequence: u32,
|
||||
pub recv_count: Option<u32>,
|
||||
pub recv_window: Option<u64>,
|
||||
pub timestamps: TimestampFields,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct TimestampFields {
|
||||
pub recv_wall: Option<i64>,
|
||||
pub recv_mono: Option<i64>,
|
||||
pub midpoint_wall: Option<i64>,
|
||||
pub midpoint_mono: Option<i64>,
|
||||
pub send_wall: Option<i64>,
|
||||
pub send_mono: Option<i64>,
|
||||
}
|
||||
|
||||
pub fn encode_echo_request(request: &EchoRequest, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
|
||||
let mut flags = 0;
|
||||
if hmac_key.is_some() {
|
||||
flags |= FLAG_HMAC;
|
||||
}
|
||||
let layout = PacketLayout::echo(hmac_key.is_some(), &request.params);
|
||||
let len = echo_packet_len(hmac_key.is_some(), &request.params);
|
||||
let mut out = Vec::with_capacity(len);
|
||||
write_header(&mut out, flags);
|
||||
if hmac_key.is_some() {
|
||||
out.extend_from_slice(&[0; HMAC_SIZE]);
|
||||
}
|
||||
out.extend_from_slice(&request.token.to_le_bytes());
|
||||
out.extend_from_slice(&request.sequence.to_le_bytes());
|
||||
push_zeroed_layout_tail(layout, &mut out);
|
||||
out.resize(len, 0);
|
||||
let payload_offset = layout.header_len();
|
||||
let payload_len = out
|
||||
.len()
|
||||
.saturating_sub(payload_offset)
|
||||
.min(request.payload.len());
|
||||
out[payload_offset..payload_offset + payload_len]
|
||||
.copy_from_slice(&request.payload[..payload_len]);
|
||||
|
||||
if let Some(key) = hmac_key {
|
||||
hmac::compute_hmac_in_place(key, &mut out, hmac::hmac_offset())?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn decode_echo_reply(
|
||||
packet: &[u8],
|
||||
params: &Params,
|
||||
hmac_key: Option<&[u8]>,
|
||||
) -> Result<EchoReply> {
|
||||
let flags = validate_header(packet)?;
|
||||
reject(flags, FLAG_OPEN)?;
|
||||
crate::open::require(flags, FLAG_REPLY)?;
|
||||
check_hmac_presence(flags, hmac_key)?;
|
||||
if let Some(key) = hmac_key {
|
||||
hmac::verify_hmac(key, packet, hmac::hmac_offset())?;
|
||||
}
|
||||
|
||||
let layout = PacketLayout::echo(has(flags, FLAG_HMAC), params);
|
||||
let header_len = layout.header_len();
|
||||
if packet.len() < header_len {
|
||||
return Err(ProtoError::PacketTooShort {
|
||||
needed: header_len,
|
||||
actual: packet.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut pos = HEADER_SIZE;
|
||||
if layout.hmac {
|
||||
pos += HMAC_SIZE;
|
||||
}
|
||||
let token = read_u64(packet, &mut pos);
|
||||
let sequence = read_u32(packet, &mut pos);
|
||||
let recv_count = layout.recv_count.then(|| read_u32(packet, &mut pos));
|
||||
let recv_window = layout.recv_window.then(|| read_u64(packet, &mut pos));
|
||||
let timestamps = TimestampFields {
|
||||
recv_wall: layout.recv_wall.then(|| read_i64(packet, &mut pos)),
|
||||
recv_mono: layout.recv_mono.then(|| read_i64(packet, &mut pos)),
|
||||
midpoint_wall: layout.midpoint_wall.then(|| read_i64(packet, &mut pos)),
|
||||
midpoint_mono: layout.midpoint_mono.then(|| read_i64(packet, &mut pos)),
|
||||
send_wall: layout.send_wall.then(|| read_i64(packet, &mut pos)),
|
||||
send_mono: layout.send_mono.then(|| read_i64(packet, &mut pos)),
|
||||
};
|
||||
|
||||
Ok(EchoReply {
|
||||
flags,
|
||||
token,
|
||||
sequence,
|
||||
recv_count,
|
||||
recv_window,
|
||||
timestamps,
|
||||
payload: packet[header_len..].to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_u32(packet: &[u8], pos: &mut usize) -> u32 {
|
||||
let value = u32::from_le_bytes(packet[*pos..*pos + SEQ_SIZE].try_into().unwrap());
|
||||
*pos += SEQ_SIZE;
|
||||
value
|
||||
}
|
||||
|
||||
fn read_u64(packet: &[u8], pos: &mut usize) -> u64 {
|
||||
let value = u64::from_le_bytes(packet[*pos..*pos + TOKEN_SIZE].try_into().unwrap());
|
||||
*pos += TOKEN_SIZE;
|
||||
value
|
||||
}
|
||||
|
||||
fn read_i64(packet: &[u8], pos: &mut usize) -> i64 {
|
||||
let value = i64::from_le_bytes(packet[*pos..*pos + TIMESTAMP_SIZE].try_into().unwrap());
|
||||
*pos += TIMESTAMP_SIZE;
|
||||
value
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn _assert_close_flag_imported(_: u8) {
|
||||
let _ = FLAG_CLOSE;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::params::{Clock, ReceivedStats, StampAt};
|
||||
|
||||
fn default_params() -> Params {
|
||||
Params {
|
||||
received_stats: ReceivedStats::Both,
|
||||
stamp_at: StampAt::Both,
|
||||
clock: Clock::Both,
|
||||
..Params::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_request_encodes_default_placeholders() {
|
||||
let packet = encode_echo_request(
|
||||
&EchoRequest {
|
||||
token: 0x7896_b6ab_8771_5213,
|
||||
sequence: 0,
|
||||
params: default_params(),
|
||||
payload: Vec::new(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(packet.len(), 60);
|
||||
assert_eq!(&packet[..4], &[0x14, 0xa7, 0x5b, 0x00]);
|
||||
assert_eq!(&packet[4..12], &0x7896_b6ab_8771_5213u64.to_le_bytes());
|
||||
assert!(packet[12..].iter().all(|byte| *byte == 0));
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use thiserror::Error;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, ProtoError>;
|
||||
|
||||
#[derive(Debug, Error, PartialEq, Eq)]
|
||||
pub enum ProtoError {
|
||||
#[error("packet is too short: needed {needed} bytes, got {actual}")]
|
||||
PacketTooShort { needed: usize, actual: usize },
|
||||
#[error("bad magic bytes")]
|
||||
BadMagic,
|
||||
#[error("reserved flag bits are set: 0x{0:02x}")]
|
||||
ReservedFlags(u8),
|
||||
#[error("required flag 0x{0:02x} is missing")]
|
||||
MissingFlag(u8),
|
||||
#[error("unexpected flag 0x{0:02x} is set")]
|
||||
UnexpectedFlag(u8),
|
||||
#[error("HMAC flag/key mismatch")]
|
||||
HmacPresenceMismatch,
|
||||
#[error("HMAC verification failed")]
|
||||
BadHmac,
|
||||
#[error("invalid HMAC field offset")]
|
||||
InvalidHmacOffset,
|
||||
#[error("varint is truncated")]
|
||||
TruncatedVarint,
|
||||
#[error("varint is too long")]
|
||||
VarintOverflow,
|
||||
#[error("string value is not valid UTF-8")]
|
||||
InvalidUtf8,
|
||||
#[error("invalid enum value {value} for {name}")]
|
||||
InvalidEnum { name: &'static str, value: i64 },
|
||||
#[error("open reply has zero token without close flag")]
|
||||
ZeroToken,
|
||||
#[error("trailing or malformed parameter payload")]
|
||||
MalformedParams,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use crate::{error::Result, ProtoError};
|
||||
|
||||
pub const FLAG_OPEN: u8 = 0x01;
|
||||
pub const FLAG_REPLY: u8 = 0x02;
|
||||
pub const FLAG_CLOSE: u8 = 0x04;
|
||||
pub const FLAG_HMAC: u8 = 0x08;
|
||||
|
||||
pub const RESERVED_FLAGS_MASK: u8 = 0xF0;
|
||||
|
||||
pub fn validate_flags(flags: u8) -> Result<()> {
|
||||
let reserved = flags & RESERVED_FLAGS_MASK;
|
||||
if reserved != 0 {
|
||||
return Err(ProtoError::ReservedFlags(reserved));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has(flags: u8, flag: u8) -> bool {
|
||||
flags & flag != 0
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use hmac::{Hmac, Mac};
|
||||
use md5::Md5;
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
use crate::{flags::FLAG_HMAC, ProtoError, Result, HEADER_SIZE, HMAC_SIZE};
|
||||
|
||||
type HmacMd5 = Hmac<Md5>;
|
||||
|
||||
pub fn compute_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(©);
|
||||
let bytes = mac.finalize().into_bytes();
|
||||
let mut out = [0u8; HMAC_SIZE];
|
||||
out.copy_from_slice(&bytes);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn compute_hmac_in_place(key: &[u8], packet: &mut [u8], hmac_offset: usize) -> Result<()> {
|
||||
if packet.len().saturating_sub(hmac_offset) < HMAC_SIZE {
|
||||
return Err(ProtoError::InvalidHmacOffset);
|
||||
}
|
||||
packet[3] |= FLAG_HMAC;
|
||||
packet[hmac_offset..hmac_offset + HMAC_SIZE].fill(0);
|
||||
let digest = compute_hmac(key, packet, hmac_offset)?;
|
||||
packet[hmac_offset..hmac_offset + HMAC_SIZE].copy_from_slice(&digest);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn verify_hmac(key: &[u8], packet: &[u8], hmac_offset: usize) -> Result<()> {
|
||||
if packet.len().saturating_sub(hmac_offset) < HMAC_SIZE {
|
||||
return Err(ProtoError::InvalidHmacOffset);
|
||||
}
|
||||
let expected = compute_hmac(key, packet, hmac_offset)?;
|
||||
let actual = &packet[hmac_offset..hmac_offset + HMAC_SIZE];
|
||||
if expected.as_slice().ct_eq(actual).into() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ProtoError::BadHmac)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hmac_offset() -> usize {
|
||||
HEADER_SIZE
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{flags::FLAG_OPEN, MAGIC};
|
||||
|
||||
#[test]
|
||||
fn compute_and_verify_hmac() {
|
||||
let mut packet = Vec::new();
|
||||
packet.extend_from_slice(&MAGIC);
|
||||
packet.push(FLAG_OPEN | FLAG_HMAC);
|
||||
packet.extend_from_slice(&[0; HMAC_SIZE]);
|
||||
packet.extend_from_slice(&[1, 2, 3, 4]);
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
use crate::{
|
||||
params::{Clock, Params, StampAt},
|
||||
HEADER_SIZE, HMAC_SIZE, RECV_COUNT_SIZE, RECV_WINDOW_SIZE, SEQ_SIZE, TIMESTAMP_SIZE,
|
||||
TOKEN_SIZE,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PacketLayout {
|
||||
pub hmac: bool,
|
||||
pub token: bool,
|
||||
pub sequence: bool,
|
||||
pub recv_count: bool,
|
||||
pub recv_window: bool,
|
||||
pub recv_wall: bool,
|
||||
pub recv_mono: bool,
|
||||
pub midpoint_wall: bool,
|
||||
pub midpoint_mono: bool,
|
||||
pub send_wall: bool,
|
||||
pub send_mono: bool,
|
||||
}
|
||||
|
||||
impl PacketLayout {
|
||||
pub fn open_request(hmac: bool) -> Self {
|
||||
Self {
|
||||
hmac,
|
||||
token: false,
|
||||
sequence: false,
|
||||
recv_count: false,
|
||||
recv_window: false,
|
||||
recv_wall: false,
|
||||
recv_mono: false,
|
||||
midpoint_wall: false,
|
||||
midpoint_mono: false,
|
||||
send_wall: false,
|
||||
send_mono: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_reply(hmac: bool) -> Self {
|
||||
Self {
|
||||
token: true,
|
||||
..Self::open_request(hmac)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn echo(hmac: bool, params: &Params) -> Self {
|
||||
let clock = params.clock;
|
||||
Self {
|
||||
hmac,
|
||||
token: true,
|
||||
sequence: true,
|
||||
recv_count: params.received_stats.has_count(),
|
||||
recv_window: params.received_stats.has_window(),
|
||||
recv_wall: matches!(params.stamp_at, StampAt::Receive | StampAt::Both)
|
||||
&& clock.has_wall(),
|
||||
recv_mono: matches!(params.stamp_at, StampAt::Receive | StampAt::Both)
|
||||
&& clock.has_mono(),
|
||||
midpoint_wall: matches!(params.stamp_at, StampAt::Midpoint) && clock.has_wall(),
|
||||
midpoint_mono: matches!(params.stamp_at, StampAt::Midpoint) && clock.has_mono(),
|
||||
send_wall: matches!(params.stamp_at, StampAt::Send | StampAt::Both) && clock.has_wall(),
|
||||
send_mono: matches!(params.stamp_at, StampAt::Send | StampAt::Both) && clock.has_mono(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_request(hmac: bool) -> Self {
|
||||
Self {
|
||||
hmac,
|
||||
token: true,
|
||||
sequence: false,
|
||||
recv_count: false,
|
||||
recv_window: false,
|
||||
recv_wall: false,
|
||||
recv_mono: false,
|
||||
midpoint_wall: false,
|
||||
midpoint_mono: false,
|
||||
send_wall: false,
|
||||
send_mono: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header_len(self) -> usize {
|
||||
HEADER_SIZE
|
||||
+ if self.hmac { HMAC_SIZE } else { 0 }
|
||||
+ if self.token { TOKEN_SIZE } else { 0 }
|
||||
+ if self.sequence { SEQ_SIZE } else { 0 }
|
||||
+ if self.recv_count { RECV_COUNT_SIZE } else { 0 }
|
||||
+ if self.recv_window {
|
||||
RECV_WINDOW_SIZE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
+ self.timestamp_count() * TIMESTAMP_SIZE
|
||||
}
|
||||
|
||||
pub fn timestamp_count(self) -> usize {
|
||||
[
|
||||
self.recv_wall,
|
||||
self.recv_mono,
|
||||
self.midpoint_wall,
|
||||
self.midpoint_mono,
|
||||
self.send_wall,
|
||||
self.send_mono,
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|present| *present)
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn echo_header_len(hmac: bool, params: &Params) -> usize {
|
||||
PacketLayout::echo(hmac, params).header_len()
|
||||
}
|
||||
|
||||
pub fn echo_packet_len(hmac: bool, params: &Params) -> usize {
|
||||
let header_len = echo_header_len(hmac, params);
|
||||
let requested = usize::try_from(params.length).unwrap_or(0);
|
||||
header_len.max(requested)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn _assert_clock_is_used(_: Clock) {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::params::{ReceivedStats, StampAt};
|
||||
|
||||
fn params(stats: ReceivedStats, stamp_at: StampAt, clock: Clock) -> Params {
|
||||
Params {
|
||||
received_stats: stats,
|
||||
stamp_at,
|
||||
clock,
|
||||
..Params::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verified_layout_lengths() {
|
||||
assert_eq!(
|
||||
echo_header_len(
|
||||
false,
|
||||
¶ms(ReceivedStats::None, StampAt::None, Clock::None)
|
||||
),
|
||||
16
|
||||
);
|
||||
assert_eq!(
|
||||
echo_header_len(
|
||||
false,
|
||||
¶ms(ReceivedStats::Count, StampAt::Send, Clock::Wall)
|
||||
),
|
||||
28
|
||||
);
|
||||
assert_eq!(
|
||||
echo_header_len(
|
||||
false,
|
||||
¶ms(ReceivedStats::Window, StampAt::Receive, Clock::Monotonic)
|
||||
),
|
||||
32
|
||||
);
|
||||
assert_eq!(
|
||||
echo_header_len(
|
||||
false,
|
||||
¶ms(ReceivedStats::Both, StampAt::Midpoint, Clock::Both)
|
||||
),
|
||||
44
|
||||
);
|
||||
assert_eq!(
|
||||
echo_header_len(
|
||||
false,
|
||||
¶ms(ReceivedStats::Both, StampAt::Both, Clock::Both)
|
||||
),
|
||||
60
|
||||
);
|
||||
assert_eq!(
|
||||
echo_header_len(
|
||||
true,
|
||||
¶ms(ReceivedStats::Both, StampAt::Both, Clock::Both)
|
||||
),
|
||||
76
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negotiated_length_never_smaller_than_header() {
|
||||
let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
|
||||
p.length = 20;
|
||||
assert_eq!(echo_packet_len(false, &p), 60);
|
||||
p.length = 92;
|
||||
assert_eq!(echo_packet_len(false, &p), 92);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod close;
|
||||
pub mod echo;
|
||||
pub mod error;
|
||||
pub mod flags;
|
||||
pub mod hmac;
|
||||
pub mod layout;
|
||||
pub mod open;
|
||||
pub mod params;
|
||||
pub mod varint;
|
||||
|
||||
pub use close::{encode_close_request, CloseRequest};
|
||||
pub use echo::{decode_echo_reply, encode_echo_request, EchoReply, EchoRequest, TimestampFields};
|
||||
pub use error::{ProtoError, Result};
|
||||
pub use flags::*;
|
||||
pub use hmac::{compute_hmac, compute_hmac_in_place, verify_hmac};
|
||||
pub use layout::{echo_header_len, echo_packet_len, PacketLayout};
|
||||
pub use open::{decode_open_reply, encode_open_request, OpenReply, OpenRequest};
|
||||
pub use params::{Clock, Params, ReceivedStats, ServerFill, StampAt};
|
||||
|
||||
pub const MAGIC: [u8; 3] = [0x14, 0xA7, 0x5B];
|
||||
pub const PROTOCOL_VERSION: i64 = 1;
|
||||
|
||||
pub const HMAC_SIZE: usize = 16;
|
||||
pub const TOKEN_SIZE: usize = 8;
|
||||
pub const SEQ_SIZE: usize = 4;
|
||||
pub const RECV_COUNT_SIZE: usize = 4;
|
||||
pub const RECV_WINDOW_SIZE: usize = 8;
|
||||
pub const TIMESTAMP_SIZE: usize = 8;
|
||||
|
||||
pub(crate) const HEADER_SIZE: usize = 4;
|
||||
|
||||
pub(crate) fn write_header(out: &mut Vec<u8>, flags: u8) {
|
||||
out.extend_from_slice(&MAGIC);
|
||||
out.push(flags);
|
||||
}
|
||||
|
||||
pub(crate) fn validate_header(packet: &[u8]) -> Result<u8> {
|
||||
if packet.len() < HEADER_SIZE {
|
||||
return Err(ProtoError::PacketTooShort {
|
||||
needed: HEADER_SIZE,
|
||||
actual: packet.len(),
|
||||
});
|
||||
}
|
||||
if packet[..3] != MAGIC {
|
||||
return Err(ProtoError::BadMagic);
|
||||
}
|
||||
let flags = packet[3];
|
||||
flags::validate_flags(flags)?;
|
||||
Ok(flags)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
use crate::{
|
||||
flags::{has, FLAG_CLOSE, FLAG_HMAC, FLAG_OPEN, FLAG_REPLY},
|
||||
hmac,
|
||||
params::Params,
|
||||
validate_header, write_header, ProtoError, Result, HEADER_SIZE, HMAC_SIZE, TOKEN_SIZE,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OpenRequest {
|
||||
pub params: Params,
|
||||
pub close: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OpenReply {
|
||||
pub flags: u8,
|
||||
pub token: u64,
|
||||
pub params: Params,
|
||||
}
|
||||
|
||||
pub fn encode_open_request(request: &OpenRequest, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
|
||||
let mut flags = FLAG_OPEN;
|
||||
if request.close {
|
||||
flags |= FLAG_CLOSE;
|
||||
}
|
||||
if hmac_key.is_some() {
|
||||
flags |= FLAG_HMAC;
|
||||
}
|
||||
|
||||
let mut out = Vec::new();
|
||||
write_header(&mut out, flags);
|
||||
if hmac_key.is_some() {
|
||||
out.extend_from_slice(&[0; HMAC_SIZE]);
|
||||
}
|
||||
out.extend_from_slice(&request.params.encode());
|
||||
|
||||
if let Some(key) = hmac_key {
|
||||
hmac::compute_hmac_in_place(key, &mut out, hmac::hmac_offset())?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn decode_open_reply(packet: &[u8], hmac_key: Option<&[u8]>) -> Result<OpenReply> {
|
||||
let flags = validate_header(packet)?;
|
||||
require(flags, FLAG_OPEN)?;
|
||||
require(flags, FLAG_REPLY)?;
|
||||
check_hmac_presence(flags, hmac_key)?;
|
||||
if let Some(key) = hmac_key {
|
||||
hmac::verify_hmac(key, packet, hmac::hmac_offset())?;
|
||||
}
|
||||
|
||||
let mut pos = HEADER_SIZE;
|
||||
if has(flags, FLAG_HMAC) {
|
||||
pos += HMAC_SIZE;
|
||||
}
|
||||
let needed = pos + TOKEN_SIZE;
|
||||
if packet.len() < needed {
|
||||
return Err(ProtoError::PacketTooShort {
|
||||
needed,
|
||||
actual: packet.len(),
|
||||
});
|
||||
}
|
||||
let token = u64::from_le_bytes(packet[pos..pos + TOKEN_SIZE].try_into().unwrap());
|
||||
pos += TOKEN_SIZE;
|
||||
if token == 0 && !has(flags, FLAG_CLOSE) {
|
||||
return Err(ProtoError::ZeroToken);
|
||||
}
|
||||
|
||||
Ok(OpenReply {
|
||||
flags,
|
||||
token,
|
||||
params: Params::decode(&packet[pos..])?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn require(flags: u8, flag: u8) -> Result<()> {
|
||||
if has(flags, flag) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ProtoError::MissingFlag(flag))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn reject(flags: u8, flag: u8) -> Result<()> {
|
||||
if has(flags, flag) {
|
||||
Err(ProtoError::UnexpectedFlag(flag))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_hmac_presence(flags: u8, hmac_key: Option<&[u8]>) -> Result<()> {
|
||||
if has(flags, FLAG_HMAC) == hmac_key.is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ProtoError::HmacPresenceMismatch)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::params::{Clock, ReceivedStats, StampAt};
|
||||
|
||||
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_request_has_no_token() {
|
||||
let packet = encode_open_request(
|
||||
&OpenRequest {
|
||||
params: default_params(),
|
||||
close: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(packet.len(), 24);
|
||||
assert_eq!(&packet[..4], &[0x14, 0xa7, 0x5b, 0x01]);
|
||||
assert_eq!(&packet[4..], &default_params().encode());
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
use crate::{varint, ProtoError, Result, PROTOCOL_VERSION};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Params {
|
||||
pub protocol_version: i64,
|
||||
pub duration_ns: i64,
|
||||
pub interval_ns: i64,
|
||||
pub length: i64,
|
||||
pub received_stats: ReceivedStats,
|
||||
pub stamp_at: StampAt,
|
||||
pub clock: Clock,
|
||||
pub dscp: i64,
|
||||
pub server_fill: Option<ServerFill>,
|
||||
}
|
||||
|
||||
impl Params {
|
||||
pub fn with_protocol_defaults() -> Self {
|
||||
Self {
|
||||
protocol_version: PROTOCOL_VERSION,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
push_int(1, self.protocol_version, &mut out);
|
||||
push_int(2, self.duration_ns, &mut out);
|
||||
push_int(3, self.interval_ns, &mut out);
|
||||
push_int(4, self.length, &mut out);
|
||||
push_int(5, self.received_stats as i64, &mut out);
|
||||
push_int(6, self.stamp_at as i64, &mut out);
|
||||
push_int(7, self.clock as i64, &mut out);
|
||||
push_int(8, self.dscp, &mut out);
|
||||
if let Some(fill) = &self.server_fill {
|
||||
varint::encode_uvarint(9, &mut out);
|
||||
varint::encode_uvarint(fill.value.len() as u64, &mut out);
|
||||
out.extend_from_slice(fill.value.as_bytes());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn decode(input: &[u8]) -> Result<Self> {
|
||||
let mut params = Self::default();
|
||||
let mut pos = 0;
|
||||
while pos < input.len() {
|
||||
let (tag, used) = varint::decode_uvarint(&input[pos..])?;
|
||||
pos += used;
|
||||
match tag {
|
||||
1 => params.protocol_version = read_int(input, &mut pos)?,
|
||||
2 => params.duration_ns = read_int(input, &mut pos)?,
|
||||
3 => params.interval_ns = read_int(input, &mut pos)?,
|
||||
4 => params.length = read_int(input, &mut pos)?,
|
||||
5 => params.received_stats = ReceivedStats::try_from(read_int(input, &mut pos)?)?,
|
||||
6 => params.stamp_at = StampAt::try_from(read_int(input, &mut pos)?)?,
|
||||
7 => params.clock = Clock::try_from(read_int(input, &mut pos)?)?,
|
||||
8 => params.dscp = read_int(input, &mut pos)?,
|
||||
9 => {
|
||||
let (len, used) = varint::decode_uvarint(&input[pos..])?;
|
||||
pos += used;
|
||||
let len = len as usize;
|
||||
if input.len().saturating_sub(pos) < len {
|
||||
return Err(ProtoError::MalformedParams);
|
||||
}
|
||||
let value = std::str::from_utf8(&input[pos..pos + len])
|
||||
.map_err(|_| ProtoError::InvalidUtf8)?
|
||||
.to_owned();
|
||||
pos += len;
|
||||
params.server_fill = Some(ServerFill { value });
|
||||
}
|
||||
_ => {
|
||||
let (_, used) = varint::decode_uvarint(&input[pos..])?;
|
||||
pos += used;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(params)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ServerFill {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[repr(i64)]
|
||||
pub enum ReceivedStats {
|
||||
#[default]
|
||||
None = 0,
|
||||
Count = 1,
|
||||
Window = 2,
|
||||
Both = 3,
|
||||
}
|
||||
|
||||
impl ReceivedStats {
|
||||
pub fn has_count(self) -> bool {
|
||||
matches!(self, Self::Count | Self::Both)
|
||||
}
|
||||
|
||||
pub fn has_window(self) -> bool {
|
||||
matches!(self, Self::Window | Self::Both)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<i64> for ReceivedStats {
|
||||
type Error = ProtoError;
|
||||
|
||||
fn try_from(value: i64) -> Result<Self> {
|
||||
match value {
|
||||
0 => Ok(Self::None),
|
||||
1 => Ok(Self::Count),
|
||||
2 => Ok(Self::Window),
|
||||
3 => Ok(Self::Both),
|
||||
_ => Err(ProtoError::InvalidEnum {
|
||||
name: "ReceivedStats",
|
||||
value,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[repr(i64)]
|
||||
pub enum StampAt {
|
||||
#[default]
|
||||
None = 0,
|
||||
Send = 1,
|
||||
Receive = 2,
|
||||
Both = 3,
|
||||
Midpoint = 4,
|
||||
}
|
||||
|
||||
impl TryFrom<i64> for StampAt {
|
||||
type Error = ProtoError;
|
||||
|
||||
fn try_from(value: i64) -> Result<Self> {
|
||||
match value {
|
||||
0 => Ok(Self::None),
|
||||
1 => Ok(Self::Send),
|
||||
2 => Ok(Self::Receive),
|
||||
3 => Ok(Self::Both),
|
||||
4 => Ok(Self::Midpoint),
|
||||
_ => Err(ProtoError::InvalidEnum {
|
||||
name: "StampAt",
|
||||
value,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[repr(i64)]
|
||||
pub enum Clock {
|
||||
#[default]
|
||||
None = 0,
|
||||
Wall = 1,
|
||||
Monotonic = 2,
|
||||
Both = 3,
|
||||
}
|
||||
|
||||
impl Clock {
|
||||
pub fn has_wall(self) -> bool {
|
||||
matches!(self, Self::Wall | Self::Both)
|
||||
}
|
||||
|
||||
pub fn has_mono(self) -> bool {
|
||||
matches!(self, Self::Monotonic | Self::Both)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<i64> for Clock {
|
||||
type Error = ProtoError;
|
||||
|
||||
fn try_from(value: i64) -> Result<Self> {
|
||||
match value {
|
||||
0 => Ok(Self::None),
|
||||
1 => Ok(Self::Wall),
|
||||
2 => Ok(Self::Monotonic),
|
||||
3 => Ok(Self::Both),
|
||||
_ => Err(ProtoError::InvalidEnum {
|
||||
name: "Clock",
|
||||
value,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_int(tag: u64, value: i64, out: &mut Vec<u8>) {
|
||||
if value == 0 {
|
||||
return;
|
||||
}
|
||||
varint::encode_uvarint(tag, out);
|
||||
varint::encode_varint(value, out);
|
||||
}
|
||||
|
||||
fn read_int(input: &[u8], pos: &mut usize) -> Result<i64> {
|
||||
let (value, used) = varint::decode_varint(&input[*pos..])?;
|
||||
*pos += used;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[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_eq!(Params::decode(¶ms.encode()), Ok(params));
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
use crate::{ProtoError, Result};
|
||||
|
||||
pub fn encode_uvarint(mut value: u64, out: &mut Vec<u8>) {
|
||||
while value >= 0x80 {
|
||||
out.push((value as u8) | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
out.push(value as u8);
|
||||
}
|
||||
|
||||
pub fn decode_uvarint(input: &[u8]) -> Result<(u64, usize)> {
|
||||
let mut value = 0u64;
|
||||
for (idx, byte) in input.iter().copied().enumerate() {
|
||||
if idx == 10 {
|
||||
return Err(ProtoError::VarintOverflow);
|
||||
}
|
||||
let low = u64::from(byte & 0x7f);
|
||||
let shift = idx * 7;
|
||||
if shift == 63 && low > 1 {
|
||||
return Err(ProtoError::VarintOverflow);
|
||||
}
|
||||
value |= low << shift;
|
||||
if byte < 0x80 {
|
||||
return Ok((value, idx + 1));
|
||||
}
|
||||
}
|
||||
Err(ProtoError::TruncatedVarint)
|
||||
}
|
||||
|
||||
pub fn zigzag_encode(value: i64) -> u64 {
|
||||
((value << 1) ^ (value >> 63)) as u64
|
||||
}
|
||||
|
||||
pub fn zigzag_decode(value: u64) -> i64 {
|
||||
((value >> 1) as i64) ^ (-((value & 1) as i64))
|
||||
}
|
||||
|
||||
pub fn encode_varint(value: i64, out: &mut Vec<u8>) {
|
||||
encode_uvarint(zigzag_encode(value), out);
|
||||
}
|
||||
|
||||
pub fn decode_varint(input: &[u8]) -> Result<(i64, usize)> {
|
||||
let (value, used) = decode_uvarint(input)?;
|
||||
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]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
use irtt_proto::{
|
||||
decode_echo_reply, decode_open_reply, encode_close_request, encode_echo_request,
|
||||
encode_open_request, verify_hmac, Clock, CloseRequest, EchoRequest, OpenRequest, Params,
|
||||
ReceivedStats, StampAt,
|
||||
};
|
||||
|
||||
fn hex(input: &str) -> Vec<u8> {
|
||||
input
|
||||
.split_whitespace()
|
||||
.map(|byte| u8::from_str_radix(byte, 16).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn default_params_3s() -> 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()
|
||||
}
|
||||
}
|
||||
|
||||
fn default_params_60s() -> Params {
|
||||
Params {
|
||||
duration_ns: 60_000_000_000,
|
||||
..default_params_3s()
|
||||
}
|
||||
}
|
||||
|
||||
fn hmac_params_2s() -> Params {
|
||||
Params {
|
||||
duration_ns: 2_000_000_000,
|
||||
..default_params_3s()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_1_open_request_no_hmac() {
|
||||
let expected = hex("14 a7 5b 01 01 02 02 80 f8 82 ad 16 03 80 a8 d6
|
||||
b9 07 05 06 06 06 07 06");
|
||||
let packet = encode_open_request(
|
||||
&OpenRequest {
|
||||
params: default_params_3s(),
|
||||
close: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(packet, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_2_open_reply_no_hmac() {
|
||||
let packet = hex("14 a7 5b 03 13 52 71 87 ab b6 96 78 01 02 02 80
|
||||
f8 82 ad 16 03 80 a8 d6 b9 07 05 06 06 06 07 06");
|
||||
let reply = decode_open_reply(&packet, None).unwrap();
|
||||
assert_eq!(reply.token, 0x7896_b6ab_8771_5213);
|
||||
assert_eq!(reply.params, default_params_3s());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_3_echo_request_no_hmac() {
|
||||
let expected = hex("14 a7 5b 00 13 52 71 87 ab b6 96 78 00 00 00 00
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||
00 00 00 00 00 00 00 00 00 00 00 00");
|
||||
let packet = encode_echo_request(
|
||||
&EchoRequest {
|
||||
token: 0x7896_b6ab_8771_5213,
|
||||
sequence: 0,
|
||||
params: default_params_3s(),
|
||||
payload: Vec::new(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(packet, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_4_echo_reply_no_hmac() {
|
||||
let packet = hex("14 a7 5b 02 13 52 71 87 ab b6 96 78 02 00 00 00
|
||||
03 00 00 00 07 00 00 00 00 00 00 00 b8 1a 33 0c
|
||||
86 6d aa 18 de 26 35 95 00 00 00 00 80 4d 33 0c
|
||||
86 6d aa 18 b2 57 35 95 00 00 00 00");
|
||||
let reply = decode_echo_reply(&packet, &default_params_3s(), None).unwrap();
|
||||
assert_eq!(reply.sequence, 2);
|
||||
assert_eq!(reply.recv_count, Some(3));
|
||||
assert_eq!(reply.recv_window, Some(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_5_close_request_no_hmac() {
|
||||
let expected = hex("14 a7 5b 04 13 52 71 87 ab b6 96 78");
|
||||
let packet = encode_close_request(
|
||||
&CloseRequest {
|
||||
token: 0x7896_b6ab_8771_5213,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(packet, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_6_hmac_open_request() {
|
||||
let expected = hex("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");
|
||||
let packet = encode_open_request(
|
||||
&OpenRequest {
|
||||
params: hmac_params_2s(),
|
||||
close: false,
|
||||
},
|
||||
Some(b"testkey"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(packet, expected);
|
||||
verify_hmac(b"testkey", &packet, 4).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_7_hmac_echo_request() {
|
||||
let expected = hex("14 a7 5b 08 d9 87 4f 82 b3 13 10 31 59 ad 2c 8f
|
||||
6b da ef ff 59 ca 3f 5d eb e9 87 43 00 00 00 00
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||
00 00 00 00 00 00 00 00 00 00 00 00");
|
||||
let packet = encode_echo_request(
|
||||
&EchoRequest {
|
||||
token: 0x4387_e9eb_5d3f_ca59,
|
||||
sequence: 0,
|
||||
params: default_params_3s(),
|
||||
payload: Vec::new(),
|
||||
},
|
||||
Some(b"testkey"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(packet, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_8_hmac_close_request() {
|
||||
let expected = hex("14 a7 5b 0c f5 cd 0f a9 de 9d 7d 66 8d 91 a5 32
|
||||
48 0e 42 e0 59 ca 3f 5d eb e9 87 43");
|
||||
let packet = encode_close_request(
|
||||
&CloseRequest {
|
||||
token: 0x4387_e9eb_5d3f_ca59,
|
||||
},
|
||||
Some(b"testkey"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(packet, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_9_no_test_open_close_request() {
|
||||
let expected = hex("14 a7 5b 05 01 02 02 80 e0 ba 84 bf 03 03 80 a8
|
||||
d6 b9 07 05 06 06 06 07 06");
|
||||
let packet = encode_open_request(
|
||||
&OpenRequest {
|
||||
params: default_params_60s(),
|
||||
close: true,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(packet, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_10_no_test_open_close_reply() {
|
||||
let packet = hex("14 a7 5b 07 00 00 00 00 00 00 00 00 01 02 02 80
|
||||
e0 ba 84 bf 03 03 80 a8 d6 b9 07 05 06 06 06 07
|
||||
06");
|
||||
let reply = decode_open_reply(&packet, None).unwrap();
|
||||
assert_eq!(reply.token, 0);
|
||||
assert_eq!(reply.params, default_params_60s());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_11_minimal_echo_packet() {
|
||||
let expected = hex("14 a7 5b 00 4e 15 61 5c a2 6f 31 a0 00 00 00 00");
|
||||
let packet = encode_echo_request(
|
||||
&EchoRequest {
|
||||
token: 0xa031_6fa2_5c61_154e,
|
||||
sequence: 0,
|
||||
params: Params::default(),
|
||||
payload: Vec::new(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(packet, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_12_midpoint_reply() {
|
||||
let packet = hex("14 a7 5b 02 91 a4 1e 7a f0 14 f6 62 00 00 00 00
|
||||
01 00 00 00 01 00 00 00 00 00 00 00 f7 0e 2c 93
|
||||
a2 6d aa 18 5a 5f 14 47 03 00 00 00");
|
||||
let params = Params {
|
||||
received_stats: ReceivedStats::Both,
|
||||
stamp_at: StampAt::Midpoint,
|
||||
clock: Clock::Both,
|
||||
..Params::default()
|
||||
};
|
||||
let reply = decode_echo_reply(&packet, ¶ms, None).unwrap();
|
||||
assert_eq!(reply.recv_count, Some(1));
|
||||
assert_eq!(reply.recv_window, Some(1));
|
||||
assert!(reply.timestamps.midpoint_wall.is_some());
|
||||
assert!(reply.timestamps.recv_wall.is_none());
|
||||
assert!(reply.timestamps.send_wall.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spec_18_1_hmac_echo_request_fixture() {
|
||||
let expected = hex("14 a7 5b 08 e7 03 41 e7 d4 08 cf 69 41 f3 f4 78
|
||||
5a 56 0c 4c ea 3e b3 22 a7 c9 6b 88 bb a1 e2 6f
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||
00 00 00 00 00 00 00 00 00 00 00 00 ff fe fd fc
|
||||
ff fe fd fc ff fe fd fc ff fe fd fc");
|
||||
let params = Params {
|
||||
length: 92,
|
||||
received_stats: ReceivedStats::Both,
|
||||
stamp_at: StampAt::Both,
|
||||
clock: Clock::Both,
|
||||
..Params::default()
|
||||
};
|
||||
let packet = encode_echo_request(
|
||||
&EchoRequest {
|
||||
token: 0x886b_c9a7_22b3_3eea,
|
||||
sequence: 0x6fe2_a1bb,
|
||||
params,
|
||||
payload: [0xff, 0xfe, 0xfd, 0xfc].repeat(4),
|
||||
},
|
||||
Some(&[0x3c, 0x68, 0x1d, 0x39, 0x41, 0x1d, 0x72, 0x43]),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(packet, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spec_18_2_hmac_echo_reply_fixture() {
|
||||
let packet = hex("14 a7 5b 08 d2 98 a3 4a 6a 13 41 02 68 b2 67 a8
|
||||
d6 7e 28 25 c3 cb 6f 76 b6 ce 66 e6 06 07 3b 1d
|
||||
19 9f bc a3 9b 3b f8 86 e5 39 e9 d7 a1 75 2a ee
|
||||
3d f1 5a 52 9a c6 4a 3e 22 62 55 2d 69 3f 29 46
|
||||
d4 05 97 58 66 50 2c dd 2f f1 1d 46 ff fe fd fc
|
||||
ff fe fd fc ff fe fd fc ff fe fd fc");
|
||||
verify_hmac(
|
||||
&[0xda, 0xb3, 0xe9, 0x04, 0xa6, 0x87, 0x92, 0x49],
|
||||
&packet,
|
||||
4,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "irtt-stats"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,4 @@
|
||||
//! Statistics crate skeleton.
|
||||
//!
|
||||
//! Statistics aggregation is intentionally out of scope for the current
|
||||
//! milestone.
|
||||
Reference in new issue
Block a user