Skip to main content

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}