1use std::{fmt, time::Duration};
2
3use irtt_proto::{Clock, Params, ReceivedStats, StampAt, PROTOCOL_VERSION};
4
5use crate::{config::NegotiationPolicy, error::ClientError};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct AcceptedSessionParameters {
13 pub duration: Option<Duration>,
15 pub interval: Duration,
17 pub length: u32,
19 pub received_stats: ReceivedStats,
20 pub stamp_at: StampAt,
21 pub clock: Clock,
22 pub dscp: u8,
24 pub server_fill: Option<String>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct NegotiationResult {
31 pub accepted: AcceptedSessionParameters,
32 pub peer_params: Params,
37 pub changes: Vec<NegotiationChange>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum NegotiationChange {
44 DurationReduced {
46 requested: Option<Duration>,
48 negotiated: Duration,
49 },
50 IntervalIncreased {
51 requested: Duration,
52 negotiated: Duration,
53 },
54 IntervalReduced {
56 requested: Duration,
57 negotiated: Duration,
58 },
59 LengthReduced {
60 requested: u32,
61 negotiated: u32,
62 },
63 ReceivedStatsChanged {
64 requested: ReceivedStats,
65 negotiated: ReceivedStats,
66 },
67 StampAtChanged {
68 requested: StampAt,
69 negotiated: StampAt,
70 },
71 ClockChanged {
72 requested: Clock,
73 negotiated: Clock,
74 },
75 DscpChanged {
77 requested: u8,
78 negotiated: u8,
79 },
80 ServerFillChanged {
81 requested: Option<String>,
82 negotiated: Option<String>,
83 },
84}
85
86impl NegotiationChange {
87 pub fn message(&self) -> String {
89 self.to_string()
90 }
91}
92
93impl fmt::Display for NegotiationChange {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 match self {
96 Self::DurationReduced {
97 requested: None,
98 negotiated,
99 } => {
100 write!(
101 f,
102 "server limited continuous duration to {} ns",
103 negotiated.as_nanos()
104 )
105 }
106 Self::DurationReduced {
107 requested: Some(requested),
108 negotiated,
109 } => {
110 write!(
111 f,
112 "server reduced duration from {} ns to {} ns",
113 requested.as_nanos(),
114 negotiated.as_nanos()
115 )
116 }
117 Self::IntervalIncreased {
118 requested,
119 negotiated,
120 } => {
121 write!(
122 f,
123 "server increased interval from {} ns to {} ns",
124 requested.as_nanos(),
125 negotiated.as_nanos()
126 )
127 }
128 Self::IntervalReduced {
129 requested,
130 negotiated,
131 } => {
132 write!(
133 f,
134 "server reduced interval from {} ns to {} ns",
135 requested.as_nanos(),
136 negotiated.as_nanos()
137 )
138 }
139 Self::LengthReduced {
140 requested,
141 negotiated,
142 } => {
143 write!(
144 f,
145 "server reduced packet length from {requested} bytes to {negotiated} bytes"
146 )
147 }
148 Self::ReceivedStatsChanged {
149 requested,
150 negotiated,
151 } => {
152 write!(
153 f,
154 "server changed received-stats from {requested:?} to {negotiated:?}"
155 )
156 }
157 Self::StampAtChanged {
158 requested,
159 negotiated,
160 } => {
161 write!(
162 f,
163 "server changed stamp-at from {requested:?} to {negotiated:?}"
164 )
165 }
166 Self::ClockChanged {
167 requested,
168 negotiated,
169 } => {
170 write!(
171 f,
172 "server changed clock from {requested:?} to {negotiated:?}"
173 )
174 }
175 Self::DscpChanged {
176 requested,
177 negotiated,
178 } => {
179 write!(f, "server changed DSCP from {requested} to {negotiated}")
180 }
181 Self::ServerFillChanged { .. } => write!(f, "server changed payload fill behavior"),
182 }
183 }
184}
185
186fn positive_duration(nanos: i64) -> Duration {
188 Duration::from_nanos(u64::try_from(nanos).expect("validated non-negative nanoseconds"))
189}
190
191fn finite_duration(nanos: i64) -> Option<Duration> {
192 (nanos != 0).then(|| positive_duration(nanos))
193}
194
195pub(crate) fn negotiate_params(
196 requested: &Params,
197 returned: Params,
198 policy: NegotiationPolicy,
199) -> Result<NegotiationResult, ClientError> {
200 if returned.protocol_version != PROTOCOL_VERSION {
201 return Err(ClientError::ProtocolVersionMismatch {
202 requested: PROTOCOL_VERSION,
203 received: returned.protocol_version,
204 });
205 }
206 let mut changes = Vec::new();
207
208 validate_duration_restriction(requested.duration_ns, returned.duration_ns)?;
209 if returned.length < 0 {
210 return Err(ClientError::NegotiationRejected {
211 reason: "length must be non-negative".to_owned(),
212 });
213 }
214 if returned.length > requested.length {
215 return Err(ClientError::NegotiationRejected {
216 reason: "length increased".to_owned(),
217 });
218 }
219 if returned.interval_ns <= 0 {
220 return Err(ClientError::NegotiationRejected {
221 reason: "interval must be positive".to_owned(),
222 });
223 }
224 validate_dscp_restriction(returned.dscp)?;
225
226 if returned.duration_ns < requested.duration_ns
227 || (requested.duration_ns == 0 && returned.duration_ns > 0)
228 {
229 record_change(
230 policy,
231 &mut changes,
232 NegotiationChange::DurationReduced {
233 requested: finite_duration(requested.duration_ns),
234 negotiated: positive_duration(returned.duration_ns),
235 },
236 )?;
237 }
238 if returned.interval_ns > requested.interval_ns {
239 record_change(
240 policy,
241 &mut changes,
242 NegotiationChange::IntervalIncreased {
243 requested: positive_duration(requested.interval_ns),
244 negotiated: positive_duration(returned.interval_ns),
245 },
246 )?;
247 }
248 if returned.interval_ns < requested.interval_ns {
249 record_change(
250 policy,
251 &mut changes,
252 NegotiationChange::IntervalReduced {
253 requested: positive_duration(requested.interval_ns),
254 negotiated: positive_duration(returned.interval_ns),
255 },
256 )?;
257 }
258 if returned.length < requested.length {
259 record_change(
260 policy,
261 &mut changes,
262 NegotiationChange::LengthReduced {
263 requested: u32::try_from(requested.length).expect("validated requested length"),
264 negotiated: u32::try_from(returned.length).expect("validated returned length"),
265 },
266 )?;
267 }
268 if returned.received_stats != requested.received_stats {
269 record_change(
270 policy,
271 &mut changes,
272 NegotiationChange::ReceivedStatsChanged {
273 requested: requested.received_stats,
274 negotiated: returned.received_stats,
275 },
276 )?;
277 }
278 if returned.stamp_at != requested.stamp_at {
279 record_change(
280 policy,
281 &mut changes,
282 NegotiationChange::StampAtChanged {
283 requested: requested.stamp_at,
284 negotiated: returned.stamp_at,
285 },
286 )?;
287 }
288 if returned.clock != requested.clock {
289 record_change(
290 policy,
291 &mut changes,
292 NegotiationChange::ClockChanged {
293 requested: requested.clock,
294 negotiated: returned.clock,
295 },
296 )?;
297 }
298 if returned.dscp != requested.dscp && returned.dscp != 0 {
299 return Err(ClientError::NegotiationRejected {
300 reason: "server returned unsupported DSCP change".to_owned(),
301 });
302 }
303 if returned.dscp == 0 && requested.dscp != 0 {
304 record_change(
305 policy,
306 &mut changes,
307 NegotiationChange::DscpChanged {
308 requested: u8::try_from(requested.dscp >> 2).expect("validated requested DSCP"),
312 negotiated: u8::try_from(returned.dscp >> 2).expect("validated returned DSCP"),
313 },
314 )?;
315 }
316 if returned.server_fill != requested.server_fill {
317 record_change(
318 policy,
319 &mut changes,
320 NegotiationChange::ServerFillChanged {
321 requested: requested
322 .server_fill
323 .as_ref()
324 .map(|fill| fill.value.clone()),
325 negotiated: returned.server_fill.as_ref().map(|fill| fill.value.clone()),
326 },
327 )?;
328 }
329
330 let accepted = AcceptedSessionParameters {
333 duration: finite_duration(returned.duration_ns),
334 interval: positive_duration(returned.interval_ns),
335 length: u32::try_from(returned.length).expect("validated returned length"),
336 received_stats: returned.received_stats,
337 stamp_at: returned.stamp_at,
338 clock: returned.clock,
339 dscp: u8::try_from(returned.dscp >> 2).expect("validated returned DSCP"),
340 server_fill: returned.server_fill.as_ref().map(|fill| fill.value.clone()),
341 };
342 Ok(NegotiationResult {
343 accepted,
344 peer_params: returned,
345 changes,
346 })
347}
348
349fn validate_duration_restriction(requested: i64, returned: i64) -> Result<(), ClientError> {
350 if returned < 0 {
351 return Err(ClientError::NegotiationRejected {
352 reason: "duration must be non-negative".to_owned(),
353 });
354 }
355
356 if requested > 0 && returned == 0 {
357 return Err(ClientError::NegotiationRejected {
358 reason: "server returned continuous duration for finite request".to_owned(),
359 });
360 }
361
362 if requested > 0 && returned > requested {
363 return Err(ClientError::NegotiationRejected {
364 reason: "duration increased".to_owned(),
365 });
366 }
367
368 Ok(())
369}
370
371fn validate_dscp_restriction(returned: i64) -> Result<(), ClientError> {
376 if !(0..=255).contains(&returned) {
377 return Err(ClientError::NegotiationRejected {
378 reason: "dscp must be in range 0..=255".to_owned(),
379 });
380 }
381
382 Ok(())
383}
384
385fn record_change(
386 policy: NegotiationPolicy,
387 changes: &mut Vec<NegotiationChange>,
388 change: NegotiationChange,
389) -> Result<(), ClientError> {
390 if policy == NegotiationPolicy::Strict {
391 return Err(ClientError::NegotiationRejected {
392 reason: change.message(),
393 });
394 }
395
396 changes.push(change);
397 Ok(())
398}