Coverage Report

Created: 2025-12-31 06:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/neqo/neqo-transport/src/ecn.rs
Line
Count
Source
1
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4
// option. This file may not be copied, modified, or distributed
5
// except according to those terms.
6
7
use std::ops::{AddAssign, Deref, DerefMut, Sub};
8
9
use enum_map::{Enum, EnumMap};
10
use neqo_common::{qdebug, qinfo, Ecn};
11
12
use crate::{packet, recovery::sent, Stats};
13
14
/// The number of packets to use for testing a path for ECN capability.
15
pub(crate) const TEST_COUNT: usize = 10;
16
17
/// The number of packets to use for testing a path for ECN capability when exchanging
18
/// Initials during the handshake. This is a lower number than [`TEST_COUNT`] to avoid
19
/// unnecessarily delaying the handshake; we would otherwise double the PTO [`TEST_COUNT`]
20
/// times.
21
const TEST_COUNT_INITIAL_PHASE: usize = 3;
22
23
/// The state information related to testing a path for ECN capability.
24
/// See RFC9000, Appendix A.4.
25
#[derive(Debug, PartialEq, Clone, Copy, Default)]
26
enum ValidationState {
27
    /// ECN validation not started yet. Reason might e.g. be still handshaking
28
    /// or not being the primary path.
29
    #[default]
30
    NotStarted,
31
    /// The path is currently being tested for ECN capability, with the number of probes sent so
32
    /// far on the path during the ECN validation.
33
    Testing {
34
        probes_sent: usize,
35
        initial_probes_acked: usize,
36
        initial_probes_lost: usize,
37
    },
38
    /// The validation test has concluded but the path's ECN capability is not yet known.
39
    Unknown,
40
    /// The path is known to **not** be ECN capable.
41
    Failed(ValidationError),
42
    /// The path is known to be ECN capable.
43
    Capable,
44
}
45
46
impl ValidationState {
47
0
    fn set(&mut self, new: Self, stats: &mut Stats) {
48
0
        let old = std::mem::replace(self, new);
49
50
0
        match old {
51
0
            Self::NotStarted | Self::Testing { .. } | Self::Unknown => {}
52
0
            Self::Failed(_) => debug_assert!(false, "Failed is a terminal state"),
53
0
            Self::Capable => stats.ecn_path_validation[ValidationOutcome::Capable] -= 1,
54
        }
55
0
        match new {
56
0
            Self::NotStarted | Self::Testing { .. } | Self::Unknown => {}
57
0
            Self::Failed(error) => {
58
0
                stats.ecn_path_validation[ValidationOutcome::NotCapable(error)] += 1;
59
0
            }
60
0
            Self::Capable => stats.ecn_path_validation[ValidationOutcome::Capable] += 1,
61
        }
62
0
    }
63
}
64
65
/// The counts for different ECN marks.
66
///
67
/// Note: [`Count`] is used both for outgoing UDP datagrams, returned by
68
/// remote through QUIC ACKs and for incoming UDP datagrams, read from IP TOS
69
/// header. In the former case, given that QUIC ACKs only carry
70
/// [`Ecn::Ect0`], [`Ecn::Ect1`] and [`Ecn::Ce`], but never
71
/// [`Ecn::NotEct`], the [`Ecn::NotEct`] value will always be 0.
72
///
73
/// See also <https://www.rfc-editor.org/rfc/rfc9000.html#section-19.3.2>.
74
#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)]
75
pub struct Count(EnumMap<Ecn, u64>);
76
77
impl Deref for Count {
78
    type Target = EnumMap<Ecn, u64>;
79
80
2.75k
    fn deref(&self) -> &Self::Target {
81
2.75k
        &self.0
82
2.75k
    }
83
}
84
85
impl DerefMut for Count {
86
3.14k
    fn deref_mut(&mut self) -> &mut Self::Target {
87
3.14k
        &mut self.0
88
3.14k
    }
89
}
90
91
impl Count {
92
    #[must_use]
93
8
    pub const fn new(not_ect: u64, ect0: u64, ect1: u64, ce: u64) -> Self {
94
        // Yes, the enum array order is different from the argument order.
95
8
        Self(EnumMap::from_array([not_ect, ect1, ect0, ce]))
96
8
    }
97
98
    /// Whether any of the ECT(0), ECT(1) or CE counts are non-zero.
99
    #[must_use]
100
918
    pub fn is_some(&self) -> bool {
101
918
        self[Ecn::Ect0] > 0 || self[Ecn::Ect1] > 0 || self[Ecn::Ce] > 0
102
918
    }
103
104
    /// Whether all of the ECN counts are zero (including Not-ECT.)
105
    #[must_use]
106
0
    pub fn is_empty(&self) -> bool {
107
0
        self.iter().all(|(_, count)| *count == 0)
108
0
    }
109
}
110
111
impl Sub<Self> for Count {
112
    type Output = Self;
113
114
    /// Subtract the ECN counts in `other` from `self`.
115
0
    fn sub(self, rhs: Self) -> Self {
116
0
        let mut diff = Self::default();
117
0
        for (ecn, count) in &mut *diff {
118
0
            *count = self[ecn].saturating_sub(rhs[ecn]);
119
0
        }
120
0
        diff
121
0
    }
122
}
123
124
impl AddAssign<Ecn> for Count {
125
3.14k
    fn add_assign(&mut self, rhs: Ecn) {
126
3.14k
        self[rhs] += 1;
127
3.14k
    }
128
}
129
130
#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)]
131
pub struct ValidationCount(EnumMap<ValidationOutcome, u64>);
132
133
impl Deref for ValidationCount {
134
    type Target = EnumMap<ValidationOutcome, u64>;
135
136
0
    fn deref(&self) -> &Self::Target {
137
0
        &self.0
138
0
    }
139
}
140
141
impl DerefMut for ValidationCount {
142
0
    fn deref_mut(&mut self) -> &mut Self::Target {
143
0
        &mut self.0
144
0
    }
145
}
146
147
#[derive(Debug, Clone, Copy, Enum, PartialEq, Eq)]
148
pub enum ValidationError {
149
    BlackHole,
150
    Bleaching,
151
    ReceivedUnsentECT1,
152
}
153
154
#[derive(Debug, Clone, Copy, Enum, PartialEq, Eq)]
155
pub enum ValidationOutcome {
156
    Capable,
157
    NotCapable(ValidationError),
158
}
159
160
#[derive(Debug, Default)]
161
pub(crate) struct Info {
162
    /// The current state of ECN validation on this path.
163
    state: ValidationState,
164
165
    /// The largest ACK seen so far.
166
    largest_acked: packet::Number,
167
168
    /// The ECN counts from the last ACK frame that increased `largest_acked`.
169
    baseline: Count,
170
}
171
172
impl Info {
173
0
    pub(crate) fn start(&mut self, stats: &mut Stats) {
174
0
        if !matches!(self.state, ValidationState::NotStarted) {
175
0
            return;
176
0
        }
177
178
0
        self.state.set(
179
0
            ValidationState::Testing {
180
0
                probes_sent: 0,
181
0
                initial_probes_acked: 0,
182
0
                initial_probes_lost: 0,
183
0
            },
184
0
            stats,
185
        );
186
0
    }
187
188
    /// Set the baseline (= the ECN counts from the last ACK Frame).
189
0
    pub(crate) fn set_baseline(&mut self, baseline: Count) {
190
0
        self.baseline = baseline;
191
0
    }
192
193
    /// Expose the current baseline.
194
0
    pub(crate) const fn baseline(&self) -> Count {
195
0
        self.baseline
196
0
    }
197
198
    /// Count the number of packets sent out on this path during ECN validation.
199
    /// Exit ECN validation if the number of packets sent exceeds `TEST_COUNT`.
200
    /// We do not implement the part of the RFC that says to exit ECN validation if the time since
201
    /// the start of ECN validation exceeds 3 * PTO, since this seems to happen much too quickly.
202
1.76k
    pub(crate) fn on_packet_sent(&mut self, num_datagrams: usize, stats: &mut Stats) {
203
1.76k
        if let ValidationState::Testing { probes_sent, .. } = &mut self.state {
204
0
            *probes_sent += num_datagrams;
205
0
            qdebug!("ECN probing: sent {probes_sent} probes");
206
0
            if *probes_sent >= TEST_COUNT {
207
0
                qdebug!("ECN probing concluded with {probes_sent} probes sent");
208
0
                self.state.set(ValidationState::Unknown, stats);
209
0
            }
210
1.76k
        }
211
1.76k
    }
212
213
    /// Disable ECN.
214
0
    pub(crate) fn disable_ecn(&mut self, stats: &mut Stats, reason: ValidationError) {
215
0
        self.state.set(ValidationState::Failed(reason), stats);
216
0
    }
217
218
    /// Process ECN counts from an ACK frame.
219
    ///
220
    /// Returns whether ECN counts contain new valid ECN CE marks.
221
0
    pub(crate) fn on_packets_acked(
222
0
        &mut self,
223
0
        acked_packets: &[sent::Packet],
224
0
        ack_ecn: Option<&Count>,
225
0
        stats: &mut Stats,
226
0
    ) -> bool {
227
0
        let prev_baseline = self.baseline;
228
229
0
        self.validate_ack_ecn_and_update(acked_packets, ack_ecn, stats);
230
231
0
        matches!(self.state, ValidationState::Capable)
232
0
            && (self.baseline - prev_baseline)[Ecn::Ce] > 0
233
0
    }
234
235
    /// An [`Ecn::Ect0`] marked packet has been acked.
236
0
    pub(crate) fn acked_ecn(&mut self) {
237
        if let ValidationState::Testing {
238
0
            initial_probes_acked: probes_acked,
239
            ..
240
0
        } = &mut self.state
241
0
        {
242
0
            *probes_acked += 1;
243
0
        }
244
0
    }
245
246
    /// An [`Ecn::Ect0`] marked packet has been declared lost.
247
0
    pub(crate) fn lost_ecn(&mut self, stats: &mut Stats) {
248
        if let ValidationState::Testing {
249
0
            initial_probes_acked: probes_acked,
250
0
            initial_probes_lost: probes_lost,
251
            ..
252
0
        } = &mut self.state
253
        {
254
0
            *probes_lost += 1;
255
            // If we have lost all initial probes a bunch of times, we can conclude that the path
256
            // is not ECN capable and likely drops all ECN marked packets.
257
0
            if *probes_acked == 0 && *probes_lost == TEST_COUNT_INITIAL_PHASE {
258
0
                qdebug!(
259
                    "ECN validation failed, all {probes_lost} initial marked packets were lost"
260
                );
261
0
                self.disable_ecn(stats, ValidationError::BlackHole);
262
0
            }
263
0
        }
264
0
    }
265
266
    /// After the ECN validation test has ended, check if the path is ECN capable.
267
0
    fn validate_ack_ecn_and_update(
268
0
        &mut self,
269
0
        acked_packets: &[sent::Packet],
270
0
        ack_ecn: Option<&Count>,
271
0
        stats: &mut Stats,
272
0
    ) {
273
        // RFC 9000, Section 13.4.2.1:
274
        //
275
        // > Validating ECN counts from reordered ACK frames can result in failure. An endpoint MUST
276
        // > NOT fail ECN validation as a result of processing an ACK frame that does not increase
277
        // > the largest acknowledged packet number.
278
0
        let largest_acked = acked_packets.first().expect("must be there");
279
0
        if largest_acked.pn() <= self.largest_acked {
280
0
            return;
281
0
        }
282
283
        // RFC 9000, Appendix A.4:
284
        //
285
        // > From the "unknown" state, successful validation of the ECN counts in an ACK frame
286
        // > (see Section 13.4.2.1) causes the ECN state for the path to become "capable", unless
287
        // > no marked packet has been acknowledged.
288
0
        match self.state {
289
            ValidationState::NotStarted
290
            | ValidationState::Testing { .. }
291
0
            | ValidationState::Failed(_) => return,
292
0
            ValidationState::Unknown | ValidationState::Capable => {}
293
        }
294
295
        // RFC 9000, Section 13.4.2.1:
296
        //
297
        // > An endpoint that receives an ACK frame with ECN counts therefore validates
298
        // > the counts before using them. It performs this validation by comparing newly
299
        // > received counts against those from the last successfully processed ACK frame.
300
        //
301
        // > If an ACK frame newly acknowledges a packet that the endpoint sent with
302
        // > either the ECT(0) or ECT(1) codepoint set, ECN validation fails if the
303
        // > corresponding ECN counts are not present in the ACK frame.
304
0
        let Some(ack_ecn) = ack_ecn else {
305
0
            qinfo!("ECN validation failed, no ECN counts in ACK frame");
306
0
            self.disable_ecn(stats, ValidationError::Bleaching);
307
0
            return;
308
        };
309
0
        let ack_ecn = *ack_ecn;
310
0
        stats.ecn_tx_acked[largest_acked.packet_type()] = ack_ecn;
311
312
        // > ECN validation also fails if the sum of the increase in ECT(0) and ECN-CE counts is
313
        // > less than the number of newly acknowledged packets that were originally sent with an
314
        // > ECT(0) marking.
315
0
        let newly_acked_sent_with_ect0: u64 = acked_packets
316
0
            .iter()
317
0
            .filter(|p| p.ecn_marked_ect0())
318
0
            .count()
319
0
            .try_into()
320
0
            .expect("usize fits into u64");
321
0
        let ecn_diff = ack_ecn - self.baseline;
322
0
        let sum_inc = ecn_diff[Ecn::Ect0] + ecn_diff[Ecn::Ce];
323
0
        if sum_inc < newly_acked_sent_with_ect0 {
324
0
            qinfo!(
325
                "ECN validation failed, ACK counted {sum_inc} new marks, but {newly_acked_sent_with_ect0} of newly acked packets were sent with ECT(0)"
326
            );
327
0
            self.disable_ecn(stats, ValidationError::Bleaching);
328
0
        } else if ecn_diff[Ecn::Ect1] > 0 {
329
0
            qinfo!("ECN validation failed, ACK counted ECT(1) marks that were never sent");
330
0
            self.disable_ecn(stats, ValidationError::ReceivedUnsentECT1);
331
0
        } else if self.state != ValidationState::Capable {
332
0
            qinfo!("ECN validation succeeded, path is capable");
333
0
            self.state.set(ValidationState::Capable, stats);
334
0
        }
335
0
        self.baseline = ack_ecn;
336
0
        self.largest_acked = largest_acked.pn();
337
0
    }
338
339
1.76k
    pub(crate) const fn is_marking(&self) -> bool {
340
1.76k
        match self.state {
341
0
            ValidationState::Testing { .. } | ValidationState::Capable => true,
342
            ValidationState::NotStarted | ValidationState::Failed(_) | ValidationState::Unknown => {
343
1.76k
                false
344
            }
345
        }
346
1.76k
    }
347
348
    /// The ECN mark to use for an outgoing UDP datagram.
349
1.76k
    pub(crate) const fn ecn_mark(&self) -> Ecn {
350
1.76k
        if self.is_marking() {
351
0
            Ecn::Ect0
352
        } else {
353
1.76k
            Ecn::NotEct
354
        }
355
1.76k
    }
356
}
357
358
#[cfg(test)]
359
#[cfg_attr(coverage_nightly, coverage(off))]
360
mod tests {
361
    use super::Count;
362
363
    #[test]
364
    fn count_predicates() {
365
        assert!(Count::default().is_empty());
366
        assert!(!Count::default().is_some());
367
        assert!(!Count::new(0, 1, 0, 0).is_empty());
368
        assert!(Count::new(0, 1, 0, 0).is_some()); // ect1 > 0
369
        assert!(Count::new(0, 0, 1, 0).is_some()); // ect0 > 0
370
        assert!(Count::new(0, 0, 0, 1).is_some()); // ce > 0
371
        assert!(!Count::new(1, 0, 0, 0).is_empty());
372
        assert!(!Count::new(1, 0, 0, 0).is_some()); // not_ect alone
373
    }
374
}