Skip to main content

moq_native/
bind.rs

1//! Dual-stack socket binding.
2//!
3//! Quinn uses a single socket and relies on the OS to route both address
4//! families. On Linux an `[::]` socket accepts IPv4 too, but Windows defaults
5//! `IPV6_V6ONLY` to on, so an IPv6 socket silently drops every IPv4 packet. The
6//! helpers here clear that before binding, so a relay on `[::]` is reachable
7//! over IPv4 and a dual-stack client can dial IPv4 servers (via IPv4-mapped
8//! addresses; the client's address-family matching lives in
9//! `resolve::Candidates::with_local`).
10//! See <https://github.com/moq-dev/moq/issues/1375>.
11
12use socket2::{Domain, Protocol, Socket, TcpKeepalive, Type};
13use std::net::{SocketAddr, TcpListener, UdpSocket};
14use std::sync::Once;
15use std::time::Duration;
16
17/// TCP keepalive idle period before the kernel starts probing a silent peer, and
18/// the interval between probes. A long-lived connection (a parked WebSocket, an
19/// idle HTTP/2 session) can otherwise sit in a `read` forever, so a peer that
20/// vanished without a FIN/RST (a yanked cable, a crashed NAT) would pin its
21/// socket and any resources behind it. Keepalive lets the kernel surface the dead
22/// peer as a read error and tear the connection down. The values are generous
23/// enough not to disturb a healthy but momentarily quiet connection.
24const KEEPALIVE_IDLE: Duration = Duration::from_secs(30);
25const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(10);
26
27/// UDP socket buffer size requested in each direction, in bytes.
28///
29/// A QUIC stack absorbs bursts in the kernel socket buffer: whatever doesn't fit
30/// while the process is off the CPU is dropped before quinn ever sees it, and
31/// congestion control reads those drops as congestion. The OS defaults are sized
32/// for a chatty TCP-era socket (208 KiB on Linux), which a single relay socket
33/// carrying every connection blows through in milliseconds. 8 MiB is roughly 64ms
34/// of a saturated 1Gbps link, generous next to a scheduler delay and cheap next to
35/// what a relay already spends per connection. quic-go asks for 7 MiB.
36///
37/// Compiled in rather than derived from the NIC: the buffer is a ceiling on queued
38/// bytes rather than an allocation, so an oversized request costs an idle socket
39/// nothing, while a link's nominal speed says little about the path it feeds (a
40/// VM's virtio NIC reports 10Gbps through a 100Mbps uplink).
41const UDP_BUFFER: usize = 8 * 1024 * 1024;
42
43/// The sysctl capping `SO_RCVBUF`, named in the warning so an operator knows what
44/// to raise. `None` on platforms that size socket buffers per socket only.
45#[cfg(any(target_os = "linux", target_os = "android"))]
46const RECV_SYSCTL: Option<&str> = Some("net.core.rmem_max");
47#[cfg(any(
48	target_vendor = "apple",
49	target_os = "freebsd",
50	target_os = "netbsd",
51	target_os = "openbsd"
52))]
53const RECV_SYSCTL: Option<&str> = Some("kern.ipc.maxsockbuf");
54#[cfg(not(any(
55	target_os = "linux",
56	target_os = "android",
57	target_vendor = "apple",
58	target_os = "freebsd",
59	target_os = "netbsd",
60	target_os = "openbsd"
61)))]
62const RECV_SYSCTL: Option<&str> = None;
63
64/// The sysctl capping `SO_SNDBUF`. See [`RECV_SYSCTL`]; the BSDs cap both
65/// directions with the same knob.
66#[cfg(any(target_os = "linux", target_os = "android"))]
67const SEND_SYSCTL: Option<&str> = Some("net.core.wmem_max");
68#[cfg(not(any(target_os = "linux", target_os = "android")))]
69const SEND_SYSCTL: Option<&str> = RECV_SYSCTL;
70
71/// Bind a UDP socket, making an IPv6 socket dual-stack so it also serves IPv4.
72///
73/// The socket buffers are grown to 8 MiB where the OS allows it, and a warning
74/// names the sysctl to raise where it doesn't.
75pub fn udp(addr: SocketAddr) -> std::io::Result<UdpSocket> {
76	let domain = if addr.is_ipv4() { Domain::IPV4 } else { Domain::IPV6 };
77	let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
78	make_dual_stack(&socket, addr);
79	grow_buffers(&socket);
80	socket.bind(&addr.into())?;
81	Ok(socket.into())
82}
83
84/// Request [`UDP_BUFFER`] in both directions, best-effort.
85fn grow_buffers(socket: &Socket) {
86	for direction in [Direction::Recv, Direction::Send] {
87		direction.grow(socket);
88	}
89}
90
91/// One direction of a socket's buffering, owning everything that differs between
92/// the two: the socket options, the sysctl to name, and the warning.
93#[derive(Clone, Copy)]
94enum Direction {
95	Recv,
96	Send,
97}
98
99impl Direction {
100	/// Raise this direction's buffer to [`UDP_BUFFER`], then read back what the
101	/// kernel actually granted and warn once when it fell short.
102	///
103	/// Reading back is the whole point: Linux silently clamps the request to its
104	/// sysctl, so `setsockopt` returning `Ok` says nothing about the size we ended
105	/// up with, and `SO_RCVBUFFORCE` (the clamp-free version) needs `CAP_NET_ADMIN`
106	/// that a relay shouldn't be asking for.
107	fn grow(self, socket: &Socket) {
108		// Never shrink a system that's already tuned above our default.
109		if self.size(socket).is_ok_and(sufficient) {
110			return;
111		}
112
113		match self.set_size(socket, UDP_BUFFER).and_then(|()| self.size(socket)) {
114			Ok(reported) if sufficient(reported) => {}
115			Ok(reported) => self.warn_short(granted(reported)),
116			Err(err) => self.warn_failed(&err),
117		}
118	}
119
120	fn size(self, socket: &Socket) -> std::io::Result<usize> {
121		match self {
122			Self::Recv => socket.recv_buffer_size(),
123			Self::Send => socket.send_buffer_size(),
124		}
125	}
126
127	fn set_size(self, socket: &Socket, size: usize) -> std::io::Result<()> {
128		match self {
129			Self::Recv => socket.set_recv_buffer_size(size),
130			Self::Send => socket.set_send_buffer_size(size),
131		}
132	}
133
134	fn name(self) -> &'static str {
135		match self {
136			Self::Recv => "receive",
137			Self::Send => "send",
138		}
139	}
140
141	fn sysctl(self) -> Option<&'static str> {
142		match self {
143			Self::Recv => RECV_SYSCTL,
144			Self::Send => SEND_SYSCTL,
145		}
146	}
147
148	/// One warning per direction per process: a client that reconnects rebinds, and
149	/// the operator only needs telling once.
150	fn warned(self) -> &'static Once {
151		static RECV: Once = Once::new();
152		static SEND: Once = Once::new();
153
154		match self {
155			Self::Recv => &RECV,
156			Self::Send => &SEND,
157		}
158	}
159
160	/// The kernel accepted the request and quietly handed back `granted` instead.
161	fn warn_short(self, granted: usize) {
162		self.warned().call_once(|| self.emit_short(granted));
163	}
164
165	/// The warning itself, minus the once-guard, so a test can read it back
166	/// whatever a previous bind on this host already consumed.
167	fn emit_short(self, granted: usize) {
168		let name = self.name();
169		match self.sysctl() {
170			Some(sysctl) => tracing::warn!(
171				wanted = UDP_BUFFER,
172				granted,
173				"UDP {name} buffer is smaller than requested; raise `{sysctl}` or expect packet loss under load"
174			),
175			None => tracing::warn!(
176				wanted = UDP_BUFFER,
177				granted,
178				"UDP {name} buffer is smaller than requested; expect packet loss under load"
179			),
180		}
181	}
182
183	/// The option itself was rejected, so we don't even know what we're running with.
184	fn warn_failed(self, err: &std::io::Error) {
185		let name = self.name();
186		self.warned()
187			.call_once(|| tracing::warn!(%err, "failed to set the UDP {name} buffer size"));
188	}
189}
190
191/// Whether a buffer size the kernel reported already covers [`UDP_BUFFER`].
192fn sufficient(reported: usize) -> bool {
193	granted(reported) >= UDP_BUFFER
194}
195
196/// The usable size behind a buffer size the kernel reported.
197///
198/// Linux reports back double what it granted, reserving the other half for
199/// per-packet bookkeeping, so halving keeps the numbers we compare and log in the
200/// same units an operator writes into the sysctl.
201fn granted(reported: usize) -> usize {
202	if cfg!(any(target_os = "linux", target_os = "android")) {
203		reported / 2
204	} else {
205		reported
206	}
207}
208
209/// Whether `socket` also reaches IPv4, through IPv4-mapped addresses.
210///
211/// [`udp`] clears `IPV6_V6ONLY` best-effort, so this reads back what the
212/// platform actually did rather than assuming it took. A socket that stayed
213/// v6-only can't send to a mapped destination, and it looks identical from the
214/// outside: `local_addr` reads `[::]` either way. Always false for an IPv4
215/// socket, which reaches IPv4 natively rather than through mapping.
216#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
217pub(crate) fn udp_is_dual_stack(socket: &UdpSocket) -> bool {
218	match socket.local_addr() {
219		Ok(addr) if addr.is_ipv6() => socket2::SockRef::from(socket).only_v6().is_ok_and(|only| !only),
220		_ => false,
221	}
222}
223
224/// Bind a TCP listener, making an IPv6 socket dual-stack so it also serves IPv4.
225///
226/// The returned listener is non-blocking, ready to be adopted by an async runtime
227/// (`tokio::net::TcpListener::from_std`, `axum_server::from_tcp`).
228pub fn tcp(addr: SocketAddr) -> std::io::Result<TcpListener> {
229	let domain = if addr.is_ipv4() { Domain::IPV4 } else { Domain::IPV6 };
230	let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?;
231	make_dual_stack(&socket, addr);
232	// Match std's TcpListener, which sets SO_REUSEADDR on Unix (not Windows) so a
233	// restarted relay can rebind a port still in TIME_WAIT.
234	#[cfg(not(windows))]
235	socket.set_reuse_address(true)?;
236	// Enable keepalive on the listening socket so every accepted connection
237	// inherits it (accept() carries socket options across on Linux, macOS, and
238	// Windows). Setting it once here reaches every HTTP/HTTPS/WebSocket connection
239	// without the serve loop touching each one. Best-effort: a platform that rejects
240	// the option keeps the connection rather than failing.
241	let keepalive = TcpKeepalive::new()
242		.with_time(KEEPALIVE_IDLE)
243		.with_interval(KEEPALIVE_INTERVAL);
244	if let Err(err) = socket.set_tcp_keepalive(&keepalive) {
245		tracing::warn!(%err, "failed to enable TCP keepalive; dead peers may linger");
246	}
247	socket.bind(&addr.into())?;
248	socket.listen(1024)?;
249	let listener: TcpListener = socket.into();
250	listener.set_nonblocking(true)?;
251	Ok(listener)
252}
253
254/// Clear `IPV6_V6ONLY` so an IPv6 socket also accepts IPv4. Best-effort: a
255/// platform that rejects the option keeps its default rather than failing the
256/// bind. No-op for IPv4 sockets.
257fn make_dual_stack(socket: &Socket, addr: SocketAddr) {
258	if addr.is_ipv6()
259		&& let Err(err) = socket.set_only_v6(false)
260	{
261		tracing::warn!(%err, "failed to enable dual-stack IPv6 socket; IPv4 clients may be unreachable");
262	}
263}
264
265#[cfg(test)]
266mod tests {
267	use super::*;
268
269	/// Skip a test when the host has no IPv6 stack (some CI sandboxes and
270	/// containers). Creating or binding an IPv6 socket then fails with an
271	/// address-family error, which is an environment limitation rather than a
272	/// bug in the dual-stack logic. The dual-stack assertion only has meaning
273	/// once a socket exists, so there's nothing to verify when IPv6 is absent.
274	fn skip_if_no_ipv6(err: &std::io::Error) -> bool {
275		// EAFNOSUPPORT / EADDRNOTAVAIL / EPROTONOSUPPORT on Unix, and the WSA*
276		// equivalents on Windows. The matching ErrorKinds round out the rest.
277		const NO_IPV6_ERRNOS: &[i32] = &[97, 99, 93, 10047, 10049, 10043];
278		let no_ipv6 = matches!(
279			err.kind(),
280			std::io::ErrorKind::AddrNotAvailable | std::io::ErrorKind::Unsupported
281		) || err.raw_os_error().is_some_and(|code| NO_IPV6_ERRNOS.contains(&code));
282		if no_ipv6 {
283			eprintln!("skipping: host has no IPv6 support ({err})");
284		}
285		no_ipv6
286	}
287
288	#[test]
289	fn udp_ipv6_is_dual_stack() {
290		// An IPv6 wildcard bind should come back dual-stack so IPv4 traffic
291		// reaches it. socket2 lets us read the option back to confirm.
292		let socket = match udp("[::]:0".parse().unwrap()) {
293			Ok(socket) => socket,
294			Err(err) if skip_if_no_ipv6(&err) => return,
295			Err(err) => panic!("failed to bind IPv6 UDP socket: {err}"),
296		};
297		let socket = Socket::from(socket);
298		assert!(!socket.only_v6().unwrap(), "IPv6 socket should be dual-stack");
299	}
300
301	#[test]
302	fn udp_buffers_grow() {
303		fn check(direction: Direction) {
304			let plain = Socket::from(std::net::UdpSocket::bind("127.0.0.1:0").unwrap());
305			let before = direction.size(&plain).unwrap();
306
307			let tuned = Socket::from(udp("127.0.0.1:0".parse().unwrap()).unwrap());
308			let after = direction.size(&tuned).unwrap();
309
310			// A host whose default already covers UDP_BUFFER is left alone. Anywhere
311			// else the bind has to have actually raised it, whatever the sysctls
312			// clamped it to.
313			if sufficient(before) {
314				assert_eq!(after, before, "{} buffer should be left alone", direction.name());
315			} else {
316				assert!(after > before, "{} buffer should grow past {before}", direction.name());
317			}
318		}
319
320		check(Direction::Recv);
321		check(Direction::Send);
322	}
323
324	#[test]
325	fn sufficient_accounts_for_the_doubled_report() {
326		// Doubled, since that's how Linux reports back a buffer it granted.
327		assert!(sufficient(UDP_BUFFER * 2));
328		assert!(!sufficient(512 * 1024));
329	}
330
331	#[tracing_test::traced_test]
332	#[test]
333	fn a_clamped_buffer_warns_and_names_the_sysctl() {
334		Direction::Recv.emit_short(512 * 1024);
335
336		assert!(logs_contain("UDP receive buffer is smaller than requested"));
337		if let Some(sysctl) = Direction::Recv.sysctl() {
338			assert!(logs_contain(sysctl));
339		}
340	}
341
342	#[test]
343	fn udp_ipv4_still_binds() {
344		let socket = udp("127.0.0.1:0".parse().unwrap()).unwrap();
345		assert!(socket.local_addr().unwrap().is_ipv4());
346	}
347
348	#[test]
349	fn tcp_ipv6_is_dual_stack() {
350		let listener = match tcp("[::]:0".parse().unwrap()) {
351			Ok(listener) => listener,
352			Err(err) if skip_if_no_ipv6(&err) => return,
353			Err(err) => panic!("failed to bind IPv6 TCP listener: {err}"),
354		};
355		let socket = Socket::from(listener);
356		assert!(!socket.only_v6().unwrap(), "IPv6 listener should be dual-stack");
357	}
358}