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