Skip to content

Commit fe70e1a

Browse files
committed
tokio-quiche: support per-connection DSCP marking
Add optional initial DSCP marking for server connections and a handle to update it as the connection progresses. Apply the marking to outgoing IPv4 and IPv6 packets, including GSO sends and stateless replies. Cover per-connection isolation, IPv4-mapped addresses, and IPv6 in tests.
1 parent 96e7dd5 commit fe70e1a

10 files changed

Lines changed: 486 additions & 19 deletions

File tree

‎tokio-quiche/src/quic/connection/mod.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ use super::io::worker::IoWorkerParams;
6767
use super::io::worker::Running;
6868
use super::io::worker::RunningOrClosing;
6969
use super::io::worker::WriteState;
70+
use super::DscpHandle;
7071
use super::QuicheConnection;
7172
use crate::metrics::Metrics;
7273
use crate::quic::io::worker::IoWorker;
@@ -281,6 +282,15 @@ where
281282
(*self.params.quiche_conn).as_mut()
282283
}
283284

285+
/// Returns the outgoing DSCP handle when server marking is configured.
286+
///
287+
/// Clone it before starting the handshake to update the worker's marking
288+
/// from a certificate-selection callback. It is initialized with the
289+
/// listener's pre-handshake value.
290+
pub fn dscp_handle(&self) -> Option<&DscpHandle> {
291+
self.params.dscp_handle.as_ref()
292+
}
293+
284294
/// A handle to the [`QuicAuditStats`] for this connection.
285295
///
286296
/// # Note
@@ -337,6 +347,7 @@ where
337347
cfg: self.params.writer_cfg,
338348
audit_log_stats: self.audit_log_stats,
339349
write_state: WriteState::default(),
350+
dscp_handle: self.params.dscp_handle,
340351
conn_map_cmd_tx: self.params.conn_map_cmd_tx,
341352
cid_generator: self.params.cid_generator,
342353
#[cfg(feature = "perf-quic-listener-metrics")]
@@ -468,6 +479,7 @@ where
468479
{
469480
pub writer_cfg: WriterConfig,
470481
pub initial_pkt: Option<Incoming>,
482+
pub dscp_handle: Option<DscpHandle>,
471483
pub shutdown_tx: mpsc::Sender<()>,
472484
pub conn_map_cmd_tx: mpsc::UnboundedSender<ConnectionMapCommand>, /* channel that signals connection map changes */
473485
pub scid: ConnectionId<'static>,

‎tokio-quiche/src/quic/dscp.rs‎

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright (C) 2025, Cloudflare, Inc.
2+
// All rights reserved.
3+
//
4+
// Redistribution and use in source and binary forms, with or without
5+
// modification, are permitted provided that the following conditions are met:
6+
//
7+
// * Redistributions of source code must retain the above copyright notice,
8+
// this list of conditions and the following disclaimer.
9+
// * Redistributions in binary form must reproduce the above copyright
10+
// notice, this list of conditions and the following disclaimer in the
11+
// documentation and/or other materials provided with the distribution.
12+
//
13+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14+
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15+
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16+
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
17+
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18+
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19+
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20+
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21+
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22+
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
23+
// POSSIBILITY OF SUCH DAMAGE.
24+
25+
use std::sync::atomic::AtomicU8;
26+
use std::sync::atomic::Ordering;
27+
use std::sync::Arc;
28+
29+
const UNMARKED: u8 = 64;
30+
31+
/// A six-bit Differentiated Services Code Point for outgoing UDP packets.
32+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33+
pub struct Dscp(u8);
34+
35+
impl Dscp {
36+
/// Creates a DSCP value, rejecting values outside the six-bit range.
37+
pub const fn new(value: u8) -> Option<Self> {
38+
if value < UNMARKED {
39+
Some(Self(value))
40+
} else {
41+
None
42+
}
43+
}
44+
45+
/// Returns the six-bit DSCP value.
46+
pub const fn value(self) -> u8 {
47+
self.0
48+
}
49+
50+
#[cfg(any(all(target_os = "linux", not(feature = "fuzzing")), test))]
51+
pub(crate) const fn tos(self) -> u8 {
52+
self.0 << 2
53+
}
54+
}
55+
56+
/// Per-connection DSCP shared with the QUIC I/O worker.
57+
///
58+
/// Clone this handle before starting the handshake and call [`Self::set`] once
59+
/// the connection's final marking is known. `Dscp::new(0)` explicitly
60+
/// clears an earlier marking even when the shared socket has a nonzero default;
61+
/// `None` omits the per-packet control message.
62+
#[derive(Clone, Debug)]
63+
pub struct DscpHandle(Arc<AtomicU8>);
64+
65+
impl DscpHandle {
66+
pub(crate) fn new(initial: Dscp) -> Self {
67+
Self(Arc::new(AtomicU8::new(initial.value())))
68+
}
69+
70+
/// Changes the DSCP used for subsequent packets on this connection.
71+
pub fn set(&self, dscp: Option<Dscp>) {
72+
// This atomic only holds the marking byte; it synchronizes no other data.
73+
self.0
74+
.store(dscp.map_or(UNMARKED, Dscp::value), Ordering::Relaxed);
75+
}
76+
77+
/// Returns the currently selected marking, or `None` if disabled.
78+
pub fn get(&self) -> Option<Dscp> {
79+
Dscp::new(self.0.load(Ordering::Relaxed))
80+
}
81+
}
82+
83+
#[cfg(test)]
84+
mod tests {
85+
use super::*;
86+
87+
#[test]
88+
fn dscp_is_six_bits() {
89+
assert_eq!(Dscp::new(0).map(Dscp::tos), Some(0));
90+
assert_eq!(Dscp::new(34).map(Dscp::tos), Some(136));
91+
assert_eq!(Dscp::new(63).map(Dscp::tos), Some(252));
92+
assert_eq!(Dscp::new(64), None);
93+
assert_eq!(Dscp::new(255), None);
94+
}
95+
96+
#[test]
97+
fn handles_share_only_their_own_connection() {
98+
let first = DscpHandle::new(Dscp::new(34).unwrap());
99+
let cloned = first.clone();
100+
let second = DscpHandle::new(Dscp::new(1).unwrap());
101+
102+
cloned.set(Dscp::new(28));
103+
assert_eq!(first.get(), Dscp::new(28));
104+
assert_eq!(second.get(), Dscp::new(1));
105+
106+
cloned.set(Dscp::new(0));
107+
assert_eq!(first.get(), Dscp::new(0));
108+
cloned.set(None);
109+
assert_eq!(first.get(), None);
110+
assert_eq!(second.get(), Dscp::new(1));
111+
}
112+
}

0 commit comments

Comments
 (0)