Path: blob/main/crates/wasi/src/sockets/util.rs
1692 views
use core::fmt;1use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};2use core::str::FromStr as _;3use core::time::Duration;45use cap_net_ext::{AddressFamily, Blocking, UdpSocketExt};6use rustix::fd::AsFd;7use rustix::io::Errno;8use rustix::net::{bind, connect_unspec, sockopt};9use tracing::debug;1011use crate::sockets::SocketAddressFamily;1213#[derive(Debug)]14pub enum ErrorCode {15Unknown,16AccessDenied,17NotSupported,18InvalidArgument,19OutOfMemory,20Timeout,21InvalidState,22AddressNotBindable,23AddressInUse,24RemoteUnreachable,25ConnectionRefused,26ConnectionReset,27ConnectionAborted,28DatagramTooLarge,29NotInProgress,30ConcurrencyConflict,31}3233impl fmt::Display for ErrorCode {34fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {35fmt::Debug::fmt(self, f)36}37}3839impl std::error::Error for ErrorCode {}4041fn is_deprecated_ipv4_compatible(addr: Ipv6Addr) -> bool {42matches!(addr.segments(), [0, 0, 0, 0, 0, 0, _, _])43&& addr != Ipv6Addr::UNSPECIFIED44&& addr != Ipv6Addr::LOCALHOST45}4647pub fn is_valid_address_family(addr: IpAddr, socket_family: SocketAddressFamily) -> bool {48match (socket_family, addr) {49(SocketAddressFamily::Ipv4, IpAddr::V4(..)) => true,50(SocketAddressFamily::Ipv6, IpAddr::V6(ipv6)) => {51// Reject IPv4-*compatible* IPv6 addresses. They have been deprecated52// since 2006, OS handling of them is inconsistent and our own53// validations don't take them into account either.54// Note that these are not the same as IPv4-*mapped* IPv6 addresses.55!is_deprecated_ipv4_compatible(ipv6) && ipv6.to_ipv4_mapped().is_none()56}57_ => false,58}59}6061pub fn is_valid_remote_address(addr: SocketAddr) -> bool {62!addr.ip().to_canonical().is_unspecified() && addr.port() != 063}6465pub fn is_valid_unicast_address(addr: IpAddr) -> bool {66match addr.to_canonical() {67IpAddr::V4(ipv4) => !ipv4.is_multicast() && !ipv4.is_broadcast(),68IpAddr::V6(ipv6) => !ipv6.is_multicast(),69}70}7172pub fn to_ipv4_addr(addr: (u8, u8, u8, u8)) -> Ipv4Addr {73let (x0, x1, x2, x3) = addr;74Ipv4Addr::new(x0, x1, x2, x3)75}7677pub fn from_ipv4_addr(addr: Ipv4Addr) -> (u8, u8, u8, u8) {78let [x0, x1, x2, x3] = addr.octets();79(x0, x1, x2, x3)80}8182pub fn to_ipv6_addr(addr: (u16, u16, u16, u16, u16, u16, u16, u16)) -> Ipv6Addr {83let (x0, x1, x2, x3, x4, x5, x6, x7) = addr;84Ipv6Addr::new(x0, x1, x2, x3, x4, x5, x6, x7)85}8687pub fn from_ipv6_addr(addr: Ipv6Addr) -> (u16, u16, u16, u16, u16, u16, u16, u16) {88let [x0, x1, x2, x3, x4, x5, x6, x7] = addr.segments();89(x0, x1, x2, x3, x4, x5, x6, x7)90}9192/*93* Syscalls wrappers with (opinionated) portability fixes.94*/9596pub fn normalize_get_buffer_size(value: usize) -> usize {97if cfg!(target_os = "linux") {98// Linux doubles the value passed to setsockopt to allow space for bookkeeping overhead.99// getsockopt returns this internally doubled value.100// We'll half the value to at least get it back into the same ballpark that the application requested it in.101//102// This normalized behavior is tested for in: test-programs/src/bin/preview2_tcp_sockopts.rs103value / 2104} else {105value106}107}108109pub fn normalize_set_buffer_size(value: usize) -> usize {110value.clamp(1, i32::MAX as usize)111}112113impl From<std::io::Error> for ErrorCode {114fn from(value: std::io::Error) -> Self {115(&value).into()116}117}118119impl From<&std::io::Error> for ErrorCode {120fn from(value: &std::io::Error) -> Self {121// Attempt the more detailed native error code first:122if let Some(errno) = Errno::from_io_error(value) {123return errno.into();124}125126match value.kind() {127std::io::ErrorKind::AddrInUse => Self::AddressInUse,128std::io::ErrorKind::AddrNotAvailable => Self::AddressNotBindable,129std::io::ErrorKind::ConnectionAborted => Self::ConnectionAborted,130std::io::ErrorKind::ConnectionRefused => Self::ConnectionRefused,131std::io::ErrorKind::ConnectionReset => Self::ConnectionReset,132std::io::ErrorKind::InvalidInput => Self::InvalidArgument,133std::io::ErrorKind::NotConnected => Self::InvalidState,134std::io::ErrorKind::OutOfMemory => Self::OutOfMemory,135std::io::ErrorKind::PermissionDenied => Self::AccessDenied,136std::io::ErrorKind::TimedOut => Self::Timeout,137std::io::ErrorKind::Unsupported => Self::NotSupported,138_ => {139debug!("unknown I/O error: {value}");140Self::Unknown141}142}143}144}145146impl From<Errno> for ErrorCode {147fn from(value: Errno) -> Self {148(&value).into()149}150}151152impl From<&Errno> for ErrorCode {153fn from(value: &Errno) -> Self {154match *value {155#[cfg(not(windows))]156Errno::PERM => Self::AccessDenied,157Errno::ACCESS => Self::AccessDenied,158Errno::ADDRINUSE => Self::AddressInUse,159Errno::ADDRNOTAVAIL => Self::AddressNotBindable,160Errno::TIMEDOUT => Self::Timeout,161Errno::CONNREFUSED => Self::ConnectionRefused,162Errno::CONNRESET => Self::ConnectionReset,163Errno::CONNABORTED => Self::ConnectionAborted,164Errno::INVAL => Self::InvalidArgument,165Errno::HOSTUNREACH => Self::RemoteUnreachable,166Errno::HOSTDOWN => Self::RemoteUnreachable,167Errno::NETDOWN => Self::RemoteUnreachable,168Errno::NETUNREACH => Self::RemoteUnreachable,169#[cfg(target_os = "linux")]170Errno::NONET => Self::RemoteUnreachable,171Errno::ISCONN => Self::InvalidState,172Errno::NOTCONN => Self::InvalidState,173Errno::DESTADDRREQ => Self::InvalidState,174Errno::MSGSIZE => Self::DatagramTooLarge,175#[cfg(not(windows))]176Errno::NOMEM => Self::OutOfMemory,177Errno::NOBUFS => Self::OutOfMemory,178Errno::OPNOTSUPP => Self::NotSupported,179Errno::NOPROTOOPT => Self::NotSupported,180Errno::PFNOSUPPORT => Self::NotSupported,181Errno::PROTONOSUPPORT => Self::NotSupported,182Errno::PROTOTYPE => Self::NotSupported,183Errno::SOCKTNOSUPPORT => Self::NotSupported,184Errno::AFNOSUPPORT => Self::NotSupported,185186// FYI, EINPROGRESS should have already been handled by connect.187_ => {188debug!("unknown I/O error: {value}");189Self::Unknown190}191}192}193}194195pub fn get_ip_ttl(fd: impl AsFd) -> Result<u8, ErrorCode> {196let v = sockopt::ip_ttl(fd)?;197let Ok(v) = v.try_into() else {198return Err(ErrorCode::NotSupported);199};200Ok(v)201}202203pub fn get_ipv6_unicast_hops(fd: impl AsFd) -> Result<u8, ErrorCode> {204let v = sockopt::ipv6_unicast_hops(fd)?;205Ok(v)206}207208pub fn get_unicast_hop_limit(fd: impl AsFd, family: SocketAddressFamily) -> Result<u8, ErrorCode> {209match family {210SocketAddressFamily::Ipv4 => get_ip_ttl(fd),211SocketAddressFamily::Ipv6 => get_ipv6_unicast_hops(fd),212}213}214215pub fn set_unicast_hop_limit(216fd: impl AsFd,217family: SocketAddressFamily,218value: u8,219) -> Result<(), ErrorCode> {220if value == 0 {221// WIT: "If the provided value is 0, an `invalid-argument` error is returned."222//223// A well-behaved IP application should never send out new packets with TTL 0.224// We validate the value ourselves because OS'es are not consistent in this.225// On Linux the validation is even inconsistent between their IPv4 and IPv6 implementation.226return Err(ErrorCode::InvalidArgument);227}228match family {229SocketAddressFamily::Ipv4 => {230sockopt::set_ip_ttl(fd, value.into())?;231}232SocketAddressFamily::Ipv6 => {233sockopt::set_ipv6_unicast_hops(fd, Some(value))?;234}235}236Ok(())237}238239pub fn receive_buffer_size(fd: impl AsFd) -> Result<u64, ErrorCode> {240let v = sockopt::socket_recv_buffer_size(fd)?;241Ok(normalize_get_buffer_size(v).try_into().unwrap_or(u64::MAX))242}243244pub fn set_receive_buffer_size(fd: impl AsFd, value: u64) -> Result<usize, ErrorCode> {245if value == 0 {246// WIT: "If the provided value is 0, an `invalid-argument` error is returned."247return Err(ErrorCode::InvalidArgument);248}249let value = value.try_into().unwrap_or(usize::MAX);250let value = normalize_set_buffer_size(value);251match sockopt::set_socket_recv_buffer_size(fd, value) {252// Most platforms (Linux, Windows, Fuchsia, Solaris, Illumos, Haiku, ESP-IDF, ..and more?) treat the value253// passed to SO_SNDBUF/SO_RCVBUF as a performance tuning hint and silently clamp the input if it exceeds254// their capability.255// As far as I can see, only the *BSD family views this option as a hard requirement and fails when the256// value is out of range. We normalize this behavior in favor of the more commonly understood257// "performance hint" semantics. In other words; even ENOBUFS is "Ok".258// A future improvement could be to query the corresponding sysctl on *BSD platforms and clamp the input259// `size` ourselves, to completely close the gap with other platforms.260//261// This normalized behavior is tested for in: test-programs/src/bin/preview2_tcp_sockopts.rs262Err(Errno::NOBUFS) => {}263Err(err) => return Err(err.into()),264_ => {}265};266Ok(value)267}268269pub fn send_buffer_size(fd: impl AsFd) -> Result<u64, ErrorCode> {270let v = sockopt::socket_send_buffer_size(fd)?;271Ok(normalize_get_buffer_size(v).try_into().unwrap_or(u64::MAX))272}273274pub fn set_send_buffer_size(fd: impl AsFd, value: u64) -> Result<usize, ErrorCode> {275if value == 0 {276// WIT: "If the provided value is 0, an `invalid-argument` error is returned."277return Err(ErrorCode::InvalidArgument);278}279let value = value.try_into().unwrap_or(usize::MAX);280let value = normalize_set_buffer_size(value);281match sockopt::set_socket_send_buffer_size(fd, value) {282Err(Errno::NOBUFS) => {}283Err(err) => return Err(err.into()),284_ => {}285};286Ok(value)287}288289pub fn set_keep_alive_idle_time(fd: impl AsFd, value: u64) -> Result<u64, ErrorCode> {290const NANOS_PER_SEC: u64 = 1_000_000_000;291292// Ensure that the value passed to the actual syscall never gets rounded down to 0.293const MIN: u64 = NANOS_PER_SEC;294295// Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms.296const MAX: u64 = (i16::MAX as u64) * NANOS_PER_SEC;297298if value <= 0 {299// WIT: "If the provided value is 0, an `invalid-argument` error is returned."300return Err(ErrorCode::InvalidArgument);301}302let value = value.clamp(MIN, MAX);303sockopt::set_tcp_keepidle(fd, Duration::from_nanos(value))?;304Ok(value)305}306307pub fn set_keep_alive_interval(fd: impl AsFd, value: Duration) -> Result<(), ErrorCode> {308// Ensure that any fractional value passed to the actual syscall never gets rounded down to 0.309const MIN: Duration = Duration::from_secs(1);310311// Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms.312const MAX: Duration = Duration::from_secs(i16::MAX as u64);313314if value <= Duration::ZERO {315// WIT: "If the provided value is 0, an `invalid-argument` error is returned."316return Err(ErrorCode::InvalidArgument);317}318sockopt::set_tcp_keepintvl(fd, value.clamp(MIN, MAX))?;319Ok(())320}321322pub fn set_keep_alive_count(fd: impl AsFd, value: u32) -> Result<(), ErrorCode> {323const MIN_CNT: u32 = 1;324// Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms.325const MAX_CNT: u32 = i8::MAX as u32;326327if value == 0 {328// WIT: "If the provided value is 0, an `invalid-argument` error is returned."329return Err(ErrorCode::InvalidArgument);330}331sockopt::set_tcp_keepcnt(fd, value.clamp(MIN_CNT, MAX_CNT))?;332Ok(())333}334335pub fn tcp_bind(336socket: &tokio::net::TcpSocket,337local_address: SocketAddr,338) -> Result<(), ErrorCode> {339// Automatically bypass the TIME_WAIT state when binding to a specific port340// Unconditionally (re)set SO_REUSEADDR, even when the value is false.341// This ensures we're not accidentally affected by any socket option342// state left behind by a previous failed call to this method.343#[cfg(not(windows))]344if let Err(err) = sockopt::set_socket_reuseaddr(&socket, local_address.port() > 0) {345return Err(err.into());346}347348// Perform the OS bind call.349socket350.bind(local_address)351.map_err(|err| match Errno::from_io_error(&err) {352// From https://pubs.opengroup.org/onlinepubs/9699919799/functions/bind.html:353// > [EAFNOSUPPORT] The specified address is not a valid address for the address family of the specified socket354//355// The most common reasons for this error should have already356// been handled by our own validation slightly higher up in this357// function. This error mapping is here just in case there is358// an edge case we didn't catch.359Some(Errno::AFNOSUPPORT) => ErrorCode::InvalidArgument,360// See: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-bind#:~:text=WSAENOBUFS361// Windows returns WSAENOBUFS when the ephemeral ports have been exhausted.362#[cfg(windows)]363Some(Errno::NOBUFS) => ErrorCode::AddressInUse,364_ => err.into(),365})366}367368pub fn udp_socket(family: AddressFamily) -> std::io::Result<cap_std::net::UdpSocket> {369// Delegate socket creation to cap_net_ext. They handle a couple of things for us:370// - On Windows: call WSAStartup if not done before.371// - Set the NONBLOCK and CLOEXEC flags. Either immediately during socket creation,372// or afterwards using ioctl or fcntl. Exact method depends on the platform.373374let socket = cap_std::net::UdpSocket::new(family, Blocking::No)?;375Ok(socket)376}377378pub fn udp_bind(sockfd: impl AsFd, addr: SocketAddr) -> Result<(), ErrorCode> {379bind(sockfd, &addr).map_err(|err| match err {380// See: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-bind#:~:text=WSAENOBUFS381// Windows returns WSAENOBUFS when the ephemeral ports have been exhausted.382#[cfg(windows)]383Errno::NOBUFS => ErrorCode::AddressInUse,384// From https://pubs.opengroup.org/onlinepubs/9699919799/functions/bind.html:385// > [EAFNOSUPPORT] The specified address is not a valid address for the address family of the specified socket386//387// The most common reasons for this error should have already388// been handled by our own validation slightly higher up in this389// function. This error mapping is here just in case there is390// an edge case we didn't catch.391Errno::AFNOSUPPORT => ErrorCode::InvalidArgument,392_ => err.into(),393})394}395396pub fn udp_disconnect(sockfd: impl AsFd) -> Result<(), ErrorCode> {397match connect_unspec(sockfd) {398// BSD platforms return an error even if the UDP socket was disconnected successfully.399//400// MacOS was kind enough to document this: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/connect.2.html401// > Datagram sockets may dissolve the association by connecting to an402// > invalid address, such as a null address or an address with the address403// > family set to AF_UNSPEC (the error EAFNOSUPPORT will be harmlessly404// > returned).405//406// ... except that this appears to be incomplete, because experiments407// have shown that MacOS actually returns EINVAL, depending on the408// address family of the socket.409#[cfg(target_os = "macos")]410Err(Errno::INVAL | Errno::AFNOSUPPORT) => Ok(()),411Err(err) => Err(err.into()),412Ok(()) => Ok(()),413}414}415416pub fn parse_host(name: &str) -> Result<url::Host, ErrorCode> {417// `url::Host::parse` serves us two functions:418// 1. validate the input is a valid domain name or IP,419// 2. convert unicode domains to punycode.420match url::Host::parse(&name) {421Ok(host) => Ok(host),422423// `url::Host::parse` doesn't understand bare IPv6 addresses without [brackets]424Err(_) => {425if let Ok(addr) = Ipv6Addr::from_str(name) {426Ok(url::Host::Ipv6(addr))427} else {428Err(ErrorCode::InvalidArgument)429}430}431}432}433434435