tokio_quiche/socket/capabilities.rs
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
6// met:
7//
8// * Redistributions of source code must retain the above copyright notice,
9// this list of conditions and the following disclaimer.
10//
11// * Redistributions in binary form must reproduce the above copyright
12// notice, this list of conditions and the following disclaimer in the
13// documentation and/or other materials provided with the distribution.
14//
15// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
16// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
19// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27#[cfg(target_os = "linux")]
28mod linux_imports {
29 pub use libc::c_int;
30 pub use libc::c_void;
31 pub use libc::sock_txtime;
32 pub use libc::socklen_t;
33 pub use libc::IPPROTO_IP;
34 pub use libc::IPPROTO_IPV6;
35 pub use libc::IPV6_MTU_DISCOVER;
36 pub use libc::IPV6_PMTUDISC_PROBE;
37 pub use libc::IP_MTU_DISCOVER;
38 pub use libc::IP_PMTUDISC_PROBE;
39 pub use libc::SOL_SOCKET;
40 pub use libc::SO_RCVMARK;
41 pub use nix::errno::Errno;
42 pub use nix::sys::socket::getsockopt;
43 pub use nix::sys::socket::setsockopt;
44 pub use nix::sys::socket::sockopt::IpFreebind;
45 pub use nix::sys::socket::sockopt::IpTransparent;
46 pub use nix::sys::socket::sockopt::Ipv4OrigDstAddr;
47 pub use nix::sys::socket::sockopt::Ipv4PacketInfo;
48 pub use nix::sys::socket::sockopt::Ipv6OrigDstAddr;
49 pub use nix::sys::socket::sockopt::Ipv6RecvPacketInfo;
50 #[cfg(feature = "perf-quic-listener-metrics")]
51 pub use nix::sys::socket::sockopt::ReceiveTimestampns;
52 pub use nix::sys::socket::sockopt::RxqOvfl;
53 pub use nix::sys::socket::sockopt::TxTime;
54 pub use nix::sys::socket::sockopt::UdpGroSegment;
55 pub use nix::sys::socket::sockopt::UdpGsoSegment;
56 pub use nix::sys::socket::SetSockOpt;
57 pub use std::io;
58 pub use std::os::fd::AsFd;
59 pub use std::os::fd::AsRawFd;
60 pub use std::os::fd::BorrowedFd;
61}
62
63#[cfg(target_os = "linux")]
64use linux_imports::*;
65
66#[cfg(target_os = "linux")]
67#[derive(Clone)]
68struct IpMtuDiscoverProbe;
69
70#[cfg(target_os = "linux")]
71impl SetSockOpt for IpMtuDiscoverProbe {
72 type Val = ();
73
74 fn set<F: AsFd>(&self, fd: &F, _val: &Self::Val) -> nix::Result<()> {
75 let pmtud_mode: c_int = IP_PMTUDISC_PROBE;
76 let ret = unsafe {
77 libc::setsockopt(
78 fd.as_fd().as_raw_fd(),
79 IPPROTO_IP,
80 IP_MTU_DISCOVER,
81 &pmtud_mode as *const c_int as *const c_void,
82 std::mem::size_of::<c_int>() as socklen_t,
83 )
84 };
85
86 match ret {
87 0 => Ok(()),
88 _ => Err(Errno::last()),
89 }
90 }
91}
92
93#[cfg(target_os = "linux")]
94#[derive(Clone)]
95struct Ipv6MtuDiscoverProbe;
96
97#[cfg(target_os = "linux")]
98impl SetSockOpt for Ipv6MtuDiscoverProbe {
99 type Val = ();
100
101 fn set<F: AsFd>(&self, fd: &F, _val: &Self::Val) -> nix::Result<()> {
102 let pmtud_mode: c_int = IPV6_PMTUDISC_PROBE;
103 let ret = unsafe {
104 libc::setsockopt(
105 fd.as_fd().as_raw_fd(),
106 IPPROTO_IPV6,
107 IPV6_MTU_DISCOVER,
108 &pmtud_mode as *const c_int as *const c_void,
109 std::mem::size_of::<c_int>() as socklen_t,
110 )
111 };
112
113 match ret {
114 0 => Ok(()),
115 _ => Err(Errno::last()),
116 }
117 }
118}
119
120#[cfg(target_os = "linux")]
121#[derive(Clone)]
122struct RcvMark;
123
124#[cfg(target_os = "linux")]
125impl SetSockOpt for RcvMark {
126 type Val = ();
127
128 fn set<F: AsFd>(&self, fd: &F, _val: &Self::Val) -> nix::Result<()> {
129 // https://elixir.bootlin.com/linux/v6.17/source/net/core/sock.c#L1523
130 const ENABLE_SOCKOPT: i32 = 1;
131
132 let ret = unsafe {
133 libc::setsockopt(
134 fd.as_fd().as_raw_fd(),
135 SOL_SOCKET,
136 SO_RCVMARK,
137 &ENABLE_SOCKOPT as *const c_int as *const c_void,
138 std::mem::size_of::<c_int>() as socklen_t,
139 )
140 };
141
142 match ret {
143 0 => Ok(()),
144 _ => Err(Errno::last()),
145 }
146 }
147}
148
149/// Builder to enable Linux sockopts which improve QUIC performance.
150#[cfg(target_os = "linux")]
151pub struct SocketCapabilitiesBuilder<'s> {
152 socket: BorrowedFd<'s>,
153 cap: SocketCapabilities,
154}
155
156#[cfg(target_os = "linux")]
157impl<'s> SocketCapabilitiesBuilder<'s> {
158 /// Creates a new sockopt builder for `socket`.
159 pub fn new<S: AsFd>(socket: &'s S) -> Self {
160 Self {
161 socket: socket.as_fd(),
162 cap: Default::default(),
163 }
164 }
165
166 /// Enables [`UDP_SEGMENT`](https://man7.org/linux/man-pages/man7/udp.7.html),
167 /// a generic segmentation offload (GSO).
168 ///
169 /// GSO improves transmit performance by treating multiple sequential UDP
170 /// packets as a single entity in the kernel. Segmentation into
171 /// individual packets happens in the NIC, if it supports GSO. The
172 /// parameter specifies the packet size.
173 pub fn gso(&mut self) -> io::Result<()> {
174 // Initialize GSO with the maximum segment size so later increases to
175 // `max_send_udp_payload_size` do not exceed the initial setting.
176 //
177 // https://elixir.bootlin.com/linux/v6.14.6/source/net/ipv4/udp.c#L2998
178 // https://elixir.bootlin.com/linux/v6.14.6/source/include/vdso/limits.h#L5
179 setsockopt(&self.socket.as_fd(), UdpGsoSegment, &(u16::MAX as i32))?;
180 self.cap.has_gso = true;
181 Ok(())
182 }
183
184 /// Enables [`SO_RXQ_OVFL`](https://man7.org/linux/man-pages/man7/socket.7.html),
185 /// which reports dropped packets due to insufficient buffer space.
186 pub fn check_udp_drop(&mut self) -> io::Result<()> {
187 setsockopt(&self.socket.as_fd(), RxqOvfl, &1)?;
188
189 self.cap.check_udp_drop = true;
190 Ok(())
191 }
192
193 /// Enables [`SO_TXTIME`](https://man7.org/linux/man-pages/man8/tc-etf.8.html)
194 /// to control packet transmit timestamps for QUIC pacing.
195 pub fn txtime(&mut self) -> io::Result<()> {
196 let cfg = sock_txtime {
197 clockid: libc::CLOCK_MONOTONIC,
198 flags: 0,
199 };
200 setsockopt(&self.socket.as_fd(), TxTime, &cfg)?;
201
202 self.cap.has_txtime = true;
203 Ok(())
204 }
205
206 /// Enables [`SO_TIMESTAMPNS`](https://man7.org/linux/man-pages/man7/socket.7.html),
207 /// which records a wall-clock timestamp for each received packet.
208 #[cfg(feature = "perf-quic-listener-metrics")]
209 pub fn rxtime(&mut self) -> io::Result<()> {
210 setsockopt(&self.socket.as_fd(), ReceiveTimestampns, &true)?;
211
212 self.cap.has_rxtime = true;
213 Ok(())
214 }
215
216 /// Enables [`UDP_GRO`](https://man7.org/linux/man-pages/man7/udp.7.html),
217 /// a generic receive offload (GRO).
218 ///
219 /// GRO improves receive performance by allowing the kernel to yield
220 /// multiple UDP packets in one [`recvmsg(2)`](https://man7.org/linux/man-pages/man2/recv.2.html)
221 /// call. It is the equivalent of GSO for the receive path.
222 pub fn gro(&mut self) -> io::Result<()> {
223 UdpGroSegment.set(&self.socket.as_fd(), &true)?;
224
225 self.cap.has_gro = true;
226 Ok(())
227 }
228
229 /// Enables [`IP_PKTINFO`](https://man7.org/linux/man-pages/man7/ip.7.html)
230 /// to control the source IP in outbound IPv4 packets.
231 pub fn ipv4_pktinfo(&mut self) -> io::Result<()> {
232 setsockopt(&self.socket.as_fd(), Ipv4PacketInfo, &true)?;
233
234 self.cap.has_ippktinfo = true;
235 Ok(())
236 }
237
238 /// Enables [`IP_RECVORIGDSTADDR`](https://man7.org/linux/man-pages/man7/ip.7.html),
239 /// which reports each packet's real IPv4 destination address.
240 ///
241 /// This can be different from the socket's local address due to netfilter
242 /// TPROXY rules or eBPF redirects.
243 pub fn ipv4_recvorigdstaddr(&mut self) -> io::Result<()> {
244 setsockopt(&self.socket.as_fd(), Ipv4OrigDstAddr, &true)?;
245
246 self.cap.has_iprecvorigdstaddr = true;
247 Ok(())
248 }
249
250 /// Enables [`IPV6_RECVPKTINFO`](https://man7.org/linux/man-pages/man7/ipv6.7.html)
251 /// to control the source IP in outbound IPv6 packets.
252 pub fn ipv6_pktinfo(&mut self) -> io::Result<()> {
253 setsockopt(&self.socket.as_fd(), Ipv6RecvPacketInfo, &true)?;
254
255 self.cap.has_ipv6pktinfo = true;
256 Ok(())
257 }
258
259 /// Enables [`IPV6_RECVORIGDSTADDR`](https://elixir.bootlin.com/linux/v6.12/source/net/ipv6/datagram.c#L722-L743),
260 /// which reports each packet's real IPv6 destination address.
261 ///
262 /// This can be different from the socket's local address due to netfilter
263 /// TPROXY rules or eBPF redirects.
264 pub fn ipv6_recvorigdstaddr(&mut self) -> io::Result<()> {
265 setsockopt(&self.socket.as_fd(), Ipv6OrigDstAddr, &true)?;
266
267 self.cap.has_ipv6recvorigdstaddr = true;
268 Ok(())
269 }
270
271 /// Sets [`IP_MTU_DISCOVER`](https://man7.org/linux/man-pages/man7/ip.7.html), to
272 /// `IP_PMTUDISC_PROBE`, which disables kernel PMTUD and sets the `DF`
273 /// (Don't Fragment) flag.
274 pub fn ip_mtu_discover_probe(&mut self) -> io::Result<()> {
275 setsockopt(&self.socket.as_fd(), IpMtuDiscoverProbe, &())?;
276
277 self.cap.has_ip_mtu_discover_probe = true;
278 Ok(())
279 }
280
281 /// Sets [`IPV6_MTU_DISCOVER`](https://man7.org/linux/man-pages/man7/ipv6.7.html), to
282 /// `IPV6_PMTUDISC_PROBE`, which disables kernel PMTUD and sets the `DF`
283 /// (Don't Fragment) flag.
284 pub fn ipv6_mtu_discover_probe(&mut self) -> io::Result<()> {
285 setsockopt(&self.socket.as_fd(), Ipv6MtuDiscoverProbe, &())?;
286
287 self.cap.has_ipv6_mtu_discover_probe = true;
288 Ok(())
289 }
290
291 /// Tests whether [`IP_FREEBIND`](https://man7.org/linux/man-pages/man7/ip.7.html)
292 /// or [`IP_TRANSPARENT`](https://man7.org/linux/man-pages/man7/ip.7.html) are
293 /// enabled for this socket.
294 ///
295 /// # Warning
296 /// These sockopts require elevated permissions to enable, so the builder
297 /// will only check their status. **If neither of them is enabled, the
298 /// `PKTINFO` sockopts will cause errors when sending packets.**
299 pub fn allows_nonlocal_source(&self) -> io::Result<bool> {
300 Ok(getsockopt(&self.socket.as_fd(), IpFreebind)? ||
301 getsockopt(&self.socket.as_fd(), IpTransparent)?)
302 }
303
304 pub fn rcvmark(&mut self) -> io::Result<()> {
305 setsockopt(&self.socket.as_fd(), RcvMark, &())?;
306
307 self.cap.has_mark = true;
308 Ok(())
309 }
310
311 /// Consumes the builder and returns the configured [`SocketCapabilities`].
312 pub fn finish(self) -> SocketCapabilities {
313 self.cap
314 }
315}
316
317// TODO(erittenhouse): use `dgram`'s SocketCapabilities when we migrate over
318#[cfg_attr(not(target_os = "linux"), expect(rustdoc::broken_intra_doc_links))]
319/// Indicators of sockopts configured for a socket.
320///
321/// On Linux, a socket can be configured using a [`SocketCapabilitiesBuilder`],
322/// which returns the sockopts that were applied successfully. By default, all
323/// options are assumed to be disabled (including on OSes besides Linux).
324///
325/// As a shortcut, you may call `apply_all_and_get_compatibility` to apply the
326/// maxmimum set of capabilities supported by this crate. The result will
327/// indicate which options were actually enabled.
328#[derive(Debug, Default)]
329pub struct SocketCapabilities {
330 /// Indicates if the socket has `UDP_SEGMENT` enabled.
331 pub(crate) has_gso: bool,
332
333 /// Indicates if the socket has `SO_RXQ_OVFL` set.
334 // NOTE: RX-side sockopts are `expect(dead_code)` because we check for
335 // received cmsgs directly
336 #[cfg_attr(not(target_os = "linux"), expect(dead_code))]
337 pub(crate) check_udp_drop: bool,
338
339 /// Indicates if the socket was configured with `SO_TXTIME`.
340 pub(crate) has_txtime: bool,
341
342 /// Indicates if the socket has `SO_TIMESTAMPNS` enabled.
343 #[cfg_attr(
344 not(all(target_os = "linux", feature = "perf-quic-listener-metrics")),
345 expect(dead_code)
346 )]
347 pub(crate) has_rxtime: bool,
348
349 /// Indicates if the socket has `UDP_GRO` enabled.
350 #[cfg_attr(not(target_os = "linux"), expect(dead_code))]
351 pub(crate) has_gro: bool,
352
353 /// Indicates if the socket has `IP_PKTINFO` set.
354 pub(crate) has_ippktinfo: bool,
355
356 /// Indicates if the socket has `IP_RECVORIGDSTADDR` set.
357 #[cfg_attr(not(target_os = "linux"), expect(dead_code))]
358 pub(crate) has_iprecvorigdstaddr: bool,
359
360 /// Indicates if the socket has `IPV6_RECVPKTINFO` set.
361 pub(crate) has_ipv6pktinfo: bool,
362
363 /// Indicates if the socket has `IPV6_RECVORIGDSTADDR` set.
364 #[cfg_attr(not(target_os = "linux"), expect(dead_code))]
365 pub(crate) has_ipv6recvorigdstaddr: bool,
366
367 // Indicates if the socket has `IP_MTU_DISCOVER` set to `IP_PMTUDISC_PROBE`.
368 #[cfg_attr(not(target_os = "linux"), expect(dead_code))]
369 pub(crate) has_ip_mtu_discover_probe: bool,
370
371 // Indicates if the socket has `IPV6_MTU_DISCOVER` set to
372 // `IPV6_PMTUDISC_PROBE`.
373 #[cfg_attr(not(target_os = "linux"), expect(dead_code))]
374 pub(crate) has_ipv6_mtu_discover_probe: bool,
375
376 /// Indicates if the socket is set to receive `SO_MARK` messages via
377 /// `SO_RCVMARK`.
378 #[cfg_attr(not(target_os = "linux"), expect(dead_code))]
379 pub(crate) has_mark: bool,
380}
381
382impl SocketCapabilities {
383 /// Tries to enable all supported sockopts and returns indicators
384 /// of which settings were successfully applied.
385 #[cfg(target_os = "linux")]
386 pub fn apply_all_and_get_compatibility<S>(socket: &S) -> Self
387 where
388 S: AsFd,
389 {
390 let mut b = SocketCapabilitiesBuilder::new(socket);
391 let _ = b.gso();
392 let _ = b.check_udp_drop();
393 let _ = b.txtime();
394 #[cfg(feature = "perf-quic-listener-metrics")]
395 let _ = b.rxtime();
396 let _ = b.gro();
397 let _ = b.rcvmark();
398
399 // We can't determine if this is an IPv4 or IPv6 socket, so try setting
400 // the relevant options for both
401 let _ = b.ip_mtu_discover_probe();
402 let _ = b.ipv6_mtu_discover_probe();
403 if let Ok(true) = b.allows_nonlocal_source() {
404 let _ = b.ipv4_pktinfo();
405 let _ = b.ipv4_recvorigdstaddr();
406 let _ = b.ipv6_pktinfo();
407 let _ = b.ipv6_recvorigdstaddr();
408 }
409 b.finish()
410 }
411}