Skip to content

Commit

Permalink
Add support for IP_RECVERR and IPV6_RECVERR
Browse files Browse the repository at this point in the history
Setting these options enables receiving errors, such as ICMP errors from
the network, via `recvmsg()` with `MSG_ERRQUEUE`.

Adds new `Ipv{4,6}RecvErr` variants to `ControlMessageOwned`.  These
control messages are produced when `Ipv4RecvErr` or `Ipv6RecvErr`
options are enabled on a raw or datagram socket.

New tests for the functionality can be run with `cargo test --test test
test_recverr`.

This commit builds on an earlier draft of the functionality authored by
Matthew McPherrin <git@mcpherrin.ca>.
  • Loading branch information
cemeyer committed Sep 6, 2021
1 parent aaec214 commit d23e7c7
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ This project adheres to [Semantic Versioning](https://semver.org/).
(#[1503](https://github.com/nix-rust/nix/pull/1503))
- Enabled `pwritev` and `preadv` for more operating systems.
(#[1511](https://github.com/nix-rust/nix/pull/1511))
- Added `Ipv4RecvErr` and `Ipv6RecvErr` sockopts and associated control messages.
(#[1514](https://github.com/nix-rust/nix/pull/1514))

### Changed

Expand Down
35 changes: 35 additions & 0 deletions src/sys/socket/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,13 @@ pub enum ControlMessageOwned {
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
RxqOvfl(u32),

/// Socket error queue control messages read with the `MSG_ERRQUEUE` flag.
#[cfg(any(target_os = "android", target_os = "linux"))]
Ipv4RecvErr(libc::sock_extended_err, Option<sockaddr_in>),
/// Socket error queue control messages read with the `MSG_ERRQUEUE` flag.
#[cfg(any(target_os = "android", target_os = "linux"))]
Ipv6RecvErr(libc::sock_extended_err, Option<sockaddr_in6>),

/// Catch-all variant for unimplemented cmsg types.
#[doc(hidden)]
Unknown(UnknownCmsg),
Expand Down Expand Up @@ -756,13 +763,41 @@ impl ControlMessageOwned {
let drop_counter = ptr::read_unaligned(p as *const u32);
ControlMessageOwned::RxqOvfl(drop_counter)
},
#[cfg(any(target_os = "android", target_os = "linux"))]
(libc::IPPROTO_IP, libc::IP_RECVERR) => {
let (err, addr) = Self::recv_err_helper::<sockaddr_in>(p, len);
ControlMessageOwned::Ipv4RecvErr(err, addr)
},
#[cfg(any(target_os = "android", target_os = "linux"))]
(libc::IPPROTO_IPV6, libc::IPV6_RECVERR) => {
let (err, addr) = Self::recv_err_helper::<sockaddr_in6>(p, len);
ControlMessageOwned::Ipv6RecvErr(err, addr)
},
(_, _) => {
let sl = slice::from_raw_parts(p, len);
let ucmsg = UnknownCmsg(*header, Vec::<u8>::from(sl));
ControlMessageOwned::Unknown(ucmsg)
}
}
}

#[cfg(any(target_os = "android", target_os = "linux"))]
unsafe fn recv_err_helper<T>(p: *mut libc::c_uchar, len: usize) -> (libc::sock_extended_err, Option<T>) {
let ee = p as *const libc::sock_extended_err;
let err = ptr::read_unaligned(ee);

// For errors originating on the network, SO_EE_OFFENDER(ee) points inside the p[..len]
// CMSG_DATA buffer. For local errors, there is no address included in the control
// message, and SO_EE_OFFENDER(ee) points beyond the end of the buffer. So, we need to
// validate that the address object is in-bounds before we attempt to copy it.
let addrp = libc::SO_EE_OFFENDER(ee) as *const T;

if addrp.offset(1) as usize - (p as usize) > len {
(err, None)
} else {
(err, Some(ptr::read_unaligned(addrp)))
}
}
}

/// A type-safe zero-copy wrapper around a single control message, as used wih
Expand Down
4 changes: 4 additions & 0 deletions src/sys/socket/sockopt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,10 @@ sockopt_impl!(Both, UdpGroSegment, libc::IPPROTO_UDP, libc::UDP_GRO, bool);
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
sockopt_impl!(Both, RxqOvfl, libc::SOL_SOCKET, libc::SO_RXQ_OVFL, libc::c_int);
sockopt_impl!(Both, Ipv6V6Only, libc::IPPROTO_IPV6, libc::IPV6_V6ONLY, bool);
#[cfg(any(target_os = "android", target_os = "linux"))]
sockopt_impl!(Both, Ipv4RecvErr, libc::IPPROTO_IP, libc::IP_RECVERR, bool);
#[cfg(any(target_os = "android", target_os = "linux"))]
sockopt_impl!(Both, Ipv6RecvErr, libc::IPPROTO_IPV6, libc::IPV6_RECVERR, bool);

#[cfg(any(target_os = "android", target_os = "linux"))]
#[derive(Copy, Clone, Debug)]
Expand Down
157 changes: 157 additions & 0 deletions test/sys/test_socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1789,3 +1789,160 @@ fn test_recvmsg_rxq_ovfl() {
nix::unistd::close(in_socket).unwrap();
nix::unistd::close(out_socket).unwrap();
}

#[cfg(any(
target_os = "linux",
target_os = "android",
))]
mod linux_errqueue {
use nix::sys::socket::*;
use super::{FromStr, SocketAddr};

// Send a UDP datagram to a bogus destination address and observe an ICMP error (v4).
//
// Disable the test on QEMU because QEMU emulation of IP_RECVERR is broken (as documented on PR
// #1514).
#[cfg_attr(qemu, ignore)]
#[test]
fn test_recverr_v4() {
#[repr(u8)]
enum IcmpTypes {
DestUnreach = 3, // ICMP_DEST_UNREACH
}
#[repr(u8)]
enum IcmpUnreachCodes {
PortUnreach = 3, // ICMP_PORT_UNREACH
}

test_recverr_impl::<sockaddr_in, _, _>(
"127.0.0.1:6800",
AddressFamily::Inet,
sockopt::Ipv4RecvErr,
libc::SO_EE_ORIGIN_ICMP,
IcmpTypes::DestUnreach as u8,
IcmpUnreachCodes::PortUnreach as u8,
// Closure handles protocol-specific testing and returns generic sock_extended_err for
// protocol-independent test impl.
|cmsg| {
if let ControlMessageOwned::Ipv4RecvErr(ext_err, err_addr) = cmsg {
if let Some(origin) = err_addr {
// Validate that our network error originated from 127.0.0.1:0.
assert_eq!(origin.sin_family, AddressFamily::Inet as _);
assert_eq!(Ipv4Addr(origin.sin_addr), Ipv4Addr::new(127, 0, 0, 1));
assert_eq!(origin.sin_port, 0);
} else {
panic!("Expected some error origin");
}
return *ext_err
} else {
panic!("Unexpected control message {:?}", cmsg);
}
},
)
}

// Essentially the same test as v4.
//
// Disable the test on QEMU because QEMU emulation of IPV6_RECVERR is broken (as documented on
// PR #1514).
#[cfg_attr(qemu, ignore)]
#[test]
fn test_recverr_v6() {
#[repr(u8)]
enum IcmpV6Types {
DestUnreach = 1, // ICMPV6_DEST_UNREACH
}
#[repr(u8)]
enum IcmpV6UnreachCodes {
PortUnreach = 4, // ICMPV6_PORT_UNREACH
}

test_recverr_impl::<sockaddr_in6, _, _>(
"[::1]:6801",
AddressFamily::Inet6,
sockopt::Ipv6RecvErr,
libc::SO_EE_ORIGIN_ICMP6,
IcmpV6Types::DestUnreach as u8,
IcmpV6UnreachCodes::PortUnreach as u8,
// Closure handles protocol-specific testing and returns generic sock_extended_err for
// protocol-independent test impl.
|cmsg| {
if let ControlMessageOwned::Ipv6RecvErr(ext_err, err_addr) = cmsg {
if let Some(origin) = err_addr {
// Validate that our network error originated from localhost:0.
assert_eq!(origin.sin6_family, AddressFamily::Inet6 as _);
assert_eq!(
Ipv6Addr(origin.sin6_addr),
Ipv6Addr::from_std(&"::1".parse().unwrap()),
);
assert_eq!(origin.sin6_port, 0);
} else {
panic!("Expected some error origin");
}
return *ext_err
} else {
panic!("Unexpected control message {:?}", cmsg);
}
},
)
}

fn test_recverr_impl<SA, OPT, TESTF>(sa: &str,
af: AddressFamily,
opt: OPT,
ee_origin: u8,
ee_type: u8,
ee_code: u8,
testf: TESTF)
where
OPT: SetSockOpt<Val = bool>,
TESTF: FnOnce(&ControlMessageOwned) -> libc::sock_extended_err,
{
use nix::errno::Errno;
use nix::sys::uio::IoVec;

const MESSAGE_CONTENTS: &str = "ABCDEF";

let sock_addr = {
let std_sa = SocketAddr::from_str(sa).unwrap();
let inet_addr = InetAddr::from_std(&std_sa);
SockAddr::new_inet(inet_addr)
};
let sock = socket(af, SockType::Datagram, SockFlag::SOCK_CLOEXEC, None).unwrap();
setsockopt(sock, opt, &true).unwrap();
if let Err(e) = sendto(sock, MESSAGE_CONTENTS.as_bytes(), &sock_addr, MsgFlags::empty()) {
assert_eq!(e, Errno::EADDRNOTAVAIL);
println!("{:?} not available, skipping test.", af);
return;
}

let mut buf = [0u8; 8];
let iovec = [IoVec::from_mut_slice(&mut buf)];
let mut cspace = cmsg_space!(libc::sock_extended_err, SA);

let msg = recvmsg(sock, &iovec, Some(&mut cspace), MsgFlags::MSG_ERRQUEUE).unwrap();
// The sent message / destination associated with the error is returned:
assert_eq!(msg.bytes, MESSAGE_CONTENTS.as_bytes().len());
assert_eq!(&buf[..msg.bytes], MESSAGE_CONTENTS.as_bytes());
// recvmsg(2): "The original destination address of the datagram that caused the error is
// supplied via msg_name;" however, this is not literally true. E.g., an earlier version
// of this test used 0.0.0.0 (::0) as the destination address, which was mutated into
// 127.0.0.1 (::1).
assert_eq!(msg.address, Some(sock_addr));

// Check for expected control message.
let ext_err = match msg.cmsgs().next() {
Some(cmsg) => testf(&cmsg),
None => panic!("No control message"),
};

assert_eq!(ext_err.ee_errno, libc::ECONNREFUSED as u32);
assert_eq!(ext_err.ee_origin, ee_origin);
// ip(7): ee_type and ee_code are set from the type and code fields of the ICMP (ICMPv6)
// header.
assert_eq!(ext_err.ee_type, ee_type);
assert_eq!(ext_err.ee_code, ee_code);
// ip(7): ee_info contains the discovered MTU for EMSGSIZE errors.
assert_eq!(ext_err.ee_info, 0);
}
}

0 comments on commit d23e7c7

Please sign in to comment.