feat(project): Establish initial IRTT workspace and protocol definitions

Set up the multi-crate project structure and add the core IRTT protocol with pinned dependencies for reproducible builds.
This commit is contained in:
Nils Andreas Svee committed 2026-04-28 16:03:50 +07:00
1 parent dd8b55ad9f
commit 82491f0f61
21 files changed
+1594 -4

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
[package]
name = "irtt-cli"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[dependencies]
+1
View File
@@ -0,0 +1 @@
fn main() {}
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "irtt-client"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[dependencies]
+4
View File
@@ -0,0 +1,4 @@
//! Client crate skeleton.
//!
//! Networking and session runtime are intentionally out of scope for the
//! current milestone.
+12
View File
@@ -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"
+46
View File
@@ -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
);
}
}
+204
View File
@@ -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());
}
}
+35
View File
@@ -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,
}
+20
View File
@@ -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
}
+71
View File
@@ -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(&copy);
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)
);
}
}
+191
View File
@@ -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,
&params(ReceivedStats::None, StampAt::None, Clock::None)
),
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 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);
}
}
+52
View File
@@ -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)
}
+143
View File
@@ -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());
}
}
+235
View File
@@ -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(&params.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);
}
}
+115
View File
@@ -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]);
}
}
+261
View File
@@ -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, &params, 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();
}
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "irtt-stats"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[dependencies]
+4
View File
@@ -0,0 +1,4 @@
//! Statistics crate skeleton.
//!
//! Statistics aggregation is intentionally out of scope for the current
//! milestone.