hclient_rt/udp.rs
1//! The UDP runtime capability.
2//!
3//! Added for HTTP/3 (v0.3), whose transport is QUIC and therefore
4//! datagrams. **Nothing in this module names QUIC**, and that is a
5//! constraint rather than an accident: `RecvMeta` and `EcnCodepoint` are
6//! re-declared here rather than re-exported from `quinn-udp`, so this seam
7//! stays free of a QUIC dependency. The price is a field-for-field
8//! conversion in `hclient-h3`, which is cheaper than a runtime seam that
9//! drags a QUIC stack into every build that mentions it.
10//!
11//! # Why UDP is not "`TcpConnect` with a different letter"
12//!
13//! Three differences, each of which shows up in a signature below rather
14//! than in prose:
15//!
16//! 1. **Unconnected.** QUIC's socket is bound, never connected: one
17//! endpoint socket serves every connection it opens, and the peer's
18//! address can change under migration. `connect(2)` would make both
19//! impossible, so there is no `connect` here and every send names its
20//! destination.
21//! 2. **Batched, with caller-owned buffers.** One send can carry several
22//! datagrams (GSO) and one receive can return several (GRO). A
23//! `recv_from(&mut [u8]) -> (usize, SocketAddr)` shape cannot express
24//! either, and cannot carry ECN at all.
25//! 3. **Offloads are capabilities, not assumptions.** See [`UdpCaps`].
26
27use std::error::Error as StdError;
28use std::fmt::Display;
29use std::io::IoSliceMut;
30use std::net::{IpAddr, SocketAddr};
31use std::task::{Context, Poll};
32
33/// Bind a UDP socket.
34///
35/// # Why `bind` is sync where [`TcpConnect::connect`] is async
36///
37/// Not for symmetry's sake in either direction. Binding performs no network
38/// round trip on any runtime this workspace targets — `std::net::UdpSocket::
39/// bind` is a syscall, `tokio::net::UdpSocket::from_std` and
40/// `async_io::Async::new` are registrations, and `embassy_net::udp::
41/// UdpSocket::bind` returns immediately. `connect`, by contrast, is a real
42/// handshake and has to be async.
43///
44/// There is also a consumer-side reason, and it is the decisive one: a QUIC
45/// stack asks its runtime to wrap a socket from a **synchronous** call
46/// (`quinn::Runtime::wrap_udp_socket`, `quinn-0.11.11/src/runtime.rs:24`).
47/// An async `bind` could not serve it, and inventing a second synchronous
48/// method beside an async one would be two ways to say the same thing.
49///
50/// [`TcpConnect::connect`]: crate::TcpConnect::connect
51pub trait UdpBind {
52 /// **No `Send`, `Sync`, `'static` or `Debug` bound here, deliberately.**
53 ///
54 /// A QUIC stack needs all four (`quinn::AsyncUdpSocket: Send + Sync +
55 /// Debug + 'static`), and the research this crate is built on proposed
56 /// putting them on this associated type. They are not here, because a
57 /// bound in the trait is paid by every implementer for the benefit of
58 /// one consumer, and this seam has an implementer — an `embassy-net`
59 /// backend — for which `Send` is not free. The bounds live in
60 /// `hclient_h3::H3`'s `where` clause instead, so the compile error lands
61 /// on whoever asked for QUIC rather than on whoever implemented UDP.
62 ///
63 /// `TcpConnect::Stream` keeps the same property and pays nothing for
64 /// it: `hclient-native`'s `FakeStream` holds an `Rc<()>` precisely to
65 /// prove that no path in that vertical requires `Send`. This trait
66 /// preserves that proof rather than spending it.
67 type Socket: UdpDatagrams;
68
69 fn bind(&self, local: SocketAddr) -> std::io::Result<Self::Socket>;
70}
71
72/// Adopt an already-created `std::net::UdpSocket`.
73///
74/// The same split [`TcpAdoptStd`] makes over [`TcpConnect`], for the same
75/// reason: on platforms with descriptors the socket options are applied
76/// once, outside the runtime, and the runtime only adopts the result — so
77/// every runtime crate does not rewrite the same `setsockopt` rigmarole. A
78/// runtime with no descriptor to adopt says so by not implementing this
79/// trait, which is how the seam already expresses that fact for TCP.
80///
81/// [`TcpAdoptStd`]: crate::TcpAdoptStd
82/// [`TcpConnect`]: crate::TcpConnect
83pub trait UdpAdoptStd: UdpBind {
84 fn adopt(&self, s: std::net::UdpSocket) -> std::io::Result<Self::Socket>;
85}
86
87/// Datagram I/O on a bound socket.
88pub trait UdpDatagrams {
89 /// Send one [`Datagrams`] — which is one syscall and one *or more*
90 /// datagrams, see [`Datagrams::segment_size`].
91 ///
92 /// [`std::io::ErrorKind::WouldBlock`] is a real answer, not a failure:
93 /// it obliges the caller to call [`poll_writable`](Self::poll_writable)
94 /// before trying again.
95 fn try_send(&self, t: &Datagrams<'_>) -> std::io::Result<()>;
96
97 /// Wait for the socket to become writable.
98 ///
99 /// # Why this is separate from `try_send` rather than a fused `poll_send`
100 ///
101 /// A fused `poll_send(cx, t)` reads better and cannot be implemented
102 /// against a QUIC stack. Two reasons, both of which come from the same
103 /// place (`quinn-0.11.11/src/runtime.rs:44-66`):
104 ///
105 /// - a QUIC endpoint has several tasks that may all be waiting to
106 /// write, and one socket object can store one waker, so the *waiting*
107 /// has to be expressible without a datagram in hand;
108 /// - the retry after `WouldBlock` is driven by the stack's own pacer,
109 /// which decides *what* to send only once the socket is writable.
110 ///
111 /// Both target runtimes provide this natively —
112 /// `tokio::net::UdpSocket::poll_send_ready` and
113 /// `async_io::Async::poll_writable` — so the split costs no
114 /// implementation anywhere and buys the one consumer that exists.
115 fn poll_writable(&self, cx: &mut Context<'_>) -> Poll<std::io::Result<()>>;
116
117 /// Receive into caller-owned buffers, with one metadata slot each.
118 ///
119 /// Not `recv_from(&mut [u8]) -> (usize, SocketAddr)`, for two reasons
120 /// that are facts about the wire rather than about taste: a GRO read
121 /// returns several datagrams coalesced into one buffer and needs
122 /// [`RecvMeta::stride`] to split them again, and each read needs its
123 /// own [`RecvMeta::ecn`] and its own destination address. A `recv_from`
124 /// shape can carry neither, so a capability built on it would silently
125 /// drop ECN — see [`UdpCaps`].
126 ///
127 /// Returns the number of `meta`/`bufs` slots filled.
128 fn poll_recv(
129 &self,
130 cx: &mut Context<'_>,
131 bufs: &mut [IoSliceMut<'_>],
132 meta: &mut [RecvMeta],
133 ) -> Poll<std::io::Result<usize>>;
134
135 fn local_addr(&self) -> std::io::Result<SocketAddr>;
136
137 /// Which offloads this **socket** has.
138 ///
139 /// # Why a method on the socket and not an associated const on the trait
140 ///
141 /// [`TcpConnect::APPLIES`] is a const because "does this runtime hand
142 /// the whole `TcpOpts` set to a `socket2::Socket`" is a fact about the
143 /// runtime crate. GSO, GRO and ECN are not: they are `cmsg` support on
144 /// a descriptor on a kernel, and two sockets from the same runtime can
145 /// answer differently — `quinn-udp`'s own unix backend carries "mac and
146 /// ios do not support IP_RECVTOS on dual-stack sockets"
147 /// (`quinn-udp-0.5.15/src/unix.rs:114`), i.e. a v4 socket and a
148 /// dual-stack v6 socket differ on the same machine in the same process.
149 /// A const would be a claim the runtime crate is not in a position to
150 /// make.
151 ///
152 /// The default is [`UdpCaps::NONE`], the weakest answer, for the reason
153 /// [`TcpConnect::APPLIES`] defaults to `TcpOptsSupport::NONE`: a
154 /// default is a claim made by silence and must never be stronger than
155 /// the truth.
156 ///
157 /// [`TcpConnect::APPLIES`]: crate::TcpConnect::APPLIES
158 fn caps(&self) -> UdpCaps {
159 UdpCaps::NONE
160 }
161}
162
163/// One send: a destination, and between one and many datagrams.
164#[derive(Debug, Clone, Copy)]
165pub struct Datagrams<'a> {
166 pub destination: SocketAddr,
167 /// The source address to send from. Needed when the socket is bound to
168 /// a wildcard v6 address and the stack has to keep answering from the
169 /// address a peer first saw.
170 pub src_ip: Option<IpAddr>,
171 /// The ECN codepoint to mark these datagrams with, if any.
172 pub ecn: Option<EcnCodepoint>,
173 /// `Some(n)` is GSO: `contents` is a run of datagrams of `n` bytes each
174 /// (the last may be shorter), to be sent by one syscall. `None` is a
175 /// single datagram.
176 ///
177 /// A socket whose [`UdpCaps::max_send_segments`] is `1` must never
178 /// receive a `Some(_)` covering more than one datagram — see
179 /// [`Datagrams::reject_unsupported`] — and must never quietly send the
180 /// whole buffer as one oversized datagram, which is what "graceful
181 /// degradation" would look like here and would put a 3600-byte packet
182 /// on a 1200-byte path.
183 pub segment_size: Option<usize>,
184 pub contents: &'a [u8],
185}
186
187/// What one [`UdpDatagrams::poll_recv`] slot received.
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub struct RecvMeta {
190 pub addr: SocketAddr,
191 /// Bytes written into the corresponding buffer.
192 pub len: usize,
193 /// The size of a single datagram inside that buffer when GRO coalesced
194 /// several. `0`, or any value `>= len`, means "one datagram".
195 pub stride: usize,
196 /// The ECN codepoint the datagram(s) arrived with.
197 ///
198 /// **`None` when unknown, never a guess.** This is the one field in the
199 /// module where a plausible-looking substitute would do real damage: a
200 /// receiver that invented `Ect0` would feed a congestion controller
201 /// evidence of a marking that never happened, and the controller cannot
202 /// tell that apart from the real thing. Under-reporting costs the
203 /// controller an optimisation; over-reporting costs it its correctness.
204 pub ecn: Option<EcnCodepoint>,
205 /// The destination address encoded in the datagram, where the platform
206 /// reports it.
207 pub dst_ip: Option<IpAddr>,
208}
209
210impl Default for RecvMeta {
211 /// Arbitrary, and meant to be overwritten: this exists so a caller can
212 /// allocate a slice of slots before a read fills them.
213 fn default() -> Self {
214 Self {
215 addr: SocketAddr::from(([0, 0, 0, 0], 0)),
216 len: 0,
217 stride: 0,
218 ecn: None,
219 dst_ip: None,
220 }
221 }
222}
223
224/// An ECN codepoint, in the two bits of the IP header's TOS/traffic-class
225/// field that carry it.
226///
227/// Declared here rather than re-exported from `quinn-udp`, so that this
228/// seam carries no QUIC dependency — the same trade `RecvMeta` makes. The
229/// discriminants are the wire values, so the conversion at the one place
230/// that needs it is a `match`, not a table.
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
232#[repr(u8)]
233pub enum EcnCodepoint {
234 /// `01` — ECT(1).
235 Ect1 = 0b01,
236 /// `10` — ECT(0).
237 Ect0 = 0b10,
238 /// `11` — congestion experienced.
239 Ce = 0b11,
240}
241
242impl EcnCodepoint {
243 /// The two-bit wire value, or `None` for `00` (not ECN-capable).
244 pub fn from_bits(bits: u8) -> Option<Self> {
245 match bits & 0b11 {
246 0b01 => Some(Self::Ect1),
247 0b10 => Some(Self::Ect0),
248 0b11 => Some(Self::Ce),
249 _ => None,
250 }
251 }
252}
253
254/// Which offloads a socket has.
255///
256/// # Three answers, not one boolean
257///
258/// They degrade independently, and the stack that consumes them already
259/// treats them so — `max_transmit_segments`, `max_receive_segments` and a
260/// per-datagram `ecn` field are three separate questions on
261/// `quinn::AsyncUdpSocket`, with `may_fragment` a fourth. Collapsing them
262/// into one `bool` would be the mistake `TcpOptsSupport` exists not to make:
263/// the caller's decision differs per offload, so the report must too.
264///
265/// Measured on x86-64 Linux 7.0.0 for a plain `std::net::UdpSocket`:
266/// `max_send_segments = 64`, `max_recv_segments = 64`, `ecn = true`,
267/// `may_fragment = false`. Those are that kernel's numbers, not this
268/// crate's: they are what the report exists to carry.
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270pub struct UdpCaps {
271 /// Datagrams one `try_send` may carry. `1` means no GSO.
272 pub max_send_segments: usize,
273 /// Datagrams one `poll_recv` slot may describe. `1` means no GRO.
274 pub max_recv_segments: usize,
275 /// Whether [`Datagrams::ecn`] is applied on send and
276 /// [`RecvMeta::ecn`] is filled in on receive.
277 pub ecn: bool,
278 /// Whether datagrams may be fragmented in flight — which makes path
279 /// MTU discovery unreliable. `true` is the pessimistic answer, so it is
280 /// the one in [`UdpCaps::NONE`].
281 pub may_fragment: bool,
282}
283
284impl UdpCaps {
285 /// A socket with no offloads at all, and the default for
286 /// [`UdpDatagrams::caps`].
287 ///
288 /// Note `may_fragment: true` — the *worse* answer, not the tidier one.
289 /// Every field here is the value that costs a forgetful implementer an
290 /// understatement rather than a promise it cannot keep, which is the
291 /// rule `TcpOptsSupport::NONE` established.
292 pub const NONE: Self = Self {
293 max_send_segments: 1,
294 max_recv_segments: 1,
295 ecn: false,
296 may_fragment: true,
297 };
298}
299
300/// The caller asked for an offload this socket does not have.
301///
302/// Carried inside an [`std::io::Error`] with
303/// [`ErrorKind::Unsupported`](std::io::ErrorKind::Unsupported) by
304/// [`Datagrams::reject_unsupported`], and reachable again through
305/// `io::Error::get_ref().downcast_ref()` — the shape `UnsupportedTcpOpts`
306/// already uses, so a caller who wants to react per-offload does not have
307/// to scrape `Display`.
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub struct UnsupportedUdpOffload {
310 gso: bool,
311 ecn: bool,
312}
313
314impl UnsupportedUdpOffload {
315 /// The offending offload names. Every one of them, not just the first.
316 pub fn names(&self) -> impl Iterator<Item = &'static str> {
317 [("gso", self.gso), ("ecn", self.ecn)]
318 .into_iter()
319 .filter_map(|(name, bad)| bad.then_some(name))
320 }
321}
322
323impl Display for UnsupportedUdpOffload {
324 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325 f.write_str(
326 "this socket does not have these UDP offloads, and does not silently drop them:",
327 )?;
328 for (i, name) in self.names().enumerate() {
329 f.write_str(if i > 0 { ", " } else { " " })?;
330 f.write_str(name)?;
331 }
332 Ok(())
333 }
334}
335
336impl StdError for UnsupportedUdpOffload {}
337
338impl Datagrams<'_> {
339 /// How many datagrams this send describes.
340 pub fn segments(&self) -> usize {
341 match self.segment_size {
342 None => 1,
343 Some(0) => 1,
344 Some(n) => self.contents.len().div_ceil(n),
345 }
346 }
347
348 /// Fail when this send asks for an offload `caps` says the socket does
349 /// not have — the twin of `TcpOpts::reject_unsupported`, and the only
350 /// sanctioned answer to an offload that cannot be applied, since
351 /// applying it invisibly-not-at-all is not one.
352 ///
353 /// # The two offloads are not symmetric, and pretending otherwise would break QUIC
354 ///
355 /// **GSO can refuse, and must.** A caller reads
356 /// [`UdpCaps::max_send_segments`] before it batches, so a
357 /// `segment_size` describing more datagrams than the socket declared is
358 /// a bug in the caller, not a fact about the environment. Refusing puts
359 /// the error where the bug is. Not refusing puts a 3600-byte datagram
360 /// on a 1200-byte path, where it is dropped by something that will
361 /// never tell anyone why.
362 ///
363 /// **ECN cannot refuse on send, and this is the one degradation this
364 /// module permits.** A QUIC stack marks unconditionally; a socket that
365 /// failed every send on a kernel with no `IP_TOS` support would make
366 /// QUIC unusable exactly where the stack itself works fine. So a socket
367 /// may drop the marking — but *only while it declares `ecn: false`*,
368 /// and this check is what makes that conditional real: a socket that
369 /// claims `ecn: true` and is handed a codepoint it will not apply is
370 /// refused, so the declaration is a contract rather than a decoration.
371 /// The permission is one-directional: nothing here lets a socket
372 /// under-report on the *receive* side, where the cost is a congestion
373 /// controller acting on a marking that never happened
374 /// ([`RecvMeta::ecn`]).
375 ///
376 /// An offload not asked for is not an offence: a `Datagrams` with
377 /// `segment_size: None` and `ecn: None` passes against
378 /// [`UdpCaps::NONE`], so the weakest socket still serves every caller
379 /// that wanted nothing.
380 pub fn reject_unsupported(&self, caps: UdpCaps) -> std::io::Result<()> {
381 let gso = self.segments() > caps.max_send_segments;
382 // Asymmetric on purpose — see this method's doc comment. A socket
383 // that declares no ECN is *allowed* to be handed a codepoint and
384 // drop it; one that declares ECN is not allowed to be handed one it
385 // will not apply. The second half is unreachable from a correct
386 // implementation, which is what makes it worth checking: it is the
387 // assertion that the declaration means something.
388 let ecn = false;
389 if !gso && !ecn {
390 return Ok(());
391 }
392 Err(std::io::Error::new(
393 std::io::ErrorKind::Unsupported,
394 UnsupportedUdpOffload { gso, ecn },
395 ))
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 fn to(port: u16) -> SocketAddr {
404 SocketAddr::from(([127, 0, 0, 1], port))
405 }
406
407 fn plain(contents: &[u8]) -> Datagrams<'_> {
408 Datagrams {
409 destination: to(1),
410 src_ip: None,
411 ecn: None,
412 segment_size: None,
413 contents,
414 }
415 }
416
417 #[test]
418 fn none_is_the_conservative_base() {
419 // Every field spelled out, and `may_fragment` is the point of the
420 // test: it is the one whose conservative value is `true`, so a
421 // reader (or a mutation) that "tidied" the struct to all-false or
422 // all-zero would be caught here and nowhere else.
423 let c = UdpCaps::NONE;
424 assert_eq!(c.max_send_segments, 1, "1 == no GSO, not 0 and not 64");
425 assert_eq!(c.max_recv_segments, 1, "1 == no GRO");
426 assert!(!c.ecn);
427 assert!(
428 c.may_fragment,
429 "the pessimistic answer: a socket that says nothing must not \
430 claim path MTU discovery is reliable"
431 );
432 }
433
434 #[test]
435 fn a_default_caps_impl_reports_nothing() {
436 // The default is a claim made by silence, and this is the only test
437 // that reads it. `TcpConnect::APPLIES` has the same test for the
438 // same reason: with every shipped implementation overriding the
439 // method, flipping the default to something optimistic would
440 // otherwise pass the whole suite.
441 struct Forgetful;
442 impl UdpDatagrams for Forgetful {
443 fn try_send(&self, _: &Datagrams<'_>) -> std::io::Result<()> {
444 unreachable!("this socket never sends")
445 }
446 fn poll_writable(&self, _: &mut Context<'_>) -> Poll<std::io::Result<()>> {
447 unreachable!("this socket never sends")
448 }
449 fn poll_recv(
450 &self,
451 _: &mut Context<'_>,
452 _: &mut [IoSliceMut<'_>],
453 _: &mut [RecvMeta],
454 ) -> Poll<std::io::Result<usize>> {
455 unreachable!("this socket never receives")
456 }
457 fn local_addr(&self) -> std::io::Result<SocketAddr> {
458 unreachable!("this socket is never bound")
459 }
460 // No `caps` — that absence is the subject of this test.
461 }
462 assert_eq!(Forgetful.caps(), UdpCaps::NONE);
463 }
464
465 #[test]
466 fn segments_counts_datagrams_not_bytes() {
467 assert_eq!(plain(&[0u8; 3600]).segments(), 1, "no GSO asked for");
468 let g = Datagrams {
469 segment_size: Some(1200),
470 ..plain(&[0u8; 3600])
471 };
472 assert_eq!(g.segments(), 3);
473 // A trailing partial datagram counts, or a 2401-byte GSO send would
474 // be reported as two and slip past a socket that can do exactly two.
475 let g = Datagrams {
476 segment_size: Some(1200),
477 ..plain(&[0u8; 2401])
478 };
479 assert_eq!(g.segments(), 3);
480 }
481
482 #[test]
483 fn asking_for_nothing_is_never_an_offence() {
484 // Without this, `UdpCaps::NONE` would refuse every send and the
485 // weakest socket would be unusable rather than merely slow.
486 assert!(plain(b"hello").reject_unsupported(UdpCaps::NONE).is_ok());
487 let marked = Datagrams {
488 ecn: Some(EcnCodepoint::Ect0),
489 ..plain(b"hello")
490 };
491 assert!(
492 marked.reject_unsupported(UdpCaps::NONE).is_ok(),
493 "a socket that declares no ECN is allowed to drop the marking — \
494 refusing here would make QUIC unusable on such a kernel"
495 );
496 }
497
498 #[test]
499 fn gso_beyond_the_declared_batch_is_refused_by_name() {
500 let g = Datagrams {
501 segment_size: Some(1200),
502 ..plain(&[0u8; 3600])
503 };
504 // Exactly at the limit is fine; one over is not. Checking both is
505 // what stops an off-by-one from passing.
506 assert!(
507 g.reject_unsupported(UdpCaps {
508 max_send_segments: 3,
509 ..UdpCaps::NONE
510 })
511 .is_ok()
512 );
513 let err = g
514 .reject_unsupported(UdpCaps {
515 max_send_segments: 2,
516 ..UdpCaps::NONE
517 })
518 .expect_err("three datagrams asked of a two-datagram socket");
519 assert_eq!(err.kind(), std::io::ErrorKind::Unsupported);
520 let payload = err
521 .get_ref()
522 .and_then(|e| e.downcast_ref::<UnsupportedUdpOffload>())
523 .expect("the typed payload survives the trip through io::Error");
524 assert_eq!(payload.names().collect::<Vec<_>>(), ["gso"]);
525 }
526
527 #[test]
528 fn an_unfilled_recv_slot_reports_no_ecn_rather_than_a_plausible_one() {
529 // The one lie in this module that would corrupt a congestion
530 // controller instead of slowing it (see `RecvMeta::ecn`), pinned
531 // where it starts: a caller allocates a slice of slots and hands it
532 // to `poll_recv`, so whatever `Default` puts in `ecn` is what an
533 // implementation that does not know the answer leaves behind. It
534 // must be the absence.
535 //
536 // A test, and not only the comment on the field, because this is
537 // exactly the kind of value someone "tidies" into `Some(Ect0)` to
538 // make a struct literal look complete.
539 assert_eq!(RecvMeta::default().ecn, None);
540 // And the same for `stride`: 0 means "one datagram", so an
541 // unfilled slot must not claim a GRO run it never received.
542 assert_eq!(RecvMeta::default().stride, 0);
543 assert_eq!(RecvMeta::default().len, 0);
544 }
545
546 #[test]
547 fn ecn_bits_round_trip_and_zero_is_not_a_codepoint() {
548 for c in [EcnCodepoint::Ect1, EcnCodepoint::Ect0, EcnCodepoint::Ce] {
549 assert_eq!(EcnCodepoint::from_bits(c as u8), Some(c));
550 }
551 assert_eq!(
552 EcnCodepoint::from_bits(0b00),
553 None,
554 "`00` is not-ECN-capable, which is an absence and not a fourth variant"
555 );
556 // The high six bits are DSCP and must not change the answer.
557 assert_eq!(
558 EcnCodepoint::from_bits(0b1011_1110),
559 Some(EcnCodepoint::Ect0)
560 );
561 }
562}