1use std::{fmt, time::Instant};
2
3use irtt_proto::{Clock, Params, ReceivedStats, StampAt, PROTOCOL_VERSION};
4
5use crate::{
6 config::{NegotiationPolicy, MAX_DSCP_CODEPOINT},
7 error::ClientError,
8 probe::{CompletedSet, PendingMap, TimedOutMap},
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct NegotiatedParams {
18 pub params: Params,
20 pub restrictions: Vec<NegotiationRestriction>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum NegotiationRestriction {
27 DurationReduced {
33 requested_ns: i64,
34 negotiated_ns: i64,
35 },
36 IntervalIncreased {
38 requested_ns: i64,
39 negotiated_ns: i64,
40 },
41 IntervalReduced {
43 requested_ns: i64,
44 negotiated_ns: i64,
45 },
46 LengthReduced { requested: i64, negotiated: i64 },
48 ReceivedStatsChanged {
50 requested: ReceivedStats,
51 negotiated: ReceivedStats,
52 },
53 StampAtChanged {
55 requested: StampAt,
56 negotiated: StampAt,
57 },
58 ClockChanged { requested: Clock, negotiated: Clock },
60 DscpChanged { requested: i64, negotiated: i64 },
62 ServerFillChanged,
64}
65
66impl NegotiationRestriction {
67 pub fn message(&self) -> String {
69 self.to_string()
70 }
71}
72
73impl fmt::Display for NegotiationRestriction {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 match self {
76 Self::DurationReduced {
77 requested_ns: 0,
78 negotiated_ns,
79 } => {
80 write!(
81 f,
82 "server limited continuous duration to {negotiated_ns} ns"
83 )
84 }
85 Self::DurationReduced {
86 requested_ns,
87 negotiated_ns,
88 } => {
89 write!(
90 f,
91 "server reduced duration from {requested_ns} ns to {negotiated_ns} ns"
92 )
93 }
94 Self::IntervalIncreased {
95 requested_ns,
96 negotiated_ns,
97 } => {
98 write!(
99 f,
100 "server increased interval from {requested_ns} ns to {negotiated_ns} ns"
101 )
102 }
103 Self::IntervalReduced {
104 requested_ns,
105 negotiated_ns,
106 } => {
107 write!(
108 f,
109 "server reduced interval from {requested_ns} ns to {negotiated_ns} ns"
110 )
111 }
112 Self::LengthReduced {
113 requested,
114 negotiated,
115 } => {
116 write!(
117 f,
118 "server reduced packet length from {requested} bytes to {negotiated} bytes"
119 )
120 }
121 Self::ReceivedStatsChanged {
122 requested,
123 negotiated,
124 } => {
125 write!(
126 f,
127 "server changed received-stats from {requested:?} to {negotiated:?}"
128 )
129 }
130 Self::StampAtChanged {
131 requested,
132 negotiated,
133 } => {
134 write!(
135 f,
136 "server changed stamp-at from {requested:?} to {negotiated:?}"
137 )
138 }
139 Self::ClockChanged {
140 requested,
141 negotiated,
142 } => {
143 write!(
144 f,
145 "server changed clock from {requested:?} to {negotiated:?}"
146 )
147 }
148 Self::DscpChanged {
149 requested,
150 negotiated,
151 } => {
152 write!(f, "server changed DSCP from {requested} to {negotiated}")
153 }
154 Self::ServerFillChanged => write!(f, "server changed payload fill behavior"),
155 }
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub(crate) enum ClientPhase {
161 Connected,
162 Open { token: u64 },
163 NoTestCompleted,
164 Closed,
165}
166
167#[derive(Debug)]
168pub(crate) struct ActiveSession {
169 pub next_wire_seq: u32,
170 pub highest_received_seq: Option<u32>,
171 pub packets_sent: u64,
172 pub start_mono: Instant,
173 pub end_mono: Option<Instant>,
174 pub next_send_at: Instant,
175 pub pending: PendingMap,
176 pub timed_out: TimedOutMap,
177 pub completed: CompletedSet,
178 pub sending_done: bool,
179}
180
181pub(crate) fn negotiate_params(
182 requested: &Params,
183 returned: Params,
184 policy: NegotiationPolicy,
185) -> Result<NegotiatedParams, ClientError> {
186 if returned.protocol_version != PROTOCOL_VERSION {
187 return Err(ClientError::ProtocolVersionMismatch {
188 requested: PROTOCOL_VERSION,
189 received: returned.protocol_version,
190 });
191 }
192 let mut restrictions = Vec::new();
193
194 validate_duration_restriction(requested.duration_ns, returned.duration_ns)?;
195 if returned.length < 0 {
196 return Err(ClientError::NegotiationRejected {
197 reason: "length must be non-negative".to_owned(),
198 });
199 }
200 if returned.length > requested.length {
201 return Err(ClientError::NegotiationRejected {
202 reason: "length increased".to_owned(),
203 });
204 }
205 if returned.interval_ns <= 0 {
206 return Err(ClientError::NegotiationRejected {
207 reason: "interval must be positive".to_owned(),
208 });
209 }
210 validate_dscp_restriction(returned.dscp)?;
211
212 if returned.duration_ns < requested.duration_ns
213 || (requested.duration_ns == 0 && returned.duration_ns > 0)
214 {
215 record_restriction(
216 policy,
217 &mut restrictions,
218 NegotiationRestriction::DurationReduced {
219 requested_ns: requested.duration_ns,
220 negotiated_ns: returned.duration_ns,
221 },
222 )?;
223 }
224 if returned.interval_ns > requested.interval_ns {
225 record_restriction(
226 policy,
227 &mut restrictions,
228 NegotiationRestriction::IntervalIncreased {
229 requested_ns: requested.interval_ns,
230 negotiated_ns: returned.interval_ns,
231 },
232 )?;
233 }
234 if returned.interval_ns < requested.interval_ns {
235 record_restriction(
236 policy,
237 &mut restrictions,
238 NegotiationRestriction::IntervalReduced {
239 requested_ns: requested.interval_ns,
240 negotiated_ns: returned.interval_ns,
241 },
242 )?;
243 }
244 if returned.length < requested.length {
245 record_restriction(
246 policy,
247 &mut restrictions,
248 NegotiationRestriction::LengthReduced {
249 requested: requested.length,
250 negotiated: returned.length,
251 },
252 )?;
253 }
254 if returned.received_stats != requested.received_stats {
255 record_restriction(
256 policy,
257 &mut restrictions,
258 NegotiationRestriction::ReceivedStatsChanged {
259 requested: requested.received_stats,
260 negotiated: returned.received_stats,
261 },
262 )?;
263 }
264 if returned.stamp_at != requested.stamp_at {
265 record_restriction(
266 policy,
267 &mut restrictions,
268 NegotiationRestriction::StampAtChanged {
269 requested: requested.stamp_at,
270 negotiated: returned.stamp_at,
271 },
272 )?;
273 }
274 if returned.clock != requested.clock {
275 record_restriction(
276 policy,
277 &mut restrictions,
278 NegotiationRestriction::ClockChanged {
279 requested: requested.clock,
280 negotiated: returned.clock,
281 },
282 )?;
283 }
284 if returned.dscp != requested.dscp && returned.dscp != 0 {
285 return Err(ClientError::NegotiationRejected {
286 reason: "server returned unsupported DSCP change".to_owned(),
287 });
288 }
289 if returned.dscp == 0 && requested.dscp != 0 {
290 record_restriction(
291 policy,
292 &mut restrictions,
293 NegotiationRestriction::DscpChanged {
294 requested: requested.dscp,
295 negotiated: returned.dscp,
296 },
297 )?;
298 }
299 if returned.server_fill != requested.server_fill {
300 record_restriction(
301 policy,
302 &mut restrictions,
303 NegotiationRestriction::ServerFillChanged,
304 )?;
305 }
306
307 Ok(NegotiatedParams {
308 params: returned,
309 restrictions,
310 })
311}
312
313fn validate_duration_restriction(requested: i64, returned: i64) -> Result<(), ClientError> {
314 if returned < 0 {
315 return Err(ClientError::NegotiationRejected {
316 reason: "duration must be non-negative".to_owned(),
317 });
318 }
319
320 if requested > 0 && returned == 0 {
321 return Err(ClientError::NegotiationRejected {
322 reason: "server returned continuous duration for finite request".to_owned(),
323 });
324 }
325
326 if requested > 0 && returned > requested {
327 return Err(ClientError::NegotiationRejected {
328 reason: "duration increased".to_owned(),
329 });
330 }
331
332 Ok(())
333}
334
335fn validate_dscp_restriction(returned: i64) -> Result<(), ClientError> {
336 if !(0..=i64::from(MAX_DSCP_CODEPOINT)).contains(&returned) {
337 return Err(ClientError::NegotiationRejected {
338 reason: format!("dscp must be in range 0..={MAX_DSCP_CODEPOINT}"),
339 });
340 }
341
342 Ok(())
343}
344
345fn record_restriction(
346 policy: NegotiationPolicy,
347 restrictions: &mut Vec<NegotiationRestriction>,
348 restriction: NegotiationRestriction,
349) -> Result<(), ClientError> {
350 if policy == NegotiationPolicy::Strict {
351 return Err(ClientError::NegotiationRejected {
352 reason: restriction.message(),
353 });
354 }
355
356 restrictions.push(restriction);
357 Ok(())
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use irtt_proto::{Clock, ReceivedStats, ServerFill, StampAt};
364
365 fn default_params() -> Params {
366 Params {
367 protocol_version: PROTOCOL_VERSION,
368 duration_ns: 3_000_000_000,
369 interval_ns: 1_000_000_000,
370 length: 256,
371 received_stats: ReceivedStats::Both,
372 stamp_at: StampAt::Both,
373 clock: Clock::Both,
374 dscp: 46,
375 server_fill: Some(ServerFill {
376 value: "rand".to_owned(),
377 }),
378 }
379 }
380
381 fn assert_rejected(requested: &Params, returned: &Params, policy: NegotiationPolicy) {
382 assert!(matches!(
383 negotiate_params(requested, returned.clone(), policy),
384 Err(ClientError::NegotiationRejected { .. })
385 ));
386 }
387
388 fn assert_negotiates(
389 requested: &Params,
390 returned: &Params,
391 policy: NegotiationPolicy,
392 ) -> NegotiatedParams {
393 negotiate_params(requested, returned.clone(), policy)
394 .unwrap_or_else(|err| panic!("expected negotiation success, got {err:?}"))
395 }
396
397 fn rejection_reason(
398 requested: &Params,
399 returned: &Params,
400 policy: NegotiationPolicy,
401 ) -> String {
402 match negotiate_params(requested, returned.clone(), policy) {
403 Err(ClientError::NegotiationRejected { reason }) => reason,
404 other => panic!("expected negotiation rejection, got {other:?}"),
405 }
406 }
407
408 #[test]
409 fn strict_rejects_changed_negotiated_fields() {
410 let requested = default_params();
411
412 let mut returned = requested.clone();
413 returned.length = 128;
414 assert_eq!(
415 rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
416 NegotiationRestriction::LengthReduced {
417 requested: requested.length,
418 negotiated: returned.length,
419 }
420 .message()
421 );
422
423 let mut returned = requested.clone();
424 returned.dscp = 0;
425 assert_eq!(
426 rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
427 NegotiationRestriction::DscpChanged {
428 requested: requested.dscp,
429 negotiated: returned.dscp,
430 }
431 .message()
432 );
433
434 let mut returned = requested.clone();
435 returned.received_stats = ReceivedStats::Count;
436 assert_eq!(
437 rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
438 NegotiationRestriction::ReceivedStatsChanged {
439 requested: requested.received_stats,
440 negotiated: returned.received_stats,
441 }
442 .message()
443 );
444
445 let mut returned = requested.clone();
446 returned.stamp_at = StampAt::Midpoint;
447 assert_eq!(
448 rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
449 NegotiationRestriction::StampAtChanged {
450 requested: requested.stamp_at,
451 negotiated: returned.stamp_at,
452 }
453 .message()
454 );
455
456 let mut returned = requested.clone();
457 returned.clock = Clock::Wall;
458 assert_eq!(
459 rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
460 NegotiationRestriction::ClockChanged {
461 requested: requested.clock,
462 negotiated: returned.clock,
463 }
464 .message()
465 );
466
467 let mut returned = requested.clone();
468 returned.server_fill = None;
469 assert_eq!(
470 rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
471 NegotiationRestriction::ServerFillChanged.message()
472 );
473 }
474
475 #[test]
476 fn loose_duration_negotiation_uses_run_duration_semantics() {
477 let requested = default_params();
478
479 let mut returned = requested.clone();
480 returned.duration_ns = requested.duration_ns / 2;
481 let negotiated = assert_negotiates(&requested, &returned, NegotiationPolicy::Loose);
482 assert_eq!(
483 negotiated.restrictions,
484 vec![NegotiationRestriction::DurationReduced {
485 requested_ns: requested.duration_ns,
486 negotiated_ns: returned.duration_ns,
487 }]
488 );
489
490 let mut returned = requested.clone();
491 returned.duration_ns = requested.duration_ns + 1;
492 assert_eq!(
493 rejection_reason(&requested, &returned, NegotiationPolicy::Loose),
494 "duration increased"
495 );
496
497 let mut returned = requested.clone();
498 returned.duration_ns = 0;
499 assert_eq!(
500 rejection_reason(&requested, &returned, NegotiationPolicy::Loose),
501 "server returned continuous duration for finite request"
502 );
503
504 let mut continuous_requested = requested.clone();
505 continuous_requested.duration_ns = 0;
506 let mut finite_returned = continuous_requested.clone();
507 finite_returned.duration_ns = 1_000_000_000;
508 let negotiated = assert_negotiates(
509 &continuous_requested,
510 &finite_returned,
511 NegotiationPolicy::Loose,
512 );
513 assert_eq!(
514 negotiated.restrictions,
515 vec![NegotiationRestriction::DurationReduced {
516 requested_ns: 0,
517 negotiated_ns: finite_returned.duration_ns,
518 }]
519 );
520
521 assert_rejected(
522 &continuous_requested,
523 &finite_returned,
524 NegotiationPolicy::Strict,
525 );
526
527 assert!(negotiate_params(
528 &continuous_requested,
529 continuous_requested.clone(),
530 NegotiationPolicy::Strict
531 )
532 .is_ok());
533 }
534}