Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions quinn-udp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,23 @@ impl EcnCodepoint {
})
}
}

#[cfg(target_os = "linux")]
#[derive(Clone, Debug, Copy)]
pub struct IcmpError {
pub dst: SocketAddr,
pub kind: IcmpErrorKind,
}

#[cfg(not(target_os = "linux"))]
pub struct IcmpError;

#[cfg(target_os = "linux")]
#[derive(Clone, Debug, Copy, PartialEq, Eq)]
pub enum IcmpErrorKind {
NetworkUnreachable,
HostUnreachable,
PortUnreachable,
PacketTooBig,
Other { icmp_type: u8, icmp_code: u8 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: drop the icmp_ prefix, which is pretty obvious for a type named IcmpErrorKind.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since type is a reserved keyword in Rust, it can’t be used directly as a field name, We can use raw identifier instead r#type. Can i do that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems okay to me.

}
183 changes: 183 additions & 0 deletions quinn-udp/src/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ use std::{

use socket2::SockRef;

use crate::IcmpError;

#[cfg(target_os = "linux")]
use crate::IcmpErrorKind;

use super::{
EcnCodepoint, IO_ERROR_LOG_INTERVAL, RecvMeta, Transmit, UdpSockRef, cmsg, log_sendmsg_error,
};
Expand All @@ -33,6 +38,66 @@ pub(crate) struct msghdr_x {
pub msg_datalen: usize,
}

#[cfg(target_os = "linux")]
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub(crate) struct SockExtendedError {
pub errno: u32,
pub origin: u8,
pub r#type: u8,
pub code: u8,
pub pad: u8,
pub info: u32,
pub data: u32,
}

#[cfg(target_os = "linux")]
impl SockExtendedError {
fn kind(&self) -> IcmpErrorKind {
const ICMP_DEST_UNREACH: u8 = 3; // Type 3: Destination Unreachable
const ICMP_NET_UNREACH: u8 = 0;
const ICMP_HOST_UNREACH: u8 = 1;
const ICMP_PORT_UNREACH: u8 = 3;
const ICMP_FRAG_NEEDED: u8 = 4;

const ICMPV6_DEST_UNREACH: u8 = 1; // Type 1: Destination Unreachable for IPv6
const ICMPV6_NO_ROUTE: u8 = 0;
const ICMPV6_ADDR_UNREACH: u8 = 1;
const ICMPV6_PORT_UNREACH: u8 = 4;

const ICMPV6_PACKET_TOO_BIG: u8 = 2;

match (self.origin, self.r#type, self.code) {
(libc::SO_EE_ORIGIN_ICMP, ICMP_DEST_UNREACH, ICMP_NET_UNREACH) => {
IcmpErrorKind::NetworkUnreachable
}
(libc::SO_EE_ORIGIN_ICMP, ICMP_DEST_UNREACH, ICMP_HOST_UNREACH) => {
IcmpErrorKind::HostUnreachable
}
(libc::SO_EE_ORIGIN_ICMP, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH) => {
IcmpErrorKind::PortUnreachable
}
(libc::SO_EE_ORIGIN_ICMP, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED) => {
IcmpErrorKind::PacketTooBig
}
(libc::SO_EE_ORIGIN_ICMP6, ICMPV6_DEST_UNREACH, ICMPV6_NO_ROUTE) => {
IcmpErrorKind::NetworkUnreachable
}
(libc::SO_EE_ORIGIN_ICMP6, ICMPV6_DEST_UNREACH, ICMPV6_ADDR_UNREACH) => {
IcmpErrorKind::HostUnreachable
}
(libc::SO_EE_ORIGIN_ICMP6, ICMPV6_DEST_UNREACH, ICMPV6_PORT_UNREACH) => {
IcmpErrorKind::PortUnreachable
}
(libc::SO_EE_ORIGIN_ICMP6, ICMPV6_PACKET_TOO_BIG, _) => IcmpErrorKind::PacketTooBig,
_ => IcmpErrorKind::Other {
icmp_type: self.r#type,
icmp_code: self.code,
},
}
}
}

#[cfg(apple_fast)]
extern "C" {
fn recvmsg_x(
Expand Down Expand Up @@ -122,6 +187,20 @@ impl UdpSocketState {
}
}

// Enable IP_RECVERR and IPV6_RECVERR for ICMP Errors
#[cfg(target_os = "linux")]
if is_ipv4 {
if let Err(_err) =
set_socket_option(&*io, libc::IPPROTO_IP, libc::IP_RECVERR, OPTION_ON)
{
crate::log::debug!("Failed to enable IP_RECVERR: {}", _err);
}
} else if let Err(_err) =
set_socket_option(&*io, libc::IPPROTO_IPV6, libc::IPV6_RECVERR, OPTION_ON)
{
crate::log::debug!("Failed to enable IPV6_RECVERR: {}", _err);
}

let mut may_fragment = false;
#[cfg(any(target_os = "linux", target_os = "android"))]
{
Expand Down Expand Up @@ -233,6 +312,16 @@ impl UdpSocketState {
recv(socket.0, bufs, meta)
}

#[cfg(target_os = "linux")]
pub fn recv_icmp_err(&self, socket: UdpSockRef<'_>) -> io::Result<Option<IcmpError>> {
recv_err(socket.0)
}

#[cfg(not(target_os = "linux"))]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should aim for a unified API across all platforms, even if they currently don't return a value. Otherwise code that uses this also needs to use a cfg to compile on multiple platforms.

pub fn recv_icmp_err(&self, socket: UdpSockRef<'_>) -> io::Result<Option<IcmpError>> {
recv_err(socket.0)
}

/// The maximum amount of segments which can be transmitted if a platform
/// supports Generic Send Offload (GSO).
///
Expand Down Expand Up @@ -500,6 +589,7 @@ fn recv(io: SockRef<'_>, bufs: &mut [IoSliceMut<'_>], meta: &mut [RecvMeta]) ->
for i in 0..(msg_count as usize) {
meta[i] = decode_recv(&names[i], &hdrs[i].msg_hdr, hdrs[i].msg_len as usize)?;
}

Ok(msg_count as usize)
}

Expand Down Expand Up @@ -795,6 +885,99 @@ fn decode_recv(
})
}

#[cfg(target_os = "linux")]
fn recv_err(io: SockRef<'_>) -> io::Result<Option<IcmpError>> {
use std::mem;

let fd = io.as_raw_fd();

let mut control = cmsg::Aligned([0u8; CMSG_LEN]);

// We don't need actual data, just the error info
Copy link
Contributor

@thomaseizinger thomaseizinger Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! I understand now. So to receive error info, we pass a flag to recvmsg but otherwise it is the same syscall. An alternative design would be to do this as part of the other recv flow and store the received ICMP messages in a (bounded) queue. That would be more efficient in terms of syscall usage, especially because most of the time, this syscall here is not going to return anything. Yet, an application wanting to handle this still needs to call this in a loop with the other recv, taking up CPU usage of the recv hot path.

Curious to hear what other people think about this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your suggestion makes sense to me.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. In case someone here knows, what is the intended use, or asked differently, how do other applications use it?

let mut iovec = libc::iovec {
iov_base: std::ptr::null_mut(),
iov_len: 0,
};

let mut addr_storage: libc::sockaddr_storage = unsafe { mem::zeroed() };

// Have followed the previous declarations
let mut hdr: libc::msghdr = unsafe { mem::zeroed() };
hdr.msg_name = &mut addr_storage as *mut _ as *mut _;
hdr.msg_namelen = mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
hdr.msg_iov = &mut iovec;
hdr.msg_iovlen = 1;
hdr.msg_control = control.0.as_mut_ptr() as *mut _;
hdr.msg_controllen = CMSG_LEN as _;

let ret = unsafe { libc::recvmsg(fd, &mut hdr, libc::MSG_ERRQUEUE) };

if ret < 0 {
let err = io::Error::last_os_error();
// EAGAIN/EWOULDBLOCK means no error in queue - this is normal
if err.kind() == io::ErrorKind::WouldBlock {
return Ok(None);
}
return Err(err);
}

let cmsg_iter = unsafe { cmsg::Iter::new(&hdr) };

for cmsg in cmsg_iter {
const IP_RECVERR: libc::c_int = 11;
const IPV6_RECVERR: libc::c_int = 25;

let is_ipv4_err = cmsg.cmsg_level == libc::IPPROTO_IP && cmsg.cmsg_type == IP_RECVERR;
let is_ipv6_err = cmsg.cmsg_level == libc::IPPROTO_IPV6 && cmsg.cmsg_type == IPV6_RECVERR;

if !is_ipv4_err && !is_ipv6_err {
continue;
}

let err_data = unsafe { cmsg::decode::<SockExtendedError, libc::cmsghdr>(cmsg) };

let dst = unsafe {
let addr_ptr = &addr_storage as *const _ as *const libc::sockaddr;
match (*addr_ptr).sa_family as i32 {
libc::AF_INET => {
let addr_in = &*(addr_ptr as *const libc::sockaddr_in);
SocketAddr::V4(std::net::SocketAddrV4::new(
std::net::Ipv4Addr::from(u32::from_be(addr_in.sin_addr.s_addr)),
u16::from_be(addr_in.sin_port),
))
}
libc::AF_INET6 => {
let addr_in6 = &*(addr_ptr as *const libc::sockaddr_in6);
SocketAddr::V6(std::net::SocketAddrV6::new(
std::net::Ipv6Addr::from(addr_in6.sin6_addr.s6_addr),
u16::from_be(addr_in6.sin6_port),
addr_in6.sin6_flowinfo,
addr_in6.sin6_scope_id,
))
}
_ => {
crate::log::warn!(
"Ignoring ICMP error with unknown address family: {}",
addr_storage.ss_family
);
continue;
}
}
};

return Ok(Some(IcmpError {
dst,
kind: err_data.kind(),
}));
}
Ok(None)
}

#[cfg(not(target_os = "linux"))]
fn recv_err(_io: SockRef<'_>) -> io::Result<Option<IcmpError>> {
Ok(None)
}

#[cfg(not(apple_slow))]
// Chosen somewhat arbitrarily; might benefit from additional tuning.
pub(crate) const BATCH_SIZE: usize = 32;
Expand Down
85 changes: 85 additions & 0 deletions quinn-udp/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,88 @@ fn ip_to_v6_mapped(x: IpAddr) -> IpAddr {
IpAddr::V6(_) => x,
}
}

#[cfg(target_os = "linux")]
#[test]
fn test_ipv4_recverr() {
use std::time::Duration;

let socket = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();

// Create UdpSocketState (this should enable IP_RECVERR)
let state = UdpSocketState::new((&socket).into()).expect("failed to create UdpSocketState");

// Send to an unreachable address in the documentation range (192.0.2.0/24)
let unreachable_addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 0, 2, 1), 12345));

let transmit = Transmit {
destination: unreachable_addr,
ecn: None,
contents: b"test packet to unreachable destination",
segment_size: None,
src_ip: None,
};

// Send the packet
let state_result = state.try_send((&socket).into(), &transmit);
assert!(state_result.is_err(), "Expected to fail to transmit");

std::thread::sleep(Duration::from_millis(200));

match state.recv_icmp_err((&socket).into()) {
Ok(Some(icmp_err)) => {
eprintln!("icmp packet recieved");
assert_eq!(unreachable_addr.ip(), icmp_err.dst.ip());
}
Ok(None) => {
eprintln!("No ICMP Recieved");
}
Err(e) => {
eprintln!("Error in reciveing icmp packet: {}", e);
}
}
}

#[cfg(target_os = "linux")]
#[test]
fn test_ipv6_recverr() {
use std::time::Duration;

let socket = UdpSocket::bind((Ipv6Addr::LOCALHOST, 0)).unwrap();

let state = UdpSocketState::new((&socket).into()).expect("failed to create UdpSocketState");

// Send to unreachable IPv6 address
let unreachable_addr = SocketAddr::V6(SocketAddrV6::new(
Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1),
12345,
0,
0,
));

let transmit = Transmit {
destination: unreachable_addr,
ecn: None,
contents: b"test IPv6 packet",
segment_size: None,
src_ip: None,
};

let state_res = state.try_send((&socket).into(), &transmit);
assert!(state_res.is_err(), "Expected to fail to transmit");

std::thread::sleep(Duration::from_millis(200));

match state.recv_icmp_err((&socket).into()) {
Ok(Some(icmp_err)) => {
eprintln!("Recived ICMPV6 Packets");
assert_eq!(unreachable_addr.ip(), icmp_err.dst.ip());
}
Ok(None) => {
eprintln!("No ICMPV6 packets are recieved")
}
Err(e) => {
eprintln!("Error in sending ICMP packets: {}", e);
}
}
}
Loading