feat(server): carry reply traffic class

This commit is contained in:
Nils Andreas Svee committed 2026-08-13 20:34:42 +07:00
1 parent 68e7d18c12
commit a4a5219b9e
12 files changed
+310 -57

No files matched your search

+15 -8
View File
@@ -93,8 +93,15 @@ fn a_valid_echo_is_answered_from_the_negotiated_params() {
"no timestamps were negotiated, so none are emitted"
);
assert!(reply.payload.is_empty());
assert_eq!(packet.len(), echo_packet_len(false, &negotiated).unwrap());
assert_eq!(packet.len(), 16, "magic, flags, token and sequence only");
assert_eq!(
packet.bytes().len(),
echo_packet_len(false, &negotiated).unwrap()
);
assert_eq!(
packet.bytes().len(),
16,
"magic, flags, token and sequence only"
);
assert_eq!(
core.session_count(),
@@ -149,7 +156,7 @@ fn a_reply_is_sized_by_the_session_not_by_the_request() {
let reply = expect_echo_reply(&packet, &negotiated, None);
assert_eq!(
packet.len(),
packet.bytes().len(),
64,
"{name}: the reply is the negotiated length"
);
@@ -418,7 +425,7 @@ fn an_authenticated_session_answers_only_authenticated_echoes() {
.unwrap()
.expect("a correctly authenticated echo must be answered");
assert!(
verify_packet_hmac(KEY, &packet).is_ok(),
verify_packet_hmac(KEY, packet.bytes()).is_ok(),
"the reply must carry a MAC that verifies with the server's key"
);
// `expect_echo_reply` pins the HMAC flag and verifies the MAC again while
@@ -596,7 +603,7 @@ fn only_the_negotiated_timestamps_are_emitted() {
// failure as much as a selected one carrying the wrong instant.
assert_eq!(reply.timestamps, expected, "{stamp_at:?} on {clock:?}");
assert_eq!(
packet.len(),
packet.bytes().len(),
echo_packet_len(false, &negotiated).unwrap(),
"{stamp_at:?} on {clock:?} must be the negotiated layout's length"
);
@@ -704,12 +711,12 @@ fn a_single_clock_midpoint_emits_exactly_one_field() {
let reply = expect_echo_reply(&packet, &negotiated, None);
assert_eq!(
packet.len(),
packet.bytes().len(),
echo_packet_len(false, &negotiated).unwrap(),
"{clock:?}: the negotiated length, not the compatibility one"
);
assert_eq!(
packet.len(),
packet.bytes().len(),
24,
"{clock:?}: one 8-byte midpoint field, not the upstream two"
);
@@ -734,7 +741,7 @@ fn a_reply_payload_is_zero_filled_and_never_reflects_the_request() {
.expect("an admissible echo must be answered");
let reply = expect_echo_reply(&packet, &negotiated, None);
assert_eq!(packet.len(), 64);
assert_eq!(packet.bytes().len(), 64);
assert_eq!(reply.payload.len(), 48);
assert!(
reply.payload.iter().all(|byte| *byte == 0),
+7 -3
View File
@@ -94,8 +94,12 @@ fn an_authenticating_server_answers_a_valid_open_with_an_authenticated_reply() {
let reply = expect_normal_open_reply(&packet, Some(KEY));
assert_eq!(reply.token, TOKEN_A);
assert_ne!(packet[3] & FLAG_HMAC, 0, "the reply must set FLAG_HMAC");
verify_packet_hmac(KEY, &packet).expect("the reply must carry a valid MAC");
assert_ne!(
packet.bytes()[3] & FLAG_HMAC,
0,
"the reply must set FLAG_HMAC"
);
verify_packet_hmac(KEY, packet.bytes()).expect("the reply must carry a valid MAC");
assert_eq!(core.session_count(), 1);
}
@@ -109,7 +113,7 @@ fn a_non_authenticating_server_answers_an_ordinary_open() {
.expect("an unauthenticated open must be answered");
expect_normal_open_reply(&packet, None);
assert_eq!(packet[3] & FLAG_HMAC, 0);
assert_eq!(packet.bytes()[3] & FLAG_HMAC, 0);
assert_eq!(core.session_count(), 1);
}
+12 -6
View File
@@ -22,7 +22,7 @@ use super::support::{
expect_closing_echo_reply, expect_echo_reply, expect_no_test_reply, manual_core,
no_test_request, open_request, other_peer, peer, ManualClock, ScriptedTokens, KEY,
};
use crate::ServerConfig;
use crate::{OutboundDatagram, ServerConfig};
const TOKEN_A: u64 = 0x0102_0304_0506_0708;
const TOKEN_B: u64 = 0x1112_1314_1516_1718;
@@ -50,9 +50,9 @@ fn idle_after(idle: Duration) -> ServerConfig {
.with_min_send_interval(Duration::ZERO)
}
fn count_of(packet: Option<Vec<u8>>, params: &Params) -> Option<u32> {
let packet = packet?;
expect_echo_reply(&packet, params, None).recv_count
fn count_of(reply: Option<OutboundDatagram>, params: &Params) -> Option<u32> {
let reply = reply?;
expect_echo_reply(reply, params, None).recv_count
}
#[test]
@@ -451,10 +451,15 @@ fn the_first_echo_past_the_maximum_duration_carries_close_and_ends_the_session()
);
assert!(reply.payload.is_empty());
assert_eq!(
packet.len(),
packet.bytes().len(),
irtt_proto::echo_packet_len(false, &negotiated).unwrap(),
"the negotiated layout, unchanged by the flag"
);
assert_eq!(
i64::from(packet.traffic_class()),
negotiated.dscp,
"a server close is an echo reply and keeps the session's marking"
);
// The token is unusable from that reply onward.
assert_eq!(core.session_count(), 0);
@@ -595,6 +600,7 @@ fn an_authenticated_session_closes_through_the_ordinary_reply_path() {
// together.
let reply = expect_closing_echo_reply(&packet, &negotiated, Some(KEY));
assert_eq!(reply.recv_count, Some(2));
irtt_proto::verify_packet_hmac(KEY, &packet).expect("the close-flagged reply is authenticated");
irtt_proto::verify_packet_hmac(KEY, packet.bytes())
.expect("the close-flagged reply is authenticated");
assert_eq!(core.session_count(), 0);
}
+1
View File
@@ -24,3 +24,4 @@ mod rate;
mod sessions;
mod support;
mod tokens;
mod traffic_class;
+1 -1
View File
@@ -194,7 +194,7 @@ fn an_authenticated_no_test_reply_is_authenticated_and_still_creates_nothing() {
.expect("an authenticated no-test open must be answered");
expect_no_test_reply(&packet, Some(KEY));
verify_packet_hmac(KEY, &packet).expect("the no-test reply must carry a valid MAC");
verify_packet_hmac(KEY, packet.bytes()).expect("the no-test reply must carry a valid MAC");
assert_eq!(core.session_count(), 0);
}
+5 -5
View File
@@ -98,7 +98,7 @@ fn an_empty_parameter_payload_is_answered_with_only_a_version() {
// header (4) + token (8) + ProtocolVersion=1 (2). Every other parameter
// negotiates to its wire-default zero and is omitted entirely, which is
// only reachable if absent parameters really did decode as zero.
assert_eq!(packet.len(), 14);
assert_eq!(packet.bytes().len(), 14);
let reply = expect_normal_open_reply(&packet, None);
assert_eq!(
@@ -139,7 +139,7 @@ fn unknown_parameter_tags_are_ignored_and_not_reflected_in_the_reply() {
);
// The reply is the same 14 bytes an empty payload produces: no unknown tag
// was copied through.
assert_eq!(packet.len(), 14);
assert_eq!(packet.bytes().len(), 14);
assert_eq!(core.session_count(), 1);
}
@@ -366,9 +366,9 @@ fn authentication_counts_toward_the_executable_echo_size() {
#[test]
fn negative_and_out_of_range_length_and_dscp_survive_negotiation() {
// These lengths are all at or below the default maximum, and DSCP is
// accepted as decoded in this slice, including values a socket could never
// carry. Restricting a DSCP the server cannot apply is a later policy
// decision, not an admission rule.
// accepted as decoded, including values a socket could never carry:
// negotiation restricts neither, and a value that is not a byte is simply
// transported unmarked (see `traffic_class`).
//
// A negative length in particular stays negative in both the reply and the
// stored session; the packet-length policy reduces oversized *positive*
+4 -4
View File
@@ -18,7 +18,7 @@ use super::support::{
core_with_sources, echo_at, echo_params, echo_request, expect_echo_reply, manual_core,
open_negotiated, other_peer, peer, ManualClock, ScriptedTokens,
};
use crate::ServerConfig;
use crate::{OutboundDatagram, ServerConfig};
const TOKEN_A: u64 = 0x0102_0304_0506_0708;
const TOKEN_B: u64 = 0x1112_1314_1516_1718;
@@ -41,9 +41,9 @@ fn params_at(interval_ns: i64) -> Params {
}
/// The received count a reply reported, or `None` if there was no reply.
fn count_of(packet: Option<Vec<u8>>, params: &Params) -> Option<u32> {
let packet = packet?;
expect_echo_reply(&packet, params, None).recv_count
fn count_of(reply: Option<OutboundDatagram>, params: &Params) -> Option<u32> {
let reply = reply?;
expect_echo_reply(reply, params, None).recv_count
}
#[test]
+15 -11
View File
@@ -12,7 +12,7 @@ use irtt_proto::{
use crate::{
clock::{ClockSample, ClockSource},
core::ServerCore,
core::{OutboundDatagram, ServerCore},
error::ServerError,
token::TokenSource,
ServerConfig,
@@ -231,7 +231,7 @@ pub(crate) fn echo_at(
sequence: u32,
params: &Params,
hmac_key: Option<&[u8]>,
) -> Option<Vec<u8>> {
) -> Option<OutboundDatagram> {
clock.set(at_ns);
core.handle_datagram(
peer(),
@@ -314,11 +314,12 @@ pub(crate) fn echo_request(
/// Decodes an echo reply the server produced and checks the flags every reply
/// must carry. Decoding verifies the MAC against `hmac_key` on the way.
pub(crate) fn expect_echo_reply(
packet: &[u8],
packet: impl AsRef<[u8]>,
params: &Params,
hmac_key: Option<&[u8]>,
) -> EchoReply {
let reply = decode_echo_reply(packet, params, hmac_key).expect("server echo reply must decode");
let reply = decode_echo_reply(packet.as_ref(), params, hmac_key)
.expect("server echo reply must decode");
// Exact equality, so Open and Close are pinned clear as well: Open makes an
// upstream client abort, and Close belongs to the server-initiated close
// slice.
@@ -334,12 +335,12 @@ pub(crate) fn expect_echo_reply(
/// by the same path as any other, which is the point: the close is not a
/// separate packet kind.
pub(crate) fn expect_closing_echo_reply(
packet: &[u8],
packet: impl AsRef<[u8]>,
params: &Params,
hmac_key: Option<&[u8]>,
) -> EchoReply {
let reply =
decode_echo_reply(packet, params, hmac_key).expect("a closing echo reply must decode");
let reply = decode_echo_reply(packet.as_ref(), params, hmac_key)
.expect("a closing echo reply must decode");
let expected = FLAG_REPLY | FLAG_CLOSE | if hmac_key.is_some() { FLAG_HMAC } else { 0 };
assert_eq!(reply.flags, expected, "closing echo reply flags");
reply
@@ -452,11 +453,14 @@ pub(crate) fn client_params() -> Params {
}
/// Decodes a reply the server produced, checking it against the server's key.
pub(crate) fn decode_reply(packet: &[u8], hmac_key: Option<&[u8]>) -> OpenReply {
decode_open_reply(packet, hmac_key).expect("server reply must decode")
pub(crate) fn decode_reply(packet: impl AsRef<[u8]>, hmac_key: Option<&[u8]>) -> OpenReply {
decode_open_reply(packet.as_ref(), hmac_key).expect("server reply must decode")
}
pub(crate) fn expect_normal_open_reply(packet: &[u8], hmac_key: Option<&[u8]>) -> OpenReply {
pub(crate) fn expect_normal_open_reply(
packet: impl AsRef<[u8]>,
hmac_key: Option<&[u8]>,
) -> OpenReply {
let reply = decode_reply(packet, hmac_key);
let expected = FLAG_OPEN | FLAG_REPLY | if hmac_key.is_some() { FLAG_HMAC } else { 0 };
assert_eq!(reply.flags, expected, "normal open reply flags");
@@ -465,7 +469,7 @@ pub(crate) fn expect_normal_open_reply(packet: &[u8], hmac_key: Option<&[u8]>) -
reply
}
pub(crate) fn expect_no_test_reply(packet: &[u8], hmac_key: Option<&[u8]>) -> OpenReply {
pub(crate) fn expect_no_test_reply(packet: impl AsRef<[u8]>, hmac_key: Option<&[u8]>) -> OpenReply {
let reply = decode_reply(packet, hmac_key);
let expected =
FLAG_OPEN | FLAG_REPLY | FLAG_CLOSE | if hmac_key.is_some() { FLAG_HMAC } else { 0 };
@@ -0,0 +1,134 @@
//! Which traffic class the core asks for each reply to be sent with.
//!
//! The negotiated DSCP parameter is the **raw IPv4 TOS / IPv6 Traffic Class
//! byte**, not a six-bit codepoint: the codepoint is its upper six bits and the
//! low two are ECN. Nothing here shifts, masks or rounds it, and these tests
//! exist to keep it that way.
//!
//! The clean specification's Section 20 settles the observable rules for values
//! in `0..=255`: an echo reply carries its own session's value verbatim, the
//! marking follows the session rather than the listener, and an open reply is
//! unmarked even on a listener that has already sent a marked echo reply. The
//! server-close case is covered where the maximum-duration close is tested, and
//! the socket-level proof that these bytes reach the wire lives in the runtime's
//! Linux tests.
use irtt_proto::Params;
use super::support::{
client_params, core_with_tokens, echo_request, expect_echo_reply, expect_no_test_reply,
expect_normal_open_reply, no_test_request, open_request, peer, unthrottled, ScriptedTokens,
};
const TOKEN_A: u64 = 0x7896_b6ab_8771_5213;
/// A raw byte whose low two bits are set on purpose: ECN is part of the value,
/// and a server rebuilding the byte from a codepoint would lose them.
const RAW_TRAFFIC_CLASS: i64 = 0xbb;
#[test]
fn an_open_reply_is_unmarked_whatever_the_session_negotiates() {
let requested = Params {
dscp: 0xb8,
..client_params()
};
let mut core = core_with_tokens(unthrottled(), ScriptedTokens::new([TOKEN_A]));
let reply = core
.handle_datagram(peer(), &open_request(&requested, None))
.unwrap()
.expect("an ordinary open must be answered");
assert_eq!(
expect_normal_open_reply(&reply, None).params.dscp,
0xb8,
"the negotiated value is returned to the client"
);
assert_eq!(
reply.traffic_class(),
0,
"an open reply is unmarked however the session it creates is marked"
);
let no_test = core
.handle_datagram(peer(), &no_test_request(&requested, None))
.unwrap()
.expect("a no-test open must be answered");
assert_eq!(expect_no_test_reply(&no_test, None).params.dscp, 0xb8);
assert_eq!(
no_test.traffic_class(),
0,
"a no-test reply is unmarked too"
);
}
#[test]
fn an_echo_reply_carries_the_sessions_raw_negotiated_byte() {
let requested = Params {
dscp: RAW_TRAFFIC_CLASS,
..client_params()
};
let mut core = core_with_tokens(unthrottled(), ScriptedTokens::new([TOKEN_A]));
let open = core
.handle_datagram(peer(), &open_request(&requested, None))
.unwrap()
.expect("the session-creating open must be answered");
let negotiated = expect_normal_open_reply(&open, None).params;
let reply = core
.handle_datagram(peer(), &echo_request(TOKEN_A, 0, &negotiated, &[], None))
.unwrap()
.expect("an admissible echo must be answered");
expect_echo_reply(&reply, &negotiated, None);
assert_eq!(
i64::from(reply.traffic_class()),
RAW_TRAFFIC_CLASS,
"the raw byte, unshifted and unmasked"
);
}
#[test]
fn a_negotiated_value_that_is_not_a_byte_is_transported_unmarked() {
// `irtt-rs` policy, not observed behavior. The server deliberately accepts
// a DSCP parameter outside `0..=255` and returns and stores it unchanged;
// the clean specification records the reference host's handling of such
// values as platform-specific and explicitly not a compatibility
// requirement. A value that is not a byte cannot be a traffic-class byte,
// so the transport asks for none — rather than wrapping −1 into 255,
// truncating 256 to 0, or refusing a session the client can otherwise use.
for dscp in [-1, 256, i64::MAX] {
let requested = Params {
dscp,
..client_params()
};
let mut core = core_with_tokens(unthrottled(), ScriptedTokens::new([TOKEN_A]));
let open = core
.handle_datagram(peer(), &open_request(&requested, None))
.unwrap()
.unwrap_or_else(|| panic!("an open requesting DSCP {dscp} must still be answered"));
let negotiated = expect_normal_open_reply(&open, None).params;
assert_eq!(negotiated.dscp, dscp, "the value is negotiated unchanged");
assert_eq!(open.traffic_class(), 0);
assert_eq!(
core.session(TOKEN_A)
.expect("the session must be live")
.params()
.dscp,
dscp,
"and stored unchanged"
);
let reply = core
.handle_datagram(peer(), &echo_request(TOKEN_A, 0, &negotiated, &[], None))
.unwrap()
.unwrap_or_else(|| panic!("the session must still serve echoes with DSCP {dscp}"));
assert_eq!(
expect_echo_reply(&reply, &negotiated, None).recv_count,
Some(1),
"an unmarkable session is an ordinary session in every other way"
);
assert_eq!(reply.traffic_class(), 0, "transported unmarked");
}
}