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 `util::pick_addr`).
9//! See <https://github.com/moq-dev/moq/issues/1375>.
10
11use socket2::{Domain, Protocol, Socket, Type};
12use std::net::{SocketAddr, TcpListener, UdpSocket};
13
14/// Bind a UDP socket, making an IPv6 socket dual-stack so it also serves IPv4.
15pub fn udp(addr: SocketAddr) -> std::io::Result<UdpSocket> {
16	let domain = if addr.is_ipv4() { Domain::IPV4 } else { Domain::IPV6 };
17	let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
18	make_dual_stack(&socket, addr);
19	socket.bind(&addr.into())?;
20	Ok(socket.into())
21}
22
23/// Bind a TCP listener, making an IPv6 socket dual-stack so it also serves IPv4.
24///
25/// The returned listener is non-blocking, ready for
26/// [`axum_server::from_tcp`](https://docs.rs/axum-server).
27pub fn tcp(addr: SocketAddr) -> std::io::Result<TcpListener> {
28	let domain = if addr.is_ipv4() { Domain::IPV4 } else { Domain::IPV6 };
29	let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?;
30	make_dual_stack(&socket, addr);
31	// Match std's TcpListener, which sets SO_REUSEADDR on Unix (not Windows) so a
32	// restarted relay can rebind a port still in TIME_WAIT.
33	#[cfg(not(windows))]
34	socket.set_reuse_address(true)?;
35	socket.bind(&addr.into())?;
36	socket.listen(1024)?;
37	let listener: TcpListener = socket.into();
38	listener.set_nonblocking(true)?;
39	Ok(listener)
40}
41
42/// Clear `IPV6_V6ONLY` so an IPv6 socket also accepts IPv4. Best-effort: a
43/// platform that rejects the option keeps its default rather than failing the
44/// bind. No-op for IPv4 sockets.
45fn make_dual_stack(socket: &Socket, addr: SocketAddr) {
46	if addr.is_ipv6()
47		&& let Err(err) = socket.set_only_v6(false)
48	{
49		tracing::warn!(%err, "failed to enable dual-stack IPv6 socket; IPv4 clients may be unreachable");
50	}
51}
52
53#[cfg(test)]
54mod tests {
55	use super::*;
56
57	/// Skip a test when the host has no IPv6 stack (some CI sandboxes and
58	/// containers). Creating or binding an IPv6 socket then fails with an
59	/// address-family error, which is an environment limitation rather than a
60	/// bug in the dual-stack logic. The dual-stack assertion only has meaning
61	/// once a socket exists, so there's nothing to verify when IPv6 is absent.
62	fn skip_if_no_ipv6(err: &std::io::Error) -> bool {
63		// EAFNOSUPPORT / EADDRNOTAVAIL / EPROTONOSUPPORT on Unix, and the WSA*
64		// equivalents on Windows. The matching ErrorKinds round out the rest.
65		const NO_IPV6_ERRNOS: &[i32] = &[97, 99, 93, 10047, 10049, 10043];
66		let no_ipv6 = matches!(
67			err.kind(),
68			std::io::ErrorKind::AddrNotAvailable | std::io::ErrorKind::Unsupported
69		) || err.raw_os_error().is_some_and(|code| NO_IPV6_ERRNOS.contains(&code));
70		if no_ipv6 {
71			eprintln!("skipping: host has no IPv6 support ({err})");
72		}
73		no_ipv6
74	}
75
76	#[test]
77	fn udp_ipv6_is_dual_stack() {
78		// An IPv6 wildcard bind should come back dual-stack so IPv4 traffic
79		// reaches it. socket2 lets us read the option back to confirm.
80		let socket = match udp("[::]:0".parse().unwrap()) {
81			Ok(socket) => socket,
82			Err(err) if skip_if_no_ipv6(&err) => return,
83			Err(err) => panic!("failed to bind IPv6 UDP socket: {err}"),
84		};
85		let socket = Socket::from(socket);
86		assert!(!socket.only_v6().unwrap(), "IPv6 socket should be dual-stack");
87	}
88
89	#[test]
90	fn udp_ipv4_still_binds() {
91		let socket = udp("127.0.0.1:0".parse().unwrap()).unwrap();
92		assert!(socket.local_addr().unwrap().is_ipv4());
93	}
94
95	#[test]
96	fn tcp_ipv6_is_dual_stack() {
97		let listener = match tcp("[::]:0".parse().unwrap()) {
98			Ok(listener) => listener,
99			Err(err) if skip_if_no_ipv6(&err) => return,
100			Err(err) => panic!("failed to bind IPv6 TCP listener: {err}"),
101		};
102		let socket = Socket::from(listener);
103		assert!(!socket.only_v6().unwrap(), "IPv6 listener should be dual-stack");
104	}
105}