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,7 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
target/
|
||||||
|
*/target/
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
Generated
+163
@@ -0,0 +1,163 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "irtt-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "irtt-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "irtt-proto"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"hmac",
|
||||||
|
"md-5",
|
||||||
|
"subtle",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "irtt-stats"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md-5"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"crates/irtt-proto",
|
||||||
|
"crates/irtt-client",
|
||||||
|
"crates/irtt-stats",
|
||||||
|
"crates/irtt-cli",
|
||||||
|
]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
edition = "2021"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
rust-version = "1.75"
|
||||||
@@ -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