feat(server): add lifetime negotiation policy
This commit is contained in:
10 files changed
+437
-34
No files matched your search
@@ -15,6 +15,7 @@ mod admission;
|
||||
mod close;
|
||||
mod echo;
|
||||
mod hmac;
|
||||
mod negotiation;
|
||||
mod no_test;
|
||||
mod open;
|
||||
mod params;
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
//! Parameter restrictions the server applies to an open request.
|
||||
//!
|
||||
//! Every case drives a real open through the core and reads the restriction out
|
||||
//! of the open reply, because the reply is the contract: a client enforces its
|
||||
//! own restriction policy against exactly these values, and the server has to
|
||||
//! honor what it returned.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use irtt_proto::Params;
|
||||
|
||||
use super::support::{
|
||||
client_params, core_with_tokens, expect_normal_open_reply, open_request, peer, ScriptedTokens,
|
||||
};
|
||||
use crate::ServerConfig;
|
||||
|
||||
const TOKEN_A: u64 = 0x2f45_9a01_c37d_6e88;
|
||||
|
||||
/// Negotiates one open against `config` and returns the restricted parameters.
|
||||
fn negotiated(config: ServerConfig, requested: &Params) -> Params {
|
||||
let mut core = core_with_tokens(config, ScriptedTokens::new([TOKEN_A]));
|
||||
let packet = core
|
||||
.handle_datagram(peer(), &open_request(requested, None))
|
||||
.unwrap()
|
||||
.expect("the open must be answered");
|
||||
expect_normal_open_reply(&packet, None).params
|
||||
}
|
||||
|
||||
fn requesting(duration_ns: i64, interval_ns: i64) -> Params {
|
||||
Params {
|
||||
duration_ns,
|
||||
interval_ns,
|
||||
..client_params()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_requested_duration_is_reduced_to_the_configured_maximum() {
|
||||
let config = || ServerConfig::default().with_max_test_duration(Duration::from_secs(2));
|
||||
|
||||
assert_eq!(
|
||||
negotiated(config(), &requesting(10_000_000_000, 1_000_000_000)).duration_ns,
|
||||
2_000_000_000,
|
||||
"a longer duration is reduced to the maximum"
|
||||
);
|
||||
assert_eq!(
|
||||
negotiated(config(), &requesting(1_000_000_000, 1_000_000_000)).duration_ns,
|
||||
1_000_000_000,
|
||||
"a duration under the maximum is left alone"
|
||||
);
|
||||
// Duration zero is absent from the request, which means continuous. A
|
||||
// server with a finite maximum is restricting continuous mode to a finite
|
||||
// test, which is a reduction the client already models.
|
||||
assert_eq!(
|
||||
negotiated(config(), &requesting(0, 1_000_000_000)).duration_ns,
|
||||
2_000_000_000,
|
||||
"a continuous request takes the maximum"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_a_configured_maximum_a_duration_is_never_restricted() {
|
||||
let config = || ServerConfig::default();
|
||||
|
||||
assert_eq!(
|
||||
negotiated(config(), &requesting(86_400_000_000_000, 1_000_000_000)).duration_ns,
|
||||
86_400_000_000_000,
|
||||
"a day-long test is the operator's business, not negotiation's"
|
||||
);
|
||||
assert_eq!(
|
||||
negotiated(config(), &requesting(0, 1_000_000_000)).duration_ns,
|
||||
0,
|
||||
"continuous stays continuous"
|
||||
);
|
||||
|
||||
// A zero maximum is stored as no maximum rather than negotiated: Duration
|
||||
// zero on the wire means continuous, so a finite maximum of zero could only
|
||||
// be expressed by turning a client's finite test into an endless one.
|
||||
let zero = ServerConfig::default().with_max_test_duration(Duration::ZERO);
|
||||
assert_eq!(zero.max_test_duration(), None);
|
||||
assert_eq!(
|
||||
negotiated(zero, &requesting(1_000_000_000, 1_000_000_000)).duration_ns,
|
||||
1_000_000_000
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_requested_interval_is_raised_to_the_configured_minimum() {
|
||||
let config = || ServerConfig::default().with_min_send_interval(Duration::from_millis(100));
|
||||
|
||||
assert_eq!(
|
||||
negotiated(config(), &requesting(3_000_000_000, 20_000_000)).interval_ns,
|
||||
100_000_000,
|
||||
"a faster interval is raised to the minimum"
|
||||
);
|
||||
assert_eq!(
|
||||
negotiated(config(), &requesting(3_000_000_000, 500_000_000)).interval_ns,
|
||||
500_000_000,
|
||||
"an interval above the minimum is left alone"
|
||||
);
|
||||
// An absent Interval is the wire default zero, not a refusal — an explicit
|
||||
// zero is rejected during request admission and never reaches negotiation —
|
||||
// so it is raised like any other value below the floor.
|
||||
assert_eq!(
|
||||
negotiated(config(), &requesting(3_000_000_000, 0)).interval_ns,
|
||||
100_000_000,
|
||||
"an absent interval takes the minimum"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_negotiated_interval_is_capped_at_a_quarter_of_the_idle_timeout() {
|
||||
// Without the cap, a client sending at the interval it was handed would
|
||||
// idle its own session out.
|
||||
let config = ServerConfig::default().with_idle_timeout(Duration::from_secs(8));
|
||||
|
||||
assert_eq!(
|
||||
negotiated(config, &requesting(3_000_000_000, 5_000_000_000)).interval_ns,
|
||||
2_000_000_000
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_idle_cap_wins_over_a_larger_configured_minimum_interval() {
|
||||
// The floor is applied first and the cap second, so a minimum above a
|
||||
// quarter of the idle timeout produces a negotiated interval below that
|
||||
// minimum. This is the configuration the specification records as an
|
||||
// upstream hazard: upstream returns 2 s here and still replenishes reply
|
||||
// allowance every 5 s, so a client obeying the reply is rate-limited.
|
||||
//
|
||||
// `irtt-rs` returns the same 2 s — the cap has to win, or the session
|
||||
// cannot stay alive — and refills at 2 s instead. `rate` proves the
|
||||
// enforcement half; this pins the value the client is told.
|
||||
let config = ServerConfig::default()
|
||||
.with_min_send_interval(Duration::from_secs(5))
|
||||
.with_idle_timeout(Duration::from_secs(8));
|
||||
|
||||
assert_eq!(
|
||||
negotiated(config, &requesting(3_000_000_000, 10_000_000)).interval_ns,
|
||||
2_000_000_000
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zero_idle_timeout_caps_nothing() {
|
||||
// A zero timeout expires a session at the first evaluation, so there is no
|
||||
// liveness to protect and a quarter of zero must not be read as a cap of
|
||||
// zero — which would negotiate an interval the client would reject.
|
||||
let config = ServerConfig::default()
|
||||
.with_idle_timeout(Duration::ZERO)
|
||||
.with_min_send_interval(Duration::from_millis(100));
|
||||
|
||||
assert_eq!(
|
||||
negotiated(config, &requesting(3_000_000_000, 3_600_000_000_000)).interval_ns,
|
||||
3_600_000_000_000
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use irtt_proto::{verify_packet_hmac, Clock, Params, StampAt};
|
||||
|
||||
use super::support::{
|
||||
client_params, core_with_tokens, expect_no_test_reply, no_test_request, open_request,
|
||||
open_request_with_raw_params, param_int, peer, ScriptedTokens, KEY,
|
||||
open_request_with_raw_params, param_int, peer, unthrottled, ScriptedTokens, KEY,
|
||||
};
|
||||
use crate::ServerConfig;
|
||||
|
||||
@@ -200,7 +200,9 @@ fn an_authenticated_no_test_reply_is_authenticated_and_still_creates_nothing() {
|
||||
|
||||
#[test]
|
||||
fn an_empty_no_test_open_is_answered() {
|
||||
let mut core = core_with_tokens(ServerConfig::default(), ScriptedTokens::new([TOKEN_A]));
|
||||
// No interval floor, so the reply shows the wire defaults an empty payload
|
||||
// decoded to rather than the timing policy negotiation would apply to them.
|
||||
let mut core = core_with_tokens(unthrottled(), ScriptedTokens::new([TOKEN_A]));
|
||||
|
||||
let mut request = open_request_with_raw_params(&[], None);
|
||||
request[3] |= irtt_proto::FLAG_CLOSE;
|
||||
|
||||
@@ -2,7 +2,7 @@ use irtt_proto::{Clock, Params, ReceivedStats, ServerFill, StampAt};
|
||||
|
||||
use super::support::{
|
||||
client_params, core_with_tokens, expect_normal_open_reply, open_request,
|
||||
open_request_with_raw_params, param_int, peer, ScriptedTokens, KEY,
|
||||
open_request_with_raw_params, param_int, peer, unthrottled, ScriptedTokens, KEY,
|
||||
};
|
||||
use crate::{ServerConfig, DEFAULT_MAX_PACKET_LENGTH};
|
||||
|
||||
@@ -84,7 +84,11 @@ fn any_requested_protocol_version_is_accepted_and_answered_with_one() {
|
||||
|
||||
#[test]
|
||||
fn an_empty_parameter_payload_is_answered_with_only_a_version() {
|
||||
let mut core = core_with_tokens(ServerConfig::default(), ScriptedTokens::new([TOKEN_A]));
|
||||
// The interval floor is disabled here so that the reply carries nothing but
|
||||
// the version: with one configured, an absent Interval negotiates up to it
|
||||
// and is encoded, which is interval policy rather than what this test is
|
||||
// about.
|
||||
let mut core = core_with_tokens(unthrottled(), ScriptedTokens::new([TOKEN_A]));
|
||||
|
||||
let packet = core
|
||||
.handle_datagram(peer(), &open_request_with_raw_params(&[], None))
|
||||
@@ -114,7 +118,7 @@ fn an_empty_parameter_payload_is_answered_with_only_a_version() {
|
||||
|
||||
#[test]
|
||||
fn unknown_parameter_tags_are_ignored_and_not_reflected_in_the_reply() {
|
||||
let mut core = core_with_tokens(ServerConfig::default(), ScriptedTokens::new([TOKEN_A]));
|
||||
let mut core = core_with_tokens(unthrottled(), ScriptedTokens::new([TOKEN_A]));
|
||||
|
||||
let mut payload = param_int(1, 1);
|
||||
payload.extend_from_slice(¶m_int(42, 7));
|
||||
@@ -187,12 +191,14 @@ fn selecting_timestamps_without_a_clock_is_silently_refused() {
|
||||
|
||||
#[test]
|
||||
fn current_non_length_parameters_survive_negotiation_unchanged() {
|
||||
// Duration, interval, statistics, timestamps, DSCP and server fill are the
|
||||
// values a later restriction-policy slice will deliberately start changing.
|
||||
// Pinning them now makes that a visible decision rather than an accident.
|
||||
// Version and an oversized packet length are already restricted, and have
|
||||
// their own tests; the length here is deliberately under the default
|
||||
// maximum so that this test says nothing about the packet-length policy.
|
||||
// Statistics, timestamps, DSCP and server fill are the values a later
|
||||
// restriction-policy slice will deliberately start changing. Pinning them
|
||||
// now makes that a visible decision rather than an accident. Version, an
|
||||
// oversized packet length, the duration maximum and the interval floor and
|
||||
// cap are already restricted and have their own tests; the length here is
|
||||
// deliberately under the default maximum, and the configuration below
|
||||
// leaves duration and interval unrestricted, so this test says nothing
|
||||
// about either policy.
|
||||
//
|
||||
// This is about what *negotiation* rewrites, which is a separate question
|
||||
// from whether the core will acknowledge the result: an open whose
|
||||
@@ -212,7 +218,9 @@ fn current_non_length_parameters_survive_negotiation_unchanged() {
|
||||
}),
|
||||
};
|
||||
|
||||
let mut core = core_with_tokens(ServerConfig::default(), ScriptedTokens::new([TOKEN_A]));
|
||||
// No duration maximum is configured by default, and the interval floor is
|
||||
// disabled, so a one-nanosecond interval is left where it was asked for.
|
||||
let mut core = core_with_tokens(unthrottled(), ScriptedTokens::new([TOKEN_A]));
|
||||
let packet = core
|
||||
.handle_datagram(peer(), &open_request(&unrestricted, None))
|
||||
.unwrap()
|
||||
|
||||
@@ -2,7 +2,7 @@ use irtt_proto::{Clock, Params, ReceivedStats, ServerFill, StampAt};
|
||||
|
||||
use super::support::{
|
||||
core_with_tokens, expect_normal_open_reply, open_request, open_request_with_raw_params,
|
||||
param_int, param_server_fill, peer, ScriptedTokens,
|
||||
param_int, param_server_fill, peer, unthrottled, ScriptedTokens,
|
||||
};
|
||||
use crate::ServerConfig;
|
||||
|
||||
@@ -70,7 +70,12 @@ fn malformed_or_out_of_range_parameters_are_dropped_without_a_session() {
|
||||
fn absent_duration_interval_and_clock_are_accepted_as_wire_defaults() {
|
||||
// The whole reason the decoder reports presence: an omitted tag and one
|
||||
// explicitly encoded as zero share a value but not a verdict.
|
||||
let mut core = core_with_tokens(ServerConfig::default(), ScriptedTokens::new([TOKEN_A]));
|
||||
//
|
||||
// The interval floor is disabled so that the accepted wire default is
|
||||
// visible in the reply. An absent Interval is a value like any other to
|
||||
// negotiation, and the default floor would raise it; that is interval
|
||||
// policy, and `negotiation` tests it.
|
||||
let mut core = core_with_tokens(unthrottled(), ScriptedTokens::new([TOKEN_A]));
|
||||
|
||||
let packet = core
|
||||
.handle_datagram(
|
||||
@@ -138,7 +143,10 @@ fn valid_optional_parameter_values_survive_unchanged() {
|
||||
},
|
||||
),
|
||||
] {
|
||||
let mut core = core_with_tokens(ServerConfig::default(), ScriptedTokens::new([TOKEN_A]));
|
||||
// No interval floor: these rows are about which *values* survive
|
||||
// decoding and negotiation, not about the timing policy applied to
|
||||
// them.
|
||||
let mut core = core_with_tokens(unthrottled(), ScriptedTokens::new([TOKEN_A]));
|
||||
|
||||
let packet = core
|
||||
.handle_datagram(peer(), &open_request(&requested, None))
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::{
|
||||
collections::VecDeque,
|
||||
net::SocketAddr,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use irtt_proto::{
|
||||
@@ -20,6 +21,22 @@ use crate::{
|
||||
pub(crate) const KEY: &[u8] = b"correctkey";
|
||||
pub(crate) const OTHER_KEY: &[u8] = b"wrongkey";
|
||||
|
||||
/// A default configuration with the minimum send interval disabled.
|
||||
///
|
||||
/// One knob, two consequences, and tests about neither of them want both gone:
|
||||
/// no requested interval is raised during negotiation, and no otherwise
|
||||
/// admissible echo ever waits for rate allowance. That keeps a test about
|
||||
/// parameter pass-through, receive statistics or timestamps from silently
|
||||
/// becoming a test of interval policy — and, since these tests drive the core as
|
||||
/// fast as the machine will run, from being rate-limited by a floor they never
|
||||
/// meant to configure.
|
||||
///
|
||||
/// Tests that *are* about interval negotiation, rate limiting or lifetime build
|
||||
/// their configuration explicitly.
|
||||
pub(crate) fn unthrottled() -> ServerConfig {
|
||||
ServerConfig::default().with_min_send_interval(Duration::ZERO)
|
||||
}
|
||||
|
||||
pub(crate) fn peer() -> SocketAddr {
|
||||
"198.51.100.7:41234".parse().unwrap()
|
||||
}
|
||||
@@ -135,7 +152,7 @@ pub(crate) fn core_with_sources(
|
||||
/// Most behavior has to hold identically with and without authentication, so
|
||||
/// tests loop over both and build the core from the key.
|
||||
pub(crate) fn core_for(hmac_key: Option<&[u8]>, tokens: ScriptedTokens) -> ServerCore {
|
||||
let mut config = ServerConfig::default();
|
||||
let mut config = unthrottled();
|
||||
if let Some(key) = hmac_key {
|
||||
config = config.with_hmac_key(key);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user