Add executable doctests documenting the consumer-facing contract of irtt-proto: the directional request encode/decode/verify split, Params construction and round-trip, omitted-vs-explicit-zero presence, open and echo reply round trips, packet length calculation, and HMAC verification. No conventional tests are migrated: every existing irtt-proto test exercises wire vectors, flag matrices, malformed input, layout rules, HMAC error behavior, or encode/decode round trips, which the crate's AGENTS.md treats as durable compatibility coverage rather than documentation cleanup targets.
136 lines
5.1 KiB
Rust
136 lines
5.1 KiB
Rust
//! Low-level wire encode/decode support for the IRTT protocol.
|
|
//!
|
|
//! `irtt-proto` intentionally mirrors protocol fields closely: request/reply
|
|
//! structs expose wire-oriented values, flags, counters, timestamps, and
|
|
//! parameter enums rather than higher-level policy decisions. Higher-level
|
|
//! crates such as `irtt-client` are responsible for session behavior and
|
|
//! user-facing validation.
|
|
//!
|
|
//! Most applications should depend on `irtt-client` or `irtt-server` instead
|
|
//! of this crate directly; use `irtt-proto` only when implementing another
|
|
//! IRTT-compatible client or server that needs the raw packet layer itself.
|
|
//!
|
|
//! [`Params::decode`] rejects malformed or incompatible incoming parameter
|
|
//! values, including invalid enum discriminants, malformed UTF-8, and oversized
|
|
//! `server_fill` values. Direct construction followed by [`Params::encode`]
|
|
//! performs no additional validation, so callers that build `Params` manually
|
|
//! are responsible for validating those values before sending them.
|
|
//!
|
|
//! Parameters are optional on the wire, and an absent tag takes its wire
|
|
//! default — zero for every integer field, and [`Clock::Unspecified`] for the
|
|
//! clock. A receiver that must distinguish an omitted tag from one explicitly
|
|
//! encoded as zero uses [`Params::decode_with_presence`], which runs the same
|
|
//! parser and additionally reports a [`ParamPresence`].
|
|
//!
|
|
//! The crate also provides packet layout calculation, packet encoding and
|
|
//! decoding, and optional HMAC placement, computation, and verification
|
|
//! helpers.
|
|
//! Encoder `hmac_key` arguments are authoritative: `Some(key)` adds
|
|
//! `FLAG_HMAC`, while `None` removes it from caller-supplied reply flags.
|
|
//!
|
|
//! # Directional model
|
|
//!
|
|
//! Requests use one encoder and one decoder for all three request kinds:
|
|
//! [`encode_request`] builds a request from borrowed sender-side values, and
|
|
//! [`decode_request`] structurally classifies an inbound request without a key,
|
|
//! negotiated [`Params`], or any session state. [`verify_packet_hmac`]
|
|
//! authenticates a packet separately, once the applicable key is known —
|
|
//! `FLAG_HMAC` being present is a statement about layout, not about validity.
|
|
//!
|
|
//! Replies keep type-specific decoders ([`decode_open_reply`],
|
|
//! [`decode_echo_reply`]) because a client already knows from its session state
|
|
//! which reply kind to expect, and the two need genuinely different semantic
|
|
//! context.
|
|
//!
|
|
//! # Example
|
|
//!
|
|
//! The normal path is directional: a sender encodes with [`encode_request`], a
|
|
//! receiver classifies with [`decode_request`], and authentication is a
|
|
//! separate step once the applicable key is known.
|
|
//!
|
|
//! ```
|
|
//! use irtt_proto::{
|
|
//! decode_request, encode_request, verify_packet_hmac, DecodedRequestKind, Params,
|
|
//! RequestToEncode,
|
|
//! };
|
|
//!
|
|
//! // Sender side: encode an authenticated open request.
|
|
//! let params = Params::with_protocol_defaults();
|
|
//! let key = b"example key";
|
|
//! let packet = encode_request(
|
|
//! RequestToEncode::Open { params: ¶ms, no_test: false },
|
|
//! Some(key),
|
|
//! ).unwrap();
|
|
//!
|
|
//! // Receiver side: classify structurally, before any key is known.
|
|
//! let request = decode_request(&packet).unwrap();
|
|
//! assert!(request.hmac_present);
|
|
//! let DecodedRequestKind::Open { no_test, params: encoded } = request.kind else {
|
|
//! panic!("expected an open request");
|
|
//! };
|
|
//! assert!(!no_test);
|
|
//! assert_eq!(Params::decode(encoded).unwrap(), params);
|
|
//!
|
|
//! // Authentication is a separate step, once the applicable key is known.
|
|
//! verify_packet_hmac(key, &packet).unwrap();
|
|
//! ```
|
|
//!
|
|
#![forbid(unsafe_code)]
|
|
|
|
pub mod echo;
|
|
mod envelope;
|
|
pub mod error;
|
|
pub mod flags;
|
|
pub mod hmac;
|
|
pub mod layout;
|
|
pub mod open;
|
|
pub mod params;
|
|
pub mod request;
|
|
pub mod varint;
|
|
|
|
pub use echo::{decode_echo_reply, encode_echo_reply, EchoReply, TimestampFields};
|
|
pub use error::{ProtoError, Result};
|
|
pub use flags::*;
|
|
pub use hmac::{compute_hmac, compute_hmac_in_place, verify_hmac, verify_packet_hmac};
|
|
pub use layout::{echo_header_len, echo_packet_len, PacketLayout};
|
|
pub use open::{decode_open_reply, encode_open_reply, OpenReply};
|
|
pub use params::{
|
|
Clock, DecodedParams, ParamPresence, Params, ReceivedStats, ServerFill, StampAt,
|
|
MAX_SERVER_FILL_BYTES,
|
|
};
|
|
pub use request::{
|
|
decode_request, encode_request, DecodedRequest, DecodedRequestKind, RequestToEncode,
|
|
};
|
|
|
|
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)
|
|
}
|