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::time::Duration;
15
16/// TCP keepalive idle period before the kernel starts probing a silent peer, and
17/// the interval between probes. A long-lived connection (a parked WebSocket, an
18/// idle HTTP/2 session) can otherwise sit in a `read` forever, so a peer that
19/// vanished without a FIN/RST (a yanked cable, a crashed NAT) would pin its
20/// socket and any resources behind it. Keepalive lets the kernel surface the dead
21/// peer as a read error and tear the connection down. The values are generous
22/// enough not to disturb a healthy but momentarily quiet connection.
23const KEEPALIVE_IDLE: Duration = Duration::from_secs(30);
24const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(10);
25
26/// Bind a UDP socket, making an IPv6 socket dual-stack so it also serves IPv4.
27pub fn udp(addr: SocketAddr) -> std::io::Result<UdpSocket> {
28 let domain = if addr.is_ipv4() { Domain::IPV4 } else { Domain::IPV6 };
29 let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
30 make_dual_stack(&socket, addr);
31 socket.bind(&addr.into())?;
32 Ok(socket.into())
33}
34
35/// Whether `socket` also reaches IPv4, through IPv4-mapped addresses.
36///
37/// [`udp`] clears `IPV6_V6ONLY` best-effort, so this reads back what the
38/// platform actually did rather than assuming it took. A socket that stayed
39/// v6-only can't send to a mapped destination, and it looks identical from the
40/// outside: `local_addr` reads `[::]` either way. Always false for an IPv4
41/// socket, which reaches IPv4 natively rather than through mapping.
42#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
43pub(crate) fn udp_is_dual_stack(socket: &UdpSocket) -> bool {
44 match socket.local_addr() {
45 Ok(addr) if addr.is_ipv6() => socket2::SockRef::from(socket).only_v6().is_ok_and(|only| !only),
46 _ => false,
47 }
48}
49
50/// Bind a TCP listener, making an IPv6 socket dual-stack so it also serves IPv4.
51///
52/// The returned listener is non-blocking, ready to be adopted by an async runtime
53/// (`tokio::net::TcpListener::from_std`, `axum_server::from_tcp`).
54pub fn tcp(addr: SocketAddr) -> std::io::Result<TcpListener> {
55 let domain = if addr.is_ipv4() { Domain::IPV4 } else { Domain::IPV6 };
56 let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?;
57 make_dual_stack(&socket, addr);
58 // Match std's TcpListener, which sets SO_REUSEADDR on Unix (not Windows) so a
59 // restarted relay can rebind a port still in TIME_WAIT.
60 #[cfg(not(windows))]
61 socket.set_reuse_address(true)?;
62 // Enable keepalive on the listening socket so every accepted connection
63 // inherits it (accept() carries socket options across on Linux, macOS, and
64 // Windows). Setting it once here reaches every HTTP/HTTPS/WebSocket connection
65 // without the serve loop touching each one. Best-effort: a platform that rejects
66 // the option keeps the connection rather than failing.
67 let keepalive = TcpKeepalive::new()
68 .with_time(KEEPALIVE_IDLE)
69 .with_interval(KEEPALIVE_INTERVAL);
70 if let Err(err) = socket.set_tcp_keepalive(&keepalive) {
71 tracing::warn!(%err, "failed to enable TCP keepalive; dead peers may linger");
72 }
73 socket.bind(&addr.into())?;
74 socket.listen(1024)?;
75 let listener: TcpListener = socket.into();
76 listener.set_nonblocking(true)?;
77 Ok(listener)
78}
79
80/// Clear `IPV6_V6ONLY` so an IPv6 socket also accepts IPv4. Best-effort: a
81/// platform that rejects the option keeps its default rather than failing the
82/// bind. No-op for IPv4 sockets.
83fn make_dual_stack(socket: &Socket, addr: SocketAddr) {
84 if addr.is_ipv6()
85 && let Err(err) = socket.set_only_v6(false)
86 {
87 tracing::warn!(%err, "failed to enable dual-stack IPv6 socket; IPv4 clients may be unreachable");
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 /// Skip a test when the host has no IPv6 stack (some CI sandboxes and
96 /// containers). Creating or binding an IPv6 socket then fails with an
97 /// address-family error, which is an environment limitation rather than a
98 /// bug in the dual-stack logic. The dual-stack assertion only has meaning
99 /// once a socket exists, so there's nothing to verify when IPv6 is absent.
100 fn skip_if_no_ipv6(err: &std::io::Error) -> bool {
101 // EAFNOSUPPORT / EADDRNOTAVAIL / EPROTONOSUPPORT on Unix, and the WSA*
102 // equivalents on Windows. The matching ErrorKinds round out the rest.
103 const NO_IPV6_ERRNOS: &[i32] = &[97, 99, 93, 10047, 10049, 10043];
104 let no_ipv6 = matches!(
105 err.kind(),
106 std::io::ErrorKind::AddrNotAvailable | std::io::ErrorKind::Unsupported
107 ) || err.raw_os_error().is_some_and(|code| NO_IPV6_ERRNOS.contains(&code));
108 if no_ipv6 {
109 eprintln!("skipping: host has no IPv6 support ({err})");
110 }
111 no_ipv6
112 }
113
114 #[test]
115 fn udp_ipv6_is_dual_stack() {
116 // An IPv6 wildcard bind should come back dual-stack so IPv4 traffic
117 // reaches it. socket2 lets us read the option back to confirm.
118 let socket = match udp("[::]:0".parse().unwrap()) {
119 Ok(socket) => socket,
120 Err(err) if skip_if_no_ipv6(&err) => return,
121 Err(err) => panic!("failed to bind IPv6 UDP socket: {err}"),
122 };
123 let socket = Socket::from(socket);
124 assert!(!socket.only_v6().unwrap(), "IPv6 socket should be dual-stack");
125 }
126
127 #[test]
128 fn udp_ipv4_still_binds() {
129 let socket = udp("127.0.0.1:0".parse().unwrap()).unwrap();
130 assert!(socket.local_addr().unwrap().is_ipv4());
131 }
132
133 #[test]
134 fn tcp_ipv6_is_dual_stack() {
135 let listener = match tcp("[::]:0".parse().unwrap()) {
136 Ok(listener) => listener,
137 Err(err) if skip_if_no_ipv6(&err) => return,
138 Err(err) => panic!("failed to bind IPv6 TCP listener: {err}"),
139 };
140 let socket = Socket::from(listener);
141 assert!(!socket.only_v6().unwrap(), "IPv6 listener should be dual-stack");
142 }
143}