Skip to content

Commit e992c68

Browse files
committed
fix: Handle ENOBUFS in UDP send
Based on a discussion with @ianswett yesterday, don't treat `ENOBUFS` as a fatal error when sending UDP packets.
1 parent 07f7838 commit e992c68

1 file changed

Lines changed: 39 additions & 28 deletions

File tree

neqo-udp/src/lib.rs

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use std::{
2121
use log::{Level, log_enabled};
2222
use neqo_common::{Datagram, Tos, datagram, qdebug, qtrace};
2323
use quinn_udp::{EcnCodepoint, RecvMeta, Transmit, UdpSocketState};
24+
#[cfg(windows)]
25+
use windows::Win32::Networking::WinSock;
2426

2527
/// Receive buffer size
2628
///
@@ -81,6 +83,12 @@ pub fn send_inner(
8183
);
8284
return Ok(());
8385
}
86+
Err(e) if is_enobufs(&e) => {
87+
// On macOS/BSD, ENOBUFS means the NIC transmit queue is momentarily
88+
// full. The packet is already dropped by the kernel. Signal WouldBlock.
89+
qdebug!("Interface send queue full (ENOBUFS), signaling WouldBlock: {e}");
90+
return Err(io::Error::from(io::ErrorKind::WouldBlock));
91+
}
8492
e @ Err(_) => return e,
8593
}
8694

@@ -96,27 +104,27 @@ pub fn send_inner(
96104
Ok(())
97105
}
98106

99-
#[expect(
100-
clippy::unnecessary_map_or,
101-
reason = "Clippy ignores the #[cfg] attribute."
102-
)]
103107
fn is_emsgsize(e: &io::Error) -> bool {
104-
e.raw_os_error().map_or(false, |e| {
108+
e.raw_os_error().is_some_and(|e| {
105109
#[cfg(unix)]
106-
{
107-
e == libc::EMSGSIZE
108-
}
110+
return e == libc::EMSGSIZE;
109111
#[cfg(windows)]
110-
{
111-
e == windows::Win32::Networking::WinSock::WSAEMSGSIZE.0
112-
// WSAEINVAL is returned when the Windows USO (UDP Segmentation Offload)
113-
// segment size exceeds the supported limit.
114-
|| e == windows::Win32::Networking::WinSock::WSAEINVAL.0
115-
}
112+
// WSAEINVAL is returned when the Windows USO (UDP Segmentation Offload)
113+
// segment size exceeds the supported limit.
114+
return e == WinSock::WSAEMSGSIZE.0 || e == WinSock::WSAEINVAL.0;
116115
#[cfg(not(any(unix, windows)))]
117-
{
118-
false
119-
}
116+
return false;
117+
})
118+
}
119+
120+
fn is_enobufs(e: &io::Error) -> bool {
121+
e.raw_os_error().is_some_and(|e| {
122+
#[cfg(unix)]
123+
return e == libc::ENOBUFS;
124+
#[cfg(windows)]
125+
return e == WinSock::WSAENOBUFS.0;
126+
#[cfg(not(any(unix, windows)))]
127+
return false;
120128
})
121129
}
122130

@@ -350,16 +358,18 @@ mod tests {
350358

351359
#[test]
352360
#[cfg(unix)]
353-
fn is_emsgsize_true_for_emsgsize() {
354-
let err = io::Error::from_raw_os_error(libc::EMSGSIZE);
355-
assert!(is_emsgsize(&err));
356-
}
357-
358-
#[test]
359-
#[cfg(unix)]
360-
fn is_emsgsize_false_for_other_errors() {
361-
let err = io::Error::from_raw_os_error(libc::EAGAIN);
362-
assert!(!is_emsgsize(&err));
361+
fn is_emsgsize_and_is_enobufs_are_disjoint() {
362+
let emsgsize = io::Error::from_raw_os_error(libc::EMSGSIZE);
363+
assert!(is_emsgsize(&emsgsize));
364+
assert!(!is_enobufs(&emsgsize));
365+
366+
let enobufs = io::Error::from_raw_os_error(libc::ENOBUFS);
367+
assert!(!is_emsgsize(&enobufs));
368+
assert!(is_enobufs(&enobufs));
369+
370+
let eagain = io::Error::from_raw_os_error(libc::EAGAIN);
371+
assert!(!is_emsgsize(&eagain));
372+
assert!(!is_enobufs(&eagain));
363373
}
364374

365375
#[test]
@@ -385,9 +395,10 @@ mod tests {
385395
}
386396

387397
#[test]
388-
fn is_emsgsize_false_for_non_os_error() {
398+
fn is_emsgsize_and_is_enobufs_false_for_non_os_error() {
389399
let err = io::Error::other("test error");
390400
assert!(!is_emsgsize(&err));
401+
assert!(!is_enobufs(&err));
391402
}
392403

393404
#[test]

0 commit comments

Comments
 (0)