noq_udp/lib.rs
1//! Uniform interface to send and receive UDP packets with advanced features useful for QUIC
2//!
3//! This crate exposes kernel UDP stack features available on most modern systems which are required
4//! for an efficient and conformant QUIC implementation. As of this writing, these are not available
5//! in std or major async runtimes, and their niche character and complexity are a barrier to adding
6//! them. Hence, a dedicated crate.
7//!
8//! Exposed features include:
9//!
10//! - Segmentation offload for bulk send and receive operations, reducing CPU load.
11//! - Reporting the exact destination address of received packets and specifying explicit source
12//! addresses for sent packets, allowing responses to be sent from the address that the peer
13//! expects when there are multiple possibilities. This is common when bound to a wildcard address
14//! in IPv6 due to [RFC 8981] temporary addresses.
15//! - [Explicit Congestion Notification], which is required by QUIC to prevent packet loss and reduce
16//! latency on congested links when supported by the network path.
17//! - Disabled IP-layer fragmentation, which allows the true physical MTU to be detected and reduces
18//! risk of QUIC packet loss.
19//!
20//! Some features are unavailable in some environments. This can be due to an outdated operating
21//! system or drivers. Some operating systems may not implement desired features at all, or may not
22//! yet be supported by the crate. When support is unavailable, functionality will gracefully
23//! degrade.
24//!
25//! [RFC 8981]: https://www.rfc-editor.org/rfc/rfc8981.html
26//! [Explicit Congestion Notification]: https://www.rfc-editor.org/rfc/rfc3168.html
27#![warn(unreachable_pub)]
28#![warn(clippy::use_self)]
29
30use core::time::Duration;
31use std::net::{IpAddr, Ipv6Addr, SocketAddr};
32#[cfg(unix)]
33use std::os::unix::io::AsFd;
34#[cfg(windows)]
35use std::os::windows::io::AsSocket;
36#[cfg(not(wasm_browser))]
37use std::{sync::Mutex, time::Instant};
38
39#[cfg(apple_fast)]
40mod apple_fast;
41
42#[cfg(any(all(unix, not(posix_minimal)), windows))]
43mod cmsg;
44
45#[cfg(all(unix, not(posix_minimal)))]
46#[path = "unix.rs"]
47mod imp;
48
49#[cfg(all(any(target_os = "linux", target_os = "android"), not(posix_minimal)))]
50mod linux;
51
52#[cfg(windows)]
53#[path = "windows.rs"]
54mod imp;
55
56// Minimal POSIX UDP for platforms without advanced socket APIs (cmsg, GSO, GRO)
57#[cfg(posix_minimal)]
58#[path = "posix_minimal.rs"]
59mod imp;
60
61#[allow(unused_imports, unused_macros)]
62mod log {
63 #[cfg(all(feature = "log", not(feature = "tracing-log")))]
64 pub(crate) use log::{debug, error, info, trace, warn};
65
66 #[cfg(feature = "tracing-log")]
67 pub(crate) use tracing::{debug, error, info, trace, warn};
68
69 #[cfg(not(any(feature = "log", feature = "tracing-log")))]
70 mod no_op {
71 macro_rules! trace ( ($($tt:tt)*) => {{}} );
72 macro_rules! debug ( ($($tt:tt)*) => {{}} );
73 macro_rules! info ( ($($tt:tt)*) => {{}} );
74 macro_rules! log_warn ( ($($tt:tt)*) => {{}} );
75 macro_rules! error ( ($($tt:tt)*) => {{}} );
76
77 pub(crate) use {debug, error, info, log_warn as warn, trace};
78 }
79
80 #[cfg(not(any(feature = "log", feature = "tracing-log")))]
81 pub(crate) use no_op::*;
82}
83
84#[cfg(not(wasm_browser))]
85pub use imp::UdpSocketState;
86
87/// Number of UDP packets to send/receive at a time
88#[cfg(not(wasm_browser))]
89pub const BATCH_SIZE: usize = imp::BATCH_SIZE;
90/// Number of UDP packets to send/receive at a time
91#[cfg(wasm_browser)]
92pub const BATCH_SIZE: usize = 1;
93
94/// Metadata for a single buffer filled with bytes received from the network
95///
96/// This associated buffer can contain one or more datagrams, see [`stride`].
97///
98/// [`stride`]: RecvMeta::stride
99#[derive(Debug, Copy, Clone)]
100#[non_exhaustive]
101pub struct RecvMeta {
102 /// The source address of the datagram(s) contained in the buffer
103 pub addr: SocketAddr,
104 /// The number of bytes the associated buffer has
105 pub len: usize,
106 /// The size of a single datagram in the associated buffer
107 ///
108 /// When GRO (Generic Receive Offload) is used this indicates the size of a single
109 /// datagram inside the buffer. If the buffer is larger, that is if [`len`] is greater
110 /// then this value, then the individual datagrams contained have their boundaries at
111 /// `stride` increments from the start. The last datagram could be smaller than
112 /// `stride`.
113 ///
114 /// [`len`]: RecvMeta::len
115 pub stride: usize,
116 /// The Explicit Congestion Notification bits for the datagram(s) in the buffer
117 pub ecn: Option<EcnCodepoint>,
118 /// The destination IP address which was encoded in this datagram
119 ///
120 /// Populated on platforms: Windows (except under Wine), Linux, Android
121 /// (API level > 25), FreeBSD, OpenBSD, NetBSD, macOS, and iOS.
122 pub dst_ip: Option<IpAddr>,
123 /// The interface index of the interface on which the datagram was received
124 pub interface_index: Option<u32>,
125 /// Kernel receive timestamp as Unix epoch
126 ///
127 /// Populated on platforms: Linux, Android.
128 pub timestamp: Option<Duration>,
129}
130
131impl Default for RecvMeta {
132 /// Constructs a value with arbitrary fields, intended to be overwritten
133 fn default() -> Self {
134 Self {
135 addr: SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 0),
136 len: 0,
137 stride: 0,
138 ecn: None,
139 dst_ip: None,
140 interface_index: None,
141 timestamp: None,
142 }
143 }
144}
145
146/// An outgoing packet
147#[derive(Debug, Clone)]
148pub struct Transmit<'a> {
149 /// The socket this datagram should be sent to
150 pub destination: SocketAddr,
151 /// Explicit congestion notification bits to set on the packet
152 pub ecn: Option<EcnCodepoint>,
153 /// Contents of the datagram
154 pub contents: &'a [u8],
155 /// The segment size if this transmission contains multiple datagrams.
156 /// This is `None` if the transmit only contains a single datagram
157 pub segment_size: Option<usize>,
158 /// Optional source IP address for the datagram
159 pub src_ip: Option<IpAddr>,
160}
161
162#[cfg(not(posix_minimal))]
163impl Transmit<'_> {
164 /// Computes the effective segment-size of the packet.
165 ///
166 /// Some (older) network drivers don't like being told to do GSO even if
167 /// there is effectively only a single segment.
168 /// (i.e. `segment_size == contents.len()`)
169 /// Additionally, a `segment_size` that is greater than the content also
170 /// means there is effectively only a single segment.
171 /// This case is actually quite common when splitting up a prepared GSO batch
172 /// again after GSO has been disabled because the last datagram in a GSO
173 /// batch is allowed to be smaller than the segment size.
174 #[cfg_attr(apple_fast, allow(dead_code))] // Used by prepare_msg, which is unused when apple_fast
175 fn effective_segment_size(&self) -> Option<usize> {
176 match self.segment_size? {
177 size if size >= self.contents.len() => None,
178 size => Some(size),
179 }
180 }
181}
182
183/// Log at most 1 IO error per minute
184#[cfg(not(wasm_browser))]
185const IO_ERROR_LOG_INTERVAL: Duration = Duration::from_secs(60);
186
187/// Logs a warning message when sendmsg fails
188///
189/// Logging will only be performed if at least [`IO_ERROR_LOG_INTERVAL`]
190/// has elapsed since the last error was logged.
191#[cfg(all(not(wasm_browser), any(feature = "tracing-log", feature = "log")))]
192fn log_sendmsg_error(
193 last_send_error: &Mutex<Instant>,
194 err: impl core::fmt::Debug,
195 transmit: &Transmit<'_>,
196) {
197 let now = Instant::now();
198 let last_send_error = &mut *last_send_error.lock().expect("poisend lock");
199 if now.saturating_duration_since(*last_send_error) > IO_ERROR_LOG_INTERVAL {
200 *last_send_error = now;
201 log::debug!(
202 "sendmsg error: {:?}, Transmit: {{ destination: {:?}, src_ip: {:?}, ecn: {:?}, len: {:?}, segment_size: {:?} }}",
203 err,
204 transmit.destination,
205 transmit.src_ip,
206 transmit.ecn,
207 transmit.contents.len(),
208 transmit.segment_size
209 );
210 }
211}
212
213// No-op
214#[cfg(not(any(wasm_browser, feature = "tracing-log", feature = "log")))]
215fn log_sendmsg_error(_: &Mutex<Instant>, _: impl core::fmt::Debug, _: &Transmit<'_>) {}
216
217/// A borrowed UDP socket
218///
219/// On Unix, constructible via `From<T: AsFd>`. On Windows, constructible via `From<T:
220/// AsSocket>`.
221// Wrapper around socket2 to avoid making it a public dependency and incurring stability risk
222#[cfg(not(wasm_browser))]
223pub struct UdpSockRef<'a>(socket2::SockRef<'a>);
224
225#[cfg(unix)]
226impl<'s, S> From<&'s S> for UdpSockRef<'s>
227where
228 S: AsFd,
229{
230 fn from(socket: &'s S) -> Self {
231 Self(socket.into())
232 }
233}
234
235#[cfg(windows)]
236impl<'s, S> From<&'s S> for UdpSockRef<'s>
237where
238 S: AsSocket,
239{
240 fn from(socket: &'s S) -> Self {
241 Self(socket.into())
242 }
243}
244
245/// Explicit congestion notification codepoint
246#[repr(u8)]
247#[derive(Debug, Copy, Clone, Eq, PartialEq)]
248pub enum EcnCodepoint {
249 /// The ECT(0) codepoint, indicating that an endpoint is ECN-capable
250 Ect0 = 0b10,
251 /// The ECT(1) codepoint, indicating that an endpoint is ECN-capable
252 Ect1 = 0b01,
253 /// The CE codepoint, signalling that congestion was experienced
254 Ce = 0b11,
255}
256
257impl EcnCodepoint {
258 /// Create new object from the given bits
259 pub fn from_bits(x: u8) -> Option<Self> {
260 use EcnCodepoint::*;
261 Some(match x & 0b11 {
262 0b10 => Ect0,
263 0b01 => Ect1,
264 0b11 => Ce,
265 _ => {
266 return None;
267 }
268 })
269 }
270}
271
272#[cfg(all(test, not(posix_minimal)))]
273mod tests {
274 use std::net::Ipv4Addr;
275
276 use super::*;
277
278 #[test]
279 fn effective_segment_size() {
280 assert_eq!(
281 make_transmit(&[0u8; 10], Some(15)).effective_segment_size(),
282 None,
283 "segment_size > content_len should yield no effective segment_size"
284 );
285 assert_eq!(
286 make_transmit(&[0u8; 10], Some(10)).effective_segment_size(),
287 None,
288 "segment_size == content_len should yield no effective segment_size"
289 );
290 assert_eq!(
291 make_transmit(&[0u8; 10], None).effective_segment_size(),
292 None,
293 "no segment_size should yield no effective segment_size"
294 );
295 assert_eq!(
296 make_transmit(&[0u8; 10], Some(5)).effective_segment_size(),
297 Some(5),
298 "segment_size < content_len should yield effective segment_size"
299 );
300 }
301
302 fn make_transmit(contents: &[u8], segment_size: Option<usize>) -> Transmit<'_> {
303 Transmit {
304 destination: SocketAddr::from((Ipv4Addr::UNSPECIFIED, 1)),
305 ecn: None,
306 contents,
307 segment_size,
308 src_ip: None,
309 }
310 }
311}