Skip to content

Commit e5b30f5

Browse files
ogyrec-omxinden
authored andcommitted
quinn-udp: make ECN best-effort on Windows (Wine/Proton)
Co-authored-by: Max Inden <mail@max-inden.de>
1 parent 803c814 commit e5b30f5

1 file changed

Lines changed: 71 additions & 13 deletions

File tree

quinn-udp/src/windows.rs

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ use crate::{
2424
#[derive(Debug)]
2525
pub struct UdpSocketState {
2626
last_send_error: Mutex<Instant>,
27+
28+
/// Whether the underlying Winsock provider supports IPv4 ECN socket options/control messages.
29+
///
30+
/// Some environments (notably Wine/Proton) don't implement IP_RECVECN/IP_ECN.
31+
/// ECN is best-effort: when unsupported we continue without it.
32+
ecn_v4_supported: bool,
33+
34+
/// Whether the underlying Winsock provider supports IPv6 ECN socket options/control messages.
35+
ecn_v6_supported: bool,
2736
}
2837

2938
impl UdpSocketState {
@@ -67,6 +76,20 @@ impl UdpSocketState {
6776
));
6877
}
6978

79+
// ECN is best-effort on Windows: if the Winsock provider doesn't support these options
80+
// (common under Wine/Proton), we disable ECN and keep working.
81+
let is_ecn_unsupported = |e: &io::Error| {
82+
matches!(
83+
e.raw_os_error(),
84+
Some(code)
85+
if code == WinSock::WSAENOPROTOOPT as i32
86+
|| code == WinSock::WSAEOPNOTSUPP as i32
87+
)
88+
};
89+
90+
let mut ecn_v4_supported = true;
91+
let mut ecn_v6_supported = true;
92+
7093
if is_ipv4 {
7194
set_socket_option(
7295
&*socket.0,
@@ -81,12 +104,20 @@ impl UdpSocketState {
81104
WinSock::IP_PKTINFO,
82105
OPTION_ON,
83106
)?;
84-
set_socket_option(
107+
108+
if let Err(e) = set_socket_option(
85109
&*socket.0,
86110
WinSock::IPPROTO_IP,
87111
WinSock::IP_RECVECN,
88112
OPTION_ON,
89-
)?;
113+
) {
114+
if is_ecn_unsupported(&e) {
115+
ecn_v4_supported = false;
116+
debug!("quinn-udp: ECN disabled for IPv4 (IP_RECVECN unsupported): {e}");
117+
} else {
118+
return Err(e);
119+
}
120+
}
90121
}
91122

92123
if is_ipv6 {
@@ -104,17 +135,26 @@ impl UdpSocketState {
104135
OPTION_ON,
105136
)?;
106137

107-
set_socket_option(
138+
if let Err(e) = set_socket_option(
108139
&*socket.0,
109140
WinSock::IPPROTO_IPV6,
110141
WinSock::IPV6_RECVECN,
111142
OPTION_ON,
112-
)?;
143+
) {
144+
if is_ecn_unsupported(&e) {
145+
ecn_v6_supported = false;
146+
debug!("quinn-udp: ECN disabled for IPv6 (IPV6_RECVECN unsupported): {e}");
147+
} else {
148+
return Err(e);
149+
}
150+
}
113151
}
114152

115153
let now = Instant::now();
116154
Ok(Self {
117155
last_send_error: Mutex::new(now.checked_sub(2 * IO_ERROR_LOG_INTERVAL).unwrap_or(now)),
156+
ecn_v4_supported,
157+
ecn_v6_supported,
118158
})
119159
}
120160

@@ -152,7 +192,12 @@ impl UdpSocketState {
152192
/// If you would like to handle these errors yourself, use [`UdpSocketState::try_send`]
153193
/// instead.
154194
pub fn send(&self, socket: UdpSockRef<'_>, transmit: &Transmit<'_>) -> io::Result<()> {
155-
match send(socket, transmit) {
195+
match send(
196+
socket,
197+
transmit,
198+
self.ecn_v4_supported,
199+
self.ecn_v6_supported,
200+
) {
156201
Ok(()) => Ok(()),
157202
Err(e) if e.kind() == io::ErrorKind::WouldBlock => Err(e),
158203
Err(e) => {
@@ -165,7 +210,12 @@ impl UdpSocketState {
165210

166211
/// Sends a [`Transmit`] on the given socket without any additional error handling.
167212
pub fn try_send(&self, socket: UdpSockRef<'_>, transmit: &Transmit<'_>) -> io::Result<()> {
168-
send(socket, transmit)
213+
send(
214+
socket,
215+
transmit,
216+
self.ecn_v4_supported,
217+
self.ecn_v6_supported,
218+
)
169219
}
170220

171221
pub fn recv(
@@ -325,7 +375,12 @@ impl UdpSocketState {
325375
}
326376
}
327377

328-
fn send(socket: UdpSockRef<'_>, transmit: &Transmit<'_>) -> io::Result<()> {
378+
fn send(
379+
socket: UdpSockRef<'_>,
380+
transmit: &Transmit<'_>,
381+
ecn_v4_supported: bool,
382+
ecn_v6_supported: bool,
383+
) -> io::Result<()> {
329384
// we cannot use [`socket2::sendmsg()`] and [`socket2::MsgHdr`] as we do not have access
330385
// to the inner field which holds the WSAMSG
331386
let mut ctrl_buf = cmsg::Aligned([0; CMSG_LEN]);
@@ -379,15 +434,18 @@ fn send(socket: UdpSockRef<'_>, transmit: &Transmit<'_>) -> io::Result<()> {
379434
}
380435
}
381436

382-
// ECN is a C integer https://learn.microsoft.com/en-us/windows/win32/winsock/winsock-ecn
383-
let ecn = transmit.ecn.map_or(0, |x| x as c_int);
384437
// True for IPv4 or IPv4-Mapped IPv6
385438
let is_ipv4 = transmit.destination.is_ipv4()
386439
|| matches!(transmit.destination.ip(), IpAddr::V6(addr) if addr.to_ipv4_mapped().is_some());
387-
if is_ipv4 {
388-
encoder.push(WinSock::IPPROTO_IP, WinSock::IP_ECN, ecn);
389-
} else {
390-
encoder.push(WinSock::IPPROTO_IPV6, WinSock::IPV6_ECN, ecn);
440+
441+
if (is_ipv4 && ecn_v4_supported) || (!is_ipv4 && ecn_v6_supported) {
442+
// ECN is a C integer https://learn.microsoft.com/en-us/windows/win32/winsock/winsock-ecn
443+
let ecn = transmit.ecn.map_or(0, |x| x as c_int);
444+
if is_ipv4 {
445+
encoder.push(WinSock::IPPROTO_IP, WinSock::IP_ECN, ecn);
446+
} else {
447+
encoder.push(WinSock::IPPROTO_IPV6, WinSock::IPV6_ECN, ecn);
448+
}
391449
}
392450

393451
// Segment size is a u32 https://learn.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-wsasetudpsendmessagesize

0 commit comments

Comments
 (0)