s2n_quic_core/path/
ecn.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{
5    counter::{Counter, Saturating},
6    event,
7    event::IntoEvent,
8    frame::ack::EcnCounts,
9    inet::ExplicitCongestionNotification,
10    number::CheckedSub,
11    random,
12    time::{timer, Duration, Timer, Timestamp},
13    transmission,
14    varint::VarInt,
15};
16use core::ops::RangeInclusive;
17
18#[cfg(test)]
19mod tests;
20
21//= https://www.rfc-editor.org/rfc/rfc9000#section-13.4.2
22//# If an endpoint has cause to expect that IP packets with an ECT codepoint
23//# might be dropped by a faulty network element, the endpoint could set an
24//# ECT codepoint for only the first ten outgoing packets on a path, or for
25//# a period of three PTOs
26const TESTING_PACKET_THRESHOLD: u8 = 10;
27
28// After a failure has been detected, the ecn::Controller will wait this duration
29// before testing for ECN support again.
30const RETEST_COOL_OFF_DURATION: Duration = Duration::from_secs(60);
31
32// The number of round trip times an ECN capable path will wait before transmitting an ECN-CE marked packet.
33const CE_SUPPRESSION_TESTING_RTT_MULTIPLIER: RangeInclusive<u16> = 10..=100;
34
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub enum ValidationOutcome {
37    /// The path is ECN capable and congestion was experienced
38    ///
39    /// Contains the incremental count of packets that experienced congestion
40    CongestionExperienced(VarInt),
41    /// The path failed validation
42    Failed,
43    /// The path passed validation
44    Passed,
45    /// Validation was not performed
46    Skipped,
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
50enum State {
51    // ECN capability is being tested, tracking the number of ECN marked packets sent
52    Testing(u8),
53    // ECN capability has been tested, but not validated yet
54    Unknown,
55    // ECN validation has failed. Validation will be restarted based on the timer
56    Failed(Timer),
57    // ECN validation has succeeded. CE suppression will be tested based on the timer.
58    Capable(Timer),
59}
60
61impl IntoEvent<event::builder::EcnState> for &State {
62    #[inline]
63    fn into_event(self) -> event::builder::EcnState {
64        match self {
65            State::Testing(_) => event::builder::EcnState::Testing,
66            State::Unknown => event::builder::EcnState::Unknown,
67            State::Failed(_) => event::builder::EcnState::Failed,
68            State::Capable(_) => event::builder::EcnState::Capable,
69        }
70    }
71}
72
73impl Default for State {
74    #[inline]
75    fn default() -> Self {
76        State::Testing(0)
77    }
78}
79
80#[derive(Clone, Debug, Default)]
81pub struct Controller {
82    state: State,
83    // A count of the number of packets with ECN marking lost since
84    // the last time a packet with ECN marking was acknowledged.
85    black_hole_counter: Counter<u8, Saturating>,
86    // The largest acknowledged packet sent with an ECN marking. Used when tracking
87    // packets that have been lost for the purpose of detecting a black hole.
88    last_acked_ecn_packet_timestamp: Option<Timestamp>,
89}
90
91impl Controller {
92    /// Restart testing of ECN capability
93    #[inline]
94    pub fn restart<Pub: event::ConnectionPublisher>(
95        &mut self,
96        path: event::builder::Path,
97        publisher: &mut Pub,
98    ) {
99        if self.state != State::Testing(0) {
100            self.change_state(State::Testing(0), path, publisher);
101        }
102        self.black_hole_counter = Default::default();
103    }
104
105    /// Called when the connection timer expires
106    #[inline]
107    pub fn on_timeout<Pub: event::ConnectionPublisher>(
108        &mut self,
109        now: Timestamp,
110        path: event::builder::Path,
111        random_generator: &mut dyn random::Generator,
112        rtt: Duration,
113        publisher: &mut Pub,
114    ) {
115        match self.state {
116            State::Failed(ref mut retest_timer) => {
117                if retest_timer.poll_expiration(now).is_ready() {
118                    self.restart(path, publisher);
119                }
120            }
121            State::Capable(ref mut ce_suppression_timer) if !ce_suppression_timer.is_armed() => {
122                ce_suppression_timer
123                    .set(now + Self::next_ce_packet_duration(random_generator, rtt));
124            }
125            State::Testing(_) | State::Unknown | State::Capable(_) => {}
126        }
127    }
128
129    /// Gets the ECN marking to use on packets sent to the peer
130    #[inline]
131    pub fn ecn(
132        &mut self,
133        transmission_mode: transmission::Mode,
134        now: Timestamp,
135    ) -> ExplicitCongestionNotification {
136        if transmission_mode.is_loss_recovery_probing() {
137            // Don't mark loss recovery probes as ECN capable in case the ECN
138            // marking is causing packet loss
139            return ExplicitCongestionNotification::NotEct;
140        }
141
142        match self.state {
143            //= https://www.rfc-editor.org/rfc/rfc9000#appendix-A.4
144            //# On paths with a "testing" or "capable" state, the endpoint
145            //# sends packets with an ECT marking -- ECT(0) by default;
146            //# otherwise, the endpoint sends unmarked packets.
147            State::Testing(_) => ExplicitCongestionNotification::Ect0,
148            State::Capable(ref mut ce_suppression_timer) => {
149                if ce_suppression_timer.poll_expiration(now).is_ready() {
150                    //= https://www.rfc-editor.org/rfc/rfc9002#section-8.3
151                    //# A sender can detect suppression of reports by marking occasional
152                    //# packets that it sends with an ECN-CE marking.
153                    ExplicitCongestionNotification::Ce
154                } else {
155                    //= https://www.rfc-editor.org/rfc/rfc9000#section-13.4.2.2
156                    //# Upon successful validation, an endpoint MAY continue to set an ECT
157                    //# codepoint in subsequent packets it sends, with the expectation that
158                    //# the path is ECN-capable.
159                    ExplicitCongestionNotification::Ect0
160                }
161            }
162            //= https://www.rfc-editor.org/rfc/rfc9000#section-13.4.2.2
163            //# If validation fails, then the endpoint MUST disable ECN. It stops setting the ECT
164            //# codepoint in IP packets that it sends, assuming that either the network path or
165            //# the peer does not support ECN.
166            State::Failed(_) | State::Unknown => ExplicitCongestionNotification::NotEct,
167        }
168    }
169
170    /// Returns a duration based on a randomly generated value in the CE_SUPPRESSION_TESTING_RTT_MULTIPLIER
171    /// range multiplied by the given round trip time. This duration represents the amount of time
172    /// to wait before an ECN-CE marked packet should be sent, to test if CE reports are being
173    /// suppressed by the peer.
174    ///
175    /// Note: This function performs a modulo operation on the random generated bytes to restrict
176    /// the result to the `CE_SUPPRESSION_TESTING_RTT_MULTIPLIER` range. This may introduce a modulo
177    /// bias in the resulting count, but does not result in any reduction in security for this
178    /// usage. Other usages that require uniform sampling should implement rejection sampling or
179    /// other methodologies and not copy this implementation.
180    #[inline]
181    fn next_ce_packet_duration(
182        random_generator: &mut dyn random::Generator,
183        rtt: Duration,
184    ) -> Duration {
185        let mut bytes = [0; core::mem::size_of::<u16>()];
186        random_generator.public_random_fill(&mut bytes);
187        let result = u16::from_le_bytes(bytes);
188
189        let max_variance = (CE_SUPPRESSION_TESTING_RTT_MULTIPLIER.end()
190            - CE_SUPPRESSION_TESTING_RTT_MULTIPLIER.start())
191        .saturating_add(1);
192        let result = CE_SUPPRESSION_TESTING_RTT_MULTIPLIER.start() + result % max_variance;
193        result as u32 * rtt
194    }
195
196    /// Returns true if the path has been determined to be capable of handling ECN marked packets
197    #[inline]
198    pub fn is_capable(&self) -> bool {
199        matches!(self.state, State::Capable(_))
200    }
201
202    //= https://www.rfc-editor.org/rfc/rfc9000#section-13.4.2.2
203    //# Network routing and path elements can change mid-connection; an endpoint
204    //# MUST disable ECN if validation later fails.
205    /// Validate the given `EcnCounts`, updating the current validation state based on the
206    /// validation outcome.
207    ///
208    /// * `newly_acked_ecn_counts` - total ECN counts that were sent on packets newly acknowledged by the peer
209    /// * `sent_packet_ecn_counts` - total ECN counts for all outstanding packets, including those newly
210    ///   acknowledged during this validation
211    /// * `baseline_ecn_counts` - the ECN counts present in the Ack frame the last time ECN counts were processed
212    /// * `ack_frame_ecn_counts` - the ECN counts present in the current Ack frame (if any)
213    /// * `now` - the time the Ack frame was received
214    #[inline]
215    pub fn validate<Pub: event::ConnectionPublisher>(
216        &mut self,
217        newly_acked_ecn_counts: EcnCounts,
218        sent_packet_ecn_counts: EcnCounts,
219        baseline_ecn_counts: EcnCounts,
220        ack_frame_ecn_counts: Option<EcnCounts>,
221        now: Timestamp,
222        rtt: Duration,
223        path: event::builder::Path,
224        publisher: &mut Pub,
225    ) -> ValidationOutcome {
226        if matches!(self.state, State::Failed(_)) {
227            // Validation had already failed
228            return ValidationOutcome::Skipped;
229        }
230
231        if ack_frame_ecn_counts.is_none() {
232            if newly_acked_ecn_counts.as_option().is_some() {
233                //= https://www.rfc-editor.org/rfc/rfc9000#section-13.4.2.1
234                //# If an ACK frame newly acknowledges a packet that the endpoint sent with
235                //# either the ECT(0) or ECT(1) codepoint set, ECN validation fails if the
236                //# corresponding ECN counts are not present in the ACK frame. This check
237                //# detects a network element that zeroes the ECN field or a peer that does
238                //# not report ECN markings.
239                self.fail(now, path, publisher);
240                return ValidationOutcome::Failed;
241            }
242
243            if baseline_ecn_counts == EcnCounts::default() {
244                // Nothing to validate
245                return ValidationOutcome::Skipped;
246            }
247        }
248
249        let congestion_experienced_count = if let Some(incremental_ecn_counts) =
250            ack_frame_ecn_counts
251                .unwrap_or_default()
252                .checked_sub(baseline_ecn_counts)
253        {
254            if Self::ce_remarking(incremental_ecn_counts, newly_acked_ecn_counts)
255                || Self::remarked_to_ect0_or_ect1(incremental_ecn_counts, sent_packet_ecn_counts)
256                || Self::ce_suppression(incremental_ecn_counts, newly_acked_ecn_counts)
257            {
258                self.fail(now, path, publisher);
259                return ValidationOutcome::Failed;
260            }
261
262            // ce_suppression check above ensures this doesn't underflow
263            incremental_ecn_counts.ce_count - newly_acked_ecn_counts.ce_count
264        } else {
265            // ECN counts decreased from the baseline
266            self.fail(now, path, publisher);
267            return ValidationOutcome::Failed;
268        };
269
270        //= https://www.rfc-editor.org/rfc/rfc9000#appendix-A.4
271        //# From the "unknown" state, successful validation of the ECN counts in an ACK frame
272        //# (see Section 13.4.2.1) causes the ECN state for the path to become "capable",
273        //# unless no marked packet has been acknowledged.
274        if matches!(self.state, State::Unknown)
275            && newly_acked_ecn_counts.ect_0_count > VarInt::from_u8(0)
276        {
277            // Arm the ce suppression timer to send a ECN-CE marked packet to test for
278            // CE suppression by the peer.
279            let mut ce_suppression_timer = Timer::default();
280            ce_suppression_timer
281                .set(now + *CE_SUPPRESSION_TESTING_RTT_MULTIPLIER.start() as u32 * rtt);
282            self.change_state(State::Capable(ce_suppression_timer), path, publisher);
283        }
284
285        if self.is_capable() && congestion_experienced_count > VarInt::ZERO {
286            return ValidationOutcome::CongestionExperienced(congestion_experienced_count);
287        }
288
289        ValidationOutcome::Passed
290    }
291
292    //= https://www.rfc-editor.org/rfc/rfc9000#section-13.4.2.1
293    //# ECN validation also fails if the sum of the increase in ECT(0)
294    //# and ECN-CE counts is less than the number of newly acknowledged
295    //# packets that were originally sent with an ECT(0) marking.
296    #[inline]
297    fn ce_remarking(incremental_ecn_counts: EcnCounts, newly_acked_ecn_counts: EcnCounts) -> bool {
298        let ect_0_increase = incremental_ecn_counts
299            .ect_0_count
300            .saturating_add(incremental_ecn_counts.ce_count);
301        ect_0_increase < newly_acked_ecn_counts.ect_0_count
302    }
303
304    //= https://www.rfc-editor.org/rfc/rfc9000#section-13.4.2.1
305    //# ECN validation can fail if the received total count for either ECT(0) or ECT(1)
306    //# exceeds the total number of packets sent with each corresponding ECT codepoint.
307    #[inline]
308    fn remarked_to_ect0_or_ect1(
309        incremental_ecn_counts: EcnCounts,
310        sent_packet_ecn_counts: EcnCounts,
311    ) -> bool {
312        incremental_ecn_counts.ect_0_count > sent_packet_ecn_counts.ect_0_count
313            || incremental_ecn_counts.ect_1_count > sent_packet_ecn_counts.ect_1_count
314    }
315
316    //= https://www.rfc-editor.org/rfc/rfc9002#section-8.3
317    //# A receiver can misreport ECN markings to alter the congestion
318    //# response of a sender.  Suppressing reports of ECN-CE markings could
319    //# cause a sender to increase their send rate.  This increase could
320    //# result in congestion and loss.
321
322    //= https://www.rfc-editor.org/rfc/rfc9002#section-8.3
323    //# A sender can detect suppression of reports by marking occasional
324    //# packets that it sends with an ECN-CE marking.  If a packet sent with
325    //# an ECN-CE marking is not reported as having been CE marked when the
326    //# packet is acknowledged, then the sender can disable ECN for that path
327    //# by not setting ECN-Capable Transport (ECT) codepoints in subsequent
328    //# packets sent on that path [RFC3168].
329    #[inline]
330    fn ce_suppression(
331        incremental_ecn_counts: EcnCounts,
332        newly_acked_ecn_counts: EcnCounts,
333    ) -> bool {
334        incremental_ecn_counts.ce_count < newly_acked_ecn_counts.ce_count
335    }
336
337    /// This method gets called when a packet has been sent
338    #[inline]
339    pub fn on_packet_sent<Pub: event::ConnectionPublisher>(
340        &mut self,
341        ecn: ExplicitCongestionNotification,
342        path: event::builder::Path,
343        publisher: &mut Pub,
344    ) {
345        debug_assert!(
346            !matches!(ecn, ExplicitCongestionNotification::Ect1),
347            "Ect1 is not used"
348        );
349
350        if let (true, State::Testing(ref mut packet_count)) = (ecn.using_ecn(), &mut self.state) {
351            *packet_count += 1;
352
353            if *packet_count >= TESTING_PACKET_THRESHOLD {
354                self.change_state(State::Unknown, path, publisher);
355            }
356        }
357    }
358
359    /// This method gets called when a packet delivery got acknowledged
360    #[inline]
361    pub fn on_packet_ack(&mut self, time_sent: Timestamp, ecn: ExplicitCongestionNotification) {
362        if self.ecn_packet_sent_after_last_acked_ecn_packet(time_sent, ecn) {
363            // Reset the black hole counter since a packet with ECN marking
364            // has been acknowledged, indicating the path may still be ECN-capable
365            self.black_hole_counter = Default::default();
366            self.last_acked_ecn_packet_timestamp = Some(time_sent);
367        }
368    }
369
370    /// This method gets called when a packet loss is reported
371    #[inline]
372    pub fn on_packet_loss<Pub: event::ConnectionPublisher>(
373        &mut self,
374        time_sent: Timestamp,
375        ecn: ExplicitCongestionNotification,
376        now: Timestamp,
377        path: event::builder::Path,
378        publisher: &mut Pub,
379    ) {
380        if matches!(self.state, State::Failed(_)) {
381            return;
382        }
383
384        if self.ecn_packet_sent_after_last_acked_ecn_packet(time_sent, ecn) {
385            // An ECN marked packet that was sent after the last
386            // acknowledged ECN marked packet has been lost
387            self.black_hole_counter += 1;
388        }
389
390        if self.black_hole_counter > TESTING_PACKET_THRESHOLD {
391            self.fail(now, path, publisher);
392        }
393    }
394
395    /// Returns true if a packet sent at the given `time_sent` with the given ECN marking
396    /// was marked as using ECN and was sent after the last time an ECN marked packet had
397    /// been acknowledged.
398    #[inline]
399    fn ecn_packet_sent_after_last_acked_ecn_packet(
400        &mut self,
401        time_sent: Timestamp,
402        ecn: ExplicitCongestionNotification,
403    ) -> bool {
404        ecn.using_ecn()
405            && self
406                .last_acked_ecn_packet_timestamp
407                .is_none_or(|last_acked| last_acked < time_sent)
408    }
409
410    /// Set the state to Failed and arm the retest timer
411    #[inline]
412    fn fail<Pub: event::ConnectionPublisher>(
413        &mut self,
414        now: Timestamp,
415        path: event::builder::Path,
416        publisher: &mut Pub,
417    ) {
418        //= https://www.rfc-editor.org/rfc/rfc9000#section-13.4.2.2
419        //# Even if validation fails, an endpoint MAY revalidate ECN for the same path at any later
420        //# time in the connection. An endpoint could continue to periodically attempt validation.
421        let mut retest_timer = Timer::default();
422        retest_timer.set(now + RETEST_COOL_OFF_DURATION);
423        self.change_state(State::Failed(retest_timer), path, publisher);
424        self.black_hole_counter = Default::default();
425    }
426
427    #[inline]
428    fn change_state<Pub: event::ConnectionPublisher>(
429        &mut self,
430        state: State,
431        path: event::builder::Path,
432        publisher: &mut Pub,
433    ) {
434        debug_assert_ne!(self.state, state);
435
436        self.state = state;
437
438        publisher.on_ecn_state_changed(event::builder::EcnStateChanged {
439            path,
440            state: self.state.into_event(),
441        })
442    }
443}
444
445impl timer::Provider for Controller {
446    #[inline]
447    fn timers<Q: timer::Query>(&self, query: &mut Q) -> timer::Result {
448        if let State::Failed(timer) = &self.state {
449            timer.timers(query)?
450        }
451        // The ce suppression timer in State::Capable is not queried here as that
452        // timer is passively polled when transmitting and does not require firing
453        // precisely.
454
455        Ok(())
456    }
457}