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}