feat(client): track and report specific negotiation restrictions
Replace basic validation with a detailed negotiation process that records specific parameter changes. This allows the client to provide better feedback when a server restricts requested session parameters like duration or DSCP.
This commit is contained in:
7 files changed
+401
-79
No files matched your search
@@ -1456,6 +1456,7 @@ mod tests {
|
|||||||
interval_ns: 1_000_000_000,
|
interval_ns: 1_000_000_000,
|
||||||
..Params::default()
|
..Params::default()
|
||||||
},
|
},
|
||||||
|
restrictions: vec![],
|
||||||
},
|
},
|
||||||
at: test_timestamp(Duration::from_secs(1)),
|
at: test_timestamp(Duration::from_secs(1)),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use crate::{
|
|||||||
metadata::ReceiveMeta,
|
metadata::ReceiveMeta,
|
||||||
probe::{CompletedSet, PendingMap, PendingProbe, TimedOutMap},
|
probe::{CompletedSet, PendingMap, PendingProbe, TimedOutMap},
|
||||||
receive::recv_datagram,
|
receive::recv_datagram,
|
||||||
session::{validate_negotiated_params, ActiveSession, ClientPhase, NegotiatedParams},
|
session::{negotiate_params, ActiveSession, ClientPhase, NegotiatedParams},
|
||||||
socket::{connect_udp_socket, resolve_remote, validate_open_timeouts},
|
socket::{connect_udp_socket, resolve_remote, validate_open_timeouts},
|
||||||
socket_options::{apply_dscp_to_socket, clear_dscp_on_socket},
|
socket_options::{apply_dscp_to_socket, clear_dscp_on_socket},
|
||||||
timing::ClientTimestamp,
|
timing::ClientTimestamp,
|
||||||
@@ -605,14 +605,12 @@ impl Client {
|
|||||||
reply: OpenReply,
|
reply: OpenReply,
|
||||||
now: ClientTimestamp,
|
now: ClientTimestamp,
|
||||||
) -> Result<OpenOutcome, ClientError> {
|
) -> Result<OpenOutcome, ClientError> {
|
||||||
validate_negotiated_params(
|
let token = reply.token;
|
||||||
|
let negotiated = negotiate_params(
|
||||||
&self.requested,
|
&self.requested,
|
||||||
&reply.params,
|
reply.params,
|
||||||
self.config.negotiation_policy,
|
self.config.negotiation_policy,
|
||||||
)?;
|
)?;
|
||||||
let negotiated = NegotiatedParams {
|
|
||||||
params: reply.params.clone(),
|
|
||||||
};
|
|
||||||
let recv_buffer_size = recv_buffer_size(self.config.hmac_key.is_some(), Some(&negotiated))?;
|
let recv_buffer_size = recv_buffer_size(self.config.hmac_key.is_some(), Some(&negotiated))?;
|
||||||
let negotiated_dscp =
|
let negotiated_dscp =
|
||||||
u8::try_from(negotiated.params.dscp).map_err(|_| ClientError::InvalidConfig {
|
u8::try_from(negotiated.params.dscp).map_err(|_| ClientError::InvalidConfig {
|
||||||
@@ -621,7 +619,7 @@ impl Client {
|
|||||||
apply_dscp_to_socket(&self.socket, self.remote, negotiated_dscp)?;
|
apply_dscp_to_socket(&self.socket, self.remote, negotiated_dscp)?;
|
||||||
self.negotiated = Some(negotiated.clone());
|
self.negotiated = Some(negotiated.clone());
|
||||||
self.recv_buffer.resize(recv_buffer_size, 0);
|
self.recv_buffer.resize(recv_buffer_size, 0);
|
||||||
self.phase = ClientPhase::Open { token: reply.token };
|
self.phase = ClientPhase::Open { token };
|
||||||
|
|
||||||
let end_mono = if negotiated.params.duration_ns > 0 {
|
let end_mono = if negotiated.params.duration_ns > 0 {
|
||||||
Some(negotiated_end_mono(
|
Some(negotiated_end_mono(
|
||||||
@@ -647,14 +645,14 @@ impl Client {
|
|||||||
|
|
||||||
let event = ClientEvent::SessionStarted {
|
let event = ClientEvent::SessionStarted {
|
||||||
remote: self.remote,
|
remote: self.remote,
|
||||||
token: reply.token,
|
token,
|
||||||
negotiated: negotiated.clone(),
|
negotiated: negotiated.clone(),
|
||||||
at: now,
|
at: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(OpenOutcome::Started {
|
Ok(OpenOutcome::Started {
|
||||||
remote: self.remote,
|
remote: self.remote,
|
||||||
token: reply.token,
|
token,
|
||||||
negotiated,
|
negotiated,
|
||||||
event,
|
event,
|
||||||
})
|
})
|
||||||
@@ -665,14 +663,11 @@ impl Client {
|
|||||||
reply: OpenReply,
|
reply: OpenReply,
|
||||||
now: ClientTimestamp,
|
now: ClientTimestamp,
|
||||||
) -> Result<OpenOutcome, ClientError> {
|
) -> Result<OpenOutcome, ClientError> {
|
||||||
validate_negotiated_params(
|
let negotiated = negotiate_params(
|
||||||
&self.requested,
|
&self.requested,
|
||||||
&reply.params,
|
reply.params,
|
||||||
self.config.negotiation_policy,
|
self.config.negotiation_policy,
|
||||||
)?;
|
)?;
|
||||||
let negotiated = NegotiatedParams {
|
|
||||||
params: reply.params.clone(),
|
|
||||||
};
|
|
||||||
self.negotiated = Some(negotiated.clone());
|
self.negotiated = Some(negotiated.clone());
|
||||||
self.phase = ClientPhase::NoTestCompleted;
|
self.phase = ClientPhase::NoTestCompleted;
|
||||||
let event = ClientEvent::NoTestCompleted {
|
let event = ClientEvent::NoTestCompleted {
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ fn normal_open_uses_negotiated_dscp_not_requested_dscp() {
|
|||||||
|
|
||||||
let negotiated = assert_open_started(client.open().unwrap());
|
let negotiated = assert_open_started(client.open().unwrap());
|
||||||
assert_eq!(negotiated.params.dscp, 0);
|
assert_eq!(negotiated.params.dscp, 0);
|
||||||
|
assert_eq!(
|
||||||
|
negotiated.restrictions,
|
||||||
|
vec![crate::NegotiationRestriction::DscpChanged {
|
||||||
|
requested: 46,
|
||||||
|
negotiated: 0,
|
||||||
|
}]
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
socket_traffic_class(&client.socket, client.remote).unwrap(),
|
socket_traffic_class(&client.socket, client.remote).unwrap(),
|
||||||
0
|
0
|
||||||
|
|||||||
@@ -1,10 +1,29 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::NegotiationRestriction;
|
||||||
|
|
||||||
|
fn assert_negotiates(
|
||||||
|
requested: &Params,
|
||||||
|
returned: &Params,
|
||||||
|
policy: NegotiationPolicy,
|
||||||
|
) -> NegotiatedParams {
|
||||||
|
negotiate_params(requested, returned.clone(), policy)
|
||||||
|
.unwrap_or_else(|err| panic!("expected negotiation success, got {err:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rejection_reason(requested: &Params, returned: &Params, policy: NegotiationPolicy) -> String {
|
||||||
|
match negotiate_params(requested, returned.clone(), policy) {
|
||||||
|
Err(ClientError::NegotiationRejected { reason }) => reason,
|
||||||
|
other => panic!("expected negotiation rejection, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn strict_negotiation_accepts_identical_params() {
|
fn strict_negotiation_accepts_identical_params() {
|
||||||
let config = ClientConfig::default();
|
let config = ClientConfig::default();
|
||||||
let params = params_from_config(&config).unwrap();
|
let params = params_from_config(&config).unwrap();
|
||||||
assert!(validate_negotiated_params(¶ms, ¶ms, NegotiationPolicy::Strict).is_ok());
|
let negotiated = assert_negotiates(¶ms, ¶ms, NegotiationPolicy::Strict);
|
||||||
|
assert_eq!(negotiated.params, params);
|
||||||
|
assert!(negotiated.restrictions.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -13,20 +32,38 @@ fn strict_negotiation_rejects_changed_params() {
|
|||||||
let requested = params_from_config(&config).unwrap();
|
let requested = params_from_config(&config).unwrap();
|
||||||
let mut returned = requested.clone();
|
let mut returned = requested.clone();
|
||||||
returned.dscp = 1;
|
returned.dscp = 1;
|
||||||
assert!(matches!(
|
assert_eq!(
|
||||||
validate_negotiated_params(&requested, &returned, NegotiationPolicy::Strict),
|
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
|
||||||
Err(ClientError::NegotiationRejected { .. })
|
NegotiationRestriction::DscpChanged {
|
||||||
));
|
requested: requested.dscp,
|
||||||
|
negotiated: returned.dscp,
|
||||||
|
}
|
||||||
|
.message()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn loose_negotiation_accepts_server_restricted_params() {
|
fn loose_negotiation_accepts_server_restricted_params() {
|
||||||
let config = ClientConfig::default();
|
let config = ClientConfig::default();
|
||||||
let requested = params_from_config(&config).unwrap();
|
let mut requested = params_from_config(&config).unwrap();
|
||||||
|
requested.length = 128;
|
||||||
let mut returned = requested.clone();
|
let mut returned = requested.clone();
|
||||||
returned.duration_ns /= 2;
|
returned.duration_ns /= 2;
|
||||||
returned.length = 0;
|
returned.length = 0;
|
||||||
assert!(validate_negotiated_params(&requested, &returned, NegotiationPolicy::Loose).is_ok());
|
let negotiated = assert_negotiates(&requested, &returned, NegotiationPolicy::Loose);
|
||||||
|
assert_eq!(
|
||||||
|
negotiated.restrictions,
|
||||||
|
vec![
|
||||||
|
NegotiationRestriction::DurationReduced {
|
||||||
|
requested_ns: requested.duration_ns,
|
||||||
|
negotiated_ns: returned.duration_ns,
|
||||||
|
},
|
||||||
|
NegotiationRestriction::LengthReduced {
|
||||||
|
requested: requested.length,
|
||||||
|
negotiated: returned.length,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -37,11 +74,10 @@ fn loose_negotiation_rejects_non_positive_returned_interval() {
|
|||||||
for interval_ns in [0, -1] {
|
for interval_ns in [0, -1] {
|
||||||
let mut returned = requested.clone();
|
let mut returned = requested.clone();
|
||||||
returned.interval_ns = interval_ns;
|
returned.interval_ns = interval_ns;
|
||||||
assert!(matches!(
|
assert_eq!(
|
||||||
validate_negotiated_params(&requested, &returned, NegotiationPolicy::Loose),
|
rejection_reason(&requested, &returned, NegotiationPolicy::Loose),
|
||||||
Err(ClientError::NegotiationRejected { reason })
|
"interval must be positive"
|
||||||
if reason == "interval must be positive"
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,21 +87,28 @@ fn loose_negotiation_accepts_documented_duration_combinations() {
|
|||||||
let requested_finite = params_from_config(&finite_config).unwrap();
|
let requested_finite = params_from_config(&finite_config).unwrap();
|
||||||
|
|
||||||
let returned_same_finite = requested_finite.clone();
|
let returned_same_finite = requested_finite.clone();
|
||||||
assert!(validate_negotiated_params(
|
assert!(assert_negotiates(
|
||||||
&requested_finite,
|
&requested_finite,
|
||||||
&returned_same_finite,
|
&returned_same_finite,
|
||||||
NegotiationPolicy::Loose
|
NegotiationPolicy::Loose
|
||||||
)
|
)
|
||||||
.is_ok());
|
.restrictions
|
||||||
|
.is_empty());
|
||||||
|
|
||||||
let mut returned_shorter_finite = requested_finite.clone();
|
let mut returned_shorter_finite = requested_finite.clone();
|
||||||
returned_shorter_finite.duration_ns /= 2;
|
returned_shorter_finite.duration_ns /= 2;
|
||||||
assert!(validate_negotiated_params(
|
assert_eq!(
|
||||||
|
assert_negotiates(
|
||||||
&requested_finite,
|
&requested_finite,
|
||||||
&returned_shorter_finite,
|
&returned_shorter_finite,
|
||||||
NegotiationPolicy::Loose
|
NegotiationPolicy::Loose
|
||||||
)
|
)
|
||||||
.is_ok());
|
.restrictions,
|
||||||
|
vec![NegotiationRestriction::DurationReduced {
|
||||||
|
requested_ns: requested_finite.duration_ns,
|
||||||
|
negotiated_ns: returned_shorter_finite.duration_ns,
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
|
||||||
let continuous_config = ClientConfig {
|
let continuous_config = ClientConfig {
|
||||||
duration: None,
|
duration: None,
|
||||||
@@ -74,21 +117,28 @@ fn loose_negotiation_accepts_documented_duration_combinations() {
|
|||||||
let requested_continuous = params_from_config(&continuous_config).unwrap();
|
let requested_continuous = params_from_config(&continuous_config).unwrap();
|
||||||
|
|
||||||
let returned_continuous = requested_continuous.clone();
|
let returned_continuous = requested_continuous.clone();
|
||||||
assert!(validate_negotiated_params(
|
assert!(assert_negotiates(
|
||||||
&requested_continuous,
|
&requested_continuous,
|
||||||
&returned_continuous,
|
&returned_continuous,
|
||||||
NegotiationPolicy::Loose
|
NegotiationPolicy::Loose
|
||||||
)
|
)
|
||||||
.is_ok());
|
.restrictions
|
||||||
|
.is_empty());
|
||||||
|
|
||||||
let mut returned_finite = requested_continuous.clone();
|
let mut returned_finite = requested_continuous.clone();
|
||||||
returned_finite.duration_ns = 1_000_000_000;
|
returned_finite.duration_ns = 1_000_000_000;
|
||||||
assert!(validate_negotiated_params(
|
assert_eq!(
|
||||||
|
assert_negotiates(
|
||||||
&requested_continuous,
|
&requested_continuous,
|
||||||
&returned_finite,
|
&returned_finite,
|
||||||
NegotiationPolicy::Loose
|
NegotiationPolicy::Loose
|
||||||
)
|
)
|
||||||
.is_ok());
|
.restrictions,
|
||||||
|
vec![NegotiationRestriction::DurationReduced {
|
||||||
|
requested_ns: requested_continuous.duration_ns,
|
||||||
|
negotiated_ns: returned_finite.duration_ns,
|
||||||
|
}]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -98,11 +148,10 @@ fn loose_negotiation_rejects_finite_request_returned_continuous() {
|
|||||||
let mut returned = requested.clone();
|
let mut returned = requested.clone();
|
||||||
returned.duration_ns = 0;
|
returned.duration_ns = 0;
|
||||||
|
|
||||||
assert!(matches!(
|
assert_eq!(
|
||||||
validate_negotiated_params(&requested, &returned, NegotiationPolicy::Loose),
|
rejection_reason(&requested, &returned, NegotiationPolicy::Loose),
|
||||||
Err(ClientError::NegotiationRejected { reason })
|
"server returned continuous duration for finite request"
|
||||||
if reason == "server returned continuous duration for finite request"
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -111,10 +160,10 @@ fn loose_negotiation_rejects_negative_returned_length() {
|
|||||||
let requested = params_from_config(&config).unwrap();
|
let requested = params_from_config(&config).unwrap();
|
||||||
let mut returned = requested.clone();
|
let mut returned = requested.clone();
|
||||||
returned.length = -1;
|
returned.length = -1;
|
||||||
assert!(matches!(
|
assert_eq!(
|
||||||
validate_negotiated_params(&requested, &returned, NegotiationPolicy::Loose),
|
rejection_reason(&requested, &returned, NegotiationPolicy::Loose),
|
||||||
Err(ClientError::NegotiationRejected { reason }) if reason == "length must be non-negative"
|
"length must be non-negative"
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -123,10 +172,10 @@ fn strict_negotiation_rejects_negative_returned_length() {
|
|||||||
let requested = params_from_config(&config).unwrap();
|
let requested = params_from_config(&config).unwrap();
|
||||||
let mut returned = requested.clone();
|
let mut returned = requested.clone();
|
||||||
returned.length = -1;
|
returned.length = -1;
|
||||||
assert!(matches!(
|
assert_eq!(
|
||||||
validate_negotiated_params(&requested, &returned, NegotiationPolicy::Strict),
|
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
|
||||||
Err(ClientError::NegotiationRejected { reason }) if reason == "length must be non-negative"
|
"length must be non-negative"
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -137,10 +186,9 @@ fn loose_negotiation_rejects_runtime_invalid_returned_dscp() {
|
|||||||
for dscp in [-1, 64] {
|
for dscp in [-1, 64] {
|
||||||
let mut returned = requested.clone();
|
let mut returned = requested.clone();
|
||||||
returned.dscp = dscp;
|
returned.dscp = dscp;
|
||||||
assert!(matches!(
|
assert_eq!(
|
||||||
validate_negotiated_params(&requested, &returned, NegotiationPolicy::Loose),
|
rejection_reason(&requested, &returned, NegotiationPolicy::Loose),
|
||||||
Err(ClientError::NegotiationRejected { reason })
|
"dscp must be in range 0..=63"
|
||||||
if reason == "dscp must be in range 0..=63"
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,13 +79,21 @@ fn no_test_loose_negotiation_accepts_restricted_params() {
|
|||||||
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||||
config.run_mode = RunMode::NoTest;
|
config.run_mode = RunMode::NoTest;
|
||||||
config.negotiation_policy = NegotiationPolicy::Loose;
|
config.negotiation_policy = NegotiationPolicy::Loose;
|
||||||
let mut params = params_from_config(&config).unwrap();
|
let requested = params_from_config(&config).unwrap();
|
||||||
|
let mut params = requested.clone();
|
||||||
params.duration_ns /= 2;
|
params.duration_ns /= 2;
|
||||||
let server = no_test_server(params.clone(), 0);
|
let server = no_test_server(params.clone(), 0);
|
||||||
config.server_addr = server.addr.to_string();
|
config.server_addr = server.addr.to_string();
|
||||||
let mut client = Client::connect(config).unwrap();
|
let mut client = Client::connect(config).unwrap();
|
||||||
let negotiated = assert_no_test_completed(client.open().unwrap());
|
let negotiated = assert_no_test_completed(client.open().unwrap());
|
||||||
assert_eq!(negotiated.params, params);
|
assert_eq!(negotiated.params, params);
|
||||||
|
assert_eq!(
|
||||||
|
negotiated.restrictions,
|
||||||
|
vec![crate::NegotiationRestriction::DurationReduced {
|
||||||
|
requested_ns: requested.duration_ns,
|
||||||
|
negotiated_ns: params.duration_ns,
|
||||||
|
}]
|
||||||
|
);
|
||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,5 +31,5 @@ pub use managed::{
|
|||||||
CancellationToken, EventHub, EventSubscription, ManagedClient, ManagedClientSession,
|
CancellationToken, EventHub, EventSubscription, ManagedClient, ManagedClientSession,
|
||||||
SessionEndReason, SessionOutcome, SubscriberConfig, SubscriberOverflow,
|
SessionEndReason, SessionOutcome, SubscriberConfig, SubscriberOverflow,
|
||||||
};
|
};
|
||||||
pub use session::NegotiatedParams;
|
pub use session::{NegotiatedParams, NegotiationRestriction};
|
||||||
pub use timing::ClientTimestamp;
|
pub use timing::ClientTimestamp;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use irtt_proto::{Params, PROTOCOL_VERSION};
|
use irtt_proto::{Clock, Params, ReceivedStats, StampAt, PROTOCOL_VERSION};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{NegotiationPolicy, MAX_DSCP_CODEPOINT},
|
config::{NegotiationPolicy, MAX_DSCP_CODEPOINT},
|
||||||
@@ -11,6 +11,106 @@ use crate::{
|
|||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct NegotiatedParams {
|
pub struct NegotiatedParams {
|
||||||
pub params: Params,
|
pub params: Params,
|
||||||
|
pub restrictions: Vec<NegotiationRestriction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum NegotiationRestriction {
|
||||||
|
DurationReduced {
|
||||||
|
requested_ns: i64,
|
||||||
|
negotiated_ns: i64,
|
||||||
|
},
|
||||||
|
IntervalIncreased {
|
||||||
|
requested_ns: i64,
|
||||||
|
negotiated_ns: i64,
|
||||||
|
},
|
||||||
|
IntervalReduced {
|
||||||
|
requested_ns: i64,
|
||||||
|
negotiated_ns: i64,
|
||||||
|
},
|
||||||
|
LengthReduced {
|
||||||
|
requested: i64,
|
||||||
|
negotiated: i64,
|
||||||
|
},
|
||||||
|
ReceivedStatsChanged {
|
||||||
|
requested: ReceivedStats,
|
||||||
|
negotiated: ReceivedStats,
|
||||||
|
},
|
||||||
|
StampAtChanged {
|
||||||
|
requested: StampAt,
|
||||||
|
negotiated: StampAt,
|
||||||
|
},
|
||||||
|
ClockChanged {
|
||||||
|
requested: Clock,
|
||||||
|
negotiated: Clock,
|
||||||
|
},
|
||||||
|
DscpChanged {
|
||||||
|
requested: i64,
|
||||||
|
negotiated: i64,
|
||||||
|
},
|
||||||
|
ServerFillChanged,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NegotiationRestriction {
|
||||||
|
pub fn message(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::DurationReduced {
|
||||||
|
requested_ns: 0,
|
||||||
|
negotiated_ns,
|
||||||
|
} => {
|
||||||
|
format!("server limited continuous duration to {negotiated_ns} ns")
|
||||||
|
}
|
||||||
|
Self::DurationReduced {
|
||||||
|
requested_ns,
|
||||||
|
negotiated_ns,
|
||||||
|
} => {
|
||||||
|
format!("server reduced duration from {requested_ns} ns to {negotiated_ns} ns")
|
||||||
|
}
|
||||||
|
Self::IntervalIncreased {
|
||||||
|
requested_ns,
|
||||||
|
negotiated_ns,
|
||||||
|
} => {
|
||||||
|
format!("server increased interval from {requested_ns} ns to {negotiated_ns} ns")
|
||||||
|
}
|
||||||
|
Self::IntervalReduced {
|
||||||
|
requested_ns,
|
||||||
|
negotiated_ns,
|
||||||
|
} => {
|
||||||
|
format!("server reduced interval from {requested_ns} ns to {negotiated_ns} ns")
|
||||||
|
}
|
||||||
|
Self::LengthReduced {
|
||||||
|
requested,
|
||||||
|
negotiated,
|
||||||
|
} => {
|
||||||
|
format!("server reduced packet length from {requested} bytes to {negotiated} bytes")
|
||||||
|
}
|
||||||
|
Self::ReceivedStatsChanged {
|
||||||
|
requested,
|
||||||
|
negotiated,
|
||||||
|
} => {
|
||||||
|
format!("server changed received-stats from {requested:?} to {negotiated:?}")
|
||||||
|
}
|
||||||
|
Self::StampAtChanged {
|
||||||
|
requested,
|
||||||
|
negotiated,
|
||||||
|
} => {
|
||||||
|
format!("server changed stamp-at from {requested:?} to {negotiated:?}")
|
||||||
|
}
|
||||||
|
Self::ClockChanged {
|
||||||
|
requested,
|
||||||
|
negotiated,
|
||||||
|
} => {
|
||||||
|
format!("server changed clock from {requested:?} to {negotiated:?}")
|
||||||
|
}
|
||||||
|
Self::DscpChanged {
|
||||||
|
requested,
|
||||||
|
negotiated,
|
||||||
|
} => {
|
||||||
|
format!("server changed dscp from {requested} to {negotiated}")
|
||||||
|
}
|
||||||
|
Self::ServerFillChanged => "server changed payload fill behavior".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -35,17 +135,19 @@ pub(crate) struct ActiveSession {
|
|||||||
pub sending_done: bool,
|
pub sending_done: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn validate_negotiated_params(
|
pub(crate) fn negotiate_params(
|
||||||
requested: &Params,
|
requested: &Params,
|
||||||
returned: &Params,
|
returned: Params,
|
||||||
policy: NegotiationPolicy,
|
policy: NegotiationPolicy,
|
||||||
) -> Result<(), ClientError> {
|
) -> Result<NegotiatedParams, ClientError> {
|
||||||
if returned.protocol_version != PROTOCOL_VERSION {
|
if returned.protocol_version != PROTOCOL_VERSION {
|
||||||
return Err(ClientError::ProtocolVersionMismatch {
|
return Err(ClientError::ProtocolVersionMismatch {
|
||||||
requested: PROTOCOL_VERSION,
|
requested: PROTOCOL_VERSION,
|
||||||
received: returned.protocol_version,
|
received: returned.protocol_version,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
let mut restrictions = Vec::new();
|
||||||
|
|
||||||
validate_duration_restriction(requested.duration_ns, returned.duration_ns)?;
|
validate_duration_restriction(requested.duration_ns, returned.duration_ns)?;
|
||||||
if returned.length < 0 {
|
if returned.length < 0 {
|
||||||
return Err(ClientError::NegotiationRejected {
|
return Err(ClientError::NegotiationRejected {
|
||||||
@@ -64,12 +166,100 @@ pub(crate) fn validate_negotiated_params(
|
|||||||
}
|
}
|
||||||
validate_dscp_restriction(returned.dscp)?;
|
validate_dscp_restriction(returned.dscp)?;
|
||||||
|
|
||||||
if policy == NegotiationPolicy::Strict && returned != requested {
|
if returned.duration_ns < requested.duration_ns
|
||||||
return Err(ClientError::NegotiationRejected {
|
|| (requested.duration_ns == 0 && returned.duration_ns > 0)
|
||||||
reason: "returned params differ from requested params".to_owned(),
|
{
|
||||||
});
|
record_restriction(
|
||||||
|
policy,
|
||||||
|
&mut restrictions,
|
||||||
|
NegotiationRestriction::DurationReduced {
|
||||||
|
requested_ns: requested.duration_ns,
|
||||||
|
negotiated_ns: returned.duration_ns,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
if returned.interval_ns > requested.interval_ns {
|
||||||
|
record_restriction(
|
||||||
|
policy,
|
||||||
|
&mut restrictions,
|
||||||
|
NegotiationRestriction::IntervalIncreased {
|
||||||
|
requested_ns: requested.interval_ns,
|
||||||
|
negotiated_ns: returned.interval_ns,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if returned.interval_ns < requested.interval_ns {
|
||||||
|
record_restriction(
|
||||||
|
policy,
|
||||||
|
&mut restrictions,
|
||||||
|
NegotiationRestriction::IntervalReduced {
|
||||||
|
requested_ns: requested.interval_ns,
|
||||||
|
negotiated_ns: returned.interval_ns,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if returned.length < requested.length {
|
||||||
|
record_restriction(
|
||||||
|
policy,
|
||||||
|
&mut restrictions,
|
||||||
|
NegotiationRestriction::LengthReduced {
|
||||||
|
requested: requested.length,
|
||||||
|
negotiated: returned.length,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if returned.received_stats != requested.received_stats {
|
||||||
|
record_restriction(
|
||||||
|
policy,
|
||||||
|
&mut restrictions,
|
||||||
|
NegotiationRestriction::ReceivedStatsChanged {
|
||||||
|
requested: requested.received_stats,
|
||||||
|
negotiated: returned.received_stats,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if returned.stamp_at != requested.stamp_at {
|
||||||
|
record_restriction(
|
||||||
|
policy,
|
||||||
|
&mut restrictions,
|
||||||
|
NegotiationRestriction::StampAtChanged {
|
||||||
|
requested: requested.stamp_at,
|
||||||
|
negotiated: returned.stamp_at,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if returned.clock != requested.clock {
|
||||||
|
record_restriction(
|
||||||
|
policy,
|
||||||
|
&mut restrictions,
|
||||||
|
NegotiationRestriction::ClockChanged {
|
||||||
|
requested: requested.clock,
|
||||||
|
negotiated: returned.clock,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if returned.dscp != requested.dscp {
|
||||||
|
record_restriction(
|
||||||
|
policy,
|
||||||
|
&mut restrictions,
|
||||||
|
NegotiationRestriction::DscpChanged {
|
||||||
|
requested: requested.dscp,
|
||||||
|
negotiated: returned.dscp,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if returned.server_fill != requested.server_fill {
|
||||||
|
record_restriction(
|
||||||
|
policy,
|
||||||
|
&mut restrictions,
|
||||||
|
NegotiationRestriction::ServerFillChanged,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(NegotiatedParams {
|
||||||
|
params: returned,
|
||||||
|
restrictions,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_duration_restriction(requested: i64, returned: i64) -> Result<(), ClientError> {
|
fn validate_duration_restriction(requested: i64, returned: i64) -> Result<(), ClientError> {
|
||||||
@@ -104,6 +294,21 @@ fn validate_dscp_restriction(returned: i64) -> Result<(), ClientError> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn record_restriction(
|
||||||
|
policy: NegotiationPolicy,
|
||||||
|
restrictions: &mut Vec<NegotiationRestriction>,
|
||||||
|
restriction: NegotiationRestriction,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
if policy == NegotiationPolicy::Strict {
|
||||||
|
return Err(ClientError::NegotiationRejected {
|
||||||
|
reason: restriction.message(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
restrictions.push(restriction);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -127,17 +332,26 @@ mod tests {
|
|||||||
|
|
||||||
fn assert_rejected(requested: &Params, returned: &Params, policy: NegotiationPolicy) {
|
fn assert_rejected(requested: &Params, returned: &Params, policy: NegotiationPolicy) {
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
validate_negotiated_params(requested, returned, policy),
|
negotiate_params(requested, returned.clone(), policy),
|
||||||
Err(ClientError::NegotiationRejected { .. })
|
Err(ClientError::NegotiationRejected { .. })
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn assert_negotiates(
|
||||||
|
requested: &Params,
|
||||||
|
returned: &Params,
|
||||||
|
policy: NegotiationPolicy,
|
||||||
|
) -> NegotiatedParams {
|
||||||
|
negotiate_params(requested, returned.clone(), policy)
|
||||||
|
.unwrap_or_else(|err| panic!("expected negotiation success, got {err:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
fn rejection_reason(
|
fn rejection_reason(
|
||||||
requested: &Params,
|
requested: &Params,
|
||||||
returned: &Params,
|
returned: &Params,
|
||||||
policy: NegotiationPolicy,
|
policy: NegotiationPolicy,
|
||||||
) -> String {
|
) -> String {
|
||||||
match validate_negotiated_params(requested, returned, policy) {
|
match negotiate_params(requested, returned.clone(), policy) {
|
||||||
Err(ClientError::NegotiationRejected { reason }) => reason,
|
Err(ClientError::NegotiationRejected { reason }) => reason,
|
||||||
other => panic!("expected negotiation rejection, got {other:?}"),
|
other => panic!("expected negotiation rejection, got {other:?}"),
|
||||||
}
|
}
|
||||||
@@ -149,27 +363,65 @@ mod tests {
|
|||||||
|
|
||||||
let mut returned = requested.clone();
|
let mut returned = requested.clone();
|
||||||
returned.length = 128;
|
returned.length = 128;
|
||||||
assert_rejected(&requested, &returned, NegotiationPolicy::Strict);
|
assert_eq!(
|
||||||
|
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
|
||||||
|
NegotiationRestriction::LengthReduced {
|
||||||
|
requested: requested.length,
|
||||||
|
negotiated: returned.length,
|
||||||
|
}
|
||||||
|
.message()
|
||||||
|
);
|
||||||
|
|
||||||
let mut returned = requested.clone();
|
let mut returned = requested.clone();
|
||||||
returned.dscp = 8;
|
returned.dscp = 8;
|
||||||
assert_rejected(&requested, &returned, NegotiationPolicy::Strict);
|
assert_eq!(
|
||||||
|
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
|
||||||
|
NegotiationRestriction::DscpChanged {
|
||||||
|
requested: requested.dscp,
|
||||||
|
negotiated: returned.dscp,
|
||||||
|
}
|
||||||
|
.message()
|
||||||
|
);
|
||||||
|
|
||||||
let mut returned = requested.clone();
|
let mut returned = requested.clone();
|
||||||
returned.received_stats = ReceivedStats::Count;
|
returned.received_stats = ReceivedStats::Count;
|
||||||
assert_rejected(&requested, &returned, NegotiationPolicy::Strict);
|
assert_eq!(
|
||||||
|
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
|
||||||
|
NegotiationRestriction::ReceivedStatsChanged {
|
||||||
|
requested: requested.received_stats,
|
||||||
|
negotiated: returned.received_stats,
|
||||||
|
}
|
||||||
|
.message()
|
||||||
|
);
|
||||||
|
|
||||||
let mut returned = requested.clone();
|
let mut returned = requested.clone();
|
||||||
returned.stamp_at = StampAt::Midpoint;
|
returned.stamp_at = StampAt::Midpoint;
|
||||||
assert_rejected(&requested, &returned, NegotiationPolicy::Strict);
|
assert_eq!(
|
||||||
|
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
|
||||||
|
NegotiationRestriction::StampAtChanged {
|
||||||
|
requested: requested.stamp_at,
|
||||||
|
negotiated: returned.stamp_at,
|
||||||
|
}
|
||||||
|
.message()
|
||||||
|
);
|
||||||
|
|
||||||
let mut returned = requested.clone();
|
let mut returned = requested.clone();
|
||||||
returned.clock = Clock::Wall;
|
returned.clock = Clock::Wall;
|
||||||
assert_rejected(&requested, &returned, NegotiationPolicy::Strict);
|
assert_eq!(
|
||||||
|
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
|
||||||
|
NegotiationRestriction::ClockChanged {
|
||||||
|
requested: requested.clock,
|
||||||
|
negotiated: returned.clock,
|
||||||
|
}
|
||||||
|
.message()
|
||||||
|
);
|
||||||
|
|
||||||
let mut returned = requested.clone();
|
let mut returned = requested.clone();
|
||||||
returned.server_fill = None;
|
returned.server_fill = None;
|
||||||
assert_rejected(&requested, &returned, NegotiationPolicy::Strict);
|
assert_eq!(
|
||||||
|
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
|
||||||
|
NegotiationRestriction::ServerFillChanged.message()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -178,8 +430,13 @@ mod tests {
|
|||||||
|
|
||||||
let mut returned = requested.clone();
|
let mut returned = requested.clone();
|
||||||
returned.duration_ns = requested.duration_ns / 2;
|
returned.duration_ns = requested.duration_ns / 2;
|
||||||
assert!(
|
let negotiated = assert_negotiates(&requested, &returned, NegotiationPolicy::Loose);
|
||||||
validate_negotiated_params(&requested, &returned, NegotiationPolicy::Loose).is_ok()
|
assert_eq!(
|
||||||
|
negotiated.restrictions,
|
||||||
|
vec![NegotiationRestriction::DurationReduced {
|
||||||
|
requested_ns: requested.duration_ns,
|
||||||
|
negotiated_ns: returned.duration_ns,
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut returned = requested.clone();
|
let mut returned = requested.clone();
|
||||||
@@ -200,12 +457,18 @@ mod tests {
|
|||||||
continuous_requested.duration_ns = 0;
|
continuous_requested.duration_ns = 0;
|
||||||
let mut finite_returned = continuous_requested.clone();
|
let mut finite_returned = continuous_requested.clone();
|
||||||
finite_returned.duration_ns = 1_000_000_000;
|
finite_returned.duration_ns = 1_000_000_000;
|
||||||
assert!(validate_negotiated_params(
|
let negotiated = assert_negotiates(
|
||||||
&continuous_requested,
|
&continuous_requested,
|
||||||
&finite_returned,
|
&finite_returned,
|
||||||
NegotiationPolicy::Loose
|
NegotiationPolicy::Loose,
|
||||||
)
|
);
|
||||||
.is_ok());
|
assert_eq!(
|
||||||
|
negotiated.restrictions,
|
||||||
|
vec![NegotiationRestriction::DurationReduced {
|
||||||
|
requested_ns: 0,
|
||||||
|
negotiated_ns: finite_returned.duration_ns,
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
|
||||||
assert_rejected(
|
assert_rejected(
|
||||||
&continuous_requested,
|
&continuous_requested,
|
||||||
@@ -213,9 +476,9 @@ mod tests {
|
|||||||
NegotiationPolicy::Strict,
|
NegotiationPolicy::Strict,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(validate_negotiated_params(
|
assert!(negotiate_params(
|
||||||
&continuous_requested,
|
|
||||||
&continuous_requested,
|
&continuous_requested,
|
||||||
|
continuous_requested.clone(),
|
||||||
NegotiationPolicy::Strict
|
NegotiationPolicy::Strict
|
||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
|
|||||||
Reference in new issue
Block a user