s2n_quic_transport/path/
challenge.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{contexts::WriteContext, transmission};
5use s2n_quic_core::{
6    ct::ConstantTimeEq,
7    event, frame,
8    time::{timer, Duration, Timer, Timestamp},
9};
10
11pub type Data = [u8; frame::path_challenge::DATA_LEN];
12const DISABLED_DATA: Data = [0; frame::path_challenge::DATA_LEN];
13
14#[derive(Clone, Debug)]
15pub struct Challenge {
16    state: State,
17    abandon_duration: Duration,
18    abandon_timer: Timer,
19    data: Data,
20}
21
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub enum State {
24    /// PATH_CHALLENGE is not used for path validation when initiating a new connection
25    InitialPathDisabled,
26
27    /// A Challenge frame must be sent. The `u8` represents the remaining number of retries
28    RequiresTransmission(u8),
29
30    /// Challenge has been sent and we are awaiting a response until the abandon timer expires
31    PendingResponse,
32
33    /// The Challenge has been abandoned due to the abandon_timer
34    Abandoned,
35
36    /// When the PATH_CHALLENGE was validated by a PATH_RESPONSE
37    Validated,
38}
39
40impl transmission::interest::Provider for State {
41    #[inline]
42    fn transmission_interest<Q: transmission::interest::Query>(
43        &self,
44        query: &mut Q,
45    ) -> transmission::interest::Result {
46        match self {
47            State::RequiresTransmission(_) => query.on_interest(transmission::Interest::NewData),
48            _ => Ok(()),
49        }
50    }
51}
52
53impl Challenge {
54    pub fn new(abandon_duration: Duration, data: Data) -> Self {
55        Self {
56            //= https://www.rfc-editor.org/rfc/rfc9000#section-8.2.1
57            //# An endpoint SHOULD NOT probe a new path with packets containing a
58            //# PATH_CHALLENGE frame more frequently than it would send an Initial
59            //# packet.
60
61            //= https://www.rfc-editor.org/rfc/rfc9000#section-8.2.1
62            //# An endpoint MAY send multiple PATH_CHALLENGE frames to guard against
63            //# packet loss.
64
65            // Re-transmitting twice guards against packet loss, while remaining
66            // below the amplification limit of 3.
67            state: State::RequiresTransmission(2),
68            abandon_duration,
69            abandon_timer: Timer::default(),
70            data,
71        }
72    }
73
74    pub fn disabled() -> Self {
75        Self {
76            state: State::InitialPathDisabled,
77            abandon_duration: Duration::ZERO,
78            abandon_timer: Timer::default(),
79            data: DISABLED_DATA,
80        }
81    }
82
83    /// When a PATH_CHALLENGE is transmitted this handles any internal state operations.
84    pub fn on_transmit<W: WriteContext>(&mut self, context: &mut W) {
85        match self.state {
86            State::RequiresTransmission(0) => self.state = State::PendingResponse,
87            State::RequiresTransmission(remaining) => {
88                //= https://www.rfc-editor.org/rfc/rfc9000#section-8.2.1
89                //# However, an endpoint SHOULD NOT send multiple
90                //# PATH_CHALLENGE frames in a single packet.
91                let frame = frame::PathChallenge { data: &self.data };
92
93                if context.write_frame(&frame).is_some() {
94                    let remaining = remaining - 1;
95                    self.state = State::RequiresTransmission(remaining);
96
97                    if !self.abandon_timer.is_armed() {
98                        self.abandon_timer
99                            .set(context.current_time() + self.abandon_duration);
100                    }
101                }
102            }
103            _ => {}
104        }
105    }
106
107    pub fn on_timeout<Pub: event::ConnectionPublisher>(
108        &mut self,
109        timestamp: Timestamp,
110        publisher: &mut Pub,
111        path: event::builder::Path,
112    ) {
113        if self.abandon_timer.poll_expiration(timestamp).is_ready() {
114            self.abandon(publisher, path);
115        }
116    }
117
118    pub fn abandon<Pub: event::ConnectionPublisher>(
119        &mut self,
120        publisher: &mut Pub,
121        path: event::builder::Path,
122    ) {
123        if self.is_pending() {
124            self.state = State::Abandoned;
125            self.abandon_timer.cancel();
126            publisher.on_path_challenge_updated(event::builder::PathChallengeUpdated {
127                path_challenge_status: event::builder::PathChallengeStatus::Abandoned,
128                path,
129                challenge_data: self.challenge_data(),
130            });
131        }
132    }
133
134    pub fn is_disabled(&self) -> bool {
135        matches!(self.state, State::InitialPathDisabled)
136    }
137
138    pub fn is_pending(&self) -> bool {
139        matches!(
140            self.state,
141            State::PendingResponse | State::RequiresTransmission(_)
142        )
143    }
144
145    pub fn on_validated(&mut self, data: &[u8]) -> bool {
146        if self.is_pending() && ConstantTimeEq::ct_eq(&self.data[..], data).into() {
147            self.state = State::Validated;
148            true
149        } else {
150            false
151        }
152    }
153
154    pub fn challenge_data(&self) -> &[u8] {
155        &self.data
156    }
157}
158
159impl timer::Provider for Challenge {
160    #[inline]
161    fn timers<Q: timer::Query>(&self, query: &mut Q) -> timer::Result {
162        self.abandon_timer.timers(query)?;
163
164        Ok(())
165    }
166}
167
168impl transmission::interest::Provider for Challenge {
169    #[inline]
170    fn transmission_interest<Q: transmission::interest::Query>(
171        &self,
172        query: &mut Q,
173    ) -> transmission::interest::Result {
174        self.state.transmission_interest(query)
175    }
176}
177
178#[cfg(any(test, feature = "testing"))]
179pub mod testing {
180    use super::*;
181    use s2n_quic_core::time::{Clock, Duration, NoopClock};
182
183    pub fn helper_challenge() -> Helper {
184        let now = NoopClock {}.get_time();
185        let abandon_duration = Duration::from_millis(10_000);
186        let expected_data: [u8; 8] = [0; 8];
187
188        let challenge = Challenge::new(abandon_duration, expected_data);
189
190        Helper {
191            now,
192            abandon_duration,
193            expected_data,
194            challenge,
195        }
196    }
197
198    #[allow(dead_code)]
199    pub struct Helper {
200        pub now: Timestamp,
201        pub abandon_duration: Duration,
202        pub expected_data: Data,
203        pub challenge: Challenge,
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use crate::contexts::testing::{MockWriteContext, OutgoingFrameBuffer};
211    use s2n_quic_core::{
212        endpoint,
213        time::{Clock, Duration, NoopClock},
214    };
215    use testing::*;
216
217    //= https://www.rfc-editor.org/rfc/rfc9000#section-8.2.1
218    //= type=test
219    //# An endpoint MAY send multiple PATH_CHALLENGE frames to guard against
220    //# packet loss.
221    #[test]
222    fn create_challenge_that_requires_two_transmissions() {
223        let helper = helper_challenge();
224        assert_eq!(helper.challenge.state, State::RequiresTransmission(2));
225    }
226
227    #[test]
228    fn create_disabled_challenge() {
229        let challenge = Challenge::disabled();
230        assert_eq!(challenge.state, State::InitialPathDisabled);
231        assert!(!challenge.abandon_timer.is_armed());
232    }
233
234    //= https://www.rfc-editor.org/rfc/rfc9000#section-8.2.1
235    //= type=test
236    //# An endpoint SHOULD NOT probe a new path with packets containing a
237    //# PATH_CHALLENGE frame more frequently than it would send an Initial
238    //# packet.
239    #[test]
240    fn transmit_challenge_only_twice() {
241        // Setup:
242        let mut helper = helper_challenge();
243        let mut frame_buffer = OutgoingFrameBuffer::new();
244        let mut context = MockWriteContext::new(
245            helper.now,
246            &mut frame_buffer,
247            transmission::Constraint::None,
248            transmission::Mode::Normal,
249            endpoint::Type::Client,
250        );
251        assert_eq!(helper.challenge.state, State::RequiresTransmission(2));
252
253        // Trigger:
254        helper.challenge.on_transmit(&mut context);
255
256        // Expectation:
257        //= https://www.rfc-editor.org/rfc/rfc9000#section-8.2.1
258        //= type=test
259        //# However, an endpoint SHOULD NOT send multiple
260        //# PATH_CHALLENGE frames in a single packet.
261        assert_eq!(context.frame_buffer.len(), 1);
262
263        assert_eq!(helper.challenge.state, State::RequiresTransmission(1));
264        let written_data = match context.frame_buffer.pop_front().unwrap().as_frame() {
265            frame::Frame::PathChallenge(frame) => Some(*frame.data),
266            _ => None,
267        }
268        .unwrap();
269        assert_eq!(written_data, helper.expected_data);
270
271        // Trigger:
272        helper.challenge.on_transmit(&mut context);
273
274        // Expectation:
275        assert_eq!(helper.challenge.state, State::RequiresTransmission(0));
276        let written_data = match context.frame_buffer.pop_front().unwrap().as_frame() {
277            frame::Frame::PathChallenge(frame) => Some(*frame.data),
278            _ => None,
279        }
280        .unwrap();
281        assert_eq!(written_data, helper.expected_data);
282
283        // Trigger:
284        helper.challenge.on_transmit(&mut context);
285
286        // Expectation:
287        assert_eq!(helper.challenge.state, State::PendingResponse);
288        assert_eq!(context.frame_buffer.len(), 0);
289    }
290
291    #[test]
292    fn successful_on_transmit_arms_the_timer() {
293        // Setup:
294        let mut helper = helper_challenge();
295        let mut frame_buffer = OutgoingFrameBuffer::new();
296        let mut context = MockWriteContext::new(
297            helper.now,
298            &mut frame_buffer,
299            transmission::Constraint::None,
300            transmission::Mode::Normal,
301            endpoint::Type::Client,
302        );
303        assert_eq!(helper.challenge.state, State::RequiresTransmission(2));
304        assert!(!helper.challenge.abandon_timer.is_armed());
305
306        // Trigger:
307        helper.challenge.on_transmit(&mut context);
308
309        // Expectation:
310        assert!(helper.challenge.abandon_timer.is_armed());
311    }
312
313    #[test]
314    fn maintain_idle_and_dont_transmit_when_pending_response_state() {
315        // Setup:
316        let mut helper = helper_challenge();
317        let mut frame_buffer = OutgoingFrameBuffer::new();
318        let mut context = MockWriteContext::new(
319            helper.now,
320            &mut frame_buffer,
321            transmission::Constraint::None,
322            transmission::Mode::Normal,
323            endpoint::Type::Client,
324        );
325        helper.challenge.state = State::PendingResponse;
326        assert_eq!(helper.challenge.state, State::PendingResponse);
327
328        // Trigger:
329        helper.challenge.on_transmit(&mut context);
330
331        // Expectation:
332        assert_eq!(helper.challenge.state, State::PendingResponse);
333        assert_eq!(context.frame_buffer.len(), 0);
334    }
335
336    #[test]
337    fn test_on_timeout() {
338        let mut helper = helper_challenge();
339        let expiration_time = helper.now + helper.abandon_duration;
340
341        let mut frame_buffer = OutgoingFrameBuffer::new();
342        let mut context = MockWriteContext::new(
343            helper.now,
344            &mut frame_buffer,
345            transmission::Constraint::None,
346            transmission::Mode::Normal,
347            endpoint::Type::Client,
348        );
349        helper.challenge.on_transmit(&mut context);
350
351        let mut publisher = event::testing::Publisher::snapshot();
352        let path = event::builder::Path::test();
353
354        helper.challenge.on_timeout(
355            expiration_time - Duration::from_millis(10),
356            &mut publisher,
357            path,
358        );
359        assert!(helper.challenge.is_pending());
360
361        let path = event::builder::Path::test();
362        helper.challenge.on_timeout(
363            expiration_time + Duration::from_millis(10),
364            &mut publisher,
365            path,
366        );
367        assert!(!helper.challenge.is_pending());
368    }
369
370    #[test]
371    fn challenge_must_remains_abandoned_once_abandoned() {
372        let mut helper = helper_challenge();
373        let expiration_time = helper.now + helper.abandon_duration;
374
375        let mut frame_buffer = OutgoingFrameBuffer::new();
376        let mut context = MockWriteContext::new(
377            helper.now,
378            &mut frame_buffer,
379            transmission::Constraint::None,
380            transmission::Mode::Normal,
381            endpoint::Type::Client,
382        );
383        helper.challenge.on_transmit(&mut context);
384
385        let mut publisher = event::testing::Publisher::snapshot();
386        let path = event::builder::Path::test();
387
388        // Trigger:
389        helper.challenge.on_timeout(
390            expiration_time + Duration::from_millis(10),
391            &mut publisher,
392            path,
393        );
394
395        // Expectation:
396        assert!(!helper.challenge.is_pending());
397
398        let path = event::builder::Path::test();
399
400        // Trigger:
401        helper.challenge.on_timeout(
402            expiration_time - Duration::from_millis(10),
403            &mut publisher,
404            path,
405        );
406
407        // Expectation:
408        assert!(!helper.challenge.is_pending());
409    }
410
411    #[test]
412    fn dont_abandon_disabled_state() {
413        let mut challenge = Challenge::disabled();
414        let now = NoopClock {}.get_time();
415
416        let mut frame_buffer = OutgoingFrameBuffer::new();
417        let mut context = MockWriteContext::new(
418            now,
419            &mut frame_buffer,
420            transmission::Constraint::None,
421            transmission::Mode::Normal,
422            endpoint::Type::Client,
423        );
424        challenge.on_transmit(&mut context);
425
426        assert_eq!(challenge.state, State::InitialPathDisabled);
427
428        let mut publisher = event::testing::Publisher::snapshot();
429        let path = event::builder::Path::test();
430
431        let large_expiration_time = now + Duration::from_secs(1_000_000);
432        challenge.on_timeout(large_expiration_time, &mut publisher, path);
433        assert_eq!(challenge.state, State::InitialPathDisabled);
434    }
435
436    #[test]
437    fn test_is_abandoned() {
438        let mut helper = helper_challenge();
439        let expiration_time = helper.now + helper.abandon_duration;
440
441        let mut frame_buffer = OutgoingFrameBuffer::new();
442        let mut context = MockWriteContext::new(
443            helper.now,
444            &mut frame_buffer,
445            transmission::Constraint::None,
446            transmission::Mode::Normal,
447            endpoint::Type::Client,
448        );
449        helper.challenge.on_transmit(&mut context);
450
451        let mut publisher = event::testing::Publisher::snapshot();
452        let path = event::builder::Path::test();
453
454        assert!(helper.challenge.is_pending());
455
456        helper.challenge.on_timeout(
457            expiration_time + Duration::from_millis(10),
458            &mut publisher,
459            path,
460        );
461        assert!(!helper.challenge.is_pending());
462    }
463
464    #[test]
465    fn test_on_validate() {
466        let mut helper = helper_challenge();
467
468        let wrong_data: [u8; 8] = [5; 8];
469        assert!(!helper.challenge.on_validated(&wrong_data));
470        assert!(helper.challenge.is_pending());
471
472        assert!(helper.challenge.on_validated(&helper.expected_data));
473        assert_eq!(helper.challenge.state, State::Validated);
474    }
475
476    #[test]
477    fn is_disabled() {
478        let challenge = Challenge::disabled();
479
480        assert_eq!(challenge.state, State::InitialPathDisabled);
481        assert!(challenge.is_disabled());
482    }
483
484    #[test]
485    fn dont_validate_disabled_state() {
486        let mut helper = helper_challenge();
487        helper.challenge.state = State::InitialPathDisabled;
488
489        assert!(!helper.challenge.on_validated(&helper.expected_data));
490        assert_eq!(helper.challenge.state, State::InitialPathDisabled);
491    }
492
493    #[test]
494    fn dont_abandon_a_validated_challenge() {
495        let mut helper = helper_challenge();
496        helper.challenge.state = State::Validated;
497        let mut publisher = event::testing::Publisher::snapshot();
498        let path = event::builder::Path::test();
499
500        helper.challenge.abandon(&mut publisher, path);
501
502        assert_eq!(helper.challenge.state, State::Validated);
503    }
504
505    #[test]
506    fn cancel_abandon_timer_on_abandon() {
507        let mut helper = helper_challenge();
508        let mut publisher = event::testing::Publisher::snapshot();
509        let path = event::builder::Path::test();
510
511        helper.challenge.abandon(&mut publisher, path);
512
513        assert!(!helper.challenge.abandon_timer.is_armed());
514    }
515}